43 Commits

Author SHA1 Message Date
Karl Seguin
afa0d5ba12 Try fixing segfault by not being as aggressive with freeing Persisted Resolvers 2025-09-17 20:14:13 +08:00
Muki Kiboigo
4d1e416299 use fetch logging scope, clean some comments 2025-09-16 12:41:35 -07:00
Muki Kiboigo
3badcdbdbd stop using destructor callback for fetch 2025-09-16 12:38:50 -07:00
Muki Kiboigo
fcd82b2c14 htmlRunner for ReadableStream tests, fix ReadableStream enqueue 2025-09-16 12:18:44 -07:00
Muki Kiboigo
d0621510cc use Env.PersistentPromiseResolver 2025-09-16 12:09:54 -07:00
Karl Seguin
2a7a8bc2a6 remove meaningless text from test 2025-09-16 11:08:28 -07:00
Karl Seguin
af916dea1d fix arena, add fetch test 2025-09-16 11:08:27 -07:00
Karl Seguin
31335fc4fb Start working on HTMLSlotElement 2025-09-16 11:08:27 -07:00
Muki Kiboigo
c84634093d use content length to reserve body size 2025-09-16 11:08:27 -07:00
Muki Kiboigo
37d8d2642d copy our Request headers into the HTTP client 2025-09-16 11:08:27 -07:00
Muki Kiboigo
0423a178e9 migrate fetch tests to htmlRunner 2025-09-16 11:08:27 -07:00
Muki Kiboigo
7acf67d668 properly handle closed for ReadableStream 2025-09-16 11:08:27 -07:00
Muki Kiboigo
ef1fece40c deinit persistent promise resolver 2025-09-16 11:08:27 -07:00
Muki Kiboigo
ebb590250f simplify cloning of Req/Resp 2025-09-16 11:08:26 -07:00
Muki Kiboigo
03130a95d8 use call arena for json in Req/Resp 2025-09-16 11:08:26 -07:00
Muki Kiboigo
e133717f7f simplify Headers 2025-09-16 11:08:26 -07:00
Muki Kiboigo
968c695da1 headers iterators should not allocate 2025-09-16 11:08:26 -07:00
Muki Kiboigo
707116a030 use destructor callback for FetchContext 2025-09-16 11:08:26 -07:00
Muki Kiboigo
01966f41ff support object as HeadersInit 2025-09-16 11:08:26 -07:00
Muki Kiboigo
141d17dd55 add logging on fetch error callback 2025-09-16 11:08:26 -07:00
sjorsdonkers
a3c2daf306 retain value, avoid str alloc 2025-09-16 11:08:25 -07:00
sjorsdonkers
dc60fac90d avoid explicit memcpy 2025-09-16 11:08:25 -07:00
sjorsdonkers
a5e2e8ea15 remove length check of fixed size 2025-09-16 11:08:25 -07:00
sjorsdonkers
8295c2abe5 jsValueToZig for fixed sized arrays 2025-09-16 11:08:25 -07:00
Muki Kiboigo
5997be89f6 implement remaining ReadableStream functionality 2025-09-16 11:08:25 -07:00
Muki Kiboigo
1c89cfe5d4 working Header iterators 2025-09-16 11:08:25 -07:00
Muki Kiboigo
b5021bd9fa TypeError when Stream is locked 2025-09-16 11:08:25 -07:00
Muki Kiboigo
4fd365b520 cleaning up various Headers routines 2025-09-16 11:08:25 -07:00
Muki Kiboigo
479cd5ab1a use proper Headers in fetch() 2025-09-16 11:08:24 -07:00
Muki Kiboigo
8285cbcaa9 expand Request/Response interfaces 2025-09-16 11:08:24 -07:00
Muki Kiboigo
545d97b5c0 expand Headers interface 2025-09-16 11:08:24 -07:00
Muki Kiboigo
11016abdd3 remove debug logging in ReadableStream 2025-09-16 11:08:24 -07:00
Muki Kiboigo
066df87dd4 move fetch() into fetch.zig 2025-09-16 11:08:24 -07:00
Muki Kiboigo
91899912d8 add bodyUsed checks on Request and Response 2025-09-16 11:08:24 -07:00
Muki Kiboigo
4ceca6b90b more Headers compatibility 2025-09-16 11:08:24 -07:00
Muki Kiboigo
ec936417c6 add fetch to cdp domain 2025-09-16 11:08:23 -07:00
Muki Kiboigo
4b75b33eb3 add json response method 2025-09-16 11:08:23 -07:00
Muki Kiboigo
1d7e731034 basic readable stream working 2025-09-16 11:08:23 -07:00
Muki Kiboigo
ab60f64452 proper fetch method and body setting 2025-09-16 11:08:23 -07:00
Muki Kiboigo
9757ea7b0f fetch callback logging 2025-09-16 11:08:23 -07:00
Muki Kiboigo
855583874f request url as null terminated 2025-09-16 11:08:23 -07:00
Muki Kiboigo
9efc27c2bb initial fetch in zig 2025-09-16 11:08:23 -07:00
Muki Kiboigo
cab5117d85 remove polyfill and add req/resp 2025-09-16 11:08:19 -07:00
259 changed files with 8628 additions and 13306 deletions

View File

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

View File

@@ -5,12 +5,8 @@ env:
AWS_SECRET_ACCESS_KEY: ${{ secrets.NIGHTLY_BUILD_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.NIGHTLY_BUILD_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.NIGHTLY_BUILD_AWS_BUCKET }} AWS_BUCKET: ${{ vars.NIGHTLY_BUILD_AWS_BUCKET }}
AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }} AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }}
RELEASE: ${{ github.ref_type == 'tag' && github.ref_name || 'nightly' }}
on: on:
push:
tags:
- '*'
schedule: schedule:
- cron: "2 2 * * *" - cron: "2 2 * * *"
@@ -30,9 +26,10 @@ jobs:
timeout-minutes: 15 timeout-minutes: 15
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive submodules: recursive
- uses: ./.github/actions/install - uses: ./.github/actions/install
@@ -41,7 +38,7 @@ jobs:
arch: ${{env.ARCH}} arch: ${{env.ARCH}}
- name: zig build - name: zig build
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseSafe -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) run: zig build --release=safe -Doptimize=ReleaseSafe -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary - name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }} run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -56,7 +53,7 @@ jobs:
with: with:
allowUpdates: true allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }} artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: ${{ env.RELEASE }} tag: nightly
build-linux-aarch64: build-linux-aarch64:
env: env:
@@ -79,7 +76,7 @@ jobs:
arch: ${{env.ARCH}} arch: ${{env.ARCH}}
- name: zig build - name: zig build
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseSafe -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) run: zig build --release=safe -Doptimize=ReleaseSafe -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary - name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }} run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -94,7 +91,7 @@ jobs:
with: with:
allowUpdates: true allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }} artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: ${{ env.RELEASE }} tag: nightly
build-macos-aarch64: build-macos-aarch64:
env: env:
@@ -119,7 +116,7 @@ jobs:
arch: ${{env.ARCH}} arch: ${{env.ARCH}}
- name: zig build - name: zig build
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary - name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }} run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -134,14 +131,19 @@ jobs:
with: with:
allowUpdates: true allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }} artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: ${{ env.RELEASE }} tag: nightly
build-macos-x86_64: build-macos-x86_64:
env: env:
ARCH: x86_64 ARCH: x86_64
OS: macos OS: macos
runs-on: macos-14-large # macos-13 runs on x86 CPU. see
# https://github.com/actions/runner-images?tab=readme-ov-file
# If we want to build for macos-14 or superior, we need to switch to
# macos-14-large.
# No need for now, but maybe we will need it in the short term.
runs-on: macos-13
timeout-minutes: 15 timeout-minutes: 15
steps: steps:
@@ -157,7 +159,7 @@ jobs:
arch: ${{env.ARCH}} arch: ${{env.ARCH}}
- name: zig build - name: zig build
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary - name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }} run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -172,4 +174,4 @@ jobs:
with: with:
allowUpdates: true allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }} artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: ${{ env.RELEASE }} tag: nightly

View File

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

View File

@@ -1,68 +0,0 @@
name: e2e-integration-test
env:
LIGHTPANDA_DISABLE_TELEMETRY: true
on:
schedule:
- cron: "4 4 * * *"
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
zig-build-release:
name: zig build release
runs-on: ubuntu-latest
timeout-minutes: 15
# Don't run the CI with draft PR.
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive
- uses: ./.github/actions/install
- name: zig build release
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: upload artifact
uses: actions/upload-artifact@v4
with:
name: lightpanda-build-release
path: |
zig-out/bin/lightpanda
retention-days: 1
demo-scripts:
name: demo-integration-scripts
needs: zig-build-release
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
repository: 'lightpanda-io/demo'
fetch-depth: 0
- run: npm install
- name: download artifact
uses: actions/download-artifact@v4
with:
name: lightpanda-build-release
- run: chmod a+x ./lightpanda
- name: run end to end integration tests
run: |
./lightpanda serve & echo $! > LPD.pid
go run integration/main.go
kill `cat LPD.pid`

View File

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

View File

@@ -22,15 +22,16 @@ jobs:
timeout-minutes: 90 timeout-minutes: 90
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive submodules: recursive
- uses: ./.github/actions/install - uses: ./.github/actions/install
- name: json output - name: json output
run: zig build wpt -- --json > wpt.json run: zig build -Doptimize=ReleaseFast wpt -- --json > wpt.json
- name: write commit - name: write commit
run: | run: |

View File

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

View File

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

1
.gitignore vendored
View File

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

6
.gitmodules vendored
View File

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

View File

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

View File

@@ -34,7 +34,7 @@ endif
## Display this help screen ## Display this help screen
help: help:
@printf "\033[36m%-35s %s\033[0m\n" "Command" "Usage" @printf "\e[36m%-35s %s\e[0m\n" "Command" "Usage"
@sed -n -e '/^## /{'\ @sed -n -e '/^## /{'\
-e 's/## //g;'\ -e 's/## //g;'\
-e 'h;'\ -e 'h;'\
@@ -47,60 +47,77 @@ help:
# $(ZIG) commands # $(ZIG) commands
# ------------ # ------------
.PHONY: build build-dev run run-release shell test bench wpt data end2end .PHONY: build build-dev run run-release shell test bench download-zig wpt data get-v8 build-v8 build-v8-dev
.PHONY: end2end
zig_version = $(shell grep 'recommended_zig_version = "' "vendor/zig-js-runtime/build.zig" | cut -d'"' -f2)
## Download the zig recommended version
download-zig:
$(eval url = "https://ziglang.org/download/$(zig_version)/zig-$(OS)-$(ARCH)-$(zig_version).tar.xz")
$(eval dest = "/tmp/zig-$(OS)-$(ARCH)-$(zig_version).tar.xz")
@printf "\e[36mDownload zig version $(zig_version)...\e[0m\n"
@curl -o "$(dest)" -L "$(url)" || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@printf "\e[33mDownloaded $(dest)\e[0m\n"
## Build in release-safe mode ## Build in release-safe mode
build: build:
@printf "\033[36mBuilding (release safe)...\033[0m\n" @printf "\e[36mBuilding (release safe)...\e[0m\n"
$(ZIG) build -Doptimize=ReleaseSafe -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;) $(ZIG) build -Doptimize=ReleaseSafe -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@printf "\033[33mBuild OK\033[0m\n" @printf "\e[33mBuild OK\e[0m\n"
## Build in debug mode ## Build in debug mode
build-dev: build-dev:
@printf "\033[36mBuilding (debug)...\033[0m\n" @printf "\e[36mBuilding (debug)...\e[0m\n"
@$(ZIG) build -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;) @$(ZIG) build -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@printf "\033[33mBuild OK\033[0m\n" @printf "\e[33mBuild OK\e[0m\n"
## Run the server in release mode ## Run the server in release mode
run: build run: build
@printf "\033[36mRunning...\033[0m\n" @printf "\e[36mRunning...\e[0m\n"
@./zig-out/bin/lightpanda || (printf "\033[33mRun ERROR\033[0m\n"; exit 1;) @./zig-out/bin/lightpanda || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
## Run the server in debug mode ## Run the server in debug mode
run-debug: build-dev run-debug: build-dev
@printf "\033[36mRunning...\033[0m\n" @printf "\e[36mRunning...\e[0m\n"
@./zig-out/bin/lightpanda || (printf "\033[33mRun ERROR\033[0m\n"; exit 1;) @./zig-out/bin/lightpanda || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
## Run a JS shell in debug mode ## Run a JS shell in debug mode
shell: shell:
@printf "\033[36mBuilding shell...\033[0m\n" @printf "\e[36mBuilding shell...\e[0m\n"
@$(ZIG) build shell || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;) @$(ZIG) build shell || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
## Run WPT tests ## Run WPT tests
wpt: wpt:
@printf "\033[36mBuilding wpt...\033[0m\n" @printf "\e[36mBuilding wpt...\e[0m\n"
@$(ZIG) build wpt -- $(filter-out $@,$(MAKECMDGOALS)) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;) @$(ZIG) build wpt -- $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
wpt-summary: wpt-summary:
@printf "\033[36mBuilding wpt...\033[0m\n" @printf "\e[36mBuilding wpt...\e[0m\n"
@$(ZIG) build wpt -- --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;) @$(ZIG) build wpt -- --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
## Test - `grep` is used to filter out the huge compile command on build ## Test
ifeq ($(OS), macos)
test: test:
@script -q /dev/null sh -c 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace --summary all' 2>&1 \ @TEST_FILTER='${F}' $(ZIG) build test -freference-trace --summary all
| grep --line-buffered -v "^/.*zig test -freference-trace"
else
test:
@script -qec 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace --summary all' /dev/null 2>&1 \
| grep --line-buffered -v "^/.*zig test -freference-trace"
endif
## Run demo/runner end to end tests ## Run demo/runner end to end tests
end2end: end2end:
@test -d ../demo @test -d ../demo
cd ../demo && go run runner/main.go cd ../demo && go run runner/main.go
## v8
get-v8:
@printf "\e[36mGetting v8 source...\e[0m\n"
@$(ZIG) build get-v8
build-v8-dev:
@printf "\e[36mBuilding v8 (dev)...\e[0m\n"
@$(ZIG) build build-v8
build-v8:
@printf "\e[36mBuilding v8...\e[0m\n"
@$(ZIG) build -Doptimize=ReleaseSafe build-v8
# Install and build required dependencies commands # Install and build required dependencies commands
# ------------ # ------------
.PHONY: install-submodule .PHONY: install-submodule
@@ -127,27 +144,27 @@ ICONV := $(BC)vendor/libiconv/out/$(OS)-$(ARCH)
# TODO: this way of linking libiconv is not ideal. We should have a more generic way # TODO: this way of linking libiconv is not ideal. We should have a more generic way
# and stick to a specif version. Maybe build from source. Anyway not now. # and stick to a specif version. Maybe build from source. Anyway not now.
_install-netsurf: clean-netsurf _install-netsurf: clean-netsurf
@printf "\033[36mInstalling NetSurf...\033[0m\n" && \ @printf "\e[36mInstalling NetSurf...\e[0m\n" && \
ls $(ICONV)/lib/libiconv.a 1> /dev/null || (printf "\033[33mERROR: you need to execute 'make install-libiconv'\033[0m\n"; exit 1;) && \ ls $(ICONV)/lib/libiconv.a 1> /dev/null || (printf "\e[33mERROR: you need to execute 'make install-libiconv'\e[0m\n"; exit 1;) && \
mkdir -p $(BC_NS) && \ mkdir -p $(BC_NS) && \
cp -R vendor/netsurf/share $(BC_NS) && \ cp -R vendor/netsurf/share $(BC_NS) && \
export PREFIX=$(BC_NS) && \ export PREFIX=$(BC_NS) && \
export OPTLDFLAGS="-L$(ICONV)/lib" && \ export OPTLDFLAGS="-L$(ICONV)/lib" && \
export OPTCFLAGS="$(OPTCFLAGS) -I$(ICONV)/include" && \ export OPTCFLAGS="$(OPTCFLAGS) -I$(ICONV)/include" && \
printf "\033[33mInstalling libwapcaplet...\033[0m\n" && \ printf "\e[33mInstalling libwapcaplet...\e[0m\n" && \
cd vendor/netsurf/libwapcaplet && \ cd vendor/netsurf/libwapcaplet && \
BUILDDIR=$(BC_NS)/build/libwapcaplet make install && \ BUILDDIR=$(BC_NS)/build/libwapcaplet make install && \
cd ../libparserutils && \ cd ../libparserutils && \
printf "\033[33mInstalling libparserutils...\033[0m\n" && \ printf "\e[33mInstalling libparserutils...\e[0m\n" && \
BUILDDIR=$(BC_NS)/build/libparserutils make install && \ BUILDDIR=$(BC_NS)/build/libparserutils make install && \
cd ../libhubbub && \ cd ../libhubbub && \
printf "\033[33mInstalling libhubbub...\033[0m\n" && \ printf "\e[33mInstalling libhubbub...\e[0m\n" && \
BUILDDIR=$(BC_NS)/build/libhubbub make install && \ BUILDDIR=$(BC_NS)/build/libhubbub make install && \
rm src/treebuilder/autogenerated-element-type.c && \ rm src/treebuilder/autogenerated-element-type.c && \
cd ../libdom && \ cd ../libdom && \
printf "\033[33mInstalling libdom...\033[0m\n" && \ printf "\e[33mInstalling libdom...\e[0m\n" && \
BUILDDIR=$(BC_NS)/build/libdom make install && \ BUILDDIR=$(BC_NS)/build/libdom make install && \
printf "\033[33mRunning libdom example...\033[0m\n" && \ printf "\e[33mRunning libdom example...\e[0m\n" && \
cd examples && \ cd examples && \
$(ZIG) cc \ $(ZIG) cc \
-I$(ICONV)/include \ -I$(ICONV)/include \
@@ -164,14 +181,14 @@ _install-netsurf: clean-netsurf
$(ICONV)/lib/libiconv.a && \ $(ICONV)/lib/libiconv.a && \
./a.out > /dev/null && \ ./a.out > /dev/null && \
rm a.out && \ rm a.out && \
printf "\033[36mDone NetSurf $(OS)\033[0m\n" printf "\e[36mDone NetSurf $(OS)\e[0m\n"
clean-netsurf: clean-netsurf:
@printf "\033[36mCleaning NetSurf build...\033[0m\n" && \ @printf "\e[36mCleaning NetSurf build...\e[0m\n" && \
rm -Rf $(BC_NS) rm -Rf $(BC_NS)
test-netsurf: test-netsurf:
@printf "\033[36mTesting NetSurf...\033[0m\n" && \ @printf "\e[36mTesting NetSurf...\e[0m\n" && \
export PREFIX=$(BC_NS) && \ export PREFIX=$(BC_NS) && \
export LDFLAGS="-L$(ICONV)/lib -L$(BC_NS)/lib" && \ export LDFLAGS="-L$(ICONV)/lib -L$(BC_NS)/lib" && \
export CFLAGS="-I$(ICONV)/include -I$(BC_NS)/include" && \ export CFLAGS="-I$(ICONV)/include -I$(BC_NS)/include" && \

View File

@@ -18,7 +18,7 @@ Lightpanda is the open-source browser made for headless usage:
- Javascript execution - Javascript execution
- Support of Web APIs (partial, WIP) - Support of Web APIs (partial, WIP)
- Compatible with Playwright[^1], Puppeteer, chromedp through [CDP](https://chromedevtools.github.io/devtools-protocol/) - Compatible with Playwright[^1], Puppeteer, chromedp through CDP
Fast web automation for AI agents, LLM training, scraping and testing: Fast web automation for AI agents, LLM training, scraping and testing:
@@ -158,21 +158,22 @@ Here are the key features we have implemented:
NOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time. NOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time.
You can also follow the progress of our Javascript support in our dedicated [zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime#development) project.
## Build from sources ## Build from sources
### Prerequisites ### Prerequisites
Lightpanda is written with [Zig](https://ziglang.org/) `0.15.2`. You have to Lightpanda is written with [Zig](https://ziglang.org/) `0.15.1`. You have to
install it with the right version in order to build the project. install it with the right version in order to build the project.
Lightpanda also depends on Lightpanda also depends on
[zig-v8-fork](https://github.com/lightpanda-io/zig-v8-fork/), [zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime/) (with v8),
[Libcurl](https://curl.se/libcurl/), [Libcurl](https://curl.se/libcurl/),
[Brotli](https://github.com/google/brotli),
[Netsurf libs](https://www.netsurf-browser.org/) and [Netsurf libs](https://www.netsurf-browser.org/) and
[Mimalloc](https://microsoft.github.io/mimalloc). [Mimalloc](https://microsoft.github.io/mimalloc).
To be able to build the v8 engine, you have to install some libs: To be able to build the v8 engine for zig-js-runtime, you have to install some libs:
For Debian/Ubuntu based Linux: For Debian/Ubuntu based Linux:
@@ -189,10 +190,10 @@ For systems with [Nix](https://nixos.org/download/), you can use the devShell:
nix develop nix develop
``` ```
For MacOS, you need [Xcode](https://developer.apple.com/xcode/) and the following pacakges from homebrew: For MacOS, you only need cmake:
``` ```
brew install cmake pkgconf brew install cmake
``` ```
### Install and build dependencies ### Install and build dependencies
@@ -245,6 +246,22 @@ Note: when Mimalloc is built in dev mode, you can dump memory stats with the
env var `MIMALLOC_SHOW_STATS=1`. See env var `MIMALLOC_SHOW_STATS=1`. See
[https://microsoft.github.io/mimalloc/environment.html](https://microsoft.github.io/mimalloc/environment.html). [https://microsoft.github.io/mimalloc/environment.html](https://microsoft.github.io/mimalloc/environment.html).
**v8**
First, get the tools necessary for building V8, as well as the V8 source code:
```
make get-v8
```
Next, build v8. This build task is very long and cpu consuming, as you will build v8 from sources.
```
make build-v8
```
For dev env, use `make build-v8-dev`.
## Test ## Test
### Unit Tests ### Unit Tests

198
build.zig
View File

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

View File

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

12
flake.lock generated
View File

@@ -75,11 +75,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1760968520, "lastModified": 1756822655,
"narHash": "sha256-EjGslHDzCBKOVr+dnDB1CAD7wiQSHfUt3suOpFj9O1Q=", "narHash": "sha256-xQAk8xLy7srAkR5NMZFsQFioL02iTHuuEIs3ohGpgdk=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "e755547441a0413942a37692f7bf7fc6315bb7f6", "rev": "4bdac60bfe32c41103ae500ddf894c258291dd61",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -136,11 +136,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1760747435, "lastModified": 1756555914,
"narHash": "sha256-wNB/W3x+or4mdNxFPNOH5/WFckNpKgFRZk7OnOsLtm0=", "narHash": "sha256-7yoSPIVEuL+3Wzf6e7NHuW3zmruHizRrYhGerjRHTLI=",
"owner": "mitchellh", "owner": "mitchellh",
"repo": "zig-overlay", "repo": "zig-overlay",
"rev": "d0f239b887b1ac736c0f3dde91bf5bf2ecf3a420", "rev": "d0df3a2fd0f11134409d6d5ea0e510e5e477f7d6",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -49,7 +49,7 @@
targetPkgs = targetPkgs =
pkgs: with pkgs; [ pkgs: with pkgs; [
# Build Tools # Build Tools
zigpkgs."0.15.2" zigpkgs."0.15.1"
zls zls
python3 python3
pkg-config pkg-config

View File

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

View File

@@ -4,7 +4,7 @@ const Allocator = std.mem.Allocator;
const log = @import("log.zig"); const log = @import("log.zig");
const Http = @import("http/Http.zig"); const Http = @import("http/Http.zig");
const Platform = @import("browser/js/Platform.zig"); const Platform = @import("runtime/js.zig").Platform;
const Telemetry = @import("telemetry/telemetry.zig").Telemetry; const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
const Notification = @import("notification.zig").Notification; const Notification = @import("notification.zig").Notification;
@@ -19,7 +19,6 @@ pub const App = struct {
telemetry: Telemetry, telemetry: Telemetry,
app_dir_path: ?[]const u8, app_dir_path: ?[]const u8,
notification: *Notification, notification: *Notification,
shutdown: bool = false,
pub const RunMode = enum { pub const RunMode = enum {
help, help,
@@ -37,7 +36,6 @@ pub const App = struct {
http_connect_timeout_ms: ?u31 = null, http_connect_timeout_ms: ?u31 = null,
http_max_host_open: ?u8 = null, http_max_host_open: ?u8 = null,
http_max_concurrent: ?u8 = null, http_max_concurrent: ?u8 = null,
user_agent: [:0]const u8,
}; };
pub fn init(allocator: Allocator, config: Config) !*App { pub fn init(allocator: Allocator, config: Config) !*App {
@@ -55,7 +53,6 @@ pub const App = struct {
.http_proxy = config.http_proxy, .http_proxy = config.http_proxy,
.tls_verify_host = config.tls_verify_host, .tls_verify_host = config.tls_verify_host,
.proxy_bearer_token = config.proxy_bearer_token, .proxy_bearer_token = config.proxy_bearer_token,
.user_agent = config.user_agent,
}); });
errdefer http.deinit(); errdefer http.deinit();
@@ -83,14 +80,9 @@ pub const App = struct {
} }
pub fn deinit(self: *App) void { pub fn deinit(self: *App) void {
if (@atomicRmw(bool, &self.shutdown, .Xchg, true, .monotonic)) {
return;
}
const allocator = self.allocator; const allocator = self.allocator;
if (self.app_dir_path) |app_dir_path| { if (self.app_dir_path) |app_dir_path| {
allocator.free(app_dir_path); allocator.free(app_dir_path);
self.app_dir_path = null;
} }
self.telemetry.deinit(); self.telemetry.deinit();
self.notification.deinit(); self.notification.deinit();

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const log = @import("../log.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Scheduler = @This(); const Scheduler = @This();
@@ -37,10 +38,8 @@ pub fn init(allocator: Allocator) Scheduler {
} }
pub fn reset(self: *Scheduler) void { pub fn reset(self: *Scheduler) void {
// Our allocator is the page arena, it's been reset. We cannot use self.high_priority.clearRetainingCapacity();
// clearAndRetainCapacity, since that space is no longer ours self.low_priority.clearRetainingCapacity();
self.high_priority.clearAndFree();
self.low_priority.clearAndFree();
} }
const AddOpts = struct { const AddOpts = struct {

File diff suppressed because it is too large Load Diff

View File

@@ -1,189 +0,0 @@
const std = @import("std");
const log = @import("../log.zig");
const parser = @import("netsurf.zig");
const collection = @import("dom/html_collection.zig");
const Page = @import("page.zig").Page;
const SlotChangeMonitor = @This();
page: *Page,
event_node: parser.EventNode,
slots_changed: std.ArrayList(*parser.Slot),
// Monitors the document in order to trigger slotchange events.
pub fn init(page: *Page) !*SlotChangeMonitor {
// on the heap, we need a stable address for event_node
const self = try page.arena.create(SlotChangeMonitor);
self.* = .{
.page = page,
.slots_changed = .empty,
.event_node = .{ .func = mutationCallback },
};
const root = parser.documentToNode(parser.documentHTMLToDocument(page.window.document));
_ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, root),
"DOMNodeInserted",
&self.event_node,
false,
);
_ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, root),
"DOMNodeRemoved",
&self.event_node,
false,
);
_ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, root),
"DOMAttrModified",
&self.event_node,
false,
);
return self;
}
// Given a element, finds its slot, if any.
pub fn findSlot(element: *parser.Element, page: *const Page) !?*parser.Slot {
const target_name = (try parser.elementGetAttribute(element, "slot")) orelse return null;
return findNamedSlot(element, target_name, page);
}
// Given an element and a name, find the slo, if any. This is only useful for
// MutationEvents where findSlot is unreliable because parser.elementGetAttribute(element, "slot")
// could return the new or old value.
fn findNamedSlot(element: *parser.Element, target_name: []const u8, page: *const Page) !?*parser.Slot {
// I believe elements need to be added as direct descendents of the host,
// so we don't need to go find the host, we just grab the parent.
const host = parser.nodeParentNode(@ptrCast(element)) orelse return null;
const state = page.getNodeState(host) orelse return null;
const shadow_root = state.shadow_root orelse return null;
// if we're here, we found a host, now find the slot
var nodes = collection.HTMLCollectionByTagName(
@ptrCast(@alignCast(shadow_root.proto)),
"slot",
.{ .include_root = false },
);
for (0..1000) |i| {
const n = (try nodes.item(@intCast(i))) orelse return null;
const slot_name = (try parser.elementGetAttribute(@ptrCast(n), "name")) orelse "";
if (std.mem.eql(u8, target_name, slot_name)) {
return @ptrCast(n);
}
}
return null;
}
// Event callback from the mutation event, signaling either the addition of
// a node, removal of a node, or a change in attribute
fn mutationCallback(en: *parser.EventNode, event: *parser.Event) void {
const mutation_event = parser.eventToMutationEvent(event);
const self: *SlotChangeMonitor = @fieldParentPtr("event_node", en);
self._mutationCallback(mutation_event) catch |err| {
log.err(.web_api, "slot change callback", .{ .err = err });
};
}
fn _mutationCallback(self: *SlotChangeMonitor, event: *parser.MutationEvent) !void {
const event_type = parser.eventType(@ptrCast(event));
if (std.mem.eql(u8, event_type, "DOMNodeInserted")) {
const event_target = parser.eventTarget(@ptrCast(event)) orelse return;
return self.nodeAddedOrRemoved(@ptrCast(event_target));
}
if (std.mem.eql(u8, event_type, "DOMNodeRemoved")) {
const event_target = parser.eventTarget(@ptrCast(event)) orelse return;
return self.nodeAddedOrRemoved(@ptrCast(event_target));
}
if (std.mem.eql(u8, event_type, "DOMAttrModified")) {
const attribute_name = try parser.mutationEventAttributeName(event);
if (std.mem.eql(u8, attribute_name, "slot") == false) {
return;
}
const new_value = parser.mutationEventNewValue(event);
const prev_value = parser.mutationEventPrevValue(event);
const event_target = parser.eventTarget(@ptrCast(event)) orelse return;
return self.nodeAttributeChanged(@ptrCast(event_target), new_value, prev_value);
}
}
// A node was removed or added. If it's an element, and if it has a slot attribute
// then we'll dispatch a slotchange event.
fn nodeAddedOrRemoved(self: *SlotChangeMonitor, node: *parser.Node) !void {
if (parser.nodeType(node) != .element) {
return;
}
const el: *parser.Element = @ptrCast(node);
if (try findSlot(el, self.page)) |slot| {
return self.scheduleSlotChange(slot);
}
}
// An attribute was modified. If the attribute is "slot", then we'll trigger 1
// slotchange for the old slot (if there was one) and 1 slotchange for the new
// one (if there is one)
fn nodeAttributeChanged(self: *SlotChangeMonitor, node: *parser.Node, new_value: ?[]const u8, prev_value: ?[]const u8) !void {
if (parser.nodeType(node) != .element) {
return;
}
const el: *parser.Element = @ptrCast(node);
if (try findNamedSlot(el, prev_value orelse "", self.page)) |slot| {
try self.scheduleSlotChange(slot);
}
if (try findNamedSlot(el, new_value orelse "", self.page)) |slot| {
try self.scheduleSlotChange(slot);
}
}
// OK. Our MutationEvent is not a MutationObserver - it's an older, deprecated
// API. It gets dispatched in the middle of the change. While I'm sure it has
// some rules, from our point of view, it fires too early. DOMAttrModified fires
// before the attribute is actually updated and DOMNodeRemoved before the node
// is actually removed. This is a problem if the callback will call
// `slot.assignedNodes`, since that won't return the new state.
// So, we use the page schedule to schedule the dispatching of the slotchange
// event.
fn scheduleSlotChange(self: *SlotChangeMonitor, slot: *parser.Slot) !void {
for (self.slots_changed.items) |changed| {
if (slot == changed) {
return;
}
}
try self.slots_changed.append(self.page.arena, slot);
if (self.slots_changed.items.len == 1) {
// first item added, schedule the callback
try self.page.scheduler.add(self, scheduleCallback, 0, .{ .name = "slot change" });
}
}
// Callback from the schedule. Time to dispatch the slotchange event
fn scheduleCallback(ctx: *anyopaque) ?u32 {
var self: *SlotChangeMonitor = @ptrCast(@alignCast(ctx));
self._scheduleCallback() catch |err| {
log.err(.app, "slot change schedule", .{ .err = err });
};
return null;
}
fn _scheduleCallback(self: *SlotChangeMonitor) !void {
for (self.slots_changed.items) |slot| {
const event = try parser.eventCreate();
defer parser.eventDestroy(event);
try parser.eventInit(event, "slotchange", .{});
_ = try parser.eventTargetDispatchEvent(
parser.toEventTarget(parser.Element, @ptrCast(@alignCast(slot))),
event,
);
}
self.slots_changed.clearRetainingCapacity();
}

View File

@@ -26,16 +26,17 @@
// this quickly proved necessary, since different fields are needed on the same // 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. // data at different levels of the prototype chain. This isn't memory efficient.
const js = @import("js/js.zig"); const Env = @import("env.zig").Env;
const parser = @import("netsurf.zig"); const parser = @import("netsurf.zig");
const DataSet = @import("html/DataSet.zig"); const DataSet = @import("html/DataSet.zig");
const ShadowRoot = @import("dom/shadow_root.zig").ShadowRoot; const ShadowRoot = @import("dom/shadow_root.zig").ShadowRoot;
const StyleSheet = @import("cssom/StyleSheet.zig"); const StyleSheet = @import("cssom/StyleSheet.zig");
const CSSStyleSheet = @import("cssom/CSSStyleSheet.zig");
const CSSStyleDeclaration = @import("cssom/CSSStyleDeclaration.zig"); const CSSStyleDeclaration = @import("cssom/CSSStyleDeclaration.zig");
// for HTMLScript (but probably needs to be added to more) // for HTMLScript (but probably needs to be added to more)
onload: ?js.Function = null, onload: ?Env.Function = null,
onerror: ?js.Function = null, onerror: ?Env.Function = null,
// for HTMLElement // for HTMLElement
style: CSSStyleDeclaration = .empty, style: CSSStyleDeclaration = .empty,
@@ -53,7 +54,7 @@ style_sheet: ?*StyleSheet = null,
// for dom/document // for dom/document
active_element: ?*parser.Element = null, active_element: ?*parser.Element = null,
adopted_style_sheets: ?js.Object = null, adopted_style_sheets: ?Env.JsObject = null,
// for HTMLSelectElement // for HTMLSelectElement
// By default, if no option is explicitly selected, the first option should // By default, if no option is explicitly selected, the first option should

View File

@@ -21,8 +21,8 @@ const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator; const ArenaAllocator = std.heap.ArenaAllocator;
const js = @import("js/js.zig");
const State = @import("State.zig"); const State = @import("State.zig");
const Env = @import("env.zig").Env;
const App = @import("../app.zig").App; const App = @import("../app.zig").App;
const Session = @import("session.zig").Session; const Session = @import("session.zig").Session;
const Notification = @import("../notification.zig").Notification; const Notification = @import("../notification.zig").Notification;
@@ -34,12 +34,11 @@ const HttpClient = @import("../http/Client.zig");
// You can create multiple browser instances. // You can create multiple browser instances.
// A browser contains only one session. // A browser contains only one session.
pub const Browser = struct { pub const Browser = struct {
env: *js.Env, env: *Env,
app: *App, app: *App,
session: ?Session, session: ?Session,
allocator: Allocator, allocator: Allocator,
http_client: *HttpClient, http_client: *HttpClient,
call_arena: ArenaAllocator,
page_arena: ArenaAllocator, page_arena: ArenaAllocator,
session_arena: ArenaAllocator, session_arena: ArenaAllocator,
transfer_arena: ArenaAllocator, transfer_arena: ArenaAllocator,
@@ -49,7 +48,7 @@ pub const Browser = struct {
pub fn init(app: *App) !Browser { pub fn init(app: *App) !Browser {
const allocator = app.allocator; const allocator = app.allocator;
const env = try js.Env.init(allocator, &app.platform, .{}); const env = try Env.init(allocator, &app.platform, .{});
errdefer env.deinit(); errdefer env.deinit();
const notification = try Notification.init(allocator, app.notification); const notification = try Notification.init(allocator, app.notification);
@@ -64,7 +63,6 @@ pub const Browser = struct {
.allocator = allocator, .allocator = allocator,
.notification = notification, .notification = notification,
.http_client = app.http.client, .http_client = app.http.client,
.call_arena = ArenaAllocator.init(allocator),
.page_arena = ArenaAllocator.init(allocator), .page_arena = ArenaAllocator.init(allocator),
.session_arena = ArenaAllocator.init(allocator), .session_arena = ArenaAllocator.init(allocator),
.transfer_arena = ArenaAllocator.init(allocator), .transfer_arena = ArenaAllocator.init(allocator),
@@ -75,7 +73,6 @@ pub const Browser = struct {
pub fn deinit(self: *Browser) void { pub fn deinit(self: *Browser) void {
self.closeSession(); self.closeSession();
self.env.deinit(); self.env.deinit();
self.call_arena.deinit();
self.page_arena.deinit(); self.page_arena.deinit();
self.session_arena.deinit(); self.session_arena.deinit();
self.transfer_arena.deinit(); self.transfer_arena.deinit();

View File

@@ -1,58 +0,0 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const color = @import("../cssom/color.zig");
const Page = @import("../page.zig").Page;
/// This class doesn't implement a `constructor`.
/// It can be obtained with a call to `HTMLCanvasElement#getContext`.
/// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D
const CanvasRenderingContext2D = @This();
/// Fill color.
/// TODO: Add support for `CanvasGradient` and `CanvasPattern`.
fill_style: color.RGBA = color.RGBA.Named.black,
pub fn _fillRect(
self: *const CanvasRenderingContext2D,
x: f64,
y: f64,
width: f64,
height: f64,
) void {
_ = self;
_ = x;
_ = y;
_ = width;
_ = height;
}
pub fn get_fillStyle(self: *const CanvasRenderingContext2D, page: *Page) ![]const u8 {
var w = std.Io.Writer.Allocating.init(page.call_arena);
try self.fill_style.format(&w.writer);
return w.written();
}
pub fn set_fillStyle(
self: *CanvasRenderingContext2D,
value: []const u8,
) !void {
// Prefer the same fill_style if fails.
self.fill_style = color.RGBA.parse(value) catch self.fill_style;
}

View File

@@ -1,145 +0,0 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const WebGLRenderingContext = @This();
_: u8 = 0,
/// On Chrome and Safari, a call to `getSupportedExtensions` returns total of 39.
/// The reference for it lists lesser number of extensions:
/// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Using_Extensions#extension_list
pub const Extension = union(enum) {
ANGLE_instanced_arrays: void,
EXT_blend_minmax: void,
EXT_clip_control: void,
EXT_color_buffer_half_float: void,
EXT_depth_clamp: void,
EXT_disjoint_timer_query: void,
EXT_float_blend: void,
EXT_frag_depth: void,
EXT_polygon_offset_clamp: void,
EXT_shader_texture_lod: void,
EXT_texture_compression_bptc: void,
EXT_texture_compression_rgtc: void,
EXT_texture_filter_anisotropic: void,
EXT_texture_mirror_clamp_to_edge: void,
EXT_sRGB: void,
KHR_parallel_shader_compile: void,
OES_element_index_uint: void,
OES_fbo_render_mipmap: void,
OES_standard_derivatives: void,
OES_texture_float: void,
OES_texture_float_linear: void,
OES_texture_half_float: void,
OES_texture_half_float_linear: void,
OES_vertex_array_object: void,
WEBGL_blend_func_extended: void,
WEBGL_color_buffer_float: void,
WEBGL_compressed_texture_astc: void,
WEBGL_compressed_texture_etc: void,
WEBGL_compressed_texture_etc1: void,
WEBGL_compressed_texture_pvrtc: void,
WEBGL_compressed_texture_s3tc: void,
WEBGL_compressed_texture_s3tc_srgb: void,
WEBGL_debug_renderer_info: Type.WEBGL_debug_renderer_info,
WEBGL_debug_shaders: void,
WEBGL_depth_texture: void,
WEBGL_draw_buffers: void,
WEBGL_lose_context: Type.WEBGL_lose_context,
WEBGL_multi_draw: void,
WEBGL_polygon_mode: void,
/// Reified enum type from the fields of this union.
const Kind = blk: {
const info = @typeInfo(Extension).@"union";
const fields = info.fields;
var items: [fields.len]std.builtin.Type.EnumField = undefined;
for (fields, 0..) |field, i| {
items[i] = .{ .name = field.name, .value = i };
}
break :blk @Type(.{
.@"enum" = .{
.tag_type = std.math.IntFittingRange(0, if (fields.len == 0) 0 else fields.len - 1),
.fields = &items,
.decls = &.{},
.is_exhaustive = true,
},
});
};
/// Returns the `Extension.Kind` by its name.
fn find(name: []const u8) ?Kind {
// Just to make you really sad, this function has to be case-insensitive.
// So here we copy what's being done in `std.meta.stringToEnum` but replace
// the comparison function.
const kvs = comptime build_kvs: {
const T = Extension.Kind;
const EnumKV = struct { []const u8, T };
var kvs_array: [@typeInfo(T).@"enum".fields.len]EnumKV = undefined;
for (@typeInfo(T).@"enum".fields, 0..) |enumField, i| {
kvs_array[i] = .{ enumField.name, @field(T, enumField.name) };
}
break :build_kvs kvs_array[0..];
};
const Map = std.StaticStringMapWithEql(Extension.Kind, std.static_string_map.eqlAsciiIgnoreCase);
const map = Map.initComptime(kvs);
return map.get(name);
}
/// Extension types.
pub const Type = struct {
pub const WEBGL_debug_renderer_info = struct {
pub const UNMASKED_VENDOR_WEBGL: u64 = 0x9245;
pub const UNMASKED_RENDERER_WEBGL: u64 = 0x9246;
pub fn get_UNMASKED_VENDOR_WEBGL() u64 {
return UNMASKED_VENDOR_WEBGL;
}
pub fn get_UNMASKED_RENDERER_WEBGL() u64 {
return UNMASKED_RENDERER_WEBGL;
}
};
pub const WEBGL_lose_context = struct {
_: u8 = 0,
pub fn _loseContext(_: *const WEBGL_lose_context) void {}
pub fn _restoreContext(_: *const WEBGL_lose_context) void {}
};
};
};
/// Enables a WebGL extension.
pub fn _getExtension(self: *const WebGLRenderingContext, name: []const u8) ?Extension {
_ = self;
const tag = Extension.find(name) orelse return null;
return switch (tag) {
.WEBGL_debug_renderer_info => @unionInit(Extension, "WEBGL_debug_renderer_info", .{}),
.WEBGL_lose_context => @unionInit(Extension, "WEBGL_lose_context", .{}),
inline else => |comptime_enum| @unionInit(Extension, @tagName(comptime_enum), {}),
};
}
/// Returns a list of all the supported WebGL extensions.
pub fn _getSupportedExtensions(_: *const WebGLRenderingContext) []const []const u8 {
return std.meta.fieldNames(Extension.Kind);
}

View File

@@ -1,13 +0,0 @@
//! Canvas API.
//! https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API
const CanvasRenderingContext2D = @import("CanvasRenderingContext2D.zig");
const WebGLRenderingContext = @import("WebGLRenderingContext.zig");
const Extension = WebGLRenderingContext.Extension;
pub const Interfaces = .{
CanvasRenderingContext2D,
WebGLRenderingContext,
Extension.Type.WEBGL_debug_renderer_info,
Extension.Type.WEBGL_lose_context,
};

View File

@@ -20,47 +20,48 @@ const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const js = @import("../js/js.zig"); const Allocator = std.mem.Allocator;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const JsObject = @import("../env.zig").Env.JsObject;
pub const Console = struct { pub const Console = struct {
// TODO: configurable writer // TODO: configurable writer
timers: std.StringHashMapUnmanaged(u32) = .{}, timers: std.StringHashMapUnmanaged(u32) = .{},
counts: std.StringHashMapUnmanaged(u32) = .{}, counts: std.StringHashMapUnmanaged(u32) = .{},
pub fn _lp(values: []js.Object, page: *Page) !void { pub fn _lp(values: []JsObject, page: *Page) !void {
if (values.len == 0) { if (values.len == 0) {
return; return;
} }
log.fatal(.console, "lightpanda", .{ .args = try serializeValues(values, page) }); log.fatal(.console, "lightpanda", .{ .args = try serializeValues(values, page) });
} }
pub fn _log(values: []js.Object, page: *Page) !void { pub fn _log(values: []JsObject, page: *Page) !void {
if (values.len == 0) { if (values.len == 0) {
return; return;
} }
log.info(.console, "info", .{ .args = try serializeValues(values, page) }); log.info(.console, "info", .{ .args = try serializeValues(values, page) });
} }
pub fn _info(values: []js.Object, page: *Page) !void { pub fn _info(values: []JsObject, page: *Page) !void {
return _log(values, page); return _log(values, page);
} }
pub fn _debug(values: []js.Object, page: *Page) !void { pub fn _debug(values: []JsObject, page: *Page) !void {
if (values.len == 0) { if (values.len == 0) {
return; return;
} }
log.debug(.console, "debug", .{ .args = try serializeValues(values, page) }); log.debug(.console, "debug", .{ .args = try serializeValues(values, page) });
} }
pub fn _warn(values: []js.Object, page: *Page) !void { pub fn _warn(values: []JsObject, page: *Page) !void {
if (values.len == 0) { if (values.len == 0) {
return; return;
} }
log.warn(.console, "warn", .{ .args = try serializeValues(values, page) }); log.warn(.console, "warn", .{ .args = try serializeValues(values, page) });
} }
pub fn _error(values: []js.Object, page: *Page) !void { pub fn _error(values: []JsObject, page: *Page) !void {
if (values.len == 0) { if (values.len == 0) {
return; return;
} }
@@ -71,16 +72,6 @@ pub const Console = struct {
}); });
} }
pub fn _trace(values: []js.Object, page: *Page) !void {
if (values.len == 0) {
return;
}
log.debug(.console, "debug", .{
.stack = page.js.stackTrace() catch "???",
.args = try serializeValues(values, page),
});
}
pub fn _clear() void {} pub fn _clear() void {}
pub fn _count(self: *Console, label_: ?[]const u8, page: *Page) !void { pub fn _count(self: *Console, label_: ?[]const u8, page: *Page) !void {
@@ -142,7 +133,7 @@ pub const Console = struct {
log.warn(.console, "timer stop", .{ .label = label, .elapsed = elapsed - kv.value }); log.warn(.console, "timer stop", .{ .label = label, .elapsed = elapsed - kv.value });
} }
pub fn _assert(assertion: js.Object, values: []js.Object, page: *Page) !void { pub fn _assert(assertion: JsObject, values: []JsObject, page: *Page) !void {
if (assertion.isTruthy()) { if (assertion.isTruthy()) {
return; return;
} }
@@ -153,7 +144,7 @@ pub const Console = struct {
log.info(.console, "assertion failed", .{ .values = serialized_values }); log.info(.console, "assertion failed", .{ .values = serialized_values });
} }
fn serializeValues(values: []js.Object, page: *Page) ![]const u8 { fn serializeValues(values: []JsObject, page: *Page) ![]const u8 {
if (values.len == 0) { if (values.len == 0) {
return ""; return "";
} }

View File

@@ -17,14 +17,14 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig"); const Env = @import("../env.zig").Env;
const uuidv4 = @import("../../id.zig").uuidv4; const uuidv4 = @import("../../id.zig").uuidv4;
// https://w3c.github.io/webcrypto/#crypto-interface // https://w3c.github.io/webcrypto/#crypto-interface
pub const Crypto = struct { pub const Crypto = struct {
_not_empty: bool = true, _not_empty: bool = true,
pub fn _getRandomValues(_: *const Crypto, js_obj: js.Object) !js.Object { pub fn _getRandomValues(_: *const Crypto, js_obj: Env.JsObject) !Env.JsObject {
var into = try js_obj.toZig(Crypto, "getRandomValues", RandomValues); var into = try js_obj.toZig(Crypto, "getRandomValues", RandomValues);
const buf = into.asBuffer(); const buf = into.asBuffer();
if (buf.len > 65_536) { if (buf.len > 65_536) {

View File

@@ -46,15 +46,17 @@ 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 // 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. // 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: *const Selector, node: anytype, m: anytype) !bool {
var child = node.firstChild(); var c = try node.firstChild();
while (child) |c| { while (true) {
if (try s.match(c)) { if (c == null) break;
try m.match(c);
if (try s.match(c.?)) {
try m.match(c.?);
return true; return true;
} }
if (try matchFirst(s, c, m)) return true; if (try matchFirst(s, c.?, m)) return true;
child = c.nextSibling(); c = try c.?.nextSibling();
} }
return false; return false;
} }
@@ -62,11 +64,13 @@ 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 // matchAll call m.match with the all the nodes that matches the selector s, from the
// descendants of n. // descendants of n.
pub fn matchAll(s: *const Selector, node: anytype, m: anytype) !void { pub fn matchAll(s: *const Selector, node: anytype, m: anytype) !void {
var child = node.firstChild(); var c = try node.firstChild();
while (child) |c| { while (true) {
if (try s.match(c)) try m.match(c); if (c == null) break;
try matchAll(s, c, m);
child = c.nextSibling(); if (try s.match(c.?)) try m.match(c.?);
try matchAll(s, c.?, m);
c = try c.?.nextSibling();
} }
} }

View File

@@ -26,67 +26,71 @@ const Allocator = std.mem.Allocator;
pub const Node = struct { pub const Node = struct {
node: *parser.Node, node: *parser.Node,
pub fn firstChild(n: Node) ?Node { pub fn firstChild(n: Node) !?Node {
const c = parser.nodeFirstChild(n.node); const c = try parser.nodeFirstChild(n.node);
if (c) |cc| return .{ .node = cc }; if (c) |cc| return .{ .node = cc };
return null; return null;
} }
pub fn lastChild(n: Node) ?Node { pub fn lastChild(n: Node) !?Node {
const c = parser.nodeLastChild(n.node); const c = try parser.nodeLastChild(n.node);
if (c) |cc| return .{ .node = cc }; if (c) |cc| return .{ .node = cc };
return null; return null;
} }
pub fn nextSibling(n: Node) ?Node { pub fn nextSibling(n: Node) !?Node {
const c = parser.nodeNextSibling(n.node); const c = try parser.nodeNextSibling(n.node);
if (c) |cc| return .{ .node = cc }; if (c) |cc| return .{ .node = cc };
return null; return null;
} }
pub fn prevSibling(n: Node) ?Node { pub fn prevSibling(n: Node) !?Node {
const c = parser.nodePreviousSibling(n.node); const c = try parser.nodePreviousSibling(n.node);
if (c) |cc| return .{ .node = cc }; if (c) |cc| return .{ .node = cc };
return null; return null;
} }
pub fn parent(n: Node) ?Node { pub fn parent(n: Node) !?Node {
const c = parser.nodeParentNode(n.node); const c = try parser.nodeParentNode(n.node);
if (c) |cc| return .{ .node = cc }; if (c) |cc| return .{ .node = cc };
return null; return null;
} }
pub fn isElement(n: Node) bool { pub fn isElement(n: Node) bool {
return parser.nodeType(n.node) == .element; const t = parser.nodeType(n.node) catch return false;
return t == .element;
} }
pub fn isDocument(n: Node) bool { pub fn isDocument(n: Node) bool {
return parser.nodeType(n.node) == .document; const t = parser.nodeType(n.node) catch return false;
return t == .document;
} }
pub fn isComment(n: Node) bool { pub fn isComment(n: Node) bool {
return parser.nodeType(n.node) == .comment; const t = parser.nodeType(n.node) catch return false;
return t == .comment;
} }
pub fn isText(n: Node) bool { pub fn isText(n: Node) bool {
return parser.nodeType(n.node) == .text; const t = parser.nodeType(n.node) catch return false;
return t == .text;
} }
pub fn text(n: Node) ?[]const u8 { pub fn text(n: Node) !?[]const u8 {
const data = parser.nodeTextContent(n.node); const data = try parser.nodeTextContent(n.node);
if (data == null) return null; if (data == null) return null;
if (data.?.len == 0) return null; if (data.?.len == 0) return null;
return std.mem.trim(u8, data.?, &std.ascii.whitespace); return std.mem.trim(u8, data.?, &std.ascii.whitespace);
} }
pub fn isEmptyText(n: Node) bool { pub fn isEmptyText(n: Node) !bool {
const data = parser.nodeTextContent(n.node); const data = try parser.nodeTextContent(n.node);
if (data == null) return true; if (data == null) return true;
if (data.?.len == 0) return true; if (data.?.len == 0) return true;
@@ -94,7 +98,7 @@ pub const Node = struct {
} }
pub fn tag(n: Node) ![]const u8 { pub fn tag(n: Node) ![]const u8 {
return parser.nodeName(n.node); return try parser.nodeName(n.node);
} }
pub fn attr(n: Node, key: []const u8) !?[]const u8 { pub fn attr(n: Node, key: []const u8) !?[]const u8 {
@@ -136,7 +140,7 @@ const MatcherTest = struct {
test "Browser.CSS.Libdom: matchFirst" { test "Browser.CSS.Libdom: matchFirst" {
const alloc = std.testing.allocator; const alloc = std.testing.allocator;
parser.init(); try parser.init();
defer parser.deinit(); defer parser.deinit();
var matcher = MatcherTest.init(alloc); var matcher = MatcherTest.init(alloc);
@@ -281,7 +285,7 @@ test "Browser.CSS.Libdom: matchFirst" {
test "Browser.CSS.Libdom: matchAll" { test "Browser.CSS.Libdom: matchAll" {
const alloc = std.testing.allocator; const alloc = std.testing.allocator;
parser.init(); try parser.init();
defer parser.deinit(); defer parser.deinit();
var matcher = MatcherTest.init(alloc); var matcher = MatcherTest.init(alloc);

View File

@@ -821,8 +821,7 @@ pub const Parser = struct {
// nameStart returns whether c can be the first character of an identifier // nameStart returns whether c can be the first character of an identifier
// (not counting an initial hyphen, or an escape sequence). // (not counting an initial hyphen, or an escape sequence).
fn nameStart(c: u8) bool { fn nameStart(c: u8) bool {
return 'a' <= c and c <= 'z' or 'A' <= c and c <= 'Z' or c == '_' or c > 127 or return 'a' <= c and c <= 'z' or 'A' <= c and c <= 'Z' or c == '_' or c > 127;
'0' <= c and c <= '9';
} }
// nameChar returns whether c can be a character within an identifier // nameChar returns whether c can be a character within an identifier
@@ -891,7 +890,7 @@ test "parser.parseIdentifier" {
err: bool = false, err: bool = false,
}{ }{
.{ .s = "x", .exp = "x" }, .{ .s = "x", .exp = "x" },
.{ .s = "96", .exp = "96", .err = false }, .{ .s = "96", .exp = "", .err = true },
.{ .s = "-x", .exp = "-x" }, .{ .s = "-x", .exp = "-x" },
.{ .s = "r\\e9 sumé", .exp = "résumé" }, .{ .s = "r\\e9 sumé", .exp = "résumé" },
.{ .s = "r\\0000e9 sumé", .exp = "résumé" }, .{ .s = "r\\0000e9 sumé", .exp = "résumé" },
@@ -976,7 +975,6 @@ test "parser.parse" {
.{ .s = ":root", .exp = .{ .pseudo_class = .root } }, .{ .s = ":root", .exp = .{ .pseudo_class = .root } },
.{ .s = ".\\:bar", .exp = .{ .class = ":bar" } }, .{ .s = ".\\:bar", .exp = .{ .class = ":bar" } },
.{ .s = ".foo\\:bar", .exp = .{ .class = "foo:bar" } }, .{ .s = ".foo\\:bar", .exp = .{ .class = "foo:bar" } },
.{ .s = "[class=75c0fa18a94b9e3a6b8e14d6cbe688a27f5da10a]", .exp = .{ .attribute = .{ .key = "class", .val = "75c0fa18a94b9e3a6b8e14d6cbe688a27f5da10a", .op = .eql } } },
}; };
for (testcases) |tc| { for (testcases) |tc| {

View File

@@ -334,39 +334,41 @@ pub const Selector = union(enum) {
if (!try v.second.match(n)) return false; if (!try v.second.match(n)) return false;
// The first must match a ascendent. // The first must match a ascendent.
var parent = n.parent(); var p = try n.parent();
while (parent) |p| { while (p != null) {
if (try v.first.match(p)) { if (try v.first.match(p.?)) {
return true; return true;
} }
parent = p.parent(); p = try p.?.parent();
} }
return false; return false;
}, },
.child => { .child => {
const p = n.parent() orelse return false; const p = try n.parent();
return try v.second.match(n) and try v.first.match(p); if (p == null) return false;
return try v.second.match(n) and try v.first.match(p.?);
}, },
.next_sibling => { .next_sibling => {
if (!try v.second.match(n)) return false; if (!try v.second.match(n)) return false;
var child = n.prevSibling(); var c = try n.prevSibling();
while (child) |c| { while (c != null) {
if (c.isText() or c.isComment()) { if (c.?.isText() or c.?.isComment()) {
child = c.prevSibling(); c = try c.?.prevSibling();
continue; continue;
} }
return try v.first.match(c); return try v.first.match(c.?);
} }
return false; return false;
}, },
.subsequent_sibling => { .subsequent_sibling => {
if (!try v.second.match(n)) return false; if (!try v.second.match(n)) return false;
var child = n.prevSibling(); var c = try n.prevSibling();
while (child) |c| { while (c != null) {
if (try v.first.match(c)) return true; if (try v.first.match(c.?)) return true;
child = c.prevSibling(); c = try c.?.prevSibling();
} }
return false; return false;
}, },
@@ -436,10 +438,10 @@ pub const Selector = union(enum) {
// Only containsOwn is implemented. // Only containsOwn is implemented.
if (v.own == false) return Error.UnsupportedContainsPseudoClass; if (v.own == false) return Error.UnsupportedContainsPseudoClass;
var child = n.firstChild(); var c = try n.firstChild();
while (child) |c| { while (c != null) {
if (c.isText()) { if (c.?.isText()) {
const text = c.text(); const text = try c.?.text();
if (text) |_text| { if (text) |_text| {
if (contains(_text, v.val, false)) { // we are case sensitive. Is this correct behavior? if (contains(_text, v.val, false)) { // we are case sensitive. Is this correct behavior?
return true; return true;
@@ -447,7 +449,7 @@ pub const Selector = union(enum) {
} }
} }
child = c.nextSibling(); c = try c.?.nextSibling();
} }
return false; return false;
}, },
@@ -475,16 +477,16 @@ pub const Selector = union(enum) {
.empty => { .empty => {
if (!n.isElement()) return false; if (!n.isElement()) return false;
var child = n.firstChild(); var c = try n.firstChild();
while (child) |c| { while (c != null) {
if (c.isElement()) return false; if (c.?.isElement()) return false;
if (c.isText()) { if (c.?.isText()) {
if (c.isEmptyText()) continue; if (try c.?.isEmptyText()) continue;
return false; return false;
} }
child = c.nextSibling(); c = try c.?.nextSibling();
} }
return true; return true;
@@ -492,7 +494,7 @@ pub const Selector = union(enum) {
.root => { .root => {
if (!n.isElement()) return false; if (!n.isElement()) return false;
const p = n.parent(); const p = try n.parent();
return (p != null and p.?.isDocument()); return (p != null and p.?.isDocument());
}, },
.link => { .link => {
@@ -562,7 +564,7 @@ pub const Selector = union(enum) {
const ntag = try n.tag(); const ntag = try n.tag();
if (std.ascii.eqlIgnoreCase("input", ntag)) { if (std.ascii.eqlIgnoreCase("intput", ntag)) {
const ntype = try n.attr("type"); const ntype = try n.attr("type");
if (ntype == null) return false; if (ntype == null) return false;
@@ -607,23 +609,24 @@ pub const Selector = union(enum) {
} }
fn hasLegendInPreviousSiblings(n: anytype) anyerror!bool { fn hasLegendInPreviousSiblings(n: anytype) anyerror!bool {
var child = n.prevSibling(); var c = try n.prevSibling();
while (child) |c| { while (c != null) {
const ctag = try c.tag(); const ctag = try c.?.tag();
if (std.ascii.eqlIgnoreCase("legend", ctag)) return true; if (std.ascii.eqlIgnoreCase("legend", ctag)) return true;
child = c.prevSibling(); c = try c.?.prevSibling();
} }
return false; return false;
} }
fn inDisabledFieldset(n: anytype) anyerror!bool { fn inDisabledFieldset(n: anytype) anyerror!bool {
const p = n.parent() orelse return false; const p = try n.parent();
if (p == null) return false;
const ntag = try n.tag(); const ntag = try n.tag();
const ptag = try p.tag(); const ptag = try p.?.tag();
if (std.ascii.eqlIgnoreCase("fieldset", ptag) and if (std.ascii.eqlIgnoreCase("fieldset", ptag) and
try p.attr("disabled") != null and try p.?.attr("disabled") != null and
(!std.ascii.eqlIgnoreCase("legend", ntag) or try hasLegendInPreviousSiblings(n))) (!std.ascii.eqlIgnoreCase("legend", ntag) or try hasLegendInPreviousSiblings(n)))
{ {
return true; return true;
@@ -639,7 +642,7 @@ pub const Selector = union(enum) {
// ``` // ```
// https://github.com/andybalholm/cascadia/blob/master/pseudo_classes.go#L434 // https://github.com/andybalholm/cascadia/blob/master/pseudo_classes.go#L434
return try inDisabledFieldset(p); return try inDisabledFieldset(p.?);
} }
fn langMatch(lang: []const u8, n: anytype) anyerror!bool { fn langMatch(lang: []const u8, n: anytype) anyerror!bool {
@@ -653,8 +656,10 @@ pub const Selector = union(enum) {
} }
// if the tag doesn't match, try the parent. // if the tag doesn't match, try the parent.
const p = n.parent() orelse return false; const p = try n.parent();
return langMatch(lang, p); if (p == null) return false;
return langMatch(lang, p.?);
} }
// onlyChildMatch implements :only-child // onlyChildMatch implements :only-child
@@ -662,24 +667,25 @@ pub const Selector = union(enum) {
fn onlyChildMatch(of_type: bool, n: anytype) anyerror!bool { fn onlyChildMatch(of_type: bool, n: anytype) anyerror!bool {
if (!n.isElement()) return false; if (!n.isElement()) return false;
const p = n.parent() orelse return false; const p = try n.parent();
if (p == null) return false;
const ntag = try n.tag(); const ntag = try n.tag();
var count: usize = 0; var count: usize = 0;
var child = p.firstChild(); var c = try p.?.firstChild();
// loop hover all n siblings. // loop hover all n siblings.
while (child) |c| { while (c != null) {
// ignore non elements or others tags if of-type is true. // ignore non elements or others tags if of-type is true.
if (!c.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.tag()))) { if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
child = c.nextSibling(); c = try c.?.nextSibling();
continue; continue;
} }
count += 1; count += 1;
if (count > 1) return false; if (count > 1) return false;
child = c.nextSibling(); c = try c.?.nextSibling();
} }
return count == 1; return count == 1;
@@ -690,25 +696,27 @@ pub const Selector = union(enum) {
fn simpleNthLastChildMatch(b: isize, of_type: bool, n: anytype) anyerror!bool { fn simpleNthLastChildMatch(b: isize, of_type: bool, n: anytype) anyerror!bool {
if (!n.isElement()) return false; if (!n.isElement()) return false;
const p = n.parent() orelse return false; const p = try n.parent();
if (p == null) return false;
const ntag = try n.tag(); const ntag = try n.tag();
var count: isize = 0; var count: isize = 0;
var child = p.lastChild(); var c = try p.?.lastChild();
// loop hover all n siblings. // loop hover all n siblings.
while (child) |c| { while (c != null) {
// ignore non elements or others tags if of-type is true. // ignore non elements or others tags if of-type is true.
if (!c.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.tag()))) { if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
child = c.prevSibling(); c = try c.?.prevSibling();
continue; continue;
} }
count += 1; count += 1;
if (n.eql(c)) return count == b; if (n.eql(c.?)) return count == b;
if (count >= b) return false; if (count >= b) return false;
child = c.prevSibling(); c = try c.?.prevSibling();
} }
return false; return false;
@@ -719,25 +727,27 @@ pub const Selector = union(enum) {
fn simpleNthChildMatch(b: isize, of_type: bool, n: anytype) anyerror!bool { fn simpleNthChildMatch(b: isize, of_type: bool, n: anytype) anyerror!bool {
if (!n.isElement()) return false; if (!n.isElement()) return false;
const p = n.parent() orelse return false; const p = try n.parent();
if (p == null) return false;
const ntag = try n.tag(); const ntag = try n.tag();
var count: isize = 0; var count: isize = 0;
var child = p.firstChild(); var c = try p.?.firstChild();
// loop hover all n siblings. // loop hover all n siblings.
while (child) |c| { while (c != null) {
// ignore non elements or others tags if of-type is true. // ignore non elements or others tags if of-type is true.
if (!c.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.tag()))) { if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
child = c.nextSibling(); c = try c.?.nextSibling();
continue; continue;
} }
count += 1; count += 1;
if (n.eql(c)) return count == b; if (n.eql(c.?)) return count == b;
if (count >= b) return false; if (count >= b) return false;
child = c.nextSibling(); c = try c.?.nextSibling();
} }
return false; return false;
@@ -749,27 +759,29 @@ pub const Selector = union(enum) {
fn nthChildMatch(a: isize, b: isize, last: bool, of_type: bool, n: anytype) anyerror!bool { fn nthChildMatch(a: isize, b: isize, last: bool, of_type: bool, n: anytype) anyerror!bool {
if (!n.isElement()) return false; if (!n.isElement()) return false;
const p = n.parent() orelse return false; const p = try n.parent();
if (p == null) return false;
const ntag = try n.tag(); const ntag = try n.tag();
var i: isize = -1; var i: isize = -1;
var count: isize = 0; var count: isize = 0;
var child = p.firstChild(); var c = try p.?.firstChild();
// loop hover all n siblings. // loop hover all n siblings.
while (child) |c| { while (c != null) {
// ignore non elements or others tags if of-type is true. // ignore non elements or others tags if of-type is true.
if (!c.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.tag()))) { if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
child = c.nextSibling(); c = try c.?.nextSibling();
continue; continue;
} }
count += 1; count += 1;
if (n.eql(c)) { if (n.eql(c.?)) {
i = count; i = count;
if (!last) break; if (!last) break;
} }
child = c.nextSibling(); c = try c.?.nextSibling();
} }
if (i == -1) return false; if (i == -1) return false;
@@ -782,21 +794,21 @@ pub const Selector = union(enum) {
} }
fn hasDescendantMatch(s: *const Selector, n: anytype) anyerror!bool { fn hasDescendantMatch(s: *const Selector, n: anytype) anyerror!bool {
var child = n.firstChild(); var c = try n.firstChild();
while (child) |c| { while (c != null) {
if (try s.match(c)) return true; if (try s.match(c.?)) return true;
if (c.isElement() and try hasDescendantMatch(s, c)) return true; if (c.?.isElement() and try hasDescendantMatch(s, c.?)) return true;
child = c.nextSibling(); c = try c.?.nextSibling();
} }
return false; return false;
} }
fn hasChildMatch(s: *const Selector, n: anytype) anyerror!bool { fn hasChildMatch(s: *const Selector, n: anytype) anyerror!bool {
var child = n.firstChild(); var c = try n.firstChild();
while (child) |c| { while (c != null) {
if (try s.match(c)) return true; if (try s.match(c.?)) return true;
child = c.nextSibling(); c = try c.?.nextSibling();
} }
return false; return false;
@@ -847,23 +859,23 @@ pub const NodeTest = struct {
name: []const u8 = "", name: []const u8 = "",
att: ?[]const u8 = null, att: ?[]const u8 = null,
pub fn firstChild(n: *const NodeTest) ?*const NodeTest { pub fn firstChild(n: *const NodeTest) !?*const NodeTest {
return n.child; return n.child;
} }
pub fn lastChild(n: *const NodeTest) ?*const NodeTest { pub fn lastChild(n: *const NodeTest) !?*const NodeTest {
return n.last; return n.last;
} }
pub fn nextSibling(n: *const NodeTest) ?*const NodeTest { pub fn nextSibling(n: *const NodeTest) !?*const NodeTest {
return n.sibling; return n.sibling;
} }
pub fn prevSibling(n: *const NodeTest) ?*const NodeTest { pub fn prevSibling(n: *const NodeTest) !?*const NodeTest {
return n.prev; return n.prev;
} }
pub fn parent(n: *const NodeTest) ?*const NodeTest { pub fn parent(n: *const NodeTest) !?*const NodeTest {
return n.par; return n.par;
} }
@@ -879,7 +891,7 @@ pub const NodeTest = struct {
return false; return false;
} }
pub fn text(_: *const NodeTest) ?[]const u8 { pub fn text(_: *const NodeTest) !?[]const u8 {
return null; return null;
} }
@@ -887,7 +899,7 @@ pub const NodeTest = struct {
return false; return false;
} }
pub fn isEmptyText(_: *const NodeTest) bool { pub fn isEmptyText(_: *const NodeTest) !bool {
return false; return false;
} }
@@ -981,11 +993,6 @@ test "Browser.CSS.Selector: matchFirst" {
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 0, .exp = 0,
}, },
.{
.q = "[foo=1baz]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
.exp = 0,
},
.{ .{
.q = "[foo!=bar]", .q = "[foo!=bar]",
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },

View File

@@ -19,6 +19,7 @@
const std = @import("std"); const std = @import("std");
const CSSRule = @import("CSSRule.zig"); const CSSRule = @import("CSSRule.zig");
const StyleSheet = @import("StyleSheet.zig").StyleSheet;
const CSSImportRule = CSSRule.CSSImportRule; const CSSImportRule = CSSRule.CSSImportRule;

View File

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

View File

@@ -18,7 +18,7 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig"); const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const StyleSheet = @import("StyleSheet.zig"); const StyleSheet = @import("StyleSheet.zig");
const CSSRuleList = @import("CSSRuleList.zig"); const CSSRuleList = @import("CSSRuleList.zig");
@@ -73,13 +73,15 @@ pub fn _deleteRule(self: *CSSStyleSheet, index: usize) !void {
_ = self.css_rules.list.orderedRemove(index); _ = self.css_rules.list.orderedRemove(index);
} }
pub fn _replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !js.Promise { pub fn _replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !Env.Promise {
_ = self; _ = self;
_ = text; _ = text;
// TODO: clear self.css_rules // TODO: clear self.css_rules
// parse text and re-populate self.css_rules // parse text and re-populate self.css_rules
return page.js.resolvePromise({}); const resolver = page.main_context.createPromiseResolver();
try resolver.resolve({});
return resolver.promise();
} }
pub fn _replaceSync(self: *CSSStyleSheet, text: []const u8) !void { pub fn _replaceSync(self: *CSSStyleSheet, text: []const u8) !void {

View File

@@ -1,283 +0,0 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Io = std.Io;
const CSSParser = @import("CSSParser.zig");
const isHexColor = @import("CSSStyleDeclaration.zig").isHexColor;
pub const RGBA = packed struct(u32) {
r: u8,
g: u8,
b: u8,
/// Opaque by default.
a: u8 = std.math.maxInt(u8),
pub const Named = struct {
// Basic colors (CSS Level 1)
pub const black: RGBA = .init(0, 0, 0, 1);
pub const silver: RGBA = .init(192, 192, 192, 1);
pub const gray: RGBA = .init(128, 128, 128, 1);
pub const white: RGBA = .init(255, 255, 255, 1);
pub const maroon: RGBA = .init(128, 0, 0, 1);
pub const red: RGBA = .init(255, 0, 0, 1);
pub const purple: RGBA = .init(128, 0, 128, 1);
pub const fuchsia: RGBA = .init(255, 0, 255, 1);
pub const green: RGBA = .init(0, 128, 0, 1);
pub const lime: RGBA = .init(0, 255, 0, 1);
pub const olive: RGBA = .init(128, 128, 0, 1);
pub const yellow: RGBA = .init(255, 255, 0, 1);
pub const navy: RGBA = .init(0, 0, 128, 1);
pub const blue: RGBA = .init(0, 0, 255, 1);
pub const teal: RGBA = .init(0, 128, 128, 1);
pub const aqua: RGBA = .init(0, 255, 255, 1);
// Extended colors (CSS Level 2+)
pub const aliceblue: RGBA = .init(240, 248, 255, 1);
pub const antiquewhite: RGBA = .init(250, 235, 215, 1);
pub const aquamarine: RGBA = .init(127, 255, 212, 1);
pub const azure: RGBA = .init(240, 255, 255, 1);
pub const beige: RGBA = .init(245, 245, 220, 1);
pub const bisque: RGBA = .init(255, 228, 196, 1);
pub const blanchedalmond: RGBA = .init(255, 235, 205, 1);
pub const blueviolet: RGBA = .init(138, 43, 226, 1);
pub const brown: RGBA = .init(165, 42, 42, 1);
pub const burlywood: RGBA = .init(222, 184, 135, 1);
pub const cadetblue: RGBA = .init(95, 158, 160, 1);
pub const chartreuse: RGBA = .init(127, 255, 0, 1);
pub const chocolate: RGBA = .init(210, 105, 30, 1);
pub const coral: RGBA = .init(255, 127, 80, 1);
pub const cornflowerblue: RGBA = .init(100, 149, 237, 1);
pub const cornsilk: RGBA = .init(255, 248, 220, 1);
pub const crimson: RGBA = .init(220, 20, 60, 1);
pub const cyan: RGBA = .init(0, 255, 255, 1); // Synonym of aqua
pub const darkblue: RGBA = .init(0, 0, 139, 1);
pub const darkcyan: RGBA = .init(0, 139, 139, 1);
pub const darkgoldenrod: RGBA = .init(184, 134, 11, 1);
pub const darkgray: RGBA = .init(169, 169, 169, 1);
pub const darkgreen: RGBA = .init(0, 100, 0, 1);
pub const darkgrey: RGBA = .init(169, 169, 169, 1); // Synonym of darkgray
pub const darkkhaki: RGBA = .init(189, 183, 107, 1);
pub const darkmagenta: RGBA = .init(139, 0, 139, 1);
pub const darkolivegreen: RGBA = .init(85, 107, 47, 1);
pub const darkorange: RGBA = .init(255, 140, 0, 1);
pub const darkorchid: RGBA = .init(153, 50, 204, 1);
pub const darkred: RGBA = .init(139, 0, 0, 1);
pub const darksalmon: RGBA = .init(233, 150, 122, 1);
pub const darkseagreen: RGBA = .init(143, 188, 143, 1);
pub const darkslateblue: RGBA = .init(72, 61, 139, 1);
pub const darkslategray: RGBA = .init(47, 79, 79, 1);
pub const darkslategrey: RGBA = .init(47, 79, 79, 1); // Synonym of darkslategray
pub const darkturquoise: RGBA = .init(0, 206, 209, 1);
pub const darkviolet: RGBA = .init(148, 0, 211, 1);
pub const deeppink: RGBA = .init(255, 20, 147, 1);
pub const deepskyblue: RGBA = .init(0, 191, 255, 1);
pub const dimgray: RGBA = .init(105, 105, 105, 1);
pub const dimgrey: RGBA = .init(105, 105, 105, 1); // Synonym of dimgray
pub const dodgerblue: RGBA = .init(30, 144, 255, 1);
pub const firebrick: RGBA = .init(178, 34, 34, 1);
pub const floralwhite: RGBA = .init(255, 250, 240, 1);
pub const forestgreen: RGBA = .init(34, 139, 34, 1);
pub const gainsboro: RGBA = .init(220, 220, 220, 1);
pub const ghostwhite: RGBA = .init(248, 248, 255, 1);
pub const gold: RGBA = .init(255, 215, 0, 1);
pub const goldenrod: RGBA = .init(218, 165, 32, 1);
pub const greenyellow: RGBA = .init(173, 255, 47, 1);
pub const grey: RGBA = .init(128, 128, 128, 1); // Synonym of gray
pub const honeydew: RGBA = .init(240, 255, 240, 1);
pub const hotpink: RGBA = .init(255, 105, 180, 1);
pub const indianred: RGBA = .init(205, 92, 92, 1);
pub const indigo: RGBA = .init(75, 0, 130, 1);
pub const ivory: RGBA = .init(255, 255, 240, 1);
pub const khaki: RGBA = .init(240, 230, 140, 1);
pub const lavender: RGBA = .init(230, 230, 250, 1);
pub const lavenderblush: RGBA = .init(255, 240, 245, 1);
pub const lawngreen: RGBA = .init(124, 252, 0, 1);
pub const lemonchiffon: RGBA = .init(255, 250, 205, 1);
pub const lightblue: RGBA = .init(173, 216, 230, 1);
pub const lightcoral: RGBA = .init(240, 128, 128, 1);
pub const lightcyan: RGBA = .init(224, 255, 255, 1);
pub const lightgoldenrodyellow: RGBA = .init(250, 250, 210, 1);
pub const lightgray: RGBA = .init(211, 211, 211, 1);
pub const lightgreen: RGBA = .init(144, 238, 144, 1);
pub const lightgrey: RGBA = .init(211, 211, 211, 1); // Synonym of lightgray
pub const lightpink: RGBA = .init(255, 182, 193, 1);
pub const lightsalmon: RGBA = .init(255, 160, 122, 1);
pub const lightseagreen: RGBA = .init(32, 178, 170, 1);
pub const lightskyblue: RGBA = .init(135, 206, 250, 1);
pub const lightslategray: RGBA = .init(119, 136, 153, 1);
pub const lightslategrey: RGBA = .init(119, 136, 153, 1); // Synonym of lightslategray
pub const lightsteelblue: RGBA = .init(176, 196, 222, 1);
pub const lightyellow: RGBA = .init(255, 255, 224, 1);
pub const limegreen: RGBA = .init(50, 205, 50, 1);
pub const linen: RGBA = .init(250, 240, 230, 1);
pub const magenta: RGBA = .init(255, 0, 255, 1); // Synonym of fuchsia
pub const mediumaquamarine: RGBA = .init(102, 205, 170, 1);
pub const mediumblue: RGBA = .init(0, 0, 205, 1);
pub const mediumorchid: RGBA = .init(186, 85, 211, 1);
pub const mediumpurple: RGBA = .init(147, 112, 219, 1);
pub const mediumseagreen: RGBA = .init(60, 179, 113, 1);
pub const mediumslateblue: RGBA = .init(123, 104, 238, 1);
pub const mediumspringgreen: RGBA = .init(0, 250, 154, 1);
pub const mediumturquoise: RGBA = .init(72, 209, 204, 1);
pub const mediumvioletred: RGBA = .init(199, 21, 133, 1);
pub const midnightblue: RGBA = .init(25, 25, 112, 1);
pub const mintcream: RGBA = .init(245, 255, 250, 1);
pub const mistyrose: RGBA = .init(255, 228, 225, 1);
pub const moccasin: RGBA = .init(255, 228, 181, 1);
pub const navajowhite: RGBA = .init(255, 222, 173, 1);
pub const oldlace: RGBA = .init(253, 245, 230, 1);
pub const olivedrab: RGBA = .init(107, 142, 35, 1);
pub const orange: RGBA = .init(255, 165, 0, 1);
pub const orangered: RGBA = .init(255, 69, 0, 1);
pub const orchid: RGBA = .init(218, 112, 214, 1);
pub const palegoldenrod: RGBA = .init(238, 232, 170, 1);
pub const palegreen: RGBA = .init(152, 251, 152, 1);
pub const paleturquoise: RGBA = .init(175, 238, 238, 1);
pub const palevioletred: RGBA = .init(219, 112, 147, 1);
pub const papayawhip: RGBA = .init(255, 239, 213, 1);
pub const peachpuff: RGBA = .init(255, 218, 185, 1);
pub const peru: RGBA = .init(205, 133, 63, 1);
pub const pink: RGBA = .init(255, 192, 203, 1);
pub const plum: RGBA = .init(221, 160, 221, 1);
pub const powderblue: RGBA = .init(176, 224, 230, 1);
pub const rebeccapurple: RGBA = .init(102, 51, 153, 1);
pub const rosybrown: RGBA = .init(188, 143, 143, 1);
pub const royalblue: RGBA = .init(65, 105, 225, 1);
pub const saddlebrown: RGBA = .init(139, 69, 19, 1);
pub const salmon: RGBA = .init(250, 128, 114, 1);
pub const sandybrown: RGBA = .init(244, 164, 96, 1);
pub const seagreen: RGBA = .init(46, 139, 87, 1);
pub const seashell: RGBA = .init(255, 245, 238, 1);
pub const sienna: RGBA = .init(160, 82, 45, 1);
pub const skyblue: RGBA = .init(135, 206, 235, 1);
pub const slateblue: RGBA = .init(106, 90, 205, 1);
pub const slategray: RGBA = .init(112, 128, 144, 1);
pub const slategrey: RGBA = .init(112, 128, 144, 1); // Synonym of slategray
pub const snow: RGBA = .init(255, 250, 250, 1);
pub const springgreen: RGBA = .init(0, 255, 127, 1);
pub const steelblue: RGBA = .init(70, 130, 180, 1);
pub const tan: RGBA = .init(210, 180, 140, 1);
pub const thistle: RGBA = .init(216, 191, 216, 1);
pub const tomato: RGBA = .init(255, 99, 71, 1);
pub const transparent: RGBA = .init(0, 0, 0, 0);
pub const turquoise: RGBA = .init(64, 224, 208, 1);
pub const violet: RGBA = .init(238, 130, 238, 1);
pub const wheat: RGBA = .init(245, 222, 179, 1);
pub const whitesmoke: RGBA = .init(245, 245, 245, 1);
pub const yellowgreen: RGBA = .init(154, 205, 50, 1);
};
pub fn init(r: u8, g: u8, b: u8, a: f32) RGBA {
const clamped = std.math.clamp(a, 0, 1);
return .{ .r = r, .g = g, .b = b, .a = @intFromFloat(clamped * 255) };
}
/// Finds a color by its name.
pub fn find(name: []const u8) ?RGBA {
const match = std.meta.stringToEnum(std.meta.DeclEnum(Named), name) orelse return null;
return switch (match) {
inline else => |comptime_enum| @field(Named, @tagName(comptime_enum)),
};
}
/// Parses the given color.
/// Currently we only parse hex colors and named colors; other variants
/// require CSS evaluation.
pub fn parse(input: []const u8) !RGBA {
if (!isHexColor(input)) {
// Try named colors.
return find(input) orelse return error.Invalid;
}
const slice = input[1..];
switch (slice.len) {
// This means the digit for a color is repeated.
// Given HEX is #f0c, its interpreted the same as #FF00CC.
3 => {
const r = try std.fmt.parseInt(u8, &.{ slice[0], slice[0] }, 16);
const g = try std.fmt.parseInt(u8, &.{ slice[1], slice[1] }, 16);
const b = try std.fmt.parseInt(u8, &.{ slice[2], slice[2] }, 16);
return .{ .r = r, .g = g, .b = b, .a = 255 };
},
4 => {
const r = try std.fmt.parseInt(u8, &.{ slice[0], slice[0] }, 16);
const g = try std.fmt.parseInt(u8, &.{ slice[1], slice[1] }, 16);
const b = try std.fmt.parseInt(u8, &.{ slice[2], slice[2] }, 16);
const a = try std.fmt.parseInt(u8, &.{ slice[3], slice[3] }, 16);
return .{ .r = r, .g = g, .b = b, .a = a };
},
// Regular HEX format.
6 => {
const r = try std.fmt.parseInt(u8, slice[0..2], 16);
const g = try std.fmt.parseInt(u8, slice[2..4], 16);
const b = try std.fmt.parseInt(u8, slice[4..6], 16);
return .{ .r = r, .g = g, .b = b, .a = 255 };
},
8 => {
const r = try std.fmt.parseInt(u8, slice[0..2], 16);
const g = try std.fmt.parseInt(u8, slice[2..4], 16);
const b = try std.fmt.parseInt(u8, slice[4..6], 16);
const a = try std.fmt.parseInt(u8, slice[6..8], 16);
return .{ .r = r, .g = g, .b = b, .a = a };
},
else => return error.Invalid,
}
}
/// By default, browsers prefer lowercase formatting.
const format_upper = false;
/// Formats the `Color` according to web expectations.
/// If color is opaque, HEX is preferred; RGBA otherwise.
pub fn format(self: *const RGBA, writer: *Io.Writer) Io.Writer.Error!void {
if (self.isOpaque()) {
// Convert RGB to HEX.
// https://gristle.tripod.com/hexconv.html
// Hexadecimal characters up to 15.
const char: []const u8 = "0123456789" ++ if (format_upper) "ABCDEF" else "abcdef";
// This variant always prefers 6 digit format, +1 is for hash char.
const buffer = [7]u8{
'#',
char[self.r >> 4],
char[self.r & 15],
char[self.g >> 4],
char[self.g & 15],
char[self.b >> 4],
char[self.b & 15],
};
return writer.writeAll(&buffer);
}
// Prefer RGBA format for everything else.
return writer.print("rgba({d}, {d}, {d}, {d:.2})", .{ self.r, self.g, self.b, self.normalizedAlpha() });
}
/// Returns true if `Color` is opaque.
pub inline fn isOpaque(self: *const RGBA) bool {
return self.a == std.math.maxInt(u8);
}
/// Returns the normalized alpha value.
pub inline fn normalizedAlpha(self: *const RGBA) f32 {
return @as(f32, @floatFromInt(self.a)) / 255;
}
};

View File

@@ -18,17 +18,19 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const Page = @import("../page.zig").Page; 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(); const Animation = @This();
effect: ?js.Object, effect: ?JsObject,
timeline: ?js.Object, timeline: ?JsObject,
ready_resolver: ?js.PromiseResolver, ready_resolver: ?PromiseResolver,
finished_resolver: ?js.PromiseResolver, finished_resolver: ?PromiseResolver,
pub fn constructor(effect: ?js.Object, timeline: ?js.Object) !Animation { pub fn constructor(effect: ?JsObject, timeline: ?JsObject) !Animation {
return .{ return .{
.effect = if (effect) |eo| try eo.persist() else null, .effect = if (effect) |eo| try eo.persist() else null,
.timeline = if (timeline) |to| try to.persist() else null, .timeline = if (timeline) |to| try to.persist() else null,
@@ -47,37 +49,37 @@ pub fn get_pending(self: *const Animation) bool {
return false; return false;
} }
pub fn get_finished(self: *Animation, page: *Page) !js.Promise { pub fn get_finished(self: *Animation, page: *Page) !Promise {
if (self.finished_resolver == null) { if (self.finished_resolver == null) {
const resolver = page.js.createPromiseResolver(.none); const resolver = page.main_context.createPromiseResolver();
try resolver.resolve(self); try resolver.resolve(self);
self.finished_resolver = resolver; self.finished_resolver = resolver;
} }
return self.finished_resolver.?.promise(); return self.finished_resolver.?.promise();
} }
pub fn get_ready(self: *Animation, page: *Page) !js.Promise { pub fn get_ready(self: *Animation, page: *Page) !Promise {
// never resolved, because we're always "finished" // never resolved, because we're always "finished"
if (self.ready_resolver == null) { if (self.ready_resolver == null) {
const resolver = page.js.createPromiseResolver(.none); const resolver = page.main_context.createPromiseResolver();
self.ready_resolver = resolver; self.ready_resolver = resolver;
} }
return self.ready_resolver.?.promise(); return self.ready_resolver.?.promise();
} }
pub fn get_effect(self: *const Animation) ?js.Object { pub fn get_effect(self: *const Animation) ?JsObject {
return self.effect; return self.effect;
} }
pub fn set_effect(self: *Animation, effect: js.Object) !void { pub fn set_effect(self: *Animation, effect: JsObject) !void {
self.effect = try effect.persist(); self.effect = try effect.persist();
} }
pub fn get_timeline(self: *const Animation) ?js.Object { pub fn get_timeline(self: *const Animation) ?JsObject {
return self.timeline; return self.timeline;
} }
pub fn set_timeline(self: *Animation, timeline: js.Object) !void { pub fn set_timeline(self: *Animation, timeline: JsObject) !void {
self.timeline = try timeline.persist(); self.timeline = try timeline.persist();
} }

View File

@@ -1,329 +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 js = @import("../js/js.zig");
const log = @import("../../log.zig");
const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page;
const Node = @import("node.zig").Node;
const Element = @import("element.zig").Element;
pub const Interfaces = .{
IntersectionObserver,
Entry,
};
// This implementation attempts to be as less wrong as possible. Since we don't
// render, or know how things are positioned, our best guess isn't very good.
const IntersectionObserver = @This();
page: *Page,
root: *parser.Node,
callback: js.Function,
event_node: parser.EventNode,
observed_entries: std.ArrayList(Entry),
pending_elements: std.ArrayList(*parser.Element),
ready_elements: std.ArrayList(*parser.Element),
pub fn constructor(callback: js.Function, opts_: ?IntersectionObserverOptions, page: *Page) !*IntersectionObserver {
const opts = opts_ orelse IntersectionObserverOptions{};
const self = try page.arena.create(IntersectionObserver);
self.* = .{
.page = page,
.callback = callback,
.ready_elements = .{},
.observed_entries = .{},
.pending_elements = .{},
.event_node = .{ .func = mutationCallback },
.root = opts.root orelse parser.documentToNode(parser.documentHTMLToDocument(page.window.document)),
};
_ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, self.root),
"DOMNodeInserted",
&self.event_node,
false,
);
_ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, self.root),
"DOMNodeRemoved",
&self.event_node,
false,
);
return self;
}
pub fn _disconnect(self: *IntersectionObserver) !void {
// We don't free as it is on an arena
self.ready_elements = .{};
self.observed_entries = .{};
self.pending_elements = .{};
}
pub fn _observe(self: *IntersectionObserver, target_element: *parser.Element, page: *Page) !void {
for (self.observed_entries.items) |*observer| {
if (observer.target == target_element) {
return; // Already observed
}
}
if (self.isPending(target_element)) {
return; // Already pending
}
for (self.ready_elements.items) |element| {
if (element == target_element) {
return; // Already primed
}
}
// We can never fire callbacks synchronously. Code like React expects any
// callback to fire in the future (e.g. via microtasks).
try self.ready_elements.append(self.page.arena, target_element);
if (self.ready_elements.items.len == 1) {
// this is our first ready entry, schedule a callback
try page.scheduler.add(self, processReady, 0, .{
.name = "intersection ready",
});
}
}
pub fn _unobserve(self: *IntersectionObserver, target: *parser.Element) !void {
if (self.removeObserved(target)) {
return;
}
for (self.ready_elements.items, 0..) |el, index| {
if (el == target) {
_ = self.ready_elements.swapRemove(index);
return;
}
}
for (self.pending_elements.items, 0..) |el, index| {
if (el == target) {
_ = self.pending_elements.swapRemove(index);
return;
}
}
}
pub fn _takeRecords(self: *IntersectionObserver) []Entry {
return self.observed_entries.items;
}
fn processReady(ctx: *anyopaque) ?u32 {
const self: *IntersectionObserver = @ptrCast(@alignCast(ctx));
self._processReady() catch |err| {
log.err(.web_api, "intersection ready", .{ .err = err });
};
return null;
}
fn _processReady(self: *IntersectionObserver) !void {
defer self.ready_elements.clearRetainingCapacity();
for (self.ready_elements.items) |element| {
// IntersectionObserver probably doesn't work like what your intuition
// thinks. As long as a node has a parent, even if that parent isn't
// connected and even if the two nodes don't intersect, it'll fire the
// callback once.
if (try Node.get_parentNode(@ptrCast(element)) == null) {
if (!self.isPending(element)) {
try self.pending_elements.append(self.page.arena, element);
}
continue;
}
try self.forceObserve(element);
}
}
fn isPending(self: *IntersectionObserver, element: *parser.Element) bool {
for (self.pending_elements.items) |el| {
if (el == element) {
return true;
}
}
return false;
}
fn mutationCallback(en: *parser.EventNode, event: *parser.Event) void {
const mutation_event = parser.eventToMutationEvent(event);
const self: *IntersectionObserver = @fieldParentPtr("event_node", en);
self._mutationCallback(mutation_event) catch |err| {
log.err(.web_api, "mutation callback", .{ .err = err, .source = "intersection observer" });
};
}
fn _mutationCallback(self: *IntersectionObserver, event: *parser.MutationEvent) !void {
const event_type = parser.eventType(@ptrCast(event));
if (std.mem.eql(u8, event_type, "DOMNodeInserted")) {
const node = parser.mutationEventRelatedNode(event) catch return orelse return;
if (parser.nodeType(node) != .element) {
return;
}
const el: *parser.Element = @ptrCast(node);
if (self.removePending(el)) {
// It was pending (because it wasn't in the root), but now it is
// we should observe it.
try self.forceObserve(el);
}
return;
}
if (std.mem.eql(u8, event_type, "DOMNodeRemoved")) {
const node = parser.mutationEventRelatedNode(event) catch return orelse return;
if (parser.nodeType(node) != .element) {
return;
}
const el: *parser.Element = @ptrCast(node);
if (self.removeObserved(el)) {
// It _was_ observed, it no longer is in our root, but if it was
// to get re-added, it should be observed again (I think), so
// we add it to our pending list
try self.pending_elements.append(self.page.arena, el);
}
return;
}
// impossible event type
unreachable;
}
// Exists to skip the checks made _observe when called from a DOMNodeInserted
// event. In such events, the event handler has alread done the necessary
// checks.
fn forceObserve(self: *IntersectionObserver, target: *parser.Element) !void {
try self.observed_entries.append(self.page.arena, .{
.page = self.page,
.root = self.root,
.target = target,
});
var result: js.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",
});
};
}
fn removeObserved(self: *IntersectionObserver, target: *parser.Element) bool {
for (self.observed_entries.items, 0..) |*observer, index| {
if (observer.target == target) {
_ = self.observed_entries.swapRemove(index);
return true;
}
}
return false;
}
fn removePending(self: *IntersectionObserver, target: *parser.Element) bool {
for (self.pending_elements.items, 0..) |el, index| {
if (el == target) {
_ = self.pending_elements.swapRemove(index);
return true;
}
}
return false;
}
const IntersectionObserverOptions = struct {
root: ?*parser.Node = null, // Element or Document
rootMargin: ?[]const u8 = "0px 0px 0px 0px",
threshold: ?Threshold = .{ .single = 0.0 },
const Threshold = union(enum) {
single: f32,
list: []const f32,
};
};
// https://developer.mozilla.org/en-US/docs/Web/API/Entry
// https://w3c.github.io/IntersectionObserver/#intersection-observer-entry
pub const Entry = struct {
page: *Page,
root: *parser.Node,
target: *parser.Element,
// 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 Entry) !Element.DOMRect {
return Element._getBoundingClientRect(self.target, self.page);
}
// Returns the ratio of the intersectionRect to the boundingClientRect.
pub fn get_intersectionRatio(_: *const Entry) f32 {
return 1.0;
}
// Returns a DOMRectReadOnly representing the target's visible area.
pub fn get_intersectionRect(self: *const Entry) !Element.DOMRect {
return Element._getBoundingClientRect(self.target, self.page);
}
// A Boolean value which is true if the target element intersects with the
// intersection observer's root. If this is true, then, the
// Entry 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 Entry) bool {
return true;
}
// Returns a DOMRectReadOnly for the intersection observer's root.
pub fn get_rootBounds(self: *const Entry) !Element.DOMRect {
const root = self.root;
if (@intFromPtr(root) == @intFromPtr(self.page.window.document)) {
return self.page.renderer.boundingRect();
}
const root_type = parser.nodeType(root);
var element: *parser.Element = undefined;
switch (root_type) {
.element => element = parser.nodeToElement(root),
.document => {
const doc = parser.nodeToDocument(root);
element = (try parser.documentGetDocumentElement(doc)).?;
},
else => return error.InvalidState,
}
return Element._getBoundingClientRect(element, self.page);
}
// The Element whose intersection with the root changed.
pub fn get_target(self: *const Entry) *parser.Element {
return self.target;
}
// TODO: pub fn get_time(self: *const Entry)
};
const testing = @import("../../testing.zig");
test "Browser: DOM.IntersectionObserver" {
try testing.htmlRunner("dom/intersection_observer.html");
}

View File

@@ -20,11 +20,13 @@ const std = @import("std");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const js = @import("../js/js.zig"); const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const EventTarget = @import("../dom/event_target.zig").EventTarget; const EventTarget = @import("../dom/event_target.zig").EventTarget;
const EventHandler = @import("../events/event.zig").EventHandler; const EventHandler = @import("../events/event.zig").EventHandler;
const JsObject = Env.JsObject;
const Function = Env.Function;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const MAX_QUEUE_SIZE = 10; const MAX_QUEUE_SIZE = 10;
@@ -70,28 +72,29 @@ pub const MessagePort = struct {
pair: *MessagePort, pair: *MessagePort,
closed: bool = false, closed: bool = false,
started: bool = false, started: bool = false,
onmessage_cbk: ?js.Function = null, onmessage_cbk: ?Function = null,
onmessageerror_cbk: ?js.Function = null, onmessageerror_cbk: ?Function = null,
// This is the queue of messages to dispatch to THIS MessagePort when the // This is the queue of messages to dispatch to THIS MessagePort when the
// MessagePort is started. // MessagePort is started.
queue: std.ArrayListUnmanaged(js.Object) = .empty, queue: std.ArrayListUnmanaged(JsObject) = .empty,
pub const PostMessageOption = union(enum) { pub const PostMessageOption = union(enum) {
transfer: js.Object, transfer: JsObject,
options: Opts, options: Opts,
pub const Opts = struct { pub const Opts = struct {
transfer: js.Object, transfer: JsObject,
}; };
}; };
pub fn _postMessage(self: *MessagePort, obj: js.Object, opts_: ?PostMessageOption, page: *Page) !void { pub fn _postMessage(self: *MessagePort, obj: JsObject, opts_: ?PostMessageOption, page: *Page) !void {
if (self.closed) { if (self.closed) {
return; return;
} }
if (opts_ != null) { if (opts_ != null) {
log.warn(.web_api, "not implemented", .{ .feature = "MessagePort postMessage options" }); log.warn(.web_api, "not implemented", .{ .feature = "MessagePort postMessage options" });
return error.NotImplemented;
} }
try self.pair.dispatchOrQueue(obj, page.arena); try self.pair.dispatchOrQueue(obj, page.arena);
@@ -122,10 +125,10 @@ pub const MessagePort = struct {
self.pair.closed = true; self.pair.closed = true;
} }
pub fn get_onmessage(self: *MessagePort) ?js.Function { pub fn get_onmessage(self: *MessagePort) ?Function {
return self.onmessage_cbk; return self.onmessage_cbk;
} }
pub fn get_onmessageerror(self: *MessagePort) ?js.Function { pub fn get_onmessageerror(self: *MessagePort) ?Function {
return self.onmessageerror_cbk; return self.onmessageerror_cbk;
} }
@@ -150,7 +153,7 @@ pub const MessagePort = struct {
// called from our pair. If port1.postMessage("x") is called, then this // called from our pair. If port1.postMessage("x") is called, then this
// will be called on port2. // will be called on port2.
fn dispatchOrQueue(self: *MessagePort, obj: js.Object, arena: Allocator) !void { fn dispatchOrQueue(self: *MessagePort, obj: JsObject, arena: Allocator) !void {
// our pair should have checked this already // our pair should have checked this already
std.debug.assert(self.closed == false); std.debug.assert(self.closed == false);
@@ -165,7 +168,7 @@ pub const MessagePort = struct {
return self.queue.append(arena, try obj.persist()); return self.queue.append(arena, try obj.persist());
} }
fn dispatch(self: *MessagePort, obj: js.Object) !void { fn dispatch(self: *MessagePort, obj: JsObject) !void {
// obj is already persisted, don't use `MessageEvent.constructor`, but // obj is already persisted, don't use `MessageEvent.constructor`, but
// go directly to `init`, which assumes persisted objects. // go directly to `init`, which assumes persisted objects.
var evt = try MessageEvent.init(.{ .data = obj }); var evt = try MessageEvent.init(.{ .data = obj });
@@ -180,7 +183,7 @@ pub const MessagePort = struct {
alloc: Allocator, alloc: Allocator,
typ: []const u8, typ: []const u8,
listener: EventHandler.Listener, listener: EventHandler.Listener,
) !?js.Function { ) !?Function {
const target = @as(*parser.EventTarget, @ptrCast(self)); const target = @as(*parser.EventTarget, @ptrCast(self));
const eh = (try EventHandler.register(alloc, target, typ, listener, null)) orelse unreachable; const eh = (try EventHandler.register(alloc, target, typ, listener, null)) orelse unreachable;
return eh.callback; return eh.callback;
@@ -205,12 +208,12 @@ pub const MessageEvent = struct {
pub const union_make_copy = true; pub const union_make_copy = true;
proto: parser.Event, proto: parser.Event,
data: ?js.Object, data: ?JsObject,
// You would think if port1 sends to port2, the source would be port2 // 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 // (which is how I read the documentation), but it appears to always be
// null. It can always be set explicitly via the constructor; // null. It can always be set explicitly via the constructor;
source: ?js.Object, source: ?JsObject,
origin: []const u8, origin: []const u8,
@@ -224,8 +227,8 @@ pub const MessageEvent = struct {
ports: []*MessagePort, ports: []*MessagePort,
const Options = struct { const Options = struct {
data: ?js.Object = null, data: ?JsObject = null,
source: ?js.Object = null, source: ?JsObject = null,
origin: []const u8 = "", origin: []const u8 = "",
lastEventId: []const u8 = "", lastEventId: []const u8 = "",
ports: []*MessagePort = &.{}, ports: []*MessagePort = &.{},
@@ -241,7 +244,7 @@ pub const MessageEvent = struct {
}); });
} }
// This is like "constructor", but it assumes js.Objects have already been // This is like "constructor", but it assumes JsObjects have already been
// persisted. Necessary because this `new MessageEvent()` can be called // persisted. Necessary because this `new MessageEvent()` can be called
// directly from JS OR from a port.postMessage. In the latter case, data // 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); // may have already been persisted (as it might need to be queued);
@@ -261,7 +264,7 @@ pub const MessageEvent = struct {
}; };
} }
pub fn get_data(self: *const MessageEvent) !?js.Object { pub fn get_data(self: *const MessageEvent) !?JsObject {
return self.data; return self.data;
} }
@@ -269,7 +272,7 @@ pub const MessageEvent = struct {
return self.origin; return self.origin;
} }
pub fn get_source(self: *const MessageEvent) ?js.Object { pub fn get_source(self: *const MessageEvent) ?JsObject {
return self.source; return self.source;
} }

View File

@@ -25,24 +25,24 @@ pub const Attr = struct {
pub const prototype = *Node; pub const prototype = *Node;
pub const subtype = .node; pub const subtype = .node;
pub fn get_namespaceURI(self: *parser.Attribute) ?[]const u8 { pub fn get_namespaceURI(self: *parser.Attribute) !?[]const u8 {
return parser.nodeGetNamespace(parser.attributeToNode(self)); return try parser.nodeGetNamespace(parser.attributeToNode(self));
} }
pub fn get_prefix(self: *parser.Attribute) ?[]const u8 { pub fn get_prefix(self: *parser.Attribute) !?[]const u8 {
return parser.nodeGetPrefix(parser.attributeToNode(self)); return try parser.nodeGetPrefix(parser.attributeToNode(self));
} }
pub fn get_localName(self: *parser.Attribute) ![]const u8 { pub fn get_localName(self: *parser.Attribute) ![]const u8 {
return parser.nodeLocalName(parser.attributeToNode(self)); return try parser.nodeLocalName(parser.attributeToNode(self));
} }
pub fn get_name(self: *parser.Attribute) ![]const u8 { pub fn get_name(self: *parser.Attribute) ![]const u8 {
return parser.attributeGetName(self); return try parser.attributeGetName(self);
} }
pub fn get_value(self: *parser.Attribute) !?[]const u8 { pub fn get_value(self: *parser.Attribute) !?[]const u8 {
return parser.attributeGetValue(self); return try parser.attributeGetValue(self);
} }
pub fn set_value(self: *parser.Attribute, v: []const u8) !?[]const u8 { pub fn set_value(self: *parser.Attribute, v: []const u8) !?[]const u8 {

View File

@@ -51,7 +51,7 @@ pub const CharacterData = struct {
} }
pub fn get_nextElementSibling(self: *parser.CharacterData) !?ElementUnion { pub fn get_nextElementSibling(self: *parser.CharacterData) !?ElementUnion {
const res = parser.nodeNextElementSibling(parser.characterDataToNode(self)); const res = try parser.nodeNextElementSibling(parser.characterDataToNode(self));
if (res == null) { if (res == null) {
return null; return null;
} }
@@ -59,7 +59,7 @@ pub const CharacterData = struct {
} }
pub fn get_previousElementSibling(self: *parser.CharacterData) !?ElementUnion { pub fn get_previousElementSibling(self: *parser.CharacterData) !?ElementUnion {
const res = parser.nodePreviousElementSibling(parser.characterDataToNode(self)); const res = try parser.nodePreviousElementSibling(parser.characterDataToNode(self));
if (res == null) { if (res == null) {
return null; return null;
} }
@@ -68,8 +68,8 @@ pub const CharacterData = struct {
// Read/Write attributes // Read/Write attributes
pub fn get_data(self: *parser.CharacterData) []const u8 { pub fn get_data(self: *parser.CharacterData) ![]const u8 {
return parser.characterDataData(self); return try parser.characterDataData(self);
} }
pub fn set_data(self: *parser.CharacterData, data: []const u8) !void { pub fn set_data(self: *parser.CharacterData, data: []const u8) !void {
@@ -96,18 +96,18 @@ pub const CharacterData = struct {
} }
pub fn _substringData(self: *parser.CharacterData, offset: u32, count: u32) ![]const u8 { pub fn _substringData(self: *parser.CharacterData, offset: u32, count: u32) ![]const u8 {
return parser.characterDataSubstringData(self, offset, count); return try parser.characterDataSubstringData(self, offset, count);
} }
// netsurf's CharacterData (text, comment) doesn't implement the // netsurf's CharacterData (text, comment) doesn't implement the
// dom_node_get_attributes and thus will crash if we try to call nodeIsEqualNode. // 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 { pub fn _isEqualNode(self: *parser.CharacterData, other_node: *parser.Node) !bool {
if (parser.nodeType(@ptrCast(@alignCast(self))) != parser.nodeType(other_node)) { if (try parser.nodeType(@ptrCast(@alignCast(self))) != try parser.nodeType(other_node)) {
return false; return false;
} }
const other: *parser.CharacterData = @ptrCast(other_node); const other: *parser.CharacterData = @ptrCast(other_node);
if (std.mem.eql(u8, get_data(self), get_data(other)) == false) { if (std.mem.eql(u8, try get_data(self), try get_data(other)) == false) {
return false; return false;
} }

View File

@@ -17,9 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const log = @import("../../log.zig");
const js = @import("../js/js.zig"); const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
@@ -39,6 +38,8 @@ const Range = @import("range.zig").Range;
const CustomEvent = @import("../events/custom_event.zig").CustomEvent; const CustomEvent = @import("../events/custom_event.zig").CustomEvent;
const Env = @import("../env.zig").Env;
const DOMImplementation = @import("implementation.zig").DOMImplementation; const DOMImplementation = @import("implementation.zig").DOMImplementation;
// WEB IDL https://dom.spec.whatwg.org/#document // WEB IDL https://dom.spec.whatwg.org/#document
@@ -155,14 +156,22 @@ pub const Document = struct {
// the spec changed to return an HTMLCollection instead. // the spec changed to return an HTMLCollection instead.
// That's why we reimplemented getElementsByTagName by using an // That's why we reimplemented getElementsByTagName by using an
// HTMLCollection in zig here. // HTMLCollection in zig here.
pub fn _getElementsByTagName(self: *parser.Document, tag_name: js.String) !collection.HTMLCollection { pub fn _getElementsByTagName(
return collection.HTMLCollectionByTagName(parser.documentToNode(self), tag_name.string, .{ self: *parser.Document,
tag_name: []const u8,
page: *Page,
) !collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(page.arena, parser.documentToNode(self), tag_name, .{
.include_root = true, .include_root = true,
}); });
} }
pub fn _getElementsByClassName(self: *parser.Document, class_names: js.String) !collection.HTMLCollection { pub fn _getElementsByClassName(
return collection.HTMLCollectionByClassName(parser.documentToNode(self), class_names.string, .{ self: *parser.Document,
classNames: []const u8,
page: *Page,
) !collection.HTMLCollection {
return try collection.HTMLCollectionByClassName(page.arena, parser.documentToNode(self), classNames, .{
.include_root = true, .include_root = true,
}); });
} }
@@ -299,87 +308,24 @@ pub const Document = struct {
return &.{}; return &.{};
} }
pub fn get_adoptedStyleSheets(self: *parser.Document, page: *Page) !js.Object { pub fn get_adoptedStyleSheets(self: *parser.Document, page: *Page) !Env.JsObject {
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self))); const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
if (state.adopted_style_sheets) |obj| { if (state.adopted_style_sheets) |obj| {
return obj; return obj;
} }
const obj = try page.js.createArray(0).persist(); const obj = try page.main_context.newArray(0).persist();
state.adopted_style_sheets = obj; state.adopted_style_sheets = obj;
return obj; return obj;
} }
pub fn set_adoptedStyleSheets(self: *parser.Document, sheets: js.Object, page: *Page) !void { pub fn set_adoptedStyleSheets(self: *parser.Document, sheets: Env.JsObject, page: *Page) !void {
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self))); const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
state.adopted_style_sheets = try sheets.persist(); state.adopted_style_sheets = try sheets.persist();
} }
pub fn _hasFocus(_: *parser.Document) bool {
log.debug(.web_api, "not implemented", .{ .feature = "Document hasFocus" });
return true;
}
pub fn _open(_: *parser.Document, page: *Page) !*parser.DocumentHTML {
if (page.open) {
return page.window.document;
}
// This implementation is invalid.
// According to MDN, we should cleanup registered listeners.
// So we sould cleanup previous DOM memory.
// But this implementation is more simple for now.
const html_doc = try parser.documentHTMLParseFromStr("");
try page.setDocument(html_doc);
page.open = true;
return page.window.document;
}
pub fn _close(_: *parser.Document, page: *Page) !void {
page.open = false;
}
pub fn _write(self: *parser.Document, str: []const u8, page: *Page) !void {
_ = try _open(self, page);
const document = parser.documentHTMLToDocument(page.window.document);
const fragment = try parser.documentParseFragmentFromStr(document, str);
const fragment_node = parser.documentFragmentToNode(fragment);
const fragment_html = parser.nodeFirstChild(fragment_node) orelse return;
const fragment_head = parser.nodeFirstChild(fragment_html) orelse return;
const fragment_body = parser.nodeNextSibling(fragment_head) orelse return;
const document_node = parser.documentToNode(document);
const document_html = parser.nodeFirstChild(document_node) orelse return;
const document_head = parser.nodeFirstChild(document_html) orelse return;
const document_body = parser.nodeNextSibling(document_head) orelse return;
{
const children = try parser.nodeGetChildNodes(fragment_head);
// always index 0, because nodeAppendChild moves the node out of
// the nodeList and into the new tree
while (parser.nodeListItem(children, 0)) |child| {
_ = try parser.nodeAppendChild(document_head, child);
}
}
{
const children = try parser.nodeGetChildNodes(fragment_body);
// always index 0, because nodeAppendChild moves the node out of
// the nodeList and into the new tree
while (parser.nodeListItem(children, 0)) |child| {
_ = try parser.nodeAppendChild(document_body, child);
}
}
}
}; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: DOM.Document" { test "Browser: DOM.Document" {
try testing.htmlRunner("dom/document.html"); try testing.htmlRunner("dom/document.html");
} }
test "Browser: DOM.Document.write" {
try testing.htmlRunner("dom/document_write.html");
}

View File

@@ -38,8 +38,8 @@ pub const DocumentFragment = struct {
); );
} }
pub fn _isEqualNode(self: *parser.DocumentFragment, other_node: *parser.Node) bool { pub fn _isEqualNode(self: *parser.DocumentFragment, other_node: *parser.Node) !bool {
const other_type = parser.nodeType(other_node); const other_type = try parser.nodeType(other_node);
if (other_type != .document_fragment) { if (other_type != .document_fragment) {
return false; return false;
} }

View File

@@ -29,21 +29,21 @@ pub const DocumentType = struct {
pub const subtype = .node; pub const subtype = .node;
pub fn get_name(self: *parser.DocumentType) ![]const u8 { pub fn get_name(self: *parser.DocumentType) ![]const u8 {
return parser.documentTypeGetName(self); return try parser.documentTypeGetName(self);
} }
pub fn get_publicId(self: *parser.DocumentType) []const u8 { pub fn get_publicId(self: *parser.DocumentType) ![]const u8 {
return parser.documentTypeGetPublicId(self); return try parser.documentTypeGetPublicId(self);
} }
pub fn get_systemId(self: *parser.DocumentType) []const u8 { pub fn get_systemId(self: *parser.DocumentType) ![]const u8 {
return parser.documentTypeGetSystemId(self); return try parser.documentTypeGetSystemId(self);
} }
// netsurf's DocumentType doesn't implement the dom_node_get_attributes // netsurf's DocumentType doesn't implement the dom_node_get_attributes
// and thus will crash if we try to call nodeIsEqualNode. // and thus will crash if we try to call nodeIsEqualNode.
pub fn _isEqualNode(self: *parser.DocumentType, other_node: *parser.Node) !bool { pub fn _isEqualNode(self: *parser.DocumentType, other_node: *parser.Node) !bool {
if (parser.nodeType(other_node) != .document_type) { if (try parser.nodeType(other_node) != .document_type) {
return false; return false;
} }
@@ -51,10 +51,10 @@ pub const DocumentType = struct {
if (std.mem.eql(u8, try get_name(self), try get_name(other)) == false) { if (std.mem.eql(u8, try get_name(self), try get_name(other)) == false) {
return false; return false;
} }
if (std.mem.eql(u8, get_publicId(self), get_publicId(other)) == false) { if (std.mem.eql(u8, try get_publicId(self), try get_publicId(other)) == false) {
return false; return false;
} }
if (std.mem.eql(u8, get_systemId(self), get_systemId(other)) == false) { if (std.mem.eql(u8, try get_systemId(self), try get_systemId(other)) == false) {
return false; return false;
} }
return true; return true;

View File

@@ -25,6 +25,7 @@ const NodeList = @import("nodelist.zig");
const Node = @import("node.zig"); const Node = @import("node.zig");
const ResizeObserver = @import("resize_observer.zig"); const ResizeObserver = @import("resize_observer.zig");
const MutationObserver = @import("mutation_observer.zig"); const MutationObserver = @import("mutation_observer.zig");
const IntersectionObserver = @import("intersection_observer.zig");
const DOMParser = @import("dom_parser.zig").DOMParser; const DOMParser = @import("dom_parser.zig").DOMParser;
const TreeWalker = @import("tree_walker.zig").TreeWalker; const TreeWalker = @import("tree_walker.zig").TreeWalker;
const NodeIterator = @import("node_iterator.zig").NodeIterator; const NodeIterator = @import("node_iterator.zig").NodeIterator;
@@ -43,6 +44,7 @@ pub const Interfaces = .{
Node.Interfaces, Node.Interfaces,
ResizeObserver.Interfaces, ResizeObserver.Interfaces,
MutationObserver.Interfaces, MutationObserver.Interfaces,
IntersectionObserver.Interfaces,
DOMParser, DOMParser,
TreeWalker, TreeWalker,
NodeIterator, NodeIterator,
@@ -52,5 +54,4 @@ pub const Interfaces = .{
@import("range.zig").Interfaces, @import("range.zig").Interfaces,
@import("Animation.zig"), @import("Animation.zig"),
@import("MessageChannel.zig").Interfaces, @import("MessageChannel.zig").Interfaces,
@import("IntersectionObserver.zig").Interfaces,
}; };

View File

@@ -18,7 +18,6 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
@@ -34,6 +33,7 @@ const HTMLElem = @import("../html/elements.zig");
const ShadowRoot = @import("../dom/shadow_root.zig").ShadowRoot; const ShadowRoot = @import("../dom/shadow_root.zig").ShadowRoot;
const Animation = @import("Animation.zig"); const Animation = @import("Animation.zig");
const JsObject = @import("../env.zig").JsObject;
pub const Union = @import("../html/elements.zig").Union; pub const Union = @import("../html/elements.zig").Union;
@@ -61,7 +61,7 @@ pub const Element = struct {
pub fn toInterfaceT(comptime T: type, e: *parser.Element) !T { pub fn toInterfaceT(comptime T: type, e: *parser.Element) !T {
const tagname = try parser.elementGetTagName(e) orelse { const tagname = try parser.elementGetTagName(e) orelse {
// If the owner's document is HTML, assume we have an HTMLElement. // If the owner's document is HTML, assume we have an HTMLElement.
const doc = parser.nodeOwnerDocument(parser.elementToNode(e)); const doc = try parser.nodeOwnerDocument(parser.elementToNode(e));
if (doc != null and !doc.?.is_html) { if (doc != null and !doc.?.is_html) {
return .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(e)) }; return .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(e)) };
} }
@@ -73,7 +73,7 @@ pub const Element = struct {
const tag = parser.Tag.fromString(tagname) catch { const tag = parser.Tag.fromString(tagname) catch {
// If the owner's document is HTML, assume we have an HTMLElement. // If the owner's document is HTML, assume we have an HTMLElement.
const doc = parser.nodeOwnerDocument(parser.elementToNode(e)); const doc = try parser.nodeOwnerDocument(parser.elementToNode(e));
if (doc != null and doc.?.is_html) { if (doc != null and doc.?.is_html) {
return .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(e)) }; return .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(e)) };
} }
@@ -87,12 +87,12 @@ pub const Element = struct {
// JS funcs // JS funcs
// -------- // --------
pub fn get_namespaceURI(self: *parser.Element) ?[]const u8 { pub fn get_namespaceURI(self: *parser.Element) !?[]const u8 {
return parser.nodeGetNamespace(parser.elementToNode(self)); return try parser.nodeGetNamespace(parser.elementToNode(self));
} }
pub fn get_prefix(self: *parser.Element) ?[]const u8 { pub fn get_prefix(self: *parser.Element) !?[]const u8 {
return parser.nodeGetPrefix(parser.elementToNode(self)); return try parser.nodeGetPrefix(parser.elementToNode(self));
} }
pub fn get_localName(self: *parser.Element) ![]const u8 { pub fn get_localName(self: *parser.Element) ![]const u8 {
@@ -103,14 +103,6 @@ pub const Element = struct {
return try parser.nodeName(parser.elementToNode(self)); return try parser.nodeName(parser.elementToNode(self));
} }
pub fn get_dir(self: *parser.Element) ![]const u8 {
return try parser.elementGetAttribute(self, "dir") orelse "";
}
pub fn set_dir(self: *parser.Element, dir: []const u8) !void {
return parser.elementSetAttribute(self, "dir", dir);
}
pub fn get_id(self: *parser.Element) ![]const u8 { pub fn get_id(self: *parser.Element) ![]const u8 {
return try parser.elementGetAttribute(self, "id") orelse ""; return try parser.elementGetAttribute(self, "id") orelse "";
} }
@@ -135,10 +127,6 @@ pub const Element = struct {
return try parser.elementSetAttribute(self, "slot", slot); return try parser.elementSetAttribute(self, "slot", slot);
} }
pub fn get_assignedSlot(self: *parser.Element, page: *const Page) !?*parser.Slot {
return @import("../SlotChangeMonitor.zig").findSlot(self, page);
}
pub fn get_classList(self: *parser.Element) !*parser.TokenList { pub fn get_classList(self: *parser.Element) !*parser.TokenList {
return try parser.tokenListCreate(self, "class"); return try parser.tokenListCreate(self, "class");
} }
@@ -162,7 +150,7 @@ pub const Element = struct {
pub fn set_innerHTML(self: *parser.Element, str: []const u8, page: *Page) !void { pub fn set_innerHTML(self: *parser.Element, str: []const u8, page: *Page) !void {
const node = parser.elementToNode(self); const node = parser.elementToNode(self);
const doc = parser.nodeOwnerDocument(node) orelse return parser.DOMError.WrongDocument; const doc = try parser.nodeOwnerDocument(node) orelse return parser.DOMError.WrongDocument;
// parse the fragment // parse the fragment
const fragment = try parser.documentParseFragmentFromStr(doc, str); const fragment = try parser.documentParseFragmentFromStr(doc, str);
@@ -180,9 +168,9 @@ pub const Element = struct {
// or an actual document. In a blank page, something like: // or an actual document. In a blank page, something like:
// x.innerHTML = '<script></script>'; // x.innerHTML = '<script></script>';
// does _not_ create an empty script, but in a real page, it does. Weird. // does _not_ create an empty script, but in a real page, it does. Weird.
const html = parser.nodeFirstChild(fragment_node) orelse return; const html = try parser.nodeFirstChild(fragment_node) orelse return;
const head = parser.nodeFirstChild(html) orelse return; const head = try parser.nodeFirstChild(html) orelse return;
const body = parser.nodeNextSibling(head) orelse return; const body = try parser.nodeNextSibling(head) orelse return;
if (try parser.elementTag(self) == .template) { if (try parser.elementTag(self) == .template) {
// HTMLElementTemplate is special. We don't append these as children // HTMLElementTemplate is special. We don't append these as children
@@ -191,9 +179,11 @@ pub const Element = struct {
// a new fragment // a new fragment
const clean = try parser.documentCreateDocumentFragment(doc); const clean = try parser.documentCreateDocumentFragment(doc);
const children = try parser.nodeGetChildNodes(body); const children = try parser.nodeGetChildNodes(body);
// always index 0, because nodeAppendChild moves the node out of const ln = try parser.nodeListLength(children);
// the nodeList and into the new tree for (0..ln) |_| {
while (parser.nodeListItem(children, 0)) |child| { // 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); _ = try parser.nodeAppendChild(@ptrCast(@alignCast(clean)), child);
} }
@@ -207,102 +197,27 @@ pub const Element = struct {
{ {
// First, copy some of the head element // First, copy some of the head element
const children = try parser.nodeGetChildNodes(head); const children = try parser.nodeGetChildNodes(head);
// always index 0, because nodeAppendChild moves the node out of const ln = try parser.nodeListLength(children);
// the nodeList and into the new tree for (0..ln) |_| {
while (parser.nodeListItem(children, 0)) |child| { // 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); _ = try parser.nodeAppendChild(node, child);
} }
} }
{ {
const children = try parser.nodeGetChildNodes(body); const children = try parser.nodeGetChildNodes(body);
// always index 0, because nodeAppendChild moves the node out of const ln = try parser.nodeListLength(children);
// the nodeList and into the new tree for (0..ln) |_| {
while (parser.nodeListItem(children, 0)) |child| { // 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); _ = try parser.nodeAppendChild(node, child);
} }
} }
} }
/// Parses the given `input` string and inserts its children to an element at given `position`.
/// https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML
///
/// TODO: Support for XML parsing and `TrustedHTML` instances.
pub fn _insertAdjacentHTML(self: *parser.Element, position: []const u8, input: []const u8) !void {
const self_node = parser.elementToNode(self);
const doc = parser.nodeOwnerDocument(self_node) orelse {
return parser.DOMError.WrongDocument;
};
// Parse the fragment.
// Should return error.Syntax on fail?
const fragment = try parser.documentParseFragmentFromStr(doc, input);
const fragment_node = parser.documentFragmentToNode(fragment);
// We always get it wrapped like so:
// <html><head></head><body>{ ... }</body></html>
// None of the following can be null.
const maybe_html = parser.nodeFirstChild(fragment_node);
std.debug.assert(maybe_html != null);
const html = maybe_html orelse return;
const maybe_body = parser.nodeLastChild(html);
std.debug.assert(maybe_body != null);
const body = maybe_body orelse return;
const children = try parser.nodeGetChildNodes(body);
// * `target_node` is `*Node` (where we actually insert),
// * `prev_node` is `?*Node`.
const target_node, const prev_node = blk: {
// Prefer case-sensitive match.
// "beforeend" was the most common case in my tests; we might adjust the order
// depending on which ones websites prefer most.
if (std.mem.eql(u8, position, "beforeend")) {
break :blk .{ self_node, null };
}
if (std.mem.eql(u8, position, "afterbegin")) {
// Get the first child; null indicates there are no children.
const first_child = parser.nodeFirstChild(self_node);
break :blk .{ self_node, first_child };
}
if (std.mem.eql(u8, position, "beforebegin")) {
// The node must have a parent node in order to use this variant.
const parent = parser.nodeParentNode(self_node) orelse return error.NoModificationAllowed;
// Parent cannot be Document.
// Should have checks for document_fragment and document_type?
if (parser.nodeType(parent) == .document) {
return error.NoModificationAllowed;
}
break :blk .{ parent, self_node };
}
if (std.mem.eql(u8, position, "afterend")) {
// The node must have a parent node in order to use this variant.
const parent = parser.nodeParentNode(self_node) orelse return error.NoModificationAllowed;
// Parent cannot be Document.
if (parser.nodeType(parent) == .document) {
return error.NoModificationAllowed;
}
// Get the next sibling or null; null indicates our node is the only one.
const sibling = parser.nodeNextSibling(self_node);
break :blk .{ parent, sibling };
}
// Thrown if:
// * position is not one of the four listed values.
// * The input is XML that is not well-formed.
return error.Syntax;
};
while (parser.nodeListItem(children, 0)) |child| {
_ = try parser.nodeInsertBefore(target_node, child, prev_node);
}
}
// 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. // 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. // 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, page: *Page) !?*parser.Element {
@@ -319,7 +234,7 @@ pub const Element = struct {
} }
return parser.nodeToElement(current.node); return parser.nodeToElement(current.node);
} }
current = current.parent() orelse return null; current = try current.parent() orelse return null;
} }
} }
@@ -435,18 +350,28 @@ pub const Element = struct {
return try parser.elementRemoveAttributeNode(self, attr); return try parser.elementRemoveAttributeNode(self, attr);
} }
pub fn _getElementsByTagName(self: *parser.Element, tag_name: js.String) !collection.HTMLCollection { pub fn _getElementsByTagName(
return collection.HTMLCollectionByTagName( self: *parser.Element,
tag_name: []const u8,
page: *Page,
) !collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(
page.arena,
parser.elementToNode(self), parser.elementToNode(self),
tag_name.string, tag_name,
.{ .include_root = false }, .{ .include_root = false },
); );
} }
pub fn _getElementsByClassName(self: *parser.Element, class_names: js.String) !collection.HTMLCollection { pub fn _getElementsByClassName(
self: *parser.Element,
classNames: []const u8,
page: *Page,
) !collection.HTMLCollection {
return try collection.HTMLCollectionByClassName( return try collection.HTMLCollectionByClassName(
page.arena,
parser.elementToNode(self), parser.elementToNode(self),
class_names.string, classNames,
.{ .include_root = false }, .{ .include_root = false },
); );
} }
@@ -482,13 +407,13 @@ pub const Element = struct {
// NonDocumentTypeChildNode // NonDocumentTypeChildNode
// https://dom.spec.whatwg.org/#interface-nondocumenttypechildnode // https://dom.spec.whatwg.org/#interface-nondocumenttypechildnode
pub fn get_previousElementSibling(self: *parser.Element) !?Union { pub fn get_previousElementSibling(self: *parser.Element) !?Union {
const res = parser.nodePreviousElementSibling(parser.elementToNode(self)); const res = try parser.nodePreviousElementSibling(parser.elementToNode(self));
if (res == null) return null; if (res == null) return null;
return try toInterface(res.?); return try toInterface(res.?);
} }
pub fn get_nextElementSibling(self: *parser.Element) !?Union { pub fn get_nextElementSibling(self: *parser.Element) !?Union {
const res = parser.nodeNextElementSibling(parser.elementToNode(self)); const res = try parser.nodeNextElementSibling(parser.elementToNode(self));
if (res == null) return null; if (res == null) return null;
return try toInterface(res.?); return try toInterface(res.?);
} }
@@ -501,7 +426,7 @@ pub const Element = struct {
while (true) { while (true) {
next = try walker.get_next(root, next) orelse return null; next = try walker.get_next(root, next) orelse return null;
// ignore non-element nodes. // ignore non-element nodes.
if (parser.nodeType(next.?) != .element) { if (try parser.nodeType(next.?) != .element) {
continue; continue;
} }
const e = parser.nodeToElement(next.?); const e = parser.nodeToElement(next.?);
@@ -549,7 +474,7 @@ pub const Element = struct {
// Returns a 0 DOMRect object if the element is eventually detached from the main window // Returns a 0 DOMRect object if the element is eventually detached from the main window
pub fn _getBoundingClientRect(self: *parser.Element, page: *Page) !DOMRect { 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. // 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 (!page.isNodeAttached(parser.elementToNode(self))) { if (!try page.isNodeAttached(parser.elementToNode(self))) {
return DOMRect{ return DOMRect{
.x = 0, .x = 0,
.y = 0, .y = 0,
@@ -568,7 +493,7 @@ pub const Element = struct {
// We do not render so it only always return the element's bounding rect. // 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 // Returns an empty array if the element is eventually detached from the main window
pub fn _getClientRects(self: *parser.Element, page: *Page) ![]DOMRect { pub fn _getClientRects(self: *parser.Element, page: *Page) ![]DOMRect {
if (!page.isNodeAttached(parser.elementToNode(self))) { if (!try page.isNodeAttached(parser.elementToNode(self))) {
return &.{}; return &.{};
} }
const heap_ptr = try page.call_arena.create(DOMRect); const heap_ptr = try page.call_arena.create(DOMRect);
@@ -599,8 +524,6 @@ pub const Element = struct {
contentVisibilityAuto: bool, contentVisibilityAuto: bool,
opacityProperty: bool, opacityProperty: bool,
visibilityProperty: bool, visibilityProperty: bool,
checkVisibilityCSS: bool,
checkOpacity: bool,
}; };
pub fn _checkVisibility(self: *parser.Element, opts: ?CheckVisibilityOpts) bool { pub fn _checkVisibility(self: *parser.Element, opts: ?CheckVisibilityOpts) bool {
@@ -626,7 +549,7 @@ pub const Element = struct {
} }
// Not sure what to do if there is no owner document // Not sure what to do if there is no owner document
const doc = parser.nodeOwnerDocument(@ptrCast(self)) orelse return error.InvalidArgument; const doc = try parser.nodeOwnerDocument(@ptrCast(self)) orelse return error.InvalidArgument;
const fragment = try parser.documentCreateDocumentFragment(doc); const fragment = try parser.documentCreateDocumentFragment(doc);
const sr = try page.arena.create(ShadowRoot); const sr = try page.arena.create(ShadowRoot);
sr.* = .{ sr.* = .{
@@ -660,7 +583,7 @@ pub const Element = struct {
return sr; return sr;
} }
pub fn _animate(self: *parser.Element, effect: js.Object, opts: js.Object) !Animation { pub fn _animate(self: *parser.Element, effect: JsObject, opts: JsObject) !Animation {
_ = self; _ = self;
_ = opts; _ = opts;
return Animation.constructor(effect, null); return Animation.constructor(effect, null);
@@ -672,7 +595,7 @@ pub const Element = struct {
// for related elements JIT by walking the tree, but there could be // for related elements JIT by walking the tree, but there could be
// cases in libdom or the Zig WebAPI where this reference is kept // cases in libdom or the Zig WebAPI where this reference is kept
const as_node: *parser.Node = @ptrCast(self); const as_node: *parser.Node = @ptrCast(self);
const parent = parser.nodeParentNode(as_node) orelse return; const parent = try parser.nodeParentNode(as_node) orelse return;
_ = try Node._removeChild(parent, as_node); _ = try Node._removeChild(parent, as_node);
} }
}; };

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const Env = @import("../env.zig").Env;
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
@@ -34,7 +35,6 @@ pub const Union = union(enum) {
screen_orientation: *@import("../html/screen.zig").ScreenOrientation, screen_orientation: *@import("../html/screen.zig").ScreenOrientation,
performance: *@import("performance.zig").Performance, performance: *@import("performance.zig").Performance,
media_query_list: *@import("../html/media_query_list.zig").MediaQueryList, media_query_list: *@import("../html/media_query_list.zig").MediaQueryList,
navigation: *@import("../navigation/Navigation.zig"),
}; };
// EventTarget implementation // EventTarget implementation
@@ -48,7 +48,7 @@ pub const EventTarget = struct {
pub fn toInterface(et: *parser.EventTarget, page: *Page) !Union { pub fn toInterface(et: *parser.EventTarget, page: *Page) !Union {
// libdom assumes that all event targets are libdom nodes. They are not. // libdom assumes that all event targets are libdom nodes. They are not.
switch (parser.eventTargetInternalType(et)) { switch (try parser.eventTargetInternalType(et)) {
.libdom_node => { .libdom_node => {
return .{ .node = try nod.Node.toInterface(@as(*parser.Node, @ptrCast(et))) }; return .{ .node = try nod.Node.toInterface(@as(*parser.Node, @ptrCast(et))) };
}, },
@@ -83,11 +83,6 @@ pub const EventTarget = struct {
.media_query_list => { .media_query_list => {
return .{ .media_query_list = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et))) }; return .{ .media_query_list = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et))) };
}, },
.navigation => {
const NavigationEventTarget = @import("../navigation/NavigationEventTarget.zig");
const base: *NavigationEventTarget = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et)));
return .{ .navigation = @fieldParentPtr("proto", base) };
},
} }
} }
@@ -95,7 +90,6 @@ pub const EventTarget = struct {
// -------- // --------
pub fn constructor(page: *Page) !*parser.EventTarget { pub fn constructor(page: *Page) !*parser.EventTarget {
const et = try page.arena.create(EventTarget); const et = try page.arena.create(EventTarget);
et.* = .{};
return @ptrCast(&et.base); return @ptrCast(&et.base);
} }
@@ -107,9 +101,6 @@ pub const EventTarget = struct {
page: *Page, page: *Page,
) !void { ) !void {
_ = try EventHandler.register(page.arena, self, typ, listener, opts); _ = try EventHandler.register(page.arena, self, typ, listener, opts);
if (std.mem.eql(u8, typ, "slotchange")) {
try page.registerSlotChangeMonitor();
}
} }
const RemoveEventListenerOpts = union(enum) { const RemoveEventListenerOpts = union(enum) {

View File

@@ -23,6 +23,7 @@ const parser = @import("../netsurf.zig");
const Element = @import("element.zig").Element; const Element = @import("element.zig").Element;
const Union = @import("element.zig").Union; const Union = @import("element.zig").Union;
const JsThis = @import("../env.zig").JsThis;
const Walker = @import("walker.zig").Walker; const Walker = @import("walker.zig").Walker;
const Matcher = union(enum) { const Matcher = union(enum) {
@@ -51,13 +52,13 @@ pub const MatchByTagName = struct {
tag: []const u8, tag: []const u8,
is_wildcard: bool, is_wildcard: bool,
fn init(tag_name: []const u8) MatchByTagName { fn init(arena: Allocator, tag_name: []const u8) !MatchByTagName {
if (std.mem.eql(u8, tag_name, "*")) { if (std.mem.eql(u8, tag_name, "*")) {
return .{ .tag = "*", .is_wildcard = true }; return .{ .tag = "*", .is_wildcard = true };
} }
return .{ return .{
.tag = tag_name, .tag = try arena.dupe(u8, tag_name),
.is_wildcard = false, .is_wildcard = false,
}; };
} }
@@ -68,14 +69,15 @@ pub const MatchByTagName = struct {
}; };
pub fn HTMLCollectionByTagName( pub fn HTMLCollectionByTagName(
arena: Allocator,
root: ?*parser.Node, root: ?*parser.Node,
tag_name: []const u8, tag_name: []const u8,
opts: Opts, opts: Opts,
) HTMLCollection { ) !HTMLCollection {
return .{ return HTMLCollection{
.root = root, .root = root,
.walker = .{ .walkerDepthFirst = .{} }, .walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByTagName = MatchByTagName.init(tag_name) }, .matcher = .{ .matchByTagName = try MatchByTagName.init(arena, tag_name) },
.mutable = opts.mutable, .mutable = opts.mutable,
.include_root = opts.include_root, .include_root = opts.include_root,
}; };
@@ -84,9 +86,9 @@ pub fn HTMLCollectionByTagName(
pub const MatchByClassName = struct { pub const MatchByClassName = struct {
class_names: []const u8, class_names: []const u8,
fn init(class_names: []const u8) !MatchByClassName { fn init(arena: Allocator, class_names: []const u8) !MatchByClassName {
return .{ return .{
.class_names = class_names, .class_names = try arena.dupe(u8, class_names),
}; };
} }
@@ -105,14 +107,15 @@ pub const MatchByClassName = struct {
}; };
pub fn HTMLCollectionByClassName( pub fn HTMLCollectionByClassName(
arena: Allocator,
root: ?*parser.Node, root: ?*parser.Node,
class_names: []const u8, classNames: []const u8,
opts: Opts, opts: Opts,
) !HTMLCollection { ) !HTMLCollection {
return HTMLCollection{ return HTMLCollection{
.root = root, .root = root,
.walker = .{ .walkerDepthFirst = .{} }, .walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByClassName = try MatchByClassName.init(class_names) }, .matcher = .{ .matchByClassName = try MatchByClassName.init(arena, classNames) },
.mutable = opts.mutable, .mutable = opts.mutable,
.include_root = opts.include_root, .include_root = opts.include_root,
}; };
@@ -121,8 +124,10 @@ pub fn HTMLCollectionByClassName(
pub const MatchByName = struct { pub const MatchByName = struct {
name: []const u8, name: []const u8,
fn init(name: []const u8) !MatchByName { fn init(arena: Allocator, name: []const u8) !MatchByName {
return .{ .name = name }; return .{
.name = try arena.dupe(u8, name),
};
} }
pub fn match(self: MatchByName, node: *parser.Node) !bool { pub fn match(self: MatchByName, node: *parser.Node) !bool {
@@ -133,6 +138,7 @@ pub const MatchByName = struct {
}; };
pub fn HTMLCollectionByName( pub fn HTMLCollectionByName(
arena: Allocator,
root: ?*parser.Node, root: ?*parser.Node,
name: []const u8, name: []const u8,
opts: Opts, opts: Opts,
@@ -140,7 +146,7 @@ pub fn HTMLCollectionByName(
return HTMLCollection{ return HTMLCollection{
.root = root, .root = root,
.walker = .{ .walkerDepthFirst = .{} }, .walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByName = try MatchByName.init(name) }, .matcher = .{ .matchByName = try MatchByName.init(arena, name) },
.mutable = opts.mutable, .mutable = opts.mutable,
.include_root = opts.include_root, .include_root = opts.include_root,
}; };
@@ -197,8 +203,8 @@ pub fn HTMLCollectionChildren(
}; };
} }
pub fn HTMLCollectionEmpty() HTMLCollection { pub fn HTMLCollectionEmpty() !HTMLCollection {
return .{ return HTMLCollection{
.root = null, .root = null,
.walker = .{ .walkerNone = .{} }, .walker = .{ .walkerNone = .{} },
.matcher = .{ .matchFalse = .{} }, .matcher = .{ .matchFalse = .{} },
@@ -220,11 +226,14 @@ pub const MatchByLinks = struct {
} }
}; };
pub fn HTMLCollectionByLinks(root: ?*parser.Node, opts: Opts) HTMLCollection { pub fn HTMLCollectionByLinks(
return .{ root: ?*parser.Node,
opts: Opts,
) !HTMLCollection {
return HTMLCollection{
.root = root, .root = root,
.walker = .{ .walkerDepthFirst = .{} }, .walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByLinks = .{} }, .matcher = .{ .matchByLinks = MatchByLinks{} },
.mutable = opts.mutable, .mutable = opts.mutable,
.include_root = opts.include_root, .include_root = opts.include_root,
}; };
@@ -243,11 +252,14 @@ pub const MatchByAnchors = struct {
} }
}; };
pub fn HTMLCollectionByAnchors(root: ?*parser.Node, opts: Opts) HTMLCollection { pub fn HTMLCollectionByAnchors(
return .{ root: ?*parser.Node,
opts: Opts,
) !HTMLCollection {
return HTMLCollection{
.root = root, .root = root,
.walker = .{ .walkerDepthFirst = .{} }, .walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByAnchors = .{} }, .matcher = .{ .matchByAnchors = MatchByAnchors{} },
.mutable = opts.mutable, .mutable = opts.mutable,
.include_root = opts.include_root, .include_root = opts.include_root,
}; };
@@ -286,7 +298,7 @@ const Opts = struct {
// WEB IDL https://dom.spec.whatwg.org/#htmlcollection // WEB IDL https://dom.spec.whatwg.org/#htmlcollection
// HTMLCollection is re implemented in zig here because libdom // HTMLCollection is re implemented in zig here because libdom
// dom_html_collection expects a comparison function callback as argument. // dom_html_collection expects a comparison function callback as arguement.
// But we wanted a dynamically comparison here, according to the match tagname. // But we wanted a dynamically comparison here, according to the match tagname.
pub const HTMLCollection = struct { pub const HTMLCollection = struct {
matcher: Matcher, matcher: Matcher,
@@ -332,7 +344,7 @@ pub const HTMLCollection = struct {
var node = try self.start() orelse return 0; var node = try self.start() orelse return 0;
while (true) { while (true) {
if (parser.nodeType(node) == .element) { if (try parser.nodeType(node) == .element) {
if (try self.matcher.match(node)) { if (try self.matcher.match(node)) {
len += 1; len += 1;
} }
@@ -359,7 +371,7 @@ pub const HTMLCollection = struct {
} }
while (true) { while (true) {
if (parser.nodeType(node) == .element) { if (try parser.nodeType(node) == .element) {
if (try self.matcher.match(node)) { if (try self.matcher.match(node)) {
// check if we found the searched element. // check if we found the searched element.
if (i == index) { if (i == index) {
@@ -393,7 +405,7 @@ pub const HTMLCollection = struct {
var node = try self.start() orelse return null; var node = try self.start() orelse return null;
while (true) { while (true) {
if (parser.nodeType(node) == .element) { if (try parser.nodeType(node) == .element) {
if (try self.matcher.match(node)) { if (try self.matcher.match(node)) {
const elem = @as(*parser.Element, @ptrCast(node)); const elem = @as(*parser.Element, @ptrCast(node));
@@ -428,23 +440,24 @@ pub const HTMLCollection = struct {
return null; return null;
} }
pub fn indexed_get(self: *HTMLCollection, index: u32, has_value: *bool) !?Union { pub fn postAttach(self: *HTMLCollection, js_this: JsThis) !void {
return (try _item(self, index)) orelse { const len = try self.get_length();
has_value.* = false; for (0..len) |i| {
return undefined; 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, .{});
pub fn named_get(self: *const HTMLCollection, name: []const u8, has_value: *bool) !?Union { if (try item_name(e)) |name| {
// Even though an entry might have an empty id, the spec says // Even though an entry might have an empty id, the spec says
// that namedItem("") should always return null // that namedItem("") should always return null
if (name.len == 0) { if (name.len > 0) {
return null; // Named fields should not be enumerable (it is defined with
// the LegacyUnenumerableNamedProperties flag.)
try js_this.set(name, as_interface, .{ .DONT_ENUM = true });
}
}
} }
return (try _namedItem(self, name)) orelse {
has_value.* = false;
return undefined;
};
} }
}; };

View File

@@ -0,0 +1,186 @@
// 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 Page = @import("../page.zig").Page;
const Env = @import("../env.zig").Env;
const Element = @import("element.zig").Element;
pub const Interfaces = .{
IntersectionObserver,
IntersectionObserverEntry,
};
// 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.
// Instead we keep a list of all entries that are being observed.
// The callback is called with all entries everytime a new entry is added(observed).
// Potentially we should also call the callback at a regular interval.
// 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,
options: IntersectionObserverOptions,
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 {
var options = IntersectionObserverOptions{
.root = parser.documentToNode(parser.documentHTMLToDocument(page.window.document)),
.rootMargin = "0px 0px 0px 0px",
.threshold = .{ .single = 0.0 },
};
if (options_) |*o| {
if (o.root) |root| {
options.root = root;
} // Other properties are not used due to the way we render
}
return .{
.page = page,
.callback = callback,
.options = options,
.observed_entries = .{},
};
}
pub fn _disconnect(self: *IntersectionObserver) !void {
self.observed_entries = .{}; // We don't free as it is on an arena
}
pub fn _observe(self: *IntersectionObserver, target_element: *parser.Element) !void {
for (self.observed_entries.items) |*observer| {
if (observer.target == target_element) {
return; // Already observed
}
}
try self.observed_entries.append(self.page.arena, .{
.page = self.page,
.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",
});
};
}
pub fn _unobserve(self: *IntersectionObserver, target: *parser.Element) !void {
for (self.observed_entries.items, 0..) |*observer, index| {
if (observer.target == target) {
_ = self.observed_entries.swapRemove(index);
break;
}
}
}
pub fn _takeRecords(self: *IntersectionObserver) []IntersectionObserverEntry {
return self.observed_entries.items;
}
};
const IntersectionObserverOptions = struct {
root: ?*parser.Node, // Element or Document
rootMargin: ?[]const u8,
threshold: ?Threshold,
const Threshold = union(enum) {
single: f32,
list: []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,
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);
}
// Returns the ratio of the intersectionRect to the boundingClientRect.
pub fn get_intersectionRatio(_: *const IntersectionObserverEntry) f32 {
return 1.0;
}
// 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);
}
// 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;
}
// 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();
}
const root_type = try parser.nodeType(root);
var element: *parser.Element = undefined;
switch (root_type) {
.element => element = parser.nodeToElement(root),
.document => {
const doc = parser.nodeToDocument(root);
element = (try parser.documentGetDocumentElement(doc)).?;
},
else => return error.InvalidState,
}
return Element._getBoundingClientRect(element, self.page);
}
// The Element whose intersection with the root changed.
pub fn get_target(self: *const IntersectionObserverEntry) *parser.Element {
return self.target;
}
// TODO: pub fn get_time(self: *const IntersectionObserverEntry)
};
const testing = @import("../../testing.zig");
test "Browser: DOM.IntersectionObserver" {
try testing.htmlRunner("dom/intersection_observer.html");
}

View File

@@ -17,12 +17,13 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator;
const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const Env = @import("../env.zig").Env;
const NodeList = @import("nodelist.zig").NodeList; const NodeList = @import("nodelist.zig").NodeList;
pub const Interfaces = .{ pub const Interfaces = .{
@@ -35,21 +36,21 @@ const Walker = @import("../dom/walker.zig").WalkerChildren;
// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver // WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
pub const MutationObserver = struct { pub const MutationObserver = struct {
page: *Page, page: *Page,
cbk: js.Function, cbk: Env.Function,
connected: bool,
scheduled: bool, scheduled: bool,
observers: std.ArrayListUnmanaged(*Observer),
// List of records which were observed. When the call scope ends, we need to // List of records which were observed. When the call scope ends, we need to
// execute our callback with it. // execute our callback with it.
observed: std.ArrayListUnmanaged(MutationRecord), observed: std.ArrayListUnmanaged(MutationRecord),
pub fn constructor(cbk: js.Function, page: *Page) !MutationObserver { pub fn constructor(cbk: Env.Function, page: *Page) !MutationObserver {
return .{ return .{
.cbk = cbk, .cbk = cbk,
.page = page, .page = page,
.observed = .{}, .observed = .{},
.connected = true,
.scheduled = false, .scheduled = false,
.observers = .empty,
}; };
} }
@@ -68,17 +69,15 @@ pub const MutationObserver = struct {
.event_node = .{ .id = self.cbk.id, .func = Observer.handle }, .event_node = .{ .id = self.cbk.id, .func = Observer.handle },
}; };
try self.observers.append(arena, observer);
// register node's events // register node's events
if (options.childList or options.subtree) { if (options.childList or options.subtree) {
observer.dom_node_inserted_listener = try parser.eventTargetAddEventListener( _ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
"DOMNodeInserted", "DOMNodeInserted",
&observer.event_node, &observer.event_node,
false, false,
); );
observer.dom_node_removed_listener = try parser.eventTargetAddEventListener( _ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
"DOMNodeRemoved", "DOMNodeRemoved",
&observer.event_node, &observer.event_node,
@@ -86,7 +85,7 @@ pub const MutationObserver = struct {
); );
} }
if (options.attr()) { if (options.attr()) {
observer.dom_node_attribute_modified_listener = try parser.eventTargetAddEventListener( _ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
"DOMAttrModified", "DOMAttrModified",
&observer.event_node, &observer.event_node,
@@ -94,7 +93,7 @@ pub const MutationObserver = struct {
); );
} }
if (options.cdata()) { if (options.cdata()) {
observer.dom_cdata_modified_listener = try parser.eventTargetAddEventListener( _ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
"DOMCharacterDataModified", "DOMCharacterDataModified",
&observer.event_node, &observer.event_node,
@@ -102,7 +101,7 @@ pub const MutationObserver = struct {
); );
} }
if (options.subtree) { if (options.subtree) {
observer.dom_subtree_modified_listener = try parser.eventTargetAddEventListener( _ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
"DOMSubtreeModified", "DOMSubtreeModified",
&observer.event_node, &observer.event_node,
@@ -113,6 +112,10 @@ pub const MutationObserver = struct {
fn callback(ctx: *anyopaque) ?u32 { fn callback(ctx: *anyopaque) ?u32 {
const self: *MutationObserver = @ptrCast(@alignCast(ctx)); const self: *MutationObserver = @ptrCast(@alignCast(ctx));
if (self.connected == false) {
self.scheduled = true;
return null;
}
self.scheduled = false; self.scheduled = false;
const records = self.observed.items; const records = self.observed.items;
@@ -122,8 +125,8 @@ pub const MutationObserver = struct {
defer self.observed.clearRetainingCapacity(); defer self.observed.clearRetainingCapacity();
var result: js.Function.Result = undefined; var result: Env.Function.Result = undefined;
self.cbk.tryCallWithThis(void, self, .{records}, &result) catch { self.cbk.tryCall(void, .{records}, &result) catch {
log.debug(.user_script, "callback error", .{ log.debug(.user_script, "callback error", .{
.err = result.exception, .err = result.exception,
.stack = result.stack, .stack = result.stack,
@@ -133,55 +136,9 @@ pub const MutationObserver = struct {
return null; return null;
} }
// TODO
pub fn _disconnect(self: *MutationObserver) !void { pub fn _disconnect(self: *MutationObserver) !void {
for (self.observers.items) |observer| { self.connected = false;
const event_target = parser.toEventTarget(parser.Node, observer.node);
if (observer.dom_node_inserted_listener) |listener| {
try parser.eventTargetRemoveEventListener(
event_target,
"DOMNodeInserted",
listener,
false,
);
}
if (observer.dom_node_removed_listener) |listener| {
try parser.eventTargetRemoveEventListener(
event_target,
"DOMNodeRemoved",
listener,
false,
);
}
if (observer.dom_node_attribute_modified_listener) |listener| {
try parser.eventTargetRemoveEventListener(
event_target,
"DOMAttrModified",
listener,
false,
);
}
if (observer.dom_cdata_modified_listener) |listener| {
try parser.eventTargetRemoveEventListener(
event_target,
"DOMCharacterDataModified",
listener,
false,
);
}
if (observer.dom_subtree_modified_listener) |listener| {
try parser.eventTargetRemoveEventListener(
event_target,
"DOMSubtreeModified",
listener,
false,
);
}
}
self.observers.clearRetainingCapacity();
} }
// TODO // TODO
@@ -266,12 +223,6 @@ const Observer = struct {
event_node: parser.EventNode, event_node: parser.EventNode,
dom_node_inserted_listener: ?*parser.EventListener = null,
dom_node_removed_listener: ?*parser.EventListener = null,
dom_node_attribute_modified_listener: ?*parser.EventListener = null,
dom_cdata_modified_listener: ?*parser.EventListener = null,
dom_subtree_modified_listener: ?*parser.EventListener = null,
fn appliesTo( fn appliesTo(
self: *const Observer, self: *const Observer,
target: *parser.Node, target: *parser.Node,
@@ -333,7 +284,7 @@ const Observer = struct {
const mutation_event = parser.eventToMutationEvent(event); const mutation_event = parser.eventToMutationEvent(event);
const event_type = blk: { const event_type = blk: {
const t = parser.eventType(event); const t = try parser.eventType(event);
break :blk std.meta.stringToEnum(MutationEventType, t) orelse return; break :blk std.meta.stringToEnum(MutationEventType, t) orelse return;
}; };
@@ -351,12 +302,12 @@ const Observer = struct {
.DOMAttrModified => { .DOMAttrModified => {
record.attribute_name = parser.mutationEventAttributeName(mutation_event) catch null; record.attribute_name = parser.mutationEventAttributeName(mutation_event) catch null;
if (self.options.attributeOldValue) { if (self.options.attributeOldValue) {
record.old_value = parser.mutationEventPrevValue(mutation_event); record.old_value = parser.mutationEventPrevValue(mutation_event) catch null;
} }
}, },
.DOMCharacterDataModified => { .DOMCharacterDataModified => {
if (self.options.characterDataOldValue) { if (self.options.characterDataOldValue) {
record.old_value = parser.mutationEventPrevValue(mutation_event); record.old_value = parser.mutationEventPrevValue(mutation_event) catch null;
} }
}, },
.DOMNodeInserted => { .DOMNodeInserted => {

View File

@@ -20,7 +20,7 @@ const std = @import("std");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const generate = @import("../js/generate.zig"); const generate = @import("../../runtime/generate.zig");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const EventTarget = @import("event_target.zig").EventTarget; const EventTarget = @import("event_target.zig").EventTarget;
@@ -67,7 +67,7 @@ pub const Node = struct {
pub const subtype = .node; pub const subtype = .node;
pub fn toInterface(node: *parser.Node) !Union { pub fn toInterface(node: *parser.Node) !Union {
return switch (parser.nodeType(node)) { return switch (try parser.nodeType(node)) {
.element => try Element.toInterfaceT( .element => try Element.toInterfaceT(
Union, Union,
@as(*parser.Element, @ptrCast(node)), @as(*parser.Element, @ptrCast(node)),
@@ -124,7 +124,7 @@ pub const Node = struct {
} }
pub fn get_firstChild(self: *parser.Node) !?Union { pub fn get_firstChild(self: *parser.Node) !?Union {
const res = parser.nodeFirstChild(self); const res = try parser.nodeFirstChild(self);
if (res == null) { if (res == null) {
return null; return null;
} }
@@ -132,7 +132,7 @@ pub const Node = struct {
} }
pub fn get_lastChild(self: *parser.Node) !?Union { pub fn get_lastChild(self: *parser.Node) !?Union {
const res = parser.nodeLastChild(self); const res = try parser.nodeLastChild(self);
if (res == null) { if (res == null) {
return null; return null;
} }
@@ -140,7 +140,7 @@ pub const Node = struct {
} }
pub fn get_nextSibling(self: *parser.Node) !?Union { pub fn get_nextSibling(self: *parser.Node) !?Union {
const res = parser.nodeNextSibling(self); const res = try parser.nodeNextSibling(self);
if (res == null) { if (res == null) {
return null; return null;
} }
@@ -148,7 +148,7 @@ pub const Node = struct {
} }
pub fn get_previousSibling(self: *parser.Node) !?Union { pub fn get_previousSibling(self: *parser.Node) !?Union {
const res = parser.nodePreviousSibling(self); const res = try parser.nodePreviousSibling(self);
if (res == null) { if (res == null) {
return null; return null;
} }
@@ -156,7 +156,7 @@ pub const Node = struct {
} }
pub fn get_parentNode(self: *parser.Node) !?Union { pub fn get_parentNode(self: *parser.Node) !?Union {
const res = parser.nodeParentNode(self); const res = try parser.nodeParentNode(self);
if (res == null) { if (res == null) {
return null; return null;
} }
@@ -164,7 +164,7 @@ pub const Node = struct {
} }
pub fn get_parentElement(self: *parser.Node) !?ElementUnion { pub fn get_parentElement(self: *parser.Node) !?ElementUnion {
const res = parser.nodeParentElement(self); const res = try parser.nodeParentElement(self);
if (res == null) { if (res == null) {
return null; return null;
} }
@@ -176,11 +176,11 @@ pub const Node = struct {
} }
pub fn get_nodeType(self: *parser.Node) !u8 { pub fn get_nodeType(self: *parser.Node) !u8 {
return @intFromEnum(parser.nodeType(self)); return @intFromEnum(try parser.nodeType(self));
} }
pub fn get_ownerDocument(self: *parser.Node) !?*parser.DocumentHTML { pub fn get_ownerDocument(self: *parser.Node) !?*parser.DocumentHTML {
const res = parser.nodeOwnerDocument(self); const res = try parser.nodeOwnerDocument(self);
if (res == null) { if (res == null) {
return null; return null;
} }
@@ -190,12 +190,12 @@ pub const Node = struct {
pub fn get_isConnected(self: *parser.Node) !bool { pub fn get_isConnected(self: *parser.Node) !bool {
var node = self; var node = self;
while (true) { while (true) {
const node_type = parser.nodeType(node); const node_type = try parser.nodeType(node);
if (node_type == .document) { if (node_type == .document) {
return true; return true;
} }
if (parser.nodeParentNode(node)) |parent| { if (try parser.nodeParentNode(node)) |parent| {
// didn't find a document, but node has a parent, let's see // didn't find a document, but node has a parent, let's see
// if it's connected; // if it's connected;
node = parent; node = parent;
@@ -222,15 +222,15 @@ pub const Node = struct {
// Read/Write attributes // Read/Write attributes
pub fn get_nodeValue(self: *parser.Node) !?[]const u8 { pub fn get_nodeValue(self: *parser.Node) !?[]const u8 {
return parser.nodeValue(self); return try parser.nodeValue(self);
} }
pub fn set_nodeValue(self: *parser.Node, data: []u8) !void { pub fn set_nodeValue(self: *parser.Node, data: []u8) !void {
try parser.nodeSetValue(self, data); try parser.nodeSetValue(self, data);
} }
pub fn get_textContent(self: *parser.Node) ?[]const u8 { pub fn get_textContent(self: *parser.Node) !?[]const u8 {
return parser.nodeTextContent(self); return try parser.nodeTextContent(self);
} }
pub fn set_textContent(self: *parser.Node, data: []u8) !void { pub fn set_textContent(self: *parser.Node, data: []u8) !void {
@@ -240,8 +240,8 @@ pub const Node = struct {
// Methods // Methods
pub fn _appendChild(self: *parser.Node, child: *parser.Node) !Union { pub fn _appendChild(self: *parser.Node, child: *parser.Node) !Union {
const self_owner = parser.nodeOwnerDocument(self); const self_owner = try parser.nodeOwnerDocument(self);
const child_owner = parser.nodeOwnerDocument(child); const child_owner = try parser.nodeOwnerDocument(child);
// If the node to be inserted has a different ownerDocument than the parent 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 // modern browsers automatically adopt the node and its descendants into
@@ -272,14 +272,14 @@ pub const Node = struct {
return 0; return 0;
} }
const docself = parser.nodeOwnerDocument(self) orelse blk: { const docself = try parser.nodeOwnerDocument(self) orelse blk: {
if (parser.nodeType(self) == .document) { if (try parser.nodeType(self) == .document) {
break :blk @as(*parser.Document, @ptrCast(self)); break :blk @as(*parser.Document, @ptrCast(self));
} }
break :blk null; break :blk null;
}; };
const docother = parser.nodeOwnerDocument(other) orelse blk: { const docother = try parser.nodeOwnerDocument(other) orelse blk: {
if (parser.nodeType(other) == .document) { if (try parser.nodeType(other) == .document) {
break :blk @as(*parser.Document, @ptrCast(other)); break :blk @as(*parser.Document, @ptrCast(other));
} }
break :blk null; break :blk null;
@@ -299,8 +299,8 @@ pub const Node = struct {
@intFromEnum(parser.DocumentPosition.contained_by); @intFromEnum(parser.DocumentPosition.contained_by);
} }
const rootself = parser.nodeGetRootNode(self); const rootself = try parser.nodeGetRootNode(self);
const rootother = parser.nodeGetRootNode(other); const rootother = try parser.nodeGetRootNode(other);
if (rootself != rootother) { if (rootself != rootother) {
return @intFromEnum(parser.DocumentPosition.disconnected) + return @intFromEnum(parser.DocumentPosition.disconnected) +
@intFromEnum(parser.DocumentPosition.implementation_specific) + @intFromEnum(parser.DocumentPosition.implementation_specific) +
@@ -347,8 +347,8 @@ pub const Node = struct {
return 0; return 0;
} }
pub fn _contains(self: *parser.Node, other: *parser.Node) bool { pub fn _contains(self: *parser.Node, other: *parser.Node) !bool {
return parser.nodeContains(self, other); return try parser.nodeContains(self, other);
} }
// Returns itself or ancestor object inheriting from Node. // Returns itself or ancestor object inheriting from Node.
@@ -360,60 +360,32 @@ pub const Node = struct {
node: Union, node: Union,
}; };
pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }, page: *Page) !GetRootNodeResult { pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }, page: *Page) !GetRootNodeResult {
const composed = if (options) |opts| opts.composed else false; if (options) |options_| if (options_.composed) {
log.warn(.web_api, "not implemented", .{ .feature = "getRootNode composed" });
};
var current_root = parser.nodeGetRootNode(self); const root = try parser.nodeGetRootNode(self);
if (page.getNodeState(root)) |state| {
while (true) { if (state.shadow_root) |sr| {
const node_type = parser.nodeType(current_root); return .{ .shadow_root = sr };
if (node_type == .document_fragment) {
if (parser.documentFragmentGetHost(@ptrCast(current_root))) |host| {
if (page.getNodeState(host)) |state| {
if (state.shadow_root) |sr| {
if (!composed) {
return .{ .shadow_root = sr };
}
current_root = parser.nodeGetRootNode(@ptrCast(sr.host));
continue;
}
}
}
} }
break;
} }
return .{ .node = try Node.toInterface(current_root) }; return .{ .node = try Node.toInterface(root) };
} }
pub fn _hasChildNodes(self: *parser.Node) bool { pub fn _hasChildNodes(self: *parser.Node) !bool {
return parser.nodeHasChildNodes(self); return try parser.nodeHasChildNodes(self);
}
fn is_template(self: *parser.Node) !bool {
if (parser.nodeType(self) != .element) {
return false;
}
const e = parser.nodeToElement(self);
return try parser.elementTag(e) == .template;
} }
pub fn get_childNodes(self: *parser.Node, page: *Page) !NodeList { pub fn get_childNodes(self: *parser.Node, page: *Page) !NodeList {
// special case for template:
// > The Node.childNodes property of the <template> element is always empty
// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/template#usage_notes
if (try is_template(self)) {
return .{};
}
const allocator = page.arena; const allocator = page.arena;
var list: NodeList = .{}; var list: NodeList = .{};
var n = parser.nodeFirstChild(self) orelse return list; var n = try parser.nodeFirstChild(self) orelse return list;
while (true) { while (true) {
try list.append(allocator, n); try list.append(allocator, n);
n = parser.nodeNextSibling(n) orelse return list; n = try parser.nodeNextSibling(n) orelse return list;
} }
} }
@@ -422,8 +394,8 @@ pub const Node = struct {
return _appendChild(self, new_node); return _appendChild(self, new_node);
} }
const self_owner = parser.nodeOwnerDocument(self); const self_owner = try parser.nodeOwnerDocument(self);
const new_node_owner = parser.nodeOwnerDocument(new_node); const new_node_owner = try parser.nodeOwnerDocument(new_node);
// If the node to be inserted has a different ownerDocument than the parent 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 // modern browsers automatically adopt the node and its descendants into
@@ -443,7 +415,7 @@ pub const Node = struct {
} }
pub fn _isDefaultNamespace(self: *parser.Node, namespace: ?[]const u8) !bool { pub fn _isDefaultNamespace(self: *parser.Node, namespace: ?[]const u8) !bool {
return parser.nodeIsDefaultNamespace(self, namespace); return try parser.nodeIsDefaultNamespace(self, namespace);
} }
pub fn _isEqualNode(self: *parser.Node, other: *parser.Node) !bool { pub fn _isEqualNode(self: *parser.Node, other: *parser.Node) !bool {
@@ -451,10 +423,10 @@ pub const Node = struct {
return try parser.nodeIsEqualNode(self, other); return try parser.nodeIsEqualNode(self, other);
} }
pub fn _isSameNode(self: *parser.Node, other: *parser.Node) bool { pub fn _isSameNode(self: *parser.Node, other: *parser.Node) !bool {
// TODO: other is not an optional parameter, but can be null. // TODO: other is not an optional parameter, but can be null.
// NOTE: there is no need to use isSameNode(); instead use the === strict equality operator // NOTE: there is no need to use isSameNode(); instead use the === strict equality operator
return parser.nodeIsSameNode(self, other); return try parser.nodeIsSameNode(self, other);
} }
pub fn _lookupPrefix(self: *parser.Node, namespace: ?[]const u8) !?[]const u8 { pub fn _lookupPrefix(self: *parser.Node, namespace: ?[]const u8) !?[]const u8 {
@@ -489,7 +461,7 @@ pub const Node = struct {
// Check if the hierarchy node tree constraints are respected. // Check if the hierarchy node tree constraints are respected.
// For now, it checks only if new nodes are not self. // For now, it checks only if new nodes are not self.
// TODO implements the others constraints. // TODO implements the others contraints.
// see https://dom.spec.whatwg.org/#concept-node-tree // see https://dom.spec.whatwg.org/#concept-node-tree
pub fn hierarchy(self: *parser.Node, nodes: []const NodeOrText) bool { pub fn hierarchy(self: *parser.Node, nodes: []const NodeOrText) bool {
for (nodes) |n| { for (nodes) |n| {
@@ -510,9 +482,9 @@ pub const Node = struct {
return parser.DOMError.HierarchyRequest; return parser.DOMError.HierarchyRequest;
} }
const doc = (parser.nodeOwnerDocument(self)) orelse return; const doc = (try parser.nodeOwnerDocument(self)) orelse return;
if (parser.nodeFirstChild(self)) |first| { if (try parser.nodeFirstChild(self)) |first| {
for (nodes) |node| { for (nodes) |node| {
_ = try parser.nodeInsertBefore(self, try node.toNode(doc), first); _ = try parser.nodeInsertBefore(self, try node.toNode(doc), first);
} }
@@ -534,7 +506,7 @@ pub const Node = struct {
return parser.DOMError.HierarchyRequest; return parser.DOMError.HierarchyRequest;
} }
const doc = (parser.nodeOwnerDocument(self)) orelse return; const doc = (try parser.nodeOwnerDocument(self)) orelse return;
for (nodes) |node| { for (nodes) |node| {
_ = try parser.nodeAppendChild(self, try node.toNode(doc)); _ = try parser.nodeAppendChild(self, try node.toNode(doc));
} }
@@ -553,7 +525,7 @@ pub const Node = struct {
// remove existing children // remove existing children
try removeChildren(self); try removeChildren(self);
const doc = (parser.nodeOwnerDocument(self)) orelse return; const doc = (try parser.nodeOwnerDocument(self)) orelse return;
// add new children // add new children
for (nodes) |node| { for (nodes) |node| {
_ = try parser.nodeAppendChild(self, try node.toNode(doc)); _ = try parser.nodeAppendChild(self, try node.toNode(doc));
@@ -561,30 +533,30 @@ pub const Node = struct {
} }
pub fn removeChildren(self: *parser.Node) !void { pub fn removeChildren(self: *parser.Node) !void {
if (!parser.nodeHasChildNodes(self)) return; if (!try parser.nodeHasChildNodes(self)) return;
const children = try parser.nodeGetChildNodes(self); const children = try parser.nodeGetChildNodes(self);
const ln = parser.nodeListLength(children); const ln = try parser.nodeListLength(children);
var i: u32 = 0; var i: u32 = 0;
while (i < ln) { while (i < ln) {
defer i += 1; defer i += 1;
// we always retrieve the 0 index child on purpose: libdom nodelist // we always retrieve the 0 index child on purpose: libdom nodelist
// are dynamic. So the next child to remove is always as pos 0. // are dynamic. So the next child to remove is always as pos 0.
const child = parser.nodeListItem(children, 0) orelse continue; const child = try parser.nodeListItem(children, 0) orelse continue;
_ = try parser.nodeRemoveChild(self, child); _ = try parser.nodeRemoveChild(self, child);
} }
} }
pub fn before(self: *parser.Node, nodes: []const NodeOrText) !void { pub fn before(self: *parser.Node, nodes: []const NodeOrText) !void {
const parent = parser.nodeParentNode(self) orelse return; const parent = try parser.nodeParentNode(self) orelse return;
const doc = (parser.nodeOwnerDocument(parent)) orelse return; const doc = (try parser.nodeOwnerDocument(parent)) orelse return;
var sibling: ?*parser.Node = self; var sibling: ?*parser.Node = self;
// have to find the first sibling that isn't in nodes // have to find the first sibling that isn't in nodes
CHECK: while (sibling) |s| { CHECK: while (sibling) |s| {
for (nodes) |n| { for (nodes) |n| {
if (n.is(s)) { if (n.is(s)) {
sibling = parser.nodePreviousSibling(s); sibling = try parser.nodePreviousSibling(s);
continue :CHECK; continue :CHECK;
} }
} }
@@ -592,7 +564,7 @@ pub const Node = struct {
} }
if (sibling == null) { if (sibling == null) {
sibling = parser.nodeFirstChild(parent); sibling = try parser.nodeFirstChild(parent);
} }
if (sibling) |ref_node| { if (sibling) |ref_node| {
@@ -606,15 +578,15 @@ pub const Node = struct {
} }
pub fn after(self: *parser.Node, nodes: []const NodeOrText) !void { pub fn after(self: *parser.Node, nodes: []const NodeOrText) !void {
const parent = parser.nodeParentNode(self) orelse return; const parent = try parser.nodeParentNode(self) orelse return;
const doc = (parser.nodeOwnerDocument(parent)) orelse return; const doc = (try parser.nodeOwnerDocument(parent)) orelse return;
// have to find the first sibling that isn't in nodes // have to find the first sibling that isn't in nodes
var sibling = parser.nodeNextSibling(self); var sibling = try parser.nodeNextSibling(self);
CHECK: while (sibling) |s| { CHECK: while (sibling) |s| {
for (nodes) |n| { for (nodes) |n| {
if (n.is(s)) { if (n.is(s)) {
sibling = parser.nodeNextSibling(s); sibling = try parser.nodeNextSibling(s);
continue :CHECK; continue :CHECK;
} }
} }

View File

@@ -17,8 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const Node = @import("node.zig").Node; const Node = @import("node.zig").Node;
pub const NodeFilter = struct { pub const NodeFilter = struct {
@@ -43,13 +43,10 @@ pub const NodeFilter = struct {
const VerifyResult = enum { accept, skip, reject }; const VerifyResult = enum { accept, skip, reject };
pub fn verify(what_to_show: u32, filter: ?js.Function, node: *parser.Node) !VerifyResult { pub fn verify(what_to_show: u32, filter: ?Env.Function, node: *parser.Node) !VerifyResult {
const node_type = parser.nodeType(node); const node_type = try parser.nodeType(node);
// Verify that we can show this node type. // Verify that we can show this node type.
// Per the DOM spec, what_to_show filters which nodes to return, but should
// still traverse children. So we return .skip (not .reject) when the node
// type doesn't match.
if (!switch (node_type) { if (!switch (node_type) {
.attribute => what_to_show & NodeFilter._SHOW_ATTRIBUTE != 0, .attribute => what_to_show & NodeFilter._SHOW_ATTRIBUTE != 0,
.cdata_section => what_to_show & NodeFilter._SHOW_CDATA_SECTION != 0, .cdata_section => what_to_show & NodeFilter._SHOW_CDATA_SECTION != 0,
@@ -63,7 +60,7 @@ pub fn verify(what_to_show: u32, filter: ?js.Function, node: *parser.Node) !Veri
.notation => what_to_show & NodeFilter._SHOW_NOTATION != 0, .notation => what_to_show & NodeFilter._SHOW_NOTATION != 0,
.processing_instruction => what_to_show & NodeFilter._SHOW_PROCESSING_INSTRUCTION != 0, .processing_instruction => what_to_show & NodeFilter._SHOW_PROCESSING_INSTRUCTION != 0,
.text => what_to_show & NodeFilter._SHOW_TEXT != 0, .text => what_to_show & NodeFilter._SHOW_TEXT != 0,
}) return .skip; }) return .reject;
// Verify that we aren't filtering it out. // Verify that we aren't filtering it out.
if (filter) |f| { if (filter) |f| {

View File

@@ -18,8 +18,8 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const NodeFilter = @import("node_filter.zig"); const NodeFilter = @import("node_filter.zig");
const Node = @import("node.zig").Node; const Node = @import("node.zig").Node;
const NodeUnion = @import("node.zig").Union; const NodeUnion = @import("node.zig").Union;
@@ -37,7 +37,7 @@ pub const NodeIterator = struct {
reference_node: *parser.Node, reference_node: *parser.Node,
what_to_show: u32, what_to_show: u32,
filter: ?NodeIteratorOpts, filter: ?NodeIteratorOpts,
filter_func: ?js.Function, filter_func: ?Env.Function,
pointer_before_current: bool = true, pointer_before_current: bool = true,
// used to track / block recursive filters // used to track / block recursive filters
is_in_callback: bool = false, is_in_callback: bool = false,
@@ -45,15 +45,15 @@ pub const NodeIterator = struct {
// One of the few cases where null and undefined resolve to different default. // 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: // We need the raw JsObject so that we can probe the tri state:
// null, undefined or i32. // null, undefined or i32.
pub const WhatToShow = js.Object; pub const WhatToShow = Env.JsObject;
pub const NodeIteratorOpts = union(enum) { pub const NodeIteratorOpts = union(enum) {
function: js.Function, function: Env.Function,
object: struct { acceptNode: js.Function }, object: struct { acceptNode: Env.Function },
}; };
pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?NodeIteratorOpts) !NodeIterator { pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?NodeIteratorOpts) !NodeIterator {
var filter_func: ?js.Function = null; var filter_func: ?Env.Function = null;
if (filter) |f| { if (filter) |f| {
filter_func = switch (f) { filter_func = switch (f) {
.function => |func| func, .function => |func| func,
@@ -74,10 +74,10 @@ pub const NodeIterator = struct {
return .{ return .{
.root = node, .root = node,
.filter = filter,
.reference_node = node, .reference_node = node,
.filter_func = filter_func,
.what_to_show = what_to_show, .what_to_show = what_to_show,
.filter = filter,
.filter_func = filter_func,
}; };
} }
@@ -115,30 +115,17 @@ pub const NodeIterator = struct {
if (try self.firstChild(self.reference_node)) |child| { if (try self.firstChild(self.reference_node)) |child| {
self.reference_node = child; self.reference_node = child;
self.pointer_before_current = false;
return try Node.toInterface(child); return try Node.toInterface(child);
} }
var current = self.reference_node; var current = self.reference_node;
while (current != self.root) { while (current != self.root) {
// Try to get next sibling (including .skip/.reject nodes we need to descend into) if (try self.nextSibling(current)) |sibling| {
if (try self.nextSiblingOrSkipReject(current)) |result| { self.reference_node = sibling;
if (result.should_descend) { return try Node.toInterface(sibling);
// This is a .skip/.reject node - try to find acceptable children within it
if (try self.firstChild(result.node)) |child| {
self.reference_node = child;
return try Node.toInterface(child);
}
// No acceptable children, continue looking at this node's siblings
current = result.node;
continue;
}
// This is an .accept node - return it
self.reference_node = result.node;
return try Node.toInterface(result.node);
} }
current = (parser.nodeParentNode(current)) orelse break; current = (try parser.nodeParentNode(current)) orelse break;
} }
return null; return null;
@@ -160,7 +147,7 @@ pub const NodeIterator = struct {
} }
var current = self.reference_node; var current = self.reference_node;
while (parser.nodePreviousSibling(current)) |previous| { while (try parser.nodePreviousSibling(current)) |previous| {
current = previous; current = previous;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) { switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
@@ -202,11 +189,11 @@ pub const NodeIterator = struct {
fn firstChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node { fn firstChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
const children = try parser.nodeGetChildNodes(node); const children = try parser.nodeGetChildNodes(node);
const child_count = parser.nodeListLength(children); const child_count = try parser.nodeListLength(children);
for (0..child_count) |i| { for (0..child_count) |i| {
const index: u32 = @intCast(i); const index: u32 = @intCast(i);
const child = (parser.nodeListItem(children, index)) orelse return null; const child = (try parser.nodeListItem(children, index)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) { 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 .accept => return child, // NOTE: Skip and reject are equivalent for NodeIterator, this is different from TreeWalker
@@ -219,12 +206,12 @@ pub const NodeIterator = struct {
fn lastChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node { fn lastChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
const children = try parser.nodeGetChildNodes(node); const children = try parser.nodeGetChildNodes(node);
const child_count = parser.nodeListLength(children); const child_count = try parser.nodeListLength(children);
var index: u32 = child_count; var index: u32 = child_count;
while (index > 0) { while (index > 0) {
index -= 1; index -= 1;
const child = (parser.nodeListItem(children, index)) orelse return null; const child = (try parser.nodeListItem(children, index)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) { 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 .accept => return child, // NOTE: Skip and reject are equivalent for NodeIterator, this is different from TreeWalker
@@ -242,7 +229,7 @@ pub const NodeIterator = struct {
var current = node; var current = node;
while (true) { while (true) {
if (current == self.root) return null; if (current == self.root) return null;
current = (parser.nodeParentNode(current)) orelse return null; current = (try parser.nodeParentNode(current)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) { switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => return current, .accept => return current,
@@ -256,7 +243,7 @@ pub const NodeIterator = struct {
var current = node; var current = node;
while (true) { while (true) {
current = (parser.nodeNextSibling(current)) orelse return null; current = (try parser.nodeNextSibling(current)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) { switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => return current, .accept => return current,
@@ -267,22 +254,6 @@ pub const NodeIterator = struct {
return null; return null;
} }
// Get the next sibling that is either acceptable or should be descended into (skip/reject)
fn nextSiblingOrSkipReject(self: *const NodeIterator, node: *parser.Node) !?struct { node: *parser.Node, should_descend: bool } {
var current = node;
while (true) {
current = (parser.nodeNextSibling(current)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => return .{ .node = current, .should_descend = false },
.skip, .reject => return .{ .node = current, .should_descend = true },
}
}
return null;
}
fn callbackStart(self: *NodeIterator) !void { fn callbackStart(self: *NodeIterator) !void {
if (self.is_in_callback) { if (self.is_in_callback) {
// this is the correct DOMExeption // this is the correct DOMExeption

View File

@@ -17,12 +17,13 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator;
const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const JsThis = @import("../env.zig").JsThis;
const Function = @import("../env.zig").Function;
const NodeUnion = @import("node.zig").Union; const NodeUnion = @import("node.zig").Union;
const Node = @import("node.zig").Node; const Node = @import("node.zig").Node;
@@ -100,20 +101,13 @@ pub const NodeList = struct {
nodes: NodesArrayList = .{}, nodes: NodesArrayList = .{},
pub fn deinit(self: *NodeList, allocator: Allocator) void { pub fn deinit(self: *NodeList, alloc: std.mem.Allocator) void {
self.nodes.deinit(allocator); // TODO unref all nodes
self.nodes.deinit(alloc);
} }
pub fn ensureTotalCapacity(self: *NodeList, allocator: Allocator, n: usize) !void { pub fn append(self: *NodeList, alloc: std.mem.Allocator, node: *parser.Node) !void {
return self.nodes.ensureTotalCapacity(allocator, n); try self.nodes.append(alloc, node);
}
pub fn append(self: *NodeList, allocator: Allocator, node: *parser.Node) !void {
try self.nodes.append(allocator, node);
}
pub fn appendAssumeCapacity(self: *NodeList, node: *parser.Node) void {
self.nodes.appendAssumeCapacity(node);
} }
pub fn get_length(self: *const NodeList) u32 { pub fn get_length(self: *const NodeList) u32 {
@@ -146,10 +140,10 @@ pub const NodeList = struct {
// }; // };
// } // }
pub fn _forEach(self: *NodeList, cbk: js.Function) !void { // TODO handle thisArg pub fn _forEach(self: *NodeList, cbk: Function) !void { // TODO handle thisArg
for (self.nodes.items, 0..) |n, i| { for (self.nodes.items, 0..) |n, i| {
const ii: u32 = @intCast(i); const ii: u32 = @intCast(i);
var result: js.Function.Result = undefined; var result: Function.Result = undefined;
cbk.tryCall(void, .{ n, ii, self }, &result) catch { cbk.tryCall(void, .{ n, ii, self }, &result) catch {
log.debug(.user_script, "forEach callback", .{ .err = result.exception, .stack = result.stack }); log.debug(.user_script, "forEach callback", .{ .err = result.exception, .stack = result.stack });
}; };
@@ -173,7 +167,7 @@ pub const NodeList = struct {
} }
// TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries // TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries
pub fn postAttach(self: *NodeList, js_this: js.This) !void { pub fn postAttach(self: *NodeList, js_this: JsThis) !void {
const len = self.get_length(); const len = self.get_length();
for (0..len) |i| { for (0..len) |i| {
const node = try self._item(@intCast(i)) orelse unreachable; const node = try self._item(@intCast(i)) orelse unreachable;

View File

@@ -18,9 +18,9 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const EventTarget = @import("../dom/event_target.zig").EventTarget; const EventTarget = @import("../dom/event_target.zig").EventTarget;
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const milliTimestamp = @import("../../datetime.zig").milliTimestamp; const milliTimestamp = @import("../../datetime.zig").milliTimestamp;
@@ -61,7 +61,7 @@ pub const Performance = struct {
return milliTimestamp() - self.time_origin; return milliTimestamp() - self.time_origin;
} }
pub fn _mark(_: *Performance, name: js.String, _options: ?PerformanceMark.Options, page: *Page) !PerformanceMark { pub fn _mark(_: *Performance, name: []const u8, _options: ?PerformanceMark.Options, page: *Page) !PerformanceMark {
const mark: PerformanceMark = try .constructor(name, _options, page); const mark: PerformanceMark = try .constructor(name, _options, page);
// TODO: Should store this in an entries list // TODO: Should store this in an entries list
return mark; return mark;
@@ -148,14 +148,14 @@ pub const PerformanceMark = struct {
pub const prototype = *PerformanceEntry; pub const prototype = *PerformanceEntry;
proto: PerformanceEntry, proto: PerformanceEntry,
detail: ?js.Object, detail: ?Env.JsObject,
const Options = struct { const Options = struct {
detail: ?js.Object = null, detail: ?Env.JsObject = null,
startTime: ?f64 = null, startTime: ?f64 = null,
}; };
pub fn constructor(name: js.String, _options: ?Options, page: *Page) !PerformanceMark { pub fn constructor(name: []const u8, _options: ?Options, page: *Page) !PerformanceMark {
const perf = &page.window.performance; const perf = &page.window.performance;
const options = _options orelse Options{}; const options = _options orelse Options{};
@@ -166,12 +166,14 @@ pub const PerformanceMark = struct {
} }
const detail = if (options.detail) |d| try d.persist() else null; const detail = if (options.detail) |d| try d.persist() else null;
const proto = PerformanceEntry{ .name = name.string, .entry_type = .mark, .start_time = start_time };
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 }; return .{ .proto = proto, .detail = detail };
} }
pub fn get_detail(self: *const PerformanceMark) ?js.Object { pub fn get_detail(self: *const PerformanceMark) ?Env.JsObject {
return self.detail; return self.detail;
} }
}; };
@@ -195,7 +197,7 @@ test "Performance: now" {
} }
var after = perf._now(); var after = perf._now();
while (after <= now) { // Loop until after > now while (after <= now) { // Loop untill after > now
try testing.expectEqual(after, now); try testing.expectEqual(after, now);
after = perf._now(); after = perf._now();
} }

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig"); const Env = @import("../env.zig").Env;
const PerformanceEntry = @import("performance.zig").PerformanceEntry; const PerformanceEntry = @import("performance.zig").PerformanceEntry;
@@ -25,7 +25,7 @@ const PerformanceEntry = @import("performance.zig").PerformanceEntry;
pub const PerformanceObserver = struct { pub const PerformanceObserver = struct {
pub const _supportedEntryTypes = [0][]const u8{}; pub const _supportedEntryTypes = [0][]const u8{};
pub fn constructor(cbk: js.Function) PerformanceObserver { pub fn constructor(cbk: Env.Function) PerformanceObserver {
_ = cbk; _ = cbk;
return .{}; return .{};
} }

View File

@@ -48,7 +48,7 @@ pub const ProcessingInstruction = struct {
} }
pub fn get_data(self: *parser.ProcessingInstruction) !?[]const u8 { pub fn get_data(self: *parser.ProcessingInstruction) !?[]const u8 {
return parser.nodeValue(parser.processingInstructionToNode(self)); return try parser.nodeValue(parser.processingInstructionToNode(self));
} }
pub fn set_data(self: *parser.ProcessingInstruction, data: []u8) !void { pub fn set_data(self: *parser.ProcessingInstruction, data: []u8) !void {
@@ -58,7 +58,7 @@ pub const ProcessingInstruction = struct {
// netsurf's ProcessInstruction doesn't implement the dom_node_get_attributes // netsurf's ProcessInstruction doesn't implement the dom_node_get_attributes
// and thus will crash if we try to call nodeIsEqualNode. // and thus will crash if we try to call nodeIsEqualNode.
pub fn _isEqualNode(self: *parser.ProcessingInstruction, other_node: *parser.Node) !bool { pub fn _isEqualNode(self: *parser.ProcessingInstruction, other_node: *parser.Node) !bool {
if (parser.nodeType(other_node) != .processing_instruction) { if (try parser.nodeType(other_node) != .processing_instruction) {
return false; return false;
} }

View File

@@ -92,7 +92,7 @@ pub const Range = struct {
pub fn _setStart(self: *Range, node: *parser.Node, offset_: i32) !void { pub fn _setStart(self: *Range, node: *parser.Node, offset_: i32) !void {
try ensureValidOffset(node, offset_); try ensureValidOffset(node, offset_);
const offset: u32 = @intCast(offset_); const offset: u32 = @intCast(offset_);
const position = compare(node, offset, self.proto.end_node, self.proto.end_offset) catch |err| switch (err) { const position = compare(node, offset, self.proto.start_node, self.proto.start_offset) catch |err| switch (err) {
error.WrongDocument => blk: { error.WrongDocument => blk: {
// allow a node with a different root than the current, or // allow a node with a different root than the current, or
// a disconnected one. Treat it as if it's "after", so that // a disconnected one. Treat it as if it's "after", so that
@@ -103,7 +103,7 @@ pub const Range = struct {
}; };
if (position == 1) { if (position == 1) {
// if we're setting the node after the current end, the end must // if we're setting the node after the current start, the end must
// be set too. // be set too.
self.proto.end_offset = offset; self.proto.end_offset = offset;
self.proto.end_node = node; self.proto.end_node = node;
@@ -176,10 +176,10 @@ pub const Range = struct {
self.proto.end_node = node; self.proto.end_node = node;
// Set end_offset // Set end_offset
switch (parser.nodeType(node)) { switch (try parser.nodeType(node)) {
.text, .cdata_section, .comment, .processing_instruction => { .text, .cdata_section, .comment, .processing_instruction => {
// For text-like nodes, end_offset should be the length of the text data // For text-like nodes, end_offset should be the length of the text data
if (parser.nodeValue(node)) |text_data| { if (try parser.nodeValue(node)) |text_data| {
self.proto.end_offset = @intCast(text_data.len); self.proto.end_offset = @intCast(text_data.len);
} else { } else {
self.proto.end_offset = 0; self.proto.end_offset = 0;
@@ -188,7 +188,7 @@ pub const Range = struct {
else => { else => {
// For element and other nodes, end_offset is the number of children // For element and other nodes, end_offset is the number of children
const child_nodes = try parser.nodeGetChildNodes(node); const child_nodes = try parser.nodeGetChildNodes(node);
const child_count = parser.nodeListLength(child_nodes); const child_count = try parser.nodeListLength(child_nodes);
self.proto.end_offset = @intCast(child_count); self.proto.end_offset = @intCast(child_count);
}, },
} }
@@ -211,7 +211,7 @@ pub const Range = struct {
pub fn _comparePoint(self: *const Range, node: *parser.Node, offset_: i32) !i32 { pub fn _comparePoint(self: *const Range, node: *parser.Node, offset_: i32) !i32 {
const start = self.proto.start_node; const start = self.proto.start_node;
if (parser.nodeGetRootNode(start) != parser.nodeGetRootNode(node)) { if (try parser.nodeGetRootNode(start) != try parser.nodeGetRootNode(node)) {
// WPT really wants this error to be first. Later, when we check // WPT really wants this error to be first. Later, when we check
// if the relative position is 'disconnected', it'll also catch this // if the relative position is 'disconnected', it'll also catch this
// case, but WPT will complain because it sometimes also sends // case, but WPT will complain because it sometimes also sends
@@ -219,7 +219,7 @@ pub const Range = struct {
return error.WrongDocument; return error.WrongDocument;
} }
if (parser.nodeType(node) == .document_type) { if (try parser.nodeType(node) == .document_type) {
return error.InvalidNodeType; return error.InvalidNodeType;
} }
@@ -245,8 +245,8 @@ pub const Range = struct {
} }
pub fn _intersectsNode(self: *const Range, node: *parser.Node) !bool { pub fn _intersectsNode(self: *const Range, node: *parser.Node) !bool {
const start_root = parser.nodeGetRootNode(self.proto.start_node); const start_root = try parser.nodeGetRootNode(self.proto.start_node);
const node_root = parser.nodeGetRootNode(node); const node_root = try parser.nodeGetRootNode(node);
if (start_root != node_root) { if (start_root != node_root) {
return false; return false;
} }
@@ -299,29 +299,29 @@ fn ensureValidOffset(node: *parser.Node, offset: i32) !void {
fn nodeLength(node: *parser.Node) !usize { fn nodeLength(node: *parser.Node) !usize {
switch (try isTextual(node)) { switch (try isTextual(node)) {
true => return ((parser.nodeTextContent(node)) orelse "").len, true => return ((try parser.nodeTextContent(node)) orelse "").len,
false => { false => {
const children = try parser.nodeGetChildNodes(node); const children = try parser.nodeGetChildNodes(node);
return @intCast(parser.nodeListLength(children)); return @intCast(try parser.nodeListLength(children));
}, },
} }
} }
fn isTextual(node: *parser.Node) !bool { fn isTextual(node: *parser.Node) !bool {
return switch (parser.nodeType(node)) { return switch (try parser.nodeType(node)) {
.text, .comment, .cdata_section => true, .text, .comment, .cdata_section => true,
else => false, else => false,
}; };
} }
fn getParentAndIndex(child: *parser.Node) !struct { *parser.Node, u32 } { fn getParentAndIndex(child: *parser.Node) !struct { *parser.Node, u32 } {
const parent = (parser.nodeParentNode(child)) orelse return error.InvalidNodeType; const parent = (try parser.nodeParentNode(child)) orelse return error.InvalidNodeType;
const children = try parser.nodeGetChildNodes(parent); const children = try parser.nodeGetChildNodes(parent);
const ln = parser.nodeListLength(children); const ln = try parser.nodeListLength(children);
var i: u32 = 0; var i: u32 = 0;
while (i < ln) { while (i < ln) {
defer i += 1; defer i += 1;
const c = parser.nodeListItem(children, i) orelse continue; const c = try parser.nodeListItem(children, i) orelse continue;
if (c == child) { if (c == child) {
return .{ parent, i }; return .{ parent, i };
} }
@@ -363,7 +363,7 @@ fn compare(node_a: *parser.Node, offset_a: u32, node_b: *parser.Node, offset_b:
if (position & @intFromEnum(parser.DocumentPosition.contains) == @intFromEnum(parser.DocumentPosition.contains)) { if (position & @intFromEnum(parser.DocumentPosition.contains) == @intFromEnum(parser.DocumentPosition.contains)) {
// node_a contains node_b // node_a contains node_b
var child = node_b; var child = node_b;
while (parser.nodeParentNode(child)) |parent| { while (try parser.nodeParentNode(child)) |parent| {
if (parent == node_a) { if (parent == node_a) {
// child.parentNode == node_a // child.parentNode == node_a
break; break;
@@ -378,7 +378,7 @@ fn compare(node_a: *parser.Node, offset_a: u32, node_b: *parser.Node, offset_b:
const child_parent, const child_index = try getParentAndIndex(child); const child_parent, const child_index = try getParentAndIndex(child);
std.debug.assert(node_a == child_parent); std.debug.assert(node_a == child_parent);
return if (offset_a <= child_index) -1 else 1; return if (child_index < offset_a) -1 else 1;
} }
return -1; return -1;

View File

@@ -16,7 +16,7 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("../js/js.zig"); const Env = @import("../env.zig").Env;
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
pub const Interfaces = .{ pub const Interfaces = .{
@@ -25,7 +25,7 @@ pub const Interfaces = .{
// WEB IDL https://drafts.csswg.org/resize-observer/#resize-observer-interface // WEB IDL https://drafts.csswg.org/resize-observer/#resize-observer-interface
pub const ResizeObserver = struct { pub const ResizeObserver = struct {
pub fn constructor(cbk: js.Function) ResizeObserver { pub fn constructor(cbk: Env.Function) ResizeObserver {
_ = cbk; _ = cbk;
return .{}; return .{};
} }

View File

@@ -20,7 +20,7 @@ const std = @import("std");
const dump = @import("../dump.zig"); const dump = @import("../dump.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const js = @import(".././js/js.zig"); const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const Node = @import("node.zig").Node; const Node = @import("node.zig").Node;
const Element = @import("element.zig").Element; const Element = @import("element.zig").Element;
@@ -34,7 +34,7 @@ pub const ShadowRoot = struct {
mode: Mode, mode: Mode,
host: *parser.Element, host: *parser.Element,
proto: *parser.DocumentFragment, proto: *parser.DocumentFragment,
adopted_style_sheets: ?js.Object = null, adopted_style_sheets: ?Env.JsObject = null,
pub const Mode = enum { pub const Mode = enum {
open, open,
@@ -45,17 +45,17 @@ pub const ShadowRoot = struct {
return Element.toInterface(self.host); return Element.toInterface(self.host);
} }
pub fn get_adoptedStyleSheets(self: *ShadowRoot, page: *Page) !js.Object { pub fn get_adoptedStyleSheets(self: *ShadowRoot, page: *Page) !Env.JsObject {
if (self.adopted_style_sheets) |obj| { if (self.adopted_style_sheets) |obj| {
return obj; return obj;
} }
const obj = try page.js.createArray(0).persist(); const obj = try page.main_context.newArray(0).persist();
self.adopted_style_sheets = obj; self.adopted_style_sheets = obj;
return obj; return obj;
} }
pub fn set_adoptedStyleSheets(self: *ShadowRoot, sheets: js.Object) !void { pub fn set_adoptedStyleSheets(self: *ShadowRoot, sheets: Env.JsObject) !void {
self.adopted_style_sheets = try sheets.persist(); self.adopted_style_sheets = try sheets.persist();
} }
@@ -67,7 +67,7 @@ pub const ShadowRoot = struct {
pub fn set_innerHTML(self: *ShadowRoot, str_: ?[]const u8) !void { pub fn set_innerHTML(self: *ShadowRoot, str_: ?[]const u8) !void {
const sr_doc = parser.documentFragmentToNode(self.proto); const sr_doc = parser.documentFragmentToNode(self.proto);
const doc = parser.nodeOwnerDocument(sr_doc) orelse return parser.DOMError.WrongDocument; const doc = try parser.nodeOwnerDocument(sr_doc) orelse return parser.DOMError.WrongDocument;
try Node.removeChildren(sr_doc); try Node.removeChildren(sr_doc);
const str = str_ orelse return; const str = str_ orelse return;
@@ -80,16 +80,16 @@ pub const ShadowRoot = struct {
// element. // element.
// For ShadowRoot, it appears the only the children within the body should // For ShadowRoot, it appears the only the children within the body should
// be set. // be set.
const html = parser.nodeFirstChild(fragment_node) orelse return; const html = try parser.nodeFirstChild(fragment_node) orelse return;
const head = parser.nodeFirstChild(html) orelse return; const head = try parser.nodeFirstChild(html) orelse return;
const body = parser.nodeNextSibling(head) orelse return; const body = try parser.nodeNextSibling(head) orelse return;
const children = try parser.nodeGetChildNodes(body); const children = try parser.nodeGetChildNodes(body);
const ln = parser.nodeListLength(children); const ln = try parser.nodeListLength(children);
for (0..ln) |_| { for (0..ln) |_| {
// always index 0, because nodeAppendChild moves the node out of // always index 0, because nodeAppendChild moves the node out of
// the nodeList and into the new tree // the nodeList and into the new tree
const child = parser.nodeListItem(children, 0) orelse continue; const child = try parser.nodeListItem(children, 0) orelse continue;
_ = try parser.nodeAppendChild(sr_doc, child); _ = try parser.nodeAppendChild(sr_doc, child);
} }
} }

View File

@@ -18,11 +18,12 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const iterator = @import("../iterator/iterator.zig"); const iterator = @import("../iterator/iterator.zig");
const Function = @import("../env.zig").Function;
const JsObject = @import("../env.zig").JsObject;
const DOMException = @import("exceptions.zig").DOMException; const DOMException = @import("exceptions.zig").DOMException;
pub const Interfaces = .{ pub const Interfaces = .{
@@ -136,10 +137,10 @@ pub const DOMTokenList = struct {
} }
// TODO handle thisArg // TODO handle thisArg
pub fn _forEach(self: *parser.TokenList, cbk: js.Function, this_arg: js.Object) !void { pub fn _forEach(self: *parser.TokenList, cbk: Function, this_arg: JsObject) !void {
var entries = _entries(self); var entries = _entries(self);
while (try entries._next()) |entry| { while (try entries._next()) |entry| {
var result: js.Function.Result = undefined; var result: Function.Result = undefined;
cbk.tryCallWithThis(void, this_arg, .{ entry.@"1", entry.@"0", self }, &result) catch { cbk.tryCallWithThis(void, this_arg, .{ entry.@"1", entry.@"0", self }, &result) catch {
log.debug(.user_script, "callback error", .{ log.debug(.user_script, "callback error", .{
.err = result.exception, .err = result.exception,

View File

@@ -17,10 +17,11 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const NodeFilter = @import("node_filter.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 Node = @import("node.zig").Node;
const NodeUnion = @import("node.zig").Union; const NodeUnion = @import("node.zig").Union;
@@ -30,20 +31,20 @@ pub const TreeWalker = struct {
current_node: *parser.Node, current_node: *parser.Node,
what_to_show: u32, what_to_show: u32,
filter: ?TreeWalkerOpts, filter: ?TreeWalkerOpts,
filter_func: ?js.Function, filter_func: ?Env.Function,
// One of the few cases where null and undefined resolve to different default. // 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: // We need the raw JsObject so that we can probe the tri state:
// null, undefined or i32. // null, undefined or i32.
pub const WhatToShow = js.Object; pub const WhatToShow = Env.JsObject;
pub const TreeWalkerOpts = union(enum) { pub const TreeWalkerOpts = union(enum) {
function: js.Function, function: Env.Function,
object: struct { acceptNode: js.Function }, object: struct { acceptNode: Env.Function },
}; };
pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?TreeWalkerOpts) !TreeWalker { pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?TreeWalkerOpts) !TreeWalker {
var filter_func: ?js.Function = null; var filter_func: ?Env.Function = null;
if (filter) |f| { if (filter) |f| {
filter_func = switch (f) { filter_func = switch (f) {
@@ -94,11 +95,11 @@ pub const TreeWalker = struct {
fn firstChild(self: *const TreeWalker, node: *parser.Node) !?*parser.Node { fn firstChild(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
const children = try parser.nodeGetChildNodes(node); const children = try parser.nodeGetChildNodes(node);
const child_count = parser.nodeListLength(children); const child_count = try parser.nodeListLength(children);
for (0..child_count) |i| { for (0..child_count) |i| {
const index: u32 = @intCast(i); const index: u32 = @intCast(i);
const child = (parser.nodeListItem(children, index)) orelse return null; const child = (try parser.nodeListItem(children, index)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) { switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
.accept => return child, .accept => return child,
@@ -112,12 +113,12 @@ pub const TreeWalker = struct {
fn lastChild(self: *const TreeWalker, node: *parser.Node) !?*parser.Node { fn lastChild(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
const children = try parser.nodeGetChildNodes(node); const children = try parser.nodeGetChildNodes(node);
const child_count = parser.nodeListLength(children); const child_count = try parser.nodeListLength(children);
var index: u32 = child_count; var index: u32 = child_count;
while (index > 0) { while (index > 0) {
index -= 1; index -= 1;
const child = (parser.nodeListItem(children, index)) orelse return null; const child = (try parser.nodeListItem(children, index)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) { switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
.accept => return child, .accept => return child,
@@ -133,7 +134,7 @@ pub const TreeWalker = struct {
var current = node; var current = node;
while (true) { while (true) {
current = (parser.nodeNextSibling(current)) orelse return null; current = (try parser.nodeNextSibling(current)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) { switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => return current, .accept => return current,
@@ -144,28 +145,11 @@ pub const TreeWalker = struct {
return null; return null;
} }
// Get the next sibling that is either acceptable or should be descended into (skip)
fn nextSiblingOrSkip(self: *const TreeWalker, node: *parser.Node) !?struct { node: *parser.Node, should_descend: bool } {
var current = node;
while (true) {
current = (parser.nodeNextSibling(current)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => return .{ .node = current, .should_descend = false },
.skip => return .{ .node = current, .should_descend = true },
.reject => continue,
}
}
return null;
}
fn previousSibling(self: *const TreeWalker, node: *parser.Node) !?*parser.Node { fn previousSibling(self: *const TreeWalker, node: *parser.Node) !?*parser.Node {
var current = node; var current = node;
while (true) { while (true) {
current = (parser.nodePreviousSibling(current)) orelse return null; current = (try parser.nodePreviousSibling(current)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) { switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => return current, .accept => return current,
@@ -182,7 +166,7 @@ pub const TreeWalker = struct {
var current = node; var current = node;
while (true) { while (true) {
if (current == self.root) return null; if (current == self.root) return null;
current = (parser.nodeParentNode(current)) orelse return null; current = (try parser.nodeParentNode(current)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) { switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => return current, .accept => return current,
@@ -210,36 +194,19 @@ pub const TreeWalker = struct {
} }
pub fn _nextNode(self: *TreeWalker) !?NodeUnion { pub fn _nextNode(self: *TreeWalker) !?NodeUnion {
var current = self.current_node; if (try self.firstChild(self.current_node)) |child| {
// First, try to go to first child of current node
if (try self.firstChild(current)) |child| {
self.current_node = child; self.current_node = child;
return try Node.toInterface(child); return try Node.toInterface(child);
} }
// No acceptable children, move to next node in tree var current = self.current_node;
while (current != self.root) { while (current != self.root) {
const result = try self.nextSiblingOrSkip(current) orelse { if (try self.nextSibling(current)) |sibling| {
// No next sibling, go up to parent and continue self.current_node = sibling;
// or, if there is no parent, we're done return try Node.toInterface(sibling);
current = (parser.nodeParentNode(current)) orelse break;
continue;
};
if (!result.should_descend) {
// This is an .accept node - return it
self.current_node = result.node;
return try Node.toInterface(result.node);
} }
// This is a .skip node - try to find acceptable children within it current = (try parser.nodeParentNode(current)) orelse break;
if (try self.firstChild(result.node)) |child| {
self.current_node = child;
return try Node.toInterface(child);
}
// No acceptable children, continue looking at this node's siblings
current = result.node;
} }
return null; return null;
@@ -267,7 +234,7 @@ pub const TreeWalker = struct {
if (self.current_node == self.root) return null; if (self.current_node == self.root) return null;
var current = self.current_node; var current = self.current_node;
while (parser.nodePreviousSibling(current)) |previous| { while (try parser.nodePreviousSibling(current)) |previous| {
current = previous; current = previous;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) { switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {

View File

@@ -44,39 +44,39 @@ pub const WalkerDepthFirst = struct {
var n = cur orelse root; var n = cur orelse root;
// TODO deinit next // TODO deinit next
if (parser.nodeFirstChild(n)) |next| { if (try parser.nodeFirstChild(n)) |next| {
return next; return next;
} }
// TODO deinit next // TODO deinit next
if (parser.nodeNextSibling(n)) |next| { if (try parser.nodeNextSibling(n)) |next| {
return next; return next;
} }
// TODO deinit parent // TODO deinit parent
// Back to the parent of cur. // Back to the parent of cur.
// If cur has no parent, then the iteration is over. // If cur has no parent, then the iteration is over.
var parent = parser.nodeParentNode(n) orelse return null; var parent = try parser.nodeParentNode(n) orelse return null;
// TODO deinit lastchild // TODO deinit lastchild
var lastchild = parser.nodeLastChild(parent); var lastchild = try parser.nodeLastChild(parent);
while (n != root and n == lastchild) { while (n != root and n == lastchild) {
n = parent; n = parent;
// TODO deinit parent // TODO deinit parent
// Back to the prev's parent. // Back to the prev's parent.
// If prev has no parent, then the loop must stop. // If prev has no parent, then the loop must stop.
parent = parser.nodeParentNode(n) orelse break; parent = try parser.nodeParentNode(n) orelse break;
// TODO deinit lastchild // TODO deinit lastchild
lastchild = parser.nodeLastChild(parent); lastchild = try parser.nodeLastChild(parent);
} }
if (n == root) { if (n == root) {
return null; return null;
} }
return parser.nodeNextSibling(n); return try parser.nodeNextSibling(n);
} }
}; };
@@ -84,14 +84,14 @@ pub const WalkerDepthFirst = struct {
pub const WalkerChildren = struct { pub const WalkerChildren = struct {
pub fn get_next(_: WalkerChildren, root: *parser.Node, cur: ?*parser.Node) !?*parser.Node { pub fn get_next(_: WalkerChildren, root: *parser.Node, cur: ?*parser.Node) !?*parser.Node {
// On walk start, we return the first root's child. // On walk start, we return the first root's child.
if (cur == null) return parser.nodeFirstChild(root); if (cur == null) return try parser.nodeFirstChild(root);
// If cur is root, then return null. // If cur is root, then return null.
// This is a special case, if the root is included in the walk, we // This is a special case, if the root is included in the walk, we
// don't want to go further to find children. // don't want to go further to find children.
if (root == cur.?) return null; if (root == cur.?) return null;
return parser.nodeNextSibling(cur.?); return try parser.nodeNextSibling(cur.?);
} }
}; };

View File

@@ -26,13 +26,7 @@ pub const Opts = struct {
// set to include element shadowroots in the dump // set to include element shadowroots in the dump
page: ?*const Page = null, page: ?*const Page = null,
strip_mode: StripMode = .{}, exclude_scripts: bool = false,
pub const StripMode = struct {
js: bool = false,
ui: bool = false,
css: bool = false,
};
}; };
// writer must be a std.io.Writer // writer must be a std.io.Writer
@@ -47,8 +41,8 @@ pub fn writeDocType(doc_type: *parser.DocumentType, writer: *std.Io.Writer) !voi
try writer.writeAll("<!DOCTYPE "); try writer.writeAll("<!DOCTYPE ");
try writer.writeAll(try parser.documentTypeGetName(doc_type)); try writer.writeAll(try parser.documentTypeGetName(doc_type));
const public_id = parser.documentTypeGetPublicId(doc_type); const public_id = try parser.documentTypeGetPublicId(doc_type);
const system_id = parser.documentTypeGetSystemId(doc_type); const system_id = try parser.documentTypeGetSystemId(doc_type);
if (public_id.len != 0 and system_id.len != 0) { if (public_id.len != 0 and system_id.len != 0) {
try writer.writeAll(" PUBLIC \""); try writer.writeAll(" PUBLIC \"");
try writeEscapedAttributeValue(writer, public_id); try writeEscapedAttributeValue(writer, public_id);
@@ -69,11 +63,11 @@ pub fn writeDocType(doc_type: *parser.DocumentType, writer: *std.Io.Writer) !voi
} }
pub fn writeNode(node: *parser.Node, opts: Opts, writer: *std.Io.Writer) anyerror!void { pub fn writeNode(node: *parser.Node, opts: Opts, writer: *std.Io.Writer) anyerror!void {
switch (parser.nodeType(node)) { switch (try parser.nodeType(node)) {
.element => { .element => {
// open the tag // open the tag
const tag_type = try parser.nodeHTMLGetTagType(node) orelse .undef; const tag_type = try parser.nodeHTMLGetTagType(node) orelse .undef;
if (try isStripped(tag_type, node, opts.strip_mode)) { if (opts.exclude_scripts and try isScriptOrRelated(tag_type, node)) {
return; return;
} }
@@ -110,7 +104,7 @@ pub fn writeNode(node: *parser.Node, opts: Opts, writer: *std.Io.Writer) anyerro
if (try isVoid(parser.nodeToElement(node))) return; if (try isVoid(parser.nodeToElement(node))) return;
if (tag_type == .script) { if (tag_type == .script) {
try writer.writeAll(parser.nodeTextContent(node) orelse ""); try writer.writeAll(try parser.nodeTextContent(node) orelse "");
} else { } else {
// write the children // write the children
// TODO avoid recursion // TODO avoid recursion
@@ -123,17 +117,17 @@ pub fn writeNode(node: *parser.Node, opts: Opts, writer: *std.Io.Writer) anyerro
try writer.writeAll(">"); try writer.writeAll(">");
}, },
.text => { .text => {
const v = parser.nodeValue(node) orelse return; const v = try parser.nodeValue(node) orelse return;
try writeEscapedTextNode(writer, v); try writeEscapedTextNode(writer, v);
}, },
.cdata_section => { .cdata_section => {
const v = parser.nodeValue(node) orelse return; const v = try parser.nodeValue(node) orelse return;
try writer.writeAll("<![CDATA["); try writer.writeAll("<![CDATA[");
try writer.writeAll(v); try writer.writeAll(v);
try writer.writeAll("]]>"); try writer.writeAll("]]>");
}, },
.comment => { .comment => {
const v = parser.nodeValue(node) orelse return; const v = try parser.nodeValue(node) orelse return;
try writer.writeAll("<!--"); try writer.writeAll("<!--");
try writer.writeAll(v); try writer.writeAll(v);
try writer.writeAll("-->"); try writer.writeAll("-->");
@@ -165,22 +159,9 @@ pub fn writeChildren(root: *parser.Node, opts: Opts, writer: *std.Io.Writer) !vo
} }
} }
fn isStripped(tag_type: parser.Tag, node: *parser.Node, strip_mode: Opts.StripMode) !bool { // When `exclude_scripts` is passed to dump, we don't include <script> tags.
if (strip_mode.js and try isJsRelated(tag_type, node)) { // We also want to omit <link rel=preload as=ascript>
return true; fn isScriptOrRelated(tag_type: parser.Tag, node: *parser.Node) !bool {
}
if (strip_mode.css and try isCssRelated(tag_type, node)) {
return true;
}
if (strip_mode.ui and try isUIRelated(tag_type, node)) {
return true;
}
return false;
}
fn isJsRelated(tag_type: parser.Tag, node: *parser.Node) !bool {
if (tag_type == .script) { if (tag_type == .script) {
return true; return true;
} }
@@ -197,34 +178,6 @@ fn isJsRelated(tag_type: parser.Tag, node: *parser.Node) !bool {
return false; return false;
} }
fn isCssRelated(tag_type: parser.Tag, node: *parser.Node) !bool {
if (tag_type == .style) {
return true;
}
if (tag_type == .link) {
const el = parser.nodeToElement(node);
const rel = try parser.elementGetAttribute(el, "rel") orelse return false;
return std.ascii.eqlIgnoreCase(rel, "stylesheet");
}
return false;
}
fn isUIRelated(tag_type: parser.Tag, node: *parser.Node) !bool {
if (try isCssRelated(tag_type, node)) {
return true;
}
if (tag_type == .img or tag_type == .picture or tag_type == .video) {
return true;
}
if (tag_type == .undef) {
const name = try parser.nodeLocalName(node);
if (std.mem.eql(u8, name, "svg")) {
return true;
}
}
return false;
}
// area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr // area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr
// https://html.spec.whatwg.org/#void-elements // https://html.spec.whatwg.org/#void-elements
fn isVoid(elem: *parser.Element) !bool { fn isVoid(elem: *parser.Element) !bool {
@@ -236,10 +189,10 @@ fn isVoid(elem: *parser.Element) !bool {
}; };
} }
fn writeEscapedTextNode(writer: *std.Io.Writer, value: []const u8) !void { fn writeEscapedTextNode(writer: anytype, value: []const u8) !void {
var v = value; var v = value;
while (v.len > 0) { while (v.len > 0) {
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>', 194 }) orelse { const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>' }) orelse {
return writer.writeAll(v); return writer.writeAll(v);
}; };
try writer.writeAll(v[0..index]); try writer.writeAll(v[0..index]);
@@ -247,22 +200,13 @@ fn writeEscapedTextNode(writer: *std.Io.Writer, value: []const u8) !void {
'&' => try writer.writeAll("&amp;"), '&' => try writer.writeAll("&amp;"),
'<' => try writer.writeAll("&lt;"), '<' => try writer.writeAll("&lt;"),
'>' => try writer.writeAll("&gt;"), '>' => try writer.writeAll("&gt;"),
194 => {
// non breaking space
if (v.len > index + 1 and v[index + 1] == 160) {
try writer.writeAll("&nbsp;");
v = v[index + 2 ..];
continue;
}
try writer.writeByte(194);
},
else => unreachable, else => unreachable,
} }
v = v[index + 1 ..]; v = v[index + 1 ..];
} }
} }
fn writeEscapedAttributeValue(writer: *std.Io.Writer, value: []const u8) !void { fn writeEscapedAttributeValue(writer: anytype, value: []const u8) !void {
var v = value; var v = value;
while (v.len > 0) { while (v.len > 0) {
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>', '"' }) orelse { const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>', '"' }) orelse {
@@ -282,7 +226,7 @@ fn writeEscapedAttributeValue(writer: *std.Io.Writer, value: []const u8) !void {
const testing = std.testing; const testing = std.testing;
test "dump.writeHTML" { test "dump.writeHTML" {
parser.init(); try parser.init();
defer parser.deinit(); defer parser.deinit();
try testWriteHTML( try testWriteHTML(

View File

@@ -19,6 +19,7 @@
const std = @import("std"); const std = @import("std");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
// https://encoding.spec.whatwg.org/#interface-textdecoder // https://encoding.spec.whatwg.org/#interface-textdecoder
@@ -69,8 +70,8 @@ pub fn get_fatal(self: *const TextDecoder) bool {
const DecodeOptions = struct { const DecodeOptions = struct {
stream: bool = false, stream: bool = false,
}; };
pub fn _decode(self: *TextDecoder, str_: ?[]const u8, opts_: ?DecodeOptions, page: *Page) ![]const u8 { pub fn _decode(self: *TextDecoder, input_: ?[]const u8, opts_: ?DecodeOptions, page: *Page) ![]const u8 {
var str = str_ orelse return ""; var str = input_ orelse return "";
const opts: DecodeOptions = opts_ orelse .{}; const opts: DecodeOptions = opts_ orelse .{};
if (self.stream.items.len > 0) { if (self.stream.items.len > 0) {

View File

@@ -18,7 +18,7 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig"); const Env = @import("../env.zig").Env;
// https://encoding.spec.whatwg.org/#interface-textencoder // https://encoding.spec.whatwg.org/#interface-textencoder
const TextEncoder = @This(); const TextEncoder = @This();
@@ -31,7 +31,7 @@ pub fn get_encoding(_: *const TextEncoder) []const u8 {
return "utf-8"; return "utf-8";
} }
pub fn _encode(_: *const TextEncoder, v: []const u8) !js.TypedArray(u8) { pub fn _encode(_: *const TextEncoder, v: []const u8) !Env.TypedArray(u8) {
// Ensure the input is a valid utf-8 // Ensure the input is a valid utf-8
// It seems chrome accepts invalid utf-8 sequence. // It seems chrome accepts invalid utf-8 sequence.
// //

51
src/browser/env.zig Normal file
View File

@@ -0,0 +1,51 @@
const std = @import("std");
const Page = @import("page.zig").Page;
const js = @import("../runtime/js.zig");
const generate = @import("../runtime/generate.zig");
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);
//
// 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)
//
// 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);
//
// 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...})
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("events/event.zig").Interfaces,
@import("html/html.zig").Interfaces,
@import("iterator/iterator.zig").Interfaces,
@import("storage/storage.zig").Interfaces,
@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,
@import("fetch/fetch.zig").Interfaces,
@import("streams/streams.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 Global = @import("html/window.zig").Window;

View File

@@ -1,84 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const log = @import("../../log.zig");
const Window = @import("../html/window.zig").Window;
const parser = @import("../netsurf.zig");
const Event = @import("../events/event.zig").Event;
// https://developer.mozilla.org/en-US/docs/Web/API/PageTransitionEvent
const PageTransitionEvent = @This();
pub const prototype = *Event;
pub const union_make_copy = true;
pub const EventInit = struct {
persisted: ?bool,
};
proto: parser.Event,
persisted: bool,
pub fn constructor(event_type: []const u8, opts: EventInit) !PageTransitionEvent {
const event = try parser.eventCreate();
defer parser.eventDestroy(event);
try parser.eventInit(event, event_type, .{});
parser.eventSetInternalType(event, .page_transition_event);
return .{
.proto = event.*,
.persisted = opts.persisted orelse false,
};
}
const PageTransitionKind = enum { show, hide };
pub fn dispatch(window: *Window, kind: PageTransitionKind, persisted: bool) void {
const evt_type = switch (kind) {
.show => "pageshow",
.hide => "pagehide",
};
log.debug(.script_event, "dispatch event", .{
.type = evt_type,
.source = "navigation",
});
var evt = PageTransitionEvent.constructor(evt_type, .{ .persisted = persisted }) catch |err| {
log.err(.app, "event constructor error", .{
.err = err,
.type = evt_type,
.source = "navigation",
});
return;
};
_ = parser.eventTargetDispatchEvent(
@as(*parser.EventTarget, @ptrCast(window)),
&evt.proto,
) catch |err| {
log.err(.app, "dispatch event error", .{
.err = err,
.type = evt_type,
.source = "navigation",
});
};
}

View File

@@ -1,57 +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");
// https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent
pub const CompositionEvent = struct {
data: []const u8,
proto: parser.Event,
pub const union_make_copy = true;
pub const prototype = *parser.Event;
pub const ConstructorOptions = struct {
data: []const u8 = "",
};
pub fn constructor(event_type: []const u8, options_: ?ConstructorOptions) !CompositionEvent {
const options: ConstructorOptions = options_ orelse .{};
const event = try parser.eventCreate();
defer parser.eventDestroy(event);
try parser.eventInit(event, event_type, .{});
parser.eventSetInternalType(event, .composition_event);
return .{
.proto = event.*,
.data = options.data,
};
}
pub fn get_data(self: *const CompositionEvent) []const u8 {
return self.data;
}
};
const testing = @import("../../testing.zig");
test "Browser: Events.Composition" {
try testing.htmlRunner("events/composition.html");
}

View File

@@ -16,10 +16,9 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Event = @import("event.zig").Event; const Event = @import("event.zig").Event;
const JsObject = @import("../env.zig").JsObject;
const netsurf = @import("../netsurf.zig"); const netsurf = @import("../netsurf.zig");
// https://dom.spec.whatwg.org/#interface-customevent // https://dom.spec.whatwg.org/#interface-customevent
@@ -28,13 +27,13 @@ pub const CustomEvent = struct {
pub const union_make_copy = true; pub const union_make_copy = true;
proto: parser.Event, proto: parser.Event,
detail: ?js.Object, detail: ?JsObject,
const CustomEventInit = struct { const CustomEventInit = struct {
bubbles: bool = false, bubbles: bool = false,
cancelable: bool = false, cancelable: bool = false,
composed: bool = false, composed: bool = false,
detail: ?js.Object = null, detail: ?JsObject = null,
}; };
pub fn constructor(event_type: []const u8, opts_: ?CustomEventInit) !CustomEvent { pub fn constructor(event_type: []const u8, opts_: ?CustomEventInit) !CustomEvent {
@@ -54,7 +53,7 @@ pub const CustomEvent = struct {
}; };
} }
pub fn get_detail(self: *CustomEvent) ?js.Object { pub fn get_detail(self: *CustomEvent) ?JsObject {
return self.detail; return self.detail;
} }
@@ -65,7 +64,7 @@ pub const CustomEvent = struct {
event_type: []const u8, event_type: []const u8,
can_bubble: bool, can_bubble: bool,
cancelable: bool, cancelable: bool,
maybe_detail: ?js.Object, maybe_detail: ?JsObject,
) !void { ) !void {
// This function can only be called after the constructor has called. // This function can only be called after the constructor has called.
// So we assume proto is initialized already by constructor. // So we assume proto is initialized already by constructor.

View File

@@ -17,12 +17,11 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const generate = @import("../js/generate.zig"); const generate = @import("../../runtime/generate.zig");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const Node = @import("../dom/node.zig").Node; const Node = @import("../dom/node.zig").Node;
@@ -37,10 +36,6 @@ const MouseEvent = @import("mouse_event.zig").MouseEvent;
const KeyboardEvent = @import("keyboard_event.zig").KeyboardEvent; const KeyboardEvent = @import("keyboard_event.zig").KeyboardEvent;
const ErrorEvent = @import("../html/error_event.zig").ErrorEvent; const ErrorEvent = @import("../html/error_event.zig").ErrorEvent;
const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent; const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
const PopStateEvent = @import("../html/History.zig").PopStateEvent;
const CompositionEvent = @import("composition_event.zig").CompositionEvent;
const NavigationCurrentEntryChangeEvent = @import("../navigation/root.zig").NavigationCurrentEntryChangeEvent;
const PageTransitionEvent = @import("../events/PageTransitionEvent.zig");
// Event interfaces // Event interfaces
pub const Interfaces = .{ pub const Interfaces = .{
@@ -51,10 +46,6 @@ pub const Interfaces = .{
KeyboardEvent, KeyboardEvent,
ErrorEvent, ErrorEvent,
MessageEvent, MessageEvent,
PopStateEvent,
CompositionEvent,
NavigationCurrentEntryChangeEvent,
PageTransitionEvent,
}; };
pub const Union = generate.Union(Interfaces); pub const Union = generate.Union(Interfaces);
@@ -79,15 +70,9 @@ pub const Event = struct {
.custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* }, .custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* },
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* }, .progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
.mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) }, .mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) },
.error_event => .{ .ErrorEvent = (@as(*ErrorEvent, @fieldParentPtr("proto", evt))).* }, .error_event => .{ .ErrorEvent = @as(*ErrorEvent, @ptrCast(evt)).* },
.message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* }, .message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* },
.keyboard_event => .{ .KeyboardEvent = @as(*parser.KeyboardEvent, @ptrCast(evt)) }, .keyboard_event => .{ .KeyboardEvent = @as(*parser.KeyboardEvent, @ptrCast(evt)) },
.pop_state => .{ .PopStateEvent = @as(*PopStateEvent, @ptrCast(evt)).* },
.composition_event => .{ .CompositionEvent = (@as(*CompositionEvent, @fieldParentPtr("proto", evt))).* },
.navigation_current_entry_change_event => .{
.NavigationCurrentEntryChangeEvent = @as(*NavigationCurrentEntryChangeEvent, @ptrCast(evt)).*,
},
.page_transition_event => .{ .PageTransitionEvent = @as(*PageTransitionEvent, @ptrCast(evt)).* },
}; };
} }
@@ -99,8 +84,8 @@ pub const Event = struct {
// Getters // Getters
pub fn get_type(self: *parser.Event) []const u8 { pub fn get_type(self: *parser.Event) ![]const u8 {
return parser.eventType(self); return try parser.eventType(self);
} }
pub fn get_target(self: *parser.Event, page: *Page) !?EventTargetUnion { pub fn get_target(self: *parser.Event, page: *Page) !?EventTargetUnion {
@@ -173,7 +158,7 @@ pub const Event = struct {
const et_ = parser.eventTarget(self); const et_ = parser.eventTarget(self);
const et = et_ orelse return &.{}; const et = et_ orelse return &.{};
var node: ?*parser.Node = switch (parser.eventTargetInternalType(et)) { var node: ?*parser.Node = switch (try parser.eventTargetInternalType(et)) {
.libdom_node => @as(*parser.Node, @ptrCast(et)), .libdom_node => @as(*parser.Node, @ptrCast(et)),
.plain => parser.eventTargetToNode(et), .plain => parser.eventTargetToNode(et),
else => { else => {
@@ -189,8 +174,8 @@ pub const Event = struct {
.node = try Node.toInterface(n), .node = try Node.toInterface(n),
}); });
node = parser.nodeParentNode(n); node = try parser.nodeParentNode(n);
if (node == null and parser.nodeType(n) == .document_fragment) { if (node == null and try parser.nodeType(n) == .document_fragment) {
// we have a non-continuous hook from a shadowroot to its host ( // we have a non-continuous hook from a shadowroot to its host (
// it's parent element). libdom doesn't really support ShdowRoots // it's parent element). libdom doesn't really support ShdowRoots
// and, for the most part, that works out well since it naturally // and, for the most part, that works out well since it naturally
@@ -231,15 +216,18 @@ pub const Event = struct {
pub const EventHandler = struct { pub const EventHandler = struct {
once: bool, once: bool,
capture: bool, capture: bool,
callback: js.Function, callback: Function,
node: parser.EventNode, node: parser.EventNode,
listener: *parser.EventListener, listener: *parser.EventListener,
pub const Listener = union(enum) { const Env = @import("../env.zig").Env;
function: js.Function, const Function = Env.Function;
object: js.Object,
pub fn callback(self: Listener, target: *parser.EventTarget) !?js.Function { pub const Listener = union(enum) {
function: Function,
object: Env.JsObject,
pub fn callback(self: Listener, target: *parser.EventTarget) !?Function {
return switch (self) { return switch (self) {
.function => |func| try func.withThis(target), .function => |func| try func.withThis(target),
.object => |obj| blk: { .object => |obj| blk: {
@@ -340,7 +328,7 @@ pub const EventHandler = struct {
fn handle(node: *parser.EventNode, event: *parser.Event) void { fn handle(node: *parser.EventNode, event: *parser.Event) void {
const ievent = Event.toInterface(event); const ievent = Event.toInterface(event);
const self: *EventHandler = @fieldParentPtr("node", node); const self: *EventHandler = @fieldParentPtr("node", node);
var result: js.Function.Result = undefined; var result: Function.Result = undefined;
self.callback.tryCall(void, .{ievent}, &result) catch { self.callback.tryCall(void, .{ievent}, &result) catch {
log.debug(.user_script, "callback error", .{ log.debug(.user_script, "callback error", .{
.err = result.exception, .err = result.exception,
@@ -351,7 +339,7 @@ pub const EventHandler = struct {
if (self.once) { if (self.once) {
const target = parser.eventTarget(event).?; const target = parser.eventTarget(event).?;
const typ = parser.eventType(event); const typ = parser.eventType(event) catch return;
parser.eventTargetRemoveEventListener( parser.eventTargetRemoveEventListener(
target, target,
typ, typ,
@@ -406,40 +394,6 @@ const SignalCallback = struct {
} }
}; };
pub fn DirectEventHandler(
comptime TargetT: type,
target: *TargetT,
event_type: []const u8,
maybe_listener: ?EventHandler.Listener,
cb: *?js.Function,
page_arena: std.mem.Allocator,
) !void {
const event_target = parser.toEventTarget(TargetT, target);
// Check if we have a listener set.
if (cb.*) |callback| {
const listener = try parser.eventTargetHasListener(event_target, event_type, false, callback.id);
std.debug.assert(listener != null);
try parser.eventTargetRemoveEventListener(event_target, event_type, listener.?, false);
}
if (maybe_listener) |listener| {
switch (listener) {
// If an object is given as listener, do nothing.
.object => {},
.function => |callback| {
_ = try EventHandler.register(page_arena, event_target, event_type, listener, null) orelse unreachable;
cb.* = callback;
return;
},
}
}
// Just unset the listener.
cb.* = null;
}
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: Event" { test "Browser: Event" {
try testing.htmlRunner("events/event.html"); try testing.htmlRunner("events/event.html");

View File

@@ -22,6 +22,7 @@ const builtin = @import("builtin");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Event = @import("event.zig").Event; 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. // 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 // https://developer.mozilla.org/en-US/docs/Web/API/UIEvent
@@ -53,8 +54,8 @@ pub const KeyboardEvent = struct {
pub fn constructor(event_type: []const u8, maybe_options: ?ConstructorOptions) !*parser.KeyboardEvent { pub fn constructor(event_type: []const u8, maybe_options: ?ConstructorOptions) !*parser.KeyboardEvent {
const options: ConstructorOptions = maybe_options orelse .{}; const options: ConstructorOptions = maybe_options orelse .{};
const event = try parser.keyboardEventCreate(); var event = try parser.keyboardEventCreate();
parser.eventSetInternalType(@ptrCast(event), .keyboard_event); parser.eventSetInternalType(@ptrCast(&event), .keyboard_event);
try parser.keyboardEventInit( try parser.keyboardEventInit(
event, event,

View File

@@ -21,6 +21,7 @@ const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Event = @import("event.zig").Event; 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. // 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 // https://developer.mozilla.org/en-US/docs/Web/API/UIEvent
@@ -54,8 +55,8 @@ pub const MouseEvent = struct {
pub fn constructor(event_type: []const u8, opts_: ?MouseEventInit) !*parser.MouseEvent { pub fn constructor(event_type: []const u8, opts_: ?MouseEventInit) !*parser.MouseEvent {
const opts = opts_ orelse MouseEventInit{}; const opts = opts_ orelse MouseEventInit{};
const mouse_event = try parser.mouseEventCreate(); var mouse_event = try parser.mouseEventCreate();
parser.eventSetInternalType(@ptrCast(mouse_event), .mouse_event); parser.eventSetInternalType(@ptrCast(&mouse_event), .mouse_event);
try parser.mouseEventInit(mouse_event, event_type, .{ try parser.mouseEventInit(mouse_event, event_type, .{
.x = opts.clientX, .x = opts.clientX,
@@ -68,7 +69,7 @@ pub const MouseEvent = struct {
}); });
if (!std.mem.eql(u8, event_type, "click")) { if (!std.mem.eql(u8, event_type, "click")) {
log.warn(.browser, "unsupported mouse event", .{ .event = event_type }); log.warn(.mouse_event, "unsupported mouse event", .{ .event = event_type });
} }
return mouse_event; return mouse_event;

View File

@@ -17,13 +17,15 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const URL = @import("../../url.zig").URL; const URL = @import("../../url.zig").URL;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const iterator = @import("../iterator/iterator.zig"); const iterator = @import("../iterator/iterator.zig");
const v8 = @import("v8");
const Env = @import("../env.zig").Env;
// https://developer.mozilla.org/en-US/docs/Web/API/Headers // https://developer.mozilla.org/en-US/docs/Web/API/Headers
const Headers = @This(); const Headers = @This();
@@ -67,7 +69,7 @@ pub const HeadersInit = union(enum) {
// Headers // Headers
headers: *Headers, headers: *Headers,
// Mappings // Mappings
object: js.Object, object: Env.JsObject,
}; };
pub fn constructor(_init: ?HeadersInit, page: *Page) !Headers { pub fn constructor(_init: ?HeadersInit, page: *Page) !Headers {
@@ -157,7 +159,7 @@ pub fn _entries(self: *const Headers) HeadersEntryIterable {
}; };
} }
pub fn _forEach(self: *Headers, callback_fn: js.Function, this_arg: ?js.Object) !void { pub fn _forEach(self: *Headers, callback_fn: Env.Function, this_arg: ?Env.JsObject) !void {
var iter = self.headers.iterator(); var iter = self.headers.iterator();
const cb = if (this_arg) |this| try callback_fn.withThis(this) else callback_fn; const cb = if (this_arg) |this| try callback_fn.withThis(this) else callback_fn;

View File

@@ -17,7 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const URL = @import("../../url.zig").URL; const URL = @import("../../url.zig").URL;
@@ -27,6 +26,9 @@ const Response = @import("./Response.zig");
const Http = @import("../../http/Http.zig"); const Http = @import("../../http/Http.zig");
const ReadableStream = @import("../streams/ReadableStream.zig"); const ReadableStream = @import("../streams/ReadableStream.zig");
const v8 = @import("v8");
const Env = @import("../env.zig").Env;
const Headers = @import("Headers.zig"); const Headers = @import("Headers.zig");
const HeadersInit = @import("Headers.zig").HeadersInit; const HeadersInit = @import("Headers.zig").HeadersInit;
@@ -78,27 +80,6 @@ pub const RequestCredentials = enum {
} }
}; };
pub const RequestMode = enum {
cors,
@"no-cors",
@"same-origin",
navigate,
pub fn fromString(str: []const u8) ?RequestMode {
for (std.enums.values(RequestMode)) |cache| {
if (std.ascii.eqlIgnoreCase(str, @tagName(cache))) {
return cache;
}
} else {
return null;
}
}
pub fn toString(self: RequestMode) []const u8 {
return @tagName(self);
}
};
// https://developer.mozilla.org/en-US/docs/Web/API/RequestInit // https://developer.mozilla.org/en-US/docs/Web/API/RequestInit
pub const RequestInit = struct { pub const RequestInit = struct {
body: ?[]const u8 = null, body: ?[]const u8 = null,
@@ -107,7 +88,6 @@ pub const RequestInit = struct {
headers: ?HeadersInit = null, headers: ?HeadersInit = null,
integrity: ?[]const u8 = null, integrity: ?[]const u8 = null,
method: ?[]const u8 = null, method: ?[]const u8 = null,
mode: ?[]const u8 = null,
}; };
// https://developer.mozilla.org/en-US/docs/Web/API/Request/Request // https://developer.mozilla.org/en-US/docs/Web/API/Request/Request
@@ -117,8 +97,6 @@ method: Http.Method,
url: [:0]const u8, url: [:0]const u8,
cache: RequestCache, cache: RequestCache,
credentials: RequestCredentials, credentials: RequestCredentials,
// no-cors is default is not built with constructor.
mode: RequestMode = .@"no-cors",
headers: Headers, headers: Headers,
body: ?[]const u8, body: ?[]const u8,
body_used: bool = false, body_used: bool = false,
@@ -137,11 +115,11 @@ pub fn constructor(input: RequestInput, _options: ?RequestInit, page: *Page) !Re
}, },
}; };
const body = if (options.body) |body| try arena.dupe(u8, body) else null;
const cache = (if (options.cache) |cache| RequestCache.fromString(cache) else null) orelse RequestCache.default; const cache = (if (options.cache) |cache| RequestCache.fromString(cache) else null) orelse RequestCache.default;
const credentials = (if (options.credentials) |creds| RequestCredentials.fromString(creds) else null) orelse RequestCredentials.@"same-origin"; const credentials = (if (options.credentials) |creds| RequestCredentials.fromString(creds) else null) orelse RequestCredentials.@"same-origin";
const integrity = if (options.integrity) |integ| try arena.dupe(u8, integ) else ""; const integrity = if (options.integrity) |integ| try arena.dupe(u8, integ) else "";
const headers: Headers = if (options.headers) |hdrs| try Headers.constructor(hdrs, page) else .{}; const headers: Headers = if (options.headers) |hdrs| try Headers.constructor(hdrs, page) else .{};
const mode = (if (options.mode) |mode| RequestMode.fromString(mode) else null) orelse RequestMode.cors;
const method: Http.Method = blk: { const method: Http.Method = blk: {
if (options.method) |given_method| { if (options.method) |given_method| {
@@ -157,19 +135,11 @@ pub fn constructor(input: RequestInput, _options: ?RequestInit, page: *Page) !Re
} }
}; };
// Can't have a body on .GET or .HEAD.
const body: ?[]const u8 = blk: {
if (method == .GET or method == .HEAD) {
break :blk null;
} else break :blk if (options.body) |body| try arena.dupe(u8, body) else null;
};
return .{ return .{
.method = method, .method = method,
.url = url, .url = url,
.cache = cache, .cache = cache,
.credentials = credentials, .credentials = credentials,
.mode = mode,
.headers = headers, .headers = headers,
.body = body, .body = body,
.integrity = integrity, .integrity = integrity,
@@ -179,7 +149,7 @@ pub fn constructor(input: RequestInput, _options: ?RequestInit, page: *Page) !Re
pub fn get_body(self: *const Request, page: *Page) !?*ReadableStream { pub fn get_body(self: *const Request, page: *Page) !?*ReadableStream {
if (self.body) |body| { if (self.body) |body| {
const stream = try ReadableStream.constructor(null, null, page); const stream = try ReadableStream.constructor(null, null, page);
try stream.queue.append(page.arena, .{ .string = body }); try stream.queue.append(page.arena, body);
return stream; return stream;
} else return null; } else return null;
} }
@@ -211,10 +181,6 @@ pub fn get_method(self: *const Request) []const u8 {
return @tagName(self.method); return @tagName(self.method);
} }
pub fn get_mode(self: *const Request) RequestMode {
return self.mode;
}
pub fn get_url(self: *const Request) []const u8 { pub fn get_url(self: *const Request) []const u8 {
return self.url; return self.url;
} }
@@ -239,38 +205,59 @@ pub fn _clone(self: *Request) !Request {
}; };
} }
pub fn _bytes(self: *Response, page: *Page) !js.Promise { pub fn _bytes(self: *Response, page: *Page) !Env.Promise {
if (self.body_used) { if (self.body_used) {
return error.TypeError; return error.TypeError;
} }
const resolver = Env.PromiseResolver{
.js_context = page.main_context,
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
};
try resolver.resolve(self.body);
self.body_used = true; self.body_used = true;
return page.js.resolvePromise(self.body); return resolver.promise();
} }
pub fn _json(self: *Response, page: *Page) !js.Promise { pub fn _json(self: *Response, page: *Page) !Env.Promise {
if (self.body_used) { if (self.body_used) {
return error.TypeError; return error.TypeError;
} }
const resolver = Env.PromiseResolver{
.js_context = page.main_context,
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
};
const p = std.json.parseFromSliceLeaky(
std.json.Value,
page.call_arena,
self.body,
.{},
) catch |e| {
log.info(.browser, "invalid json", .{ .err = e, .source = "Request" });
return error.SyntaxError;
};
try resolver.resolve(p);
self.body_used = true; self.body_used = true;
return resolver.promise();
if (self.body) |body| {
const value = js.Value.fromJson(page.js, body) catch |e| {
log.info(.browser, "invalid json", .{ .err = e, .source = "Request" });
return error.SyntaxError;
};
const pvalue = try value.persist(page.js);
return page.js.resolvePromise(pvalue);
}
return page.js.resolvePromise(null);
} }
pub fn _text(self: *Response, page: *Page) !js.Promise { pub fn _text(self: *Response, page: *Page) !Env.Promise {
if (self.body_used) { if (self.body_used) {
return error.TypeError; return error.TypeError;
} }
const resolver = Env.PromiseResolver{
.js_context = page.main_context,
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
};
try resolver.resolve(self.body);
self.body_used = true; self.body_used = true;
return page.js.resolvePromise(self.body); return resolver.promise();
} }
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");

View File

@@ -17,9 +17,10 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const v8 = @import("v8");
const HttpClient = @import("../../http/Client.zig"); const HttpClient = @import("../../http/Client.zig");
const Http = @import("../../http/Http.zig"); const Http = @import("../../http/Http.zig");
const URL = @import("../../url.zig").URL; const URL = @import("../../url.zig").URL;
@@ -28,6 +29,7 @@ const ReadableStream = @import("../streams/ReadableStream.zig");
const Headers = @import("Headers.zig"); const Headers = @import("Headers.zig");
const HeadersInit = @import("Headers.zig").HeadersInit; const HeadersInit = @import("Headers.zig").HeadersInit;
const Env = @import("../env.zig").Env;
const Mime = @import("../mime.zig").Mime; const Mime = @import("../mime.zig").Mime;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
@@ -39,10 +41,9 @@ status_text: []const u8 = "",
headers: Headers, headers: Headers,
mime: ?Mime = null, mime: ?Mime = null,
url: []const u8 = "", url: []const u8 = "",
body: ?[]const u8 = null, body: []const u8 = "",
body_used: bool = false, body_used: bool = false,
redirected: bool = false, redirected: bool = false,
type: ResponseType = .basic,
const ResponseBody = union(enum) { const ResponseBody = union(enum) {
string: []const u8, string: []const u8,
@@ -54,28 +55,6 @@ const ResponseOptions = struct {
headers: ?HeadersInit = null, headers: ?HeadersInit = null,
}; };
pub const ResponseType = enum {
basic,
cors,
@"error",
@"opaque",
opaqueredirect,
pub fn fromString(str: []const u8) ?ResponseType {
for (std.enums.values(ResponseType)) |cache| {
if (std.ascii.eqlIgnoreCase(str, @tagName(cache))) {
return cache;
}
} else {
return null;
}
}
pub fn toString(self: ResponseType) []const u8 {
return @tagName(self);
}
};
pub fn constructor(_input: ?ResponseBody, _options: ?ResponseOptions, page: *Page) !Response { pub fn constructor(_input: ?ResponseBody, _options: ?ResponseOptions, page: *Page) !Response {
const arena = page.arena; const arena = page.arena;
@@ -89,7 +68,7 @@ pub fn constructor(_input: ?ResponseBody, _options: ?ResponseOptions, page: *Pag
}, },
} }
} else { } else {
break :blk null; break :blk "";
} }
}; };
@@ -106,9 +85,7 @@ pub fn constructor(_input: ?ResponseBody, _options: ?ResponseOptions, page: *Pag
pub fn get_body(self: *const Response, page: *Page) !*ReadableStream { pub fn get_body(self: *const Response, page: *Page) !*ReadableStream {
const stream = try ReadableStream.constructor(null, null, page); const stream = try ReadableStream.constructor(null, null, page);
if (self.body) |body| { try stream.queue.append(page.arena, self.body);
try stream.queue.append(page.arena, .{ .string = body });
}
return stream; return stream;
} }
@@ -136,10 +113,6 @@ pub fn get_statusText(self: *const Response) []const u8 {
return self.status_text; return self.status_text;
} }
pub fn get_type(self: *const Response) ResponseType {
return self.type;
}
pub fn get_url(self: *const Response) []const u8 { pub fn get_url(self: *const Response) []const u8 {
return self.url; return self.url;
} }
@@ -159,44 +132,62 @@ pub fn _clone(self: *const Response) !Response {
.redirected = self.redirected, .redirected = self.redirected,
.status = self.status, .status = self.status,
.url = self.url, .url = self.url,
.type = self.type,
}; };
} }
pub fn _bytes(self: *Response, page: *Page) !js.Promise { pub fn _bytes(self: *Response, page: *Page) !Env.Promise {
if (self.body_used) { if (self.body_used) {
return error.TypeError; return error.TypeError;
} }
const resolver = Env.PromiseResolver{
.js_context = page.main_context,
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
};
try resolver.resolve(self.body);
self.body_used = true; self.body_used = true;
return page.js.resolvePromise(self.body); return resolver.promise();
} }
pub fn _json(self: *Response, page: *Page) !js.Promise { pub fn _json(self: *Response, page: *Page) !Env.Promise {
if (self.body_used) { if (self.body_used) {
return error.TypeError; return error.TypeError;
} }
if (self.body) |body| { const resolver = Env.PromiseResolver{
self.body_used = true; .js_context = page.main_context,
const value = js.Value.fromJson(page.js, body) catch |e| { .resolver = v8.PromiseResolver.init(page.main_context.v8_context),
log.info(.browser, "invalid json", .{ .err = e, .source = "Response" }); };
return error.SyntaxError;
};
const pvalue = try value.persist(page.js);
return page.js.resolvePromise(pvalue); const p = std.json.parseFromSliceLeaky(
} std.json.Value,
return page.js.resolvePromise(null); page.call_arena,
self.body,
.{},
) catch |e| {
log.info(.browser, "invalid json", .{ .err = e, .source = "Response" });
return error.SyntaxError;
};
try resolver.resolve(p);
self.body_used = true;
return resolver.promise();
} }
pub fn _text(self: *Response, page: *Page) !js.Promise { pub fn _text(self: *Response, page: *Page) !Env.Promise {
if (self.body_used) { if (self.body_used) {
return error.TypeError; return error.TypeError;
} }
self.body_used = true;
return page.js.resolvePromise(self.body); const resolver = Env.PromiseResolver{
.js_context = page.main_context,
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
};
try resolver.resolve(self.body);
self.body_used = true;
return resolver.promise();
} }
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");

View File

@@ -19,7 +19,7 @@
const std = @import("std"); const std = @import("std");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const js = @import("../js/js.zig"); const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const Http = @import("../../http/Http.zig"); const Http = @import("../../http/Http.zig");
@@ -43,9 +43,9 @@ pub const Interfaces = .{
}; };
pub const FetchContext = struct { pub const FetchContext = struct {
page: *Page,
arena: std.mem.Allocator, arena: std.mem.Allocator,
promise_resolver: js.PersistentPromiseResolver, js_ctx: *Env.JsContext,
promise_resolver: Env.PersistentPromiseResolver,
method: Http.Method, method: Http.Method,
url: []const u8, url: []const u8,
@@ -53,7 +53,6 @@ pub const FetchContext = struct {
headers: std.ArrayListUnmanaged([]const u8) = .empty, headers: std.ArrayListUnmanaged([]const u8) = .empty,
status: u16 = 0, status: u16 = 0,
mime: ?Mime = null, mime: ?Mime = null,
mode: Request.RequestMode,
transfer: ?*HttpClient.Transfer = null, transfer: ?*HttpClient.Transfer = null,
/// This effectively takes ownership of the FetchContext. /// This effectively takes ownership of the FetchContext.
@@ -63,22 +62,6 @@ pub const FetchContext = struct {
pub fn toResponse(self: *const FetchContext) !Response { pub fn toResponse(self: *const FetchContext) !Response {
var headers: Headers = .{}; var headers: Headers = .{};
// seems to be the highest priority
const same_origin = try self.page.isSameOrigin(self.url);
// If the mode is "no-cors", we need to return this opaque/stripped Response.
// https://developer.mozilla.org/en-US/docs/Web/API/Response/type
if (!same_origin and self.mode == .@"no-cors") {
return Response{
.status = 0,
.headers = headers,
.mime = self.mime,
.body = null,
.url = self.url,
.type = .@"opaque",
};
}
// convert into Headers // convert into Headers
for (self.headers.items) |hdr| { for (self.headers.items) |hdr| {
var iter = std.mem.splitScalar(u8, hdr, ':'); var iter = std.mem.splitScalar(u8, hdr, ':');
@@ -87,35 +70,22 @@ pub const FetchContext = struct {
try headers.append(name, value, self.arena); try headers.append(name, value, self.arena);
} }
const resp_type: Response.ResponseType = blk: {
if (same_origin or std.mem.startsWith(u8, self.url, "data:")) {
break :blk .basic;
}
break :blk switch (self.mode) {
.cors => .cors,
.@"same-origin", .navigate => .basic,
.@"no-cors" => unreachable,
};
};
return Response{ return Response{
.status = self.status, .status = self.status,
.headers = headers, .headers = headers,
.mime = self.mime, .mime = self.mime,
.body = self.body.items, .body = self.body.items,
.url = self.url, .url = self.url,
.type = resp_type,
}; };
} }
}; };
// https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch // https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
pub fn fetch(input: RequestInput, options: ?RequestInit, page: *Page) !js.Promise { pub fn fetch(input: RequestInput, options: ?RequestInit, page: *Page) !Env.Promise {
const arena = page.arena; const arena = page.arena;
const req = try Request.constructor(input, options, page); const req = try Request.constructor(input, options, page);
var headers = try page.http_client.newHeaders(); var headers = try Http.Headers.init();
// Copy our headers into the HTTP headers. // Copy our headers into the HTTP headers.
var header_iter = req.headers.headers.iterator(); var header_iter = req.headers.headers.iterator();
@@ -131,16 +101,15 @@ pub fn fetch(input: RequestInput, options: ?RequestInit, page: *Page) !js.Promis
try page.requestCookie(.{}).headersForRequest(arena, req.url, &headers); try page.requestCookie(.{}).headersForRequest(arena, req.url, &headers);
const resolver = try page.js.createPromiseResolver(.page); const resolver = try page.main_context.createPersistentPromiseResolver();
const fetch_ctx = try arena.create(FetchContext); const fetch_ctx = try arena.create(FetchContext);
fetch_ctx.* = .{ fetch_ctx.* = .{
.page = page,
.arena = arena, .arena = arena,
.js_ctx = page.main_context,
.promise_resolver = resolver, .promise_resolver = resolver,
.method = req.method, .method = req.method,
.url = req.url, .url = req.url,
.mode = req.mode,
}; };
try page.http_client.request(.{ try page.http_client.request(.{

View File

@@ -1,284 +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 Writer = std.Io.Writer;
const Page = @import("../page.zig").Page;
const js = @import("../js/js.zig");
const ReadableStream = @import("../streams/ReadableStream.zig");
/// https://w3c.github.io/FileAPI/#blob-section
/// https://developer.mozilla.org/en-US/docs/Web/API/Blob
const Blob = @This();
/// Immutable slice of blob.
/// Note that another blob may hold a pointer/slice to this,
/// so its better to leave the deallocation of it to arena allocator.
slice: []const u8,
/// MIME attached to blob. Can be an empty string.
mime: []const u8,
const ConstructorOptions = struct {
/// MIME type.
type: []const u8 = "",
/// How to handle line endings (CR and LF).
/// `transparent` means do nothing, `native` expects CRLF (\r\n) on Windows.
endings: []const u8 = "transparent",
};
/// Creates a new Blob.
pub fn constructor(
maybe_blob_parts: ?[]const []const u8,
maybe_options: ?ConstructorOptions,
page: *Page,
) !Blob {
const options: ConstructorOptions = maybe_options orelse .{};
// Setup MIME; This can be any string according to my observations.
const mime: []const u8 = blk: {
const t = options.type;
if (t.len == 0) {
break :blk "";
}
break :blk try page.arena.dupe(u8, t);
};
if (maybe_blob_parts) |blob_parts| {
var w: Writer.Allocating = .init(page.arena);
const use_native_endings = std.mem.eql(u8, options.endings, "native");
try writeBlobParts(&w.writer, blob_parts, use_native_endings);
return .{ .slice = w.written(), .mime = mime };
}
// We don't have `blob_parts`, why would you want a Blob anyway then?
return .{ .slice = "", .mime = mime };
}
const largest_vector = @max(std.simd.suggestVectorLength(u8) orelse 1, 8);
/// Array of possible vector sizes for the current arch in decrementing order.
/// We may move this to some file for SIMD helpers in the future.
const vector_sizes = blk: {
// Required for length calculation.
var n: usize = largest_vector;
var total: usize = 0;
while (n != 2) : (n /= 2) total += 1;
// Populate an array with vector sizes.
n = largest_vector;
var i: usize = 0;
var items: [total]usize = undefined;
while (n != 2) : (n /= 2) {
defer i += 1;
items[i] = n;
}
break :blk items;
};
/// Writes blob parts to given `Writer` with desired endings.
fn writeBlobParts(
writer: *Writer,
blob_parts: []const []const u8,
use_native_endings: bool,
) !void {
// Transparent.
if (!use_native_endings) {
for (blob_parts) |part| {
try writer.writeAll(part);
}
return;
}
// TODO: Windows support.
// Linux & Unix.
// Both Firefox and Chrome implement it as such:
// CRLF => LF
// CR => LF
// So even though CR is not followed by LF, it gets replaced.
//
// I believe this is because such scenario is possible:
// ```
// let parts = [ "the quick\r", "\nbrown fox" ];
// ```
// In the example, one should have to check the part before in order to
// understand that CRLF is being presented in the final buffer.
// So they took a simpler approach, here's what given blob parts produce:
// ```
// "the quick\n\nbrown fox"
// ```
scan_parts: for (blob_parts) |part| {
var end: usize = 0;
inline for (vector_sizes) |vector_len| {
const Vec = @Vector(vector_len, u8);
while (end + vector_len <= part.len) : (end += vector_len) {
const cr: Vec = @splat('\r');
// Load chunk as vectors.
const slice = part[end..][0..vector_len];
const chunk: Vec = slice.*;
// Look for CR.
const match = chunk == cr;
// Create a bitset out of match vector.
const bitset = std.bit_set.IntegerBitSet(vector_len){
.mask = @bitCast(@intFromBool(match)),
};
var iter = bitset.iterator(.{});
var relative_start: usize = 0;
while (iter.next()) |index| {
_ = try writer.writeVec(&.{ slice[relative_start..index], "\n" });
if (index + 1 != slice.len and slice[index + 1] == '\n') {
relative_start = index + 2;
} else {
relative_start = index + 1;
}
}
_ = try writer.writeVec(&.{slice[relative_start..]});
}
}
// Scalar scan fallback.
var relative_start: usize = end;
while (end < part.len) {
if (part[end] == '\r') {
_ = try writer.writeVec(&.{ part[relative_start..end], "\n" });
// Part ends with CR. We can continue to next part.
if (end + 1 == part.len) {
continue :scan_parts;
}
// If next char is LF, skip it too.
if (part[end + 1] == '\n') {
relative_start = end + 2;
} else {
relative_start = end + 1;
}
}
end += 1;
}
// Write the remaining. We get this in such situations:
// `the quick brown\rfox`
// `the quick brown\r\nfox`
try writer.writeAll(part[relative_start..end]);
}
}
/// Returns a Promise that resolves with the contents of the blob
/// as binary data contained in an ArrayBuffer.
pub fn _arrayBuffer(self: *const Blob, page: *Page) !js.Promise {
return page.js.resolvePromise(js.ArrayBuffer{ .values = self.slice });
}
/// Returns a ReadableStream which upon reading returns the data
/// contained within the Blob.
pub fn _stream(self: *const Blob, page: *Page) !*ReadableStream {
const stream = try ReadableStream.constructor(null, null, page);
try stream.queue.append(page.arena, .{
.uint8array = .{ .values = self.slice },
});
return stream;
}
/// Returns a Promise that resolves with a string containing
/// the contents of the blob, interpreted as UTF-8.
pub fn _text(self: *const Blob, page: *Page) !js.Promise {
return page.js.resolvePromise(self.slice);
}
/// Extension to Blob; works on Firefox and Safari.
/// https://developer.mozilla.org/en-US/docs/Web/API/Blob/bytes
/// Returns a Promise that resolves with a Uint8Array containing
/// the contents of the blob as an array of bytes.
pub fn _bytes(self: *const Blob, page: *Page) !js.Promise {
return page.js.resolvePromise(js.TypedArray(u8){ .values = self.slice });
}
/// Returns a new Blob object which contains data
/// from a subset of the blob on which it's called.
pub fn _slice(
self: *const Blob,
maybe_start: ?i32,
maybe_end: ?i32,
maybe_content_type: ?[]const u8,
page: *Page,
) !Blob {
const mime: []const u8 = blk: {
if (maybe_content_type) |content_type| {
if (content_type.len == 0) {
break :blk "";
}
break :blk try page.arena.dupe(u8, content_type);
}
break :blk "";
};
const slice = self.slice;
if (maybe_start) |_start| {
const start = blk: {
if (_start < 0) {
break :blk slice.len -| @abs(_start);
}
break :blk @min(slice.len, @as(u31, @intCast(_start)));
};
const end: usize = blk: {
if (maybe_end) |_end| {
if (_end < 0) {
break :blk @max(start, slice.len -| @abs(_end));
}
break :blk @min(slice.len, @max(start, @as(u31, @intCast(_end))));
}
break :blk slice.len;
};
return .{ .slice = slice[start..end], .mime = mime };
}
return .{ .slice = slice, .mime = mime };
}
/// Returns the size of the Blob in bytes.
pub fn get_size(self: *const Blob) usize {
return self.slice.len;
}
/// Returns the type of Blob; likely a MIME type, yet anything can be given.
pub fn get_type(self: *const Blob) []const u8 {
return self.mime;
}
const testing = @import("../../testing.zig");
test "Browser: File.Blob" {
try testing.htmlRunner("file/blob.html");
}

View File

@@ -1,7 +0,0 @@
//! File API.
//! https://developer.mozilla.org/en-US/docs/Web/API/File_API
pub const Interfaces = .{
@import("./Blob.zig"),
@import("./File.zig"),
};

View File

@@ -17,9 +17,9 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const EventTarget = @import("../dom/event_target.zig").EventTarget; const EventTarget = @import("../dom/event_target.zig").EventTarget;
@@ -113,12 +113,12 @@ pub const AbortSignal = struct {
} }
const ThrowIfAborted = union(enum) { const ThrowIfAborted = union(enum) {
exception: js.Exception, exception: Env.Exception,
undefined: void, undefined: void,
}; };
pub fn _throwIfAborted(self: *const AbortSignal, page: *Page) ThrowIfAborted { pub fn _throwIfAborted(self: *const AbortSignal, page: *Page) ThrowIfAborted {
if (self.aborted) { if (self.aborted) {
const ex = page.js.throw(self.reason orelse DEFAULT_REASON); const ex = page.main_context.throw(self.reason orelse DEFAULT_REASON);
return .{ .exception = ex }; return .{ .exception = ex };
} }
return .{ .undefined = {} }; return .{ .undefined = {} };

View File

@@ -17,8 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const js = @import("../js/js.zig"); const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
@@ -27,7 +26,7 @@ const DataSet = @This();
element: *parser.Element, element: *parser.Element,
pub fn named_get(self: *const DataSet, name: []const u8, _: *bool, page: *Page) !js.UndefinedOr([]const u8) { 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); const normalized_name = try normalize(page.call_arena, name);
if (try parser.elementGetAttribute(self.element, normalized_name)) |value| { if (try parser.elementGetAttribute(self.element, normalized_name)) |value| {
return .{ .value = value }; return .{ .value = value };

View File

@@ -1,180 +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 js = @import("../js/js.zig");
const Page = @import("../page.zig").Page;
const Window = @import("window.zig").Window;
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
const History = @This();
const ScrollRestorationMode = enum {
pub const ENUM_JS_USE_TAG = true;
auto,
manual,
};
scroll_restoration: ScrollRestorationMode = .auto,
pub fn get_length(_: *History, page: *Page) u32 {
return @intCast(page.session.navigation.entries.items.len);
}
pub fn get_scrollRestoration(self: *History) ScrollRestorationMode {
return self.scroll_restoration;
}
pub fn set_scrollRestoration(self: *History, mode: ScrollRestorationMode) void {
self.scroll_restoration = mode;
}
pub fn get_state(_: *History, page: *Page) !?js.Value {
if (page.session.navigation.currentEntry().state.value) |state| {
const value = try js.Value.fromJson(page.js, state);
return value;
} else {
return null;
}
}
pub fn _pushState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
const arena = page.session.arena;
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
const json = state.toJson(arena) catch return error.DataClone;
_ = try page.session.navigation.pushEntry(url, .{ .source = .history, .value = json }, page, true);
}
pub fn _replaceState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
const arena = page.session.arena;
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
const json = try state.toJson(arena);
_ = try page.session.navigation.replaceEntry(url, .{ .source = .history, .value = json }, page, true);
}
pub fn go(_: *const History, delta: i32, page: *Page) !void {
// 0 behaves the same as no argument, both reloading the page.
const current = page.session.navigation.index;
const index_s: i64 = @intCast(@as(i64, @intCast(current)) + @as(i64, @intCast(delta)));
if (index_s < 0 or index_s > page.session.navigation.entries.items.len - 1) {
return;
}
const index = @as(usize, @intCast(index_s));
const entry = page.session.navigation.entries.items[index];
if (entry.url) |url| {
if (try page.isSameOrigin(url)) {
PopStateEvent.dispatch(entry.state.value, page);
}
}
_ = try page.session.navigation.navigate(entry.url, .{ .traverse = index }, page);
}
pub fn _go(self: *History, _delta: ?i32, page: *Page) !void {
try self.go(_delta orelse 0, page);
}
pub fn _back(self: *History, page: *Page) !void {
try self.go(-1, page);
}
pub fn _forward(self: *History, page: *Page) !void {
try self.go(1, page);
}
const parser = @import("../netsurf.zig");
const Event = @import("../events/event.zig").Event;
pub const PopStateEvent = struct {
pub const prototype = *Event;
pub const union_make_copy = true;
pub const EventInit = struct {
state: ?[]const u8 = null,
};
proto: parser.Event,
state: ?[]const u8,
pub fn constructor(event_type: []const u8, opts: ?EventInit) !PopStateEvent {
const event = try parser.eventCreate();
defer parser.eventDestroy(event);
try parser.eventInit(event, event_type, .{});
parser.eventSetInternalType(event, .pop_state);
const o = opts orelse EventInit{};
return .{
.proto = event.*,
.state = o.state,
};
}
// `hasUAVisualTransition` is not implemented. It isn't baseline so this is okay.
pub fn get_state(self: *const PopStateEvent, page: *Page) !?js.Value {
if (self.state) |state| {
const value = try js.Value.fromJson(page.js, state);
return value;
} else {
return null;
}
}
pub fn dispatch(state: ?[]const u8, page: *Page) void {
log.debug(.script_event, "dispatch popstate event", .{
.type = "popstate",
.source = "history",
});
var evt = PopStateEvent.constructor("popstate", .{ .state = state }) catch |err| {
log.err(.app, "event constructor error", .{
.err = err,
.type = "popstate",
.source = "history",
});
return;
};
_ = parser.eventTargetDispatchEvent(
parser.toEventTarget(Window, &page.window),
&evt.proto,
) catch |err| {
log.err(.app, "dispatch popstate event error", .{
.err = err,
.type = "popstate",
.source = "history",
});
};
}
};
const testing = @import("../../testing.zig");
test "Browser: HTML.History" {
try testing.htmlRunner("html/history/history.html");
try testing.htmlRunner("html/history/history2.html");
}

View File

@@ -42,12 +42,12 @@ pub const HTMLDocument = struct {
// JS funcs // JS funcs
// -------- // --------
pub fn get_domain(self: *parser.DocumentHTML) ![]const u8 { pub fn get_domain(self: *parser.DocumentHTML, page: *Page) ![]const u8 {
// libdom's document_html get_domain always returns null, this is // libdom's document_html get_domain always returns null, this is
// the way MDN recommends getting the domain anyways, since document.domain // the way MDN recommends getting the domain anyways, since document.domain
// is deprecated. // is deprecated.
const location = try parser.documentHTMLGetLocation(Location, self) orelse return ""; const location = try parser.documentHTMLGetLocation(Location, self) orelse return "";
return location.get_host(); return location.get_host(page);
} }
pub fn set_domain(_: *parser.DocumentHTML, _: []const u8) ![]const u8 { pub fn set_domain(_: *parser.DocumentHTML, _: []const u8) ![]const u8 {
@@ -115,69 +115,67 @@ pub const HTMLDocument = struct {
} }
pub fn _getElementsByName(self: *parser.DocumentHTML, name: []const u8, page: *Page) !NodeList { pub fn _getElementsByName(self: *parser.DocumentHTML, name: []const u8, page: *Page) !NodeList {
const arena = page.arena;
var list: NodeList = .{}; var list: NodeList = .{};
if (name.len == 0) { if (name.len == 0) return list;
return list;
}
const root = parser.documentHTMLToNode(self); const root = parser.documentHTMLToNode(self);
var c = try collection.HTMLCollectionByName(root, name, .{ var c = try collection.HTMLCollectionByName(arena, root, name, .{
.include_root = false, .include_root = false,
}); });
const ln = try c.get_length(); const ln = try c.get_length();
try list.ensureTotalCapacity(page.arena, ln);
var i: u32 = 0; var i: u32 = 0;
while (i < ln) : (i += 1) { while (i < ln) {
const n = try c.item(i) orelse break; const n = try c.item(i) orelse break;
list.appendAssumeCapacity(n); try list.append(arena, n);
i += 1;
} }
return list; return list;
} }
pub fn get_images(self: *parser.DocumentHTML) collection.HTMLCollection { pub fn get_images(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
return collection.HTMLCollectionByTagName(parser.documentHTMLToNode(self), "img", .{ return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "img", .{
.include_root = false, .include_root = false,
}); });
} }
pub fn get_embeds(self: *parser.DocumentHTML) collection.HTMLCollection { pub fn get_embeds(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
return collection.HTMLCollectionByTagName(parser.documentHTMLToNode(self), "embed", .{ return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "embed", .{
.include_root = false, .include_root = false,
}); });
} }
pub fn get_plugins(self: *parser.DocumentHTML) collection.HTMLCollection { pub fn get_plugins(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
return get_embeds(self); return get_embeds(self, page);
} }
pub fn get_forms(self: *parser.DocumentHTML) collection.HTMLCollection { pub fn get_forms(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
return collection.HTMLCollectionByTagName(parser.documentHTMLToNode(self), "form", .{ return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "form", .{
.include_root = false, .include_root = false,
}); });
} }
pub fn get_scripts(self: *parser.DocumentHTML) collection.HTMLCollection { pub fn get_scripts(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
return collection.HTMLCollectionByTagName(parser.documentHTMLToNode(self), "script", .{ return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "script", .{
.include_root = false, .include_root = false,
}); });
} }
pub fn get_applets(_: *parser.DocumentHTML) collection.HTMLCollection { pub fn get_applets(_: *parser.DocumentHTML) !collection.HTMLCollection {
return collection.HTMLCollectionEmpty(); return try collection.HTMLCollectionEmpty();
} }
pub fn get_links(self: *parser.DocumentHTML) collection.HTMLCollection { pub fn get_links(self: *parser.DocumentHTML) !collection.HTMLCollection {
return collection.HTMLCollectionByLinks(parser.documentHTMLToNode(self), .{ return try collection.HTMLCollectionByLinks(parser.documentHTMLToNode(self), .{
.include_root = false, .include_root = false,
}); });
} }
pub fn get_anchors(self: *parser.DocumentHTML) collection.HTMLCollection { pub fn get_anchors(self: *parser.DocumentHTML) !collection.HTMLCollection {
return collection.HTMLCollectionByAnchors(parser.documentHTMLToNode(self), .{ return try collection.HTMLCollectionByAnchors(parser.documentHTMLToNode(self), .{
.include_root = false, .include_root = false,
}); });
} }
@@ -195,7 +193,7 @@ pub const HTMLDocument = struct {
} }
pub fn set_location(_: *const parser.DocumentHTML, url: []const u8, page: *Page) !void { pub fn set_location(_: *const parser.DocumentHTML, url: []const u8, page: *Page) !void {
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null }); return page.navigateFromWebAPI(url, .{ .reason = .script });
} }
pub fn get_designMode(_: *parser.DocumentHTML) []const u8 { pub fn get_designMode(_: *parser.DocumentHTML) []const u8 {

View File

@@ -18,9 +18,9 @@
const std = @import("std"); const std = @import("std");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const generate = @import("../js/generate.zig"); const generate = @import("../../runtime/generate.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const urlStitch = @import("../../url.zig").URL.stitch; const urlStitch = @import("../../url.zig").URL.stitch;
@@ -32,10 +32,6 @@ const DataSet = @import("DataSet.zig");
const StyleSheet = @import("../cssom/StyleSheet.zig"); const StyleSheet = @import("../cssom/StyleSheet.zig");
const CSSStyleDeclaration = @import("../cssom/CSSStyleDeclaration.zig"); const CSSStyleDeclaration = @import("../cssom/CSSStyleDeclaration.zig");
const CanvasRenderingContext2D = @import("../canvas/CanvasRenderingContext2D.zig");
const WebGLRenderingContext = @import("../canvas/WebGLRenderingContext.zig");
const WalkerChildren = @import("../dom/walker.zig").WalkerChildren;
// HTMLElement interfaces // HTMLElement interfaces
pub const Interfaces = .{ pub const Interfaces = .{
@@ -137,14 +133,14 @@ pub const HTMLElement = struct {
pub fn get_innerText(e: *parser.ElementHTML) ![]const u8 { pub fn get_innerText(e: *parser.ElementHTML) ![]const u8 {
const n = @as(*parser.Node, @ptrCast(e)); const n = @as(*parser.Node, @ptrCast(e));
return parser.nodeTextContent(n) orelse ""; return try parser.nodeTextContent(n) orelse "";
} }
pub fn set_innerText(e: *parser.ElementHTML, s: []const u8) !void { pub fn set_innerText(e: *parser.ElementHTML, s: []const u8) !void {
const n = @as(*parser.Node, @ptrCast(e)); const n = @as(*parser.Node, @ptrCast(e));
// create text node. // create text node.
const doc = parser.nodeOwnerDocument(n) orelse return error.NoDocument; const doc = try parser.nodeOwnerDocument(n) orelse return error.NoDocument;
const t = try parser.documentCreateTextNode(doc, s); const t = try parser.documentCreateTextNode(doc, s);
// remove existing children. // remove existing children.
@@ -171,12 +167,12 @@ pub const HTMLElement = struct {
focusVisible: bool, focusVisible: bool,
}; };
pub fn _focus(e: *parser.ElementHTML, _: ?FocusOpts, page: *Page) !void { pub fn _focus(e: *parser.ElementHTML, _: ?FocusOpts, page: *Page) !void {
if (!page.isNodeAttached(@ptrCast(e))) { if (!try page.isNodeAttached(@ptrCast(e))) {
return; return;
} }
const Document = @import("../dom/document.zig").Document; const Document = @import("../dom/document.zig").Document;
const root_node = parser.nodeGetRootNode(@ptrCast(e)); const root_node = try parser.nodeGetRootNode(@ptrCast(e));
try Document.setFocus(@ptrCast(root_node), e, page); try Document.setFocus(@ptrCast(root_node), e, page);
} }
}; };
@@ -222,40 +218,40 @@ pub const HTMLAnchorElement = struct {
} }
pub fn get_href(self: *parser.Anchor) ![]const u8 { pub fn get_href(self: *parser.Anchor) ![]const u8 {
return parser.anchorGetHref(self); return try parser.anchorGetHref(self);
} }
pub fn set_href(self: *parser.Anchor, href: []const u8, page: *const Page) !void { pub fn set_href(self: *parser.Anchor, href: []const u8, page: *const Page) !void {
const full = try urlStitch(page.call_arena, href, page.url.raw, .{}); const full = try urlStitch(page.call_arena, href, page.url.raw, .{});
return parser.anchorSetHref(self, full); return try parser.anchorSetHref(self, full);
} }
pub fn get_hreflang(self: *parser.Anchor) ![]const u8 { pub fn get_hreflang(self: *parser.Anchor) ![]const u8 {
return parser.anchorGetHrefLang(self); return try parser.anchorGetHrefLang(self);
} }
pub fn set_hreflang(self: *parser.Anchor, href: []const u8) !void { pub fn set_hreflang(self: *parser.Anchor, href: []const u8) !void {
return parser.anchorSetHrefLang(self, href); return try parser.anchorSetHrefLang(self, href);
} }
pub fn get_type(self: *parser.Anchor) ![]const u8 { pub fn get_type(self: *parser.Anchor) ![]const u8 {
return parser.anchorGetType(self); return try parser.anchorGetType(self);
} }
pub fn set_type(self: *parser.Anchor, t: []const u8) !void { pub fn set_type(self: *parser.Anchor, t: []const u8) !void {
return parser.anchorSetType(self, t); return try parser.anchorSetType(self, t);
} }
pub fn get_rel(self: *parser.Anchor) ![]const u8 { pub fn get_rel(self: *parser.Anchor) ![]const u8 {
return parser.anchorGetRel(self); return try parser.anchorGetRel(self);
} }
pub fn set_rel(self: *parser.Anchor, t: []const u8) !void { pub fn set_rel(self: *parser.Anchor, t: []const u8) !void {
return parser.anchorSetRel(self, t); return try parser.anchorSetRel(self, t);
} }
pub fn get_text(self: *parser.Anchor) !?[]const u8 { pub fn get_text(self: *parser.Anchor) !?[]const u8 {
return parser.nodeTextContent(parser.anchorToNode(self)); return try parser.nodeTextContent(parser.anchorToNode(self));
} }
pub fn set_text(self: *parser.Anchor, v: []const u8) !void { pub fn set_text(self: *parser.Anchor, v: []const u8) !void {
@@ -273,175 +269,182 @@ pub const HTMLAnchorElement = struct {
if (try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "href")) |href| { if (try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "href")) |href| {
return URL.constructor(.{ .string = href }, null, page); // TODO inject base url return URL.constructor(.{ .string = href }, null, page); // TODO inject base url
} }
return error.NotProvided; return .empty;
} }
// TODO return a disposable string
pub fn get_origin(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_origin(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = url(self, page) catch return ""; var u = try url(self, page);
defer u.destructor(); return try u.get_origin(page);
return u.get_origin(page);
} }
// TODO return a disposable string
pub fn get_protocol(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_protocol(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = url(self, page) catch return "";
defer u.destructor();
return page.call_arena.dupe(u8, u.get_protocol());
}
pub fn set_protocol(self: *parser.Anchor, protocol: []const u8, page: *Page) !void {
var u = try url(self, page); var u = try url(self, page);
defer u.destructor(); return u.get_protocol(page);
try u.set_protocol(protocol);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
} }
pub fn set_protocol(self: *parser.Anchor, v: []const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page);
u.uri.scheme = v;
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_host(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_host(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = url(self, page) catch return ""; var u = try url(self, page);
defer u.destructor(); return try u.get_host(page);
return page.call_arena.dupe(u8, u.get_host());
} }
pub fn set_host(self: *parser.Anchor, host: []const u8, page: *Page) !void { pub fn set_host(self: *parser.Anchor, v: []const u8, page: *Page) !void {
var u = try url(self, page); // search : separator
defer u.destructor(); var p: ?u16 = null;
try u.set_host(host); var h: []const u8 = undefined;
for (v, 0..) |c, i| {
if (c == ':') {
h = v[0..i];
p = try std.fmt.parseInt(u16, v[i + 1 ..], 10);
break;
}
}
const href = try u._toString(page); const arena = page.arena;
return parser.anchorSetHref(self, href); var u = try url(self, page);
if (p) |pp| {
u.uri.host = .{ .raw = h };
u.uri.port = pp;
} else {
u.uri.host = .{ .raw = v };
u.uri.port = null;
}
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
} }
pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = url(self, page) catch return "";
defer u.destructor();
return page.call_arena.dupe(u8, u.get_hostname());
}
pub fn set_hostname(self: *parser.Anchor, hostname: []const u8, page: *Page) !void {
var u = try url(self, page); var u = try url(self, page);
defer u.destructor(); return u.get_hostname();
try u.set_hostname(hostname);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
} }
pub fn set_hostname(self: *parser.Anchor, v: []const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page);
u.uri.host = .{ .raw = v };
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = url(self, page) catch return ""; var u = try url(self, page);
defer u.destructor(); return try u.get_port(page);
return page.call_arena.dupe(u8, u.get_port());
} }
pub fn set_port(self: *parser.Anchor, maybe_port: ?[]const u8, page: *Page) !void { pub fn set_port(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page); var u = try url(self, page);
defer u.destructor();
if (maybe_port) |port| { if (v != null and v.?.len > 0) {
try u.set_port(port); u.uri.port = try std.fmt.parseInt(u16, v.?, 10);
} else { } else {
u.clearPort(); u.uri.port = null;
} }
const href = try u._toString(page); const href = try u.toString(arena);
return parser.anchorSetHref(self, href); try parser.anchorSetHref(self, href);
} }
// TODO return a disposable string
pub fn get_username(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_username(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = url(self, page) catch return ""; var u = try url(self, page);
defer u.destructor(); return u.get_username();
}
const username = u.get_username(); pub fn set_username(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
if (username.len == 0) { const arena = page.arena;
return ""; var u = try url(self, page);
if (v) |vv| {
u.uri.user = .{ .raw = vv };
} else {
u.uri.user = null;
} }
const href = try u.toString(arena);
return page.call_arena.dupe(u8, username); try parser.anchorSetHref(self, href);
}
pub fn set_username(self: *parser.Anchor, maybe_username: ?[]const u8, page: *Page) !void {
var u = try url(self, page);
defer u.destructor();
const username = if (maybe_username) |username| username else "";
try u.set_username(username);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
} }
// TODO return a disposable string
pub fn get_password(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_password(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = url(self, page) catch return "";
defer u.destructor();
return page.call_arena.dupe(u8, u.get_password());
}
pub fn set_password(self: *parser.Anchor, maybe_password: ?[]const u8, page: *Page) !void {
var u = try url(self, page); var u = try url(self, page);
defer u.destructor(); return try page.arena.dupe(u8, u.get_password());
const password = if (maybe_password) |password| password else "";
try u.set_password(password);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
} }
pub fn set_password(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page);
if (v) |vv| {
u.uri.password = .{ .raw = vv };
} else {
u.uri.password = null;
}
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
}
// TODO return a disposable string
pub fn get_pathname(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_pathname(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = url(self, page) catch return ""; var u = try url(self, page);
defer u.destructor(); return u.get_pathname();
return page.call_arena.dupe(u8, u.get_pathname());
} }
pub fn set_pathname(self: *parser.Anchor, pathname: []const u8, page: *Page) !void { pub fn set_pathname(self: *parser.Anchor, v: []const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page); var u = try url(self, page);
defer u.destructor(); u.uri.path = .{ .raw = v };
const href = try u.toString(arena);
try u.set_pathname(pathname); try parser.anchorSetHref(self, href);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
} }
pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = url(self, page) catch return ""; var u = try url(self, page);
defer u.destructor(); return try u.get_search(page);
// This allocates in page arena so no need to dupe.
return u.get_search(page);
} }
pub fn set_search(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { pub fn set_search(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
var u = try url(self, page); var u = try url(self, page);
defer u.destructor();
try u.set_search(v, page); try u.set_search(v, page);
const href = try u._toString(page); const href = try u.toString(page.call_arena);
return parser.anchorSetHref(self, href); try parser.anchorSetHref(self, href);
} }
// TODO return a disposable string
pub fn get_hash(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_hash(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = url(self, page) catch return ""; var u = try url(self, page);
defer u.destructor(); return try u.get_hash(page);
return page.call_arena.dupe(u8, u.get_hash());
} }
pub fn set_hash(self: *parser.Anchor, maybe_hash: ?[]const u8, page: *Page) !void { pub fn set_hash(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page); var u = try url(self, page);
defer u.destructor();
if (maybe_hash) |hash| { if (v) |vv| {
try u.set_hash(hash); u.uri.fragment = .{ .raw = vv };
} else { } else {
u.clearHash(); u.uri.fragment = null;
} }
const href = try u.toString(arena);
const href = try u._toString(page); try parser.anchorSetHref(self, href);
return parser.anchorSetHref(self, href);
} }
}; };
@@ -491,29 +494,6 @@ pub const HTMLCanvasElement = struct {
pub const Self = parser.Canvas; pub const Self = parser.Canvas;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const subtype = .node; pub const subtype = .node;
/// This should be a union once we support other context types.
const ContextAttributes = struct {
alpha: bool,
color_space: []const u8 = "srgb",
};
/// Returns a drawing context on the canvas, or null if the context identifier
/// is not supported, or the canvas has already been set to a different context mode.
pub fn _getContext(
ctx_type: []const u8,
_: ?ContextAttributes,
) ?union(enum) { @"2d": CanvasRenderingContext2D, webgl: WebGLRenderingContext } {
if (std.mem.eql(u8, ctx_type, "2d")) {
return .{ .@"2d" = .{} };
}
if (std.mem.eql(u8, ctx_type, "webgl") or std.mem.eql(u8, ctx_type, "experimental-webgl")) {
return .{ .webgl = .{} };
}
return null;
}
}; };
pub const HTMLDListElement = struct { pub const HTMLDListElement = struct {
@@ -752,9 +732,6 @@ pub const HTMLInputElement = struct {
pub fn set_value(self: *parser.Input, value: []const u8) !void { pub fn set_value(self: *parser.Input, value: []const u8) !void {
try parser.inputSetValue(self, value); try parser.inputSetValue(self, value);
} }
pub fn _select(_: *parser.Input) void {
log.debug(.web_api, "not implemented", .{ .feature = "HTMLInputElement select" });
}
}; };
pub const HTMLLIElement = struct { pub const HTMLLIElement = struct {
@@ -780,21 +757,13 @@ pub const HTMLLinkElement = struct {
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const subtype = .node; pub const subtype = .node;
pub fn get_rel(self: *parser.Link) ![]const u8 {
return parser.linkGetRel(self);
}
pub fn set_rel(self: *parser.Link, rel: []const u8) !void {
return parser.linkSetRel(self, rel);
}
pub fn get_href(self: *parser.Link) ![]const u8 { pub fn get_href(self: *parser.Link) ![]const u8 {
return parser.linkGetHref(self); return try parser.linkGetHref(self);
} }
pub fn set_href(self: *parser.Link, href: []const u8, page: *const Page) !void { pub fn set_href(self: *parser.Link, href: []const u8, page: *const Page) !void {
const full = try urlStitch(page.call_arena, href, page.url.raw, .{}); const full = try urlStitch(page.call_arena, href, page.url.raw, .{});
return parser.linkSetHref(self, full); return try parser.linkSetHref(self, full);
} }
}; };
@@ -910,7 +879,7 @@ pub const HTMLScriptElement = struct {
// s.src = '...'; // s.src = '...';
// This should load the script. // This should load the script.
// addFromElement protects against double execution. // addFromElement protects against double execution.
try page.script_manager.addFromElement(@ptrCast(@alignCast(self)), "dynamic"); try page.script_manager.addFromElement(@ptrCast(@alignCast(self)));
} }
} }
@@ -1023,22 +992,22 @@ pub const HTMLScriptElement = struct {
); );
} }
pub fn get_onload(self: *parser.Script, page: *Page) !?js.Function { pub fn get_onload(self: *parser.Script, page: *Page) !?Env.Function {
const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null; const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null;
return state.onload; return state.onload;
} }
pub fn set_onload(self: *parser.Script, function: ?js.Function, page: *Page) !void { pub fn set_onload(self: *parser.Script, function: ?Env.Function, page: *Page) !void {
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self))); const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
state.onload = function; state.onload = function;
} }
pub fn get_onerror(self: *parser.Script, page: *Page) !?js.Function { pub fn get_onerror(self: *parser.Script, page: *Page) !?Env.Function {
const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null; const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null;
return state.onerror; return state.onerror;
} }
pub fn set_onerror(self: *parser.Script, function: ?js.Function, page: *Page) !void { pub fn set_onerror(self: *parser.Script, function: ?Env.Function, page: *Page) !void {
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self))); const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
state.onerror = function; state.onerror = function;
} }
@@ -1073,84 +1042,68 @@ pub const HTMLSlotElement = struct {
flatten: bool = false, flatten: bool = false,
}; };
pub fn _assignedNodes(self: *parser.Slot, opts_: ?AssignedNodesOpts, page: *Page) ![]NodeUnion { pub fn _assignedNodes(self: *parser.Slot, opts_: ?AssignedNodesOpts, page: *Page) ![]NodeUnion {
return findAssignedSlotNodes(self, opts_, false, page);
}
// This should return Union, instead of NodeUnion, but we want to re-use
// findAssignedSlotNodes. Returning NodeUnion is fine, as long as every element
// within is an Element. This could be more efficient
pub fn _assignedElements(self: *parser.Slot, opts_: ?AssignedNodesOpts, page: *Page) ![]NodeUnion {
return findAssignedSlotNodes(self, opts_, true, page);
}
fn findAssignedSlotNodes(self: *parser.Slot, opts_: ?AssignedNodesOpts, element_only: bool, page: *Page) ![]NodeUnion {
const opts = opts_ orelse AssignedNodesOpts{ .flatten = false }; const opts = opts_ orelse AssignedNodesOpts{ .flatten = false };
if (opts.flatten) { if (try findAssignedSlotNodes(self, opts, page)) |nodes| {
log.debug(.web_api, "not implemented", .{ .feature = "HTMLSlotElement flatten assignedNodes" }); return nodes;
}
if (!opts.flatten) {
return &.{};
} }
const node: *parser.Node = @ptrCast(@alignCast(self)); const node: *parser.Node = @ptrCast(@alignCast(self));
const nl = try parser.nodeGetChildNodes(node);
const len = try parser.nodeListLength(nl);
if (len == 0) {
return &.{};
}
// First we look for any explicitly assigned nodes (via the slot attribute) var assigned = try page.call_arena.alloc(NodeUnion, len);
{ var i: usize = 0;
const slot_name = try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "name"); while (true) : (i += 1) {
var root = parser.nodeGetRootNode(node); const child = try parser.nodeListItem(nl, @intCast(i)) orelse break;
if (page.getNodeState(root)) |state| { assigned[i] = try Node.toInterface(child);
if (state.shadow_root) |sr| { }
root = @ptrCast(@alignCast(sr.host)); return assigned[0..i];
} }
fn findAssignedSlotNodes(self: *parser.Slot, opts: AssignedNodesOpts, page: *Page) !?[]NodeUnion {
if (opts.flatten) {
log.warn(.web_api, "not implemented", .{ .feature = "HTMLSlotElement flatten assignedNodes" });
}
const slot_name = try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "name");
const node: *parser.Node = @ptrCast(@alignCast(self));
var root = try parser.nodeGetRootNode(node);
if (page.getNodeState(root)) |state| {
if (state.shadow_root) |sr| {
root = @ptrCast(@alignCast(sr.host));
} }
}
var arr: std.ArrayList(NodeUnion) = .empty; var arr: std.ArrayList(NodeUnion) = .empty;
const w = @import("../dom/walker.zig").WalkerChildren{}; const w = @import("../dom/walker.zig").WalkerChildren{};
var next: ?*parser.Node = null; var next: ?*parser.Node = null;
while (true) { while (true) {
next = try w.get_next(root, next) orelse break; next = try w.get_next(root, next) orelse break;
if (parser.nodeType(next.?) != .element) { if (try parser.nodeType(next.?) != .element) {
if (slot_name == null and !element_only) { if (slot_name == null) {
// default slot (with no name), takes everything // default slot (with no name), takes everything
try arr.append(page.call_arena, try Node.toInterface(next.?));
}
continue;
}
const el: *parser.Element = @ptrCast(@alignCast(next.?));
const element_slot = try parser.elementGetAttribute(el, "slot");
if (nullableStringsAreEqual(slot_name, element_slot)) {
// either they're the same string or they are both null
try arr.append(page.call_arena, try Node.toInterface(next.?)); try arr.append(page.call_arena, try Node.toInterface(next.?));
continue;
} }
continue;
} }
if (arr.items.len > 0) { const el: *parser.Element = @ptrCast(@alignCast(next.?));
return arr.items; const element_slot = try parser.elementGetAttribute(el, "slot");
}
if (!opts.flatten) { if (nullableStringsAreEqual(slot_name, element_slot)) {
return &.{}; // either they're the same string or they are both null
try arr.append(page.call_arena, try Node.toInterface(next.?));
continue;
} }
} }
return if (arr.items.len == 0) null else arr.items;
// Since, we have no explicitly assigned nodes and flatten == false,
// we'll collect the children of the slot - the defaults.
{
const nl = try parser.nodeGetChildNodes(node);
const len = parser.nodeListLength(nl);
if (len == 0) {
return &.{};
}
var assigned = try page.call_arena.alloc(NodeUnion, len);
var i: usize = 0;
while (true) : (i += 1) {
const child = parser.nodeListItem(nl, @intCast(i)) orelse break;
if (!element_only or parser.nodeType(child) == .element) {
assigned[i] = try Node.toInterface(child);
}
}
return assigned[0..i];
}
} }
fn nullableStringsAreEqual(a: ?[]const u8, b: ?[]const u8) bool { fn nullableStringsAreEqual(a: ?[]const u8, b: ?[]const u8) bool {
@@ -1227,22 +1180,11 @@ pub const HTMLTemplateElement = struct {
pub const subtype = .node; pub const subtype = .node;
pub fn get_content(self: *parser.Template, page: *Page) !*parser.DocumentFragment { pub fn get_content(self: *parser.Template, page: *Page) !*parser.DocumentFragment {
const n: *parser.Node = @ptrCast(@alignCast(self)); const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
const state = try page.getOrCreateNodeState(n);
if (state.template_content) |tc| { if (state.template_content) |tc| {
return tc; return tc;
} }
const tc = try parser.documentCreateDocumentFragment(@ptrCast(page.window.document)); const tc = try parser.documentCreateDocumentFragment(@ptrCast(page.window.document));
const ntc: *parser.Node = @ptrCast(@alignCast(tc));
// move existing template's childnodes to the fragment.
const walker = WalkerChildren{};
var next: ?*parser.Node = null;
while (true) {
next = try walker.get_next(n, next) orelse break;
_ = try parser.nodeAppendChild(ntc, next.?);
}
state.template_content = tc; state.template_content = tc;
return tc; return tc;
} }
@@ -1385,16 +1327,41 @@ test "Browser: HTML.HtmlStyleElement" {
test "Browser: HTML.HtmlScriptElement" { test "Browser: HTML.HtmlScriptElement" {
try testing.htmlRunner("html/script/script.html"); try testing.htmlRunner("html/script/script.html");
try testing.htmlRunner("html/script/inline_defer.html"); try testing.htmlRunner("html/script/inline_defer.html");
try testing.htmlRunner("html/script/import.html");
try testing.htmlRunner("html/script/dynamic_import.html");
try testing.htmlRunner("html/script/importmap.html");
try testing.htmlRunner("html/script/order.html");
} }
test "Browser: HTML.HtmlSlotElement" { test "Browser: HTML.HTMLSlotElement" {
try testing.htmlRunner("html/slot.html"); try testing.htmlRunner("html/html_slot_element.html");
} }
test "Browser: HTML.HTMLCanvasElement" { const Check = struct {
try testing.htmlRunner("html/canvas.html"); input: []const u8,
expected: ?[]const u8 = null, // Needed when input != expected
};
const bool_valids = [_]Check{
.{ .input = "true" },
.{ .input = "''", .expected = "false" },
.{ .input = "13.5", .expected = "true" },
};
const str_valids = [_]Check{
.{ .input = "'foo'", .expected = "foo" },
.{ .input = "5", .expected = "5" },
.{ .input = "''", .expected = "" },
.{ .input = "document", .expected = "[object HTMLDocument]" },
};
// .{ "elem.type = '5'", "5" },
// .{ "elem.type", "text" },
fn testProperty(
arena: std.mem.Allocator,
runner: *testing.JsRunner,
elem_dot_prop: []const u8,
always: ?[]const u8, // Ignores checks' expected if set
checks: []const Check,
) !void {
for (checks) |check| {
try runner.testCases(&.{
.{ try std.mem.concat(arena, u8, &.{ elem_dot_prop, " = ", check.input }), null },
.{ elem_dot_prop, always orelse check.expected orelse check.input },
}, .{});
}
} }

View File

@@ -15,7 +15,7 @@
// //
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("../js/js.zig"); const Env = @import("../env.zig").Env;
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
// https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent // https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent
@@ -28,21 +28,21 @@ pub const ErrorEvent = struct {
filename: []const u8, filename: []const u8,
lineno: i32, lineno: i32,
colno: i32, colno: i32,
@"error": ?js.Object, @"error": ?Env.JsObject,
const ErrorEventInit = struct { const ErrorEventInit = struct {
message: []const u8 = "", message: []const u8 = "",
filename: []const u8 = "", filename: []const u8 = "",
lineno: i32 = 0, lineno: i32 = 0,
colno: i32 = 0, colno: i32 = 0,
@"error": ?js.Object = null, @"error": ?Env.JsObject = null,
}; };
pub fn constructor(event_type: []const u8, opts: ?ErrorEventInit) !ErrorEvent { pub fn constructor(event_type: []const u8, opts: ?ErrorEventInit) !ErrorEvent {
const event = try parser.eventCreate(); const event = try parser.eventCreate();
defer parser.eventDestroy(event); defer parser.eventDestroy(event);
try parser.eventInit(event, event_type, .{}); try parser.eventInit(event, event_type, .{});
parser.eventSetInternalType(event, .error_event); parser.eventSetInternalType(event, .event);
const o = opts orelse ErrorEventInit{}; const o = opts orelse ErrorEventInit{};
@@ -72,7 +72,7 @@ pub const ErrorEvent = struct {
return self.colno; return self.colno;
} }
pub fn get_error(self: *const ErrorEvent) js.UndefinedOr(js.Object) { pub fn get_error(self: *const ErrorEvent) Env.UndefinedOr(Env.JsObject) {
if (self.@"error") |e| { if (self.@"error") |e| {
return .{ .value = e }; return .{ .value = e };
} }

View File

@@ -21,6 +21,7 @@ const Allocator = std.mem.Allocator;
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const HTMLElement = @import("elements.zig").HTMLElement; const HTMLElement = @import("elements.zig").HTMLElement;
const FormData = @import("../xhr/form_data.zig").FormData;
pub const HTMLFormElement = struct { pub const HTMLFormElement = struct {
pub const Self = parser.Form; pub const Self = parser.Form;

View File

@@ -0,0 +1,93 @@
// 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");
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
pub const History = struct {
const ScrollRestorationMode = enum {
auto,
manual,
};
scrollRestoration: ScrollRestorationMode = .auto,
state: std.json.Value = .null,
// count tracks the history length until we implement correctly pushstate.
count: u32 = 0,
pub fn get_length(self: *History) u32 {
// TODO return the real history length value.
return self.count;
}
pub fn get_scrollRestoration(self: *History) []const u8 {
return switch (self.scrollRestoration) {
.auto => "auto",
.manual => "manual",
};
}
pub fn set_scrollRestoration(self: *History, mode: []const u8) void {
if (std.mem.eql(u8, "manual", mode)) self.scrollRestoration = .manual;
if (std.mem.eql(u8, "auto", mode)) self.scrollRestoration = .auto;
}
pub fn get_state(self: *History) std.json.Value {
return self.state;
}
// TODO implement the function
// data must handle any argument. We could expect a std.json.Value but
// https://github.com/lightpanda-io/zig-js-runtime/issues/267 is missing.
pub fn _pushState(self: *History, data: []const u8, _: ?[]const u8, url: ?[]const u8) void {
self.count += 1;
_ = url;
_ = data;
}
// TODO implement the function
// data must handle any argument. We could expect a std.json.Value but
// https://github.com/lightpanda-io/zig-js-runtime/issues/267 is missing.
pub fn _replaceState(self: *History, data: []const u8, _: ?[]const u8, url: ?[]const u8) void {
_ = self;
_ = url;
_ = data;
}
// TODO implement the function
pub fn _go(self: *History, delta: ?i32) void {
_ = self;
_ = delta;
}
// TODO implement the function
pub fn _back(self: *History) void {
_ = self;
}
// TODO implement the function
pub fn _forward(self: *History) void {
_ = self;
}
};
const testing = @import("../../testing.zig");
test "Browser: HTML.History" {
try testing.htmlRunner("html/history.html");
}

View File

@@ -21,7 +21,7 @@ const HTMLElem = @import("elements.zig");
const SVGElem = @import("svg_elements.zig"); const SVGElem = @import("svg_elements.zig");
const Window = @import("window.zig").Window; const Window = @import("window.zig").Window;
const Navigator = @import("navigator.zig").Navigator; const Navigator = @import("navigator.zig").Navigator;
const History = @import("History.zig"); const History = @import("history.zig").History;
const Location = @import("location.zig").Location; const Location = @import("location.zig").Location;
const MediaQueryList = @import("media_query_list.zig").MediaQueryList; const MediaQueryList = @import("media_query_list.zig").MediaQueryList;

View File

@@ -16,8 +16,10 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator;
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page;
const HTMLElement = @import("elements.zig").HTMLElement; const HTMLElement = @import("elements.zig").HTMLElement;
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#htmliframeelement // https://html.spec.whatwg.org/multipage/iframe-embed-object.html#htmliframeelement

View File

@@ -16,95 +16,73 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const URL = @import("../url/url.zig").URL; const URL = @import("../url/url.zig").URL;
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-location-interface // https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-location-interface
pub const Location = struct { pub const Location = struct {
url: URL, url: ?URL = null,
/// Initializes the `Location` to be used in `Window`.
/// Browsers give such initial values when user not navigated yet:
/// Chrome -> chrome://new-tab-page/
/// Firefox -> about:newtab
/// Safari -> favorites://
pub fn init(url: []const u8) !Location {
return .{ .url = try .initForLocation(url) };
}
pub fn get_href(self: *Location, page: *Page) ![]const u8 { pub fn get_href(self: *Location, page: *Page) ![]const u8 {
return self.url.get_href(page); if (self.url) |*u| return u.get_href(page);
return "";
} }
pub fn set_href(_: *const Location, href: []const u8, page: *Page) !void { pub fn get_protocol(self: *Location, page: *Page) ![]const u8 {
return page.navigateFromWebAPI(href, .{ .reason = .script }, .{ .push = null }); if (self.url) |*u| return u.get_protocol(page);
return "";
} }
pub fn set_hash(_: *const Location, hash: []const u8, page: *Page) !void { pub fn get_host(self: *Location, page: *Page) ![]const u8 {
const normalized_hash = blk: { if (self.url) |*u| return u.get_host(page);
if (hash.len == 0) { return "";
const old_url = page.url.raw;
break :blk if (std.mem.indexOfScalar(u8, old_url, '#')) |index|
old_url[0..index]
else
old_url;
} else if (hash[0] == '#')
break :blk hash
else
break :blk try std.fmt.allocPrint(page.arena, "#{s}", .{hash});
};
return page.navigateFromWebAPI(normalized_hash, .{ .reason = .script }, .{ .replace = null });
}
pub fn get_protocol(self: *Location) []const u8 {
return self.url.get_protocol();
}
pub fn get_host(self: *Location) []const u8 {
return self.url.get_host();
} }
pub fn get_hostname(self: *Location) []const u8 { pub fn get_hostname(self: *Location) []const u8 {
return self.url.get_hostname(); if (self.url) |*u| return u.get_hostname();
return "";
} }
pub fn get_port(self: *Location) []const u8 { pub fn get_port(self: *Location, page: *Page) ![]const u8 {
return self.url.get_port(); if (self.url) |*u| return u.get_port(page);
return "";
} }
pub fn get_pathname(self: *Location) []const u8 { pub fn get_pathname(self: *Location) []const u8 {
return self.url.get_pathname(); if (self.url) |*u| return u.get_pathname();
return "";
} }
pub fn get_search(self: *Location, page: *Page) ![]const u8 { pub fn get_search(self: *Location, page: *Page) ![]const u8 {
return self.url.get_search(page); if (self.url) |*u| return u.get_search(page);
return "";
} }
pub fn get_hash(self: *Location) []const u8 { pub fn get_hash(self: *Location, page: *Page) ![]const u8 {
return self.url.get_hash(); if (self.url) |*u| return u.get_hash(page);
return "";
} }
pub fn get_origin(self: *Location, page: *Page) ![]const u8 { pub fn get_origin(self: *Location, page: *Page) ![]const u8 {
return self.url.get_origin(page); if (self.url) |*u| return u.get_origin(page);
return "";
} }
pub fn _assign(_: *const Location, url: []const u8, page: *Page) !void { pub fn _assign(_: *const Location, url: []const u8, page: *Page) !void {
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null }); return page.navigateFromWebAPI(url, .{ .reason = .script });
} }
pub fn _replace(_: *const Location, url: []const u8, page: *Page) !void { pub fn _replace(_: *const Location, url: []const u8, page: *Page) !void {
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .replace = null }); return page.navigateFromWebAPI(url, .{ .reason = .script });
} }
pub fn _reload(_: *const Location, page: *Page) !void { pub fn _reload(_: *const Location, page: *Page) !void {
return page.navigateFromWebAPI(page.url.raw, .{ .reason = .script }, .reload); return page.navigateFromWebAPI(page.url.raw, .{ .reason = .script });
} }
pub fn _toString(self: *Location, page: *Page) ![]const u8 { pub fn _toString(self: *Location, page: *Page) ![]const u8 {
return self.get_href(page); return try self.get_href(page);
} }
}; };

View File

@@ -16,8 +16,8 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Function = @import("../env.zig").Function;
const EventTarget = @import("../dom/event_target.zig").EventTarget; const EventTarget = @import("../dom/event_target.zig").EventTarget;
// https://drafts.csswg.org/cssom-view/#the-mediaquerylist-interface // https://drafts.csswg.org/cssom-view/#the-mediaquerylist-interface
@@ -39,7 +39,7 @@ pub const MediaQueryList = struct {
return self.media; return self.media;
} }
pub fn _addListener(_: *const MediaQueryList, _: js.Function) void {} pub fn _addListener(_: *const MediaQueryList, _: Function) void {}
pub fn _removeListener(_: *const MediaQueryList, _: js.Function) void {} pub fn _removeListener(_: *const MediaQueryList, _: Function) void {}
}; };

View File

@@ -25,7 +25,7 @@ pub const SVGElement = struct {
// Currently the prototype chain is not implemented (will not be returned by toInterface()) // Currently the prototype chain is not implemented (will not be returned by toInterface())
// For that we need parser.SvgElement and the derived types with tags in the v-table. // For that we need parser.SvgElement and the derived types with tags in the v-table.
pub const prototype = *Element; pub const prototype = *Element;
// While this is a Node, could consider not exposing the subtype until we have // While this is a Node, could consider not exposing the subtype untill we have
// a Self type to cast to. // a Self type to cast to.
pub const subtype = .node; pub const subtype = .node;
}; };

View File

@@ -18,14 +18,13 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const Navigator = @import("navigator.zig").Navigator; const Navigator = @import("navigator.zig").Navigator;
const History = @import("History.zig"); const History = @import("history.zig").History;
const Navigation = @import("../navigation/Navigation.zig");
const Location = @import("location.zig").Location; const Location = @import("location.zig").Location;
const Crypto = @import("../crypto/crypto.zig").Crypto; const Crypto = @import("../crypto/crypto.zig").Crypto;
const Console = @import("../console/console.zig").Console; const Console = @import("../console/console.zig").Console;
@@ -36,15 +35,15 @@ const CSSStyleDeclaration = @import("../cssom/CSSStyleDeclaration.zig");
const Screen = @import("screen.zig").Screen; const Screen = @import("screen.zig").Screen;
const domcss = @import("../dom/css.zig"); const domcss = @import("../dom/css.zig");
const Css = @import("../css/css.zig").Css; const Css = @import("../css/css.zig").Css;
const EventHandler = @import("../events/event.zig").EventHandler;
const Function = Env.Function;
const JsObject = Env.JsObject;
const v8 = @import("v8");
const Request = @import("../fetch/Request.zig"); const Request = @import("../fetch/Request.zig");
const fetchFn = @import("../fetch/fetch.zig").fetch; const fetchFn = @import("../fetch/fetch.zig").fetch;
const storage = @import("../storage/storage.zig"); const storage = @import("../storage/storage.zig");
const ErrorEvent = @import("error_event.zig").ErrorEvent;
const DirectEventHandler = @import("../events/event.zig").DirectEventHandler;
// https://dom.spec.whatwg.org/#interface-window-extensions // https://dom.spec.whatwg.org/#interface-window-extensions
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window // https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
@@ -56,7 +55,8 @@ pub const Window = struct {
document: *parser.DocumentHTML, document: *parser.DocumentHTML,
target: []const u8 = "", target: []const u8 = "",
location: Location, history: History = .{},
location: Location = .{},
storage_shelf: ?*storage.Shelf = null, storage_shelf: ?*storage.Shelf = null,
// counter for having unique timer ids // counter for having unique timer ids
@@ -69,10 +69,6 @@ pub const Window = struct {
performance: Performance, performance: Performance,
screen: Screen = .{}, screen: Screen = .{},
css: Css = .{}, css: Css = .{},
scroll_x: u32 = 0,
scroll_y: u32 = 0,
onload_callback: ?js.Function = null,
onpopstate_callback: ?js.Function = null,
pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window { pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window {
var fbs = std.io.fixedBufferStream(""); var fbs = std.io.fixedBufferStream("");
@@ -83,7 +79,6 @@ pub const Window = struct {
return .{ return .{
.document = html_doc, .document = html_doc,
.target = target orelse "", .target = target orelse "",
.location = try .init("about:blank"),
.navigator = navigator orelse .{}, .navigator = navigator orelse .{},
.performance = Performance.init(), .performance = Performance.init(),
}; };
@@ -94,10 +89,6 @@ pub const Window = struct {
try parser.documentHTMLSetLocation(Location, self.document, &self.location); try parser.documentHTMLSetLocation(Location, self.document, &self.location);
} }
pub fn changeLocation(self: *Window, new_url: []const u8, page: *Page) !void {
return self.location.url.reinit(new_url, page);
}
pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) !void { pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) !void {
self.performance.reset(); // When to reset see: https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin self.performance.reset(); // When to reset see: https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin
self.document = doc; self.document = doc;
@@ -108,28 +99,16 @@ pub const Window = struct {
self.storage_shelf = shelf; self.storage_shelf = shelf;
} }
pub fn _fetch(_: *Window, input: Request.RequestInput, options: ?Request.RequestInit, page: *Page) !js.Promise { pub fn _fetch(_: *Window, input: Request.RequestInput, options: ?Request.RequestInit, page: *Page) !Env.Promise {
return fetchFn(input, options, page); return fetchFn(input, options, page);
} }
/// Returns `onload_callback`. pub fn get_window(self: *Window) *Window {
pub fn get_onload(self: *const Window) ?js.Function { return self;
return self.onload_callback;
} }
/// Sets `onload_callback`. pub fn get_navigator(self: *Window) *Navigator {
pub fn set_onload(self: *Window, maybe_listener: ?EventHandler.Listener, page: *Page) !void { return &self.navigator;
try DirectEventHandler(Window, self, "load", maybe_listener, &self.onload_callback, page.arena);
}
/// Returns `onpopstate_callback`.
pub fn get_onpopstate(self: *const Window) ?js.Function {
return self.onpopstate_callback;
}
/// Sets `onpopstate_callback`.
pub fn set_onpopstate(self: *Window, maybe_listener: ?EventHandler.Listener, page: *Page) !void {
try DirectEventHandler(Window, self, "popstate", maybe_listener, &self.onpopstate_callback, page.arena);
} }
pub fn get_location(self: *Window) *Location { pub fn get_location(self: *Window) *Location {
@@ -137,7 +116,23 @@ pub const Window = struct {
} }
pub fn set_location(_: *const Window, url: []const u8, page: *Page) !void { pub fn set_location(_: *const Window, url: []const u8, page: *Page) !void {
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null }); return page.navigateFromWebAPI(url, .{ .reason = .script });
}
pub fn get_console(self: *Window) *Console {
return &self.console;
}
pub fn get_crypto(self: *Window) *Crypto {
return &self.crypto;
}
pub fn get_self(self: *Window) *Window {
return self;
}
pub fn get_parent(self: *Window) *Window {
return self;
} }
// frames return the window itself, but accessing it via a pseudo // frames return the window itself, but accessing it via a pseudo
@@ -177,16 +172,16 @@ pub const Window = struct {
return frames.get_length(); return frames.get_length();
} }
pub fn get_top(self: *Window) *Window {
return self;
}
pub fn get_document(self: *Window) ?*parser.DocumentHTML { pub fn get_document(self: *Window) ?*parser.DocumentHTML {
return self.document; return self.document;
} }
pub fn get_history(_: *Window, page: *Page) *History { pub fn get_history(self: *Window) *History {
return &page.session.history; return &self.history;
}
pub fn get_navigation(_: *Window, page: *Page) *Navigation {
return &page.session.navigation;
} }
// The interior height of the window in pixels, including the height of the horizontal scroll bar, if present. // The interior height of the window in pixels, including the height of the horizontal scroll bar, if present.
@@ -215,11 +210,19 @@ pub const Window = struct {
return &self.storage_shelf.?.bucket.session; return &self.storage_shelf.?.bucket.session;
} }
pub fn get_performance(self: *Window) *Performance {
return &self.performance;
}
pub fn get_screen(self: *Window) *Screen {
return &self.screen;
}
pub fn get_CSS(self: *Window) *Css { pub fn get_CSS(self: *Window) *Css {
return &self.css; return &self.css;
} }
pub fn _requestAnimationFrame(self: *Window, cbk: js.Function, page: *Page) !u32 { pub fn _requestAnimationFrame(self: *Window, cbk: Function, page: *Page) !u32 {
return self.createTimeout(cbk, 5, page, .{ return self.createTimeout(cbk, 5, page, .{
.animation_frame = true, .animation_frame = true,
.name = "animationFrame", .name = "animationFrame",
@@ -231,11 +234,11 @@ pub const Window = struct {
_ = self.timers.remove(id); _ = self.timers.remove(id);
} }
pub fn _setTimeout(self: *Window, cbk: js.Function, delay: ?u32, params: []js.Object, page: *Page) !u32 { 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" }); return self.createTimeout(cbk, delay, page, .{ .args = params, .name = "setTimeout" });
} }
pub fn _setInterval(self: *Window, cbk: js.Function, delay: ?u32, params: []js.Object, page: *Page) !u32 { 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" }); return self.createTimeout(cbk, delay, page, .{ .repeat = true, .args = params, .name = "setInterval" });
} }
@@ -247,22 +250,14 @@ pub const Window = struct {
_ = self.timers.remove(id); _ = self.timers.remove(id);
} }
pub fn _queueMicrotask(self: *Window, cbk: js.Function, page: *Page) !void { pub fn _queueMicrotask(self: *Window, cbk: Function, page: *Page) !u32 {
_ = try self.createTimeout(cbk, 0, page, .{ .name = "queueMicrotask" }); return self.createTimeout(cbk, 0, page, .{ .name = "queueMicrotask" });
} }
pub fn _setImmediate(self: *Window, cbk: js.Function, page: *Page) !u32 { pub fn _matchMedia(_: *const Window, media: []const u8, page: *Page) !MediaQueryList {
return self.createTimeout(cbk, 0, page, .{ .name = "setImmediate" });
}
pub fn _clearImmediate(self: *Window, id: u32) void {
_ = self.timers.remove(id);
}
pub fn _matchMedia(_: *const Window, media: js.String) !MediaQueryList {
return .{ return .{
.matches = false, // TODO? .matches = false, // TODO?
.media = media.string, .media = try page.arena.dupe(u8, media),
}; };
} }
@@ -281,33 +276,14 @@ pub const Window = struct {
return out; return out;
} }
pub fn _reportError(self: *Window, err: js.Object, page: *Page) !void {
var error_event = try ErrorEvent.constructor("error", .{
.@"error" = err,
});
_ = try parser.eventTargetDispatchEvent(
parser.toEventTarget(Window, self),
@as(*parser.Event, &error_event.proto),
);
if (parser.eventDefaultPrevented(&error_event.proto) == false) {
const err_string = err.toString() catch "Unknown error";
log.info(.user_script, "error", .{
.err = err_string,
.stack = page.stackTrace() catch "???",
.source = "window.reportError",
});
}
}
const CreateTimeoutOpts = struct { const CreateTimeoutOpts = struct {
name: []const u8, name: []const u8,
args: []js.Object = &.{}, args: []Env.JsObject = &.{},
repeat: bool = false, repeat: bool = false,
animation_frame: bool = false, animation_frame: bool = false,
low_priority: bool = false, low_priority: bool = false,
}; };
fn createTimeout(self: *Window, cbk: js.Function, delay_: ?u32, page: *Page, opts: CreateTimeoutOpts) !u32 { fn createTimeout(self: *Window, cbk: Function, delay_: ?u32, page: *Page, opts: CreateTimeoutOpts) !u32 {
const delay = delay_ orelse 0; const delay = delay_ orelse 0;
if (self.timers.count() > 512) { if (self.timers.count() > 512) {
return error.TooManyTimeout; return error.TooManyTimeout;
@@ -327,9 +303,9 @@ pub const Window = struct {
errdefer _ = self.timers.remove(timer_id); errdefer _ = self.timers.remove(timer_id);
const args = opts.args; const args = opts.args;
var persisted_args: []js.Object = &.{}; var persisted_args: []Env.JsObject = &.{};
if (args.len > 0) { if (args.len > 0) {
persisted_args = try page.arena.alloc(js.Object, args.len); persisted_args = try page.arena.alloc(Env.JsObject, args.len);
for (args, persisted_args) |a, *ca| { for (args, persisted_args) |a, *ca| {
ca.* = try a.persist(); ca.* = try a.persist();
} }
@@ -370,20 +346,12 @@ pub const Window = struct {
const Opts = struct { const Opts = struct {
top: i32, top: i32,
left: i32, left: i32,
behavior: []const u8 = "", behavior: []const u8,
}; };
}; };
pub fn _scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32) !void { pub fn _scrollTo(self: *Window, opts: ScrollToOpts, y: ?u32) !void {
switch (opts) { _ = opts;
.x => |x| { _ = y;
self.scroll_x = @intCast(@max(x, 0));
self.scroll_y = @intCast(@max(0, y orelse 0));
},
.opts => |o| {
self.scroll_y = @intCast(@max(0, o.top));
self.scroll_x = @intCast(@max(0, o.left));
},
}
{ {
const scroll_event = try parser.eventCreate(); const scroll_event = try parser.eventCreate();
@@ -407,28 +375,6 @@ pub const Window = struct {
); );
} }
} }
pub fn _scroll(self: *Window, opts: ScrollToOpts, y: ?i32) !void {
// just an alias for scrollTo
return self._scrollTo(opts, y);
}
pub fn get_scrollX(self: *const Window) u32 {
return self.scroll_x;
}
pub fn get_scrollY(self: *const Window) u32 {
return self.scroll_y;
}
pub fn get_pageXOffset(self: *const Window) u32 {
// just an alias for scrollX
return self.get_scrollX();
}
pub fn get_pageYOffset(self: *const Window) u32 {
// just an alias for scrollY
return self.get_scrollY();
}
// libdom's document doesn't have a parent, which is correct, but // libdom's document doesn't have a parent, which is correct, but
// breaks the event bubbling that happens for many events from // breaks the event bubbling that happens for many events from
@@ -446,18 +392,6 @@ pub const Window = struct {
// and thus the target has already been set to the document. // and thus the target has already been set to the document.
return self.base.redispatchEvent(evt); return self.base.redispatchEvent(evt);
} }
pub fn postAttach(self: *Window, js_this: js.This) !void {
try js_this.set("top", self, .{});
try js_this.set("self", self, .{});
try js_this.set("parent", self, .{});
try js_this.set("window", self, .{});
try js_this.set("crypto", &self.crypto, .{});
try js_this.set("screen", &self.screen, .{});
try js_this.set("console", &self.console, .{});
try js_this.set("navigator", &self.navigator, .{});
try js_this.set("performance", &self.performance, .{});
}
}; };
const TimerCallback = struct { const TimerCallback = struct {
@@ -468,13 +402,13 @@ const TimerCallback = struct {
repeat: ?u32, repeat: ?u32,
// The JavaScript callback to execute // The JavaScript callback to execute
cbk: js.Function, cbk: Function,
animation_frame: bool = false, animation_frame: bool = false,
window: *Window, window: *Window,
args: []js.Object = &.{}, args: []Env.JsObject = &.{},
fn run(ctx: *anyopaque) ?u32 { fn run(ctx: *anyopaque) ?u32 {
const self: *TimerCallback = @ptrCast(@alignCast(ctx)); const self: *TimerCallback = @ptrCast(@alignCast(ctx));
@@ -488,7 +422,7 @@ const TimerCallback = struct {
return null; return null;
} }
var result: js.Function.Result = undefined; var result: Function.Result = undefined;
var call: anyerror!void = undefined; var call: anyerror!void = undefined;
if (self.animation_frame) { if (self.animation_frame) {

View File

@@ -1,561 +0,0 @@
const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const log = @import("../../log.zig");
const Page = @import("../page.zig").Page;
const types = @import("types.zig");
const Context = @import("Context.zig");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const CALL_ARENA_RETAIN = 1024 * 16;
// Responsible for calling Zig functions from JS invocations. This could
// probably just contained in ExecutionWorld, but having this specific logic, which
// is somewhat repetitive between constructors, functions, getters, etc contained
// here does feel like it makes it cleaner.
const Caller = @This();
context: *Context,
v8_context: v8.Context,
isolate: v8.Isolate,
call_arena: Allocator,
// info is a v8.PropertyCallbackInfo or a v8.FunctionCallback
// All we really want from it is the isolate.
// executor = Isolate -> getCurrentContext -> getEmbedderData()
pub fn init(info: anytype) Caller {
const isolate = info.getIsolate();
const v8_context = isolate.getCurrentContext();
const context: *Context = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64());
context.call_depth += 1;
return .{
.context = context,
.isolate = isolate,
.v8_context = v8_context,
.call_arena = context.call_arena,
};
}
pub fn deinit(self: *Caller) void {
const context = self.context;
const call_depth = context.call_depth - 1;
// Because of callbacks, calls can be nested. Because of this, we
// can't clear the call_arena after _every_ call. Imagine we have
// arr.forEach((i) => { console.log(i); }
//
// First we call forEach. Inside of our forEach call,
// we call console.log. If we reset the call_arena after this call,
// it'll reset it for the `forEach` call after, which might still
// need the data.
//
// Therefore, we keep a call_depth, and only reset the call_arena
// when a top-level (call_depth == 0) function ends.
if (call_depth == 0) {
const arena: *ArenaAllocator = @ptrCast(@alignCast(context.call_arena.ptr));
_ = arena.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN });
}
// Set this _after_ we've executed the above code, so that if the
// above code executes any callbacks, they aren't being executed
// at scope 0, which would be wrong.
context.call_depth = call_depth;
}
pub fn constructor(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void {
const args = try self.getArgs(Struct, named_function, 0, info);
const res = @call(.auto, Struct.constructor, args);
const ReturnType = @typeInfo(@TypeOf(Struct.constructor)).@"fn".return_type orelse {
@compileError(@typeName(Struct) ++ " has a constructor without a return type");
};
const this = info.getThis();
if (@typeInfo(ReturnType) == .error_union) {
const non_error_res = res catch |err| return err;
_ = try self.context.mapZigInstanceToJs(this, non_error_res);
} else {
_ = try self.context.mapZigInstanceToJs(this, res);
}
info.getReturnValue().set(this);
}
pub fn method(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void {
if (comptime isSelfReceiver(Struct, named_function) == false) {
return self.function(Struct, named_function, info);
}
const context = self.context;
const func = @field(Struct, named_function.name);
var args = try self.getArgs(Struct, named_function, 1, info);
const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
// inject 'self' as the first parameter
@field(args, "0") = zig_instance;
const res = @call(.auto, func, args);
info.getReturnValue().set(try context.zigValueToJs(res));
}
pub fn function(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void {
const context = self.context;
const func = @field(Struct, named_function.name);
const args = try self.getArgs(Struct, named_function, 0, info);
const res = @call(.auto, func, args);
info.getReturnValue().set(try context.zigValueToJs(res));
}
pub fn getIndex(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, idx: u32, info: v8.PropertyCallbackInfo) !u8 {
const context = self.context;
const func = @field(Struct, named_function.name);
const IndexedGet = @TypeOf(func);
if (@typeInfo(IndexedGet).@"fn".return_type == null) {
@compileError(named_function.full_name ++ " must have a return type");
}
var has_value = true;
var args: ParamterTypes(IndexedGet) = undefined;
const arg_fields = @typeInfo(@TypeOf(args)).@"struct".fields;
switch (arg_fields.len) {
0, 1, 2 => @compileError(named_function.full_name ++ " must take at least a u32 and *bool parameter"),
3, 4 => {
const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
comptime assertSelfReceiver(Struct, named_function);
@field(args, "0") = zig_instance;
@field(args, "1") = idx;
@field(args, "2") = &has_value;
if (comptime arg_fields.len == 4) {
comptime assertIsPageArg(Struct, named_function, 3);
@field(args, "3") = context.page;
}
},
else => @compileError(named_function.full_name ++ " has too many parmaters"),
}
const res = @call(.auto, func, args);
if (has_value == false) {
return v8.Intercepted.No;
}
info.getReturnValue().set(try context.zigValueToJs(res));
return v8.Intercepted.Yes;
}
pub fn getNamedIndex(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, info: v8.PropertyCallbackInfo) !u8 {
const context = self.context;
const func = @field(Struct, named_function.name);
comptime assertSelfReceiver(Struct, named_function);
var has_value = true;
var args = try self.getArgs(Struct, named_function, 3, info);
const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
@field(args, "0") = zig_instance;
@field(args, "1") = try self.nameToString(name);
@field(args, "2") = &has_value;
const res = @call(.auto, func, args);
if (has_value == false) {
return v8.Intercepted.No;
}
info.getReturnValue().set(try self.context.zigValueToJs(res));
return v8.Intercepted.Yes;
}
pub fn setNamedIndex(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, js_value: v8.Value, info: v8.PropertyCallbackInfo) !u8 {
const context = self.context;
const func = @field(Struct, named_function.name);
comptime assertSelfReceiver(Struct, named_function);
var has_value = true;
var args = try self.getArgs(Struct, named_function, 4, info);
const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
@field(args, "0") = zig_instance;
@field(args, "1") = try self.nameToString(name);
@field(args, "2") = try context.jsValueToZig(named_function, @TypeOf(@field(args, "2")), js_value);
@field(args, "3") = &has_value;
const res = @call(.auto, func, args);
return namedSetOrDeleteCall(res, has_value);
}
pub fn deleteNamedIndex(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, info: v8.PropertyCallbackInfo) !u8 {
const context = self.context;
const func = @field(Struct, named_function.name);
comptime assertSelfReceiver(Struct, named_function);
var has_value = true;
var args = try self.getArgs(Struct, named_function, 3, info);
const zig_instance = try context.typeTaggedAnyOpaque(named_function, *types.Receiver(Struct), info.getThis());
@field(args, "0") = zig_instance;
@field(args, "1") = try self.nameToString(name);
@field(args, "2") = &has_value;
const res = @call(.auto, func, args);
return namedSetOrDeleteCall(res, has_value);
}
fn namedSetOrDeleteCall(res: anytype, has_value: bool) !u8 {
if (@typeInfo(@TypeOf(res)) == .error_union) {
_ = try res;
}
if (has_value == false) {
return v8.Intercepted.No;
}
return v8.Intercepted.Yes;
}
fn nameToString(self: *Caller, name: v8.Name) ![]const u8 {
return self.context.valueToString(.{ .handle = name.handle }, .{});
}
fn isSelfReceiver(comptime Struct: type, comptime named_function: NamedFunction) bool {
return checkSelfReceiver(Struct, named_function, false);
}
fn assertSelfReceiver(comptime Struct: type, comptime named_function: NamedFunction) void {
_ = checkSelfReceiver(Struct, named_function, true);
}
fn checkSelfReceiver(comptime Struct: type, comptime named_function: NamedFunction, comptime fail: bool) bool {
const func = @field(Struct, named_function.name);
const params = @typeInfo(@TypeOf(func)).@"fn".params;
if (params.len == 0) {
if (fail) {
@compileError(named_function.full_name ++ " must have a self parameter");
}
return false;
}
const R = types.Receiver(Struct);
const first_param = params[0].type.?;
if (first_param != *R and first_param != *const R) {
if (fail) {
@compileError(std.fmt.comptimePrint("The first parameter to {s} must be a *{s} or *const {s}. Got: {s}", .{
named_function.full_name,
@typeName(R),
@typeName(R),
@typeName(first_param),
}));
}
return false;
}
return true;
}
fn assertIsPageArg(comptime Struct: type, comptime named_function: NamedFunction, index: comptime_int) void {
const F = @TypeOf(@field(Struct, named_function.name));
const param = @typeInfo(F).@"fn".params[index].type.?;
if (isPage(param)) {
return;
}
@compileError(std.fmt.comptimePrint("The {d} parameter to {s} must be a *Page or *const Page. Got: {s}", .{ index, named_function.full_name, @typeName(param) }));
}
pub fn handleError(self: *Caller, comptime Struct: type, comptime named_function: NamedFunction, err: anyerror, info: anytype) void {
const isolate = self.isolate;
if (comptime @import("builtin").mode == .Debug and @hasDecl(@TypeOf(info), "length")) {
if (log.enabled(.js, .warn)) {
self.logFunctionCallError(err, named_function.full_name, info);
}
}
var js_err: ?v8.Value = switch (err) {
error.InvalidArgument => createTypeException(isolate, "invalid argument"),
error.OutOfMemory => js._createException(isolate, "out of memory"),
error.IllegalConstructor => js._createException(isolate, "Illegal Contructor"),
else => blk: {
const func = @field(Struct, named_function.name);
const return_type = @typeInfo(@TypeOf(func)).@"fn".return_type orelse {
// void return type;
break :blk null;
};
if (@typeInfo(return_type) != .error_union) {
// type defines a custom exception, but this function should
// not fail. We failed somewhere inside of js.zig and
// should return the error as-is, since it isn't related
// to our Struct
break :blk null;
}
const function_error_set = @typeInfo(return_type).error_union.error_set;
const E = comptime getCustomException(Struct) orelse break :blk null;
if (function_error_set == E or isErrorSetException(E, err)) {
const custom_exception = E.init(self.call_arena, err, named_function.js_name) catch |init_err| {
switch (init_err) {
// if a custom exceptions' init wants to return a
// different error, we need to think about how to
// handle that failure.
error.OutOfMemory => break :blk js._createException(isolate, "out of memory"),
}
};
// ughh..how to handle an error here?
break :blk self.context.zigValueToJs(custom_exception) catch js._createException(isolate, "internal error");
}
// this error isn't part of a custom exception
break :blk null;
},
};
if (js_err == null) {
js_err = js._createException(isolate, @errorName(err));
}
const js_exception = isolate.throwException(js_err.?);
info.getReturnValue().setValueHandle(js_exception.handle);
}
// walk the prototype chain to see if a type declares a custom Exception
fn getCustomException(comptime Struct: type) ?type {
var S = Struct;
while (true) {
if (@hasDecl(S, "Exception")) {
return S.Exception;
}
if (@hasDecl(S, "prototype") == false) {
return null;
}
// long ago, we validated that every prototype declaration
// is a pointer.
S = @typeInfo(S.prototype).pointer.child;
}
}
// Does the error we want to return belong to the custom exeception's ErrorSet
fn isErrorSetException(comptime E: type, err: anytype) bool {
const Entry = std.meta.Tuple(&.{ []const u8, void });
const error_set = @typeInfo(E.ErrorSet).error_set.?;
const entries = comptime blk: {
var kv: [error_set.len]Entry = undefined;
for (error_set, 0..) |e, i| {
kv[i] = .{ e.name, {} };
}
break :blk kv;
};
const lookup = std.StaticStringMap(void).initComptime(entries);
return lookup.has(@errorName(err));
}
// If we call a method in javascript: cat.lives('nine');
//
// Then we'd expect a Zig function with 2 parameters: a self and the string.
// In this case, offset == 1. Offset is always 1 for setters or methods.
//
// Offset is always 0 for constructors.
//
// For constructors, setters and methods, we can further increase offset + 1
// if the first parameter is an instance of Page.
//
// Finally, if the JS function is called with _more_ parameters and
// the last parameter in Zig is an array, we'll try to slurp the additional
// parameters into the array.
fn getArgs(self: *const Caller, comptime Struct: type, comptime named_function: NamedFunction, comptime offset: usize, info: anytype) !ParamterTypes(@TypeOf(@field(Struct, named_function.name))) {
const context = self.context;
const F = @TypeOf(@field(Struct, named_function.name));
var args: ParamterTypes(F) = undefined;
const params = @typeInfo(F).@"fn".params[offset..];
// Except for the constructor, the first parameter is always `self`
// This isn't something we'll bind from JS, so skip it.
const params_to_map = blk: {
if (params.len == 0) {
return args;
}
// If the last parameter is the Page, set it, and exclude it
// from our params slice, because we don't want to bind it to
// a JS argument
if (comptime isPage(params[params.len - 1].type.?)) {
@field(args, tupleFieldName(params.len - 1 + offset)) = self.context.page;
break :blk params[0 .. params.len - 1];
}
// If the last parameter is a special JsThis, set it, and exclude it
// from our params slice, because we don't want to bind it to
// a JS argument
if (comptime params[params.len - 1].type.? == js.This) {
@field(args, tupleFieldName(params.len - 1 + offset)) = .{ .obj = .{
.context = context,
.js_obj = info.getThis(),
} };
// AND the 2nd last parameter is state
if (params.len > 1 and comptime isPage(params[params.len - 2].type.?)) {
@field(args, tupleFieldName(params.len - 2 + offset)) = self.context.page;
break :blk params[0 .. params.len - 2];
}
break :blk params[0 .. params.len - 1];
}
// we have neither a Page nor a JsObject. All params must be
// bound to a JavaScript value.
break :blk params;
};
if (params_to_map.len == 0) {
return args;
}
const js_parameter_count = info.length();
const last_js_parameter = params_to_map.len - 1;
var is_variadic = false;
{
// This is going to get complicated. If the last Zig parameter
// is a slice AND the corresponding javascript parameter is
// NOT an an array, then we'll treat it as a variadic.
const last_parameter_type = params_to_map[params_to_map.len - 1].type.?;
const last_parameter_type_info = @typeInfo(last_parameter_type);
if (last_parameter_type_info == .pointer and last_parameter_type_info.pointer.size == .slice) {
const slice_type = last_parameter_type_info.pointer.child;
const corresponding_js_value = info.getArg(@as(u32, @intCast(last_js_parameter)));
if (corresponding_js_value.isArray() == false and corresponding_js_value.isTypedArray() == false and slice_type != u8) {
is_variadic = true;
if (js_parameter_count == 0) {
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
} else if (js_parameter_count >= params_to_map.len) {
const arr = try self.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);
for (arr, last_js_parameter..) |*a, i| {
const js_value = info.getArg(@as(u32, @intCast(i)));
a.* = try context.jsValueToZig(named_function, slice_type, js_value);
}
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = arr;
} else {
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
}
}
}
}
inline for (params_to_map, 0..) |param, i| {
const field_index = comptime i + offset;
if (comptime i == params_to_map.len - 1) {
if (is_variadic) {
break;
}
}
if (comptime isPage(param.type.?)) {
@compileError("Page must be the last parameter (or 2nd last if there's a JsThis): " ++ named_function.full_name);
} else if (comptime param.type.? == js.This) {
@compileError("JsThis must be the last parameter: " ++ named_function.full_name);
} else if (i >= js_parameter_count) {
if (@typeInfo(param.type.?) != .optional) {
return error.InvalidArgument;
}
@field(args, tupleFieldName(field_index)) = null;
} else {
const js_value = info.getArg(@as(u32, @intCast(i)));
@field(args, tupleFieldName(field_index)) = context.jsValueToZig(named_function, param.type.?, js_value) catch {
return error.InvalidArgument;
};
}
}
return args;
}
// This is extracted to speed up compilation. When left inlined in handleError,
// this can add as much as 10 seconds of compilation time.
fn logFunctionCallError(self: *Caller, err: anyerror, function_name: []const u8, info: v8.FunctionCallbackInfo) void {
const args_dump = self.serializeFunctionArgs(info) catch "failed to serialize args";
log.info(.js, "function call error", .{
.name = function_name,
.err = err,
.args = args_dump,
.stack = self.context.stackTrace() catch |err1| @errorName(err1),
});
}
fn serializeFunctionArgs(self: *Caller, info: v8.FunctionCallbackInfo) ![]const u8 {
const separator = log.separator();
const js_parameter_count = info.length();
const context = self.context;
var arr: std.ArrayListUnmanaged(u8) = .{};
for (0..js_parameter_count) |i| {
const js_value = info.getArg(@intCast(i));
const value_string = try context.valueToDetailString(js_value);
const value_type = try context.jsStringToZig(try js_value.typeOf(self.isolate), .{});
try std.fmt.format(arr.writer(context.call_arena), "{s}{d}: {s} ({s})", .{
separator,
i + 1,
value_string,
value_type,
});
}
return arr.items;
}
// We want the function name, or more precisely, the "Struct.function" for
// displaying helpful @compileError.
// However, there's no way to get the name from a std.Builtin.Fn, so we create
// a NamedFunction as part of our binding, and pass it around incase we need
// to display an error
pub const NamedFunction = struct {
name: []const u8,
js_name: []const u8,
full_name: []const u8,
pub fn init(comptime Struct: type, comptime name: []const u8) NamedFunction {
return .{
.name = name,
.js_name = if (name[0] == '_') name[1..] else name,
.full_name = @typeName(Struct) ++ "." ++ name,
};
}
};
// Takes a function, and returns a tuple for its argument. Used when we
// @call a function
fn ParamterTypes(comptime F: type) type {
const params = @typeInfo(F).@"fn".params;
var fields: [params.len]std.builtin.Type.StructField = undefined;
inline for (params, 0..) |param, i| {
fields[i] = .{
.name = tupleFieldName(i),
.type = param.type.?,
.default_value_ptr = null,
.is_comptime = false,
.alignment = @alignOf(param.type.?),
};
}
return @Type(.{ .@"struct" = .{
.layout = .auto,
.decls = &.{},
.fields = &fields,
.is_tuple = true,
} });
}
fn tupleFieldName(comptime i: usize) [:0]const u8 {
return switch (i) {
0 => "0",
1 => "1",
2 => "2",
3 => "3",
4 => "4",
5 => "5",
6 => "6",
7 => "7",
8 => "8",
9 => "9",
else => std.fmt.comptimePrint("{d}", .{i}),
};
}
fn isPage(comptime T: type) bool {
return T == *Page or T == *const Page;
}
fn createTypeException(isolate: v8.Isolate, msg: []const u8) v8.Value {
return v8.Exception.initTypeError(v8.String.initUtf8(isolate, msg));
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,537 +0,0 @@
const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const log = @import("../../log.zig");
const types = @import("types.zig");
const Types = types.Types;
const Caller = @import("Caller.zig");
const Context = @import("Context.zig");
const Platform = @import("Platform.zig");
const Inspector = @import("Inspector.zig");
const ExecutionWorld = @import("ExecutionWorld.zig");
const NamedFunction = Caller.NamedFunction;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
// The Env maps to a V8 isolate, which represents a isolated sandbox for
// executing JavaScript. The Env is where we'll define our V8 <-> Zig bindings,
// and it's where we'll start ExecutionWorlds, which actually execute JavaScript.
// The `S` parameter is arbitrary state. When we start an ExecutionWorld, an instance
// of S must be given. This instance is available to any Zig binding.
// The `types` parameter is a tuple of Zig structures we want to bind to V8.
const Env = @This();
allocator: Allocator,
platform: *const Platform,
// the global isolate
isolate: v8.Isolate,
// just kept around because we need to free it on deinit
isolate_params: *v8.CreateParams,
// Given a type, we can lookup its index in TYPE_LOOKUP and then have
// access to its TunctionTemplate (the thing we need to create an instance
// of it)
// I.e.:
// const index = @field(TYPE_LOOKUP, @typeName(type_name))
// const template = templates[index];
templates: [Types.len]v8.FunctionTemplate,
// Given a type index (retrieved via the TYPE_LOOKUP), we can retrieve
// the index of its prototype. Types without a prototype have their own
// index.
prototype_lookup: [Types.len]u16,
meta_lookup: [Types.len]types.Meta,
context_id: usize,
const Opts = struct {};
pub fn init(allocator: Allocator, platform: *const Platform, _: Opts) !*Env {
// var params = v8.initCreateParams();
var params = try allocator.create(v8.CreateParams);
errdefer allocator.destroy(params);
v8.c.v8__Isolate__CreateParams__CONSTRUCT(params);
params.array_buffer_allocator = v8.createDefaultArrayBufferAllocator();
errdefer v8.destroyArrayBufferAllocator(params.array_buffer_allocator.?);
var isolate = v8.Isolate.init(params);
errdefer isolate.deinit();
// This is the callback that runs whenever a module is dynamically imported.
isolate.setHostImportModuleDynamicallyCallback(Context.dynamicModuleCallback);
isolate.setPromiseRejectCallback(promiseRejectCallback);
isolate.setMicrotasksPolicy(v8.c.kExplicit);
isolate.enter();
errdefer isolate.exit();
isolate.setHostInitializeImportMetaObjectCallback(Context.metaObjectCallback);
var temp_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&temp_scope, isolate);
defer temp_scope.deinit();
const env = try allocator.create(Env);
errdefer allocator.destroy(env);
env.* = .{
.context_id = 0,
.platform = platform,
.isolate = isolate,
.templates = undefined,
.allocator = allocator,
.isolate_params = params,
.meta_lookup = undefined,
.prototype_lookup = undefined,
};
// Populate our templates lookup. generateClass creates the
// v8.FunctionTemplate, which we store in our env.templates.
// The ordering doesn't matter. What matters is that, given a type
// we can get its index via: @field(types.LOOKUP, type_name)
const templates = &env.templates;
inline for (Types, 0..) |s, i| {
@setEvalBranchQuota(10_000);
templates[i] = v8.Persistent(v8.FunctionTemplate).init(isolate, generateClass(s.defaultValue().?, isolate)).castToFunctionTemplate();
}
// Above, we've created all our our FunctionTemplates. Now that we
// have them all, we can hook up the prototypes.
const meta_lookup = &env.meta_lookup;
inline for (Types, 0..) |s, i| {
const Struct = s.defaultValue().?;
if (@hasDecl(Struct, "prototype")) {
const TI = @typeInfo(Struct.prototype);
const ProtoType = types.Receiver(TI.pointer.child);
if (!types.has(ProtoType)) {
@compileError(std.fmt.comptimePrint("Prototype '{s}' for '{s}' is undefined", .{ @typeName(ProtoType), @typeName(Struct) }));
}
// Hey, look! This is our first real usage of the `types.Index`.
// Just like we said above, given a type, we can get its
// template index.
templates[i].inherit(templates[types.getId(ProtoType)]);
}
// while we're here, let's populate our meta lookup
const subtype: ?types.Sub = if (@hasDecl(Struct, "subtype")) Struct.subtype else null;
const proto_offset = comptime blk: {
if (!@hasField(Struct, "proto")) {
break :blk 0;
}
const proto_info = std.meta.fieldInfo(Struct, .proto);
if (@typeInfo(proto_info.type) == .pointer) {
// we store the offset as a negative, to so that,
// when we reverse this, we know that it's
// behind a pointer that we need to resolve.
break :blk -@offsetOf(Struct, "proto");
}
break :blk @offsetOf(Struct, "proto");
};
meta_lookup[i] = .{
.index = i,
.subtype = subtype,
.proto_offset = proto_offset,
};
}
return env;
}
pub fn deinit(self: *Env) void {
self.isolate.exit();
self.isolate.deinit();
v8.destroyArrayBufferAllocator(self.isolate_params.array_buffer_allocator.?);
self.allocator.destroy(self.isolate_params);
self.allocator.destroy(self);
}
pub fn newInspector(self: *Env, arena: Allocator, ctx: anytype) !Inspector {
return Inspector.init(arena, self.isolate, ctx);
}
pub fn runMicrotasks(self: *const Env) void {
self.isolate.performMicrotasksCheckpoint();
}
pub fn pumpMessageLoop(self: *const Env) bool {
return self.platform.inner.pumpMessageLoop(self.isolate, false);
}
pub fn runIdleTasks(self: *const Env) void {
return self.platform.inner.runIdleTasks(self.isolate, 1);
}
pub fn newExecutionWorld(self: *Env) !ExecutionWorld {
return .{
.env = self,
.context = null,
.context_arena = ArenaAllocator.init(self.allocator),
};
}
// V8 doesn't immediately free memory associated with
// a Context, it's managed by the garbage collector. We use the
// `lowMemoryNotification` call on the isolate to encourage v8 to free
// any contexts which have been freed.
pub fn lowMemoryNotification(self: *Env) void {
var handle_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&handle_scope, self.isolate);
defer handle_scope.deinit();
self.isolate.lowMemoryNotification();
}
pub fn dumpMemoryStats(self: *Env) void {
const stats = self.isolate.getHeapStatistics();
std.debug.print(
\\ Total Heap Size: {d}
\\ Total Heap Size Executable: {d}
\\ Total Physical Size: {d}
\\ Total Available Size: {d}
\\ Used Heap Size: {d}
\\ Heap Size Limit: {d}
\\ Malloced Memory: {d}
\\ External Memory: {d}
\\ Peak Malloced Memory: {d}
\\ Number Of Native Contexts: {d}
\\ Number Of Detached Contexts: {d}
\\ Total Global Handles Size: {d}
\\ Used Global Handles Size: {d}
\\ Zap Garbage: {any}
\\
, .{ stats.total_heap_size, stats.total_heap_size_executable, stats.total_physical_size, stats.total_available_size, stats.used_heap_size, stats.heap_size_limit, stats.malloced_memory, stats.external_memory, stats.peak_malloced_memory, stats.number_of_native_contexts, stats.number_of_detached_contexts, stats.total_global_handles_size, stats.used_global_handles_size, stats.does_zap_garbage });
}
fn promiseRejectCallback(v8_msg: v8.C_PromiseRejectMessage) callconv(.c) void {
const msg = v8.PromiseRejectMessage.initFromC(v8_msg);
const isolate = msg.getPromise().toObject().getIsolate();
const context = Context.fromIsolate(isolate);
const value =
if (msg.getValue()) |v8_value| context.valueToString(v8_value, .{}) catch |err| @errorName(err) else "no value";
log.debug(.js, "unhandled rejection", .{ .value = value });
}
// Give it a Zig struct, get back a v8.FunctionTemplate.
// The FunctionTemplate is a bit like a struct container - it's where
// we'll attach functions/getters/setters and where we'll "inherit" a
// prototype type (if there is any)
fn generateClass(comptime Struct: type, isolate: v8.Isolate) v8.FunctionTemplate {
const template = generateConstructor(Struct, isolate);
attachClass(Struct, isolate, template);
return template;
}
// Normally this is called from generateClass. Where generateClass creates
// the constructor (hence, the FunctionTemplate), attachClass adds all
// of its functions, getters, setters, ...
// But it's extracted from generateClass because we also have 1 global
// object (i.e. the Window), which gets attached not only to the Window
// constructor/FunctionTemplate as normal, but also through the default
// FunctionTemplate of the isolate (in createContext)
pub fn attachClass(comptime Struct: type, isolate: v8.Isolate, template: v8.FunctionTemplate) void {
const template_proto = template.getPrototypeTemplate();
inline for (@typeInfo(Struct).@"struct".decls) |declaration| {
const name = declaration.name;
if (comptime name[0] == '_') {
switch (@typeInfo(@TypeOf(@field(Struct, name)))) {
.@"fn" => generateMethod(Struct, name, isolate, template_proto),
else => |ti| if (!comptime js.isComplexAttributeType(ti)) {
generateAttribute(Struct, name, isolate, template, template_proto);
},
}
} else if (comptime std.mem.startsWith(u8, name, "get_")) {
generateProperty(Struct, name[4..], isolate, template_proto);
} else if (comptime std.mem.startsWith(u8, name, "static_")) {
generateFunction(Struct, name[7..], isolate, template);
}
}
if (@hasDecl(Struct, "get_symbol_toStringTag") == false) {
// If this WAS defined, then we would have created it in generateProperty.
// But if it isn't, we create a default one
const string_tag_callback = v8.FunctionTemplate.initCallback(isolate, struct {
fn stringTag(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
const class_name = v8.String.initUtf8(info.getIsolate(), comptime js.classNameForStruct(Struct));
info.getReturnValue().set(class_name);
}
}.stringTag);
const key = v8.Symbol.getToStringTag(isolate).toName();
template_proto.setAccessorGetter(key, string_tag_callback);
}
generateIndexer(Struct, template_proto);
generateNamedIndexer(Struct, template.getInstanceTemplate());
generateUndetectable(Struct, template.getInstanceTemplate());
}
// Even if a struct doesn't have a `constructor` function, we still
// `generateConstructor`, because this is how we create our
// FunctionTemplate. Such classes exist, but they can't be instantiated
// via `new ClassName()` - but they could, for example, be created in
// Zig and returned from a function call, which is why we need the
// FunctionTemplate.
fn generateConstructor(comptime Struct: type, isolate: v8.Isolate) v8.FunctionTemplate {
const template = v8.FunctionTemplate.initCallback(isolate, struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
// See comment above. We generateConstructor on all types
// in order to create the FunctionTemplate, but there might
// not be an actual "constructor" function. So if someone
// does `new ClassName()` where ClassName doesn't have
// a constructor function, we'll return an error.
if (@hasDecl(Struct, "constructor") == false) {
const iso = caller.isolate;
log.warn(.js, "Illegal constructor call", .{ .name = @typeName(Struct) });
const js_exception = iso.throwException(js._createException(iso, "Illegal Constructor"));
info.getReturnValue().set(js_exception);
return;
}
// Safe to call now, because if Struct.constructor didn't
// exist, the above if block would have returned.
const named_function = comptime NamedFunction.init(Struct, "constructor");
caller.constructor(Struct, named_function, info) catch |err| {
caller.handleError(Struct, named_function, err, info);
};
}
}.callback);
if (comptime types.isEmpty(types.Receiver(Struct)) == false) {
// If the struct is empty, we won't store a Zig reference inside
// the JS object, so we don't need to set the internal field count
template.getInstanceTemplate().setInternalFieldCount(1);
}
const class_name = v8.String.initUtf8(isolate, comptime js.classNameForStruct(Struct));
template.setClassName(class_name);
return template;
}
fn generateMethod(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template_proto: v8.ObjectTemplate) void {
var js_name: v8.Name = undefined;
if (comptime std.mem.eql(u8, name, "_symbol_iterator")) {
js_name = v8.Symbol.getIterator(isolate).toName();
} else {
js_name = v8.String.initUtf8(isolate, name[1..]).toName();
}
const function_template = v8.FunctionTemplate.initCallback(isolate, struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, name);
caller.method(Struct, named_function, info) catch |err| {
caller.handleError(Struct, named_function, err, info);
};
}
}.callback);
template_proto.set(js_name, function_template, v8.PropertyAttribute.None);
}
fn generateFunction(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template: v8.FunctionTemplate) void {
const js_name = v8.String.initUtf8(isolate, name).toName();
const function_template = v8.FunctionTemplate.initCallback(isolate, struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "static_" ++ name);
caller.function(Struct, named_function, info) catch |err| {
caller.handleError(Struct, named_function, err, info);
};
}
}.callback);
template.set(js_name, function_template, v8.PropertyAttribute.None);
}
fn generateAttribute(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template: v8.FunctionTemplate, template_proto: v8.ObjectTemplate) void {
const zig_value = @field(Struct, name);
const js_value = js.simpleZigValueToJs(isolate, zig_value, true);
const js_name = v8.String.initUtf8(isolate, name[1..]).toName();
// apply it both to the type itself
template.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
// and to instances of the type
template_proto.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
}
fn generateProperty(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template_proto: v8.ObjectTemplate) void {
var js_name: v8.Name = undefined;
if (comptime std.mem.eql(u8, name, "symbol_toStringTag")) {
js_name = v8.Symbol.getToStringTag(isolate).toName();
} else {
js_name = v8.String.initUtf8(isolate, name).toName();
}
const getter_callback = v8.FunctionTemplate.initCallback(isolate, struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "get_" ++ name);
caller.method(Struct, named_function, info) catch |err| {
caller.handleError(Struct, named_function, err, info);
};
}
}.callback);
const setter_name = "set_" ++ name;
if (@hasDecl(Struct, setter_name) == false) {
template_proto.setAccessorGetter(js_name, getter_callback);
return;
}
const setter_callback = v8.FunctionTemplate.initCallback(isolate, struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
std.debug.assert(info.length() == 1);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "set_" ++ name);
caller.method(Struct, named_function, info) catch |err| {
caller.handleError(Struct, named_function, err, info);
};
}
}.callback);
template_proto.setAccessorGetterAndSetter(js_name, getter_callback, setter_callback);
}
fn generateIndexer(comptime Struct: type, template_proto: v8.ObjectTemplate) void {
if (@hasDecl(Struct, "indexed_get") == false) {
return;
}
const configuration = v8.IndexedPropertyHandlerConfiguration{
.getter = struct {
fn callback(idx: u32, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "indexed_get");
return caller.getIndex(Struct, named_function, idx, info) catch |err| blk: {
caller.handleError(Struct, named_function, err, info);
break :blk v8.Intercepted.No;
};
}
}.callback,
};
// If you're trying to implement setter, read:
// https://groups.google.com/g/v8-users/c/8tahYBsHpgY/m/IteS7Wn2AAAJ
// The issue I had was
// (a) where to attache it: does it go on the instance_template
// instead of the prototype?
// (b) defining the getter or query to respond with the
// PropertyAttribute to indicate if the property can be set
template_proto.setIndexedProperty(configuration, null);
}
fn generateNamedIndexer(comptime Struct: type, template_proto: v8.ObjectTemplate) void {
if (@hasDecl(Struct, "named_get") == false) {
return;
}
var configuration = v8.NamedPropertyHandlerConfiguration{
.getter = struct {
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "named_get");
return caller.getNamedIndex(Struct, named_function, .{ .handle = c_name.? }, info) catch |err| blk: {
caller.handleError(Struct, named_function, err, info);
break :blk v8.Intercepted.No;
};
}
}.callback,
// This is really cool. Without this, we'd intercept _all_ properties
// even those explicitly set. So, node.length for example would get routed
// to our `named_get`, rather than a `get_length`. This might be
// useful if we run into a type that we can't model properly in Zig.
.flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking,
};
if (@hasDecl(Struct, "named_set")) {
configuration.setter = struct {
fn callback(c_name: ?*const v8.C_Name, c_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "named_set");
return caller.setNamedIndex(Struct, named_function, .{ .handle = c_name.? }, .{ .handle = c_value.? }, info) catch |err| blk: {
caller.handleError(Struct, named_function, err, info);
break :blk v8.Intercepted.No;
};
}
}.callback;
}
if (@hasDecl(Struct, "named_delete")) {
configuration.deleter = struct {
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "named_delete");
return caller.deleteNamedIndex(Struct, named_function, .{ .handle = c_name.? }, info) catch |err| blk: {
caller.handleError(Struct, named_function, err, info);
break :blk v8.Intercepted.No;
};
}
}.callback;
}
template_proto.setNamedProperty(configuration, null);
}
fn generateUndetectable(comptime Struct: type, template: v8.ObjectTemplate) void {
const has_js_call_as_function = @hasDecl(Struct, "jsCallAsFunction");
if (has_js_call_as_function) {
template.setCallAsFunctionHandler(struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "jsCallAsFunction");
caller.method(Struct, named_function, info) catch |err| {
caller.handleError(Struct, named_function, err, info);
};
}
}.callback);
}
if (@hasDecl(Struct, "mark_as_undetectable") and Struct.mark_as_undetectable) {
if (!has_js_call_as_function) {
@compileError(@typeName(Struct) ++ ": mark_as_undetectable required jsCallAsFunction to be defined. This is a hard-coded requirement in V8, because mark_as_undetectable only exists for HTMLAllCollection which is also callable.");
}
template.markAsUndetectable();
}
}

View File

@@ -1,247 +0,0 @@
const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const log = @import("../../log.zig");
const Page = @import("../page.zig").Page;
const ScriptManager = @import("../ScriptManager.zig");
const types = @import("types.zig");
const Types = types.Types;
const Env = @import("Env.zig");
const Context = @import("Context.zig");
const ArenaAllocator = std.heap.ArenaAllocator;
const CONTEXT_ARENA_RETAIN = 1024 * 64;
// ExecutionWorld closely models a JS World.
// https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/core/v8/V8BindingDesign.md#World
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld
const ExecutionWorld = @This();
env: *Env,
// Arena whose lifetime is for a single page load. Where
// the call_arena lives for a single function call, the context_arena
// lives for the lifetime of the entire page. The allocator will be
// owned by the Context, but the arena itself is owned by the ExecutionWorld
// so that we can re-use it from context to context.
context_arena: ArenaAllocator,
// Currently a context maps to a Browser's Page. Here though, it's only a
// mechanism to organization page-specific memory. The ExecutionWorld
// does all the work, but having all page-specific data structures
// grouped together helps keep things clean.
context: ?Context = null,
// no init, must be initialized via env.newExecutionWorld()
pub fn deinit(self: *ExecutionWorld) void {
if (self.context != null) {
self.removeContext();
}
self.context_arena.deinit();
}
// Only the top Context in the Main ExecutionWorld should hold a handle_scope.
// A v8.HandleScope is like an arena. Once created, any "Local" that
// v8 creates will be released (or at least, releasable by the v8 GC)
// when the handle_scope is freed.
// We also maintain our own "context_arena" which allows us to have
// all page related memory easily managed.
pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool, global_callback: ?js.GlobalMissingCallback) !*Context {
std.debug.assert(self.context == null);
const env = self.env;
const isolate = env.isolate;
const Global = @TypeOf(page.window);
const templates = &self.env.templates;
var v8_context: v8.Context = blk: {
var temp_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&temp_scope, isolate);
defer temp_scope.deinit();
const js_global = v8.FunctionTemplate.initDefault(isolate);
Env.attachClass(Global, isolate, js_global);
const global_template = js_global.getInstanceTemplate();
global_template.setInternalFieldCount(1);
// Configure the missing property callback on the global
// object.
if (global_callback != null) {
const configuration = v8.NamedPropertyHandlerConfiguration{
.getter = struct {
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
const context = Context.fromIsolate(info.getIsolate());
const property = context.valueToString(.{ .handle = c_name.? }, .{}) catch "???";
if (context.global_callback.?.missing(property, context)) {
return v8.Intercepted.Yes;
}
return v8.Intercepted.No;
}
}.callback,
.flags = v8.PropertyHandlerFlags.NonMasking | v8.PropertyHandlerFlags.OnlyInterceptStrings,
};
global_template.setNamedProperty(configuration, null);
}
// All the FunctionTemplates that we created and setup in Env.init
// are now going to get associated with our global instance.
inline for (Types, 0..) |s, i| {
const Struct = s.defaultValue().?;
const class_name = v8.String.initUtf8(isolate, comptime js.classNameForStruct(Struct));
global_template.set(class_name.toName(), templates[i], v8.PropertyAttribute.None);
}
// The global object (Window) has already been hooked into the v8
// engine when the Env was initialized - like every other type.
// But the V8 global is its own FunctionTemplate instance so even
// though it's also a Window, we need to set the prototype for this
// specific instance of the the Window.
if (@hasDecl(Global, "prototype")) {
const ProtoType = types.Receiver(@typeInfo(Global.prototype).pointer.child);
js_global.inherit(templates[types.getId(ProtoType)]);
}
const context_local = v8.Context.init(isolate, global_template, null);
const v8_context = v8.Persistent(v8.Context).init(isolate, context_local).castToContext();
v8_context.enter();
errdefer if (enter) v8_context.exit();
defer if (!enter) v8_context.exit();
// This shouldn't be necessary, but it is:
// https://groups.google.com/g/v8-users/c/qAQQBmbi--8
// TODO: see if newer V8 engines have a way around this.
inline for (Types, 0..) |s, i| {
const Struct = s.defaultValue().?;
if (@hasDecl(Struct, "prototype")) {
const ProtoType = types.Receiver(@typeInfo(Struct.prototype).pointer.child);
if (!types.has(ProtoType)) {
@compileError("Type '" ++ @typeName(Struct) ++ "' defines an unknown prototype: " ++ @typeName(ProtoType));
}
const proto_obj = templates[types.getId(ProtoType)].getFunction(v8_context).toObject();
const self_obj = templates[i].getFunction(v8_context).toObject();
_ = self_obj.setPrototype(v8_context, proto_obj);
}
}
break :blk v8_context;
};
// For a Page we only create one HandleScope, it is stored in the main World (enter==true). A page can have multple contexts, 1 for each World.
// The main Context that enters and holds the HandleScope should therefore always be created first. Following other worlds for this page
// like isolated Worlds, will thereby place their objects on the main page's HandleScope. Note: In the furure the number of context will multiply multiple frames support
var handle_scope: ?v8.HandleScope = null;
if (enter) {
handle_scope = @as(v8.HandleScope, undefined);
v8.HandleScope.init(&handle_scope.?, isolate);
}
errdefer if (enter) handle_scope.?.deinit();
{
// If we want to overwrite the built-in console, we have to
// delete the built-in one.
const js_obj = v8_context.getGlobal();
const console_key = v8.String.initUtf8(isolate, "console");
if (js_obj.deleteValue(v8_context, console_key) == false) {
return error.ConsoleDeleteError;
}
}
const context_id = env.context_id;
env.context_id = context_id + 1;
self.context = Context{
.page = page,
.id = context_id,
.isolate = isolate,
.v8_context = v8_context,
.templates = &env.templates,
.meta_lookup = &env.meta_lookup,
.handle_scope = handle_scope,
.script_manager = &page.script_manager,
.call_arena = page.call_arena,
.arena = self.context_arena.allocator(),
.global_callback = global_callback,
};
var context = &self.context.?;
{
// Store a pointer to our context inside the v8 context so that, given
// a v8 context, we can get our context out
const data = isolate.initBigIntU64(@intCast(@intFromPtr(context)));
v8_context.setEmbedderData(1, data);
}
// Custom exception
// NOTE: there is no way in v8 to subclass the Error built-in type
// TODO: this is an horrible hack
inline for (Types) |s| {
const Struct = s.defaultValue().?;
if (@hasDecl(Struct, "ErrorSet")) {
const script = comptime js.classNameForStruct(Struct) ++ ".prototype.__proto__ = Error.prototype";
_ = try context.exec(script, "errorSubclass");
}
}
// Primitive attributes are set directly on the FunctionTemplate
// when we setup the environment. But we cannot set more complex
// types (v8 will crash).
//
// Plus, just to create more complex types, we always need a
// context, i.e. an Array has to have a Context to exist.
//
// As far as I can tell, getting the FunctionTemplate's object
// and setting values directly on it, for each context, is the
// way to do this.
inline for (Types, 0..) |s, i| {
const Struct = s.defaultValue().?;
inline for (@typeInfo(Struct).@"struct".decls) |declaration| {
const name = declaration.name;
if (comptime name[0] == '_') {
const value = @field(Struct, name);
if (comptime js.isComplexAttributeType(@typeInfo(@TypeOf(value)))) {
const js_obj = templates[i].getFunction(v8_context).toObject();
const js_name = v8.String.initUtf8(isolate, name[1..]).toName();
const js_val = try context.zigValueToJs(value);
if (!js_obj.setValue(v8_context, js_name, js_val)) {
log.fatal(.app, "set class attribute", .{
.@"struct" = @typeName(Struct),
.name = name,
});
}
}
}
}
}
try context.setupGlobal();
return context;
}
pub fn removeContext(self: *ExecutionWorld) void {
// Force running the micro task to drain the queue before reseting the
// context arena.
// Tasks in the queue are relying to the arena memory could be present in
// the queue. Running them later could lead to invalid memory accesses.
self.env.runMicrotasks();
self.context.?.deinit();
self.context = null;
_ = self.context_arena.reset(.{ .retain_with_limit = CONTEXT_ARENA_RETAIN });
}
pub fn terminateExecution(self: *const ExecutionWorld) void {
self.env.isolate.terminateExecution();
}
pub fn resumeExecution(self: *const ExecutionWorld) void {
self.env.isolate.cancelTerminateExecution();
}

View File

@@ -1,144 +0,0 @@
const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const Caller = @import("Caller.zig");
const Context = @import("Context.zig");
const PersistentFunction = v8.Persistent(v8.Function);
const Allocator = std.mem.Allocator;
const Function = @This();
id: usize,
context: *js.Context,
this: ?v8.Object = null,
func: PersistentFunction,
pub const Result = struct {
stack: ?[]const u8,
exception: []const u8,
};
pub fn getName(self: *const Function, allocator: Allocator) ![]const u8 {
const name = self.func.castToFunction().getName();
return self.context.valueToString(name, .{ .allocator = allocator });
}
pub fn setName(self: *const Function, name: []const u8) void {
const v8_name = v8.String.initUtf8(self.context.isolate, name);
self.func.castToFunction().setName(v8_name);
}
pub fn withThis(self: *const Function, value: anytype) !Function {
const this_obj = if (@TypeOf(value) == js.Object)
value.js_obj
else
(try self.context.zigValueToJs(value)).castTo(v8.Object);
return .{
.id = self.id,
.this = this_obj,
.func = self.func,
.context = self.context,
};
}
pub fn newInstance(self: *const Function, result: *Result) !js.Object {
const context = self.context;
var try_catch: js.TryCatch = undefined;
try_catch.init(context);
defer try_catch.deinit();
// This creates a new instance using this Function as a constructor.
// This returns a generic Object
const js_obj = self.func.castToFunction().initInstance(context.v8_context, &.{}) orelse {
if (try_catch.hasCaught()) {
const allocator = context.call_arena;
result.stack = try_catch.stack(allocator) catch null;
result.exception = (try_catch.exception(allocator) catch "???") orelse "???";
} else {
result.stack = null;
result.exception = "???";
}
return error.JsConstructorFailed;
};
return .{
.context = context,
.js_obj = js_obj,
};
}
pub fn call(self: *const Function, comptime T: type, args: anytype) !T {
return self.callWithThis(T, self.getThis(), args);
}
pub fn tryCall(self: *const Function, comptime T: type, args: anytype, result: *Result) !T {
return self.tryCallWithThis(T, self.getThis(), args, result);
}
pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, result: *Result) !T {
var try_catch: js.TryCatch = undefined;
try_catch.init(self.context);
defer try_catch.deinit();
return self.callWithThis(T, this, args) catch |err| {
if (try_catch.hasCaught()) {
const allocator = self.context.call_arena;
result.stack = try_catch.stack(allocator) catch null;
result.exception = (try_catch.exception(allocator) catch @errorName(err)) orelse @errorName(err);
} else {
result.stack = null;
result.exception = @errorName(err);
}
return err;
};
}
pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T {
const context = self.context;
const js_this = try context.valueToExistingObject(this);
const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args;
const js_args: []const v8.Value = switch (@typeInfo(@TypeOf(aargs))) {
.@"struct" => |s| blk: {
const fields = s.fields;
var js_args: [fields.len]v8.Value = undefined;
inline for (fields, 0..) |f, i| {
js_args[i] = try context.zigValueToJs(@field(aargs, f.name));
}
const cargs: [fields.len]v8.Value = js_args;
break :blk &cargs;
},
.pointer => blk: {
var values = try context.call_arena.alloc(v8.Value, args.len);
for (args, 0..) |a, i| {
values[i] = try context.zigValueToJs(a);
}
break :blk values;
},
else => @compileError("JS Function called with invalid paremter type"),
};
const result = self.func.castToFunction().call(context.v8_context, js_this, js_args);
if (result == null) {
return error.JSExecCallback;
}
if (@typeInfo(T) == .void) return {};
const named_function = comptime Caller.NamedFunction.init(T, "callResult");
return context.jsValueToZig(named_function, T, result.?);
}
fn getThis(self: *const Function) v8.Object {
return self.this orelse self.context.v8_context.getGlobal();
}
pub fn src(self: *const Function) ![]const u8 {
const value = self.func.castToFunction().toValue();
return self.context.valueToString(value, .{});
}

View File

@@ -1,134 +0,0 @@
const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const Context = @import("Context.zig");
const Allocator = std.mem.Allocator;
const Inspector = @This();
pub const RemoteObject = v8.RemoteObject;
isolate: v8.Isolate,
inner: *v8.Inspector,
session: v8.InspectorSession,
// We expect allocator to be an arena
pub fn init(allocator: Allocator, isolate: v8.Isolate, ctx: anytype) !Inspector {
const ContextT = @TypeOf(ctx);
const InspectorContainer = switch (@typeInfo(ContextT)) {
.@"struct" => ContextT,
.pointer => |ptr| ptr.child,
.void => NoopInspector,
else => @compileError("invalid context type"),
};
// If necessary, turn a void context into something we can safely ptrCast
const safe_context: *anyopaque = if (ContextT == void) @ptrCast(@constCast(&{})) else ctx;
const channel = v8.InspectorChannel.init(
safe_context,
InspectorContainer.onInspectorResponse,
InspectorContainer.onInspectorEvent,
InspectorContainer.onRunMessageLoopOnPause,
InspectorContainer.onQuitMessageLoopOnPause,
isolate,
);
const client = v8.InspectorClient.init();
const inner = try allocator.create(v8.Inspector);
v8.Inspector.init(inner, client, channel, isolate);
return .{ .inner = inner, .isolate = isolate, .session = inner.connect() };
}
pub fn deinit(self: *const Inspector) void {
self.session.deinit();
self.inner.deinit();
}
pub fn send(self: *const Inspector, msg: []const u8) void {
// Can't assume the main Context exists (with its HandleScope)
// available when doing this. Pages (and thus the HandleScope)
// comes and goes, but CDP can keep sending messages.
const isolate = self.isolate;
var temp_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&temp_scope, isolate);
defer temp_scope.deinit();
self.session.dispatchProtocolMessage(isolate, msg);
}
// From CDP docs
// https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-ExecutionContextDescription
// ----
// - name: Human readable name describing given context.
// - origin: Execution context origin (ie. URL who initialised the request)
// - auxData: Embedder-specific auxiliary data likely matching
// {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}
// - is_default_context: Whether the execution context is default, should match the auxData
pub fn contextCreated(
self: *const Inspector,
context: *const Context,
name: []const u8,
origin: []const u8,
aux_data: ?[]const u8,
is_default_context: bool,
) void {
self.inner.contextCreated(context.v8_context, name, origin, aux_data, is_default_context);
}
// Retrieves the RemoteObject for a given value.
// The value is loaded through the ExecutionWorld's mapZigInstanceToJs function,
// just like a method return value. Therefore, if we've mapped this
// value before, we'll get the existing JS PersistedObject and if not
// we'll create it and track it for cleanup when the context ends.
pub fn getRemoteObject(
self: *const Inspector,
context: *Context,
group: []const u8,
value: anytype,
) !RemoteObject {
const js_value = try context.zigValueToJs(value);
// We do not want to expose this as a parameter for now
const generate_preview = false;
return self.session.wrapObject(
context.isolate,
context.v8_context,
js_value,
group,
generate_preview,
);
}
// Gets a value by object ID regardless of which context it is in.
pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []const u8) !?*anyopaque {
const unwrapped = try self.session.unwrapObject(allocator, object_id);
// The values context and groupId are not used here
const toa = getTaggedAnyOpaque(unwrapped.value) orelse return null;
if (toa.subtype == null or toa.subtype != .node) return error.ObjectIdIsNotANode;
return toa.ptr;
}
const NoopInspector = struct {
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}
pub fn onRunMessageLoopOnPause(_: *anyopaque, _: u32) void {}
pub fn onQuitMessageLoopOnPause(_: *anyopaque) void {}
};
pub fn getTaggedAnyOpaque(value: v8.Value) ?*js.TaggedAnyOpaque {
if (value.isObject() == false) {
return null;
}
const obj = value.castTo(v8.Object);
if (obj.internalFieldCount() == 0) {
return null;
}
const external_data = obj.getInternalField(0).castTo(v8.External).get().?;
return @ptrCast(@alignCast(external_data));
}

Some files were not shown because too many files have changed in this diff Show More