mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-28 22:53:28 +00:00
Compare commits
1 Commits
nikneym/mo
...
normalize-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a849e5693 |
18
.github/actions/install/action.yml
vendored
18
.github/actions/install/action.yml
vendored
@@ -5,7 +5,7 @@ inputs:
|
||||
zig:
|
||||
description: 'Zig version to install'
|
||||
required: false
|
||||
default: '0.15.1'
|
||||
default: '0.14.0'
|
||||
arch:
|
||||
description: 'CPU arch used to select the v8 lib'
|
||||
required: false
|
||||
@@ -17,7 +17,7 @@ inputs:
|
||||
zig-v8:
|
||||
description: 'zig v8 version to install'
|
||||
required: false
|
||||
default: 'v0.1.28'
|
||||
default: 'v0.1.23'
|
||||
v8:
|
||||
description: 'v8 version to install'
|
||||
required: false
|
||||
@@ -34,11 +34,9 @@ runs:
|
||||
- name: Install apt deps
|
||||
if: ${{ inputs.os == 'linux' }}
|
||||
shell: bash
|
||||
run: |
|
||||
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
|
||||
run: sudo apt-get install -y wget xz-utils python3 ca-certificates git pkg-config libglib2.0-dev gperf libexpat1-dev cmake clang
|
||||
|
||||
- uses: mlugg/setup-zig@v2
|
||||
- uses: mlugg/setup-zig@v1
|
||||
with:
|
||||
version: ${{ inputs.zig }}
|
||||
|
||||
@@ -61,11 +59,11 @@ runs:
|
||||
- name: install v8
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p v8/out/${{ inputs.os }}/debug/obj/zig/
|
||||
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/out/${{ inputs.os }}/debug/obj/zig/libc_v8.a
|
||||
mkdir -p v8/out/debug/obj/zig/
|
||||
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/out/debug/obj/zig/libc_v8.a
|
||||
|
||||
mkdir -p v8/out/${{ inputs.os }}/release/obj/zig/
|
||||
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/out/${{ inputs.os }}/release/obj/zig/libc_v8.a
|
||||
mkdir -p v8/out/release/obj/zig/
|
||||
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/out/release/obj/zig/libc_v8.a
|
||||
|
||||
- name: libiconv
|
||||
shell: bash
|
||||
|
||||
99
.github/workflows/build.yml
vendored
99
.github/workflows/build.yml
vendored
@@ -1,11 +1,5 @@
|
||||
name: nightly build
|
||||
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ vars.NIGHTLY_BUILD_AWS_ACCESS_ID }}
|
||||
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 }}
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "2 2 * * *"
|
||||
@@ -43,11 +37,6 @@ jobs:
|
||||
- name: Rename binary
|
||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
|
||||
- name: upload on s3
|
||||
run: |
|
||||
export DIR=`git show --no-patch --no-notes --pretty='%cs_%h'`
|
||||
aws s3 cp --storage-class=GLACIER_IR lightpanda-${{ env.ARCH }}-${{ env.OS }} s3://lpd-nightly-build/${DIR}/lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
|
||||
- name: Upload the build
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
@@ -60,47 +49,7 @@ jobs:
|
||||
ARCH: aarch64
|
||||
OS: linux
|
||||
|
||||
runs-on: ubuntu-22.04-arm
|
||||
timeout-minutes: 15
|
||||
|
||||
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
|
||||
with:
|
||||
os: ${{env.OS}}
|
||||
arch: ${{env.ARCH}}
|
||||
|
||||
- name: zig build
|
||||
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 }}
|
||||
|
||||
- name: upload on s3
|
||||
run: |
|
||||
export DIR=`git show --no-patch --no-notes --pretty='%cs_%h'`
|
||||
aws s3 cp --storage-class=GLACIER_IR lightpanda-${{ env.ARCH }}-${{ env.OS }} s3://lpd-nightly-build/${DIR}/lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
|
||||
- name: Upload the build
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
tag: nightly
|
||||
|
||||
build-macos-aarch64:
|
||||
env:
|
||||
ARCH: aarch64
|
||||
OS: macos
|
||||
|
||||
# macos-14 runs on arm CPU. see
|
||||
# https://github.com/actions/runner-images?tab=readme-ov-file
|
||||
runs-on: macos-14
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
@@ -121,10 +70,38 @@ jobs:
|
||||
- name: Rename binary
|
||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
|
||||
- name: upload on s3
|
||||
run: |
|
||||
export DIR=`git show --no-patch --no-notes --pretty='%cs_%h'`
|
||||
aws s3 cp --storage-class=GLACIER_IR lightpanda-${{ env.ARCH }}-${{ env.OS }} s3://lpd-nightly-build/${DIR}/lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
- name: Upload the build
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
tag: nightly
|
||||
|
||||
build-macos-aarch64:
|
||||
env:
|
||||
ARCH: aarch64
|
||||
OS: macos
|
||||
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
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
|
||||
with:
|
||||
os: ${{env.OS}}
|
||||
arch: ${{env.ARCH}}
|
||||
|
||||
- name: zig build
|
||||
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 }}
|
||||
|
||||
- name: Upload the build
|
||||
uses: ncipollo/release-action@v1
|
||||
@@ -138,11 +115,6 @@ jobs:
|
||||
ARCH: x86_64
|
||||
OS: macos
|
||||
|
||||
# macos-13 runs on x86 CPU. see
|
||||
# https://github.com/actions/runner-images?tab=readme-ov-file
|
||||
# If we want to build for macos-14 or superior, we need to switch to
|
||||
# macos-14-large.
|
||||
# No need for now, but maybe we will need it in the short term.
|
||||
runs-on: macos-13
|
||||
timeout-minutes: 15
|
||||
|
||||
@@ -164,11 +136,6 @@ jobs:
|
||||
- name: Rename binary
|
||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
|
||||
- name: upload on s3
|
||||
run: |
|
||||
export DIR=`git show --no-patch --no-notes --pretty='%cs_%h'`
|
||||
aws s3 cp --storage-class=GLACIER_IR lightpanda-${{ env.ARCH }}-${{ env.OS }} s3://lpd-nightly-build/${DIR}/lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
|
||||
- name: Upload the build
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
|
||||
190
.github/workflows/e2e-test.yml
vendored
190
.github/workflows/e2e-test.yml
vendored
@@ -1,12 +1,5 @@
|
||||
name: e2e-test
|
||||
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ vars.LPD_PERF_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
|
||||
AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
|
||||
LIGHTPANDA_DISABLE_TELEMETRY: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -45,9 +38,6 @@ jobs:
|
||||
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:
|
||||
@@ -58,7 +48,7 @@ jobs:
|
||||
- uses: ./.github/actions/install
|
||||
|
||||
- name: zig build release
|
||||
run: zig build -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||
run: zig build -Doptimize=ReleaseSafe
|
||||
|
||||
- name: upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -68,6 +58,56 @@ jobs:
|
||||
zig-out/bin/lightpanda
|
||||
retention-days: 1
|
||||
|
||||
puppeteer-perf:
|
||||
name: puppeteer-perf
|
||||
needs: zig-build-release
|
||||
|
||||
env:
|
||||
MAX_MEMORY: 28000
|
||||
MAX_AVG_DURATION: 24
|
||||
LIGHTPANDA_DISABLE_TELEMETRY: true
|
||||
|
||||
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 puppeteer
|
||||
run: |
|
||||
python3 -m http.server 1234 -d ./public & echo $! > PYTHON.pid
|
||||
./lightpanda serve --gc_hints & echo $! > LPD.pid
|
||||
RUNS=100 npm run bench-puppeteer-cdp > puppeteer.out || exit 1
|
||||
cat /proc/`cat LPD.pid`/status |grep VmHWM|grep -oP '\d+' > LPD.VmHWM
|
||||
kill `cat LPD.pid` `cat PYTHON.pid`
|
||||
|
||||
- name: puppeteer result
|
||||
run: cat puppeteer.out
|
||||
|
||||
- name: memory regression
|
||||
run: |
|
||||
export LPD_VmHWM=`cat LPD.VmHWM`
|
||||
echo "Peak resident set size: $LPD_VmHWM"
|
||||
test "$LPD_VmHWM" -le "$MAX_MEMORY"
|
||||
|
||||
- name: duration regression
|
||||
run: |
|
||||
export PUPPETEER_AVG_DURATION=`cat puppeteer.out|grep 'avg run'|sed 's/avg run duration (ms) //'`
|
||||
echo "puppeteer avg duration: $PUPPETEER_AVG_DURATION"
|
||||
test "$PUPPETEER_AVG_DURATION" -le "$MAX_AVG_DURATION"
|
||||
|
||||
demo-scripts:
|
||||
name: demo-scripts
|
||||
needs: zig-build-release
|
||||
@@ -93,131 +133,5 @@ jobs:
|
||||
- name: run end to end tests
|
||||
run: |
|
||||
./lightpanda serve & echo $! > LPD.pid
|
||||
go run runner/main.go
|
||||
go run runner/main.go --verbose
|
||||
kill `cat LPD.pid`
|
||||
|
||||
- name: build proxy
|
||||
run: |
|
||||
cd proxy
|
||||
go build
|
||||
|
||||
- name: run end to end tests through proxy
|
||||
run: |
|
||||
./proxy/proxy & echo $! > PROXY.id
|
||||
./lightpanda serve --http_proxy 'http://127.0.0.1:3000' & echo $! > LPD.pid
|
||||
go run runner/main.go
|
||||
kill `cat LPD.pid` `cat PROXY.id`
|
||||
|
||||
cdp-and-hyperfine-bench:
|
||||
name: cdp-and-hyperfine-bench
|
||||
needs: zig-build-release
|
||||
|
||||
env:
|
||||
MAX_MEMORY: 27000
|
||||
MAX_AVG_DURATION: 23
|
||||
LIGHTPANDA_DISABLE_TELEMETRY: true
|
||||
|
||||
# use a self host runner.
|
||||
runs-on: lpd-bench-hetzner
|
||||
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: start http
|
||||
run: |
|
||||
go run ws/main.go & echo $! > WS.pid
|
||||
sleep 2
|
||||
|
||||
- name: run puppeteer
|
||||
run: |
|
||||
./lightpanda serve & echo $! > LPD.pid
|
||||
sleep 2
|
||||
RUNS=100 npm run bench-puppeteer-cdp > puppeteer.out || exit 1
|
||||
cat /proc/`cat LPD.pid`/status |grep VmHWM|grep -oP '\d+' > LPD.VmHWM
|
||||
kill `cat LPD.pid`
|
||||
|
||||
- name: puppeteer result
|
||||
run: cat puppeteer.out
|
||||
|
||||
- name: memory regression
|
||||
run: |
|
||||
export LPD_VmHWM=`cat LPD.VmHWM`
|
||||
echo "Peak resident set size: $LPD_VmHWM"
|
||||
test "$LPD_VmHWM" -le "$MAX_MEMORY"
|
||||
|
||||
- name: duration regression
|
||||
run: |
|
||||
export PUPPETEER_AVG_DURATION=`cat puppeteer.out|grep 'avg run'|sed 's/avg run duration (ms) //'`
|
||||
echo "puppeteer avg duration: $PUPPETEER_AVG_DURATION"
|
||||
test "$PUPPETEER_AVG_DURATION" -le "$MAX_AVG_DURATION"
|
||||
|
||||
- name: json output
|
||||
run: |
|
||||
export AVG_DURATION=`cat puppeteer.out|grep 'avg run'|sed 's/avg run duration (ms) //'`
|
||||
export TOTAL_DURATION=`cat puppeteer.out|grep 'total duration'|sed 's/total duration (ms) //'`
|
||||
export LPD_VmHWM=`cat LPD.VmHWM`
|
||||
echo "{\"duration_total\":${TOTAL_DURATION},\"duration_avg\":${AVG_DURATION},\"mem_peak\":${LPD_VmHWM}}" > bench.json
|
||||
cat bench.json
|
||||
|
||||
- name: run hyperfine
|
||||
run: |
|
||||
hyperfine --export-json=hyperfine.json --warmup 3 --runs 20 --shell=none "./lightpanda --dump http://127.0.0.1:1234/campfire-commerce/"
|
||||
|
||||
- name: stop http
|
||||
run: kill `cat WS.pid`
|
||||
|
||||
- name: write commit
|
||||
run: |
|
||||
echo "${{github.sha}}" > commit.txt
|
||||
|
||||
- name: upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bench-results
|
||||
path: |
|
||||
bench.json
|
||||
hyperfine.json
|
||||
commit.txt
|
||||
retention-days: 10
|
||||
|
||||
|
||||
perf-fmt:
|
||||
name: perf-fmt
|
||||
needs: cdp-and-hyperfine-bench
|
||||
|
||||
# Don't execute on PR
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
container:
|
||||
image: ghcr.io/lightpanda-io/perf-fmt:latest
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: bench-results
|
||||
|
||||
- name: format and send json result
|
||||
run: /perf-fmt cdp ${{ github.sha }} bench.json
|
||||
|
||||
- name: format and send json result
|
||||
run: /perf-fmt hyperfine ${{ github.sha }} hyperfine.json
|
||||
|
||||
4
.github/workflows/zig-fmt.yml
vendored
4
.github/workflows/zig-fmt.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: zig-fmt
|
||||
|
||||
env:
|
||||
ZIG_VERSION: 0.15.1
|
||||
ZIG_VERSION: 0.14.0
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- uses: mlugg/setup-zig@v2
|
||||
- uses: mlugg/setup-zig@v1
|
||||
with:
|
||||
version: ${{ env.ZIG_VERSION }}
|
||||
|
||||
|
||||
12
.gitmodules
vendored
12
.gitmodules
vendored
@@ -19,15 +19,3 @@
|
||||
[submodule "vendor/mimalloc"]
|
||||
path = vendor/mimalloc
|
||||
url = https://github.com/microsoft/mimalloc.git/
|
||||
[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
|
||||
[submodule "vendor/curl"]
|
||||
path = vendor/curl
|
||||
url = https://github.com/curl/curl.git
|
||||
|
||||
55
Dockerfile
55
Dockerfile
@@ -1,11 +1,11 @@
|
||||
FROM debian:stable
|
||||
FROM ubuntu:24.04
|
||||
|
||||
ARG MINISIG=0.12
|
||||
ARG ZIG=0.15.1
|
||||
ARG ZIG=0.14.0
|
||||
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
||||
ARG V8=13.6.233.8
|
||||
ARG ZIG_V8=v0.1.28
|
||||
ARG TARGETPLATFORM
|
||||
ARG ARCH=x86_64
|
||||
ARG V8=11.1.134
|
||||
ARG ZIG_V8=v0.1.23
|
||||
|
||||
RUN apt-get update -yq && \
|
||||
apt-get install -yq xz-utils \
|
||||
@@ -20,19 +20,30 @@ RUN curl --fail -L -O https://github.com/jedisct1/minisign/releases/download/${M
|
||||
tar xvzf minisign-${MINISIG}-linux.tar.gz
|
||||
|
||||
# install zig
|
||||
RUN case $TARGETPLATFORM in \
|
||||
"linux/arm64") ARCH="aarch64" ;; \
|
||||
*) ARCH="x86_64" ;; \
|
||||
esac && \
|
||||
curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-${ARCH}-linux-${ZIG}.tar.xz && \
|
||||
curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-${ARCH}-linux-${ZIG}.tar.xz.minisig && \
|
||||
minisign-linux/${ARCH}/minisign -Vm zig-${ARCH}-linux-${ZIG}.tar.xz -P ${ZIG_MINISIG} && \
|
||||
tar xvf zig-${ARCH}-linux-${ZIG}.tar.xz && \
|
||||
mv zig-${ARCH}-linux-${ZIG} /usr/local/lib && \
|
||||
ln -s /usr/local/lib/zig-${ARCH}-linux-${ZIG}/zig /usr/local/bin/zig
|
||||
RUN curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-linux-${ARCH}-${ZIG}.tar.xz
|
||||
RUN curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-linux-${ARCH}-${ZIG}.tar.xz.minisig
|
||||
|
||||
RUN minisign-linux/${ARCH}/minisign -Vm zig-linux-${ARCH}-${ZIG}.tar.xz -P ${ZIG_MINISIG}
|
||||
|
||||
# clean minisg
|
||||
RUN rm -fr minisign-0.11-linux.tar.gz minisign-linux
|
||||
|
||||
# install zig
|
||||
RUN tar xvf zig-linux-${ARCH}-${ZIG}.tar.xz && \
|
||||
mv zig-linux-${ARCH}-${ZIG} /usr/local/lib && \
|
||||
ln -s /usr/local/lib/zig-linux-${ARCH}-${ZIG}/zig /usr/local/bin/zig
|
||||
|
||||
# clean up zig install
|
||||
RUN rm -fr zig-linux-${ARCH}-${ZIG}.tar.xz zig-linux-${ARCH}-${ZIG}.tar.xz.minisig
|
||||
|
||||
# force use of http instead of ssh with github
|
||||
RUN cat <<EOF > /root/.gitconfig
|
||||
[url "https://github.com/"]
|
||||
insteadOf="git@github.com:"
|
||||
EOF
|
||||
|
||||
# clone lightpanda
|
||||
RUN git clone https://github.com/lightpanda-io/browser.git
|
||||
RUN git clone git@github.com:lightpanda-io/browser.git
|
||||
|
||||
WORKDIR /browser
|
||||
|
||||
@@ -45,18 +56,14 @@ RUN make install-libiconv && \
|
||||
make install-mimalloc
|
||||
|
||||
# download and install v8
|
||||
RUN case $TARGETPLATFORM in \
|
||||
"linux/arm64") ARCH="aarch64" ;; \
|
||||
*) ARCH="x86_64" ;; \
|
||||
esac && \
|
||||
curl --fail -L -o libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${ZIG_V8}/libc_v8_${V8}_linux_${ARCH}.a && \
|
||||
mkdir -p v8/out/linux/release/obj/zig/ && \
|
||||
mv libc_v8.a v8/out/linux/release/obj/zig/libc_v8.a
|
||||
RUN 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/build/${ARCH}-linux/release/ninja/obj/zig/ && \
|
||||
mv libc_v8.a v8/build/${ARCH}-linux/release/ninja/obj/zig/libc_v8.a
|
||||
|
||||
# build release
|
||||
RUN make build
|
||||
|
||||
FROM debian:stable-slim
|
||||
FROM ubuntu:24.04
|
||||
|
||||
# copy ca certificates
|
||||
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
7
Makefile
7
Makefile
@@ -72,13 +72,8 @@ build-dev:
|
||||
@$(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 "\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
|
||||
run: build
|
||||
@printf "\e[36mRunning...\e[0m\n"
|
||||
@./zig-out/bin/lightpanda || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
|
||||
|
||||
|
||||
42
README.md
42
README.md
@@ -18,7 +18,7 @@ Lightpanda is the open-source browser made for headless usage:
|
||||
|
||||
- Javascript execution
|
||||
- Support of Web APIs (partial, WIP)
|
||||
- Compatible with Playwright[^1], Puppeteer, chromedp through CDP
|
||||
- Compatible with Playwright, Puppeteer through CDP (WIP)
|
||||
|
||||
Fast web automation for AI agents, LLM training, scraping and testing:
|
||||
|
||||
@@ -36,13 +36,9 @@ Fast web automation for AI agents, LLM training, scraping and testing:
|
||||
_Puppeteer requesting 100 pages from a local website on a AWS EC2 m5.large instance.
|
||||
See [benchmark details](https://github.com/lightpanda-io/demo)._
|
||||
|
||||
[^1]: **Playwright support disclaimer:**
|
||||
Due to the nature of Playwright, a script that works with the current version of the browser may not function correctly with a future version. Playwright uses an intermediate JavaScript layer that selects an execution strategy based on the browser's available features. If Lightpanda adds a new [Web API](https://developer.mozilla.org/en-US/docs/Web/API), Playwright may choose to execute different code for the same script. This new code path could attempt to use features that are not yet implemented. Lightpanda makes an effort to add compatibility tests, but we can't cover all scenarios. If you encounter an issue, please create a [GitHub issue](https://github.com/lightpanda-io/browser/issues) and include the last known working version of the script.
|
||||
|
||||
## Quick start
|
||||
|
||||
### Install
|
||||
**Install from the nightly builds**
|
||||
### Install from the nightly builds
|
||||
|
||||
You can download the last binary from the [nightly
|
||||
builds](https://github.com/lightpanda-io/browser/releases/tag/nightly) for
|
||||
@@ -65,16 +61,6 @@ chmod a+x ./lightpanda
|
||||
The Lightpanda browser is compatible to run on windows inside WSL. Follow the Linux instruction for installation from a WSL terminal.
|
||||
It is recommended to install clients like Puppeteer on the Windows host.
|
||||
|
||||
**Install from Docker**
|
||||
|
||||
Lightpanda provides [official Docker
|
||||
images](https://hub.docker.com/r/lightpanda/browser) for both Linux amd64 and
|
||||
arm64 architectures.
|
||||
The following command fetches the Docker image and starts a new container exposing Lightpanda's CDP server on port `9222`.
|
||||
```console
|
||||
docker run -d --name lightpanda -p 9222:9222 lightpanda/browser:nightly
|
||||
```
|
||||
|
||||
### Dump a URL
|
||||
|
||||
```console
|
||||
@@ -135,26 +121,21 @@ By default, Lightpanda collects and sends usage telemetry. This can be disabled
|
||||
|
||||
## Status
|
||||
|
||||
Lightpanda is in Beta and currently a work in progress. Stability and coverage are improving and many websites now work.
|
||||
You may still encounter errors or crashes. Please open an issue with specifics if so.
|
||||
Lightpanda is still a work in progress and is currently at a Beta stage.
|
||||
|
||||
:warning: You should expect most websites to fail or crash.
|
||||
|
||||
Here are the key features we have implemented:
|
||||
|
||||
- [x] HTTP loader (based on Libcurl)
|
||||
- [x] HTTP loader
|
||||
- [x] HTML parser and DOM tree (based on Netsurf libs)
|
||||
- [x] Javascript support (v8)
|
||||
- [x] DOM APIs
|
||||
- [x] Basic DOM APIs
|
||||
- [x] Ajax
|
||||
- [x] XHR API
|
||||
- [x] Fetch API (polyfill)
|
||||
- [x] Fetch API
|
||||
- [x] DOM dump
|
||||
- [x] CDP/websockets server
|
||||
- [x] Click
|
||||
- [x] Input form
|
||||
- [x] Cookies
|
||||
- [x] Custom HTTP headers
|
||||
- [x] Proxy support
|
||||
- [x] Network interception
|
||||
- [x] Basic CDP/websockets server
|
||||
|
||||
NOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time.
|
||||
|
||||
@@ -164,12 +145,11 @@ You can also follow the progress of our Javascript support in our dedicated [zig
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Lightpanda is written with [Zig](https://ziglang.org/) `0.15.1`. You have to
|
||||
Lightpanda is written with [Zig](https://ziglang.org/) `0.14.0`. You have to
|
||||
install it with the right version in order to build the project.
|
||||
|
||||
Lightpanda also depends on
|
||||
[zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime/) (with v8),
|
||||
[Libcurl](https://curl.se/libcurl/),
|
||||
[Netsurf libs](https://www.netsurf-browser.org/) and
|
||||
[Mimalloc](https://microsoft.github.io/mimalloc).
|
||||
|
||||
@@ -181,7 +161,7 @@ For Debian/Ubuntu based Linux:
|
||||
sudo apt install xz-utils \
|
||||
python3 ca-certificates git \
|
||||
pkg-config libglib2.0-dev \
|
||||
gperf libexpat1-dev unzip rsync \
|
||||
gperf libexpat1-dev unzip \
|
||||
cmake clang
|
||||
```
|
||||
|
||||
|
||||
759
build.zig
759
build.zig
@@ -19,13 +19,11 @@
|
||||
const std = @import("std");
|
||||
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.1";
|
||||
const recommended_zig_version = "0.14.0";
|
||||
|
||||
pub fn build(b: *Build) !void {
|
||||
pub fn build(b: *std.Build) !void {
|
||||
switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) {
|
||||
.eq => {},
|
||||
.lt => {
|
||||
@@ -49,18 +47,6 @@ pub fn build(b: *Build) !void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
// We're still using llvm because the new x86 backend seems to crash
|
||||
// with v8. This can be reproduced in zig-v8-fork.
|
||||
|
||||
const lightpanda_module = b.addModule("lightpanda", .{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.link_libc = true,
|
||||
.link_libcpp = true,
|
||||
});
|
||||
try addDependencies(b, lightpanda_module, opts);
|
||||
|
||||
{
|
||||
// browser
|
||||
// -------
|
||||
@@ -68,9 +54,12 @@ pub fn build(b: *Build) !void {
|
||||
// compile and install
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "lightpanda",
|
||||
.use_llvm = true,
|
||||
.root_module = lightpanda_module,
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
});
|
||||
|
||||
try common(b, opts, exe);
|
||||
b.installArtifact(exe);
|
||||
|
||||
// run
|
||||
@@ -84,54 +73,6 @@ pub fn build(b: *Build) !void {
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
}
|
||||
|
||||
{
|
||||
// tests
|
||||
// ----
|
||||
|
||||
// compile
|
||||
const tests = b.addTest(.{
|
||||
.root_module = lightpanda_module,
|
||||
.use_llvm = true,
|
||||
.test_runner = .{ .path = b.path("src/test_runner.zig"), .mode = .simple },
|
||||
});
|
||||
|
||||
const run_tests = b.addRunArtifact(tests);
|
||||
if (b.args) |args| {
|
||||
run_tests.addArgs(args);
|
||||
}
|
||||
|
||||
// step
|
||||
const tests_step = b.step("test", "Run unit tests");
|
||||
tests_step.dependOn(&run_tests.step);
|
||||
}
|
||||
|
||||
{
|
||||
// wpt
|
||||
// -----
|
||||
const wpt_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main_wpt.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
try addDependencies(b, wpt_module, opts);
|
||||
|
||||
// compile and install
|
||||
const wpt = b.addExecutable(.{
|
||||
.name = "lightpanda-wpt",
|
||||
.use_llvm = true,
|
||||
.root_module = wpt_module,
|
||||
});
|
||||
|
||||
// run
|
||||
const wpt_cmd = b.addRunArtifact(wpt);
|
||||
if (b.args) |args| {
|
||||
wpt_cmd.addArgs(args);
|
||||
}
|
||||
// step
|
||||
const wpt_step = b.step("wpt", "WPT tests");
|
||||
wpt_step.dependOn(&wpt_cmd.step);
|
||||
}
|
||||
|
||||
{
|
||||
// get v8
|
||||
// -------
|
||||
@@ -149,19 +90,63 @@ pub fn build(b: *Build) !void {
|
||||
const build_step = b.step("build-v8", "Build v8");
|
||||
build_step.dependOn(&build_v8.step);
|
||||
}
|
||||
|
||||
{
|
||||
// tests
|
||||
// ----
|
||||
|
||||
// compile
|
||||
const tests = b.addTest(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.test_runner = .{ .path = b.path("src/test_runner.zig"), .mode = .simple },
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
try common(b, opts, tests);
|
||||
|
||||
const run_tests = b.addRunArtifact(tests);
|
||||
if (b.args) |args| {
|
||||
run_tests.addArgs(args);
|
||||
}
|
||||
|
||||
// step
|
||||
const tests_step = b.step("test", "Run unit tests");
|
||||
tests_step.dependOn(&run_tests.step);
|
||||
}
|
||||
|
||||
{
|
||||
// wpt
|
||||
// -----
|
||||
|
||||
// compile and install
|
||||
const wpt = b.addExecutable(.{
|
||||
.name = "lightpanda-wpt",
|
||||
.root_source_file = b.path("src/main_wpt.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
try common(b, opts, wpt);
|
||||
|
||||
// run
|
||||
const wpt_cmd = b.addRunArtifact(wpt);
|
||||
if (b.args) |args| {
|
||||
wpt_cmd.addArgs(args);
|
||||
}
|
||||
// step
|
||||
const wpt_step = b.step("wpt", "WPT tests");
|
||||
wpt_step.dependOn(&wpt_cmd.step);
|
||||
}
|
||||
}
|
||||
|
||||
fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !void {
|
||||
try moduleNetSurf(b, mod);
|
||||
mod.addImport("build_config", opts.createModule());
|
||||
|
||||
fn common(b: *std.Build, opts: *std.Build.Step.Options, step: *std.Build.Step.Compile) !void {
|
||||
const mod = step.root_module;
|
||||
const target = mod.resolved_target.?;
|
||||
const dep_opts = .{
|
||||
.target = target,
|
||||
.optimize = mod.optimize.?,
|
||||
};
|
||||
const optimize = mod.optimize.?;
|
||||
const dep_opts = .{ .target = target, .optimize = optimize };
|
||||
|
||||
mod.addIncludePath(b.path("vendor/lightpanda"));
|
||||
try moduleNetSurf(b, step, target);
|
||||
mod.addImport("tls", b.dependency("tls", dep_opts).module("tls"));
|
||||
mod.addImport("tigerbeetle-io", b.dependency("tigerbeetle_io", .{}).module("tigerbeetle_io"));
|
||||
|
||||
{
|
||||
// v8
|
||||
@@ -171,232 +156,30 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo
|
||||
const v8_mod = b.dependency("v8", dep_opts).module("v8");
|
||||
v8_mod.addOptions("default_exports", v8_opts);
|
||||
mod.addImport("v8", v8_mod);
|
||||
|
||||
const release_dir = if (mod.optimize.? == .Debug) "debug" else "release";
|
||||
const os = switch (target.result.os.tag) {
|
||||
.linux => "linux",
|
||||
.macos => "macos",
|
||||
else => return error.UnsupportedPlatform,
|
||||
};
|
||||
var lib_path = try std.fmt.allocPrint(
|
||||
mod.owner.allocator,
|
||||
"v8/out/{s}/{s}/obj/zig/libc_v8.a",
|
||||
.{ os, release_dir },
|
||||
);
|
||||
std.fs.cwd().access(lib_path, .{}) catch {
|
||||
// legacy path
|
||||
lib_path = try std.fmt.allocPrint(
|
||||
mod.owner.allocator,
|
||||
"v8/out/{s}/obj/zig/libc_v8.a",
|
||||
.{release_dir},
|
||||
);
|
||||
};
|
||||
mod.addObjectFile(mod.owner.path(lib_path));
|
||||
|
||||
switch (target.result.os.tag) {
|
||||
.macos => {
|
||||
// v8 has a dependency, abseil-cpp, which, on Mac, uses CoreFoundation
|
||||
mod.addSystemFrameworkPath(.{ .cwd_relative = "/System/Library/Frameworks" });
|
||||
mod.linkFramework("CoreFoundation", .{});
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
//curl
|
||||
{
|
||||
const is_linux = target.result.os.tag == .linux;
|
||||
if (is_linux) {
|
||||
mod.addCMacro("HAVE_LINUX_TCP_H", "1");
|
||||
mod.addCMacro("HAVE_MSG_NOSIGNAL", "1");
|
||||
mod.addCMacro("HAVE_GETHOSTBYNAME_R", "1");
|
||||
}
|
||||
mod.addCMacro("_FILE_OFFSET_BITS", "64");
|
||||
mod.addCMacro("BUILDING_LIBCURL", "1");
|
||||
mod.addCMacro("CURL_DISABLE_AWS", "1");
|
||||
mod.addCMacro("CURL_DISABLE_DICT", "1");
|
||||
mod.addCMacro("CURL_DISABLE_DOH", "1");
|
||||
mod.addCMacro("CURL_DISABLE_FILE", "1");
|
||||
mod.addCMacro("CURL_DISABLE_FTP", "1");
|
||||
mod.addCMacro("CURL_DISABLE_GOPHER", "1");
|
||||
mod.addCMacro("CURL_DISABLE_KERBEROS", "1");
|
||||
mod.addCMacro("CURL_DISABLE_IMAP", "1");
|
||||
mod.addCMacro("CURL_DISABLE_IPFS", "1");
|
||||
mod.addCMacro("CURL_DISABLE_LDAP", "1");
|
||||
mod.addCMacro("CURL_DISABLE_LDAPS", "1");
|
||||
mod.addCMacro("CURL_DISABLE_MQTT", "1");
|
||||
mod.addCMacro("CURL_DISABLE_NTLM", "1");
|
||||
mod.addCMacro("CURL_DISABLE_PROGRESS_METER", "1");
|
||||
mod.addCMacro("CURL_DISABLE_POP3", "1");
|
||||
mod.addCMacro("CURL_DISABLE_RTSP", "1");
|
||||
mod.addCMacro("CURL_DISABLE_SMB", "1");
|
||||
mod.addCMacro("CURL_DISABLE_SMTP", "1");
|
||||
mod.addCMacro("CURL_DISABLE_TELNET", "1");
|
||||
mod.addCMacro("CURL_DISABLE_TFTP", "1");
|
||||
mod.addCMacro("CURL_EXTERN_SYMBOL", "__attribute__ ((__visibility__ (\"default\"))");
|
||||
mod.addCMacro("CURL_OS", if (is_linux) "\"Linux\"" else "\"mac\"");
|
||||
mod.addCMacro("CURL_STATICLIB", "1");
|
||||
mod.addCMacro("ENABLE_IPV6", "1");
|
||||
mod.addCMacro("HAVE_ALARM", "1");
|
||||
mod.addCMacro("HAVE_ALLOCA_H", "1");
|
||||
mod.addCMacro("HAVE_ARPA_INET_H", "1");
|
||||
mod.addCMacro("HAVE_ARPA_TFTP_H", "1");
|
||||
mod.addCMacro("HAVE_ASSERT_H", "1");
|
||||
mod.addCMacro("HAVE_BASENAME", "1");
|
||||
mod.addCMacro("HAVE_BOOL_T", "1");
|
||||
mod.addCMacro("HAVE_BUILTIN_AVAILABLE", "1");
|
||||
mod.addCMacro("HAVE_CLOCK_GETTIME_MONOTONIC", "1");
|
||||
mod.addCMacro("HAVE_DLFCN_H", "1");
|
||||
mod.addCMacro("HAVE_ERRNO_H", "1");
|
||||
mod.addCMacro("HAVE_FCNTL", "1");
|
||||
mod.addCMacro("HAVE_FCNTL_H", "1");
|
||||
mod.addCMacro("HAVE_FCNTL_O_NONBLOCK", "1");
|
||||
mod.addCMacro("HAVE_FREEADDRINFO", "1");
|
||||
mod.addCMacro("HAVE_FSETXATTR", "1");
|
||||
mod.addCMacro("HAVE_FSETXATTR_5", "1");
|
||||
mod.addCMacro("HAVE_FTRUNCATE", "1");
|
||||
mod.addCMacro("HAVE_GETADDRINFO", "1");
|
||||
mod.addCMacro("HAVE_GETEUID", "1");
|
||||
mod.addCMacro("HAVE_GETHOSTBYNAME", "1");
|
||||
mod.addCMacro("HAVE_GETHOSTBYNAME_R_6", "1");
|
||||
mod.addCMacro("HAVE_GETHOSTNAME", "1");
|
||||
mod.addCMacro("HAVE_GETPEERNAME", "1");
|
||||
mod.addCMacro("HAVE_GETPPID", "1");
|
||||
mod.addCMacro("HAVE_GETPPID", "1");
|
||||
mod.addCMacro("HAVE_GETPROTOBYNAME", "1");
|
||||
mod.addCMacro("HAVE_GETPWUID", "1");
|
||||
mod.addCMacro("HAVE_GETPWUID_R", "1");
|
||||
mod.addCMacro("HAVE_GETRLIMIT", "1");
|
||||
mod.addCMacro("HAVE_GETSOCKNAME", "1");
|
||||
mod.addCMacro("HAVE_GETTIMEOFDAY", "1");
|
||||
mod.addCMacro("HAVE_GMTIME_R", "1");
|
||||
mod.addCMacro("HAVE_IDN2_H", "1");
|
||||
mod.addCMacro("HAVE_IF_NAMETOINDEX", "1");
|
||||
mod.addCMacro("HAVE_IFADDRS_H", "1");
|
||||
mod.addCMacro("HAVE_INET_ADDR", "1");
|
||||
mod.addCMacro("HAVE_INET_PTON", "1");
|
||||
mod.addCMacro("HAVE_INTTYPES_H", "1");
|
||||
mod.addCMacro("HAVE_IOCTL", "1");
|
||||
mod.addCMacro("HAVE_IOCTL_FIONBIO", "1");
|
||||
mod.addCMacro("HAVE_IOCTL_SIOCGIFADDR", "1");
|
||||
mod.addCMacro("HAVE_LDAP_URL_PARSE", "1");
|
||||
mod.addCMacro("HAVE_LIBGEN_H", "1");
|
||||
mod.addCMacro("HAVE_LIBZ", "1");
|
||||
mod.addCMacro("HAVE_LL", "1");
|
||||
mod.addCMacro("HAVE_LOCALE_H", "1");
|
||||
mod.addCMacro("HAVE_LOCALTIME_R", "1");
|
||||
mod.addCMacro("HAVE_LONGLONG", "1");
|
||||
mod.addCMacro("HAVE_MALLOC_H", "1");
|
||||
mod.addCMacro("HAVE_MEMORY_H", "1");
|
||||
mod.addCMacro("HAVE_NET_IF_H", "1");
|
||||
mod.addCMacro("HAVE_NETDB_H", "1");
|
||||
mod.addCMacro("HAVE_NETINET_IN_H", "1");
|
||||
mod.addCMacro("HAVE_NETINET_TCP_H", "1");
|
||||
mod.addCMacro("HAVE_PIPE", "1");
|
||||
mod.addCMacro("HAVE_POLL", "1");
|
||||
mod.addCMacro("HAVE_POLL_FINE", "1");
|
||||
mod.addCMacro("HAVE_POLL_H", "1");
|
||||
mod.addCMacro("HAVE_POSIX_STRERROR_R", "1");
|
||||
mod.addCMacro("HAVE_PTHREAD_H", "1");
|
||||
mod.addCMacro("HAVE_PWD_H", "1");
|
||||
mod.addCMacro("HAVE_RECV", "1");
|
||||
mod.addCMacro("HAVE_SA_FAMILY_T", "1");
|
||||
mod.addCMacro("HAVE_SELECT", "1");
|
||||
mod.addCMacro("HAVE_SEND", "1");
|
||||
mod.addCMacro("HAVE_SETJMP_H", "1");
|
||||
mod.addCMacro("HAVE_SETLOCALE", "1");
|
||||
mod.addCMacro("HAVE_SETRLIMIT", "1");
|
||||
mod.addCMacro("HAVE_SETSOCKOPT", "1");
|
||||
mod.addCMacro("HAVE_SIGACTION", "1");
|
||||
mod.addCMacro("HAVE_SIGINTERRUPT", "1");
|
||||
mod.addCMacro("HAVE_SIGNAL", "1");
|
||||
mod.addCMacro("HAVE_SIGNAL_H", "1");
|
||||
mod.addCMacro("HAVE_SIGSETJMP", "1");
|
||||
mod.addCMacro("HAVE_SOCKADDR_IN6_SIN6_SCOPE_ID", "1");
|
||||
mod.addCMacro("HAVE_SOCKET", "1");
|
||||
mod.addCMacro("HAVE_STDBOOL_H", "1");
|
||||
mod.addCMacro("HAVE_STDINT_H", "1");
|
||||
mod.addCMacro("HAVE_STDIO_H", "1");
|
||||
mod.addCMacro("HAVE_STDLIB_H", "1");
|
||||
mod.addCMacro("HAVE_STRCASECMP", "1");
|
||||
mod.addCMacro("HAVE_STRDUP", "1");
|
||||
mod.addCMacro("HAVE_STRERROR_R", "1");
|
||||
mod.addCMacro("HAVE_STRING_H", "1");
|
||||
mod.addCMacro("HAVE_STRINGS_H", "1");
|
||||
mod.addCMacro("HAVE_STRSTR", "1");
|
||||
mod.addCMacro("HAVE_STRTOK_R", "1");
|
||||
mod.addCMacro("HAVE_STRTOLL", "1");
|
||||
mod.addCMacro("HAVE_STRUCT_SOCKADDR_STORAGE", "1");
|
||||
mod.addCMacro("HAVE_STRUCT_TIMEVAL", "1");
|
||||
mod.addCMacro("HAVE_SYS_IOCTL_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_PARAM_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_POLL_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_RESOURCE_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_SELECT_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_SOCKET_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_STAT_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_TIME_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_TYPES_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_UIO_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_UN_H", "1");
|
||||
mod.addCMacro("HAVE_TERMIO_H", "1");
|
||||
mod.addCMacro("HAVE_TERMIOS_H", "1");
|
||||
mod.addCMacro("HAVE_TIME_H", "1");
|
||||
mod.addCMacro("HAVE_UNAME", "1");
|
||||
mod.addCMacro("HAVE_UNISTD_H", "1");
|
||||
mod.addCMacro("HAVE_UTIME", "1");
|
||||
mod.addCMacro("HAVE_UTIME_H", "1");
|
||||
mod.addCMacro("HAVE_UTIMES", "1");
|
||||
mod.addCMacro("HAVE_VARIADIC_MACROS_C99", "1");
|
||||
mod.addCMacro("HAVE_VARIADIC_MACROS_GCC", "1");
|
||||
mod.addCMacro("HAVE_ZLIB_H", "1");
|
||||
mod.addCMacro("RANDOM_FILE", "\"/dev/urandom\"");
|
||||
mod.addCMacro("RECV_TYPE_ARG1", "int");
|
||||
mod.addCMacro("RECV_TYPE_ARG2", "void *");
|
||||
mod.addCMacro("RECV_TYPE_ARG3", "size_t");
|
||||
mod.addCMacro("RECV_TYPE_ARG4", "int");
|
||||
mod.addCMacro("RECV_TYPE_RETV", "ssize_t");
|
||||
mod.addCMacro("SEND_QUAL_ARG2", "const");
|
||||
mod.addCMacro("SEND_TYPE_ARG1", "int");
|
||||
mod.addCMacro("SEND_TYPE_ARG2", "void *");
|
||||
mod.addCMacro("SEND_TYPE_ARG3", "size_t");
|
||||
mod.addCMacro("SEND_TYPE_ARG4", "int");
|
||||
mod.addCMacro("SEND_TYPE_RETV", "ssize_t");
|
||||
mod.addCMacro("SIZEOF_CURL_OFF_T", "8");
|
||||
mod.addCMacro("SIZEOF_INT", "4");
|
||||
mod.addCMacro("SIZEOF_LONG", "8");
|
||||
mod.addCMacro("SIZEOF_OFF_T", "8");
|
||||
mod.addCMacro("SIZEOF_SHORT", "2");
|
||||
mod.addCMacro("SIZEOF_SIZE_T", "8");
|
||||
mod.addCMacro("SIZEOF_TIME_T", "8");
|
||||
mod.addCMacro("STDC_HEADERS", "1");
|
||||
mod.addCMacro("TIME_WITH_SYS_TIME", "1");
|
||||
mod.addCMacro("USE_NGHTTP2", "1");
|
||||
mod.addCMacro("USE_MBEDTLS", "1");
|
||||
mod.addCMacro("USE_THREADS_POSIX", "1");
|
||||
mod.addCMacro("USE_UNIX_SOCKETS", "1");
|
||||
}
|
||||
const lib_path = try std.fmt.allocPrint(
|
||||
mod.owner.allocator,
|
||||
"v8/out/{s}/obj/zig/libc_v8.a",
|
||||
.{if (mod.optimize.? == .Debug) "debug" else "release"},
|
||||
);
|
||||
mod.link_libcpp = true;
|
||||
mod.addObjectFile(mod.owner.path(lib_path));
|
||||
|
||||
try buildZlib(b, mod);
|
||||
try buildMbedtls(b, mod);
|
||||
try buildNghttp2(b, mod);
|
||||
try buildCurl(b, mod);
|
||||
|
||||
switch (target.result.os.tag) {
|
||||
.macos => {
|
||||
// needed for proxying on mac
|
||||
mod.addSystemFrameworkPath(.{ .cwd_relative = "/System/Library/Frameworks" });
|
||||
mod.linkFramework("CoreFoundation", .{});
|
||||
mod.linkFramework("SystemConfiguration", .{});
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
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 => {},
|
||||
}
|
||||
|
||||
mod.addImport("build_info", opts.createModule());
|
||||
mod.addObjectFile(mod.owner.path(lib_path));
|
||||
}
|
||||
|
||||
fn moduleNetSurf(b: *Build, mod: *Build.Module) !void {
|
||||
const target = mod.resolved_target.?;
|
||||
fn moduleNetSurf(b: *std.Build, step: *std.Build.Step.Compile, target: std.Build.ResolvedTarget) !void {
|
||||
const os = target.result.os.tag;
|
||||
const arch = target.result.cpu.arch;
|
||||
|
||||
@@ -411,8 +194,8 @@ fn moduleNetSurf(b: *Build, mod: *Build.Module) !void {
|
||||
"vendor/libiconv/out/{s}-{s}/lib/libiconv.a",
|
||||
.{ @tagName(os), @tagName(arch) },
|
||||
);
|
||||
mod.addObjectFile(b.path(libiconv_lib_path));
|
||||
mod.addIncludePath(b.path(libiconv_include_path));
|
||||
step.addObjectFile(b.path(libiconv_lib_path));
|
||||
step.addIncludePath(b.path(libiconv_include_path));
|
||||
|
||||
{
|
||||
// mimalloc
|
||||
@@ -422,8 +205,8 @@ fn moduleNetSurf(b: *Build, mod: *Build.Module) !void {
|
||||
mimalloc ++ "/out/{s}-{s}/lib/libmimalloc.a",
|
||||
.{ @tagName(os), @tagName(arch) },
|
||||
);
|
||||
mod.addObjectFile(b.path(lib_path));
|
||||
mod.addIncludePath(b.path(mimalloc ++ "/include"));
|
||||
step.addObjectFile(b.path(lib_path));
|
||||
step.addIncludePath(b.path(mimalloc ++ "/include"));
|
||||
}
|
||||
|
||||
// netsurf libs
|
||||
@@ -433,7 +216,7 @@ fn moduleNetSurf(b: *Build, mod: *Build.Module) !void {
|
||||
ns ++ "/out/{s}-{s}/include",
|
||||
.{ @tagName(os), @tagName(arch) },
|
||||
);
|
||||
mod.addIncludePath(b.path(ns_include_path));
|
||||
step.addIncludePath(b.path(ns_include_path));
|
||||
|
||||
const libs: [4][]const u8 = .{
|
||||
"libdom",
|
||||
@@ -447,379 +230,7 @@ fn moduleNetSurf(b: *Build, mod: *Build.Module) !void {
|
||||
ns ++ "/out/{s}-{s}/lib/" ++ lib ++ ".a",
|
||||
.{ @tagName(os), @tagName(arch) },
|
||||
);
|
||||
mod.addObjectFile(b.path(ns_lib_path));
|
||||
mod.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src"));
|
||||
step.addObjectFile(b.path(ns_lib_path));
|
||||
step.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src"));
|
||||
}
|
||||
}
|
||||
|
||||
fn buildZlib(b: *Build, m: *Build.Module) !void {
|
||||
const zlib = b.addLibrary(.{
|
||||
.name = "zlib",
|
||||
.root_module = m,
|
||||
});
|
||||
|
||||
const root = "vendor/zlib/";
|
||||
zlib.installHeader(b.path(root ++ "zlib.h"), "zlib.h");
|
||||
zlib.installHeader(b.path(root ++ "zconf.h"), "zconf.h");
|
||||
zlib.addCSourceFiles(.{ .flags = &.{
|
||||
"-DHAVE_SYS_TYPES_H",
|
||||
"-DHAVE_STDINT_H",
|
||||
"-DHAVE_STDDEF_H",
|
||||
}, .files = &.{
|
||||
root ++ "adler32.c",
|
||||
root ++ "compress.c",
|
||||
root ++ "crc32.c",
|
||||
root ++ "deflate.c",
|
||||
root ++ "gzclose.c",
|
||||
root ++ "gzlib.c",
|
||||
root ++ "gzread.c",
|
||||
root ++ "gzwrite.c",
|
||||
root ++ "inflate.c",
|
||||
root ++ "infback.c",
|
||||
root ++ "inftrees.c",
|
||||
root ++ "inffast.c",
|
||||
root ++ "trees.c",
|
||||
root ++ "uncompr.c",
|
||||
root ++ "zutil.c",
|
||||
} });
|
||||
}
|
||||
|
||||
fn buildMbedtls(b: *Build, m: *Build.Module) !void {
|
||||
const mbedtls = b.addLibrary(.{
|
||||
.name = "mbedtls",
|
||||
.root_module = m,
|
||||
});
|
||||
|
||||
const root = "vendor/mbedtls/";
|
||||
mbedtls.addIncludePath(b.path(root ++ "include"));
|
||||
mbedtls.addIncludePath(b.path(root ++ "library"));
|
||||
|
||||
mbedtls.addCSourceFiles(.{ .flags = &.{}, .files = &.{
|
||||
root ++ "library/aes.c",
|
||||
root ++ "library/aesni.c",
|
||||
root ++ "library/aesce.c",
|
||||
root ++ "library/aria.c",
|
||||
root ++ "library/asn1parse.c",
|
||||
root ++ "library/asn1write.c",
|
||||
root ++ "library/base64.c",
|
||||
root ++ "library/bignum.c",
|
||||
root ++ "library/bignum_core.c",
|
||||
root ++ "library/bignum_mod.c",
|
||||
root ++ "library/bignum_mod_raw.c",
|
||||
root ++ "library/camellia.c",
|
||||
root ++ "library/ccm.c",
|
||||
root ++ "library/chacha20.c",
|
||||
root ++ "library/chachapoly.c",
|
||||
root ++ "library/cipher.c",
|
||||
root ++ "library/cipher_wrap.c",
|
||||
root ++ "library/constant_time.c",
|
||||
root ++ "library/cmac.c",
|
||||
root ++ "library/ctr_drbg.c",
|
||||
root ++ "library/des.c",
|
||||
root ++ "library/dhm.c",
|
||||
root ++ "library/ecdh.c",
|
||||
root ++ "library/ecdsa.c",
|
||||
root ++ "library/ecjpake.c",
|
||||
root ++ "library/ecp.c",
|
||||
root ++ "library/ecp_curves.c",
|
||||
root ++ "library/entropy.c",
|
||||
root ++ "library/entropy_poll.c",
|
||||
root ++ "library/error.c",
|
||||
root ++ "library/gcm.c",
|
||||
root ++ "library/hkdf.c",
|
||||
root ++ "library/hmac_drbg.c",
|
||||
root ++ "library/lmots.c",
|
||||
root ++ "library/lms.c",
|
||||
root ++ "library/md.c",
|
||||
root ++ "library/md5.c",
|
||||
root ++ "library/memory_buffer_alloc.c",
|
||||
root ++ "library/nist_kw.c",
|
||||
root ++ "library/oid.c",
|
||||
root ++ "library/padlock.c",
|
||||
root ++ "library/pem.c",
|
||||
root ++ "library/pk.c",
|
||||
root ++ "library/pk_ecc.c",
|
||||
root ++ "library/pk_wrap.c",
|
||||
root ++ "library/pkcs12.c",
|
||||
root ++ "library/pkcs5.c",
|
||||
root ++ "library/pkparse.c",
|
||||
root ++ "library/pkwrite.c",
|
||||
root ++ "library/platform.c",
|
||||
root ++ "library/platform_util.c",
|
||||
root ++ "library/poly1305.c",
|
||||
root ++ "library/psa_crypto.c",
|
||||
root ++ "library/psa_crypto_aead.c",
|
||||
root ++ "library/psa_crypto_cipher.c",
|
||||
root ++ "library/psa_crypto_client.c",
|
||||
root ++ "library/psa_crypto_ffdh.c",
|
||||
root ++ "library/psa_crypto_driver_wrappers_no_static.c",
|
||||
root ++ "library/psa_crypto_ecp.c",
|
||||
root ++ "library/psa_crypto_hash.c",
|
||||
root ++ "library/psa_crypto_mac.c",
|
||||
root ++ "library/psa_crypto_pake.c",
|
||||
root ++ "library/psa_crypto_rsa.c",
|
||||
root ++ "library/psa_crypto_se.c",
|
||||
root ++ "library/psa_crypto_slot_management.c",
|
||||
root ++ "library/psa_crypto_storage.c",
|
||||
root ++ "library/psa_its_file.c",
|
||||
root ++ "library/psa_util.c",
|
||||
root ++ "library/ripemd160.c",
|
||||
root ++ "library/rsa.c",
|
||||
root ++ "library/rsa_alt_helpers.c",
|
||||
root ++ "library/sha1.c",
|
||||
root ++ "library/sha3.c",
|
||||
root ++ "library/sha256.c",
|
||||
root ++ "library/sha512.c",
|
||||
root ++ "library/threading.c",
|
||||
root ++ "library/timing.c",
|
||||
root ++ "library/version.c",
|
||||
root ++ "library/version_features.c",
|
||||
root ++ "library/pkcs7.c",
|
||||
root ++ "library/x509.c",
|
||||
root ++ "library/x509_create.c",
|
||||
root ++ "library/x509_crl.c",
|
||||
root ++ "library/x509_crt.c",
|
||||
root ++ "library/x509_csr.c",
|
||||
root ++ "library/x509write.c",
|
||||
root ++ "library/x509write_crt.c",
|
||||
root ++ "library/x509write_csr.c",
|
||||
root ++ "library/debug.c",
|
||||
root ++ "library/mps_reader.c",
|
||||
root ++ "library/mps_trace.c",
|
||||
root ++ "library/net_sockets.c",
|
||||
root ++ "library/ssl_cache.c",
|
||||
root ++ "library/ssl_ciphersuites.c",
|
||||
root ++ "library/ssl_client.c",
|
||||
root ++ "library/ssl_cookie.c",
|
||||
root ++ "library/ssl_debug_helpers_generated.c",
|
||||
root ++ "library/ssl_msg.c",
|
||||
root ++ "library/ssl_ticket.c",
|
||||
root ++ "library/ssl_tls.c",
|
||||
root ++ "library/ssl_tls12_client.c",
|
||||
root ++ "library/ssl_tls12_server.c",
|
||||
root ++ "library/ssl_tls13_keys.c",
|
||||
root ++ "library/ssl_tls13_server.c",
|
||||
root ++ "library/ssl_tls13_client.c",
|
||||
root ++ "library/ssl_tls13_generic.c",
|
||||
} });
|
||||
}
|
||||
|
||||
fn buildNghttp2(b: *Build, m: *Build.Module) !void {
|
||||
const nghttp2 = b.addLibrary(.{
|
||||
.name = "nghttp2",
|
||||
.root_module = m,
|
||||
});
|
||||
|
||||
const root = "vendor/nghttp2/";
|
||||
nghttp2.addIncludePath(b.path(root ++ "lib"));
|
||||
nghttp2.addIncludePath(b.path(root ++ "lib/includes"));
|
||||
nghttp2.addCSourceFiles(.{ .flags = &.{
|
||||
"-DNGHTTP2_STATICLIB",
|
||||
"-DHAVE_NETINET_IN",
|
||||
"-DHAVE_TIME_H",
|
||||
}, .files = &.{
|
||||
root ++ "lib/sfparse.c",
|
||||
root ++ "lib/nghttp2_alpn.c",
|
||||
root ++ "lib/nghttp2_buf.c",
|
||||
root ++ "lib/nghttp2_callbacks.c",
|
||||
root ++ "lib/nghttp2_debug.c",
|
||||
root ++ "lib/nghttp2_extpri.c",
|
||||
root ++ "lib/nghttp2_frame.c",
|
||||
root ++ "lib/nghttp2_hd.c",
|
||||
root ++ "lib/nghttp2_hd_huffman.c",
|
||||
root ++ "lib/nghttp2_hd_huffman_data.c",
|
||||
root ++ "lib/nghttp2_helper.c",
|
||||
root ++ "lib/nghttp2_http.c",
|
||||
root ++ "lib/nghttp2_map.c",
|
||||
root ++ "lib/nghttp2_mem.c",
|
||||
root ++ "lib/nghttp2_option.c",
|
||||
root ++ "lib/nghttp2_outbound_item.c",
|
||||
root ++ "lib/nghttp2_pq.c",
|
||||
root ++ "lib/nghttp2_priority_spec.c",
|
||||
root ++ "lib/nghttp2_queue.c",
|
||||
root ++ "lib/nghttp2_rcbuf.c",
|
||||
root ++ "lib/nghttp2_session.c",
|
||||
root ++ "lib/nghttp2_stream.c",
|
||||
root ++ "lib/nghttp2_submit.c",
|
||||
root ++ "lib/nghttp2_version.c",
|
||||
root ++ "lib/nghttp2_ratelim.c",
|
||||
root ++ "lib/nghttp2_time.c",
|
||||
} });
|
||||
}
|
||||
|
||||
fn buildCurl(b: *Build, m: *Build.Module) !void {
|
||||
const curl = b.addLibrary(.{
|
||||
.name = "curl",
|
||||
.root_module = m,
|
||||
});
|
||||
|
||||
const root = "vendor/curl/";
|
||||
|
||||
curl.addIncludePath(b.path(root ++ "lib"));
|
||||
curl.addIncludePath(b.path(root ++ "include"));
|
||||
curl.addCSourceFiles(.{
|
||||
.flags = &.{},
|
||||
.files = &.{
|
||||
root ++ "lib/altsvc.c",
|
||||
root ++ "lib/amigaos.c",
|
||||
root ++ "lib/asyn-ares.c",
|
||||
root ++ "lib/asyn-base.c",
|
||||
root ++ "lib/asyn-thrdd.c",
|
||||
root ++ "lib/bufq.c",
|
||||
root ++ "lib/bufref.c",
|
||||
root ++ "lib/cf-h1-proxy.c",
|
||||
root ++ "lib/cf-h2-proxy.c",
|
||||
root ++ "lib/cf-haproxy.c",
|
||||
root ++ "lib/cf-https-connect.c",
|
||||
root ++ "lib/cf-socket.c",
|
||||
root ++ "lib/cfilters.c",
|
||||
root ++ "lib/conncache.c",
|
||||
root ++ "lib/connect.c",
|
||||
root ++ "lib/content_encoding.c",
|
||||
root ++ "lib/cookie.c",
|
||||
root ++ "lib/cshutdn.c",
|
||||
root ++ "lib/curl_addrinfo.c",
|
||||
root ++ "lib/curl_des.c",
|
||||
root ++ "lib/curl_endian.c",
|
||||
root ++ "lib/curl_fnmatch.c",
|
||||
root ++ "lib/curl_get_line.c",
|
||||
root ++ "lib/curl_gethostname.c",
|
||||
root ++ "lib/curl_gssapi.c",
|
||||
root ++ "lib/curl_memrchr.c",
|
||||
root ++ "lib/curl_ntlm_core.c",
|
||||
root ++ "lib/curl_range.c",
|
||||
root ++ "lib/curl_rtmp.c",
|
||||
root ++ "lib/curl_sasl.c",
|
||||
root ++ "lib/curl_sha512_256.c",
|
||||
root ++ "lib/curl_sspi.c",
|
||||
root ++ "lib/curl_threads.c",
|
||||
root ++ "lib/curl_trc.c",
|
||||
root ++ "lib/cw-out.c",
|
||||
root ++ "lib/cw-pause.c",
|
||||
root ++ "lib/dict.c",
|
||||
root ++ "lib/doh.c",
|
||||
root ++ "lib/dynhds.c",
|
||||
root ++ "lib/easy.c",
|
||||
root ++ "lib/easygetopt.c",
|
||||
root ++ "lib/easyoptions.c",
|
||||
root ++ "lib/escape.c",
|
||||
root ++ "lib/fake_addrinfo.c",
|
||||
root ++ "lib/file.c",
|
||||
root ++ "lib/fileinfo.c",
|
||||
root ++ "lib/fopen.c",
|
||||
root ++ "lib/formdata.c",
|
||||
root ++ "lib/ftp.c",
|
||||
root ++ "lib/ftplistparser.c",
|
||||
root ++ "lib/getenv.c",
|
||||
root ++ "lib/getinfo.c",
|
||||
root ++ "lib/gopher.c",
|
||||
root ++ "lib/hash.c",
|
||||
root ++ "lib/headers.c",
|
||||
root ++ "lib/hmac.c",
|
||||
root ++ "lib/hostip.c",
|
||||
root ++ "lib/hostip4.c",
|
||||
root ++ "lib/hostip6.c",
|
||||
root ++ "lib/hsts.c",
|
||||
root ++ "lib/http.c",
|
||||
root ++ "lib/http1.c",
|
||||
root ++ "lib/http2.c",
|
||||
root ++ "lib/http_aws_sigv4.c",
|
||||
root ++ "lib/http_chunks.c",
|
||||
root ++ "lib/http_digest.c",
|
||||
root ++ "lib/http_negotiate.c",
|
||||
root ++ "lib/http_ntlm.c",
|
||||
root ++ "lib/http_proxy.c",
|
||||
root ++ "lib/httpsrr.c",
|
||||
root ++ "lib/idn.c",
|
||||
root ++ "lib/if2ip.c",
|
||||
root ++ "lib/imap.c",
|
||||
root ++ "lib/krb5.c",
|
||||
root ++ "lib/ldap.c",
|
||||
root ++ "lib/llist.c",
|
||||
root ++ "lib/macos.c",
|
||||
root ++ "lib/md4.c",
|
||||
root ++ "lib/md5.c",
|
||||
root ++ "lib/memdebug.c",
|
||||
root ++ "lib/mime.c",
|
||||
root ++ "lib/mprintf.c",
|
||||
root ++ "lib/mqtt.c",
|
||||
root ++ "lib/multi.c",
|
||||
root ++ "lib/multi_ev.c",
|
||||
root ++ "lib/netrc.c",
|
||||
root ++ "lib/noproxy.c",
|
||||
root ++ "lib/openldap.c",
|
||||
root ++ "lib/parsedate.c",
|
||||
root ++ "lib/pingpong.c",
|
||||
root ++ "lib/pop3.c",
|
||||
root ++ "lib/progress.c",
|
||||
root ++ "lib/psl.c",
|
||||
root ++ "lib/rand.c",
|
||||
root ++ "lib/rename.c",
|
||||
root ++ "lib/request.c",
|
||||
root ++ "lib/rtsp.c",
|
||||
root ++ "lib/select.c",
|
||||
root ++ "lib/sendf.c",
|
||||
root ++ "lib/setopt.c",
|
||||
root ++ "lib/sha256.c",
|
||||
root ++ "lib/share.c",
|
||||
root ++ "lib/slist.c",
|
||||
root ++ "lib/smb.c",
|
||||
root ++ "lib/smtp.c",
|
||||
root ++ "lib/socketpair.c",
|
||||
root ++ "lib/socks.c",
|
||||
root ++ "lib/socks_gssapi.c",
|
||||
root ++ "lib/socks_sspi.c",
|
||||
root ++ "lib/speedcheck.c",
|
||||
root ++ "lib/splay.c",
|
||||
root ++ "lib/strcase.c",
|
||||
root ++ "lib/strdup.c",
|
||||
root ++ "lib/strequal.c",
|
||||
root ++ "lib/strerror.c",
|
||||
root ++ "lib/system_win32.c",
|
||||
root ++ "lib/telnet.c",
|
||||
root ++ "lib/tftp.c",
|
||||
root ++ "lib/transfer.c",
|
||||
root ++ "lib/uint-bset.c",
|
||||
root ++ "lib/uint-hash.c",
|
||||
root ++ "lib/uint-spbset.c",
|
||||
root ++ "lib/uint-table.c",
|
||||
root ++ "lib/url.c",
|
||||
root ++ "lib/urlapi.c",
|
||||
root ++ "lib/version.c",
|
||||
root ++ "lib/ws.c",
|
||||
root ++ "lib/curlx/base64.c",
|
||||
root ++ "lib/curlx/dynbuf.c",
|
||||
root ++ "lib/curlx/inet_ntop.c",
|
||||
root ++ "lib/curlx/nonblock.c",
|
||||
root ++ "lib/curlx/strparse.c",
|
||||
root ++ "lib/curlx/timediff.c",
|
||||
root ++ "lib/curlx/timeval.c",
|
||||
root ++ "lib/curlx/wait.c",
|
||||
root ++ "lib/curlx/warnless.c",
|
||||
root ++ "lib/vquic/curl_ngtcp2.c",
|
||||
root ++ "lib/vquic/curl_osslq.c",
|
||||
root ++ "lib/vquic/curl_quiche.c",
|
||||
root ++ "lib/vquic/vquic.c",
|
||||
root ++ "lib/vquic/vquic-tls.c",
|
||||
root ++ "lib/vauth/cleartext.c",
|
||||
root ++ "lib/vauth/cram.c",
|
||||
root ++ "lib/vauth/digest.c",
|
||||
root ++ "lib/vauth/digest_sspi.c",
|
||||
root ++ "lib/vauth/gsasl.c",
|
||||
root ++ "lib/vauth/krb5_gssapi.c",
|
||||
root ++ "lib/vauth/krb5_sspi.c",
|
||||
root ++ "lib/vauth/ntlm.c",
|
||||
root ++ "lib/vauth/ntlm_sspi.c",
|
||||
root ++ "lib/vauth/oauth2.c",
|
||||
root ++ "lib/vauth/spnego_gssapi.c",
|
||||
root ++ "lib/vauth/spnego_sspi.c",
|
||||
root ++ "lib/vauth/vauth.c",
|
||||
root ++ "lib/vtls/cipher_suite.c",
|
||||
root ++ "lib/vtls/mbedtls.c",
|
||||
root ++ "lib/vtls/mbedtls_threadlock.c",
|
||||
root ++ "lib/vtls/vtls.c",
|
||||
root ++ "lib/vtls/vtls_scache.c",
|
||||
root ++ "lib/vtls/x509asn1.c",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,10 +4,19 @@
|
||||
.version = "0.0.0",
|
||||
.fingerprint = 0xda130f3af836cea0,
|
||||
.dependencies = .{
|
||||
.v8 = .{
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/d3040a953e5a37290dae20e7ddf138b7aeb5e67d.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH6w_EAwA8vK0NAxfxfI7IcbnpkUAcXKNujn7qwnmY",
|
||||
.tls = .{
|
||||
.url = "https://github.com/ianic/tls.zig/archive/b29a8b45fc59fc2d202769c4f54509bb9e17d0a2.tar.gz",
|
||||
.hash = "tls-0.1.0-ER2e0uAxBQDm_TmSDdbiiyvAZoh4ejlDD4hW8Fl813xE",
|
||||
},
|
||||
//.v8 = .{ .path = "../zig-v8-fork" }
|
||||
.tigerbeetle_io = .{
|
||||
.url = "https://github.com/lightpanda-io/tigerbeetle-io/archive/61d9652f1a957b7f4db723ea6aa0ce9635e840ce.tar.gz",
|
||||
.hash = "tigerbeetle_io-0.0.0-ViLgxpyRBAB5BMfIcj3KMXfbJzwARs9uSl8aRy2OXULd",
|
||||
},
|
||||
.v8 = .{
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/6f1ee74a0e7002ea3568e337ab716c1e75c53769.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH6z2yAwCOPUGmy1IgXysI1yWt8ftd2Z3D5zp0I9tV",
|
||||
},
|
||||
//.v8 = .{ .path = "../zig-v8-fork" },
|
||||
//.tigerbeetle_io = .{ .path = "../tigerbeetle-io" },
|
||||
},
|
||||
}
|
||||
|
||||
134
flake.lock
generated
134
flake.lock
generated
@@ -3,11 +3,11 @@
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||
"lastModified": 1733328505,
|
||||
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -39,11 +39,11 @@
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1705309234,
|
||||
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -52,39 +52,74 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"flake-utils_3": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"zlsPkg",
|
||||
"nixpkgs"
|
||||
]
|
||||
"systems": "systems_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709087332,
|
||||
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"iguana": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"zigPkgs": "zigPkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1746539192,
|
||||
"narHash": "sha256-32nN8JlRqNuCFfrDooyre+gDSnxZuCtK/qaHhRmGMhg=",
|
||||
"owner": "mookums",
|
||||
"repo": "iguana",
|
||||
"rev": "5569f95694edf59803429400ff6cb1c7522da801",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "mookums",
|
||||
"repo": "iguana",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1756822655,
|
||||
"narHash": "sha256-xQAk8xLy7srAkR5NMZFsQFioL02iTHuuEIs3ohGpgdk=",
|
||||
"lastModified": 1746397377,
|
||||
"narHash": "sha256-5oLdRa3vWSRbuqPIFFmQBGGUqaYZBxX+GGtN9f/n4lU=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "ed30f8aba41605e3ab46421e3dcb4510ec560ff8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1746481231,
|
||||
"narHash": "sha256-U3VKPi5D2oLBFzaMI0jJLJp8J64ZLjz+EwodUS//QWc=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "4bdac60bfe32c41103ae500ddf894c258291dd61",
|
||||
"rev": "c6aca34d2ca2ce9e20b722f54e684cda64b275c2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "release-25.05",
|
||||
"ref": "release-24.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
@@ -92,9 +127,8 @@
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"zigPkgs": "zigPkgs",
|
||||
"zlsPkg": "zlsPkg"
|
||||
"iguana": "iguana",
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
@@ -127,50 +161,38 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"zigPkgs": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"systems_3": {
|
||||
"locked": {
|
||||
"lastModified": 1756555914,
|
||||
"narHash": "sha256-7yoSPIVEuL+3Wzf6e7NHuW3zmruHizRrYhGerjRHTLI=",
|
||||
"owner": "mitchellh",
|
||||
"repo": "zig-overlay",
|
||||
"rev": "d0df3a2fd0f11134409d6d5ea0e510e5e477f7d6",
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "mitchellh",
|
||||
"repo": "zig-overlay",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"zlsPkg": {
|
||||
"zigPkgs": {
|
||||
"inputs": {
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"zig-overlay": [
|
||||
"zigPkgs"
|
||||
]
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-utils": "flake-utils_3",
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1756048867,
|
||||
"narHash": "sha256-GFzSHUljcxy7sM1PaabbkQUdUnLwpherekPWJFxXtnk=",
|
||||
"owner": "zigtools",
|
||||
"repo": "zls",
|
||||
"rev": "ce6c8f02c78e622421cfc2405c67c5222819ec03",
|
||||
"lastModified": 1746475050,
|
||||
"narHash": "sha256-KJC7BNY+NPCc1I+quGkWtoHXOMvFVEyer8Y0haOtTCA=",
|
||||
"owner": "mookums",
|
||||
"repo": "zig-overlay",
|
||||
"rev": "dfa488aa462932e46f44fddf6677ff22f1244c22",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "zigtools",
|
||||
"ref": "0.15.0",
|
||||
"repo": "zls",
|
||||
"owner": "mookums",
|
||||
"repo": "zig-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
|
||||
55
flake.nix
55
flake.nix
@@ -2,14 +2,10 @@
|
||||
description = "headless browser designed for AI and automation";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/release-25.05";
|
||||
nixpkgs.url = "github:nixos/nixpkgs/release-24.11";
|
||||
|
||||
zigPkgs.url = "github:mitchellh/zig-overlay";
|
||||
zigPkgs.inputs.nixpkgs.follows = "nixpkgs";
|
||||
|
||||
zlsPkg.url = "github:zigtools/zls/0.15.0";
|
||||
zlsPkg.inputs.zig-overlay.follows = "zigPkgs";
|
||||
zlsPkg.inputs.nixpkgs.follows = "nixpkgs";
|
||||
iguana.url = "github:mookums/iguana";
|
||||
iguana.inputs.nixpkgs.follows = "nixpkgs";
|
||||
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
@@ -17,56 +13,45 @@
|
||||
outputs =
|
||||
{
|
||||
nixpkgs,
|
||||
zigPkgs,
|
||||
zlsPkg,
|
||||
iguana,
|
||||
flake-utils,
|
||||
...
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
overlays = [
|
||||
(final: prev: {
|
||||
zigpkgs = zigPkgs.packages.${prev.system};
|
||||
zls = zlsPkg.packages.${prev.system}.default;
|
||||
})
|
||||
];
|
||||
zigVersion = "0_14_0";
|
||||
iguanaLib = iguana.lib.${system};
|
||||
|
||||
pkgs = import nixpkgs {
|
||||
inherit system overlays;
|
||||
inherit system;
|
||||
overlays = [
|
||||
(iguanaLib.mkZigOverlay zigVersion)
|
||||
(iguanaLib.mkZlsOverlay zigVersion)
|
||||
];
|
||||
};
|
||||
|
||||
# We need crtbeginS.o for building.
|
||||
crtFiles = pkgs.runCommand "crt-files" { } ''
|
||||
mkdir -p $out/lib
|
||||
cp -r ${pkgs.gcc.cc}/lib/gcc $out/lib/gcc
|
||||
'';
|
||||
|
||||
# This build pipeline is very unhappy without an FHS-compliant env.
|
||||
fhs = pkgs.buildFHSEnv {
|
||||
fhs = pkgs.buildFHSUserEnv {
|
||||
name = "fhs-shell";
|
||||
multiArch = true;
|
||||
targetPkgs =
|
||||
pkgs: with pkgs; [
|
||||
# Build Tools
|
||||
zigpkgs."0.15.1"
|
||||
zig
|
||||
zls
|
||||
python3
|
||||
pkg-config
|
||||
cmake
|
||||
gperf
|
||||
|
||||
# GCC
|
||||
gcc
|
||||
gcc.cc.lib
|
||||
crtFiles
|
||||
|
||||
# Libaries
|
||||
expat.dev
|
||||
python3
|
||||
glib.dev
|
||||
glibc.dev
|
||||
zlib
|
||||
zlib.dev
|
||||
ninja
|
||||
gn
|
||||
gcc-unwrapped
|
||||
binutils
|
||||
clang
|
||||
clang-tools
|
||||
];
|
||||
};
|
||||
in
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
const TestHTTPServer = @This();
|
||||
|
||||
shutdown: bool,
|
||||
listener: ?std.net.Server,
|
||||
handler: Handler,
|
||||
|
||||
const Handler = *const fn (req: *std.http.Server.Request) anyerror!void;
|
||||
|
||||
pub fn init(handler: Handler) TestHTTPServer {
|
||||
return .{
|
||||
.shutdown = true,
|
||||
.listener = null,
|
||||
.handler = handler,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *TestHTTPServer) void {
|
||||
self.shutdown = true;
|
||||
if (self.listener) |*listener| {
|
||||
listener.deinit();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(self: *TestHTTPServer, wg: *std.Thread.WaitGroup) !void {
|
||||
const address = try std.net.Address.parseIp("127.0.0.1", 9582);
|
||||
|
||||
self.listener = try address.listen(.{ .reuse_address = true });
|
||||
var listener = &self.listener.?;
|
||||
|
||||
wg.finish();
|
||||
|
||||
while (true) {
|
||||
const conn = listener.accept() catch |err| {
|
||||
if (self.shutdown) {
|
||||
return;
|
||||
}
|
||||
return err;
|
||||
};
|
||||
const thrd = try std.Thread.spawn(.{}, handleConnection, .{ self, conn });
|
||||
thrd.detach();
|
||||
}
|
||||
}
|
||||
|
||||
fn handleConnection(self: *TestHTTPServer, conn: std.net.Server.Connection) !void {
|
||||
defer conn.stream.close();
|
||||
|
||||
var req_buf: [2048]u8 = undefined;
|
||||
var conn_reader = conn.stream.reader(&req_buf);
|
||||
var conn_writer = conn.stream.writer(&req_buf);
|
||||
|
||||
var http_server = std.http.Server.init(conn_reader.interface(), &conn_writer.interface);
|
||||
|
||||
while (true) {
|
||||
var req = http_server.receiveHead() catch |err| switch (err) {
|
||||
error.ReadFailed => continue,
|
||||
error.HttpConnectionClosing => continue,
|
||||
else => {
|
||||
std.debug.print("Test HTTP Server error: {}\n", .{err});
|
||||
return err;
|
||||
},
|
||||
};
|
||||
self.handler(&req) catch |err| {
|
||||
std.debug.print("test http error '{s}': {}\n", .{ req.head.target, err });
|
||||
try req.respond("server error", .{ .status = .internal_server_error });
|
||||
return;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sendFile(req: *std.http.Server.Request, file_path: []const u8) !void {
|
||||
var file = std.fs.cwd().openFile(file_path, .{}) catch |err| switch (err) {
|
||||
error.FileNotFound => return req.respond("server error", .{ .status = .not_found }),
|
||||
else => return err,
|
||||
};
|
||||
|
||||
const stat = try file.stat();
|
||||
var send_buffer: [4096]u8 = undefined;
|
||||
|
||||
var res = try req.respondStreaming(&send_buffer, .{
|
||||
.content_length = stat.size,
|
||||
.respond_options = .{
|
||||
.extra_headers = &.{
|
||||
.{ .name = "content-type", .value = getContentType(file_path) },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
var read_buffer: [4096]u8 = undefined;
|
||||
var reader = file.reader(&read_buffer);
|
||||
_ = try res.writer.sendFileAll(&reader, .unlimited);
|
||||
try res.writer.flush();
|
||||
try res.end();
|
||||
}
|
||||
|
||||
fn getContentType(file_path: []const u8) []const u8 {
|
||||
if (std.mem.endsWith(u8, file_path, ".js")) {
|
||||
return "application/json";
|
||||
}
|
||||
|
||||
if (std.mem.endsWith(u8, file_path, ".html")) {
|
||||
return "text/html";
|
||||
}
|
||||
|
||||
if (std.mem.endsWith(u8, file_path, ".htm")) {
|
||||
return "text/html";
|
||||
}
|
||||
|
||||
if (std.mem.endsWith(u8, file_path, ".xml")) {
|
||||
// some wpt tests do this
|
||||
return "text/xml";
|
||||
}
|
||||
|
||||
std.debug.print("TestHTTPServer asked to serve an unknown file type: {s}\n", .{file_path});
|
||||
return "text/html";
|
||||
}
|
||||
62
src/app.zig
62
src/app.zig
@@ -1,22 +1,21 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const log = @import("log.zig");
|
||||
const Http = @import("http/Http.zig");
|
||||
const Platform = @import("runtime/js.zig").Platform;
|
||||
|
||||
const Loop = @import("runtime/loop.zig").Loop;
|
||||
const HttpClient = @import("http/client.zig").Client;
|
||||
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
|
||||
const Notification = @import("notification.zig").Notification;
|
||||
|
||||
const log = std.log.scoped(.app);
|
||||
|
||||
// Container for global state / objects that various parts of the system
|
||||
// might need.
|
||||
pub const App = struct {
|
||||
http: Http,
|
||||
loop: *Loop,
|
||||
config: Config,
|
||||
platform: Platform,
|
||||
allocator: Allocator,
|
||||
telemetry: Telemetry,
|
||||
http_client: HttpClient,
|
||||
app_dir_path: ?[]const u8,
|
||||
notification: *Notification,
|
||||
|
||||
@@ -29,51 +28,39 @@ pub const App = struct {
|
||||
|
||||
pub const Config = struct {
|
||||
run_mode: RunMode,
|
||||
gc_hints: bool = false,
|
||||
tls_verify_host: bool = true,
|
||||
http_proxy: ?[:0]const u8 = null,
|
||||
proxy_bearer_token: ?[:0]const u8 = null,
|
||||
http_timeout_ms: ?u31 = null,
|
||||
http_connect_timeout_ms: ?u31 = null,
|
||||
http_max_host_open: ?u8 = null,
|
||||
http_max_concurrent: ?u8 = null,
|
||||
http_proxy: ?std.Uri = null,
|
||||
};
|
||||
|
||||
pub fn init(allocator: Allocator, config: Config) !*App {
|
||||
const app = try allocator.create(App);
|
||||
errdefer allocator.destroy(app);
|
||||
|
||||
const loop = try allocator.create(Loop);
|
||||
errdefer allocator.destroy(loop);
|
||||
|
||||
loop.* = try Loop.init(allocator);
|
||||
errdefer loop.deinit();
|
||||
|
||||
const notification = try Notification.init(allocator, null);
|
||||
errdefer notification.deinit();
|
||||
|
||||
var http = try Http.init(allocator, .{
|
||||
.max_host_open = config.http_max_host_open orelse 4,
|
||||
.max_concurrent = config.http_max_concurrent orelse 10,
|
||||
.timeout_ms = config.http_timeout_ms orelse 5000,
|
||||
.connect_timeout_ms = config.http_connect_timeout_ms orelse 0,
|
||||
.http_proxy = config.http_proxy,
|
||||
.tls_verify_host = config.tls_verify_host,
|
||||
.proxy_bearer_token = config.proxy_bearer_token,
|
||||
});
|
||||
errdefer http.deinit();
|
||||
|
||||
const platform = try Platform.init();
|
||||
errdefer platform.deinit();
|
||||
|
||||
const app_dir_path = getAndMakeAppDir(allocator);
|
||||
|
||||
app.* = .{
|
||||
.http = http,
|
||||
.loop = loop,
|
||||
.allocator = allocator,
|
||||
.telemetry = undefined,
|
||||
.platform = platform,
|
||||
.app_dir_path = app_dir_path,
|
||||
.notification = notification,
|
||||
.http_client = try HttpClient.init(allocator, 5, .{
|
||||
.http_proxy = config.http_proxy,
|
||||
.tls_verify_host = config.tls_verify_host,
|
||||
}),
|
||||
.config = config,
|
||||
};
|
||||
|
||||
app.telemetry = try Telemetry.init(app, config.run_mode);
|
||||
errdefer app.telemetry.deinit();
|
||||
|
||||
app.telemetry = Telemetry.init(app, config.run_mode);
|
||||
try app.telemetry.register(app.notification);
|
||||
|
||||
return app;
|
||||
@@ -85,9 +72,10 @@ pub const App = struct {
|
||||
allocator.free(app_dir_path);
|
||||
}
|
||||
self.telemetry.deinit();
|
||||
self.loop.deinit();
|
||||
allocator.destroy(self.loop);
|
||||
self.http_client.deinit();
|
||||
self.notification.deinit();
|
||||
self.http.deinit();
|
||||
self.platform.deinit();
|
||||
allocator.destroy(self);
|
||||
}
|
||||
};
|
||||
@@ -97,7 +85,7 @@ fn getAndMakeAppDir(allocator: Allocator) ?[]const u8 {
|
||||
return allocator.dupe(u8, "/tmp") catch unreachable;
|
||||
}
|
||||
const app_dir_path = std.fs.getAppDataDir(allocator, "lightpanda") catch |err| {
|
||||
log.warn(.app, "get data dir", .{ .err = err });
|
||||
log.warn("failed to get lightpanda data dir: {}", .{err});
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -105,7 +93,7 @@ fn getAndMakeAppDir(allocator: Allocator) ?[]const u8 {
|
||||
error.PathAlreadyExists => return app_dir_path,
|
||||
else => {
|
||||
allocator.free(app_dir_path);
|
||||
log.warn(.app, "create data dir", .{ .err = err, .path = app_dir_path });
|
||||
log.warn("failed to create lightpanda data dir: {}", .{err});
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
// Parses data:[<media-type>][;base64],<data>
|
||||
pub fn parse(allocator: Allocator, src: []const u8) !?[]const u8 {
|
||||
if (!std.mem.startsWith(u8, src, "data:")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uri = src[5..];
|
||||
const data_starts = std.mem.indexOfScalar(u8, uri, ',') orelse return null;
|
||||
|
||||
var data = uri[data_starts + 1 ..];
|
||||
|
||||
// Extract the encoding.
|
||||
const metadata = uri[0..data_starts];
|
||||
if (std.mem.endsWith(u8, metadata, ";base64")) {
|
||||
const decoder = std.base64.standard.Decoder;
|
||||
const decoded_size = try decoder.calcSizeForSlice(data);
|
||||
|
||||
const buffer = try allocator.alloc(u8, decoded_size);
|
||||
errdefer allocator.free(buffer);
|
||||
|
||||
try decoder.decode(buffer, data);
|
||||
data = buffer;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
test "DataURI: parse valid" {
|
||||
try test_valid("data:text/javascript; charset=utf-8;base64,Zm9v", "foo");
|
||||
try test_valid("data:text/javascript; charset=utf-8;,foo", "foo");
|
||||
try test_valid("data:,foo", "foo");
|
||||
}
|
||||
|
||||
test "DataURI: parse invalid" {
|
||||
try test_cannot_parse("atad:,foo");
|
||||
try test_cannot_parse("data:foo");
|
||||
try test_cannot_parse("data:");
|
||||
}
|
||||
|
||||
fn test_valid(uri: []const u8, expected: []const u8) !void {
|
||||
defer testing.reset();
|
||||
const data_uri = try parse(testing.arena_allocator, uri) orelse return error.TestFailed;
|
||||
try testing.expectEqual(expected, data_uri);
|
||||
}
|
||||
|
||||
fn test_cannot_parse(uri: []const u8) !void {
|
||||
try testing.expectEqual(null, parse(undefined, uri));
|
||||
}
|
||||
@@ -1,174 +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 log = @import("../log.zig");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Scheduler = @This();
|
||||
|
||||
high_priority: Queue,
|
||||
|
||||
// For repeating tasks. We only want to run these if there are other things to
|
||||
// do. We don't, for example, want a window.setInterval or the page.runMicrotasks
|
||||
// to block the page.wait.
|
||||
low_priority: Queue,
|
||||
|
||||
// we expect allocator to be the page arena, hence we never call high_priority.deinit
|
||||
pub fn init(allocator: Allocator) Scheduler {
|
||||
return .{
|
||||
.high_priority = Queue.init(allocator, {}),
|
||||
.low_priority = Queue.init(allocator, {}),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn reset(self: *Scheduler) void {
|
||||
self.high_priority.clearRetainingCapacity();
|
||||
self.low_priority.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
const AddOpts = struct {
|
||||
name: []const u8 = "",
|
||||
low_priority: bool = false,
|
||||
};
|
||||
pub fn add(self: *Scheduler, ctx: *anyopaque, func: Task.Func, ms: u32, opts: AddOpts) !void {
|
||||
var low_priority = opts.low_priority;
|
||||
if (ms > 5_000) {
|
||||
// we don't want tasks in the far future to block page.wait from
|
||||
// completing. However, if page.wait is called multiple times (maybe
|
||||
// a CDP driver is wait for something to happen), then we do want
|
||||
// to [eventually] run these when their time is up.
|
||||
low_priority = true;
|
||||
}
|
||||
|
||||
var q = if (low_priority) &self.low_priority else &self.high_priority;
|
||||
return q.add(.{
|
||||
.ms = std.time.milliTimestamp() + ms,
|
||||
.ctx = ctx,
|
||||
.func = func,
|
||||
.name = opts.name,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn runHighPriority(self: *Scheduler) !?i32 {
|
||||
return self.runQueue(&self.high_priority);
|
||||
}
|
||||
|
||||
pub fn runLowPriority(self: *Scheduler) !?i32 {
|
||||
return self.runQueue(&self.low_priority);
|
||||
}
|
||||
|
||||
fn runQueue(self: *Scheduler, queue: *Queue) !?i32 {
|
||||
// this is O(1)
|
||||
if (queue.count() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = std.time.milliTimestamp();
|
||||
|
||||
var next = queue.peek();
|
||||
while (next) |task| {
|
||||
const time_to_next = task.ms - now;
|
||||
if (time_to_next > 0) {
|
||||
// @intCast is petty safe since we limit tasks to just 5 seconds
|
||||
// in the future
|
||||
return @intCast(time_to_next);
|
||||
}
|
||||
|
||||
if (task.func(task.ctx)) |repeat_delay| {
|
||||
// if we do (now + 0) then our WHILE loop will run endlessly.
|
||||
// no task should ever return 0
|
||||
std.debug.assert(repeat_delay != 0);
|
||||
|
||||
var copy = task;
|
||||
copy.ms = now + repeat_delay;
|
||||
try self.low_priority.add(copy);
|
||||
}
|
||||
_ = queue.remove();
|
||||
next = queue.peek();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const Task = struct {
|
||||
ms: i64,
|
||||
func: Func,
|
||||
ctx: *anyopaque,
|
||||
name: []const u8,
|
||||
|
||||
const Func = *const fn (ctx: *anyopaque) ?u32;
|
||||
};
|
||||
|
||||
const Queue = std.PriorityQueue(Task, void, struct {
|
||||
fn compare(_: void, a: Task, b: Task) std.math.Order {
|
||||
return std.math.order(a.ms, b.ms);
|
||||
}
|
||||
}.compare);
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
test "Scheduler" {
|
||||
defer testing.reset();
|
||||
|
||||
var task = TestTask{ .allocator = testing.arena_allocator };
|
||||
|
||||
var s = Scheduler.init(testing.arena_allocator);
|
||||
try testing.expectEqual(null, s.runHighPriority());
|
||||
try testing.expectEqual(0, task.calls.items.len);
|
||||
|
||||
try s.add(&task, TestTask.run1, 3, .{});
|
||||
|
||||
try testing.expectDelta(3, try s.runHighPriority(), 1);
|
||||
try testing.expectEqual(0, task.calls.items.len);
|
||||
|
||||
std.Thread.sleep(std.time.ns_per_ms * 5);
|
||||
try testing.expectEqual(null, s.runHighPriority());
|
||||
try testing.expectEqualSlices(u32, &.{1}, task.calls.items);
|
||||
|
||||
try s.add(&task, TestTask.run2, 3, .{});
|
||||
try s.add(&task, TestTask.run1, 2, .{});
|
||||
|
||||
std.Thread.sleep(std.time.ns_per_ms * 5);
|
||||
try testing.expectDelta(null, try s.runHighPriority(), 1);
|
||||
try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items);
|
||||
|
||||
std.Thread.sleep(std.time.ns_per_ms * 5);
|
||||
// won't run low_priority
|
||||
try testing.expectEqual(null, try s.runHighPriority());
|
||||
try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items);
|
||||
|
||||
//runs low_priority
|
||||
try testing.expectDelta(2, try s.runLowPriority(), 1);
|
||||
try testing.expectEqualSlices(u32, &.{ 1, 1, 2, 2 }, task.calls.items);
|
||||
}
|
||||
|
||||
const TestTask = struct {
|
||||
allocator: Allocator,
|
||||
calls: std.ArrayListUnmanaged(u32) = .{},
|
||||
|
||||
fn run1(ctx: *anyopaque) ?u32 {
|
||||
var self: *TestTask = @ptrCast(@alignCast(ctx));
|
||||
self.calls.append(self.allocator, 1) catch unreachable;
|
||||
return null;
|
||||
}
|
||||
|
||||
fn run2(ctx: *anyopaque) ?u32 {
|
||||
var self: *TestTask = @ptrCast(@alignCast(ctx));
|
||||
self.calls.append(self.allocator, 2) catch unreachable;
|
||||
return 2;
|
||||
}
|
||||
};
|
||||
@@ -1,835 +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 log = @import("../log.zig");
|
||||
const parser = @import("netsurf.zig");
|
||||
|
||||
const Env = @import("env.zig").Env;
|
||||
const Page = @import("page.zig").Page;
|
||||
const DataURI = @import("DataURI.zig");
|
||||
const Http = @import("../http/Http.zig");
|
||||
const Browser = @import("browser.zig").Browser;
|
||||
const URL = @import("../url.zig").URL;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArrayListUnmanaged = std.ArrayListUnmanaged;
|
||||
|
||||
const ScriptManager = @This();
|
||||
|
||||
page: *Page,
|
||||
|
||||
// used to prevent recursive evalutaion
|
||||
is_evaluating: bool,
|
||||
|
||||
// used to prevent executing scripts while we're doing a blocking load
|
||||
is_blocking: bool = false,
|
||||
|
||||
// Only once this is true can deferred scripts be run
|
||||
static_scripts_done: bool,
|
||||
|
||||
// List of async scripts. We don't care about the execution order of these, but
|
||||
// on shutdown/abort, we need to cleanup any pending ones.
|
||||
asyncs: OrderList,
|
||||
|
||||
// When an async script is ready to be evaluated, it's moved from asyncs to
|
||||
// this list. You might think we can evaluate an async script as soon as it's
|
||||
// done, but we can only evaluate scripts when `is_blocking == false`. So this
|
||||
// becomes a list of scripts to execute on the next evaluate().
|
||||
asyncs_ready: OrderList,
|
||||
|
||||
// Normal scripts (non-deferred & non-async). These must be executed in order
|
||||
scripts: OrderList,
|
||||
|
||||
// List of deferred scripts. These must be executed in order, but only once
|
||||
// dom_loaded == true,
|
||||
deferreds: OrderList,
|
||||
|
||||
shutdown: bool = false,
|
||||
|
||||
client: *Http.Client,
|
||||
allocator: Allocator,
|
||||
buffer_pool: BufferPool,
|
||||
script_pool: std.heap.MemoryPool(PendingScript),
|
||||
|
||||
const OrderList = std.DoublyLinkedList;
|
||||
|
||||
pub fn init(browser: *Browser, page: *Page) ScriptManager {
|
||||
// page isn't fully initialized, we can setup our reference, but that's it.
|
||||
const allocator = browser.allocator;
|
||||
return .{
|
||||
.page = page,
|
||||
.asyncs = .{},
|
||||
.scripts = .{},
|
||||
.deferreds = .{},
|
||||
.asyncs_ready = .{},
|
||||
.is_evaluating = false,
|
||||
.allocator = allocator,
|
||||
.client = browser.http_client,
|
||||
.static_scripts_done = false,
|
||||
.buffer_pool = BufferPool.init(allocator, 5),
|
||||
.script_pool = std.heap.MemoryPool(PendingScript).init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *ScriptManager) void {
|
||||
self.reset();
|
||||
self.buffer_pool.deinit();
|
||||
self.script_pool.deinit();
|
||||
}
|
||||
|
||||
pub fn reset(self: *ScriptManager) void {
|
||||
self.clearList(&self.asyncs);
|
||||
self.clearList(&self.scripts);
|
||||
self.clearList(&self.deferreds);
|
||||
self.clearList(&self.asyncs_ready);
|
||||
self.static_scripts_done = false;
|
||||
}
|
||||
|
||||
fn clearList(_: *const ScriptManager, list: *OrderList) void {
|
||||
while (list.first) |node| {
|
||||
const pending_script: *PendingScript = @fieldParentPtr("node", node);
|
||||
// this removes it from the list
|
||||
pending_script.deinit();
|
||||
}
|
||||
std.debug.assert(list.first == null);
|
||||
}
|
||||
|
||||
pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
|
||||
if (try parser.elementGetAttribute(element, "nomodule") != null) {
|
||||
// these scripts should only be loaded if we don't support modules
|
||||
// but since we do support modules, we can just skip them.
|
||||
return;
|
||||
}
|
||||
|
||||
// If a script tag gets dynamically created and added to the dom:
|
||||
// document.getElementsByTagName('head')[0].appendChild(script)
|
||||
// that script tag will immediately get executed by our scriptAddedCallback.
|
||||
// However, if the location where the script tag is inserted happens to be
|
||||
// below where processHTMLDoc currently is, then we'll re-run that same script
|
||||
// again in processHTMLDoc. This flag is used to let us know if a specific
|
||||
// <script> has already been processed.
|
||||
if (try parser.scriptGetProcessed(@ptrCast(element))) {
|
||||
return;
|
||||
}
|
||||
try parser.scriptSetProcessed(@ptrCast(element), true);
|
||||
|
||||
const kind: Script.Kind = blk: {
|
||||
const script_type = try parser.elementGetAttribute(element, "type") orelse break :blk .javascript;
|
||||
if (script_type.len == 0) {
|
||||
break :blk .javascript;
|
||||
}
|
||||
if (std.ascii.eqlIgnoreCase(script_type, "application/javascript")) {
|
||||
break :blk .javascript;
|
||||
}
|
||||
if (std.ascii.eqlIgnoreCase(script_type, "text/javascript")) {
|
||||
break :blk .javascript;
|
||||
}
|
||||
if (std.ascii.eqlIgnoreCase(script_type, "module")) {
|
||||
break :blk .module;
|
||||
}
|
||||
|
||||
// "type" could be anything, but only the above are ones we need to process.
|
||||
// Common other ones are application/json, application/ld+json, text/template
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
const page = self.page;
|
||||
var source: Script.Source = undefined;
|
||||
var remote_url: ?[:0]const u8 = null;
|
||||
if (try parser.elementGetAttribute(element, "src")) |src| {
|
||||
if (try DataURI.parse(page.arena, src)) |data_uri| {
|
||||
source = .{ .@"inline" = data_uri };
|
||||
}
|
||||
remote_url = try URL.stitch(page.arena, src, page.url.raw, .{ .null_terminated = true });
|
||||
source = .{ .remote = .{} };
|
||||
} else {
|
||||
const inline_source = try parser.nodeTextContent(@ptrCast(element)) orelse return;
|
||||
source = .{ .@"inline" = inline_source };
|
||||
}
|
||||
|
||||
var script = Script{
|
||||
.kind = kind,
|
||||
.element = element,
|
||||
.source = source,
|
||||
.url = remote_url orelse page.url.raw,
|
||||
.is_defer = if (remote_url == null) false else try parser.elementGetAttribute(element, "defer") != null,
|
||||
.is_async = if (remote_url == null) false else try parser.elementGetAttribute(element, "async") != null,
|
||||
};
|
||||
|
||||
if (source == .@"inline" and self.scripts.first == null) {
|
||||
// inline script with no pending scripts, execute it immediately.
|
||||
// (if there is a pending script, then we cannot execute this immediately
|
||||
// as it needs to best executed in order)
|
||||
return script.eval(page);
|
||||
}
|
||||
|
||||
const pending_script = try self.script_pool.create();
|
||||
errdefer self.script_pool.destroy(pending_script);
|
||||
pending_script.* = .{
|
||||
.script = script,
|
||||
.complete = false,
|
||||
.manager = self,
|
||||
.node = .{},
|
||||
};
|
||||
|
||||
if (source == .@"inline") {
|
||||
// if we're here, it means that we have pending scripts (i.e. self.scripts
|
||||
// is not empty). Because the script is inline, it's complete/ready, but
|
||||
// we need to process them in order
|
||||
pending_script.complete = true;
|
||||
self.scripts.append(&pending_script.node);
|
||||
return;
|
||||
} else {
|
||||
log.debug(.http, "script queue", .{ .url = remote_url.? });
|
||||
}
|
||||
|
||||
pending_script.getList().append(&pending_script.node);
|
||||
|
||||
errdefer pending_script.deinit();
|
||||
|
||||
var headers = try Http.Headers.init();
|
||||
try page.requestCookie(.{}).headersForRequest(page.arena, remote_url.?, &headers);
|
||||
|
||||
try self.client.request(.{
|
||||
.url = remote_url.?,
|
||||
.ctx = pending_script,
|
||||
.method = .GET,
|
||||
.headers = headers,
|
||||
.cookie_jar = page.cookie_jar,
|
||||
.resource_type = .script,
|
||||
.start_callback = if (log.enabled(.http, .debug)) startCallback else null,
|
||||
.header_callback = headerCallback,
|
||||
.data_callback = dataCallback,
|
||||
.done_callback = doneCallback,
|
||||
.error_callback = errorCallback,
|
||||
});
|
||||
}
|
||||
|
||||
// @TODO: Improving this would have the simplest biggest performance improvement
|
||||
// for most sites.
|
||||
//
|
||||
// For JS imports (both static and dynamic), we currently block to get the
|
||||
// result (the content of the file).
|
||||
//
|
||||
// For static imports, this is necessary, since v8 is expecting the compiled module
|
||||
// as part of the function return. (we should try to pre-load the JavaScript
|
||||
// source via module.GetModuleRequests(), but that's for a later time).
|
||||
//
|
||||
// For dynamic dynamic imports, this is not strictly necessary since the v8
|
||||
// call returns a Promise; we could make this a normal get call, associated with
|
||||
// the promise, and when done, resolve the promise.
|
||||
//
|
||||
// In both cases, for now at least, we just issue a "blocking" request. We block
|
||||
// by ticking the http client until the script is complete.
|
||||
//
|
||||
// This uses the client.blockingRequest call which has a dedicated handle for
|
||||
// these blocking requests. Because they are blocking, we're guaranteed to have
|
||||
// only 1 at a time, thus the 1 reserved handle.
|
||||
//
|
||||
// You almost don't need the http client's blocking handle. In most cases, you
|
||||
// should always have 1 free handle whenever you get here, because we always
|
||||
// release the handle before executing the doneCallback. So, if a module does:
|
||||
// import * as x from 'blah'
|
||||
// And we need to load 'blah', there should always be 1 free handle - the handle
|
||||
// of the http GET we just completed before executing the module.
|
||||
// The exception to this, and the reason we need a special blocking handle, is
|
||||
// for inline modules within the HTML page itself:
|
||||
// <script type=module>import ....</script>
|
||||
// Unlike external modules which can only ever be executed after releasing an
|
||||
// http handle, these are executed without there necessarily being a free handle.
|
||||
// Thus, Http/Client.zig maintains a dedicated handle for these calls.
|
||||
pub fn blockingGet(self: *ScriptManager, url: [:0]const u8) !BlockingResult {
|
||||
std.debug.assert(self.is_blocking == false);
|
||||
|
||||
self.is_blocking = true;
|
||||
defer {
|
||||
self.is_blocking = false;
|
||||
|
||||
// we blocked evaluation while loading this script, there could be
|
||||
// scripts ready to process.
|
||||
self.evaluate();
|
||||
}
|
||||
|
||||
var blocking = Blocking{
|
||||
.allocator = self.allocator,
|
||||
.buffer_pool = &self.buffer_pool,
|
||||
};
|
||||
|
||||
var headers = try Http.Headers.init();
|
||||
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
|
||||
|
||||
var client = self.client;
|
||||
try client.blockingRequest(.{
|
||||
.url = url,
|
||||
.method = .GET,
|
||||
.headers = headers,
|
||||
.cookie_jar = self.page.cookie_jar,
|
||||
.ctx = &blocking,
|
||||
.resource_type = .script,
|
||||
.start_callback = if (log.enabled(.http, .debug)) Blocking.startCallback else null,
|
||||
.header_callback = Blocking.headerCallback,
|
||||
.data_callback = Blocking.dataCallback,
|
||||
.done_callback = Blocking.doneCallback,
|
||||
.error_callback = Blocking.errorCallback,
|
||||
});
|
||||
|
||||
// rely on http's timeout settings to avoid an endless/long loop.
|
||||
while (true) {
|
||||
_ = try client.tick(200);
|
||||
switch (blocking.state) {
|
||||
.running => {},
|
||||
.done => |result| return result,
|
||||
.err => |err| return err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn staticScriptsDone(self: *ScriptManager) void {
|
||||
std.debug.assert(self.static_scripts_done == false);
|
||||
self.static_scripts_done = true;
|
||||
}
|
||||
|
||||
// try to evaluate completed scripts (in order). This is called whenever a script
|
||||
// is completed.
|
||||
fn evaluate(self: *ScriptManager) void {
|
||||
if (self.is_evaluating) {
|
||||
// It's possible for a script.eval to cause evaluate to be called again.
|
||||
// This is particularly true with blockingGet, but even without this,
|
||||
// it's theoretically possible (but unlikely). We could make this work
|
||||
// but there's little reason to support the complexity.
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.is_blocking) {
|
||||
// Cannot evaluate scripts while a blocking-load is in progress. Not
|
||||
// only could that result in incorrect evaluation order, it could
|
||||
// trigger another blocking request, while we're doing a blocking request.
|
||||
return;
|
||||
}
|
||||
|
||||
const page = self.page;
|
||||
self.is_evaluating = true;
|
||||
defer self.is_evaluating = false;
|
||||
|
||||
// every script in asyncs_ready is ready to be evaluated.
|
||||
while (self.asyncs_ready.first) |n| {
|
||||
var pending_script: *PendingScript = @fieldParentPtr("node", n);
|
||||
defer pending_script.deinit();
|
||||
pending_script.script.eval(page);
|
||||
}
|
||||
|
||||
while (self.scripts.first) |n| {
|
||||
var pending_script: *PendingScript = @fieldParentPtr("node", n);
|
||||
if (pending_script.complete == false) {
|
||||
return;
|
||||
}
|
||||
defer pending_script.deinit();
|
||||
pending_script.script.eval(page);
|
||||
}
|
||||
|
||||
if (self.static_scripts_done == false) {
|
||||
// We can only execute deferred scripts if
|
||||
// 1 - all the normal scripts are done
|
||||
// 2 - we've finished parsing the HTML and at least queued all the scripts
|
||||
// The last one isn't obvious, but it's possible for self.scripts to
|
||||
// be empty not because we're done executing all the normal scripts
|
||||
// but because we're done executing some (or maybe none), but we're still
|
||||
// parsing the HTML.
|
||||
return;
|
||||
}
|
||||
|
||||
while (self.deferreds.first) |n| {
|
||||
var pending_script: *PendingScript = @fieldParentPtr("node", n);
|
||||
if (pending_script.complete == false) {
|
||||
return;
|
||||
}
|
||||
defer pending_script.deinit();
|
||||
pending_script.script.eval(page);
|
||||
}
|
||||
|
||||
// When all scripts (normal and deferred) are done loading, the document
|
||||
// state changes (this ultimately triggers the DOMContentLoaded event)
|
||||
page.documentIsLoaded();
|
||||
|
||||
if (self.asyncs.first == null) {
|
||||
// 1 - there are no async scripts pending
|
||||
// 2 - we checkecked static_scripts_done == true above
|
||||
// 3 - we drained self.scripts above
|
||||
// 4 - we drained self.deferred above
|
||||
page.documentIsComplete();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn isDone(self: *const ScriptManager) bool {
|
||||
return self.asyncs.first == null and // there are no more async scripts
|
||||
self.static_scripts_done and // and we've finished parsing the HTML to queue all <scripts>
|
||||
self.scripts.first == null and // and there are no more <script src=> to wait for
|
||||
self.deferreds.first == null; // and there are no more <script defer src=> to wait for
|
||||
}
|
||||
|
||||
fn startCallback(transfer: *Http.Transfer) !void {
|
||||
const script: *PendingScript = @ptrCast(@alignCast(transfer.ctx));
|
||||
script.startCallback(transfer) catch |err| {
|
||||
log.err(.http, "SM.startCallback", .{ .err = err, .transfer = transfer });
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
fn headerCallback(transfer: *Http.Transfer) !void {
|
||||
const script: *PendingScript = @ptrCast(@alignCast(transfer.ctx));
|
||||
try script.headerCallback(transfer);
|
||||
}
|
||||
|
||||
fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void {
|
||||
const script: *PendingScript = @ptrCast(@alignCast(transfer.ctx));
|
||||
script.dataCallback(transfer, data) catch |err| {
|
||||
log.err(.http, "SM.dataCallback", .{ .err = err, .transfer = transfer, .len = data.len });
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
fn doneCallback(ctx: *anyopaque) !void {
|
||||
const script: *PendingScript = @ptrCast(@alignCast(ctx));
|
||||
script.doneCallback();
|
||||
}
|
||||
|
||||
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||
const script: *PendingScript = @ptrCast(@alignCast(ctx));
|
||||
script.errorCallback(err);
|
||||
}
|
||||
|
||||
// A script which is pending execution.
|
||||
// It could be pending because:
|
||||
// (a) we're still downloading its content or
|
||||
// (b) this is a non-async script that has to be executed in order
|
||||
pub const PendingScript = struct {
|
||||
script: Script,
|
||||
complete: bool,
|
||||
node: OrderList.Node,
|
||||
manager: *ScriptManager,
|
||||
|
||||
fn deinit(self: *PendingScript) void {
|
||||
const script = &self.script;
|
||||
const manager = self.manager;
|
||||
|
||||
if (script.source == .remote) {
|
||||
manager.buffer_pool.release(script.source.remote);
|
||||
}
|
||||
self.getList().remove(&self.node);
|
||||
}
|
||||
|
||||
fn remove(self: *PendingScript) void {
|
||||
if (self.node) |*node| {
|
||||
self.getList().remove(node);
|
||||
self.node = null;
|
||||
}
|
||||
}
|
||||
|
||||
fn startCallback(self: *PendingScript, transfer: *Http.Transfer) !void {
|
||||
_ = self;
|
||||
log.debug(.http, "script fetch start", .{ .req = transfer });
|
||||
}
|
||||
|
||||
fn headerCallback(self: *PendingScript, transfer: *Http.Transfer) !void {
|
||||
const header = &transfer.response_header.?;
|
||||
if (header.status != 200) {
|
||||
log.info(.http, "script header", .{
|
||||
.req = transfer,
|
||||
.status = header.status,
|
||||
.content_type = header.contentType(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug(.http, "script header", .{
|
||||
.req = transfer,
|
||||
.status = header.status,
|
||||
.content_type = header.contentType(),
|
||||
});
|
||||
|
||||
// If this isn't true, then we'll likely leak memory. If you don't
|
||||
// set `CURLOPT_SUPPRESS_CONNECT_HEADERS` and CONNECT to a proxy, this
|
||||
// will fail. This assertion exists to catch incorrect assumptions about
|
||||
// how libcurl works, or about how we've configured it.
|
||||
std.debug.assert(self.script.source.remote.capacity == 0);
|
||||
var buffer = self.manager.buffer_pool.get();
|
||||
if (transfer.getContentLength()) |cl| {
|
||||
try buffer.ensureTotalCapacity(self.manager.allocator, cl);
|
||||
}
|
||||
self.script.source = .{ .remote = buffer };
|
||||
}
|
||||
|
||||
fn dataCallback(self: *PendingScript, transfer: *Http.Transfer, data: []const u8) !void {
|
||||
_ = transfer;
|
||||
// too verbose
|
||||
// log.debug(.http, "script data chunk", .{
|
||||
// .req = transfer,
|
||||
// .len = data.len,
|
||||
// });
|
||||
|
||||
try self.script.source.remote.appendSlice(self.manager.allocator, data);
|
||||
}
|
||||
|
||||
fn doneCallback(self: *PendingScript) void {
|
||||
log.debug(.http, "script fetch complete", .{ .req = self.script.url });
|
||||
|
||||
const manager = self.manager;
|
||||
self.complete = true;
|
||||
if (self.script.is_async) {
|
||||
manager.asyncs.remove(&self.node);
|
||||
manager.asyncs_ready.append(&self.node);
|
||||
}
|
||||
manager.evaluate();
|
||||
}
|
||||
|
||||
fn errorCallback(self: *PendingScript, err: anyerror) void {
|
||||
log.warn(.http, "script fetch error", .{ .req = self.script.url, .err = err });
|
||||
|
||||
const manager = self.manager;
|
||||
|
||||
self.deinit();
|
||||
|
||||
if (manager.shutdown) {
|
||||
return;
|
||||
}
|
||||
|
||||
manager.evaluate();
|
||||
}
|
||||
|
||||
fn getList(self: *const PendingScript) *OrderList {
|
||||
// When a script has both the async and defer flag set, it should be
|
||||
// treated as async. Async is newer, so some websites use both so that
|
||||
// if async isn't known, it'll fallback to defer.
|
||||
|
||||
const script = &self.script;
|
||||
if (script.is_async) {
|
||||
return if (self.complete) &self.manager.asyncs_ready else &self.manager.asyncs;
|
||||
}
|
||||
|
||||
if (script.is_defer) {
|
||||
return &self.manager.deferreds;
|
||||
}
|
||||
|
||||
return &self.manager.scripts;
|
||||
}
|
||||
};
|
||||
|
||||
const Script = struct {
|
||||
kind: Kind,
|
||||
url: []const u8,
|
||||
is_async: bool,
|
||||
is_defer: bool,
|
||||
source: Source,
|
||||
element: *parser.Element,
|
||||
|
||||
const Kind = enum {
|
||||
module,
|
||||
javascript,
|
||||
};
|
||||
|
||||
const Callback = union(enum) {
|
||||
string: []const u8,
|
||||
function: Env.Function,
|
||||
};
|
||||
|
||||
const Source = union(enum) {
|
||||
@"inline": []const u8,
|
||||
remote: std.ArrayListUnmanaged(u8),
|
||||
|
||||
fn content(self: Source) []const u8 {
|
||||
return switch (self) {
|
||||
.remote => |buf| buf.items,
|
||||
.@"inline" => |c| c,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
fn eval(self: *Script, page: *Page) void {
|
||||
page.setCurrentScript(@ptrCast(self.element)) catch |err| {
|
||||
log.err(.browser, "set document script", .{ .err = err });
|
||||
return;
|
||||
};
|
||||
|
||||
defer page.setCurrentScript(null) catch |err| {
|
||||
log.err(.browser, "clear document script", .{ .err = err });
|
||||
};
|
||||
|
||||
// inline scripts aren't cached. remote ones are.
|
||||
const cacheable = self.source == .remote;
|
||||
|
||||
const url = self.url;
|
||||
|
||||
log.info(.browser, "executing script", .{
|
||||
.src = url,
|
||||
.kind = self.kind,
|
||||
.cacheable = cacheable,
|
||||
});
|
||||
|
||||
const js_context = page.main_context;
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(js_context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const success = blk: {
|
||||
const content = self.source.content();
|
||||
switch (self.kind) {
|
||||
.javascript => _ = js_context.eval(content, url) catch break :blk false,
|
||||
.module => {
|
||||
// We don't care about waiting for the evaluation here.
|
||||
_ = js_context.module(content, url, cacheable) catch break :blk false;
|
||||
},
|
||||
}
|
||||
break :blk true;
|
||||
};
|
||||
|
||||
if (success) {
|
||||
self.executeCallback("onload", page);
|
||||
return;
|
||||
}
|
||||
|
||||
if (page.delayed_navigation) {
|
||||
// If we're navigating to another page, an error is expected
|
||||
// since we probably terminated the script forcefully.
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = try_catch.err(page.arena) catch |err| @errorName(err) orelse "unknown";
|
||||
log.warn(.user_script, "eval script", .{
|
||||
.url = url,
|
||||
.err = msg,
|
||||
.cacheable = cacheable,
|
||||
});
|
||||
|
||||
self.executeCallback("onerror", page);
|
||||
}
|
||||
|
||||
fn executeCallback(self: *const Script, comptime typ: []const u8, page: *Page) void {
|
||||
const callback = self.getCallback(typ, page) orelse return;
|
||||
|
||||
switch (callback) {
|
||||
.string => |str| {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(page.main_context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
_ = page.main_context.exec(str, typ) catch |err| {
|
||||
const msg = try_catch.err(page.arena) catch @errorName(err) orelse "unknown";
|
||||
log.warn(.user_script, "script callback", .{
|
||||
.url = self.url,
|
||||
.err = msg,
|
||||
.type = typ,
|
||||
.@"inline" = true,
|
||||
});
|
||||
};
|
||||
},
|
||||
.function => |f| {
|
||||
const Event = @import("events/event.zig").Event;
|
||||
const loadevt = parser.eventCreate() catch |err| {
|
||||
log.err(.browser, "SM event creation", .{ .err = err });
|
||||
return;
|
||||
};
|
||||
defer parser.eventDestroy(loadevt);
|
||||
|
||||
var result: Env.Function.Result = undefined;
|
||||
const iface = Event.toInterface(loadevt) catch |err| {
|
||||
log.err(.browser, "SM event interface", .{ .err = err });
|
||||
return;
|
||||
};
|
||||
f.tryCall(void, .{iface}, &result) catch {
|
||||
log.warn(.user_script, "script callback", .{
|
||||
.url = self.url,
|
||||
.type = typ,
|
||||
.err = result.exception,
|
||||
.stack = result.stack,
|
||||
.@"inline" = false,
|
||||
});
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn getCallback(self: *const Script, comptime typ: []const u8, page: *Page) ?Callback {
|
||||
const element = self.element;
|
||||
// first we check if there was an el.onload set directly on the
|
||||
// element in JavaScript (if so, it'd be stored in the node state)
|
||||
if (page.getNodeState(@ptrCast(element))) |se| {
|
||||
if (@field(se, typ)) |function| {
|
||||
return .{ .function = function };
|
||||
}
|
||||
}
|
||||
// if we have no node state, or if the node state has no onload/onerror
|
||||
// then check for the onload/onerror attribute
|
||||
if (parser.elementGetAttribute(element, typ) catch null) |string| {
|
||||
return .{ .string = string };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const BufferPool = struct {
|
||||
count: usize,
|
||||
available: List = .{},
|
||||
allocator: Allocator,
|
||||
max_concurrent_transfers: u8,
|
||||
mem_pool: std.heap.MemoryPool(Container),
|
||||
|
||||
const List = std.DoublyLinkedList;
|
||||
|
||||
const Container = struct {
|
||||
node: List.Node,
|
||||
buf: std.ArrayListUnmanaged(u8),
|
||||
};
|
||||
|
||||
fn init(allocator: Allocator, max_concurrent_transfers: u8) BufferPool {
|
||||
return .{
|
||||
.available = .{},
|
||||
.count = 0,
|
||||
.allocator = allocator,
|
||||
.max_concurrent_transfers = max_concurrent_transfers,
|
||||
.mem_pool = std.heap.MemoryPool(Container).init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
fn deinit(self: *BufferPool) void {
|
||||
const allocator = self.allocator;
|
||||
|
||||
var node = self.available.first;
|
||||
while (node) |n| {
|
||||
const container: *Container = @fieldParentPtr("node", n);
|
||||
container.buf.deinit(allocator);
|
||||
node = n.next;
|
||||
}
|
||||
self.mem_pool.deinit();
|
||||
}
|
||||
|
||||
fn get(self: *BufferPool) std.ArrayListUnmanaged(u8) {
|
||||
const node = self.available.popFirst() orelse {
|
||||
// return a new buffer
|
||||
return .{};
|
||||
};
|
||||
|
||||
self.count -= 1;
|
||||
const container: *Container = @fieldParentPtr("node", node);
|
||||
defer self.mem_pool.destroy(container);
|
||||
return container.buf;
|
||||
}
|
||||
|
||||
fn release(self: *BufferPool, buffer: ArrayListUnmanaged(u8)) void {
|
||||
// create mutable copy
|
||||
var b = buffer;
|
||||
|
||||
if (self.count == self.max_concurrent_transfers) {
|
||||
b.deinit(self.allocator);
|
||||
return;
|
||||
}
|
||||
|
||||
const container = self.mem_pool.create() catch |err| {
|
||||
b.deinit(self.allocator);
|
||||
log.err(.http, "SM BufferPool release", .{ .err = err });
|
||||
return;
|
||||
};
|
||||
|
||||
b.clearRetainingCapacity();
|
||||
container.* = .{ .buf = b, .node = .{} };
|
||||
self.count += 1;
|
||||
self.available.append(&container.node);
|
||||
}
|
||||
};
|
||||
|
||||
const Blocking = struct {
|
||||
allocator: Allocator,
|
||||
buffer_pool: *BufferPool,
|
||||
state: State = .{ .running = {} },
|
||||
buffer: std.ArrayListUnmanaged(u8) = .{},
|
||||
|
||||
const State = union(enum) {
|
||||
running: void,
|
||||
err: anyerror,
|
||||
done: BlockingResult,
|
||||
};
|
||||
|
||||
fn startCallback(transfer: *Http.Transfer) !void {
|
||||
log.debug(.http, "script fetch start", .{ .req = transfer, .blocking = true });
|
||||
}
|
||||
|
||||
fn headerCallback(transfer: *Http.Transfer) !void {
|
||||
const header = &transfer.response_header.?;
|
||||
log.debug(.http, "script header", .{
|
||||
.req = transfer,
|
||||
.blocking = true,
|
||||
.status = header.status,
|
||||
.content_type = header.contentType(),
|
||||
});
|
||||
|
||||
if (header.status != 200) {
|
||||
return error.InvalidStatusCode;
|
||||
}
|
||||
|
||||
var self: *Blocking = @ptrCast(@alignCast(transfer.ctx));
|
||||
self.buffer = self.buffer_pool.get();
|
||||
}
|
||||
|
||||
fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void {
|
||||
// too verbose
|
||||
// log.debug(.http, "script data chunk", .{
|
||||
// .req = transfer,
|
||||
// .blocking = true,
|
||||
// });
|
||||
|
||||
var self: *Blocking = @ptrCast(@alignCast(transfer.ctx));
|
||||
self.buffer.appendSlice(self.allocator, data) catch |err| {
|
||||
log.err(.http, "SM.dataCallback", .{
|
||||
.err = err,
|
||||
.len = data.len,
|
||||
.blocking = true,
|
||||
.transfer = transfer,
|
||||
});
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
fn doneCallback(ctx: *anyopaque) !void {
|
||||
var self: *Blocking = @ptrCast(@alignCast(ctx));
|
||||
self.state = .{ .done = .{
|
||||
.buffer = self.buffer,
|
||||
.buffer_pool = self.buffer_pool,
|
||||
} };
|
||||
}
|
||||
|
||||
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||
var self: *Blocking = @ptrCast(@alignCast(ctx));
|
||||
self.state = .{ .err = err };
|
||||
self.buffer_pool.release(self.buffer);
|
||||
}
|
||||
};
|
||||
|
||||
pub const BlockingResult = struct {
|
||||
buffer: std.ArrayListUnmanaged(u8),
|
||||
buffer_pool: *BufferPool,
|
||||
|
||||
pub fn deinit(self: *BlockingResult) void {
|
||||
self.buffer_pool.release(self.buffer);
|
||||
}
|
||||
|
||||
pub fn src(self: *const BlockingResult) []const u8 {
|
||||
return self.buffer.items;
|
||||
}
|
||||
};
|
||||
@@ -1,78 +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/>.
|
||||
|
||||
// Sometimes we need to extend libdom. For example, its HTMLDocument doesn't
|
||||
// have a readyState. We have a couple different options, such as making the
|
||||
// correction in libdom directly. Another option stems from the fact that every
|
||||
// libdom node has an opaque embedder_data field. This is the struct that we
|
||||
// lazily load into that field.
|
||||
//
|
||||
// It didn't originally start off as a collection of every single extension, but
|
||||
// this quickly proved necessary, since different fields are needed on the same
|
||||
// data at different levels of the prototype chain. This isn't memory efficient.
|
||||
|
||||
const Env = @import("env.zig").Env;
|
||||
const parser = @import("netsurf.zig");
|
||||
const DataSet = @import("html/DataSet.zig");
|
||||
const ShadowRoot = @import("dom/shadow_root.zig").ShadowRoot;
|
||||
const StyleSheet = @import("cssom/StyleSheet.zig");
|
||||
const CSSStyleSheet = @import("cssom/CSSStyleSheet.zig");
|
||||
const CSSStyleDeclaration = @import("cssom/CSSStyleDeclaration.zig");
|
||||
|
||||
// for HTMLScript (but probably needs to be added to more)
|
||||
onload: ?Env.Function = null,
|
||||
onerror: ?Env.Function = null,
|
||||
|
||||
// for HTMLElement
|
||||
style: CSSStyleDeclaration = .empty,
|
||||
dataset: ?DataSet = null,
|
||||
template_content: ?*parser.DocumentFragment = null,
|
||||
|
||||
// For dom/element
|
||||
shadow_root: ?*ShadowRoot = null,
|
||||
|
||||
// for html/document
|
||||
ready_state: ReadyState = .loading,
|
||||
|
||||
// for html/HTMLStyleElement
|
||||
style_sheet: ?*StyleSheet = null,
|
||||
|
||||
// for dom/document
|
||||
active_element: ?*parser.Element = null,
|
||||
adopted_style_sheets: ?Env.JsObject = null,
|
||||
|
||||
// for HTMLSelectElement
|
||||
// By default, if no option is explicitly selected, the first option should
|
||||
// be selected. However, libdom doesn't do this, and it sets the
|
||||
// selectedIndex to -1, which is a valid value for "nothing selected".
|
||||
// Therefore, when libdom says the selectedIndex == -1, we don't know if
|
||||
// it means that nothing is selected, or if the first option is selected by
|
||||
// default.
|
||||
// There are cases where this won't work, but when selectedIndex is
|
||||
// explicitly set, we set this boolean flag. Then, when we're getting then
|
||||
// selectedIndex, if this flag is == false, which is to say that if
|
||||
// selectedIndex hasn't been explicitly set AND if we have at least 1 option
|
||||
// AND if it isn't a multi select, we can make the 1st item selected by
|
||||
// default (by returning selectedIndex == 0).
|
||||
explicit_index_set: bool = false,
|
||||
|
||||
const ReadyState = enum {
|
||||
loading,
|
||||
interactive,
|
||||
complete,
|
||||
};
|
||||
@@ -21,14 +21,12 @@ const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
|
||||
const State = @import("State.zig");
|
||||
const Env = @import("env.zig").Env;
|
||||
const App = @import("../app.zig").App;
|
||||
const Session = @import("session.zig").Session;
|
||||
const Notification = @import("../notification.zig").Notification;
|
||||
|
||||
const log = @import("../log.zig");
|
||||
const HttpClient = @import("../http/Client.zig");
|
||||
const http = @import("../http/client.zig");
|
||||
|
||||
// Browser is an instance of the browser.
|
||||
// You can create multiple browser instances.
|
||||
@@ -38,22 +36,17 @@ pub const Browser = struct {
|
||||
app: *App,
|
||||
session: ?Session,
|
||||
allocator: Allocator,
|
||||
http_client: *HttpClient,
|
||||
http_client: *http.Client,
|
||||
page_arena: ArenaAllocator,
|
||||
session_arena: ArenaAllocator,
|
||||
transfer_arena: ArenaAllocator,
|
||||
notification: *Notification,
|
||||
state_pool: std.heap.MemoryPool(State),
|
||||
|
||||
pub fn init(app: *App) !Browser {
|
||||
const allocator = app.allocator;
|
||||
|
||||
const env = try Env.init(allocator, &app.platform, .{});
|
||||
const env = try Env.init(allocator, .{});
|
||||
errdefer env.deinit();
|
||||
|
||||
const notification = try Notification.init(allocator, app.notification);
|
||||
app.http.client.notification = notification;
|
||||
app.http.client.next_request_id = 0; // Should we track ids in CDP only?
|
||||
errdefer notification.deinit();
|
||||
|
||||
return .{
|
||||
@@ -62,11 +55,8 @@ pub const Browser = struct {
|
||||
.session = null,
|
||||
.allocator = allocator,
|
||||
.notification = notification,
|
||||
.http_client = app.http.client,
|
||||
.http_client = &app.http_client,
|
||||
.page_arena = ArenaAllocator.init(allocator),
|
||||
.session_arena = ArenaAllocator.init(allocator),
|
||||
.transfer_arena = ArenaAllocator.init(allocator),
|
||||
.state_pool = std.heap.MemoryPool(State).init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -74,11 +64,7 @@ pub const Browser = struct {
|
||||
self.closeSession();
|
||||
self.env.deinit();
|
||||
self.page_arena.deinit();
|
||||
self.session_arena.deinit();
|
||||
self.transfer_arena.deinit();
|
||||
self.http_client.notification = null;
|
||||
self.notification.deinit();
|
||||
self.state_pool.deinit();
|
||||
}
|
||||
|
||||
pub fn newSession(self: *Browser) !*Session {
|
||||
@@ -93,24 +79,24 @@ pub const Browser = struct {
|
||||
if (self.session) |*session| {
|
||||
session.deinit();
|
||||
self.session = null;
|
||||
_ = self.session_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
||||
self.env.lowMemoryNotification();
|
||||
if (self.app.config.gc_hints) {
|
||||
self.env.lowMemoryNotification();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn runMicrotasks(self: *const Browser) void {
|
||||
self.env.runMicrotasks();
|
||||
}
|
||||
|
||||
pub fn runMessageLoop(self: *const Browser) void {
|
||||
while (self.env.pumpMessageLoop()) {
|
||||
log.debug(.browser, "pumpMessageLoop", .{});
|
||||
}
|
||||
self.env.runIdleTasks();
|
||||
return self.env.runMicrotasks();
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
test "Browser" {
|
||||
try testing.htmlRunner("browser.html");
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
// this will crash if ICU isn't properly configured / ininitialized
|
||||
try runner.testCases(&.{
|
||||
.{ "new Intl.DateTimeFormat()", "[object Intl.DateTimeFormat]" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -18,97 +18,87 @@
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const JsObject = @import("../env.zig").Env.JsObject;
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
const log = if (builtin.is_test) &test_capture else std.log.scoped(.console);
|
||||
|
||||
pub const Console = struct {
|
||||
// TODO: configurable writer
|
||||
timers: std.StringHashMapUnmanaged(u32) = .{},
|
||||
counts: std.StringHashMapUnmanaged(u32) = .{},
|
||||
|
||||
pub fn _lp(values: []JsObject, page: *Page) !void {
|
||||
pub fn _log(_: *const Console, values: []JsObject, state: *SessionState) !void {
|
||||
if (values.len == 0) {
|
||||
return;
|
||||
}
|
||||
log.fatal(.console, "lightpanda", .{ .args = try serializeValues(values, page) });
|
||||
log.info("{s}", .{try serializeValues(values, state)});
|
||||
}
|
||||
|
||||
pub fn _log(values: []JsObject, page: *Page) !void {
|
||||
pub fn _info(console: *const Console, values: []JsObject, state: *SessionState) !void {
|
||||
return console._log(values, state);
|
||||
}
|
||||
|
||||
pub fn _debug(_: *const Console, values: []JsObject, state: *SessionState) !void {
|
||||
if (values.len == 0) {
|
||||
return;
|
||||
}
|
||||
log.info(.console, "info", .{ .args = try serializeValues(values, page) });
|
||||
log.debug("{s}", .{try serializeValues(values, state)});
|
||||
}
|
||||
|
||||
pub fn _info(values: []JsObject, page: *Page) !void {
|
||||
return _log(values, page);
|
||||
}
|
||||
|
||||
pub fn _debug(values: []JsObject, page: *Page) !void {
|
||||
pub fn _warn(_: *const Console, values: []JsObject, state: *SessionState) !void {
|
||||
if (values.len == 0) {
|
||||
return;
|
||||
}
|
||||
log.debug(.console, "debug", .{ .args = try serializeValues(values, page) });
|
||||
log.warn("{s}", .{try serializeValues(values, state)});
|
||||
}
|
||||
|
||||
pub fn _warn(values: []JsObject, page: *Page) !void {
|
||||
pub fn _error(_: *const Console, values: []JsObject, state: *SessionState) !void {
|
||||
if (values.len == 0) {
|
||||
return;
|
||||
}
|
||||
log.warn(.console, "warn", .{ .args = try serializeValues(values, page) });
|
||||
log.err("{s}", .{try serializeValues(values, state)});
|
||||
}
|
||||
|
||||
pub fn _error(values: []JsObject, page: *Page) !void {
|
||||
if (values.len == 0) {
|
||||
return;
|
||||
}
|
||||
pub fn _clear(_: *const Console) void {}
|
||||
|
||||
log.warn(.console, "error", .{
|
||||
.args = try serializeValues(values, page),
|
||||
.stack = page.stackTrace() catch "???",
|
||||
});
|
||||
}
|
||||
|
||||
pub fn _clear() void {}
|
||||
|
||||
pub fn _count(self: *Console, label_: ?[]const u8, page: *Page) !void {
|
||||
pub fn _count(self: *Console, label_: ?[]const u8, state: *SessionState) !void {
|
||||
const label = label_ orelse "default";
|
||||
const gop = try self.counts.getOrPut(page.arena, label);
|
||||
const gop = try self.counts.getOrPut(state.arena, label);
|
||||
|
||||
var current: u32 = 0;
|
||||
if (gop.found_existing) {
|
||||
current = gop.value_ptr.*;
|
||||
} else {
|
||||
gop.key_ptr.* = try page.arena.dupe(u8, label);
|
||||
gop.key_ptr.* = try state.arena.dupe(u8, label);
|
||||
}
|
||||
|
||||
const count = current + 1;
|
||||
gop.value_ptr.* = count;
|
||||
|
||||
log.info(.console, "count", .{ .label = label, .count = count });
|
||||
log.info("{s}: {d}", .{ label, count });
|
||||
}
|
||||
|
||||
pub fn _countReset(self: *Console, label_: ?[]const u8) !void {
|
||||
const label = label_ orelse "default";
|
||||
const kv = self.counts.fetchRemove(label) orelse {
|
||||
log.info(.console, "invalid counter", .{ .label = label });
|
||||
log.warn("Counter \"{s}\" doesn't exist.", .{label});
|
||||
return;
|
||||
};
|
||||
log.info(.console, "count reset", .{ .label = label, .count = kv.value });
|
||||
|
||||
log.info("{s}: {d}", .{ label, kv.value });
|
||||
}
|
||||
|
||||
pub fn _time(self: *Console, label_: ?[]const u8, page: *Page) !void {
|
||||
pub fn _time(self: *Console, label_: ?[]const u8, state: *SessionState) !void {
|
||||
const label = label_ orelse "default";
|
||||
const gop = try self.timers.getOrPut(page.arena, label);
|
||||
const gop = try self.timers.getOrPut(state.arena, label);
|
||||
|
||||
if (gop.found_existing) {
|
||||
log.info(.console, "duplicate timer", .{ .label = label });
|
||||
log.warn("Timer \"{s}\" already exists.", .{label});
|
||||
return;
|
||||
}
|
||||
gop.key_ptr.* = try page.arena.dupe(u8, label);
|
||||
gop.key_ptr.* = try state.arena.dupe(u8, label);
|
||||
gop.value_ptr.* = timestamp();
|
||||
}
|
||||
|
||||
@@ -116,124 +106,135 @@ pub const Console = struct {
|
||||
const elapsed = timestamp();
|
||||
const label = label_ orelse "default";
|
||||
const start = self.timers.get(label) orelse {
|
||||
log.info(.console, "invalid timer", .{ .label = label });
|
||||
log.warn("Timer \"{s}\" doesn't exist.", .{label});
|
||||
return;
|
||||
};
|
||||
log.info(.console, "timer", .{ .label = label, .elapsed = elapsed - start });
|
||||
|
||||
log.info("\"{s}\": {d}ms", .{ label, elapsed - start });
|
||||
}
|
||||
|
||||
pub fn _timeStop(self: *Console, label_: ?[]const u8) void {
|
||||
const elapsed = timestamp();
|
||||
const label = label_ orelse "default";
|
||||
const kv = self.timers.fetchRemove(label) orelse {
|
||||
log.info(.console, "invalid timer", .{ .label = label });
|
||||
log.warn("Timer \"{s}\" doesn't exist.", .{label});
|
||||
return;
|
||||
};
|
||||
|
||||
log.warn(.console, "timer stop", .{ .label = label, .elapsed = elapsed - kv.value });
|
||||
log.info("\"{s}\": {d}ms - timer ended", .{ label, elapsed - kv.value });
|
||||
}
|
||||
|
||||
pub fn _assert(assertion: JsObject, values: []JsObject, page: *Page) !void {
|
||||
pub fn _assert(_: *Console, assertion: JsObject, values: []JsObject, state: *SessionState) !void {
|
||||
if (assertion.isTruthy()) {
|
||||
return;
|
||||
}
|
||||
var serialized_values: []const u8 = "";
|
||||
if (values.len > 0) {
|
||||
serialized_values = try serializeValues(values, page);
|
||||
serialized_values = try serializeValues(values, state);
|
||||
}
|
||||
log.info(.console, "assertion failed", .{ .values = serialized_values });
|
||||
log.err("Assertion failed: {s}", .{serialized_values});
|
||||
}
|
||||
|
||||
fn serializeValues(values: []JsObject, page: *Page) ![]const u8 {
|
||||
if (values.len == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const arena = page.call_arena;
|
||||
const separator = log.separator();
|
||||
fn serializeValues(values: []JsObject, state: *SessionState) ![]const u8 {
|
||||
const arena = state.call_arena;
|
||||
var arr: std.ArrayListUnmanaged(u8) = .{};
|
||||
|
||||
for (values, 1..) |value, i| {
|
||||
try arr.appendSlice(arena, separator);
|
||||
try arr.writer(arena).print("{d}: ", .{i});
|
||||
const serialized = if (builtin.mode == .Debug) value.toDetailString() else value.toString();
|
||||
try arr.appendSlice(arena, try serialized);
|
||||
try arr.appendSlice(arena, try values[0].toString());
|
||||
for (values[1..]) |value| {
|
||||
try arr.append(arena, ' ');
|
||||
try arr.appendSlice(arena, try value.toString());
|
||||
}
|
||||
return arr.items;
|
||||
}
|
||||
};
|
||||
|
||||
fn timestamp() u32 {
|
||||
return @import("../../datetime.zig").timestamp();
|
||||
const ts = std.posix.clock_gettime(std.posix.CLOCK.MONOTONIC) catch unreachable;
|
||||
return @intCast(ts.sec);
|
||||
}
|
||||
|
||||
// const testing = @import("../../testing.zig");
|
||||
// test "Browser.Console" {
|
||||
// defer testing.reset();
|
||||
var test_capture = TestCapture{};
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.Console" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
// var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
// defer runner.deinit();
|
||||
defer testing.reset();
|
||||
|
||||
// {
|
||||
// try runner.testCases(&.{
|
||||
// .{ "console.log('a')", "undefined" },
|
||||
// .{ "console.warn('hello world', 23, true, new Object())", "undefined" },
|
||||
// }, .{});
|
||||
{
|
||||
try runner.testCases(&.{
|
||||
.{ "console.log('a')", "undefined" },
|
||||
.{ "console.warn('hello world', 23, true, new Object())", "undefined" },
|
||||
}, .{});
|
||||
|
||||
// const captured = test_capture.captured.items;
|
||||
// try testing.expectEqual("[info] args= 1: a", captured[0]);
|
||||
// try testing.expectEqual("[warn] args= 1: hello world 2: 23 3: true 4: #<Object>", captured[1]);
|
||||
// }
|
||||
const captured = test_capture.captured.items;
|
||||
try testing.expectEqual("a", captured[0]);
|
||||
try testing.expectEqual("hello world 23 true [object Object]", captured[1]);
|
||||
}
|
||||
|
||||
// {
|
||||
// test_capture.reset();
|
||||
// try runner.testCases(&.{
|
||||
// .{ "console.countReset()", "undefined" },
|
||||
// .{ "console.count()", "undefined" },
|
||||
// .{ "console.count('teg')", "undefined" },
|
||||
// .{ "console.count('teg')", "undefined" },
|
||||
// .{ "console.count('teg')", "undefined" },
|
||||
// .{ "console.count()", "undefined" },
|
||||
// .{ "console.countReset('teg')", "undefined" },
|
||||
// .{ "console.countReset()", "undefined" },
|
||||
// .{ "console.count()", "undefined" },
|
||||
// }, .{});
|
||||
{
|
||||
test_capture.reset();
|
||||
try runner.testCases(&.{
|
||||
.{ "console.countReset()", "undefined" },
|
||||
.{ "console.count()", "undefined" },
|
||||
.{ "console.count('teg')", "undefined" },
|
||||
.{ "console.count('teg')", "undefined" },
|
||||
.{ "console.count('teg')", "undefined" },
|
||||
.{ "console.count()", "undefined" },
|
||||
.{ "console.countReset('teg')", "undefined" },
|
||||
.{ "console.countReset()", "undefined" },
|
||||
.{ "console.count()", "undefined" },
|
||||
}, .{});
|
||||
|
||||
// const captured = test_capture.captured.items;
|
||||
// try testing.expectEqual("[invalid counter] label=default", captured[0]);
|
||||
// try testing.expectEqual("[count] label=default count=1", captured[1]);
|
||||
// try testing.expectEqual("[count] label=teg count=1", captured[2]);
|
||||
// try testing.expectEqual("[count] label=teg count=2", captured[3]);
|
||||
// try testing.expectEqual("[count] label=teg count=3", captured[4]);
|
||||
// try testing.expectEqual("[count] label=default count=2", captured[5]);
|
||||
// try testing.expectEqual("[count reset] label=teg count=3", captured[6]);
|
||||
// try testing.expectEqual("[count reset] label=default count=2", captured[7]);
|
||||
// try testing.expectEqual("[count] label=default count=1", captured[8]);
|
||||
// }
|
||||
const captured = test_capture.captured.items;
|
||||
try testing.expectEqual("Counter \"default\" doesn't exist.", captured[0]);
|
||||
try testing.expectEqual("default: 1", captured[1]);
|
||||
try testing.expectEqual("teg: 1", captured[2]);
|
||||
try testing.expectEqual("teg: 2", captured[3]);
|
||||
try testing.expectEqual("teg: 3", captured[4]);
|
||||
try testing.expectEqual("default: 2", captured[5]);
|
||||
try testing.expectEqual("teg: 3", captured[6]);
|
||||
try testing.expectEqual("default: 2", captured[7]);
|
||||
try testing.expectEqual("default: 1", captured[8]);
|
||||
}
|
||||
|
||||
// {
|
||||
// test_capture.reset();
|
||||
// try runner.testCases(&.{
|
||||
// .{ "console.assert(true)", "undefined" },
|
||||
// .{ "console.assert('a', 2, 3, 4)", "undefined" },
|
||||
// .{ "console.assert('')", "undefined" },
|
||||
// .{ "console.assert('', 'x', true)", "undefined" },
|
||||
// .{ "console.assert(false, 'x')", "undefined" },
|
||||
// }, .{});
|
||||
{
|
||||
test_capture.reset();
|
||||
try runner.testCases(&.{
|
||||
.{ "console.assert(true)", "undefined" },
|
||||
.{ "console.assert('a', 2, 3, 4)", "undefined" },
|
||||
.{ "console.assert('')", "undefined" },
|
||||
.{ "console.assert('', 'x', true)", "undefined" },
|
||||
.{ "console.assert(false, 'x')", "undefined" },
|
||||
}, .{});
|
||||
|
||||
// const captured = test_capture.captured.items;
|
||||
// try testing.expectEqual("[assertion failed] values=", captured[0]);
|
||||
// try testing.expectEqual("[assertion failed] values= 1: x 2: true", captured[1]);
|
||||
// try testing.expectEqual("[assertion failed] values= 1: x", captured[2]);
|
||||
// }
|
||||
const captured = test_capture.captured.items;
|
||||
try testing.expectEqual("Assertion failed: ", captured[0]);
|
||||
try testing.expectEqual("Assertion failed: x true", captured[1]);
|
||||
try testing.expectEqual("Assertion failed: x", captured[2]);
|
||||
}
|
||||
}
|
||||
|
||||
// {
|
||||
// test_capture.reset();
|
||||
// try runner.testCases(&.{
|
||||
// .{ "[1].forEach(console.log)", null },
|
||||
// }, .{});
|
||||
const TestCapture = struct {
|
||||
captured: std.ArrayListUnmanaged([]const u8) = .{},
|
||||
|
||||
// const captured = test_capture.captured.items;
|
||||
// try testing.expectEqual("[info] args= 1: 1 2: 0 3: [1]", captured[0]);
|
||||
// }
|
||||
// }
|
||||
fn reset(self: *TestCapture) void {
|
||||
self.captured = .{};
|
||||
}
|
||||
|
||||
fn debug(self: *TestCapture, comptime fmt: []const u8, args: anytype) void {
|
||||
const str = std.fmt.allocPrint(testing.arena_allocator, fmt, args) catch unreachable;
|
||||
self.captured.append(testing.arena_allocator, str) catch unreachable;
|
||||
}
|
||||
|
||||
fn info(self: *TestCapture, comptime fmt: []const u8, args: anytype) void {
|
||||
self.debug(fmt, args);
|
||||
}
|
||||
|
||||
fn warn(self: *TestCapture, comptime fmt: []const u8, args: anytype) void {
|
||||
self.debug(fmt, args);
|
||||
}
|
||||
|
||||
fn err(self: *TestCapture, comptime fmt: []const u8, args: anytype) void {
|
||||
self.debug(fmt, args);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -17,21 +17,16 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const uuidv4 = @import("../../id.zig").uuidv4;
|
||||
|
||||
// https://w3c.github.io/webcrypto/#crypto-interface
|
||||
pub const Crypto = struct {
|
||||
_not_empty: bool = true,
|
||||
|
||||
pub fn _getRandomValues(_: *const Crypto, js_obj: Env.JsObject) !Env.JsObject {
|
||||
var into = try js_obj.toZig(Crypto, "getRandomValues", RandomValues);
|
||||
pub fn _getRandomValues(_: *const Crypto, into: RandomValues) !void {
|
||||
const buf = into.asBuffer();
|
||||
if (buf.len > 65_536) {
|
||||
return error.QuotaExceededError;
|
||||
}
|
||||
std.crypto.random.bytes(buf);
|
||||
return js_obj;
|
||||
}
|
||||
|
||||
pub fn _randomUUID(_: *const Crypto) [36]u8 {
|
||||
@@ -52,20 +47,36 @@ const RandomValues = union(enum) {
|
||||
uint64: []u64,
|
||||
|
||||
fn asBuffer(self: RandomValues) []u8 {
|
||||
return switch (self) {
|
||||
.int8 => |b| (@as([]u8, @ptrCast(b)))[0..b.len],
|
||||
.uint8 => |b| (@as([]u8, @ptrCast(b)))[0..b.len],
|
||||
.int16 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 2],
|
||||
.uint16 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 2],
|
||||
.int32 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 4],
|
||||
.uint32 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 4],
|
||||
.int64 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 8],
|
||||
.uint64 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 8],
|
||||
};
|
||||
switch (self) {
|
||||
.int8 => |b| return (@as([]u8, @ptrCast(b)))[0..b.len],
|
||||
.uint8 => |b| return (@as([]u8, @ptrCast(b)))[0..b.len],
|
||||
.int16 => |b| return (@as([]u8, @ptrCast(b)))[0 .. b.len * 2],
|
||||
.uint16 => |b| return (@as([]u8, @ptrCast(b)))[0 .. b.len * 2],
|
||||
.int32 => |b| return (@as([]u8, @ptrCast(b)))[0 .. b.len * 4],
|
||||
.uint32 => |b| return (@as([]u8, @ptrCast(b)))[0 .. b.len * 4],
|
||||
.int64 => |b| return (@as([]u8, @ptrCast(b)))[0 .. b.len * 8],
|
||||
.uint64 => |b| return (@as([]u8, @ptrCast(b)))[0 .. b.len * 8],
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: Crypto" {
|
||||
try testing.htmlRunner("crypto.html");
|
||||
test "Browser.Crypto" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const a = crypto.randomUUID();", "undefined" },
|
||||
.{ "const b = crypto.randomUUID();", "undefined" },
|
||||
.{ "a.length;", "36" },
|
||||
.{ "a.length;", "36" },
|
||||
.{ "a == b;", "false" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "try { crypto.getRandomValues(new BigUint64Array(8193)) } catch(e) { e.message == 'QuotaExceededError' }", "true" },
|
||||
.{ "let r1 = new Int32Array(5)", "undefined" },
|
||||
.{ "crypto.getRandomValues(r1)", "undefined" },
|
||||
.{ "new Set(r1).size", "5" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -23,20 +23,6 @@ const std = @import("std");
|
||||
const Selector = @import("selector.zig").Selector;
|
||||
const parser = @import("parser.zig");
|
||||
|
||||
pub const Interfaces = .{
|
||||
Css,
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CSS
|
||||
pub const Css = struct {
|
||||
_not_empty: bool = true,
|
||||
|
||||
pub fn _supports(_: *Css, _: []const u8, _: ?[]const u8) bool {
|
||||
// TODO: Actually respond with which CSS features we support.
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// parse parse a selector string and returns the parsed result or an error.
|
||||
pub fn parse(alloc: std.mem.Allocator, s: []const u8, opts: parser.ParseOptions) parser.ParseError!Selector {
|
||||
var p = parser.Parser{ .s = s, .i = 0, .opts = opts };
|
||||
@@ -45,7 +31,7 @@ pub fn parse(alloc: std.mem.Allocator, s: []const u8, opts: parser.ParseOptions)
|
||||
|
||||
// matchFirst call m.match with the first node that matches the selector s, from the
|
||||
// descendants of n and returns true. If none matches, it returns false.
|
||||
pub fn matchFirst(s: *const Selector, node: anytype, m: anytype) !bool {
|
||||
pub fn matchFirst(s: Selector, node: anytype, m: anytype) !bool {
|
||||
var c = try node.firstChild();
|
||||
while (true) {
|
||||
if (c == null) break;
|
||||
@@ -63,7 +49,7 @@ pub fn matchFirst(s: *const Selector, node: anytype, m: anytype) !bool {
|
||||
|
||||
// matchAll call m.match with the all the nodes that matches the selector s, from the
|
||||
// descendants of n.
|
||||
pub fn matchAll(s: *const Selector, node: anytype, m: anytype) !void {
|
||||
pub fn matchAll(s: Selector, node: anytype, m: anytype) !void {
|
||||
var c = try node.firstChild();
|
||||
while (true) {
|
||||
if (c == null) break;
|
||||
@@ -188,8 +174,3 @@ test "parse" {
|
||||
defer s.deinit(alloc);
|
||||
}
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: CSS" {
|
||||
try testing.htmlRunner("css.html");
|
||||
}
|
||||
|
||||
@@ -20,23 +20,18 @@ const std = @import("std");
|
||||
const css = @import("css.zig");
|
||||
const Node = @import("libdom.zig").Node;
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Matcher = struct {
|
||||
const Nodes = std.ArrayListUnmanaged(Node);
|
||||
const Nodes = std.ArrayList(Node);
|
||||
|
||||
nodes: Nodes,
|
||||
allocator: Allocator,
|
||||
|
||||
fn init(allocator: Allocator) Matcher {
|
||||
return .{
|
||||
.nodes = .empty,
|
||||
.allocator = allocator,
|
||||
};
|
||||
fn init(alloc: std.mem.Allocator) Matcher {
|
||||
return .{ .nodes = Nodes.init(alloc) };
|
||||
}
|
||||
|
||||
fn deinit(m: *Matcher) void {
|
||||
m.nodes.deinit(m.allocator);
|
||||
m.nodes.deinit();
|
||||
}
|
||||
|
||||
fn reset(m: *Matcher) void {
|
||||
@@ -44,7 +39,7 @@ const Matcher = struct {
|
||||
}
|
||||
|
||||
pub fn match(m: *Matcher, n: Node) !void {
|
||||
try m.nodes.append(m.allocator, n);
|
||||
try m.nodes.append(n);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -166,7 +161,7 @@ test "matchFirst" {
|
||||
for (testcases) |tc| {
|
||||
matcher.reset();
|
||||
|
||||
const doc = try parser.documentHTMLParseFromStr(tc.html);
|
||||
const doc = try parser.documentHTMLParseFromStr(alloc, tc.html);
|
||||
defer parser.documentHTMLClose(doc) catch {};
|
||||
|
||||
const s = css.parse(alloc, tc.q, .{}) catch |e| {
|
||||
|
||||
@@ -84,20 +84,16 @@ pub const Node = struct {
|
||||
};
|
||||
|
||||
const Matcher = struct {
|
||||
const Nodes = std.ArrayListUnmanaged(*const Node);
|
||||
const Nodes = std.ArrayList(*const Node);
|
||||
|
||||
nodes: Nodes,
|
||||
allocator: Allocator,
|
||||
|
||||
fn init(allocator: Allocator) Matcher {
|
||||
return .{
|
||||
.nodes = .empty,
|
||||
.allocator = allocator,
|
||||
};
|
||||
fn init(alloc: std.mem.Allocator) Matcher {
|
||||
return .{ .nodes = Nodes.init(alloc) };
|
||||
}
|
||||
|
||||
fn deinit(m: *Matcher) void {
|
||||
m.nodes.deinit(self.allocator);
|
||||
m.nodes.deinit();
|
||||
}
|
||||
|
||||
fn reset(m: *Matcher) void {
|
||||
@@ -105,7 +101,7 @@ const Matcher = struct {
|
||||
}
|
||||
|
||||
pub fn match(m: *Matcher, n: *const Node) !void {
|
||||
try m.nodes.append(self.allocator, n);
|
||||
try m.nodes.append(n);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
// see https://github.com/andybalholm/cascadia/blob/master/parser.go
|
||||
const std = @import("std");
|
||||
const ascii = std.ascii;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const selector = @import("selector.zig");
|
||||
const Selector = selector.Selector;
|
||||
@@ -78,8 +77,8 @@ pub const Parser = struct {
|
||||
|
||||
opts: ParseOptions,
|
||||
|
||||
pub fn parse(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
return p.parseSelectorGroup(allocator);
|
||||
pub fn parse(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
return p.parseSelectorGroup(alloc);
|
||||
}
|
||||
|
||||
// skipWhitespace consumes whitespace characters and comments.
|
||||
@@ -116,13 +115,13 @@ pub const Parser = struct {
|
||||
|
||||
// parseSimpleSelectorSequence parses a selector sequence that applies to
|
||||
// a single element.
|
||||
fn parseSimpleSelectorSequence(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
fn parseSimpleSelectorSequence(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
if (p.i >= p.s.len) {
|
||||
return ParseError.ExpectedSelector;
|
||||
}
|
||||
|
||||
var buf: std.ArrayListUnmanaged(Selector) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
var buf = std.ArrayList(Selector).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
switch (p.s[p.i]) {
|
||||
'*' => {
|
||||
@@ -139,20 +138,20 @@ pub const Parser = struct {
|
||||
// There's no type selector. Wait to process the other till the
|
||||
// main loop.
|
||||
},
|
||||
else => try buf.append(allocator, try p.parseTypeSelector(allocator)),
|
||||
else => try buf.append(try p.parseTypeSelector(alloc)),
|
||||
}
|
||||
|
||||
var pseudo_elt: ?PseudoClass = null;
|
||||
|
||||
loop: while (p.i < p.s.len) {
|
||||
var ns: Selector = switch (p.s[p.i]) {
|
||||
'#' => try p.parseIDSelector(allocator),
|
||||
'.' => try p.parseClassSelector(allocator),
|
||||
'[' => try p.parseAttributeSelector(allocator),
|
||||
':' => try p.parsePseudoclassSelector(allocator),
|
||||
'#' => try p.parseIDSelector(alloc),
|
||||
'.' => try p.parseClassSelector(alloc),
|
||||
'[' => try p.parseAttributeSelector(alloc),
|
||||
':' => try p.parsePseudoclassSelector(alloc),
|
||||
else => break :loop,
|
||||
};
|
||||
errdefer ns.deinit(allocator);
|
||||
errdefer ns.deinit(alloc);
|
||||
|
||||
// From https://drafts.csswg.org/selectors-3/#pseudo-elements :
|
||||
// "Only one pseudo-element may appear per selector, and if present
|
||||
@@ -166,32 +165,28 @@ pub const Parser = struct {
|
||||
if (!p.opts.accept_pseudo_elts) return ParseError.PseudoElementDisabled;
|
||||
|
||||
pseudo_elt = e;
|
||||
ns.deinit(allocator);
|
||||
ns.deinit(alloc);
|
||||
},
|
||||
else => {
|
||||
if (pseudo_elt != null) return ParseError.PseudoElementNotAtSelectorEnd;
|
||||
try buf.append(allocator, ns);
|
||||
try buf.append(ns);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// no need wrap the selectors in compoundSelector
|
||||
if (buf.items.len == 1 and pseudo_elt == null) {
|
||||
return buf.items[0];
|
||||
}
|
||||
if (buf.items.len == 1 and pseudo_elt == null) return buf.items[0];
|
||||
|
||||
return .{
|
||||
.compound = .{ .selectors = try buf.toOwnedSlice(allocator), .pseudo_elt = pseudo_elt },
|
||||
};
|
||||
return .{ .compound = .{ .selectors = try buf.toOwnedSlice(), .pseudo_elt = pseudo_elt } };
|
||||
}
|
||||
|
||||
// parseTypeSelector parses a type selector (one that matches by tag name).
|
||||
fn parseTypeSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
try p.parseIdentifier(buf.writer(allocator));
|
||||
fn parseTypeSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
try p.parseIdentifier(buf.writer());
|
||||
|
||||
return .{ .tag = try buf.toOwnedSlice(allocator) };
|
||||
return .{ .tag = try buf.toOwnedSlice() };
|
||||
}
|
||||
|
||||
// parseIdentifier parses an identifier.
|
||||
@@ -209,7 +204,7 @@ pub const Parser = struct {
|
||||
}
|
||||
|
||||
const c = p.s[p.i];
|
||||
if (!(nameStart(c) or c == '\\')) {
|
||||
if (!nameStart(c) or c == '\\') {
|
||||
return ParseError.ExpectedSelector;
|
||||
}
|
||||
|
||||
@@ -319,47 +314,47 @@ pub const Parser = struct {
|
||||
}
|
||||
|
||||
// parseIDSelector parses a selector that matches by id attribute.
|
||||
fn parseIDSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
fn parseIDSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedIDSelector;
|
||||
if (p.s[p.i] != '#') return ParseError.ExpectedIDSelector;
|
||||
|
||||
p.i += 1;
|
||||
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
try p.parseName(buf.writer(allocator));
|
||||
return .{ .id = try buf.toOwnedSlice(allocator) };
|
||||
try p.parseName(buf.writer());
|
||||
return .{ .id = try buf.toOwnedSlice() };
|
||||
}
|
||||
|
||||
// parseClassSelector parses a selector that matches by class attribute.
|
||||
fn parseClassSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
fn parseClassSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedClassSelector;
|
||||
if (p.s[p.i] != '.') return ParseError.ExpectedClassSelector;
|
||||
|
||||
p.i += 1;
|
||||
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
try p.parseIdentifier(buf.writer(allocator));
|
||||
return .{ .class = try buf.toOwnedSlice(allocator) };
|
||||
try p.parseIdentifier(buf.writer());
|
||||
return .{ .class = try buf.toOwnedSlice() };
|
||||
}
|
||||
|
||||
// parseAttributeSelector parses a selector that matches by attribute value.
|
||||
fn parseAttributeSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
fn parseAttributeSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedAttributeSelector;
|
||||
if (p.s[p.i] != '[') return ParseError.ExpectedAttributeSelector;
|
||||
|
||||
p.i += 1;
|
||||
_ = p.skipWhitespace();
|
||||
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
try p.parseIdentifier(buf.writer(allocator));
|
||||
const key = try buf.toOwnedSlice(allocator);
|
||||
errdefer allocator.free(key);
|
||||
try p.parseIdentifier(buf.writer());
|
||||
const key = try buf.toOwnedSlice();
|
||||
errdefer alloc.free(key);
|
||||
|
||||
lowerstr(key);
|
||||
|
||||
@@ -382,12 +377,12 @@ pub const Parser = struct {
|
||||
var is_val: bool = undefined;
|
||||
if (op == .regexp) {
|
||||
is_val = false;
|
||||
try p.parseRegex(buf.writer(allocator));
|
||||
try p.parseRegex(buf.writer());
|
||||
} else {
|
||||
is_val = true;
|
||||
switch (p.s[p.i]) {
|
||||
'\'', '"' => try p.parseString(buf.writer(allocator)),
|
||||
else => try p.parseIdentifier(buf.writer(allocator)),
|
||||
'\'', '"' => try p.parseString(buf.writer()),
|
||||
else => try p.parseIdentifier(buf.writer()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,8 +404,8 @@ pub const Parser = struct {
|
||||
|
||||
return .{ .attribute = .{
|
||||
.key = key,
|
||||
.val = if (is_val) try buf.toOwnedSlice(allocator) else null,
|
||||
.regexp = if (!is_val) try buf.toOwnedSlice(allocator) else null,
|
||||
.val = if (is_val) try buf.toOwnedSlice() else null,
|
||||
.regexp = if (!is_val) try buf.toOwnedSlice() else null,
|
||||
.op = op,
|
||||
.ci = ci,
|
||||
} };
|
||||
@@ -503,7 +498,7 @@ pub const Parser = struct {
|
||||
// parsePseudoclassSelector parses a pseudoclass selector like :not(p) or a pseudo-element
|
||||
// For backwards compatibility, both ':' and '::' prefix are allowed for pseudo-elements.
|
||||
// https://drafts.csswg.org/selectors-3/#pseudo-elements
|
||||
fn parsePseudoclassSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
fn parsePseudoclassSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedPseudoClassSelector;
|
||||
if (p.s[p.i] != ':') return ParseError.ExpectedPseudoClassSelector;
|
||||
|
||||
@@ -516,10 +511,10 @@ pub const Parser = struct {
|
||||
p.i += 1;
|
||||
}
|
||||
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
try p.parseIdentifier(buf.writer(allocator));
|
||||
try p.parseIdentifier(buf.writer());
|
||||
|
||||
const pseudo_class = try PseudoClass.parse(buf.items);
|
||||
|
||||
@@ -532,11 +527,11 @@ pub const Parser = struct {
|
||||
.not, .has, .haschild => {
|
||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
||||
|
||||
const sel = try p.parseSelectorGroup(allocator);
|
||||
const sel = try p.parseSelectorGroup(alloc);
|
||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
||||
|
||||
const s = try allocator.create(Selector);
|
||||
errdefer allocator.destroy(s);
|
||||
const s = try alloc.create(Selector);
|
||||
errdefer alloc.destroy(s);
|
||||
s.* = sel;
|
||||
|
||||
return .{ .pseudo_class_relative = .{ .pseudo_class = pseudo_class, .match = s } };
|
||||
@@ -546,16 +541,16 @@ pub const Parser = struct {
|
||||
if (p.i == p.s.len) return ParseError.UnmatchParenthesis;
|
||||
|
||||
switch (p.s[p.i]) {
|
||||
'\'', '"' => try p.parseString(buf.writer(allocator)),
|
||||
else => try p.parseString(buf.writer(allocator)),
|
||||
'\'', '"' => try p.parseString(buf.writer()),
|
||||
else => try p.parseString(buf.writer()),
|
||||
}
|
||||
|
||||
_ = p.skipWhitespace();
|
||||
if (p.i >= p.s.len) return ParseError.InvalidPseudoClass;
|
||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
||||
|
||||
const val = try buf.toOwnedSlice(allocator);
|
||||
errdefer allocator.free(val);
|
||||
const val = try buf.toOwnedSlice();
|
||||
errdefer alloc.free(val);
|
||||
|
||||
lowerstr(val);
|
||||
|
||||
@@ -564,15 +559,15 @@ pub const Parser = struct {
|
||||
.matches, .matchesown => {
|
||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
||||
|
||||
try p.parseRegex(buf.writer(allocator));
|
||||
try p.parseRegex(buf.writer());
|
||||
if (p.i >= p.s.len) return ParseError.InvalidPseudoClassSelector;
|
||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
||||
|
||||
return .{ .pseudo_class_regexp = .{ .own = pseudo_class == .matchesown, .regexp = try buf.toOwnedSlice(allocator) } };
|
||||
return .{ .pseudo_class_regexp = .{ .own = pseudo_class == .matchesown, .regexp = try buf.toOwnedSlice() } };
|
||||
},
|
||||
.nth_child, .nth_last_child, .nth_of_type, .nth_last_of_type => {
|
||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
||||
const nth = try p.parseNth(allocator);
|
||||
const nth = try p.parseNth(alloc);
|
||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
||||
|
||||
const last = pseudo_class == .nth_last_child or pseudo_class == .nth_last_of_type;
|
||||
@@ -587,19 +582,18 @@ pub const Parser = struct {
|
||||
.only_of_type => return .{ .pseudo_class_only_child = true },
|
||||
.input, .empty, .root, .link => return .{ .pseudo_class = pseudo_class },
|
||||
.enabled, .disabled, .checked => return .{ .pseudo_class = pseudo_class },
|
||||
.visible => return .{ .pseudo_class = pseudo_class },
|
||||
.lang => {
|
||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
||||
if (p.i == p.s.len) return ParseError.UnmatchParenthesis;
|
||||
|
||||
try p.parseIdentifier(buf.writer(allocator));
|
||||
try p.parseIdentifier(buf.writer());
|
||||
|
||||
_ = p.skipWhitespace();
|
||||
if (p.i >= p.s.len) return ParseError.InvalidPseudoClass;
|
||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
||||
|
||||
const val = try buf.toOwnedSlice(allocator);
|
||||
errdefer allocator.free(val);
|
||||
const val = try buf.toOwnedSlice();
|
||||
errdefer alloc.free(val);
|
||||
lowerstr(val);
|
||||
|
||||
return .{ .pseudo_class_lang = val };
|
||||
@@ -611,7 +605,6 @@ pub const Parser = struct {
|
||||
.after, .backdrop, .before, .cue, .first_letter => return .{ .pseudo_element = pseudo_class },
|
||||
.first_line, .grammar_error, .marker, .placeholder => return .{ .pseudo_element = pseudo_class },
|
||||
.selection, .spelling_error => return .{ .pseudo_element = pseudo_class },
|
||||
.modal, .popover_open => return .{ .pseudo_element = pseudo_class },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,32 +620,30 @@ pub const Parser = struct {
|
||||
}
|
||||
|
||||
// parseSelectorGroup parses a group of selectors, separated by commas.
|
||||
fn parseSelectorGroup(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
const s = try p.parseSelector(allocator);
|
||||
fn parseSelectorGroup(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
const s = try p.parseSelector(alloc);
|
||||
|
||||
var buf: std.ArrayListUnmanaged(Selector) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
var buf = std.ArrayList(Selector).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
try buf.append(allocator, s);
|
||||
try buf.append(s);
|
||||
|
||||
while (p.i < p.s.len) {
|
||||
if (p.s[p.i] != ',') break;
|
||||
p.i += 1;
|
||||
const ss = try p.parseSelector(allocator);
|
||||
try buf.append(allocator, ss);
|
||||
const ss = try p.parseSelector(alloc);
|
||||
try buf.append(ss);
|
||||
}
|
||||
|
||||
if (buf.items.len == 1) {
|
||||
return buf.items[0];
|
||||
}
|
||||
if (buf.items.len == 1) return buf.items[0];
|
||||
|
||||
return .{ .group = try buf.toOwnedSlice(allocator) };
|
||||
return .{ .group = try buf.toOwnedSlice() };
|
||||
}
|
||||
|
||||
// parseSelector parses a selector that may include combinators.
|
||||
fn parseSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
fn parseSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
_ = p.skipWhitespace();
|
||||
var s = try p.parseSimpleSelectorSequence(allocator);
|
||||
var s = try p.parseSimpleSelectorSequence(alloc);
|
||||
|
||||
while (true) {
|
||||
var combinator: Combinator = .empty;
|
||||
@@ -680,21 +671,17 @@ pub const Parser = struct {
|
||||
return s;
|
||||
}
|
||||
|
||||
const c = try p.parseSimpleSelectorSequence(allocator);
|
||||
const c = try p.parseSimpleSelectorSequence(alloc);
|
||||
|
||||
const first = try allocator.create(Selector);
|
||||
errdefer allocator.destroy(first);
|
||||
const first = try alloc.create(Selector);
|
||||
errdefer alloc.destroy(first);
|
||||
first.* = s;
|
||||
|
||||
const second = try allocator.create(Selector);
|
||||
errdefer allocator.destroy(second);
|
||||
const second = try alloc.create(Selector);
|
||||
errdefer alloc.destroy(second);
|
||||
second.* = c;
|
||||
|
||||
s = Selector{ .combined = .{
|
||||
.first = first,
|
||||
.second = second,
|
||||
.combinator = combinator,
|
||||
} };
|
||||
s = Selector{ .combined = .{ .first = first, .second = second, .combinator = combinator } };
|
||||
}
|
||||
|
||||
return s;
|
||||
@@ -787,7 +774,7 @@ pub const Parser = struct {
|
||||
}
|
||||
|
||||
// parseNth parses the argument for :nth-child (normally of the form an+b).
|
||||
fn parseNth(p: *Parser, allocator: Allocator) ParseError![2]isize {
|
||||
fn parseNth(p: *Parser, alloc: std.mem.Allocator) ParseError![2]isize {
|
||||
// initial state
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedNthExpression;
|
||||
return switch (p.s[p.i]) {
|
||||
@@ -805,10 +792,10 @@ pub const Parser = struct {
|
||||
return p.parseNthReadN(1);
|
||||
},
|
||||
'o', 'O', 'e', 'E' => {
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
try p.parseName(buf.writer(allocator));
|
||||
try p.parseName(buf.writer());
|
||||
|
||||
if (std.ascii.eqlIgnoreCase("odd", buf.items)) return .{ 2, 1 };
|
||||
if (std.ascii.eqlIgnoreCase("even", buf.items)) return .{ 2, 0 };
|
||||
@@ -884,7 +871,7 @@ test "parser.skipWhitespace" {
|
||||
}
|
||||
|
||||
test "parser.parseIdentifier" {
|
||||
const allocator = std.testing.allocator;
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
const testcases = [_]struct {
|
||||
s: []const u8, // given value
|
||||
@@ -900,14 +887,14 @@ test "parser.parseIdentifier" {
|
||||
.{ .s = "a\\\"b", .exp = "a\"b" },
|
||||
};
|
||||
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
for (testcases) |tc| {
|
||||
buf.clearRetainingCapacity();
|
||||
|
||||
var p = Parser{ .s = tc.s, .opts = .{} };
|
||||
p.parseIdentifier(buf.writer(allocator)) catch |e| {
|
||||
p.parseIdentifier(buf.writer()) catch |e| {
|
||||
// if error was expected, continue.
|
||||
if (tc.err) continue;
|
||||
|
||||
@@ -922,7 +909,7 @@ test "parser.parseIdentifier" {
|
||||
}
|
||||
|
||||
test "parser.parseString" {
|
||||
const allocator = std.testing.allocator;
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
const testcases = [_]struct {
|
||||
s: []const u8, // given value
|
||||
@@ -941,14 +928,14 @@ test "parser.parseString" {
|
||||
.{ .s = "\"hello world\"", .exp = "hello world" },
|
||||
};
|
||||
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
for (testcases) |tc| {
|
||||
buf.clearRetainingCapacity();
|
||||
|
||||
var p = Parser{ .s = tc.s, .opts = .{} };
|
||||
p.parseString(buf.writer(allocator)) catch |e| {
|
||||
p.parseString(buf.writer()) catch |e| {
|
||||
// if error was expected, continue.
|
||||
if (tc.err) continue;
|
||||
|
||||
@@ -961,36 +948,3 @@ test "parser.parseString" {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test "parser.parse" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const allocator = arena.allocator();
|
||||
|
||||
const testcases = [_]struct {
|
||||
s: []const u8, // given value
|
||||
exp: Selector, // expected value
|
||||
err: bool = false,
|
||||
}{
|
||||
.{ .s = "root", .exp = .{ .tag = "root" } },
|
||||
.{ .s = ".root", .exp = .{ .class = "root" } },
|
||||
.{ .s = ":root", .exp = .{ .pseudo_class = .root } },
|
||||
.{ .s = ".\\:bar", .exp = .{ .class = ":bar" } },
|
||||
.{ .s = ".foo\\:bar", .exp = .{ .class = "foo:bar" } },
|
||||
};
|
||||
|
||||
for (testcases) |tc| {
|
||||
var p = Parser{ .s = tc.s, .opts = .{} };
|
||||
const sel = p.parse(allocator) catch |e| {
|
||||
// if error was expected, continue.
|
||||
if (tc.err) continue;
|
||||
|
||||
std.debug.print("test case {s}\n", .{tc.s});
|
||||
return e;
|
||||
};
|
||||
std.testing.expectEqualDeep(tc.exp, sel) catch |e| {
|
||||
std.debug.print("test case {s} : {}\n", .{ tc.s, sel });
|
||||
return e;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,9 +98,6 @@ pub const PseudoClass = enum {
|
||||
placeholder,
|
||||
selection,
|
||||
spelling_error,
|
||||
modal,
|
||||
popover_open,
|
||||
visible,
|
||||
|
||||
pub const Error = error{
|
||||
InvalidPseudoClass,
|
||||
@@ -116,108 +113,51 @@ pub const PseudoClass = enum {
|
||||
}
|
||||
|
||||
pub fn parse(s: []const u8) Error!PseudoClass {
|
||||
const longest_selector = "nth-last-of-type";
|
||||
if (s.len > longest_selector.len) {
|
||||
return Error.InvalidPseudoClass;
|
||||
}
|
||||
|
||||
var buf: [longest_selector.len]u8 = undefined;
|
||||
const selector = std.ascii.lowerString(&buf, s);
|
||||
|
||||
switch (selector.len) {
|
||||
3 => switch (@as(u24, @bitCast(selector[0..3].*))) {
|
||||
asUint(u24, "cue") => return .cue,
|
||||
asUint(u24, "has") => return .has,
|
||||
asUint(u24, "not") => return .not,
|
||||
else => {},
|
||||
},
|
||||
4 => switch (@as(u32, @bitCast(selector[0..4].*))) {
|
||||
asUint(u32, "lang") => return .lang,
|
||||
asUint(u32, "link") => return .link,
|
||||
asUint(u32, "root") => return .root,
|
||||
else => {},
|
||||
},
|
||||
5 => switch (@as(u40, @bitCast(selector[0..5].*))) {
|
||||
asUint(u40, "after") => return .after,
|
||||
asUint(u40, "empty") => return .empty,
|
||||
asUint(u40, "focus") => return .focus,
|
||||
asUint(u40, "hover") => return .hover,
|
||||
asUint(u40, "input") => return .input,
|
||||
asUint(u40, "modal") => return .modal,
|
||||
else => {},
|
||||
},
|
||||
6 => switch (@as(u48, @bitCast(selector[0..6].*))) {
|
||||
asUint(u48, "active") => return .active,
|
||||
asUint(u48, "before") => return .before,
|
||||
asUint(u48, "marker") => return .marker,
|
||||
asUint(u48, "target") => return .target,
|
||||
else => {},
|
||||
},
|
||||
7 => switch (@as(u56, @bitCast(selector[0..7].*))) {
|
||||
asUint(u56, "checked") => return .checked,
|
||||
asUint(u56, "enabled") => return .enabled,
|
||||
asUint(u56, "matches") => return .matches,
|
||||
asUint(u56, "visited") => return .visited,
|
||||
asUint(u56, "visible") => return .visible,
|
||||
else => {},
|
||||
},
|
||||
8 => switch (@as(u64, @bitCast(selector[0..8].*))) {
|
||||
asUint(u64, "backdrop") => return .backdrop,
|
||||
asUint(u64, "contains") => return .contains,
|
||||
asUint(u64, "disabled") => return .disabled,
|
||||
asUint(u64, "haschild") => return .haschild,
|
||||
else => {},
|
||||
},
|
||||
9 => switch (@as(u72, @bitCast(selector[0..9].*))) {
|
||||
asUint(u72, "nth-child") => return .nth_child,
|
||||
asUint(u72, "selection") => return .selection,
|
||||
else => {},
|
||||
},
|
||||
10 => switch (@as(u80, @bitCast(selector[0..10].*))) {
|
||||
asUint(u80, "first-line") => return .first_line,
|
||||
asUint(u80, "last-child") => return .last_child,
|
||||
asUint(u80, "matchesown") => return .matchesown,
|
||||
asUint(u80, "only-child") => return .only_child,
|
||||
else => {},
|
||||
},
|
||||
11 => switch (@as(u88, @bitCast(selector[0..11].*))) {
|
||||
asUint(u88, "containsown") => return .containsown,
|
||||
asUint(u88, "first-child") => return .first_child,
|
||||
asUint(u88, "nth-of-type") => return .nth_of_type,
|
||||
asUint(u88, "placeholder") => return .placeholder,
|
||||
else => {},
|
||||
},
|
||||
12 => switch (@as(u96, @bitCast(selector[0..12].*))) {
|
||||
asUint(u96, "first-letter") => return .first_letter,
|
||||
asUint(u96, "last-of-type") => return .last_of_type,
|
||||
asUint(u96, "only-of-type") => return .only_of_type,
|
||||
asUint(u96, "popover-open") => return .popover_open,
|
||||
else => {},
|
||||
},
|
||||
13 => switch (@as(u104, @bitCast(selector[0..13].*))) {
|
||||
asUint(u104, "first-of-type") => return .first_of_type,
|
||||
asUint(u104, "grammar-error") => return .grammar_error,
|
||||
else => {},
|
||||
},
|
||||
14 => switch (@as(u112, @bitCast(selector[0..14].*))) {
|
||||
asUint(u112, "nth-last-child") => return .nth_last_child,
|
||||
asUint(u112, "spelling-error") => return .spelling_error,
|
||||
else => {},
|
||||
},
|
||||
16 => switch (@as(u128, @bitCast(selector[0..16].*))) {
|
||||
asUint(u128, "nth-last-of-type") => return .nth_last_of_type,
|
||||
else => {},
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
if (std.ascii.eqlIgnoreCase(s, "not")) return .not;
|
||||
if (std.ascii.eqlIgnoreCase(s, "has")) return .has;
|
||||
if (std.ascii.eqlIgnoreCase(s, "haschild")) return .haschild;
|
||||
if (std.ascii.eqlIgnoreCase(s, "contains")) return .contains;
|
||||
if (std.ascii.eqlIgnoreCase(s, "containsown")) return .containsown;
|
||||
if (std.ascii.eqlIgnoreCase(s, "matches")) return .matches;
|
||||
if (std.ascii.eqlIgnoreCase(s, "matchesown")) return .matchesown;
|
||||
if (std.ascii.eqlIgnoreCase(s, "nth-child")) return .nth_child;
|
||||
if (std.ascii.eqlIgnoreCase(s, "nth-last-child")) return .nth_last_child;
|
||||
if (std.ascii.eqlIgnoreCase(s, "nth-of-type")) return .nth_of_type;
|
||||
if (std.ascii.eqlIgnoreCase(s, "nth-last-of-type")) return .nth_last_of_type;
|
||||
if (std.ascii.eqlIgnoreCase(s, "first-child")) return .first_child;
|
||||
if (std.ascii.eqlIgnoreCase(s, "last-child")) return .last_child;
|
||||
if (std.ascii.eqlIgnoreCase(s, "first-of-type")) return .first_of_type;
|
||||
if (std.ascii.eqlIgnoreCase(s, "last-of-type")) return .last_of_type;
|
||||
if (std.ascii.eqlIgnoreCase(s, "only-child")) return .only_child;
|
||||
if (std.ascii.eqlIgnoreCase(s, "only-of-type")) return .only_of_type;
|
||||
if (std.ascii.eqlIgnoreCase(s, "input")) return .input;
|
||||
if (std.ascii.eqlIgnoreCase(s, "empty")) return .empty;
|
||||
if (std.ascii.eqlIgnoreCase(s, "root")) return .root;
|
||||
if (std.ascii.eqlIgnoreCase(s, "link")) return .link;
|
||||
if (std.ascii.eqlIgnoreCase(s, "lang")) return .lang;
|
||||
if (std.ascii.eqlIgnoreCase(s, "enabled")) return .enabled;
|
||||
if (std.ascii.eqlIgnoreCase(s, "disabled")) return .disabled;
|
||||
if (std.ascii.eqlIgnoreCase(s, "checked")) return .checked;
|
||||
if (std.ascii.eqlIgnoreCase(s, "visited")) return .visited;
|
||||
if (std.ascii.eqlIgnoreCase(s, "hover")) return .hover;
|
||||
if (std.ascii.eqlIgnoreCase(s, "active")) return .active;
|
||||
if (std.ascii.eqlIgnoreCase(s, "focus")) return .focus;
|
||||
if (std.ascii.eqlIgnoreCase(s, "target")) return .target;
|
||||
if (std.ascii.eqlIgnoreCase(s, "after")) return .after;
|
||||
if (std.ascii.eqlIgnoreCase(s, "backdrop")) return .backdrop;
|
||||
if (std.ascii.eqlIgnoreCase(s, "before")) return .before;
|
||||
if (std.ascii.eqlIgnoreCase(s, "cue")) return .cue;
|
||||
if (std.ascii.eqlIgnoreCase(s, "first-letter")) return .first_letter;
|
||||
if (std.ascii.eqlIgnoreCase(s, "first-line")) return .first_line;
|
||||
if (std.ascii.eqlIgnoreCase(s, "grammar-error")) return .grammar_error;
|
||||
if (std.ascii.eqlIgnoreCase(s, "marker")) return .marker;
|
||||
if (std.ascii.eqlIgnoreCase(s, "placeholder")) return .placeholder;
|
||||
if (std.ascii.eqlIgnoreCase(s, "selection")) return .selection;
|
||||
if (std.ascii.eqlIgnoreCase(s, "spelling-error")) return .spelling_error;
|
||||
return Error.InvalidPseudoClass;
|
||||
}
|
||||
};
|
||||
|
||||
fn asUint(comptime T: type, comptime string: []const u8) T {
|
||||
return @bitCast(string[0..string.len].*);
|
||||
}
|
||||
|
||||
pub const Selector = union(enum) {
|
||||
pub const Error = error{
|
||||
UnknownCombinedCombinator,
|
||||
@@ -306,8 +246,8 @@ pub const Selector = union(enum) {
|
||||
}
|
||||
|
||||
// match returns true if the node matches the selector query.
|
||||
pub fn match(s: *const Selector, n: anytype) !bool {
|
||||
return switch (s.*) {
|
||||
pub fn match(s: Selector, n: anytype) !bool {
|
||||
return switch (s) {
|
||||
.tag => |v| n.isElement() and std.ascii.eqlIgnoreCase(v, try n.tag()),
|
||||
.id => |v| return n.isElement() and std.mem.eql(u8, v, try n.attr("id") orelse return false),
|
||||
.class => |v| return n.isElement() and word(try n.attr("class") orelse return false, v, false),
|
||||
@@ -569,8 +509,6 @@ pub const Selector = union(enum) {
|
||||
// TODO implement using the url fragment.
|
||||
// see https://developer.mozilla.org/en-US/docs/Web/CSS/:target
|
||||
.target => return false,
|
||||
// visible always returns true.
|
||||
.visible => return true,
|
||||
|
||||
// all others pseudo class are handled by specialized
|
||||
// pseudo_class_X selectors.
|
||||
@@ -794,8 +732,8 @@ pub const Selector = union(enum) {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn deinit(sel: *const Selector, alloc: std.mem.Allocator) void {
|
||||
switch (sel.*) {
|
||||
pub fn deinit(sel: Selector, alloc: std.mem.Allocator) void {
|
||||
switch (sel) {
|
||||
.group => |v| {
|
||||
for (v) |vv| vv.deinit(alloc);
|
||||
alloc.free(v);
|
||||
|
||||
@@ -1,289 +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");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const CSSConstants = struct {
|
||||
const IMPORTANT = "!important";
|
||||
const URL_PREFIX = "url(";
|
||||
};
|
||||
|
||||
const CSSParserState = enum {
|
||||
seek_name,
|
||||
in_name,
|
||||
seek_colon,
|
||||
seek_value,
|
||||
in_value,
|
||||
in_quoted_value,
|
||||
in_single_quoted_value,
|
||||
in_url,
|
||||
in_important,
|
||||
};
|
||||
|
||||
const CSSDeclaration = struct {
|
||||
name: []const u8,
|
||||
value: []const u8,
|
||||
is_important: bool,
|
||||
};
|
||||
|
||||
const CSSParser = @This();
|
||||
state: CSSParserState,
|
||||
name_start: usize,
|
||||
name_end: usize,
|
||||
value_start: usize,
|
||||
position: usize,
|
||||
paren_depth: usize,
|
||||
escape_next: bool,
|
||||
|
||||
pub fn init() CSSParser {
|
||||
return .{
|
||||
.state = .seek_name,
|
||||
.name_start = 0,
|
||||
.name_end = 0,
|
||||
.value_start = 0,
|
||||
.position = 0,
|
||||
.paren_depth = 0,
|
||||
.escape_next = false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn parseDeclarations(arena: Allocator, text: []const u8) ![]CSSDeclaration {
|
||||
var parser = init();
|
||||
var declarations: std.ArrayListUnmanaged(CSSDeclaration) = .empty;
|
||||
|
||||
while (parser.position < text.len) {
|
||||
const c = text[parser.position];
|
||||
|
||||
switch (parser.state) {
|
||||
.seek_name => {
|
||||
if (!std.ascii.isWhitespace(c)) {
|
||||
parser.name_start = parser.position;
|
||||
parser.state = .in_name;
|
||||
continue;
|
||||
}
|
||||
},
|
||||
.in_name => {
|
||||
if (c == ':') {
|
||||
parser.name_end = parser.position;
|
||||
parser.state = .seek_value;
|
||||
} else if (std.ascii.isWhitespace(c)) {
|
||||
parser.name_end = parser.position;
|
||||
parser.state = .seek_colon;
|
||||
}
|
||||
},
|
||||
.seek_colon => {
|
||||
if (c == ':') {
|
||||
parser.state = .seek_value;
|
||||
} else if (!std.ascii.isWhitespace(c)) {
|
||||
parser.state = .seek_name;
|
||||
continue;
|
||||
}
|
||||
},
|
||||
.seek_value => {
|
||||
if (!std.ascii.isWhitespace(c)) {
|
||||
parser.value_start = parser.position;
|
||||
if (c == '"') {
|
||||
parser.state = .in_quoted_value;
|
||||
} else if (c == '\'') {
|
||||
parser.state = .in_single_quoted_value;
|
||||
} else if (c == 'u' and parser.position + CSSConstants.URL_PREFIX.len <= text.len and std.mem.startsWith(u8, text[parser.position..], CSSConstants.URL_PREFIX)) {
|
||||
parser.state = .in_url;
|
||||
parser.paren_depth = 1;
|
||||
parser.position += 3;
|
||||
} else {
|
||||
parser.state = .in_value;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
},
|
||||
.in_value => {
|
||||
if (parser.escape_next) {
|
||||
parser.escape_next = false;
|
||||
} else if (c == '\\') {
|
||||
parser.escape_next = true;
|
||||
} else if (c == '(') {
|
||||
parser.paren_depth += 1;
|
||||
} else if (c == ')' and parser.paren_depth > 0) {
|
||||
parser.paren_depth -= 1;
|
||||
} else if (c == ';' and parser.paren_depth == 0) {
|
||||
try parser.finishDeclaration(arena, &declarations, text);
|
||||
parser.state = .seek_name;
|
||||
}
|
||||
},
|
||||
.in_quoted_value => {
|
||||
if (parser.escape_next) {
|
||||
parser.escape_next = false;
|
||||
} else if (c == '\\') {
|
||||
parser.escape_next = true;
|
||||
} else if (c == '"') {
|
||||
parser.state = .in_value;
|
||||
}
|
||||
},
|
||||
.in_single_quoted_value => {
|
||||
if (parser.escape_next) {
|
||||
parser.escape_next = false;
|
||||
} else if (c == '\\') {
|
||||
parser.escape_next = true;
|
||||
} else if (c == '\'') {
|
||||
parser.state = .in_value;
|
||||
}
|
||||
},
|
||||
.in_url => {
|
||||
if (parser.escape_next) {
|
||||
parser.escape_next = false;
|
||||
} else if (c == '\\') {
|
||||
parser.escape_next = true;
|
||||
} else if (c == '(') {
|
||||
parser.paren_depth += 1;
|
||||
} else if (c == ')') {
|
||||
parser.paren_depth -= 1;
|
||||
if (parser.paren_depth == 0) {
|
||||
parser.state = .in_value;
|
||||
}
|
||||
}
|
||||
},
|
||||
.in_important => {},
|
||||
}
|
||||
|
||||
parser.position += 1;
|
||||
}
|
||||
|
||||
try parser.finalize(arena, &declarations, text);
|
||||
|
||||
return declarations.items;
|
||||
}
|
||||
|
||||
fn finishDeclaration(self: *CSSParser, arena: Allocator, declarations: *std.ArrayListUnmanaged(CSSDeclaration), text: []const u8) !void {
|
||||
const name = std.mem.trim(u8, text[self.name_start..self.name_end], &std.ascii.whitespace);
|
||||
if (name.len == 0) return;
|
||||
|
||||
const raw_value = text[self.value_start..self.position];
|
||||
const value = std.mem.trim(u8, raw_value, &std.ascii.whitespace);
|
||||
|
||||
var final_value = value;
|
||||
var is_important = false;
|
||||
|
||||
if (std.mem.endsWith(u8, value, CSSConstants.IMPORTANT)) {
|
||||
is_important = true;
|
||||
final_value = std.mem.trimRight(u8, value[0 .. value.len - CSSConstants.IMPORTANT.len], &std.ascii.whitespace);
|
||||
}
|
||||
|
||||
try declarations.append(arena, .{
|
||||
.name = name,
|
||||
.value = final_value,
|
||||
.is_important = is_important,
|
||||
});
|
||||
}
|
||||
|
||||
fn finalize(self: *CSSParser, arena: Allocator, declarations: *std.ArrayListUnmanaged(CSSDeclaration), text: []const u8) !void {
|
||||
if (self.state != .in_value) {
|
||||
return;
|
||||
}
|
||||
return self.finishDeclaration(arena, declarations, text);
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: CSS.Parser - Simple property" {
|
||||
defer testing.reset();
|
||||
|
||||
const text = "color: red;";
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
||||
|
||||
try testing.expectEqual(1, declarations.len);
|
||||
try testing.expectEqual("color", declarations[0].name);
|
||||
try testing.expectEqual("red", declarations[0].value);
|
||||
try testing.expectEqual(false, declarations[0].is_important);
|
||||
}
|
||||
|
||||
test "Browser: CSS.Parser - Property with !important" {
|
||||
defer testing.reset();
|
||||
const text = "margin: 10px !important;";
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
||||
|
||||
try testing.expectEqual(1, declarations.len);
|
||||
try testing.expectEqual("margin", declarations[0].name);
|
||||
try testing.expectEqual("10px", declarations[0].value);
|
||||
try testing.expectEqual(true, declarations[0].is_important);
|
||||
}
|
||||
|
||||
test "Browser: CSS.Parser - Multiple properties" {
|
||||
defer testing.reset();
|
||||
const text = "color: red; font-size: 12px; margin: 5px !important;";
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
||||
|
||||
try testing.expect(declarations.len == 3);
|
||||
|
||||
try testing.expectEqual("color", declarations[0].name);
|
||||
try testing.expectEqual("red", declarations[0].value);
|
||||
try testing.expectEqual(false, declarations[0].is_important);
|
||||
|
||||
try testing.expectEqual("font-size", declarations[1].name);
|
||||
try testing.expectEqual("12px", declarations[1].value);
|
||||
try testing.expectEqual(false, declarations[1].is_important);
|
||||
|
||||
try testing.expectEqual("margin", declarations[2].name);
|
||||
try testing.expectEqual("5px", declarations[2].value);
|
||||
try testing.expectEqual(true, declarations[2].is_important);
|
||||
}
|
||||
|
||||
test "Browser: CSS.Parser - Quoted value with semicolon" {
|
||||
defer testing.reset();
|
||||
const text = "content: \"Hello; world!\";";
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
||||
|
||||
try testing.expectEqual(1, declarations.len);
|
||||
try testing.expectEqual("content", declarations[0].name);
|
||||
try testing.expectEqual("\"Hello; world!\"", declarations[0].value);
|
||||
try testing.expectEqual(false, declarations[0].is_important);
|
||||
}
|
||||
|
||||
test "Browser: CSS.Parser - URL value" {
|
||||
defer testing.reset();
|
||||
const text = "background-image: url(\"test.png\");";
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
||||
|
||||
try testing.expectEqual(1, declarations.len);
|
||||
try testing.expectEqual("background-image", declarations[0].name);
|
||||
try testing.expectEqual("url(\"test.png\")", declarations[0].value);
|
||||
try testing.expectEqual(false, declarations[0].is_important);
|
||||
}
|
||||
|
||||
test "Browser: CSS.Parser - Whitespace handling" {
|
||||
defer testing.reset();
|
||||
const text = " color : purple ; margin : 10px ; ";
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
||||
|
||||
try testing.expectEqual(2, declarations.len);
|
||||
try testing.expectEqual("color", declarations[0].name);
|
||||
try testing.expectEqual("purple", declarations[0].value);
|
||||
try testing.expectEqual("margin", declarations[1].name);
|
||||
try testing.expectEqual("10px", declarations[1].value);
|
||||
}
|
||||
@@ -1,41 +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");
|
||||
|
||||
const CSSStyleSheet = @import("CSSStyleSheet.zig");
|
||||
|
||||
pub const Interfaces = .{
|
||||
CSSRule,
|
||||
CSSImportRule,
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CSSRule
|
||||
const CSSRule = @This();
|
||||
css_text: []const u8,
|
||||
parent_rule: ?*CSSRule = null,
|
||||
parent_stylesheet: ?*CSSStyleSheet = null,
|
||||
|
||||
pub const CSSImportRule = struct {
|
||||
pub const prototype = *CSSRule;
|
||||
href: []const u8,
|
||||
layer_name: ?[]const u8,
|
||||
media: void,
|
||||
style_sheet: CSSStyleSheet,
|
||||
supports_text: ?[]const u8,
|
||||
};
|
||||
@@ -1,52 +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");
|
||||
|
||||
const CSSRule = @import("CSSRule.zig");
|
||||
const StyleSheet = @import("StyleSheet.zig").StyleSheet;
|
||||
|
||||
const CSSImportRule = CSSRule.CSSImportRule;
|
||||
|
||||
const CSSRuleList = @This();
|
||||
list: std.ArrayListUnmanaged([]const u8),
|
||||
|
||||
pub fn constructor() CSSRuleList {
|
||||
return .{ .list = .empty };
|
||||
}
|
||||
|
||||
pub fn _item(self: *CSSRuleList, _index: u32) ?CSSRule {
|
||||
const index: usize = @intCast(_index);
|
||||
|
||||
if (index > self.list.items.len) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// todo: for now, just return null.
|
||||
// this depends on properly parsing CSSRule
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn get_length(self: *CSSRuleList) u32 {
|
||||
return @intCast(self.list.items.len);
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: CSS.CSSRuleList" {
|
||||
try testing.htmlRunner("cssom/css_rule_list.html");
|
||||
}
|
||||
@@ -1,958 +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");
|
||||
|
||||
const Page = @import("../page.zig").Page;
|
||||
const CSSRule = @import("CSSRule.zig");
|
||||
const CSSParser = @import("CSSParser.zig");
|
||||
|
||||
const Property = struct {
|
||||
value: []const u8,
|
||||
priority: bool,
|
||||
};
|
||||
|
||||
const CSSStyleDeclaration = @This();
|
||||
|
||||
properties: std.StringArrayHashMapUnmanaged(Property),
|
||||
|
||||
pub const empty: CSSStyleDeclaration = .{
|
||||
.properties = .empty,
|
||||
};
|
||||
|
||||
pub fn get_cssFloat(self: *const CSSStyleDeclaration) []const u8 {
|
||||
return self._getPropertyValue("float");
|
||||
}
|
||||
|
||||
pub fn set_cssFloat(self: *CSSStyleDeclaration, value: ?[]const u8, page: *Page) !void {
|
||||
const final_value = value orelse "";
|
||||
return self._setProperty("float", final_value, null, page);
|
||||
}
|
||||
|
||||
pub fn get_cssText(self: *const CSSStyleDeclaration, page: *Page) ![]const u8 {
|
||||
var buffer: std.ArrayListUnmanaged(u8) = .empty;
|
||||
const writer = buffer.writer(page.call_arena);
|
||||
var it = self.properties.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const name = entry.key_ptr.*;
|
||||
const property = entry.value_ptr;
|
||||
const escaped = try escapeCSSValue(page.call_arena, property.value);
|
||||
try writer.print("{s}: {s}", .{ name, escaped });
|
||||
if (property.priority) {
|
||||
try writer.writeAll(" !important; ");
|
||||
} else {
|
||||
try writer.writeAll("; ");
|
||||
}
|
||||
}
|
||||
return buffer.items;
|
||||
}
|
||||
|
||||
// TODO Propagate also upward to parent node
|
||||
pub fn set_cssText(self: *CSSStyleDeclaration, text: []const u8, page: *Page) !void {
|
||||
self.properties.clearRetainingCapacity();
|
||||
|
||||
// call_arena is safe here, because _setProperty will dupe the name
|
||||
// using the page's longer-living arena.
|
||||
const declarations = try CSSParser.parseDeclarations(page.call_arena, text);
|
||||
|
||||
for (declarations) |decl| {
|
||||
if (!isValidPropertyName(decl.name)) {
|
||||
continue;
|
||||
}
|
||||
const priority: ?[]const u8 = if (decl.is_important) "important" else null;
|
||||
try self._setProperty(decl.name, decl.value, priority, page);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_length(self: *const CSSStyleDeclaration) usize {
|
||||
return self.properties.count();
|
||||
}
|
||||
|
||||
pub fn get_parentRule(_: *const CSSStyleDeclaration) ?CSSRule {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn _getPropertyPriority(self: *const CSSStyleDeclaration, name: []const u8) []const u8 {
|
||||
const property = self.properties.getPtr(name) orelse return "";
|
||||
return if (property.priority) "important" else "";
|
||||
}
|
||||
|
||||
// TODO should handle properly shorthand properties and canonical forms
|
||||
pub fn _getPropertyValue(self: *const CSSStyleDeclaration, name: []const u8) []const u8 {
|
||||
if (self.properties.getPtr(name)) |property| {
|
||||
return property.value;
|
||||
}
|
||||
|
||||
// default to everything being visible (unless it's been explicitly set)
|
||||
if (std.mem.eql(u8, name, "visibility")) {
|
||||
return "visible";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn _item(self: *const CSSStyleDeclaration, index: usize) []const u8 {
|
||||
const values = self.properties.entries.items(.key);
|
||||
if (index >= values.len) {
|
||||
return "";
|
||||
}
|
||||
return values[index];
|
||||
}
|
||||
|
||||
pub fn _removeProperty(self: *CSSStyleDeclaration, name: []const u8) ![]const u8 {
|
||||
const property = self.properties.fetchOrderedRemove(name) orelse return "";
|
||||
return property.value.value;
|
||||
}
|
||||
|
||||
pub fn _setProperty(self: *CSSStyleDeclaration, name: []const u8, value: []const u8, priority: ?[]const u8, page: *Page) !void {
|
||||
const gop = try self.properties.getOrPut(page.arena, name);
|
||||
if (!gop.found_existing) {
|
||||
const owned_name = try page.arena.dupe(u8, name);
|
||||
gop.key_ptr.* = owned_name;
|
||||
}
|
||||
|
||||
const owned_value = try page.arena.dupe(u8, value);
|
||||
const is_important = priority != null and std.ascii.eqlIgnoreCase(priority.?, "important");
|
||||
gop.value_ptr.* = .{ .value = owned_value, .priority = is_important };
|
||||
}
|
||||
|
||||
pub fn named_get(self: *const CSSStyleDeclaration, name: []const u8, _: *bool) []const u8 {
|
||||
return self._getPropertyValue(name);
|
||||
}
|
||||
|
||||
pub fn named_set(self: *CSSStyleDeclaration, name: []const u8, value: []const u8, _: *bool, page: *Page) !void {
|
||||
return self._setProperty(name, value, null, page);
|
||||
}
|
||||
|
||||
fn isNumericWithUnit(value: []const u8) bool {
|
||||
if (value.len == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const first = value[0];
|
||||
|
||||
if (!std.ascii.isDigit(first) and first != '+' and first != '-' and first != '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
var i: usize = 0;
|
||||
var has_digit = false;
|
||||
var decimal_point = false;
|
||||
|
||||
while (i < value.len) : (i += 1) {
|
||||
const c = value[i];
|
||||
if (std.ascii.isDigit(c)) {
|
||||
has_digit = true;
|
||||
} else if (c == '.' and !decimal_point) {
|
||||
decimal_point = true;
|
||||
} else if ((c == 'e' or c == 'E') and has_digit) {
|
||||
if (i + 1 >= value.len) return false;
|
||||
if (value[i + 1] != '+' and value[i + 1] != '-' and !std.ascii.isDigit(value[i + 1])) break;
|
||||
i += 1;
|
||||
if (value[i] == '+' or value[i] == '-') {
|
||||
i += 1;
|
||||
}
|
||||
var has_exp_digits = false;
|
||||
while (i < value.len and std.ascii.isDigit(value[i])) : (i += 1) {
|
||||
has_exp_digits = true;
|
||||
}
|
||||
if (!has_exp_digits) return false;
|
||||
break;
|
||||
} else if (c != '-' and c != '+') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_digit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (i == value.len) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const unit = value[i..];
|
||||
return CSSKeywords.isValidUnit(unit);
|
||||
}
|
||||
|
||||
fn isHexColor(value: []const u8) bool {
|
||||
if (value.len == 0) {
|
||||
return false;
|
||||
}
|
||||
if (value[0] != '#') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hex_part = value[1..];
|
||||
if (hex_part.len != 3 and hex_part.len != 6 and hex_part.len != 8) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (hex_part) |c| {
|
||||
if (!std.ascii.isHex(c)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fn isMultiValueProperty(value: []const u8) bool {
|
||||
var parts = std.mem.splitAny(u8, value, " ");
|
||||
var multi_value_parts: usize = 0;
|
||||
var all_parts_valid = true;
|
||||
|
||||
while (parts.next()) |part| {
|
||||
if (part.len == 0) continue;
|
||||
multi_value_parts += 1;
|
||||
|
||||
if (isNumericWithUnit(part)) {
|
||||
continue;
|
||||
}
|
||||
if (isHexColor(part)) {
|
||||
continue;
|
||||
}
|
||||
if (CSSKeywords.isKnownKeyword(part)) {
|
||||
continue;
|
||||
}
|
||||
if (CSSKeywords.startsWithFunction(part)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
all_parts_valid = false;
|
||||
break;
|
||||
}
|
||||
|
||||
return multi_value_parts >= 2 and all_parts_valid;
|
||||
}
|
||||
|
||||
fn isAlreadyQuoted(value: []const u8) bool {
|
||||
return value.len >= 2 and ((value[0] == '"' and value[value.len - 1] == '"') or
|
||||
(value[0] == '\'' and value[value.len - 1] == '\''));
|
||||
}
|
||||
|
||||
fn isValidPropertyName(name: []const u8) bool {
|
||||
if (name.len == 0) return false;
|
||||
|
||||
if (std.mem.startsWith(u8, name, "--")) {
|
||||
if (name.len == 2) return false;
|
||||
for (name[2..]) |c| {
|
||||
if (!std.ascii.isAlphanumeric(c) and c != '-' and c != '_') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const first_char = name[0];
|
||||
if (!std.ascii.isAlphabetic(first_char) and first_char != '-') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (first_char == '-') {
|
||||
if (name.len < 2) return false;
|
||||
|
||||
if (!std.ascii.isAlphabetic(name[1])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (name[2..]) |c| {
|
||||
if (!std.ascii.isAlphanumeric(c) and c != '-') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (name[1..]) |c| {
|
||||
if (!std.ascii.isAlphanumeric(c) and c != '-') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fn extractImportant(value: []const u8) struct { value: []const u8, is_important: bool } {
|
||||
const trimmed = std.mem.trim(u8, value, &std.ascii.whitespace);
|
||||
|
||||
if (std.mem.endsWith(u8, trimmed, "!important")) {
|
||||
const clean_value = std.mem.trimRight(u8, trimmed[0 .. trimmed.len - 10], &std.ascii.whitespace);
|
||||
return .{ .value = clean_value, .is_important = true };
|
||||
}
|
||||
|
||||
return .{ .value = trimmed, .is_important = false };
|
||||
}
|
||||
|
||||
fn needsQuotes(value: []const u8) bool {
|
||||
if (value.len == 0) return true;
|
||||
if (isAlreadyQuoted(value)) return false;
|
||||
|
||||
if (CSSKeywords.containsSpecialChar(value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.indexOfScalar(u8, value, ' ') == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const is_url = std.mem.startsWith(u8, value, "url(");
|
||||
const is_function = CSSKeywords.startsWithFunction(value);
|
||||
|
||||
return !isMultiValueProperty(value) and
|
||||
!is_url and
|
||||
!is_function;
|
||||
}
|
||||
|
||||
fn escapeCSSValue(arena: std.mem.Allocator, value: []const u8) ![]const u8 {
|
||||
if (!needsQuotes(value)) {
|
||||
return value;
|
||||
}
|
||||
var out: std.ArrayListUnmanaged(u8) = .empty;
|
||||
|
||||
// We'll need at least this much space, +2 for the quotes
|
||||
try out.ensureTotalCapacity(arena, value.len + 2);
|
||||
const writer = out.writer(arena);
|
||||
|
||||
try writer.writeByte('"');
|
||||
|
||||
for (value, 0..) |c, i| {
|
||||
switch (c) {
|
||||
'"' => try writer.writeAll("\\\""),
|
||||
'\\' => try writer.writeAll("\\\\"),
|
||||
'\n' => try writer.writeAll("\\A "),
|
||||
'\r' => try writer.writeAll("\\D "),
|
||||
'\t' => try writer.writeAll("\\9 "),
|
||||
0...8, 11, 12, 14...31, 127 => {
|
||||
try writer.print("\\{x}", .{c});
|
||||
if (i + 1 < value.len and std.ascii.isHex(value[i + 1])) {
|
||||
try writer.writeByte(' ');
|
||||
}
|
||||
},
|
||||
else => try writer.writeByte(c),
|
||||
}
|
||||
}
|
||||
|
||||
try writer.writeByte('"');
|
||||
return out.items;
|
||||
}
|
||||
|
||||
fn isKnownKeyword(value: []const u8) bool {
|
||||
return CSSKeywords.isKnownKeyword(value);
|
||||
}
|
||||
|
||||
fn containsSpecialChar(value: []const u8) bool {
|
||||
return CSSKeywords.containsSpecialChar(value);
|
||||
}
|
||||
|
||||
const CSSKeywords = struct {
|
||||
const BORDER_STYLES = [_][]const u8{
|
||||
"none", "solid", "dotted", "dashed", "double", "groove", "ridge", "inset", "outset",
|
||||
};
|
||||
|
||||
const COLOR_NAMES = [_][]const u8{
|
||||
"black", "white", "red", "green", "blue", "yellow", "purple", "gray", "transparent",
|
||||
"currentColor", "inherit",
|
||||
};
|
||||
|
||||
const POSITION_KEYWORDS = [_][]const u8{
|
||||
"auto", "center", "left", "right", "top", "bottom",
|
||||
};
|
||||
|
||||
const BACKGROUND_REPEAT = [_][]const u8{
|
||||
"repeat", "no-repeat", "repeat-x", "repeat-y", "space", "round",
|
||||
};
|
||||
|
||||
const FONT_STYLES = [_][]const u8{
|
||||
"normal", "italic", "oblique", "bold", "bolder", "lighter",
|
||||
};
|
||||
|
||||
const FONT_SIZES = [_][]const u8{
|
||||
"xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large",
|
||||
"smaller", "larger",
|
||||
};
|
||||
|
||||
const FONT_FAMILIES = [_][]const u8{
|
||||
"serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui",
|
||||
};
|
||||
|
||||
const CSS_GLOBAL = [_][]const u8{
|
||||
"initial", "inherit", "unset", "revert",
|
||||
};
|
||||
|
||||
const DISPLAY_VALUES = [_][]const u8{
|
||||
"block", "inline", "inline-block", "flex", "grid", "none",
|
||||
};
|
||||
|
||||
const UNITS = [_][]const u8{
|
||||
// LENGTH
|
||||
"px", "em", "rem", "vw", "vh", "vmin", "vmax", "%", "pt", "pc", "in", "cm", "mm",
|
||||
"ex", "ch", "fr",
|
||||
|
||||
// ANGLE
|
||||
"deg", "rad", "grad", "turn",
|
||||
|
||||
// TIME
|
||||
"s", "ms",
|
||||
|
||||
// FREQUENCY
|
||||
"hz", "khz",
|
||||
|
||||
// RESOLUTION
|
||||
"dpi", "dpcm",
|
||||
"dppx",
|
||||
};
|
||||
|
||||
const SPECIAL_CHARS = [_]u8{
|
||||
'"', '\'', ';', '{', '}', '\\', '<', '>', '/', '\n', '\t', '\r', '\x00', '\x7F',
|
||||
};
|
||||
|
||||
const FUNCTIONS = [_][]const u8{
|
||||
"rgb(", "rgba(", "hsl(", "hsla(", "url(", "calc(", "var(", "attr(",
|
||||
"linear-gradient(", "radial-gradient(", "conic-gradient(", "translate(", "rotate(", "scale(", "skew(", "matrix(",
|
||||
};
|
||||
|
||||
const KEYWORDS = BORDER_STYLES ++ COLOR_NAMES ++ POSITION_KEYWORDS ++
|
||||
BACKGROUND_REPEAT ++ FONT_STYLES ++ FONT_SIZES ++ FONT_FAMILIES ++
|
||||
CSS_GLOBAL ++ DISPLAY_VALUES;
|
||||
|
||||
const MAX_KEYWORD_LEN = lengthOfLongestValue(&KEYWORDS);
|
||||
|
||||
pub fn isKnownKeyword(value: []const u8) bool {
|
||||
if (value.len > MAX_KEYWORD_LEN) {
|
||||
return false;
|
||||
}
|
||||
var buf: [MAX_KEYWORD_LEN]u8 = undefined;
|
||||
const normalized = std.ascii.lowerString(&buf, value);
|
||||
|
||||
for (KEYWORDS) |keyword| {
|
||||
if (std.ascii.eqlIgnoreCase(normalized, keyword)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn containsSpecialChar(value: []const u8) bool {
|
||||
return std.mem.indexOfAny(u8, value, &SPECIAL_CHARS) != null;
|
||||
}
|
||||
|
||||
const MAX_UNIT_LEN = lengthOfLongestValue(&UNITS);
|
||||
|
||||
pub fn isValidUnit(unit: []const u8) bool {
|
||||
if (unit.len > MAX_UNIT_LEN) {
|
||||
return false;
|
||||
}
|
||||
var buf: [MAX_UNIT_LEN]u8 = undefined;
|
||||
const normalized = std.ascii.lowerString(&buf, unit);
|
||||
|
||||
for (UNITS) |u| {
|
||||
if (std.mem.eql(u8, normalized, u)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn startsWithFunction(value: []const u8) bool {
|
||||
const pos = std.mem.indexOfScalar(u8, value, '(') orelse return false;
|
||||
if (pos == 0) return false;
|
||||
|
||||
if (std.mem.indexOfScalarPos(u8, value, pos, ')') == null) {
|
||||
return false;
|
||||
}
|
||||
const function_name = value[0..pos];
|
||||
return isValidFunctionName(function_name);
|
||||
}
|
||||
|
||||
fn isValidFunctionName(name: []const u8) bool {
|
||||
if (name.len == 0) return false;
|
||||
|
||||
const first = name[0];
|
||||
if (!std.ascii.isAlphabetic(first) and first != '_' and first != '-') {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (name[1..]) |c| {
|
||||
if (!std.ascii.isAlphanumeric(c) and c != '_' and c != '-') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
fn lengthOfLongestValue(values: []const []const u8) usize {
|
||||
var max: usize = 0;
|
||||
for (values) |v| {
|
||||
max = @max(v.len, max);
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: CSS.StyleDeclaration" {
|
||||
try testing.htmlRunner("cssom/css_style_declaration.html");
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isNumericWithUnit - valid numbers with units" {
|
||||
try testing.expect(isNumericWithUnit("10px"));
|
||||
try testing.expect(isNumericWithUnit("3.14em"));
|
||||
try testing.expect(isNumericWithUnit("-5rem"));
|
||||
try testing.expect(isNumericWithUnit("+12.5%"));
|
||||
try testing.expect(isNumericWithUnit("0vh"));
|
||||
try testing.expect(isNumericWithUnit(".5vw"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isNumericWithUnit - scientific notation" {
|
||||
try testing.expect(isNumericWithUnit("1e5px"));
|
||||
try testing.expect(isNumericWithUnit("2.5E-3em"));
|
||||
try testing.expect(isNumericWithUnit("1e+2rem"));
|
||||
try testing.expect(isNumericWithUnit("-3.14e10px"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isNumericWithUnit - edge cases and invalid inputs" {
|
||||
try testing.expect(!isNumericWithUnit(""));
|
||||
|
||||
try testing.expect(!isNumericWithUnit("px"));
|
||||
try testing.expect(!isNumericWithUnit("--px"));
|
||||
try testing.expect(!isNumericWithUnit(".px"));
|
||||
|
||||
try testing.expect(!isNumericWithUnit("1e"));
|
||||
try testing.expect(!isNumericWithUnit("1epx"));
|
||||
try testing.expect(!isNumericWithUnit("1e+"));
|
||||
try testing.expect(!isNumericWithUnit("1e+px"));
|
||||
|
||||
try testing.expect(!isNumericWithUnit("1.2.3px"));
|
||||
|
||||
try testing.expect(!isNumericWithUnit("10xyz"));
|
||||
try testing.expect(!isNumericWithUnit("5invalid"));
|
||||
|
||||
try testing.expect(isNumericWithUnit("10"));
|
||||
try testing.expect(isNumericWithUnit("3.14"));
|
||||
try testing.expect(isNumericWithUnit("-5"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isHexColor - valid hex colors" {
|
||||
try testing.expect(isHexColor("#000"));
|
||||
try testing.expect(isHexColor("#fff"));
|
||||
try testing.expect(isHexColor("#123456"));
|
||||
try testing.expect(isHexColor("#abcdef"));
|
||||
try testing.expect(isHexColor("#ABCDEF"));
|
||||
try testing.expect(isHexColor("#12345678"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isHexColor - invalid hex colors" {
|
||||
try testing.expect(!isHexColor(""));
|
||||
try testing.expect(!isHexColor("#"));
|
||||
try testing.expect(!isHexColor("000"));
|
||||
try testing.expect(!isHexColor("#00"));
|
||||
try testing.expect(!isHexColor("#0000"));
|
||||
try testing.expect(!isHexColor("#00000"));
|
||||
try testing.expect(!isHexColor("#0000000"));
|
||||
try testing.expect(!isHexColor("#000000000"));
|
||||
try testing.expect(!isHexColor("#gggggg"));
|
||||
try testing.expect(!isHexColor("#123xyz"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isMultiValueProperty - valid multi-value properties" {
|
||||
try testing.expect(isMultiValueProperty("10px 20px"));
|
||||
try testing.expect(isMultiValueProperty("solid red"));
|
||||
try testing.expect(isMultiValueProperty("#fff black"));
|
||||
try testing.expect(isMultiValueProperty("1em 2em 3em 4em"));
|
||||
try testing.expect(isMultiValueProperty("rgb(255,0,0) solid"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isMultiValueProperty - invalid multi-value properties" {
|
||||
try testing.expect(!isMultiValueProperty(""));
|
||||
try testing.expect(!isMultiValueProperty("10px"));
|
||||
try testing.expect(!isMultiValueProperty("invalid unknown"));
|
||||
try testing.expect(!isMultiValueProperty("10px invalid"));
|
||||
try testing.expect(!isMultiValueProperty(" "));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isAlreadyQuoted - various quoting scenarios" {
|
||||
try testing.expect(isAlreadyQuoted("\"hello\""));
|
||||
try testing.expect(isAlreadyQuoted("'world'"));
|
||||
try testing.expect(isAlreadyQuoted("\"\""));
|
||||
try testing.expect(isAlreadyQuoted("''"));
|
||||
|
||||
try testing.expect(!isAlreadyQuoted(""));
|
||||
try testing.expect(!isAlreadyQuoted("hello"));
|
||||
try testing.expect(!isAlreadyQuoted("\""));
|
||||
try testing.expect(!isAlreadyQuoted("'"));
|
||||
try testing.expect(!isAlreadyQuoted("\"hello'"));
|
||||
try testing.expect(!isAlreadyQuoted("'hello\""));
|
||||
try testing.expect(!isAlreadyQuoted("\"hello"));
|
||||
try testing.expect(!isAlreadyQuoted("hello\""));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isValidPropertyName - valid property names" {
|
||||
try testing.expect(isValidPropertyName("color"));
|
||||
try testing.expect(isValidPropertyName("background-color"));
|
||||
try testing.expect(isValidPropertyName("-webkit-transform"));
|
||||
try testing.expect(isValidPropertyName("font-size"));
|
||||
try testing.expect(isValidPropertyName("margin-top"));
|
||||
try testing.expect(isValidPropertyName("z-index"));
|
||||
try testing.expect(isValidPropertyName("line-height"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isValidPropertyName - invalid property names" {
|
||||
try testing.expect(!isValidPropertyName(""));
|
||||
try testing.expect(!isValidPropertyName("123color"));
|
||||
try testing.expect(!isValidPropertyName("color!"));
|
||||
try testing.expect(!isValidPropertyName("color space"));
|
||||
try testing.expect(!isValidPropertyName("@color"));
|
||||
try testing.expect(!isValidPropertyName("color.test"));
|
||||
try testing.expect(!isValidPropertyName("color_test"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: extractImportant - with and without !important" {
|
||||
var result = extractImportant("red !important");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("red", result.value);
|
||||
|
||||
result = extractImportant("blue");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("blue", result.value);
|
||||
|
||||
result = extractImportant(" green !important ");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("green", result.value);
|
||||
|
||||
result = extractImportant("!important");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("", result.value);
|
||||
|
||||
result = extractImportant("important");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("important", result.value);
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: needsQuotes - various scenarios" {
|
||||
try testing.expect(needsQuotes(""));
|
||||
try testing.expect(needsQuotes("hello world"));
|
||||
try testing.expect(needsQuotes("test;"));
|
||||
try testing.expect(needsQuotes("a{b}"));
|
||||
try testing.expect(needsQuotes("test\"quote"));
|
||||
|
||||
try testing.expect(!needsQuotes("\"already quoted\""));
|
||||
try testing.expect(!needsQuotes("'already quoted'"));
|
||||
try testing.expect(!needsQuotes("url(image.png)"));
|
||||
try testing.expect(!needsQuotes("rgb(255, 0, 0)"));
|
||||
try testing.expect(!needsQuotes("10px 20px"));
|
||||
try testing.expect(!needsQuotes("simple"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: escapeCSSValue - escaping various characters" {
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
var result = try escapeCSSValue(allocator, "simple");
|
||||
try testing.expectEqual("simple", result);
|
||||
|
||||
result = try escapeCSSValue(allocator, "\"already quoted\"");
|
||||
try testing.expectEqual("\"already quoted\"", result);
|
||||
|
||||
result = try escapeCSSValue(allocator, "test\"quote");
|
||||
try testing.expectEqual("\"test\\\"quote\"", result);
|
||||
|
||||
result = try escapeCSSValue(allocator, "test\nline");
|
||||
try testing.expectEqual("\"test\\A line\"", result);
|
||||
|
||||
result = try escapeCSSValue(allocator, "test\\back");
|
||||
try testing.expectEqual("\"test\\\\back\"", result);
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: CSSKeywords.isKnownKeyword - case sensitivity" {
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("red"));
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("solid"));
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("center"));
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("inherit"));
|
||||
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("RED"));
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("Red"));
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("SOLID"));
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("Center"));
|
||||
|
||||
try testing.expect(!CSSKeywords.isKnownKeyword("invalid"));
|
||||
try testing.expect(!CSSKeywords.isKnownKeyword("unknown"));
|
||||
try testing.expect(!CSSKeywords.isKnownKeyword(""));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: CSSKeywords.containsSpecialChar - various special characters" {
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test\"quote"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test'quote"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test;end"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test{brace"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test}brace"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test\\back"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test<angle"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test>angle"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test/slash"));
|
||||
|
||||
try testing.expect(!CSSKeywords.containsSpecialChar("normal-text"));
|
||||
try testing.expect(!CSSKeywords.containsSpecialChar("text123"));
|
||||
try testing.expect(!CSSKeywords.containsSpecialChar(""));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: CSSKeywords.isValidUnit - various units" {
|
||||
try testing.expect(CSSKeywords.isValidUnit("px"));
|
||||
try testing.expect(CSSKeywords.isValidUnit("em"));
|
||||
try testing.expect(CSSKeywords.isValidUnit("rem"));
|
||||
try testing.expect(CSSKeywords.isValidUnit("%"));
|
||||
|
||||
try testing.expect(CSSKeywords.isValidUnit("deg"));
|
||||
try testing.expect(CSSKeywords.isValidUnit("rad"));
|
||||
|
||||
try testing.expect(CSSKeywords.isValidUnit("s"));
|
||||
try testing.expect(CSSKeywords.isValidUnit("ms"));
|
||||
|
||||
try testing.expect(CSSKeywords.isValidUnit("PX"));
|
||||
|
||||
try testing.expect(!CSSKeywords.isValidUnit("invalid"));
|
||||
try testing.expect(!CSSKeywords.isValidUnit(""));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: CSSKeywords.startsWithFunction - function detection" {
|
||||
try testing.expect(CSSKeywords.startsWithFunction("rgb(255, 0, 0)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("rgba(255, 0, 0, 0.5)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("url(image.png)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("calc(100% - 20px)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("var(--custom-property)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("linear-gradient(to right, red, blue)"));
|
||||
|
||||
try testing.expect(CSSKeywords.startsWithFunction("custom-function(args)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("unknown(test)"));
|
||||
|
||||
try testing.expect(!CSSKeywords.startsWithFunction("not-a-function"));
|
||||
try testing.expect(!CSSKeywords.startsWithFunction("missing-paren)"));
|
||||
try testing.expect(!CSSKeywords.startsWithFunction("missing-close("));
|
||||
try testing.expect(!CSSKeywords.startsWithFunction(""));
|
||||
try testing.expect(!CSSKeywords.startsWithFunction("rgb"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isNumericWithUnit - whitespace handling" {
|
||||
try testing.expect(!isNumericWithUnit(" 10px"));
|
||||
try testing.expect(!isNumericWithUnit("10 px"));
|
||||
try testing.expect(!isNumericWithUnit("10px "));
|
||||
try testing.expect(!isNumericWithUnit(" 10 px "));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: extractImportant - whitespace edge cases" {
|
||||
var result = extractImportant(" ");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("", result.value);
|
||||
|
||||
result = extractImportant("\t\n\r !important\t\n");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("", result.value);
|
||||
|
||||
result = extractImportant("red\t!important");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("red", result.value);
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isHexColor - mixed case handling" {
|
||||
try testing.expect(isHexColor("#AbC"));
|
||||
try testing.expect(isHexColor("#123aBc"));
|
||||
try testing.expect(isHexColor("#FFffFF"));
|
||||
try testing.expect(isHexColor("#000FFF"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: edge case - very long inputs" {
|
||||
const long_valid = "a" ** 1000 ++ "px";
|
||||
try testing.expect(!isNumericWithUnit(long_valid)); // not numeric
|
||||
|
||||
const long_property = "a-" ** 100 ++ "property";
|
||||
try testing.expect(isValidPropertyName(long_property));
|
||||
|
||||
const long_hex = "#" ++ "a" ** 20;
|
||||
try testing.expect(!isHexColor(long_hex));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: boundary conditions - numeric parsing" {
|
||||
try testing.expect(isNumericWithUnit("0px"));
|
||||
try testing.expect(isNumericWithUnit("0.0px"));
|
||||
try testing.expect(isNumericWithUnit(".0px"));
|
||||
try testing.expect(isNumericWithUnit("0.px"));
|
||||
|
||||
try testing.expect(isNumericWithUnit("999999999px"));
|
||||
try testing.expect(isNumericWithUnit("1.7976931348623157e+308px"));
|
||||
|
||||
try testing.expect(isNumericWithUnit("0.000000001px"));
|
||||
try testing.expect(isNumericWithUnit("1e-100px"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: extractImportant - malformed important declarations" {
|
||||
var result = extractImportant("red ! important");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("red ! important", result.value);
|
||||
|
||||
result = extractImportant("red !Important");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("red !Important", result.value);
|
||||
|
||||
result = extractImportant("red !IMPORTANT");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("red !IMPORTANT", result.value);
|
||||
|
||||
result = extractImportant("!importantred");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("!importantred", result.value);
|
||||
|
||||
result = extractImportant("red !important !important");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("red !important", result.value);
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isMultiValueProperty - complex spacing scenarios" {
|
||||
try testing.expect(isMultiValueProperty("10px 20px"));
|
||||
try testing.expect(isMultiValueProperty("solid red"));
|
||||
|
||||
try testing.expect(isMultiValueProperty(" 10px 20px "));
|
||||
|
||||
try testing.expect(!isMultiValueProperty("10px\t20px"));
|
||||
try testing.expect(!isMultiValueProperty("10px\n20px"));
|
||||
|
||||
try testing.expect(isMultiValueProperty("10px 20px 30px"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isAlreadyQuoted - edge cases with quotes" {
|
||||
try testing.expect(isAlreadyQuoted("\"'hello'\""));
|
||||
try testing.expect(isAlreadyQuoted("'\"hello\"'"));
|
||||
|
||||
try testing.expect(isAlreadyQuoted("\"hello\\\"world\""));
|
||||
try testing.expect(isAlreadyQuoted("'hello\\'world'"));
|
||||
|
||||
try testing.expect(!isAlreadyQuoted("\"hello"));
|
||||
try testing.expect(!isAlreadyQuoted("hello\""));
|
||||
try testing.expect(!isAlreadyQuoted("'hello"));
|
||||
try testing.expect(!isAlreadyQuoted("hello'"));
|
||||
|
||||
try testing.expect(isAlreadyQuoted("\"a\""));
|
||||
try testing.expect(isAlreadyQuoted("'b'"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: needsQuotes - function and URL edge cases" {
|
||||
try testing.expect(!needsQuotes("rgb(255, 0, 0)"));
|
||||
try testing.expect(!needsQuotes("calc(100% - 20px)"));
|
||||
|
||||
try testing.expect(!needsQuotes("url(path with spaces.jpg)"));
|
||||
|
||||
try testing.expect(!needsQuotes("linear-gradient(to right, red, blue)"));
|
||||
|
||||
try testing.expect(needsQuotes("rgb(255, 0, 0"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: escapeCSSValue - control characters and Unicode" {
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
var result = try escapeCSSValue(allocator, "test\ttab");
|
||||
try testing.expectEqual("\"test\\9 tab\"", result);
|
||||
|
||||
result = try escapeCSSValue(allocator, "test\rreturn");
|
||||
try testing.expectEqual("\"test\\D return\"", result);
|
||||
|
||||
result = try escapeCSSValue(allocator, "test\x00null");
|
||||
try testing.expectEqual("\"test\\0null\"", result);
|
||||
|
||||
result = try escapeCSSValue(allocator, "test\x7Fdel");
|
||||
try testing.expectEqual("\"test\\7f del\"", result);
|
||||
|
||||
result = try escapeCSSValue(allocator, "test\"quote\nline\\back");
|
||||
try testing.expectEqual("\"test\\\"quote\\A line\\\\back\"", result);
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isValidPropertyName - CSS custom properties and vendor prefixes" {
|
||||
try testing.expect(isValidPropertyName("--custom-color"));
|
||||
try testing.expect(isValidPropertyName("--my-variable"));
|
||||
try testing.expect(isValidPropertyName("--123"));
|
||||
|
||||
try testing.expect(isValidPropertyName("-webkit-transform"));
|
||||
try testing.expect(isValidPropertyName("-moz-border-radius"));
|
||||
try testing.expect(isValidPropertyName("-ms-filter"));
|
||||
try testing.expect(isValidPropertyName("-o-transition"));
|
||||
|
||||
try testing.expect(!isValidPropertyName("-123invalid"));
|
||||
try testing.expect(!isValidPropertyName("--"));
|
||||
try testing.expect(!isValidPropertyName("-"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: startsWithFunction - case sensitivity and partial matches" {
|
||||
try testing.expect(CSSKeywords.startsWithFunction("RGB(255, 0, 0)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("Rgb(255, 0, 0)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("URL(image.png)"));
|
||||
|
||||
try testing.expect(CSSKeywords.startsWithFunction("rg(something)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("ur(something)"));
|
||||
|
||||
try testing.expect(CSSKeywords.startsWithFunction("rgb(1,2,3)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("rgba(1,2,3,4)"));
|
||||
|
||||
try testing.expect(CSSKeywords.startsWithFunction("my-custom-function(args)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("function-with-dashes(test)"));
|
||||
|
||||
try testing.expect(!CSSKeywords.startsWithFunction("123function(test)"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isHexColor - Unicode and invalid characters" {
|
||||
try testing.expect(!isHexColor("#ghijkl"));
|
||||
try testing.expect(!isHexColor("#12345g"));
|
||||
try testing.expect(!isHexColor("#xyz"));
|
||||
|
||||
try testing.expect(!isHexColor("#АВС"));
|
||||
|
||||
try testing.expect(!isHexColor("#1234567g"));
|
||||
try testing.expect(!isHexColor("#g2345678"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: complex integration scenarios" {
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
try testing.expect(isMultiValueProperty("rgb(255,0,0) url(bg.jpg)"));
|
||||
|
||||
try testing.expect(!needsQuotes("calc(100% - 20px)"));
|
||||
|
||||
const result = try escapeCSSValue(allocator, "fake(function with spaces");
|
||||
try testing.expectEqual("\"fake(function with spaces\"", result);
|
||||
|
||||
const important_result = extractImportant("rgb(255,0,0) !important");
|
||||
try testing.expect(important_result.is_important);
|
||||
try testing.expectEqual("rgb(255,0,0)", important_result.value);
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: performance edge cases - empty and minimal inputs" {
|
||||
try testing.expect(!isNumericWithUnit(""));
|
||||
try testing.expect(!isHexColor(""));
|
||||
try testing.expect(!isMultiValueProperty(""));
|
||||
try testing.expect(!isAlreadyQuoted(""));
|
||||
try testing.expect(!isValidPropertyName(""));
|
||||
try testing.expect(needsQuotes(""));
|
||||
try testing.expect(!CSSKeywords.isKnownKeyword(""));
|
||||
try testing.expect(!CSSKeywords.containsSpecialChar(""));
|
||||
try testing.expect(!CSSKeywords.isValidUnit(""));
|
||||
try testing.expect(!CSSKeywords.startsWithFunction(""));
|
||||
|
||||
try testing.expect(!isNumericWithUnit("a"));
|
||||
try testing.expect(!isHexColor("a"));
|
||||
try testing.expect(!isMultiValueProperty("a"));
|
||||
try testing.expect(!isAlreadyQuoted("a"));
|
||||
try testing.expect(isValidPropertyName("a"));
|
||||
try testing.expect(!needsQuotes("a"));
|
||||
}
|
||||
@@ -1,97 +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");
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const StyleSheet = @import("StyleSheet.zig");
|
||||
const CSSRuleList = @import("CSSRuleList.zig");
|
||||
const CSSImportRule = @import("CSSRule.zig").CSSImportRule;
|
||||
|
||||
const CSSStyleSheet = @This();
|
||||
pub const prototype = *StyleSheet;
|
||||
|
||||
proto: StyleSheet,
|
||||
css_rules: CSSRuleList,
|
||||
owner_rule: ?*CSSImportRule,
|
||||
|
||||
const CSSStyleSheetOpts = struct {
|
||||
base_url: ?[]const u8 = null,
|
||||
// TODO: Suupport media
|
||||
disabled: bool = false,
|
||||
};
|
||||
|
||||
pub fn constructor(_opts: ?CSSStyleSheetOpts) !CSSStyleSheet {
|
||||
const opts = _opts orelse CSSStyleSheetOpts{};
|
||||
return .{
|
||||
.proto = .{ .disabled = opts.disabled },
|
||||
.css_rules = .constructor(),
|
||||
.owner_rule = null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_ownerRule(_: *CSSStyleSheet) ?*CSSImportRule {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn get_cssRules(self: *CSSStyleSheet) *CSSRuleList {
|
||||
return &self.css_rules;
|
||||
}
|
||||
|
||||
pub fn _insertRule(self: *CSSStyleSheet, rule: []const u8, _index: ?usize, page: *Page) !usize {
|
||||
const index = _index orelse 0;
|
||||
if (index > self.css_rules.list.items.len) {
|
||||
return error.IndexSize;
|
||||
}
|
||||
|
||||
const arena = page.arena;
|
||||
try self.css_rules.list.insert(arena, index, try arena.dupe(u8, rule));
|
||||
return index;
|
||||
}
|
||||
|
||||
pub fn _deleteRule(self: *CSSStyleSheet, index: usize) !void {
|
||||
if (index > self.css_rules.list.items.len) {
|
||||
return error.IndexSize;
|
||||
}
|
||||
|
||||
_ = self.css_rules.list.orderedRemove(index);
|
||||
}
|
||||
|
||||
pub fn _replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !Env.Promise {
|
||||
_ = self;
|
||||
_ = text;
|
||||
// TODO: clear self.css_rules
|
||||
// parse text and re-populate self.css_rules
|
||||
|
||||
const resolver = page.main_context.createPromiseResolver();
|
||||
try resolver.resolve({});
|
||||
return resolver.promise();
|
||||
}
|
||||
|
||||
pub fn _replaceSync(self: *CSSStyleSheet, text: []const u8) !void {
|
||||
_ = self;
|
||||
_ = text;
|
||||
// TODO: clear self.css_rules
|
||||
// parse text and re-populate self.css_rules
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: CSS.StyleSheet" {
|
||||
try testing.htmlRunner("cssom/css_stylesheet.html");
|
||||
}
|
||||
@@ -1,55 +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 parser = @import("../netsurf.zig");
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/StyleSheet#specifications
|
||||
const StyleSheet = @This();
|
||||
|
||||
disabled: bool = false,
|
||||
href: []const u8 = "",
|
||||
owner_node: ?*parser.Node = null,
|
||||
parent_stylesheet: ?*StyleSheet = null,
|
||||
title: []const u8 = "",
|
||||
type: []const u8 = "text/css",
|
||||
|
||||
pub fn get_disabled(self: *const StyleSheet) bool {
|
||||
return self.disabled;
|
||||
}
|
||||
|
||||
pub fn get_href(self: *const StyleSheet) []const u8 {
|
||||
return self.href;
|
||||
}
|
||||
|
||||
// TODO: media
|
||||
|
||||
pub fn get_ownerNode(self: *const StyleSheet) ?*parser.Node {
|
||||
return self.owner_node;
|
||||
}
|
||||
|
||||
pub fn get_parentStyleSheet(self: *const StyleSheet) ?*StyleSheet {
|
||||
return self.parent_stylesheet;
|
||||
}
|
||||
|
||||
pub fn get_title(self: *const StyleSheet) []const u8 {
|
||||
return self.title;
|
||||
}
|
||||
|
||||
pub fn get_type(self: *const StyleSheet) []const u8 {
|
||||
return self.type;
|
||||
}
|
||||
@@ -1,25 +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/>.
|
||||
|
||||
pub const Interfaces = .{
|
||||
@import("StyleSheet.zig"),
|
||||
@import("CSSStyleSheet.zig"),
|
||||
@import("CSSStyleDeclaration.zig"),
|
||||
@import("CSSRuleList.zig"),
|
||||
@import("CSSRule.zig").Interfaces,
|
||||
};
|
||||
79
src/browser/datauri.zig
Normal file
79
src/browser/datauri.zig
Normal file
@@ -0,0 +1,79 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
// Represents https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data
|
||||
pub const DataURI = struct {
|
||||
was_base64_encoded: bool,
|
||||
// The contents in the uri. It will be base64 decoded but not prepared in
|
||||
// any way for mime.charset.
|
||||
data: []const u8,
|
||||
|
||||
// Parses data:[<media-type>][;base64],<data>
|
||||
pub fn parse(allocator: Allocator, src: []const u8) !?DataURI {
|
||||
if (!std.mem.startsWith(u8, src, "data:")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uri = src[5..];
|
||||
const data_starts = std.mem.indexOfScalar(u8, uri, ',') orelse return null;
|
||||
|
||||
// Extract the encoding.
|
||||
var metadata = uri[0..data_starts];
|
||||
var base64_encoded = false;
|
||||
if (std.mem.endsWith(u8, metadata, ";base64")) {
|
||||
base64_encoded = true;
|
||||
metadata = metadata[0 .. metadata.len - 7];
|
||||
}
|
||||
|
||||
// TODO: Extract mime type. This not trivial because Mime.parse requires
|
||||
// a []u8 and might mutate the src. And, the DataURI.parse references atm
|
||||
// do not have deinit calls.
|
||||
|
||||
// Prepare the data.
|
||||
var data = uri[data_starts + 1 ..];
|
||||
if (base64_encoded) {
|
||||
const decoder = std.base64.standard.Decoder;
|
||||
const decoded_size = try decoder.calcSizeForSlice(data);
|
||||
|
||||
const buffer = try allocator.alloc(u8, decoded_size);
|
||||
errdefer allocator.free(buffer);
|
||||
|
||||
try decoder.decode(buffer, data);
|
||||
data = buffer;
|
||||
}
|
||||
|
||||
return .{
|
||||
.was_base64_encoded = base64_encoded,
|
||||
.data = data,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const DataURI, allocator: Allocator) void {
|
||||
if (self.was_base64_encoded) {
|
||||
allocator.free(self.data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const testing = std.testing;
|
||||
test "DataURI: parse valid" {
|
||||
try test_valid("data:text/javascript; charset=utf-8;base64,Zm9v", "foo");
|
||||
try test_valid("data:text/javascript; charset=utf-8;,foo", "foo");
|
||||
try test_valid("data:,foo", "foo");
|
||||
}
|
||||
|
||||
test "DataURI: parse invalid" {
|
||||
try test_cannot_parse("atad:,foo");
|
||||
try test_cannot_parse("data:foo");
|
||||
try test_cannot_parse("data:");
|
||||
}
|
||||
|
||||
fn test_valid(uri: []const u8, expected: []const u8) !void {
|
||||
const data_uri = try DataURI.parse(std.testing.allocator, uri) orelse return error.TestFailed;
|
||||
defer data_uri.deinit(testing.allocator);
|
||||
try testing.expectEqualStrings(expected, data_uri.data);
|
||||
}
|
||||
|
||||
fn test_cannot_parse(uri: []const u8) !void {
|
||||
try testing.expectEqual(null, DataURI.parse(std.testing.allocator, uri));
|
||||
}
|
||||
@@ -1,109 +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 Page = @import("../page.zig").Page;
|
||||
const JsObject = @import("../env.zig").JsObject;
|
||||
const Promise = @import("../env.zig").Promise;
|
||||
const PromiseResolver = @import("../env.zig").PromiseResolver;
|
||||
|
||||
const Animation = @This();
|
||||
|
||||
effect: ?JsObject,
|
||||
timeline: ?JsObject,
|
||||
ready_resolver: ?PromiseResolver,
|
||||
finished_resolver: ?PromiseResolver,
|
||||
|
||||
pub fn constructor(effect: ?JsObject, timeline: ?JsObject) !Animation {
|
||||
return .{
|
||||
.effect = if (effect) |eo| try eo.persist() else null,
|
||||
.timeline = if (timeline) |to| try to.persist() else null,
|
||||
.ready_resolver = null,
|
||||
.finished_resolver = null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_playState(self: *const Animation) []const u8 {
|
||||
_ = self;
|
||||
return "finished";
|
||||
}
|
||||
|
||||
pub fn get_pending(self: *const Animation) bool {
|
||||
_ = self;
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn get_finished(self: *Animation, page: *Page) !Promise {
|
||||
if (self.finished_resolver == null) {
|
||||
const resolver = page.main_context.createPromiseResolver();
|
||||
try resolver.resolve(self);
|
||||
self.finished_resolver = resolver;
|
||||
}
|
||||
return self.finished_resolver.?.promise();
|
||||
}
|
||||
|
||||
pub fn get_ready(self: *Animation, page: *Page) !Promise {
|
||||
// never resolved, because we're always "finished"
|
||||
if (self.ready_resolver == null) {
|
||||
const resolver = page.main_context.createPromiseResolver();
|
||||
self.ready_resolver = resolver;
|
||||
}
|
||||
return self.ready_resolver.?.promise();
|
||||
}
|
||||
|
||||
pub fn get_effect(self: *const Animation) ?JsObject {
|
||||
return self.effect;
|
||||
}
|
||||
|
||||
pub fn set_effect(self: *Animation, effect: JsObject) !void {
|
||||
self.effect = try effect.persist();
|
||||
}
|
||||
|
||||
pub fn get_timeline(self: *const Animation) ?JsObject {
|
||||
return self.timeline;
|
||||
}
|
||||
|
||||
pub fn set_timeline(self: *Animation, timeline: JsObject) !void {
|
||||
self.timeline = try timeline.persist();
|
||||
}
|
||||
|
||||
pub fn _play(self: *const Animation) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
pub fn _pause(self: *const Animation) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
pub fn _cancel(self: *const Animation) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
pub fn _finish(self: *const Animation) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
pub fn _reverse(self: *const Animation) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.Animation" {
|
||||
try testing.htmlRunner("dom/animation.html");
|
||||
}
|
||||
@@ -1,291 +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 log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const EventHandler = @import("../events/event.zig").EventHandler;
|
||||
|
||||
const JsObject = Env.JsObject;
|
||||
const Function = Env.Function;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const MAX_QUEUE_SIZE = 10;
|
||||
|
||||
pub const Interfaces = .{ MessageChannel, MessagePort };
|
||||
|
||||
const MessageChannel = @This();
|
||||
|
||||
port1: *MessagePort,
|
||||
port2: *MessagePort,
|
||||
|
||||
pub fn constructor(page: *Page) !MessageChannel {
|
||||
// Why do we allocate this rather than storing directly in the struct?
|
||||
// https://github.com/lightpanda-io/project/discussions/165
|
||||
const port1 = try page.arena.create(MessagePort);
|
||||
const port2 = try page.arena.create(MessagePort);
|
||||
port1.* = .{
|
||||
.pair = port2,
|
||||
};
|
||||
port2.* = .{
|
||||
.pair = port1,
|
||||
};
|
||||
|
||||
return .{
|
||||
.port1 = port1,
|
||||
.port2 = port2,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_port1(self: *const MessageChannel) *MessagePort {
|
||||
return self.port1;
|
||||
}
|
||||
|
||||
pub fn get_port2(self: *const MessageChannel) *MessagePort {
|
||||
return self.port2;
|
||||
}
|
||||
|
||||
pub const MessagePort = struct {
|
||||
pub const prototype = *EventTarget;
|
||||
|
||||
proto: parser.EventTargetTBase = .{ .internal_target_type = .message_port },
|
||||
|
||||
pair: *MessagePort,
|
||||
closed: bool = false,
|
||||
started: bool = false,
|
||||
onmessage_cbk: ?Function = null,
|
||||
onmessageerror_cbk: ?Function = null,
|
||||
// This is the queue of messages to dispatch to THIS MessagePort when the
|
||||
// MessagePort is started.
|
||||
queue: std.ArrayListUnmanaged(JsObject) = .empty,
|
||||
|
||||
pub const PostMessageOption = union(enum) {
|
||||
transfer: JsObject,
|
||||
options: Opts,
|
||||
|
||||
pub const Opts = struct {
|
||||
transfer: JsObject,
|
||||
};
|
||||
};
|
||||
|
||||
pub fn _postMessage(self: *MessagePort, obj: JsObject, opts_: ?PostMessageOption, page: *Page) !void {
|
||||
if (self.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts_ != null) {
|
||||
log.warn(.web_api, "not implemented", .{ .feature = "MessagePort postMessage options" });
|
||||
return error.NotImplemented;
|
||||
}
|
||||
|
||||
try self.pair.dispatchOrQueue(obj, page.arena);
|
||||
}
|
||||
|
||||
// Start impacts the ability to receive a message.
|
||||
// Given pair1 (started) and pair2 (not started), then:
|
||||
// pair2.postMessage('x'); //will be dispatched to pair1.onmessage
|
||||
// pair1.postMessage('x'); // will be queued until pair2 is started
|
||||
pub fn _start(self: *MessagePort) !void {
|
||||
if (self.started) {
|
||||
return;
|
||||
}
|
||||
self.started = true;
|
||||
for (self.queue.items) |data| {
|
||||
try self.dispatch(data);
|
||||
}
|
||||
// we'll never use this queue again, but it's allocated with an arena
|
||||
// we don't even need to clear it, but it seems a bit safer to do at
|
||||
// least that
|
||||
self.queue.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
// Closing seems to stop both the publishing and receiving of messages,
|
||||
// effectively rendering the channel useless. It cannot be reversed.
|
||||
pub fn _close(self: *MessagePort) void {
|
||||
self.closed = true;
|
||||
self.pair.closed = true;
|
||||
}
|
||||
|
||||
pub fn get_onmessage(self: *MessagePort) ?Function {
|
||||
return self.onmessage_cbk;
|
||||
}
|
||||
pub fn get_onmessageerror(self: *MessagePort) ?Function {
|
||||
return self.onmessageerror_cbk;
|
||||
}
|
||||
|
||||
pub fn set_onmessage(self: *MessagePort, listener: EventHandler.Listener, page: *Page) !void {
|
||||
if (self.onmessage_cbk) |cbk| {
|
||||
try self.unregister("message", cbk.id);
|
||||
}
|
||||
self.onmessage_cbk = try self.register(page.arena, "message", listener);
|
||||
|
||||
// When onmessage is set directly, then it's like start() was called.
|
||||
// If addEventListener('message') is used, the app has to call start()
|
||||
// explicitly.
|
||||
try self._start();
|
||||
}
|
||||
|
||||
pub fn set_onmessageerror(self: *MessagePort, listener: EventHandler.Listener, page: *Page) !void {
|
||||
if (self.onmessageerror_cbk) |cbk| {
|
||||
try self.unregister("messageerror", cbk.id);
|
||||
}
|
||||
self.onmessageerror_cbk = try self.register(page.arena, "messageerror", listener);
|
||||
}
|
||||
|
||||
// called from our pair. If port1.postMessage("x") is called, then this
|
||||
// will be called on port2.
|
||||
fn dispatchOrQueue(self: *MessagePort, obj: JsObject, arena: Allocator) !void {
|
||||
// our pair should have checked this already
|
||||
std.debug.assert(self.closed == false);
|
||||
|
||||
if (self.started) {
|
||||
return self.dispatch(try obj.persist());
|
||||
}
|
||||
|
||||
if (self.queue.items.len > MAX_QUEUE_SIZE) {
|
||||
// This isn't part of the spec, but not putting a limit is reckless
|
||||
return error.MessageQueueLimit;
|
||||
}
|
||||
return self.queue.append(arena, try obj.persist());
|
||||
}
|
||||
|
||||
fn dispatch(self: *MessagePort, obj: JsObject) !void {
|
||||
// obj is already persisted, don't use `MessageEvent.constructor`, but
|
||||
// go directly to `init`, which assumes persisted objects.
|
||||
var evt = try MessageEvent.init(.{ .data = obj });
|
||||
_ = try parser.eventTargetDispatchEvent(
|
||||
parser.toEventTarget(MessagePort, self),
|
||||
@as(*parser.Event, @ptrCast(&evt)),
|
||||
);
|
||||
}
|
||||
|
||||
fn register(
|
||||
self: *MessagePort,
|
||||
alloc: Allocator,
|
||||
typ: []const u8,
|
||||
listener: EventHandler.Listener,
|
||||
) !?Function {
|
||||
const target = @as(*parser.EventTarget, @ptrCast(self));
|
||||
const eh = (try EventHandler.register(alloc, target, typ, listener, null)) orelse unreachable;
|
||||
return eh.callback;
|
||||
}
|
||||
|
||||
fn unregister(self: *MessagePort, typ: []const u8, cbk_id: usize) !void {
|
||||
const et = @as(*parser.EventTarget, @ptrCast(self));
|
||||
const lst = try parser.eventTargetHasListener(et, typ, false, cbk_id);
|
||||
if (lst == null) {
|
||||
return;
|
||||
}
|
||||
try parser.eventTargetRemoveEventListener(et, typ, lst.?, false);
|
||||
}
|
||||
};
|
||||
|
||||
pub const MessageEvent = struct {
|
||||
const Event = @import("../events/event.zig").Event;
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
|
||||
pub const prototype = *Event;
|
||||
pub const Exception = DOMException;
|
||||
pub const union_make_copy = true;
|
||||
|
||||
proto: parser.Event,
|
||||
data: ?JsObject,
|
||||
|
||||
// You would think if port1 sends to port2, the source would be port2
|
||||
// (which is how I read the documentation), but it appears to always be
|
||||
// null. It can always be set explicitly via the constructor;
|
||||
source: ?JsObject,
|
||||
|
||||
origin: []const u8,
|
||||
|
||||
// This is used for Server-Sent events. Appears to always be an empty
|
||||
// string for MessagePort messages.
|
||||
last_event_id: []const u8,
|
||||
|
||||
// This might be related to the "transfer" option of postMessage which
|
||||
// we don't yet support. For "normal" message, it's always an empty array.
|
||||
// Though it could be set explicitly via the constructor
|
||||
ports: []*MessagePort,
|
||||
|
||||
const Options = struct {
|
||||
data: ?JsObject = null,
|
||||
source: ?JsObject = null,
|
||||
origin: []const u8 = "",
|
||||
lastEventId: []const u8 = "",
|
||||
ports: []*MessagePort = &.{},
|
||||
};
|
||||
|
||||
pub fn constructor(opts: Options) !MessageEvent {
|
||||
return init(.{
|
||||
.data = if (opts.data) |obj| try obj.persist() else null,
|
||||
.source = if (opts.source) |obj| try obj.persist() else null,
|
||||
.ports = opts.ports,
|
||||
.origin = opts.origin,
|
||||
.lastEventId = opts.lastEventId,
|
||||
});
|
||||
}
|
||||
|
||||
// This is like "constructor", but it assumes JsObjects have already been
|
||||
// persisted. Necessary because this `new MessageEvent()` can be called
|
||||
// directly from JS OR from a port.postMessage. In the latter case, data
|
||||
// may have already been persisted (as it might need to be queued);
|
||||
fn init(opts: Options) !MessageEvent {
|
||||
const event = try parser.eventCreate();
|
||||
defer parser.eventDestroy(event);
|
||||
try parser.eventInit(event, "message", .{});
|
||||
try parser.eventSetInternalType(event, .message_event);
|
||||
|
||||
return .{
|
||||
.proto = event.*,
|
||||
.data = opts.data,
|
||||
.source = opts.source,
|
||||
.ports = opts.ports,
|
||||
.origin = opts.origin,
|
||||
.last_event_id = opts.lastEventId,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_data(self: *const MessageEvent) !?JsObject {
|
||||
return self.data;
|
||||
}
|
||||
|
||||
pub fn get_origin(self: *const MessageEvent) []const u8 {
|
||||
return self.origin;
|
||||
}
|
||||
|
||||
pub fn get_source(self: *const MessageEvent) ?JsObject {
|
||||
return self.source;
|
||||
}
|
||||
|
||||
pub fn get_ports(self: *const MessageEvent) []*MessagePort {
|
||||
return self.ports;
|
||||
}
|
||||
|
||||
pub fn get_lastEventId(self: *const MessageEvent) []const u8 {
|
||||
return self.last_event_id;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.MessageChannel" {
|
||||
try testing.htmlRunner("dom/message_channel.html");
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
//
|
||||
// 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 parser = @import("../netsurf.zig");
|
||||
|
||||
const Node = @import("node.zig").Node;
|
||||
@@ -46,14 +47,7 @@ pub const Attr = struct {
|
||||
}
|
||||
|
||||
pub fn set_value(self: *parser.Attribute, v: []const u8) !?[]const u8 {
|
||||
if (try parser.attributeGetOwnerElement(self)) |el| {
|
||||
// if possible, go through the element, as that triggers a
|
||||
// DOMAttrModified event (which MutationObserver cares about)
|
||||
const name = try parser.attributeGetName(self);
|
||||
try parser.elementSetAttribute(el, name, v);
|
||||
} else {
|
||||
try parser.attributeSetValue(self, v);
|
||||
}
|
||||
try parser.attributeSetValue(self, v);
|
||||
return v;
|
||||
}
|
||||
|
||||
@@ -70,6 +64,32 @@ pub const Attr = struct {
|
||||
// -----
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.Attribute" {
|
||||
try testing.htmlRunner("dom/attribute.html");
|
||||
test "Browser.DOM.Attribute" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let a = document.createAttributeNS('foo', 'bar')", "undefined" },
|
||||
.{ "a.namespaceURI", "foo" },
|
||||
.{ "a.prefix", "null" },
|
||||
.{ "a.localName", "bar" },
|
||||
.{ "a.name", "bar" },
|
||||
.{ "a.value", "" },
|
||||
// TODO: libdom has a bug here: the created attr has no parent, it
|
||||
// causes a panic w/ libdom when setting the value.
|
||||
//.{ "a.value = 'nok'", "nok" },
|
||||
.{ "a.ownerElement", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let b = document.getElementById('link').getAttributeNode('class')", "undefined" },
|
||||
.{ "b.name", "class" },
|
||||
.{ "b.value", "ok" },
|
||||
.{ "b.value = 'nok'", "nok" },
|
||||
.{ "b.value", "nok" },
|
||||
.{ "b.value = null", "null" },
|
||||
.{ "b.value", "null" },
|
||||
.{ "b.value = 'ok'", "ok" },
|
||||
.{ "b.ownerElement.id", "link" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -24,8 +24,7 @@ const Node = @import("node.zig").Node;
|
||||
const Comment = @import("comment.zig").Comment;
|
||||
const Text = @import("text.zig");
|
||||
const ProcessingInstruction = @import("processing_instruction.zig").ProcessingInstruction;
|
||||
const Element = @import("element.zig").Element;
|
||||
const ElementUnion = @import("element.zig").Union;
|
||||
const HTMLElem = @import("../html/elements.zig");
|
||||
|
||||
// CharacterData interfaces
|
||||
pub const Interfaces = .{
|
||||
@@ -50,20 +49,20 @@ pub const CharacterData = struct {
|
||||
return try parser.characterDataLength(self);
|
||||
}
|
||||
|
||||
pub fn get_nextElementSibling(self: *parser.CharacterData) !?ElementUnion {
|
||||
pub fn get_nextElementSibling(self: *parser.CharacterData) !?HTMLElem.Union {
|
||||
const res = try parser.nodeNextElementSibling(parser.characterDataToNode(self));
|
||||
if (res == null) {
|
||||
return null;
|
||||
}
|
||||
return try Element.toInterface(res.?);
|
||||
return try HTMLElem.toInterface(HTMLElem.Union, res.?);
|
||||
}
|
||||
|
||||
pub fn get_previousElementSibling(self: *parser.CharacterData) !?ElementUnion {
|
||||
pub fn get_previousElementSibling(self: *parser.CharacterData) !?HTMLElem.Union {
|
||||
const res = try parser.nodePreviousElementSibling(parser.characterDataToNode(self));
|
||||
if (res == null) {
|
||||
return null;
|
||||
}
|
||||
return try Element.toInterface(res.?);
|
||||
return try HTMLElem.toInterface(HTMLElem.Union, res.?);
|
||||
}
|
||||
|
||||
// Read/Write attributes
|
||||
@@ -102,7 +101,7 @@ pub const CharacterData = struct {
|
||||
// netsurf's CharacterData (text, comment) doesn't implement the
|
||||
// dom_node_get_attributes and thus will crash if we try to call nodeIsEqualNode.
|
||||
pub fn _isEqualNode(self: *parser.CharacterData, other_node: *parser.Node) !bool {
|
||||
if (try parser.nodeType(@ptrCast(@alignCast(self))) != try parser.nodeType(other_node)) {
|
||||
if (try parser.nodeType(@ptrCast(self)) != try parser.nodeType(other_node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -129,6 +128,69 @@ pub const CharacterData = struct {
|
||||
// -----
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.CharacterData" {
|
||||
try testing.htmlRunner("dom/character_data.html");
|
||||
test "Browser.DOM.CharacterData" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let link = document.getElementById('link')", "undefined" },
|
||||
.{ "let cdata = link.firstChild", "undefined" },
|
||||
.{ "cdata.data", "OK" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.data = 'OK modified'", "OK modified" },
|
||||
.{ "cdata.data === 'OK modified'", "true" },
|
||||
.{ "cdata.data = 'OK'", "OK" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.length === 2", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.nextElementSibling === null", "true" },
|
||||
// create a next element
|
||||
.{ "let next = document.createElement('a')", "undefined" },
|
||||
.{ "link.appendChild(next, cdata) !== undefined", "true" },
|
||||
.{ "cdata.nextElementSibling.localName === 'a' ", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.previousElementSibling === null", "true" },
|
||||
// create a prev element
|
||||
.{ "let prev = document.createElement('div')", "undefined" },
|
||||
.{ "link.insertBefore(prev, cdata) !== undefined", "true" },
|
||||
.{ "cdata.previousElementSibling.localName === 'div' ", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.appendData(' modified')", "undefined" },
|
||||
.{ "cdata.data === 'OK modified' ", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.deleteData('OK'.length, ' modified'.length)", "undefined" },
|
||||
.{ "cdata.data == 'OK'", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.insertData('OK'.length-1, 'modified')", "undefined" },
|
||||
.{ "cdata.data == 'OmodifiedK'", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.replaceData('OK'.length-1, 'modified'.length, 'replaced')", "undefined" },
|
||||
.{ "cdata.data == 'OreplacedK'", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.substringData('OK'.length-1, 'replaced'.length) == 'replaced'", "true" },
|
||||
.{ "cdata.substringData('OK'.length-1, 0) == ''", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.substringData('OK'.length-1, 'replaced'.length) == 'replaced'", "true" },
|
||||
.{ "cdata.substringData('OK'.length-1, 0) == ''", "true" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ const parser = @import("../netsurf.zig");
|
||||
|
||||
const CharacterData = @import("character_data.zig").CharacterData;
|
||||
|
||||
const Page = @import("../page.zig").Page;
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
// https://dom.spec.whatwg.org/#interface-comment
|
||||
pub const Comment = struct {
|
||||
@@ -28,9 +28,9 @@ pub const Comment = struct {
|
||||
pub const prototype = *CharacterData;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(data: ?[]const u8, page: *const Page) !*parser.Comment {
|
||||
pub fn constructor(data: ?[]const u8, state: *const SessionState) !*parser.Comment {
|
||||
return parser.documentCreateComment(
|
||||
parser.documentHTMLToDocument(page.window.document),
|
||||
parser.documentHTMLToDocument(state.document.?),
|
||||
data orelse "",
|
||||
);
|
||||
}
|
||||
@@ -40,6 +40,15 @@ pub const Comment = struct {
|
||||
// -----
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.Comment" {
|
||||
try testing.htmlRunner("dom/comment.html");
|
||||
test "Browser.DOM.Comment" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let comment = new Comment('foo')", "undefined" },
|
||||
.{ "comment.data", "foo" },
|
||||
|
||||
.{ "let emptycomment = new Comment()", "undefined" },
|
||||
.{ "emptycomment.data", "" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ pub fn querySelector(alloc: std.mem.Allocator, n: *parser.Node, selector: []cons
|
||||
|
||||
var m = MatchFirst{};
|
||||
|
||||
_ = try css.matchFirst(&ps, Node{ .node = n }, &m);
|
||||
_ = try css.matchFirst(ps, Node{ .node = n }, &m);
|
||||
return m.n;
|
||||
}
|
||||
|
||||
@@ -75,6 +75,6 @@ pub fn querySelectorAll(alloc: std.mem.Allocator, n: *parser.Node, selector: []c
|
||||
var m = MatchAll.init(alloc);
|
||||
defer m.deinit();
|
||||
|
||||
try css.matchAll(&ps, Node{ .node = n }, &m);
|
||||
try css.matchAll(ps, Node{ .node = n }, &m);
|
||||
return m.toOwnedList();
|
||||
}
|
||||
|
||||
@@ -18,9 +18,8 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
const Node = @import("node.zig").Node;
|
||||
const NodeList = @import("nodelist.zig").NodeList;
|
||||
@@ -31,14 +30,6 @@ const css = @import("css.zig");
|
||||
|
||||
const Element = @import("element.zig").Element;
|
||||
const ElementUnion = @import("element.zig").Union;
|
||||
const TreeWalker = @import("tree_walker.zig").TreeWalker;
|
||||
const CSSStyleSheet = @import("../cssom/CSSStyleSheet.zig");
|
||||
const NodeIterator = @import("node_iterator.zig").NodeIterator;
|
||||
const Range = @import("range.zig").Range;
|
||||
|
||||
const CustomEvent = @import("../events/custom_event.zig").CustomEvent;
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
|
||||
const DOMImplementation = @import("implementation.zig").DOMImplementation;
|
||||
|
||||
@@ -48,14 +39,14 @@ pub const Document = struct {
|
||||
pub const prototype = *Node;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *const Page) !*parser.DocumentHTML {
|
||||
pub fn constructor(state: *const SessionState) !*parser.DocumentHTML {
|
||||
const doc = try parser.documentCreateDocument(
|
||||
try parser.documentHTMLGetTitle(page.window.document),
|
||||
try parser.documentHTMLGetTitle(state.document.?),
|
||||
);
|
||||
|
||||
// we have to work w/ document instead of html document.
|
||||
const ddoc = parser.documentHTMLToDocument(doc);
|
||||
const ccur = parser.documentHTMLToDocument(page.window.document);
|
||||
const ccur = parser.documentHTMLToDocument(state.document.?);
|
||||
try parser.documentSetDocumentURI(ddoc, try parser.documentGetDocumentURI(ccur));
|
||||
try parser.documentSetInputEncoding(ddoc, try parser.documentGetInputEncoding(ccur));
|
||||
|
||||
@@ -112,21 +103,13 @@ pub const Document = struct {
|
||||
return try parser.documentGetDoctype(self);
|
||||
}
|
||||
|
||||
pub fn _createEvent(_: *parser.Document, eventCstr: []const u8) !union(enum) {
|
||||
base: *parser.Event,
|
||||
custom: CustomEvent,
|
||||
} {
|
||||
pub fn _createEvent(_: *parser.Document, eventCstr: []const u8) !*parser.Event {
|
||||
// TODO: for now only "Event" constructor is supported
|
||||
// see table on https://dom.spec.whatwg.org/#dom-document-createevent $2
|
||||
if (std.ascii.eqlIgnoreCase(eventCstr, "Event") or std.ascii.eqlIgnoreCase(eventCstr, "Events")) {
|
||||
return .{ .base = try parser.eventCreate() };
|
||||
return try parser.eventCreate();
|
||||
}
|
||||
|
||||
// Not documented in MDN but supported in Chrome.
|
||||
// This is actually both instance of `Event` and `CustomEvent`.
|
||||
if (std.ascii.eqlIgnoreCase(eventCstr, "CustomEvent")) {
|
||||
return .{ .custom = try CustomEvent.constructor(eventCstr, null) };
|
||||
}
|
||||
|
||||
return error.NotSupported;
|
||||
return parser.DOMError.NotSupported;
|
||||
}
|
||||
|
||||
pub fn _getElementById(self: *parser.Document, id: []const u8) !?ElementUnion {
|
||||
@@ -135,10 +118,8 @@ pub const Document = struct {
|
||||
}
|
||||
|
||||
pub fn _createElement(self: *parser.Document, tag_name: []const u8) !ElementUnion {
|
||||
// The element’s namespace is the HTML namespace when document is an HTML document
|
||||
// https://dom.spec.whatwg.org/#ref-for-dom-document-createelement%E2%91%A0
|
||||
const e = try parser.documentCreateElementNS(self, "http://www.w3.org/1999/xhtml", tag_name);
|
||||
return Element.toInterface(e);
|
||||
const e = try parser.documentCreateElement(self, tag_name);
|
||||
return try Element.toInterface(e);
|
||||
}
|
||||
|
||||
pub fn _createElementNS(self: *parser.Document, ns: []const u8, tag_name: []const u8) !ElementUnion {
|
||||
@@ -157,21 +138,18 @@ pub const Document = struct {
|
||||
pub fn _getElementsByTagName(
|
||||
self: *parser.Document,
|
||||
tag_name: []const u8,
|
||||
page: *Page,
|
||||
state: *SessionState,
|
||||
) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentToNode(self), tag_name, .{
|
||||
.include_root = true,
|
||||
});
|
||||
return try collection.HTMLCollectionByTagName(state.arena, parser.documentToNode(self), tag_name, true);
|
||||
}
|
||||
|
||||
pub fn _getElementsByClassName(
|
||||
self: *parser.Document,
|
||||
classNames: []const u8,
|
||||
page: *Page,
|
||||
state: *SessionState,
|
||||
) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByClassName(page.arena, parser.documentToNode(self), classNames, .{
|
||||
.include_root = true,
|
||||
});
|
||||
const allocator = state.arena;
|
||||
return try collection.HTMLCollectionByClassName(allocator, parser.documentToNode(self), classNames, true);
|
||||
}
|
||||
|
||||
pub fn _createDocumentFragment(self: *parser.Document) !*parser.DocumentFragment {
|
||||
@@ -215,9 +193,7 @@ pub const Document = struct {
|
||||
// ParentNode
|
||||
// https://dom.spec.whatwg.org/#parentnode
|
||||
pub fn get_children(self: *parser.Document) !collection.HTMLCollection {
|
||||
return collection.HTMLCollectionChildren(parser.documentToNode(self), .{
|
||||
.include_root = false,
|
||||
});
|
||||
return try collection.HTMLCollectionChildren(parser.documentToNode(self), false);
|
||||
}
|
||||
|
||||
pub fn get_firstElementChild(self: *parser.Document) !?ElementUnion {
|
||||
@@ -235,18 +211,20 @@ pub const Document = struct {
|
||||
return 1;
|
||||
}
|
||||
|
||||
pub fn _querySelector(self: *parser.Document, selector: []const u8, page: *Page) !?ElementUnion {
|
||||
pub fn _querySelector(self: *parser.Document, selector: []const u8, state: *SessionState) !?ElementUnion {
|
||||
if (selector.len == 0) return null;
|
||||
|
||||
const n = try css.querySelector(page.call_arena, parser.documentToNode(self), selector);
|
||||
const allocator = state.arena;
|
||||
const n = try css.querySelector(allocator, parser.documentToNode(self), selector);
|
||||
|
||||
if (n == null) return null;
|
||||
|
||||
return try Element.toInterface(parser.nodeToElement(n.?));
|
||||
}
|
||||
|
||||
pub fn _querySelectorAll(self: *parser.Document, selector: []const u8, page: *Page) !NodeList {
|
||||
return css.querySelectorAll(page.arena, parser.documentToNode(self), selector);
|
||||
pub fn _querySelectorAll(self: *parser.Document, selector: []const u8, state: *SessionState) !NodeList {
|
||||
const allocator = state.arena;
|
||||
return css.querySelectorAll(allocator, parser.documentToNode(self), selector);
|
||||
}
|
||||
|
||||
pub fn _prepend(self: *parser.Document, nodes: []const Node.NodeOrText) !void {
|
||||
@@ -260,70 +238,195 @@ pub const Document = struct {
|
||||
pub fn _replaceChildren(self: *parser.Document, nodes: []const Node.NodeOrText) !void {
|
||||
return Node.replaceChildren(parser.documentToNode(self), nodes);
|
||||
}
|
||||
|
||||
pub fn _createTreeWalker(_: *parser.Document, root: *parser.Node, what_to_show: ?TreeWalker.WhatToShow, filter: ?TreeWalker.TreeWalkerOpts) !TreeWalker {
|
||||
return TreeWalker.init(root, what_to_show, filter);
|
||||
}
|
||||
|
||||
pub fn _createNodeIterator(_: *parser.Document, root: *parser.Node, what_to_show: ?NodeIterator.WhatToShow, filter: ?NodeIterator.NodeIteratorOpts) !NodeIterator {
|
||||
return NodeIterator.init(root, what_to_show, filter);
|
||||
}
|
||||
|
||||
pub fn getActiveElement(self: *parser.Document, page: *Page) !?*parser.Element {
|
||||
if (page.getNodeState(@ptrCast(@alignCast(self)))) |state| {
|
||||
if (state.active_element) |ae| {
|
||||
return ae;
|
||||
}
|
||||
}
|
||||
|
||||
if (try parser.documentHTMLBody(page.window.document)) |body| {
|
||||
return @ptrCast(@alignCast(body));
|
||||
}
|
||||
|
||||
return try parser.documentGetDocumentElement(self);
|
||||
}
|
||||
|
||||
pub fn get_activeElement(self: *parser.Document, page: *Page) !?ElementUnion {
|
||||
const ae = (try getActiveElement(self, page)) orelse return null;
|
||||
return try Element.toInterface(ae);
|
||||
}
|
||||
|
||||
// TODO: some elements can't be focused, like if they're disabled
|
||||
// but there doesn't seem to be a generic way to check this. For example
|
||||
// we could look for the "disabled" attribute, but that's only meaningful
|
||||
// on certain types, and libdom's vtable doesn't seem to expose this.
|
||||
pub fn setFocus(self: *parser.Document, e: *parser.ElementHTML, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
state.active_element = @ptrCast(e);
|
||||
}
|
||||
|
||||
pub fn _createRange(_: *parser.Document, page: *Page) Range {
|
||||
return Range.constructor(page);
|
||||
}
|
||||
|
||||
// TODO: dummy implementation
|
||||
pub fn get_styleSheets(_: *parser.Document) []CSSStyleSheet {
|
||||
return &.{};
|
||||
}
|
||||
|
||||
pub fn get_adoptedStyleSheets(self: *parser.Document, page: *Page) !Env.JsObject {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
if (state.adopted_style_sheets) |obj| {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const obj = try page.main_context.newArray(0).persist();
|
||||
state.adopted_style_sheets = obj;
|
||||
return obj;
|
||||
}
|
||||
|
||||
pub fn set_adoptedStyleSheets(self: *parser.Document, sheets: Env.JsObject, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
state.adopted_style_sheets = try sheets.persist();
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.Document" {
|
||||
try testing.htmlRunner("dom/document.html");
|
||||
test "Browser.DOM.Document" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.__proto__.__proto__.constructor.name", "Document" },
|
||||
.{ "document.__proto__.__proto__.__proto__.constructor.name", "Node" },
|
||||
.{ "document.__proto__.__proto__.__proto__.__proto__.constructor.name", "EventTarget" },
|
||||
|
||||
.{ "let newdoc = new Document()", "undefined" },
|
||||
.{ "newdoc.documentElement", "null" },
|
||||
.{ "newdoc.children.length", "0" },
|
||||
.{ "newdoc.getElementsByTagName('*').length", "0" },
|
||||
.{ "newdoc.getElementsByTagName('*').item(0)", "null" },
|
||||
.{ "newdoc.inputEncoding === document.inputEncoding", "true" },
|
||||
.{ "newdoc.documentURI === document.documentURI", "true" },
|
||||
.{ "newdoc.URL === document.URL", "true" },
|
||||
.{ "newdoc.compatMode === document.compatMode", "true" },
|
||||
.{ "newdoc.characterSet === document.characterSet", "true" },
|
||||
.{ "newdoc.charset === document.charset", "true" },
|
||||
.{ "newdoc.contentType === document.contentType", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let getElementById = document.getElementById('content')", "undefined" },
|
||||
.{ "getElementById.constructor.name", "HTMLDivElement" },
|
||||
.{ "getElementById.localName", "div" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let getElementsByTagName = document.getElementsByTagName('p')", "undefined" },
|
||||
.{ "getElementsByTagName.length", "2" },
|
||||
.{ "getElementsByTagName.item(0).localName", "p" },
|
||||
.{ "getElementsByTagName.item(1).localName", "p" },
|
||||
.{ "let getElementsByTagNameAll = document.getElementsByTagName('*')", "undefined" },
|
||||
.{ "getElementsByTagNameAll.length", "8" },
|
||||
.{ "getElementsByTagNameAll.item(0).localName", "html" },
|
||||
.{ "getElementsByTagNameAll.item(7).localName", "p" },
|
||||
.{ "getElementsByTagNameAll.namedItem('para-empty-child').localName", "span" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let ok = document.getElementsByClassName('ok')", "undefined" },
|
||||
.{ "ok.length", "2" },
|
||||
.{ "let empty = document.getElementsByClassName('empty')", "undefined" },
|
||||
.{ "empty.length", "1" },
|
||||
.{ "let emptyok = document.getElementsByClassName('empty ok')", "undefined" },
|
||||
.{ "emptyok.length", "1" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let e = document.documentElement", "undefined" },
|
||||
.{ "e.localName", "html" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.characterSet", "UTF-8" },
|
||||
.{ "document.charset", "UTF-8" },
|
||||
.{ "document.inputEncoding", "UTF-8" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.compatMode", "CSS1Compat" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.contentType", "text/html" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.documentURI", "about:blank" },
|
||||
.{ "document.URL", "about:blank" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let impl = document.implementation", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let d = new Document()", "undefined" },
|
||||
.{ "d.characterSet", "UTF-8" },
|
||||
.{ "d.URL", "about:blank" },
|
||||
.{ "d.documentURI", "about:blank" },
|
||||
.{ "d.compatMode", "CSS1Compat" },
|
||||
.{ "d.contentType", "text/html" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var v = document.createDocumentFragment()", "undefined" },
|
||||
.{ "v.nodeName", "#document-fragment" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var v = document.createTextNode('foo')", "undefined" },
|
||||
.{ "v.nodeName", "#text" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var v = document.createCDATASection('foo')", "undefined" },
|
||||
.{ "v.nodeName", "#cdata-section" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var v = document.createComment('foo')", "undefined" },
|
||||
.{ "v.nodeName", "#comment" },
|
||||
.{ "let v2 = v.cloneNode()", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let pi = document.createProcessingInstruction('foo', 'bar')", "undefined" },
|
||||
.{ "pi.target", "foo" },
|
||||
.{ "let pi2 = pi.cloneNode()", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let nimp = document.getElementById('content')", "undefined" },
|
||||
.{ "var v = document.importNode(nimp)", "undefined" },
|
||||
.{ "v.nodeName", "DIV" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var v = document.createAttribute('foo')", "undefined" },
|
||||
.{ "v.nodeName", "foo" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.children.length", "1" },
|
||||
.{ "document.children.item(0).nodeName", "HTML" },
|
||||
.{ "document.firstElementChild.nodeName", "HTML" },
|
||||
.{ "document.lastElementChild.nodeName", "HTML" },
|
||||
.{ "document.childElementCount", "1" },
|
||||
|
||||
.{ "let nd = new Document()", "undefined" },
|
||||
.{ "nd.children.length", "0" },
|
||||
.{ "nd.children.item(0)", "null" },
|
||||
.{ "nd.firstElementChild", "null" },
|
||||
.{ "nd.lastElementChild", "null" },
|
||||
.{ "nd.childElementCount", "0" },
|
||||
|
||||
.{ "let emptydoc = document.createElement('html')", "undefined" },
|
||||
.{ "emptydoc.prepend(document.createElement('html'))", "undefined" },
|
||||
|
||||
.{ "let emptydoc2 = document.createElement('html')", "undefined" },
|
||||
.{ "emptydoc2.append(document.createElement('html'))", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.querySelector('')", "null" },
|
||||
.{ "document.querySelector('*').nodeName", "HTML" },
|
||||
.{ "document.querySelector('#content').id", "content" },
|
||||
.{ "document.querySelector('#para').id", "para" },
|
||||
.{ "document.querySelector('.ok').id", "link" },
|
||||
.{ "document.querySelector('a ~ p').id", "para-empty" },
|
||||
.{ "document.querySelector(':root').nodeName", "HTML" },
|
||||
|
||||
.{ "document.querySelectorAll('p').length", "2" },
|
||||
.{
|
||||
\\ Array.from(document.querySelectorAll('#content > p#para-empty'))
|
||||
\\ .map(row => row.querySelector('span').textContent)
|
||||
\\ .length;
|
||||
,
|
||||
"1",
|
||||
},
|
||||
}, .{});
|
||||
|
||||
// this test breaks the doc structure, keep it at the end of the test
|
||||
// suite.
|
||||
try runner.testCases(&.{
|
||||
.{ "let nadop = document.getElementById('content')", "undefined" },
|
||||
.{ "var v = document.adoptNode(nadop)", "undefined" },
|
||||
.{ "v.nodeName", "DIV" },
|
||||
}, .{});
|
||||
|
||||
const Case = testing.JsRunner.Case;
|
||||
const tags = comptime parser.Tag.all();
|
||||
var createElements: [(tags.len) * 2]Case = undefined;
|
||||
inline for (tags, 0..) |tag, i| {
|
||||
const tag_name = @tagName(tag);
|
||||
createElements[i * 2] = Case{
|
||||
"var " ++ tag_name ++ "Elem = document.createElement('" ++ tag_name ++ "')",
|
||||
"undefined",
|
||||
};
|
||||
createElements[(i * 2) + 1] = Case{
|
||||
tag_name ++ "Elem.localName",
|
||||
tag_name,
|
||||
};
|
||||
}
|
||||
try runner.testCases(&createElements, .{});
|
||||
}
|
||||
|
||||
@@ -16,13 +16,8 @@
|
||||
// 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 css = @import("css.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
const NodeList = @import("nodelist.zig").NodeList;
|
||||
const Element = @import("element.zig").Element;
|
||||
const ElementUnion = @import("element.zig").Union;
|
||||
const collection = @import("html_collection.zig");
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
const Node = @import("node.zig").Node;
|
||||
|
||||
@@ -32,9 +27,9 @@ pub const DocumentFragment = struct {
|
||||
pub const prototype = *Node;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *const Page) !*parser.DocumentFragment {
|
||||
pub fn constructor(state: *const SessionState) !*parser.DocumentFragment {
|
||||
return parser.documentCreateDocumentFragment(
|
||||
parser.documentHTMLToDocument(page.window.document),
|
||||
parser.documentHTMLToDocument(state.document.?),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,39 +53,22 @@ pub const DocumentFragment = struct {
|
||||
pub fn _replaceChildren(self: *parser.DocumentFragment, nodes: []const Node.NodeOrText) !void {
|
||||
return Node.replaceChildren(parser.documentFragmentToNode(self), nodes);
|
||||
}
|
||||
|
||||
pub fn _querySelector(self: *parser.DocumentFragment, selector: []const u8, page: *Page) !?ElementUnion {
|
||||
if (selector.len == 0) return null;
|
||||
|
||||
const n = try css.querySelector(page.call_arena, parser.documentFragmentToNode(self), selector);
|
||||
|
||||
if (n == null) return null;
|
||||
|
||||
return try Element.toInterface(parser.nodeToElement(n.?));
|
||||
}
|
||||
|
||||
pub fn _querySelectorAll(self: *parser.DocumentFragment, selector: []const u8, page: *Page) !NodeList {
|
||||
return css.querySelectorAll(page.arena, parser.documentFragmentToNode(self), selector);
|
||||
}
|
||||
|
||||
pub fn get_childElementCount(self: *parser.DocumentFragment) !u32 {
|
||||
var children = try get_children(self);
|
||||
return children.get_length();
|
||||
}
|
||||
|
||||
pub fn get_children(self: *parser.DocumentFragment) !collection.HTMLCollection {
|
||||
return collection.HTMLCollectionChildren(parser.documentFragmentToNode(self), .{
|
||||
.include_root = false,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn _getElementById(self: *parser.DocumentFragment, id: []const u8) !?ElementUnion {
|
||||
const e = try parser.nodeGetElementById(@ptrCast(@alignCast(self)), id) orelse return null;
|
||||
return try Element.toInterface(e);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.DocumentFragment" {
|
||||
try testing.htmlRunner("dom/document_fragment.html");
|
||||
test "Browser.DOM.DocumentFragment" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const dc = new DocumentFragment()", "undefined" },
|
||||
.{ "dc.constructor.name", "DocumentFragment" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const dc1 = new DocumentFragment()", "undefined" },
|
||||
.{ "const dc2 = new DocumentFragment()", "undefined" },
|
||||
.{ "dc1.isEqualNode(dc1)", "true" },
|
||||
.{ "dc1.isEqualNode(dc2)", "true" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -62,6 +62,19 @@ pub const DocumentType = struct {
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.DocumentType" {
|
||||
try testing.htmlRunner("dom/document_type.html");
|
||||
test "Browser.DOM.DocumentType" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let dt1 = document.implementation.createDocumentType('qname1', 'pid1', 'sys1');", "undefined" },
|
||||
.{ "let dt2 = document.implementation.createDocumentType('qname2', 'pid2', 'sys2');", "undefined" },
|
||||
.{ "let dt3 = document.implementation.createDocumentType('qname1', 'pid1', 'sys1');", "undefined" },
|
||||
.{ "dt1.isEqualNode(dt1)", "true" },
|
||||
.{ "dt1.isEqualNode(dt3)", "true" },
|
||||
.{ "dt1.isEqualNode(dt2)", "false" },
|
||||
.{ "dt2.isEqualNode(dt3)", "false" },
|
||||
.{ "dt1.isEqualNode(document)", "false" },
|
||||
.{ "document.isEqualNode(dt1)", "false" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -23,35 +23,18 @@ const NamedNodeMap = @import("namednodemap.zig").NamedNodeMap;
|
||||
const DOMTokenList = @import("token_list.zig");
|
||||
const NodeList = @import("nodelist.zig");
|
||||
const Node = @import("node.zig");
|
||||
const ResizeObserver = @import("resize_observer.zig");
|
||||
const MutationObserver = @import("mutation_observer.zig");
|
||||
const IntersectionObserver = @import("intersection_observer.zig");
|
||||
const DOMParser = @import("dom_parser.zig").DOMParser;
|
||||
const TreeWalker = @import("tree_walker.zig").TreeWalker;
|
||||
const NodeIterator = @import("node_iterator.zig").NodeIterator;
|
||||
const NodeFilter = @import("node_filter.zig").NodeFilter;
|
||||
const PerformanceObserver = @import("performance_observer.zig").PerformanceObserver;
|
||||
|
||||
pub const Interfaces = .{
|
||||
DOMException,
|
||||
EventTarget,
|
||||
DOMImplementation,
|
||||
NamedNodeMap,
|
||||
NamedNodeMap.Iterator,
|
||||
DOMTokenList.Interfaces,
|
||||
NodeList.Interfaces,
|
||||
Node.Node,
|
||||
Node.Interfaces,
|
||||
ResizeObserver.Interfaces,
|
||||
MutationObserver.Interfaces,
|
||||
IntersectionObserver.Interfaces,
|
||||
DOMParser,
|
||||
TreeWalker,
|
||||
NodeIterator,
|
||||
NodeFilter,
|
||||
@import("performance.zig").Interfaces,
|
||||
PerformanceObserver,
|
||||
@import("range.zig").Interfaces,
|
||||
@import("Animation.zig"),
|
||||
@import("MessageChannel.zig").Interfaces,
|
||||
};
|
||||
|
||||
@@ -1,41 +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");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/DOMParser
|
||||
pub const DOMParser = struct {
|
||||
pub fn constructor() !DOMParser {
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn _parseFromString(_: *DOMParser, string: []const u8, mime_type: []const u8) !*parser.DocumentHTML {
|
||||
if (!std.mem.eql(u8, mime_type, "text/html")) {
|
||||
// TODO: Support XML
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
return try parser.documentHTMLParseFromStr(string);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.Parser" {
|
||||
try testing.htmlRunner("dom/dom_parser.html");
|
||||
}
|
||||
@@ -19,24 +19,20 @@
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
const css = @import("css.zig");
|
||||
const log = @import("../../log.zig");
|
||||
const dump = @import("../dump.zig");
|
||||
const collection = @import("html_collection.zig");
|
||||
const dump = @import("../dump.zig");
|
||||
const css = @import("css.zig");
|
||||
|
||||
const Node = @import("node.zig").Node;
|
||||
const Walker = @import("walker.zig").WalkerDepthFirst;
|
||||
const NodeList = @import("nodelist.zig").NodeList;
|
||||
const HTMLElem = @import("../html/elements.zig");
|
||||
const ShadowRoot = @import("../dom/shadow_root.zig").ShadowRoot;
|
||||
|
||||
const Animation = @import("Animation.zig");
|
||||
const JsObject = @import("../env.zig").JsObject;
|
||||
|
||||
pub const Union = @import("../html/elements.zig").Union;
|
||||
|
||||
const log = std.log.scoped(.element);
|
||||
|
||||
// WEB IDL https://dom.spec.whatwg.org/#element
|
||||
pub const Element = struct {
|
||||
pub const Self = parser.Element;
|
||||
@@ -48,40 +44,11 @@ pub const Element = struct {
|
||||
y: f64,
|
||||
width: f64,
|
||||
height: f64,
|
||||
bottom: f64,
|
||||
right: f64,
|
||||
top: f64,
|
||||
left: f64,
|
||||
};
|
||||
|
||||
pub fn toInterface(e: *parser.Element) !Union {
|
||||
return toInterfaceT(Union, e);
|
||||
}
|
||||
|
||||
pub fn toInterfaceT(comptime T: type, e: *parser.Element) !T {
|
||||
const tagname = try parser.elementGetTagName(e) orelse {
|
||||
// If the owner's document is HTML, assume we have an HTMLElement.
|
||||
const doc = try parser.nodeOwnerDocument(parser.elementToNode(e));
|
||||
if (doc != null and !doc.?.is_html) {
|
||||
return .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(e)) };
|
||||
}
|
||||
|
||||
return .{ .Element = e };
|
||||
};
|
||||
|
||||
// TODO SVGElement and MathML are not supported yet.
|
||||
|
||||
const tag = parser.Tag.fromString(tagname) catch {
|
||||
// If the owner's document is HTML, assume we have an HTMLElement.
|
||||
const doc = try parser.nodeOwnerDocument(parser.elementToNode(e));
|
||||
if (doc != null and doc.?.is_html) {
|
||||
return .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(e)) };
|
||||
}
|
||||
|
||||
return .{ .Element = e };
|
||||
};
|
||||
|
||||
return HTMLElem.toInterfaceFromTag(T, e, tag);
|
||||
return try HTMLElem.toInterface(Union, e);
|
||||
// SVGElement and MathML are not supported yet.
|
||||
}
|
||||
|
||||
// JS funcs
|
||||
@@ -136,19 +103,19 @@ pub const Element = struct {
|
||||
return try parser.nodeGetAttributes(parser.elementToNode(self)) orelse unreachable;
|
||||
}
|
||||
|
||||
pub fn get_innerHTML(self: *parser.Element, page: *Page) ![]const u8 {
|
||||
var aw = std.Io.Writer.Allocating.init(page.call_arena);
|
||||
try dump.writeChildren(parser.elementToNode(self), .{}, &aw.writer);
|
||||
return aw.written();
|
||||
pub fn get_innerHTML(self: *parser.Element, state: *SessionState) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).init(state.arena);
|
||||
try dump.writeChildren(parser.elementToNode(self), buf.writer());
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
pub fn get_outerHTML(self: *parser.Element, page: *Page) ![]const u8 {
|
||||
var aw = std.Io.Writer.Allocating.init(page.call_arena);
|
||||
try dump.writeNode(parser.elementToNode(self), .{}, &aw.writer);
|
||||
return aw.written();
|
||||
pub fn get_outerHTML(self: *parser.Element, state: *SessionState) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).init(state.arena);
|
||||
try dump.writeNode(parser.elementToNode(self), buf.writer());
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
pub fn set_innerHTML(self: *parser.Element, str: []const u8, page: *Page) !void {
|
||||
pub fn set_innerHTML(self: *parser.Element, str: []const u8) !void {
|
||||
const node = parser.elementToNode(self);
|
||||
const doc = try parser.nodeOwnerDocument(node) orelse return parser.DOMError.WrongDocument;
|
||||
// parse the fragment
|
||||
@@ -157,79 +124,31 @@ pub const Element = struct {
|
||||
// remove existing children
|
||||
try Node.removeChildren(node);
|
||||
|
||||
const fragment_node = parser.documentFragmentToNode(fragment);
|
||||
// get fragment body children
|
||||
const children = try parser.documentFragmentBodyChildren(fragment) orelse return;
|
||||
|
||||
// I'm not sure what the exact behavior is supposed to be. Initially,
|
||||
// we were only copying the body of the document fragment. But it seems
|
||||
// like head elements should be copied too. Specifically, some sites
|
||||
// create script tags via innerHTML, which we need to capture.
|
||||
// If you play with this in a browser, you should notice that the
|
||||
// behavior is different depending on whether you're in a blank page
|
||||
// or an actual document. In a blank page, something like:
|
||||
// x.innerHTML = '<script></script>';
|
||||
// does _not_ create an empty script, but in a real page, it does. Weird.
|
||||
const html = try parser.nodeFirstChild(fragment_node) orelse return;
|
||||
const head = try parser.nodeFirstChild(html) orelse return;
|
||||
const body = try parser.nodeNextSibling(head) orelse return;
|
||||
|
||||
if (try parser.elementTag(self) == .template) {
|
||||
// HTMLElementTemplate is special. We don't append these as children
|
||||
// of the template, but instead set its content as the body of the
|
||||
// fragment. Simpler to do this by copying the body children into
|
||||
// a new fragment
|
||||
const clean = try parser.documentCreateDocumentFragment(doc);
|
||||
const children = try parser.nodeGetChildNodes(body);
|
||||
const ln = try parser.nodeListLength(children);
|
||||
for (0..ln) |_| {
|
||||
// always index 0, because nodeAppendChild moves the node out of
|
||||
// the nodeList and into the new tree
|
||||
const child = try parser.nodeListItem(children, 0) orelse continue;
|
||||
_ = try parser.nodeAppendChild(@ptrCast(@alignCast(clean)), child);
|
||||
}
|
||||
|
||||
const state = try page.getOrCreateNodeState(node);
|
||||
state.template_content = clean;
|
||||
return;
|
||||
}
|
||||
|
||||
// For any node other than a template, we copy the head and body elements
|
||||
// as child nodes of the element
|
||||
{
|
||||
// First, copy some of the head element
|
||||
const children = try parser.nodeGetChildNodes(head);
|
||||
const ln = try parser.nodeListLength(children);
|
||||
for (0..ln) |_| {
|
||||
// always index 0, because nodeAppendChild moves the node out of
|
||||
// the nodeList and into the new tree
|
||||
const child = try parser.nodeListItem(children, 0) orelse continue;
|
||||
_ = try parser.nodeAppendChild(node, child);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const children = try parser.nodeGetChildNodes(body);
|
||||
const ln = try parser.nodeListLength(children);
|
||||
for (0..ln) |_| {
|
||||
// always index 0, because nodeAppendChild moves the node out of
|
||||
// the nodeList and into the new tree
|
||||
const child = try parser.nodeListItem(children, 0) orelse continue;
|
||||
_ = try parser.nodeAppendChild(node, child);
|
||||
}
|
||||
// append children to the node
|
||||
const ln = try parser.nodeListLength(children);
|
||||
var i: u32 = 0;
|
||||
while (i < ln) {
|
||||
defer i += 1;
|
||||
const child = try parser.nodeListItem(children, i) orelse continue;
|
||||
_ = try parser.nodeAppendChild(node, child);
|
||||
}
|
||||
}
|
||||
|
||||
// The closest() method of the Element interface traverses the element and its parents (heading toward the document root) until it finds a node that matches the specified CSS selector.
|
||||
// Returns the closest ancestor Element or itself, which matches the selectors. If there are no such element, null.
|
||||
pub fn _closest(self: *parser.Element, selector: []const u8, page: *Page) !?*parser.Element {
|
||||
pub fn _closest(self: *parser.Element, selector: []const u8, state: *SessionState) !?*parser.Element {
|
||||
const cssParse = @import("../css/css.zig").parse;
|
||||
const CssNodeWrap = @import("../css/libdom.zig").Node;
|
||||
const select = try cssParse(page.call_arena, selector, .{});
|
||||
const select = try cssParse(state.call_arena, selector, .{});
|
||||
|
||||
var current: CssNodeWrap = .{ .node = parser.elementToNode(self) };
|
||||
while (true) {
|
||||
if (try select.match(current)) {
|
||||
if (!current.isElement()) {
|
||||
log.err(.browser, "closest invalid type", .{ .type = try current.tag() });
|
||||
log.err("closest: is not an element: {s}", .{try current.tag()});
|
||||
return null;
|
||||
}
|
||||
return parser.nodeToElement(current.node);
|
||||
@@ -238,14 +157,8 @@ pub const Element = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// don't use parser.nodeHasAttributes(...) because that returns true/false
|
||||
// based on the type, e.g. a node never as attributes, an element always has
|
||||
// attributes. But, Element.hasAttributes is supposed to return true only
|
||||
// if the element has at least 1 attribute.
|
||||
pub fn _hasAttributes(self: *parser.Element) !bool {
|
||||
// an element _must_ have at least an empty attribute
|
||||
const node_map = try parser.nodeGetAttributes(parser.elementToNode(self)) orelse unreachable;
|
||||
return try parser.namedNodeMapGetLength(node_map) > 0;
|
||||
return try parser.nodeHasAttributes(parser.elementToNode(self));
|
||||
}
|
||||
|
||||
pub fn _getAttribute(self: *parser.Element, qname: []const u8) !?[]const u8 {
|
||||
@@ -314,22 +227,6 @@ pub const Element = struct {
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn _getAttributeNames(self: *parser.Element, page: *Page) ![]const []const u8 {
|
||||
const attributes = try parser.nodeGetAttributes(@ptrCast(self)) orelse return &.{};
|
||||
const ln = try parser.namedNodeMapGetLength(attributes);
|
||||
|
||||
const names = try page.call_arena.alloc([]const u8, ln);
|
||||
var at: usize = 0;
|
||||
|
||||
for (0..ln) |i| {
|
||||
const attribute = try parser.namedNodeMapItem(attributes, @intCast(i)) orelse break;
|
||||
names[at] = try parser.attributeGetName(attribute);
|
||||
at += 1;
|
||||
}
|
||||
|
||||
return names[0..at];
|
||||
}
|
||||
|
||||
pub fn _getAttributeNode(self: *parser.Element, name: []const u8) !?*parser.Attribute {
|
||||
return try parser.elementGetAttributeNode(self, name);
|
||||
}
|
||||
@@ -353,35 +250,33 @@ pub const Element = struct {
|
||||
pub fn _getElementsByTagName(
|
||||
self: *parser.Element,
|
||||
tag_name: []const u8,
|
||||
page: *Page,
|
||||
state: *SessionState,
|
||||
) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(
|
||||
page.arena,
|
||||
state.arena,
|
||||
parser.elementToNode(self),
|
||||
tag_name,
|
||||
.{ .include_root = false },
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn _getElementsByClassName(
|
||||
self: *parser.Element,
|
||||
classNames: []const u8,
|
||||
page: *Page,
|
||||
state: *SessionState,
|
||||
) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByClassName(
|
||||
page.arena,
|
||||
state.arena,
|
||||
parser.elementToNode(self),
|
||||
classNames,
|
||||
.{ .include_root = false },
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
// ParentNode
|
||||
// https://dom.spec.whatwg.org/#parentnode
|
||||
pub fn get_children(self: *parser.Element) !collection.HTMLCollection {
|
||||
return collection.HTMLCollectionChildren(parser.elementToNode(self), .{
|
||||
.include_root = false,
|
||||
});
|
||||
return try collection.HTMLCollectionChildren(parser.elementToNode(self), false);
|
||||
}
|
||||
|
||||
pub fn get_firstElementChild(self: *parser.Element) !?Union {
|
||||
@@ -409,13 +304,13 @@ pub const Element = struct {
|
||||
pub fn get_previousElementSibling(self: *parser.Element) !?Union {
|
||||
const res = try parser.nodePreviousElementSibling(parser.elementToNode(self));
|
||||
if (res == null) return null;
|
||||
return try toInterface(res.?);
|
||||
return try HTMLElem.toInterface(HTMLElem.Union, res.?);
|
||||
}
|
||||
|
||||
pub fn get_nextElementSibling(self: *parser.Element) !?Union {
|
||||
const res = try parser.nodeNextElementSibling(parser.elementToNode(self));
|
||||
if (res == null) return null;
|
||||
return try toInterface(res.?);
|
||||
return try HTMLElem.toInterface(HTMLElem.Union, res.?);
|
||||
}
|
||||
|
||||
fn getElementById(self: *parser.Element, id: []const u8) !?*parser.Node {
|
||||
@@ -434,18 +329,18 @@ pub const Element = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _querySelector(self: *parser.Element, selector: []const u8, page: *Page) !?Union {
|
||||
pub fn _querySelector(self: *parser.Element, selector: []const u8, state: *SessionState) !?Union {
|
||||
if (selector.len == 0) return null;
|
||||
|
||||
const n = try css.querySelector(page.call_arena, parser.elementToNode(self), selector);
|
||||
const n = try css.querySelector(state.arena, parser.elementToNode(self), selector);
|
||||
|
||||
if (n == null) return null;
|
||||
|
||||
return try toInterface(parser.nodeToElement(n.?));
|
||||
}
|
||||
|
||||
pub fn _querySelectorAll(self: *parser.Element, selector: []const u8, page: *Page) !NodeList {
|
||||
return css.querySelectorAll(page.arena, parser.elementToNode(self), selector);
|
||||
pub fn _querySelectorAll(self: *parser.Element, selector: []const u8, state: *SessionState) !NodeList {
|
||||
return css.querySelectorAll(state.arena, parser.elementToNode(self), selector);
|
||||
}
|
||||
|
||||
pub fn _prepend(self: *parser.Element, nodes: []const Node.NodeOrText) !void {
|
||||
@@ -470,140 +365,247 @@ pub const Element = struct {
|
||||
return Node.replaceChildren(parser.elementToNode(self), nodes);
|
||||
}
|
||||
|
||||
// A DOMRect object providing information about the size of an element and its position relative to the viewport.
|
||||
// Returns a 0 DOMRect object if the element is eventually detached from the main window
|
||||
pub fn _getBoundingClientRect(self: *parser.Element, page: *Page) !DOMRect {
|
||||
// Since we are lazy rendering we need to do this check. We could store the renderer in a viewport such that it could cache these, but it would require tracking changes.
|
||||
if (!try page.isNodeAttached(parser.elementToNode(self))) {
|
||||
return DOMRect{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.width = 0,
|
||||
.height = 0,
|
||||
.bottom = 0,
|
||||
.right = 0,
|
||||
.top = 0,
|
||||
.left = 0,
|
||||
};
|
||||
}
|
||||
return page.renderer.getRect(self);
|
||||
pub fn _getBoundingClientRect(self: *parser.Element, state: *SessionState) !DOMRect {
|
||||
return state.renderer.getRect(self);
|
||||
}
|
||||
|
||||
// Returns a collection of DOMRect objects that indicate the bounding rectangles for each CSS border box in a client.
|
||||
// We do not render so it only always return the element's bounding rect.
|
||||
// Returns an empty array if the element is eventually detached from the main window
|
||||
pub fn _getClientRects(self: *parser.Element, page: *Page) ![]DOMRect {
|
||||
if (!try page.isNodeAttached(parser.elementToNode(self))) {
|
||||
return &.{};
|
||||
}
|
||||
const heap_ptr = try page.call_arena.create(DOMRect);
|
||||
heap_ptr.* = try page.renderer.getRect(self);
|
||||
return heap_ptr[0..1];
|
||||
// returns a collection of DOMRect objects that indicate the bounding rectangles for each CSS border box in a client.
|
||||
// We do not render so just always return the element's rect.
|
||||
pub fn _getClientRects(self: *parser.Element, state: *SessionState) ![1]DOMRect {
|
||||
return [_]DOMRect{try state.renderer.getRect(self)};
|
||||
}
|
||||
|
||||
pub fn get_clientWidth(_: *parser.Element, page: *Page) u32 {
|
||||
return page.renderer.width();
|
||||
pub fn get_clientWidth(_: *parser.Element, state: *SessionState) u32 {
|
||||
return state.renderer.width();
|
||||
}
|
||||
|
||||
pub fn get_clientHeight(_: *parser.Element, page: *Page) u32 {
|
||||
return page.renderer.height();
|
||||
pub fn get_clientHeight(_: *parser.Element, state: *SessionState) u32 {
|
||||
return state.renderer.height();
|
||||
}
|
||||
|
||||
pub fn _matches(self: *parser.Element, selectors: []const u8, page: *Page) !bool {
|
||||
pub fn _matches(self: *parser.Element, selectors: []const u8, state: *SessionState) !bool {
|
||||
const cssParse = @import("../css/css.zig").parse;
|
||||
const CssNodeWrap = @import("../css/libdom.zig").Node;
|
||||
const s = try cssParse(page.call_arena, selectors, .{});
|
||||
const s = try cssParse(state.call_arena, selectors, .{});
|
||||
return s.match(CssNodeWrap{ .node = parser.elementToNode(self) });
|
||||
}
|
||||
|
||||
pub fn _scrollIntoViewIfNeeded(_: *parser.Element, center_if_needed: ?bool) void {
|
||||
_ = center_if_needed;
|
||||
}
|
||||
|
||||
const CheckVisibilityOpts = struct {
|
||||
contentVisibilityAuto: bool,
|
||||
opacityProperty: bool,
|
||||
visibilityProperty: bool,
|
||||
};
|
||||
|
||||
pub fn _checkVisibility(self: *parser.Element, opts: ?CheckVisibilityOpts) bool {
|
||||
_ = self;
|
||||
_ = opts;
|
||||
return true;
|
||||
}
|
||||
|
||||
const AttachShadowOpts = struct {
|
||||
mode: []const u8, // must be specified
|
||||
};
|
||||
pub fn _attachShadow(self: *parser.Element, opts: AttachShadowOpts, page: *Page) !*ShadowRoot {
|
||||
const mode = std.meta.stringToEnum(ShadowRoot.Mode, opts.mode) orelse return error.InvalidArgument;
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
if (state.shadow_root) |sr| {
|
||||
if (mode != sr.mode) {
|
||||
// this is the behavior per the spec
|
||||
return error.NotSupportedError;
|
||||
}
|
||||
|
||||
try Node.removeChildren(@ptrCast(@alignCast(sr.proto)));
|
||||
return sr;
|
||||
}
|
||||
|
||||
// Not sure what to do if there is no owner document
|
||||
const doc = try parser.nodeOwnerDocument(@ptrCast(self)) orelse return error.InvalidArgument;
|
||||
const fragment = try parser.documentCreateDocumentFragment(doc);
|
||||
const sr = try page.arena.create(ShadowRoot);
|
||||
sr.* = .{
|
||||
.host = self,
|
||||
.mode = mode,
|
||||
.proto = fragment,
|
||||
};
|
||||
state.shadow_root = sr;
|
||||
parser.documentFragmentSetHost(sr.proto, @ptrCast(@alignCast(self)));
|
||||
|
||||
// Storing the ShadowRoot on the element makes sense, it's the ShadowRoot's
|
||||
// parent. When we render, we go top-down, so we'll have the element, get
|
||||
// its shadowroot, and go on. that's what the above code does.
|
||||
// But we sometimes need to go bottom-up, e.g when we have a slot element
|
||||
// and want to find the containing parent. Unforatunately , we don't have
|
||||
// that link, so we need to create it. In the DOM, the ShadowRoot is
|
||||
// represented by this DocumentFragment (it's the ShadowRoot's base prototype)
|
||||
// So we can also store the ShadowRoot in the DocumentFragment's state.
|
||||
const fragment_state = try page.getOrCreateNodeState(@ptrCast(@alignCast(fragment)));
|
||||
fragment_state.shadow_root = sr;
|
||||
|
||||
return sr;
|
||||
}
|
||||
|
||||
pub fn get_shadowRoot(self: *parser.Element, page: *Page) ?*ShadowRoot {
|
||||
const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null;
|
||||
const sr = state.shadow_root orelse return null;
|
||||
if (sr.mode == .closed) {
|
||||
return null;
|
||||
}
|
||||
return sr;
|
||||
}
|
||||
|
||||
pub fn _animate(self: *parser.Element, effect: JsObject, opts: JsObject) !Animation {
|
||||
_ = self;
|
||||
_ = opts;
|
||||
return Animation.constructor(effect, null);
|
||||
}
|
||||
|
||||
pub fn _remove(self: *parser.Element) !void {
|
||||
// TODO: This hasn't been tested to make sure all references to this
|
||||
// node are properly updated. A lot of libdom is lazy and will look
|
||||
// for related elements JIT by walking the tree, but there could be
|
||||
// cases in libdom or the Zig WebAPI where this reference is kept
|
||||
const as_node: *parser.Node = @ptrCast(self);
|
||||
const parent = try parser.nodeParentNode(as_node) orelse return;
|
||||
_ = try Node._removeChild(parent, as_node);
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.Element" {
|
||||
try testing.htmlRunner("dom/element.html");
|
||||
test "Browser.DOM.Element" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let g = document.getElementById('content')", "undefined" },
|
||||
.{ "g.namespaceURI", "http://www.w3.org/1999/xhtml" },
|
||||
.{ "g.prefix", "null" },
|
||||
.{ "g.localName", "div" },
|
||||
.{ "g.tagName", "DIV" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let gs = document.getElementById('content')", "undefined" },
|
||||
.{ "gs.id", "content" },
|
||||
.{ "gs.id = 'foo'", "foo" },
|
||||
.{ "gs.id", "foo" },
|
||||
.{ "gs.id = 'content'", "content" },
|
||||
.{ "gs.className", "" },
|
||||
.{ "let gs2 = document.getElementById('para-empty')", "undefined" },
|
||||
.{ "gs2.className", "ok empty" },
|
||||
.{ "gs2.className = 'foo bar baz'", "foo bar baz" },
|
||||
.{ "gs2.className", "foo bar baz" },
|
||||
.{ "gs2.className = 'ok empty'", "ok empty" },
|
||||
.{ "let cl = gs2.classList", "undefined" },
|
||||
.{ "cl.length", "2" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const el2 = document.createElement('div');", "undefined" },
|
||||
.{ "el2.id = 'closest'; el2.className = 'ok';", "ok" },
|
||||
.{ "el2.closest('#closest')", "[object HTMLDivElement]" },
|
||||
.{ "el2.closest('.ok')", "[object HTMLDivElement]" },
|
||||
.{ "el2.closest('#9000')", "null" },
|
||||
.{ "el2.closest('.notok')", "null" },
|
||||
|
||||
.{ "const sp = document.createElement('span');", "undefined" },
|
||||
.{ "el2.appendChild(sp);", "[object HTMLSpanElement]" },
|
||||
.{ "sp.closest('#closest')", "[object HTMLDivElement]" },
|
||||
.{ "sp.closest('#9000')", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let a = document.getElementById('content')", "undefined" },
|
||||
.{ "a.hasAttributes()", "true" },
|
||||
.{ "a.attributes.length", "1" },
|
||||
|
||||
.{ "a.getAttribute('id')", "content" },
|
||||
|
||||
.{ "a.hasAttribute('foo')", "false" },
|
||||
.{ "a.getAttribute('foo')", "null" },
|
||||
|
||||
.{ "a.setAttribute('foo', 'bar')", "undefined" },
|
||||
.{ "a.hasAttribute('foo')", "true" },
|
||||
.{ "a.getAttribute('foo')", "bar" },
|
||||
|
||||
.{ "a.setAttribute('foo', 'baz')", "undefined" },
|
||||
.{ "a.hasAttribute('foo')", "true" },
|
||||
.{ "a.getAttribute('foo')", "baz" },
|
||||
|
||||
.{ "a.removeAttribute('foo')", "undefined" },
|
||||
.{ "a.hasAttribute('foo')", "false" },
|
||||
.{ "a.getAttribute('foo')", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let b = document.getElementById('content')", "undefined" },
|
||||
.{ "b.toggleAttribute('foo')", "true" },
|
||||
.{ "b.hasAttribute('foo')", "true" },
|
||||
.{ "b.getAttribute('foo')", "" },
|
||||
|
||||
.{ "b.toggleAttribute('foo')", "false" },
|
||||
.{ "b.hasAttribute('foo')", "false" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let c = document.getElementById('content')", "undefined" },
|
||||
.{ "c.children.length", "3" },
|
||||
.{ "c.firstElementChild.nodeName", "A" },
|
||||
.{ "c.lastElementChild.nodeName", "P" },
|
||||
.{ "c.childElementCount", "3" },
|
||||
|
||||
.{ "c.prepend(document.createTextNode('foo'))", "undefined" },
|
||||
.{ "c.append(document.createTextNode('bar'))", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let d = document.getElementById('para')", "undefined" },
|
||||
.{ "d.previousElementSibling.nodeName", "P" },
|
||||
.{ "d.nextElementSibling", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let e = document.getElementById('content')", "undefined" },
|
||||
.{ "e.querySelector('foo')", "null" },
|
||||
.{ "e.querySelector('#foo')", "null" },
|
||||
.{ "e.querySelector('#link').id", "link" },
|
||||
.{ "e.querySelector('#para').id", "para" },
|
||||
.{ "e.querySelector('*').id", "link" },
|
||||
.{ "e.querySelector('')", "null" },
|
||||
.{ "e.querySelector('*').id", "link" },
|
||||
.{ "e.querySelector('#content')", "null" },
|
||||
.{ "e.querySelector('#para').id", "para" },
|
||||
.{ "e.querySelector('.ok').id", "link" },
|
||||
.{ "e.querySelector('a ~ p').id", "para-empty" },
|
||||
|
||||
.{ "e.querySelectorAll('foo').length", "0" },
|
||||
.{ "e.querySelectorAll('#foo').length", "0" },
|
||||
.{ "e.querySelectorAll('#link').length", "1" },
|
||||
.{ "e.querySelectorAll('#link').item(0).id", "link" },
|
||||
.{ "e.querySelectorAll('#para').length", "1" },
|
||||
.{ "e.querySelectorAll('#para').item(0).id", "para" },
|
||||
.{ "e.querySelectorAll('*').length", "4" },
|
||||
.{ "e.querySelectorAll('p').length", "2" },
|
||||
.{ "e.querySelectorAll('.ok').item(0).id", "link" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let f = document.getElementById('content')", "undefined" },
|
||||
.{ "let ff = document.createAttribute('foo')", "undefined" },
|
||||
.{ "f.setAttributeNode(ff)", "null" },
|
||||
.{ "f.getAttributeNode('foo').name", "foo" },
|
||||
.{ "f.removeAttributeNode(ff).name", "foo" },
|
||||
.{ "f.getAttributeNode('bar')", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.getElementById('para').innerHTML", " And" },
|
||||
.{ "document.getElementById('para-empty').innerHTML.trim()", "<span id=\"para-empty-child\"></span>" },
|
||||
|
||||
.{ "let h = document.getElementById('para-empty')", "undefined" },
|
||||
.{ "const prev = h.innerHTML", "undefined" },
|
||||
.{ "h.innerHTML = '<p id=\"hello\">hello world</p>'", "<p id=\"hello\">hello world</p>" },
|
||||
.{ "h.innerHTML", "<p id=\"hello\">hello world</p>" },
|
||||
.{ "h.firstChild.nodeName", "P" },
|
||||
.{ "h.firstChild.id", "hello" },
|
||||
.{ "h.firstChild.textContent", "hello world" },
|
||||
.{ "h.innerHTML = prev; true", "true" },
|
||||
.{ "document.getElementById('para-empty').innerHTML.trim()", "<span id=\"para-empty-child\"></span>" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.getElementById('para').outerHTML", "<p id=\"para\"> And</p>" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.getElementById('para').clientWidth", "1" },
|
||||
.{ "document.getElementById('para').clientHeight", "1" },
|
||||
|
||||
.{ "let r1 = document.getElementById('para').getBoundingClientRect()", "undefined" },
|
||||
.{ "r1.x", "0" },
|
||||
.{ "r1.y", "0" },
|
||||
.{ "r1.width", "1" },
|
||||
.{ "r1.height", "1" },
|
||||
|
||||
.{ "let r2 = document.getElementById('content').getBoundingClientRect()", "undefined" },
|
||||
.{ "r2.x", "1" },
|
||||
.{ "r2.y", "0" },
|
||||
.{ "r2.width", "1" },
|
||||
.{ "r2.height", "1" },
|
||||
|
||||
.{ "let r3 = document.getElementById('para').getBoundingClientRect()", "undefined" },
|
||||
.{ "r3.x", "0" },
|
||||
.{ "r3.y", "0" },
|
||||
.{ "r3.width", "1" },
|
||||
.{ "r3.height", "1" },
|
||||
|
||||
.{ "document.getElementById('para').clientWidth", "2" },
|
||||
.{ "document.getElementById('para').clientHeight", "1" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const el = document.createElement('div');", "undefined" },
|
||||
.{ "el.id = 'matches'; el.className = 'ok';", "ok" },
|
||||
.{ "el.matches('#matches')", "true" },
|
||||
.{ "el.matches('.ok')", "true" },
|
||||
.{ "el.matches('#9000')", "false" },
|
||||
.{ "el.matches('.notok')", "false" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const el3 = document.createElement('div');", "undefined" },
|
||||
.{ "el3.scrollIntoViewIfNeeded();", "undefined" },
|
||||
.{ "el3.scrollIntoViewIfNeeded(false);", "undefined" },
|
||||
}, .{});
|
||||
|
||||
// before
|
||||
try runner.testCases(&.{
|
||||
.{ "const before_container = document.createElement('div');", "undefined" },
|
||||
.{ "document.append(before_container);", "undefined" },
|
||||
.{ "const b1 = document.createElement('div');", "undefined" },
|
||||
.{ "before_container.append(b1);", "undefined" },
|
||||
|
||||
.{ "const b1_a = document.createElement('p');", "undefined" },
|
||||
.{ "b1.before(b1_a, 'over 9000');", "undefined" },
|
||||
.{ "before_container.innerHTML", "<p></p>over 9000<div></div>" },
|
||||
}, .{});
|
||||
|
||||
// after
|
||||
try runner.testCases(&.{
|
||||
.{ "const after_container = document.createElement('div');", "undefined" },
|
||||
.{ "document.append(after_container);", "undefined" },
|
||||
.{ "const a1 = document.createElement('div');", "undefined" },
|
||||
.{ "after_container.append(a1);", "undefined" },
|
||||
|
||||
.{ "const a1_a = document.createElement('p');", "undefined" },
|
||||
.{ "a1.after('over 9000', a1_a);", "undefined" },
|
||||
.{ "after_container.innerHTML", "<div></div>over 9000<p></p>" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -16,118 +16,70 @@
|
||||
// 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 Env = @import("../env.zig").Env;
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
const EventHandler = @import("../events/event.zig").EventHandler;
|
||||
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
const nod = @import("node.zig");
|
||||
const Nod = @import("node.zig");
|
||||
|
||||
pub const Union = union(enum) {
|
||||
node: nod.Union,
|
||||
xhr: *@import("../xhr/xhr.zig").XMLHttpRequest,
|
||||
plain: *parser.EventTarget,
|
||||
message_port: *@import("MessageChannel.zig").MessagePort,
|
||||
screen: *@import("../html/screen.zig").Screen,
|
||||
screen_orientation: *@import("../html/screen.zig").ScreenOrientation,
|
||||
performance: *@import("performance.zig").Performance,
|
||||
media_query_list: *@import("../html/media_query_list.zig").MediaQueryList,
|
||||
};
|
||||
// EventTarget interfaces
|
||||
pub const Union = Nod.Union;
|
||||
|
||||
// EventTarget implementation
|
||||
pub const EventTarget = struct {
|
||||
pub const Self = parser.EventTarget;
|
||||
pub const Exception = DOMException;
|
||||
|
||||
// Extend libdom event target for pure zig struct.
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .plain },
|
||||
|
||||
pub fn toInterface(et: *parser.EventTarget, page: *Page) !Union {
|
||||
// libdom assumes that all event targets are libdom nodes. They are not.
|
||||
|
||||
switch (try parser.eventTargetInternalType(et)) {
|
||||
.libdom_node => {
|
||||
return .{ .node = try nod.Node.toInterface(@as(*parser.Node, @ptrCast(et))) };
|
||||
},
|
||||
.plain => return .{ .plain = et },
|
||||
.abort_signal => {
|
||||
// AbortSignal is a special case, it has its own internal type.
|
||||
// We return it as a node, but we need to handle it differently.
|
||||
return .{ .node = .{ .AbortSignal = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) } };
|
||||
},
|
||||
.window => {
|
||||
// The window is a common non-node target, but it's easy to handle as its a singleton.
|
||||
std.debug.assert(@intFromPtr(et) == @intFromPtr(&page.window.base));
|
||||
return .{ .node = .{ .Window = &page.window } };
|
||||
},
|
||||
.xhr => {
|
||||
const XMLHttpRequestEventTarget = @import("../xhr/event_target.zig").XMLHttpRequestEventTarget;
|
||||
const base: *XMLHttpRequestEventTarget = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et)));
|
||||
return .{ .xhr = @fieldParentPtr("proto", base) };
|
||||
},
|
||||
.message_port => {
|
||||
return .{ .message_port = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) };
|
||||
},
|
||||
.screen => {
|
||||
return .{ .screen = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) };
|
||||
},
|
||||
.screen_orientation => {
|
||||
return .{ .screen_orientation = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) };
|
||||
},
|
||||
.performance => {
|
||||
return .{ .performance = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et))) };
|
||||
},
|
||||
.media_query_list => {
|
||||
return .{ .media_query_list = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et))) };
|
||||
},
|
||||
}
|
||||
pub fn toInterface(et: *parser.EventTarget) !Union {
|
||||
// NOTE: for now we state that all EventTarget are Nodes
|
||||
// TODO: handle other types (eg. Window)
|
||||
return Nod.Node.toInterface(@as(*parser.Node, @ptrCast(et)));
|
||||
}
|
||||
|
||||
// JS funcs
|
||||
// --------
|
||||
pub fn constructor(page: *Page) !*parser.EventTarget {
|
||||
const et = try page.arena.create(EventTarget);
|
||||
return @ptrCast(&et.base);
|
||||
}
|
||||
|
||||
pub fn _addEventListener(
|
||||
self: *parser.EventTarget,
|
||||
typ: []const u8,
|
||||
listener: EventHandler.Listener,
|
||||
opts: ?EventHandler.Opts,
|
||||
page: *Page,
|
||||
) !void {
|
||||
_ = try EventHandler.register(page.arena, self, typ, listener, opts);
|
||||
}
|
||||
|
||||
const RemoveEventListenerOpts = union(enum) {
|
||||
const AddEventListenerOpts = union(enum) {
|
||||
opts: Opts,
|
||||
capture: bool,
|
||||
|
||||
const Opts = struct {
|
||||
capture: ?bool,
|
||||
once: ?bool, // currently does nothing
|
||||
passive: ?bool, // currently does nothing
|
||||
signal: ?bool, // currently does nothing
|
||||
};
|
||||
};
|
||||
|
||||
pub fn _removeEventListener(
|
||||
pub fn _addEventListener(
|
||||
self: *parser.EventTarget,
|
||||
typ: []const u8,
|
||||
listener: EventHandler.Listener,
|
||||
opts_: ?RemoveEventListenerOpts,
|
||||
cbk: Env.Callback,
|
||||
opts_: ?AddEventListenerOpts,
|
||||
state: *SessionState,
|
||||
) !void {
|
||||
var capture = false;
|
||||
if (opts_) |opts| {
|
||||
capture = switch (opts) {
|
||||
.capture => |c| c,
|
||||
.opts => |o| o.capture orelse false,
|
||||
};
|
||||
switch (opts) {
|
||||
.capture => |c| capture = c,
|
||||
.opts => |o| {
|
||||
// Done this way so that, for common cases that _only_ set
|
||||
// capture, i.e. {captrue: true}, it works.
|
||||
// But for any case that sets any of the other flags, we
|
||||
// error. If we don't error, this function call would succeed
|
||||
// but the behavior might be wrong. At this point, it's
|
||||
// better to be explicit and error.
|
||||
if (o.once orelse false) return error.NotImplemented;
|
||||
if (o.signal orelse false) return error.NotImplemented;
|
||||
if (o.passive orelse false) return error.NotImplemented;
|
||||
capture = o.capture orelse false;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const cbk = (try listener.callback(self)) orelse return;
|
||||
|
||||
// check if event target has already this listener
|
||||
const lst = try parser.eventTargetHasListener(
|
||||
self,
|
||||
@@ -135,6 +87,35 @@ pub const EventTarget = struct {
|
||||
capture,
|
||||
cbk.id,
|
||||
);
|
||||
if (lst != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eh = try EventHandler.init(state.arena, try cbk.withThis(self));
|
||||
|
||||
try parser.eventTargetAddEventListener(
|
||||
self,
|
||||
typ,
|
||||
&eh.node,
|
||||
capture,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn _removeEventListener(
|
||||
self: *parser.EventTarget,
|
||||
typ: []const u8,
|
||||
cbk: Env.Callback,
|
||||
capture: ?bool,
|
||||
// TODO: hanle EventListenerOptions
|
||||
// see #https://github.com/lightpanda-io/jsruntime-lib/issues/114
|
||||
) !void {
|
||||
// check if event target has already this listener
|
||||
const lst = try parser.eventTargetHasListener(
|
||||
self,
|
||||
typ,
|
||||
capture orelse false,
|
||||
cbk.id,
|
||||
);
|
||||
if (lst == null) {
|
||||
return;
|
||||
}
|
||||
@@ -144,16 +125,127 @@ pub const EventTarget = struct {
|
||||
self,
|
||||
typ,
|
||||
lst.?,
|
||||
capture,
|
||||
capture orelse false,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn _dispatchEvent(self: *parser.EventTarget, event: *parser.Event) !bool {
|
||||
return try parser.eventTargetDispatchEvent(self, event);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *parser.EventTarget, state: *SessionState) void {
|
||||
parser.eventTargetRemoveAllEventListeners(self, state.arena) catch unreachable;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.EventTarget" {
|
||||
try testing.htmlRunner("dom/event_target.html");
|
||||
test "Browser.DOM.EventTarget" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let content = document.getElementById('content')", "undefined" },
|
||||
.{ "let para = document.getElementById('para')", "undefined" },
|
||||
// NOTE: as some event properties will change during the event dispatching phases
|
||||
// we need to copy thoses values in order to check them afterwards
|
||||
.{
|
||||
\\ var nb = 0; var evt; var phase; var cur;
|
||||
\\ function cbk(event) {
|
||||
\\ evt = event;
|
||||
\\ phase = event.eventPhase;
|
||||
\\ cur = event.currentTarget;
|
||||
\\ nb ++;
|
||||
\\ }
|
||||
,
|
||||
"undefined",
|
||||
},
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "content.addEventListener('basic', cbk)", "undefined" },
|
||||
.{ "content.dispatchEvent(new Event('basic'))", "true" },
|
||||
.{ "nb", "1" },
|
||||
.{ "evt instanceof Event", "true" },
|
||||
.{ "evt.type", "basic" },
|
||||
.{ "phase", "2" },
|
||||
.{ "cur.getAttribute('id')", "content" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
|
||||
.{ "para.dispatchEvent(new Event('basic'))", "true" },
|
||||
.{ "nb", "0" }, // handler is not called, no capture, not the target, no bubbling
|
||||
.{ "evt === undefined", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0", "0" },
|
||||
.{ "content.addEventListener('basic', cbk)", "undefined" },
|
||||
.{ "content.dispatchEvent(new Event('basic'))", "true" },
|
||||
.{ "nb", "1" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0", "0" },
|
||||
.{ "content.addEventListener('basic', cbk, true)", "undefined" },
|
||||
.{ "content.dispatchEvent(new Event('basic'))", "true" },
|
||||
.{ "nb", "2" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0", "0" },
|
||||
.{ "content.removeEventListener('basic', cbk)", "undefined" },
|
||||
.{ "content.dispatchEvent(new Event('basic'))", "true" },
|
||||
.{ "nb", "1" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0", "0" },
|
||||
.{ "content.removeEventListener('basic', cbk, {capture: true})", "undefined" },
|
||||
.{ "content.dispatchEvent(new Event('basic'))", "true" },
|
||||
.{ "nb", "0" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
|
||||
.{ "content.addEventListener('capture', cbk, true)", "undefined" },
|
||||
.{ "content.dispatchEvent(new Event('capture'))", "true" },
|
||||
.{ "nb", "1" },
|
||||
.{ "evt instanceof Event", "true" },
|
||||
.{ "evt.type", "capture" },
|
||||
.{ "phase", "2" },
|
||||
.{ "cur.getAttribute('id')", "content" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
|
||||
.{ "para.dispatchEvent(new Event('capture'))", "true" },
|
||||
.{ "nb", "1" },
|
||||
.{ "evt instanceof Event", "true" },
|
||||
.{ "evt.type", "capture" },
|
||||
.{ "phase", "1" },
|
||||
.{ "cur.getAttribute('id')", "content" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
|
||||
.{ "content.addEventListener('bubbles', cbk)", "undefined" },
|
||||
.{ "content.dispatchEvent(new Event('bubbles', {bubbles: true}))", "true" },
|
||||
.{ "nb", "1" },
|
||||
.{ "evt instanceof Event", "true" },
|
||||
.{ "evt.type", "bubbles" },
|
||||
.{ "evt.bubbles", "true" },
|
||||
.{ "phase", "2" },
|
||||
.{ "cur.getAttribute('id')", "content" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
|
||||
.{ "para.dispatchEvent(new Event('bubbles', {bubbles: true}))", "true" },
|
||||
.{ "nb", "1" },
|
||||
.{ "evt instanceof Event", "true" },
|
||||
.{ "evt.type", "bubbles" },
|
||||
.{ "phase", "3" },
|
||||
.{ "cur.getAttribute('id')", "content" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -20,11 +20,10 @@ const std = @import("std");
|
||||
const allocPrint = std.fmt.allocPrint;
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
// https://webidl.spec.whatwg.org/#idl-DOMException
|
||||
pub const DOMException = struct {
|
||||
err: ?parser.DOMError,
|
||||
err: parser.DOMError,
|
||||
str: []const u8,
|
||||
|
||||
pub const ErrorSet = parser.DOMError;
|
||||
@@ -56,84 +55,34 @@ pub const DOMException = struct {
|
||||
pub const _INVALID_NODE_TYPE_ERR = 24;
|
||||
pub const _DATA_CLONE_ERR = 25;
|
||||
|
||||
pub fn constructor(message_: ?[]const u8, name_: ?[]const u8, page: *const Page) !DOMException {
|
||||
const message = message_ orelse "";
|
||||
const err = if (name_) |n| error_from_str(n) else null;
|
||||
const fixed_name = name(err);
|
||||
|
||||
if (message.len == 0) return .{ .err = err, .str = fixed_name };
|
||||
|
||||
const str = try allocPrint(page.arena, "{s}: {s}", .{ fixed_name, message });
|
||||
return .{ .err = err, .str = str };
|
||||
}
|
||||
|
||||
// TODO: deinit
|
||||
pub fn init(alloc: std.mem.Allocator, err: anyerror, caller_name: []const u8) !DOMException {
|
||||
const dom_error = @as(parser.DOMError, @errorCast(err));
|
||||
const error_name = DOMException.name(dom_error);
|
||||
const str = switch (dom_error) {
|
||||
pub fn init(alloc: std.mem.Allocator, err: anyerror, callerName: []const u8) !DOMException {
|
||||
const errCast = @as(parser.DOMError, @errorCast(err));
|
||||
const errName = DOMException.name(errCast);
|
||||
const str = switch (errCast) {
|
||||
error.HierarchyRequest => try allocPrint(
|
||||
alloc,
|
||||
"{s}: Failed to execute '{s}' on 'Node': The new child element contains the parent.",
|
||||
.{ error_name, caller_name },
|
||||
),
|
||||
// todo add more custom error messages
|
||||
else => try allocPrint(
|
||||
alloc,
|
||||
"{s}: Failed to execute '{s}' : {s}",
|
||||
.{ error_name, caller_name, error_name },
|
||||
.{ errName, callerName },
|
||||
),
|
||||
error.NoError => unreachable,
|
||||
else => try allocPrint(
|
||||
alloc,
|
||||
"{s}: TODO message", // TODO: implement other messages
|
||||
.{DOMException.name(errCast)},
|
||||
),
|
||||
};
|
||||
return .{ .err = dom_error, .str = str };
|
||||
return .{ .err = errCast, .str = str };
|
||||
}
|
||||
|
||||
fn error_from_str(name_: []const u8) ?parser.DOMError {
|
||||
// @speed: Consider length first, left as is for maintainability, awaiting switch on string support
|
||||
if (std.mem.eql(u8, name_, "IndexSizeError")) return error.IndexSize;
|
||||
if (std.mem.eql(u8, name_, "StringSizeError")) return error.StringSize;
|
||||
if (std.mem.eql(u8, name_, "HierarchyRequestError")) return error.HierarchyRequest;
|
||||
if (std.mem.eql(u8, name_, "WrongDocumentError")) return error.WrongDocument;
|
||||
if (std.mem.eql(u8, name_, "InvalidCharacterError")) return error.InvalidCharacter;
|
||||
if (std.mem.eql(u8, name_, "NoDataAllowedError")) return error.NoDataAllowed;
|
||||
if (std.mem.eql(u8, name_, "NoModificationAllowedError")) return error.NoModificationAllowed;
|
||||
if (std.mem.eql(u8, name_, "NotFoundError")) return error.NotFound;
|
||||
if (std.mem.eql(u8, name_, "NotSupportedError")) return error.NotSupported;
|
||||
if (std.mem.eql(u8, name_, "InuseAttributeError")) return error.InuseAttribute;
|
||||
if (std.mem.eql(u8, name_, "InvalidStateError")) return error.InvalidState;
|
||||
if (std.mem.eql(u8, name_, "SyntaxError")) return error.Syntax;
|
||||
if (std.mem.eql(u8, name_, "InvalidModificationError")) return error.InvalidModification;
|
||||
if (std.mem.eql(u8, name_, "NamespaceError")) return error.Namespace;
|
||||
if (std.mem.eql(u8, name_, "InvalidAccessError")) return error.InvalidAccess;
|
||||
if (std.mem.eql(u8, name_, "ValidationError")) return error.Validation;
|
||||
if (std.mem.eql(u8, name_, "TypeMismatchError")) return error.TypeMismatch;
|
||||
if (std.mem.eql(u8, name_, "SecurityError")) return error.Security;
|
||||
if (std.mem.eql(u8, name_, "NetworkError")) return error.Network;
|
||||
if (std.mem.eql(u8, name_, "AbortError")) return error.Abort;
|
||||
if (std.mem.eql(u8, name_, "URLismatchError")) return error.URLismatch;
|
||||
if (std.mem.eql(u8, name_, "QuotaExceededError")) return error.QuotaExceeded;
|
||||
if (std.mem.eql(u8, name_, "TimeoutError")) return error.Timeout;
|
||||
if (std.mem.eql(u8, name_, "InvalidNodeTypeError")) return error.InvalidNodeType;
|
||||
if (std.mem.eql(u8, name_, "DataCloneError")) return error.DataClone;
|
||||
|
||||
// custom netsurf error
|
||||
if (std.mem.eql(u8, name_, "UnspecifiedEventTypeError")) return error.UnspecifiedEventType;
|
||||
if (std.mem.eql(u8, name_, "DispatchRequestError")) return error.DispatchRequest;
|
||||
if (std.mem.eql(u8, name_, "NoMemoryError")) return error.NoMemory;
|
||||
if (std.mem.eql(u8, name_, "AttributeWrongTypeError")) return error.AttributeWrongType;
|
||||
return null;
|
||||
}
|
||||
|
||||
fn name(err_: ?parser.DOMError) []const u8 {
|
||||
const err = err_ orelse return "Error";
|
||||
|
||||
fn name(err: parser.DOMError) []const u8 {
|
||||
return switch (err) {
|
||||
error.IndexSize => "IndexSizeError",
|
||||
error.StringSize => "StringSizeError", // Legacy: DOMSTRING_SIZE_ERR
|
||||
error.StringSize => "StringSizeError",
|
||||
error.HierarchyRequest => "HierarchyRequestError",
|
||||
error.WrongDocument => "WrongDocumentError",
|
||||
error.InvalidCharacter => "InvalidCharacterError",
|
||||
error.NoDataAllowed => "NoDataAllowedError", // Legacy: NO_DATA_ALLOWED_ERR
|
||||
error.NoDataAllowed => "NoDataAllowedError",
|
||||
error.NoModificationAllowed => "NoModificationAllowedError",
|
||||
error.NotFound => "NotFoundError",
|
||||
error.NotSupported => "NotSupportedError",
|
||||
@@ -143,7 +92,7 @@ pub const DOMException = struct {
|
||||
error.InvalidModification => "InvalidModificationError",
|
||||
error.Namespace => "NamespaceError",
|
||||
error.InvalidAccess => "InvalidAccessError",
|
||||
error.Validation => "ValidationError", // Legacy: VALIDATION_ERR
|
||||
error.Validation => "ValidationError",
|
||||
error.TypeMismatch => "TypeMismatchError",
|
||||
error.Security => "SecurityError",
|
||||
error.Network => "NetworkError",
|
||||
@@ -166,8 +115,7 @@ pub const DOMException = struct {
|
||||
// JS properties and methods
|
||||
|
||||
pub fn get_code(self: *const DOMException) u8 {
|
||||
const err = self.err orelse return 0;
|
||||
return switch (err) {
|
||||
return switch (self.err) {
|
||||
error.IndexSize => 1,
|
||||
error.StringSize => 2,
|
||||
error.HierarchyRequest => 3,
|
||||
@@ -209,8 +157,7 @@ pub const DOMException = struct {
|
||||
|
||||
pub fn get_message(self: *const DOMException) []const u8 {
|
||||
const errName = DOMException.name(self.err);
|
||||
if (self.str.len <= errName.len + 2) return "";
|
||||
return self.str[errName.len + 2 ..]; // ! Requires str is formatted as "{name}: {message}"
|
||||
return self.str[errName.len + 2 ..];
|
||||
}
|
||||
|
||||
pub fn _toString(self: *const DOMException) []const u8 {
|
||||
@@ -219,6 +166,26 @@ pub const DOMException = struct {
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.Exceptions" {
|
||||
try testing.htmlRunner("dom/exceptions.html");
|
||||
test "Browser.DOM.Exception" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
const err = "Failed to execute 'appendChild' on 'Node': The new child element contains the parent.";
|
||||
try runner.testCases(&.{
|
||||
.{ "let content = document.getElementById('content')", "undefined" },
|
||||
.{ "let link = document.getElementById('link')", "undefined" },
|
||||
// HierarchyRequestError
|
||||
.{
|
||||
\\ var he;
|
||||
\\ try { link.appendChild(content) } catch (error) { he = error}
|
||||
\\ he.name
|
||||
,
|
||||
"HierarchyRequestError",
|
||||
},
|
||||
.{ "he.code", "3" },
|
||||
.{ "he.message", err },
|
||||
.{ "he.toString()", "HierarchyRequestError: " ++ err },
|
||||
.{ "he instanceof DOMException", "true" },
|
||||
.{ "he instanceof Error", "true" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -72,14 +72,13 @@ pub fn HTMLCollectionByTagName(
|
||||
arena: Allocator,
|
||||
root: ?*parser.Node,
|
||||
tag_name: []const u8,
|
||||
opts: Opts,
|
||||
include_root: bool,
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = .{ .walkerDepthFirst = .{} },
|
||||
.matcher = .{ .matchByTagName = try MatchByTagName.init(arena, tag_name) },
|
||||
.mutable = opts.mutable,
|
||||
.include_root = opts.include_root,
|
||||
.include_root = include_root,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -110,14 +109,13 @@ pub fn HTMLCollectionByClassName(
|
||||
arena: Allocator,
|
||||
root: ?*parser.Node,
|
||||
classNames: []const u8,
|
||||
opts: Opts,
|
||||
include_root: bool,
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = .{ .walkerDepthFirst = .{} },
|
||||
.matcher = .{ .matchByClassName = try MatchByClassName.init(arena, classNames) },
|
||||
.mutable = opts.mutable,
|
||||
.include_root = opts.include_root,
|
||||
.include_root = include_root,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -141,14 +139,13 @@ pub fn HTMLCollectionByName(
|
||||
arena: Allocator,
|
||||
root: ?*parser.Node,
|
||||
name: []const u8,
|
||||
opts: Opts,
|
||||
include_root: bool,
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = .{ .walkerDepthFirst = .{} },
|
||||
.matcher = .{ .matchByName = try MatchByName.init(arena, name) },
|
||||
.mutable = opts.mutable,
|
||||
.include_root = opts.include_root,
|
||||
.include_root = include_root,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -192,14 +189,13 @@ pub const HTMLAllCollection = struct {
|
||||
|
||||
pub fn HTMLCollectionChildren(
|
||||
root: ?*parser.Node,
|
||||
opts: Opts,
|
||||
) HTMLCollection {
|
||||
include_root: bool,
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = .{ .walkerChildren = .{} },
|
||||
.matcher = .{ .matchTrue = .{} },
|
||||
.mutable = opts.mutable,
|
||||
.include_root = opts.include_root,
|
||||
.include_root = include_root,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -228,14 +224,13 @@ pub const MatchByLinks = struct {
|
||||
|
||||
pub fn HTMLCollectionByLinks(
|
||||
root: ?*parser.Node,
|
||||
opts: Opts,
|
||||
include_root: bool,
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = .{ .walkerDepthFirst = .{} },
|
||||
.matcher = .{ .matchByLinks = MatchByLinks{} },
|
||||
.mutable = opts.mutable,
|
||||
.include_root = opts.include_root,
|
||||
.include_root = include_root,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -254,14 +249,13 @@ pub const MatchByAnchors = struct {
|
||||
|
||||
pub fn HTMLCollectionByAnchors(
|
||||
root: ?*parser.Node,
|
||||
opts: Opts,
|
||||
include_root: bool,
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = .{ .walkerDepthFirst = .{} },
|
||||
.matcher = .{ .matchByAnchors = MatchByAnchors{} },
|
||||
.mutable = opts.mutable,
|
||||
.include_root = opts.include_root,
|
||||
.include_root = include_root,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -291,11 +285,6 @@ pub const HTMLCollectionIterator = struct {
|
||||
}
|
||||
};
|
||||
|
||||
const Opts = struct {
|
||||
include_root: bool,
|
||||
mutable: bool = false,
|
||||
};
|
||||
|
||||
// WEB IDL https://dom.spec.whatwg.org/#htmlcollection
|
||||
// HTMLCollection is re implemented in zig here because libdom
|
||||
// dom_html_collection expects a comparison function callback as arguement.
|
||||
@@ -311,8 +300,6 @@ pub const HTMLCollection = struct {
|
||||
// itself.
|
||||
include_root: bool = false,
|
||||
|
||||
mutable: bool = false,
|
||||
|
||||
// save a state for the collection to improve the _item speed.
|
||||
cur_idx: ?u32 = null,
|
||||
cur_node: ?*parser.Node = null,
|
||||
@@ -363,7 +350,7 @@ pub const HTMLCollection = struct {
|
||||
var node: *parser.Node = undefined;
|
||||
|
||||
// Use the current state to improve speed if possible.
|
||||
if (self.mutable == false and self.cur_idx != null and index >= self.cur_idx.?) {
|
||||
if (self.cur_idx != null and index >= self.cur_idx.?) {
|
||||
i = self.cur_idx.?;
|
||||
node = self.cur_node.?;
|
||||
} else {
|
||||
@@ -445,8 +432,7 @@ pub const HTMLCollection = struct {
|
||||
for (0..len) |i| {
|
||||
const node = try self.item(@intCast(i)) orelse unreachable;
|
||||
const e = @as(*parser.Element, @ptrCast(node));
|
||||
const as_interface = try Element.toInterface(e);
|
||||
try js_this.setIndex(@intCast(i), as_interface, .{});
|
||||
try js_this.setIndex(@intCast(i), e, .{});
|
||||
|
||||
if (try item_name(e)) |name| {
|
||||
// Even though an entry might have an empty id, the spec says
|
||||
@@ -454,7 +440,7 @@ pub const HTMLCollection = struct {
|
||||
if (name.len > 0) {
|
||||
// Named fields should not be enumerable (it is defined with
|
||||
// the LegacyUnenumerableNamedProperties flag.)
|
||||
try js_this.set(name, as_interface, .{ .DONT_ENUM = true });
|
||||
try js_this.set(name, e, .{ .DONT_ENUM = true });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -462,6 +448,52 @@ pub const HTMLCollection = struct {
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.HTMLCollection" {
|
||||
try testing.htmlRunner("dom/html_collection.html");
|
||||
test "Browser.DOM.HTMLCollection" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let getElementsByTagName = document.getElementsByTagName('p')", "undefined" },
|
||||
.{ "getElementsByTagName.length", "2" },
|
||||
.{ "let getElementsByTagNameCI = document.getElementsByTagName('P')", "undefined" },
|
||||
.{ "getElementsByTagNameCI.length", "2" },
|
||||
.{ "getElementsByTagName.item(0).localName", "p" },
|
||||
.{ "getElementsByTagName.item(1).localName", "p" },
|
||||
.{ "let getElementsByTagNameAll = document.getElementsByTagName('*')", "undefined" },
|
||||
.{ "getElementsByTagNameAll.length", "8" },
|
||||
.{ "getElementsByTagNameAll.item(0).localName", "html" },
|
||||
.{ "getElementsByTagNameAll.item(0).localName", "html" },
|
||||
.{ "getElementsByTagNameAll.item(1).localName", "head" },
|
||||
.{ "getElementsByTagNameAll.item(0).localName", "html" },
|
||||
.{ "getElementsByTagNameAll.item(2).localName", "body" },
|
||||
.{ "getElementsByTagNameAll.item(3).localName", "div" },
|
||||
.{ "getElementsByTagNameAll.item(7).localName", "p" },
|
||||
.{ "getElementsByTagNameAll.namedItem('para-empty-child').localName", "span" },
|
||||
|
||||
// array like
|
||||
.{ "getElementsByTagNameAll[0].localName", "html" },
|
||||
.{ "getElementsByTagNameAll[7].localName", "p" },
|
||||
.{ "getElementsByTagNameAll[8]", "undefined" },
|
||||
.{ "getElementsByTagNameAll['para-empty-child'].localName", "span" },
|
||||
.{ "getElementsByTagNameAll['foo']", "undefined" },
|
||||
|
||||
.{ "document.getElementById('content').getElementsByTagName('*').length", "4" },
|
||||
.{ "document.getElementById('content').getElementsByTagName('p').length", "2" },
|
||||
.{ "document.getElementById('content').getElementsByTagName('div').length", "0" },
|
||||
|
||||
.{ "document.children.length", "1" },
|
||||
.{ "document.getElementById('content').children.length", "3" },
|
||||
|
||||
// check liveness
|
||||
.{ "let content = document.getElementById('content')", "undefined" },
|
||||
.{ "let pe = document.getElementById('para-empty')", "undefined" },
|
||||
.{ "let p = document.createElement('p')", "undefined" },
|
||||
.{ "p.textContent = 'OK live'", "OK live" },
|
||||
.{ "getElementsByTagName.item(1).textContent", " And" },
|
||||
.{ "content.appendChild(p) != undefined", "true" },
|
||||
.{ "getElementsByTagName.length", "3" },
|
||||
.{ "getElementsByTagName.item(2).textContent", "OK live" },
|
||||
.{ "content.insertBefore(p, pe) != undefined", "true" },
|
||||
.{ "getElementsByTagName.item(0).textContent", "OK live" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -50,7 +50,20 @@ pub const DOMImplementation = struct {
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.Implementation" {
|
||||
try testing.htmlRunner("dom/implementation.html");
|
||||
test "Browser.DOM.Implementation" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let impl = document.implementation", "undefined" },
|
||||
.{ "impl.createHTMLDocument();", "[object HTMLDocument]" },
|
||||
.{ "impl.createHTMLDocument('foo');", "[object HTMLDocument]" },
|
||||
.{ "impl.createDocument(null, 'foo');", "[object Document]" },
|
||||
.{ "impl.createDocumentType('foo', 'bar', 'baz')", "[object DocumentType]" },
|
||||
.{ "impl.hasFeature()", "true" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -18,9 +18,8 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Element = @import("element.zig").Element;
|
||||
@@ -30,6 +29,8 @@ pub const Interfaces = .{
|
||||
IntersectionObserverEntry,
|
||||
};
|
||||
|
||||
const log = std.log.scoped(.events);
|
||||
|
||||
// This is supposed to listen to change between the root and observation targets.
|
||||
// However, our rendered stores everything as 1 pixel sized boxes in a long row that never changes.
|
||||
// As such, there are no changes to intersections between the root and any target.
|
||||
@@ -39,19 +40,19 @@ pub const Interfaces = .{
|
||||
// The returned Entries are phony, they always indicate full intersection.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver
|
||||
pub const IntersectionObserver = struct {
|
||||
page: *Page,
|
||||
callback: Env.Function,
|
||||
callback: Env.Callback,
|
||||
options: IntersectionObserverOptions,
|
||||
state: *SessionState,
|
||||
|
||||
observed_entries: std.ArrayListUnmanaged(IntersectionObserverEntry),
|
||||
|
||||
// new IntersectionObserver(callback)
|
||||
// new IntersectionObserver(callback, options) [not supported yet]
|
||||
pub fn constructor(callback: Env.Function, options_: ?IntersectionObserverOptions, page: *Page) !IntersectionObserver {
|
||||
pub fn constructor(callback: Env.Callback, options_: ?IntersectionObserverOptions, state: *SessionState) !IntersectionObserver {
|
||||
var options = IntersectionObserverOptions{
|
||||
.root = parser.documentToNode(parser.documentHTMLToDocument(page.window.document)),
|
||||
.root = parser.documentToNode(parser.documentHTMLToDocument(state.document.?)),
|
||||
.rootMargin = "0px 0px 0px 0px",
|
||||
.threshold = .{ .single = 0.0 },
|
||||
.threshold = &.{0.0},
|
||||
};
|
||||
if (options_) |*o| {
|
||||
if (o.root) |root| {
|
||||
@@ -60,9 +61,9 @@ pub const IntersectionObserver = struct {
|
||||
}
|
||||
|
||||
return .{
|
||||
.page = page,
|
||||
.callback = callback,
|
||||
.options = options,
|
||||
.state = state,
|
||||
.observed_entries = .{},
|
||||
};
|
||||
}
|
||||
@@ -78,19 +79,16 @@ pub const IntersectionObserver = struct {
|
||||
}
|
||||
}
|
||||
|
||||
try self.observed_entries.append(self.page.arena, .{
|
||||
.page = self.page,
|
||||
try self.observed_entries.append(self.state.arena, .{
|
||||
.state = self.state,
|
||||
.target = target_element,
|
||||
.options = &self.options,
|
||||
});
|
||||
|
||||
var result: Env.Function.Result = undefined;
|
||||
self.callback.tryCall(void, .{self.observed_entries.items}, &result) catch {
|
||||
log.debug(.user_script, "callback error", .{
|
||||
.err = result.exception,
|
||||
.stack = result.stack,
|
||||
.source = "intersection observer",
|
||||
});
|
||||
var result: Env.Callback.Result = undefined;
|
||||
self.callback.tryCall(.{self.observed_entries.items}, &result) catch {
|
||||
log.err("intersection observer callback error: {s}", .{result.exception});
|
||||
log.debug("stack:\n{s}", .{result.stack orelse "???"});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -111,24 +109,19 @@ pub const IntersectionObserver = struct {
|
||||
const IntersectionObserverOptions = struct {
|
||||
root: ?*parser.Node, // Element or Document
|
||||
rootMargin: ?[]const u8,
|
||||
threshold: ?Threshold,
|
||||
|
||||
const Threshold = union(enum) {
|
||||
single: f32,
|
||||
list: []const f32,
|
||||
};
|
||||
threshold: ?[]const f32,
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry
|
||||
// https://w3c.github.io/IntersectionObserver/#intersection-observer-entry
|
||||
pub const IntersectionObserverEntry = struct {
|
||||
page: *Page,
|
||||
state: *SessionState,
|
||||
target: *parser.Element,
|
||||
options: *IntersectionObserverOptions,
|
||||
|
||||
// Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in the documentation for Element.getBoundingClientRect().
|
||||
pub fn get_boundingClientRect(self: *const IntersectionObserverEntry) !Element.DOMRect {
|
||||
return Element._getBoundingClientRect(self.target, self.page);
|
||||
return self.state.renderer.getRect(self.target);
|
||||
}
|
||||
|
||||
// Returns the ratio of the intersectionRect to the boundingClientRect.
|
||||
@@ -138,14 +131,10 @@ pub const IntersectionObserverEntry = struct {
|
||||
|
||||
// Returns a DOMRectReadOnly representing the target's visible area.
|
||||
pub fn get_intersectionRect(self: *const IntersectionObserverEntry) !Element.DOMRect {
|
||||
return Element._getBoundingClientRect(self.target, self.page);
|
||||
return self.state.renderer.getRect(self.target);
|
||||
}
|
||||
|
||||
// A Boolean value which is true if the target element intersects with the
|
||||
// intersection observer's root. If this is true, then, the
|
||||
// IntersectionObserverEntry describes a transition into a state of
|
||||
// intersection; if it's false, then you know the transition is from
|
||||
// intersecting to not-intersecting.
|
||||
// A Boolean value which is true if the target element intersects with the intersection observer's root. If this is true, then, the IntersectionObserverEntry describes a transition into a state of intersection; if it's false, then you know the transition is from intersecting to not-intersecting.
|
||||
pub fn get_isIntersecting(_: *const IntersectionObserverEntry) bool {
|
||||
return true;
|
||||
}
|
||||
@@ -153,8 +142,8 @@ pub const IntersectionObserverEntry = struct {
|
||||
// Returns a DOMRectReadOnly for the intersection observer's root.
|
||||
pub fn get_rootBounds(self: *const IntersectionObserverEntry) !Element.DOMRect {
|
||||
const root = self.options.root.?;
|
||||
if (@intFromPtr(root) == @intFromPtr(self.page.window.document)) {
|
||||
return self.page.renderer.boundingRect();
|
||||
if (@intFromPtr(root) == @intFromPtr(self.state.document.?)) {
|
||||
return self.state.renderer.boundingRect();
|
||||
}
|
||||
|
||||
const root_type = try parser.nodeType(root);
|
||||
@@ -169,7 +158,7 @@ pub const IntersectionObserverEntry = struct {
|
||||
else => return error.InvalidState,
|
||||
}
|
||||
|
||||
return Element._getBoundingClientRect(element, self.page);
|
||||
return try self.state.renderer.getRect(element);
|
||||
}
|
||||
|
||||
// The Element whose intersection with the root changed.
|
||||
@@ -181,6 +170,107 @@ pub const IntersectionObserverEntry = struct {
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.IntersectionObserver" {
|
||||
try testing.htmlRunner("dom/intersection_observer.html");
|
||||
test "Browser.DOM.IntersectionObserver" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "new IntersectionObserver(() => {}).observe(document.documentElement);", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let count_a = 0;", "undefined" },
|
||||
.{ "const a1 = document.createElement('div');", "undefined" },
|
||||
.{ "new IntersectionObserver(entries => {count_a += 1;}).observe(a1);", "undefined" },
|
||||
.{ "count_a;", "1" },
|
||||
}, .{});
|
||||
|
||||
// This test is documenting current behavior, not correct behavior.
|
||||
// Currently every time observe is called, the callback is called with all entries.
|
||||
try runner.testCases(&.{
|
||||
.{ "let count_b = 0;", "undefined" },
|
||||
.{ "let observer_b = new IntersectionObserver(entries => {count_b = entries.length;});", "undefined" },
|
||||
.{ "const b1 = document.createElement('div');", "undefined" },
|
||||
.{ "observer_b.observe(b1);", "undefined" },
|
||||
.{ "count_b;", "1" },
|
||||
.{ "const b2 = document.createElement('div');", "undefined" },
|
||||
.{ "observer_b.observe(b2);", "undefined" },
|
||||
.{ "count_b;", "2" },
|
||||
}, .{});
|
||||
|
||||
// Re-observing is a no-op
|
||||
try runner.testCases(&.{
|
||||
.{ "let count_bb = 0;", "undefined" },
|
||||
.{ "let observer_bb = new IntersectionObserver(entries => {count_bb = entries.length;});", "undefined" },
|
||||
.{ "const bb1 = document.createElement('div');", "undefined" },
|
||||
.{ "observer_bb.observe(bb1);", "undefined" },
|
||||
.{ "count_bb;", "1" },
|
||||
.{ "observer_bb.observe(bb1);", "undefined" },
|
||||
.{ "count_bb;", "1" }, // Still 1, not 2
|
||||
}, .{});
|
||||
|
||||
// Unobserve
|
||||
try runner.testCases(&.{
|
||||
.{ "let count_c = 0;", "undefined" },
|
||||
.{ "let observer_c = new IntersectionObserver(entries => { count_c = entries.length;});", "undefined" },
|
||||
.{ "const c1 = document.createElement('div');", "undefined" },
|
||||
.{ "observer_c.observe(c1);", "undefined" },
|
||||
.{ "count_c;", "1" },
|
||||
.{ "observer_c.unobserve(c1);", "undefined" },
|
||||
.{ "const c2 = document.createElement('div');", "undefined" },
|
||||
.{ "observer_c.observe(c2);", "undefined" },
|
||||
.{ "count_c;", "1" },
|
||||
}, .{});
|
||||
|
||||
// Disconnect
|
||||
try runner.testCases(&.{
|
||||
.{ "let observer_d = new IntersectionObserver(entries => {});", "undefined" },
|
||||
.{ "let d1 = document.createElement('div');", "undefined" },
|
||||
.{ "observer_d.observe(d1);", "undefined" },
|
||||
.{ "observer_d.disconnect();", "undefined" },
|
||||
.{ "observer_d.takeRecords().length;", "0" },
|
||||
}, .{});
|
||||
|
||||
// takeRecords
|
||||
try runner.testCases(&.{
|
||||
.{ "let observer_e = new IntersectionObserver(entries => {});", "undefined" },
|
||||
.{ "let e1 = document.createElement('div');", "undefined" },
|
||||
.{ "observer_e.observe(e1);", "undefined" },
|
||||
.{ "const e2 = document.createElement('div');", "undefined" },
|
||||
.{ "observer_e.observe(e2);", "undefined" },
|
||||
.{ "observer_e.takeRecords().length;", "2" },
|
||||
}, .{});
|
||||
|
||||
// Entry
|
||||
try runner.testCases(&.{
|
||||
.{ "let entry;", "undefined" },
|
||||
.{ "new IntersectionObserver(entries => { entry = entries[0]; }).observe(document.createElement('div'));", "undefined" },
|
||||
.{ "entry.boundingClientRect.x;", "0" },
|
||||
.{ "entry.intersectionRatio;", "1" },
|
||||
.{ "entry.intersectionRect.x;", "0" },
|
||||
.{ "entry.intersectionRect.y;", "0" },
|
||||
.{ "entry.intersectionRect.width;", "1" },
|
||||
.{ "entry.intersectionRect.height;", "1" },
|
||||
.{ "entry.isIntersecting;", "true" },
|
||||
.{ "entry.rootBounds.x;", "0" },
|
||||
.{ "entry.rootBounds.y;", "0" },
|
||||
.{ "entry.rootBounds.width;", "1" },
|
||||
.{ "entry.rootBounds.height;", "1" },
|
||||
.{ "entry.target;", "[object HTMLDivElement]" },
|
||||
}, .{});
|
||||
|
||||
// Options
|
||||
try runner.testCases(&.{
|
||||
.{ "const new_root = document.createElement('span');", "undefined" },
|
||||
.{ "let new_entry;", "undefined" },
|
||||
.{
|
||||
\\ const new_observer = new IntersectionObserver(
|
||||
\\ entries => { new_entry = entries[0]; },
|
||||
\\ {root: new_root, rootMargin: '0px 0px 0px 0px', threshold: [0]});
|
||||
,
|
||||
"undefined",
|
||||
},
|
||||
.{ "new_observer.observe(document.createElement('div'));", "undefined" },
|
||||
.{ "new_entry.rootBounds.x;", "1" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -19,9 +19,8 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const NodeList = @import("nodelist.zig").NodeList;
|
||||
@@ -33,35 +32,29 @@ pub const Interfaces = .{
|
||||
|
||||
const Walker = @import("../dom/walker.zig").WalkerChildren;
|
||||
|
||||
const log = std.log.scoped(.events);
|
||||
|
||||
// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
|
||||
pub const MutationObserver = struct {
|
||||
page: *Page,
|
||||
cbk: Env.Function,
|
||||
connected: bool,
|
||||
scheduled: bool,
|
||||
cbk: Env.Callback,
|
||||
arena: Allocator,
|
||||
|
||||
// List of records which were observed. When the call scope ends, we need to
|
||||
// List of records which were observed. When the scopeEnds, we need to
|
||||
// execute our callback with it.
|
||||
observed: std.ArrayListUnmanaged(MutationRecord),
|
||||
observed: std.ArrayListUnmanaged(*MutationRecord),
|
||||
|
||||
pub fn constructor(cbk: Env.Function, page: *Page) !MutationObserver {
|
||||
pub fn constructor(cbk: Env.Callback, state: *SessionState) !MutationObserver {
|
||||
return .{
|
||||
.cbk = cbk,
|
||||
.page = page,
|
||||
.observed = .{},
|
||||
.connected = true,
|
||||
.scheduled = false,
|
||||
.arena = state.arena,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _observe(self: *MutationObserver, node: *parser.Node, options_: ?Options) !void {
|
||||
const arena = self.page.arena;
|
||||
var options = options_ orelse Options{};
|
||||
if (options.attributeFilter.len > 0) {
|
||||
options.attributeFilter = try arena.dupe([]const u8, options.attributeFilter);
|
||||
}
|
||||
pub fn _observe(self: *MutationObserver, node: *parser.Node, options_: ?MutationObserverInit) !void {
|
||||
const options = options_ orelse MutationObserverInit{};
|
||||
|
||||
const observer = try arena.create(Observer);
|
||||
const observer = try self.arena.create(Observer);
|
||||
observer.* = .{
|
||||
.node = node,
|
||||
.options = options,
|
||||
@@ -71,13 +64,13 @@ pub const MutationObserver = struct {
|
||||
|
||||
// register node's events
|
||||
if (options.childList or options.subtree) {
|
||||
_ = try parser.eventTargetAddEventListener(
|
||||
try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Node, node),
|
||||
"DOMNodeInserted",
|
||||
&observer.event_node,
|
||||
false,
|
||||
);
|
||||
_ = try parser.eventTargetAddEventListener(
|
||||
try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Node, node),
|
||||
"DOMNodeRemoved",
|
||||
&observer.event_node,
|
||||
@@ -85,7 +78,7 @@ pub const MutationObserver = struct {
|
||||
);
|
||||
}
|
||||
if (options.attr()) {
|
||||
_ = try parser.eventTargetAddEventListener(
|
||||
try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Node, node),
|
||||
"DOMAttrModified",
|
||||
&observer.event_node,
|
||||
@@ -93,7 +86,7 @@ pub const MutationObserver = struct {
|
||||
);
|
||||
}
|
||||
if (options.cdata()) {
|
||||
_ = try parser.eventTargetAddEventListener(
|
||||
try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Node, node),
|
||||
"DOMCharacterDataModified",
|
||||
&observer.event_node,
|
||||
@@ -101,7 +94,7 @@ pub const MutationObserver = struct {
|
||||
);
|
||||
}
|
||||
if (options.subtree) {
|
||||
_ = try parser.eventTargetAddEventListener(
|
||||
try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Node, node),
|
||||
"DOMSubtreeModified",
|
||||
&observer.event_node,
|
||||
@@ -110,35 +103,27 @@ pub const MutationObserver = struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn callback(ctx: *anyopaque) ?u32 {
|
||||
const self: *MutationObserver = @ptrCast(@alignCast(ctx));
|
||||
if (self.connected == false) {
|
||||
self.scheduled = true;
|
||||
return null;
|
||||
}
|
||||
self.scheduled = false;
|
||||
|
||||
const records = self.observed.items;
|
||||
if (records.len == 0) {
|
||||
return null;
|
||||
pub fn jsCallScopeEnd(self: *MutationObserver, _: anytype) void {
|
||||
const record = self.observed.items;
|
||||
if (record.len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
defer self.observed.clearRetainingCapacity();
|
||||
|
||||
var result: Env.Function.Result = undefined;
|
||||
self.cbk.tryCall(void, .{records}, &result) catch {
|
||||
log.debug(.user_script, "callback error", .{
|
||||
.err = result.exception,
|
||||
.stack = result.stack,
|
||||
.source = "mutation observer",
|
||||
});
|
||||
};
|
||||
return null;
|
||||
for (record) |r| {
|
||||
const records = [_]MutationRecord{r.*};
|
||||
var result: Env.Callback.Result = undefined;
|
||||
self.cbk.tryCall(.{records}, &result) catch {
|
||||
log.err("mutation observer callback error: {s}", .{result.exception});
|
||||
log.debug("stack:\n{s}", .{result.stack orelse "???"});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
pub fn _disconnect(self: *MutationObserver) !void {
|
||||
self.connected = false;
|
||||
pub fn _disconnect(_: *MutationObserver) !void {
|
||||
// TODO unregister listeners.
|
||||
}
|
||||
|
||||
// TODO
|
||||
@@ -195,27 +180,31 @@ pub const MutationRecord = struct {
|
||||
}
|
||||
};
|
||||
|
||||
const Options = struct {
|
||||
const MutationObserverInit = struct {
|
||||
childList: bool = false,
|
||||
attributes: bool = false,
|
||||
characterData: bool = false,
|
||||
subtree: bool = false,
|
||||
attributeOldValue: bool = false,
|
||||
characterDataOldValue: bool = false,
|
||||
attributeFilter: [][]const u8 = &.{},
|
||||
// TODO
|
||||
// attributeFilter: [][]const u8,
|
||||
|
||||
fn attr(self: Options) bool {
|
||||
return self.attributes or self.attributeOldValue or self.attributeFilter.len > 0;
|
||||
fn attr(self: MutationObserverInit) bool {
|
||||
return self.attributes or self.attributeOldValue;
|
||||
}
|
||||
|
||||
fn cdata(self: Options) bool {
|
||||
fn cdata(self: MutationObserverInit) bool {
|
||||
return self.characterData or self.characterDataOldValue;
|
||||
}
|
||||
};
|
||||
|
||||
const Observer = struct {
|
||||
node: *parser.Node,
|
||||
options: Options,
|
||||
options: MutationObserverInit,
|
||||
|
||||
// record of the mutation, all observed changes in 1 call are batched
|
||||
record: ?MutationRecord = null,
|
||||
|
||||
// reference back to the MutationObserver so that we can access the arena
|
||||
// and batch the mutation records.
|
||||
@@ -223,34 +212,19 @@ const Observer = struct {
|
||||
|
||||
event_node: parser.EventNode,
|
||||
|
||||
fn appliesTo(
|
||||
self: *const Observer,
|
||||
target: *parser.Node,
|
||||
event_type: MutationEventType,
|
||||
event: *parser.MutationEvent,
|
||||
) !bool {
|
||||
if (event_type == .DOMAttrModified and self.options.attributeFilter.len > 0) {
|
||||
const attribute_name = try parser.mutationEventAttributeName(event);
|
||||
for (self.options.attributeFilter) |needle| blk: {
|
||||
if (std.mem.eql(u8, attribute_name, needle)) {
|
||||
break :blk;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn appliesTo(o: *const Observer, target: *parser.Node) bool {
|
||||
// mutation on any target is always ok.
|
||||
if (self.options.subtree) {
|
||||
if (o.options.subtree) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if target equals node, alway ok.
|
||||
if (target == self.node) {
|
||||
if (target == o.node) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// no subtree, no same target and no childlist, always noky.
|
||||
if (!self.options.childList) {
|
||||
if (!o.options.childList) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -258,7 +232,7 @@ const Observer = struct {
|
||||
const walker = Walker{};
|
||||
var next: ?*parser.Node = null;
|
||||
while (true) {
|
||||
next = walker.get_next(self.node, next) catch break orelse break;
|
||||
next = walker.get_next(o.node, next) catch break orelse break;
|
||||
if (next.? == target) {
|
||||
return true;
|
||||
}
|
||||
@@ -269,35 +243,44 @@ const Observer = struct {
|
||||
|
||||
fn handle(en: *parser.EventNode, event: *parser.Event) void {
|
||||
const self: *Observer = @fieldParentPtr("event_node", en);
|
||||
self._handle(event) catch |err| {
|
||||
log.err(.web_api, "handle error", .{ .err = err, .source = "mutation observer" });
|
||||
};
|
||||
}
|
||||
|
||||
fn _handle(self: *Observer, event: *parser.Event) !void {
|
||||
var mutation_observer = self.mutation_observer;
|
||||
|
||||
const node = blk: {
|
||||
const event_target = try parser.eventTarget(event) orelse return;
|
||||
const event_target = parser.eventTarget(event) catch |e| {
|
||||
log.err("mutation observer event target: {any}", .{e});
|
||||
return;
|
||||
} orelse return;
|
||||
|
||||
break :blk parser.eventTargetToNode(event_target);
|
||||
};
|
||||
|
||||
const mutation_event = parser.eventToMutationEvent(event);
|
||||
const event_type = blk: {
|
||||
const t = try parser.eventType(event);
|
||||
break :blk std.meta.stringToEnum(MutationEventType, t) orelse return;
|
||||
};
|
||||
|
||||
if (try self.appliesTo(node, event_type, mutation_event) == false) {
|
||||
if (self.appliesTo(node) == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
var record = MutationRecord{
|
||||
.target = self.node,
|
||||
.type = event_type.recordType(),
|
||||
const event_type = blk: {
|
||||
const t = parser.eventType(event) catch |e| {
|
||||
log.err("mutation observer event type: {any}", .{e});
|
||||
return;
|
||||
};
|
||||
break :blk std.meta.stringToEnum(MutationEventType, t) orelse return;
|
||||
};
|
||||
|
||||
const arena = mutation_observer.page.arena;
|
||||
const arena = mutation_observer.arena;
|
||||
if (self.record == null) {
|
||||
self.record = .{
|
||||
.target = self.node,
|
||||
.type = event_type.recordType(),
|
||||
};
|
||||
mutation_observer.observed.append(arena, &self.record.?) catch |err| {
|
||||
log.err("mutation_observer append: {}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
var record = &self.record.?;
|
||||
const mutation_event = parser.eventToMutationEvent(event);
|
||||
|
||||
switch (event_type) {
|
||||
.DOMAttrModified => {
|
||||
record.attribute_name = parser.mutationEventAttributeName(mutation_event) catch null;
|
||||
@@ -312,27 +295,21 @@ const Observer = struct {
|
||||
},
|
||||
.DOMNodeInserted => {
|
||||
if (parser.mutationEventRelatedNode(mutation_event) catch null) |related_node| {
|
||||
try record.added_nodes.append(arena, related_node);
|
||||
record.added_nodes.append(arena, related_node) catch |e| {
|
||||
log.err("mutation event handler error: {any}", .{e});
|
||||
return;
|
||||
};
|
||||
}
|
||||
},
|
||||
.DOMNodeRemoved => {
|
||||
if (parser.mutationEventRelatedNode(mutation_event) catch null) |related_node| {
|
||||
try record.removed_nodes.append(arena, related_node);
|
||||
record.removed_nodes.append(arena, related_node) catch |e| {
|
||||
log.err("mutation event handler error: {any}", .{e});
|
||||
return;
|
||||
};
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
try mutation_observer.observed.append(arena, record);
|
||||
|
||||
if (mutation_observer.scheduled == false) {
|
||||
mutation_observer.scheduled = true;
|
||||
try mutation_observer.page.scheduler.add(
|
||||
mutation_observer,
|
||||
MutationObserver.callback,
|
||||
0,
|
||||
.{ .name = "mutation_observer" },
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -353,6 +330,68 @@ const MutationEventType = enum {
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.MutationObserver" {
|
||||
try testing.htmlRunner("dom/mutation_observer.html");
|
||||
test "Browser.DOM.MutationObserver" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "new MutationObserver(() => {}).observe(document, { childList: true });", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ var nb = 0;
|
||||
\\ var mrs;
|
||||
\\ new MutationObserver((mu) => {
|
||||
\\ mrs = mu;
|
||||
\\ nb++;
|
||||
\\ }).observe(document.firstElementChild, { attributes: true, attributeOldValue: true });
|
||||
\\ document.firstElementChild.setAttribute("foo", "bar");
|
||||
\\ // ignored b/c it's about another target.
|
||||
\\ document.firstElementChild.firstChild.setAttribute("foo", "bar");
|
||||
\\ nb;
|
||||
,
|
||||
"1",
|
||||
},
|
||||
.{ "mrs[0].type", "attributes" },
|
||||
.{ "mrs[0].target == document.firstElementChild", "true" },
|
||||
.{ "mrs[0].target.getAttribute('foo')", "bar" },
|
||||
.{ "mrs[0].attributeName", "foo" },
|
||||
.{ "mrs[0].oldValue", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ var node = document.getElementById("para").firstChild;
|
||||
\\ var nb2 = 0;
|
||||
\\ var mrs2;
|
||||
\\ new MutationObserver((mu) => {
|
||||
\\ mrs2 = mu;
|
||||
\\ nb2++;
|
||||
\\ }).observe(node, { characterData: true, characterDataOldValue: true });
|
||||
\\ node.data = "foo";
|
||||
\\ nb2;
|
||||
,
|
||||
"1",
|
||||
},
|
||||
.{ "mrs2[0].type", "characterData" },
|
||||
.{ "mrs2[0].target == node", "true" },
|
||||
.{ "mrs2[0].target.data", "foo" },
|
||||
.{ "mrs2[0].oldValue", " And" },
|
||||
}, .{});
|
||||
|
||||
// tests that mutation observers that have a callback which trigger the
|
||||
// mutation observer don't crash.
|
||||
// https://github.com/lightpanda-io/browser/issues/550
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ var node = document.getElementById("para");
|
||||
\\ new MutationObserver(() => {
|
||||
\\ node.innerText = 'a';
|
||||
\\ }).observe(document, { subtree:true,childList:true });
|
||||
\\ node.innerText = "2";
|
||||
,
|
||||
"2",
|
||||
},
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ pub const NamedNodeMap = struct {
|
||||
pub const Self = parser.NamedNodeMap;
|
||||
|
||||
pub const Exception = DOMException;
|
||||
pub const Iterator = NamedNodeMapIterator;
|
||||
|
||||
// TODO implement LegacyUnenumerableNamedProperties.
|
||||
// https://webidl.spec.whatwg.org/#LegacyUnenumerableNamedProperties
|
||||
@@ -71,51 +70,28 @@ pub const NamedNodeMap = struct {
|
||||
}
|
||||
|
||||
pub fn indexed_get(self: *parser.NamedNodeMap, index: u32, has_value: *bool) !*parser.Attribute {
|
||||
return (try _item(self, index)) orelse {
|
||||
return (try NamedNodeMap._item(self, index)) orelse {
|
||||
has_value.* = false;
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
pub fn named_get(self: *parser.NamedNodeMap, name: []const u8, has_value: *bool) !*parser.Attribute {
|
||||
return (try _getNamedItem(self, name)) orelse {
|
||||
has_value.* = false;
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _symbol_iterator(self: *parser.NamedNodeMap) NamedNodeMapIterator {
|
||||
return .{ .map = self };
|
||||
}
|
||||
};
|
||||
|
||||
pub const NamedNodeMapIterator = struct {
|
||||
index: u32 = 0,
|
||||
map: *parser.NamedNodeMap,
|
||||
|
||||
pub const Return = struct {
|
||||
done: bool,
|
||||
value: ?*parser.Attribute,
|
||||
};
|
||||
|
||||
pub fn _next(self: *NamedNodeMapIterator) !Return {
|
||||
const e = try NamedNodeMap._item(self.map, self.index);
|
||||
if (e == null) {
|
||||
return .{
|
||||
.value = null,
|
||||
.done = true,
|
||||
};
|
||||
}
|
||||
|
||||
self.index += 1;
|
||||
return .{
|
||||
.value = e,
|
||||
.done = false,
|
||||
};
|
||||
}
|
||||
};
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: DOM.NamedNodeMap" {
|
||||
try testing.htmlRunner("dom/named_node_map.html");
|
||||
test "Browser.DOM.NamedNodeMap" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let a = document.getElementById('content').attributes", "undefined" },
|
||||
.{ "a.length", "1" },
|
||||
.{ "a.item(0)", "[object Attr]" },
|
||||
.{ "a.item(1)", "null" },
|
||||
.{ "a.getNamedItem('id')", "[object Attr]" },
|
||||
.{ "a.getNamedItem('foo')", "null" },
|
||||
.{ "a.setNamedItem(a.getNamedItem('id'))", "[object Attr]" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -18,18 +18,16 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const generate = @import("../../runtime/generate.zig");
|
||||
|
||||
const Page = @import("../page.zig").Page;
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
const EventTarget = @import("event_target.zig").EventTarget;
|
||||
|
||||
// DOM
|
||||
const Attr = @import("attribute.zig").Attr;
|
||||
const CData = @import("character_data.zig");
|
||||
const Element = @import("element.zig").Element;
|
||||
const ElementUnion = @import("element.zig").Union;
|
||||
const NodeList = @import("nodelist.zig").NodeList;
|
||||
const Document = @import("document.zig").Document;
|
||||
const DocumentType = @import("document_type.zig").DocumentType;
|
||||
@@ -37,11 +35,11 @@ const DocumentFragment = @import("document_fragment.zig").DocumentFragment;
|
||||
const HTMLCollection = @import("html_collection.zig").HTMLCollection;
|
||||
const HTMLAllCollection = @import("html_collection.zig").HTMLAllCollection;
|
||||
const HTMLCollectionIterator = @import("html_collection.zig").HTMLCollectionIterator;
|
||||
const ShadowRoot = @import("shadow_root.zig").ShadowRoot;
|
||||
const Walker = @import("walker.zig").WalkerDepthFirst;
|
||||
|
||||
// HTML
|
||||
const HTML = @import("../html/html.zig");
|
||||
const HTMLElem = @import("../html/elements.zig");
|
||||
|
||||
// Node interfaces
|
||||
pub const Interfaces = .{
|
||||
@@ -68,7 +66,7 @@ pub const Node = struct {
|
||||
|
||||
pub fn toInterface(node: *parser.Node) !Union {
|
||||
return switch (try parser.nodeType(node)) {
|
||||
.element => try Element.toInterfaceT(
|
||||
.element => try HTMLElem.toInterface(
|
||||
Union,
|
||||
@as(*parser.Element, @ptrCast(node)),
|
||||
),
|
||||
@@ -76,14 +74,7 @@ pub const Node = struct {
|
||||
.text => .{ .Text = @as(*parser.Text, @ptrCast(node)) },
|
||||
.cdata_section => .{ .CDATASection = @as(*parser.CDATASection, @ptrCast(node)) },
|
||||
.processing_instruction => .{ .ProcessingInstruction = @as(*parser.ProcessingInstruction, @ptrCast(node)) },
|
||||
.document => blk: {
|
||||
const doc: *parser.Document = @ptrCast(node);
|
||||
if (doc.is_html) {
|
||||
break :blk .{ .HTMLDocument = @as(*parser.DocumentHTML, @ptrCast(node)) };
|
||||
}
|
||||
|
||||
break :blk .{ .Document = doc };
|
||||
},
|
||||
.document => .{ .HTMLDocument = @as(*parser.DocumentHTML, @ptrCast(node)) },
|
||||
.document_type => .{ .DocumentType = @as(*parser.DocumentType, @ptrCast(node)) },
|
||||
.attribute => .{ .Attr = @as(*parser.Attribute, @ptrCast(node)) },
|
||||
.document_fragment => .{ .DocumentFragment = @as(*parser.DocumentFragment, @ptrCast(node)) },
|
||||
@@ -108,20 +99,10 @@ pub const Node = struct {
|
||||
pub const _ENTITY_NODE = @intFromEnum(parser.NodeType.entity);
|
||||
pub const _NOTATION_NODE = @intFromEnum(parser.NodeType.notation);
|
||||
|
||||
pub const _DOCUMENT_POSITION_DISCONNECTED = @intFromEnum(parser.DocumentPosition.disconnected);
|
||||
pub const _DOCUMENT_POSITION_PRECEDING = @intFromEnum(parser.DocumentPosition.preceding);
|
||||
pub const _DOCUMENT_POSITION_FOLLOWING = @intFromEnum(parser.DocumentPosition.following);
|
||||
pub const _DOCUMENT_POSITION_CONTAINS = @intFromEnum(parser.DocumentPosition.contains);
|
||||
pub const _DOCUMENT_POSITION_CONTAINED_BY = @intFromEnum(parser.DocumentPosition.contained_by);
|
||||
pub const _DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = @intFromEnum(parser.DocumentPosition.implementation_specific);
|
||||
|
||||
// JS funcs
|
||||
// --------
|
||||
|
||||
// Read-only attributes
|
||||
pub fn get_baseURI(_: *parser.Node, page: *Page) ![]const u8 {
|
||||
return page.url.raw;
|
||||
}
|
||||
|
||||
pub fn get_firstChild(self: *parser.Node) !?Union {
|
||||
const res = try parser.nodeFirstChild(self);
|
||||
@@ -163,12 +144,12 @@ pub const Node = struct {
|
||||
return try Node.toInterface(res.?);
|
||||
}
|
||||
|
||||
pub fn get_parentElement(self: *parser.Node) !?ElementUnion {
|
||||
pub fn get_parentElement(self: *parser.Node) !?HTMLElem.Union {
|
||||
const res = try parser.nodeParentElement(self);
|
||||
if (res == null) {
|
||||
return null;
|
||||
}
|
||||
return try Element.toInterface(res.?);
|
||||
return try HTMLElem.toInterface(HTMLElem.Union, @as(*parser.Element, @ptrCast(res.?)));
|
||||
}
|
||||
|
||||
pub fn get_nodeName(self: *parser.Node) ![]const u8 {
|
||||
@@ -188,35 +169,11 @@ pub const Node = struct {
|
||||
}
|
||||
|
||||
pub fn get_isConnected(self: *parser.Node) !bool {
|
||||
var node = self;
|
||||
while (true) {
|
||||
const node_type = try parser.nodeType(node);
|
||||
if (node_type == .document) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (try parser.nodeParentNode(node)) |parent| {
|
||||
// didn't find a document, but node has a parent, let's see
|
||||
// if it's connected;
|
||||
node = parent;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node_type != .document_fragment) {
|
||||
// doesn't have a parent and isn't a document_fragment
|
||||
// can't be connected
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parser.documentFragmentGetHost(@ptrCast(node))) |host| {
|
||||
// node doesn't have a parent, but it's a document fragment
|
||||
// with a host. The host is like the parent, but we only want to
|
||||
// traverse up (or down) to it in specific cases, like isConnected.
|
||||
node = host;
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
// TODO: handle Shadow DOM
|
||||
if (try parser.nodeType(self) == .document) {
|
||||
return true;
|
||||
}
|
||||
return try Node.get_parentNode(self) != null;
|
||||
}
|
||||
|
||||
// Read/Write attributes
|
||||
@@ -240,23 +197,6 @@ pub const Node = struct {
|
||||
// Methods
|
||||
|
||||
pub fn _appendChild(self: *parser.Node, child: *parser.Node) !Union {
|
||||
const self_owner = try parser.nodeOwnerDocument(self);
|
||||
const child_owner = try parser.nodeOwnerDocument(child);
|
||||
|
||||
// If the node to be inserted has a different ownerDocument than the parent node,
|
||||
// modern browsers automatically adopt the node and its descendants into
|
||||
// the parent's ownerDocument.
|
||||
// This process is known as adoption.
|
||||
// (7.1) https://dom.spec.whatwg.org/#concept-node-insert
|
||||
if (child_owner == null or (self_owner != null and child_owner.? != self_owner.?)) {
|
||||
const w = Walker{};
|
||||
var current = child;
|
||||
while (true) {
|
||||
current.owner = self_owner;
|
||||
current = try w.get_next(child, current) orelse break;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: DocumentFragment special case
|
||||
const res = try parser.nodeAppendChild(self, child);
|
||||
return try Node.toInterface(res);
|
||||
@@ -268,43 +208,14 @@ pub const Node = struct {
|
||||
}
|
||||
|
||||
pub fn _compareDocumentPosition(self: *parser.Node, other: *parser.Node) !u32 {
|
||||
if (self == other) {
|
||||
return 0;
|
||||
}
|
||||
if (self == other) return 0;
|
||||
|
||||
const docself = try parser.nodeOwnerDocument(self) orelse blk: {
|
||||
if (try parser.nodeType(self) == .document) {
|
||||
break :blk @as(*parser.Document, @ptrCast(self));
|
||||
}
|
||||
break :blk null;
|
||||
};
|
||||
const docother = try parser.nodeOwnerDocument(other) orelse blk: {
|
||||
if (try parser.nodeType(other) == .document) {
|
||||
break :blk @as(*parser.Document, @ptrCast(other));
|
||||
}
|
||||
break :blk null;
|
||||
};
|
||||
const docself = try parser.nodeOwnerDocument(self);
|
||||
const docother = try parser.nodeOwnerDocument(other);
|
||||
|
||||
// Both are in different document.
|
||||
if (docself == null or docother == null or docself.? != docother.?) {
|
||||
return @intFromEnum(parser.DocumentPosition.disconnected) +
|
||||
@intFromEnum(parser.DocumentPosition.implementation_specific) +
|
||||
@intFromEnum(parser.DocumentPosition.preceding);
|
||||
}
|
||||
|
||||
if (@intFromPtr(self) == @intFromPtr(docself.?)) {
|
||||
// if self is the document, and we already know other is in the
|
||||
// document, then other is contained by and following self.
|
||||
return @intFromEnum(parser.DocumentPosition.following) +
|
||||
@intFromEnum(parser.DocumentPosition.contained_by);
|
||||
}
|
||||
|
||||
const rootself = try parser.nodeGetRootNode(self);
|
||||
const rootother = try parser.nodeGetRootNode(other);
|
||||
if (rootself != rootother) {
|
||||
return @intFromEnum(parser.DocumentPosition.disconnected) +
|
||||
@intFromEnum(parser.DocumentPosition.implementation_specific) +
|
||||
@intFromEnum(parser.DocumentPosition.preceding);
|
||||
if (docself == null or docother == null or docother.? != docself.?) {
|
||||
return @intFromEnum(parser.DocumentPosition.disconnected);
|
||||
}
|
||||
|
||||
// TODO Both are in a different trees in the same document.
|
||||
@@ -351,35 +262,21 @@ pub const Node = struct {
|
||||
return try parser.nodeContains(self, other);
|
||||
}
|
||||
|
||||
// Returns itself or ancestor object inheriting from Node.
|
||||
// - An Element inside a standard web page will return an HTMLDocument object representing the entire page (or <iframe>).
|
||||
// - An Element inside a shadow DOM will return the associated ShadowRoot.
|
||||
// - An Element that is not attached to a document or a shadow tree will return the root of the DOM tree it belongs to
|
||||
const GetRootNodeResult = union(enum) {
|
||||
shadow_root: *ShadowRoot,
|
||||
node: Union,
|
||||
};
|
||||
pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }, page: *Page) !GetRootNodeResult {
|
||||
if (options) |options_| if (options_.composed) {
|
||||
log.warn(.web_api, "not implemented", .{ .feature = "getRootNode composed" });
|
||||
};
|
||||
|
||||
const root = try parser.nodeGetRootNode(self);
|
||||
if (page.getNodeState(root)) |state| {
|
||||
if (state.shadow_root) |sr| {
|
||||
return .{ .shadow_root = sr };
|
||||
}
|
||||
pub fn _getRootNode(self: *parser.Node) !?HTMLElem.Union {
|
||||
// TODO return this’s shadow-including root if options["composed"] is true
|
||||
const res = try parser.nodeOwnerDocument(self);
|
||||
if (res == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return .{ .node = try Node.toInterface(root) };
|
||||
return try HTMLElem.toInterface(HTMLElem.Union, @as(*parser.Element, @ptrCast(res.?)));
|
||||
}
|
||||
|
||||
pub fn _hasChildNodes(self: *parser.Node) !bool {
|
||||
return try parser.nodeHasChildNodes(self);
|
||||
}
|
||||
|
||||
pub fn get_childNodes(self: *parser.Node, page: *Page) !NodeList {
|
||||
const allocator = page.arena;
|
||||
pub fn get_childNodes(self: *parser.Node, state: *SessionState) !NodeList {
|
||||
const allocator = state.arena;
|
||||
var list: NodeList = .{};
|
||||
|
||||
var n = try parser.nodeFirstChild(self) orelse return list;
|
||||
@@ -389,29 +286,8 @@ pub const Node = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _insertBefore(self: *parser.Node, new_node: *parser.Node, ref_node_: ?*parser.Node) !Union {
|
||||
if (ref_node_ == null) {
|
||||
return _appendChild(self, new_node);
|
||||
}
|
||||
|
||||
const self_owner = try parser.nodeOwnerDocument(self);
|
||||
const new_node_owner = try parser.nodeOwnerDocument(new_node);
|
||||
|
||||
// If the node to be inserted has a different ownerDocument than the parent node,
|
||||
// modern browsers automatically adopt the node and its descendants into
|
||||
// the parent's ownerDocument.
|
||||
// This process is known as adoption.
|
||||
// (7.1) https://dom.spec.whatwg.org/#concept-node-insert
|
||||
if (new_node_owner == null or (self_owner != null and new_node_owner.? != self_owner.?)) {
|
||||
const w = Walker{};
|
||||
var current = new_node;
|
||||
while (true) {
|
||||
current.owner = self_owner;
|
||||
current = try w.get_next(new_node, current) orelse break;
|
||||
}
|
||||
}
|
||||
|
||||
return Node.toInterface(try parser.nodeInsertBefore(self, new_node, ref_node_.?));
|
||||
pub fn _insertBefore(self: *parser.Node, new_node: *parser.Node, ref_node: *parser.Node) !*parser.Node {
|
||||
return try parser.nodeInsertBefore(self, new_node, ref_node);
|
||||
}
|
||||
|
||||
pub fn _isDefaultNamespace(self: *parser.Node, namespace: ?[]const u8) !bool {
|
||||
@@ -614,7 +490,7 @@ pub const Node = struct {
|
||||
fn toNode(self: NodeOrText, doc: *parser.Document) !*parser.Node {
|
||||
return switch (self) {
|
||||
.node => |n| n,
|
||||
.text => |txt| @ptrCast(@alignCast(try parser.documentCreateTextNode(doc, txt))),
|
||||
.text => |txt| @ptrCast(try parser.documentCreateTextNode(doc, txt)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -725,13 +601,7 @@ test "Browser.DOM.node" {
|
||||
try runner.testCases(&.{
|
||||
.{ "content.isConnected", "true" },
|
||||
.{ "document.isConnected", "true" },
|
||||
.{ "const connDiv = document.createElement('div')", null },
|
||||
.{ "connDiv.isConnected", "false" },
|
||||
.{ "const connParentDiv = document.createElement('div')", null },
|
||||
.{ "connParentDiv.appendChild(connDiv)", null },
|
||||
.{ "connDiv.isConnected", "false" },
|
||||
.{ "content.appendChild(connParentDiv)", null },
|
||||
.{ "connDiv.isConnected", "true" },
|
||||
.{ "document.createElement('div').isConnected", "false" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
@@ -789,10 +659,6 @@ test "Browser.DOM.node" {
|
||||
.{ "let insertBefore = document.createElement('a')", "undefined" },
|
||||
.{ "link.insertBefore(insertBefore, text) !== undefined", "true" },
|
||||
.{ "link.firstChild.localName === 'a'", "true" },
|
||||
|
||||
.{ "let insertBefore2 = document.createElement('b')", null },
|
||||
.{ "link.insertBefore(insertBefore2, null).localName", "b" },
|
||||
.{ "link.childNodes[link.childNodes.length - 1].localName", "b" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
@@ -819,10 +685,6 @@ test "Browser.DOM.node" {
|
||||
.{ "link.normalize()", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "link.baseURI", "https://lightpanda.io/opensource-browser/" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "content.removeChild(append) !== undefined", "true" },
|
||||
.{ "last_child.__proto__.constructor.name !== 'HTMLHeadingElement'", "true" },
|
||||
@@ -848,45 +710,3 @@ test "Browser.DOM.node" {
|
||||
.{ "Node.NOTATION_NODE", "12" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
test "Browser.DOM.node.owner" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
|
||||
\\ <div id="target-container">
|
||||
\\ <p id="reference-node">
|
||||
\\ I am the original reference node.
|
||||
\\ </p>
|
||||
\\ </div>"
|
||||
});
|
||||
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ const parser = new DOMParser();
|
||||
\\ const newDoc = parser.parseFromString('<div id="new-node"><p>Hey</p><span>Marked</span></div>', 'text/html');
|
||||
\\ const newNode = newDoc.getElementById('new-node');
|
||||
\\ const parent = document.getElementById('target-container');
|
||||
\\ const referenceNode = document.getElementById('reference-node');
|
||||
\\ parent.insertBefore(newNode, referenceNode);
|
||||
\\ const k = document.getElementById('new-node');
|
||||
\\ const ptag = k.querySelector('p');
|
||||
\\ const spanTag = k.querySelector('span');
|
||||
\\ const anotherDoc = parser.parseFromString('<div id="another-new-node"></div>', 'text/html');
|
||||
\\ const anotherNewNode = anotherDoc.getElementById('another-new-node');
|
||||
\\
|
||||
\\ parent.appendChild(anotherNewNode)
|
||||
,
|
||||
"[object HTMLDivElement]",
|
||||
},
|
||||
|
||||
.{ "parent.ownerDocument === newNode.ownerDocument", "true" },
|
||||
.{ "parent.ownerDocument === anotherNewNode.ownerDocument", "true" },
|
||||
.{ "newNode.firstChild.nodeName", "P" },
|
||||
.{ "ptag.ownerDocument === parent.ownerDocument", "true" },
|
||||
.{ "spanTag.ownerDocument === parent.ownerDocument", "true" },
|
||||
.{ "parent.contains(newNode)", "true" },
|
||||
.{ "parent.contains(anotherNewNode)", "true" },
|
||||
.{ "anotherDoc.contains(anotherNewNode)", "false" },
|
||||
.{ "newDoc.contains(newNode)", "false" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -1,89 +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");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Node = @import("node.zig").Node;
|
||||
|
||||
pub const NodeFilter = struct {
|
||||
pub const _FILTER_ACCEPT: u16 = 1;
|
||||
pub const _FILTER_REJECT: u16 = 2;
|
||||
pub const _FILTER_SKIP: u16 = 3;
|
||||
|
||||
pub const _SHOW_ALL: u32 = std.math.maxInt(u32);
|
||||
pub const _SHOW_ELEMENT: u32 = 0b1;
|
||||
pub const _SHOW_ATTRIBUTE: u32 = 0b10;
|
||||
pub const _SHOW_TEXT: u32 = 0b100;
|
||||
pub const _SHOW_CDATA_SECTION: u32 = 0b1000;
|
||||
pub const _SHOW_ENTITY_REFERENCE: u32 = 0b10000;
|
||||
pub const _SHOW_ENTITY: u32 = 0b100000;
|
||||
pub const _SHOW_PROCESSING_INSTRUCTION: u32 = 0b1000000;
|
||||
pub const _SHOW_COMMENT: u32 = 0b10000000;
|
||||
pub const _SHOW_DOCUMENT: u32 = 0b100000000;
|
||||
pub const _SHOW_DOCUMENT_TYPE: u32 = 0b1000000000;
|
||||
pub const _SHOW_DOCUMENT_FRAGMENT: u32 = 0b10000000000;
|
||||
pub const _SHOW_NOTATION: u32 = 0b100000000000;
|
||||
};
|
||||
|
||||
const VerifyResult = enum { accept, skip, reject };
|
||||
|
||||
pub fn verify(what_to_show: u32, filter: ?Env.Function, node: *parser.Node) !VerifyResult {
|
||||
const node_type = try parser.nodeType(node);
|
||||
|
||||
// Verify that we can show this node type.
|
||||
if (!switch (node_type) {
|
||||
.attribute => what_to_show & NodeFilter._SHOW_ATTRIBUTE != 0,
|
||||
.cdata_section => what_to_show & NodeFilter._SHOW_CDATA_SECTION != 0,
|
||||
.comment => what_to_show & NodeFilter._SHOW_COMMENT != 0,
|
||||
.document => what_to_show & NodeFilter._SHOW_DOCUMENT != 0,
|
||||
.document_fragment => what_to_show & NodeFilter._SHOW_DOCUMENT_FRAGMENT != 0,
|
||||
.document_type => what_to_show & NodeFilter._SHOW_DOCUMENT_TYPE != 0,
|
||||
.element => what_to_show & NodeFilter._SHOW_ELEMENT != 0,
|
||||
.entity => what_to_show & NodeFilter._SHOW_ENTITY != 0,
|
||||
.entity_reference => what_to_show & NodeFilter._SHOW_ENTITY_REFERENCE != 0,
|
||||
.notation => what_to_show & NodeFilter._SHOW_NOTATION != 0,
|
||||
.processing_instruction => what_to_show & NodeFilter._SHOW_PROCESSING_INSTRUCTION != 0,
|
||||
.text => what_to_show & NodeFilter._SHOW_TEXT != 0,
|
||||
}) return .reject;
|
||||
|
||||
// Verify that we aren't filtering it out.
|
||||
if (filter) |f| {
|
||||
const acceptance = try f.call(u16, .{try Node.toInterface(node)});
|
||||
return switch (acceptance) {
|
||||
NodeFilter._FILTER_ACCEPT => .accept,
|
||||
NodeFilter._FILTER_REJECT => .reject,
|
||||
NodeFilter._FILTER_SKIP => .skip,
|
||||
else => .reject,
|
||||
};
|
||||
} else return .accept;
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.NodeFilter" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "NodeFilter.FILTER_ACCEPT", "1" },
|
||||
.{ "NodeFilter.FILTER_REJECT", "2" },
|
||||
.{ "NodeFilter.FILTER_SKIP", "3" },
|
||||
.{ "NodeFilter.SHOW_ALL", "4294967295" },
|
||||
.{ "NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT", "129" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -1,338 +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 parser = @import("../netsurf.zig");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const NodeFilter = @import("node_filter.zig");
|
||||
const Node = @import("node.zig").Node;
|
||||
const NodeUnion = @import("node.zig").Union;
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/NodeIterator
|
||||
// While this is similar to TreeWalker it has its own implementation as there are several subtle differences
|
||||
// For example:
|
||||
// - nextNode returns the reference node, whereas TreeWalker returns the next node
|
||||
// - Skip and reject are equivalent for NodeIterator, for TreeWalker they are different
|
||||
pub const NodeIterator = struct {
|
||||
pub const Exception = DOMException;
|
||||
|
||||
root: *parser.Node,
|
||||
reference_node: *parser.Node,
|
||||
what_to_show: u32,
|
||||
filter: ?NodeIteratorOpts,
|
||||
filter_func: ?Env.Function,
|
||||
pointer_before_current: bool = true,
|
||||
// used to track / block recursive filters
|
||||
is_in_callback: bool = false,
|
||||
|
||||
// One of the few cases where null and undefined resolve to different default.
|
||||
// We need the raw JsObject so that we can probe the tri state:
|
||||
// null, undefined or i32.
|
||||
pub const WhatToShow = Env.JsObject;
|
||||
|
||||
pub const NodeIteratorOpts = union(enum) {
|
||||
function: Env.Function,
|
||||
object: struct { acceptNode: Env.Function },
|
||||
};
|
||||
|
||||
pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?NodeIteratorOpts) !NodeIterator {
|
||||
var filter_func: ?Env.Function = null;
|
||||
if (filter) |f| {
|
||||
filter_func = switch (f) {
|
||||
.function => |func| func,
|
||||
.object => |o| o.acceptNode,
|
||||
};
|
||||
}
|
||||
|
||||
var what_to_show: u32 = undefined;
|
||||
if (what_to_show_) |wts| {
|
||||
switch (try wts.triState(NodeIterator, "what_to_show", u32)) {
|
||||
.null => what_to_show = 0,
|
||||
.undefined => what_to_show = NodeFilter.NodeFilter._SHOW_ALL,
|
||||
.value => |v| what_to_show = v,
|
||||
}
|
||||
} else {
|
||||
what_to_show = NodeFilter.NodeFilter._SHOW_ALL;
|
||||
}
|
||||
|
||||
return .{
|
||||
.root = node,
|
||||
.reference_node = node,
|
||||
.what_to_show = what_to_show,
|
||||
.filter = filter,
|
||||
.filter_func = filter_func,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_filter(self: *const NodeIterator) ?NodeIteratorOpts {
|
||||
return self.filter;
|
||||
}
|
||||
|
||||
pub fn get_pointerBeforeReferenceNode(self: *const NodeIterator) bool {
|
||||
return self.pointer_before_current;
|
||||
}
|
||||
|
||||
pub fn get_referenceNode(self: *const NodeIterator) !NodeUnion {
|
||||
return try Node.toInterface(self.reference_node);
|
||||
}
|
||||
|
||||
pub fn get_root(self: *const NodeIterator) !NodeUnion {
|
||||
return try Node.toInterface(self.root);
|
||||
}
|
||||
|
||||
pub fn get_whatToShow(self: *const NodeIterator) u32 {
|
||||
return self.what_to_show;
|
||||
}
|
||||
|
||||
pub fn _nextNode(self: *NodeIterator) !?NodeUnion {
|
||||
try self.callbackStart();
|
||||
defer self.callbackEnd();
|
||||
|
||||
if (self.pointer_before_current) {
|
||||
// Unlike TreeWalker, NodeIterator starts at the first node
|
||||
if (.accept == try NodeFilter.verify(self.what_to_show, self.filter_func, self.reference_node)) {
|
||||
self.pointer_before_current = false;
|
||||
return try Node.toInterface(self.reference_node);
|
||||
}
|
||||
}
|
||||
|
||||
if (try self.firstChild(self.reference_node)) |child| {
|
||||
self.reference_node = child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
|
||||
var current = self.reference_node;
|
||||
while (current != self.root) {
|
||||
if (try self.nextSibling(current)) |sibling| {
|
||||
self.reference_node = sibling;
|
||||
return try Node.toInterface(sibling);
|
||||
}
|
||||
|
||||
current = (try parser.nodeParentNode(current)) orelse break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn _previousNode(self: *NodeIterator) !?NodeUnion {
|
||||
try self.callbackStart();
|
||||
defer self.callbackEnd();
|
||||
|
||||
if (!self.pointer_before_current) {
|
||||
if (.accept == try NodeFilter.verify(self.what_to_show, self.filter_func, self.reference_node)) {
|
||||
self.pointer_before_current = true;
|
||||
// Still need to verify as last may be first as well
|
||||
return try Node.toInterface(self.reference_node);
|
||||
}
|
||||
}
|
||||
if (self.reference_node == self.root) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var current = self.reference_node;
|
||||
while (try parser.nodePreviousSibling(current)) |previous| {
|
||||
current = previous;
|
||||
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||
.accept => {
|
||||
// Get last child if it has one.
|
||||
if (try self.lastChild(current)) |child| {
|
||||
self.reference_node = child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
|
||||
// Otherwise, this node is our previous one.
|
||||
self.reference_node = current;
|
||||
return try Node.toInterface(current);
|
||||
},
|
||||
.reject, .skip => {
|
||||
// Get last child if it has one.
|
||||
if (try self.lastChild(current)) |child| {
|
||||
self.reference_node = child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (current != self.root) {
|
||||
if (try self.parentNode(current)) |parent| {
|
||||
self.reference_node = parent;
|
||||
return try Node.toInterface(parent);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn _detach(self: *const NodeIterator) void {
|
||||
// no-op as per spec
|
||||
_ = self;
|
||||
}
|
||||
|
||||
fn firstChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
|
||||
const children = try parser.nodeGetChildNodes(node);
|
||||
const child_count = try parser.nodeListLength(children);
|
||||
|
||||
for (0..child_count) |i| {
|
||||
const index: u32 = @intCast(i);
|
||||
const child = (try parser.nodeListItem(children, index)) orelse return null;
|
||||
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
|
||||
.accept => return child, // NOTE: Skip and reject are equivalent for NodeIterator, this is different from TreeWalker
|
||||
.reject, .skip => if (try self.firstChild(child)) |gchild| return gchild,
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn lastChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
|
||||
const children = try parser.nodeGetChildNodes(node);
|
||||
const child_count = try parser.nodeListLength(children);
|
||||
|
||||
var index: u32 = child_count;
|
||||
while (index > 0) {
|
||||
index -= 1;
|
||||
const child = (try parser.nodeListItem(children, index)) orelse return null;
|
||||
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
|
||||
.accept => return child, // NOTE: Skip and reject are equivalent for NodeIterator, this is different from TreeWalker
|
||||
.reject, .skip => if (try self.lastChild(child)) |gchild| return gchild,
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// This implementation is actually the same as :TreeWalker
|
||||
fn parentNode(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
|
||||
if (self.root == node) return null;
|
||||
|
||||
var current = node;
|
||||
while (true) {
|
||||
if (current == self.root) return null;
|
||||
current = (try parser.nodeParentNode(current)) orelse return null;
|
||||
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||
.accept => return current,
|
||||
.reject, .skip => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This implementation is actually the same as :TreeWalker
|
||||
fn nextSibling(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
|
||||
var current = node;
|
||||
|
||||
while (true) {
|
||||
current = (try parser.nodeNextSibling(current)) orelse return null;
|
||||
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||
.accept => return current,
|
||||
.skip, .reject => continue,
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn callbackStart(self: *NodeIterator) !void {
|
||||
if (self.is_in_callback) {
|
||||
// this is the correct DOMExeption
|
||||
return error.InvalidState;
|
||||
}
|
||||
self.is_in_callback = true;
|
||||
}
|
||||
|
||||
fn callbackEnd(self: *NodeIterator) void {
|
||||
self.is_in_callback = false;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.NodeFilter" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ const nodeIterator = document.createNodeIterator(
|
||||
\\ document.body,
|
||||
\\ NodeFilter.SHOW_ELEMENT,
|
||||
\\ {
|
||||
\\ acceptNode(node) {
|
||||
\\ return NodeFilter.FILTER_ACCEPT;
|
||||
\\ },
|
||||
\\ },
|
||||
\\ );
|
||||
\\ nodeIterator.nextNode().nodeName;
|
||||
,
|
||||
"BODY",
|
||||
},
|
||||
.{ "nodeIterator.nextNode().nodeName", "DIV" },
|
||||
.{ "nodeIterator.nextNode().nodeName", "A" },
|
||||
.{ "nodeIterator.previousNode().nodeName", "A" }, // pointer_before_current flips
|
||||
.{ "nodeIterator.nextNode().nodeName", "A" }, // pointer_before_current flips
|
||||
.{ "nodeIterator.previousNode().nodeName", "A" }, // pointer_before_current flips
|
||||
.{ "nodeIterator.previousNode().nodeName", "DIV" },
|
||||
.{ "nodeIterator.previousNode().nodeName", "BODY" },
|
||||
.{ "nodeIterator.previousNode()", "null" }, // Not HEAD since body is root
|
||||
.{ "nodeIterator.previousNode()", "null" }, // Keeps returning null
|
||||
.{ "nodeIterator.nextNode().nodeName", "BODY" },
|
||||
|
||||
.{ "nodeIterator.nextNode().nodeName", null },
|
||||
.{ "nodeIterator.nextNode().nodeName", null },
|
||||
.{ "nodeIterator.nextNode().nodeName", null },
|
||||
.{ "nodeIterator.nextNode().nodeName", "SPAN" },
|
||||
.{ "nodeIterator.nextNode().nodeName", "P" },
|
||||
.{ "nodeIterator.nextNode()", "null" }, // Just the last one
|
||||
.{ "nodeIterator.nextNode()", "null" }, // Keeps returning null
|
||||
.{ "nodeIterator.previousNode().nodeName", "P" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ const notationIterator = document.createNodeIterator(
|
||||
\\ document.body,
|
||||
\\ NodeFilter.SHOW_NOTATION,
|
||||
\\ );
|
||||
\\ notationIterator.nextNode();
|
||||
,
|
||||
"null",
|
||||
},
|
||||
.{ "notationIterator.previousNode()", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nodeIterator.filter.acceptNode(document.body)", "1" },
|
||||
.{ "notationIterator.filter", "null" },
|
||||
.{
|
||||
\\ const rejectIterator = document.createNodeIterator(
|
||||
\\ document.body,
|
||||
\\ NodeFilter.SHOW_ALL,
|
||||
\\ (e => { return NodeFilter.FILTER_REJECT}),
|
||||
\\ );
|
||||
\\ rejectIterator.filter(document.body);
|
||||
,
|
||||
"2",
|
||||
},
|
||||
}, .{});
|
||||
}
|
||||
@@ -18,17 +18,18 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const JsThis = @import("../env.zig").JsThis;
|
||||
const Function = @import("../env.zig").Function;
|
||||
const Callback = @import("../env.zig").Callback;
|
||||
|
||||
const NodeUnion = @import("node.zig").Union;
|
||||
const Node = @import("node.zig").Node;
|
||||
|
||||
const U32Iterator = @import("../iterator/iterator.zig").U32Iterator;
|
||||
|
||||
const log = std.log.scoped(.nodelist);
|
||||
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
|
||||
pub const Interfaces = .{
|
||||
@@ -110,7 +111,7 @@ pub const NodeList = struct {
|
||||
try self.nodes.append(alloc, node);
|
||||
}
|
||||
|
||||
pub fn get_length(self: *const NodeList) u32 {
|
||||
pub fn get_length(self: *NodeList) u32 {
|
||||
return @intCast(self.nodes.items.len);
|
||||
}
|
||||
|
||||
@@ -140,12 +141,13 @@ pub const NodeList = struct {
|
||||
// };
|
||||
// }
|
||||
|
||||
pub fn _forEach(self: *NodeList, cbk: Function) !void { // TODO handle thisArg
|
||||
pub fn _forEach(self: *NodeList, cbk: Callback) !void { // TODO handle thisArg
|
||||
for (self.nodes.items, 0..) |n, i| {
|
||||
const ii: u32 = @intCast(i);
|
||||
var result: Function.Result = undefined;
|
||||
cbk.tryCall(void, .{ n, ii, self }, &result) catch {
|
||||
log.debug(.user_script, "forEach callback", .{ .err = result.exception, .stack = result.stack });
|
||||
var result: Callback.Result = undefined;
|
||||
cbk.tryCall(.{ n, ii, self }, &result) catch {
|
||||
log.err("callback error: {s}", .{result.exception});
|
||||
log.debug("stack:\n{s}", .{result.stack orelse "???"});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,224 +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 parser = @import("../netsurf.zig");
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
const milliTimestamp = @import("../../datetime.zig").milliTimestamp;
|
||||
|
||||
pub const Interfaces = .{
|
||||
Performance,
|
||||
PerformanceEntry,
|
||||
PerformanceMark,
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Performance
|
||||
pub const Performance = struct {
|
||||
pub const prototype = *EventTarget;
|
||||
|
||||
// Extend libdom event target for pure zig struct.
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .performance },
|
||||
|
||||
time_origin: u64,
|
||||
// if (Window.crossOriginIsolated) -> Resolution in isolated contexts: 5 microseconds
|
||||
// else -> Resolution in non-isolated contexts: 100 microseconds
|
||||
const ms_resolution = 100;
|
||||
|
||||
pub fn init() Performance {
|
||||
return .{
|
||||
.time_origin = milliTimestamp(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_timeOrigin(self: *const Performance) u64 {
|
||||
return self.time_origin;
|
||||
}
|
||||
|
||||
pub fn reset(self: *Performance) void {
|
||||
self.time_origin = milliTimestamp();
|
||||
}
|
||||
|
||||
pub fn _now(self: *const Performance) u64 {
|
||||
return milliTimestamp() - self.time_origin;
|
||||
}
|
||||
|
||||
pub fn _mark(_: *Performance, name: []const u8, _options: ?PerformanceMark.Options, page: *Page) !PerformanceMark {
|
||||
const mark: PerformanceMark = try .constructor(name, _options, page);
|
||||
// TODO: Should store this in an entries list
|
||||
return mark;
|
||||
}
|
||||
|
||||
// TODO: fn _mark should record the marks in a lookup
|
||||
pub fn _clearMarks(_: *Performance, name: ?[]const u8) void {
|
||||
_ = name;
|
||||
}
|
||||
|
||||
// TODO: fn _measures should record the marks in a lookup
|
||||
pub fn _clearMeasures(_: *Performance, name: ?[]const u8) void {
|
||||
_ = name;
|
||||
}
|
||||
|
||||
// TODO: fn _measures should record the marks in a lookup
|
||||
pub fn _getEntriesByName(_: *Performance, name: []const u8, typ: ?[]const u8) []PerformanceEntry {
|
||||
_ = name;
|
||||
_ = typ;
|
||||
return &.{};
|
||||
}
|
||||
|
||||
// TODO: fn _measures should record the marks in a lookup
|
||||
pub fn _getEntriesByType(_: *Performance, typ: []const u8) []PerformanceEntry {
|
||||
_ = typ;
|
||||
return &.{};
|
||||
}
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry
|
||||
pub const PerformanceEntry = struct {
|
||||
const PerformanceEntryType = enum {
|
||||
element,
|
||||
event,
|
||||
first_input,
|
||||
largest_contentful_paint,
|
||||
layout_shift,
|
||||
long_animation_frame,
|
||||
longtask,
|
||||
mark,
|
||||
measure,
|
||||
navigation,
|
||||
paint,
|
||||
resource,
|
||||
taskattribution,
|
||||
visibility_state,
|
||||
|
||||
pub fn toString(self: PerformanceEntryType) []const u8 {
|
||||
return switch (self) {
|
||||
.first_input => "first-input",
|
||||
.largest_contentful_paint => "largest-contentful-paint",
|
||||
.layout_shift => "layout-shift",
|
||||
.long_animation_frame => "long-animation-frame",
|
||||
.visibility_state => "visibility-state",
|
||||
else => @tagName(self),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
duration: f64 = 0.0,
|
||||
entry_type: PerformanceEntryType,
|
||||
name: []const u8,
|
||||
start_time: f64 = 0.0,
|
||||
|
||||
pub fn get_duration(self: *const PerformanceEntry) f64 {
|
||||
return self.duration;
|
||||
}
|
||||
|
||||
pub fn get_entryType(self: *const PerformanceEntry) PerformanceEntryType {
|
||||
return self.entry_type;
|
||||
}
|
||||
|
||||
pub fn get_name(self: *const PerformanceEntry) []const u8 {
|
||||
return self.name;
|
||||
}
|
||||
|
||||
pub fn get_startTime(self: *const PerformanceEntry) f64 {
|
||||
return self.start_time;
|
||||
}
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceMark
|
||||
pub const PerformanceMark = struct {
|
||||
pub const prototype = *PerformanceEntry;
|
||||
|
||||
proto: PerformanceEntry,
|
||||
detail: ?Env.JsObject,
|
||||
|
||||
const Options = struct {
|
||||
detail: ?Env.JsObject = null,
|
||||
startTime: ?f64 = null,
|
||||
};
|
||||
|
||||
pub fn constructor(name: []const u8, _options: ?Options, page: *Page) !PerformanceMark {
|
||||
const perf = &page.window.performance;
|
||||
|
||||
const options = _options orelse Options{};
|
||||
const start_time = options.startTime orelse @as(f64, @floatFromInt(perf._now()));
|
||||
|
||||
if (start_time < 0.0) {
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
const detail = if (options.detail) |d| try d.persist() else null;
|
||||
|
||||
const duped_name = try page.arena.dupe(u8, name);
|
||||
const proto = PerformanceEntry{ .name = duped_name, .entry_type = .mark, .start_time = start_time };
|
||||
|
||||
return .{ .proto = proto, .detail = detail };
|
||||
}
|
||||
|
||||
pub fn get_detail(self: *const PerformanceMark) ?Env.JsObject {
|
||||
return self.detail;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("./../../testing.zig");
|
||||
|
||||
test "Performance: get_timeOrigin" {
|
||||
var perf = Performance.init();
|
||||
const time_origin = perf.get_timeOrigin();
|
||||
try testing.expect(time_origin >= 0);
|
||||
}
|
||||
|
||||
test "Performance: now" {
|
||||
var perf = Performance.init();
|
||||
|
||||
// Monotonically increasing
|
||||
var now = perf._now();
|
||||
while (now <= 0) { // Loop for now to not be 0
|
||||
try testing.expectEqual(now, 0);
|
||||
now = perf._now();
|
||||
}
|
||||
|
||||
var after = perf._now();
|
||||
while (after <= now) { // Loop untill after > now
|
||||
try testing.expectEqual(after, now);
|
||||
after = perf._now();
|
||||
}
|
||||
}
|
||||
|
||||
test "Browser.Performance.Mark" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let performance = window.performance", null },
|
||||
.{ "performance instanceof Performance", "true" },
|
||||
|
||||
.{ "let mark1 = performance.mark(\"start\")", null },
|
||||
.{ "mark1 instanceof PerformanceMark", "true" },
|
||||
.{ "mark1.name", "start" },
|
||||
.{ "mark1.entryType", "mark" },
|
||||
.{ "mark1.duration", "0" },
|
||||
.{ "mark1.detail", "null" },
|
||||
|
||||
.{ "let mark2 = performance.mark(\"start\", {startTime: 32939393.9})", null },
|
||||
.{ "mark2.startTime", "32939393.9" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -1,63 +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 Env = @import("../env.zig").Env;
|
||||
|
||||
const PerformanceEntry = @import("performance.zig").PerformanceEntry;
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver
|
||||
pub const PerformanceObserver = struct {
|
||||
pub const _supportedEntryTypes = [0][]const u8{};
|
||||
|
||||
pub fn constructor(cbk: Env.Function) PerformanceObserver {
|
||||
_ = cbk;
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn _observe(self: *const PerformanceObserver, options_: ?Options) void {
|
||||
_ = self;
|
||||
_ = options_;
|
||||
return;
|
||||
}
|
||||
|
||||
pub fn _disconnect(self: *PerformanceObserver) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
pub fn _takeRecords(_: *const PerformanceObserver) []PerformanceEntry {
|
||||
return &[_]PerformanceEntry{};
|
||||
}
|
||||
};
|
||||
|
||||
const Options = struct {
|
||||
buffered: ?bool = null,
|
||||
durationThreshold: ?f64 = null,
|
||||
entryTypes: ?[]const []const u8 = null,
|
||||
type: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.PerformanceObserver" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "PerformanceObserver.supportedEntryTypes.length", "0" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -20,7 +20,7 @@ const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Node = @import("node.zig").Node;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
// https://dom.spec.whatwg.org/#processinginstruction
|
||||
pub const ProcessingInstruction = struct {
|
||||
@@ -39,9 +39,9 @@ pub const ProcessingInstruction = struct {
|
||||
// There's something wrong when we try to clone a ProcessInstruction normally.
|
||||
// The resulting object can't be cast back into a node (it crashes). This is
|
||||
// a simple workaround.
|
||||
pub fn _cloneNode(self: *parser.ProcessingInstruction, _: ?bool, page: *Page) !*parser.ProcessingInstruction {
|
||||
pub fn _cloneNode(self: *parser.ProcessingInstruction, _: ?bool, state: *SessionState) !*parser.ProcessingInstruction {
|
||||
return try parser.documentCreateProcessingInstruction(
|
||||
@ptrCast(page.window.document),
|
||||
@ptrCast(state.document),
|
||||
try get_target(self),
|
||||
(try get_data(self)) orelse "",
|
||||
);
|
||||
|
||||
@@ -1,428 +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");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
const Node = @import("node.zig").Node;
|
||||
const NodeUnion = @import("node.zig").Union;
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
|
||||
pub const Interfaces = .{
|
||||
AbstractRange,
|
||||
Range,
|
||||
};
|
||||
|
||||
pub const AbstractRange = struct {
|
||||
collapsed: bool,
|
||||
end_node: *parser.Node,
|
||||
end_offset: u32,
|
||||
start_node: *parser.Node,
|
||||
start_offset: u32,
|
||||
|
||||
pub fn updateCollapsed(self: *AbstractRange) void {
|
||||
// TODO: Eventually, compare properly.
|
||||
self.collapsed = false;
|
||||
}
|
||||
|
||||
pub fn get_collapsed(self: *const AbstractRange) bool {
|
||||
return self.collapsed;
|
||||
}
|
||||
|
||||
pub fn get_endContainer(self: *const AbstractRange) !NodeUnion {
|
||||
return Node.toInterface(self.end_node);
|
||||
}
|
||||
|
||||
pub fn get_endOffset(self: *const AbstractRange) u32 {
|
||||
return self.end_offset;
|
||||
}
|
||||
|
||||
pub fn get_startContainer(self: *const AbstractRange) !NodeUnion {
|
||||
return Node.toInterface(self.start_node);
|
||||
}
|
||||
|
||||
pub fn get_startOffset(self: *const AbstractRange) u32 {
|
||||
return self.start_offset;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Range = struct {
|
||||
pub const Exception = DOMException;
|
||||
pub const prototype = *AbstractRange;
|
||||
|
||||
proto: AbstractRange,
|
||||
|
||||
pub const _START_TO_START = 0;
|
||||
pub const _START_TO_END = 1;
|
||||
pub const _END_TO_END = 2;
|
||||
pub const _END_TO_START = 3;
|
||||
|
||||
// The Range() constructor returns a newly created Range object whose start
|
||||
// and end is the global Document object.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Range/Range
|
||||
pub fn constructor(page: *Page) Range {
|
||||
const proto: AbstractRange = .{
|
||||
.collapsed = true,
|
||||
.end_node = parser.documentHTMLToNode(page.window.document),
|
||||
.end_offset = 0,
|
||||
.start_node = parser.documentHTMLToNode(page.window.document),
|
||||
.start_offset = 0,
|
||||
};
|
||||
|
||||
return .{ .proto = proto };
|
||||
}
|
||||
|
||||
pub fn _setStart(self: *Range, node: *parser.Node, offset_: i32) !void {
|
||||
try ensureValidOffset(node, offset_);
|
||||
const offset: u32 = @intCast(offset_);
|
||||
const position = compare(node, offset, self.proto.start_node, self.proto.start_offset) catch |err| switch (err) {
|
||||
error.WrongDocument => blk: {
|
||||
// allow a node with a different root than the current, or
|
||||
// a disconnected one. Treat it as if it's "after", so that
|
||||
// we also update the end_offset and end_node.
|
||||
break :blk 1;
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
|
||||
if (position == 1) {
|
||||
// if we're setting the node after the current start, the end must
|
||||
// be set too.
|
||||
self.proto.end_offset = offset;
|
||||
self.proto.end_node = node;
|
||||
}
|
||||
self.proto.start_node = node;
|
||||
self.proto.start_offset = offset;
|
||||
self.proto.updateCollapsed();
|
||||
}
|
||||
|
||||
pub fn _setStartBefore(self: *Range, node: *parser.Node) !void {
|
||||
const parent, const index = try getParentAndIndex(node);
|
||||
self.proto.start_node = parent;
|
||||
self.proto.start_offset = index;
|
||||
}
|
||||
|
||||
pub fn _setStartAfter(self: *Range, node: *parser.Node) !void {
|
||||
const parent, const index = try getParentAndIndex(node);
|
||||
self.proto.start_node = parent;
|
||||
self.proto.start_offset = index + 1;
|
||||
}
|
||||
|
||||
pub fn _setEnd(self: *Range, node: *parser.Node, offset_: i32) !void {
|
||||
try ensureValidOffset(node, offset_);
|
||||
const offset: u32 = @intCast(offset_);
|
||||
|
||||
const position = compare(node, offset, self.proto.start_node, self.proto.start_offset) catch |err| switch (err) {
|
||||
error.WrongDocument => blk: {
|
||||
// allow a node with a different root than the current, or
|
||||
// a disconnected one. Treat it as if it's "before", so that
|
||||
// we also update the end_offset and end_node.
|
||||
break :blk -1;
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
|
||||
if (position == -1) {
|
||||
// if we're setting the node before the current start, the start
|
||||
// must be set too.
|
||||
self.proto.start_offset = offset;
|
||||
self.proto.start_node = node;
|
||||
}
|
||||
|
||||
self.proto.end_node = node;
|
||||
self.proto.end_offset = offset;
|
||||
self.proto.updateCollapsed();
|
||||
}
|
||||
|
||||
pub fn _setEndBefore(self: *Range, node: *parser.Node) !void {
|
||||
const parent, const index = try getParentAndIndex(node);
|
||||
self.proto.end_node = parent;
|
||||
self.proto.end_offset = index;
|
||||
}
|
||||
|
||||
pub fn _setEndAfter(self: *Range, node: *parser.Node) !void {
|
||||
const parent, const index = try getParentAndIndex(node);
|
||||
self.proto.end_node = parent;
|
||||
self.proto.end_offset = index + 1;
|
||||
}
|
||||
|
||||
pub fn _createContextualFragment(_: *Range, fragment: []const u8, page: *Page) !*parser.DocumentFragment {
|
||||
const document_html = page.window.document;
|
||||
const document = parser.documentHTMLToDocument(document_html);
|
||||
const doc_frag = try parser.documentParseFragmentFromStr(document, fragment);
|
||||
return doc_frag;
|
||||
}
|
||||
|
||||
pub fn _selectNodeContents(self: *Range, node: *parser.Node) !void {
|
||||
self.proto.start_node = node;
|
||||
self.proto.start_offset = 0;
|
||||
self.proto.end_node = node;
|
||||
|
||||
// Set end_offset
|
||||
switch (try parser.nodeType(node)) {
|
||||
.text, .cdata_section, .comment, .processing_instruction => {
|
||||
// For text-like nodes, end_offset should be the length of the text data
|
||||
if (try parser.nodeValue(node)) |text_data| {
|
||||
self.proto.end_offset = @intCast(text_data.len);
|
||||
} else {
|
||||
self.proto.end_offset = 0;
|
||||
}
|
||||
},
|
||||
else => {
|
||||
// For element and other nodes, end_offset is the number of children
|
||||
const child_nodes = try parser.nodeGetChildNodes(node);
|
||||
const child_count = try parser.nodeListLength(child_nodes);
|
||||
self.proto.end_offset = @intCast(child_count);
|
||||
},
|
||||
}
|
||||
|
||||
self.proto.updateCollapsed();
|
||||
}
|
||||
|
||||
// creates a copy
|
||||
pub fn _cloneRange(self: *const Range) Range {
|
||||
return .{
|
||||
.proto = .{
|
||||
.collapsed = self.proto.collapsed,
|
||||
.end_node = self.proto.end_node,
|
||||
.end_offset = self.proto.end_offset,
|
||||
.start_node = self.proto.start_node,
|
||||
.start_offset = self.proto.start_offset,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _comparePoint(self: *const Range, node: *parser.Node, offset_: i32) !i32 {
|
||||
const start = self.proto.start_node;
|
||||
if (try parser.nodeGetRootNode(start) != try parser.nodeGetRootNode(node)) {
|
||||
// WPT really wants this error to be first. Later, when we check
|
||||
// if the relative position is 'disconnected', it'll also catch this
|
||||
// case, but WPT will complain because it sometimes also sends
|
||||
// invalid offsets, and it wants WrongDocument to be raised.
|
||||
return error.WrongDocument;
|
||||
}
|
||||
|
||||
if (try parser.nodeType(node) == .document_type) {
|
||||
return error.InvalidNodeType;
|
||||
}
|
||||
|
||||
try ensureValidOffset(node, offset_);
|
||||
|
||||
const offset: u32 = @intCast(offset_);
|
||||
if (try compare(node, offset, start, self.proto.start_offset) == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (try compare(node, offset, self.proto.end_node, self.proto.end_offset) == 1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
pub fn _isPointInRange(self: *const Range, node: *parser.Node, offset_: i32) !bool {
|
||||
return self._comparePoint(node, offset_) catch |err| switch (err) {
|
||||
error.WrongDocument => return false,
|
||||
else => return err,
|
||||
} == 0;
|
||||
}
|
||||
|
||||
pub fn _intersectsNode(self: *const Range, node: *parser.Node) !bool {
|
||||
const start_root = try parser.nodeGetRootNode(self.proto.start_node);
|
||||
const node_root = try parser.nodeGetRootNode(node);
|
||||
if (start_root != node_root) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parent, const index = getParentAndIndex(node) catch |err| switch (err) {
|
||||
error.InvalidNodeType => return true, // if node has no parent, we return true.
|
||||
else => return err,
|
||||
};
|
||||
|
||||
if (try compare(parent, index + 1, self.proto.start_node, self.proto.start_offset) != 1) {
|
||||
// node isn't after start, can't intersect
|
||||
return false;
|
||||
}
|
||||
|
||||
if (try compare(parent, index, self.proto.end_node, self.proto.end_offset) != -1) {
|
||||
// node isn't before end, can't intersect
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn _compareBoundaryPoints(self: *const Range, how: i32, other: *const Range) !i32 {
|
||||
return switch (how) {
|
||||
_START_TO_START => compare(self.proto.start_node, self.proto.start_offset, other.proto.start_node, other.proto.start_offset),
|
||||
_START_TO_END => compare(self.proto.start_node, self.proto.start_offset, other.proto.end_node, other.proto.end_offset),
|
||||
_END_TO_END => compare(self.proto.end_node, self.proto.end_offset, other.proto.end_node, other.proto.end_offset),
|
||||
_END_TO_START => compare(self.proto.end_node, self.proto.end_offset, other.proto.start_node, other.proto.start_offset),
|
||||
else => error.NotSupported, // this is the correct DOM Exception to return
|
||||
};
|
||||
}
|
||||
|
||||
// The Range.detach() method does nothing. It used to disable the Range
|
||||
// object and enable the browser to release associated resources. The
|
||||
// method has been kept for compatibility.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Range/detach
|
||||
pub fn _detach(_: *Range) void {}
|
||||
};
|
||||
|
||||
fn ensureValidOffset(node: *parser.Node, offset: i32) !void {
|
||||
if (offset < 0) {
|
||||
return error.IndexSize;
|
||||
}
|
||||
|
||||
// not >= because 0 seems to represent the node itself.
|
||||
if (offset > try nodeLength(node)) {
|
||||
return error.IndexSize;
|
||||
}
|
||||
}
|
||||
|
||||
fn nodeLength(node: *parser.Node) !usize {
|
||||
switch (try isTextual(node)) {
|
||||
true => return ((try parser.nodeTextContent(node)) orelse "").len,
|
||||
false => {
|
||||
const children = try parser.nodeGetChildNodes(node);
|
||||
return @intCast(try parser.nodeListLength(children));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn isTextual(node: *parser.Node) !bool {
|
||||
return switch (try parser.nodeType(node)) {
|
||||
.text, .comment, .cdata_section => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn getParentAndIndex(child: *parser.Node) !struct { *parser.Node, u32 } {
|
||||
const parent = (try parser.nodeParentNode(child)) orelse return error.InvalidNodeType;
|
||||
const children = try parser.nodeGetChildNodes(parent);
|
||||
const ln = try parser.nodeListLength(children);
|
||||
var i: u32 = 0;
|
||||
while (i < ln) {
|
||||
defer i += 1;
|
||||
const c = try parser.nodeListItem(children, i) orelse continue;
|
||||
if (c == child) {
|
||||
return .{ parent, i };
|
||||
}
|
||||
}
|
||||
|
||||
// should not be possible to reach this point
|
||||
return error.InvalidNodeType;
|
||||
}
|
||||
|
||||
// implementation is largely copied from the WPT helper called getPosition in
|
||||
// the common.js of the dom folder.
|
||||
fn compare(node_a: *parser.Node, offset_a: u32, node_b: *parser.Node, offset_b: u32) !i32 {
|
||||
if (node_a == node_b) {
|
||||
// This is a simple and common case, where the two nodes are the same
|
||||
// We just need to compare their offsets
|
||||
if (offset_a == offset_b) {
|
||||
return 0;
|
||||
}
|
||||
return if (offset_a < offset_b) -1 else 1;
|
||||
}
|
||||
|
||||
// We're probably comparing two different nodes. "Probably", because the
|
||||
// above case on considered the offset if the two nodes were the same
|
||||
// as-is. They could still be the same here, if we first consider the
|
||||
// offset.
|
||||
const position = try Node._compareDocumentPosition(node_b, node_a);
|
||||
if (position & @intFromEnum(parser.DocumentPosition.disconnected) == @intFromEnum(parser.DocumentPosition.disconnected)) {
|
||||
return error.WrongDocument;
|
||||
}
|
||||
|
||||
if (position & @intFromEnum(parser.DocumentPosition.following) == @intFromEnum(parser.DocumentPosition.following)) {
|
||||
return switch (try compare(node_b, offset_b, node_a, offset_a)) {
|
||||
-1 => 1,
|
||||
1 => -1,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
if (position & @intFromEnum(parser.DocumentPosition.contains) == @intFromEnum(parser.DocumentPosition.contains)) {
|
||||
// node_a contains node_b
|
||||
var child = node_b;
|
||||
while (try parser.nodeParentNode(child)) |parent| {
|
||||
if (parent == node_a) {
|
||||
// child.parentNode == node_a
|
||||
break;
|
||||
}
|
||||
child = parent;
|
||||
} else {
|
||||
// this should not happen, because Node._compareDocumentPosition
|
||||
// has told us that node_a contains node_b, so one of node_b's
|
||||
// parent's MUST be node_a. But somehow we do end up here sometimes.
|
||||
return -1;
|
||||
}
|
||||
|
||||
const child_parent, const child_index = try getParentAndIndex(child);
|
||||
std.debug.assert(node_a == child_parent);
|
||||
return if (child_index < offset_a) -1 else 1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.Range" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
// Test Range constructor
|
||||
.{ "let range = new Range()", "undefined" },
|
||||
.{ "range instanceof Range", "true" },
|
||||
.{ "range instanceof AbstractRange", "true" },
|
||||
|
||||
// Test initial state - collapsed range
|
||||
.{ "range.collapsed", "true" },
|
||||
.{ "range.startOffset", "0" },
|
||||
.{ "range.endOffset", "0" },
|
||||
.{ "range.startContainer instanceof HTMLDocument", "true" },
|
||||
.{ "range.endContainer instanceof HTMLDocument", "true" },
|
||||
|
||||
// Test document.createRange()
|
||||
.{ "let docRange = document.createRange()", "undefined" },
|
||||
.{ "docRange instanceof Range", "true" },
|
||||
.{ "docRange.collapsed", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const container = document.getElementById('content');", null },
|
||||
|
||||
// Test text range
|
||||
.{ "const commentNode = container.childNodes[7];", null },
|
||||
.{ "commentNode.nodeValue", "comment" },
|
||||
.{ "const textRange = document.createRange();", null },
|
||||
.{ "textRange.selectNodeContents(commentNode)", "undefined" },
|
||||
.{ "textRange.startOffset", "0" },
|
||||
.{ "textRange.endOffset", "7" }, // length of `comment`
|
||||
|
||||
// Test Node range
|
||||
.{ "const nodeRange = document.createRange();", null },
|
||||
.{ "nodeRange.selectNodeContents(container)", "undefined" },
|
||||
.{ "nodeRange.startOffset", "0" },
|
||||
.{ "nodeRange.endOffset", "9" }, // length of container.childNodes
|
||||
}, .{});
|
||||
}
|
||||
@@ -1,54 +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 Env = @import("../env.zig").Env;
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
pub const Interfaces = .{
|
||||
ResizeObserver,
|
||||
};
|
||||
|
||||
// WEB IDL https://drafts.csswg.org/resize-observer/#resize-observer-interface
|
||||
pub const ResizeObserver = struct {
|
||||
pub fn constructor(cbk: Env.Function) ResizeObserver {
|
||||
_ = cbk;
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn _observe(self: *const ResizeObserver, element: *parser.Element, options_: ?Options) void {
|
||||
_ = self;
|
||||
_ = element;
|
||||
_ = options_;
|
||||
return;
|
||||
}
|
||||
|
||||
pub fn _unobserve(self: *const ResizeObserver, element: *parser.Element) void {
|
||||
_ = self;
|
||||
_ = element;
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO
|
||||
pub fn _disconnect(self: *ResizeObserver) void {
|
||||
_ = self;
|
||||
}
|
||||
};
|
||||
|
||||
const Options = struct {
|
||||
box: []const u8,
|
||||
};
|
||||
@@ -1,155 +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 dump = @import("../dump.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const Node = @import("node.zig").Node;
|
||||
const Element = @import("element.zig").Element;
|
||||
const ElementUnion = @import("element.zig").Union;
|
||||
|
||||
// WEB IDL https://dom.spec.whatwg.org/#interface-shadowroot
|
||||
pub const ShadowRoot = struct {
|
||||
pub const prototype = *parser.DocumentFragment;
|
||||
pub const subtype = .node;
|
||||
|
||||
mode: Mode,
|
||||
host: *parser.Element,
|
||||
proto: *parser.DocumentFragment,
|
||||
adopted_style_sheets: ?Env.JsObject = null,
|
||||
|
||||
pub const Mode = enum {
|
||||
open,
|
||||
closed,
|
||||
};
|
||||
|
||||
pub fn get_host(self: *const ShadowRoot) !ElementUnion {
|
||||
return Element.toInterface(self.host);
|
||||
}
|
||||
|
||||
pub fn get_adoptedStyleSheets(self: *ShadowRoot, page: *Page) !Env.JsObject {
|
||||
if (self.adopted_style_sheets) |obj| {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const obj = try page.main_context.newArray(0).persist();
|
||||
self.adopted_style_sheets = obj;
|
||||
return obj;
|
||||
}
|
||||
|
||||
pub fn set_adoptedStyleSheets(self: *ShadowRoot, sheets: Env.JsObject) !void {
|
||||
self.adopted_style_sheets = try sheets.persist();
|
||||
}
|
||||
|
||||
pub fn get_innerHTML(self: *ShadowRoot, page: *Page) ![]const u8 {
|
||||
var aw = std.Io.Writer.Allocating.init(page.call_arena);
|
||||
try dump.writeChildren(parser.documentFragmentToNode(self.proto), .{}, &aw.writer);
|
||||
return aw.written();
|
||||
}
|
||||
|
||||
pub fn set_innerHTML(self: *ShadowRoot, str_: ?[]const u8) !void {
|
||||
const sr_doc = parser.documentFragmentToNode(self.proto);
|
||||
const doc = try parser.nodeOwnerDocument(sr_doc) orelse return parser.DOMError.WrongDocument;
|
||||
try Node.removeChildren(sr_doc);
|
||||
const str = str_ orelse return;
|
||||
|
||||
const fragment = try parser.documentParseFragmentFromStr(doc, str);
|
||||
const fragment_node = parser.documentFragmentToNode(fragment);
|
||||
|
||||
// Element.set_innerHTML also has some weirdness here. It isn't clear
|
||||
// what should and shouldn't be set. Whatever string you pass to libdom,
|
||||
// it always creates a full HTML document, with an html, head and body
|
||||
// element.
|
||||
// For ShadowRoot, it appears the only the children within the body should
|
||||
// be set.
|
||||
const html = try parser.nodeFirstChild(fragment_node) orelse return;
|
||||
const head = try parser.nodeFirstChild(html) orelse return;
|
||||
const body = try parser.nodeNextSibling(head) orelse return;
|
||||
|
||||
const children = try parser.nodeGetChildNodes(body);
|
||||
const ln = try parser.nodeListLength(children);
|
||||
for (0..ln) |_| {
|
||||
// always index 0, because nodeAppendChild moves the node out of
|
||||
// the nodeList and into the new tree
|
||||
const child = try parser.nodeListItem(children, 0) orelse continue;
|
||||
_ = try parser.nodeAppendChild(sr_doc, child);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.ShadowRoot" {
|
||||
defer testing.reset();
|
||||
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
|
||||
\\ <div id=conflict>nope</div>
|
||||
});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const div1 = document.createElement('div');", null },
|
||||
.{ "let sr1 = div1.attachShadow({mode: 'open'})", null },
|
||||
.{ "sr1.host == div1", "true" },
|
||||
.{ "div1.attachShadow({mode: 'open'}) == sr1", "true" },
|
||||
.{ "div1.shadowRoot == sr1", "true" },
|
||||
|
||||
.{ "try { div1.attachShadow({mode: 'closed'}) } catch (e) { e }", "Error: NotSupportedError" },
|
||||
|
||||
.{ " sr1.append(document.createElement('div'))", null },
|
||||
.{ " sr1.append(document.createElement('span'))", null },
|
||||
.{ "sr1.childElementCount", "2" },
|
||||
// re-attaching clears it
|
||||
.{ "div1.attachShadow({mode: 'open'}) == sr1", "true" },
|
||||
.{ "sr1.childElementCount", "0" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const div2 = document.createElement('di2');", null },
|
||||
.{ "let sr2 = div2.attachShadow({mode: 'closed'})", null },
|
||||
.{ "sr2.host == div2", "true" },
|
||||
.{ "div2.shadowRoot", "null" }, // null when attached with 'closed'
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "sr2.getElementById('conflict')", "null" },
|
||||
.{ "const n1 = document.createElement('div')", null },
|
||||
.{ "n1.id = 'conflict'", null },
|
||||
.{ "sr2.append(n1)", null },
|
||||
.{ "sr2.getElementById('conflict') == n1", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const acss = sr2.adoptedStyleSheets", null },
|
||||
.{ "acss.length", "0" },
|
||||
.{ "acss.push(new CSSStyleSheet())", null },
|
||||
.{ "sr2.adoptedStyleSheets.length", "1" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "sr1.innerHTML = '<p>hello</p>'", null },
|
||||
.{ "sr1.innerHTML", "<p>hello</p>" },
|
||||
.{ "sr1.querySelector('*')", "[object HTMLParagraphElement]" },
|
||||
|
||||
.{ "sr1.innerHTML = null", null },
|
||||
.{ "sr1.innerHTML", "" },
|
||||
.{ "sr1.querySelector('*')", "null" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
const CharacterData = @import("character_data.zig").CharacterData;
|
||||
const CDATASection = @import("cdata_section.zig").CDATASection;
|
||||
@@ -32,9 +32,9 @@ pub const Text = struct {
|
||||
pub const prototype = *CharacterData;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(data: ?[]const u8, page: *const Page) !*parser.Text {
|
||||
pub fn constructor(data: ?[]const u8, state: *const SessionState) !*parser.Text {
|
||||
return parser.documentCreateTextNode(
|
||||
parser.documentHTMLToDocument(page.window.document),
|
||||
parser.documentHTMLToDocument(state.document.?),
|
||||
data orelse "",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,14 +18,15 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const iterator = @import("../iterator/iterator.zig");
|
||||
|
||||
const Function = @import("../env.zig").Function;
|
||||
const Callback = @import("../env.zig").Callback;
|
||||
const JsObject = @import("../env.zig").JsObject;
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
|
||||
const log = std.log.scoped(.token_list);
|
||||
|
||||
pub const Interfaces = .{
|
||||
DOMTokenList,
|
||||
DOMTokenListIterable,
|
||||
@@ -137,16 +138,13 @@ pub const DOMTokenList = struct {
|
||||
}
|
||||
|
||||
// TODO handle thisArg
|
||||
pub fn _forEach(self: *parser.TokenList, cbk: Function, this_arg: JsObject) !void {
|
||||
pub fn _forEach(self: *parser.TokenList, cbk: Callback, this_arg: JsObject) !void {
|
||||
var entries = _entries(self);
|
||||
while (try entries._next()) |entry| {
|
||||
var result: Function.Result = undefined;
|
||||
cbk.tryCallWithThis(void, this_arg, .{ entry.@"1", entry.@"0", self }, &result) catch {
|
||||
log.debug(.user_script, "callback error", .{
|
||||
.err = result.exception,
|
||||
.stack = result.stack,
|
||||
.soure = "tokenList foreach",
|
||||
});
|
||||
var result: Callback.Result = undefined;
|
||||
cbk.tryCallWithThis(this_arg, .{ entry.@"1", entry.@"0", self }, &result) catch {
|
||||
log.err("callback error: {s}", .{result.exception});
|
||||
log.debug("stack:\n{s}", .{result.stack orelse "???"});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,281 +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");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const NodeFilter = @import("node_filter.zig");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const Node = @import("node.zig").Node;
|
||||
const NodeUnion = @import("node.zig").Union;
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker
|
||||
pub const TreeWalker = struct {
|
||||
root: *parser.Node,
|
||||
current_node: *parser.Node,
|
||||
what_to_show: u32,
|
||||
filter: ?TreeWalkerOpts,
|
||||
filter_func: ?Env.Function,
|
||||
|
||||
// One of the few cases where null and undefined resolve to different default.
|
||||
// We need the raw JsObject so that we can probe the tri state:
|
||||
// null, undefined or i32.
|
||||
pub const WhatToShow = Env.JsObject;
|
||||
|
||||
pub const TreeWalkerOpts = union(enum) {
|
||||
function: Env.Function,
|
||||
object: struct { acceptNode: Env.Function },
|
||||
};
|
||||
|
||||
pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?TreeWalkerOpts) !TreeWalker {
|
||||
var filter_func: ?Env.Function = null;
|
||||
|
||||
if (filter) |f| {
|
||||
filter_func = switch (f) {
|
||||
.function => |func| func,
|
||||
.object => |o| o.acceptNode,
|
||||
};
|
||||
}
|
||||
|
||||
var what_to_show: u32 = undefined;
|
||||
if (what_to_show_) |wts| {
|
||||
switch (try wts.triState(TreeWalker, "what_to_show", u32)) {
|
||||
.null => what_to_show = 0,
|
||||
.undefined => what_to_show = NodeFilter.NodeFilter._SHOW_ALL,
|
||||
.value => |v| what_to_show = v,
|
||||
}
|
||||
} else {
|
||||
what_to_show = NodeFilter.NodeFilter._SHOW_ALL;
|
||||
}
|
||||
|
||||
return .{
|
||||
.root = node,
|
||||
.current_node = node,
|
||||
.what_to_show = what_to_show,
|
||||
.filter = filter,
|
||||
.filter_func = filter_func,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_root(self: *TreeWalker) !NodeUnion {
|
||||
return try Node.toInterface(self.root);
|
||||
}
|
||||
|
||||
pub fn get_currentNode(self: *TreeWalker) !NodeUnion {
|
||||
return try Node.toInterface(self.current_node);
|
||||
}
|
||||
|
||||
pub fn get_whatToShow(self: *TreeWalker) u32 {
|
||||
return self.what_to_show;
|
||||
}
|
||||
|
||||
pub fn get_filter(self: *TreeWalker) ?TreeWalkerOpts {
|
||||
return self.filter;
|
||||
}
|
||||
|
||||
pub fn set_currentNode(self: *TreeWalker, node: *parser.Node) !void {
|
||||
self.current_node = node;
|
||||
}
|
||||
|
||||
fn firstChild(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
|
||||
const children = try parser.nodeGetChildNodes(node);
|
||||
const child_count = try parser.nodeListLength(children);
|
||||
|
||||
for (0..child_count) |i| {
|
||||
const index: u32 = @intCast(i);
|
||||
const child = (try parser.nodeListItem(children, index)) orelse return null;
|
||||
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
|
||||
.accept => return child,
|
||||
.reject => continue,
|
||||
.skip => if (try self.firstChild(child)) |gchild| return gchild,
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn lastChild(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
|
||||
const children = try parser.nodeGetChildNodes(node);
|
||||
const child_count = try parser.nodeListLength(children);
|
||||
|
||||
var index: u32 = child_count;
|
||||
while (index > 0) {
|
||||
index -= 1;
|
||||
const child = (try parser.nodeListItem(children, index)) orelse return null;
|
||||
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
|
||||
.accept => return child,
|
||||
.reject => continue,
|
||||
.skip => if (try self.lastChild(child)) |gchild| return gchild,
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn nextSibling(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
|
||||
var current = node;
|
||||
|
||||
while (true) {
|
||||
current = (try parser.nodeNextSibling(current)) orelse return null;
|
||||
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||
.accept => return current,
|
||||
.skip, .reject => continue,
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn previousSibling(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
|
||||
var current = node;
|
||||
|
||||
while (true) {
|
||||
current = (try parser.nodePreviousSibling(current)) orelse return null;
|
||||
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||
.accept => return current,
|
||||
.skip, .reject => continue,
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn parentNode(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
|
||||
if (self.root == node) return null;
|
||||
|
||||
var current = node;
|
||||
while (true) {
|
||||
if (current == self.root) return null;
|
||||
current = (try parser.nodeParentNode(current)) orelse return null;
|
||||
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||
.accept => return current,
|
||||
.reject, .skip => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _firstChild(self: *TreeWalker) !?NodeUnion {
|
||||
if (try self.firstChild(self.current_node)) |child| {
|
||||
self.current_node = child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn _lastChild(self: *TreeWalker) !?NodeUnion {
|
||||
if (try self.lastChild(self.current_node)) |child| {
|
||||
self.current_node = child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn _nextNode(self: *TreeWalker) !?NodeUnion {
|
||||
if (try self.firstChild(self.current_node)) |child| {
|
||||
self.current_node = child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
|
||||
var current = self.current_node;
|
||||
while (current != self.root) {
|
||||
if (try self.nextSibling(current)) |sibling| {
|
||||
self.current_node = sibling;
|
||||
return try Node.toInterface(sibling);
|
||||
}
|
||||
|
||||
current = (try parser.nodeParentNode(current)) orelse break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn _nextSibling(self: *TreeWalker) !?NodeUnion {
|
||||
if (try self.nextSibling(self.current_node)) |sibling| {
|
||||
self.current_node = sibling;
|
||||
return try Node.toInterface(sibling);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn _parentNode(self: *TreeWalker) !?NodeUnion {
|
||||
if (try self.parentNode(self.current_node)) |parent| {
|
||||
self.current_node = parent;
|
||||
return try Node.toInterface(parent);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn _previousNode(self: *TreeWalker) !?NodeUnion {
|
||||
if (self.current_node == self.root) return null;
|
||||
|
||||
var current = self.current_node;
|
||||
while (try parser.nodePreviousSibling(current)) |previous| {
|
||||
current = previous;
|
||||
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||
.accept => {
|
||||
// Get last child if it has one.
|
||||
if (try self.lastChild(current)) |child| {
|
||||
self.current_node = child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
|
||||
// Otherwise, this node is our previous one.
|
||||
self.current_node = current;
|
||||
return try Node.toInterface(current);
|
||||
},
|
||||
.reject => continue,
|
||||
.skip => {
|
||||
// Get last child if it has one.
|
||||
if (try self.lastChild(current)) |child| {
|
||||
self.current_node = child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (current != self.root) {
|
||||
if (try self.parentNode(current)) |parent| {
|
||||
self.current_node = parent;
|
||||
return try Node.toInterface(parent);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn _previousSibling(self: *TreeWalker) !?NodeUnion {
|
||||
if (try self.previousSibling(self.current_node)) |sibling| {
|
||||
self.current_node = sibling;
|
||||
return try Node.toInterface(sibling);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -19,58 +19,19 @@
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("netsurf.zig");
|
||||
const Page = @import("page.zig").Page;
|
||||
const Walker = @import("dom/walker.zig").WalkerChildren;
|
||||
|
||||
pub const Opts = struct {
|
||||
// set to include element shadowroots in the dump
|
||||
page: ?*const Page = null,
|
||||
|
||||
exclude_scripts: bool = false,
|
||||
};
|
||||
|
||||
// writer must be a std.io.Writer
|
||||
pub fn writeHTML(doc: *parser.Document, opts: Opts, writer: *std.Io.Writer) !void {
|
||||
pub fn writeHTML(doc: *parser.Document, writer: anytype) !void {
|
||||
try writer.writeAll("<!DOCTYPE html>\n");
|
||||
try writeChildren(parser.documentToNode(doc), opts, writer);
|
||||
try writeChildren(parser.documentToNode(doc), writer);
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
|
||||
// Spec: https://www.w3.org/TR/xml/#sec-prolog-dtd
|
||||
pub fn writeDocType(doc_type: *parser.DocumentType, writer: *std.Io.Writer) !void {
|
||||
try writer.writeAll("<!DOCTYPE ");
|
||||
try writer.writeAll(try parser.documentTypeGetName(doc_type));
|
||||
|
||||
const public_id = try parser.documentTypeGetPublicId(doc_type);
|
||||
const system_id = try parser.documentTypeGetSystemId(doc_type);
|
||||
if (public_id.len != 0 and system_id.len != 0) {
|
||||
try writer.writeAll(" PUBLIC \"");
|
||||
try writeEscapedAttributeValue(writer, public_id);
|
||||
try writer.writeAll("\" \"");
|
||||
try writeEscapedAttributeValue(writer, system_id);
|
||||
try writer.writeAll("\"");
|
||||
} else if (public_id.len != 0) {
|
||||
try writer.writeAll(" PUBLIC \"");
|
||||
try writeEscapedAttributeValue(writer, public_id);
|
||||
try writer.writeAll("\"");
|
||||
} else if (system_id.len != 0) {
|
||||
try writer.writeAll(" SYSTEM \"");
|
||||
try writeEscapedAttributeValue(writer, system_id);
|
||||
try writer.writeAll("\"");
|
||||
}
|
||||
// Internal subset is not implemented
|
||||
try writer.writeAll(">");
|
||||
}
|
||||
|
||||
pub fn writeNode(node: *parser.Node, opts: Opts, writer: *std.Io.Writer) anyerror!void {
|
||||
pub fn writeNode(node: *parser.Node, writer: anytype) anyerror!void {
|
||||
switch (try parser.nodeType(node)) {
|
||||
.element => {
|
||||
// open the tag
|
||||
const tag_type = try parser.nodeHTMLGetTagType(node) orelse .undef;
|
||||
if (opts.exclude_scripts and try isScriptOrRelated(tag_type, node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tag = try parser.nodeLocalName(node);
|
||||
try writer.writeAll("<");
|
||||
try writer.writeAll(tag);
|
||||
@@ -92,24 +53,12 @@ pub fn writeNode(node: *parser.Node, opts: Opts, writer: *std.Io.Writer) anyerro
|
||||
|
||||
try writer.writeAll(">");
|
||||
|
||||
if (opts.page) |page| {
|
||||
if (page.getNodeState(node)) |state| {
|
||||
if (state.shadow_root) |sr| {
|
||||
try writeChildren(@ptrCast(@alignCast(sr.proto)), opts, writer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// void elements can't have any content.
|
||||
if (try isVoid(parser.nodeToElement(node))) return;
|
||||
|
||||
if (tag_type == .script) {
|
||||
try writer.writeAll(try parser.nodeTextContent(node) orelse "");
|
||||
} else {
|
||||
// write the children
|
||||
// TODO avoid recursion
|
||||
try writeChildren(node, opts, writer);
|
||||
}
|
||||
// write the children
|
||||
// TODO avoid recursion
|
||||
try writeChildren(node, writer);
|
||||
|
||||
// close the tag
|
||||
try writer.writeAll("</");
|
||||
@@ -139,7 +88,7 @@ pub fn writeNode(node: *parser.Node, opts: Opts, writer: *std.Io.Writer) anyerro
|
||||
.document_fragment => return,
|
||||
// document will never be called, but required for completeness.
|
||||
.document => return,
|
||||
// done globally instead, but required for completeness. Only the outer DOCTYPE should be written
|
||||
// done globally instead, but required for completeness.
|
||||
.document_type => return,
|
||||
// deprecated
|
||||
.attribute => return,
|
||||
@@ -150,38 +99,19 @@ pub fn writeNode(node: *parser.Node, opts: Opts, writer: *std.Io.Writer) anyerro
|
||||
}
|
||||
|
||||
// writer must be a std.io.Writer
|
||||
pub fn writeChildren(root: *parser.Node, opts: Opts, writer: *std.Io.Writer) !void {
|
||||
pub fn writeChildren(root: *parser.Node, writer: anytype) !void {
|
||||
const walker = Walker{};
|
||||
var next: ?*parser.Node = null;
|
||||
while (true) {
|
||||
next = try walker.get_next(root, next) orelse break;
|
||||
try writeNode(next.?, opts, writer);
|
||||
try writeNode(next.?, writer);
|
||||
}
|
||||
}
|
||||
|
||||
// When `exclude_scripts` is passed to dump, we don't include <script> tags.
|
||||
// We also want to omit <link rel=preload as=ascript>
|
||||
fn isScriptOrRelated(tag_type: parser.Tag, node: *parser.Node) !bool {
|
||||
if (tag_type == .script) {
|
||||
return true;
|
||||
}
|
||||
if (tag_type == .link) {
|
||||
const el = parser.nodeToElement(node);
|
||||
const as = try parser.elementGetAttribute(el, "as") orelse return false;
|
||||
if (!std.ascii.eqlIgnoreCase(as, "script")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rel = try parser.elementGetAttribute(el, "rel") orelse return false;
|
||||
return std.ascii.eqlIgnoreCase(rel, "preload");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr
|
||||
// https://html.spec.whatwg.org/#void-elements
|
||||
fn isVoid(elem: *parser.Element) !bool {
|
||||
const tag = try parser.elementTag(elem);
|
||||
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(elem)));
|
||||
return switch (tag) {
|
||||
.area, .base, .br, .col, .embed, .hr, .img, .input, .link => true,
|
||||
.meta, .source, .track, .wbr => true,
|
||||
@@ -226,9 +156,6 @@ fn writeEscapedAttributeValue(writer: anytype, value: []const u8) !void {
|
||||
|
||||
const testing = std.testing;
|
||||
test "dump.writeHTML" {
|
||||
try parser.init();
|
||||
defer parser.deinit();
|
||||
|
||||
try testWriteHTML(
|
||||
"<div id=\"content\">Over 9000!</div>",
|
||||
"<div id=\"content\">Over 9000!</div>",
|
||||
@@ -255,11 +182,6 @@ test "dump.writeHTML" {
|
||||
\\</head><body>9000</body></html>
|
||||
\\
|
||||
, "<html><title>It's over what?</title><meta name=a value=\"b\">\n<body>9000");
|
||||
|
||||
try testWriteHTML(
|
||||
"<p>hi</p><script>alert(power > 9000)</script>",
|
||||
"<p>hi</p><script>alert(power > 9000)</script>",
|
||||
);
|
||||
}
|
||||
|
||||
fn testWriteHTML(comptime expected_body: []const u8, src: []const u8) !void {
|
||||
@@ -271,13 +193,16 @@ fn testWriteHTML(comptime expected_body: []const u8, src: []const u8) !void {
|
||||
}
|
||||
|
||||
fn testWriteFullHTML(comptime expected: []const u8, src: []const u8) !void {
|
||||
var aw = std.Io.Writer.Allocating.init(testing.allocator);
|
||||
defer aw.deinit();
|
||||
var buf = std.ArrayListUnmanaged(u8){};
|
||||
defer buf.deinit(testing.allocator);
|
||||
|
||||
const doc_html = try parser.documentHTMLParseFromStr(src);
|
||||
var aa = std.heap.ArenaAllocator.init(testing.allocator);
|
||||
defer aa.deinit();
|
||||
|
||||
const doc_html = try parser.documentHTMLParseFromStr(aa.allocator(), src);
|
||||
defer parser.documentHTMLClose(doc_html) catch {};
|
||||
|
||||
const doc = parser.documentHTMLToDocument(doc_html);
|
||||
try writeHTML(doc, .{}, &aw.writer);
|
||||
try testing.expectEqualStrings(expected, aw.written());
|
||||
try writeHTML(doc, buf.writer(testing.allocator));
|
||||
try testing.expectEqualStrings(expected, buf.items);
|
||||
}
|
||||
|
||||
@@ -1,84 +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 log = @import("../../log.zig");
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
|
||||
// https://encoding.spec.whatwg.org/#interface-textdecoder
|
||||
const TextDecoder = @This();
|
||||
|
||||
const SupportedLabels = enum {
|
||||
utf8,
|
||||
@"utf-8",
|
||||
@"unicode-1-1-utf-8",
|
||||
};
|
||||
|
||||
const Options = struct {
|
||||
fatal: bool = false,
|
||||
ignoreBOM: bool = false,
|
||||
};
|
||||
|
||||
fatal: bool,
|
||||
ignore_bom: bool,
|
||||
|
||||
pub fn constructor(label_: ?[]const u8, opts_: ?Options) !TextDecoder {
|
||||
if (label_) |l| {
|
||||
_ = std.meta.stringToEnum(SupportedLabels, l) orelse {
|
||||
log.warn(.web_api, "not implemented", .{ .feature = "TextDecoder label", .label = l });
|
||||
return error.NotImplemented;
|
||||
};
|
||||
}
|
||||
const opts = opts_ orelse Options{};
|
||||
return .{
|
||||
.fatal = opts.fatal,
|
||||
.ignore_bom = opts.ignoreBOM,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_encoding(_: *const TextDecoder) []const u8 {
|
||||
return "utf-8";
|
||||
}
|
||||
|
||||
pub fn get_ignoreBOM(self: *const TextDecoder) bool {
|
||||
return self.ignore_bom;
|
||||
}
|
||||
|
||||
pub fn get_fatal(self: *const TextDecoder) bool {
|
||||
return self.fatal;
|
||||
}
|
||||
|
||||
// TODO: Should accept an ArrayBuffer, TypedArray or DataView
|
||||
// js.zig will currently only map a TypedArray to our []const u8.
|
||||
pub fn _decode(self: *const TextDecoder, v: []const u8) ![]const u8 {
|
||||
if (self.fatal and !std.unicode.utf8ValidateSlice(v)) {
|
||||
return error.InvalidUtf8;
|
||||
}
|
||||
|
||||
if (self.ignore_bom == false and std.mem.startsWith(u8, v, &.{ 0xEF, 0xBB, 0xBF })) {
|
||||
return v[3..];
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: Encoding.TextDecoder" {
|
||||
try testing.htmlRunner("encoding/decoder.html");
|
||||
}
|
||||
@@ -1,48 +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 Env = @import("../env.zig").Env;
|
||||
|
||||
// https://encoding.spec.whatwg.org/#interface-textencoder
|
||||
const TextEncoder = @This();
|
||||
|
||||
pub fn constructor() !TextEncoder {
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn get_encoding(_: *const TextEncoder) []const u8 {
|
||||
return "utf-8";
|
||||
}
|
||||
|
||||
pub fn _encode(_: *const TextEncoder, v: []const u8) !Env.TypedArray(u8) {
|
||||
// Ensure the input is a valid utf-8
|
||||
// It seems chrome accepts invalid utf-8 sequence.
|
||||
//
|
||||
if (!std.unicode.utf8ValidateSlice(v)) {
|
||||
return error.InvalidUtf8;
|
||||
}
|
||||
|
||||
return .{ .values = v };
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: Encoding.TextEncoder" {
|
||||
try testing.htmlRunner("encoding/encoder.html");
|
||||
}
|
||||
@@ -1,22 +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/>.
|
||||
|
||||
pub const Interfaces = .{
|
||||
@import("TextDecoder.zig"),
|
||||
@import("TextEncoder.zig"),
|
||||
};
|
||||
64
src/browser/encoding/text_encoder.zig
Normal file
64
src/browser/encoding/text_encoder.zig
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
|
||||
pub const Interfaces = .{
|
||||
TextEncoder,
|
||||
};
|
||||
|
||||
// https://encoding.spec.whatwg.org/#interface-textencoder
|
||||
pub const TextEncoder = struct {
|
||||
pub fn constructor() !TextEncoder {
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn get_encoding(_: *const TextEncoder) []const u8 {
|
||||
return "utf-8";
|
||||
}
|
||||
|
||||
pub fn _encode(_: *const TextEncoder, v: []const u8) !Env.TypedArray(u8) {
|
||||
// Ensure the input is a valid utf-8
|
||||
// It seems chrome accepts invalid utf-8 sequence.
|
||||
//
|
||||
if (!std.unicode.utf8ValidateSlice(v)) {
|
||||
return error.InvalidUtf8;
|
||||
}
|
||||
|
||||
return .{ .values = v };
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.Encoding.TextEncoder" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var encoder = new TextEncoder();", "undefined" },
|
||||
.{ "encoder.encoding;", "utf-8" },
|
||||
.{ "encoder.encode('€');", "226,130,172" },
|
||||
|
||||
// Invalid utf-8 sequence.
|
||||
// Result with chrome:
|
||||
// .{ "encoder.encode(new Uint8Array([0xE2,0x28,0xA1]))", "50,50,54,44,52,48,44,49,54,49" },
|
||||
.{ "try {encoder.encode(new Uint8Array([0xE2,0x28,0xA1])) } catch (e) { e };", "Error: InvalidUtf8" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -1,32 +1,34 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Page = @import("page.zig").Page;
|
||||
const parser = @import("netsurf.zig");
|
||||
const URL = @import("../url.zig").URL;
|
||||
const js = @import("../runtime/js.zig");
|
||||
const storage = @import("storage/storage.zig");
|
||||
const generate = @import("../runtime/generate.zig");
|
||||
const Renderer = @import("renderer.zig").Renderer;
|
||||
const Loop = @import("../runtime/loop.zig").Loop;
|
||||
const HttpClient = @import("../http/client.zig").Client;
|
||||
|
||||
const WebApis = struct {
|
||||
// Wrapped like this for debug ergonomics.
|
||||
// When we create our Env, a few lines down, we define it as:
|
||||
// pub const Env = js.Env(*Page, WebApis);
|
||||
// pub const Env = js.Env(*SessionState, WebApis);
|
||||
//
|
||||
// If there's a compile time error witht he Env, it's type will be readable,
|
||||
// i.e.: runtime.js.Env(*browser.env.Page, browser.env.WebApis)
|
||||
// i.e.: runtime.js.Env(*browser.env.SessionState, browser.env.WebApis)
|
||||
//
|
||||
// But if we didn't wrap it in the struct, like we once didn't, and defined
|
||||
// env as:
|
||||
// pub const Env = js.Env(*Page, Interfaces);
|
||||
// pub const Env = js.Env(*SessionState, Interfaces);
|
||||
//
|
||||
// Because Interfaces is an anynoumous type, it doesn't have a friendly name
|
||||
// and errors would be something like:
|
||||
// runtime.js.Env(*browser.Page, .{...A HUNDRED TYPES...})
|
||||
// runtime.js.Env(*browser.env.SessionState, .{...A HUNDRED TYPES...})
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
@import("crypto/crypto.zig").Crypto,
|
||||
@import("console/console.zig").Console,
|
||||
@import("css/css.zig").Interfaces,
|
||||
@import("cssom/cssom.zig").Interfaces,
|
||||
@import("dom/dom.zig").Interfaces,
|
||||
@import("dom/shadow_root.zig").ShadowRoot,
|
||||
@import("encoding/encoding.zig").Interfaces,
|
||||
@import("encoding/text_encoder.zig").Interfaces,
|
||||
@import("events/event.zig").Interfaces,
|
||||
@import("html/html.zig").Interfaces,
|
||||
@import("iterator/iterator.zig").Interfaces,
|
||||
@@ -34,16 +36,27 @@ const WebApis = struct {
|
||||
@import("url/url.zig").Interfaces,
|
||||
@import("xhr/xhr.zig").Interfaces,
|
||||
@import("xhr/form_data.zig").Interfaces,
|
||||
@import("xhr/File.zig"),
|
||||
@import("xmlserializer/xmlserializer.zig").Interfaces,
|
||||
});
|
||||
};
|
||||
|
||||
pub const JsThis = Env.JsThis;
|
||||
pub const JsObject = Env.JsObject;
|
||||
pub const Function = Env.Function;
|
||||
pub const Promise = Env.Promise;
|
||||
pub const PromiseResolver = Env.PromiseResolver;
|
||||
|
||||
pub const Env = js.Env(*Page, WebApis);
|
||||
pub const Callback = Env.Callback;
|
||||
pub const Env = js.Env(*SessionState, WebApis);
|
||||
pub const Global = @import("html/window.zig").Window;
|
||||
|
||||
pub const SessionState = struct {
|
||||
loop: *Loop,
|
||||
url: *const URL,
|
||||
renderer: *Renderer,
|
||||
arena: std.mem.Allocator,
|
||||
http_client: *HttpClient,
|
||||
cookie_jar: *storage.CookieJar,
|
||||
document: ?*parser.DocumentHTML,
|
||||
|
||||
// dangerous, but set by the JS framework
|
||||
// shorter-lived than the arena above, which
|
||||
// exists for the entire rendering of the page
|
||||
call_arena: std.mem.Allocator = undefined,
|
||||
};
|
||||
|
||||
@@ -19,12 +19,10 @@
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Event = @import("event.zig").Event;
|
||||
const JsObject = @import("../env.zig").JsObject;
|
||||
const netsurf = @import("../netsurf.zig");
|
||||
|
||||
// https://dom.spec.whatwg.org/#interface-customevent
|
||||
pub const CustomEvent = struct {
|
||||
pub const prototype = *Event;
|
||||
pub const union_make_copy = true;
|
||||
|
||||
proto: parser.Event,
|
||||
detail: ?JsObject,
|
||||
@@ -56,30 +54,26 @@ pub const CustomEvent = struct {
|
||||
pub fn get_detail(self: *CustomEvent) ?JsObject {
|
||||
return self.detail;
|
||||
}
|
||||
|
||||
// Initializes an already created `CustomEvent`.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/initCustomEvent
|
||||
pub fn _initCustomEvent(
|
||||
self: *CustomEvent,
|
||||
event_type: []const u8,
|
||||
can_bubble: bool,
|
||||
cancelable: bool,
|
||||
maybe_detail: ?JsObject,
|
||||
) !void {
|
||||
// This function can only be called after the constructor has called.
|
||||
// So we assume proto is initialized already by constructor.
|
||||
self.proto.type = try netsurf.strFromData(event_type);
|
||||
self.proto.bubble = can_bubble;
|
||||
self.proto.cancelable = cancelable;
|
||||
self.proto.is_initialised = true;
|
||||
// Detail is stored separately.
|
||||
if (maybe_detail) |detail| {
|
||||
self.detail = try detail.persist();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: Events.Custom" {
|
||||
try testing.htmlRunner("events/custom.html");
|
||||
test "Browser.CustomEvent" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let capture = null", "undefined" },
|
||||
.{ "const el = document.createElement('div');", "undefined" },
|
||||
.{ "el.addEventListener('c1', (e) => { capture = 'c1-' + new String(e.detail)})", "undefined" },
|
||||
.{ "el.addEventListener('c2', (e) => { capture = 'c2-' + new String(e.detail.over)})", "undefined" },
|
||||
|
||||
.{ "el.dispatchEvent(new CustomEvent('c1'));", "true" },
|
||||
.{ "capture", "c1-null" },
|
||||
|
||||
.{ "el.dispatchEvent(new CustomEvent('c1', {detail: '123'}));", "true" },
|
||||
.{ "capture", "c1-123" },
|
||||
|
||||
.{ "el.dispatchEvent(new CustomEvent('c2', {detail: {over: 9000}}));", "true" },
|
||||
.{ "capture", "c2-9000" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -19,25 +19,25 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Callback = @import("../env.zig").Callback;
|
||||
const generate = @import("../../runtime/generate.zig");
|
||||
|
||||
const Page = @import("../page.zig").Page;
|
||||
const Node = @import("../dom/node.zig").Node;
|
||||
const DOMException = @import("../dom/exceptions.zig").DOMException;
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const EventTargetUnion = @import("../dom/event_target.zig").Union;
|
||||
const AbortSignal = @import("../html/AbortController.zig").AbortSignal;
|
||||
|
||||
const CustomEvent = @import("custom_event.zig").CustomEvent;
|
||||
const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
|
||||
const MouseEvent = @import("mouse_event.zig").MouseEvent;
|
||||
const ErrorEvent = @import("../html/error_event.zig").ErrorEvent;
|
||||
const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
|
||||
|
||||
const log = std.log.scoped(.events);
|
||||
|
||||
// Event interfaces
|
||||
pub const Interfaces = .{ Event, CustomEvent, ProgressEvent, MouseEvent, ErrorEvent, MessageEvent };
|
||||
pub const Interfaces = .{
|
||||
Event,
|
||||
CustomEvent,
|
||||
ProgressEvent,
|
||||
};
|
||||
|
||||
pub const Union = generate.Union(Interfaces);
|
||||
|
||||
@@ -57,12 +57,9 @@ pub const Event = struct {
|
||||
|
||||
pub fn toInterface(evt: *parser.Event) !Union {
|
||||
return switch (try parser.eventGetInternalType(evt)) {
|
||||
.event, .abort_signal, .xhr_event => .{ .Event = evt },
|
||||
.event => .{ .Event = evt },
|
||||
.custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* },
|
||||
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
|
||||
.mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) },
|
||||
.error_event => .{ .ErrorEvent = @as(*ErrorEvent, @ptrCast(evt)).* },
|
||||
.message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -78,16 +75,16 @@ pub const Event = struct {
|
||||
return try parser.eventType(self);
|
||||
}
|
||||
|
||||
pub fn get_target(self: *parser.Event, page: *Page) !?EventTargetUnion {
|
||||
pub fn get_target(self: *parser.Event) !?EventTargetUnion {
|
||||
const et = try parser.eventTarget(self);
|
||||
if (et == null) return null;
|
||||
return try EventTarget.toInterface(et.?, page);
|
||||
return try EventTarget.toInterface(et.?);
|
||||
}
|
||||
|
||||
pub fn get_currentTarget(self: *parser.Event, page: *Page) !?EventTargetUnion {
|
||||
pub fn get_currentTarget(self: *parser.Event) !?EventTargetUnion {
|
||||
const et = try parser.eventCurrentTarget(self);
|
||||
if (et == null) return null;
|
||||
return try EventTarget.toInterface(et.?, page);
|
||||
return try EventTarget.toInterface(et.?);
|
||||
}
|
||||
|
||||
pub fn get_eventPhase(self: *parser.Event) !u8 {
|
||||
@@ -110,11 +107,8 @@ pub const Event = struct {
|
||||
return try parser.eventIsTrusted(self);
|
||||
}
|
||||
|
||||
// Even though this is supposed to to provide microsecond resolution, browser
|
||||
// return coarser values to protect against fingerprinting. libdom returns
|
||||
// seconds, which is good enough.
|
||||
pub fn get_timeStamp(self: *parser.Event) !u32 {
|
||||
return parser.eventTimestamp(self);
|
||||
pub fn get_timestamp(self: *parser.Event) !u32 {
|
||||
return try parser.eventTimestamp(self);
|
||||
}
|
||||
|
||||
// Methods
|
||||
@@ -143,252 +137,134 @@ pub const Event = struct {
|
||||
pub fn _preventDefault(self: *parser.Event) !void {
|
||||
return try parser.eventPreventDefault(self);
|
||||
}
|
||||
|
||||
pub fn _composedPath(self: *parser.Event, page: *Page) ![]const EventTargetUnion {
|
||||
const et_ = try parser.eventTarget(self);
|
||||
const et = et_ orelse return &.{};
|
||||
|
||||
var node: ?*parser.Node = switch (try parser.eventTargetInternalType(et)) {
|
||||
.libdom_node => @as(*parser.Node, @ptrCast(et)),
|
||||
.plain => parser.eventTargetToNode(et),
|
||||
else => {
|
||||
// Window, XHR, MessagePort, etc...no path beyond the event itself
|
||||
return &.{try EventTarget.toInterface(et, page)};
|
||||
},
|
||||
};
|
||||
|
||||
const arena = page.call_arena;
|
||||
var path: std.ArrayListUnmanaged(EventTargetUnion) = .empty;
|
||||
while (node) |n| {
|
||||
try path.append(arena, .{
|
||||
.node = try Node.toInterface(n),
|
||||
});
|
||||
|
||||
node = try parser.nodeParentNode(n);
|
||||
if (node == null and try parser.nodeType(n) == .document_fragment) {
|
||||
// we have a non-continuous hook from a shadowroot to its host (
|
||||
// it's parent element). libdom doesn't really support ShdowRoots
|
||||
// and, for the most part, that works out well since it naturally
|
||||
// provides isolation. But events don't follow the same
|
||||
// shadowroot isolation as most other things, so, if this is
|
||||
// a parent-less document fragment, we need to check if it has
|
||||
// a host.
|
||||
if (parser.documentFragmentGetHost(@ptrCast(n))) |host| {
|
||||
node = host;
|
||||
|
||||
// If a document fragment has a host, then that host
|
||||
// _has_ to have a state and that state _has_ to have
|
||||
// a shadow_root field. All of this is set in Element._attachShadow
|
||||
if (page.getNodeState(host).?.shadow_root.?.mode == .closed) {
|
||||
// if the shadow root is closed, then the composedPath
|
||||
// starts at the host element.
|
||||
path.clearRetainingCapacity();
|
||||
}
|
||||
} else {
|
||||
// Our document fragement has no parent and no host, we
|
||||
// can break out of the loop.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (path.getLastOrNull()) |last| {
|
||||
// the Window isn't part of the DOM hierarchy, but for events, it
|
||||
// is, so we need to glue it on.
|
||||
if (last.node == .HTMLDocument and last.node.HTMLDocument == page.window.document) {
|
||||
try path.append(arena, .{ .node = .{ .Window = &page.window } });
|
||||
}
|
||||
}
|
||||
return path.items;
|
||||
}
|
||||
};
|
||||
|
||||
pub const EventHandler = struct {
|
||||
once: bool,
|
||||
capture: bool,
|
||||
callback: Function,
|
||||
callback: Callback,
|
||||
node: parser.EventNode,
|
||||
listener: *parser.EventListener,
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Function = Env.Function;
|
||||
|
||||
pub const Listener = union(enum) {
|
||||
function: Function,
|
||||
object: Env.JsObject,
|
||||
|
||||
pub fn callback(self: Listener, target: *parser.EventTarget) !?Function {
|
||||
return switch (self) {
|
||||
.function => |func| try func.withThis(target),
|
||||
.object => |obj| blk: {
|
||||
const func = (try obj.getFunction("handleEvent")) orelse return null;
|
||||
break :blk try func.withThis(try obj.persist());
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Opts = union(enum) {
|
||||
flags: Flags,
|
||||
capture: bool,
|
||||
|
||||
const Flags = struct {
|
||||
once: ?bool,
|
||||
capture: ?bool,
|
||||
// We ignore this property. It seems to be largely used to help the
|
||||
// browser make certain performance tweaks (i.e. the browser knows
|
||||
// that the listener won't call preventDefault() and thus can safely
|
||||
// run the default as needed).
|
||||
passive: ?bool,
|
||||
signal: ?*AbortSignal, // currently does nothing
|
||||
};
|
||||
};
|
||||
|
||||
pub fn register(
|
||||
allocator: Allocator,
|
||||
target: *parser.EventTarget,
|
||||
typ: []const u8,
|
||||
listener: Listener,
|
||||
opts_: ?Opts,
|
||||
) !?*EventHandler {
|
||||
var once = false;
|
||||
var capture = false;
|
||||
var signal: ?*AbortSignal = null;
|
||||
|
||||
if (opts_) |opts| {
|
||||
switch (opts) {
|
||||
.capture => |c| capture = c,
|
||||
.flags => |f| {
|
||||
once = f.once orelse false;
|
||||
signal = f.signal orelse null;
|
||||
capture = f.capture orelse false;
|
||||
},
|
||||
}
|
||||
}
|
||||
const callback = (try listener.callback(target)) orelse return null;
|
||||
|
||||
if (signal) |s| {
|
||||
const signal_target = parser.toEventTarget(AbortSignal, s);
|
||||
|
||||
const scb = try allocator.create(SignalCallback);
|
||||
scb.* = .{
|
||||
.target = target,
|
||||
.capture = capture,
|
||||
.callback_id = callback.id,
|
||||
.typ = try allocator.dupe(u8, typ),
|
||||
.signal_target = signal_target,
|
||||
.signal_listener = undefined,
|
||||
.node = .{ .func = SignalCallback.handle },
|
||||
};
|
||||
|
||||
scb.signal_listener = try parser.eventTargetAddEventListener(
|
||||
signal_target,
|
||||
"abort",
|
||||
&scb.node,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
// check if event target has already this listener
|
||||
if (try parser.eventTargetHasListener(target, typ, capture, callback.id) != null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn init(allocator: Allocator, callback: Callback) !*EventHandler {
|
||||
const eh = try allocator.create(EventHandler);
|
||||
eh.* = .{
|
||||
.once = once,
|
||||
.capture = capture,
|
||||
.callback = callback,
|
||||
.node = .{
|
||||
.id = callback.id,
|
||||
.func = handle,
|
||||
},
|
||||
.listener = undefined,
|
||||
};
|
||||
|
||||
eh.listener = try parser.eventTargetAddEventListener(
|
||||
target,
|
||||
typ,
|
||||
&eh.node,
|
||||
capture,
|
||||
);
|
||||
return eh;
|
||||
}
|
||||
|
||||
fn handle(node: *parser.EventNode, event: *parser.Event) void {
|
||||
const ievent = Event.toInterface(event) catch |err| {
|
||||
log.err(.app, "toInterface error", .{ .err = err });
|
||||
log.err("Event.toInterface: {}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
const self: *EventHandler = @fieldParentPtr("node", node);
|
||||
var result: Function.Result = undefined;
|
||||
self.callback.tryCall(void, .{ievent}, &result) catch {
|
||||
log.debug(.user_script, "callback error", .{
|
||||
.err = result.exception,
|
||||
.stack = result.stack,
|
||||
.source = "event handler",
|
||||
});
|
||||
var result: Callback.Result = undefined;
|
||||
self.callback.tryCall(.{ievent}, &result) catch {
|
||||
log.err("event handler error: {s}", .{result.exception});
|
||||
log.debug("stack:\n{s}", .{result.stack orelse "???"});
|
||||
};
|
||||
|
||||
if (self.once) {
|
||||
const target = (parser.eventTarget(event) catch return).?;
|
||||
const typ = parser.eventType(event) catch return;
|
||||
parser.eventTargetRemoveEventListener(
|
||||
target,
|
||||
typ,
|
||||
self.listener,
|
||||
self.capture,
|
||||
) catch {};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const SignalCallback = struct {
|
||||
typ: []const u8,
|
||||
capture: bool,
|
||||
callback_id: usize,
|
||||
node: parser.EventNode,
|
||||
target: *parser.EventTarget,
|
||||
signal_target: *parser.EventTarget,
|
||||
signal_listener: *parser.EventListener,
|
||||
|
||||
fn handle(node: *parser.EventNode, _: *parser.Event) void {
|
||||
const self: *SignalCallback = @fieldParentPtr("node", node);
|
||||
self._handle() catch |err| {
|
||||
log.err(.app, "event signal handler", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
fn _handle(self: *SignalCallback) !void {
|
||||
const lst = try parser.eventTargetHasListener(
|
||||
self.target,
|
||||
self.typ,
|
||||
self.capture,
|
||||
self.callback_id,
|
||||
);
|
||||
if (lst == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try parser.eventTargetRemoveEventListener(
|
||||
self.target,
|
||||
self.typ,
|
||||
lst.?,
|
||||
self.capture,
|
||||
);
|
||||
|
||||
// remove the abort signal listener itself
|
||||
try parser.eventTargetRemoveEventListener(
|
||||
self.signal_target,
|
||||
"abort",
|
||||
self.signal_listener,
|
||||
false,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: Event" {
|
||||
try testing.htmlRunner("events/event.html");
|
||||
test "Browser.Event" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let content = document.getElementById('content')", "undefined" },
|
||||
.{ "let para = document.getElementById('para')", "undefined" },
|
||||
.{ "var nb = 0; var evt", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ content.addEventListener('target', function(e) {
|
||||
\\ evt = e; nb = nb + 1;
|
||||
\\ e.preventDefault();
|
||||
\\ })
|
||||
,
|
||||
"undefined",
|
||||
},
|
||||
.{ "content.dispatchEvent(new Event('target', {bubbles: true, cancelable: true}))", "false" },
|
||||
.{ "nb", "1" },
|
||||
.{ "evt.target === content", "true" },
|
||||
.{ "evt.bubbles", "true" },
|
||||
.{ "evt.cancelable", "true" },
|
||||
.{ "evt.defaultPrevented", "true" },
|
||||
.{ "evt.isTrusted", "true" },
|
||||
.{ "evt.timestamp > 1704063600", "true" }, // 2024/01/01 00:00
|
||||
// event.type, event.currentTarget, event.phase checked in EventTarget
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0", "0" },
|
||||
.{
|
||||
\\ content.addEventListener('stop',function(e) {
|
||||
\\ e.stopPropagation();
|
||||
\\ nb = nb + 1;
|
||||
\\ }, true)
|
||||
,
|
||||
"undefined",
|
||||
},
|
||||
// the following event listener will not be invoked
|
||||
.{
|
||||
\\ para.addEventListener('stop',function(e) {
|
||||
\\ nb = nb + 1;
|
||||
\\ })
|
||||
,
|
||||
"undefined",
|
||||
},
|
||||
.{ "para.dispatchEvent(new Event('stop'))", "true" },
|
||||
.{ "nb", "1" }, // will be 2 if event was not stopped at content event listener
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0", "0" },
|
||||
.{
|
||||
\\ content.addEventListener('immediate', function(e) {
|
||||
\\ e.stopImmediatePropagation();
|
||||
\\ nb = nb + 1;
|
||||
\\ })
|
||||
,
|
||||
"undefined",
|
||||
},
|
||||
// the following event listener will not be invoked
|
||||
.{
|
||||
\\ content.addEventListener('immediate', function(e) {
|
||||
\\ nb = nb + 1;
|
||||
\\ })
|
||||
,
|
||||
"undefined",
|
||||
},
|
||||
.{ "content.dispatchEvent(new Event('immediate'))", "true" },
|
||||
.{ "nb", "1" }, // will be 2 if event was not stopped at first content event listener
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0", "0" },
|
||||
.{
|
||||
\\ content.addEventListener('legacy', function(e) {
|
||||
\\ evt = e; nb = nb + 1;
|
||||
\\ })
|
||||
,
|
||||
"undefined",
|
||||
},
|
||||
.{ "let evtLegacy = document.createEvent('Event')", "undefined" },
|
||||
.{ "evtLegacy.initEvent('legacy')", "undefined" },
|
||||
.{ "content.dispatchEvent(evtLegacy)", "true" },
|
||||
.{ "nb", "1" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var nb = 0; var evt = null; function cbk(event) { nb ++; evt=event; }", "undefined" },
|
||||
.{ "document.addEventListener('count', cbk)", "undefined" },
|
||||
.{ "document.removeEventListener('count', cbk)", "undefined" },
|
||||
.{ "document.dispatchEvent(new Event('count'))", "true" },
|
||||
.{ "nb", "0" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -1,108 +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");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Event = @import("event.zig").Event;
|
||||
const JsObject = @import("../env.zig").JsObject;
|
||||
|
||||
// TODO: We currently don't have a UIEvent interface so we skip it in the prototype chain.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent
|
||||
const UIEvent = Event;
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent
|
||||
pub const MouseEvent = struct {
|
||||
pub const Self = parser.MouseEvent;
|
||||
pub const prototype = *UIEvent;
|
||||
|
||||
const MouseButton = enum(u16) {
|
||||
main_button = 0,
|
||||
auxillary_button = 1,
|
||||
secondary_button = 2,
|
||||
fourth_button = 3,
|
||||
fifth_button = 4,
|
||||
};
|
||||
|
||||
const MouseEventInit = struct {
|
||||
screenX: i32 = 0,
|
||||
screenY: i32 = 0,
|
||||
clientX: i32 = 0,
|
||||
clientY: i32 = 0,
|
||||
ctrlKey: bool = false,
|
||||
shiftKey: bool = false,
|
||||
altKey: bool = false,
|
||||
metaKey: bool = false,
|
||||
button: MouseButton = .main_button,
|
||||
};
|
||||
|
||||
pub fn constructor(event_type: []const u8, opts_: ?MouseEventInit) !*parser.MouseEvent {
|
||||
const opts = opts_ orelse MouseEventInit{};
|
||||
|
||||
var mouse_event = try parser.mouseEventCreate();
|
||||
try parser.eventSetInternalType(@ptrCast(&mouse_event), .mouse_event);
|
||||
|
||||
try parser.mouseEventInit(mouse_event, event_type, .{
|
||||
.x = opts.clientX,
|
||||
.y = opts.clientY,
|
||||
.ctrl = opts.ctrlKey,
|
||||
.shift = opts.shiftKey,
|
||||
.alt = opts.altKey,
|
||||
.meta = opts.metaKey,
|
||||
.button = @intFromEnum(opts.button),
|
||||
});
|
||||
|
||||
return mouse_event;
|
||||
}
|
||||
|
||||
pub fn get_button(self: *parser.MouseEvent) u16 {
|
||||
return self.button;
|
||||
}
|
||||
|
||||
// These is just an alias for clientX.
|
||||
pub fn get_x(self: *parser.MouseEvent) i32 {
|
||||
return self.cx;
|
||||
}
|
||||
|
||||
// These is just an alias for clientY.
|
||||
pub fn get_y(self: *parser.MouseEvent) i32 {
|
||||
return self.cy;
|
||||
}
|
||||
|
||||
pub fn get_clientX(self: *parser.MouseEvent) i32 {
|
||||
return self.cx;
|
||||
}
|
||||
|
||||
pub fn get_clientY(self: *parser.MouseEvent) i32 {
|
||||
return self.cy;
|
||||
}
|
||||
|
||||
pub fn get_screenX(self: *parser.MouseEvent) i32 {
|
||||
return self.sx;
|
||||
}
|
||||
|
||||
pub fn get_screenY(self: *parser.MouseEvent) i32 {
|
||||
return self.sy;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: Events.Mouse" {
|
||||
try testing.htmlRunner("events/mouse.html");
|
||||
}
|
||||
@@ -1,181 +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");
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
|
||||
pub const Interfaces = .{
|
||||
AbortController,
|
||||
AbortSignal,
|
||||
};
|
||||
|
||||
const AbortController = @This();
|
||||
|
||||
signal: *AbortSignal,
|
||||
|
||||
pub fn constructor(page: *Page) !AbortController {
|
||||
// Why do we allocate this rather than storing directly in the struct?
|
||||
// https://github.com/lightpanda-io/project/discussions/165
|
||||
const signal = try page.arena.create(AbortSignal);
|
||||
signal.* = .init;
|
||||
|
||||
return .{
|
||||
.signal = signal,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_signal(self: *AbortController) *AbortSignal {
|
||||
return self.signal;
|
||||
}
|
||||
|
||||
pub fn _abort(self: *AbortController, reason_: ?[]const u8) !void {
|
||||
return self.signal.abort(reason_);
|
||||
}
|
||||
|
||||
pub const AbortSignal = struct {
|
||||
const DEFAULT_REASON = "AbortError";
|
||||
|
||||
pub const prototype = *EventTarget;
|
||||
proto: parser.EventTargetTBase = .{ .internal_target_type = .abort_signal },
|
||||
|
||||
aborted: bool,
|
||||
reason: ?[]const u8,
|
||||
|
||||
pub const init: AbortSignal = .{
|
||||
.reason = null,
|
||||
.aborted = false,
|
||||
};
|
||||
|
||||
pub fn static_abort(reason_: ?[]const u8) AbortSignal {
|
||||
return .{
|
||||
.aborted = true,
|
||||
.reason = reason_ orelse DEFAULT_REASON,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn static_timeout(delay: u32, page: *Page) !*AbortSignal {
|
||||
const callback = try page.arena.create(TimeoutCallback);
|
||||
callback.* = .{
|
||||
.signal = .init,
|
||||
};
|
||||
|
||||
try page.scheduler.add(callback, TimeoutCallback.run, delay, .{ .name = "abort_signal" });
|
||||
return &callback.signal;
|
||||
}
|
||||
|
||||
pub fn get_aborted(self: *const AbortSignal) bool {
|
||||
return self.aborted;
|
||||
}
|
||||
|
||||
fn abort(self: *AbortSignal, reason_: ?[]const u8) !void {
|
||||
self.aborted = true;
|
||||
self.reason = reason_ orelse DEFAULT_REASON;
|
||||
|
||||
const abort_event = try parser.eventCreate();
|
||||
try parser.eventSetInternalType(abort_event, .abort_signal);
|
||||
|
||||
defer parser.eventDestroy(abort_event);
|
||||
try parser.eventInit(abort_event, "abort", .{});
|
||||
_ = try parser.eventTargetDispatchEvent(
|
||||
parser.toEventTarget(AbortSignal, self),
|
||||
abort_event,
|
||||
);
|
||||
}
|
||||
|
||||
const Reason = union(enum) {
|
||||
reason: []const u8,
|
||||
undefined: void,
|
||||
};
|
||||
pub fn get_reason(self: *const AbortSignal) Reason {
|
||||
if (self.reason) |r| {
|
||||
return .{ .reason = r };
|
||||
}
|
||||
return .{ .undefined = {} };
|
||||
}
|
||||
|
||||
const ThrowIfAborted = union(enum) {
|
||||
exception: Env.Exception,
|
||||
undefined: void,
|
||||
};
|
||||
pub fn _throwIfAborted(self: *const AbortSignal, page: *Page) ThrowIfAborted {
|
||||
if (self.aborted) {
|
||||
const ex = page.main_context.throw(self.reason orelse DEFAULT_REASON);
|
||||
return .{ .exception = ex };
|
||||
}
|
||||
return .{ .undefined = {} };
|
||||
}
|
||||
};
|
||||
|
||||
const TimeoutCallback = struct {
|
||||
signal: AbortSignal,
|
||||
|
||||
fn run(ctx: *anyopaque) ?u32 {
|
||||
const self: *TimeoutCallback = @ptrCast(@alignCast(ctx));
|
||||
self.signal.abort("TimeoutError") catch |err| {
|
||||
log.warn(.app, "abort signal timeout", .{ .err = err });
|
||||
};
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.AbortController" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var called = 0", null },
|
||||
.{ "var a1 = new AbortController()", null },
|
||||
.{ "var s1 = a1.signal", null },
|
||||
.{ "s1.throwIfAborted()", "undefined" },
|
||||
.{ "s1.reason", "undefined" },
|
||||
.{ "var target;", null },
|
||||
.{
|
||||
\\ s1.addEventListener('abort', (e) => {
|
||||
\\ called += 1;
|
||||
\\ target = e.target;
|
||||
\\
|
||||
\\ });
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "a1.abort()", null },
|
||||
.{ "s1.aborted", "true" },
|
||||
.{ "target == s1", "true" },
|
||||
.{ "s1.reason", "AbortError" },
|
||||
.{ "called", "1" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var s2 = AbortSignal.abort('over 9000')", null },
|
||||
.{ "s2.aborted", "true" },
|
||||
.{ "s2.reason", "over 9000" },
|
||||
.{ "AbortSignal.abort().reason", "AbortError" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var s3 = AbortSignal.timeout(10)", null },
|
||||
.{ "s3.aborted", "true" },
|
||||
.{ "s3.reason", "TimeoutError" },
|
||||
.{ "try { s3.throwIfAborted() } catch (e) { e }", "Error: TimeoutError" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -1,97 +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");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const DataSet = @This();
|
||||
|
||||
element: *parser.Element,
|
||||
|
||||
pub fn named_get(self: *const DataSet, name: []const u8, _: *bool, page: *Page) !Env.UndefinedOr([]const u8) {
|
||||
const normalized_name = try normalize(page.call_arena, name);
|
||||
if (try parser.elementGetAttribute(self.element, normalized_name)) |value| {
|
||||
return .{ .value = value };
|
||||
}
|
||||
return .undefined;
|
||||
}
|
||||
|
||||
pub fn named_set(self: *DataSet, name: []const u8, value: []const u8, _: *bool, page: *Page) !void {
|
||||
const normalized_name = try normalize(page.call_arena, name);
|
||||
try parser.elementSetAttribute(self.element, normalized_name, value);
|
||||
}
|
||||
|
||||
pub fn named_delete(self: *DataSet, name: []const u8, _: *bool, page: *Page) !void {
|
||||
const normalized_name = try normalize(page.call_arena, name);
|
||||
try parser.elementRemoveAttribute(self.element, normalized_name);
|
||||
}
|
||||
|
||||
fn normalize(allocator: Allocator, name: []const u8) ![]const u8 {
|
||||
var upper_count: usize = 0;
|
||||
for (name) |c| {
|
||||
if (std.ascii.isUpper(c)) {
|
||||
upper_count += 1;
|
||||
}
|
||||
}
|
||||
// for every upper-case letter, we'll probably need a dash before it
|
||||
// and we need the 'data-' prefix
|
||||
var normalized = try allocator.alloc(u8, name.len + upper_count + 5);
|
||||
|
||||
@memcpy(normalized[0..5], "data-");
|
||||
if (upper_count == 0) {
|
||||
@memcpy(normalized[5..], name);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
var pos: usize = 5;
|
||||
for (name) |c| {
|
||||
if (std.ascii.isUpper(c)) {
|
||||
normalized[pos] = '-';
|
||||
pos += 1;
|
||||
normalized[pos] = c + 32;
|
||||
} else {
|
||||
normalized[pos] = c;
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.DataSet" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "" });
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let el1 = document.createElement('div')", null },
|
||||
.{ "el1.dataset.x", "undefined" },
|
||||
.{ "el1.dataset.x = '123'", "123" },
|
||||
.{ "delete el1.dataset.x", "true" },
|
||||
.{ "el1.dataset.x", "undefined" },
|
||||
.{ "delete el1.dataset.other", "true" }, // yes, this is right
|
||||
|
||||
.{ "let ds1 = el1.dataset", null },
|
||||
.{ "ds1.helloWorld = 'yes'", null },
|
||||
.{ "el1.getAttribute('data-hello-world')", "yes" },
|
||||
.{ "el1.setAttribute('data-this-will-work', 'positive')", null },
|
||||
.{ "ds1.thisWillWork", "positive" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -18,13 +18,9 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
const Window = @import("window.zig").Window;
|
||||
const Element = @import("../dom/element.zig").Element;
|
||||
const ElementUnion = @import("../dom/element.zig").Union;
|
||||
const Document = @import("../dom/document.zig").Document;
|
||||
const NodeList = @import("../dom/nodelist.zig").NodeList;
|
||||
const Location = @import("location.zig").Location;
|
||||
@@ -33,6 +29,19 @@ const collection = @import("../dom/html_collection.zig");
|
||||
const Walker = @import("../dom/walker.zig").WalkerDepthFirst;
|
||||
const Cookie = @import("../storage/cookie.zig").Cookie;
|
||||
|
||||
pub fn normalizeWhitespace(arena: std.mem.Allocator, title: []const u8) ![]const u8 {
|
||||
var normalized = try std.ArrayListUnmanaged(u8).initCapacity(arena, title.len);
|
||||
var tokens = std.mem.tokenizeAny(u8, title, &std.ascii.whitespace);
|
||||
|
||||
var prepend = false;
|
||||
while (tokens.next()) |token| {
|
||||
if (prepend) normalized.appendAssumeCapacity(' ') else prepend = true;
|
||||
normalized.appendSliceAssumeCapacity(token);
|
||||
}
|
||||
|
||||
return normalized.items;
|
||||
}
|
||||
|
||||
// WEB IDL https://html.spec.whatwg.org/#the-document-object
|
||||
pub const HTMLDocument = struct {
|
||||
pub const Self = parser.DocumentHTML;
|
||||
@@ -42,12 +51,8 @@ pub const HTMLDocument = struct {
|
||||
// JS funcs
|
||||
// --------
|
||||
|
||||
pub fn get_domain(self: *parser.DocumentHTML, page: *Page) ![]const u8 {
|
||||
// libdom's document_html get_domain always returns null, this is
|
||||
// the way MDN recommends getting the domain anyways, since document.domain
|
||||
// is deprecated.
|
||||
const location = try parser.documentHTMLGetLocation(Location, self) orelse return "";
|
||||
return location.get_host(page);
|
||||
pub fn get_domain(self: *parser.DocumentHTML) ![]const u8 {
|
||||
return try parser.documentHTMLGetDomain(self);
|
||||
}
|
||||
|
||||
pub fn set_domain(_: *parser.DocumentHTML, _: []const u8) ![]const u8 {
|
||||
@@ -83,25 +88,18 @@ pub const HTMLDocument = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_cookie(_: *parser.DocumentHTML, page: *Page) ![]const u8 {
|
||||
pub fn get_cookie(_: *parser.DocumentHTML, state: *SessionState) ![]const u8 {
|
||||
var buf: std.ArrayListUnmanaged(u8) = .{};
|
||||
try page.cookie_jar.forRequest(&page.url.uri, buf.writer(page.arena), .{
|
||||
.is_http = false,
|
||||
.is_navigation = true,
|
||||
});
|
||||
try state.cookie_jar.forRequest(&state.url.uri, buf.writer(state.arena), .{ .navigation = true });
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
pub fn set_cookie(_: *parser.DocumentHTML, cookie_str: []const u8, page: *Page) ![]const u8 {
|
||||
pub fn set_cookie(_: *parser.DocumentHTML, cookie_str: []const u8, state: *SessionState) ![]const u8 {
|
||||
// we use the cookie jar's allocator to parse the cookie because it
|
||||
// outlives the page's arena.
|
||||
const c = try Cookie.parse(page.cookie_jar.allocator, &page.url.uri, cookie_str);
|
||||
const c = try Cookie.parse(state.cookie_jar.allocator, &state.url.uri, cookie_str);
|
||||
errdefer c.deinit();
|
||||
if (c.http_only) {
|
||||
c.deinit();
|
||||
return ""; // HttpOnly cookies cannot be set from JS
|
||||
}
|
||||
try page.cookie_jar.add(c, std.time.timestamp());
|
||||
try state.cookie_jar.add(c, std.time.timestamp());
|
||||
return cookie_str;
|
||||
}
|
||||
|
||||
@@ -109,21 +107,20 @@ pub const HTMLDocument = struct {
|
||||
return try parser.documentHTMLGetTitle(self);
|
||||
}
|
||||
|
||||
pub fn set_title(self: *parser.DocumentHTML, v: []const u8) ![]const u8 {
|
||||
try parser.documentHTMLSetTitle(self, v);
|
||||
return v;
|
||||
pub fn set_title(self: *parser.DocumentHTML, v: []const u8, state: *SessionState) ![]const u8 {
|
||||
const normalized = try normalizeWhitespace(state.arena, v);
|
||||
try parser.documentHTMLSetTitle(self, normalized);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
pub fn _getElementsByName(self: *parser.DocumentHTML, name: []const u8, page: *Page) !NodeList {
|
||||
const arena = page.arena;
|
||||
pub fn _getElementsByName(self: *parser.DocumentHTML, name: []const u8, state: *SessionState) !NodeList {
|
||||
const arena = state.arena;
|
||||
var list: NodeList = .{};
|
||||
|
||||
if (name.len == 0) return list;
|
||||
|
||||
const root = parser.documentHTMLToNode(self);
|
||||
var c = try collection.HTMLCollectionByName(arena, root, name, .{
|
||||
.include_root = false,
|
||||
});
|
||||
var c = try collection.HTMLCollectionByName(arena, root, name, false);
|
||||
|
||||
const ln = try c.get_length();
|
||||
var i: u32 = 0;
|
||||
@@ -136,32 +133,24 @@ pub const HTMLDocument = struct {
|
||||
return list;
|
||||
}
|
||||
|
||||
pub fn get_images(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "img", .{
|
||||
.include_root = false,
|
||||
});
|
||||
pub fn get_images(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(state.arena, parser.documentHTMLToNode(self), "img", false);
|
||||
}
|
||||
|
||||
pub fn get_embeds(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "embed", .{
|
||||
.include_root = false,
|
||||
});
|
||||
pub fn get_embeds(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(state.arena, parser.documentHTMLToNode(self), "embed", false);
|
||||
}
|
||||
|
||||
pub fn get_plugins(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
||||
return get_embeds(self, page);
|
||||
pub fn get_plugins(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection {
|
||||
return get_embeds(self, state);
|
||||
}
|
||||
|
||||
pub fn get_forms(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "form", .{
|
||||
.include_root = false,
|
||||
});
|
||||
pub fn get_forms(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(state.arena, parser.documentHTMLToNode(self), "form", false);
|
||||
}
|
||||
|
||||
pub fn get_scripts(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "script", .{
|
||||
.include_root = false,
|
||||
});
|
||||
pub fn get_scripts(self: *parser.DocumentHTML, state: *SessionState) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(state.arena, parser.documentHTMLToNode(self), "script", false);
|
||||
}
|
||||
|
||||
pub fn get_applets(_: *parser.DocumentHTML) !collection.HTMLCollection {
|
||||
@@ -169,15 +158,11 @@ pub const HTMLDocument = struct {
|
||||
}
|
||||
|
||||
pub fn get_links(self: *parser.DocumentHTML) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByLinks(parser.documentHTMLToNode(self), .{
|
||||
.include_root = false,
|
||||
});
|
||||
return try collection.HTMLCollectionByLinks(parser.documentHTMLToNode(self), false);
|
||||
}
|
||||
|
||||
pub fn get_anchors(self: *parser.DocumentHTML) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByAnchors(parser.documentHTMLToNode(self), .{
|
||||
.include_root = false,
|
||||
});
|
||||
return try collection.HTMLCollectionByAnchors(parser.documentHTMLToNode(self), false);
|
||||
}
|
||||
|
||||
pub fn get_all(self: *parser.DocumentHTML) collection.HTMLAllCollection {
|
||||
@@ -192,10 +177,6 @@ pub const HTMLDocument = struct {
|
||||
return try parser.documentHTMLGetLocation(Location, self);
|
||||
}
|
||||
|
||||
pub fn set_location(_: *const parser.DocumentHTML, url: []const u8, page: *Page) !void {
|
||||
return page.navigateFromWebAPI(url, .{ .reason = .script });
|
||||
}
|
||||
|
||||
pub fn get_designMode(_: *parser.DocumentHTML) []const u8 {
|
||||
return "off";
|
||||
}
|
||||
@@ -204,15 +185,6 @@ pub const HTMLDocument = struct {
|
||||
return "off";
|
||||
}
|
||||
|
||||
pub fn get_defaultView(_: *parser.DocumentHTML, page: *Page) *Window {
|
||||
return &page.window;
|
||||
}
|
||||
|
||||
pub fn get_readyState(self: *parser.DocumentHTML, page: *Page) ![]const u8 {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
return @tagName(state.ready_state);
|
||||
}
|
||||
|
||||
// noop legacy functions
|
||||
// https://html.spec.whatwg.org/#Document-partial
|
||||
pub fn _clear(_: *parser.DocumentHTML) void {}
|
||||
@@ -249,69 +221,6 @@ pub const HTMLDocument = struct {
|
||||
pub fn set_bgColor(_: *parser.DocumentHTML, _: []const u8) []const u8 {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Returns the topmost Element at the specified coordinates (relative to the viewport).
|
||||
// Since LightPanda requires the client to know what they are clicking on we do not return the underlying element at this moment
|
||||
// This can currenty only happen if the first pixel is clicked without having rendered any element. This will change when css properties are supported.
|
||||
// This returns an ElementUnion instead of a *Parser.Element in case the element somehow hasn't passed through the js runtime yet.
|
||||
// While x and y should be f32, here we take i32 since that's what our
|
||||
// "renderer" uses. By specifying i32 here, rather than f32 and doing the
|
||||
// conversion ourself, we rely on v8's type conversion which is both more
|
||||
// flexible (e.g. handles NaN) and will be more consistent with a browser.
|
||||
pub fn _elementFromPoint(_: *parser.DocumentHTML, x: i32, y: i32, page: *Page) !?ElementUnion {
|
||||
const element = page.renderer.getElementAtPosition(x, y) orelse return null;
|
||||
// TODO if pointer-events set to none the underlying element should be returned (parser.documentGetDocumentElement(self.document);?)
|
||||
return try Element.toInterface(element);
|
||||
}
|
||||
|
||||
// Returns an array of all elements at the specified coordinates (relative to the viewport). The elements are ordered from the topmost to the bottommost box of the viewport.
|
||||
// While x and y should be f32, here we take i32 since that's what our
|
||||
// "renderer" uses. By specifying i32 here, rather than f32 and doing the
|
||||
// conversion ourself, we rely on v8's type conversion which is both more
|
||||
// flexible (e.g. handles NaN) and will be more consistent with a browser.
|
||||
pub fn _elementsFromPoint(_: *parser.DocumentHTML, x: i32, y: i32, page: *Page) ![]ElementUnion {
|
||||
const element = page.renderer.getElementAtPosition(x, y) orelse return &.{};
|
||||
// TODO if pointer-events set to none the underlying element should be returned (parser.documentGetDocumentElement(self.document);?)
|
||||
|
||||
var list: std.ArrayListUnmanaged(ElementUnion) = .empty;
|
||||
try list.ensureTotalCapacity(page.call_arena, 3);
|
||||
list.appendAssumeCapacity(try Element.toInterface(element));
|
||||
|
||||
// Since we are using a flat renderer there is no hierarchy of elements. What we do know is that the element is part of the main document.
|
||||
// Thus we can add the HtmlHtmlElement and it's child HTMLBodyElement to the returned list.
|
||||
// TBD Should we instead return every parent that is an element? Note that a child does not physically need to be overlapping the parent.
|
||||
// Should we do a render pass on demand?
|
||||
const doc_elem = try parser.documentGetDocumentElement(parser.documentHTMLToDocument(page.window.document)) orelse {
|
||||
return list.items;
|
||||
};
|
||||
if (try parser.documentHTMLBody(page.window.document)) |body| {
|
||||
list.appendAssumeCapacity(try Element.toInterface(parser.bodyToElement(body)));
|
||||
}
|
||||
list.appendAssumeCapacity(try Element.toInterface(doc_elem));
|
||||
return list.items;
|
||||
}
|
||||
|
||||
pub fn documentIsLoaded(self: *parser.DocumentHTML, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
state.ready_state = .interactive;
|
||||
|
||||
log.debug(.script_event, "dispatch event", .{
|
||||
.type = "DOMContentLoaded",
|
||||
.source = "document",
|
||||
});
|
||||
|
||||
const evt = try parser.eventCreate();
|
||||
defer parser.eventDestroy(evt);
|
||||
try parser.eventInit(evt, "DOMContentLoaded", .{ .bubbles = true, .cancelable = true });
|
||||
_ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, self), evt);
|
||||
|
||||
try page.window.dispatchForDocumentTarget(evt);
|
||||
}
|
||||
|
||||
pub fn documentIsComplete(self: *parser.DocumentHTML, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
state.ready_state = .complete;
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
@@ -330,7 +239,7 @@ test "Browser.HTML.Document" {
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.domain", "lightpanda.io" },
|
||||
.{ "document.domain", "" },
|
||||
.{ "document.referrer", "" },
|
||||
.{ "document.title", "" },
|
||||
.{ "document.body.localName", "body" },
|
||||
@@ -364,42 +273,6 @@ test "Browser.HTML.Document" {
|
||||
.{ "document.cookie = 'name=Oeschger; SameSite=None; Secure'", "name=Oeschger; SameSite=None; Secure" },
|
||||
.{ "document.cookie = 'favorite_food=tripe; SameSite=None; Secure'", "favorite_food=tripe; SameSite=None; Secure" },
|
||||
.{ "document.cookie", "name=Oeschger; favorite_food=tripe" },
|
||||
.{ "document.cookie = 'IgnoreMy=Ghost; HttpOnly'", null }, // "" should be returned, but the framework overrules it atm
|
||||
.{ "document.cookie", "name=Oeschger; favorite_food=tripe" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.elementFromPoint(0.5, 0.5)", "null" }, // Return null since we only return element s when they have previously been localized
|
||||
.{ "document.elementsFromPoint(0.5, 0.5)", "" },
|
||||
.{
|
||||
\\ let div1 = document.createElement('div');
|
||||
\\ document.body.appendChild(div1);
|
||||
\\ div1.getClientRects();
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "document.elementFromPoint(0.5, 0.5)", "[object HTMLDivElement]" },
|
||||
.{ "let elems = document.elementsFromPoint(0.5, 0.5)", null },
|
||||
.{ "elems.length", "3" },
|
||||
.{ "elems[0]", "[object HTMLDivElement]" },
|
||||
.{ "elems[1]", "[object HTMLBodyElement]" },
|
||||
.{ "elems[2]", "[object HTMLHtmlElement]" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ let a = document.createElement('a');
|
||||
\\ a.href = "https://lightpanda.io";
|
||||
\\ document.body.appendChild(a);
|
||||
\\ a.getClientRects();
|
||||
, // Note this will be placed after the div of previous test
|
||||
null,
|
||||
},
|
||||
.{ "let a_again = document.elementFromPoint(1.5, 0.5)", null },
|
||||
.{ "a_again", "[object HTMLAnchorElement]" },
|
||||
.{ "a_again.href", "https://lightpanda.io" },
|
||||
.{ "let a_agains = document.elementsFromPoint(1.5, 0.5)", null },
|
||||
.{ "a_agains[0].href", "https://lightpanda.io" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
@@ -408,22 +281,4 @@ test "Browser.HTML.Document" {
|
||||
.{ "document.all(5)", "[object HTMLParagraphElement]" },
|
||||
.{ "document.all('content')", "[object HTMLDivElement]" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.defaultView.document == document", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.readyState", "loading" },
|
||||
}, .{});
|
||||
|
||||
try HTMLDocument.documentIsLoaded(runner.page.window.document, runner.page);
|
||||
try runner.testCases(&.{
|
||||
.{ "document.readyState", "interactive" },
|
||||
}, .{});
|
||||
|
||||
try HTMLDocument.documentIsComplete(runner.page.window.document, runner.page);
|
||||
try runner.testCases(&.{
|
||||
.{ "document.readyState", "complete" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,86 +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 Env = @import("../env.zig").Env;
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent
|
||||
pub const ErrorEvent = struct {
|
||||
pub const prototype = *parser.Event;
|
||||
pub const union_make_copy = true;
|
||||
|
||||
proto: parser.Event,
|
||||
message: []const u8,
|
||||
filename: []const u8,
|
||||
lineno: i32,
|
||||
colno: i32,
|
||||
@"error": ?Env.JsObject,
|
||||
|
||||
const ErrorEventInit = struct {
|
||||
message: []const u8 = "",
|
||||
filename: []const u8 = "",
|
||||
lineno: i32 = 0,
|
||||
colno: i32 = 0,
|
||||
@"error": ?Env.JsObject = null,
|
||||
};
|
||||
|
||||
pub fn constructor(event_type: []const u8, opts: ?ErrorEventInit) !ErrorEvent {
|
||||
const event = try parser.eventCreate();
|
||||
defer parser.eventDestroy(event);
|
||||
try parser.eventInit(event, event_type, .{});
|
||||
try parser.eventSetInternalType(event, .event);
|
||||
|
||||
const o = opts orelse ErrorEventInit{};
|
||||
|
||||
return .{
|
||||
.proto = event.*,
|
||||
.message = o.message,
|
||||
.filename = o.filename,
|
||||
.lineno = o.lineno,
|
||||
.colno = o.colno,
|
||||
.@"error" = if (o.@"error") |e| try e.persist() else null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_message(self: *const ErrorEvent) []const u8 {
|
||||
return self.message;
|
||||
}
|
||||
|
||||
pub fn get_filename(self: *const ErrorEvent) []const u8 {
|
||||
return self.filename;
|
||||
}
|
||||
|
||||
pub fn get_lineno(self: *const ErrorEvent) i32 {
|
||||
return self.lineno;
|
||||
}
|
||||
|
||||
pub fn get_colno(self: *const ErrorEvent) i32 {
|
||||
return self.colno;
|
||||
}
|
||||
|
||||
pub fn get_error(self: *const ErrorEvent) Env.UndefinedOr(Env.JsObject) {
|
||||
if (self.@"error") |e| {
|
||||
return .{ .value = e };
|
||||
}
|
||||
return .undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: HTML.ErrorEvent" {
|
||||
try testing.htmlRunner("html/error_event.html");
|
||||
}
|
||||
@@ -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");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
const HTMLElement = @import("elements.zig").HTMLElement;
|
||||
const FormData = @import("../xhr/form_data.zig").FormData;
|
||||
|
||||
pub const HTMLFormElement = struct {
|
||||
pub const Self = parser.Form;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn _submit(self: *parser.Form, page: *Page) !void {
|
||||
return page.submitForm(self, null);
|
||||
}
|
||||
|
||||
pub fn _reset(self: *parser.Form) !void {
|
||||
try parser.formElementReset(self);
|
||||
}
|
||||
};
|
||||
@@ -24,6 +24,7 @@ const Navigator = @import("navigator.zig").Navigator;
|
||||
const History = @import("history.zig").History;
|
||||
const Location = @import("location.zig").Location;
|
||||
const MediaQueryList = @import("media_query_list.zig").MediaQueryList;
|
||||
const Performance = @import("performance.zig").Performance;
|
||||
|
||||
pub const Interfaces = .{
|
||||
HTMLDocument,
|
||||
@@ -36,8 +37,5 @@ pub const Interfaces = .{
|
||||
History,
|
||||
Location,
|
||||
MediaQueryList,
|
||||
@import("DataSet.zig"),
|
||||
@import("screen.zig").Interfaces,
|
||||
@import("error_event.zig").ErrorEvent,
|
||||
@import("AbortController.zig").Interfaces,
|
||||
Performance,
|
||||
};
|
||||
|
||||
@@ -1,30 +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 Allocator = std.mem.Allocator;
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
const HTMLElement = @import("elements.zig").HTMLElement;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#htmliframeelement
|
||||
pub const HTMLIFrameElement = struct {
|
||||
pub const Self = parser.IFrame;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
@@ -16,7 +16,7 @@
|
||||
// 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 Page = @import("../page.zig").Page;
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
|
||||
const URL = @import("../url/url.zig").URL;
|
||||
|
||||
@@ -24,18 +24,18 @@ const URL = @import("../url/url.zig").URL;
|
||||
pub const Location = struct {
|
||||
url: ?URL = null,
|
||||
|
||||
pub fn get_href(self: *Location, page: *Page) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_href(page);
|
||||
pub fn get_href(self: *Location, state: *SessionState) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_href(state);
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn get_protocol(self: *Location, page: *Page) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_protocol(page);
|
||||
pub fn get_protocol(self: *Location, state: *SessionState) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_protocol(state);
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn get_host(self: *Location, page: *Page) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_host(page);
|
||||
pub fn get_host(self: *Location, state: *SessionState) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_host(state);
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ pub const Location = struct {
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn get_port(self: *Location, page: *Page) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_port(page);
|
||||
pub fn get_port(self: *Location, state: *SessionState) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_port(state);
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -54,35 +54,36 @@ pub const Location = struct {
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn get_search(self: *Location, page: *Page) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_search(page);
|
||||
pub fn get_search(self: *Location, state: *SessionState) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_search(state);
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn get_hash(self: *Location, page: *Page) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_hash(page);
|
||||
pub fn get_hash(self: *Location, state: *SessionState) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_hash(state);
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn get_origin(self: *Location, page: *Page) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_origin(page);
|
||||
pub fn get_origin(self: *Location, state: *SessionState) ![]const u8 {
|
||||
if (self.url) |*u| return u.get_origin(state);
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn _assign(_: *const Location, url: []const u8, page: *Page) !void {
|
||||
return page.navigateFromWebAPI(url, .{ .reason = .script });
|
||||
// TODO
|
||||
pub fn _assign(_: *Location, url: []const u8) !void {
|
||||
_ = url;
|
||||
}
|
||||
|
||||
pub fn _replace(_: *const Location, url: []const u8, page: *Page) !void {
|
||||
return page.navigateFromWebAPI(url, .{ .reason = .script });
|
||||
// TODO
|
||||
pub fn _replace(_: *Location, url: []const u8) !void {
|
||||
_ = url;
|
||||
}
|
||||
|
||||
pub fn _reload(_: *const Location, page: *Page) !void {
|
||||
return page.navigateFromWebAPI(page.url.raw, .{ .reason = .script });
|
||||
}
|
||||
// TODO
|
||||
pub fn _reload(_: *Location) !void {}
|
||||
|
||||
pub fn _toString(self: *Location, page: *Page) ![]const u8 {
|
||||
return try self.get_href(page);
|
||||
pub fn _toString(self: *Location, state: *SessionState) ![]const u8 {
|
||||
return try self.get_href(state);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Function = @import("../env.zig").Function;
|
||||
const Callback = @import("../env.zig").Callback;
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
|
||||
// https://drafts.csswg.org/cssom-view/#the-mediaquerylist-interface
|
||||
@@ -26,7 +26,7 @@ pub const MediaQueryList = struct {
|
||||
|
||||
// Extend libdom event target for pure zig struct.
|
||||
// This is not safe as it relies on a structure layout that isn't guaranteed
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .media_query_list },
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{},
|
||||
|
||||
matches: bool,
|
||||
media: []const u8,
|
||||
@@ -39,7 +39,7 @@ pub const MediaQueryList = struct {
|
||||
return self.media;
|
||||
}
|
||||
|
||||
pub fn _addListener(_: *const MediaQueryList, _: Function) void {}
|
||||
pub fn _addListener(_: *const MediaQueryList, _: Callback) void {}
|
||||
|
||||
pub fn _removeListener(_: *const MediaQueryList, _: Function) void {}
|
||||
pub fn _removeListener(_: *const MediaQueryList, _: Callback) void {}
|
||||
};
|
||||
|
||||
87
src/browser/html/performance.zig
Normal file
87
src/browser/html/performance.zig
Normal file
@@ -0,0 +1,87 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Performance
|
||||
pub const Performance = struct {
|
||||
pub const prototype = *EventTarget;
|
||||
|
||||
// Extend libdom event target for pure zig struct.
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{},
|
||||
|
||||
time_origin: std.time.Timer,
|
||||
// if (Window.crossOriginIsolated) -> Resolution in isolated contexts: 5 microseconds
|
||||
// else -> Resolution in non-isolated contexts: 100 microseconds
|
||||
const ms_resolution = 100;
|
||||
|
||||
fn limitedResolutionMs(nanoseconds: u64) f64 {
|
||||
const elapsed_at_resolution = ((nanoseconds / std.time.ns_per_us) + ms_resolution / 2) / ms_resolution * ms_resolution;
|
||||
const elapsed = @as(f64, @floatFromInt(elapsed_at_resolution));
|
||||
return elapsed / @as(f64, std.time.us_per_ms);
|
||||
}
|
||||
|
||||
pub fn get_timeOrigin(self: *const Performance) f64 {
|
||||
const is_posix = switch (@import("builtin").os.tag) { // From std.time.zig L125
|
||||
.windows, .uefi, .wasi => false,
|
||||
else => true,
|
||||
};
|
||||
const zero = std.time.Instant{ .timestamp = if (!is_posix) 0 else .{ .sec = 0, .nsec = 0 } };
|
||||
const started = self.time_origin.started.since(zero);
|
||||
return limitedResolutionMs(started);
|
||||
}
|
||||
|
||||
pub fn _now(self: *Performance) f64 {
|
||||
return limitedResolutionMs(self.time_origin.read());
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("./../../testing.zig");
|
||||
|
||||
test "Performance: get_timeOrigin" {
|
||||
var perf = Performance{ .time_origin = try std.time.Timer.start() };
|
||||
const time_origin = perf.get_timeOrigin();
|
||||
try testing.expect(time_origin >= 0);
|
||||
|
||||
// Check resolution
|
||||
try testing.expectDelta(@rem(time_origin * std.time.us_per_ms, 100.0), 0.0, 0.1);
|
||||
}
|
||||
|
||||
test "Performance: now" {
|
||||
var perf = Performance{ .time_origin = try std.time.Timer.start() };
|
||||
|
||||
// Monotonically increasing
|
||||
var now = perf._now();
|
||||
while (now <= 0) { // Loop for now to not be 0
|
||||
try testing.expectEqual(now, 0);
|
||||
now = perf._now();
|
||||
}
|
||||
// Check resolution
|
||||
try testing.expectDelta(@rem(now * std.time.us_per_ms, 100.0), 0.0, 0.1);
|
||||
|
||||
var after = perf._now();
|
||||
while (after <= now) { // Loop untill after > now
|
||||
try testing.expectEqual(after, now);
|
||||
after = perf._now();
|
||||
}
|
||||
// Check resolution
|
||||
try testing.expectDelta(@rem(after * std.time.us_per_ms, 100.0), 0.0, 0.1);
|
||||
}
|
||||
@@ -1,103 +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");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
|
||||
pub const Interfaces = .{
|
||||
Screen,
|
||||
ScreenOrientation,
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Screen
|
||||
pub const Screen = struct {
|
||||
pub const prototype = *EventTarget;
|
||||
|
||||
proto: parser.EventTargetTBase = .{ .internal_target_type = .screen },
|
||||
|
||||
height: u32 = 1080,
|
||||
width: u32 = 1920,
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Screen/colorDepth
|
||||
color_depth: u32 = 8,
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Screen/pixelDepth
|
||||
pixel_depth: u32 = 8,
|
||||
orientation: ScreenOrientation = .{ .type = .landscape_primary },
|
||||
|
||||
pub fn get_availHeight(self: *const Screen) u32 {
|
||||
return self.height;
|
||||
}
|
||||
|
||||
pub fn get_availWidth(self: *const Screen) u32 {
|
||||
return self.width;
|
||||
}
|
||||
|
||||
pub fn get_height(self: *const Screen) u32 {
|
||||
return self.height;
|
||||
}
|
||||
|
||||
pub fn get_width(self: *const Screen) u32 {
|
||||
return self.width;
|
||||
}
|
||||
|
||||
pub fn get_pixelDepth(self: *const Screen) u32 {
|
||||
return self.pixel_depth;
|
||||
}
|
||||
|
||||
pub fn get_orientation(self: *const Screen) ScreenOrientation {
|
||||
return self.orientation;
|
||||
}
|
||||
};
|
||||
|
||||
const ScreenOrientationType = enum {
|
||||
portrait_primary,
|
||||
portrait_secondary,
|
||||
landscape_primary,
|
||||
landscape_secondary,
|
||||
|
||||
pub fn toString(self: ScreenOrientationType) []const u8 {
|
||||
return switch (self) {
|
||||
.portrait_primary => "portrait-primary",
|
||||
.portrait_secondary => "portrait-secondary",
|
||||
.landscape_primary => "landscape-primary",
|
||||
.landscape_secondary => "landscape-secondary",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const ScreenOrientation = struct {
|
||||
pub const prototype = *EventTarget;
|
||||
|
||||
angle: u32 = 0,
|
||||
type: ScreenOrientationType,
|
||||
proto: parser.EventTargetTBase = .{ .internal_target_type = .screen_orientation },
|
||||
|
||||
pub fn get_angle(self: *const ScreenOrientation) u32 {
|
||||
return self.angle;
|
||||
}
|
||||
|
||||
pub fn get_type(self: *const ScreenOrientation) []const u8 {
|
||||
return self.type.toString();
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: HTML.Screen" {
|
||||
try testing.htmlRunner("html/screen.html");
|
||||
}
|
||||
@@ -1,278 +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");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const collection = @import("../dom/html_collection.zig");
|
||||
|
||||
const Page = @import("../page.zig").Page;
|
||||
const HTMLElement = @import("elements.zig").HTMLElement;
|
||||
|
||||
pub const Interfaces = .{
|
||||
HTMLSelectElement,
|
||||
HTMLOptionElement,
|
||||
HTMLOptionsCollection,
|
||||
};
|
||||
|
||||
pub const HTMLSelectElement = struct {
|
||||
pub const Self = parser.Select;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn get_length(select: *parser.Select) !u32 {
|
||||
return parser.selectGetLength(select);
|
||||
}
|
||||
|
||||
pub fn get_form(select: *parser.Select) !?*parser.Form {
|
||||
return parser.selectGetForm(select);
|
||||
}
|
||||
|
||||
pub fn get_name(select: *parser.Select) ![]const u8 {
|
||||
return parser.selectGetName(select);
|
||||
}
|
||||
pub fn set_name(select: *parser.Select, name: []const u8) !void {
|
||||
return parser.selectSetName(select, name);
|
||||
}
|
||||
|
||||
pub fn get_disabled(select: *parser.Select) !bool {
|
||||
return parser.selectGetDisabled(select);
|
||||
}
|
||||
pub fn set_disabled(select: *parser.Select, disabled: bool) !void {
|
||||
return parser.selectSetDisabled(select, disabled);
|
||||
}
|
||||
|
||||
pub fn get_multiple(select: *parser.Select) !bool {
|
||||
return parser.selectGetMultiple(select);
|
||||
}
|
||||
pub fn set_multiple(select: *parser.Select, multiple: bool) !void {
|
||||
return parser.selectSetMultiple(select, multiple);
|
||||
}
|
||||
|
||||
pub fn get_selectedIndex(select: *parser.Select, page: *Page) !i32 {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(select)));
|
||||
const selected_index = try parser.selectGetSelectedIndex(select);
|
||||
|
||||
// See the explicit_index_set field documentation
|
||||
if (!state.explicit_index_set) {
|
||||
if (selected_index == -1) {
|
||||
if (try parser.selectGetMultiple(select) == false) {
|
||||
if (try get_length(select) > 0) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return selected_index;
|
||||
}
|
||||
|
||||
// Libdom's dom_html_select_select_set_selected_index will crash if index
|
||||
// is out of range, and it doesn't properly unset options
|
||||
pub fn set_selectedIndex(select: *parser.Select, index: i32, page: *Page) !void {
|
||||
var state = try page.getOrCreateNodeState(@ptrCast(@alignCast(select)));
|
||||
state.explicit_index_set = true;
|
||||
|
||||
const options = try parser.selectGetOptions(select);
|
||||
const len = try parser.optionCollectionGetLength(options);
|
||||
for (0..len) |i| {
|
||||
const option = try parser.optionCollectionItem(options, @intCast(i));
|
||||
try parser.optionSetSelected(option, false);
|
||||
}
|
||||
if (index >= 0 and index < try get_length(select)) {
|
||||
const option = try parser.optionCollectionItem(options, @intCast(index));
|
||||
try parser.optionSetSelected(option, true);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_options(select: *parser.Select) HTMLOptionsCollection {
|
||||
return .{
|
||||
.select = select,
|
||||
.proto = collection.HTMLCollectionChildren(@ptrCast(@alignCast(select)), .{
|
||||
.mutable = true,
|
||||
.include_root = false,
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLOptionElement = struct {
|
||||
pub const Self = parser.Option;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn get_value(self: *parser.Option) ![]const u8 {
|
||||
return parser.optionGetValue(self);
|
||||
}
|
||||
pub fn set_value(self: *parser.Option, value: []const u8) !void {
|
||||
return parser.optionSetValue(self, value);
|
||||
}
|
||||
|
||||
pub fn get_label(self: *parser.Option) ![]const u8 {
|
||||
return parser.optionGetLabel(self);
|
||||
}
|
||||
pub fn set_label(self: *parser.Option, label: []const u8) !void {
|
||||
return parser.optionSetLabel(self, label);
|
||||
}
|
||||
|
||||
pub fn get_selected(self: *parser.Option) !bool {
|
||||
return parser.optionGetSelected(self);
|
||||
}
|
||||
pub fn set_selected(self: *parser.Option, value: bool) !void {
|
||||
return parser.optionSetSelected(self, value);
|
||||
}
|
||||
|
||||
pub fn get_disabled(self: *parser.Option) !bool {
|
||||
return parser.optionGetDisabled(self);
|
||||
}
|
||||
pub fn set_disabled(self: *parser.Option, value: bool) !void {
|
||||
return parser.optionSetDisabled(self, value);
|
||||
}
|
||||
|
||||
pub fn get_text(self: *parser.Option) ![]const u8 {
|
||||
return parser.optionGetText(self);
|
||||
}
|
||||
|
||||
pub fn get_form(self: *parser.Option) !?*parser.Form {
|
||||
return parser.optionGetForm(self);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLOptionsCollection = struct {
|
||||
pub const prototype = *collection.HTMLCollection;
|
||||
|
||||
proto: collection.HTMLCollection,
|
||||
select: *parser.Select,
|
||||
|
||||
pub fn get_selectedIndex(self: *HTMLOptionsCollection, page: *Page) !i32 {
|
||||
return HTMLSelectElement.get_selectedIndex(self.select, page);
|
||||
}
|
||||
|
||||
pub fn set_selectedIndex(self: *HTMLOptionsCollection, index: i32, page: *Page) !void {
|
||||
return HTMLSelectElement.set_selectedIndex(self.select, index, page);
|
||||
}
|
||||
|
||||
const BeforeOpts = union(enum) {
|
||||
index: u32,
|
||||
option: *parser.Option,
|
||||
};
|
||||
pub fn _add(self: *HTMLOptionsCollection, option: *parser.Option, before_: ?BeforeOpts) !void {
|
||||
const Node = @import("../dom/node.zig").Node;
|
||||
const before = before_ orelse {
|
||||
return self.appendOption(option);
|
||||
};
|
||||
|
||||
const insert_before: *parser.Node = switch (before) {
|
||||
.option => |o| @ptrCast(@alignCast(o)),
|
||||
.index => |i| (try self.proto.item(i)) orelse return self.appendOption(option),
|
||||
};
|
||||
return Node.before(insert_before, &.{
|
||||
.{ .node = @ptrCast(@alignCast(option)) },
|
||||
});
|
||||
}
|
||||
|
||||
pub fn _remove(self: *HTMLOptionsCollection, index: u32) !void {
|
||||
const Node = @import("../dom/node.zig").Node;
|
||||
const option = (try self.proto.item(index)) orelse return;
|
||||
_ = try Node._removeChild(@ptrCast(@alignCast(self.select)), option);
|
||||
}
|
||||
|
||||
fn appendOption(self: *HTMLOptionsCollection, option: *parser.Option) !void {
|
||||
const Node = @import("../dom/node.zig").Node;
|
||||
return Node.append(@ptrCast(@alignCast(self.select)), &.{
|
||||
.{ .node = @ptrCast(@alignCast(option)) },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.Select" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
|
||||
\\ <form id=f1>
|
||||
\\ <select id=s1 name=s1><option>o1<option>o2</select>
|
||||
\\ </form>
|
||||
\\ <select id=s2></select>
|
||||
});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const s = document.getElementById('s1');", null },
|
||||
.{ "s.form", "[object HTMLFormElement]" },
|
||||
|
||||
.{ "document.getElementById('s2').form", "null" },
|
||||
|
||||
.{ "s.disabled", "false" },
|
||||
.{ "s.disabled = true", null },
|
||||
.{ "s.disabled", "true" },
|
||||
.{ "s.disabled = false", null },
|
||||
.{ "s.disabled", "false" },
|
||||
|
||||
.{ "s.multiple", "false" },
|
||||
.{ "s.multiple = true", null },
|
||||
.{ "s.multiple", "true" },
|
||||
.{ "s.multiple = false", null },
|
||||
.{ "s.multiple", "false" },
|
||||
|
||||
.{ "s.name;", "s1" },
|
||||
.{ "s.name = 'sel1';", null },
|
||||
.{ "s.name", "sel1" },
|
||||
|
||||
.{ "s.length;", "2" },
|
||||
|
||||
.{ "s.selectedIndex", "0" },
|
||||
.{ "s.selectedIndex = 2", null }, // out of range
|
||||
.{ "s.selectedIndex", "-1" },
|
||||
|
||||
.{ "s.selectedIndex = -1", null },
|
||||
.{ "s.selectedIndex", "-1" },
|
||||
|
||||
.{ "s.selectedIndex = 0", null },
|
||||
.{ "s.selectedIndex", "0" },
|
||||
|
||||
.{ "s.selectedIndex = 1", null },
|
||||
.{ "s.selectedIndex", "1" },
|
||||
|
||||
.{ "s.selectedIndex = -323", null },
|
||||
.{ "s.selectedIndex", "-1" },
|
||||
|
||||
.{ "let options = s.options", null },
|
||||
.{ "options.length", "2" },
|
||||
.{ "options.item(1).value", "o2" },
|
||||
.{ "options.selectedIndex", "-1" },
|
||||
|
||||
.{ "let o3 = document.createElement('option');", null },
|
||||
.{ "o3.value = 'o3';", null },
|
||||
.{ "options.add(o3)", null },
|
||||
.{ "options.length", "3" },
|
||||
.{ "options.item(2).value", "o3" },
|
||||
|
||||
.{ "let o4 = document.createElement('option');", null },
|
||||
.{ "o4.value = 'o4';", null },
|
||||
.{ "options.add(o4, 1)", null },
|
||||
.{ "options.length", "4" },
|
||||
.{ "options.item(1).value", "o4" },
|
||||
|
||||
.{ "let o5 = document.createElement('option');", null },
|
||||
.{ "o5.value = 'o5';", null },
|
||||
.{ "options.add(o5, o3)", null },
|
||||
.{ "options.length", "5" },
|
||||
.{ "options.item(3).value", "o5" },
|
||||
|
||||
.{ "options.remove(3)", null },
|
||||
.{ "options.length", "4" },
|
||||
.{ "options.item(3).value", "o3" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -18,10 +18,10 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const Callback = @import("../env.zig").Callback;
|
||||
const SessionState = @import("../env.zig").SessionState;
|
||||
const Loop = @import("../../runtime/loop.zig").Loop;
|
||||
|
||||
const Navigator = @import("navigator.zig").Navigator;
|
||||
const History = @import("history.zig").History;
|
||||
@@ -30,63 +30,52 @@ const Crypto = @import("../crypto/crypto.zig").Crypto;
|
||||
const Console = @import("../console/console.zig").Console;
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const MediaQueryList = @import("media_query_list.zig").MediaQueryList;
|
||||
const Performance = @import("../dom/performance.zig").Performance;
|
||||
const CSSStyleDeclaration = @import("../cssom/CSSStyleDeclaration.zig");
|
||||
const Screen = @import("screen.zig").Screen;
|
||||
const domcss = @import("../dom/css.zig");
|
||||
const Css = @import("../css/css.zig").Css;
|
||||
|
||||
const Function = Env.Function;
|
||||
const JsObject = Env.JsObject;
|
||||
const Performance = @import("performance.zig").Performance;
|
||||
|
||||
const storage = @import("../storage/storage.zig");
|
||||
|
||||
const log = std.log.scoped(.window);
|
||||
|
||||
// https://dom.spec.whatwg.org/#interface-window-extensions
|
||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
|
||||
pub const Window = struct {
|
||||
pub const prototype = *EventTarget;
|
||||
|
||||
// Extend libdom event target for pure zig struct.
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .window },
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{},
|
||||
|
||||
document: *parser.DocumentHTML,
|
||||
document: ?*parser.DocumentHTML = null,
|
||||
target: []const u8 = "",
|
||||
history: History = .{},
|
||||
location: Location = .{},
|
||||
storage_shelf: ?*storage.Shelf = null,
|
||||
|
||||
// counter for having unique timer ids
|
||||
timer_id: u30 = 0,
|
||||
timers: std.AutoHashMapUnmanaged(u32, void) = .{},
|
||||
timer_id: u31 = 0,
|
||||
timers: std.AutoHashMapUnmanaged(u32, *TimerCallback) = .{},
|
||||
|
||||
crypto: Crypto = .{},
|
||||
console: Console = .{},
|
||||
navigator: Navigator = .{},
|
||||
performance: Performance,
|
||||
screen: Screen = .{},
|
||||
css: Css = .{},
|
||||
|
||||
pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window {
|
||||
var fbs = std.io.fixedBufferStream("");
|
||||
const html_doc = try parser.documentHTMLParse(fbs.reader(), "utf-8");
|
||||
const doc = parser.documentHTMLToDocument(html_doc);
|
||||
try parser.documentSetDocumentURI(doc, "about:blank");
|
||||
|
||||
return .{
|
||||
.document = html_doc,
|
||||
.target = target orelse "",
|
||||
.navigator = navigator orelse .{},
|
||||
.performance = Performance.init(),
|
||||
.performance = .{ .time_origin = try std.time.Timer.start() },
|
||||
};
|
||||
}
|
||||
|
||||
pub fn replaceLocation(self: *Window, loc: Location) !void {
|
||||
self.location = loc;
|
||||
try parser.documentHTMLSetLocation(Location, self.document, &self.location);
|
||||
if (self.document) |doc| {
|
||||
try parser.documentHTMLSetLocation(Location, doc, &self.location);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) !void {
|
||||
self.performance.reset(); // When to reset see: https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin
|
||||
self.performance.time_origin.reset(); // When to reset see: https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin
|
||||
self.document = doc;
|
||||
try parser.documentHTMLSetLocation(Location, doc, &self.location);
|
||||
}
|
||||
@@ -107,10 +96,6 @@ pub const Window = struct {
|
||||
return &self.location;
|
||||
}
|
||||
|
||||
pub fn set_location(_: *const Window, url: []const u8, page: *Page) !void {
|
||||
return page.navigateFromWebAPI(url, .{ .reason = .script });
|
||||
}
|
||||
|
||||
pub fn get_console(self: *Window) *Console {
|
||||
return &self.console;
|
||||
}
|
||||
@@ -127,47 +112,6 @@ pub const Window = struct {
|
||||
return self;
|
||||
}
|
||||
|
||||
// frames return the window itself, but accessing it via a pseudo
|
||||
// array returns the Window object corresponding to the given frame or
|
||||
// iframe.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Window/frames
|
||||
pub fn get_frames(self: *Window) *Window {
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn indexed_get(self: *Window, index: u32, has_value: *bool, page: *Page) !*Window {
|
||||
const frames = try domcss.querySelectorAll(
|
||||
page.call_arena,
|
||||
parser.documentHTMLToNode(self.document),
|
||||
"iframe",
|
||||
);
|
||||
|
||||
if (index >= frames.nodes.items.len) {
|
||||
has_value.* = false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
has_value.* = true;
|
||||
// TODO return the correct frame's window
|
||||
// frames.nodes.items[indexed]
|
||||
return error.TODO;
|
||||
}
|
||||
|
||||
// Retrieve the numbre of frames/iframes from the DOM dynamically.
|
||||
pub fn get_length(self: *const Window, page: *Page) !u32 {
|
||||
const frames = try domcss.querySelectorAll(
|
||||
page.call_arena,
|
||||
parser.documentHTMLToNode(self.document),
|
||||
"iframe",
|
||||
);
|
||||
|
||||
return frames.get_length();
|
||||
}
|
||||
|
||||
pub fn get_top(self: *Window) *Window {
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn get_document(self: *Window) ?*parser.DocumentHTML {
|
||||
return self.document;
|
||||
}
|
||||
@@ -177,15 +121,15 @@ pub const Window = struct {
|
||||
}
|
||||
|
||||
// The interior height of the window in pixels, including the height of the horizontal scroll bar, if present.
|
||||
pub fn get_innerHeight(_: *Window, page: *Page) u32 {
|
||||
pub fn get_innerHeight(_: *Window, state: *SessionState) u32 {
|
||||
// We do not have scrollbars or padding so this is the same as Element.clientHeight
|
||||
return page.renderer.height();
|
||||
return state.renderer.height();
|
||||
}
|
||||
|
||||
// The interior width of the window in pixels. That includes the width of the vertical scroll bar, if one is present.
|
||||
pub fn get_innerWidth(_: *Window, page: *Page) u32 {
|
||||
pub fn get_innerWidth(_: *Window, state: *SessionState) u32 {
|
||||
// We do not have scrollbars or padding so this is the same as Element.clientWidth
|
||||
return page.renderer.width();
|
||||
return state.renderer.width();
|
||||
}
|
||||
|
||||
pub fn get_name(self: *Window) []const u8 {
|
||||
@@ -206,237 +150,166 @@ pub const Window = struct {
|
||||
return &self.performance;
|
||||
}
|
||||
|
||||
pub fn get_screen(self: *Window) *Screen {
|
||||
return &self.screen;
|
||||
// Tells the browser you wish to perform an animation. It requests the browser to call a user-supplied callback function before the next repaint.
|
||||
// fn callback(timestamp: f64)
|
||||
// Returns the request ID, that uniquely identifies the entry in the callback list.
|
||||
pub fn _requestAnimationFrame(
|
||||
self: *Window,
|
||||
callback: Callback,
|
||||
) !u32 {
|
||||
// We immediately execute the callback, but this may not be correct TBD.
|
||||
// Since: When multiple callbacks queued by requestAnimationFrame() begin to fire in a single frame, each receives the same timestamp even though time has passed during the computation of every previous callback's workload.
|
||||
var result: Callback.Result = undefined;
|
||||
callback.tryCall(.{self.performance._now()}, &result) catch {
|
||||
log.err("Window.requestAnimationFrame(): {s}", .{result.exception});
|
||||
log.debug("stack:\n{s}", .{result.stack orelse "???"});
|
||||
};
|
||||
return 99; // not unique, but user cannot make assumptions about it. cancelAnimationFrame will be too late anyway.
|
||||
}
|
||||
|
||||
pub fn get_CSS(self: *Window) *Css {
|
||||
return &self.css;
|
||||
// Cancels an animation frame request previously scheduled through requestAnimationFrame().
|
||||
// This is a no-op since _requestAnimationFrame immediately executes the callback.
|
||||
pub fn _cancelAnimationFrame(_: *Window, request_id: u32) void {
|
||||
_ = request_id;
|
||||
}
|
||||
|
||||
pub fn _requestAnimationFrame(self: *Window, cbk: Function, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, 5, page, .{
|
||||
.animation_frame = true,
|
||||
.name = "animationFrame",
|
||||
.low_priority = true,
|
||||
});
|
||||
// TODO handle callback arguments.
|
||||
pub fn _setTimeout(self: *Window, cbk: Callback, delay: ?u32, state: *SessionState) !u32 {
|
||||
return self.createTimeout(cbk, delay, state, false);
|
||||
}
|
||||
|
||||
pub fn _cancelAnimationFrame(self: *Window, id: u32) !void {
|
||||
_ = self.timers.remove(id);
|
||||
// TODO handle callback arguments.
|
||||
pub fn _setInterval(self: *Window, cbk: Callback, delay: ?u32, state: *SessionState) !u32 {
|
||||
return self.createTimeout(cbk, delay, state, true);
|
||||
}
|
||||
|
||||
pub fn _setTimeout(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, delay, page, .{ .args = params, .name = "setTimeout" });
|
||||
pub fn _clearTimeout(self: *Window, id: u32, state: *SessionState) !void {
|
||||
const kv = self.timers.fetchRemove(id) orelse return;
|
||||
try state.loop.cancel(kv.value.loop_id);
|
||||
}
|
||||
|
||||
pub fn _setInterval(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, delay, page, .{ .repeat = true, .args = params, .name = "setInterval" });
|
||||
pub fn _clearInterval(self: *Window, id: u32, state: *SessionState) !void {
|
||||
const kv = self.timers.fetchRemove(id) orelse return;
|
||||
try state.loop.cancel(kv.value.loop_id);
|
||||
}
|
||||
|
||||
pub fn _clearTimeout(self: *Window, id: u32) !void {
|
||||
_ = self.timers.remove(id);
|
||||
}
|
||||
|
||||
pub fn _clearInterval(self: *Window, id: u32) !void {
|
||||
_ = self.timers.remove(id);
|
||||
}
|
||||
|
||||
pub fn _queueMicrotask(self: *Window, cbk: Function, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, 0, page, .{ .name = "queueMicrotask" });
|
||||
}
|
||||
|
||||
pub fn _matchMedia(_: *const Window, media: []const u8, page: *Page) !MediaQueryList {
|
||||
pub fn _matchMedia(_: *const Window, media: []const u8, state: *SessionState) !MediaQueryList {
|
||||
return .{
|
||||
.matches = false, // TODO?
|
||||
.media = try page.arena.dupe(u8, media),
|
||||
.media = try state.arena.dupe(u8, media),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _btoa(_: *const Window, value: []const u8, page: *Page) ![]const u8 {
|
||||
const Encoder = std.base64.standard.Encoder;
|
||||
const out = try page.call_arena.alloc(u8, Encoder.calcSize(value.len));
|
||||
return Encoder.encode(out, value);
|
||||
}
|
||||
|
||||
pub fn _atob(_: *const Window, value: []const u8, page: *Page) ![]const u8 {
|
||||
const Decoder = std.base64.standard.Decoder;
|
||||
const size = Decoder.calcSizeForSlice(value) catch return error.InvalidCharacterError;
|
||||
|
||||
const out = try page.call_arena.alloc(u8, size);
|
||||
Decoder.decode(out, value) catch return error.InvalidCharacterError;
|
||||
return out;
|
||||
}
|
||||
|
||||
const CreateTimeoutOpts = struct {
|
||||
name: []const u8,
|
||||
args: []Env.JsObject = &.{},
|
||||
repeat: bool = false,
|
||||
animation_frame: bool = false,
|
||||
low_priority: bool = false,
|
||||
};
|
||||
fn createTimeout(self: *Window, cbk: Function, delay_: ?u32, page: *Page, opts: CreateTimeoutOpts) !u32 {
|
||||
const delay = delay_ orelse 0;
|
||||
fn createTimeout(self: *Window, cbk: Callback, delay_: ?u32, state: *SessionState, comptime repeat: bool) !u32 {
|
||||
if (self.timers.count() > 512) {
|
||||
return error.TooManyTimeout;
|
||||
}
|
||||
const timer_id = self.timer_id +% 1;
|
||||
self.timer_id = timer_id;
|
||||
|
||||
const arena = page.arena;
|
||||
const arena = state.arena;
|
||||
|
||||
const gop = try self.timers.getOrPut(arena, timer_id);
|
||||
if (gop.found_existing) {
|
||||
// this can only happen if we've created 2^31 timeouts.
|
||||
return error.TooManyTimeout;
|
||||
} else {
|
||||
gop.value_ptr.* = {};
|
||||
}
|
||||
errdefer _ = self.timers.remove(timer_id);
|
||||
|
||||
const args = opts.args;
|
||||
var persisted_args: []Env.JsObject = &.{};
|
||||
if (args.len > 0) {
|
||||
persisted_args = try page.arena.alloc(Env.JsObject, args.len);
|
||||
for (args, persisted_args) |a, *ca| {
|
||||
ca.* = try a.persist();
|
||||
}
|
||||
}
|
||||
|
||||
const delay: u63 = @as(u63, (delay_ orelse 0)) * std.time.ns_per_ms;
|
||||
const callback = try arena.create(TimerCallback);
|
||||
|
||||
callback.* = .{
|
||||
.cbk = cbk,
|
||||
.loop_id = 0, // we're going to set this to a real value shortly
|
||||
.window = self,
|
||||
.timer_id = timer_id,
|
||||
.args = persisted_args,
|
||||
.animation_frame = opts.animation_frame,
|
||||
// setting a repeat time of 0 is illegal, doing + 1 is a simple way to avoid that
|
||||
.repeat = if (opts.repeat) delay + 1 else null,
|
||||
.node = .{ .func = TimerCallback.run },
|
||||
.repeat = if (repeat) delay else null,
|
||||
};
|
||||
callback.loop_id = try state.loop.timeout(delay, &callback.node);
|
||||
|
||||
try page.scheduler.add(callback, TimerCallback.run, delay, .{
|
||||
.name = opts.name,
|
||||
.low_priority = opts.low_priority,
|
||||
});
|
||||
|
||||
gop.value_ptr.* = callback;
|
||||
return timer_id;
|
||||
}
|
||||
|
||||
// TODO: getComputedStyle should return a read-only CSSStyleDeclaration.
|
||||
// We currently don't have a read-only one, so we return a new instance on
|
||||
// each call.
|
||||
pub fn _getComputedStyle(_: *const Window, element: *parser.Element, pseudo_element: ?[]const u8) !CSSStyleDeclaration {
|
||||
_ = element;
|
||||
_ = pseudo_element;
|
||||
return .empty;
|
||||
}
|
||||
|
||||
const ScrollToOpts = union(enum) {
|
||||
x: i32,
|
||||
opts: Opts,
|
||||
|
||||
const Opts = struct {
|
||||
top: i32,
|
||||
left: i32,
|
||||
behavior: []const u8,
|
||||
};
|
||||
};
|
||||
pub fn _scrollTo(self: *Window, opts: ScrollToOpts, y: ?u32) !void {
|
||||
_ = opts;
|
||||
_ = y;
|
||||
|
||||
{
|
||||
const scroll_event = try parser.eventCreate();
|
||||
defer parser.eventDestroy(scroll_event);
|
||||
|
||||
try parser.eventInit(scroll_event, "scroll", .{});
|
||||
_ = try parser.eventTargetDispatchEvent(
|
||||
parser.toEventTarget(Window, self),
|
||||
scroll_event,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const scroll_end = try parser.eventCreate();
|
||||
defer parser.eventDestroy(scroll_end);
|
||||
|
||||
try parser.eventInit(scroll_end, "scrollend", .{});
|
||||
_ = try parser.eventTargetDispatchEvent(
|
||||
parser.toEventTarget(parser.DocumentHTML, self.document),
|
||||
scroll_end,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// libdom's document doesn't have a parent, which is correct, but
|
||||
// breaks the event bubbling that happens for many events from
|
||||
// document -> window.
|
||||
// We need to force dispatch this event on the window, with the
|
||||
// document target.
|
||||
// In theory, we should do this for a lot of events and might need
|
||||
// to come up with a good way to solve this more generically. But
|
||||
// this specific event, and maybe a few others in the near future,
|
||||
// are blockers.
|
||||
// Worth noting that NetSurf itself appears to do something similar:
|
||||
// https://github.com/netsurf-browser/netsurf/blob/a32e1a03e1c91ee9f0aa211937dbae7a96831149/content/handlers/html/html.c#L380
|
||||
pub fn dispatchForDocumentTarget(self: *Window, evt: *parser.Event) !void {
|
||||
// we assume that this evt has already been dispatched on the document
|
||||
// and thus the target has already been set to the document.
|
||||
return self.base.redispatchEvent(evt);
|
||||
}
|
||||
};
|
||||
|
||||
const TimerCallback = struct {
|
||||
// the internal loop id, need it when cancelling
|
||||
loop_id: usize,
|
||||
|
||||
// the id of our timer (windows.timers key)
|
||||
timer_id: u31,
|
||||
|
||||
// if false, we'll remove the timer_id from the window.timers lookup on run
|
||||
repeat: ?u32,
|
||||
|
||||
// The JavaScript callback to execute
|
||||
cbk: Function,
|
||||
cbk: Callback,
|
||||
|
||||
animation_frame: bool = false,
|
||||
// This is the internal data that the event loop tracks. We'll get this
|
||||
// back in run and, from it, can get our TimerCallback instance
|
||||
node: Loop.CallbackNode = undefined,
|
||||
|
||||
// if the event should be repeated
|
||||
repeat: ?u63 = null,
|
||||
|
||||
window: *Window,
|
||||
|
||||
args: []Env.JsObject = &.{},
|
||||
fn run(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
|
||||
const self: *TimerCallback = @fieldParentPtr("node", node);
|
||||
|
||||
fn run(ctx: *anyopaque) ?u32 {
|
||||
const self: *TimerCallback = @ptrCast(@alignCast(ctx));
|
||||
if (self.repeat != null) {
|
||||
if (self.window.timers.contains(self.timer_id) == false) {
|
||||
// it was called
|
||||
return null;
|
||||
}
|
||||
} else if (self.window.timers.remove(self.timer_id) == false) {
|
||||
// it was cancelled
|
||||
return null;
|
||||
}
|
||||
|
||||
var result: Function.Result = undefined;
|
||||
|
||||
var call: anyerror!void = undefined;
|
||||
if (self.animation_frame) {
|
||||
call = self.cbk.tryCall(void, .{self.window.performance._now()}, &result);
|
||||
} else {
|
||||
call = self.cbk.tryCall(void, self.args, &result);
|
||||
}
|
||||
|
||||
call catch {
|
||||
log.warn(.user_script, "callback error", .{
|
||||
.err = result.exception,
|
||||
.stack = result.stack,
|
||||
.source = "window timeout",
|
||||
});
|
||||
var result: Callback.Result = undefined;
|
||||
self.cbk.tryCall(.{}, &result) catch {
|
||||
log.err("timeout callback error: {s}", .{result.exception});
|
||||
log.debug("stack:\n{s}", .{result.stack orelse "???"});
|
||||
};
|
||||
|
||||
return self.repeat;
|
||||
if (self.repeat) |r| {
|
||||
// setInterval
|
||||
repeat_delay.* = r;
|
||||
return;
|
||||
}
|
||||
|
||||
// setTimeout
|
||||
_ = self.window.timers.remove(self.timer_id);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: Window" {
|
||||
try testing.htmlRunner("window/window.html");
|
||||
try testing.htmlRunner("window/frames.html");
|
||||
test "Browser.HTML.Window" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
// requestAnimationFrame should be able to wait by recursively calling itself
|
||||
// Note however that we in this test do not wait as the request is just send to the browser
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ let start;
|
||||
\\ function step(timestamp) {
|
||||
\\ if (start === undefined) {
|
||||
\\ start = timestamp;
|
||||
\\ }
|
||||
\\ const elapsed = timestamp - start;
|
||||
\\ if (elapsed < 2000) {
|
||||
\\ requestAnimationFrame(step);
|
||||
\\ }
|
||||
\\ }
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "requestAnimationFrame(step);", null }, // returned id is checked in the next test
|
||||
}, .{});
|
||||
|
||||
// cancelAnimationFrame should be able to cancel a request with the given id
|
||||
try runner.testCases(&.{
|
||||
.{ "let request_id = requestAnimationFrame(timestamp => {});", null },
|
||||
.{ "cancelAnimationFrame(request_id);", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "innerHeight", "1" },
|
||||
.{ "innerWidth", "1" }, // Width is 1 even if there are no elements
|
||||
.{ "document.createElement('div').getClientRects()", null },
|
||||
.{ "document.createElement('div').getClientRects()", null },
|
||||
.{ "innerHeight", "1" },
|
||||
.{ "innerWidth", "2" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -1,284 +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");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
// Used by FormDAta and URLSearchParams.
|
||||
//
|
||||
// We store the values in an ArrayList rather than a an
|
||||
// StringArrayHashMap([]const u8) because of the way the iterators (i.e., keys(),
|
||||
// values() and entries()) work. The FormData can contain duplicate keys, and
|
||||
// each iteration yields 1 key=>value pair. So, given:
|
||||
//
|
||||
// let f = new FormData();
|
||||
// f.append('a', '1');
|
||||
// f.append('a', '2');
|
||||
//
|
||||
// Then we'd expect f.keys(), f.values() and f.entries() to yield 2 results:
|
||||
// ['a', '1']
|
||||
// ['a', '2']
|
||||
//
|
||||
// This is much easier to do with an ArrayList than a HashMap, especially given
|
||||
// that the FormData could be mutated while iterating.
|
||||
// The downside is that most of the normal operations are O(N).
|
||||
pub const List = struct {
|
||||
entries: std.ArrayListUnmanaged(KeyValue) = .{},
|
||||
|
||||
pub fn init(entries: std.ArrayListUnmanaged(KeyValue)) List {
|
||||
return .{ .entries = entries };
|
||||
}
|
||||
|
||||
pub fn clone(self: *const List, arena: Allocator) !List {
|
||||
const entries = self.entries.items;
|
||||
|
||||
var c: std.ArrayListUnmanaged(KeyValue) = .{};
|
||||
try c.ensureTotalCapacity(arena, entries.len);
|
||||
for (entries) |kv| {
|
||||
c.appendAssumeCapacity(kv);
|
||||
}
|
||||
|
||||
return .{ .entries = c };
|
||||
}
|
||||
|
||||
pub fn fromOwnedSlice(entries: []KeyValue) List {
|
||||
return .{
|
||||
.entries = std.ArrayListUnmanaged(KeyValue).fromOwnedSlice(entries),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn count(self: *const List) usize {
|
||||
return self.entries.items.len;
|
||||
}
|
||||
|
||||
pub fn get(self: *const List, key: []const u8) ?[]const u8 {
|
||||
const result = self.find(key) orelse return null;
|
||||
return result.entry.value;
|
||||
}
|
||||
|
||||
pub fn getAll(self: *const List, arena: Allocator, key: []const u8) ![]const []const u8 {
|
||||
var arr: std.ArrayListUnmanaged([]const u8) = .empty;
|
||||
for (self.entries.items) |entry| {
|
||||
if (std.mem.eql(u8, key, entry.key)) {
|
||||
try arr.append(arena, entry.value);
|
||||
}
|
||||
}
|
||||
return arr.items;
|
||||
}
|
||||
|
||||
pub fn has(self: *const List, key: []const u8) bool {
|
||||
return self.find(key) != null;
|
||||
}
|
||||
|
||||
pub fn set(self: *List, arena: Allocator, key: []const u8, value: []const u8) !void {
|
||||
self.delete(key);
|
||||
return self.append(arena, key, value);
|
||||
}
|
||||
|
||||
pub fn append(self: *List, arena: Allocator, key: []const u8, value: []const u8) !void {
|
||||
return self.appendOwned(arena, try arena.dupe(u8, key), try arena.dupe(u8, value));
|
||||
}
|
||||
|
||||
pub fn appendOwned(self: *List, arena: Allocator, key: []const u8, value: []const u8) !void {
|
||||
return self.entries.append(arena, .{
|
||||
.key = key,
|
||||
.value = value,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn appendOwnedAssumeCapacity(self: *List, key: []const u8, value: []const u8) void {
|
||||
self.entries.appendAssumeCapacity(.{
|
||||
.key = key,
|
||||
.value = value,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn delete(self: *List, key: []const u8) void {
|
||||
var i: usize = 0;
|
||||
while (i < self.entries.items.len) {
|
||||
const entry = self.entries.items[i];
|
||||
if (std.mem.eql(u8, key, entry.key)) {
|
||||
_ = self.entries.swapRemove(i);
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deleteKeyValue(self: *List, key: []const u8, value: []const u8) void {
|
||||
var i: usize = 0;
|
||||
while (i < self.entries.items.len) {
|
||||
const entry = self.entries.items[i];
|
||||
if (std.mem.eql(u8, key, entry.key) and std.mem.eql(u8, value, entry.value)) {
|
||||
_ = self.entries.swapRemove(i);
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn keyIterator(self: *const List) KeyIterator {
|
||||
return .{ .entries = &self.entries };
|
||||
}
|
||||
|
||||
pub fn valueIterator(self: *const List) ValueIterator {
|
||||
return .{ .entries = &self.entries };
|
||||
}
|
||||
|
||||
pub fn entryIterator(self: *const List) EntryIterator {
|
||||
return .{ .entries = &self.entries };
|
||||
}
|
||||
|
||||
pub fn ensureTotalCapacity(self: *List, arena: Allocator, len: usize) !void {
|
||||
return self.entries.ensureTotalCapacity(arena, len);
|
||||
}
|
||||
|
||||
const FindResult = struct {
|
||||
index: usize,
|
||||
entry: KeyValue,
|
||||
};
|
||||
|
||||
fn find(self: *const List, key: []const u8) ?FindResult {
|
||||
for (self.entries.items, 0..) |entry, i| {
|
||||
if (std.mem.eql(u8, key, entry.key)) {
|
||||
return .{ .index = i, .entry = entry };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
pub const KeyValue = struct {
|
||||
key: []const u8,
|
||||
value: []const u8,
|
||||
};
|
||||
|
||||
pub const KeyIterator = struct {
|
||||
index: usize = 0,
|
||||
entries: *const std.ArrayListUnmanaged(KeyValue),
|
||||
|
||||
pub fn _next(self: *KeyIterator) ?[]const u8 {
|
||||
const entries = self.entries.items;
|
||||
|
||||
const index = self.index;
|
||||
if (index == entries.len) {
|
||||
return null;
|
||||
}
|
||||
self.index += 1;
|
||||
return entries[index].key;
|
||||
}
|
||||
};
|
||||
|
||||
pub const ValueIterator = struct {
|
||||
index: usize = 0,
|
||||
entries: *const std.ArrayListUnmanaged(KeyValue),
|
||||
|
||||
pub fn _next(self: *ValueIterator) ?[]const u8 {
|
||||
const entries = self.entries.items;
|
||||
|
||||
const index = self.index;
|
||||
if (index == entries.len) {
|
||||
return null;
|
||||
}
|
||||
self.index += 1;
|
||||
return entries[index].value;
|
||||
}
|
||||
};
|
||||
|
||||
pub const EntryIterator = struct {
|
||||
index: usize = 0,
|
||||
entries: *const std.ArrayListUnmanaged(KeyValue),
|
||||
|
||||
pub fn _next(self: *EntryIterator) ?struct { []const u8, []const u8 } {
|
||||
const entries = self.entries.items;
|
||||
|
||||
const index = self.index;
|
||||
if (index == entries.len) {
|
||||
return null;
|
||||
}
|
||||
self.index += 1;
|
||||
const entry = entries[index];
|
||||
return .{ entry.key, entry.value };
|
||||
}
|
||||
};
|
||||
|
||||
const URLEncodeMode = enum {
|
||||
form,
|
||||
query,
|
||||
};
|
||||
|
||||
pub fn urlEncode(list: List, mode: URLEncodeMode, writer: anytype) !void {
|
||||
const entries = list.entries.items;
|
||||
if (entries.len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try urlEncodeEntry(entries[0], mode, writer);
|
||||
for (entries[1..]) |entry| {
|
||||
try writer.writeByte('&');
|
||||
try urlEncodeEntry(entry, mode, writer);
|
||||
}
|
||||
}
|
||||
|
||||
fn urlEncodeEntry(entry: KeyValue, mode: URLEncodeMode, writer: anytype) !void {
|
||||
try urlEncodeValue(entry.key, mode, writer);
|
||||
|
||||
// for a form, for an empty value, we'll do "spice="
|
||||
// but for a query, we do "spice"
|
||||
if (mode == .query and entry.value.len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try writer.writeByte('=');
|
||||
try urlEncodeValue(entry.value, mode, writer);
|
||||
}
|
||||
|
||||
fn urlEncodeValue(value: []const u8, mode: URLEncodeMode, writer: anytype) !void {
|
||||
if (!urlEncodeShouldEscape(value, mode)) {
|
||||
return writer.writeAll(value);
|
||||
}
|
||||
|
||||
for (value) |b| {
|
||||
if (urlEncodeUnreserved(b, mode)) {
|
||||
try writer.writeByte(b);
|
||||
} else if (b == ' ' and mode == .form) {
|
||||
// for form submission, space should be encoded as '+', not '%20'
|
||||
try writer.writeByte('+');
|
||||
} else {
|
||||
try writer.print("%{X:0>2}", .{b});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn urlEncodeShouldEscape(value: []const u8, mode: URLEncodeMode) bool {
|
||||
for (value) |b| {
|
||||
if (!urlEncodeUnreserved(b, mode)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn urlEncodeUnreserved(b: u8, mode: URLEncodeMode) bool {
|
||||
return switch (b) {
|
||||
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_' => true,
|
||||
'~' => mode == .query,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
@@ -20,7 +20,6 @@
|
||||
// management.
|
||||
// We replace the libdom default usage of allocations with mimalloc heap
|
||||
// allocation to be able to free all memory used at once, like an arena usage.
|
||||
const std = @import("std");
|
||||
|
||||
const c = @cImport({
|
||||
@cInclude("mimalloc.h");
|
||||
@@ -34,43 +33,43 @@ const Error = error{
|
||||
var heap: ?*c.mi_heap_t = null;
|
||||
|
||||
pub fn create() Error!void {
|
||||
std.debug.assert(heap == null);
|
||||
if (heap != null) return Error.HeapNotNull;
|
||||
heap = c.mi_heap_new();
|
||||
std.debug.assert(heap != null);
|
||||
if (heap == null) return Error.HeapNull;
|
||||
}
|
||||
|
||||
pub fn destroy() void {
|
||||
std.debug.assert(heap != null);
|
||||
if (heap == null) return;
|
||||
c.mi_heap_destroy(heap.?);
|
||||
heap = null;
|
||||
}
|
||||
|
||||
pub export fn m_alloc(size: usize) callconv(.c) ?*anyopaque {
|
||||
std.debug.assert(heap != null);
|
||||
pub export fn m_alloc(size: usize) callconv(.C) ?*anyopaque {
|
||||
if (heap == null) return null;
|
||||
return c.mi_heap_malloc(heap.?, size);
|
||||
}
|
||||
|
||||
pub export fn re_alloc(ptr: ?*anyopaque, size: usize) callconv(.c) ?*anyopaque {
|
||||
std.debug.assert(heap != null);
|
||||
pub export fn re_alloc(ptr: ?*anyopaque, size: usize) callconv(.C) ?*anyopaque {
|
||||
if (heap == null) return null;
|
||||
return c.mi_heap_realloc(heap.?, ptr, size);
|
||||
}
|
||||
|
||||
pub export fn c_alloc(nmemb: usize, size: usize) callconv(.c) ?*anyopaque {
|
||||
std.debug.assert(heap != null);
|
||||
pub export fn c_alloc(nmemb: usize, size: usize) callconv(.C) ?*anyopaque {
|
||||
if (heap == null) return null;
|
||||
return c.mi_heap_calloc(heap.?, nmemb, size);
|
||||
}
|
||||
|
||||
pub export fn str_dup(s: [*c]const u8) callconv(.c) [*c]u8 {
|
||||
std.debug.assert(heap != null);
|
||||
pub export fn str_dup(s: [*c]const u8) callconv(.C) [*c]u8 {
|
||||
if (heap == null) return null;
|
||||
return c.mi_heap_strdup(heap.?, s);
|
||||
}
|
||||
|
||||
pub export fn strn_dup(s: [*c]const u8, size: usize) callconv(.c) [*c]u8 {
|
||||
std.debug.assert(heap != null);
|
||||
pub export fn strn_dup(s: [*c]const u8, size: usize) callconv(.C) [*c]u8 {
|
||||
if (heap == null) return null;
|
||||
return c.mi_heap_strndup(heap.?, s, size);
|
||||
}
|
||||
|
||||
// NOOP, use destroy to clear all the memory allocated at once.
|
||||
pub export fn f_ree(_: ?*anyopaque) callconv(.c) void {
|
||||
pub export fn f_ree(_: ?*anyopaque) callconv(.C) void {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -22,11 +22,11 @@ const Allocator = std.mem.Allocator;
|
||||
pub const Mime = struct {
|
||||
content_type: ContentType,
|
||||
params: []const u8 = "",
|
||||
charset: ?[:0]const u8 = null,
|
||||
charset: ?[]const u8 = null,
|
||||
|
||||
pub const unknown = Mime{
|
||||
.params = "",
|
||||
.charset = null,
|
||||
.charset = "",
|
||||
.content_type = .{ .unknown = {} },
|
||||
};
|
||||
|
||||
@@ -35,8 +35,6 @@ pub const Mime = struct {
|
||||
text_html,
|
||||
text_javascript,
|
||||
text_plain,
|
||||
text_css,
|
||||
application_json,
|
||||
unknown,
|
||||
other,
|
||||
};
|
||||
@@ -46,13 +44,11 @@ pub const Mime = struct {
|
||||
text_html: void,
|
||||
text_javascript: void,
|
||||
text_plain: void,
|
||||
text_css: void,
|
||||
application_json: void,
|
||||
unknown: void,
|
||||
other: struct { type: []const u8, sub_type: []const u8 },
|
||||
};
|
||||
|
||||
pub fn parse(input: []u8) !Mime {
|
||||
pub fn parse(arena: Allocator, input: []u8) !Mime {
|
||||
if (input.len > 255) {
|
||||
return error.TooBig;
|
||||
}
|
||||
@@ -69,7 +65,7 @@ pub const Mime = struct {
|
||||
|
||||
const params = trimLeft(normalized[type_len..]);
|
||||
|
||||
var charset: ?[:0]const u8 = null;
|
||||
var charset: ?[]const u8 = null;
|
||||
|
||||
var it = std.mem.splitScalar(u8, params, ';');
|
||||
while (it.next()) |attr| {
|
||||
@@ -86,37 +82,7 @@ pub const Mime = struct {
|
||||
}, name) orelse continue;
|
||||
|
||||
switch (attribute_name) {
|
||||
.charset => {
|
||||
// We used to have a proper value parser, but we currently
|
||||
// only care about the charset attribute, plus only about
|
||||
// the UTF-8 value. It's a lot easier to do it this way,
|
||||
// and it doesn't require an allocation to (a) unescape the
|
||||
// value or (b) ensure the correct lifetime.
|
||||
if (value.len == 0) {
|
||||
break;
|
||||
}
|
||||
var attribute_value = value;
|
||||
if (value[0] == '"') {
|
||||
if (value.len < 3 or value[value.len - 1] != '"') {
|
||||
return error.Invalid;
|
||||
}
|
||||
attribute_value = value[1 .. value.len - 1];
|
||||
}
|
||||
|
||||
if (std.ascii.eqlIgnoreCase(attribute_value, "utf-8")) {
|
||||
charset = "UTF-8";
|
||||
} else if (std.ascii.eqlIgnoreCase(attribute_value, "iso-8859-1")) {
|
||||
charset = "ISO-8859-1";
|
||||
} else {
|
||||
// we only care about null (which we default to UTF-8)
|
||||
// or UTF-8. If this is actually set (i.e. not null)
|
||||
// and isn't UTF-8, we'll just put a dummy value. If
|
||||
// we want to capture the actual value, we'll need to
|
||||
// dupe/allocate it. Since, for now, we don't need that
|
||||
// we can avoid the allocation.
|
||||
charset = "lightpanda:UNSUPPORTED";
|
||||
}
|
||||
},
|
||||
.charset => charset = try parseAttributeValue(arena, value),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,22 +174,18 @@ pub const Mime = struct {
|
||||
if (std.meta.stringToEnum(enum {
|
||||
@"text/xml",
|
||||
@"text/html",
|
||||
@"text/css",
|
||||
@"text/plain",
|
||||
|
||||
@"text/javascript",
|
||||
@"application/javascript",
|
||||
@"application/x-javascript",
|
||||
|
||||
@"application/json",
|
||||
@"text/plain",
|
||||
}, type_name)) |known_type| {
|
||||
const ct: ContentType = switch (known_type) {
|
||||
.@"text/xml" => .{ .text_xml = {} },
|
||||
.@"text/html" => .{ .text_html = {} },
|
||||
.@"text/javascript", .@"application/javascript", .@"application/x-javascript" => .{ .text_javascript = {} },
|
||||
.@"text/plain" => .{ .text_plain = {} },
|
||||
.@"text/css" => .{ .text_css = {} },
|
||||
.@"application/json" => .{ .application_json = {} },
|
||||
};
|
||||
return .{ ct, attribute_start };
|
||||
}
|
||||
@@ -254,6 +216,56 @@ pub const Mime = struct {
|
||||
break :blk v;
|
||||
};
|
||||
|
||||
fn parseAttributeValue(arena: Allocator, value: []const u8) ![]const u8 {
|
||||
if (value[0] != '"') {
|
||||
return value;
|
||||
}
|
||||
|
||||
// 1 to skip the opening quote
|
||||
var value_pos: usize = 1;
|
||||
var unescaped_len: usize = 0;
|
||||
const last = value.len - 1;
|
||||
|
||||
while (value_pos < value.len) {
|
||||
switch (value[value_pos]) {
|
||||
'"' => break,
|
||||
'\\' => {
|
||||
if (value_pos == last) {
|
||||
return error.Invalid;
|
||||
}
|
||||
const next = value[value_pos + 1];
|
||||
if (T_SPECIAL[next] == false) {
|
||||
return error.Invalid;
|
||||
}
|
||||
value_pos += 2;
|
||||
},
|
||||
else => value_pos += 1,
|
||||
}
|
||||
unescaped_len += 1;
|
||||
}
|
||||
|
||||
if (unescaped_len == 0) {
|
||||
return error.Invalid;
|
||||
}
|
||||
|
||||
value_pos = 1;
|
||||
const owned = try arena.alloc(u8, unescaped_len);
|
||||
for (0..unescaped_len) |i| {
|
||||
switch (value[value_pos]) {
|
||||
'"' => break,
|
||||
'\\' => {
|
||||
owned[i] = value[value_pos + 1];
|
||||
value_pos += 2;
|
||||
},
|
||||
else => |c| {
|
||||
owned[i] = c;
|
||||
value_pos += 1;
|
||||
},
|
||||
}
|
||||
}
|
||||
return owned;
|
||||
}
|
||||
|
||||
const VALID_CODEPOINTS = blk: {
|
||||
var v: [256]bool = undefined;
|
||||
for (0..256) |i| {
|
||||
@@ -284,7 +296,7 @@ pub const Mime = struct {
|
||||
};
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
test "Mime: invalid" {
|
||||
test "Mime: invalid " {
|
||||
defer testing.reset();
|
||||
|
||||
const invalids = [_][]const u8{
|
||||
@@ -302,11 +314,12 @@ test "Mime: invalid" {
|
||||
"text/html; charset=\"\"",
|
||||
"text/html; charset=\"",
|
||||
"text/html; charset=\"\\",
|
||||
"text/html; charset=\"\\a\"", // invalid to escape non special characters
|
||||
};
|
||||
|
||||
for (invalids) |invalid| {
|
||||
const mutable_input = try testing.arena_allocator.dupe(u8, invalid);
|
||||
try testing.expectError(error.Invalid, Mime.parse(mutable_input));
|
||||
try testing.expectError(error.Invalid, Mime.parse(undefined, mutable_input));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,9 +349,6 @@ test "Mime: parse common" {
|
||||
try expect(.{ .content_type = .{ .text_javascript = {} } }, "text/javascript");
|
||||
try expect(.{ .content_type = .{ .text_javascript = {} } }, "Application/JavaScript");
|
||||
try expect(.{ .content_type = .{ .text_javascript = {} } }, "application/x-javascript");
|
||||
|
||||
try expect(.{ .content_type = .{ .application_json = {} } }, "application/json");
|
||||
try expect(.{ .content_type = .{ .text_css = {} } }, "text/css");
|
||||
}
|
||||
|
||||
test "Mime: parse uncommon" {
|
||||
@@ -363,19 +373,19 @@ test "Mime: parse charset" {
|
||||
|
||||
try expect(.{
|
||||
.content_type = .{ .text_xml = {} },
|
||||
.charset = "UTF-8",
|
||||
.charset = "utf-8",
|
||||
.params = "charset=utf-8",
|
||||
}, "text/xml; charset=utf-8");
|
||||
|
||||
try expect(.{
|
||||
.content_type = .{ .text_xml = {} },
|
||||
.charset = "UTF-8",
|
||||
.charset = "utf-8",
|
||||
.params = "charset=\"utf-8\"",
|
||||
}, "text/xml;charset=\"utf-8\"");
|
||||
|
||||
try expect(.{
|
||||
.content_type = .{ .text_xml = {} },
|
||||
.charset = "lightpanda:UNSUPPORTED",
|
||||
.charset = "\\ \" ",
|
||||
.params = "charset=\"\\\\ \\\" \"",
|
||||
}, "text/xml;charset=\"\\\\ \\\" \" ");
|
||||
}
|
||||
@@ -386,7 +396,7 @@ test "Mime: isHTML" {
|
||||
const isHTML = struct {
|
||||
fn isHTML(expected: bool, input: []const u8) !void {
|
||||
const mutable_input = try testing.arena_allocator.dupe(u8, input);
|
||||
var mime = try Mime.parse(mutable_input);
|
||||
var mime = try Mime.parse(testing.arena_allocator, mutable_input);
|
||||
try testing.expectEqual(expected, mime.isHTML());
|
||||
}
|
||||
}.isHTML;
|
||||
@@ -472,7 +482,7 @@ const Expectation = struct {
|
||||
fn expect(expected: Expectation, input: []const u8) !void {
|
||||
const mutable_input = try testing.arena_allocator.dupe(u8, input);
|
||||
|
||||
const actual = try Mime.parse(mutable_input);
|
||||
const actual = try Mime.parse(testing.arena_allocator, mutable_input);
|
||||
try testing.expectEqual(
|
||||
std.meta.activeTag(expected.content_type),
|
||||
std.meta.activeTag(actual.content_type),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1453
src/browser/page.zig
1453
src/browser/page.zig
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,8 @@ test "Browser.fetch" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try @import("polyfill.zig").load(testing.allocator, runner.scope);
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ var ok = false;
|
||||
|
||||
@@ -19,102 +19,36 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Env = @import("../env.zig").Env;
|
||||
|
||||
pub const Loader = struct {
|
||||
state: enum { empty, loading } = .empty,
|
||||
const log = std.log.scoped(.polyfill);
|
||||
|
||||
done: struct {
|
||||
fetch: bool = false,
|
||||
webcomponents: bool = false,
|
||||
} = .{},
|
||||
|
||||
fn load(self: *Loader, comptime name: []const u8, source: []const u8, js_context: *Env.JsContext) void {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(js_context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
self.state = .loading;
|
||||
defer self.state = .empty;
|
||||
|
||||
log.debug(.js, "polyfill load", .{ .name = name });
|
||||
_ = js_context.exec(source, name) catch |err| {
|
||||
log.fatal(.app, "polyfill error", .{
|
||||
.name = name,
|
||||
.err = try_catch.err(js_context.call_arena) catch @errorName(err) orelse @errorName(err),
|
||||
});
|
||||
};
|
||||
|
||||
@field(self.done, name) = true;
|
||||
}
|
||||
|
||||
pub fn missing(self: *Loader, name: []const u8, js_context: *Env.JsContext) bool {
|
||||
// Avoid recursive calls during polyfill loading.
|
||||
if (self.state == .loading) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!self.done.fetch and isFetch(name)) {
|
||||
const source = @import("fetch.zig").source;
|
||||
self.load("fetch", source, js_context);
|
||||
|
||||
// We return false here: We want v8 to continue the calling chain
|
||||
// to finally find the polyfill we just inserted. If we want to
|
||||
// return false and stops the call chain, we have to use
|
||||
// `info.GetReturnValue.Set()` function, or `undefined` will be
|
||||
// returned immediately.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!self.done.webcomponents and isWebcomponents(name)) {
|
||||
const source = @import("webcomponents.zig").source;
|
||||
self.load("webcomponents", source, js_context);
|
||||
// We return false here: We want v8 to continue the calling chain
|
||||
// to finally find the polyfill we just inserted. If we want to
|
||||
// return false and stops the call chain, we have to use
|
||||
// `info.GetReturnValue.Set()` function, or `undefined` will be
|
||||
// returned immediately.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (comptime builtin.mode == .Debug) {
|
||||
log.debug(.unknown_prop, "unkown global property", .{
|
||||
.info = "but the property can exist in pure JS",
|
||||
.property = name,
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
fn isFetch(name: []const u8) bool {
|
||||
if (std.mem.eql(u8, name, "fetch")) return true;
|
||||
if (std.mem.eql(u8, name, "Request")) return true;
|
||||
if (std.mem.eql(u8, name, "Response")) return true;
|
||||
if (std.mem.eql(u8, name, "Headers")) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
fn isWebcomponents(name: []const u8) bool {
|
||||
if (std.mem.eql(u8, name, "customElements")) return true;
|
||||
return false;
|
||||
}
|
||||
const modules = [_]struct {
|
||||
name: []const u8,
|
||||
source: []const u8,
|
||||
}{
|
||||
.{ .name = "polyfill-fetch", .source = @import("fetch.zig").source },
|
||||
};
|
||||
|
||||
pub fn preload(allocator: Allocator, js_context: *Env.JsContext) !void {
|
||||
pub fn load(allocator: Allocator, scope: *Env.Scope) !void {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(js_context);
|
||||
try_catch.init(scope);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const name = "webcomponents-pre";
|
||||
const source = @import("webcomponents.zig").pre;
|
||||
_ = js_context.exec(source, name) catch |err| {
|
||||
if (try try_catch.err(allocator)) |msg| {
|
||||
for (modules) |m| {
|
||||
const res = scope.exec(m.source, m.name) catch |err| {
|
||||
if (try try_catch.err(allocator)) |msg| {
|
||||
defer allocator.free(msg);
|
||||
log.err("load {s}: {s}", .{ m.name, msg });
|
||||
}
|
||||
return err;
|
||||
};
|
||||
|
||||
if (builtin.mode == .Debug) {
|
||||
const msg = try res.toString(allocator);
|
||||
defer allocator.free(msg);
|
||||
log.fatal(.app, "polyfill error", .{ .name = name, .err = msg });
|
||||
log.debug("load {s}: {s}", .{ m.name, msg });
|
||||
}
|
||||
return err;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
/**
|
||||
@license @nocompile
|
||||
Copyright (c) 2018 The Polymer Project Authors. All rights reserved.
|
||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
||||
Code distributed by Google as part of the polymer project is also
|
||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
||||
*/
|
||||
(function(){/*
|
||||
|
||||
Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
|
||||
This code may only be used under the BSD style license found at
|
||||
http://polymer.github.io/LICENSE.txt The complete set of authors may be found
|
||||
at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
|
||||
be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
|
||||
Google as part of the polymer project is also subject to an additional IP
|
||||
rights grant found at http://polymer.github.io/PATENTS.txt
|
||||
*/
|
||||
'use strict';var n=window.Document.prototype.createElement,p=window.Document.prototype.createElementNS,aa=window.Document.prototype.importNode,ba=window.Document.prototype.prepend,ca=window.Document.prototype.append,da=window.DocumentFragment.prototype.prepend,ea=window.DocumentFragment.prototype.append,q=window.Node.prototype.cloneNode,r=window.Node.prototype.appendChild,t=window.Node.prototype.insertBefore,u=window.Node.prototype.removeChild,v=window.Node.prototype.replaceChild,w=Object.getOwnPropertyDescriptor(window.Node.prototype,
|
||||
"textContent"),y=window.Element.prototype.attachShadow,z=Object.getOwnPropertyDescriptor(window.Element.prototype,"innerHTML"),A=window.Element.prototype.getAttribute,B=window.Element.prototype.setAttribute,C=window.Element.prototype.removeAttribute,D=window.Element.prototype.toggleAttribute,E=window.Element.prototype.getAttributeNS,F=window.Element.prototype.setAttributeNS,G=window.Element.prototype.removeAttributeNS,H=window.Element.prototype.insertAdjacentElement,fa=window.Element.prototype.insertAdjacentHTML,
|
||||
ha=window.Element.prototype.prepend,ia=window.Element.prototype.append,ja=window.Element.prototype.before,ka=window.Element.prototype.after,la=window.Element.prototype.replaceWith,ma=window.Element.prototype.remove,na=window.HTMLElement,I=Object.getOwnPropertyDescriptor(window.HTMLElement.prototype,"innerHTML"),oa=window.HTMLElement.prototype.insertAdjacentElement,pa=window.HTMLElement.prototype.insertAdjacentHTML;var qa=new Set;"annotation-xml color-profile font-face font-face-src font-face-uri font-face-format font-face-name missing-glyph".split(" ").forEach(function(a){return qa.add(a)});function ra(a){var b=qa.has(a);a=/^[a-z][.0-9_a-z]*-[-.0-9_a-z]*$/.test(a);return!b&&a}var sa=document.contains?document.contains.bind(document):document.documentElement.contains.bind(document.documentElement);
|
||||
function J(a){var b=a.isConnected;if(void 0!==b)return b;if(sa(a))return!0;for(;a&&!(a.__CE_isImportDocument||a instanceof Document);)a=a.parentNode||(window.ShadowRoot&&a instanceof ShadowRoot?a.host:void 0);return!(!a||!(a.__CE_isImportDocument||a instanceof Document))}function K(a){var b=a.children;if(b)return Array.prototype.slice.call(b);b=[];for(a=a.firstChild;a;a=a.nextSibling)a.nodeType===Node.ELEMENT_NODE&&b.push(a);return b}
|
||||
function L(a,b){for(;b&&b!==a&&!b.nextSibling;)b=b.parentNode;return b&&b!==a?b.nextSibling:null}
|
||||
function M(a,b,d){for(var f=a;f;){if(f.nodeType===Node.ELEMENT_NODE){var c=f;b(c);var e=c.localName;if("link"===e&&"import"===c.getAttribute("rel")){f=c.import;void 0===d&&(d=new Set);if(f instanceof Node&&!d.has(f))for(d.add(f),f=f.firstChild;f;f=f.nextSibling)M(f,b,d);f=L(a,c);continue}else if("template"===e){f=L(a,c);continue}if(c=c.__CE_shadowRoot)for(c=c.firstChild;c;c=c.nextSibling)M(c,b,d)}f=f.firstChild?f.firstChild:L(a,f)}};function N(){var a=!(null===O||void 0===O||!O.noDocumentConstructionObserver),b=!(null===O||void 0===O||!O.shadyDomFastWalk);this.m=[];this.g=[];this.j=!1;this.shadyDomFastWalk=b;this.I=!a}function P(a,b,d,f){var c=window.ShadyDOM;if(a.shadyDomFastWalk&&c&&c.inUse){if(b.nodeType===Node.ELEMENT_NODE&&d(b),b.querySelectorAll)for(a=c.nativeMethods.querySelectorAll.call(b,"*"),b=0;b<a.length;b++)d(a[b])}else M(b,d,f)}function ta(a,b){a.j=!0;a.m.push(b)}function ua(a,b){a.j=!0;a.g.push(b)}
|
||||
function Q(a,b){a.j&&P(a,b,function(d){return R(a,d)})}function R(a,b){if(a.j&&!b.__CE_patched){b.__CE_patched=!0;for(var d=0;d<a.m.length;d++)a.m[d](b);for(d=0;d<a.g.length;d++)a.g[d](b)}}function S(a,b){var d=[];P(a,b,function(c){return d.push(c)});for(b=0;b<d.length;b++){var f=d[b];1===f.__CE_state?a.connectedCallback(f):T(a,f)}}function U(a,b){var d=[];P(a,b,function(c){return d.push(c)});for(b=0;b<d.length;b++){var f=d[b];1===f.__CE_state&&a.disconnectedCallback(f)}}
|
||||
function V(a,b,d){d=void 0===d?{}:d;var f=d.J,c=d.upgrade||function(g){return T(a,g)},e=[];P(a,b,function(g){a.j&&R(a,g);if("link"===g.localName&&"import"===g.getAttribute("rel")){var h=g.import;h instanceof Node&&(h.__CE_isImportDocument=!0,h.__CE_registry=document.__CE_registry);h&&"complete"===h.readyState?h.__CE_documentLoadHandled=!0:g.addEventListener("load",function(){var k=g.import;if(!k.__CE_documentLoadHandled){k.__CE_documentLoadHandled=!0;var l=new Set;f&&(f.forEach(function(m){return l.add(m)}),
|
||||
l.delete(k));V(a,k,{J:l,upgrade:c})}})}else e.push(g)},f);for(b=0;b<e.length;b++)c(e[b])}
|
||||
function T(a,b){try{var d=b.ownerDocument,f=d.__CE_registry;var c=f&&(d.defaultView||d.__CE_isImportDocument)?W(f,b.localName):void 0;if(c&&void 0===b.__CE_state){c.constructionStack.push(b);try{try{if(new c.constructorFunction!==b)throw Error("The custom element constructor did not produce the element being upgraded.");}finally{c.constructionStack.pop()}}catch(k){throw b.__CE_state=2,k;}b.__CE_state=1;b.__CE_definition=c;if(c.attributeChangedCallback&&b.hasAttributes()){var e=c.observedAttributes;
|
||||
for(c=0;c<e.length;c++){var g=e[c],h=b.getAttribute(g);null!==h&&a.attributeChangedCallback(b,g,null,h,null)}}J(b)&&a.connectedCallback(b)}}catch(k){X(k)}}N.prototype.connectedCallback=function(a){var b=a.__CE_definition;if(b.connectedCallback)try{b.connectedCallback.call(a)}catch(d){X(d)}};N.prototype.disconnectedCallback=function(a){var b=a.__CE_definition;if(b.disconnectedCallback)try{b.disconnectedCallback.call(a)}catch(d){X(d)}};
|
||||
N.prototype.attributeChangedCallback=function(a,b,d,f,c){var e=a.__CE_definition;if(e.attributeChangedCallback&&-1<e.observedAttributes.indexOf(b))try{e.attributeChangedCallback.call(a,b,d,f,c)}catch(g){X(g)}};
|
||||
function va(a,b,d,f){var c=b.__CE_registry;if(c&&(null===f||"http://www.w3.org/1999/xhtml"===f)&&(c=W(c,d)))try{var e=new c.constructorFunction;if(void 0===e.__CE_state||void 0===e.__CE_definition)throw Error("Failed to construct '"+d+"': The returned value was not constructed with the HTMLElement constructor.");if("http://www.w3.org/1999/xhtml"!==e.namespaceURI)throw Error("Failed to construct '"+d+"': The constructed element's namespace must be the HTML namespace.");if(e.hasAttributes())throw Error("Failed to construct '"+
|
||||
d+"': The constructed element must not have any attributes.");if(null!==e.firstChild)throw Error("Failed to construct '"+d+"': The constructed element must not have any children.");if(null!==e.parentNode)throw Error("Failed to construct '"+d+"': The constructed element must not have a parent node.");if(e.ownerDocument!==b)throw Error("Failed to construct '"+d+"': The constructed element's owner document is incorrect.");if(e.localName!==d)throw Error("Failed to construct '"+d+"': The constructed element's local name is incorrect.");
|
||||
return e}catch(g){return X(g),b=null===f?n.call(b,d):p.call(b,f,d),Object.setPrototypeOf(b,HTMLUnknownElement.prototype),b.__CE_state=2,b.__CE_definition=void 0,R(a,b),b}b=null===f?n.call(b,d):p.call(b,f,d);R(a,b);return b}
|
||||
function X(a){var b="",d="",f=0,c=0;a instanceof Error?(b=a.message,d=a.sourceURL||a.fileName||"",f=a.line||a.lineNumber||0,c=a.column||a.columnNumber||0):b="Uncaught "+String(a);var e=void 0;void 0===ErrorEvent.prototype.initErrorEvent?e=new ErrorEvent("error",{cancelable:!0,message:b,filename:d,lineno:f,colno:c,error:a}):(e=document.createEvent("ErrorEvent"),e.initErrorEvent("error",!1,!0,b,d,f),e.preventDefault=function(){Object.defineProperty(this,"defaultPrevented",{configurable:!0,get:function(){return!0}})});
|
||||
void 0===e.error&&Object.defineProperty(e,"error",{configurable:!0,enumerable:!0,get:function(){return a}});window.dispatchEvent(e);e.defaultPrevented||console.error(a)};function wa(){var a=this;this.g=void 0;this.F=new Promise(function(b){a.l=b})}wa.prototype.resolve=function(a){if(this.g)throw Error("Already resolved.");this.g=a;this.l(a)};function xa(a){var b=document;this.l=void 0;this.h=a;this.g=b;V(this.h,this.g);"loading"===this.g.readyState&&(this.l=new MutationObserver(this.G.bind(this)),this.l.observe(this.g,{childList:!0,subtree:!0}))}function ya(a){a.l&&a.l.disconnect()}xa.prototype.G=function(a){var b=this.g.readyState;"interactive"!==b&&"complete"!==b||ya(this);for(b=0;b<a.length;b++)for(var d=a[b].addedNodes,f=0;f<d.length;f++)V(this.h,d[f])};function Y(a){this.s=new Map;this.u=new Map;this.C=new Map;this.A=!1;this.B=new Map;this.o=function(b){return b()};this.i=!1;this.v=[];this.h=a;this.D=a.I?new xa(a):void 0}Y.prototype.H=function(a,b){var d=this;if(!(b instanceof Function))throw new TypeError("Custom element constructor getters must be functions.");za(this,a);this.s.set(a,b);this.v.push(a);this.i||(this.i=!0,this.o(function(){return Aa(d)}))};
|
||||
Y.prototype.define=function(a,b){var d=this;if(!(b instanceof Function))throw new TypeError("Custom element constructors must be functions.");za(this,a);Ba(this,a,b);this.v.push(a);this.i||(this.i=!0,this.o(function(){return Aa(d)}))};function za(a,b){if(!ra(b))throw new SyntaxError("The element name '"+b+"' is not valid.");if(W(a,b))throw Error("A custom element with name '"+(b+"' has already been defined."));if(a.A)throw Error("A custom element is already being defined.");}
|
||||
function Ba(a,b,d){a.A=!0;var f;try{var c=d.prototype;if(!(c instanceof Object))throw new TypeError("The custom element constructor's prototype is not an object.");var e=function(m){var x=c[m];if(void 0!==x&&!(x instanceof Function))throw Error("The '"+m+"' callback must be a function.");return x};var g=e("connectedCallback");var h=e("disconnectedCallback");var k=e("adoptedCallback");var l=(f=e("attributeChangedCallback"))&&d.observedAttributes||[]}catch(m){throw m;}finally{a.A=!1}d={localName:b,
|
||||
constructorFunction:d,connectedCallback:g,disconnectedCallback:h,adoptedCallback:k,attributeChangedCallback:f,observedAttributes:l,constructionStack:[]};a.u.set(b,d);a.C.set(d.constructorFunction,d);return d}Y.prototype.upgrade=function(a){V(this.h,a)};
|
||||
function Aa(a){if(!1!==a.i){a.i=!1;for(var b=[],d=a.v,f=new Map,c=0;c<d.length;c++)f.set(d[c],[]);V(a.h,document,{upgrade:function(k){if(void 0===k.__CE_state){var l=k.localName,m=f.get(l);m?m.push(k):a.u.has(l)&&b.push(k)}}});for(c=0;c<b.length;c++)T(a.h,b[c]);for(c=0;c<d.length;c++){for(var e=d[c],g=f.get(e),h=0;h<g.length;h++)T(a.h,g[h]);(e=a.B.get(e))&&e.resolve(void 0)}d.length=0}}Y.prototype.get=function(a){if(a=W(this,a))return a.constructorFunction};
|
||||
Y.prototype.whenDefined=function(a){if(!ra(a))return Promise.reject(new SyntaxError("'"+a+"' is not a valid custom element name."));var b=this.B.get(a);if(b)return b.F;b=new wa;this.B.set(a,b);var d=this.u.has(a)||this.s.has(a);a=-1===this.v.indexOf(a);d&&a&&b.resolve(void 0);return b.F};Y.prototype.polyfillWrapFlushCallback=function(a){this.D&&ya(this.D);var b=this.o;this.o=function(d){return a(function(){return b(d)})}};
|
||||
function W(a,b){var d=a.u.get(b);if(d)return d;if(d=a.s.get(b)){a.s.delete(b);try{return Ba(a,b,d())}catch(f){X(f)}}}Y.prototype.define=Y.prototype.define;Y.prototype.upgrade=Y.prototype.upgrade;Y.prototype.get=Y.prototype.get;Y.prototype.whenDefined=Y.prototype.whenDefined;Y.prototype.polyfillDefineLazy=Y.prototype.H;Y.prototype.polyfillWrapFlushCallback=Y.prototype.polyfillWrapFlushCallback;function Z(a,b,d){function f(c){return function(e){for(var g=[],h=0;h<arguments.length;++h)g[h]=arguments[h];h=[];for(var k=[],l=0;l<g.length;l++){var m=g[l];m instanceof Element&&J(m)&&k.push(m);if(m instanceof DocumentFragment)for(m=m.firstChild;m;m=m.nextSibling)h.push(m);else h.push(m)}c.apply(this,g);for(g=0;g<k.length;g++)U(a,k[g]);if(J(this))for(g=0;g<h.length;g++)k=h[g],k instanceof Element&&S(a,k)}}void 0!==d.prepend&&(b.prepend=f(d.prepend));void 0!==d.append&&(b.append=f(d.append))};function Ca(a){Document.prototype.createElement=function(b){return va(a,this,b,null)};Document.prototype.importNode=function(b,d){b=aa.call(this,b,!!d);this.__CE_registry?V(a,b):Q(a,b);return b};Document.prototype.createElementNS=function(b,d){return va(a,this,d,b)};Z(a,Document.prototype,{prepend:ba,append:ca})};function Da(a){function b(f){return function(c){for(var e=[],g=0;g<arguments.length;++g)e[g]=arguments[g];g=[];for(var h=[],k=0;k<e.length;k++){var l=e[k];l instanceof Element&&J(l)&&h.push(l);if(l instanceof DocumentFragment)for(l=l.firstChild;l;l=l.nextSibling)g.push(l);else g.push(l)}f.apply(this,e);for(e=0;e<h.length;e++)U(a,h[e]);if(J(this))for(e=0;e<g.length;e++)h=g[e],h instanceof Element&&S(a,h)}}var d=Element.prototype;void 0!==ja&&(d.before=b(ja));void 0!==ka&&(d.after=b(ka));void 0!==la&&
|
||||
(d.replaceWith=function(f){for(var c=[],e=0;e<arguments.length;++e)c[e]=arguments[e];e=[];for(var g=[],h=0;h<c.length;h++){var k=c[h];k instanceof Element&&J(k)&&g.push(k);if(k instanceof DocumentFragment)for(k=k.firstChild;k;k=k.nextSibling)e.push(k);else e.push(k)}h=J(this);la.apply(this,c);for(c=0;c<g.length;c++)U(a,g[c]);if(h)for(U(a,this),c=0;c<e.length;c++)g=e[c],g instanceof Element&&S(a,g)});void 0!==ma&&(d.remove=function(){var f=J(this);ma.call(this);f&&U(a,this)})};function Ea(a){function b(c,e){Object.defineProperty(c,"innerHTML",{enumerable:e.enumerable,configurable:!0,get:e.get,set:function(g){var h=this,k=void 0;J(this)&&(k=[],P(a,this,function(x){x!==h&&k.push(x)}));e.set.call(this,g);if(k)for(var l=0;l<k.length;l++){var m=k[l];1===m.__CE_state&&a.disconnectedCallback(m)}this.ownerDocument.__CE_registry?V(a,this):Q(a,this);return g}})}function d(c,e){c.insertAdjacentElement=function(g,h){var k=J(h);g=e.call(this,g,h);k&&U(a,h);J(g)&&S(a,h);return g}}function f(c,
|
||||
e){function g(h,k){for(var l=[];h!==k;h=h.nextSibling)l.push(h);for(k=0;k<l.length;k++)V(a,l[k])}c.insertAdjacentHTML=function(h,k){h=h.toLowerCase();if("beforebegin"===h){var l=this.previousSibling;e.call(this,h,k);g(l||this.parentNode.firstChild,this)}else if("afterbegin"===h)l=this.firstChild,e.call(this,h,k),g(this.firstChild,l);else if("beforeend"===h)l=this.lastChild,e.call(this,h,k),g(l||this.firstChild,null);else if("afterend"===h)l=this.nextSibling,e.call(this,h,k),g(this.nextSibling,l);
|
||||
else throw new SyntaxError("The value provided ("+String(h)+") is not one of 'beforebegin', 'afterbegin', 'beforeend', or 'afterend'.");}}y&&(Element.prototype.attachShadow=function(c){c=y.call(this,c);if(a.j&&!c.__CE_patched){c.__CE_patched=!0;for(var e=0;e<a.m.length;e++)a.m[e](c)}return this.__CE_shadowRoot=c});z&&z.get?b(Element.prototype,z):I&&I.get?b(HTMLElement.prototype,I):ua(a,function(c){b(c,{enumerable:!0,configurable:!0,get:function(){return q.call(this,!0).innerHTML},set:function(e){var g=
|
||||
"template"===this.localName,h=g?this.content:this,k=p.call(document,this.namespaceURI,this.localName);for(k.innerHTML=e;0<h.childNodes.length;)u.call(h,h.childNodes[0]);for(e=g?k.content:k;0<e.childNodes.length;)r.call(h,e.childNodes[0])}})});Element.prototype.setAttribute=function(c,e){if(1!==this.__CE_state)return B.call(this,c,e);var g=A.call(this,c);B.call(this,c,e);e=A.call(this,c);a.attributeChangedCallback(this,c,g,e,null)};Element.prototype.setAttributeNS=function(c,e,g){if(1!==this.__CE_state)return F.call(this,
|
||||
c,e,g);var h=E.call(this,c,e);F.call(this,c,e,g);g=E.call(this,c,e);a.attributeChangedCallback(this,e,h,g,c)};Element.prototype.removeAttribute=function(c){if(1!==this.__CE_state)return C.call(this,c);var e=A.call(this,c);C.call(this,c);null!==e&&a.attributeChangedCallback(this,c,e,null,null)};D&&(Element.prototype.toggleAttribute=function(c,e){if(1!==this.__CE_state)return D.call(this,c,e);var g=A.call(this,c),h=null!==g;e=D.call(this,c,e);h!==e&&a.attributeChangedCallback(this,c,g,e?"":null,null);
|
||||
return e});Element.prototype.removeAttributeNS=function(c,e){if(1!==this.__CE_state)return G.call(this,c,e);var g=E.call(this,c,e);G.call(this,c,e);var h=E.call(this,c,e);g!==h&&a.attributeChangedCallback(this,e,g,h,c)};oa?d(HTMLElement.prototype,oa):H&&d(Element.prototype,H);pa?f(HTMLElement.prototype,pa):fa&&f(Element.prototype,fa);Z(a,Element.prototype,{prepend:ha,append:ia});Da(a)};var Fa={};function Ga(a){function b(){var d=this.constructor;var f=document.__CE_registry.C.get(d);if(!f)throw Error("Failed to construct a custom element: The constructor was not registered with `customElements`.");var c=f.constructionStack;if(0===c.length)return c=n.call(document,f.localName),Object.setPrototypeOf(c,d.prototype),c.__CE_state=1,c.__CE_definition=f,R(a,c),c;var e=c.length-1,g=c[e];if(g===Fa)throw Error("Failed to construct '"+f.localName+"': This element was already constructed.");c[e]=Fa;
|
||||
Object.setPrototypeOf(g,d.prototype);R(a,g);return g}b.prototype=na.prototype;Object.defineProperty(HTMLElement.prototype,"constructor",{writable:!0,configurable:!0,enumerable:!1,value:b});window.HTMLElement=b};function Ha(a){function b(d,f){Object.defineProperty(d,"textContent",{enumerable:f.enumerable,configurable:!0,get:f.get,set:function(c){if(this.nodeType===Node.TEXT_NODE)f.set.call(this,c);else{var e=void 0;if(this.firstChild){var g=this.childNodes,h=g.length;if(0<h&&J(this)){e=Array(h);for(var k=0;k<h;k++)e[k]=g[k]}}f.set.call(this,c);if(e)for(c=0;c<e.length;c++)U(a,e[c])}}})}Node.prototype.insertBefore=function(d,f){if(d instanceof DocumentFragment){var c=K(d);d=t.call(this,d,f);if(J(this))for(f=
|
||||
0;f<c.length;f++)S(a,c[f]);return d}c=d instanceof Element&&J(d);f=t.call(this,d,f);c&&U(a,d);J(this)&&S(a,d);return f};Node.prototype.appendChild=function(d){if(d instanceof DocumentFragment){var f=K(d);d=r.call(this,d);if(J(this))for(var c=0;c<f.length;c++)S(a,f[c]);return d}f=d instanceof Element&&J(d);c=r.call(this,d);f&&U(a,d);J(this)&&S(a,d);return c};Node.prototype.cloneNode=function(d){d=q.call(this,!!d);this.ownerDocument.__CE_registry?V(a,d):Q(a,d);return d};Node.prototype.removeChild=function(d){var f=
|
||||
d instanceof Element&&J(d),c=u.call(this,d);f&&U(a,d);return c};Node.prototype.replaceChild=function(d,f){if(d instanceof DocumentFragment){var c=K(d);d=v.call(this,d,f);if(J(this))for(U(a,f),f=0;f<c.length;f++)S(a,c[f]);return d}c=d instanceof Element&&J(d);var e=v.call(this,d,f),g=J(this);g&&U(a,f);c&&U(a,d);g&&S(a,d);return e};w&&w.get?b(Node.prototype,w):ta(a,function(d){b(d,{enumerable:!0,configurable:!0,get:function(){for(var f=[],c=this.firstChild;c;c=c.nextSibling)c.nodeType!==Node.COMMENT_NODE&&
|
||||
f.push(c.textContent);return f.join("")},set:function(f){for(;this.firstChild;)u.call(this,this.firstChild);null!=f&&""!==f&&r.call(this,document.createTextNode(f))}})})};var O=window.customElements;function Ia(){var a=new N;Ga(a);Ca(a);Z(a,DocumentFragment.prototype,{prepend:da,append:ea});Ha(a);Ea(a);window.CustomElementRegistry=Y;a=new Y(a);document.__CE_registry=a;Object.defineProperty(window,"customElements",{configurable:!0,enumerable:!0,value:a})}O&&!O.forcePolyfill&&"function"==typeof O.define&&"function"==typeof O.get||Ia();window.__CE_installPolyfill=Ia;/*
|
||||
|
||||
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
||||
Code distributed by Google as part of the polymer project is also
|
||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
||||
*/
|
||||
}).call(this);
|
||||
@@ -1,42 +0,0 @@
|
||||
// webcomponents.js code comes from
|
||||
// https://github.com/webcomponents/polyfills/tree/master/packages/webcomponentsjs
|
||||
//
|
||||
// The original code source is available in a "BSD style license".
|
||||
//
|
||||
// This is the `webcomponents-ce.js` bundle
|
||||
pub const source = @embedFile("webcomponents.js");
|
||||
|
||||
// The main webcomponents.js is lazilly loaded when window.customElements is
|
||||
// called. But, if you look at the test below, you'll notice that we declare
|
||||
// our custom element (LightPanda) before we call `customElements.define`. We
|
||||
// _have_ to declare it before we can register it.
|
||||
// That causes an issue, because the LightPanda class extends HTMLElement, which
|
||||
// hasn't been monkeypatched by the polyfill yet. If you were to try it as-is
|
||||
// you'd get an "Illegal Constructor", because that's what the Zig HTMLElement
|
||||
// constructor does (and that's correct).
|
||||
// However, once HTMLElement is monkeypatched, it'll work. One simple solution
|
||||
// is to run the webcomponents.js polyfill proactively on each page, ensuring
|
||||
// that HTMLElement is monkeypatched before any other JavaScript is run. But
|
||||
// that adds _a lot_ of overhead.
|
||||
// So instead of always running the [large and intrusive] webcomponents.js
|
||||
// polyfill, we'll always run this little snippet. It wraps the HTMLElement
|
||||
// constructor. When the Lightpanda class is created, it'll extend our little
|
||||
// wrapper. But, unlike the Zig default constructor which throws, our code
|
||||
// calls the "real" constructor. That might seem like the same thing, but by the
|
||||
// time our wrapper is called, the webcomponents.js polyfill will have been
|
||||
// loaded and the "real" constructor will be the monkeypatched version.
|
||||
// TL;DR creates a layer of indirection for the constructor, so that, when it's
|
||||
// actually instantiated, the webcomponents.js polyfill will have been loaded.
|
||||
pub const pre =
|
||||
\\ (() => {
|
||||
\\ const HE = window.HTMLElement;
|
||||
\\ const b = function() { return HE.prototype.constructor.call(this); }
|
||||
\\ b.prototype = HE.prototype;
|
||||
\\ window.HTMLElement = b;
|
||||
\\ })();
|
||||
;
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: Polyfill.WebComponents" {
|
||||
try testing.htmlRunner("polyfill/webcomponents.html");
|
||||
}
|
||||
@@ -50,8 +50,6 @@ const FlatRenderer = struct {
|
||||
};
|
||||
}
|
||||
|
||||
// The DOMRect is always relative to the viewport, not the document the element belongs to.
|
||||
// Element that are not part of the main document, either detached or in a shadow DOM should not call this function.
|
||||
pub fn getRect(self: *FlatRenderer, e: *parser.Element) !Element.DOMRect {
|
||||
var elements = &self.elements;
|
||||
const gop = try self.positions.getOrPut(self.allocator, @intFromPtr(e));
|
||||
@@ -62,38 +60,20 @@ const FlatRenderer = struct {
|
||||
gop.value_ptr.* = x;
|
||||
}
|
||||
|
||||
const _x: f64 = @floatFromInt(x);
|
||||
const y: f64 = 0.0;
|
||||
const w: f64 = 1.0;
|
||||
const h: f64 = 1.0;
|
||||
|
||||
return .{
|
||||
.x = _x,
|
||||
.y = y,
|
||||
.width = w,
|
||||
.height = h,
|
||||
.left = _x,
|
||||
.top = y,
|
||||
.right = _x + w,
|
||||
.bottom = y + h,
|
||||
.x = @floatFromInt(x),
|
||||
.y = 0.0,
|
||||
.width = 1.0,
|
||||
.height = 1.0,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn boundingRect(self: *const FlatRenderer) Element.DOMRect {
|
||||
const x: f64 = 0.0;
|
||||
const y: f64 = 0.0;
|
||||
const w: f64 = @floatFromInt(self.width());
|
||||
const h: f64 = @floatFromInt(self.width());
|
||||
|
||||
return .{
|
||||
.x = x,
|
||||
.y = y,
|
||||
.width = w,
|
||||
.height = h,
|
||||
.left = x,
|
||||
.top = y,
|
||||
.right = x + w,
|
||||
.bottom = y + h,
|
||||
.x = 0.0,
|
||||
.y = 0.0,
|
||||
.width = @floatFromInt(self.width()),
|
||||
.height = @floatFromInt(self.height()),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -18,18 +18,17 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
|
||||
const Env = @import("env.zig").Env;
|
||||
const Page = @import("page.zig").Page;
|
||||
const URL = @import("../url.zig").URL;
|
||||
const Browser = @import("browser.zig").Browser;
|
||||
const NavigateOpts = @import("page.zig").NavigateOpts;
|
||||
|
||||
const log = @import("../log.zig");
|
||||
const parser = @import("netsurf.zig");
|
||||
const storage = @import("storage/storage.zig");
|
||||
|
||||
const log = std.log.scoped(.session);
|
||||
|
||||
// Session is like a browser's tab.
|
||||
// It owns the js env and the loader for all the pages of the session.
|
||||
// You can create successively multiple pages for a session, but you must
|
||||
@@ -38,43 +37,25 @@ pub const Session = struct {
|
||||
browser: *Browser,
|
||||
|
||||
// Used to create our Inspector and in the BrowserContext.
|
||||
arena: Allocator,
|
||||
arena: ArenaAllocator,
|
||||
|
||||
// The page's arena is unsuitable for data that has to existing while
|
||||
// navigating from one page to another. For example, if we're clicking
|
||||
// on an HREF, the URL exists in the original page (where the click
|
||||
// originated) but also has to exist in the new page.
|
||||
// While we could use the Session's arena, this could accumulate a lot of
|
||||
// memory if we do many navigation events. The `transfer_arena` is meant to
|
||||
// bridge the gap: existing long enough to store any data needed to end one
|
||||
// page and start another.
|
||||
transfer_arena: Allocator,
|
||||
|
||||
executor: Env.ExecutionWorld,
|
||||
executor: Env.Executor,
|
||||
storage_shed: storage.Shed,
|
||||
cookie_jar: storage.CookieJar,
|
||||
|
||||
page: ?Page = null,
|
||||
|
||||
// If the current page want to navigate to a new page
|
||||
// (form submit, link click, top.location = xxx)
|
||||
// the details are stored here so that, on the next call to session.wait
|
||||
// we can destroy the current page and start a new one.
|
||||
queued_navigation: ?QueuedNavigation,
|
||||
|
||||
pub fn init(self: *Session, browser: *Browser) !void {
|
||||
var executor = try browser.env.newExecutionWorld();
|
||||
var executor = try browser.env.newExecutor();
|
||||
errdefer executor.deinit();
|
||||
|
||||
const allocator = browser.app.allocator;
|
||||
self.* = .{
|
||||
.browser = browser,
|
||||
.executor = executor,
|
||||
.queued_navigation = null,
|
||||
.arena = browser.session_arena.allocator(),
|
||||
.arena = ArenaAllocator.init(allocator),
|
||||
.storage_shed = storage.Shed.init(allocator),
|
||||
.cookie_jar = storage.CookieJar.init(allocator),
|
||||
.transfer_arena = browser.transfer_arena.allocator(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,6 +63,7 @@ pub const Session = struct {
|
||||
if (self.page != null) {
|
||||
self.removePage();
|
||||
}
|
||||
self.arena.deinit();
|
||||
self.cookie_jar.deinit();
|
||||
self.storage_shed.deinit();
|
||||
self.executor.deinit();
|
||||
@@ -98,14 +80,13 @@ pub const Session = struct {
|
||||
|
||||
const page_arena = &self.browser.page_arena;
|
||||
_ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
||||
_ = self.browser.state_pool.reset(.{ .retain_with_limit = 4 * 1024 });
|
||||
|
||||
self.page = @as(Page, undefined);
|
||||
const page = &self.page.?;
|
||||
try Page.init(page, page_arena.allocator(), self);
|
||||
|
||||
log.debug(.browser, "create page", .{});
|
||||
// start JS env
|
||||
log.debug("start new js scope", .{});
|
||||
// Inform CDP the main page has been created such that additional context for other Worlds can be created as well
|
||||
self.browser.notification.dispatch(.page_created, page);
|
||||
|
||||
@@ -117,66 +98,35 @@ pub const Session = struct {
|
||||
self.browser.notification.dispatch(.page_remove, .{});
|
||||
|
||||
std.debug.assert(self.page != null);
|
||||
|
||||
// RemoveJsContext() will execute the destructor of any type that
|
||||
// registered a destructor (e.g. XMLHttpRequest).
|
||||
// Should be called before we deinit the page, because these objects
|
||||
// could be referencing it.
|
||||
self.executor.removeJsContext();
|
||||
|
||||
self.page.?.deinit();
|
||||
// Reset all existing callbacks.
|
||||
self.browser.app.loop.reset();
|
||||
self.executor.endScope();
|
||||
self.page = null;
|
||||
|
||||
// clear netsurf memory arena.
|
||||
parser.deinit();
|
||||
|
||||
log.debug(.browser, "remove page", .{});
|
||||
}
|
||||
|
||||
pub fn currentPage(self: *Session) ?*Page {
|
||||
return &(self.page orelse return null);
|
||||
}
|
||||
|
||||
pub const WaitResult = enum {
|
||||
done,
|
||||
no_page,
|
||||
extra_socket,
|
||||
};
|
||||
pub fn pageNavigate(self: *Session, url_string: []const u8) !void {
|
||||
// currently, this is only called from the page, so let's hope
|
||||
// it isn't null!
|
||||
std.debug.assert(self.page != null);
|
||||
|
||||
pub fn wait(self: *Session, wait_ms: i32) WaitResult {
|
||||
if (self.queued_navigation) |qn| {
|
||||
// This was already aborted on the page, but it would be pretty
|
||||
// bad if old requests went to the new page, so let's make double sure
|
||||
self.browser.http_client.abort();
|
||||
// can't use the page arena, because we're about to reset it
|
||||
// and don't want to use the session's arena, because that'll start to
|
||||
// look like a leak if we navigate from page to page a lot.
|
||||
var buf: [2048]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buf);
|
||||
const url = try self.page.?.url.resolve(fba.allocator(), url_string);
|
||||
|
||||
// Page.navigateFromWebAPI terminatedExecution. If we don't resume
|
||||
// it before doing a shutdown we'll get an error.
|
||||
self.executor.resumeExecution();
|
||||
self.removePage();
|
||||
self.queued_navigation = null;
|
||||
|
||||
const page = self.createPage() catch |err| {
|
||||
log.err(.browser, "queued navigation page error", .{
|
||||
.err = err,
|
||||
.url = qn.url,
|
||||
});
|
||||
return .done;
|
||||
};
|
||||
|
||||
page.navigate(qn.url, qn.opts) catch |err| {
|
||||
log.err(.browser, "queued navigation error", .{ .err = err, .url = qn.url });
|
||||
return .done;
|
||||
};
|
||||
}
|
||||
|
||||
if (self.page) |*page| {
|
||||
return page.wait(wait_ms);
|
||||
}
|
||||
return .no_page;
|
||||
self.removePage();
|
||||
var page = try self.createPage();
|
||||
return page.navigate(url, .{
|
||||
.reason = .anchor,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const QueuedNavigation = struct {
|
||||
url: []const u8,
|
||||
opts: NavigateOpts,
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user