1 Commits

338 changed files with 20742 additions and 58032 deletions

View File

@@ -5,7 +5,7 @@ inputs:
zig:
description: 'Zig version to install'
required: false
default: '0.15.2'
default: '0.13.0'
arch:
description: 'CPU arch used to select the v8 lib'
required: false
@@ -17,11 +17,11 @@ inputs:
zig-v8:
description: 'zig v8 version to install'
required: false
default: 'v0.1.33'
default: 'v0.1.6'
v8:
description: 'v8 version to install'
required: false
default: '14.0.365.4'
default: '11.1.134'
cache-dir:
description: 'cache dir to use'
required: false
@@ -34,11 +34,9 @@ runs:
- name: Install apt deps
if: ${{ inputs.os == 'linux' }}
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y wget xz-utils python3 ca-certificates git pkg-config libglib2.0-dev gperf libexpat1-dev cmake clang
run: sudo apt-get install -y wget xz-utils python3 ca-certificates git pkg-config libglib2.0-dev gperf libexpat1-dev cmake clang
- uses: mlugg/setup-zig@v2
- uses: mlugg/setup-zig@v1
with:
version: ${{ inputs.zig }}
@@ -49,7 +47,7 @@ runs:
cache-name: cache-v8
with:
path: ${{ inputs.cache-dir }}/v8
key: libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}_${{ inputs.zig-v8 }}.a
key: libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}.a
- if: ${{ steps.cache-v8.outputs.cache-hit != 'true' }}
shell: bash
@@ -61,29 +59,15 @@ runs:
- name: install v8
shell: bash
run: |
mkdir -p v8/out/${{ inputs.os }}/debug/obj/zig/
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/out/${{ inputs.os }}/debug/obj/zig/libc_v8.a
mkdir -p vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/debug
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/debug/libc_v8.a
mkdir -p v8/out/${{ inputs.os }}/release/obj/zig/
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/out/${{ inputs.os }}/release/obj/zig/libc_v8.a
mkdir -p vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/release
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a vendor/zig-js-runtime/vendor/v8/${{inputs.arch}}-${{inputs.os}}/release/libc_v8.a
- name: Cache libiconv
id: cache-libiconv
uses: actions/cache@v4
env:
cache-name: cache-libiconv
with:
path: ${{ inputs.cache-dir }}/libiconv
key: vendor/libiconv/libiconv-1.17
- name: download libiconv
if: ${{ steps.cache-libiconv.outputs.cache-hit != 'true' }}
- name: libiconv
shell: bash
run: make download-libiconv
- name: build libiconv
shell: bash
run: make build-libiconv
run: make install-libiconv
- name: build mimalloc
shell: bash

View File

@@ -1,11 +1,5 @@
name: nightly build
env:
AWS_ACCESS_KEY_ID: ${{ vars.NIGHTLY_BUILD_AWS_ACCESS_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NIGHTLY_BUILD_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.NIGHTLY_BUILD_AWS_BUCKET }}
AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }}
on:
schedule:
- cron: "2 2 * * *"
@@ -22,75 +16,29 @@ jobs:
ARCH: x86_64
OS: linux
runs-on: ubuntu-22.04
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_CI_PAT }}
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive
- uses: ./.github/actions/install
with:
os: ${{env.OS}}
arch: ${{env.ARCH}}
- name: zig build
run: zig build --release=safe -Doptimize=ReleaseSafe -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8 -Dcpu=x86_64
- name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
- name: upload on s3
run: |
export DIR=`git show --no-patch --no-notes --pretty='%cs_%h'`
aws s3 cp --storage-class=GLACIER_IR lightpanda-${{ env.ARCH }}-${{ env.OS }} s3://lpd-nightly-build/${DIR}/lightpanda-${{ env.ARCH }}-${{ env.OS }}
run: mv zig-out/bin/browsercore-get lightpanda-get-${{ env.ARCH }}-${{ env.OS }}
- name: Upload the build
uses: ncipollo/release-action@v1
with:
allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: nightly
build-linux-aarch64:
env:
ARCH: aarch64
OS: linux
runs-on: ubuntu-22.04-arm
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive
- uses: ./.github/actions/install
with:
os: ${{env.OS}}
arch: ${{env.ARCH}}
- name: zig build
run: zig build --release=safe -Doptimize=ReleaseSafe -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
- name: upload on s3
run: |
export DIR=`git show --no-patch --no-notes --pretty='%cs_%h'`
aws s3 cp --storage-class=GLACIER_IR lightpanda-${{ env.ARCH }}-${{ env.OS }} s3://lpd-nightly-build/${DIR}/lightpanda-${{ env.ARCH }}-${{ env.OS }}
- name: Upload the build
uses: ncipollo/release-action@v1
with:
allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
artifacts: lightpanda-get-${{ env.ARCH }}-${{ env.OS }}
tag: nightly
build-macos-aarch64:
@@ -98,15 +46,13 @@ jobs:
ARCH: aarch64
OS: macos
# macos-14 runs on arm CPU. see
# https://github.com/actions/runner-images?tab=readme-ov-file
runs-on: macos-14
timeout-minutes: 15
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_CI_PAT }}
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive
@@ -116,62 +62,14 @@ jobs:
arch: ${{env.ARCH}}
- name: zig build
run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8
- name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
- name: upload on s3
run: |
export DIR=`git show --no-patch --no-notes --pretty='%cs_%h'`
aws s3 cp --storage-class=GLACIER_IR lightpanda-${{ env.ARCH }}-${{ env.OS }} s3://lpd-nightly-build/${DIR}/lightpanda-${{ env.ARCH }}-${{ env.OS }}
run: mv zig-out/bin/browsercore-get lightpanda-get-${{ env.ARCH }}-${{ env.OS }}
- name: Upload the build
uses: ncipollo/release-action@v1
with:
allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: nightly
build-macos-x86_64:
env:
ARCH: x86_64
OS: macos
# macos-13 runs on x86 CPU. see
# https://github.com/actions/runner-images?tab=readme-ov-file
# If we want to build for macos-14 or superior, we need to switch to
# macos-14-large.
# No need for now, but maybe we will need it in the short term.
runs-on: macos-13
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive
- uses: ./.github/actions/install
with:
os: ${{env.OS}}
arch: ${{env.ARCH}}
- name: zig build
run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
- name: upload on s3
run: |
export DIR=`git show --no-patch --no-notes --pretty='%cs_%h'`
aws s3 cp --storage-class=GLACIER_IR lightpanda-${{ env.ARCH }}-${{ env.OS }} s3://lpd-nightly-build/${DIR}/lightpanda-${{ env.ARCH }}-${{ env.OS }}
- name: Upload the build
uses: ncipollo/release-action@v1
with:
allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
artifacts: lightpanda-get-${{ env.ARCH }}-${{ env.OS }}
tag: nightly

View File

@@ -1,34 +0,0 @@
name: "CLA Assistant"
on:
issue_comment:
types: [created]
pull_request_target:
types: [opened,closed,synchronize]
permissions:
actions: write
contents: read
pull-requests: write
statuses: write
jobs:
CLAAssistant:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: "CLA Assistant"
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
uses: contributor-assistant/github-action@v2.6.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_GH_PAT }}
with:
path-to-signatures: 'signatures/browser/version1/cla.json'
path-to-document: 'https://github.com/lightpanda-io/browser/blob/main/CLA.md'
# branch should not be protected
branch: 'main'
allowlist: krichprollsch,francisbouvier,katie-lpd,sjorsdonkers,bornlex
remote-organization-name: lightpanda-io
remote-repository-name: cla

View File

@@ -1,232 +0,0 @@
name: e2e-test
env:
AWS_ACCESS_KEY_ID: ${{ vars.LPD_PERF_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
LIGHTPANDA_DISABLE_TELEMETRY: true
on:
push:
branches:
- main
paths:
- "build.zig"
- "src/**/*.zig"
- "src/*.zig"
- "vendor/zig-js-runtime"
- ".github/**"
- "vendor/**"
pull_request:
# By default GH trigger on types opened, synchronize and reopened.
# see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
# Since we skip the job when the PR is in draft state, we want to force CI
# running when the PR is marked ready_for_review w/o other change.
# see https://github.com/orgs/community/discussions/25722#discussioncomment-3248917
types: [opened, synchronize, reopened, ready_for_review]
paths:
- ".github/**"
- "build.zig"
- "src/**/*.zig"
- "src/*.zig"
- "vendor/**"
- ".github/**"
- "vendor/**"
# 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 -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-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 tests
run: |
./lightpanda serve & echo $! > LPD.pid
go run runner/main.go
kill `cat LPD.pid`
- name: build proxy
run: |
cd proxy
go build
- name: run end to end tests through proxy
run: |
./proxy/proxy & echo $! > PROXY.id
./lightpanda serve --http_proxy 'http://127.0.0.1:3000' & echo $! > LPD.pid
go run runner/main.go
kill `cat LPD.pid` `cat PROXY.id`
- name: run request interception through proxy
run: |
export PROXY_USERNAME=username PROXY_PASSWORD=password
./proxy/proxy & echo $! > PROXY.id
./lightpanda serve & echo $! > LPD.pid
URL=https://demo-browser.lightpanda.io/campfire-commerce/ node puppeteer/proxy_auth.js
BASE_URL=https://demo-browser.lightpanda.io/ node playwright/proxy_auth.js
kill `cat LPD.pid` `cat PROXY.id`
cdp-and-hyperfine-bench:
name: cdp-and-hyperfine-bench
needs: zig-build-release
env:
MAX_MEMORY: 27000
MAX_AVG_DURATION: 23
LIGHTPANDA_DISABLE_TELEMETRY: true
# use a self host runner.
runs-on: lpd-bench-hetzner
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
repository: 'lightpanda-io/demo'
fetch-depth: 0
- run: npm install
- name: download artifact
uses: actions/download-artifact@v4
with:
name: lightpanda-build-release
- run: chmod a+x ./lightpanda
- name: start http
run: |
go run ws/main.go & echo $! > WS.pid
sleep 2
- name: run puppeteer
run: |
./lightpanda serve & echo $! > LPD.pid
sleep 2
RUNS=100 npm run bench-puppeteer-cdp > puppeteer.out || exit 1
cat /proc/`cat LPD.pid`/status |grep VmHWM|grep -oP '\d+' > LPD.VmHWM
kill `cat LPD.pid`
- name: puppeteer result
run: cat puppeteer.out
- name: memory regression
run: |
export LPD_VmHWM=`cat LPD.VmHWM`
echo "Peak resident set size: $LPD_VmHWM"
test "$LPD_VmHWM" -le "$MAX_MEMORY"
- name: duration regression
run: |
export PUPPETEER_AVG_DURATION=`cat puppeteer.out|grep 'avg run'|sed 's/avg run duration (ms) //'`
echo "puppeteer avg duration: $PUPPETEER_AVG_DURATION"
test "$PUPPETEER_AVG_DURATION" -le "$MAX_AVG_DURATION"
- name: json output
run: |
export AVG_DURATION=`cat puppeteer.out|grep 'avg run'|sed 's/avg run duration (ms) //'`
export TOTAL_DURATION=`cat puppeteer.out|grep 'total duration'|sed 's/total duration (ms) //'`
export LPD_VmHWM=`cat LPD.VmHWM`
echo "{\"duration_total\":${TOTAL_DURATION},\"duration_avg\":${AVG_DURATION},\"mem_peak\":${LPD_VmHWM}}" > bench.json
cat bench.json
- name: run hyperfine
run: |
hyperfine --export-json=hyperfine.json --warmup 3 --runs 20 --shell=none "./lightpanda --dump http://127.0.0.1:1234/campfire-commerce/"
- name: stop http
run: kill `cat WS.pid`
- name: write commit
run: |
echo "${{github.sha}}" > commit.txt
- name: upload artifact
uses: actions/upload-artifact@v4
with:
name: bench-results
path: |
bench.json
hyperfine.json
commit.txt
retention-days: 10
perf-fmt:
name: perf-fmt
needs: cdp-and-hyperfine-bench
# Don't execute on PR
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 15
container:
image: ghcr.io/lightpanda-io/perf-fmt:latest
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: download artifact
uses: actions/download-artifact@v4
with:
name: bench-results
- name: format and send json result
run: /perf-fmt cdp ${{ github.sha }} bench.json
- name: format and send json result
run: /perf-fmt hyperfine ${{ github.sha }} hyperfine.json

View File

@@ -5,33 +5,66 @@ env:
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
LIGHTPANDA_DISABLE_TELEMETRY: true
on:
schedule:
- cron: "23 2 * * *"
push:
branches:
- main
paths:
- "build.zig"
- "src/**/*.zig"
- "src/*.zig"
- "tests/wpt/**"
- "vendor/**"
- ".github/**"
pull_request:
# By default GH trigger on types opened, synchronize and reopened.
# see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
# Since we skip the job when the PR is in draft state, we want to force CI
# running when the PR is marked ready_for_review w/o other change.
# see https://github.com/orgs/community/discussions/25722#discussioncomment-3248917
types: [opened, synchronize, reopened, ready_for_review]
paths:
- ".github/**"
- "build.zig"
- "src/**/*.zig"
- "src/*.zig"
- "tests/wpt/**"
- "vendor/**"
- ".github/**"
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
wpt:
name: web platform tests json output
name: web platform tests
# Don't run the CI with draft PR.
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_CI_PAT }}
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive
- uses: ./.github/actions/install
- run: zig build wpt -Dengine=v8 -- --safe --summary
# For now WPT tests doesn't pass at all.
# We accept then to continue the job on failure.
# TODO remove the continue-on-error when tests will pass.
continue-on-error: true
- name: json output
run: zig build wpt -- --json > wpt.json
run: zig build wpt -Dengine=v8 -- --safe --json > wpt.json
- name: write commit
run: |
@@ -50,9 +83,10 @@ jobs:
name: perf-fmt
needs: wpt
runs-on: ubuntu-latest
timeout-minutes: 15
# Don't execute on PR
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
container:
image: ghcr.io/lightpanda-io/perf-fmt:latest
credentials:

View File

@@ -1,7 +1,7 @@
name: zig-fmt
env:
ZIG_VERSION: 0.15.2
ZIG_VERSION: 0.13.0
on:
pull_request:
@@ -29,10 +29,9 @@ jobs:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: mlugg/setup-zig@v2
- uses: mlugg/setup-zig@v1
with:
version: ${{ env.ZIG_VERSION }}

View File

@@ -16,7 +16,6 @@ on:
- "src/*.zig"
- "vendor/zig-js-runtime"
- ".github/**"
- "vendor/**"
pull_request:
# By default GH trigger on types opened, synchronize and reopened.
@@ -33,7 +32,6 @@ on:
- "src/*.zig"
- "vendor/**"
- ".github/**"
- "vendor/**"
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
@@ -50,44 +48,38 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_CI_PAT }}
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive
- uses: ./.github/actions/install
- name: zig build debug
run: zig build
run: zig build -Dengine=v8
- name: upload artifact
uses: actions/upload-artifact@v4
with:
name: lightpanda-build-dev
path: |
zig-out/bin/lightpanda
retention-days: 1
zig-build-release:
name: zig build release
browser-fetch:
name: browser fetch
needs: zig-build-dev
# Don't run the CI with draft PR.
if: github.event.pull_request.draft == false
# Don't run the CI on PR
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
steps:
- name: download artifact
uses: actions/download-artifact@v4
- uses: actions/checkout@v4
with:
name: lightpanda-build-dev
fetch-depth: 0
token: ${{ secrets.GH_CI_PAT }}
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive
- run: chmod a+x ./lightpanda
- uses: ./.github/actions/install
- run: ./lightpanda fetch https://httpbin.io/xhr/get
- name: zig build release
run: zig build -Doptimize=ReleaseSafe -Dengine=v8
zig-test:
name: zig test
timeout-minutes: 15
# Don't run the CI with draft PR.
if: github.event.pull_request.draft == false
@@ -98,13 +90,14 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_CI_PAT }}
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive
- uses: ./.github/actions/install
- name: zig build test
run: zig build test -- --json > bench.json
run: zig build test -Dengine=v8 -- --json > bench.json
- name: write commit
run: |
@@ -127,8 +120,6 @@ jobs:
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 15
container:
image: ghcr.io/lightpanda-io/perf-fmt:latest
credentials:

11
.gitignore vendored
View File

@@ -1,6 +1,7 @@
zig-cache
/.zig-cache/
/zig-out/
lightpanda.id
/v8/
/build/
src/html5ever/target/
zig-out
/vendor/netsurf/build/
/vendor/netsurf/lib/
/vendor/netsurf/include/
/vendor/libiconv/

39
.gitmodules vendored
View File

@@ -1,18 +1,27 @@
[submodule "vendor/zig-js-runtime"]
path = vendor/zig-js-runtime
url = git@github.com:lightpanda-io/zig-js-runtime.git
[submodule "vendor/netsurf/libwapcaplet"]
path = vendor/netsurf/libwapcaplet
url = git@github.com:lightpanda-io/libwapcaplet.git
[submodule "vendor/netsurf/libparserutils"]
path = vendor/netsurf/libparserutils
url = git@github.com:lightpanda-io/libparserutils.git
[submodule "vendor/netsurf/libdom"]
path = vendor/netsurf/libdom
url = git@github.com:lightpanda-io/libdom.git
[submodule "vendor/netsurf/share/netsurf-buildsystem"]
path = vendor/netsurf/share/netsurf-buildsystem
url = https://source.netsurf-browser.org/buildsystem.git
[submodule "vendor/netsurf/libhubbub"]
path = vendor/netsurf/libhubbub
url = git@github.com:lightpanda-io/libhubbub.git
[submodule "tests/wpt"]
path = tests/wpt
url = https://github.com/lightpanda-io/wpt
[submodule "vendor/nghttp2"]
path = vendor/nghttp2
url = https://github.com/nghttp2/nghttp2.git
[submodule "vendor/mbedtls"]
path = vendor/mbedtls
url = https://github.com/Mbed-TLS/mbedtls.git
[submodule "vendor/zlib"]
path = vendor/zlib
url = https://github.com/madler/zlib.git
[submodule "vendor/curl"]
path = vendor/curl
url = https://github.com/curl/curl.git
[submodule "vendor/brotli"]
path = vendor/brotli
url = https://github.com/google/brotli
[submodule "vendor/mimalloc"]
path = vendor/mimalloc
url = git@github.com:microsoft/mimalloc.git
[submodule "vendor/tls.zig"]
path = vendor/tls.zig
url = git@github.com:ianic/tls.zig.git

93
CLA.md
View File

@@ -1,93 +0,0 @@
# Lightpanda (Selecy SAS) Grant and Contributor License Agreement (“Agreement”)
This agreement is based on the Apache Software Foundation Contributor License
Agreement. (v r190612)
Thank you for your interest in software projects stewarded by Lightpanda
(Selecy SAS) (“Lightpanda”). In order to clarify the intellectual property
license granted with Contributions from any person or entity, Lightpanda must
have a Contributor License Agreement (CLA) on file that has been agreed to by
each Contributor, indicating agreement to the license terms below. This license
is for your protection as a Contributor as well as the protection of Lightpanda
and its users; it does not change your rights to use your own Contributions for
any other purpose. This Agreement allows an individual to contribute to
Lightpanda on that individuals own behalf, or an entity (the “Corporation”) to
submit Contributions to Lightpanda, to authorize Contributions submitted by its
designated employees to Lightpanda, and to grant copyright and patent licenses
thereto.
You accept and agree to the following terms and conditions for Your present and
future Contributions submitted to Lightpanda. Except for the license granted
herein to Lightpanda and recipients of software distributed by Lightpanda, You
reserve all right, title, and interest in and to Your Contributions.
1. Definitions. “You” (or “Your”) shall mean the copyright owner or legal
entity authorized by the copyright owner that is making this Agreement with
Lightpanda. For legal entities, the entity making a Contribution and all
other entities that control, are controlled by, or are under common control
with that entity are considered to be a single Contributor. For the purposes
of this definition, “control” means (i) the power, direct or indirect, to
cause the direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
“Contribution” shall mean any work, as well as any modifications or
additions to an existing work, that is intentionally submitted by You to
Lightpanda for inclusion in, or documentation of, any of the products owned
or managed by Lightpanda (the “Work”). For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication
sent to Lightpanda or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems (such
as GitHub), and issue tracking systems that are managed by, or on behalf of,
Lightpanda for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise designated
in writing by You as “Not a Contribution.”
2. Grant of Copyright License. Subject to the terms and conditions of this
Agreement, You hereby grant to Lightpanda and to recipients of software
distributed by Lightpanda a perpetual, worldwide, non-exclusive, no-charge,
royalty-free, irrevocable copyright license to reproduce, prepare derivative
works of, publicly display, publicly perform, sublicense, and distribute
Your Contributions and such derivative works.
3. Grant of Patent License. Subject to the terms and conditions of this
Agreement, You hereby grant to Lightpanda and to recipients of software
distributed by Lightpanda a perpetual, worldwide, non-exclusive, no-charge,
royalty-free, irrevocable (except as stated in this section) patent license
to make, have made, use, offer to sell, sell, import, and otherwise transfer
the Work, where such license applies only to those patent claims licensable
by You that are necessarily infringed by Your Contribution(s) alone or by
combination of Your Contribution(s) with the Work to which such
Contribution(s) were submitted. If any entity institutes patent litigation
against You or any other entity (including a cross-claim or counterclaim in
a lawsuit) alleging that your Contribution, or the Work to which you have
contributed, constitutes direct or contributory patent infringement, then
any patent licenses granted to that entity under this Agreement for that
Contribution or Work shall terminate as of the date such litigation is
filed.
4. You represent that You are legally entitled to grant the above license. If
You are an individual, and if Your employer(s) has rights to intellectual
property that you create that includes Your Contributions, you represent
that You have received permission to make Contributions on behalf of that
employer, or that Your employer has waived such rights for your
Contributions to Lightpanda. If You are a Corporation, any individual who
makes a contribution from an account associated with You will be considered
authorized to Contribute on Your behalf.
5. You represent that each of Your Contributions is Your original creation (see
section 7 for submissions on behalf of others).
6. You are not expected to provide support for Your Contributions,except to the
extent You desire to provide support. You may provide support for free, for
a fee, or not at all. Unless required by applicable law or agreed to in
writing, You provide Your Contributions on an “AS IS” BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including,
without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT,
MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
7. Should You wish to submit work that is not Your original creation, You may
submit it to Lightpanda separately from any Contribution, identifying the
complete details of its source and of any license or other restriction
(including, but not limited to, related patents, trademarks, and license
agreements) of which you are personally aware, and conspicuously marking the
work as “Submitted on behalf of a third-party: [named here]”.

View File

@@ -1,10 +0,0 @@
# Contributing
Lightpanda accepts pull requests through GitHub.
You have to sign our [CLA](CLA.md) during your first pull request process
otherwise we're not able to accept your contributions.
The process signature uses the [CLA assistant
lite](https://github.com/marketplace/actions/cla-assistant-lite). You can see
an example of the process in [#303](https://github.com/lightpanda-io/browser/pull/303).

View File

@@ -1,68 +0,0 @@
FROM debian:stable
ARG MINISIG=0.12
ARG ZIG=0.15.2
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
ARG V8=14.0.365.4
ARG ZIG_V8=v0.1.33
ARG TARGETPLATFORM
RUN apt-get update -yq && \
apt-get install -yq xz-utils \
python3 ca-certificates git \
pkg-config libglib2.0-dev \
gperf libexpat1-dev \
cmake clang \
curl git
# install minisig
RUN curl --fail -L -O https://github.com/jedisct1/minisign/releases/download/${MINISIG}/minisign-${MINISIG}-linux.tar.gz && \
tar xvzf minisign-${MINISIG}-linux.tar.gz
# install zig
RUN case $TARGETPLATFORM in \
"linux/arm64") ARCH="aarch64" ;; \
*) ARCH="x86_64" ;; \
esac && \
curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-${ARCH}-linux-${ZIG}.tar.xz && \
curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-${ARCH}-linux-${ZIG}.tar.xz.minisig && \
minisign-linux/${ARCH}/minisign -Vm zig-${ARCH}-linux-${ZIG}.tar.xz -P ${ZIG_MINISIG} && \
tar xvf zig-${ARCH}-linux-${ZIG}.tar.xz && \
mv zig-${ARCH}-linux-${ZIG} /usr/local/lib && \
ln -s /usr/local/lib/zig-${ARCH}-linux-${ZIG}/zig /usr/local/bin/zig
# clone lightpanda
RUN git clone https://github.com/lightpanda-io/browser.git
WORKDIR /browser
# install deps
RUN git submodule init && \
git submodule update --recursive
RUN make install-libiconv && \
make install-netsurf && \
make install-mimalloc
# download and install v8
RUN case $TARGETPLATFORM in \
"linux/arm64") ARCH="aarch64" ;; \
*) ARCH="x86_64" ;; \
esac && \
curl --fail -L -o libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${ZIG_V8}/libc_v8_${V8}_linux_${ARCH}.a && \
mkdir -p v8/out/linux/release/obj/zig/ && \
mv libc_v8.a v8/out/linux/release/obj/zig/libc_v8.a
# build release
RUN make build
FROM debian:stable-slim
# copy ca certificates
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
EXPOSE 9222/tcp
CMD ["/bin/lightpanda", "serve", "--host", "0.0.0.0", "--port", "9222"]

View File

@@ -1,22 +0,0 @@
# Licensing
License names used in this document are as per [SPDX License
List](https://spdx.org/licenses/).
The default license for this project is [AGPL-3.0-only](LICENSE).
## MIT
The following files are licensed under MIT:
```
src/polyfill/fetch.js
```
The following directories and their subdirectories are licensed under their
original upstream licenses:
```
vendor/
tests/wpt/
```

216
Makefile
View File

@@ -3,30 +3,6 @@
ZIG := zig
BC := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
# option test filter make test F="server"
F=
# OS and ARCH
kernel = $(shell uname -ms)
ifeq ($(kernel), Darwin arm64)
OS := macos
ARCH := aarch64
else ifeq ($(kernel), Darwin x86_64)
OS := macos
ARCH := x86_64
else ifeq ($(kernel), Linux aarch64)
OS := linux
ARCH := aarch64
else ifeq ($(kernel), Linux arm64)
OS := linux
ARCH := aarch64
else ifeq ($(kernel), Linux x86_64)
OS := linux
ARCH := x86_64
else
$(error "Unhandled kernel: $(kernel)")
endif
# Infos
# -----
@@ -47,15 +23,33 @@ help:
# $(ZIG) commands
# ------------
.PHONY: build build-dev run run-release shell test bench download-zig wpt data get-v8 build-v8 build-v8-dev
.PHONY: end2end
.PHONY: build build-dev run run-release shell test bench download-zig wpt
zig_version = $(shell grep 'recommended_zig_version = "' "vendor/zig-js-runtime/build.zig" | cut -d'"' -f2)
kernel = $(shell uname -ms)
## 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")
ifeq ($(kernel), Darwin x86_64)
$(eval target="macos")
$(eval arch="x86_64")
else ifeq ($(kernel), Darwin arm64)
$(eval target="macos")
$(eval arch="aarch64")
else ifeq ($(kernel), Linux aarch64)
$(eval target="linux")
$(eval arch="aarch64")
else ifeq ($(kernel), Linux arm64)
$(eval target="linux")
$(eval arch="aarch64")
else ifeq ($(kernel), Linux x86_64)
$(eval target="linux")
$(eval arch="x86_64")
else
$(error "Unhandled kernel: $(kernel)")
endif
$(eval url = "https://ziglang.org/builds/zig-$(target)-$(arch)-$(zig_version).tar.xz")
$(eval dest = "/tmp/zig-$(target)-$(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"
@@ -63,87 +57,155 @@ download-zig:
## Build in release-safe mode
build:
@printf "\e[36mBuilding (release safe)...\e[0m\n"
$(ZIG) build -Doptimize=ReleaseSafe -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@$(ZIG) build -Doptimize=ReleaseSafe -Dengine=v8 || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@printf "\e[33mBuild OK\e[0m\n"
## Build in debug mode
build-dev:
@printf "\e[36mBuilding (debug)...\e[0m\n"
@$(ZIG) build -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@$(ZIG) build -Dengine=v8 || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@printf "\e[33mBuild OK\e[0m\n"
## Run the server in release mode
## Run the server in debug mode
run: build
@printf "\e[36mRunning...\e[0m\n"
@./zig-out/bin/lightpanda || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
## Run the server in debug mode
run-debug: build-dev
@printf "\e[36mRunning...\e[0m\n"
@./zig-out/bin/lightpanda || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
@./zig-out/bin/browsercore || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
## Run a JS shell in debug mode
shell:
@printf "\e[36mBuilding shell...\e[0m\n"
@$(ZIG) build shell || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@$(ZIG) build shell -Dengine=v8 || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
## Run WPT tests
wpt:
@printf "\e[36mBuilding wpt...\e[0m\n"
@$(ZIG) build wpt -- $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@$(ZIG) build wpt -Dengine=v8 -- --safe $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
wpt-summary:
@printf "\e[36mBuilding wpt...\e[0m\n"
@$(ZIG) build wpt -- --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@$(ZIG) build wpt -Dengine=v8 -- --safe --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
## Test - `grep` is used to filter out the huge compile command on build
ifeq ($(OS), macos)
## Test
test:
@script -q /dev/null sh -c 'TEST_FILTER="${F}" $(ZIG) build test -freference-trace --summary all' 2>&1 \
| 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
end2end:
@test -d ../demo
cd ../demo && go run runner/main.go
## v8
get-v8:
@printf "\e[36mGetting v8 source...\e[0m\n"
@$(ZIG) build get-v8
build-v8-dev:
@printf "\e[36mBuilding v8 (dev)...\e[0m\n"
@$(ZIG) build build-v8
build-v8:
@printf "\e[36mBuilding v8...\e[0m\n"
@$(ZIG) build -Doptimize=ReleaseSafe build-v8
@printf "\e[36mTesting...\e[0m\n"
@$(ZIG) build test -Dengine=v8 || (printf "\e[33mTest ERROR\e[0m\n"; exit 1;)
@printf "\e[33mTest OK\e[0m\n"
# Install and build required dependencies commands
# ------------
.PHONY: install-html5ever install-html5ever-dev
.PHONY: install install-dev
.PHONY: install-submodule
.PHONY: install-zig-js-runtime install-zig-js-runtime-dev install-libiconv
.PHONY: _install-netsurf install-netsurf clean-netsurf test-netsurf install-netsurf-dev
.PHONY: install-mimalloc install-mimalloc-dev clean-mimalloc
.PHONY: install-dev install
## Install and build dependencies for release
install: install-submodule install-html5ever
install: install-submodule install-zig-js-runtime install-netsurf install-mimalloc
## Install and build dependencies for dev
install-dev: install-submodule install-html5ever-dev
install-dev: install-submodule install-zig-js-runtime-dev install-netsurf-dev install-mimalloc-dev
install-html5ever:
cd src/html5ever && cargo build --release --target-dir ../../build/html5ever/
install-netsurf-dev: _install-netsurf
install-netsurf-dev: OPTCFLAGS := -O0 -g -DNDEBUG
install-html5ever-dev:
cd src/html5ever && cargo build --target-dir ../../build/html5ever/
install-netsurf: _install-netsurf
install-netsurf: OPTCFLAGS := -DNDEBUG
data:
cd src/data && go run public_suffix_list_gen.go > public_suffix_list.zig
BC_NS := $(BC)vendor/netsurf
ICONV := $(BC)vendor/libiconv
# TODO: add Linux iconv path (I guess it depends on the distro)
# TODO: this way of linking libiconv is not ideal. We should have a more generic way
# and stick to a specif version. Maybe build from source. Anyway not now.
_install-netsurf: install-libiconv
@printf "\e[36mInstalling NetSurf...\e[0m\n" && \
ls $(ICONV) 1> /dev/null || (printf "\e[33mERROR: you need to install libiconv in your system (on MacOS on with Homebrew)\e[0m\n"; exit 1;) && \
export PREFIX=$(BC_NS) && \
export OPTLDFLAGS="-L$(ICONV)/lib" && \
export OPTCFLAGS="$(OPTCFLAGS) -I$(ICONV)/include" && \
printf "\e[33mInstalling libwapcaplet...\e[0m\n" && \
cd vendor/netsurf/libwapcaplet && \
BUILDDIR=$(BC_NS)/build/libwapcaplet make install && \
cd ../libparserutils && \
printf "\e[33mInstalling libparserutils...\e[0m\n" && \
BUILDDIR=$(BC_NS)/build/libparserutils make install && \
cd ../libhubbub && \
printf "\e[33mInstalling libhubbub...\e[0m\n" && \
BUILDDIR=$(BC_NS)/build/libhubbub make install && \
rm src/treebuilder/autogenerated-element-type.c && \
cd ../libdom && \
printf "\e[33mInstalling libdom...\e[0m\n" && \
BUILDDIR=$(BC_NS)/build/libdom make install && \
printf "\e[33mRunning libdom example...\e[0m\n" && \
cd examples && \
zig cc \
-I$(ICONV)/include \
-I$(BC_NS)/include \
-L$(ICONV)/lib \
-L$(BC_NS)/lib \
-liconv \
-ldom \
-lhubbub \
-lparserutils \
-lwapcaplet \
-o a.out \
dom-structure-dump.c \
$(ICONV)/lib/libiconv.a && \
./a.out > /dev/null && \
rm a.out && \
printf "\e[36mDone NetSurf $(OS)\e[0m\n"
clean-netsurf:
@printf "\e[36mCleaning NetSurf build...\e[0m\n" && \
cd vendor/netsurf && \
rm -R build && \
rm -R lib && \
rm -R include
test-netsurf:
@printf "\e[36mTesting NetSurf...\e[0m\n" && \
export PREFIX=$(BC_NS) && \
export LDFLAGS="-L$(ICONV)/lib -L$(BC_NS)/lib" && \
export CFLAGS="-I$(ICONV)/include -I$(BC_NS)/include" && \
cd vendor/netsurf/libdom && \
BUILDDIR=$(BC_NS)/build/libdom make test
install-libiconv:
ifeq ("$(wildcard vendor/libiconv/lib/libiconv.a)","")
@mkdir -p vendor/libiconv
@cd vendor/libiconv && \
curl https://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.17.tar.gz | tar -xvzf -
@cd vendor/libiconv/libiconv-1.17 && \
./configure --prefix=$(BC)vendor/libiconv --enable-static && \
make && make install
endif
install-zig-js-runtime-dev:
@cd vendor/zig-js-runtime && \
make install-dev
install-zig-js-runtime:
@cd vendor/zig-js-runtime && \
make install
.PHONY: _build_mimalloc
_build_mimalloc:
@cd vendor/mimalloc && \
mkdir -p out/include && \
cp include/mimalloc.h out/include/ && \
cd out && \
cmake -DMI_BUILD_SHARED=OFF -DMI_BUILD_OBJECT=OFF -DMI_BUILD_TESTS=OFF -DMI_OVERRIDE=OFF $(OPTS) .. && \
make
install-mimalloc-dev: _build_mimalloc
install-mimalloc-dev: OPTS=-DCMAKE_BUILD_TYPE=Debug
install-mimalloc-dev:
@cd vendor/mimalloc/out && \
mv libmimalloc-debug.a libmimalloc.a
install-mimalloc: _build_mimalloc
clean-mimalloc:
@rm -fr vendor/mimalloc/lib/*
## Init and update git submodule
install-submodule:

246
README.md
View File

@@ -2,174 +2,85 @@
<a href="https://lightpanda.io"><img src="https://cdn.lightpanda.io/assets/images/logo/lpd-logo.png" alt="Logo" height=170></a>
</p>
<h1 align="center">Lightpanda Browser</h1>
<p align="center"><a href="https://lightpanda.io/">lightpanda.io</a></p>
<h1 align="center">Lightpanda</h1>
<div align="center">
[![License](https://img.shields.io/github/license/lightpanda-io/browser)](https://github.com/lightpanda-io/browser/blob/main/LICENSE)
[![Twitter Follow](https://img.shields.io/twitter/follow/lightpanda_io)](https://twitter.com/lightpanda_io)
[![GitHub stars](https://img.shields.io/github/stars/lightpanda-io/browser)](https://github.com/lightpanda-io/browser)
<br />
</div>
Lightpanda is the open-source browser made for headless usage:
- Javascript execution
- Support of Web APIs (partial, WIP)
- Compatible with Playwright[^1], Puppeteer, chromedp through CDP
- Support of the Web APIs (partial, WIP)
- Compatible with Playwright, Puppeteer through CDP (WIP)
Fast web automation for AI agents, LLM training, scraping and testing:
Fast scraping and web automation with minimal memory footprint:
- Ultra-low memory footprint (9x less than Chrome)
- Exceptionally fast execution (11x faster than Chrome)
- Instant startup
- Ultra-low memory footprint (12x less than Chrome)
- Blazingly fast & instant startup (64x faster than Chrome)
[<img width="350px" src="https://cdn.lightpanda.io/assets/images/github/execution-time.svg">
](https://github.com/lightpanda-io/demo)
&emsp;
[<img width="350px" src="https://cdn.lightpanda.io/assets/images/github/memory-frame.svg">
](https://github.com/lightpanda-io/demo)
</div>
<img width=500px src="https://cdn.lightpanda.io/assets/images/benchmark2.png">
_Puppeteer requesting 100 pages from a local website on a AWS EC2 m5.large instance.
See [benchmark details](https://github.com/lightpanda-io/demo)._
See [benchmark details](https://github.com/lightpanda-io/demo).
[^1]: **Playwright support disclaimer:**
Due to the nature of Playwright, a script that works with the current version of the browser may not function correctly with a future version. Playwright uses an intermediate JavaScript layer that selects an execution strategy based on the browser's available features. If Lightpanda adds a new [Web API](https://developer.mozilla.org/en-US/docs/Web/API), Playwright may choose to execute different code for the same script. This new code path could attempt to use features that are not yet implemented. Lightpanda makes an effort to add compatibility tests, but we can't cover all scenarios. If you encounter an issue, please create a [GitHub issue](https://github.com/lightpanda-io/browser/issues) and include the last known working version of the script.
## Why?
## Quick start
### Javascript execution is mandatory for the modern web
### Install
**Install from the nightly builds**
Back in the good old times, grabbing a webpage was as easy as making an HTTP request, cURL-like. Its not possible anymore, because Javascript is everywhere, like it or not:
You can download the last binary from the [nightly
builds](https://github.com/lightpanda-io/browser/releases/tag/nightly) for
Linux x86_64 and MacOS aarch64.
- Ajax, Single Page App, Infinite loading, “click to display”, instant search, etc.
- JS web frameworks: React, Vue, Angular & others
*For Linux*
```console
curl -L -o lightpanda https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-x86_64-linux && \
chmod a+x ./lightpanda
```
### Chrome is not the right tool
*For MacOS*
```console
curl -L -o lightpanda https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-aarch64-macos && \
chmod a+x ./lightpanda
```
So if we need Javascript, why not use a real web browser. Lets take a huge desktop application, hack it, and run it on the server, right? Hundreds of instance of Chrome if you use it at scale. Are you sure its such a good idea?
*For Windows + WSL2*
- Heavy on RAM and CPU, expensive to run
- Hard to package, deploy and maintain at scale
- Bloated, lots of features are not useful in headless usage
The Lightpanda browser is compatible to run on windows inside WSL. Follow the Linux instruction for installation from a WSL terminal.
It is recommended to install clients like Puppeteer on the Windows host.
### Lightpanda is built for performance
**Install from Docker**
If we want both Javascript and performance, for a real headless browser, we need to start from scratch. Not yet another iteration of Chromium, really from a blank page. Crazy right? But thats we did:
Lightpanda provides [official Docker
images](https://hub.docker.com/r/lightpanda/browser) for both Linux amd64 and
arm64 architectures.
The following command fetches the Docker image and starts a new container exposing Lightpanda's CDP server on port `9222`.
```console
docker run -d --name lightpanda -p 9222:9222 lightpanda/browser:nightly
```
### Dump a URL
```console
./lightpanda fetch --dump https://lightpanda.io
```
```console
info(browser): GET https://lightpanda.io/ http.Status.ok
info(browser): fetch script https://api.website.lightpanda.io/js/script.js: http.Status.ok
info(browser): eval remote https://api.website.lightpanda.io/js/script.js: TypeError: Cannot read properties of undefined (reading 'pushState')
<!DOCTYPE html>
```
### Start a CDP server
```console
./lightpanda serve --host 127.0.0.1 --port 9222
```
```console
info(websocket): starting blocking worker to listen on 127.0.0.1:9222
info(server): accepting new conn...
```
Once the CDP server started, you can run a Puppeteer script by configuring the
`browserWSEndpoint`.
```js
'use strict'
import puppeteer from 'puppeteer-core';
// use browserWSEndpoint to pass the Lightpanda's CDP server address.
const browser = await puppeteer.connect({
browserWSEndpoint: "ws://127.0.0.1:9222",
});
// The rest of your script remains the same.
const context = await browser.createBrowserContext();
const page = await context.newPage();
// Dump all the links from the page.
await page.goto('https://wikipedia.com/');
const links = await page.evaluate(() => {
return Array.from(document.querySelectorAll('a')).map(row => {
return row.getAttribute('href');
});
});
console.log(links);
await page.close();
await context.close();
await browser.disconnect();
```
### Telemetry
By default, Lightpanda collects and sends usage telemetry. This can be disabled by setting an environment variable `LIGHTPANDA_DISABLE_TELEMETRY=true`. You can read Lightpanda's privacy policy at: [https://lightpanda.io/privacy-policy](https://lightpanda.io/privacy-policy).
- Not based on Chromium, Blink or WebKit
- Low-level system programming language (Zig) with optimisations in mind
- Opinionated, no rendering
## Status
Lightpanda is in Beta and currently a work in progress. Stability and coverage are improving and many websites now work.
You may still encounter errors or crashes. Please open an issue with specifics if so.
Lightpanda is still a work in progress and is currently at the Alpha stage.
Here are the key features we have implemented:
Here are the key features we want to implement before releasing a Beta version:
- [x] HTTP loader (based on Libcurl)
- [x] HTML parser and DOM tree (based on Netsurf libs)
- [x] Javascript support (v8)
- [x] DOM APIs
- [x] Loader
- [x] HTML parser and DOM tree
- [x] Javascript support
- [x] Basic DOM APIs
- [x] Ajax
- [x] XHR API
- [x] Fetch API (polyfill)
- [ ] Fetch API
- [x] DOM dump
- [x] CDP/websockets server
- [x] Click
- [x] Input form
- [x] Cookies
- [x] Custom HTTP headers
- [x] Proxy support
- [x] Network interception
- [ ] Basic CDP server
NOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time.
We will not provide binary versions until we reach at least the Beta stage.
NOTE: There are hundreds of Web APIs. Developing a browser, even just for headless mode, is a huge task. It's more about coverage than a _working/not working_ binary situation.
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
We do not provide yet binary versions of Lightpanda, you have to compile it from source.
### Prerequisites
Lightpanda is written with [Zig](https://ziglang.org/) `0.15.2`. You have to
Lightpanda is written with [Zig](https://ziglang.org/) `0.13.0`. You have to
install it with the right version in order to build the project.
Lightpanda also depends on
[zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime/) (with v8),
[Libcurl](https://curl.se/libcurl/),
[Netsurf libs](https://www.netsurf-browser.org/) and
[Mimalloc](https://microsoft.github.io/mimalloc).
@@ -181,15 +92,10 @@ For Debian/Ubuntu based Linux:
sudo apt install xz-utils \
python3 ca-certificates git \
pkg-config libglib2.0-dev \
gperf libexpat1-dev unzip rsync \
gperf libexpat1-dev \
cmake clang
```
For systems with [Nix](https://nixos.org/download/), you can use the devShell:
```
nix develop
```
For MacOS, you only need cmake:
```
@@ -202,9 +108,9 @@ brew install cmake
You can run `make install` to install deps all in one (or `make install-dev` if you need the development versions).
Be aware that the build task is very long and cpu consuming, as you will build from sources all dependencies, including the v8 Javascript engine.
Be aware that the build task is very long and cpu consuming, as you will build from sources all dependancies, including the v8 Javascript engine.
#### Step by step build dependency
#### Step by step build dependancy
The project uses git submodules for dependencies.
@@ -214,14 +120,6 @@ To init or update the submodules in the `vendor/` directory:
make install-submodule
```
**iconv**
libiconv is an internationalization library used by Netsurf.
```
make install-libiconv
```
**Netsurf libs**
Netsurf libs are used for HTML parsing and DOM tree generation.
@@ -246,21 +144,17 @@ Note: when Mimalloc is built in dev mode, you can dump memory stats with the
env var `MIMALLOC_SHOW_STATS=1`. See
[https://microsoft.github.io/mimalloc/environment.html](https://microsoft.github.io/mimalloc/environment.html).
**v8**
**zig-js-runtime**
First, get the tools necessary for building V8, as well as the V8 source code:
Our own Zig/Javascript runtime, which includes the v8 Javascript engine.
This build task is very long and cpu consuming, as you will build v8 from sources.
```
make get-v8
make install-zig-js-runtime
```
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`.
For dev env, use `make iinstall-zig-js-runtime-dev`.
## Test
@@ -268,20 +162,6 @@ For dev env, use `make build-v8-dev`.
You can test Lightpanda by running `make test`.
### End to end tests
To run end to end tests, you need to clone the [demo
repository](https://github.com/lightpanda-io/demo) into `../demo` dir.
You have to install the [demo's node
requirements](https://github.com/lightpanda-io/demo?tab=readme-ov-file#dependencies-1)
You also need to install [Go](https://go.dev) > v1.24.
```
make end2end
```
### Web Platform Tests
Lightpanda is tested against the standardized [Web Platform
@@ -316,35 +196,3 @@ To add a new test, copy the file you want from the [WPT
repo](https://github.com/web-platform-tests/wpt) into the `tests/wpt` directory.
:warning: Please keep the original directory tree structure of `tests/wpt`.
## Contributing
Lightpanda accepts pull requests through GitHub.
You have to sign our [CLA](CLA.md) during the pull request process otherwise
we're not able to accept your contributions.
## Why?
### Javascript execution is mandatory for the modern web
In the good old days, scraping a webpage was as easy as making an HTTP request, cURL-like. Its not possible anymore, because Javascript is everywhere, like it or not:
- Ajax, Single Page App, infinite loading, “click to display”, instant search, etc.
- JS web frameworks: React, Vue, Angular & others
### Chrome is not the right tool
If we need Javascript, why not use a real web browser? Take a huge desktop application, hack it, and run it on the server. Hundreds or thousands of instances of Chrome if you use it at scale. Are you sure its such a good idea?
- Heavy on RAM and CPU, expensive to run
- Hard to package, deploy and maintain at scale
- Bloated, lots of features are not useful in headless usage
### Lightpanda is built for performance
If we want both Javascript and performance in a true headless browser, we need to start from scratch. Not another iteration of Chromium, really from a blank page. Crazy right? But thats what we did:
- Not based on Chromium, Blink or WebKit
- Low-level system programming language (Zig) with optimisations in mind
- Opinionated: without graphical rendering

934
build.zig
View File

@@ -17,15 +17,18 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const builtin = @import("builtin");
const Build = std.Build;
const jsruntime_path = "vendor/zig-js-runtime/";
const jsruntime = @import("vendor/zig-js-runtime/build.zig");
const jsruntime_pkgs = jsruntime.packages(jsruntime_path);
/// Do not rename this constant. It is scanned by some scripts to determine
/// which zig version to install.
const recommended_zig_version = "0.15.2";
const recommended_zig_version = jsruntime.recommended_zig_version;
pub fn build(b: *Build) !void {
pub fn build(b: *std.Build) !void {
switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) {
.eq => {},
.lt => {
@@ -39,761 +42,198 @@ pub fn build(b: *Build) !void {
},
}
var opts = b.addOptions();
opts.addOption(
[]const u8,
"git_commit",
b.option([]const u8, "git_commit", "Current git commit") orelse "dev",
);
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const mode = b.standardOptimizeOption(.{});
const enable_tsan = b.option(bool, "tsan", "Enable Thread Sanitizer");
const enable_csan = b.option(std.zig.SanitizeC, "csan", "Enable C Sanitizers");
const options = try jsruntime.buildOptions(b);
const lightpanda_module = blk: {
const mod = b.addModule("lightpanda", .{
.root_source_file = b.path("src/lightpanda.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
.link_libcpp = true,
.sanitize_c = enable_csan,
.sanitize_thread = enable_tsan,
});
const x86 = b.option(bool, "x86", "Use x86 backend") orelse false;
try addDependencies(b, mod, opts);
// browser
// -------
if (optimize == .ReleaseFast or optimize == .ReleaseSmall) {
mod.addLibraryPath(b.path("build/html5ever/release"));
} else {
mod.addLibraryPath(b.path("build/html5ever/debug"));
}
mod.linkSystemLibrary("litefetch_html5ever", .{});
break :blk mod;
};
{
// browser
const exe = b.addExecutable(.{
.name = "lightpanda",
.use_llvm = true,
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.sanitize_c = enable_csan,
.sanitize_thread = enable_tsan,
.imports = &.{
.{.name = "lightpanda", .module = lightpanda_module},
},
}),
});
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}
{
// test
const tests = b.addTest(.{
.root_module = lightpanda_module,
.test_runner = .{ .path = b.path("src/test_runner.zig"), .mode = .simple },
});
const run_tests = b.addRunArtifact(tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_tests.step);
}
{
// wpt
const exe = b.addExecutable(.{
.name = "lightpanda-wpt",
.use_llvm = true,
.root_module = b.createModule(.{
.root_source_file = b.path("src/main_wpt.zig"),
.target = target,
.optimize = optimize,
.sanitize_c = enable_csan,
.sanitize_thread = enable_tsan,
.imports = &.{
.{.name = "lightpanda", .module = lightpanda_module},
},
}),
});
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("wpt", "Run WPT tests");
run_step.dependOn(&run_cmd.step);
}
{
// get v8
// -------
const v8 = b.dependency("v8", .{ .target = target, .optimize = optimize });
const get_v8 = b.addRunArtifact(v8.artifact("get-v8"));
const get_step = b.step("get-v8", "Get v8");
get_step.dependOn(&get_v8.step);
}
{
// build v8
// -------
const v8 = b.dependency("v8", .{ .target = target, .optimize = optimize });
const build_v8 = b.addRunArtifact(v8.artifact("build-v8"));
const build_step = b.step("build-v8", "Build v8");
build_step.dependOn(&build_v8.step);
}
}
fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !void {
mod.addImport("build_config", opts.createModule());
const target = mod.resolved_target.?;
const dep_opts = .{
// compile and install
const exe = b.addExecutable(.{
.name = "browsercore",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = mod.optimize.?,
.optimize = mode,
.use_llvm = !x86,
.use_lld = !x86,
});
try common(b, exe, options);
b.installArtifact(exe);
// run
const run_cmd = b.addRunArtifact(exe);
if (b.args) |args| {
run_cmd.addArgs(args);
}
// step
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
// shell
// -----
// compile and install
const shell = b.addExecutable(.{
.name = "browsercore-shell",
.root_source_file = b.path("src/main_shell.zig"),
.target = target,
.optimize = mode,
.use_llvm = !x86,
.use_lld = !x86,
});
try common(b, shell, options);
try jsruntime_pkgs.add_shell(shell);
// run
const shell_cmd = b.addRunArtifact(shell);
if (b.args) |args| {
shell_cmd.addArgs(args);
}
// step
const shell_step = b.step("shell", "Run JS shell");
shell_step.dependOn(&shell_cmd.step);
// test
// ----
// compile
const tests = b.addTest(.{
.root_source_file = b.path("src/run_tests.zig"),
.test_runner = b.path("src/test_runner.zig"),
.target = target,
.optimize = mode,
.use_llvm = !x86,
.use_lld = !x86,
});
try common(b, tests, options);
// add jsruntime pretty deps
tests.root_module.addAnonymousImport("pretty", .{
.root_source_file = b.path("vendor/zig-js-runtime/src/pretty.zig"),
});
const run_tests = b.addRunArtifact(tests);
if (b.args) |args| {
run_tests.addArgs(args);
}
// step
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_tests.step);
// wpt
// -----
// compile and install
const wpt = b.addExecutable(.{
.name = "browsercore-wpt",
.root_source_file = b.path("src/main_wpt.zig"),
.target = target,
.optimize = mode,
.use_llvm = !x86,
.use_lld = !x86,
});
try common(b, wpt, options);
// run
const wpt_cmd = b.addRunArtifact(wpt);
if (b.args) |args| {
wpt_cmd.addArgs(args);
}
// step
const wpt_step = b.step("wpt", "WPT tests");
wpt_step.dependOn(&wpt_cmd.step);
// get
// -----
// compile and install
const get = b.addExecutable(.{
.name = "browsercore-get",
.root_source_file = b.path("src/main_get.zig"),
.target = target,
.optimize = mode,
.use_llvm = !x86,
.use_lld = !x86,
});
try common(b, get, options);
b.installArtifact(get);
// run
const get_cmd = b.addRunArtifact(get);
if (b.args) |args| {
get_cmd.addArgs(args);
}
// step
const get_step = b.step("get", "request URL");
get_step.dependOn(&get_cmd.step);
}
fn common(
b: *std.Build,
step: *std.Build.Step.Compile,
options: jsruntime.Options,
) !void {
const jsruntimemod = try jsruntime_pkgs.module(
b,
options,
step.root_module.optimize.?,
step.root_module.resolved_target.?,
);
step.root_module.addImport("jsruntime", jsruntimemod);
const netsurf = moduleNetSurf(b);
netsurf.addImport("jsruntime", jsruntimemod);
step.root_module.addImport("netsurf", netsurf);
const tlsmod = b.addModule("tls", .{
.root_source_file = b.path("vendor/tls.zig/src/main.zig"),
});
step.root_module.addImport("tls", tlsmod);
}
fn moduleNetSurf(b: *std.Build) *std.Build.Module {
const mod = b.addModule("netsurf", .{
.root_source_file = b.path("src/netsurf/netsurf.zig"),
});
// iconv
mod.addObjectFile(b.path("vendor/libiconv/lib/libiconv.a"));
mod.addIncludePath(b.path("vendor/libiconv/include"));
// mimalloc
mod.addImport("mimalloc", moduleMimalloc(b));
// netsurf libs
const ns = "vendor/netsurf";
mod.addIncludePath(b.path(ns ++ "/include"));
const libs: [4][]const u8 = .{
"libdom",
"libhubbub",
"libparserutils",
"libwapcaplet",
};
mod.addIncludePath(b.path("vendor/lightpanda"));
{
// v8
const v8_opts = b.addOptions();
v8_opts.addOption(bool, "inspector_subtype", false);
const v8_mod = b.dependency("v8", dep_opts).module("v8");
v8_mod.addOptions("default_exports", v8_opts);
mod.addImport("v8", v8_mod);
const release_dir = if (mod.optimize.? == .Debug) "debug" else "release";
const os = switch (target.result.os.tag) {
.linux => "linux",
.macos => "macos",
else => return error.UnsupportedPlatform,
};
var lib_path = try std.fmt.allocPrint(
mod.owner.allocator,
"v8/out/{s}/{s}/obj/zig/libc_v8.a",
.{ os, release_dir },
);
std.fs.cwd().access(lib_path, .{}) catch {
// legacy path
lib_path = try std.fmt.allocPrint(
mod.owner.allocator,
"v8/out/{s}/obj/zig/libc_v8.a",
.{release_dir},
);
};
mod.addObjectFile(mod.owner.path(lib_path));
switch (target.result.os.tag) {
.macos => {
// v8 has a dependency, abseil-cpp, which, on Mac, uses CoreFoundation
mod.addSystemFrameworkPath(.{ .cwd_relative = "/System/Library/Frameworks" });
mod.linkFramework("CoreFoundation", .{});
},
else => {},
}
inline for (libs) |lib| {
mod.addObjectFile(b.path(ns ++ "/lib/" ++ lib ++ ".a"));
mod.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src"));
}
{
//curl
{
const is_linux = target.result.os.tag == .linux;
if (is_linux) {
mod.addCMacro("HAVE_LINUX_TCP_H", "1");
mod.addCMacro("HAVE_MSG_NOSIGNAL", "1");
mod.addCMacro("HAVE_GETHOSTBYNAME_R", "1");
}
mod.addCMacro("_FILE_OFFSET_BITS", "64");
mod.addCMacro("BUILDING_LIBCURL", "1");
mod.addCMacro("CURL_DISABLE_AWS", "1");
mod.addCMacro("CURL_DISABLE_DICT", "1");
mod.addCMacro("CURL_DISABLE_DOH", "1");
mod.addCMacro("CURL_DISABLE_FILE", "1");
mod.addCMacro("CURL_DISABLE_FTP", "1");
mod.addCMacro("CURL_DISABLE_GOPHER", "1");
mod.addCMacro("CURL_DISABLE_KERBEROS", "1");
mod.addCMacro("CURL_DISABLE_IMAP", "1");
mod.addCMacro("CURL_DISABLE_IPFS", "1");
mod.addCMacro("CURL_DISABLE_LDAP", "1");
mod.addCMacro("CURL_DISABLE_LDAPS", "1");
mod.addCMacro("CURL_DISABLE_MQTT", "1");
mod.addCMacro("CURL_DISABLE_NTLM", "1");
mod.addCMacro("CURL_DISABLE_PROGRESS_METER", "1");
mod.addCMacro("CURL_DISABLE_POP3", "1");
mod.addCMacro("CURL_DISABLE_RTSP", "1");
mod.addCMacro("CURL_DISABLE_SMB", "1");
mod.addCMacro("CURL_DISABLE_SMTP", "1");
mod.addCMacro("CURL_DISABLE_TELNET", "1");
mod.addCMacro("CURL_DISABLE_TFTP", "1");
mod.addCMacro("CURL_EXTERN_SYMBOL", "__attribute__ ((__visibility__ (\"default\"))");
mod.addCMacro("CURL_OS", if (is_linux) "\"Linux\"" else "\"mac\"");
mod.addCMacro("CURL_STATICLIB", "1");
mod.addCMacro("ENABLE_IPV6", "1");
mod.addCMacro("HAVE_ALARM", "1");
mod.addCMacro("HAVE_ALLOCA_H", "1");
mod.addCMacro("HAVE_ARPA_INET_H", "1");
mod.addCMacro("HAVE_ARPA_TFTP_H", "1");
mod.addCMacro("HAVE_ASSERT_H", "1");
mod.addCMacro("HAVE_BASENAME", "1");
mod.addCMacro("HAVE_BOOL_T", "1");
mod.addCMacro("HAVE_BROTLI", "1");
mod.addCMacro("HAVE_BUILTIN_AVAILABLE", "1");
mod.addCMacro("HAVE_CLOCK_GETTIME_MONOTONIC", "1");
mod.addCMacro("HAVE_DLFCN_H", "1");
mod.addCMacro("HAVE_ERRNO_H", "1");
mod.addCMacro("HAVE_FCNTL", "1");
mod.addCMacro("HAVE_FCNTL_H", "1");
mod.addCMacro("HAVE_FCNTL_O_NONBLOCK", "1");
mod.addCMacro("HAVE_FREEADDRINFO", "1");
mod.addCMacro("HAVE_FSETXATTR", "1");
mod.addCMacro("HAVE_FSETXATTR_5", "1");
mod.addCMacro("HAVE_FTRUNCATE", "1");
mod.addCMacro("HAVE_GETADDRINFO", "1");
mod.addCMacro("HAVE_GETEUID", "1");
mod.addCMacro("HAVE_GETHOSTBYNAME", "1");
mod.addCMacro("HAVE_GETHOSTBYNAME_R_6", "1");
mod.addCMacro("HAVE_GETHOSTNAME", "1");
mod.addCMacro("HAVE_GETPEERNAME", "1");
mod.addCMacro("HAVE_GETPPID", "1");
mod.addCMacro("HAVE_GETPPID", "1");
mod.addCMacro("HAVE_GETPROTOBYNAME", "1");
mod.addCMacro("HAVE_GETPWUID", "1");
mod.addCMacro("HAVE_GETPWUID_R", "1");
mod.addCMacro("HAVE_GETRLIMIT", "1");
mod.addCMacro("HAVE_GETSOCKNAME", "1");
mod.addCMacro("HAVE_GETTIMEOFDAY", "1");
mod.addCMacro("HAVE_GMTIME_R", "1");
mod.addCMacro("HAVE_IDN2_H", "1");
mod.addCMacro("HAVE_IF_NAMETOINDEX", "1");
mod.addCMacro("HAVE_IFADDRS_H", "1");
mod.addCMacro("HAVE_INET_ADDR", "1");
mod.addCMacro("HAVE_INET_PTON", "1");
mod.addCMacro("HAVE_INTTYPES_H", "1");
mod.addCMacro("HAVE_IOCTL", "1");
mod.addCMacro("HAVE_IOCTL_FIONBIO", "1");
mod.addCMacro("HAVE_IOCTL_SIOCGIFADDR", "1");
mod.addCMacro("HAVE_LDAP_URL_PARSE", "1");
mod.addCMacro("HAVE_LIBGEN_H", "1");
mod.addCMacro("HAVE_LIBZ", "1");
mod.addCMacro("HAVE_LL", "1");
mod.addCMacro("HAVE_LOCALE_H", "1");
mod.addCMacro("HAVE_LOCALTIME_R", "1");
mod.addCMacro("HAVE_LONGLONG", "1");
mod.addCMacro("HAVE_MALLOC_H", "1");
mod.addCMacro("HAVE_MEMORY_H", "1");
mod.addCMacro("HAVE_NET_IF_H", "1");
mod.addCMacro("HAVE_NETDB_H", "1");
mod.addCMacro("HAVE_NETINET_IN_H", "1");
mod.addCMacro("HAVE_NETINET_TCP_H", "1");
mod.addCMacro("HAVE_PIPE", "1");
mod.addCMacro("HAVE_POLL", "1");
mod.addCMacro("HAVE_POLL_FINE", "1");
mod.addCMacro("HAVE_POLL_H", "1");
mod.addCMacro("HAVE_POSIX_STRERROR_R", "1");
mod.addCMacro("HAVE_PTHREAD_H", "1");
mod.addCMacro("HAVE_PWD_H", "1");
mod.addCMacro("HAVE_RECV", "1");
mod.addCMacro("HAVE_SA_FAMILY_T", "1");
mod.addCMacro("HAVE_SELECT", "1");
mod.addCMacro("HAVE_SEND", "1");
mod.addCMacro("HAVE_SETJMP_H", "1");
mod.addCMacro("HAVE_SETLOCALE", "1");
mod.addCMacro("HAVE_SETRLIMIT", "1");
mod.addCMacro("HAVE_SETSOCKOPT", "1");
mod.addCMacro("HAVE_SIGACTION", "1");
mod.addCMacro("HAVE_SIGINTERRUPT", "1");
mod.addCMacro("HAVE_SIGNAL", "1");
mod.addCMacro("HAVE_SIGNAL_H", "1");
mod.addCMacro("HAVE_SIGSETJMP", "1");
mod.addCMacro("HAVE_SOCKADDR_IN6_SIN6_SCOPE_ID", "1");
mod.addCMacro("HAVE_SOCKET", "1");
mod.addCMacro("HAVE_STDBOOL_H", "1");
mod.addCMacro("HAVE_STDINT_H", "1");
mod.addCMacro("HAVE_STDIO_H", "1");
mod.addCMacro("HAVE_STDLIB_H", "1");
mod.addCMacro("HAVE_STRCASECMP", "1");
mod.addCMacro("HAVE_STRDUP", "1");
mod.addCMacro("HAVE_STRERROR_R", "1");
mod.addCMacro("HAVE_STRING_H", "1");
mod.addCMacro("HAVE_STRINGS_H", "1");
mod.addCMacro("HAVE_STRSTR", "1");
mod.addCMacro("HAVE_STRTOK_R", "1");
mod.addCMacro("HAVE_STRTOLL", "1");
mod.addCMacro("HAVE_STRUCT_SOCKADDR_STORAGE", "1");
mod.addCMacro("HAVE_STRUCT_TIMEVAL", "1");
mod.addCMacro("HAVE_SYS_IOCTL_H", "1");
mod.addCMacro("HAVE_SYS_PARAM_H", "1");
mod.addCMacro("HAVE_SYS_POLL_H", "1");
mod.addCMacro("HAVE_SYS_RESOURCE_H", "1");
mod.addCMacro("HAVE_SYS_SELECT_H", "1");
mod.addCMacro("HAVE_SYS_SOCKET_H", "1");
mod.addCMacro("HAVE_SYS_STAT_H", "1");
mod.addCMacro("HAVE_SYS_TIME_H", "1");
mod.addCMacro("HAVE_SYS_TYPES_H", "1");
mod.addCMacro("HAVE_SYS_UIO_H", "1");
mod.addCMacro("HAVE_SYS_UN_H", "1");
mod.addCMacro("HAVE_TERMIO_H", "1");
mod.addCMacro("HAVE_TERMIOS_H", "1");
mod.addCMacro("HAVE_TIME_H", "1");
mod.addCMacro("HAVE_UNAME", "1");
mod.addCMacro("HAVE_UNISTD_H", "1");
mod.addCMacro("HAVE_UTIME", "1");
mod.addCMacro("HAVE_UTIME_H", "1");
mod.addCMacro("HAVE_UTIMES", "1");
mod.addCMacro("HAVE_VARIADIC_MACROS_C99", "1");
mod.addCMacro("HAVE_VARIADIC_MACROS_GCC", "1");
mod.addCMacro("HAVE_ZLIB_H", "1");
mod.addCMacro("RANDOM_FILE", "\"/dev/urandom\"");
mod.addCMacro("RECV_TYPE_ARG1", "int");
mod.addCMacro("RECV_TYPE_ARG2", "void *");
mod.addCMacro("RECV_TYPE_ARG3", "size_t");
mod.addCMacro("RECV_TYPE_ARG4", "int");
mod.addCMacro("RECV_TYPE_RETV", "ssize_t");
mod.addCMacro("SEND_QUAL_ARG2", "const");
mod.addCMacro("SEND_TYPE_ARG1", "int");
mod.addCMacro("SEND_TYPE_ARG2", "void *");
mod.addCMacro("SEND_TYPE_ARG3", "size_t");
mod.addCMacro("SEND_TYPE_ARG4", "int");
mod.addCMacro("SEND_TYPE_RETV", "ssize_t");
mod.addCMacro("SIZEOF_CURL_OFF_T", "8");
mod.addCMacro("SIZEOF_INT", "4");
mod.addCMacro("SIZEOF_LONG", "8");
mod.addCMacro("SIZEOF_OFF_T", "8");
mod.addCMacro("SIZEOF_SHORT", "2");
mod.addCMacro("SIZEOF_SIZE_T", "8");
mod.addCMacro("SIZEOF_TIME_T", "8");
mod.addCMacro("STDC_HEADERS", "1");
mod.addCMacro("TIME_WITH_SYS_TIME", "1");
mod.addCMacro("USE_NGHTTP2", "1");
mod.addCMacro("USE_MBEDTLS", "1");
mod.addCMacro("USE_THREADS_POSIX", "1");
mod.addCMacro("USE_UNIX_SOCKETS", "1");
}
try buildZlib(b, mod);
try buildBrotli(b, mod);
try buildMbedtls(b, mod);
try buildNghttp2(b, mod);
try buildCurl(b, mod);
switch (target.result.os.tag) {
.macos => {
// needed for proxying on mac
mod.addSystemFrameworkPath(.{ .cwd_relative = "/System/Library/Frameworks" });
mod.linkFramework("CoreFoundation", .{});
mod.linkFramework("SystemConfiguration", .{});
},
else => {},
}
}
return mod;
}
fn buildZlib(b: *Build, m: *Build.Module) !void {
const zlib = b.addLibrary(.{
.name = "zlib",
.root_module = m,
fn moduleMimalloc(b: *std.Build) *std.Build.Module {
const mod = b.addModule("mimalloc", .{
.root_source_file = b.path("src/mimalloc/mimalloc.zig"),
});
const root = "vendor/zlib/";
zlib.installHeader(b.path(root ++ "zlib.h"), "zlib.h");
zlib.installHeader(b.path(root ++ "zconf.h"), "zconf.h");
zlib.addCSourceFiles(.{ .flags = &.{
"-DHAVE_SYS_TYPES_H",
"-DHAVE_STDINT_H",
"-DHAVE_STDDEF_H",
}, .files = &.{
root ++ "adler32.c",
root ++ "compress.c",
root ++ "crc32.c",
root ++ "deflate.c",
root ++ "gzclose.c",
root ++ "gzlib.c",
root ++ "gzread.c",
root ++ "gzwrite.c",
root ++ "inflate.c",
root ++ "infback.c",
root ++ "inftrees.c",
root ++ "inffast.c",
root ++ "trees.c",
root ++ "uncompr.c",
root ++ "zutil.c",
} });
}
fn 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 {
const mbedtls = b.addLibrary(.{
.name = "mbedtls",
.root_module = m,
});
const root = "vendor/mbedtls/";
mbedtls.addIncludePath(b.path(root ++ "include"));
mbedtls.addIncludePath(b.path(root ++ "library"));
mbedtls.addCSourceFiles(.{ .flags = &.{}, .files = &.{
root ++ "library/aes.c",
root ++ "library/aesni.c",
root ++ "library/aesce.c",
root ++ "library/aria.c",
root ++ "library/asn1parse.c",
root ++ "library/asn1write.c",
root ++ "library/base64.c",
root ++ "library/bignum.c",
root ++ "library/bignum_core.c",
root ++ "library/bignum_mod.c",
root ++ "library/bignum_mod_raw.c",
root ++ "library/camellia.c",
root ++ "library/ccm.c",
root ++ "library/chacha20.c",
root ++ "library/chachapoly.c",
root ++ "library/cipher.c",
root ++ "library/cipher_wrap.c",
root ++ "library/constant_time.c",
root ++ "library/cmac.c",
root ++ "library/ctr_drbg.c",
root ++ "library/des.c",
root ++ "library/dhm.c",
root ++ "library/ecdh.c",
root ++ "library/ecdsa.c",
root ++ "library/ecjpake.c",
root ++ "library/ecp.c",
root ++ "library/ecp_curves.c",
root ++ "library/entropy.c",
root ++ "library/entropy_poll.c",
root ++ "library/error.c",
root ++ "library/gcm.c",
root ++ "library/hkdf.c",
root ++ "library/hmac_drbg.c",
root ++ "library/lmots.c",
root ++ "library/lms.c",
root ++ "library/md.c",
root ++ "library/md5.c",
root ++ "library/memory_buffer_alloc.c",
root ++ "library/nist_kw.c",
root ++ "library/oid.c",
root ++ "library/padlock.c",
root ++ "library/pem.c",
root ++ "library/pk.c",
root ++ "library/pk_ecc.c",
root ++ "library/pk_wrap.c",
root ++ "library/pkcs12.c",
root ++ "library/pkcs5.c",
root ++ "library/pkparse.c",
root ++ "library/pkwrite.c",
root ++ "library/platform.c",
root ++ "library/platform_util.c",
root ++ "library/poly1305.c",
root ++ "library/psa_crypto.c",
root ++ "library/psa_crypto_aead.c",
root ++ "library/psa_crypto_cipher.c",
root ++ "library/psa_crypto_client.c",
root ++ "library/psa_crypto_ffdh.c",
root ++ "library/psa_crypto_driver_wrappers_no_static.c",
root ++ "library/psa_crypto_ecp.c",
root ++ "library/psa_crypto_hash.c",
root ++ "library/psa_crypto_mac.c",
root ++ "library/psa_crypto_pake.c",
root ++ "library/psa_crypto_rsa.c",
root ++ "library/psa_crypto_se.c",
root ++ "library/psa_crypto_slot_management.c",
root ++ "library/psa_crypto_storage.c",
root ++ "library/psa_its_file.c",
root ++ "library/psa_util.c",
root ++ "library/ripemd160.c",
root ++ "library/rsa.c",
root ++ "library/rsa_alt_helpers.c",
root ++ "library/sha1.c",
root ++ "library/sha3.c",
root ++ "library/sha256.c",
root ++ "library/sha512.c",
root ++ "library/threading.c",
root ++ "library/timing.c",
root ++ "library/version.c",
root ++ "library/version_features.c",
root ++ "library/pkcs7.c",
root ++ "library/x509.c",
root ++ "library/x509_create.c",
root ++ "library/x509_crl.c",
root ++ "library/x509_crt.c",
root ++ "library/x509_csr.c",
root ++ "library/x509write.c",
root ++ "library/x509write_crt.c",
root ++ "library/x509write_csr.c",
root ++ "library/debug.c",
root ++ "library/mps_reader.c",
root ++ "library/mps_trace.c",
root ++ "library/net_sockets.c",
root ++ "library/ssl_cache.c",
root ++ "library/ssl_ciphersuites.c",
root ++ "library/ssl_client.c",
root ++ "library/ssl_cookie.c",
root ++ "library/ssl_debug_helpers_generated.c",
root ++ "library/ssl_msg.c",
root ++ "library/ssl_ticket.c",
root ++ "library/ssl_tls.c",
root ++ "library/ssl_tls12_client.c",
root ++ "library/ssl_tls12_server.c",
root ++ "library/ssl_tls13_keys.c",
root ++ "library/ssl_tls13_server.c",
root ++ "library/ssl_tls13_client.c",
root ++ "library/ssl_tls13_generic.c",
} });
}
fn buildNghttp2(b: *Build, m: *Build.Module) !void {
const nghttp2 = b.addLibrary(.{
.name = "nghttp2",
.root_module = m,
});
const root = "vendor/nghttp2/";
nghttp2.addIncludePath(b.path(root ++ "lib"));
nghttp2.addIncludePath(b.path(root ++ "lib/includes"));
nghttp2.addCSourceFiles(.{ .flags = &.{
"-DNGHTTP2_STATICLIB",
"-DHAVE_NETINET_IN",
"-DHAVE_TIME_H",
}, .files = &.{
root ++ "lib/sfparse.c",
root ++ "lib/nghttp2_alpn.c",
root ++ "lib/nghttp2_buf.c",
root ++ "lib/nghttp2_callbacks.c",
root ++ "lib/nghttp2_debug.c",
root ++ "lib/nghttp2_extpri.c",
root ++ "lib/nghttp2_frame.c",
root ++ "lib/nghttp2_hd.c",
root ++ "lib/nghttp2_hd_huffman.c",
root ++ "lib/nghttp2_hd_huffman_data.c",
root ++ "lib/nghttp2_helper.c",
root ++ "lib/nghttp2_http.c",
root ++ "lib/nghttp2_map.c",
root ++ "lib/nghttp2_mem.c",
root ++ "lib/nghttp2_option.c",
root ++ "lib/nghttp2_outbound_item.c",
root ++ "lib/nghttp2_pq.c",
root ++ "lib/nghttp2_priority_spec.c",
root ++ "lib/nghttp2_queue.c",
root ++ "lib/nghttp2_rcbuf.c",
root ++ "lib/nghttp2_session.c",
root ++ "lib/nghttp2_stream.c",
root ++ "lib/nghttp2_submit.c",
root ++ "lib/nghttp2_version.c",
root ++ "lib/nghttp2_ratelim.c",
root ++ "lib/nghttp2_time.c",
} });
}
fn buildCurl(b: *Build, m: *Build.Module) !void {
const curl = b.addLibrary(.{
.name = "curl",
.root_module = m,
});
const root = "vendor/curl/";
curl.addIncludePath(b.path(root ++ "lib"));
curl.addIncludePath(b.path(root ++ "include"));
curl.addCSourceFiles(.{
.flags = &.{},
.files = &.{
root ++ "lib/altsvc.c",
root ++ "lib/amigaos.c",
root ++ "lib/asyn-ares.c",
root ++ "lib/asyn-base.c",
root ++ "lib/asyn-thrdd.c",
root ++ "lib/bufq.c",
root ++ "lib/bufref.c",
root ++ "lib/cf-h1-proxy.c",
root ++ "lib/cf-h2-proxy.c",
root ++ "lib/cf-haproxy.c",
root ++ "lib/cf-https-connect.c",
root ++ "lib/cf-socket.c",
root ++ "lib/cfilters.c",
root ++ "lib/conncache.c",
root ++ "lib/connect.c",
root ++ "lib/content_encoding.c",
root ++ "lib/cookie.c",
root ++ "lib/cshutdn.c",
root ++ "lib/curl_addrinfo.c",
root ++ "lib/curl_des.c",
root ++ "lib/curl_endian.c",
root ++ "lib/curl_fnmatch.c",
root ++ "lib/curl_get_line.c",
root ++ "lib/curl_gethostname.c",
root ++ "lib/curl_gssapi.c",
root ++ "lib/curl_memrchr.c",
root ++ "lib/curl_ntlm_core.c",
root ++ "lib/curl_range.c",
root ++ "lib/curl_rtmp.c",
root ++ "lib/curl_sasl.c",
root ++ "lib/curl_sha512_256.c",
root ++ "lib/curl_sspi.c",
root ++ "lib/curl_threads.c",
root ++ "lib/curl_trc.c",
root ++ "lib/cw-out.c",
root ++ "lib/cw-pause.c",
root ++ "lib/dict.c",
root ++ "lib/doh.c",
root ++ "lib/dynhds.c",
root ++ "lib/easy.c",
root ++ "lib/easygetopt.c",
root ++ "lib/easyoptions.c",
root ++ "lib/escape.c",
root ++ "lib/fake_addrinfo.c",
root ++ "lib/file.c",
root ++ "lib/fileinfo.c",
root ++ "lib/fopen.c",
root ++ "lib/formdata.c",
root ++ "lib/ftp.c",
root ++ "lib/ftplistparser.c",
root ++ "lib/getenv.c",
root ++ "lib/getinfo.c",
root ++ "lib/gopher.c",
root ++ "lib/hash.c",
root ++ "lib/headers.c",
root ++ "lib/hmac.c",
root ++ "lib/hostip.c",
root ++ "lib/hostip4.c",
root ++ "lib/hostip6.c",
root ++ "lib/hsts.c",
root ++ "lib/http.c",
root ++ "lib/http1.c",
root ++ "lib/http2.c",
root ++ "lib/http_aws_sigv4.c",
root ++ "lib/http_chunks.c",
root ++ "lib/http_digest.c",
root ++ "lib/http_negotiate.c",
root ++ "lib/http_ntlm.c",
root ++ "lib/http_proxy.c",
root ++ "lib/httpsrr.c",
root ++ "lib/idn.c",
root ++ "lib/if2ip.c",
root ++ "lib/imap.c",
root ++ "lib/krb5.c",
root ++ "lib/ldap.c",
root ++ "lib/llist.c",
root ++ "lib/macos.c",
root ++ "lib/md4.c",
root ++ "lib/md5.c",
root ++ "lib/memdebug.c",
root ++ "lib/mime.c",
root ++ "lib/mprintf.c",
root ++ "lib/mqtt.c",
root ++ "lib/multi.c",
root ++ "lib/multi_ev.c",
root ++ "lib/netrc.c",
root ++ "lib/noproxy.c",
root ++ "lib/openldap.c",
root ++ "lib/parsedate.c",
root ++ "lib/pingpong.c",
root ++ "lib/pop3.c",
root ++ "lib/progress.c",
root ++ "lib/psl.c",
root ++ "lib/rand.c",
root ++ "lib/rename.c",
root ++ "lib/request.c",
root ++ "lib/rtsp.c",
root ++ "lib/select.c",
root ++ "lib/sendf.c",
root ++ "lib/setopt.c",
root ++ "lib/sha256.c",
root ++ "lib/share.c",
root ++ "lib/slist.c",
root ++ "lib/smb.c",
root ++ "lib/smtp.c",
root ++ "lib/socketpair.c",
root ++ "lib/socks.c",
root ++ "lib/socks_gssapi.c",
root ++ "lib/socks_sspi.c",
root ++ "lib/speedcheck.c",
root ++ "lib/splay.c",
root ++ "lib/strcase.c",
root ++ "lib/strdup.c",
root ++ "lib/strequal.c",
root ++ "lib/strerror.c",
root ++ "lib/system_win32.c",
root ++ "lib/telnet.c",
root ++ "lib/tftp.c",
root ++ "lib/transfer.c",
root ++ "lib/uint-bset.c",
root ++ "lib/uint-hash.c",
root ++ "lib/uint-spbset.c",
root ++ "lib/uint-table.c",
root ++ "lib/url.c",
root ++ "lib/urlapi.c",
root ++ "lib/version.c",
root ++ "lib/ws.c",
root ++ "lib/curlx/base64.c",
root ++ "lib/curlx/dynbuf.c",
root ++ "lib/curlx/inet_ntop.c",
root ++ "lib/curlx/nonblock.c",
root ++ "lib/curlx/strparse.c",
root ++ "lib/curlx/timediff.c",
root ++ "lib/curlx/timeval.c",
root ++ "lib/curlx/wait.c",
root ++ "lib/curlx/warnless.c",
root ++ "lib/vquic/curl_ngtcp2.c",
root ++ "lib/vquic/curl_osslq.c",
root ++ "lib/vquic/curl_quiche.c",
root ++ "lib/vquic/vquic.c",
root ++ "lib/vquic/vquic-tls.c",
root ++ "lib/vauth/cleartext.c",
root ++ "lib/vauth/cram.c",
root ++ "lib/vauth/digest.c",
root ++ "lib/vauth/digest_sspi.c",
root ++ "lib/vauth/gsasl.c",
root ++ "lib/vauth/krb5_gssapi.c",
root ++ "lib/vauth/krb5_sspi.c",
root ++ "lib/vauth/ntlm.c",
root ++ "lib/vauth/ntlm_sspi.c",
root ++ "lib/vauth/oauth2.c",
root ++ "lib/vauth/spnego_gssapi.c",
root ++ "lib/vauth/spnego_sspi.c",
root ++ "lib/vauth/vauth.c",
root ++ "lib/vtls/cipher_suite.c",
root ++ "lib/vtls/mbedtls.c",
root ++ "lib/vtls/mbedtls_threadlock.c",
root ++ "lib/vtls/vtls.c",
root ++ "lib/vtls/vtls_scache.c",
root ++ "lib/vtls/x509asn1.c",
},
});
mod.addObjectFile(b.path("vendor/mimalloc/out/libmimalloc.a"));
mod.addIncludePath(b.path("vendor/mimalloc/out/include"));
return mod;
}

View File

@@ -1,13 +0,0 @@
.{
.name = .browser,
.paths = .{""},
.version = "0.0.0",
.fingerprint = 0xda130f3af836cea0,
.dependencies = .{
.v8 = .{
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/305bb3706716d32d59b2ffa674731556caa1002b.tar.gz",
.hash = "v8-0.0.0-xddH63bVAwBSEobaUok9J0er1FqsvEujCDDVy6ItqKQ5",
},
//.v8 = .{ .path = "../zig-v8-fork" }
},
}

180
flake.lock generated
View File

@@ -1,180 +0,0 @@
{
"nodes": {
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1705309234,
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"zlsPkg",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1756822655,
"narHash": "sha256-xQAk8xLy7srAkR5NMZFsQFioL02iTHuuEIs3ohGpgdk=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "4bdac60bfe32c41103ae500ddf894c258291dd61",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "release-25.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"zigPkgs": "zigPkgs",
"zlsPkg": "zlsPkg"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"zigPkgs": {
"inputs": {
"flake-compat": "flake-compat",
"flake-utils": "flake-utils_2",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1756555914,
"narHash": "sha256-7yoSPIVEuL+3Wzf6e7NHuW3zmruHizRrYhGerjRHTLI=",
"owner": "mitchellh",
"repo": "zig-overlay",
"rev": "d0df3a2fd0f11134409d6d5ea0e510e5e477f7d6",
"type": "github"
},
"original": {
"owner": "mitchellh",
"repo": "zig-overlay",
"type": "github"
}
},
"zlsPkg": {
"inputs": {
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
],
"zig-overlay": [
"zigPkgs"
]
},
"locked": {
"lastModified": 1756048867,
"narHash": "sha256-GFzSHUljcxy7sM1PaabbkQUdUnLwpherekPWJFxXtnk=",
"owner": "zigtools",
"repo": "zls",
"rev": "ce6c8f02c78e622421cfc2405c67c5222819ec03",
"type": "github"
},
"original": {
"owner": "zigtools",
"ref": "0.15.0",
"repo": "zls",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -1,77 +0,0 @@
{
description = "headless browser designed for AI and automation";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/release-25.05";
zigPkgs.url = "github:mitchellh/zig-overlay";
zigPkgs.inputs.nixpkgs.follows = "nixpkgs";
zlsPkg.url = "github:zigtools/zls/0.15.0";
zlsPkg.inputs.zig-overlay.follows = "zigPkgs";
zlsPkg.inputs.nixpkgs.follows = "nixpkgs";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{
nixpkgs,
zigPkgs,
zlsPkg,
flake-utils,
...
}:
flake-utils.lib.eachDefaultSystem (
system:
let
overlays = [
(final: prev: {
zigpkgs = zigPkgs.packages.${prev.system};
zls = zlsPkg.packages.${prev.system}.default;
})
];
pkgs = import nixpkgs {
inherit system overlays;
};
# We need crtbeginS.o for building.
crtFiles = pkgs.runCommand "crt-files" { } ''
mkdir -p $out/lib
cp -r ${pkgs.gcc.cc}/lib/gcc $out/lib/gcc
'';
# This build pipeline is very unhappy without an FHS-compliant env.
fhs = pkgs.buildFHSEnv {
name = "fhs-shell";
multiArch = true;
targetPkgs =
pkgs: with pkgs; [
# Build Tools
zigpkgs."0.15.2"
zls
python3
pkg-config
cmake
gperf
# GCC
gcc
gcc.cc.lib
crtFiles
# Libaries
expat.dev
glib.dev
glibc.dev
zlib
zlib.dev
];
};
in
{
devShells.default = fhs.env;
}
);
}

View File

@@ -1,109 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const log = @import("log.zig");
const Http = @import("http/Http.zig");
const Platform = @import("browser/js/Platform.zig");
const Notification = @import("Notification.zig");
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
// Container for global state / objects that various parts of the system
// might need.
const App = @This();
http: Http,
config: Config,
platform: Platform,
telemetry: Telemetry,
allocator: Allocator,
app_dir_path: ?[]const u8,
notification: *Notification,
pub const RunMode = enum {
help,
fetch,
serve,
version,
};
pub const Config = struct {
run_mode: RunMode,
tls_verify_host: bool = true,
http_proxy: ?[:0]const u8 = null,
proxy_bearer_token: ?[:0]const u8 = null,
http_timeout_ms: ?u31 = null,
http_connect_timeout_ms: ?u31 = null,
http_max_host_open: ?u8 = null,
http_max_concurrent: ?u8 = null,
user_agent: [:0]const u8,
};
pub fn init(allocator: Allocator, config: Config) !*App {
const app = try allocator.create(App);
errdefer allocator.destroy(app);
app.config = config;
app.allocator = allocator;
app.notification = try Notification.init(allocator, null);
errdefer app.notification.deinit();
app.http = try Http.init(allocator, .{
.max_host_open = config.http_max_host_open orelse 4,
.max_concurrent = config.http_max_concurrent orelse 10,
.timeout_ms = config.http_timeout_ms orelse 5000,
.connect_timeout_ms = config.http_connect_timeout_ms orelse 0,
.http_proxy = config.http_proxy,
.tls_verify_host = config.tls_verify_host,
.proxy_bearer_token = config.proxy_bearer_token,
.user_agent = config.user_agent,
});
errdefer app.http.deinit();
app.platform = try Platform.init();
errdefer app.platform.deinit();
app.app_dir_path = getAndMakeAppDir(allocator);
app.telemetry = try Telemetry.init(app, config.run_mode);
errdefer app.telemetry.deinit();
try app.telemetry.register(app.notification);
return app;
}
pub fn deinit(self: *App) void {
const allocator = self.allocator;
if (self.app_dir_path) |app_dir_path| {
allocator.free(app_dir_path);
}
self.telemetry.deinit();
self.notification.deinit();
self.http.deinit();
self.platform.deinit();
allocator.destroy(self);
}
fn getAndMakeAppDir(allocator: Allocator) ?[]const u8 {
if (@import("builtin").is_test) {
return allocator.dupe(u8, "/tmp") catch unreachable;
}
const app_dir_path = std.fs.getAppDataDir(allocator, "lightpanda") catch |err| {
log.warn(.app, "get data dir", .{ .err = err });
return null;
};
std.fs.cwd().makePath(app_dir_path) catch |err| switch (err) {
error.PathAlreadyExists => return app_dir_path,
else => {
allocator.free(app_dir_path);
log.warn(.app, "create data dir", .{ .err = err, .path = app_dir_path });
return null;
},
};
return app_dir_path;
}

View File

@@ -1,386 +0,0 @@
const std = @import("std");
const log = @import("log.zig");
const Page = @import("browser/Page.zig");
const Transfer = @import("http/Client.zig").Transfer;
const Allocator = std.mem.Allocator;
const List = std.DoublyLinkedList;
// Allows code to register for and emit events.
// Keeps two lists
// 1 - for a given event type, a linked list of all the listeners
// 2 - for a given listener, a list of all it's registration
// The 2nd one is so that a listener can unregister all of it's listeners
// (there's currently no need for a listener to unregister only 1 or more
// specific listener).
//
// Scoping is important. Imagine we created a global singleton registry, and our
// CDP code registers for the "network_bytes_sent" event, because it needs to
// send messages to the client when this happens. Our HTTP client could then
// emit a "network_bytes_sent" message. It would be easy, and it would work.
// That is, it would work until the Telemetry code makes an HTTP request, and
// because everything's just one big global, that gets picked up by the
// registered CDP listener, and the telemetry network activity gets sent to the
// CDP client.
//
// To avoid this, one way or another, we need scoping. We could still have
// a global registry but every "register" and every "emit" has some type of
// "scope". This would have a run-time cost and still require some coordination
// between components to share a common scope.
//
// Instead, the approach that we take is to have a notification instance per
// scope. This makes some things harder, but we only plan on having 2
// notification instances at a given time: one in a Browser and one in the App.
// What about something like Telemetry, which lives outside of a Browser but
// still cares about Browser-events (like .page_navigate)? When the Browser
// notification is created, a `notification_created` event is raised in the
// App's notification, which Telemetry is registered for. This allows Telemetry
// to register for events in the Browser notification. See the Telemetry's
// register function.
const Notification = @This();
// Every event type (which are hard-coded), has a list of Listeners.
// When the event happens, we dispatch to those listener.
event_listeners: EventListeners,
// list of listeners for a specified receiver
// @intFromPtr(receiver) -> [listener1, listener2, ...]
// Used when `unregisterAll` is called.
listeners: std.AutoHashMapUnmanaged(usize, std.ArrayListUnmanaged(*Listener)),
allocator: Allocator,
mem_pool: std.heap.MemoryPool(Listener),
const EventListeners = struct {
page_remove: List = .{},
page_created: List = .{},
page_navigate: List = .{},
page_navigated: List = .{},
page_network_idle: List = .{},
page_network_almost_idle: List = .{},
http_request_fail: List = .{},
http_request_start: List = .{},
http_request_intercept: List = .{},
http_request_done: List = .{},
http_request_auth_required: List = .{},
http_response_data: List = .{},
http_response_header_done: List = .{},
notification_created: List = .{},
};
const Events = union(enum) {
page_remove: PageRemove,
page_created: *Page,
page_navigate: *const PageNavigate,
page_navigated: *const PageNavigated,
page_network_idle: *const PageNetworkIdle,
page_network_almost_idle: *const PageNetworkAlmostIdle,
http_request_fail: *const RequestFail,
http_request_start: *const RequestStart,
http_request_intercept: *const RequestIntercept,
http_request_auth_required: *const RequestAuthRequired,
http_request_done: *const RequestDone,
http_response_data: *const ResponseData,
http_response_header_done: *const ResponseHeaderDone,
notification_created: *Notification,
};
const EventType = std.meta.FieldEnum(Events);
pub const PageRemove = struct {};
pub const PageNavigate = struct {
timestamp: u64,
url: []const u8,
opts: Page.NavigateOpts,
};
pub const PageNavigated = struct {
timestamp: u64,
url: []const u8,
};
pub const PageNetworkIdle = struct {
timestamp: u64,
};
pub const PageNetworkAlmostIdle = struct {
timestamp: u64,
};
pub const RequestStart = struct {
transfer: *Transfer,
};
pub const RequestIntercept = struct {
transfer: *Transfer,
wait_for_interception: *bool,
};
pub const RequestAuthRequired = struct {
transfer: *Transfer,
wait_for_interception: *bool,
};
pub const ResponseData = struct {
data: []const u8,
transfer: *Transfer,
};
pub const ResponseHeaderDone = struct {
transfer: *Transfer,
};
pub const RequestDone = struct {
transfer: *Transfer,
};
pub const RequestFail = struct {
transfer: *Transfer,
err: anyerror,
};
pub fn init(allocator: Allocator, parent: ?*Notification) !*Notification {
// This is put on the heap because we want to raise a .notification_created
// event, so that, something like Telemetry, can receive the
// .page_navigate event on all notification instances. That can only work
// if we dispatch .notification_created with a *Notification.
const notification = try allocator.create(Notification);
errdefer allocator.destroy(notification);
notification.* = .{
.listeners = .{},
.event_listeners = .{},
.allocator = allocator,
.mem_pool = std.heap.MemoryPool(Listener).init(allocator),
};
if (parent) |pn| {
pn.dispatch(.notification_created, notification);
}
return notification;
}
pub fn deinit(self: *Notification) void {
const allocator = self.allocator;
var it = self.listeners.valueIterator();
while (it.next()) |listener| {
listener.deinit(allocator);
}
self.listeners.deinit(allocator);
self.mem_pool.deinit();
allocator.destroy(self);
}
pub fn register(self: *Notification, comptime event: EventType, receiver: anytype, func: EventFunc(event)) !void {
var list = &@field(self.event_listeners, @tagName(event));
var listener = try self.mem_pool.create();
errdefer self.mem_pool.destroy(listener);
listener.* = .{
.node = .{},
.list = list,
.receiver = receiver,
.event = event,
.func = @ptrCast(func),
.struct_name = @typeName(@typeInfo(@TypeOf(receiver)).pointer.child),
};
const allocator = self.allocator;
const gop = try self.listeners.getOrPut(allocator, @intFromPtr(receiver));
if (gop.found_existing == false) {
gop.value_ptr.* = .{};
}
try gop.value_ptr.append(allocator, listener);
// we don't add this until we've successfully added the entry to
// self.listeners
list.append(&listener.node);
}
pub fn unregister(self: *Notification, comptime event: EventType, receiver: anytype) void {
var listeners = self.listeners.getPtr(@intFromPtr(receiver)) orelse return;
var i: usize = 0;
while (i < listeners.items.len) {
const listener = listeners.items[i];
if (listener.event != event) {
i += 1;
continue;
}
listener.list.remove(&listener.node);
self.mem_pool.destroy(listener);
_ = listeners.swapRemove(i);
}
if (listeners.items.len == 0) {
listeners.deinit(self.allocator);
const removed = self.listeners.remove(@intFromPtr(receiver));
std.debug.assert(removed == true);
}
}
pub fn unregisterAll(self: *Notification, receiver: *anyopaque) void {
var kv = self.listeners.fetchRemove(@intFromPtr(receiver)) orelse return;
for (kv.value.items) |listener| {
listener.list.remove(&listener.node);
self.mem_pool.destroy(listener);
}
kv.value.deinit(self.allocator);
}
pub fn dispatch(self: *Notification, comptime event: EventType, data: ArgType(event)) void {
const list = &@field(self.event_listeners, @tagName(event));
var node = list.first;
while (node) |n| {
const listener: *Listener = @fieldParentPtr("node", n);
const func: EventFunc(event) = @ptrCast(@alignCast(listener.func));
func(listener.receiver, data) catch |err| {
log.err(.app, "dispatch error", .{
.err = err,
.event = event,
.source = "notification",
.listener = listener.struct_name,
});
};
node = n.next;
}
}
// Given an event type enum, returns the type of arg the event emits
fn ArgType(comptime event: Notification.EventType) type {
inline for (std.meta.fields(Notification.Events)) |f| {
if (std.mem.eql(u8, f.name, @tagName(event))) {
return f.type;
}
}
unreachable;
}
// Given an event type enum, returns the listening function type
fn EventFunc(comptime event: Notification.EventType) type {
return *const fn (*anyopaque, ArgType(event)) anyerror!void;
}
// A listener. This is 1 receiver, with its function, and the linked list
// node that goes in the appropriate EventListeners list.
const Listener = struct {
// the receiver of the event, i.e. the self parameter to `func`
receiver: *anyopaque,
// the function to call
func: *const anyopaque,
// For logging slightly better error
struct_name: []const u8,
event: Notification.EventType,
// intrusive linked list node
node: List.Node,
// The event list this listener belongs to.
// We need this in order to be able to remove the node from the list
list: *List,
};
const testing = std.testing;
test "Notification" {
var notifier = try Notification.init(testing.allocator, null);
defer notifier.deinit();
// noop
notifier.dispatch(.page_navigate, &.{
.timestamp = 4,
.url = undefined,
.opts = .{},
});
var tc = TestClient{};
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
notifier.dispatch(.page_navigate, &.{
.timestamp = 4,
.url = undefined,
.opts = .{},
});
try testing.expectEqual(4, tc.page_navigate);
notifier.unregisterAll(&tc);
notifier.dispatch(.page_navigate, &.{
.timestamp = 10,
.url = undefined,
.opts = .{},
});
try testing.expectEqual(4, tc.page_navigate);
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
notifier.dispatch(.page_navigate, &.{
.timestamp = 10,
.url = undefined,
.opts = .{},
});
notifier.dispatch(.page_navigated, &.{ .timestamp = 6, .url = undefined });
try testing.expectEqual(14, tc.page_navigate);
try testing.expectEqual(6, tc.page_navigated);
notifier.unregisterAll(&tc);
notifier.dispatch(.page_navigate, &.{
.timestamp = 100,
.url = undefined,
.opts = .{},
});
notifier.dispatch(.page_navigated, &.{ .timestamp = 100, .url = undefined });
try testing.expectEqual(14, tc.page_navigate);
try testing.expectEqual(6, tc.page_navigated);
{
// unregister
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
try testing.expectEqual(114, tc.page_navigate);
try testing.expectEqual(1006, tc.page_navigated);
notifier.unregister(.page_navigate, &tc);
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
try testing.expectEqual(114, tc.page_navigate);
try testing.expectEqual(2006, tc.page_navigated);
notifier.unregister(.page_navigated, &tc);
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
try testing.expectEqual(114, tc.page_navigate);
try testing.expectEqual(2006, tc.page_navigated);
// already unregistered, try anyways
notifier.unregister(.page_navigated, &tc);
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
try testing.expectEqual(114, tc.page_navigate);
try testing.expectEqual(2006, tc.page_navigated);
}
}
const TestClient = struct {
page_navigate: u64 = 0,
page_navigated: u64 = 0,
fn pageNavigate(ptr: *anyopaque, data: *const Notification.PageNavigate) !void {
const self: *TestClient = @ptrCast(@alignCast(ptr));
self.page_navigate += data.timestamp;
}
fn pageNavigated(ptr: *anyopaque, data: *const Notification.PageNavigated) !void {
const self: *TestClient = @ptrCast(@alignCast(ptr));
self.page_navigated += data.timestamp;
}
};

View File

@@ -1,88 +0,0 @@
const std = @import("std");
const log = @import("log.zig");
const timestamp = @import("datetime.zig").milliTimestamp;
const Queue = std.PriorityQueue(Task, void, struct {
fn compare(_: void, a: Task, b: Task) std.math.Order {
return std.math.order(a.run_at, b.run_at);
}
}.compare);
const Scheduler = @This();
low_priority: Queue,
high_priority: Queue,
pub fn init(allocator: std.mem.Allocator) Scheduler {
return .{
.low_priority = Queue.init(allocator, {}),
.high_priority = Queue.init(allocator, {}),
};
}
pub fn reset(self: *Scheduler) void {
self.low_priority.cap = 0;
self.low_priority.items.len = 0;
self.high_priority.cap = 0;
self.high_priority.items.len = 0;
}
const AddOpts = struct {
name: []const u8 = "",
low_priority: bool = false,
};
pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts: AddOpts) !void {
log.debug(.scheduler, "scheduler.add", .{ .name = opts.name, .run_in_ms = run_in_ms, .low_priority = opts.low_priority });
var queue = if (opts.low_priority) &self.low_priority else &self.high_priority;
return queue.add(.{
.ctx = ctx,
.callback = cb,
.name = opts.name,
.run_at = timestamp(.monotonic) + run_in_ms,
});
}
pub fn run(self: *Scheduler) !?u64 {
_ = try self.runQueue(&self.low_priority);
return self.runQueue(&self.high_priority);
}
fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
if (queue.count() == 0) {
return null;
}
const now = timestamp(.monotonic);
while (queue.peek()) |*task_| {
if (task_.run_at > now) {
return @intCast(task_.run_at - now);
}
var task = queue.remove();
log.debug(.scheduler, "scheduler.runTask", .{ .name = task.name });
const repeat_in_ms = task.callback(task.ctx) catch |err| {
log.warn(.scheduler, "task.callback", .{ .name = task.name, .err = err });
continue;
};
if (repeat_in_ms) |ms| {
// Task cannot be repeated immediately, and they should know that
std.debug.assert(ms != 0);
task.run_at = now + ms;
try self.low_priority.add(task);
}
}
return null;
}
const Task = struct {
run_at: u64,
ctx: *anyopaque,
name: []const u8,
callback: Callback,
};
const Callback = *const fn (ctx: *anyopaque) anyerror!?u32;

File diff suppressed because it is too large Load Diff

View File

@@ -1,119 +0,0 @@
const std = @import("std");
const TestHTTPServer = @This();
shutdown: bool,
listener: ?std.net.Server,
handler: Handler,
const Handler = *const fn (req: *std.http.Server.Request) anyerror!void;
pub fn init(handler: Handler) TestHTTPServer {
return .{
.shutdown = true,
.listener = null,
.handler = handler,
};
}
pub fn deinit(self: *TestHTTPServer) void {
self.shutdown = true;
if (self.listener) |*listener| {
listener.deinit();
}
}
pub fn run(self: *TestHTTPServer, wg: *std.Thread.WaitGroup) !void {
const address = try std.net.Address.parseIp("127.0.0.1", 9582);
self.listener = try address.listen(.{ .reuse_address = true });
var listener = &self.listener.?;
wg.finish();
while (true) {
const conn = listener.accept() catch |err| {
if (self.shutdown) {
return;
}
return err;
};
const thrd = try std.Thread.spawn(.{}, handleConnection, .{ self, conn });
thrd.detach();
}
}
fn handleConnection(self: *TestHTTPServer, conn: std.net.Server.Connection) !void {
defer conn.stream.close();
var req_buf: [2048]u8 = undefined;
var conn_reader = conn.stream.reader(&req_buf);
var conn_writer = conn.stream.writer(&req_buf);
var http_server = std.http.Server.init(conn_reader.interface(), &conn_writer.interface);
while (true) {
var req = http_server.receiveHead() catch |err| switch (err) {
error.ReadFailed => continue,
error.HttpConnectionClosing => continue,
else => {
std.debug.print("Test HTTP Server error: {}\n", .{err});
return err;
},
};
self.handler(&req) catch |err| {
std.debug.print("test http error '{s}': {}\n", .{ req.head.target, err });
try req.respond("server error", .{ .status = .internal_server_error });
return;
};
}
}
pub fn sendFile(req: *std.http.Server.Request, file_path: []const u8) !void {
var file = std.fs.cwd().openFile(file_path, .{}) catch |err| switch (err) {
error.FileNotFound => return req.respond("server error", .{ .status = .not_found }),
else => return err,
};
defer file.close();
const stat = try file.stat();
var send_buffer: [4096]u8 = undefined;
var res = try req.respondStreaming(&send_buffer, .{
.content_length = stat.size,
.respond_options = .{
.extra_headers = &.{
.{ .name = "content-type", .value = getContentType(file_path) },
},
},
});
var read_buffer: [4096]u8 = undefined;
var reader = file.reader(&read_buffer);
_ = try res.writer.sendFileAll(&reader, .unlimited);
try res.writer.flush();
try res.end();
}
fn getContentType(file_path: []const u8) []const u8 {
if (std.mem.endsWith(u8, file_path, ".js")) {
return "application/json";
}
if (std.mem.endsWith(u8, file_path, ".html")) {
return "text/html";
}
if (std.mem.endsWith(u8, file_path, ".htm")) {
return "text/html";
}
if (std.mem.endsWith(u8, file_path, ".xml")) {
// some wpt tests do this
return "text/xml";
}
std.debug.print("TestHTTPServer asked to serve an unknown file type: {s}\n", .{file_path});
return "text/html";
}

43
src/apiweb.zig Normal file
View File

@@ -0,0 +1,43 @@
// 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 generate = @import("generate.zig");
const Console = @import("jsruntime").Console;
const DOM = @import("dom/dom.zig");
const HTML = @import("html/html.zig");
const Events = @import("events/event.zig");
const XHR = @import("xhr/xhr.zig");
const Storage = @import("storage/storage.zig");
const URL = @import("url/url.zig");
pub const HTMLDocument = @import("html/document.zig").HTMLDocument;
// Interfaces
pub const Interfaces = generate.Tuple(.{
Console,
DOM.Interfaces,
Events.Interfaces,
HTML.Interfaces,
XHR.Interfaces,
Storage.Interfaces,
URL.Interfaces,
});
pub const UserContext = @import("user_context.zig").UserContext;

1766
src/async/Client.zig Normal file

File diff suppressed because it is too large Load Diff

133
src/async/stream.zig Normal file
View File

@@ -0,0 +1,133 @@
// 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 builtin = @import("builtin");
const posix = std.posix;
const io = std.io;
const assert = std.debug.assert;
const tcp = @import("tcp.zig");
pub const Stream = struct {
alloc: std.mem.Allocator,
conn: *tcp.Conn,
handle: posix.socket_t,
pub fn close(self: Stream) void {
posix.close(self.handle);
self.alloc.destroy(self.conn);
}
pub const ReadError = posix.ReadError;
pub const WriteError = posix.WriteError;
pub const Reader = io.Reader(Stream, ReadError, read);
pub const Writer = io.Writer(Stream, WriteError, write);
pub fn reader(self: Stream) Reader {
return .{ .context = self };
}
pub fn writer(self: Stream) Writer {
return .{ .context = self };
}
pub fn read(self: Stream, buffer: []u8) ReadError!usize {
return self.conn.receive(self.handle, buffer) catch |err| switch (err) {
else => return error.Unexpected,
};
}
pub fn readv(s: Stream, iovecs: []const posix.iovec) ReadError!usize {
return posix.readv(s.handle, iovecs);
}
/// Returns the number of bytes read. If the number read is smaller than
/// `buffer.len`, it means the stream reached the end. Reaching the end of
/// a stream is not an error condition.
pub fn readAll(s: Stream, buffer: []u8) ReadError!usize {
return readAtLeast(s, buffer, buffer.len);
}
/// Returns the number of bytes read, calling the underlying read function
/// the minimal number of times until the buffer has at least `len` bytes
/// filled. If the number read is less than `len` it means the stream
/// reached the end. Reaching the end of the stream is not an error
/// condition.
pub fn readAtLeast(s: Stream, buffer: []u8, len: usize) ReadError!usize {
assert(len <= buffer.len);
var index: usize = 0;
while (index < len) {
const amt = try s.read(buffer[index..]);
if (amt == 0) break;
index += amt;
}
return index;
}
/// TODO in evented I/O mode, this implementation incorrectly uses the event loop's
/// file system thread instead of non-blocking. It needs to be reworked to properly
/// use non-blocking I/O.
pub fn write(self: Stream, buffer: []const u8) WriteError!usize {
return self.conn.send(self.handle, buffer) catch |err| switch (err) {
error.AccessDenied => error.AccessDenied,
error.WouldBlock => error.WouldBlock,
error.ConnectionResetByPeer => error.ConnectionResetByPeer,
error.MessageTooBig => error.FileTooBig,
error.BrokenPipe => error.BrokenPipe,
else => return error.Unexpected,
};
}
pub fn writeAll(self: Stream, bytes: []const u8) WriteError!void {
var index: usize = 0;
while (index < bytes.len) {
index += try self.write(bytes[index..]);
}
}
/// See https://github.com/ziglang/zig/issues/7699
/// See equivalent function: `std.fs.File.writev`.
pub fn writev(self: Stream, iovecs: []const posix.iovec_const) WriteError!usize {
if (iovecs.len == 0) return 0;
const first_buffer = iovecs[0].base[0..iovecs[0].len];
return try self.write(first_buffer);
}
/// The `iovecs` parameter is mutable because this function needs to mutate the fields in
/// order to handle partial writes from the underlying OS layer.
/// See https://github.com/ziglang/zig/issues/7699
/// See equivalent function: `std.fs.File.writevAll`.
pub fn writevAll(self: Stream, iovecs: []posix.iovec_const) WriteError!void {
if (iovecs.len == 0) return;
var i: usize = 0;
while (true) {
var amt = try self.writev(iovecs[i..]);
while (amt >= iovecs[i].len) {
amt -= iovecs[i].len;
i += 1;
if (i >= iovecs.len) return;
}
iovecs[i].base += amt;
iovecs[i].len -= amt;
}
}
};

112
src/async/tcp.zig Normal file
View File

@@ -0,0 +1,112 @@
// 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 net = std.net;
const Stream = @import("stream.zig").Stream;
const Loop = @import("jsruntime").Loop;
const NetworkImpl = Loop.Network(Conn.Command);
// Conn is a TCP connection using jsruntime Loop async I/O.
// connect, send and receive are blocking, but use async I/O in the background.
// Client doesn't own the socket used for the connection, the caller is
// responsible for closing it.
pub const Conn = struct {
const Command = struct {
impl: NetworkImpl,
done: bool = false,
err: ?anyerror = null,
ln: usize = 0,
fn ok(self: *Command, err: ?anyerror, ln: usize) void {
self.err = err;
self.ln = ln;
self.done = true;
}
fn wait(self: *Command) !usize {
while (!self.done) try self.impl.tick();
if (self.err) |err| return err;
return self.ln;
}
pub fn onConnect(self: *Command, err: ?anyerror) void {
self.ok(err, 0);
}
pub fn onSend(self: *Command, ln: usize, err: ?anyerror) void {
self.ok(err, ln);
}
pub fn onReceive(self: *Command, ln: usize, err: ?anyerror) void {
self.ok(err, ln);
}
};
loop: *Loop,
pub fn connect(self: *Conn, socket: std.posix.socket_t, address: std.net.Address) !void {
var cmd = Command{ .impl = NetworkImpl.init(self.loop) };
cmd.impl.connect(&cmd, socket, address);
_ = try cmd.wait();
}
pub fn send(self: *Conn, socket: std.posix.socket_t, buffer: []const u8) !usize {
var cmd = Command{ .impl = NetworkImpl.init(self.loop) };
cmd.impl.send(&cmd, socket, buffer);
return try cmd.wait();
}
pub fn receive(self: *Conn, socket: std.posix.socket_t, buffer: []u8) !usize {
var cmd = Command{ .impl = NetworkImpl.init(self.loop) };
cmd.impl.receive(&cmd, socket, buffer);
return try cmd.wait();
}
};
pub fn tcpConnectToHost(alloc: std.mem.Allocator, loop: *Loop, name: []const u8, port: u16) !Stream {
// TODO async resolve
const list = try net.getAddressList(alloc, name, port);
defer list.deinit();
if (list.addrs.len == 0) return error.UnknownHostName;
for (list.addrs) |addr| {
return tcpConnectToAddress(alloc, loop, addr) catch |err| switch (err) {
error.ConnectionRefused => {
continue;
},
else => return err,
};
}
return std.posix.ConnectError.ConnectionRefused;
}
pub fn tcpConnectToAddress(alloc: std.mem.Allocator, loop: *Loop, addr: net.Address) !Stream {
const sockfd = try std.posix.socket(addr.any.family, std.posix.SOCK.STREAM, std.posix.IPPROTO.TCP);
errdefer std.posix.close(sockfd);
var conn = try alloc.create(Conn);
conn.* = Conn{ .loop = loop };
try conn.connect(sockfd, addr);
return Stream{
.alloc = alloc,
.conn = conn,
.handle = sockfd,
};
}

189
src/async/test.zig Normal file
View File

@@ -0,0 +1,189 @@
// 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 http = std.http;
const Client = @import("Client.zig");
const Request = @import("Client.zig").Request;
pub const Loop = @import("jsruntime").Loop;
const url = "https://w3.org";
test "blocking mode fetch API" {
const alloc = std.testing.allocator;
var loop = try Loop.init(alloc);
defer loop.deinit();
var client: Client = .{
.allocator = alloc,
.loop = &loop,
};
defer client.deinit();
// force client's CA cert scan from system.
try client.ca_bundle.rescan(client.allocator);
const res = try client.fetch(.{
.location = .{ .uri = try std.Uri.parse(url) },
});
try std.testing.expect(res.status == .ok);
}
test "blocking mode open/send/wait API" {
const alloc = std.testing.allocator;
var loop = try Loop.init(alloc);
defer loop.deinit();
var client: Client = .{
.allocator = alloc,
.loop = &loop,
};
defer client.deinit();
// force client's CA cert scan from system.
try client.ca_bundle.rescan(client.allocator);
var buf: [2014]u8 = undefined;
var req = try client.open(.GET, try std.Uri.parse(url), .{
.server_header_buffer = &buf,
});
defer req.deinit();
try req.send();
try req.finish();
try req.wait();
try std.testing.expect(req.response.status == .ok);
}
// Example how to write an async http client using the modified standard client.
const AsyncClient = struct {
cli: Client,
const YieldImpl = Loop.Yield(AsyncRequest);
const AsyncRequest = struct {
const State = enum { new, open, send, finish, wait, done };
cli: *Client,
uri: std.Uri,
req: ?Request = undefined,
state: State = .new,
impl: YieldImpl,
err: ?anyerror = null,
buf: [2014]u8 = undefined,
pub fn deinit(self: *AsyncRequest) void {
if (self.req) |*r| r.deinit();
}
pub fn fetch(self: *AsyncRequest) void {
self.state = .new;
return self.impl.yield(self);
}
fn onerr(self: *AsyncRequest, err: anyerror) void {
self.state = .done;
self.err = err;
}
pub fn onYield(self: *AsyncRequest, err: ?anyerror) void {
if (err) |e| return self.onerr(e);
switch (self.state) {
.new => {
self.state = .open;
self.req = self.cli.open(.GET, self.uri, .{
.server_header_buffer = &self.buf,
}) catch |e| return self.onerr(e);
},
.open => {
self.state = .send;
self.req.?.send() catch |e| return self.onerr(e);
},
.send => {
self.state = .finish;
self.req.?.finish() catch |e| return self.onerr(e);
},
.finish => {
self.state = .wait;
self.req.?.wait() catch |e| return self.onerr(e);
},
.wait => {
self.state = .done;
return;
},
.done => return,
}
return self.impl.yield(self);
}
pub fn wait(self: *AsyncRequest) !void {
while (self.state != .done) try self.impl.tick();
if (self.err) |err| return err;
}
};
pub fn init(alloc: std.mem.Allocator, loop: *Loop) AsyncClient {
return .{
.cli = .{
.allocator = alloc,
.loop = loop,
},
};
}
pub fn deinit(self: *AsyncClient) void {
self.cli.deinit();
}
pub fn createRequest(self: *AsyncClient, uri: std.Uri) !AsyncRequest {
return .{
.impl = YieldImpl.init(self.cli.loop),
.cli = &self.cli,
.uri = uri,
};
}
};
test "non blocking client" {
const alloc = std.testing.allocator;
var loop = try Loop.init(alloc);
defer loop.deinit();
var client = AsyncClient.init(alloc, &loop);
defer client.deinit();
var reqs: [3]AsyncClient.AsyncRequest = undefined;
for (0..reqs.len) |i| {
reqs[i] = try client.createRequest(try std.Uri.parse(url));
reqs[i].fetch();
}
for (0..reqs.len) |i| {
try reqs[i].wait();
reqs[i].deinit();
}
}

View File

@@ -1,115 +0,0 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const js = @import("js/js.zig");
const log = @import("../log.zig");
const App = @import("../App.zig");
const HttpClient = @import("../http/Client.zig");
const Notification = @import("../Notification.zig");
const Session = @import("Session.zig");
// Browser is an instance of the browser.
// You can create multiple browser instances.
// A browser contains only one session.
const Browser = @This();
env: *js.Env,
app: *App,
session: ?Session,
allocator: Allocator,
http_client: *HttpClient,
call_arena: ArenaAllocator,
page_arena: ArenaAllocator,
session_arena: ArenaAllocator,
transfer_arena: ArenaAllocator,
notification: *Notification,
pub fn init(app: *App) !Browser {
const allocator = app.allocator;
const env = try js.Env.init(allocator, &app.platform, .{});
errdefer env.deinit();
const notification = try Notification.init(allocator, app.notification);
app.http.client.notification = notification;
app.http.client.next_request_id = 0; // Should we track ids in CDP only?
errdefer notification.deinit();
return .{
.app = app,
.env = env,
.session = null,
.allocator = allocator,
.notification = notification,
.http_client = app.http.client,
.call_arena = ArenaAllocator.init(allocator),
.page_arena = ArenaAllocator.init(allocator),
.session_arena = ArenaAllocator.init(allocator),
.transfer_arena = ArenaAllocator.init(allocator),
};
}
pub fn deinit(self: *Browser) void {
self.closeSession();
self.env.deinit();
self.call_arena.deinit();
self.page_arena.deinit();
self.session_arena.deinit();
self.transfer_arena.deinit();
self.http_client.notification = null;
self.notification.deinit();
}
pub fn newSession(self: *Browser) !*Session {
self.closeSession();
self.session = @as(Session, undefined);
const session = &self.session.?;
try Session.init(session, self);
return session;
}
pub fn closeSession(self: *Browser) void {
if (self.session) |*session| {
session.deinit();
self.session = null;
_ = self.session_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
self.env.lowMemoryNotification();
}
}
pub fn runMicrotasks(self: *const Browser) void {
self.env.runMicrotasks();
}
pub fn runMessageLoop(self: *const Browser) void {
while (self.env.pumpMessageLoop()) {
log.debug(.browser, "pumpMessageLoop", .{});
}
self.env.runIdleTasks();
}
const testing = @import("../testing.zig");
test "Browser" {
try testing.htmlRunner("browser.html", .{});
}

View File

@@ -1,294 +0,0 @@
const std = @import("std");
const builtin = @import("builtin");
const log = @import("../log.zig");
const String = @import("../string.zig").String;
const js = @import("js/js.zig");
const Page = @import("Page.zig");
const Node = @import("webapi/Node.zig");
const Event = @import("webapi/Event.zig");
const EventTarget = @import("webapi/EventTarget.zig");
const Allocator = std.mem.Allocator;
const IS_DEBUG = builtin.mode == .Debug;
pub const EventManager = @This();
page: *Page,
arena: Allocator,
listener_pool: std.heap.MemoryPool(Listener),
lookup: std.AutoHashMapUnmanaged(usize, std.DoublyLinkedList),
pub fn init(page: *Page) EventManager {
return .{
.page = page,
.lookup = .{},
.arena = page.arena,
.listener_pool = std.heap.MemoryPool(Listener).init(page.arena),
};
}
pub const RegisterOptions = struct {
once: bool = false,
capture: bool = false,
passive: bool = false,
signal: ?*@import("webapi/AbortSignal.zig") = null,
};
pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, function: js.Function, opts: RegisterOptions) !void {
if (comptime IS_DEBUG) {
log.debug(.event, "eventManager.register", .{ .type = typ, .capture = opts.capture, .once = opts.once });
}
// If a signal is provided and already aborted, don't register the listener
if (opts.signal) |signal| {
if (signal.getAborted()) {
return;
}
}
const gop = try self.lookup.getOrPut(self.arena, @intFromPtr(target));
if (gop.found_existing) {
// check for duplicate functions already registered
var node = gop.value_ptr.first;
while (node) |n| {
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
if (listener.function.eql(function) and listener.capture == opts.capture) {
return;
}
node = n.next;
}
} else {
gop.value_ptr.* = .{};
}
const listener = try self.listener_pool.create();
listener.* = .{
.node = .{},
.once = opts.once,
.capture = opts.capture,
.passive = opts.passive,
.function = .{ .value = function },
.signal = opts.signal,
.typ = try String.init(self.arena, typ, .{}),
};
// append the listener to the list of listeners for this target
gop.value_ptr.append(&listener.node);
}
pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, function: js.Function, use_capture: bool) void {
const list = self.lookup.getPtr(@intFromPtr(target)) orelse return;
if (findListener(list, typ, function, use_capture)) |listener| {
self.removeListener(list, listener);
}
}
pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) !void {
if (comptime IS_DEBUG) {
log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles });
}
event._target = target;
switch (target._type) {
.node => |node| try self.dispatchNode(node, event),
.xhr, .window, .abort_signal => {
const list = self.lookup.getPtr(@intFromPtr(target)) orelse return;
try self.dispatchAll(list, target, event);
},
}
}
// There are a lot of events that can be attached via addEventListener or as
// a property, like the XHR events, or window.onload. You might think that the
// property is just a shortcut for calling addEventListener, but they are distinct.
// An event set via property cannot be removed by removeEventListener. If you
// set both the property and add a listener, they both execute.
const DispatchWithFunctionOptions = struct {
context: []const u8,
inject_target: bool = true,
};
pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *Event, function_: ?js.Function, comptime opts: DispatchWithFunctionOptions) !void {
if (comptime IS_DEBUG) {
log.debug(.event, "dispatchWithFunction", .{ .type = event._type_string.str(), .context = opts.context, .has_function = function_ != null });
}
if (comptime opts.inject_target) {
event._target = target;
}
if (function_) |func| {
event._current_target = target;
func.call(void, .{event}) catch |err| {
// a non-JS error
log.warn(.event, opts.context, .{ .err = err });
};
}
const list = self.lookup.getPtr(@intFromPtr(target)) orelse return;
try self.dispatchAll(list, target, event);
}
fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void {
var path_len: usize = 0;
var path_buffer: [128]*EventTarget = undefined;
var node: ?*Node = target;
while (node) |n| : (node = n._parent) {
if (path_len >= path_buffer.len) break;
path_buffer[path_len] = n.asEventTarget();
path_len += 1;
}
// Even though the window isn't part of the DOM, events always propagate
// through it in the capture phase
if (path_len < path_buffer.len) {
path_buffer[path_len] = self.page.window.asEventTarget();
path_len += 1;
}
const path = path_buffer[0..path_len];
// Phase 1: Capturing phase (root → target, excluding target)
// This happens for all events, regardless of bubbling
event._event_phase = .capturing_phase;
var i: usize = path_len;
while (i > 1) {
i -= 1;
const current_target = path[i];
if (self.lookup.getPtr(@intFromPtr(current_target))) |list| {
try self.dispatchPhase(list, current_target, event, true);
if (event._stop_propagation) {
event._event_phase = .none;
return;
}
}
}
// Phase 2: At target
event._event_phase = .at_target;
const target_et = target.asEventTarget();
if (self.lookup.getPtr(@intFromPtr(target_et))) |list| {
try self.dispatchPhase(list, target_et, event, null);
if (event._stop_propagation) {
event._event_phase = .none;
return;
}
}
// Phase 3: Bubbling phase (target → root, excluding target)
// This only happens if the event bubbles
if (event._bubbles) {
event._event_phase = .bubbling_phase;
for (path[1..]) |current_target| {
if (self.lookup.getPtr(@intFromPtr(current_target))) |list| {
try self.dispatchPhase(list, current_target, event, false);
if (event._stop_propagation) {
break;
}
}
}
}
event._event_phase = .none;
}
fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, comptime capture_only: ?bool) !void {
const page = self.page;
const typ = event._type_string;
var node = list.first;
while (node) |n| {
// do this now, in case we need to remove n (once: true or aborted signal)
node = n.next;
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
if (!listener.typ.eql(typ)) {
continue;
}
// Can be null when dispatching to the target itself
if (comptime capture_only) |capture| {
if (listener.capture != capture) {
continue;
}
}
// If the listener has an aborted signal, remove it and skip
if (listener.signal) |signal| {
if (signal.getAborted()) {
self.removeListener(list, listener);
continue;
}
}
event._current_target = current_target;
switch (listener.function) {
.value => |value| try value.call(void, .{event}),
.string => |string| {
const str = try page.call_arena.dupeZ(u8, string.str());
try self.page.js.eval(str, null);
},
}
if (listener.once) {
self.removeListener(list, listener);
}
if (event._stop_immediate_propagation) {
return;
}
}
}
// Non-Node dispatching (XHR, Window without propagation)
fn dispatchAll(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event) !void {
return self.dispatchPhase(list, current_target, event, null);
}
fn removeListener(self: *EventManager, list: *std.DoublyLinkedList, listener: *Listener) void {
list.remove(&listener.node);
self.listener_pool.destroy(listener);
}
fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, function: js.Function, capture: bool) ?*Listener {
var node = list.first;
while (node) |n| {
node = n.next;
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
if (!listener.function.eql(function)) {
continue;
}
if (listener.capture != capture) {
continue;
}
if (!listener.typ.eqlSlice(typ)) {
continue;
}
return listener;
}
return null;
}
const Listener = struct {
typ: String,
once: bool,
capture: bool,
passive: bool,
function: Function,
signal: ?*@import("webapi/AbortSignal.zig") = null,
node: std.DoublyLinkedList.Node,
};
const Function = union(enum) {
value: js.Function,
string: String,
fn eql(self: Function, func: js.Function) bool {
return switch (self) {
.string => false,
.value => |v| return v.id == func.id,
};
}
};

View File

@@ -1,378 +0,0 @@
const std = @import("std");
const builtin = @import("builtin");
const reflect = @import("reflect.zig");
const IS_DEBUG = builtin.mode == .Debug;
const log = @import("../log.zig");
const String = @import("../string.zig").String;
const Page = @import("Page.zig");
const Node = @import("webapi/Node.zig");
const Event = @import("webapi/Event.zig");
const Element = @import("webapi/Element.zig");
const Document = @import("webapi/Document.zig");
const EventTarget = @import("webapi/EventTarget.zig");
const XMLHttpRequestEventTarget = @import("webapi/net/XMLHttpRequestEventTarget.zig");
const MemoryPoolAligned = std.heap.MemoryPoolAligned;
// 1. Generally, wrapping an ArenaAllocator within an ArenaAllocator doesn't make
// much sense. But wrapping a MemoryPool within an Arena does. Specifically, by
// doing so, we solve a major issue with Arena: freed memory can be re-used [for
// more of the same size].
// 2. Normally, you have a MemoryPool(T) where T is a `User` or something. Then
// the MemoryPool can be used for creating users. But in reality, that memory
// created by that pool could be re-used for anything with the same size (or less)
// than a User (and a compatible alignment). So that's what we do - we have size
// (and alignment) based pools.
const Factory = @This();
_page: *Page,
_size_1_8: MemoryPoolAligned([1]u8, .@"8"),
_size_8_8: MemoryPoolAligned([8]u8, .@"8"),
_size_16_8: MemoryPoolAligned([16]u8, .@"8"),
_size_24_8: MemoryPoolAligned([24]u8, .@"8"),
_size_32_8: MemoryPoolAligned([32]u8, .@"8"),
_size_32_16: MemoryPoolAligned([32]u8, .@"16"),
_size_40_8: MemoryPoolAligned([40]u8, .@"8"),
_size_48_16: MemoryPoolAligned([48]u8, .@"16"),
_size_56_8: MemoryPoolAligned([56]u8, .@"8"),
_size_64_16: MemoryPoolAligned([64]u8, .@"16"),
_size_72_8: MemoryPoolAligned([72]u8, .@"8"),
_size_80_16: MemoryPoolAligned([80]u8, .@"16"),
_size_88_8: MemoryPoolAligned([88]u8, .@"8"),
_size_96_16: MemoryPoolAligned([96]u8, .@"16"),
_size_104_8: MemoryPoolAligned([104]u8, .@"8"),
_size_112_8: MemoryPoolAligned([112]u8, .@"8"),
_size_120_8: MemoryPoolAligned([120]u8, .@"8"),
_size_128_8: MemoryPoolAligned([128]u8, .@"8"),
_size_144_8: MemoryPoolAligned([144]u8, .@"8"),
_size_456_8: MemoryPoolAligned([456]u8, .@"8"),
_size_520_8: MemoryPoolAligned([520]u8, .@"8"),
_size_648_8: MemoryPoolAligned([648]u8, .@"8"),
pub fn init(page: *Page) Factory {
return .{
._page = page,
._size_1_8 = MemoryPoolAligned([1]u8, .@"8").init(page.arena),
._size_8_8 = MemoryPoolAligned([8]u8, .@"8").init(page.arena),
._size_16_8 = MemoryPoolAligned([16]u8, .@"8").init(page.arena),
._size_24_8 = MemoryPoolAligned([24]u8, .@"8").init(page.arena),
._size_32_8 = MemoryPoolAligned([32]u8, .@"8").init(page.arena),
._size_32_16 = MemoryPoolAligned([32]u8, .@"16").init(page.arena),
._size_40_8 = MemoryPoolAligned([40]u8, .@"8").init(page.arena),
._size_48_16 = MemoryPoolAligned([48]u8, .@"16").init(page.arena),
._size_56_8 = MemoryPoolAligned([56]u8, .@"8").init(page.arena),
._size_64_16 = MemoryPoolAligned([64]u8, .@"16").init(page.arena),
._size_72_8 = MemoryPoolAligned([72]u8, .@"8").init(page.arena),
._size_80_16 = MemoryPoolAligned([80]u8, .@"16").init(page.arena),
._size_88_8 = MemoryPoolAligned([88]u8, .@"8").init(page.arena),
._size_96_16 = MemoryPoolAligned([96]u8, .@"16").init(page.arena),
._size_104_8 = MemoryPoolAligned([104]u8, .@"8").init(page.arena),
._size_112_8 = MemoryPoolAligned([112]u8, .@"8").init(page.arena),
._size_120_8 = MemoryPoolAligned([120]u8, .@"8").init(page.arena),
._size_128_8 = MemoryPoolAligned([128]u8, .@"8").init(page.arena),
._size_144_8 = MemoryPoolAligned([144]u8, .@"8").init(page.arena),
._size_456_8 = MemoryPoolAligned([456]u8, .@"8").init(page.arena),
._size_520_8 = MemoryPoolAligned([520]u8, .@"8").init(page.arena),
._size_648_8 = MemoryPoolAligned([648]u8, .@"8").init(page.arena),
};
}
// this is a root object
pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
const child_ptr = try self.createT(@TypeOf(child));
child_ptr.* = child;
const et = try self.createT(EventTarget);
child_ptr._proto = et;
et.* = .{ ._type = unionInit(EventTarget.Type, child_ptr) };
return child_ptr;
}
pub fn node(self: *Factory, child: anytype) !*@TypeOf(child) {
const child_ptr = try self.createT(@TypeOf(child));
child_ptr.* = child;
child_ptr._proto = try self.eventTarget(Node{
._proto = undefined,
._type = unionInit(Node.Type, child_ptr),
});
return child_ptr;
}
pub fn document(self: *Factory, child: anytype) !*@TypeOf(child) {
const child_ptr = try self.createT(@TypeOf(child));
child_ptr.* = child;
child_ptr._proto = try self.node(Document{
._proto = undefined,
._type = unionInit(Document.Type, child_ptr),
});
return child_ptr;
}
pub fn element(self: *Factory, child: anytype) !*@TypeOf(child) {
const child_ptr = try self.createT(@TypeOf(child));
child_ptr.* = child;
child_ptr._proto = try self.node(Element{
._proto = undefined,
._type = unionInit(Element.Type, child_ptr),
});
return child_ptr;
}
pub fn htmlElement(self: *Factory, child: anytype) !*@TypeOf(child) {
if (comptime fieldIsPointer(Element.Html.Type, @TypeOf(child))) {
const child_ptr = try self.createT(@TypeOf(child));
child_ptr.* = child;
child_ptr._proto = try self.element(Element.Html{
._proto = undefined,
._type = unionInit(Element.Html.Type, child_ptr),
});
return child_ptr;
}
// Our union type fields are usually pointers. But, at the leaf, they
// can be struct (if all they contain is the `_proto` field, then we might
// as well store it directly in the struct).
const html = try self.element(Element.Html{
._proto = undefined,
._type = unionInit(Element.Html.Type, child),
});
const field_name = comptime unionFieldName(Element.Html.Type, @TypeOf(child));
var child_ptr = &@field(html._type, field_name);
child_ptr._proto = html;
return child_ptr;
}
pub fn svgElement(self: *Factory, tag_name: []const u8, child: anytype) !*@TypeOf(child) {
if (@TypeOf(child) == Element.Svg) {
return self.element(child);
}
// will never allocate, can't fail
const tag_name_str = String.init(undefined, tag_name, .{}) catch unreachable;
if (comptime fieldIsPointer(Element.Svg.Type, @TypeOf(child))) {
const child_ptr = try self.createT(@TypeOf(child));
child_ptr.* = child;
child_ptr._proto = try self.element(Element.Svg{
._proto = undefined,
._tag_name = tag_name_str,
._type = unionInit(Element.Svg.Type, child_ptr),
});
return child_ptr;
}
// Our union type fields are usually pointers. But, at the leaf, they
// can be struct (if all they contain is the `_proto` field, then we might
// as well store it directly in the struct).
const svg = try self.element(Element.Svg{
._proto = undefined,
._tag_name = tag_name_str,
._type = unionInit(Element.Svg.Type, child),
});
const field_name = comptime unionFieldName(Element.Svg.Type, @TypeOf(child));
var child_ptr = &@field(svg._type, field_name);
child_ptr._proto = svg;
return child_ptr;
}
// this is a root object
pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) {
const child_ptr = try self.createT(@TypeOf(child));
child_ptr.* = child;
const e = try self.createT(Event);
child_ptr._proto = e;
e.* = .{
._type = unionInit(Event.Type, child_ptr),
._type_string = try String.init(self._page.arena, typ, .{}),
};
return child_ptr;
}
pub fn xhrEventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
const et = try self.eventTarget(XMLHttpRequestEventTarget{
._proto = undefined,
._type = unionInit(XMLHttpRequestEventTarget.Type, child),
});
const field_name = comptime unionFieldName(XMLHttpRequestEventTarget.Type, @TypeOf(child));
var child_ptr = &@field(et._type, field_name);
child_ptr._proto = et;
return child_ptr;
}
pub fn create(self: *Factory, value: anytype) !*@TypeOf(value) {
const ptr = try self.createT(@TypeOf(value));
ptr.* = value;
return ptr;
}
pub fn createT(self: *Factory, comptime T: type) !*T {
const SO = @sizeOf(T);
if (comptime SO == 1) return @ptrCast(try self._size_1_8.create());
if (comptime SO == 8) return @ptrCast(try self._size_8_8.create());
if (comptime SO == 16) return @ptrCast(try self._size_16_8.create());
if (comptime SO == 24) return @ptrCast(try self._size_24_8.create());
if (comptime SO == 32) {
if (comptime @alignOf(T) == 8) return @ptrCast(try self._size_32_8.create());
if (comptime @alignOf(T) == 16) return @ptrCast(try self._size_32_16.create());
}
if (comptime SO == 40) return @ptrCast(try self._size_40_8.create());
if (comptime SO == 48) return @ptrCast(try self._size_48_16.create());
if (comptime SO == 56) return @ptrCast(try self._size_56_8.create());
if (comptime SO == 64) return @ptrCast(try self._size_64_16.create());
if (comptime SO == 72) return @ptrCast(try self._size_72_8.create());
if (comptime SO == 80) return @ptrCast(try self._size_80_16.create());
if (comptime SO == 88) return @ptrCast(try self._size_88_8.create());
if (comptime SO == 96) return @ptrCast(try self._size_96_16.create());
if (comptime SO == 104) return @ptrCast(try self._size_104_8.create());
if (comptime SO == 112) return @ptrCast(try self._size_112_8.create());
if (comptime SO == 120) return @ptrCast(try self._size_120_8.create());
if (comptime SO == 128) return @ptrCast(try self._size_128_8.create());
if (comptime SO == 144) return @ptrCast(try self._size_144_8.create());
if (comptime SO == 456) return @ptrCast(try self._size_456_8.create());
if (comptime SO == 520) return @ptrCast(try self._size_520_8.create());
if (comptime SO == 648) return @ptrCast(try self._size_648_8.create());
@compileError(std.fmt.comptimePrint("No pool configured for @sizeOf({d}), @alignOf({d}): ({s})", .{ SO, @alignOf(T), @typeName(T) }));
}
pub fn destroy(self: *Factory, value: anytype) void {
const S = reflect.Struct(@TypeOf(value));
if (comptime IS_DEBUG) {
// We should always destroy from the leaf down.
if (@hasField(S, "_type") and @typeInfo(@TypeOf(value._type)) == .@"union") {
// A Event{._type == .generic} (or any other similar types)
// _should_ be destoyed directly. The _type = .generic is a pseudo
// child
if (S != Event or value._type != .generic) {
log.fatal(.bug, "factory.destroy.event", .{ .type = @typeName(S) });
unreachable;
}
}
}
self.destroyChain(value, true);
}
fn destroyChain(self: *Factory, value: anytype, comptime first: bool) void {
const S = reflect.Struct(@TypeOf(value));
// This is initially called from a deinit. We don't want to call that
// same deinit. So when this is the first time destroyChain is called
// we don't call deinit (because we're in that deinit)
if (!comptime first) {
// But if it isn't the first time
if (@hasDecl(S, "deinit")) {
// And it has a deinit, we'll call it
switch (@typeInfo(@TypeOf(S.deinit)).@"fn".params.len) {
1 => value.deinit(),
2 => value.deinit(self._page),
else => @compileLog(@typeName(S) ++ " has an invalid deinit function"),
}
}
}
if (@hasField(S, "_proto")) {
self.destroyChain(value._proto, false);
} else if (@hasDecl(S, "JsApi")) {
// Doesn't have a _proto, but has a JsApi.
if (self._page.js.removeTaggedMapping(@intFromPtr(value))) |tagged| {
self._size_24_8.destroy(@ptrCast(tagged));
}
}
// Leaf types are allowed by be placed directly within their _proto
// (which makes sense when the @sizeOf(Leaf) == 8). These don't need to
// be (cannot be) freed. But we'll still free the chain.
if (comptime wasAllocated(S)) {
switch (@sizeOf(S)) {
1 => self._size_1_8.destroy(@ptrCast(@alignCast(value))),
8 => self._size_8_8.destroy(@ptrCast(@alignCast(value))),
16 => self._size_16_8.destroy(@ptrCast(value)),
24 => self._size_24_8.destroy(@ptrCast(value)),
32 => {
if (comptime @alignOf(S) == 8) {
self._size_32_8.destroy(@ptrCast(value));
} else if (comptime @alignOf(S) == 16) {
self._size_32_16.destroy(@ptrCast(value));
}
},
40 => self._size_40_8.destroy(@ptrCast(value)),
48 => self._size_48_16.destroy(@ptrCast(@alignCast(value))),
56 => self._size_56_8.destroy(@ptrCast(value)),
64 => self._size_64_16.destroy(@ptrCast(@alignCast(value))),
72 => self._size_72_8.destroy(@ptrCast(@alignCast(value))),
80 => self._size_80_16.destroy(@ptrCast(@alignCast(value))),
88 => self._size_88_8.destroy(@ptrCast(@alignCast(value))),
96 => self._size_96_16.destroy(@ptrCast(@alignCast(value))),
104 => self._size_104_8.destroy(@ptrCast(value)),
112 => self._size_112_8.destroy(@ptrCast(value)),
120 => self._size_120_8.destroy(@ptrCast(value)),
128 => self._size_128_8.destroy(@ptrCast(value)),
144 => self._size_144_8.destroy(@ptrCast(value)),
456 => self._size_456_8.destroy(@ptrCast(value)),
520 => self._size_520_8.destroy(@ptrCast(value)),
648 => self._size_648_8.destroy(@ptrCast(value)),
else => |SO| @compileError(std.fmt.comptimePrint("Don't know what I'm being asked to destroy @sizeOf({d}), @alignOf({d}): ({s})", .{ SO, @alignOf(S), @typeName(S) })),
}
}
}
fn wasAllocated(comptime S: type) bool {
// Whether it's heap allocate or not, we should have a pointer.
// (If it isn't heap allocated, it'll be a pointer from the proto's type
// e.g. &html._type.title)
if (!@hasField(S, "_proto")) {
// a root is always on the heap.
return true;
}
// the _proto type
const P = reflect.Struct(std.meta.fieldInfo(S, ._proto).type);
// the _proto._type type (the parent's _type union)
const U = std.meta.fieldInfo(P, ._type).type;
inline for (@typeInfo(U).@"union".fields) |field| {
if (field.type == S) {
// One of the types in the proto's _type union is this non-pointer
// structure, so it isn't heap allocted.
return false;
}
}
return true;
}
fn unionInit(comptime T: type, value: anytype) T {
const V = @TypeOf(value);
const field_name = comptime unionFieldName(T, V);
return @unionInit(T, field_name, value);
}
// There can be friction between comptime and runtime. Comptime has to
// account for all possible types, even if some runtime flow makes certain
// cases impossible. At runtime, we always call `unionFieldName` with the
// correct struct or pointer type. But at comptime time, `unionFieldName`
// is called with both variants (S and *S). So we use reflect.Struct().
// This only works because we never have a union with a field S and another
// field *S.
fn unionFieldName(comptime T: type, comptime V: type) []const u8 {
inline for (@typeInfo(T).@"union".fields) |field| {
if (reflect.Struct(field.type) == reflect.Struct(V)) {
return field.name;
}
}
@compileError(@typeName(V) ++ " is not a valid type for " ++ @typeName(T) ++ ".type");
}
fn fieldIsPointer(comptime T: type, comptime V: type) bool {
inline for (@typeInfo(T).@"union".fields) |field| {
if (field.type == V) {
return false;
}
if (field.type == *V) {
return true;
}
}
@compileError(@typeName(V) ++ " is not a valid type for " ++ @typeName(T) ++ ".type");
}

View File

@@ -1,518 +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 Mime = @This();
content_type: ContentType,
params: []const u8 = "",
// IANA defines max. charset value length as 40.
// We keep 41 for null-termination since HTML parser expects in this format.
charset: [41]u8 = default_charset,
/// String "UTF-8" continued by null characters.
pub const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36;
/// Mime with unknown Content-Type, empty params and empty charset.
pub const unknown = Mime{ .content_type = .{ .unknown = {} } };
pub const ContentTypeEnum = enum {
text_xml,
text_html,
text_javascript,
text_plain,
text_css,
application_json,
unknown,
other,
};
pub const ContentType = union(ContentTypeEnum) {
text_xml: void,
text_html: void,
text_javascript: void,
text_plain: void,
text_css: void,
application_json: void,
unknown: void,
other: struct { type: []const u8, sub_type: []const u8 },
};
/// Returns the null-terminated charset value.
pub fn charsetString(mime: *const Mime) [:0]const u8 {
return @ptrCast(&mime.charset);
}
/// Removes quotes of value if quotes are given.
///
/// Currently we don't validate the charset.
/// See section 2.3 Naming Requirements:
/// https://datatracker.ietf.org/doc/rfc2978/
fn parseCharset(value: []const u8) error{ CharsetTooBig, Invalid }![]const u8 {
// Cannot be larger than 40.
// https://datatracker.ietf.org/doc/rfc2978/
if (value.len > 40) return error.CharsetTooBig;
// If the first char is a quote, look for a pair.
if (value[0] == '"') {
if (value.len < 3 or value[value.len - 1] != '"') {
return error.Invalid;
}
return value[1 .. value.len - 1];
}
// No quotes.
return value;
}
pub fn parse(input: []u8) !Mime {
if (input.len > 255) {
return error.TooBig;
}
// Zig's trim API is broken. The return type is always `[]const u8`,
// even if the input type is `[]u8`. @constCast is safe here.
var normalized = @constCast(std.mem.trim(u8, input, &std.ascii.whitespace));
_ = std.ascii.lowerString(normalized, normalized);
const content_type, const type_len = try parseContentType(normalized);
if (type_len >= normalized.len) {
return .{ .content_type = content_type };
}
const params = trimLeft(normalized[type_len..]);
var charset: [41]u8 = undefined;
var it = std.mem.splitScalar(u8, params, ';');
while (it.next()) |attr| {
const i = std.mem.indexOfScalarPos(u8, attr, 0, '=') orelse return error.Invalid;
const name = trimLeft(attr[0..i]);
const value = trimRight(attr[i + 1 ..]);
if (value.len == 0) {
return error.Invalid;
}
const attribute_name = std.meta.stringToEnum(enum {
charset,
}, name) orelse continue;
switch (attribute_name) {
.charset => {
if (value.len == 0) {
break;
}
const attribute_value = try parseCharset(value);
@memcpy(charset[0..attribute_value.len], attribute_value);
// Null-terminate right after attribute value.
charset[attribute_value.len] = 0;
},
}
}
return .{
.params = params,
.charset = charset,
.content_type = content_type,
};
}
pub fn sniff(body: []const u8) ?Mime {
// 0x0C is form feed
const content = std.mem.trimLeft(u8, body, &.{ ' ', '\t', '\n', '\r', 0x0C });
if (content.len == 0) {
return null;
}
if (content[0] != '<') {
if (std.mem.startsWith(u8, content, &.{ 0xEF, 0xBB, 0xBF })) {
// UTF-8 BOM
return .{ .content_type = .{ .text_plain = {} } };
}
if (std.mem.startsWith(u8, content, &.{ 0xFE, 0xFF })) {
// UTF-16 big-endian BOM
return .{ .content_type = .{ .text_plain = {} } };
}
if (std.mem.startsWith(u8, content, &.{ 0xFF, 0xFE })) {
// UTF-16 little-endian BOM
return .{ .content_type = .{ .text_plain = {} } };
}
return null;
}
// The longest prefix we have is "<!DOCTYPE HTML ", 15 bytes. If we're
// here, we already know content[0] == '<', so we can skip that. So 14
// bytes.
// +1 because we don't need the leading '<'
var buf: [14]u8 = undefined;
const stripped = content[1..];
const prefix_len = @min(stripped.len, buf.len);
const prefix = std.ascii.lowerString(&buf, stripped[0..prefix_len]);
// we already know it starts with a <
const known_prefixes = [_]struct { []const u8, ContentType }{
.{ "!doctype html", .{ .text_html = {} } },
.{ "html", .{ .text_html = {} } },
.{ "script", .{ .text_html = {} } },
.{ "iframe", .{ .text_html = {} } },
.{ "h1", .{ .text_html = {} } },
.{ "div", .{ .text_html = {} } },
.{ "font", .{ .text_html = {} } },
.{ "table", .{ .text_html = {} } },
.{ "a", .{ .text_html = {} } },
.{ "style", .{ .text_html = {} } },
.{ "title", .{ .text_html = {} } },
.{ "b", .{ .text_html = {} } },
.{ "body", .{ .text_html = {} } },
.{ "br", .{ .text_html = {} } },
.{ "p", .{ .text_html = {} } },
.{ "!--", .{ .text_html = {} } },
.{ "xml", .{ .text_xml = {} } },
};
inline for (known_prefixes) |kp| {
const known_prefix = kp.@"0";
if (std.mem.startsWith(u8, prefix, known_prefix) and prefix.len > known_prefix.len) {
const next = prefix[known_prefix.len];
// a "tag-terminating-byte"
if (next == ' ' or next == '>') {
return .{ .content_type = kp.@"1" };
}
}
}
return null;
}
pub fn isHTML(self: *const Mime) bool {
return self.content_type == .text_html;
}
// we expect value to be lowercase
fn parseContentType(value: []const u8) !struct { ContentType, usize } {
const end = std.mem.indexOfScalarPos(u8, value, 0, ';') orelse value.len;
const type_name = trimRight(value[0..end]);
const attribute_start = end + 1;
if (std.meta.stringToEnum(enum {
@"text/xml",
@"text/html",
@"text/css",
@"text/plain",
@"text/javascript",
@"application/javascript",
@"application/x-javascript",
@"application/json",
}, type_name)) |known_type| {
const ct: ContentType = switch (known_type) {
.@"text/xml" => .{ .text_xml = {} },
.@"text/html" => .{ .text_html = {} },
.@"text/javascript", .@"application/javascript", .@"application/x-javascript" => .{ .text_javascript = {} },
.@"text/plain" => .{ .text_plain = {} },
.@"text/css" => .{ .text_css = {} },
.@"application/json" => .{ .application_json = {} },
};
return .{ ct, attribute_start };
}
const separator = std.mem.indexOfScalarPos(u8, type_name, 0, '/') orelse return error.Invalid;
const main_type = value[0..separator];
const sub_type = trimRight(value[separator + 1 .. end]);
if (main_type.len == 0 or validType(main_type) == false) {
return error.Invalid;
}
if (sub_type.len == 0 or validType(sub_type) == false) {
return error.Invalid;
}
return .{ .{ .other = .{
.type = main_type,
.sub_type = sub_type,
} }, attribute_start };
}
const T_SPECIAL = blk: {
var v = [_]bool{false} ** 256;
for ("()<>@,;:\\\"/[]?=") |b| {
v[b] = true;
}
break :blk v;
};
const VALID_CODEPOINTS = blk: {
var v: [256]bool = undefined;
for (0..256) |i| {
v[i] = std.ascii.isAlphanumeric(i);
}
for ("!#$%&\\*+-.^'_`|~") |b| {
v[b] = true;
}
break :blk v;
};
fn validType(value: []const u8) bool {
for (value) |b| {
if (VALID_CODEPOINTS[b] == false) {
return false;
}
}
return true;
}
fn trimLeft(s: []const u8) []const u8 {
return std.mem.trimLeft(u8, s, &std.ascii.whitespace);
}
fn trimRight(s: []const u8) []const u8 {
return std.mem.trimRight(u8, s, &std.ascii.whitespace);
}
const testing = @import("../testing.zig");
test "Mime: invalid" {
defer testing.reset();
const invalids = [_][]const u8{
"",
"text",
"text /html",
"text/ html",
"text / html",
"text/html other",
"text/html; x",
"text/html; x=",
"text/html; x= ",
"text/html; = ",
"text/html;=",
"text/html; charset=\"\"",
"text/html; charset=\"",
"text/html; charset=\"\\",
};
for (invalids) |invalid| {
const mutable_input = try testing.arena_allocator.dupe(u8, invalid);
try testing.expectError(error.Invalid, Mime.parse(mutable_input));
}
}
test "Mime: parse common" {
defer testing.reset();
try expect(.{ .content_type = .{ .text_xml = {} } }, "text/xml");
try expect(.{ .content_type = .{ .text_html = {} } }, "text/html");
try expect(.{ .content_type = .{ .text_plain = {} } }, "text/plain");
try expect(.{ .content_type = .{ .text_xml = {} } }, "text/xml;");
try expect(.{ .content_type = .{ .text_html = {} } }, "text/html;");
try expect(.{ .content_type = .{ .text_plain = {} } }, "text/plain;");
try expect(.{ .content_type = .{ .text_xml = {} } }, " \ttext/xml");
try expect(.{ .content_type = .{ .text_html = {} } }, "text/html ");
try expect(.{ .content_type = .{ .text_plain = {} } }, "text/plain \t\t");
try expect(.{ .content_type = .{ .text_xml = {} } }, "TEXT/xml");
try expect(.{ .content_type = .{ .text_html = {} } }, "text/Html");
try expect(.{ .content_type = .{ .text_plain = {} } }, "TEXT/PLAIN");
try expect(.{ .content_type = .{ .text_xml = {} } }, " TeXT/xml");
try expect(.{ .content_type = .{ .text_html = {} } }, "teXt/HtML ;");
try expect(.{ .content_type = .{ .text_plain = {} } }, "tExT/PlAiN;");
try expect(.{ .content_type = .{ .text_javascript = {} } }, "text/javascript");
try expect(.{ .content_type = .{ .text_javascript = {} } }, "Application/JavaScript");
try expect(.{ .content_type = .{ .text_javascript = {} } }, "application/x-javascript");
try expect(.{ .content_type = .{ .application_json = {} } }, "application/json");
try expect(.{ .content_type = .{ .text_css = {} } }, "text/css");
}
test "Mime: parse uncommon" {
defer testing.reset();
const text_csv = Expectation{
.content_type = .{ .other = .{ .type = "text", .sub_type = "csv" } },
};
try expect(text_csv, "text/csv");
try expect(text_csv, "text/csv;");
try expect(text_csv, " text/csv\t ");
try expect(text_csv, " text/csv\t ;");
try expect(
.{ .content_type = .{ .other = .{ .type = "text", .sub_type = "csv" } } },
"Text/CSV",
);
}
test "Mime: parse charset" {
defer testing.reset();
try expect(.{
.content_type = .{ .text_xml = {} },
.charset = "utf-8",
.params = "charset=utf-8",
}, "text/xml; charset=utf-8");
try expect(.{
.content_type = .{ .text_xml = {} },
.charset = "utf-8",
.params = "charset=\"utf-8\"",
}, "text/xml;charset=\"UTF-8\"");
try expect(.{
.content_type = .{ .text_html = {} },
.charset = "iso-8859-1",
.params = "charset=\"iso-8859-1\"",
}, "text/html; charset=\"iso-8859-1\"");
try expect(.{
.content_type = .{ .text_html = {} },
.charset = "iso-8859-1",
.params = "charset=\"iso-8859-1\"",
}, "text/html; charset=\"ISO-8859-1\"");
try expect(.{
.content_type = .{ .text_xml = {} },
.charset = "custom-non-standard-charset-value",
.params = "charset=\"custom-non-standard-charset-value\"",
}, "text/xml;charset=\"custom-non-standard-charset-value\"");
}
test "Mime: isHTML" {
defer testing.reset();
const assert = struct {
fn assert(expected: bool, input: []const u8) !void {
const mutable_input = try testing.arena_allocator.dupe(u8, input);
var mime = try Mime.parse(mutable_input);
try testing.expectEqual(expected, mime.isHTML());
}
}.assert;
try assert(true, "text/html");
try assert(true, "text/html;");
try assert(true, "text/html; charset=utf-8");
try assert(false, "text/htm"); // htm not html
try assert(false, "text/plain");
try assert(false, "over/9000");
}
test "Mime: sniff" {
try testing.expectEqual(null, Mime.sniff(""));
try testing.expectEqual(null, Mime.sniff("<htm"));
try testing.expectEqual(null, Mime.sniff("<html!"));
try testing.expectEqual(null, Mime.sniff("<a_"));
try testing.expectEqual(null, Mime.sniff("<!doctype html"));
try testing.expectEqual(null, Mime.sniff("<!doctype html>"));
try testing.expectEqual(null, Mime.sniff("\n <!doctype html>"));
try testing.expectEqual(null, Mime.sniff("\n \t <font/>"));
const expectHTML = struct {
fn expect(input: []const u8) !void {
try testing.expectEqual(.text_html, std.meta.activeTag(Mime.sniff(input).?.content_type));
}
}.expect;
try expectHTML("<!doctype html ");
try expectHTML("\n \t <!DOCTYPE HTML ");
try expectHTML("<html ");
try expectHTML("\n \t <HtmL> even more stufff");
try expectHTML("<script>");
try expectHTML("\n \t <SCRIpt >alert(document.cookies)</script>");
try expectHTML("<iframe>");
try expectHTML(" \t <ifRAME >");
try expectHTML("<h1>");
try expectHTML(" <H1>");
try expectHTML("<div>");
try expectHTML("\n\r\r <DiV>");
try expectHTML("<font>");
try expectHTML(" <fonT>");
try expectHTML("<table>");
try expectHTML("\t\t<TAblE>");
try expectHTML("<a>");
try expectHTML("\n\n<A>");
try expectHTML("<style>");
try expectHTML(" \n\t <STyLE>");
try expectHTML("<title>");
try expectHTML(" \n\t <TITLE>");
try expectHTML("<b>");
try expectHTML(" \n\t <B>");
try expectHTML("<body>");
try expectHTML(" \n\t <BODY>");
try expectHTML("<br>");
try expectHTML(" \n\t <BR>");
try expectHTML("<p>");
try expectHTML(" \n\t <P>");
try expectHTML("<!-->");
try expectHTML(" \n\t <!-->");
}
const Expectation = struct {
content_type: Mime.ContentType,
params: []const u8 = "",
charset: ?[]const u8 = null,
};
fn expect(expected: Expectation, input: []const u8) !void {
const mutable_input = try testing.arena_allocator.dupe(u8, input);
const actual = try Mime.parse(mutable_input);
try testing.expectEqual(
std.meta.activeTag(expected.content_type),
std.meta.activeTag(actual.content_type),
);
switch (expected.content_type) {
.other => |e| {
const a = actual.content_type.other;
try testing.expectEqual(e.type, a.type);
try testing.expectEqual(e.sub_type, a.sub_type);
},
else => {}, // already asserted above
}
try testing.expectEqual(expected.params, actual.params);
if (expected.charset) |ec| {
// We remove the null characters for testing purposes here.
try testing.expectEqual(ec, actual.charsetString()[0..ec.len]);
} else {
const m: Mime = .unknown;
try testing.expectEqual(m.charsetString(), actual.charsetString());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,109 +0,0 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("netsurf.zig");
const Allocator = std.mem.Allocator;
const Renderer = @This();
allocator: Allocator,
// key is a @ptrFromInt of the element
// value is the index position
positions: std.AutoHashMapUnmanaged(u64, u32),
// given an index, get the element
elements: std.ArrayListUnmanaged(u64),
const Element = @import("dom/element.zig").Element;
// we expect allocator to be an arena
pub fn init(allocator: Allocator) Renderer {
return .{
.elements = .{},
.positions = .{},
.allocator = allocator,
};
}
// The DOMRect is always relative to the viewport, not the document the element belongs to.
// Element that are not part of the main document, either detached or in a shadow DOM should not call this function.
pub fn getRect(self: *Renderer, e: *parser.Element) !Element.DOMRect {
var elements = &self.elements;
const gop = try self.positions.getOrPut(self.allocator, @intFromPtr(e));
var x: u32 = gop.value_ptr.*;
if (gop.found_existing == false) {
x = @intCast(elements.items.len);
try elements.append(self.allocator, @intFromPtr(e));
gop.value_ptr.* = x;
}
const _x: f64 = @floatFromInt(x);
const y: f64 = 0.0;
const w: f64 = 1.0;
const h: f64 = 1.0;
return .{
.x = _x,
.y = y,
.width = w,
.height = h,
.left = _x,
.top = y,
.right = _x + w,
.bottom = y + h,
};
}
pub fn boundingRect(self: *const Renderer) Element.DOMRect {
const x: f64 = 0.0;
const y: f64 = 0.0;
const w: f64 = @floatFromInt(self.width());
const h: f64 = @floatFromInt(self.width());
return .{
.x = x,
.y = y,
.width = w,
.height = h,
.left = x,
.top = y,
.right = x + w,
.bottom = y + h,
};
}
pub fn width(self: *const Renderer) u32 {
return @max(@as(u32, @intCast(self.elements.items.len)), 1); // At least 1 pixel even if empty
}
pub fn height(_: *const Renderer) u32 {
return 1;
}
pub fn getElementAtPosition(self: *const Renderer, x: i32, y: i32) ?*parser.Element {
if (y != 0 or x < 0) {
return null;
}
const elements = self.elements.items;
return if (x < elements.len) @ptrFromInt(elements[@intCast(x)]) else null;
}

View File

@@ -1,95 +0,0 @@
const std = @import("std");
const builtin = @import("builtin");
const log = @import("../log.zig");
const timestamp = @import("../datetime.zig").milliTimestamp;
const IS_DEBUG = builtin.mode == .Debug;
const Queue = std.PriorityQueue(Task, void, struct {
fn compare(_: void, a: Task, b: Task) std.math.Order {
return std.math.order(a.run_at, b.run_at);
}
}.compare);
const Scheduler = @This();
low_priority: Queue,
high_priority: Queue,
pub fn init(allocator: std.mem.Allocator) Scheduler {
return .{
.low_priority = Queue.init(allocator, {}),
.high_priority = Queue.init(allocator, {}),
};
}
pub fn reset(self: *Scheduler) void {
self.low_priority.cap = 0;
self.low_priority.items.len = 0;
self.high_priority.cap = 0;
self.high_priority.items.len = 0;
}
const AddOpts = struct {
name: []const u8 = "",
low_priority: bool = false,
};
pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts: AddOpts) !void {
if (comptime IS_DEBUG) {
log.debug(.scheduler, "scheduler.add", .{ .name = opts.name, .run_in_ms = run_in_ms, .low_priority = opts.low_priority });
}
var queue = if (opts.low_priority) &self.low_priority else &self.high_priority;
return queue.add(.{
.ctx = ctx,
.callback = cb,
.name = opts.name,
.run_at = timestamp(.monotonic) + run_in_ms,
});
}
pub fn run(self: *Scheduler) !?u64 {
_ = try self.runQueue(&self.low_priority);
return self.runQueue(&self.high_priority);
}
fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
if (queue.count() == 0) {
return null;
}
const now = timestamp(.monotonic);
while (queue.peek()) |*task_| {
if (task_.run_at > now) {
return @intCast(task_.run_at - now);
}
var task = queue.remove();
if (comptime IS_DEBUG) {
log.debug(.scheduler, "scheduler.runTask", .{ .name = task.name });
}
const repeat_in_ms = task.callback(task.ctx) catch |err| {
log.warn(.scheduler, "task.callback", .{ .name = task.name, .err = err });
continue;
};
if (repeat_in_ms) |ms| {
// Task cannot be repeated immediately, and they should know that
std.debug.assert(ms != 0);
task.run_at = now + ms;
try self.low_priority.add(task);
}
}
return null;
}
const Task = struct {
run_at: u64,
ctx: *anyopaque,
name: []const u8,
callback: Callback,
};
const Callback = *const fn (ctx: *anyopaque) anyerror!?u32;

File diff suppressed because it is too large Load Diff

View File

@@ -1,197 +0,0 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = @import("../log.zig");
const js = @import("js/js.zig");
const storage = @import("webapi/storage/storage.zig");
const Page = @import("Page.zig");
const Browser = @import("Browser.zig");
const Allocator = std.mem.Allocator;
const NavigateOpts = Page.NavigateOpts;
// Session is like a browser's tab.
// It owns the js env and the loader for all the pages of the session.
// You can create successively multiple pages for a session, but you must
// deinit a page before running another one.
const Session = @This();
browser: *Browser,
// Used to create our Inspector and in the BrowserContext.
arena: Allocator,
// The page's arena is unsuitable for data that has to existing while
// navigating from one page to another. For example, if we're clicking
// on an HREF, the URL exists in the original page (where the click
// originated) but also has to exist in the new page.
// While we could use the Session's arena, this could accumulate a lot of
// memory if we do many navigation events. The `transfer_arena` is meant to
// bridge the gap: existing long enough to store any data needed to end one
// page and start another.
transfer_arena: Allocator,
executor: js.ExecutionWorld,
cookie_jar: storage.Jar,
storage_shed: storage.Shed,
page: ?*Page = null,
// If the current page want to navigate to a new page
// (form submit, link click, top.location = xxx)
// the details are stored here so that, on the next call to session.wait
// we can destroy the current page and start a new one.
queued_navigation: ?QueuedNavigation,
pub fn init(self: *Session, browser: *Browser) !void {
var executor = try browser.env.newExecutionWorld();
errdefer executor.deinit();
const allocator = browser.app.allocator;
self.* = .{
.browser = browser,
.executor = executor,
.storage_shed = .{},
.queued_navigation = null,
.arena = browser.session_arena.allocator(),
.cookie_jar = storage.Jar.init(allocator),
.transfer_arena = browser.transfer_arena.allocator(),
};
}
pub fn deinit(self: *Session) void {
if (self.page != null) {
self.removePage();
}
self.cookie_jar.deinit();
self.storage_shed.deinit(self.browser.app.allocator);
self.executor.deinit();
}
// NOTE: the caller is not the owner of the returned value,
// the pointer on Page is just returned as a convenience
pub fn createPage(self: *Session) !*Page {
std.debug.assert(self.page == null);
const page_arena = &self.browser.page_arena;
_ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
self.page = try Page.init(page_arena.allocator(), self.browser.call_arena.allocator(), self);
const page = self.page.?;
log.debug(.browser, "create page", .{});
// start JS env
// Inform CDP the main page has been created such that additional context for other Worlds can be created as well
self.browser.notification.dispatch(.page_created, page);
return page;
}
pub fn removePage(self: *Session) void {
// Inform CDP the page is going to be removed, allowing other worlds to remove themselves before the main one
self.browser.notification.dispatch(.page_remove, .{});
std.debug.assert(self.page != null);
// RemoveJsContext() will execute the destructor of any type that
// registered a destructor (e.g. XMLHttpRequest).
// Should be called before we deinit the page, because these objects
// could be referencing it.
self.executor.removeContext();
self.page.?.deinit();
self.page = null;
log.debug(.browser, "remove page", .{});
}
pub fn currentPage(self: *Session) ?*Page {
return self.page orelse return null;
}
pub const WaitResult = enum {
done,
no_page,
extra_socket,
};
pub fn wait(self: *Session, wait_ms: u32) WaitResult {
_ = self.processQueuedNavigation() catch {
// There was an error processing the queue navigation. This already
// logged the error, just return.
return .done;
};
if (self.page) |page| {
return page.wait(wait_ms);
}
return .no_page;
}
pub fn fetchWait(self: *Session, wait_ms: u32) void {
while (true) {
const page = self.page orelse return;
_ = page.wait(wait_ms);
const navigated = self.processQueuedNavigation() catch {
// There was an error processing the queue navigation. This already
// logged the error, just return.
return;
};
if (navigated == false) {
return;
}
}
}
fn processQueuedNavigation(self: *Session) !bool {
const qn = self.queued_navigation orelse return false;
// This was already aborted on the page, but it would be pretty
// bad if old requests went to the new page, so let's make double sure
self.browser.http_client.abort();
// Page.navigateFromWebAPI terminatedExecution. If we don't resume
// it before doing a shutdown we'll get an error.
self.executor.resumeExecution();
self.removePage();
self.queued_navigation = null;
const page = self.createPage() catch |err| {
log.err(.browser, "queued navigation page error", .{
.err = err,
.url = qn.url,
});
return err;
};
page.navigate(qn.url, qn.opts) catch |err| {
log.err(.browser, "queued navigation error", .{ .err = err, .url = qn.url });
return err;
};
return true;
}
const QueuedNavigation = struct {
url: [:0]const u8,
opts: NavigateOpts,
};

View File

@@ -1,393 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const ResolveOpts = struct {
always_dupe: bool = false,
};
// path is anytype, so that it can be used with both []const u8 and [:0]const u8
pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime opts: ResolveOpts) ![:0]const u8 {
const PT = @TypeOf(path);
if (base.len == 0 or isCompleteHTTPUrl(path)) {
if (comptime opts.always_dupe or !isNullTerminated(PT)) {
return allocator.dupeZ(u8, path);
}
return path;
}
if (path.len == 0) {
if (comptime opts.always_dupe) {
return allocator.dupeZ(u8, base);
}
return base;
}
if (path[0] == '?') {
const base_path_end = std.mem.indexOfAny(u8, base, "?#") orelse base.len;
return std.mem.joinZ(allocator, "", &.{ base[0..base_path_end], path });
}
if (path[0] == '#') {
const base_fragment_start = std.mem.indexOfScalar(u8, base, '#') orelse base.len;
return std.mem.joinZ(allocator, "", &.{ base[0..base_fragment_start], path });
}
if (std.mem.startsWith(u8, path, "//")) {
// network-path reference
const index = std.mem.indexOfScalar(u8, base, ':') orelse {
if (comptime isNullTerminated(PT)) {
return path;
}
return allocator.dupeZ(u8, path);
};
const protocol = base[0 .. index + 1];
return std.mem.joinZ(allocator, "", &.{ protocol, path });
}
const scheme_end = std.mem.indexOf(u8, base, "://");
const authority_start = if (scheme_end) |end| end + 3 else 0;
const path_start = std.mem.indexOfScalarPos(u8, base, authority_start, '/') orelse base.len;
if (path[0] == '/') {
return std.mem.joinZ(allocator, "", &.{ base[0..path_start], path });
}
var normalized_base: []const u8 = base;
if (std.mem.lastIndexOfScalar(u8, normalized_base[authority_start..], '/')) |pos| {
normalized_base = normalized_base[0 .. pos + authority_start];
}
// trailing space so that we always have space to append the null terminator
var out = try std.mem.join(allocator, "", &.{ normalized_base, "/", path, " " });
const end = out.len - 1;
const path_marker = path_start + 1;
// Strip out ./ and ../. This is done in-place, because doing so can
// only ever make `out` smaller. After this, `out` cannot be freed by
// an allocator, which is ok, because we expect allocator to be an arena.
var in_i: usize = 0;
var out_i: usize = 0;
while (in_i < end) {
if (std.mem.startsWith(u8, out[in_i..], "./")) {
in_i += 2;
continue;
}
if (std.mem.startsWith(u8, out[in_i..], "../")) {
std.debug.assert(out[out_i - 1] == '/');
if (out_i > path_marker) {
// go back before the /
out_i -= 2;
while (out_i > 1 and out[out_i - 1] != '/') {
out_i -= 1;
}
} else {
// if out_i == path_marker, than we've reached the start of
// the path. We can't ../ any more. E.g.:
// http://www.example.com/../hello.
// You might think that's an error, but, at least with
// new URL('../hello', 'http://www.example.com/')
// it just ignores the extra ../
}
in_i += 3;
continue;
}
out[out_i] = out[in_i];
in_i += 1;
out_i += 1;
}
// we always have an extra space
out[out_i] = 0;
return out[0..out_i :0];
}
fn isNullTerminated(comptime value: type) bool {
return @typeInfo(value).pointer.sentinel_ptr != null;
}
pub fn isCompleteHTTPUrl(url: []const u8) bool {
if (url.len < 6) {
return false;
}
// very common case
if (url[0] == '/') {
return false;
}
return std.ascii.startsWithIgnoreCase(url, "https://") or
std.ascii.startsWithIgnoreCase(url, "http://") or
std.ascii.startsWithIgnoreCase(url, "ftp://");
}
pub fn getUsername(raw: [:0]const u8) []const u8 {
const user_info = getUserInfo(raw) orelse return "";
const pos = std.mem.indexOfScalarPos(u8, user_info, 0, ':') orelse return user_info;
return user_info[0..pos];
}
pub fn getPassword(raw: [:0]const u8) []const u8 {
const user_info = getUserInfo(raw) orelse return "";
const pos = std.mem.indexOfScalarPos(u8, user_info, 0, ':') orelse return "";
return user_info[pos + 1 ..];
}
pub fn getPathname(raw: [:0]const u8) []const u8 {
const protocol_end = std.mem.indexOf(u8, raw, "://") orelse 0;
const path_start = std.mem.indexOfScalarPos(u8, raw, if (protocol_end > 0) protocol_end + 3 else 0, '/') orelse raw.len;
const query_or_hash_start = std.mem.indexOfAnyPos(u8, raw, path_start, "?#") orelse raw.len;
if (path_start >= query_or_hash_start) {
if (std.mem.indexOf(u8, raw, "://") != null) return "/";
return "";
}
return raw[path_start..query_or_hash_start];
}
pub fn getProtocol(raw: [:0]const u8) []const u8 {
const pos = std.mem.indexOfScalarPos(u8, raw, 0, ':') orelse return "";
return raw[0 .. pos + 1];
}
pub fn getHostname(raw: [:0]const u8) []const u8 {
const host = getHost(raw);
const pos = std.mem.lastIndexOfScalar(u8, host, ':') orelse return host;
return host[0..pos];
}
pub fn getPort(raw: [:0]const u8) []const u8 {
const host = getHost(raw);
const pos = std.mem.lastIndexOfScalar(u8, host, ':') orelse return "";
if (pos + 1 >= host.len) {
return "";
}
for (host[pos + 1 ..]) |c| {
if (c < '0' or c > '9') {
return "";
}
}
return host[pos + 1 ..];
}
pub fn getSearch(raw: [:0]const u8) []const u8 {
const pos = std.mem.indexOfScalarPos(u8, raw, 0, '?') orelse return "";
const query_part = raw[pos..];
if (std.mem.indexOfScalarPos(u8, query_part, 0, '#')) |fragment_start| {
return query_part[0..fragment_start];
}
return query_part;
}
pub fn getHash(raw: [:0]const u8) []const u8 {
const start = std.mem.indexOfScalarPos(u8, raw, 0, '#') orelse return "";
return raw[start..];
}
pub fn getOrigin(allocator: Allocator, raw: [:0]const u8) !?[]const u8 {
const port = getPort(raw);
const protocol = getProtocol(raw);
const hostname = getHostname(raw);
const p = std.meta.stringToEnum(KnownProtocol, getProtocol(raw)) orelse return null;
const include_port = blk: {
if (port.len == 0) {
break :blk false;
}
if (p == .@"https:" and std.mem.eql(u8, port, "443")) {
break :blk false;
}
if (p == .@"http:" and std.mem.eql(u8, port, "80")) {
break :blk false;
}
break :blk true;
};
if (include_port) {
return try std.fmt.allocPrint(allocator, "{s}//{s}:{s}", .{ protocol, hostname, port });
}
return try std.fmt.allocPrint(allocator, "{s}//{s}", .{ protocol, hostname });
}
fn getUserInfo(raw: [:0]const u8) ?[]const u8 {
const scheme_end = std.mem.indexOf(u8, raw, "://") orelse return null;
const authority_start = scheme_end + 3;
const pos = std.mem.indexOfScalar(u8, raw[authority_start..], '@') orelse return null;
const path_start = std.mem.indexOfScalarPos(u8, raw, authority_start, '/') orelse raw.len;
const full_pos = authority_start + pos;
if (full_pos < path_start) {
return raw[authority_start..full_pos];
}
return null;
}
fn getHost(raw: [:0]const u8) []const u8 {
const scheme_end = std.mem.indexOf(u8, raw, "://") orelse return "";
var authority_start = scheme_end + 3;
if (std.mem.indexOf(u8, raw[authority_start..], "@")) |pos| {
authority_start += pos + 1;
}
const authority = raw[authority_start..];
const path_start = std.mem.indexOfAny(u8, authority, "/?#") orelse return authority;
return authority[0..path_start];
}
const KnownProtocol = enum {
@"http:",
@"https:",
};
const testing = @import("../testing.zig");
test "URL: isCompleteHTTPUrl" {
try testing.expectEqual(true, isCompleteHTTPUrl("http://example.com/about"));
try testing.expectEqual(true, isCompleteHTTPUrl("HttP://example.com/about"));
try testing.expectEqual(true, isCompleteHTTPUrl("httpS://example.com/about"));
try testing.expectEqual(true, isCompleteHTTPUrl("HTTPs://example.com/about"));
try testing.expectEqual(true, isCompleteHTTPUrl("ftp://example.com/about"));
try testing.expectEqual(false, isCompleteHTTPUrl("/example.com"));
try testing.expectEqual(false, isCompleteHTTPUrl("../../about"));
try testing.expectEqual(false, isCompleteHTTPUrl("about"));
}
test "URL: resolve" {
defer testing.reset();
const Case = struct {
base: [:0]const u8,
path: [:0]const u8,
expected: [:0]const u8,
};
const cases = [_]Case{
.{
.base = "https://example/xyz/abc/123",
.path = "something.js",
.expected = "https://example/xyz/abc/something.js",
},
.{
.base = "https://example/xyz/abc/123",
.path = "/something.js",
.expected = "https://example/something.js",
},
.{
.base = "https://example/",
.path = "something.js",
.expected = "https://example/something.js",
},
.{
.base = "https://example/",
.path = "/something.js",
.expected = "https://example/something.js",
},
.{
.base = "https://example",
.path = "something.js",
.expected = "https://example/something.js",
},
.{
.base = "https://example",
.path = "abc/something.js",
.expected = "https://example/abc/something.js",
},
.{
.base = "https://example/nested",
.path = "abc/something.js",
.expected = "https://example/abc/something.js",
},
.{
.base = "https://example/nested/",
.path = "abc/something.js",
.expected = "https://example/nested/abc/something.js",
},
.{
.base = "https://example/nested/",
.path = "/abc/something.js",
.expected = "https://example/abc/something.js",
},
.{
.base = "https://example/nested/",
.path = "http://www.github.com/example/",
.expected = "http://www.github.com/example/",
},
.{
.base = "https://example/nested/",
.path = "",
.expected = "https://example/nested/",
},
.{
.base = "https://example/abc/aaa",
.path = "./hello/./world",
.expected = "https://example/abc/hello/world",
},
.{
.base = "https://example/abc/aaa/",
.path = "../hello",
.expected = "https://example/abc/hello",
},
.{
.base = "https://example/abc/aaa",
.path = "../hello",
.expected = "https://example/hello",
},
.{
.base = "https://example/abc/aaa/",
.path = "./.././.././hello",
.expected = "https://example/hello",
},
.{
.base = "some/page",
.path = "hello",
.expected = "some/hello",
},
.{
.base = "some/page/",
.path = "hello",
.expected = "some/page/hello",
},
.{
.base = "some/page/other",
.path = ".././hello",
.expected = "some/hello",
},
.{
.base = "https://www.example.com/hello/world",
.path = "//example/about",
.expected = "https://example/about",
},
.{
.base = "http:",
.path = "//example.com/over/9000",
.expected = "http://example.com/over/9000",
},
.{
.base = "https://example.com/",
.path = "../hello",
.expected = "https://example.com/hello",
},
.{
.base = "https://www.example.com/hello/world/",
.path = "../../../../example/about",
.expected = "https://www.example.com/example/about",
},
};
for (cases) |case| {
const result = try resolve(testing.arena_allocator, case.base, case.path, .{});
try testing.expectString(case.expected, result);
}
}

571
src/browser/browser.zig Normal file
View File

@@ -0,0 +1,571 @@
// 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 builtin = @import("builtin");
const Types = @import("root").Types;
const parser = @import("netsurf");
const Loader = @import("loader.zig").Loader;
const Dump = @import("dump.zig");
const Mime = @import("mime.zig");
const jsruntime = @import("jsruntime");
const Loop = jsruntime.Loop;
const Env = jsruntime.Env;
const apiweb = @import("../apiweb.zig");
const Window = @import("../html/window.zig").Window;
const Walker = @import("../dom/walker.zig").WalkerDepthFirst;
const storage = @import("../storage/storage.zig");
const FetchResult = @import("../http/Client.zig").Client.FetchResult;
const UserContext = @import("../user_context.zig").UserContext;
const HttpClient = @import("../async/Client.zig");
const log = std.log.scoped(.browser);
// Browser is an instance of the browser.
// You can create multiple browser instances.
// A browser contains only one session.
// TODO allow multiple sessions per browser.
pub const Browser = struct {
session: *Session,
pub fn init(alloc: std.mem.Allocator, vm: jsruntime.VM) !Browser {
// We want to ensure the caller initialised a VM, but the browser
// doesn't use it directly...
_ = vm;
return Browser{
.session = try Session.init(alloc, "about:blank"),
};
}
pub fn deinit(self: *Browser) void {
self.session.deinit();
}
pub fn currentSession(self: *Browser) *Session {
return self.session;
}
};
// Session is like a browser's tab.
// It owns the js env and the loader for all the pages of the session.
// You can create successively multiple pages for a session, but you must
// deinit a page before running another one.
pub const Session = struct {
// allocator used to init the arena.
alloc: std.mem.Allocator,
// The arena is used only to bound the js env init b/c it leaks memory.
// see https://github.com/lightpanda-io/jsruntime-lib/issues/181
//
// The arena is initialised with self.alloc allocator.
// all others Session deps use directly self.alloc and not the arena.
arena: std.heap.ArenaAllocator,
uri: []const u8,
// TODO handle proxy
loader: Loader,
env: Env = undefined,
loop: Loop,
window: Window,
// TODO move the shed to the browser?
storageShed: storage.Shed,
page: ?*Page = null,
httpClient: HttpClient,
jstypes: [Types.len]usize = undefined,
fn init(alloc: std.mem.Allocator, uri: []const u8) !*Session {
var self = try alloc.create(Session);
self.* = Session{
.uri = uri,
.alloc = alloc,
.arena = std.heap.ArenaAllocator.init(alloc),
.window = Window.create(null),
.loader = Loader.init(alloc),
.loop = try Loop.init(alloc),
.storageShed = storage.Shed.init(alloc),
.httpClient = undefined,
};
self.env = try Env.init(self.arena.allocator(), &self.loop, null);
self.httpClient = .{ .allocator = alloc, .loop = &self.loop };
try self.env.load(&self.jstypes);
return self;
}
fn deinit(self: *Session) void {
if (self.page) |page| page.end();
self.env.deinit();
self.arena.deinit();
self.httpClient.deinit();
self.loader.deinit();
self.storageShed.deinit();
self.loop.deinit();
self.alloc.destroy(self);
}
pub fn createPage(self: *Session) !Page {
return Page.init(self.alloc, self);
}
};
// Page navigates to an url.
// You can navigates multiple urls with the same page, but you have to call
// end() to stop the previous navigation before starting a new one.
// The page handle all its memory in an arena allocator. The arena is reseted
// when end() is called.
pub const Page = struct {
arena: std.heap.ArenaAllocator,
session: *Session,
doc: ?*parser.Document = null,
// handle url
rawuri: ?[]const u8 = null,
uri: std.Uri = undefined,
origin: ?[]const u8 = null,
raw_data: ?[]const u8 = null,
fn init(
alloc: std.mem.Allocator,
session: *Session,
) !Page {
if (session.page != null) return error.SessionPageExists;
var page = Page{
.arena = std.heap.ArenaAllocator.init(alloc),
.session = session,
};
session.page = &page;
return page;
}
// reset js env and mem arena.
pub fn end(self: *Page) void {
self.session.env.stop();
// TODO unload document: https://html.spec.whatwg.org/#unloading-documents
// clear netsurf memory arena.
parser.deinit();
_ = self.arena.reset(.free_all);
}
pub fn deinit(self: *Page) void {
self.arena.deinit();
self.session.page = null;
}
// dump writes the page content into the given file.
pub fn dump(self: *Page, out: std.fs.File) !void {
// if no HTML document pointer available, dump the data content only.
if (self.doc == null) {
// no data loaded, nothing to do.
if (self.raw_data == null) return;
return try out.writeAll(self.raw_data.?);
}
// if the page has a pointer to a document, dumps the HTML.
try Dump.writeHTML(self.doc.?, out);
}
pub fn wait(self: *Page) !void {
// try catch
var try_catch: jsruntime.TryCatch = undefined;
try_catch.init(self.session.env);
defer try_catch.deinit();
self.session.env.wait() catch |err| {
// the js env could not be started if the document wasn't an HTML.
if (err == error.EnvNotStarted) return;
const alloc = self.arena.allocator();
if (try try_catch.err(alloc, self.session.env)) |msg| {
defer alloc.free(msg);
log.info("wait error: {s}", .{msg});
return;
}
};
log.debug("wait: OK", .{});
}
// spec reference: https://html.spec.whatwg.org/#document-lifecycle
pub fn navigate(self: *Page, uri: []const u8) !void {
const alloc = self.arena.allocator();
log.debug("starting GET {s}", .{uri});
// own the url
if (self.rawuri) |prev| alloc.free(prev);
self.rawuri = try alloc.dupe(u8, uri);
self.uri = std.Uri.parse(self.rawuri.?) catch try std.Uri.parseAfterScheme("", self.rawuri.?);
// prepare origin value.
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
try self.uri.writeToStream(.{
.scheme = true,
.authority = true,
}, buf.writer());
self.origin = try buf.toOwnedSlice();
// TODO handle fragment in url.
// load the data
var resp = try self.session.loader.get(alloc, self.uri);
defer resp.deinit();
const req = resp.req;
log.info("GET {any} {d}", .{ self.uri, req.response.status });
// TODO handle redirection
if (req.response.status != .ok) {
log.debug("{?} {d} {s}", .{
req.response.version,
req.response.status,
req.response.reason,
// TODO log headers
});
return error.BadStatusCode;
}
// TODO handle charset
// https://html.spec.whatwg.org/#content-type
var it = req.response.iterateHeaders();
var ct: ?[]const u8 = null;
while (true) {
const h = it.next() orelse break;
if (std.ascii.eqlIgnoreCase(h.name, "Content-Type")) {
ct = try alloc.dupe(u8, h.value);
}
}
if (ct == null) {
// no content type in HTTP headers.
// TODO try to sniff mime type from the body.
log.info("no content-type HTTP header", .{});
return;
}
defer alloc.free(ct.?);
log.debug("header content-type: {s}", .{ct.?});
const mime = try Mime.parse(ct.?);
if (mime.eql(Mime.HTML)) {
try self.loadHTMLDoc(req.reader(), mime.charset orelse "utf-8");
} else {
log.info("non-HTML document: {s}", .{ct.?});
// save the body into the page.
self.raw_data = try req.reader().readAllAlloc(alloc, 16 * 1024 * 1024);
}
}
// https://html.spec.whatwg.org/#read-html
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8) !void {
const alloc = self.arena.allocator();
// start netsurf memory arena.
try parser.init();
log.debug("parse html with charset {s}", .{charset});
const ccharset = try alloc.dupeZ(u8, charset);
defer alloc.free(ccharset);
const html_doc = try parser.documentHTMLParse(reader, ccharset);
const doc = parser.documentHTMLToDocument(html_doc);
// save a document's pointer in the page.
self.doc = doc;
// TODO set document.readyState to interactive
// https://html.spec.whatwg.org/#reporting-document-loading-status
// inject the URL to the document including the fragment.
try parser.documentSetDocumentURI(doc, self.rawuri orelse "about:blank");
// TODO set the referrer to the document.
self.session.window.replaceDocument(html_doc);
self.session.window.setStorageShelf(
try self.session.storageShed.getOrPut(self.origin orelse "null"),
);
// https://html.spec.whatwg.org/#read-html
// start JS env
// TODO load the js env concurrently with the HTML parsing.
log.debug("start js env", .{});
try self.session.env.start();
// replace the user context document with the new one.
try self.session.env.setUserContext(.{
.document = html_doc,
.httpClient = &self.session.httpClient,
});
// add global objects
log.debug("setup global env", .{});
try self.session.env.bindGlobal(&self.session.window);
// browse the DOM tree to retrieve scripts
// TODO execute the synchronous scripts during the HTL parsing.
// TODO fetch the script resources concurrently but execute them in the
// declaration order for synchronous ones.
// sasync stores scripts which can be run asynchronously.
// for now they are just run after the non-async one in order to
// dispatch DOMContentLoaded the sooner as possible.
var sasync = std.ArrayList(*parser.Element).init(alloc);
defer sasync.deinit();
const root = parser.documentToNode(doc);
const walker = Walker{};
var next: ?*parser.Node = null;
while (true) {
next = try walker.get_next(root, next) orelse break;
// ignore non-elements nodes.
if (try parser.nodeType(next.?) != .element) {
continue;
}
const e = parser.nodeToElement(next.?);
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(e)));
// ignore non-script tags
if (tag != .script) continue;
// ignore non-js script.
// > type
// > Attribute is not set (default), an empty string, or a JavaScript MIME
// > type indicates that the script is a "classic script", containing
// > JavaScript code.
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type
const stype = try parser.elementGetAttribute(e, "type");
if (!isJS(stype)) {
continue;
}
// Ignore the defer attribute b/c we analyze all script
// after the document has been parsed.
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer
// TODO use fetchpriority
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#fetchpriority
// > async
// > For classic scripts, if the async attribute is present,
// > then the classic script will be fetched in parallel to
// > parsing and evaluated as soon as it is available.
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#async
if (try parser.elementGetAttribute(e, "async") != null) {
try sasync.append(e);
continue;
}
// TODO handle for attribute
// TODO handle event attribute
// TODO defer
// > This Boolean attribute is set to indicate to a browser
// > that the script is meant to be executed after the
// > document has been parsed, but before firing
// > DOMContentLoaded.
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer
// defer allow us to load a script w/o blocking the rest of
// evaluations.
// > Scripts without async, defer or type="module"
// > attributes, as well as inline scripts without the
// > type="module" attribute, are fetched and executed
// > immediately before the browser continues to parse the
// > page.
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#notes
self.evalScript(e) catch |err| log.warn("evaljs: {any}", .{err});
}
// TODO wait for deferred scripts
// dispatch DOMContentLoaded before the transition to "complete",
// at the point where all subresources apart from async script elements
// have loaded.
// https://html.spec.whatwg.org/#reporting-document-loading-status
const evt = try parser.eventCreate();
defer parser.eventDestroy(evt);
try parser.eventInit(evt, "DOMContentLoaded", .{ .bubbles = true, .cancelable = true });
_ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, html_doc), evt);
// eval async scripts.
for (sasync.items) |e| {
self.evalScript(e) catch |err| log.warn("evaljs: {any}", .{err});
}
// TODO wait for async scripts
// TODO set document.readyState to complete
// dispatch window.load event
const loadevt = try parser.eventCreate();
defer parser.eventDestroy(loadevt);
try parser.eventInit(loadevt, "load", .{});
_ = try parser.eventTargetDispatchEvent(
parser.toEventTarget(Window, &self.session.window),
loadevt,
);
}
// evalScript evaluates the src in priority.
// if no src is present, we evaluate the text source.
// https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model
fn evalScript(self: *Page, e: *parser.Element) !void {
const alloc = self.arena.allocator();
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
const opt_src = try parser.elementGetAttribute(e, "src");
if (opt_src) |src| {
log.debug("starting GET {s}", .{src});
self.fetchScript(src) catch |err| {
switch (err) {
FetchError.BadStatusCode => return err,
// TODO If el's result is null, then fire an event named error at
// el, and return.
FetchError.NoBody => return,
FetchError.JsErr => {}, // nothing to do here.
else => return err,
}
};
// TODO If el's from an external file is true, then fire an event
// named load at el.
return;
}
var try_catch: jsruntime.TryCatch = undefined;
try_catch.init(self.session.env);
defer try_catch.deinit();
const opt_text = try parser.nodeTextContent(parser.elementToNode(e));
if (opt_text) |text| {
// TODO handle charset attribute
const res = self.session.env.exec(text, "") catch {
if (try try_catch.err(alloc, self.session.env)) |msg| {
defer alloc.free(msg);
log.info("eval inline {s}: {s}", .{ text, msg });
}
return;
};
if (builtin.mode == .Debug) {
const msg = try res.toString(alloc, self.session.env);
defer alloc.free(msg);
log.debug("eval inline {s}", .{msg});
}
return;
}
// nothing has been loaded.
// TODO If el's result is null, then fire an event named error at
// el, and return.
}
const FetchError = error{
BadStatusCode,
NoBody,
JsErr,
};
// fetchScript senf a GET request to the src and execute the script
// received.
fn fetchScript(self: *Page, src: []const u8) !void {
const alloc = self.arena.allocator();
log.debug("starting fetch script {s}", .{src});
var buffer: [1024]u8 = undefined;
var b: []u8 = buffer[0..];
const u = try std.Uri.resolve_inplace(self.uri, src, &b);
var fetchres = try self.session.loader.get(alloc, u);
defer fetchres.deinit();
const resp = fetchres.req.response;
log.info("fech script {any}: {d}", .{ u, resp.status });
if (resp.status != .ok) return FetchError.BadStatusCode;
// TODO check content-type
const body = try fetchres.req.reader().readAllAlloc(alloc, 16 * 1024 * 1024);
defer alloc.free(body);
// check no body
if (body.len == 0) return FetchError.NoBody;
var try_catch: jsruntime.TryCatch = undefined;
try_catch.init(self.session.env);
defer try_catch.deinit();
const res = self.session.env.exec(body, src) catch {
if (try try_catch.err(alloc, self.session.env)) |msg| {
defer alloc.free(msg);
log.info("eval remote {s}: {s}", .{ src, msg });
}
return FetchError.JsErr;
};
if (builtin.mode == .Debug) {
const msg = try res.toString(alloc, self.session.env);
defer alloc.free(msg);
log.debug("eval remote {s}: {s}", .{ src, msg });
}
}
// > type
// > Attribute is not set (default), an empty string, or a JavaScript MIME
// > type indicates that the script is a "classic script", containing
// > JavaScript code.
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type
fn isJS(stype: ?[]const u8) bool {
if (stype == null or stype.?.len == 0) return true;
if (std.mem.eql(u8, stype.?, "application/javascript")) return true;
if (!std.mem.eql(u8, stype.?, "module")) return true;
return false;
}
};

View File

@@ -1,86 +1,132 @@
// 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 Node = @import("webapi/Node.zig");
const File = std.fs.File;
pub const Opts = struct {
// @ZIGDOM (none of these do anything)
with_base: bool = false,
strip_mode: StripMode = .{},
const parser = @import("netsurf");
const Walker = @import("../dom/walker.zig").WalkerChildren;
pub const StripMode = struct {
js: bool = false,
ui: bool = false,
css: bool = false,
};
};
// writer must be a std.io.Writer
pub fn writeHTML(doc: *parser.Document, writer: anytype) !void {
try writer.writeAll("<!DOCTYPE html>\n");
try writeNode(parser.documentToNode(doc), writer);
try writer.writeAll("\n");
}
pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer) error{WriteFailed}!void {
switch (node._type) {
.cdata => |cd| try writer.writeAll(cd.getData()),
.element => |el| {
try el.format(writer);
try children(node, opts, writer);
if (!isVoidElement(el)) {
// writer must be a std.io.Writer
pub fn writeNode(root: *parser.Node, writer: anytype) !void {
const walker = Walker{};
var next: ?*parser.Node = null;
while (true) {
next = try walker.get_next(root, next) orelse break;
switch (try parser.nodeType(next.?)) {
.element => {
// open the tag
const tag = try parser.nodeLocalName(next.?);
try writer.writeAll("<");
try writer.writeAll(tag);
// write the attributes
const map = try parser.nodeGetAttributes(next.?);
const ln = try parser.namedNodeMapGetLength(map);
var i: u32 = 0;
while (i < ln) {
const attr = try parser.namedNodeMapItem(map, i) orelse break;
try writer.writeAll(" ");
try writer.writeAll(try parser.attributeGetName(attr));
try writer.writeAll("=\"");
try writer.writeAll(try parser.attributeGetValue(attr) orelse "");
try writer.writeAll("\"");
i += 1;
}
try writer.writeAll(">");
// void elements can't have any content.
if (try isVoid(parser.nodeToElement(next.?))) continue;
// write the children
// TODO avoid recursion
try writeNode(next.?, writer);
// close the tag
try writer.writeAll("</");
try writer.writeAll(el.getTagNameDump());
try writer.writeByte('>');
}
},
.document => try children(node, opts, writer),
.document_fragment => try children(node, opts, writer),
.attribute => unreachable,
try writer.writeAll(tag);
try writer.writeAll(">");
},
.text => {
const v = try parser.nodeValue(next.?) orelse continue;
try writer.writeAll(v);
},
.cdata_section => {
const v = try parser.nodeValue(next.?) orelse continue;
try writer.writeAll("<![CDATA[");
try writer.writeAll(v);
try writer.writeAll("]]>");
},
.comment => {
const v = try parser.nodeValue(next.?) orelse continue;
try writer.writeAll("<!--");
try writer.writeAll(v);
try writer.writeAll("-->");
},
// TODO handle processing instruction dump
.processing_instruction => continue,
// document fragment is outside of the main document DOM, so we
// don't output it.
.document_fragment => continue,
// document will never be called, but required for completeness.
.document => continue,
// done globally instead, but required for completeness.
.document_type => continue,
// deprecated
.attribute => continue,
.entity_reference => continue,
.entity => continue,
.notation => continue,
}
}
}
pub fn children(parent: *Node, opts: Opts, writer: *std.Io.Writer) !void {
var it = parent.childrenIterator();
while (it.next()) |child| {
try deep(child, opts, writer);
}
}
pub fn toJSON(node: *Node, writer: *std.json.Stringify) !void {
try writer.beginObject();
try writer.objectField("type");
switch (node.type) {
.cdata => {
try writer.write("cdata");
},
.document => {
try writer.write("document");
},
.element => |*el| {
try writer.write("element");
try writer.objectField("tag");
try writer.write(el.tagName());
try writer.objectField("attributes");
try writer.beginObject();
var it = el.attributeIterator();
while (it.next()) |attr| {
try writer.objectField(attr.name);
try writer.write(attr.value);
}
try writer.endObject();
},
}
try writer.objectField("children");
try writer.beginArray();
var it = node.childrenIterator();
while (it.next()) |child| {
try toJSON(child, writer);
}
try writer.endArray();
try writer.endObject();
}
fn isVoidElement(el: *const Node.Element) bool {
return switch (el._type) {
.html => |html| switch (html._type) {
.br, .hr, .img, .input, .link, .meta => true,
else => false,
},
.svg => false,
// area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr
// https://html.spec.whatwg.org/#void-elements
fn isVoid(elem: *parser.Element) !bool {
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(elem)));
return switch (tag) {
.area, .base, .br, .col, .embed, .hr, .img, .input, .link => true,
.meta, .source, .track, .wbr => true,
else => false,
};
}
test "dump.writeHTML" {
const out = try std.fs.openFileAbsolute("/dev/null", .{ .mode = .write_only });
defer out.close();
const file = try std.fs.cwd().openFile("test.html", .{});
defer file.close();
const doc_html = try parser.documentHTMLParse(file.reader(), "UTF-8");
// ignore close error
defer parser.documentHTMLClose(doc_html) catch {};
const doc = parser.documentHTMLToDocument(doc_html);
try writeHTML(doc, out);
}

View File

@@ -1,492 +0,0 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = @import("../../log.zig");
const js = @import("js.zig");
const v8 = js.v8;
const bridge = @import("bridge.zig");
const Context = @import("Context.zig");
const Page = @import("../Page.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 const CallOpts = struct {
dom_exception: bool = false,
null_as_undefined: bool = false,
as_typed_array: bool = false,
};
pub fn constructor(self: *Caller, comptime T: type, func: anytype, info: v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
self._constructor(func, info) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
};
}
pub fn _constructor(self: *Caller, func: anytype, info: v8.FunctionCallbackInfo) !void {
const F = @TypeOf(func);
const args = try self.getArgs(F, 0, info);
const res = @call(.auto, func, args);
const ReturnType = @typeInfo(F).@"fn".return_type orelse {
@compileError(@typeName(F) ++ " 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 T: type, func: anytype, info: v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
self._method(T, func, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
};
}
pub fn _method(self: *Caller, comptime T: type, func: anytype, info: v8.FunctionCallbackInfo, comptime opts: CallOpts) !void {
const F = @TypeOf(func);
var args = try self.getArgs(F, 1, info);
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
const res = @call(.auto, func, args);
info.getReturnValue().set(try self.context.zigValueToJs(res, opts));
}
pub fn function(self: *Caller, comptime T: type, func: anytype, info: v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
self._function(func, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
};
}
pub fn _function(self: *Caller, func: anytype, info: v8.FunctionCallbackInfo, comptime opts: CallOpts) !void {
const F = @TypeOf(func);
const context = self.context;
const args = try self.getArgs(F, 0, info);
const res = @call(.auto, func, args);
info.getReturnValue().set(try context.zigValueToJs(res, opts));
}
pub fn getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
return self._getIndex(T, func, idx, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
return v8.Intercepted.No;
};
}
pub fn _getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args = try self.getArgs(F, 2, info);
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
@field(args, "1") = idx;
const ret = @call(.auto, func, args);
return self.handleIndexedReturn(T, F, ret, info, opts);
}
pub fn getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
return self._getNamedIndex(T, func, name, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
return v8.Intercepted.No;
};
}
pub fn _getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args = try self.getArgs(F, 2, info);
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
@field(args, "1") = try self.nameToString(name);
const ret = @call(.auto, func, args);
return self.handleIndexedReturn(T, F, ret, info, opts);
}
fn handleIndexedReturn(self: *Caller, comptime T: type, comptime F: type, ret: anytype, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
// need to unwrap this error immediately for when opts.null_as_undefined == true
// and we need to compare it to null;
const non_error_ret = switch (@typeInfo(@TypeOf(ret))) {
.error_union => |eu| blk: {
break :blk ret catch |err| {
// We can't compare err == error.NotHandled if error.NotHandled
// isn't part of the possible error set. So we first need to check
// if error.NotHandled is part of the error set.
if (isInErrorSet(error.NotHandled, eu.error_set)) {
if (err == error.NotHandled) {
return v8.Intercepted.No;
}
}
self.handleError(T, F, err, info, opts);
return v8.Intercepted.No;
};
},
else => ret,
};
info.getReturnValue().set(try self.context.zigValueToJs(non_error_ret, opts));
return v8.Intercepted.Yes;
}
fn isInErrorSet(err: anyerror, comptime T: type) bool {
inline for (@typeInfo(T).error_set.?) |e| {
if (err == @field(anyerror, e.name)) return true;
}
return false;
}
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 T: type, comptime F: type) bool {
return checkSelfReceiver(T, F, false);
}
fn assertSelfReceiver(comptime T: type, comptime F: type) void {
_ = checkSelfReceiver(T, F, true);
}
fn checkSelfReceiver(comptime T: type, comptime F: type, comptime fail: bool) bool {
const params = @typeInfo(F).@"fn".params;
if (params.len == 0) {
if (fail) {
@compileError(@typeName(F) ++ " must have a self parameter");
}
return false;
}
const first_param = params[0].type.?;
if (first_param != *T and first_param != *const T) {
if (fail) {
@compileError(std.fmt.comptimePrint("The first parameter to {s} must be a *{s} or *const {s}. Got: {s}", .{
@typeName(F),
@typeName(T),
@typeName(T),
@typeName(first_param),
}));
}
return false;
}
return true;
}
fn assertIsPageArg(comptime T: type, comptime F: type, index: comptime_int) void {
const param = @typeInfo(F).@"fn".params[index].type.?;
if (isPage(param)) {
return;
}
@compileError(std.fmt.comptimePrint("The {d} parameter of {s}.{s} must be a *Page or *const Page. Got: {s}", .{ index, @typeName(T), @typeName(F), @typeName(param) }));
}
fn handleError(self: *Caller, comptime T: type, comptime F: type, err: anyerror, info: anytype, comptime opts: CallOpts) void {
const isolate = self.isolate;
if (comptime @import("builtin").mode == .Debug and @hasDecl(@TypeOf(info), "length")) {
if (log.enabled(.js, .warn)) {
self.logFunctionCallError(@typeName(T), @typeName(F), err, 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: {
if (!comptime opts.dom_exception) {
break :blk null;
}
const DOMException = @import("../webapi/DOMException.zig");
const ex = DOMException.fromError(err) orelse break :blk null;
break :blk self.context.zigValueToJs(ex, .{}) catch js._createException(isolate, "internal error");
},
};
if (js_err == null) {
js_err = js._createException(isolate, @errorName(err));
}
const js_exception = isolate.throwException(js_err.?);
info.getReturnValue().setValueHandle(js_exception.handle);
}
// 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 F: type, comptime offset: usize, info: anytype) !ParameterTypes(F) {
const context = self.context;
var args: ParameterTypes(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(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): " ++ @typeName(F));
} else if (comptime param.type.? == js.This) {
@compileError("JsThis must be the last parameter: " ++ @typeName(F));
} 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(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, type_name: []const u8, func: []const u8, err: anyerror, info: v8.FunctionCallbackInfo) void {
const args_dump = self.serializeFunctionArgs(info) catch "failed to serialize args";
log.info(.js, "function call error", .{
.type = type_name,
.func = func,
.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;
}
// Takes a function, and returns a tuple for its argument. Used when we
// @call a function
fn ParameterTypes(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,349 +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.zig");
const v8 = js.v8;
const log = @import("../../log.zig");
const bridge = @import("bridge.zig");
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;
const JsApis = bridge.JsApis;
// 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 JS_API_LOOKUP and then have
// access to its TunctionTemplate (the thing we need to create an instance
// of it)
// I.e.:
// const index = @field(JS_API_LOOKUP, @typeName(type_name))
// const template = templates[index];
templates: [JsApis.len]v8.FunctionTemplate,
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,
};
// 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 (JsApis, 0..) |JsApi, i| {
@setEvalBranchQuota(10_000);
JsApi.Meta.class_index = i;
templates[i] = v8.Persistent(v8.FunctionTemplate).init(isolate, generateClass(JsApi, isolate)).castToFunctionTemplate();
}
// Above, we've created all our our FunctionTemplates. Now that we
// have them all, we can hook up the prototypes.
inline for (JsApis, 0..) |JsApi, i| {
if (comptime protoIndexLookup(JsApi)) |proto_index| {
templates[i].inherit(templates[proto_index]);
}
}
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 JsApi: type, isolate: v8.Isolate) v8.FunctionTemplate {
const template = generateConstructor(JsApi, isolate);
attachClass(JsApi, 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 JsApi: type, isolate: v8.Isolate, template: v8.FunctionTemplate) void {
const template_proto = template.getPrototypeTemplate();
const declarations = @typeInfo(JsApi).@"struct".decls;
inline for (declarations) |d| {
const name: [:0]const u8 = d.name;
const value = @field(JsApi, name);
const definition = @TypeOf(value);
switch (definition) {
bridge.Accessor => {
const js_name = v8.String.initUtf8(isolate, name).toName();
const getter_callback = v8.FunctionTemplate.initCallback(isolate, value.getter);
if (value.setter == null) {
template_proto.setAccessorGetter(js_name, getter_callback);
} else {
const setter_callback = v8.FunctionTemplate.initCallback(isolate, value.setter);
template_proto.setAccessorGetterAndSetter(js_name, getter_callback, setter_callback);
}
},
bridge.Function => {
const function_template = v8.FunctionTemplate.initCallback(isolate, value.func);
const js_name: v8.Name = v8.String.initUtf8(isolate, name).toName();
if (value.static) {
template.set(js_name, function_template, v8.PropertyAttribute.None);
} else {
template_proto.set(js_name, function_template, v8.PropertyAttribute.None);
}
},
bridge.Indexed => {
const configuration = v8.IndexedPropertyHandlerConfiguration{
.getter = value.getter,
};
template_proto.setIndexedProperty(configuration, null);
},
bridge.NamedIndexed => {
const configuration = v8.NamedPropertyHandlerConfiguration{
.getter = value.getter,
.flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking,
};
template_proto.setNamedProperty(configuration, null);
},
bridge.Iterator => {
// Same as a function, but with a specific name
const function_template = v8.FunctionTemplate.initCallback(isolate, value.func);
const js_name = v8.Symbol.getIterator(isolate).toName();
template_proto.set(js_name, function_template, v8.PropertyAttribute.None);
},
bridge.Property.Int => {
const js_value = js.simpleZigValueToJs(isolate, value.int, true, false);
const js_name = v8.String.initUtf8(isolate, name).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);
},
bridge.Constructor => {}, // already handled in generateClasss
else => {},
}
}
if (@hasDecl(JsApi.Meta, "htmldda")) {
const instance_template = template.getInstanceTemplate();
instance_template.markAsUndetectable();
instance_template.setCallAsFunctionHandler(JsApi.Meta.callable.func);
}
}
// 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 JsApi: type, isolate: v8.Isolate) v8.FunctionTemplate {
const callback = blk: {
if (@hasDecl(JsApi, "constructor")) {
break :blk JsApi.constructor.func;
}
break :blk struct {
fn wrap(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 iso = caller.isolate;
log.warn(.js, "Illegal constructor call", .{ .name = @typeName(JsApi) });
const js_exception = iso.throwException(js._createException(iso, "Illegal Constructor"));
info.getReturnValue().set(js_exception);
return;
}
}.wrap;
};
const template = v8.FunctionTemplate.initCallback(isolate, callback);
template.getInstanceTemplate().setInternalFieldCount(1);
const class_name = v8.String.initUtf8(isolate, if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi));
template.setClassName(class_name);
return template;
}
// fn generateUndetectable(comptime Struct: type, template: v8.ObjectTemplate) void {
// const has_js_call_as_function = @hasDecl(Struct, "jsCallAsFunction");
// if (has_js_call_as_function) {
// if (@hasDecl(Struct, "htmldda") and Struct.htmldda) {
// if (!has_js_call_as_function) {
// @compileError(@typeName(Struct) ++ ": htmldda 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();
// }
// }
pub fn protoIndexLookup(comptime JsApi: type) ?u16 {
@setEvalBranchQuota(2000);
comptime {
const T = JsApi.bridge.type;
if (!@hasField(T, "_proto")) {
return null;
}
const Ptr = std.meta.fieldInfo(T, ._proto).type;
const F = @typeInfo(Ptr).pointer.child;
return @field(bridge.JS_API_LOOKUP, @typeName(F.JsApi));
}
}

View File

@@ -1,231 +0,0 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = @import("../../log.zig");
const js = @import("js.zig");
const v8 = js.v8;
const bridge = @import("bridge.zig");
const Env = @import("Env.zig");
const Context = @import("Context.zig");
const Page = @import("../Page.zig");
const ScriptManager = @import("../ScriptManager.zig");
const ArenaAllocator = std.heap.ArenaAllocator;
const CONTEXT_ARENA_RETAIN = 1024 * 64;
const JsApis = bridge.JsApis;
// 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 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);
js_global.setClassName(v8.String.initUtf8(isolate, "Window"));
Env.attachClass(@TypeOf(page.window.*).JsApi, 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 (JsApis, 0..) |JsApi, i| {
if (@hasDecl(JsApi.Meta, "name")) {
const class_name = v8.String.initUtf8(isolate, JsApi.Meta.name);
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.
{
const proto_type = @typeInfo(@TypeOf(page.window._proto)).pointer.child;
const proto_index = @field(bridge.JS_API_LOOKUP, @typeName(proto_type.JsApi));
js_global.inherit(templates[proto_index]);
}
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 (JsApis, 0..) |JsApi, i| {
if (comptime Env.protoIndexLookup(JsApi)) |proto_index| {
const proto_obj = templates[proto_index].getFunction(v8_context).toObject();
const self_obj = templates[i].getFunction(v8_context).toObject();
_ = self_obj.setPrototype(v8_context, proto_obj);
}
}
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,
.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);
}
// @ZIGDOM
// Custom exception
// NOTE: there is no way in v8 to subclass the Error built-in type
// TODO: this is an horrible hack
// inline for (JsApi) |JsApi| {
// const Struct = s.defaultValue().?;
// if (@hasDecl(Struct, "ErrorSet")) {
// const script = comptime JsApi.Meta.name ++ ".prototype.__proto__ = Error.prototype";
// _ = try context.exec(script, "errorSubclass");
// }
// }
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,161 +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.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 {};
return context.jsValueToZig(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,143 +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.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, 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 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));
}

View File

@@ -1,157 +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.zig");
const v8 = js.v8;
const Caller = @import("Caller.zig");
const Context = @import("Context.zig");
const PersistentObject = v8.Persistent(v8.Object);
const Allocator = std.mem.Allocator;
const Object = @This();
js_obj: v8.Object,
context: *js.Context,
pub const SetOpts = packed struct(u32) {
READ_ONLY: bool = false,
DONT_ENUM: bool = false,
DONT_DELETE: bool = false,
_: u29 = 0,
};
pub fn setIndex(self: Object, index: u32, value: anytype, opts: SetOpts) !void {
@setEvalBranchQuota(10000);
const key = switch (index) {
inline 0...20 => |i| std.fmt.comptimePrint("{d}", .{i}),
else => try std.fmt.allocPrint(self.context.arena, "{d}", .{index}),
};
return self.set(key, value, opts);
}
pub fn set(self: Object, key: []const u8, value: anytype, opts: SetOpts) error{ FailedToSet, OutOfMemory }!void {
const context = self.context;
const js_key = v8.String.initUtf8(context.isolate, key);
const js_value = try context.zigValueToJs(value);
const res = self.js_obj.defineOwnProperty(context.v8_context, js_key.toName(), js_value, @bitCast(opts)) orelse false;
if (!res) {
return error.FailedToSet;
}
}
pub fn get(self: Object, key: []const u8) !js.Value {
const context = self.context;
const js_key = v8.String.initUtf8(context.isolate, key);
const js_val = try self.js_obj.getValue(context.v8_context, js_key);
return context.createValue(js_val);
}
pub fn isTruthy(self: Object) bool {
const js_value = self.js_obj.toValue();
return js_value.toBool(self.context.isolate);
}
pub fn toString(self: Object) ![]const u8 {
const js_value = self.js_obj.toValue();
return self.context.valueToString(js_value, .{});
}
pub fn toDetailString(self: Object) ![]const u8 {
const js_value = self.js_obj.toValue();
return self.context.valueToDetailString(js_value);
}
pub fn format(self: Object, writer: *std.Io.Writer) !void {
const str = self.toString() catch return error.WriteFailed;
return writer.writeAll(str);
}
pub fn toJson(self: Object, allocator: Allocator) ![]u8 {
const json_string = try v8.Json.stringify(self.context.v8_context, self.js_obj.toValue(), null);
const str = try self.context.jsStringToZig(json_string, .{ .allocator = allocator });
return str;
}
pub fn persist(self: Object) !Object {
var context = self.context;
const js_obj = self.js_obj;
const persisted = PersistentObject.init(context.isolate, js_obj);
try context.js_object_list.append(context.arena, persisted);
return .{
.context = context,
.js_obj = persisted.castToObject(),
};
}
pub fn getFunction(self: Object, name: []const u8) !?js.Function {
if (self.isNullOrUndefined()) {
return null;
}
const context = self.context;
const js_name = v8.String.initUtf8(context.isolate, name);
const js_value = try self.js_obj.getValue(context.v8_context, js_name.toName());
if (!js_value.isFunction()) {
return null;
}
return try context.createFunction(js_value);
}
pub fn isNull(self: Object) bool {
return self.js_obj.toValue().isNull();
}
pub fn isUndefined(self: Object) bool {
return self.js_obj.toValue().isUndefined();
}
pub fn isNullOrUndefined(self: Object) bool {
return self.js_obj.toValue().isNullOrUndefined();
}
pub fn nameIterator(self: Object) js.ValueIterator {
const context = self.context;
const js_obj = self.js_obj;
const array = js_obj.getPropertyNames(context.v8_context);
const count = array.length();
return .{
.count = count,
.context = context,
.js_obj = array.castTo(v8.Object),
};
}
pub fn toZig(self: Object, comptime T: type) !T {
return self.context.jsValueToZig(T, self.js_obj.toValue());
}
pub fn TriState(comptime T: type) type {
return union(enum) {
null: void,
undefined: void,
value: T,
};
}

View File

@@ -1,39 +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 js = @import("js.zig");
const v8 = js.v8;
const Platform = @This();
inner: v8.Platform,
pub fn init() !Platform {
if (v8.initV8ICU() == false) {
return error.FailedToInitializeICU;
}
const platform = v8.Platform.initDefault(0, true);
v8.initV8Platform(platform);
v8.initV8();
return .{ .inner = platform };
}
pub fn deinit(self: Platform) void {
_ = v8.deinitV8();
v8.deinitV8Platform();
self.inner.deinit();
}

View File

@@ -1,43 +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.zig");
const v8 = js.v8;
const Allocator = std.mem.Allocator;
// This only exists so that we know whether a function wants the opaque
// JS argument (js.Object), or if it wants the receiver as an opaque
// value.
// js.Object is normally used when a method wants an opaque JS object
// that it'll pass into a callback.
// This is used when the function wants to do advanced manipulation
// of the v8.Object bound to the instance. For example, postAttach is an
// example of using This.
const This = @This();
obj: js.Object,
pub fn setIndex(self: This, index: u32, value: anytype, opts: js.Object.SetOpts) !void {
return self.obj.setIndex(index, value, opts);
}
pub fn set(self: This, key: []const u8, value: anytype, opts: js.Object.SetOpts) !void {
return self.obj.set(key, value, opts);
}

View File

@@ -1,82 +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.zig");
const v8 = js.v8;
const Allocator = std.mem.Allocator;
const TryCatch = @This();
inner: v8.TryCatch,
context: *const js.Context,
pub fn init(self: *TryCatch, context: *const js.Context) void {
self.context = context;
self.inner.init(context.isolate);
}
pub fn hasCaught(self: TryCatch) bool {
return self.inner.hasCaught();
}
// the caller needs to deinit the string returned
pub fn exception(self: TryCatch, allocator: Allocator) !?[]const u8 {
const msg = self.inner.getException() orelse return null;
return try self.context.valueToString(msg, .{ .allocator = allocator });
}
// the caller needs to deinit the string returned
pub fn stack(self: TryCatch, allocator: Allocator) !?[]const u8 {
const context = self.context;
const s = self.inner.getStackTrace(context.v8_context) orelse return null;
return try context.valueToString(s, .{ .allocator = allocator });
}
// the caller needs to deinit the string returned
pub fn sourceLine(self: TryCatch, allocator: Allocator) !?[]const u8 {
const context = self.context;
const msg = self.inner.getMessage() orelse return null;
const sl = msg.getSourceLine(context.v8_context) orelse return null;
return try context.jsStringToZig(sl, .{ .allocator = allocator });
}
pub fn sourceLineNumber(self: TryCatch) ?u32 {
const context = self.context;
const msg = self.inner.getMessage() orelse return null;
return msg.getLineNumber(context.v8_context);
}
// a shorthand method to return either the entire stack message
// or just the exception message
// - in Debug mode return the stack if available
// - otherwise return the exception if available
// the caller needs to deinit the string returned
pub fn err(self: TryCatch, allocator: Allocator) !?[]const u8 {
if (comptime @import("builtin").mode == .Debug) {
if (try self.stack(allocator)) |msg| {
return msg;
}
}
return try self.exception(allocator);
}
pub fn deinit(self: *TryCatch) void {
self.inner.deinit();
}

View File

@@ -1,502 +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.zig");
const log = @import("../../log.zig");
const v8 = js.v8;
const Caller = @import("Caller.zig");
pub fn Builder(comptime T: type) type {
return struct {
pub const @"type" = T;
pub fn constructor(comptime func: anytype, comptime opts: Constructor.Opts) Constructor {
return Constructor.init(T, func, opts);
}
pub fn accessor(comptime getter: anytype, comptime setter: anytype, comptime opts: Accessor.Opts) Accessor {
return Accessor.init(T, getter, setter, opts);
}
pub fn function(comptime func: anytype, comptime opts: Function.Opts) Function {
return Function.init(T, func, opts);
}
pub fn indexed(comptime getter_func: anytype, comptime opts: Indexed.Opts) Indexed {
return Indexed.init(T, getter_func, opts);
}
pub fn namedIndexed(comptime getter_func: anytype, comptime opts: NamedIndexed.Opts) NamedIndexed {
return NamedIndexed.init(T, getter_func, opts);
}
pub fn iterator(comptime func: anytype, comptime opts: Iterator.Opts) Iterator {
return Iterator.init(T, func, opts);
}
pub fn callable(comptime func: anytype, comptime opts: Callable.Opts) Callable {
return Callable.init(T, func, opts);
}
pub fn property(value: anytype) Property.GetType(@TypeOf(value)) {
return Property.GetType(@TypeOf(value)).init(value);
}
pub fn prototypeChain() [prototypeChainLength(T)]js.PrototypeChainEntry {
var entries: [prototypeChainLength(T)]js.PrototypeChainEntry = undefined;
entries[0] = .{
.offset = 0,
.index = @field(JS_API_LOOKUP, @typeName(T.JsApi)),
};
if (entries.len == 1) {
return entries;
}
var Prototype = T;
for (entries[1..]) |*entry| {
const Next = PrototypeType(Prototype).?;
entry.* = .{
.index = @field(JS_API_LOOKUP, @typeName(Next.JsApi)),
.offset = @offsetOf(Prototype, "_proto"),
};
Prototype = Next;
}
return entries;
}
};
}
pub const Constructor = struct {
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
const Opts = struct {
dom_exception: bool = false,
};
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Constructor {
return .{ .func = struct {
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
caller.constructor(T, func, info, .{
.dom_exception = opts.dom_exception,
});
}
}.wrap };
}
};
pub const Function = struct {
static: bool,
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
const Opts = struct {
static: bool = false,
dom_exception: bool = false,
as_typed_array: bool = false,
null_as_undefined: bool = false,
};
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Function {
return .{
.static = opts.static,
.func = struct {
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
if (comptime opts.static) {
caller.function(T, func, info, .{
.dom_exception = opts.dom_exception,
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
} else {
caller.method(T, func, info, .{
.dom_exception = opts.dom_exception,
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
}
}
}.wrap,
};
}
};
pub const Accessor = struct {
getter: ?*const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void = null,
setter: ?*const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void = null,
const Opts = struct {
cache: ?[]const u8 = null, // @ZIGDOM
as_typed_array: bool = false,
null_as_undefined: bool = false,
};
fn init(comptime T: type, comptime getter: anytype, comptime setter: anytype, comptime opts: Opts) Accessor {
var accessor = Accessor{};
if (@typeInfo(@TypeOf(getter)) != .null) {
accessor.getter = struct {
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
caller.method(T, getter, info, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
}
}.wrap;
}
if (@typeInfo(@TypeOf(setter)) != .null) {
accessor.setter = struct {
fn wrap(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();
caller.method(T, setter, info, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
}
}.wrap;
}
return accessor;
}
};
pub const Indexed = struct {
getter: *const fn (idx: u32, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8,
const Opts = struct {
as_typed_array: bool = false,
null_as_undefined: bool = false,
};
fn init(comptime T: type, comptime getter: anytype, comptime opts: Opts) Indexed {
return .{ .getter = struct {
fn wrap(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();
return caller.getIndex(T, getter, idx, info, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
}
}.wrap };
}
};
pub const NamedIndexed = struct {
getter: *const fn (c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8,
const Opts = struct {
as_typed_array: bool = false,
null_as_undefined: bool = false,
};
fn init(comptime T: type, comptime getter: anytype, comptime opts: Opts) NamedIndexed {
return .{ .getter = struct {
fn wrap(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();
return caller.getNamedIndex(T, getter, .{ .handle = c_name.? }, info, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
}
}.wrap };
}
};
pub const Iterator = struct {
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
const Opts = struct {};
fn init(comptime T: type, comptime struct_or_func: anytype, comptime opts: Opts) Iterator {
_ = opts;
if (@typeInfo(@TypeOf(struct_or_func)) == .type) {
return .{ .func = struct {
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
info.getReturnValue().set(info.getThis());
}
}.wrap };
}
return .{ .func = struct {
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
caller.method(T, struct_or_func, info, .{});
}
}.wrap };
}
};
pub const Callable = struct {
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
const Opts = struct {
null_as_undefined: bool = false,
};
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Callable {
return .{.func = struct {
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
caller.method(T, func, info, .{
.null_as_undefined = opts.null_as_undefined,
});
}}.wrap
};
}
};
pub const Property = struct {
fn GetType(comptime T: type) type {
switch (@typeInfo(T)) {
.comptime_int, .int => return Int,
else => @compileError("Property for " ++ @typeName(T) ++ " hasn't been defined yet"),
}
}
pub const Int = struct {
int: i64,
pub fn init(value: i64) Int {
return .{ .int = value };
}
};
};
// Given a Type, returns the length of the prototype chain, including self
fn prototypeChainLength(comptime T: type) usize {
var l: usize = 1;
var Next = T;
while (PrototypeType(Next)) |N| {
Next = N;
l += 1;
}
return l;
}
// Given a Type, gets its prototype Type (if any)
fn PrototypeType(comptime T: type) ?type {
if (!@hasField(T, "_proto")) {
return null;
}
return Struct(std.meta.fieldInfo(T, ._proto).type);
}
fn flattenTypes(comptime Types: []const type) [countFlattenedTypes(Types)]type {
var index: usize = 0;
var flat: [countFlattenedTypes(Types)]type = undefined;
for (Types) |T| {
if (@hasDecl(T, "registerTypes")) {
for (T.registerTypes()) |TT| {
flat[index] = TT.JsApi;
index += 1;
}
} else {
flat[index] = T.JsApi;
index += 1;
}
}
return flat;
}
fn countFlattenedTypes(comptime Types: []const type) usize {
var c: usize = 0;
for (Types) |T| {
c += if (@hasDecl(T, "registerTypes")) T.registerTypes().len else 1;
}
return c;
}
// T => T
// *T => T
pub fn Struct(comptime T: type) type {
return switch (@typeInfo(T)) {
.@"struct" => T,
.pointer => |ptr| ptr.child,
else => @compileError("Expecting Struct or *Struct, got: " ++ @typeName(T)),
};
}
// Imagine we have a type Cat which has a getter:
//
// fn getOwner(self: *Cat) *Owner {
// return self.owner;
// }
//
// When we execute caller.getter, we'll end up doing something like:
// const res = @call(.auto, Cat.getOwner, .{cat_instance});
//
// How do we turn `res`, which is an *Owner, into something we can return
// to v8? We need the ObjectTemplate associated with Owner. How do we
// get that? Well, we store all the ObjectTemplates in an array that's
// tied to env. So we do something like:
//
// env.templates[index_of_owner].initInstance(...);
//
// But how do we get that `index_of_owner`? `Lookup` is a struct
// that looks like:
//
// const Lookup = struct {
// comptime cat: usize = 0,
// comptime owner: usize = 1,
// ...
// }
//
// So to get the template index of `owner`, we can do:
//
// const index_id = @field(type_lookup, @typeName(@TypeOf(res));
//
pub const JsApiLookup = blk: {
var fields: [JsApis.len]std.builtin.Type.StructField = undefined;
for (JsApis, 0..) |JsApi, i| {
fields[i] = .{
.name = @typeName(JsApi),
.type = u16,
.is_comptime = true,
.alignment = @alignOf(u16),
.default_value_ptr = @ptrCast(&i),
};
}
break :blk @Type(.{ .@"struct" = .{
.layout = .auto,
.decls = &.{},
.is_tuple = false,
.fields = &fields,
} });
};
pub const JS_API_LOOKUP = JsApiLookup{};
pub const SubType = enum {
@"error",
array,
arraybuffer,
dataview,
date,
generator,
iterator,
map,
node,
promise,
proxy,
regexp,
set,
typedarray,
wasmvalue,
weakmap,
weakset,
webassemblymemory,
};
pub const JsApis = flattenTypes(&.{
@import("../webapi/AbortController.zig"),
@import("../webapi/AbortSignal.zig"),
@import("../webapi/CData.zig"),
@import("../webapi/cdata/Comment.zig"),
@import("../webapi/cdata/Text.zig"),
@import("../webapi/collections.zig"),
@import("../webapi/Console.zig"),
@import("../webapi/Crypto.zig"),
@import("../webapi/css/CSSStyleDeclaration.zig"),
@import("../webapi/css/CSSStyleProperties.zig"),
@import("../webapi/Document.zig"),
@import("../webapi/HTMLDocument.zig"),
@import("../webapi/DocumentFragment.zig"),
@import("../webapi/DOMException.zig"),
@import("../webapi/DOMTreeWalker.zig"),
@import("../webapi/DOMNodeIterator.zig"),
@import("../webapi/NodeFilter.zig"),
@import("../webapi/Element.zig"),
@import("../webapi/element/Attribute.zig"),
@import("../webapi/element/Html.zig"),
@import("../webapi/element/html/Anchor.zig"),
@import("../webapi/element/html/Body.zig"),
@import("../webapi/element/html/BR.zig"),
@import("../webapi/element/html/Button.zig"),
@import("../webapi/element/html/Custom.zig"),
@import("../webapi/element/html/Div.zig"),
@import("../webapi/element/html/Form.zig"),
@import("../webapi/element/html/Generic.zig"),
@import("../webapi/element/html/Head.zig"),
@import("../webapi/element/html/Heading.zig"),
@import("../webapi/element/html/HR.zig"),
@import("../webapi/element/html/Html.zig"),
@import("../webapi/element/html/Image.zig"),
@import("../webapi/element/html/Input.zig"),
@import("../webapi/element/html/LI.zig"),
@import("../webapi/element/html/Link.zig"),
@import("../webapi/element/html/Meta.zig"),
@import("../webapi/element/html/OL.zig"),
@import("../webapi/element/html/Option.zig"),
@import("../webapi/element/html/Paragraph.zig"),
@import("../webapi/element/html/Script.zig"),
@import("../webapi/element/html/Select.zig"),
@import("../webapi/element/html/Style.zig"),
@import("../webapi/element/html/TextArea.zig"),
@import("../webapi/element/html/Title.zig"),
@import("../webapi/element/html/UL.zig"),
@import("../webapi/element/html/Unknown.zig"),
@import("../webapi/element/Svg.zig"),
@import("../webapi/element/svg/Generic.zig"),
@import("../webapi/encoding/TextDecoder.zig"),
@import("../webapi/encoding/TextEncoder.zig"),
@import("../webapi/Event.zig"),
@import("../webapi/event/ErrorEvent.zig"),
@import("../webapi/event/ProgressEvent.zig"),
@import("../webapi/EventTarget.zig"),
@import("../webapi/Location.zig"),
@import("../webapi/Navigator.zig"),
@import("../webapi/net/Request.zig"),
@import("../webapi/net/Response.zig"),
@import("../webapi/net/URLSearchParams.zig"),
@import("../webapi/net/XMLHttpRequest.zig"),
@import("../webapi/net/XMLHttpRequestEventTarget.zig"),
@import("../webapi/Node.zig"),
@import("../webapi/storage/storage.zig"),
@import("../webapi/URL.zig"),
@import("../webapi/Window.zig"),
@import("../webapi/MutationObserver.zig"),
});

View File

@@ -1,502 +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");
pub const v8 = @import("v8");
const log = @import("../../log.zig");
pub const Env = @import("Env.zig");
pub const bridge = @import("bridge.zig");
pub const ExecutionWorld = @import("ExecutionWorld.zig");
pub const Context = @import("Context.zig");
pub const Inspector = @import("Inspector.zig");
// TODO: Is "This" really necessary?
pub const This = @import("This.zig");
pub const Object = @import("Object.zig");
pub const TryCatch = @import("TryCatch.zig");
pub const Function = @import("Function.zig");
const Caller = @import("Caller.zig");
const Page = @import("../Page.zig");
const Allocator = std.mem.Allocator;
const NamedFunction = Context.NamedFunction;
pub fn Bridge(comptime T: type) type {
return bridge.Builder(T);
}
// If a function returns a []i32, should that map to a plain-old
// JavaScript array, or a Int32Array? It's ambiguous. By default, we'll
// map arrays/slices to the JavaScript arrays. If you want a TypedArray
// wrap it in this.
// Also, this type has nothing to do with the Env. But we place it here
// for consistency. Want a callback? Env.Callback. Want a JsObject?
// Env.JsObject. Want a TypedArray? Env.TypedArray.
pub fn TypedArray(comptime T: type) type {
return struct {
pub const _TYPED_ARRAY_ID_KLUDGE = true;
values: []const T,
pub fn dupe(self: TypedArray(T), allocator: Allocator) !TypedArray(T) {
return .{ .values = try allocator.dupe(T, self.values) };
}
};
}
pub const PromiseResolver = struct {
context: *Context,
resolver: v8.PromiseResolver,
pub fn promise(self: PromiseResolver) Promise {
return self.resolver.getPromise();
}
pub fn resolve(self: PromiseResolver, value: anytype) !void {
const context = self.context;
const js_value = try context.zigValueToJs(value);
// resolver.resolve will return null if the promise isn't pending
const ok = self.resolver.resolve(context.v8_context, js_value) orelse return;
if (!ok) {
return error.FailedToResolvePromise;
}
}
pub fn reject(self: PromiseResolver, value: anytype) !void {
const context = self.context;
const js_value = try context.zigValueToJs(value);
// resolver.reject will return null if the promise isn't pending
const ok = self.resolver.reject(context.v8_context, js_value) orelse return;
if (!ok) {
return error.FailedToRejectPromise;
}
}
};
pub const PersistentPromiseResolver = struct {
context: *Context,
resolver: v8.Persistent(v8.PromiseResolver),
pub fn deinit(self: *PersistentPromiseResolver) void {
self.resolver.deinit();
}
pub fn promise(self: PersistentPromiseResolver) Promise {
return self.resolver.castToPromiseResolver().getPromise();
}
pub fn resolve(self: PersistentPromiseResolver, value: anytype) !void {
const context = self.context;
const js_value = try context.zigValueToJs(value, .{});
// resolver.resolve will return null if the promise isn't pending
const ok = self.resolver.castToPromiseResolver().resolve(context.v8_context, js_value) orelse return;
if (!ok) {
return error.FailedToResolvePromise;
}
}
pub fn reject(self: PersistentPromiseResolver, value: anytype) !void {
const context = self.context;
const js_value = try context.zigValueToJs(value, .{});
// resolver.reject will return null if the promise isn't pending
const ok = self.resolver.castToPromiseResolver().reject(context.v8_context, js_value) orelse return;
if (!ok) {
return error.FailedToRejectPromise;
}
}
};
pub const Promise = v8.Promise;
// When doing jsValueToZig, string ([]const u8) are managed by the
// call_arena. That means that if the API wants to persist the string
// (which is relatively common), it needs to dupe it again.
// If the parameter is an Env.String rather than a []const u8, then
// the page's arena will be used (rather than the call arena).
pub const String = struct {
string: []const u8,
};
pub const Exception = struct {
inner: v8.Value,
context: *const Context,
// the caller needs to deinit the string returned
pub fn exception(self: Exception, allocator: Allocator) ![]const u8 {
return self.context.valueToString(self.inner, .{ .allocator = allocator });
}
};
pub const Value = struct {
value: v8.Value,
context: *const Context,
// the caller needs to deinit the string returned
pub fn toString(self: Value, allocator: Allocator) ![]const u8 {
return self.context.valueToString(self.value, .{ .allocator = allocator });
}
pub fn fromJson(ctx: *Context, json: []const u8) !Value {
const json_string = v8.String.initUtf8(ctx.isolate, json);
const value = try v8.Json.parse(ctx.v8_context, json_string);
return Value{ .context = ctx, .value = value };
}
};
pub const ValueIterator = struct {
count: u32,
idx: u32 = 0,
js_obj: v8.Object,
context: *const Context,
pub fn next(self: *ValueIterator) !?Value {
const idx = self.idx;
if (idx == self.count) {
return null;
}
self.idx += 1;
const context = self.context;
const js_val = try self.js_obj.getAtIndex(context.v8_context, idx);
return context.createValue(js_val);
}
};
pub fn UndefinedOr(comptime T: type) type {
return union(enum) {
undefined: void,
value: T,
};
}
// An interface for types that want to have their jsScopeEnd function be
// called when the call context ends
const CallScopeEndCallback = struct {
ptr: *anyopaque,
callScopeEndFn: *const fn (ptr: *anyopaque) void,
fn init(ptr: anytype) CallScopeEndCallback {
const T = @TypeOf(ptr);
const ptr_info = @typeInfo(T);
const gen = struct {
pub fn callScopeEnd(pointer: *anyopaque) void {
const self: T = @ptrCast(@alignCast(pointer));
return ptr_info.pointer.child.jsCallScopeEnd(self);
}
};
return .{
.ptr = ptr,
.callScopeEndFn = gen.callScopeEnd,
};
}
pub fn callScopeEnd(self: CallScopeEndCallback) void {
self.callScopeEndFn(self.ptr);
}
};
// Callback called on global's property missing.
// Return true to intercept the execution or false to let the call
// continue the chain.
pub const GlobalMissingCallback = struct {
ptr: *anyopaque,
missingFn: *const fn (ptr: *anyopaque, name: []const u8, ctx: *Context) bool,
pub fn init(ptr: anytype) GlobalMissingCallback {
const T = @TypeOf(ptr);
const ptr_info = @typeInfo(T);
const gen = struct {
pub fn missing(pointer: *anyopaque, name: []const u8, ctx: *Context) bool {
const self: T = @ptrCast(@alignCast(pointer));
return ptr_info.pointer.child.missing(self, name, ctx);
}
};
return .{
.ptr = ptr,
.missingFn = gen.missing,
};
}
pub fn missing(self: GlobalMissingCallback, name: []const u8, ctx: *Context) bool {
return self.missingFn(self.ptr, name, ctx);
}
};
// Attributes that return a primitive type are setup directly on the
// FunctionTemplate when the Env is setup. More complex types need a v8.Context
// and cannot be set directly on the FunctionTemplate.
// We default to saying types are primitives because that's mostly what
// we have. If we add a new complex type that isn't explictly handled here,
// we'll get a compiler error in simpleZigValueToJs, and can then explicitly
// add the type here.
pub fn isComplexAttributeType(ti: std.builtin.Type) bool {
return switch (ti) {
.array => true,
else => false,
};
}
// These are simple types that we can convert to JS with only an isolate. This
// is separated from the Caller's zigValueToJs to make it available when we
// don't have a caller (i.e., when setting static attributes on types)
pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bool, comptime null_as_undefined: bool) if (fail) v8.Value else ?v8.Value {
switch (@typeInfo(@TypeOf(value))) {
.void => return v8.initUndefined(isolate).toValue(),
.null => if (comptime null_as_undefined) return v8.initUndefined(isolate).toValue() else return v8.initNull(isolate).toValue(),
.bool => return v8.getValue(if (value) v8.initTrue(isolate) else v8.initFalse(isolate)),
.int => |n| switch (n.signedness) {
.signed => {
if (value >= -2_147_483_648 and value <= 2_147_483_647) {
return v8.Integer.initI32(isolate, @intCast(value)).toValue();
}
if (comptime n.bits <= 64) {
return v8.getValue(v8.BigInt.initI64(isolate, @intCast(value)));
}
@compileError(@typeName(value) ++ " is not supported");
},
.unsigned => {
if (value <= 4_294_967_295) {
return v8.Integer.initU32(isolate, @intCast(value)).toValue();
}
if (comptime n.bits <= 64) {
return v8.getValue(v8.BigInt.initU64(isolate, @intCast(value)));
}
@compileError(@typeName(value) ++ " is not supported");
},
},
.comptime_int => {
if (value >= 0) {
if (value <= 4_294_967_295) {
return v8.Integer.initU32(isolate, @intCast(value)).toValue();
}
return v8.BigInt.initU64(isolate, @intCast(value)).toValue();
}
if (value >= -2_147_483_648) {
return v8.Integer.initI32(isolate, @intCast(value)).toValue();
}
return v8.BigInt.initI64(isolate, @intCast(value)).toValue();
},
.comptime_float => return v8.Number.init(isolate, value).toValue(),
.float => |f| switch (f.bits) {
64 => return v8.Number.init(isolate, value).toValue(),
32 => return v8.Number.init(isolate, @floatCast(value)).toValue(),
else => @compileError(@typeName(value) ++ " is not supported"),
},
.pointer => |ptr| {
if (ptr.size == .slice and ptr.child == u8) {
return v8.String.initUtf8(isolate, value).toValue();
}
if (ptr.size == .one) {
const one_info = @typeInfo(ptr.child);
if (one_info == .array and one_info.array.child == u8) {
return v8.String.initUtf8(isolate, value).toValue();
}
}
},
.array => return simpleZigValueToJs(isolate, &value, fail, null_as_undefined),
.optional => {
if (value) |v| {
return simpleZigValueToJs(isolate, v, fail, null_as_undefined);
}
if (comptime null_as_undefined) {
return v8.initUndefined(isolate).toValue();
}
return v8.initNull(isolate).toValue();
},
.@"struct" => {
const T = @TypeOf(value);
if (@hasDecl(T, "_TYPED_ARRAY_ID_KLUDGE")) {
const values = value.values;
const value_type = @typeInfo(@TypeOf(values)).pointer.child;
const len = values.len;
const bits = switch (@typeInfo(value_type)) {
.int => |n| n.bits,
.float => |f| f.bits,
else => @compileError("Invalid TypeArray type: " ++ @typeName(value_type)),
};
var array_buffer: v8.ArrayBuffer = undefined;
if (len == 0) {
array_buffer = v8.ArrayBuffer.init(isolate, 0);
} else {
const buffer_len = len * bits / 8;
const backing_store = v8.BackingStore.init(isolate, buffer_len);
const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData()));
@memcpy(data[0..buffer_len], @as([]const u8, @ptrCast(values))[0..buffer_len]);
array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr());
}
switch (@typeInfo(value_type)) {
.int => |n| switch (n.signedness) {
.unsigned => switch (n.bits) {
8 => return v8.Uint8Array.init(array_buffer, 0, len).toValue(),
16 => return v8.Uint16Array.init(array_buffer, 0, len).toValue(),
32 => return v8.Uint32Array.init(array_buffer, 0, len).toValue(),
64 => return v8.BigUint64Array.init(array_buffer, 0, len).toValue(),
else => {},
},
.signed => switch (n.bits) {
8 => return v8.Int8Array.init(array_buffer, 0, len).toValue(),
16 => return v8.Int16Array.init(array_buffer, 0, len).toValue(),
32 => return v8.Int32Array.init(array_buffer, 0, len).toValue(),
64 => return v8.BigInt64Array.init(array_buffer, 0, len).toValue(),
else => {},
},
},
.float => |f| switch (f.bits) {
32 => return v8.Float32Array.init(array_buffer, 0, len).toValue(),
64 => return v8.Float64Array.init(array_buffer, 0, len).toValue(),
else => {},
},
else => {},
}
// We normally don't fail in this function unless fail == true
// but this can never be valid.
@compileError("Invalid TypeArray type: " ++ @typeName(value_type));
}
},
.@"union" => return simpleZigValueToJs(isolate, std.meta.activeTag(value), fail, null_as_undefined),
.@"enum" => {
const T = @TypeOf(value);
if (@hasDecl(T, "toString")) {
return simpleZigValueToJs(isolate, value.toString(), fail, null_as_undefined);
}
},
else => {},
}
if (fail) {
@compileError("Unsupported Zig type " ++ @typeName(@TypeOf(value)));
}
return null;
}
pub fn _createException(isolate: v8.Isolate, msg: []const u8) v8.Value {
return v8.Exception.initError(v8.String.initUtf8(isolate, msg));
}
pub fn classNameForStruct(comptime Struct: type) []const u8 {
if (@hasDecl(Struct, "js_name")) {
return Struct.js_name;
}
@setEvalBranchQuota(10_000);
const full_name = @typeName(Struct);
const last = std.mem.lastIndexOfScalar(u8, full_name, '.') orelse return full_name;
return full_name[last + 1 ..];
}
// When we return a Zig object to V8, we put it on the heap and pass it into
// v8 as an *anyopaque (i.e. void *). When V8 gives us back the value, say, as a
// function parameter, we know what type it _should_ be.
//
// In a simple/perfect world, we could use this knowledge to cast the *anyopaque
// to the parameter type:
// const arg: @typeInfo(@TypeOf(function)).@"fn".params[0] = @ptrCast(v8_data);
//
// But there are 2 reasons we can't do that.
//
// == Reason 1 ==
// The JS code might pass the wrong type:
//
// var cat = new Cat();
// cat.setOwner(new Cat());
//
// The zig_setOwner method expects the 2nd parameter to be an *Owner, but
// the JS code passed a *Cat.
//
// To solve this issue, we tag every returned value so that we can check what
// type it is. In the above case, we'd expect an *Owner, but the tag would tell
// us that we got a *Cat. We use the type index in our Types lookup as the tag.
//
// == Reason 2 ==
// Because of prototype inheritance, even "correct" code can be a challenge. For
// example, say the above JavaScript is fixed:
//
// var cat = new Cat();
// cat.setOwner(new Owner("Leto"));
//
// The issue is that setOwner might not expect an *Owner, but rather a
// *Person, which is the prototype for Owner. Now our Zig code is expecting
// a *Person, but it was (correctly) given an *Owner.
// For this reason, we also store the prototype chain.
pub const TaggedAnyOpaque = struct {
prototype_len: u16,
prototype_chain: [*]const PrototypeChainEntry,
// Ptr to the Zig instance. Between the context where it's called (i.e.
// we have the comptime parameter info for all functions), and the index field
// we can figure out what type this is.
value: *anyopaque,
// When we're asked to describe an object via the Inspector, we _must_ include
// the proper subtype (and description) fields in the returned JSON.
// V8 will give us a Value and ask us for the subtype. From the v8.Value we
// can get a v8.Object, and from the v8.Object, we can get out TaggedAnyOpaque
// which is where we store the subtype.
subtype: ?bridge.SubType,
};
pub const PrototypeChainEntry = struct {
index: u16,
offset: u16, // offset to the _proto field
};
// These are here, and not in Inspector.zig, because Inspector.zig isn't always
// included (e.g. in the wpt build).
// This is called from V8. Whenever the v8 inspector has to describe a value
// it'll call this function to gets its [optional] subtype - which, from V8's
// point of view, is an arbitrary string.
pub export fn v8_inspector__Client__IMPL__valueSubtype(
_: *v8.c.InspectorClientImpl,
c_value: *const v8.C_Value,
) callconv(.c) [*c]const u8 {
const external_entry = Inspector.getTaggedAnyOpaque(.{ .handle = c_value }) orelse return null;
return if (external_entry.subtype) |st| @tagName(st) else null;
}
// Same as valueSubType above, but for the optional description field.
// From what I can tell, some drivers _need_ the description field to be
// present, even if it's empty. So if we have a subType for the value, we'll
// put an empty description.
pub export fn v8_inspector__Client__IMPL__descriptionForValueSubtype(
_: *v8.c.InspectorClientImpl,
v8_context: *const v8.C_Context,
c_value: *const v8.C_Value,
) callconv(.c) [*c]const u8 {
_ = v8_context;
// We _must_ include a non-null description in order for the subtype value
// to be included. Besides that, I don't know if the value has any meaning
const external_entry = Inspector.getTaggedAnyOpaque(.{ .handle = c_value }) orelse return null;
return if (external_entry.subtype == null) null else "";
}
test "TaggedAnyOpaque" {
// If we grow this, fine, but it should be a conscious decision
try std.testing.expectEqual(24, @sizeOf(TaggedAnyOpaque));
}

91
src/browser/loader.zig Normal file
View File

@@ -0,0 +1,91 @@
// 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 Client = @import("../http/Client.zig");
const user_agent = "Lightpanda.io/1.0";
pub const Loader = struct {
client: Client,
// use 64KB for headers buffer size.
server_header_buffer: [1024 * 64]u8 = undefined,
pub const Response = struct {
alloc: std.mem.Allocator,
req: *Client.Request,
pub fn deinit(self: *Response) void {
self.req.deinit();
self.alloc.destroy(self.req);
}
};
pub fn init(alloc: std.mem.Allocator) Loader {
return Loader{
.client = Client{
.allocator = alloc,
},
};
}
pub fn deinit(self: *Loader) void {
self.client.deinit();
}
// see
// https://ziglang.org/documentation/master/std/#A;std:http.Client.fetch
// for reference.
// The caller is responsible for calling `deinit()` on the `Response`.
pub fn get(self: *Loader, alloc: std.mem.Allocator, uri: std.Uri) !Response {
var resp = Response{
.alloc = alloc,
.req = try alloc.create(Client.Request),
};
errdefer alloc.destroy(resp.req);
resp.req.* = try self.client.open(.GET, uri, .{
.headers = .{
.user_agent = .{ .override = user_agent },
},
.extra_headers = &.{
.{ .name = "Accept", .value = "*/*" },
.{ .name = "Accept-Language", .value = "en-US,en;q=0.5" },
},
.server_header_buffer = &self.server_header_buffer,
});
errdefer resp.req.deinit();
try resp.req.send();
try resp.req.finish();
try resp.req.wait();
return resp;
}
};
test "basic url get" {
const alloc = std.testing.allocator;
var loader = Loader.init(alloc);
defer loader.deinit();
var result = try loader.get(alloc, "https://en.wikipedia.org/wiki/Main_Page");
defer result.deinit();
try std.testing.expect(result.req.response.status == std.http.Status.ok);
}

157
src/browser/mime.zig Normal file
View File

@@ -0,0 +1,157 @@
// 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 testing = std.testing;
const strparser = @import("../str/parser.zig");
const Reader = strparser.Reader;
const trim = strparser.trim;
const Self = @This();
const MimeError = error{
Empty,
TooBig,
Invalid,
InvalidChar,
};
mtype: []const u8,
msubtype: []const u8,
params: []const u8 = "",
charset: ?[]const u8 = null,
boundary: ?[]const u8 = null,
pub const Empty = Self{ .mtype = "", .msubtype = "" };
pub const HTML = Self{ .mtype = "text", .msubtype = "html" };
pub const Javascript = Self{ .mtype = "application", .msubtype = "javascript" };
// https://mimesniff.spec.whatwg.org/#http-token-code-point
fn isHTTPCodePoint(c: u8) bool {
return switch (c) {
'!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '^' => return true,
'_', '`', '|', '~' => return true,
else => std.ascii.isAlphanumeric(c),
};
}
fn valid(s: []const u8) bool {
const ln = s.len;
var i: usize = 0;
while (i < ln) {
if (!isHTTPCodePoint(s[i])) return false;
i += 1;
}
return true;
}
// https://mimesniff.spec.whatwg.org/#parsing-a-mime-type
pub fn parse(s: []const u8) Self.MimeError!Self {
const ln = s.len;
if (ln == 0) return MimeError.Empty;
// limit input size
if (ln > 255) return MimeError.TooBig;
var res = Self{ .mtype = "", .msubtype = "" };
var r = Reader{ .s = s };
res.mtype = trim(r.until('/'));
if (res.mtype.len == 0) return MimeError.Invalid;
if (!valid(res.mtype)) return MimeError.InvalidChar;
if (!r.skip()) return MimeError.Invalid;
res.msubtype = trim(r.until(';'));
if (res.msubtype.len == 0) return MimeError.Invalid;
if (!valid(res.msubtype)) return MimeError.InvalidChar;
if (!r.skip()) return res;
res.params = trim(r.tail());
if (res.params.len == 0) return MimeError.Invalid;
// parse well known parameters.
// don't check invalid parameter format.
var rp = Reader{ .s = res.params };
while (true) {
const name = trim(rp.until('='));
if (!rp.skip()) return res;
const value = trim(rp.until(';'));
if (std.ascii.eqlIgnoreCase(name, "charset")) {
res.charset = value;
}
if (std.ascii.eqlIgnoreCase(name, "boundary")) {
res.boundary = value;
}
if (!rp.skip()) return res;
}
return res;
}
test "parse valid" {
for ([_][]const u8{
"text/html",
" \ttext/html",
"text \t/html",
"text/ \thtml",
"text/html \t",
}) |tc| {
const m = try Self.parse(tc);
try testing.expectEqualStrings("text", m.mtype);
try testing.expectEqualStrings("html", m.msubtype);
}
const m2 = try Self.parse("text/javascript1.5");
try testing.expectEqualStrings("text", m2.mtype);
try testing.expectEqualStrings("javascript1.5", m2.msubtype);
const m3 = try Self.parse("text/html; charset=utf-8");
try testing.expectEqualStrings("text", m3.mtype);
try testing.expectEqualStrings("html", m3.msubtype);
try testing.expectEqualStrings("charset=utf-8", m3.params);
try testing.expectEqualStrings("utf-8", m3.charset.?);
const m4 = try Self.parse("text/html; boundary=----");
try testing.expectEqualStrings("text", m4.mtype);
try testing.expectEqualStrings("html", m4.msubtype);
try testing.expectEqualStrings("boundary=----", m4.params);
try testing.expectEqualStrings("----", m4.boundary.?);
}
test "parse invalid" {
for ([_][]const u8{
"",
"te xt/html;",
"te@xt/html;",
"text/ht@ml;",
"text/html;",
"/text/html",
"/html",
}) |tc| {
_ = Self.parse(tc) catch continue;
try testing.expect(false);
}
}
// Compare type and subtype.
pub fn eql(self: Self, b: Self) bool {
if (!std.mem.eql(u8, self.mtype, b.mtype)) return false;
return std.mem.eql(u8, self.msubtype, b.msubtype);
}

View File

@@ -1,243 +0,0 @@
const std = @import("std");
const h5e = @import("html5ever.zig");
const Page = @import("../Page.zig");
const Node = @import("../webapi/Node.zig");
const Element = @import("../webapi/Element.zig");
const Allocator = std.mem.Allocator;
pub const ParsedNode = struct {
node: *Node,
// Data associated with this element to be passed back to html5ever as needed
// We only have this for Elements. For other types, like comments, it's null.
// html5ever should never ask us for this data on a non-element, and we'll
// assert that, with this opitonal, to make sure our assumption is correct.
data: ?*anyopaque,
};
const Parser = @This();
page: *Page,
err: ?Error,
container: ParsedNode,
arena: Allocator,
strings: std.StringHashMapUnmanaged(void),
pub fn init(arena: Allocator, node: *Node, page: *Page) Parser {
return .{
.err = null,
.page = page,
.strings = .empty,
.arena = arena,
.container = ParsedNode{
.data = null,
.node = node,
},
};
}
const Error = struct {
err: anyerror,
source: Source,
const Source = enum {
pop,
append,
create_element,
create_comment,
};
};
pub fn parse(self: *Parser, html: []const u8) void {
h5e.html5ever_parse_document(
html.ptr,
html.len,
&self.container,
self,
createElementCallback,
getDataCallback,
appendCallback,
parseErrorCallback,
popCallback,
createCommentCallback,
appendDoctypeToDocument,
);
}
pub fn parseFragment(self: *Parser, html: []const u8) void {
h5e.html5ever_parse_fragment(
html.ptr,
html.len,
&self.container,
self,
createElementCallback,
getDataCallback,
appendCallback,
parseErrorCallback,
popCallback,
createCommentCallback,
appendDoctypeToDocument,
);
}
pub const Streaming = struct {
parser: Parser,
handle: ?*anyopaque,
pub fn init(arena: Allocator, node: *Node, page: *Page) Streaming {
return .{
.handle = null,
.parser = Parser.init(arena, node, page),
};
}
pub fn deinit(self: *Streaming) void {
if (self.handle) |handle| {
h5e.html5ever_streaming_parser_destroy(handle);
}
}
pub fn start(self: *Streaming) !void {
std.debug.assert(self.handle == null);
self.handle = h5e.html5ever_streaming_parser_create(
&self.parser.container,
&self.parser,
createElementCallback,
getDataCallback,
appendCallback,
parseErrorCallback,
popCallback,
createCommentCallback,
appendDoctypeToDocument,
) orelse return error.ParserCreationFailed;
}
pub fn read(self: *Streaming, data: []const u8) void {
h5e.html5ever_streaming_parser_feed(
self.handle.?,
data.ptr,
data.len,
);
}
pub fn done(self: *Streaming) void {
h5e.html5ever_streaming_parser_finish(self.handle.?);
}
};
fn parseErrorCallback(ctx: *anyopaque, err: h5e.StringSlice) callconv(.c) void {
_ = ctx;
_ = err;
// std.debug.print("PEC: {s}\n", .{err.slice()});
}
fn popCallback(ctx: *anyopaque, node_ref: *anyopaque) callconv(.c) void {
const self: *Parser = @ptrCast(@alignCast(ctx));
self._popCallback(getNode(node_ref)) catch |err| {
self.err = .{ .err = err, .source = .pop };
};
}
fn _popCallback(self: *Parser, node: *Node) !void {
try self.page.nodeComplete(node);
}
fn createElementCallback(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator) callconv(.c) ?*anyopaque {
const self: *Parser = @ptrCast(@alignCast(ctx));
return self._createElementCallback(data, qname, attributes) catch |err| {
self.err = .{ .err = err, .source = .create_element };
return null;
};
}
fn _createElementCallback(self: *Parser, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator) !*anyopaque {
const page = self.page;
const name = qname.local.slice();
const namespace = qname.ns.slice();
const node = try page.createElement(namespace, name, attributes);
const pn = try self.arena.create(ParsedNode);
pn.* = .{
.data = data,
.node = node,
};
return pn;
}
fn createCommentCallback(ctx: *anyopaque, str: h5e.StringSlice) callconv(.c) ?*anyopaque {
const self: *Parser = @ptrCast(@alignCast(ctx));
return self._createCommentCallback(str.slice()) catch |err| {
self.err = .{ .err = err, .source = .create_comment };
return null;
};
}
fn _createCommentCallback(self: *Parser, str: []const u8) !*anyopaque {
const page = self.page;
const node = try page.createComment(str);
const pn = try self.arena.create(ParsedNode);
pn.* = .{
.data = null,
.node = node,
};
return pn;
}
fn appendDoctypeToDocument(ctx: *anyopaque, name: h5e.StringSlice, public_id: h5e.StringSlice, system_id: h5e.StringSlice) callconv(.c) void {
_ = public_id;
_ = system_id;
const self: *Parser = @ptrCast(@alignCast(ctx));
self._appendDoctypeToDocument(name.slice()) catch |err| {
self.err = .{ .err = err, .source = .append_doctype_to_document };
};
}
fn _appendDoctypeToDocument(self: *Parser, name: []const u8) !void {
_ = self;
_ = name;
}
fn getDataCallback(ctx: *anyopaque) callconv(.c) *anyopaque {
const pn: *ParsedNode = @ptrCast(@alignCast(ctx));
// For non-elements, data is null. But, we expect this to only ever
// be called for elements.
std.debug.assert(pn.data != null);
return pn.data.?;
}
fn appendCallback(ctx: *anyopaque, parent_ref: *anyopaque, node_or_text: h5e.NodeOrText) callconv(.c) void {
const self: *Parser = @ptrCast(@alignCast(ctx));
self._appendCallback(getNode(parent_ref), node_or_text) catch |err| {
self.err = .{ .err = err, .source = .append };
};
}
fn _appendCallback(self: *Parser, parent: *Node, node_or_text: h5e.NodeOrText) !void {
switch (node_or_text.toUnion()) {
.node => |cpn| {
const child = getNode(cpn);
// child node is guaranteed not to belong to another parent
try self.page.appendNew(parent, .{ .node = child });
},
.text => |txt| {
try self.page.appendNew(parent, .{ .text = txt });
},
}
}
fn getNode(ref: *anyopaque) *Node {
const pn: *ParsedNode = @ptrCast(@alignCast(ref));
return pn.node;
}
fn asUint(comptime string: anytype) std.meta.Int(
.unsigned,
@bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0
) {
const byteLength = @sizeOf(@TypeOf(string.*)) - 1;
const expectedType = *const [byteLength:0]u8;
if (@TypeOf(string) != expectedType) {
@compileError("expected : " ++ @typeName(expectedType) ++ ", got: " ++ @typeName(@TypeOf(string)));
}
return @bitCast(@as(*const [byteLength]u8, string).*);
}

View File

@@ -1,134 +0,0 @@
const ParsedNode = @import("Parser.zig").ParsedNode;
pub extern "c" fn html5ever_parse_document(
html: [*c]const u8,
len: usize,
doc: *anyopaque,
ctx: *anyopaque,
createElementCallback: *const fn (ctx: *anyopaque, data: *anyopaque, QualName, AttributeIterator) callconv(.c) ?*anyopaque,
elemNameCallback: *const fn (node_ref: *anyopaque) callconv(.c) *anyopaque,
appendCallback: *const fn (ctx: *anyopaque, parent_ref: *anyopaque, NodeOrText) callconv(.c) void,
parseErrorCallback: *const fn (ctx: *anyopaque, StringSlice) callconv(.c) void,
popCallback: *const fn (ctx: *anyopaque, node_ref: *anyopaque) callconv(.c) void,
createCommentCallback: *const fn (ctx: *anyopaque, StringSlice) callconv(.c) ?*anyopaque,
appendDoctypeToDocument: *const fn (ctx: *anyopaque, StringSlice, StringSlice, StringSlice) callconv(.c) void,
) void;
pub extern "c" fn html5ever_parse_fragment(
html: [*c]const u8,
len: usize,
doc: *anyopaque,
ctx: *anyopaque,
createElementCallback: *const fn (ctx: *anyopaque, data: *anyopaque, QualName, AttributeIterator) callconv(.c) ?*anyopaque,
elemNameCallback: *const fn (node_ref: *anyopaque) callconv(.c) *anyopaque,
appendCallback: *const fn (ctx: *anyopaque, parent_ref: *anyopaque, NodeOrText) callconv(.c) void,
parseErrorCallback: *const fn (ctx: *anyopaque, StringSlice) callconv(.c) void,
popCallback: *const fn (ctx: *anyopaque, node_ref: *anyopaque) callconv(.c) void,
createCommentCallback: *const fn (ctx: *anyopaque, StringSlice) callconv(.c) ?*anyopaque,
appendDoctypeToDocument: *const fn (ctx: *anyopaque, StringSlice, StringSlice, StringSlice) callconv(.c) void,
) void;
pub extern "c" fn html5ever_attribute_iterator_next(ctx: *anyopaque) Nullable(Attribute);
pub extern "c" fn html5ever_attribute_iterator_count(ctx: *anyopaque) usize;
pub extern "c" fn html5ever_get_memory_usage() MemoryUsage;
pub const MemoryUsage = extern struct {
resident: usize,
allocated: usize,
};
// Streaming parser API
pub extern "c" fn html5ever_streaming_parser_create(
doc: *anyopaque,
ctx: *anyopaque,
createElementCallback: *const fn (ctx: *anyopaque, data: *anyopaque, QualName, AttributeIterator) callconv(.c) ?*anyopaque,
elemNameCallback: *const fn (node_ref: *anyopaque) callconv(.c) *anyopaque,
appendCallback: *const fn (ctx: *anyopaque, parent_ref: *anyopaque, NodeOrText) callconv(.c) void,
parseErrorCallback: *const fn (ctx: *anyopaque, StringSlice) callconv(.c) void,
popCallback: *const fn (ctx: *anyopaque, node_ref: *anyopaque) callconv(.c) void,
createCommentCallback: *const fn (ctx: *anyopaque, StringSlice) callconv(.c) ?*anyopaque,
appendDoctypeToDocument: *const fn (ctx: *anyopaque, StringSlice, StringSlice, StringSlice) callconv(.c) void,
) ?*anyopaque;
pub extern "c" fn html5ever_streaming_parser_feed(
parser: *anyopaque,
html: [*c]const u8,
len: usize,
) void;
pub extern "c" fn html5ever_streaming_parser_finish(
parser: *anyopaque,
) void;
pub extern "c" fn html5ever_streaming_parser_destroy(
parser: *anyopaque,
) void;
pub fn Nullable(comptime T: type) type {
return extern struct {
tag: u8,
value: T,
pub fn unwrap(self: @This()) ?T {
return if (self.tag == 0) null else self.value;
}
pub fn none() @This() {
return .{ .tag = 0, .value = undefined };
}
};
}
pub const StringSlice = Slice(u8);
pub fn Slice(comptime T: type) type {
return extern struct {
ptr: [*]const T,
len: usize,
pub fn slice(self: @This()) []const T {
return self.ptr[0..self.len];
}
};
}
pub const QualName = extern struct {
prefix: Nullable(StringSlice),
ns: StringSlice,
local: StringSlice,
};
pub const Attribute = extern struct {
name: QualName,
value: StringSlice,
};
pub const AttributeIterator = extern struct {
iter: *anyopaque,
pub fn next(self: AttributeIterator) ?Attribute {
return html5ever_attribute_iterator_next(self.iter).unwrap();
}
pub fn count(self: AttributeIterator) usize {
return html5ever_attribute_iterator_count(self.iter);
}
};
pub const NodeOrText = extern struct {
tag: u8,
node: *anyopaque,
text: StringSlice,
pub fn toUnion(self: NodeOrText) Union {
if (self.tag == 0) {
return .{ .node = @ptrCast(@alignCast(self.node)) };
}
return .{ .text = self.text.slice() };
}
const Union = union(enum) {
node: *ParsedNode,
text: []const u8,
};
};

View File

@@ -1,100 +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 builtin = @import("builtin");
const js = @import("../js/js.zig");
const log = @import("../../log.zig");
const Allocator = std.mem.Allocator;
pub const Loader = struct {
state: enum { empty, loading } = .empty,
done: struct {
webcomponents: bool = false,
} = .{},
fn load(self: *Loader, comptime name: []const u8, source: []const u8, js_context: *js.Context) void {
var try_catch: js.TryCatch = undefined;
try_catch.init(js_context);
defer try_catch.deinit();
self.state = .loading;
defer self.state = .empty;
log.debug(.js, "polyfill load", .{ .name = name });
_ = js_context.exec(source, name) catch |err| {
log.fatal(.app, "polyfill error", .{
.name = name,
.err = try_catch.err(js_context.call_arena) catch @errorName(err) orelse @errorName(err),
});
};
@field(self.done, name) = true;
}
pub fn missing(self: *Loader, name: []const u8, js_context: *js.Context) bool {
// Avoid recursive calls during polyfill loading.
if (self.state == .loading) {
return false;
}
if (!self.done.webcomponents and isWebcomponents(name)) {
const source = @import("webcomponents.zig").source;
self.load("webcomponents", source, js_context);
// We return false here: We want v8 to continue the calling chain
// to finally find the polyfill we just inserted. If we want to
// return false and stops the call chain, we have to use
// `info.GetReturnValue.Set()` function, or `undefined` will be
// returned immediately.
return false;
}
if (comptime builtin.mode == .Debug) {
log.debug(.unknown_prop, "unkown global property", .{
.info = "but the property can exist in pure JS",
.stack = js_context.stackTrace() catch "???",
.property = name,
});
}
return false;
}
fn isWebcomponents(name: []const u8) bool {
if (std.mem.eql(u8, name, "customElements")) return true;
return false;
}
};
pub fn preload(allocator: Allocator, js_context: *js.Context) !void {
var try_catch: js.TryCatch = undefined;
try_catch.init(js_context);
defer try_catch.deinit();
const name = "webcomponents-pre";
const source = @import("webcomponents.zig").pre;
_ = js_context.exec(source, name) catch |err| {
if (try try_catch.err(allocator)) |msg| {
defer allocator.free(msg);
log.fatal(.app, "polyfill error", .{ .name = name, .err = msg });
}
return err;
};
}

View File

@@ -1,61 +0,0 @@
/**
@license @nocompile
Copyright (c) 2018 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
(function(){/*
Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found
at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
Google as part of the polymer project is also subject to an additional IP
rights grant found at http://polymer.github.io/PATENTS.txt
*/
'use strict';var n=window.Document.prototype.createElement,p=window.Document.prototype.createElementNS,aa=window.Document.prototype.importNode,ba=window.Document.prototype.prepend,ca=window.Document.prototype.append,da=window.DocumentFragment.prototype.prepend,ea=window.DocumentFragment.prototype.append,q=window.Node.prototype.cloneNode,r=window.Node.prototype.appendChild,t=window.Node.prototype.insertBefore,u=window.Node.prototype.removeChild,v=window.Node.prototype.replaceChild,w=Object.getOwnPropertyDescriptor(window.Node.prototype,
"textContent"),y=window.Element.prototype.attachShadow,z=Object.getOwnPropertyDescriptor(window.Element.prototype,"innerHTML"),A=window.Element.prototype.getAttribute,B=window.Element.prototype.setAttribute,C=window.Element.prototype.removeAttribute,D=window.Element.prototype.toggleAttribute,E=window.Element.prototype.getAttributeNS,F=window.Element.prototype.setAttributeNS,G=window.Element.prototype.removeAttributeNS,H=window.Element.prototype.insertAdjacentElement,fa=window.Element.prototype.insertAdjacentHTML,
ha=window.Element.prototype.prepend,ia=window.Element.prototype.append,ja=window.Element.prototype.before,ka=window.Element.prototype.after,la=window.Element.prototype.replaceWith,ma=window.Element.prototype.remove,na=window.HTMLElement,I=Object.getOwnPropertyDescriptor(window.HTMLElement.prototype,"innerHTML"),oa=window.HTMLElement.prototype.insertAdjacentElement,pa=window.HTMLElement.prototype.insertAdjacentHTML;var qa=new Set;"annotation-xml color-profile font-face font-face-src font-face-uri font-face-format font-face-name missing-glyph".split(" ").forEach(function(a){return qa.add(a)});function ra(a){var b=qa.has(a);a=/^[a-z][.0-9_a-z]*-[-.0-9_a-z]*$/.test(a);return!b&&a}var sa=document.contains?document.contains.bind(document):document.documentElement.contains.bind(document.documentElement);
function J(a){var b=a.isConnected;if(void 0!==b)return b;if(sa(a))return!0;for(;a&&!(a.__CE_isImportDocument||a instanceof Document);)a=a.parentNode||(window.ShadowRoot&&a instanceof ShadowRoot?a.host:void 0);return!(!a||!(a.__CE_isImportDocument||a instanceof Document))}function K(a){var b=a.children;if(b)return Array.prototype.slice.call(b);b=[];for(a=a.firstChild;a;a=a.nextSibling)a.nodeType===Node.ELEMENT_NODE&&b.push(a);return b}
function L(a,b){for(;b&&b!==a&&!b.nextSibling;)b=b.parentNode;return b&&b!==a?b.nextSibling:null}
function M(a,b,d){for(var f=a;f;){if(f.nodeType===Node.ELEMENT_NODE){var c=f;b(c);var e=c.localName;if("link"===e&&"import"===c.getAttribute("rel")){f=c.import;void 0===d&&(d=new Set);if(f instanceof Node&&!d.has(f))for(d.add(f),f=f.firstChild;f;f=f.nextSibling)M(f,b,d);f=L(a,c);continue}else if("template"===e){f=L(a,c);continue}if(c=c.__CE_shadowRoot)for(c=c.firstChild;c;c=c.nextSibling)M(c,b,d)}f=f.firstChild?f.firstChild:L(a,f)}};function N(){var a=!(null===O||void 0===O||!O.noDocumentConstructionObserver),b=!(null===O||void 0===O||!O.shadyDomFastWalk);this.m=[];this.g=[];this.j=!1;this.shadyDomFastWalk=b;this.I=!a}function P(a,b,d,f){var c=window.ShadyDOM;if(a.shadyDomFastWalk&&c&&c.inUse){if(b.nodeType===Node.ELEMENT_NODE&&d(b),b.querySelectorAll)for(a=c.nativeMethods.querySelectorAll.call(b,"*"),b=0;b<a.length;b++)d(a[b])}else M(b,d,f)}function ta(a,b){a.j=!0;a.m.push(b)}function ua(a,b){a.j=!0;a.g.push(b)}
function Q(a,b){a.j&&P(a,b,function(d){return R(a,d)})}function R(a,b){if(a.j&&!b.__CE_patched){b.__CE_patched=!0;for(var d=0;d<a.m.length;d++)a.m[d](b);for(d=0;d<a.g.length;d++)a.g[d](b)}}function S(a,b){var d=[];P(a,b,function(c){return d.push(c)});for(b=0;b<d.length;b++){var f=d[b];1===f.__CE_state?a.connectedCallback(f):T(a,f)}}function U(a,b){var d=[];P(a,b,function(c){return d.push(c)});for(b=0;b<d.length;b++){var f=d[b];1===f.__CE_state&&a.disconnectedCallback(f)}}
function V(a,b,d){d=void 0===d?{}:d;var f=d.J,c=d.upgrade||function(g){return T(a,g)},e=[];P(a,b,function(g){a.j&&R(a,g);if("link"===g.localName&&"import"===g.getAttribute("rel")){var h=g.import;h instanceof Node&&(h.__CE_isImportDocument=!0,h.__CE_registry=document.__CE_registry);h&&"complete"===h.readyState?h.__CE_documentLoadHandled=!0:g.addEventListener("load",function(){var k=g.import;if(!k.__CE_documentLoadHandled){k.__CE_documentLoadHandled=!0;var l=new Set;f&&(f.forEach(function(m){return l.add(m)}),
l.delete(k));V(a,k,{J:l,upgrade:c})}})}else e.push(g)},f);for(b=0;b<e.length;b++)c(e[b])}
function T(a,b){try{var d=b.ownerDocument,f=d.__CE_registry;var c=f&&(d.defaultView||d.__CE_isImportDocument)?W(f,b.localName):void 0;if(c&&void 0===b.__CE_state){c.constructionStack.push(b);try{try{if(new c.constructorFunction!==b)throw Error("The custom element constructor did not produce the element being upgraded.");}finally{c.constructionStack.pop()}}catch(k){throw b.__CE_state=2,k;}b.__CE_state=1;b.__CE_definition=c;if(c.attributeChangedCallback&&b.hasAttributes()){var e=c.observedAttributes;
for(c=0;c<e.length;c++){var g=e[c],h=b.getAttribute(g);null!==h&&a.attributeChangedCallback(b,g,null,h,null)}}J(b)&&a.connectedCallback(b)}}catch(k){X(k)}}N.prototype.connectedCallback=function(a){var b=a.__CE_definition;if(b.connectedCallback)try{b.connectedCallback.call(a)}catch(d){X(d)}};N.prototype.disconnectedCallback=function(a){var b=a.__CE_definition;if(b.disconnectedCallback)try{b.disconnectedCallback.call(a)}catch(d){X(d)}};
N.prototype.attributeChangedCallback=function(a,b,d,f,c){var e=a.__CE_definition;if(e.attributeChangedCallback&&-1<e.observedAttributes.indexOf(b))try{e.attributeChangedCallback.call(a,b,d,f,c)}catch(g){X(g)}};
function va(a,b,d,f){var c=b.__CE_registry;if(c&&(null===f||"http://www.w3.org/1999/xhtml"===f)&&(c=W(c,d)))try{var e=new c.constructorFunction;if(void 0===e.__CE_state||void 0===e.__CE_definition)throw Error("Failed to construct '"+d+"': The returned value was not constructed with the HTMLElement constructor.");if("http://www.w3.org/1999/xhtml"!==e.namespaceURI)throw Error("Failed to construct '"+d+"': The constructed element's namespace must be the HTML namespace.");if(e.hasAttributes())throw Error("Failed to construct '"+
d+"': The constructed element must not have any attributes.");if(null!==e.firstChild)throw Error("Failed to construct '"+d+"': The constructed element must not have any children.");if(null!==e.parentNode)throw Error("Failed to construct '"+d+"': The constructed element must not have a parent node.");if(e.ownerDocument!==b)throw Error("Failed to construct '"+d+"': The constructed element's owner document is incorrect.");if(e.localName!==d)throw Error("Failed to construct '"+d+"': The constructed element's local name is incorrect.");
return e}catch(g){return X(g),b=null===f?n.call(b,d):p.call(b,f,d),Object.setPrototypeOf(b,HTMLUnknownElement.prototype),b.__CE_state=2,b.__CE_definition=void 0,R(a,b),b}b=null===f?n.call(b,d):p.call(b,f,d);R(a,b);return b}
function X(a){var b="",d="",f=0,c=0;a instanceof Error?(b=a.message,d=a.sourceURL||a.fileName||"",f=a.line||a.lineNumber||0,c=a.column||a.columnNumber||0):b="Uncaught "+String(a);var e=void 0;void 0===ErrorEvent.prototype.initErrorEvent?e=new ErrorEvent("error",{cancelable:!0,message:b,filename:d,lineno:f,colno:c,error:a}):(e=document.createEvent("ErrorEvent"),e.initErrorEvent("error",!1,!0,b,d,f),e.preventDefault=function(){Object.defineProperty(this,"defaultPrevented",{configurable:!0,get:function(){return!0}})});
void 0===e.error&&Object.defineProperty(e,"error",{configurable:!0,enumerable:!0,get:function(){return a}});window.dispatchEvent(e);e.defaultPrevented||console.error(a)};function wa(){var a=this;this.g=void 0;this.F=new Promise(function(b){a.l=b})}wa.prototype.resolve=function(a){if(this.g)throw Error("Already resolved.");this.g=a;this.l(a)};function xa(a){var b=document;this.l=void 0;this.h=a;this.g=b;V(this.h,this.g);"loading"===this.g.readyState&&(this.l=new MutationObserver(this.G.bind(this)),this.l.observe(this.g,{childList:!0,subtree:!0}))}function ya(a){a.l&&a.l.disconnect()}xa.prototype.G=function(a){var b=this.g.readyState;"interactive"!==b&&"complete"!==b||ya(this);for(b=0;b<a.length;b++)for(var d=a[b].addedNodes,f=0;f<d.length;f++)V(this.h,d[f])};function Y(a){this.s=new Map;this.u=new Map;this.C=new Map;this.A=!1;this.B=new Map;this.o=function(b){return b()};this.i=!1;this.v=[];this.h=a;this.D=a.I?new xa(a):void 0}Y.prototype.H=function(a,b){var d=this;if(!(b instanceof Function))throw new TypeError("Custom element constructor getters must be functions.");za(this,a);this.s.set(a,b);this.v.push(a);this.i||(this.i=!0,this.o(function(){return Aa(d)}))};
Y.prototype.define=function(a,b){var d=this;if(!(b instanceof Function))throw new TypeError("Custom element constructors must be functions.");za(this,a);Ba(this,a,b);this.v.push(a);this.i||(this.i=!0,this.o(function(){return Aa(d)}))};function za(a,b){if(!ra(b))throw new SyntaxError("The element name '"+b+"' is not valid.");if(W(a,b))throw Error("A custom element with name '"+(b+"' has already been defined."));if(a.A)throw Error("A custom element is already being defined.");}
function Ba(a,b,d){a.A=!0;var f;try{var c=d.prototype;if(!(c instanceof Object))throw new TypeError("The custom element constructor's prototype is not an object.");var e=function(m){var x=c[m];if(void 0!==x&&!(x instanceof Function))throw Error("The '"+m+"' callback must be a function.");return x};var g=e("connectedCallback");var h=e("disconnectedCallback");var k=e("adoptedCallback");var l=(f=e("attributeChangedCallback"))&&d.observedAttributes||[]}catch(m){throw m;}finally{a.A=!1}d={localName:b,
constructorFunction:d,connectedCallback:g,disconnectedCallback:h,adoptedCallback:k,attributeChangedCallback:f,observedAttributes:l,constructionStack:[]};a.u.set(b,d);a.C.set(d.constructorFunction,d);return d}Y.prototype.upgrade=function(a){V(this.h,a)};
function Aa(a){if(!1!==a.i){a.i=!1;for(var b=[],d=a.v,f=new Map,c=0;c<d.length;c++)f.set(d[c],[]);V(a.h,document,{upgrade:function(k){if(void 0===k.__CE_state){var l=k.localName,m=f.get(l);m?m.push(k):a.u.has(l)&&b.push(k)}}});for(c=0;c<b.length;c++)T(a.h,b[c]);for(c=0;c<d.length;c++){for(var e=d[c],g=f.get(e),h=0;h<g.length;h++)T(a.h,g[h]);(e=a.B.get(e))&&e.resolve(void 0)}d.length=0}}Y.prototype.get=function(a){if(a=W(this,a))return a.constructorFunction};
Y.prototype.whenDefined=function(a){if(!ra(a))return Promise.reject(new SyntaxError("'"+a+"' is not a valid custom element name."));var b=this.B.get(a);if(b)return b.F;b=new wa;this.B.set(a,b);var d=this.u.has(a)||this.s.has(a);a=-1===this.v.indexOf(a);d&&a&&b.resolve(void 0);return b.F};Y.prototype.polyfillWrapFlushCallback=function(a){this.D&&ya(this.D);var b=this.o;this.o=function(d){return a(function(){return b(d)})}};
function W(a,b){var d=a.u.get(b);if(d)return d;if(d=a.s.get(b)){a.s.delete(b);try{return Ba(a,b,d())}catch(f){X(f)}}}Y.prototype.define=Y.prototype.define;Y.prototype.upgrade=Y.prototype.upgrade;Y.prototype.get=Y.prototype.get;Y.prototype.whenDefined=Y.prototype.whenDefined;Y.prototype.polyfillDefineLazy=Y.prototype.H;Y.prototype.polyfillWrapFlushCallback=Y.prototype.polyfillWrapFlushCallback;function Z(a,b,d){function f(c){return function(e){for(var g=[],h=0;h<arguments.length;++h)g[h]=arguments[h];h=[];for(var k=[],l=0;l<g.length;l++){var m=g[l];m instanceof Element&&J(m)&&k.push(m);if(m instanceof DocumentFragment)for(m=m.firstChild;m;m=m.nextSibling)h.push(m);else h.push(m)}c.apply(this,g);for(g=0;g<k.length;g++)U(a,k[g]);if(J(this))for(g=0;g<h.length;g++)k=h[g],k instanceof Element&&S(a,k)}}void 0!==d.prepend&&(b.prepend=f(d.prepend));void 0!==d.append&&(b.append=f(d.append))};function Ca(a){Document.prototype.createElement=function(b){return va(a,this,b,null)};Document.prototype.importNode=function(b,d){b=aa.call(this,b,!!d);this.__CE_registry?V(a,b):Q(a,b);return b};Document.prototype.createElementNS=function(b,d){return va(a,this,d,b)};Z(a,Document.prototype,{prepend:ba,append:ca})};function Da(a){function b(f){return function(c){for(var e=[],g=0;g<arguments.length;++g)e[g]=arguments[g];g=[];for(var h=[],k=0;k<e.length;k++){var l=e[k];l instanceof Element&&J(l)&&h.push(l);if(l instanceof DocumentFragment)for(l=l.firstChild;l;l=l.nextSibling)g.push(l);else g.push(l)}f.apply(this,e);for(e=0;e<h.length;e++)U(a,h[e]);if(J(this))for(e=0;e<g.length;e++)h=g[e],h instanceof Element&&S(a,h)}}var d=Element.prototype;void 0!==ja&&(d.before=b(ja));void 0!==ka&&(d.after=b(ka));void 0!==la&&
(d.replaceWith=function(f){for(var c=[],e=0;e<arguments.length;++e)c[e]=arguments[e];e=[];for(var g=[],h=0;h<c.length;h++){var k=c[h];k instanceof Element&&J(k)&&g.push(k);if(k instanceof DocumentFragment)for(k=k.firstChild;k;k=k.nextSibling)e.push(k);else e.push(k)}h=J(this);la.apply(this,c);for(c=0;c<g.length;c++)U(a,g[c]);if(h)for(U(a,this),c=0;c<e.length;c++)g=e[c],g instanceof Element&&S(a,g)});void 0!==ma&&(d.remove=function(){var f=J(this);ma.call(this);f&&U(a,this)})};function Ea(a){function b(c,e){Object.defineProperty(c,"innerHTML",{enumerable:e.enumerable,configurable:!0,get:e.get,set:function(g){var h=this,k=void 0;J(this)&&(k=[],P(a,this,function(x){x!==h&&k.push(x)}));e.set.call(this,g);if(k)for(var l=0;l<k.length;l++){var m=k[l];1===m.__CE_state&&a.disconnectedCallback(m)}this.ownerDocument.__CE_registry?V(a,this):Q(a,this);return g}})}function d(c,e){c.insertAdjacentElement=function(g,h){var k=J(h);g=e.call(this,g,h);k&&U(a,h);J(g)&&S(a,h);return g}}function f(c,
e){function g(h,k){for(var l=[];h!==k;h=h.nextSibling)l.push(h);for(k=0;k<l.length;k++)V(a,l[k])}c.insertAdjacentHTML=function(h,k){h=h.toLowerCase();if("beforebegin"===h){var l=this.previousSibling;e.call(this,h,k);g(l||this.parentNode.firstChild,this)}else if("afterbegin"===h)l=this.firstChild,e.call(this,h,k),g(this.firstChild,l);else if("beforeend"===h)l=this.lastChild,e.call(this,h,k),g(l||this.firstChild,null);else if("afterend"===h)l=this.nextSibling,e.call(this,h,k),g(this.nextSibling,l);
else throw new SyntaxError("The value provided ("+String(h)+") is not one of 'beforebegin', 'afterbegin', 'beforeend', or 'afterend'.");}}y&&(Element.prototype.attachShadow=function(c){c=y.call(this,c);if(a.j&&!c.__CE_patched){c.__CE_patched=!0;for(var e=0;e<a.m.length;e++)a.m[e](c)}return this.__CE_shadowRoot=c});z&&z.get?b(Element.prototype,z):I&&I.get?b(HTMLElement.prototype,I):ua(a,function(c){b(c,{enumerable:!0,configurable:!0,get:function(){return q.call(this,!0).innerHTML},set:function(e){var g=
"template"===this.localName,h=g?this.content:this,k=p.call(document,this.namespaceURI,this.localName);for(k.innerHTML=e;0<h.childNodes.length;)u.call(h,h.childNodes[0]);for(e=g?k.content:k;0<e.childNodes.length;)r.call(h,e.childNodes[0])}})});Element.prototype.setAttribute=function(c,e){if(1!==this.__CE_state)return B.call(this,c,e);var g=A.call(this,c);B.call(this,c,e);e=A.call(this,c);a.attributeChangedCallback(this,c,g,e,null)};Element.prototype.setAttributeNS=function(c,e,g){if(1!==this.__CE_state)return F.call(this,
c,e,g);var h=E.call(this,c,e);F.call(this,c,e,g);g=E.call(this,c,e);a.attributeChangedCallback(this,e,h,g,c)};Element.prototype.removeAttribute=function(c){if(1!==this.__CE_state)return C.call(this,c);var e=A.call(this,c);C.call(this,c);null!==e&&a.attributeChangedCallback(this,c,e,null,null)};D&&(Element.prototype.toggleAttribute=function(c,e){if(1!==this.__CE_state)return D.call(this,c,e);var g=A.call(this,c),h=null!==g;e=D.call(this,c,e);h!==e&&a.attributeChangedCallback(this,c,g,e?"":null,null);
return e});Element.prototype.removeAttributeNS=function(c,e){if(1!==this.__CE_state)return G.call(this,c,e);var g=E.call(this,c,e);G.call(this,c,e);var h=E.call(this,c,e);g!==h&&a.attributeChangedCallback(this,e,g,h,c)};oa?d(HTMLElement.prototype,oa):H&&d(Element.prototype,H);pa?f(HTMLElement.prototype,pa):fa&&f(Element.prototype,fa);Z(a,Element.prototype,{prepend:ha,append:ia});Da(a)};var Fa={};function Ga(a){function b(){var d=this.constructor;var f=document.__CE_registry.C.get(d);if(!f)throw Error("Failed to construct a custom element: The constructor was not registered with `customElements`.");var c=f.constructionStack;if(0===c.length)return c=n.call(document,f.localName),Object.setPrototypeOf(c,d.prototype),c.__CE_state=1,c.__CE_definition=f,R(a,c),c;var e=c.length-1,g=c[e];if(g===Fa)throw Error("Failed to construct '"+f.localName+"': This element was already constructed.");c[e]=Fa;
Object.setPrototypeOf(g,d.prototype);R(a,g);return g}b.prototype=na.prototype;Object.defineProperty(HTMLElement.prototype,"constructor",{writable:!0,configurable:!0,enumerable:!1,value:b});window.HTMLElement=b};function Ha(a){function b(d,f){Object.defineProperty(d,"textContent",{enumerable:f.enumerable,configurable:!0,get:f.get,set:function(c){if(this.nodeType===Node.TEXT_NODE)f.set.call(this,c);else{var e=void 0;if(this.firstChild){var g=this.childNodes,h=g.length;if(0<h&&J(this)){e=Array(h);for(var k=0;k<h;k++)e[k]=g[k]}}f.set.call(this,c);if(e)for(c=0;c<e.length;c++)U(a,e[c])}}})}Node.prototype.insertBefore=function(d,f){if(d instanceof DocumentFragment){var c=K(d);d=t.call(this,d,f);if(J(this))for(f=
0;f<c.length;f++)S(a,c[f]);return d}c=d instanceof Element&&J(d);f=t.call(this,d,f);c&&U(a,d);J(this)&&S(a,d);return f};Node.prototype.appendChild=function(d){if(d instanceof DocumentFragment){var f=K(d);d=r.call(this,d);if(J(this))for(var c=0;c<f.length;c++)S(a,f[c]);return d}f=d instanceof Element&&J(d);c=r.call(this,d);f&&U(a,d);J(this)&&S(a,d);return c};Node.prototype.cloneNode=function(d){d=q.call(this,!!d);this.ownerDocument.__CE_registry?V(a,d):Q(a,d);return d};Node.prototype.removeChild=function(d){var f=
d instanceof Element&&J(d),c=u.call(this,d);f&&U(a,d);return c};Node.prototype.replaceChild=function(d,f){if(d instanceof DocumentFragment){var c=K(d);d=v.call(this,d,f);if(J(this))for(U(a,f),f=0;f<c.length;f++)S(a,c[f]);return d}c=d instanceof Element&&J(d);var e=v.call(this,d,f),g=J(this);g&&U(a,f);c&&U(a,d);g&&S(a,d);return e};w&&w.get?b(Node.prototype,w):ta(a,function(d){b(d,{enumerable:!0,configurable:!0,get:function(){for(var f=[],c=this.firstChild;c;c=c.nextSibling)c.nodeType!==Node.COMMENT_NODE&&
f.push(c.textContent);return f.join("")},set:function(f){for(;this.firstChild;)u.call(this,this.firstChild);null!=f&&""!==f&&r.call(this,document.createTextNode(f))}})})};var O=window.customElements;function Ia(){var a=new N;Ga(a);Ca(a);Z(a,DocumentFragment.prototype,{prepend:da,append:ea});Ha(a);Ea(a);window.CustomElementRegistry=Y;a=new Y(a);document.__CE_registry=a;Object.defineProperty(window,"customElements",{configurable:!0,enumerable:!0,value:a})}O&&!O.forcePolyfill&&"function"==typeof O.define&&"function"==typeof O.get||Ia();window.__CE_installPolyfill=Ia;/*
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
}).call(this);

View File

@@ -1,42 +0,0 @@
// webcomponents.js code comes from
// https://github.com/webcomponents/polyfills/tree/master/packages/webcomponentsjs
//
// The original code source is available in a "BSD style license".
//
// This is the `webcomponents-ce.js` bundle
pub const source = @embedFile("webcomponents.js");
// The main webcomponents.js is lazilly loaded when window.customElements is
// called. But, if you look at the test below, you'll notice that we declare
// our custom element (LightPanda) before we call `customElements.define`. We
// _have_ to declare it before we can register it.
// That causes an issue, because the LightPanda class extends HTMLElement, which
// hasn't been monkeypatched by the polyfill yet. If you were to try it as-is
// you'd get an "Illegal Constructor", because that's what the Zig HTMLElement
// constructor does (and that's correct).
// However, once HTMLElement is monkeypatched, it'll work. One simple solution
// is to run the webcomponents.js polyfill proactively on each page, ensuring
// that HTMLElement is monkeypatched before any other JavaScript is run. But
// that adds _a lot_ of overhead.
// So instead of always running the [large and intrusive] webcomponents.js
// polyfill, we'll always run this little snippet. It wraps the HTMLElement
// constructor. When the Lightpanda class is created, it'll extend our little
// wrapper. But, unlike the Zig default constructor which throws, our code
// calls the "real" constructor. That might seem like the same thing, but by the
// time our wrapper is called, the webcomponents.js polyfill will have been
// loaded and the "real" constructor will be the monkeypatched version.
// TL;DR creates a layer of indirection for the constructor, so that, when it's
// actually instantiated, the webcomponents.js polyfill will have been loaded.
pub const pre =
\\ (() => {
\\ const HE = window.HTMLElement;
\\ const b = function() { return HE.prototype.constructor.call(this); }
\\ b.prototype = HE.prototype;
\\ window.HTMLElement = b;
\\ })();
;
const testing = @import("../../testing.zig");
test "Browser: Polyfill.WebComponents" {
try testing.htmlRunner("polyfill/webcomponents.html", .{});
}

View File

@@ -1,46 +0,0 @@
const std = @import("std");
// Gets the Parent of child.
// HtmlElement.of(script) -> *HTMLElement
pub fn Struct(comptime T: type) type {
return switch (@typeInfo(T)) {
.pointer => |ptr| ptr.child,
.@"struct" => T,
.void => T,
else => unreachable,
};
}
// Creates an enum of N enums. Doesn't perserve their underlying integer
pub fn mergeEnums(comptime enums: []const type) type {
const field_count = blk: {
var count: usize = 0;
inline for (enums) |e| {
count += @typeInfo(e).@"enum".fields.len;
}
break :blk count;
};
var i: usize = 0;
var fields: [field_count]std.builtin.Type.EnumField = undefined;
for (enums) |e| {
for (@typeInfo(e).@"enum".fields) |f| {
fields[i] = .{
.name = f.name,
.value = i,
};
i += 1;
}
}
return @Type(.{ .@"enum" = .{
.decls = &.{},
.tag_type = blk: {
if (field_count <= std.math.maxInt(u8)) break :blk u8;
if (field_count <= std.math.maxInt(u16)) break :blk u16;
unreachable;
},
.fields = &fields,
.is_exhaustive = true,
} });
}

View File

@@ -1,10 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id=a><!-- spice --></div>
<div id=b>flow</div>
<script id=data>
testing.expectEqual(' spice ', $('#a').firstChild.data);
testing.expectEqual('flow', $('#b').firstChild.data);
</script>

View File

@@ -1,58 +0,0 @@
<!DOCTYPE html>
<script src="testing.js"></script>
<script id=getRandomValues>
function isRandom(ta) {
let uniq = new Set(Array.from(ta));
testing.expectEqual(true, (uniq.size / ta.length) * 100 > 0.7)
}
{
let tu8a = new Uint8Array(100)
testing.expectEqual(tu8a, Crypto.getRandomValues(tu8a))
isRandom(tu8a)
let ti8a = new Int8Array(100)
testing.expectEqual(ti8a, Crypto.getRandomValues(ti8a))
isRandom(ti8a)
}
{
let tu16a = new Uint16Array(100)
testing.expectEqual(tu16a, Crypto.getRandomValues(tu16a))
isRandom(tu16a)
let ti16a = new Int16Array(100)
testing.expectEqual(ti16a, Crypto.getRandomValues(ti16a))
isRandom(ti16a)
}
{
let tu32a = new Uint32Array(100)
testing.expectEqual(tu32a, Crypto.getRandomValues(tu32a))
isRandom(tu32a)
let ti32a = new Int32Array(100)
testing.expectEqual(ti32a, Crypto.getRandomValues(ti32a))
isRandom(ti32a)
}
{
let tu64a = new BigUint64Array(100)
testing.expectEqual(tu64a, Crypto.getRandomValues(tu64a))
isRandom(tu64a)
let ti64a = new BigInt64Array(100)
testing.expectEqual(ti64a, Crypto.getRandomValues(ti64a))
isRandom(ti64a)
}
</script>
<script id="randomUUID">
const uuid = Crypto.randomUUID();
testing.expectEqual('string', typeof uuid);
testing.expectEqual(36, uuid.length);
const regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
testing.expectEqual(true, regex.test(uuid));
</script>

View File

@@ -1,68 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Test Page</title>
</head>
<body>
<div id="first">First</div>
<span name="second">Second</span>
<p id="third">Third</p>
<a href="#" name="link">Link</a>
<script src="../testing.js"></script>
<script id=all_collection>
testing.expectEqual('undefined', typeof document.all);
testing.expectEqual('HTMLAllCollection', document.all.constructor.name);
testing.expectEqual(true, document.all == null);
testing.expectEqual(true, document.all == undefined);
testing.expectEqual(false, document.all === undefined);
testing.expectEqual(true, !document.all);
testing.expectEqual(false, !!document.all);
testing.expectEqual(10, document.all.length);
testing.expectEqual('HTML', document.all[0].tagName);
testing.expectEqual('HEAD', document.all[1].tagName);
testing.expectEqual('TITLE', document.all[2].tagName);
testing.expectEqual('BODY', document.all[3].tagName);
testing.expectEqual('DIV', document.all[4].tagName);
testing.expectEqual('DIV', document.all.first.tagName);
testing.expectEqual('First', document.all.first.textContent);
testing.expectEqual('P', document.all.third.tagName);
testing.expectEqual('Third', document.all.third.textContent);
testing.expectEqual('SPAN', document.all.second.tagName);
testing.expectEqual('Second', document.all.second.textContent);
testing.expectEqual('A', document.all.link.tagName);
testing.expectEqual('SPAN', document.all.item(5).tagName);
testing.expectEqual(null, document.all.item(999));
testing.expectEqual(null, document.all.item(-1));
testing.expectEqual('DIV', document.all.namedItem('first').tagName);
testing.expectEqual('SPAN', document.all.namedItem('second').tagName);
testing.expectEqual(null, document.all.namedItem('nonexistent'));
// Test callable functionality: document.all(index) and document.all(name)
testing.expectEqual('HTML', document.all(0).tagName);
testing.expectEqual('HEAD', document.all(1).tagName);
testing.expectEqual('DIV', document.all(4).tagName);
testing.expectEqual('DIV', document.all('first').tagName);
testing.expectEqual('First', document.all('first').textContent);
testing.expectEqual('SPAN', document.all('second').tagName);
testing.expectEqual('Second', document.all('second').textContent);
testing.expectEqual('P', document.all('third').tagName);
testing.expectEqual(undefined, document.all(999));
testing.expectEqual(undefined, document.all('nonexistent'));
let count = 0;
for (const el of document.all) {
count++;
}
testing.expectEqual(10, count);
const plainDoc = new Document();
testing.expectEqual('undefined', typeof plainDoc.all);
</script>
</body>
</html>

View File

@@ -1,23 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<form id="form1"></form>
<img id="img1" src="about:blank">
<script></script>
<a id="link1" href="#"></a>
<a id="link2"></a>
<script id=collections>
testing.expectEqual(1, document.forms.length);
testing.expectEqual($('#form1'), document.forms[0]);
testing.expectEqual(1, document.images.length);
testing.expectEqual($('#img1'), document.images[0]);
testing.expectEqual(3, document.scripts.length);
testing.expectEqual(true, document.scripts[0].src.endsWith('testing.js'));
testing.expectEqual(document.scripts[1].src, '');
testing.expectEqual($('#collections'), document.scripts[2]);
testing.expectEqual(2, document.links.length);
testing.expectEqual($('#link1'), document.links[0]);
testing.expectEqual($('#link2'), document.links[1]);
</script>

View File

@@ -1,13 +0,0 @@
<!DOCTYPE html>
<body></body>
<script src="../testing.js"></script>
<script id=createElement>
const div = document.createElement('div');
testing.expectEqual("DIV", div.tagName);
div.id = "hello";
testing.expectEqual(null, $('#hello'));
document.getElementsByTagName('body')[0].appendChild(div);
testing.expectEqual(div, $('#hello'));
</script>

View File

@@ -1,32 +0,0 @@
<!DOCTYPE html>
<body></body>
<script src="../testing.js"></script>
<script id=createElementNS>
const htmlDiv = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
testing.expectEqual('DIV', htmlDiv.tagName);
testing.expectEqual('http://www.w3.org/1999/xhtml', htmlDiv.namespaceURI);
const svgRect = document.createElementNS('http://www.w3.org/2000/svg', 'RecT');
testing.expectEqual('RecT', svgRect.tagName);
testing.expectEqual('http://www.w3.org/2000/svg', svgRect.namespaceURI);
const mathElement = document.createElementNS('http://www.w3.org/1998/Math/MathML', 'math');
testing.expectEqual('math', mathElement.tagName);
testing.expectEqual('http://www.w3.org/1998/Math/MathML', mathElement.namespaceURI);
const xmlElement = document.createElementNS('http://www.w3.org/XML/1998/namespace', 'TEst');
testing.expectEqual('TEst', xmlElement.tagName);
testing.expectEqual('http://www.w3.org/XML/1998/namespace', xmlElement.namespaceURI);
const nullNsElement = document.createElementNS(null, 'span');
testing.expectEqual('SPAN', nullNsElement.tagName);
testing.expectEqual('http://www.w3.org/1999/xhtml', nullNsElement.namespaceURI);
const unknownNsElement = document.createElementNS('http://example.com/unknown', 'custom');
testing.expectEqual('CUSTOM', unknownNsElement.tagName);
testing.expectEqual('http://www.w3.org/1999/xhtml', unknownNsElement.namespaceURI);
const regularDiv = document.createElement('div');
testing.expectEqual('DIV', regularDiv.tagName);
testing.expectEqual('http://www.w3.org/1999/xhtml', regularDiv.namespaceURI);
</script>

View File

@@ -1,41 +0,0 @@
<!DOCTYPE html>
<head id="the_head">
<title>Test Document Title</title>
<script src="../testing.js"></script>
</head>
<body id=the_body>
</body>
<script id=document>
testing.expectEqual(null, document.parentNode);
testing.expectEqual(undefined, document.getCurrentScript);
testing.expectEqual("http://127.0.0.1:9582/src/browser/tests/document/document.html", document.URL);
</script>
<script id=headAndbody>
testing.expectEqual($('#the_head'), document.head);
testing.expectEqual($('#the_body'), document.body);
</script>
<script id=documentElement>
testing.expectEqual($('#the_body').parentNode, document.documentElement);
</script>
<script id=title>
testing.expectEqual('Test Document Title', document.title);
document.title = 'New Title';
testing.expectEqual('New Title', document.title);
document.title = '';
testing.expectEqual('', document.title);
</script>
<script id=createTextNode>
const textNode = document.createTextNode('Hello World');
testing.expectEqual(3, textNode.nodeType);
testing.expectEqual('Hello World', textNode.nodeValue);
testing.expectEqual('Hello World', textNode.textContent);
const emptyText = document.createTextNode('');
testing.expectEqual('', emptyText.nodeValue);
</script>

View File

@@ -1,35 +0,0 @@
<!DOCTYPE html>
<body>
<script src="../testing.js"></script>
<div id="div-1">x: <p id=p1>d1</p></div>
</body>
<script id=getElementById>
testing.expectEqual(null, document.getElementById(null));
testing.expectEqual(null, document.getElementById(23));
testing.expectEqual(null, document.getElementById('value'));
testing.expectEqual('x: d1', document.getElementById('div-1').textContent);
testing.expectEqual('d1', document.getElementById('p1').textContent);
document.getElementById('p1').id = 'p2'
testing.expectEqual(null, document.getElementById('p1'));
testing.expectEqual('d1', document.getElementById('p2').textContent);
const div = document.createElement('div');
div.id = 'hello';
testing.expectEqual(null, document.getElementById('hello'));
const body = document.getElementsByTagName('body')[0];
body.appendChild(div);
testing.expectEqual('hello', document.getElementById('hello').id);
div.setAttribute('ID', 'world')
testing.expectEqual(null, document.getElementById('hello'));
testing.expectEqual('world', document.getElementById('world').id);
body.replaceChildren(div);
testing.expectEqual('world', document.getElementById('world').id);
testing.expectEqual(null, document.getElementById('div-1'));
testing.expectEqual(null, document.getElementById('p1'));
div.removeAttribute('id');
testing.expectEqual(null, document.getElementById('world'));
</script>

View File

@@ -1,98 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div class="foo">foo1</div>
<span class="foo">foo2</span>
<p class="foo" id="foo3">foo3</p>
<div class="bar">bar1</div>
<div class="baz">baz1</div>
<div class="multi class names">multi1</div>
<span class="class multi">multi2</span>
<div id="container">
<div class="nested">nested1</div>
<div class="nested">nested2</div>
</div>
<script>
const foos = document.getElementsByClassName('foo');
</script>
<script id=basic>
testing.expectEqual(0, document.getElementsByClassName('nonexistent').length);
testing.expectEqual(0, document.getElementsByClassName('unknown').length);
testing.expectEqual(true, foos instanceof HTMLCollection);
testing.expectEqual(3, foos.length);
testing.expectEqual(3, foos.length); // cache test
testing.expectEqual('foo1', foos[0].textContent);
testing.expectEqual('foo2', foos[1].textContent);
testing.expectEqual('foo3', foos[2].textContent);
testing.expectEqual('foo1', foos[0].textContent); // cache test
testing.expectEqual('foo3', foos[2].textContent);
testing.expectEqual('foo2', foos[1].textContent);
testing.expectEqual(undefined, foos[-1]);
testing.expectEqual(undefined, foos[3]);
testing.expectEqual(undefined, foos[100]);
const bars = document.getElementsByClassName('bar');
testing.expectEqual(1, bars.length);
testing.expectEqual('bar1', bars[0].textContent);
const bazs = document.getElementsByClassName('baz');
testing.expectEqual(1, bazs.length);
testing.expectEqual('baz1', bazs[0].textContent);
</script>
<script id=item>
testing.expectEqual('foo2', foos.item(1).textContent);
testing.expectEqual(null, foos.item(-1));
testing.expectEqual('foo1', foos.item(0).textContent);
testing.expectEqual(null, foos.item(-100));
testing.expectEqual(null, foos.item(3));
testing.expectEqual(null, foos.item(100));
</script>
<script id=namedItem>
testing.expectEqual('foo3', foos.namedItem('foo3').id);
testing.expectEqual(null, foos.namedItem('foo1'));
testing.expectEqual(null, foos.namedItem('bar'));
testing.expectEqual(null, foos.namedItem('nonexistent'));
testing.expectEqual('foo3', foos['foo3'].id);
</script>
<script id=iterator>
let acc = [];
for (let x of foos) {
acc.push(x.textContent);
}
testing.expectEqual(['foo1', 'foo2', 'foo3'], acc);
</script>
<script id=emptyString>
const empty = document.getElementsByClassName('');
testing.expectEqual(0, empty.length);
</script>
<script id=multipleClasses>
const multi = document.getElementsByClassName('multi');
testing.expectEqual(2, multi.length);
testing.expectEqual('multi1', multi[0].textContent);
testing.expectEqual('multi2', multi[1].textContent);
const classCol = document.getElementsByClassName('class');
testing.expectEqual(2, classCol.length);
const names = document.getElementsByClassName('names');
testing.expectEqual(1, names.length);
testing.expectEqual('multi1', names[0].textContent);
</script>
<script id=nested>
const nested = document.getElementsByClassName('nested');
testing.expectEqual(2, nested.length);
testing.expectEqual('nested1', nested[0].textContent);
testing.expectEqual('nested2', nested[1].textContent);
</script>

View File

@@ -1,155 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div>0</div>
<div>1</div>
<div>2</div>
<p id=p1></p>
<p id=p2></p>
<p id=p3></p>
<h1>H1</h1>
<h2>H2</h2>
<h3>H3</h3>
<h4>H4</h4>
<h5>H5</h5>
<h6>H6</h6>
<b>Bold</b>
<i>Italic</i>
<em>Emphasized</em>
<strong>Strong</strong>
<header>Header</header>
<nav>Nav</nav>
<main>Main</main>
<script id=basic>
testing.expectEqual(0, document.getElementsByTagName('a').length);
testing.expectEqual(0, document.getElementsByTagName('unknown').length);
const divs = document.getElementsByTagName('div');
testing.expectEqual(true, divs instanceof HTMLCollection);
testing.expectEqual(3, divs.length);
testing.expectEqual(3, divs.length);
testing.expectEqual('0', divs[0].textContent);
testing.expectEqual('0', divs[0].textContent);
testing.expectEqual('0', divs[0].textContent);
testing.expectEqual('1', divs[1].textContent);
testing.expectEqual('2', divs[2].textContent);
testing.expectEqual('2', divs[2].textContent);
testing.expectEqual('1', divs[1].textContent);
testing.expectEqual('1', divs[1].textContent);
testing.expectEqual('2', divs[2].textContent);
testing.expectEqual('0', divs[0].textContent);
testing.expectEqual(undefined, divs[-1]);
testing.expectEqual(undefined, divs[4]);
testing.expectEqual('2', divs.item(2).textContent);
testing.expectEqual(null, divs.item(-3));
testing.expectEqual('0', divs.item(0).textContent);
testing.expectEqual(null, divs.item(-100));
</script>
<script id=namedItem>
testing.expectEqual(null, divs.namedItem('d1'));
const ps = document.getElementsByTagName('p');
testing.expectEqual('p1', ps.namedItem('p1').id);
testing.expectEqual('p2', ps.namedItem('p2').id);
testing.expectEqual('p3', ps.namedItem('p3').id);
testing.expectEqual(null, ps.namedItem('p4'));
testing.expectEqual('p1', ps['p1'].id);
</script>
<script id=iterator>
let acc = [];
for (let x of ps) {
acc.push(x.id);
}
testing.expectEqual(['p1', 'p2', 'p3'], acc);
</script>
<script id=extendedTags>
// Test headings
testing.expectEqual(1, document.getElementsByTagName('h1').length);
testing.expectEqual('H1', document.getElementsByTagName('h1')[0].textContent);
testing.expectEqual(1, document.getElementsByTagName('h2').length);
testing.expectEqual('H2', document.getElementsByTagName('h2')[0].textContent);
testing.expectEqual(1, document.getElementsByTagName('h3').length);
testing.expectEqual(1, document.getElementsByTagName('h4').length);
testing.expectEqual(1, document.getElementsByTagName('h5').length);
testing.expectEqual(1, document.getElementsByTagName('h6').length);
// Test case insensitivity
testing.expectEqual(1, document.getElementsByTagName('H1').length);
testing.expectEqual('H1', document.getElementsByTagName('H1')[0].textContent);
// Test text formatting elements
testing.expectEqual(1, document.getElementsByTagName('b').length);
testing.expectEqual('Bold', document.getElementsByTagName('b')[0].textContent);
testing.expectEqual(1, document.getElementsByTagName('i').length);
testing.expectEqual('Italic', document.getElementsByTagName('i')[0].textContent);
testing.expectEqual(1, document.getElementsByTagName('em').length);
testing.expectEqual('Emphasized', document.getElementsByTagName('em')[0].textContent);
testing.expectEqual(1, document.getElementsByTagName('strong').length);
testing.expectEqual('Strong', document.getElementsByTagName('strong')[0].textContent);
// Test structural elements
testing.expectEqual(1, document.getElementsByTagName('header').length);
testing.expectEqual('Header', document.getElementsByTagName('header')[0].textContent);
testing.expectEqual(1, document.getElementsByTagName('nav').length);
testing.expectEqual('Nav', document.getElementsByTagName('nav')[0].textContent);
testing.expectEqual(1, document.getElementsByTagName('main').length);
testing.expectEqual('Main', document.getElementsByTagName('main')[0].textContent);
</script>
<script id=multipleIterators>
{
const list = document.getElementsByTagName('p');
const iter1 = list[Symbol.iterator]();
const iter2 = list[Symbol.iterator]();
const val1_iter1 = iter1.next();
testing.expectEqual(false, val1_iter1.done);
testing.expectEqual('p1', val1_iter1.value.id);
const val1_iter2 = iter2.next();
testing.expectEqual(false, val1_iter2.done);
testing.expectEqual('p1', val1_iter2.value.id);
const val2_iter1 = iter1.next();
testing.expectEqual(false, val2_iter1.done);
testing.expectEqual('p2', val2_iter1.value.id);
const val2_iter2 = iter2.next();
testing.expectEqual(false, val2_iter2.done);
testing.expectEqual('p2', val2_iter2.value.id);
}
</script>
<script id=iteratorLifetime>
{
let iter;
{
const list = document.getElementsByTagName('p');
iter = list[Symbol.iterator]();
}
const val1 = iter.next();
testing.expectEqual(false, val1.done);
testing.expectEqual('p1', val1.value.id);
const val2 = iter.next();
testing.expectEqual(false, val2.done);
testing.expectEqual('p2', val2.value.id);
const val3 = iter.next();
testing.expectEqual(false, val3.done);
testing.expectEqual('p3', val3.value.id);
const val4 = iter.next();
testing.expectEqual(true, val4.done);
}
</script>

View File

@@ -1,271 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div class="test-class">div1</div>
<span class="test-class">span1</span>
<div class="multiple classes here">multi1</div>
<div class="classes">multi2</div>
<div class="">empty-class</div>
<div>no-class</div>
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h5>Heading 5</h5>
<h6>Heading 6</h6>
<b>Bold text</b>
<i>Italic text</i>
<em>Emphasized text</em>
<strong>Strong text</strong>
<header>Header element</header>
<nav>Navigation</nav>
<main>Main content</main>
<script id=byId name="test1">
testing.expectError("Syntax Error", () => document.querySelector(''));
testing.withError((err) => {
testing.expectEqual(12, err.code);
testing.expectEqual("SyntaxError", err.name);
testing.expectEqual("Syntax Error", err.message);
}, () => document.querySelector(''));
testing.expectEqual('test1', document.querySelector('#byId').getAttribute('name'));
</script>
<script id=byClass>
{
const result = document.querySelector('.test-class');
testing.expectEqual('div1', result.textContent);
testing.expectEqual('multi1', document.querySelector('.multiple').textContent);
testing.expectEqual('multi1', document.querySelector('.classes').textContent);
testing.expectEqual('multi1', document.querySelector('.here').textContent);
testing.expectEqual(null, document.querySelector('.nonexistent'));
}
</script>
<script id=byTag>
{
const result = document.querySelector('div');
testing.expectEqual('div1', result.textContent);
testing.expectEqual('span1', document.querySelector('span').textContent);
// Test that script elements can be found
const firstScript = document.querySelector('script');
testing.expectEqual('SCRIPT', firstScript.tagName);
testing.expectEqual(null, document.querySelector('article'));
testing.expectEqual(null, document.querySelector('another'));
}
</script>
<script id=byTagExtended>
{
// Test headings (h1-h6)
testing.expectEqual('Heading 1', document.querySelector('h1').textContent);
testing.expectEqual('Heading 2', document.querySelector('h2').textContent);
testing.expectEqual('Heading 3', document.querySelector('h3').textContent);
testing.expectEqual('Heading 4', document.querySelector('h4').textContent);
testing.expectEqual('Heading 5', document.querySelector('h5').textContent);
testing.expectEqual('Heading 6', document.querySelector('h6').textContent);
// Test case insensitivity
testing.expectEqual('Heading 1', document.querySelector('H1').textContent);
testing.expectEqual('Heading 2', document.querySelector('H2').textContent);
// Test text formatting elements
testing.expectEqual('Bold text', document.querySelector('b').textContent);
testing.expectEqual('Italic text', document.querySelector('i').textContent);
testing.expectEqual('Emphasized text', document.querySelector('em').textContent);
testing.expectEqual('Strong text', document.querySelector('strong').textContent);
// Test structural elements
testing.expectEqual('Header element', document.querySelector('header').textContent);
testing.expectEqual('Navigation', document.querySelector('nav').textContent);
testing.expectEqual('Main content', document.querySelector('main').textContent);
// Test case insensitivity for these too
testing.expectEqual('Navigation', document.querySelector('NAV').textContent);
testing.expectEqual('Main content', document.querySelector('Main').textContent);
}
</script>
<div id="compound-test" class="container active">Compound 1</div>
<div class="container">Compound 2</div>
<span class="container active">Compound 3</span>
<p id="compound-test2">Compound 4</p>
<script id=compoundSelectors>
{
testing.expectEqual('Compound 1', document.querySelector('div.container').textContent);
testing.expectEqual('Compound 1', document.querySelector('div.active').textContent);
testing.expectEqual('Compound 3', document.querySelector('span.active').textContent);
testing.expectEqual('Compound 1', document.querySelector('div.container.active').textContent);
testing.expectEqual('Compound 1', document.querySelector('div#compound-test').textContent);
testing.expectEqual('Compound 1', document.querySelector('.container#compound-test').textContent);
testing.expectEqual('Compound 1', document.querySelector('#compound-test.active').textContent);
testing.expectEqual('Compound 1', document.querySelector('.container.active').textContent);
testing.expectEqual('Compound 1', document.querySelector('.active.container').textContent);
testing.expectEqual('Compound 1', document.querySelector(' div.container').textContent);
testing.expectEqual('Compound 1', document.querySelector(' .active').textContent);
testing.expectEqual('Compound 1', document.querySelector('div.container ').textContent);
testing.expectEqual('Compound 4', document.querySelector('#compound-test2 ').textContent);
testing.expectEqual('Compound 3', document.querySelector(' span.active ').textContent);
testing.expectEqual(null, document.querySelector('span#compound-test'));
testing.expectEqual(null, document.querySelector('div.nonexistent'));
testing.expectEqual(null, document.querySelector('p.container'));
}
</script>
<script id=universalSelector>
{
const result = document.querySelector('*');
testing.expectEqual('HTML', result.tagName);
testing.expectEqual('div1', document.querySelector('*.test-class').textContent);
testing.expectEqual('test1', document.querySelector('*#byId').getAttribute('name'));
testing.expectEqual('Compound 1', document.querySelector('*.container.active').textContent);
}
</script>
<div id="descendant-container">
<p class="desc-text">Direct child paragraph</p>
<div class="nested">
<p class="desc-text">Nested paragraph</p>
<span>
<p class="deep">Deeply nested paragraph</p>
</span>
</div>
<article>
<p class="desc-text">Article paragraph</p>
</article>
</div>
<p class="desc-text">Outside paragraph</p>
<div id="complex">
<div class="outer">
<div class="middle">
<div class="inner">
<span id="target">Target span</span>
</div>
</div>
</div>
</div>
<script id=descendantSelectors>
{
testing.expectEqual('Direct child paragraph', document.querySelector('div p').textContent);
testing.expectEqual('Direct child paragraph', document.querySelector('#descendant-container p').textContent);
testing.expectEqual('Nested paragraph', document.querySelector('div div p').textContent);
testing.expectEqual('target', document.querySelector('div div div span').id);
testing.expectEqual('Nested paragraph', document.querySelector('div.nested p.desc-text').textContent);
testing.expectEqual('target', document.querySelector('div #target').id);
testing.expectEqual('Nested paragraph', document.querySelector('.nested p').textContent);
testing.expectEqual('Deeply nested paragraph', document.querySelector('div span p').textContent);
testing.expectEqual(null, document.querySelector('article div p'));
}
</script>
<script id=descendantWithWhitespace>
{
testing.expectEqual('Direct child paragraph', document.querySelector(' div p ').textContent);
testing.expectEqual('Nested paragraph', document.querySelector(' div.nested p.desc-text ').textContent);
}
</script>
<div id="compound-id-test" class="special">
<p class="text">Paragraph in compound ID div</p>
<div class="inner">
<span id="nested-compound" class="highlight">Nested span</span>
</div>
</div>
<script id=compoundIdSelectors>
{
testing.expectEqual('Paragraph in compound ID div', document.querySelector('div#compound-id-test p').textContent);
testing.expectEqual('Paragraph in compound ID div', document.querySelector('#compound-id-test.special p').textContent);
testing.expectEqual('Nested span', document.querySelector('div #nested-compound.highlight').textContent);
testing.expectEqual('Nested span', document.querySelector('#compound-id-test span#nested-compound').textContent);
testing.expectEqual('Nested span', document.querySelector('div.special #nested-compound.highlight').textContent);
}
</script>
<script id=idOptimizationEdgeCases>
{
testing.expectEqual(null, document.querySelector('article #compound-id-test p'));
testing.expectEqual(null, document.querySelector('#compound-id-test.wrong-class p'));
testing.expectEqual(null, document.querySelector('#compound-id-test.container p'));
testing.expectEqual(null, document.querySelector('#compound-id-test article'));
testing.expectEqual(null, document.querySelector('#compound-id-test nav'));
testing.expectEqual(null, document.querySelector('#nonexistent-id p'));
testing.expectEqual(null, document.querySelector('div #nonexistent-id'));
testing.expectEqual(null, document.querySelector('#nonexistent-id.some-class span'));
testing.expectEqual(null, document.querySelector('span#compound-id-test p'));
testing.expectEqual(null, document.querySelector('article#nested-compound'));
}
</script>
<script id=universalDescendantSelectors>
{
testing.expectEqual('div1', document.querySelector('* div').textContent);
testing.expectEqual('span1', document.querySelector('* span').textContent);
testing.expectEqual('Direct child paragraph', document.querySelector('div *').textContent);
testing.expectEqual('Nested paragraph', document.querySelector('.nested *').textContent);
testing.expectEqual('div1', document.querySelector('* .test-class').textContent);
testing.expectEqual('multi1', document.querySelector('* .multiple').textContent);
testing.expectEqual('Nested paragraph', document.querySelector('.nested *').textContent);
testing.expectEqual('Paragraph in compound ID div', document.querySelector('.special *').textContent);
testing.expectEqual('test1', document.querySelector('* #byId').getAttribute('name'));
testing.expectEqual('Compound 1', document.querySelector('* #compound-test').textContent);
testing.expectEqual('Direct child paragraph', document.querySelector('#descendant-container *').textContent);
testing.expectEqual('Paragraph in compound ID div', document.querySelector('#compound-id-test *').textContent);
testing.expectEqual('div1', document.querySelector('* * div').textContent);
testing.expectEqual(document.querySelector('p').textContent, document.querySelector('* * p').textContent);
testing.expectEqual('Nested paragraph', document.querySelector('div * p').textContent);
testing.expectEqual('Nested paragraph', document.querySelector('#descendant-container * p').textContent);
testing.expectEqual('Deeply nested paragraph', document.querySelector('.nested * p').textContent);
testing.expectEqual('target', document.querySelector('#complex * span').id);
testing.expectEqual('Nested span', document.querySelector('#compound-id-test * span').textContent);
testing.expectEqual('Nested paragraph', document.querySelector('* div.nested p').textContent);
testing.expectEqual('Paragraph in compound ID div', document.querySelector('* #compound-id-test.special p').textContent);
}
</script>
<svg id="test-svg-selector" width="100" height="100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red"/>
<text x="10" y="20">SVG Text Content</text>
<g id="svg-group">
<rect x="5" y="5" width="20" height="20"/>
</g>
</svg>
<script id=svgSelectors>
{
testing.expectEqual('svg', document.querySelector('svg').tagName);
testing.expectEqual('circle', document.querySelector('circle').tagName);
testing.expectEqual('text', document.querySelector('text').tagName);
testing.expectEqual('circle', document.querySelector('svg circle').tagName);
testing.expectEqual('text', document.querySelector('svg text').tagName);
testing.expectEqual('rect', document.querySelector('svg rect').tagName);
testing.expectEqual('rect', document.querySelector('#svg-group rect').tagName);
testing.expectEqual('rect', document.querySelector('svg #svg-group rect').tagName);
testing.expectEqual('rect', document.querySelector('g rect').tagName);
testing.expectEqual('rect', document.querySelector('svg g rect').tagName);
}
</script>

View File

@@ -1,378 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div class="test-class">div1</div>
<span class="test-class">span1</span>
<div class="multiple classes here">multi1</div>
<div class="classes">multi2</div>
<div class="">empty-class</div>
<div>no-class</div>
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h5>Heading 5</h5>
<h6>Heading 6</h6>
<b>Bold text</b>
<i>Italic text</i>
<em>Emphasized text</em>
<strong>Strong text</strong>
<header>Header element</header>
<nav>Navigation</nav>
<main>Main content</main>
<script>
function assertList(expected, result) {
testing.expectEqual(expected.length, result.length);
testing.expectEqual(expected, Array.from(result).map((e) => e.textContent));
testing.expectEqual(expected, Array.from(result.values()).map((e) => e.textContent));
testing.expectEqual(expected.map((e, i) => i), Array.from(result.keys()));
}
</script>
<script id=script1 name="test1">
testing.expectError("Syntax Error", () => document.querySelectorAll(''));
testing.withError((err) => {
testing.expectEqual(12, err.code);
testing.expectEqual("SyntaxError", err.name);
testing.expectEqual("Syntax Error", err.message);
}, () => document.querySelectorAll(''));
</script>
<script id=byId>
{
const result = document.querySelectorAll('#script1');
testing.expectEqual(true, result instanceof NodeList);
testing.expectEqual(1, result.length);
testing.expectEqual('test1', result[0].getAttribute('name'));
assertList([], document.querySelectorAll('#nonexistent'));
}
</script>
<script id=byClass>
assertList([], document.querySelectorAll('.nope'));
assertList(['div1', 'span1'], document.querySelectorAll('.test-class'));
assertList(['multi1', 'multi2'], document.querySelectorAll('.classes'));
assertList(['multi1'], document.querySelectorAll('.multiple'));
assertList(['multi1'], document.querySelectorAll('.here'));
</script>
<script id=byTag>
{
const divs = document.querySelectorAll('div');
testing.expectEqual(true, divs.length >= 5);
testing.expectEqual('div1', divs[0].textContent);
const scripts = document.querySelectorAll('script');
testing.expectEqual(true, scripts.length >= 5);
assertList(['span1'], document.querySelectorAll('span'));
assertList([], document.querySelectorAll('article'));
assertList([], document.querySelectorAll('section'));
assertList(['Heading 1'], document.querySelectorAll('h1'));
assertList(['Heading 2'], document.querySelectorAll('h2'));
assertList(['Heading 3'], document.querySelectorAll('h3'));
assertList(['Heading 1'], document.querySelectorAll('H1'));
assertList(['Navigation'], document.querySelectorAll('NAV'));
}
</script>
<div id="compound-test" class="container active">Compound 1</div>
<div class="container">Compound 2</div>
<span class="container active">Compound 3</span>
<div class="active">Compound 4</div>
<p id="compound-test2">Compound 5</p>
<script id=compoundSelectors>
{
assertList(['Compound 1', 'Compound 2'], document.querySelectorAll('div.container'));
assertList(['Compound 1', 'Compound 4'], document.querySelectorAll('div.active'));
assertList(['Compound 3'], document.querySelectorAll('span.active'));
assertList(['Compound 1', 'Compound 3'], document.querySelectorAll('.container.active'));
assertList(['Compound 1', 'Compound 3'], document.querySelectorAll('.active.container'));
assertList(['Compound 1'], document.querySelectorAll('div.container.active'));
assertList(['Compound 1'], document.querySelectorAll('div#compound-test'));
assertList(['Compound 1'], document.querySelectorAll('.container#compound-test'));
assertList(['Compound 1'], document.querySelectorAll('#compound-test.active'));
assertList(['Compound 1', 'Compound 2'], document.querySelectorAll(' div.container'));
assertList(['Compound 1', 'Compound 3', 'Compound 4'], document.querySelectorAll(' .active'));
assertList(['Compound 1', 'Compound 2'], document.querySelectorAll('div.container '));
assertList(['Compound 5'], document.querySelectorAll('#compound-test2 '));
assertList(['Compound 3'], document.querySelectorAll(' span.active '));
assertList([], document.querySelectorAll('span#compound-test'));
assertList([], document.querySelectorAll('div.nonexistent'));
assertList([], document.querySelectorAll('p.container'));
}
</script>
<script id=universalSelector>
{
const all = document.querySelectorAll('*');
testing.expectEqual(true, all.length > 20);
testing.expectEqual('HTML', all[0].tagName);
assertList(['div1', 'span1'], document.querySelectorAll('*.test-class'));
const scriptResult = document.querySelectorAll('*#script1');
testing.expectEqual(1, scriptResult.length);
testing.expectEqual('test1', scriptResult[0].getAttribute('name'));
assertList(['Compound 1', 'Compound 3'], document.querySelectorAll('*.container.active'));
}
</script>
<script id=iteratorTests>
{
const list = document.querySelectorAll('.test-class');
const entries = Array.from(list.entries());
testing.expectEqual(2, entries.length);
testing.expectEqual(0, entries[0][0]);
testing.expectEqual('div1', entries[0][1].textContent);
testing.expectEqual(1, entries[1][0]);
testing.expectEqual('span1', entries[1][1].textContent);
const keys = Array.from(list.keys());
testing.expectEqual([0, 1], keys);
const values = Array.from(list.values());
testing.expectEqual(['div1', 'span1'], values.map((e) => e.textContent));
const defaultIter = Array.from(list);
testing.expectEqual(['div1', 'span1'], defaultIter.map((e) => e.textContent));
}
</script>
<script id=multipleIterators>
{
const list = document.querySelectorAll('.test-class');
const keysIter = list.keys();
const valuesIter = list.values();
const key1 = keysIter.next();
testing.expectEqual(false, key1.done);
testing.expectEqual(0, key1.value);
const val1 = valuesIter.next();
testing.expectEqual(false, val1.done);
testing.expectEqual('div1', val1.value.textContent);
const key2 = keysIter.next();
testing.expectEqual(false, key2.done);
testing.expectEqual(1, key2.value);
const val2 = valuesIter.next();
testing.expectEqual(false, val2.done);
testing.expectEqual('span1', val2.value.textContent);
const key3 = keysIter.next();
testing.expectEqual(true, key3.done);
const val3 = valuesIter.next();
testing.expectEqual(true, val3.done);
}
</script>
<script id=iteratorLifetime>
{
let iter;
{
const list = document.querySelectorAll('.test-class');
iter = list.values();
}
const val1 = iter.next();
testing.expectEqual(false, val1.done);
testing.expectEqual('div1', val1.value.textContent);
const val2 = iter.next();
testing.expectEqual(false, val2.done);
testing.expectEqual('span1', val2.value.textContent);
const val3 = iter.next();
testing.expectEqual(true, val3.done);
}
</script>
<div id="descendant-container">
<p class="desc-text">Direct child paragraph</p>
<div class="nested">
<p class="desc-text">Nested paragraph</p>
<span>
<p class="deep">Deeply nested paragraph</p>
</span>
</div>
<article>
<p class="desc-text">Article paragraph</p>
</article>
</div>
<p class="desc-text">Outside paragraph</p>
<div id="complex">
<div class="outer">
<div class="middle">
<div class="inner">
<span id="target">Target span</span>
</div>
</div>
</div>
</div>
<script>
function assertList(expected, result) {
testing.expectEqual(expected.length, result.length);
testing.expectEqual(expected, Array.from(result).map((e) => e.textContent));
}
</script>
<script id=descendantSelectors>
{
assertList(['Direct child paragraph', 'Nested paragraph', 'Deeply nested paragraph', 'Article paragraph'], document.querySelectorAll('div p'));
assertList(['Direct child paragraph', 'Nested paragraph', 'Article paragraph'], document.querySelectorAll('div .desc-text'));
assertList(['Direct child paragraph', 'Nested paragraph', 'Deeply nested paragraph', 'Article paragraph'], document.querySelectorAll('#descendant-container p'));
assertList(['Nested paragraph', 'Deeply nested paragraph'], document.querySelectorAll('div div p'));
assertList(['Target span'], document.querySelectorAll('div div div span'));
assertList(['Nested paragraph'], document.querySelectorAll('div.nested p.desc-text'));
assertList([], document.querySelectorAll('article div p'));
}
</script>
<script id=descendantWithWhitespace>
{
assertList(['Direct child paragraph', 'Nested paragraph', 'Deeply nested paragraph', 'Article paragraph'], document.querySelectorAll(' div p '));
assertList(['Nested paragraph'], document.querySelectorAll(' div.nested p.desc-text '));
}
</script>
<script id=multiLevelDescendant>
{
assertList(['Nested paragraph', 'Deeply nested paragraph'], document.querySelectorAll('body div div p'));
assertList(['Target span'], document.querySelectorAll('body div div div div span'));
}
</script>
<script id=descendantCombinedWithCompound>
{
assertList(['Nested paragraph', 'Deeply nested paragraph'], document.querySelectorAll('.nested p'));
const bodyPs = document.querySelectorAll('body p');
testing.expectEqual(true, bodyPs.length >= 5);
assertList(['Target span'], document.querySelectorAll('#complex span'));
assertList(['Target span'], document.querySelectorAll('div.outer span'));
assertList(['Target span'], document.querySelectorAll('.middle .inner span'));
}
</script>
<div id="combinator-test">
<h1>Title</h1>
<p class="intro">First paragraph</p>
<p>Second paragraph</p>
<div class="content">
<span>Span 1</span>
<span>Span 2</span>
<span>Span 3</span>
</div>
<p>Third paragraph</p>
</div>
<script id=childCombinator>
{
assertList(['First paragraph', 'Second paragraph', 'Third paragraph'], document.querySelectorAll('#combinator-test > p'));
assertList(['Span 1', 'Span 2', 'Span 3'], document.querySelectorAll('div.content > span'));
assertList([], document.querySelectorAll('#combinator-test > span'));
assertList(['Title', 'First paragraph', 'Second paragraph', 'Third paragraph'], document.querySelectorAll('#combinator-test > *:not(div)'));
}
</script>
<script id=adjacentSiblingCombinator>
{
assertList(['First paragraph'], document.querySelectorAll('h1 + p'));
assertList(['Second paragraph'], document.querySelectorAll('p.intro + p'));
assertList([], document.querySelectorAll('#combinator-test div + h1'));
}
</script>
<script id=generalSiblingCombinator>
{
assertList(['First paragraph', 'Second paragraph', 'Third paragraph'], document.querySelectorAll('#combinator-test h1 ~ p'));
assertList(['Second paragraph', 'Third paragraph'], document.querySelectorAll('p.intro ~ p'));
assertList([], document.querySelectorAll('#combinator-test p ~ h1'));
}
</script>
<script id=combinedCombinators>
{
assertList(['Span 1'], document.querySelectorAll('#combinator-test > div > span:nth-child(1)'));
assertList(['Second paragraph'], document.querySelectorAll('#combinator-test h1 + p + p'));
assertList(['Second paragraph', 'Third paragraph'], document.querySelectorAll('#combinator-test h1 + p ~ p'));
}
</script>
<div id="pseudo-test">
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<li>Item 4</li>
<li>Item 5</li>
</ul>
<div>
<p>Para 1</p>
<span>Span</span>
<p>Para 2</p>
</div>
</div>
<script id=firstChildPseudo>
{
assertList(['Item 1'], document.querySelectorAll('#pseudo-test li:first-child'));
assertList(['Para 1'], document.querySelectorAll('#pseudo-test p:first-child'));
assertList([], document.querySelectorAll('#pseudo-test span:first-child'));
}
</script>
<script id=lastChildPseudo>
{
assertList(['Item 5'], document.querySelectorAll('#pseudo-test li:last-child'));
assertList(['Para 2'], document.querySelectorAll('#pseudo-test p:last-child'));
}
</script>
<script id=nthChildPseudo>
{
assertList(['Item 2'], document.querySelectorAll('#pseudo-test li:nth-child(2)'));
assertList(['Item 1', 'Item 3', 'Item 5'], document.querySelectorAll('#pseudo-test li:nth-child(odd)'));
assertList(['Item 2', 'Item 4'], document.querySelectorAll('#pseudo-test li:nth-child(even)'));
assertList(['Item 3', 'Item 5'], document.querySelectorAll('#pseudo-test li:nth-child(2n+3)'));
}
</script>
<script id=firstOfTypePseudo>
{
assertList(['Para 1'], document.querySelectorAll('#pseudo-test div > p:first-of-type'));
assertList(['Span'], document.querySelectorAll('#pseudo-test div > span:first-of-type'));
}
</script>
<script id=lastOfTypePseudo>
{
assertList(['Para 2'], document.querySelectorAll('#pseudo-test div > p:last-of-type'));
assertList(['Span'], document.querySelectorAll('#pseudo-test div > span:last-of-type'));
}
</script>

View File

@@ -1,113 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id="container">
<div data-test="value1">First</div>
<div data-test="value2">Second</div>
<input type="text" name="username">
<input type="password" name="password">
<input type="checkbox" checked>
<a href="https://example.com">Link 1</a>
<a href="/relative">Link 2</a>
<a href="https://example.com/page">Link 3</a>
<button disabled>Disabled Button</button>
<button>Enabled Button</button>
<span class="foo bar">Span 1</span>
<span class="foo">Span 2</span>
<span class="bar">Span 3</span>
<div lang="en">English</div>
<div lang="en-US">American English</div>
<div lang="en-GB">British English</div>
<div lang="fr">French</div>
<img src="image.png" alt="Test image">
<img src="photo.jpg">
</div>
<script id="presence">
testing.expectEqual(2, document.querySelectorAll('[data-test]').length);
testing.expectEqual(3, document.querySelectorAll('[type]').length);
testing.expectEqual(1, document.querySelectorAll('[checked]').length);
testing.expectEqual(1, document.querySelectorAll('[disabled]').length);
testing.expectEqual(1, document.querySelectorAll('[alt]').length);
const firstDataTest = document.querySelector('[data-test]');
testing.expectEqual('First', firstDataTest.innerText);
</script>
<script id="exact">
testing.expectEqual(1, document.querySelectorAll('[data-test="value1"]').length);
testing.expectEqual(1, document.querySelectorAll('[data-test="value2"]').length);
testing.expectEqual(0, document.querySelectorAll('[data-test="value3"]').length);
testing.expectEqual(1, document.querySelectorAll('[type="text"]').length);
testing.expectEqual(1, document.querySelectorAll('[type="password"]').length);
testing.expectEqual(1, document.querySelectorAll('[type="checkbox"]').length);
const textInput = document.querySelector('[type="text"]');
testing.expectEqual('username', textInput.getAttribute('name'));
</script>
<script id="word">
testing.expectEqual(2, document.querySelectorAll('[class~="foo"]').length);
testing.expectEqual(2, document.querySelectorAll('[class~="bar"]').length);
testing.expectEqual(0, document.querySelectorAll('[class~="baz"]').length);
const fooEl = document.querySelector('[class~="foo"]');
testing.expectEqual('Span 1', fooEl.innerText);
</script>
<script id="prefixDash">
testing.expectEqual(3, document.querySelectorAll('[lang|="en"]').length);
testing.expectEqual(1, document.querySelectorAll('[lang|="fr"]').length);
testing.expectEqual(0, document.querySelectorAll('[lang|="es"]').length);
const enEl = document.querySelector('[lang|="en"]');
testing.expectEqual('English', enEl.innerText);
</script>
<script id="startsWith">
testing.expectEqual(2, document.querySelectorAll('[href^="https://"]').length);
testing.expectEqual(1, document.querySelectorAll('[href^="/"]').length);
testing.expectEqual(2, document.querySelectorAll('[href^="https://example.com"]').length);
const httpsLink = document.querySelector('[href^="https://"]');
testing.expectEqual('Link 1', httpsLink.innerText);
</script>
<script id="endsWith">
testing.expectEqual(1, document.querySelectorAll('[href$=".com"]').length);
testing.expectEqual(1, document.querySelectorAll('[href$="/page"]').length);
testing.expectEqual(1, document.querySelectorAll('[href$="relative"]').length);
const comLink = document.querySelector('[href$=".com"]');
testing.expectEqual('https://example.com', comLink.getAttribute('href'));
</script>
<script id="substring">
testing.expectEqual(2, document.querySelectorAll('[href*="example"]').length);
testing.expectEqual(1, document.querySelectorAll('[href*="relative"]').length);
testing.expectEqual(1, document.querySelectorAll('[src*="image"]').length);
testing.expectEqual(1, document.querySelectorAll('[src*="photo"]').length);
const exampleLink = document.querySelector('[href*="example"]');
testing.expectEqual('Link 1', exampleLink.innerText);
</script>
<script id="compound">
testing.expectEqual(1, document.querySelectorAll('input[type="text"]').length);
testing.expectEqual(1, document.querySelectorAll('input[type="password"]').length);
testing.expectEqual(1, document.querySelectorAll('button[disabled]').length);
testing.expectEqual(1, document.querySelectorAll('div[lang="en"]').length);
testing.expectEqual(1, document.querySelectorAll('div[data-test="value1"]').length);
const compoundInput = document.querySelector('input[type="text"][name="username"]');
testing.expectEqual('username', compoundInput.getAttribute('name'));
</script>
<script id="descendant">
testing.expectEqual(2, document.querySelectorAll('#container [data-test]').length);
testing.expectEqual(3, document.querySelectorAll('#container input[type]').length);
const containerDataTest = document.querySelector('#container [data-test]');
testing.expectEqual('First', containerDataTest.innerText);
</script>

View File

@@ -1,202 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id="root">
<div id="a" class="x">
<p id="b" class="y">Para 1</p>
<div id="c" class="x y">
<span id="d">Span 1</span>
<p id="e" class="z">Para 2</p>
</div>
</div>
<article id="f">
<div id="g">
<p id="h">Para 3</p>
</div>
</article>
</div>
<script>
function assertList(expected, result) {
testing.expectEqual(expected.length, result.length);
testing.expectEqual(expected, Array.from(result).map((e) => e.textContent));
}
</script>
<script id=redundantUniversal>
{
// Universal with ID
assertList(['Para 1'], document.querySelectorAll('#b'));
assertList(['Span 1'], document.querySelectorAll('#d'));
assertList(['Span 1', 'Para 2'], document.querySelectorAll('#c *'));
// Universal with combinators
assertList(['Span 1', 'Para 2'], document.querySelectorAll('#c > *'));
assertList(['Span 1', 'Para 2'], document.querySelectorAll('#b + * *'));
}
</script>
<script id=multipleClasses>
{
// Multiple class combinations
const yClass = document.querySelectorAll('.y');
testing.expectEqual(true, yClass.length >= 1);
assertList(['Span 1', 'Para 2'], document.querySelectorAll('.x.y *'));
assertList(['Span 1', 'Para 2'], document.querySelectorAll('.y.x *'));
}
</script>
<script id=idEdgeCases>
{
// ID with universal
assertList(['Para 1'], document.querySelectorAll('*#b'));
assertList(['Para 1'], document.querySelectorAll('#b'));
// ID with tag mismatch (should return empty)
assertList([], document.querySelectorAll('div#b'));
assertList([], document.querySelectorAll('span#a'));
}
</script>
<script id=complexCombinatorChains>
{
// Long combinator chains
assertList(['Span 1'], document.querySelectorAll('div > div > div > span'));
// Mixed combinators
assertList(['Para 2'], document.querySelectorAll('#b + div > p'));
assertList(['Para 2'], document.querySelectorAll('#b ~ * > p'));
assertList(['Para 2'], document.querySelectorAll('p + div > p'));
}
</script>
<script id=descendantWithSiblings>
{
// Descendant followed by sibling
assertList(['Span 1', 'Para 2'], document.querySelectorAll('#a div *'));
assertList(['Span 1', 'Para 2'], document.querySelectorAll('#a p ~ div *'));
assertList(['Span 1', 'Para 2'], document.querySelectorAll('#a p + div *'));
}
</script>
<script id=notEdgeCases>
{
// :not() with different selectors
const notDivArticle = document.querySelectorAll('*:not(div):not(article)');
testing.expectEqual(true, notDivArticle.length >= 3);
assertList(['Para 3'], document.querySelectorAll('#root p:not(.y):not(.z)'));
// :not() with universal
const allButDiv = document.querySelectorAll('#root *:not(div)');
testing.expectEqual(true, allButDiv.length >= 3);
// :not() with ID
const allP = document.querySelectorAll('p:not(#nonexistent)');
testing.expectEqual(true, allP.length >= 3);
assertList(['Para 1', 'Para 3'], document.querySelectorAll('p:not(#e)'));
// :not() with class
const notClasses = document.querySelectorAll('#root *:not(.x):not(.y):not(.z)');
testing.expectEqual(true, notClasses.length >= 2);
}
</script>
<script id=pseudoClassCombinations>
{
// First/last child combinations
assertList(['Para 1'], document.querySelectorAll('#a > p:first-child'));
assertList(['Span 1'], document.querySelectorAll('#c > :first-child'));
assertList(['Para 2'], document.querySelectorAll('#c > :last-child'));
// nth-child with combinators
assertList(['Span 1'], document.querySelectorAll('#c > :nth-child(1)'));
assertList(['Para 2'], document.querySelectorAll('#c > :nth-child(2)'));
assertList(['Span 1'], document.querySelectorAll('#c > :nth-child(odd)'));
assertList(['Para 2'], document.querySelectorAll('#c > :nth-child(even)'));
}
</script>
<script id=whitespaceMadness>
{
// Excessive whitespace
assertList(['Para 2'], document.querySelectorAll(' #a > div > p '));
assertList(['Para 2'], document.querySelectorAll(' p + div > p '));
assertList(['Span 1', 'Para 2'], document.querySelectorAll(' #b ~ div * '));
}
</script>
<script id=compoundSelectorMadness>
{
// Very long compound selectors
assertList(['Para 1'], document.querySelectorAll('p#b.y'));
assertList(['Para 1'], document.querySelectorAll('#b.y'));
assertList(['Para 1'], document.querySelectorAll('.y#b'));
assertList(['Para 1'], document.querySelectorAll('p.y#b'));
assertList(['Para 1'], document.querySelectorAll('*.y#b'));
assertList(['Para 1'], document.querySelectorAll('*#b.y'));
assertList(['Para 1'], document.querySelectorAll('#b.y'));
// Multiple classes in different orders
assertList(['Span 1', 'Para 2'], document.querySelectorAll('.x.y *'));
assertList(['Span 1', 'Para 2'], document.querySelectorAll('.y.x *'));
assertList(['Span 1', 'Para 2'], document.querySelectorAll('div.x.y *'));
assertList(['Span 1', 'Para 2'], document.querySelectorAll('div.y.x *'));
}
</script>
<script id=rootBoundaries>
{
// Root itself can match in ancestor searches
const a = document.getElementById('a');
const divChildren = a.querySelectorAll('div *');
testing.expectEqual(true, divChildren.length >= 2);
const c = document.getElementById('c');
const cDivChildren = c.querySelectorAll('div *');
// c itself is a div, so its children match
testing.expectEqual(2, cDivChildren.length);
}
</script>
<script id=siblingEdgeCases>
{
// Sibling selector with descendants
assertList(['Span 1', 'Para 2'], document.querySelectorAll('#b + div *'));
// Multiple sibling hops
const root = document.getElementById('root');
assertList(['Para 3'], root.querySelectorAll('div ~ article p'));
assertList(['Para 3'], root.querySelectorAll('div + article p'));
}
</script>
<script id=nonExistentCombinations>
{
// Valid selectors that don't match anything
assertList([], document.querySelectorAll('p > div'));
assertList([], document.querySelectorAll('span > p'));
assertList([], document.querySelectorAll('#a + #b'));
assertList([], document.querySelectorAll('#b + #a'));
assertList([], document.querySelectorAll('article > article'));
assertList([], document.querySelectorAll('span.x'));
assertList([], document.querySelectorAll('p#a'));
assertList([], document.querySelectorAll('.nonexistent'));
assertList([], document.querySelectorAll('#nonexistent'));
assertList([], document.querySelectorAll('video'));
}
</script>
<script id=universalCombinations>
{
// Universal in different positions
const universalDesc = document.querySelectorAll('* * * p');
testing.expectEqual(true, universalDesc.length >= 2);
const universalChild = document.querySelectorAll('* > * > * > *');
testing.expectEqual(true, universalChild.length >= 1);
const mixedUniversal = document.querySelectorAll('* > * > p');
testing.expectEqual(true, mixedUniversal.length >= 1);
}
</script>

View File

@@ -1,119 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id="root">
<div id="container" class="parent">
<p id="p1" class="text">Paragraph 1</p>
<p id="p2" class="text highlight">Paragraph 2</p>
<div id="nested">
<span id="s1" class="item">Span 1</span>
<span id="s2" class="item active">Span 2</span>
<a id="link1" href="#">Link 1</a>
</div>
<article id="article1">
<p id="p3">Article paragraph</p>
<span id="s3">Article span</span>
</article>
</div>
</div>
<script id=basicNot>
{
const notDiv = document.querySelectorAll('#root *:not(div)');
testing.expectEqual(true, notDiv.length >= 7);
const notClass = document.querySelectorAll('#container *:not(.text)');
testing.expectEqual(true, notClass.length >= 5);
const notId = document.querySelectorAll('#container p:not(#p2)');
testing.expectEqual(2, notId.length);
testing.expectEqual('p1', notId[0].id);
testing.expectEqual('p3', notId[1].id);
}
</script>
<script id=notWithCombinators>
{
const notDirectChild = document.querySelectorAll('span:not(#nested > span)');
testing.expectEqual(1, notDirectChild.length);
testing.expectEqual('s3', notDirectChild[0].id);
const notDescendant = document.querySelectorAll('#container > *:not(div)');
testing.expectEqual(3, notDescendant.length);
}
</script>
<script id=notWithMultipleSelectors>
{
const notDivOrP = document.querySelectorAll('#container *:not(div, p)');
testing.expectEqual(5, notDivOrP.length);
const notTextOrItem = document.querySelectorAll('#container *:not(.text, .item)');
testing.expectEqual(true, notTextOrItem.length >= 3);
const notIdOrClass = document.querySelectorAll('p:not(#p1, .highlight)');
testing.expectEqual(1, notIdOrClass.length);
testing.expectEqual('p3', notIdOrClass[0].id);
}
</script>
<script id=notWithAttributes>
{
const notWithHref = document.querySelectorAll('#nested *:not([href])');
testing.expectEqual(2, notWithHref.length);
const notLinkWithHref = document.querySelectorAll('#nested *:not(a[href])');
testing.expectEqual(2, notLinkWithHref.length);
}
</script>
<script id=notWithPseudoClasses>
{
const notFirstChild = document.querySelectorAll('#nested *:not(:first-child)');
testing.expectEqual(2, notFirstChild.length);
const notLastChild = document.querySelectorAll('#nested *:not(:last-child)');
testing.expectEqual(2, notLastChild.length);
}
</script>
<script id=nestedNot>
{
// :not(:not(...)) matches elements that DO match the inner selector
const items = document.querySelectorAll('.item:not(:not(.active))');
testing.expectEqual(1, items.length);
testing.expectEqual('s2', items[0].id);
}
</script>
<script id=complexCombinations>
{
const complex1 = document.querySelectorAll('p:not(#nested p, .highlight)');
testing.expectEqual(2, complex1.length);
testing.expectEqual('p1', complex1[0].id);
testing.expectEqual('p3', complex1[1].id);
const complex2 = document.querySelectorAll('#container > *:not(div)');
testing.expectEqual(3, complex2.length);
}
</script>
<script id=notWithUniversal>
{
const notUniversal = document.querySelectorAll('#nested :not(*)');
testing.expectEqual(0, notUniversal.length);
}
</script>
<script id=edgeCases>
{
const multiNot = document.querySelectorAll('*:not(div):not(p):not(article)');
testing.expectEqual(true, multiNot.length >= 3);
const notAtEnd = document.querySelectorAll('#container p:not(.highlight)');
testing.expectEqual(2, notAtEnd.length);
const notAtStart = document.querySelectorAll(':not(div) > span');
testing.expectEqual(1, notAtStart.length);
}
</script>

View File

@@ -1,102 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<body></body>
<script id=document_fragment>
{
const df = new DocumentFragment();
testing.expectEqual("DocumentFragment", df.constructor.name);
testing.expectEqual("#document-fragment", df.nodeName);
}
{
const df = document.createDocumentFragment();
testing.expectEqual("DocumentFragment", df.constructor.name);
testing.expectEqual("#document-fragment", df.nodeName);
}
{
const df = document.createDocumentFragment();
const div1 = document.createElement("div");
div1.textContent = "First";
const div2 = document.createElement("div");
div2.textContent = "Second";
df.appendChild(div1);
df.appendChild(div2);
testing.expectEqual(2, df.childElementCount);
testing.expectEqual(div1, df.firstElementChild);
testing.expectEqual(div2, df.lastElementChild);
}
{
const df = document.createDocumentFragment();
df.append("text1", document.createElement("span"), "text2");
testing.expectEqual(3, df.childNodes.length);
testing.expectEqual(1, df.childElementCount);
}
{
const df = document.createDocumentFragment();
df.append("last");
df.prepend("first");
testing.expectEqual("#text", df.firstChild.nodeName);
testing.expectEqual("first", df.firstChild.textContent);
}
{
const df = document.createDocumentFragment();
const p = document.createElement("p");
p.className = "test";
p.textContent = "Hello";
df.appendChild(p);
const found = df.querySelector(".test");
testing.expectEqual(p, found);
testing.expectEqual("Hello", found.textContent);
}
{
const df = document.createDocumentFragment();
df.appendChild(document.createElement("div"));
df.appendChild(document.createElement("div"));
testing.expectEqual(2, df.childElementCount);
df.replaceChildren(document.createElement("span"));
testing.expectEqual(1, df.childElementCount);
testing.expectEqual("SPAN", df.firstElementChild.tagName);
}
{
const df = document.createDocumentFragment();
const outer = document.createElement("div");
outer.id = "outer";
const inner = document.createElement("div");
inner.id = "inner";
outer.appendChild(inner);
df.appendChild(outer);
testing.expectEqual(outer, df.getElementById("outer"));
testing.expectEqual(inner, df.getElementById("inner"));
testing.expectEqual(null, df.getElementById("notfound"));
}
{
const df = document.createDocumentFragment();
const test1 = document.createElement("div");
test1.id = "test1";
const test2 = document.createElement("div");
test2.id = "test2";
df.appendChild(test1);
df.appendChild(test2);
testing.expectEqual(2, df.childElementCount);
document.body.appendChild(df);
testing.expectEqual(0, df.childElementCount);
testing.expectEqual(test1, document.getElementById("test1"));
testing.expectEqual(test2, document.getElementById("test2"));
}
</script>

View File

@@ -1,9 +0,0 @@
<head id="the_head">
<script src="testing.js"></script>
</head>
<body id="the_body">
<script id="test-document-head-body">
testing.expectEqual(document.getElementById('the_head'), document.head);
testing.expectEqual(document.getElementById('the_body'), document.body);
</script>
</body>

View File

@@ -1,29 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id="test-container"></div>
<script id="append-prepend-tests">
const container = $('#test-container');
// Test append
const p1 = document.createElement('p');
p1.textContent = 'p1';
container.append(p1);
testing.expectEqual('<p>p1</p>', container.innerHTML, "append single element");
const p2 = document.createElement('p');
p2.textContent = 'p2';
container.append(p2, ' some text');
testing.expectEqual('<p>p1</p><p>p2</p> some text', container.innerHTML, "append multiple nodes");
// Test prepend
container.innerHTML = '';
const p3 = document.createElement('p');
p3.textContent = 'p3';
container.prepend(p3);
testing.expectEqual('<p>p3</p>', container.innerHTML, "prepend single element");
const p4 = document.createElement('p');
p4.textContent = 'p4';
container.prepend(p4, 'some text ');
testing.expectEqual('<p>p4</p>some text <p>p3</p>', container.innerHTML, "prepend multiple nodes");
</script>

View File

@@ -1,85 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id=attr1 ClasS="sHow"></div>
<script id=attributes>
const el1 = $('#attr1');
testing.expectEqual(null, el1.getAttribute('other'));
testing.expectEqual('attr1', el1.id);
testing.expectEqual('attr1', el1.getAttribute('id'));
testing.expectEqual('attr1', el1.getAttribute('Id'));
testing.expectEqual('sHow', el1.getAttribute('CLASS'));
testing.expectEqual('sHow', el1.getAttribute('class'));
testing.expectEqual(['id', 'class'], el1.getAttributeNames());
el1.setAttribute('other', 'attr-1')
testing.expectEqual('attr-1', el1.getAttribute('other'));
testing.expectEqual(['id', 'class', 'other'], el1.getAttributeNames());
el1.removeAttribute('other');
testing.expectEqual(null, el1.getAttribute('other'));
testing.expectEqual(['id', 'class'], el1.getAttributeNames());
testing.expectEqual(null, el1.getAttributeNode('unknown'));
let an1 = el1.getAttributeNode('class');
testing.expectEqual(an1 , el1.setAttributeNode(an1));
testing.expectEqual(el1, an1.ownerElement);
testing.expectEqual('class', an1.name);
testing.expectEqual('class', an1.localName);
testing.expectEqual('sHow', an1.value);
testing.expectEqual(el1.getAttributeNode('class'), an1);
testing.expectEqual(null, an1.namespaceURI);
const script_id_node = $('#attributes').getAttributeNode('id')
testing.withError((err) => {
testing.expectEqual(8, err.code);
testing.expectEqual("NotFoundError", err.name);
testing.expectEqual("Not Found", err.message);
}, () => el1.removeAttributeNode(script_id_node));
testing.expectEqual(an1, el1.removeAttributeNode(an1));
testing.expectEqual(null, an1.ownerElement);
testing.expectEqual(null, el1.getAttributeNode('class'));
testing.expectEqual(null, el1.setAttributeNode(an1))
testing.expectEqual(el1, an1.ownerElement);
</script>
<script id=namednodemap>
testing.expectEqual(true, el1.attributes instanceof NamedNodeMap);
testing.expectEqual(el1.attributes, el1.attributes);
function assertAttributes(expected) {
const attributes = el1.attributes;
testing.expectEqual(expected.length, attributes.length);
for (let i = 0; i < expected.length; i++) {
const ex = expected[i];
let attribute = attributes[ex.name];
testing.expectEqual(ex.name, attribute.name)
testing.expectEqual(ex.value, attribute.value)
attribute = attributes.getNamedItem(ex.name);
testing.expectEqual(ex.name, attribute.name)
testing.expectEqual(ex.value, attribute.value)
attribute = attributes.item(i);
testing.expectEqual(ex.name, attribute.name)
testing.expectEqual(ex.value, attribute.value)
}
testing.expectEqual(undefined, attributes["nope"]);
testing.expectEqual('attr1', attributes.id.value);
testing.expectEqual(null, attributes.getNamedItem("nope"));
testing.expectEqual(null, attributes.item(100));
testing.expectEqual(null, attributes.item(-3));
let acc = [];
for (let attribute of attributes) {
acc.push({name: attribute.name, value: attribute.value});
}
testing.expectEqual(expected, acc);
}
assertAttributes([{name: 'id', value: 'attr1'}, {name: 'class', value: 'sHow'}]);
</script>

View File

@@ -1,334 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id=test1></div>
<div id=test2 class="foo bar"></div>
<script id=basic>
{
const div = $('#test1');
testing.expectEqual(0, div.classList.length);
div.classList.add('foo');
testing.expectEqual('foo', div.className);
testing.expectEqual(1, div.classList.length);
div.classList.add('bar', 'baz');
testing.expectEqual('foo bar baz', div.className);
testing.expectEqual(3, div.classList.length);
div.classList.add('bar');
testing.expectEqual('foo bar baz', div.className);
testing.expectEqual(3, div.classList.length);
}
</script>
<script id=contains>
{
const div = $('#test2');
testing.expectEqual(false, div.classList.contains(''));
testing.expectEqual(true, div.classList.contains('foo'));
testing.expectEqual(true, div.classList.contains('bar'));
testing.expectEqual(false, div.classList.contains('baz'));
}
</script>
<script id=remove>
{
const div = $('#test2');
div.classList.remove('foo');
testing.expectEqual('bar', div.className);
testing.expectEqual(1, div.classList.length);
div.classList.remove('nonexistent');
testing.expectEqual('bar', div.className);
div.classList.remove('bar');
testing.expectEqual('', div.className);
testing.expectEqual(0, div.classList.length);
}
</script>
<script id=toggle>
{
const div = document.createElement('div');
let result = div.classList.toggle('foo');
testing.expectEqual(true, result);
testing.expectEqual('foo', div.className);
result = div.classList.toggle('foo');
testing.expectEqual(false, result);
testing.expectEqual('', div.className);
result = div.classList.toggle('bar', true);
testing.expectEqual(true, result);
testing.expectEqual('bar', div.className);
result = div.classList.toggle('bar', true);
testing.expectEqual(true, result);
testing.expectEqual('bar', div.className);
result = div.classList.toggle('baz', false);
testing.expectEqual(false, result);
testing.expectEqual('bar', div.className);
}
</script>
<script id=replace>
{
const div = document.createElement('div');
div.className = 'foo bar baz';
let result = div.classList.replace('bar', 'qux');
testing.expectEqual(true, result);
testing.expectEqual('foo qux baz', div.className);
result = div.classList.replace('nonexistent', 'other');
testing.expectEqual(false, result);
testing.expectEqual('foo qux baz', div.className);
}
</script>
<script id=item>
{
const div = document.createElement('div');
div.className = 'alpha beta gamma';
testing.expectEqual('alpha', div.classList.item(0));
testing.expectEqual('beta', div.classList.item(1));
testing.expectEqual('gamma', div.classList.item(2));
testing.expectEqual(null, div.classList.item(3));
testing.expectEqual(null, div.classList.item(-1));
testing.expectEqual('alpha', div.classList[0]);
testing.expectEqual('beta', div.classList[1]);
testing.expectEqual('gamma', div.classList[2]);
testing.expectEqual(undefined, div.classList[3]);
}
</script>
<script id=iteration>
{
const div = document.createElement('div');
div.className = 'one two three';
const result = [];
for (const cls of div.classList) {
result.push(cls);
}
testing.expectEqual(['one', 'two', 'three'], result);
}
</script>
<script id=sync>
{
const div = document.createElement('div');
div.setAttribute('class', 'alpha beta');
testing.expectEqual(2, div.classList.length);
testing.expectEqual(true, div.classList.contains('alpha'));
testing.expectEqual(true, div.classList.contains('beta'));
div.className = 'gamma delta';
testing.expectEqual(2, div.classList.length);
testing.expectEqual(true, div.classList.contains('gamma'));
testing.expectEqual(false, div.classList.contains('alpha'));
div.classList.add('epsilon');
testing.expectEqual('gamma delta epsilon', div.className);
}
</script>
<script id=whitespace>
{
const div = document.createElement('div');
div.className = ' foo bar \t\n baz ';
testing.expectEqual(3, div.classList.length);
testing.expectEqual('foo', div.classList[0]);
testing.expectEqual('bar', div.classList[1]);
testing.expectEqual('baz', div.classList[2]);
}
</script>
<script id=value>
{
const div = document.createElement('div');
div.classList.value = 'test1 test2';
testing.expectEqual('test1 test2', div.classList.value);
testing.expectEqual('test1 test2', div.className);
testing.expectEqual(2, div.classList.length);
}
</script>
<script id=errors>
{
const div = document.createElement('div');
testing.withError((err) => {
testing.expectEqual('SyntaxError', err.name);
}, () => div.classList.add(''));
testing.withError((err) => {
testing.expectEqual('InvalidCharacterError', err.name);
}, () => div.classList.add('foo bar'));
testing.withError((err) => {
testing.expectEqual('InvalidCharacterError', err.name);
}, () => div.classList.add('foo\tbar'));
testing.withError((err) => {
testing.expectEqual('InvalidCharacterError', err.name);
}, () => div.classList.add('foo\nbar'));
testing.withError((err) => {
testing.expectEqual('SyntaxError', err.name);
}, () => div.classList.toggle(''));
testing.withError((err) => {
testing.expectEqual('InvalidCharacterError', err.name);
}, () => div.classList.remove('has space'));
}
</script>
<script id=identity>
{
const div = document.createElement('div');
testing.expectEqual(div.classList, div.classList);
const classList = div.classList;
div.className = 'changed';
testing.expectEqual(classList, div.classList);
}
</script>
<script id=attribute_lifecycle>
{
const div = document.createElement('div');
testing.expectEqual(null, div.getAttribute('class'));
testing.expectEqual(0, div.classList.length);
div.classList.add('foo');
testing.expectEqual('foo', div.getAttribute('class'));
div.classList.remove('foo');
testing.expectEqual('', div.getAttribute('class'));
testing.expectEqual(0, div.classList.length);
div.className = 'bar';
testing.expectEqual('bar', div.getAttribute('class'));
div.className = '';
testing.expectEqual('', div.getAttribute('class'));
}
</script>
<script id=duplicates>
{
const div = document.createElement('div');
div.className = 'foo foo bar';
testing.expectEqual(2, div.classList.length);
testing.expectEqual('foo', div.classList[0]);
testing.expectEqual('bar', div.classList[1]);
div.classList.add('foo');
testing.expectEqual('foo bar', div.className);
div.classList.remove('foo');
testing.expectEqual('bar', div.className);
}
</script>
<script id=replace_edge_cases>
{
const div = document.createElement('div');
div.className = 'foo bar';
let result = div.classList.replace('foo', 'foo');
testing.expectEqual(true, result);
testing.expectEqual('foo bar', div.className);
result = div.classList.replace('foo', 'bar');
testing.expectEqual(true, result);
testing.expectEqual('bar', div.className);
div.className = 'alpha beta gamma';
result = div.classList.replace('beta', 'gamma');
testing.expectEqual(true, result);
testing.expectEqual('alpha gamma', div.className);
}
</script>
<script id=empty_calls>
{
const div = document.createElement('div');
div.className = 'foo bar';
div.classList.add();
testing.expectEqual('foo bar', div.className);
div.classList.remove();
testing.expectEqual('foo bar', div.className);
}
</script>
<script id=case_sensitivity>
{
const div = document.createElement('div');
div.classList.add('Foo');
div.classList.add('foo');
div.classList.add('FOO');
testing.expectEqual('Foo foo FOO', div.className);
testing.expectEqual(3, div.classList.length);
testing.expectEqual(true, div.classList.contains('Foo'));
testing.expectEqual(true, div.classList.contains('foo'));
testing.expectEqual(true, div.classList.contains('FOO'));
testing.expectEqual(false, div.classList.contains('fOO'));
}
</script>
<script id=special_characters>
{
const div = document.createElement('div');
div.classList.add('class-name');
div.classList.add('class_name');
div.classList.add('class123');
div.classList.add('123class');
div.classList.add('über');
div.classList.add('日本語');
testing.expectEqual(6, div.classList.length);
testing.expectEqual(true, div.classList.contains('class-name'));
testing.expectEqual(true, div.classList.contains('class_name'));
testing.expectEqual(true, div.classList.contains('class123'));
testing.expectEqual(true, div.classList.contains('123class'));
testing.expectEqual(true, div.classList.contains('über'));
testing.expectEqual(true, div.classList.contains('日本語'));
}
</script>
<script id=multiple_operations>
{
const div = document.createElement('div');
div.classList.add('a', 'b', 'c', 'd', 'e');
testing.expectEqual('a b c d e', div.className);
div.classList.remove('b', 'd');
testing.expectEqual('a c e', div.className);
div.classList.add('b', 'c', 'f');
testing.expectEqual('a c e b f', div.className);
}
</script>

View File

@@ -1,133 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id="test-div"></div>
<script id="camelCaseAccess">
{
const div = $('#test-div');
div.style.cssText = '';
testing.expectEqual('', div.style.backgroundColor);
testing.expectEqual('', div.style.fontSize);
testing.expectEqual('', div.style.marginTop);
div.style.setProperty('background-color', 'blue');
div.style.setProperty('font-size', '14px');
div.style.setProperty('margin-top', '10px');
testing.expectEqual('blue', div.style.backgroundColor);
testing.expectEqual('14px', div.style.fontSize);
testing.expectEqual('10px', div.style.marginTop);
}
</script>
<script id="dashCaseAccess">
{
const div = $('#test-div');
div.style.cssText = '';
div.style.setProperty('background-color', 'red');
div.style.setProperty('font-size', '16px');
testing.expectEqual('red', div.style['background-color']);
testing.expectEqual('16px', div.style['font-size']);
}
</script>
<script id="mixedAccess">
{
const div = $('#test-div');
div.style.cssText = '';
div.style.setProperty('color', 'green');
testing.expectEqual('green', div.style.color);
testing.expectEqual('green', div.style['color']);
testing.expectEqual('green', div.style.getPropertyValue('color'));
}
</script>
<script id="cssFloat">
{
const div = $('#test-div');
div.style.cssText = '';
div.style.setProperty('float', 'left');
testing.expectEqual('left', div.style.cssFloat);
testing.expectEqual('left', div.style['float']);
testing.expectEqual('left', div.style.getPropertyValue('float'));
div.style.setProperty('float', 'right');
testing.expectEqual('right', div.style.cssFloat);
}
</script>
<script id="vendorPrefixes">
{
const div = $('#test-div');
div.style.cssText = '';
div.style.setProperty('-webkit-transform', 'rotate(45deg)');
div.style.setProperty('-moz-user-select', 'none');
div.style.setProperty('-ms-filter', 'blur(5px)');
div.style.setProperty('-o-transition', 'all');
testing.expectEqual('rotate(45deg)', div.style.webkitTransform);
testing.expectEqual(undefined, div.style.mozUserSelect);
testing.expectEqual(undefined, div.style.msFilter);
testing.expectEqual(undefined, div.style.oTransition);
testing.expectEqual('rotate(45deg)', div.style['-webkit-transform']);
testing.expectEqual('none', div.style['-moz-user-select']);
}
</script>
<script id="nonExistentProperties">
{
const div = $('#test-div');
div.style.cssText = '';
testing.expectEqual(undefined, div.style.notARealProperty);
testing.expectEqual(undefined, div.style['not-a-real-property']);
testing.expectEqual(undefined, div.style.somethingTotallyMadeUp);
}
</script>
<script id="edgeCaseCamelConversion">
{
const div = $('#test-div');
div.style.cssText = '';
div.style.setProperty('border-top-left-radius', '5px');
testing.expectEqual('5px', div.style.borderTopLeftRadius);
div.style.setProperty('z-index', '100');
testing.expectEqual('100', div.style.zIndex);
}
</script>
<script id="vendorPrefixEdgeCases">
{
const div = $('#test-div');
div.style.cssText = '';
div.style.setProperty('-webkit-border-radius', '5px');
testing.expectEqual('5px', div.style.webkitBorderRadius);
testing.expectEqual(undefined, div.style.webkitNotSet);
testing.expectEqual(undefined, div.style.mozNotSet);
}
</script>
<script id="prototypeChainCheck">
{
const div = $('#test-div');
testing.expectEqual(true, typeof div.style.getPropertyValue === 'function');
testing.expectEqual(true, typeof div.style.setProperty === 'function');
testing.expectEqual(true, typeof div.style.removeProperty === 'function');
testing.expectEqual(true, typeof div.style.getPropertyPriority === 'function');
}
</script>

View File

@@ -1,54 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id="container">
<!-- comment -->
<p id="p1">Paragraph 1</p>
<div id="div1">Div 1</div>
<!-- comment -->
<span id="span1">Span 1</span>
<p id="p2">Paragraph 2</p>
</div>
<div id="empty"></div>
<script id="relatedElements">
const container = $('#container');
const p1 = $('#p1');
const div1 = $('#div1');
const span1 = $('#span1');
const p2 = $('#p2');
const empty = $('#empty');
testing.expectEqual(p1, container.firstElementChild);
testing.expectEqual(p2, container.lastElementChild);
testing.expectEqual(div1, p1.nextElementSibling);
testing.expectEqual(span1, div1.nextElementSibling);
testing.expectEqual(p2, span1.nextElementSibling);
testing.expectEqual(null, p2.nextElementSibling);
testing.expectEqual(span1, p2.previousElementSibling);
testing.expectEqual(div1, span1.previousElementSibling);
testing.expectEqual(p1, div1.previousElementSibling);
testing.expectEqual(null, p1.previousElementSibling);
testing.expectEqual(null, empty.firstElementChild);
testing.expectEqual(null, empty.lastElementChild);
</script>
<script id=children>
const cc = container.children;
testing.expectEqual(4, cc.length);
testing.expectEqual(p1, cc[0]);
testing.expectEqual(p1, cc.item(0));
testing.expectEqual(div1, cc[1]);
testing.expectEqual(div1, cc.item(1));
testing.expectEqual(span1, cc[2]);
testing.expectEqual(span1, cc.item(2));
testing.expectEqual(p2, cc[3]);
testing.expectEqual(p2, cc.item(3));
const ec = empty.children;
testing.expectEqual(0, ec.length);
testing.expectEqual(undefined, ec[0]);
</script>

View File

@@ -1,187 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id="container" class="container-class">
<div class="foo" id="foo1">foo1</div>
<span class="foo" id="foo2">foo2</span>
<p class="foo" id="foo3">foo3</p>
<div class="bar" id="bar1">bar1</div>
<div class="baz" id="baz1">baz1</div>
</div>
<div id="nested" class="nested-root">
<div class="outer" id="outer">
<div class="inner" id="inner1">inner1</div>
<div class="inner" id="inner2">inner2</div>
<p class="inner" id="inner3">inner3</p>
</div>
</div>
<div id="empty"></div>
<div id="multi" class="multi class names">
<div class="multi" id="multi1">multi1</div>
<span class="class" id="multi2">multi2</span>
<p class="names" id="multi3">multi3</p>
</div>
<div id="classMatch" class="test-class">
<span class="other">span1</span>
<p class="other">p1</p>
</div>
<script id=basic>
{
const container = $('#container');
testing.expectEqual(0, container.getElementsByClassName('nonexistent').length);
testing.expectEqual(0, container.getElementsByClassName('unknown').length);
const foos = container.getElementsByClassName('foo');
testing.expectEqual(true, foos instanceof HTMLCollection);
testing.expectEqual(3, foos.length);
testing.expectEqual(3, foos.length); // cache test
testing.expectEqual('foo1', foos[0].id);
testing.expectEqual('foo2', foos[1].id);
testing.expectEqual('foo3', foos[2].id);
testing.expectEqual('foo1', foos[0].id); // cache test
testing.expectEqual('foo3', foos[2].id);
testing.expectEqual('foo2', foos[1].id);
testing.expectEqual(undefined, foos[-1]);
testing.expectEqual(undefined, foos[3]);
const bars = container.getElementsByClassName('bar');
testing.expectEqual(1, bars.length);
testing.expectEqual('bar1', bars[0].id);
const bazs = container.getElementsByClassName('baz');
testing.expectEqual(1, bazs.length);
testing.expectEqual('baz1', bazs[0].id);
}
</script>
<script id=nestedSearch>
{
const nested = $('#nested');
const inners = nested.getElementsByClassName('inner');
testing.expectEqual(3, inners.length);
testing.expectEqual('inner1', inners[0].id);
testing.expectEqual('inner2', inners[1].id);
testing.expectEqual('inner3', inners[2].id);
const outer = $('#outer');
const outerInners = outer.getElementsByClassName('inner');
testing.expectEqual(3, outerInners.length);
testing.expectEqual('inner1', outerInners[0].id);
testing.expectEqual('inner2', outerInners[1].id);
testing.expectEqual('inner3', outerInners[2].id);
}
</script>
<script id=emptyResult>
{
const empty = $('#empty');
testing.expectEqual(0, empty.getElementsByClassName('foo').length);
testing.expectEqual(0, empty.getElementsByClassName('bar').length);
testing.expectEqual(0, empty.getElementsByClassName('').length);
}
</script>
<script id=excludeSelf>
{
// Element itself should NOT be included in results, even if it matches
const container = $('#container');
const containerClass = container.getElementsByClassName('container-class');
testing.expectEqual(0, containerClass.length); // container has 'container-class' but should not be included
const classMatch = $('#classMatch');
const testClass = classMatch.getElementsByClassName('test-class');
testing.expectEqual(0, testClass.length); // classMatch itself has 'test-class' but should not be included
const others = classMatch.getElementsByClassName('other');
testing.expectEqual(2, others.length); // Only the child elements
}
</script>
<script id=item>
{
const container = $('#container');
const foos = container.getElementsByClassName('foo');
testing.expectEqual('foo1', foos.item(0).id);
testing.expectEqual('foo2', foos.item(1).id);
testing.expectEqual('foo3', foos.item(2).id);
testing.expectEqual(null, foos.item(3));
testing.expectEqual(null, foos.item(-1));
testing.expectEqual(null, foos.item(100));
}
</script>
<script id=namedItem>
{
const container = $('#container');
const foos = container.getElementsByClassName('foo');
testing.expectEqual('foo1', foos.namedItem('foo1').id);
testing.expectEqual('foo2', foos.namedItem('foo2').id);
testing.expectEqual('foo3', foos.namedItem('foo3').id);
testing.expectEqual(null, foos.namedItem('foo4'));
testing.expectEqual(null, foos.namedItem('container'));
testing.expectEqual('foo1', foos['foo1'].id);
testing.expectEqual('foo2', foos['foo2'].id);
}
</script>
<script id=iterator>
{
const container = $('#container');
const foos = container.getElementsByClassName('foo');
let acc = [];
for (let el of foos) {
acc.push(el.id);
}
testing.expectEqual(['foo1', 'foo2', 'foo3'], acc);
}
</script>
<script id=multipleClasses>
{
const multi = $('#multi');
const multiClass = multi.getElementsByClassName('multi');
testing.expectEqual(1, multiClass.length);
testing.expectEqual('multi1', multiClass[0].id);
const classClass = multi.getElementsByClassName('class');
testing.expectEqual(1, classClass.length);
testing.expectEqual('multi2', classClass[0].id);
const namesClass = multi.getElementsByClassName('names');
testing.expectEqual(1, namesClass.length);
testing.expectEqual('multi3', namesClass[0].id);
}
</script>
<script id=arrayIndexing>
{
const container = $('#container');
const foos = container.getElementsByClassName('foo');
testing.expectEqual('foo1', foos[0].id);
testing.expectEqual('foo2', foos[1].id);
testing.expectEqual('foo3', foos[2].id);
testing.expectEqual(undefined, foos[3]);
testing.expectEqual(undefined, foos[-1]);
testing.expectEqual(undefined, foos[100]);
}
</script>
<script id=emptyString>
{
const container = $('#container');
const empty = container.getElementsByClassName('');
testing.expectEqual(0, empty.length);
}
</script>

View File

@@ -1,186 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id="container">
<div id="div1">div1</div>
<p id="p1">p1</p>
<div id="div2">div2</div>
<span id="span1">span1</span>
<p id="p2">p2</p>
</div>
<div id="nested">
<div id="outer">
<div id="inner1">inner1</div>
<p id="innerP">innerP</p>
<div id="inner2">inner2</div>
</div>
</div>
<div id="empty"></div>
<div id="divMatch" class="test">
<span>span1</span>
<p>p1</p>
<span>span2</span>
</div>
<h1>H1</h1>
<h2>H2</h2>
<h3>H3</h3>
<section id="headings">
<h1>Section H1</h1>
<h2>Section H2</h2>
<h3>Section H3</h3>
<h4>Section H4</h4>
<h5>Section H5</h5>
<h6>Section H6</h6>
</section>
<script id=basic>
{
const container = $('#container');
testing.expectEqual(0, container.getElementsByTagName('a').length);
testing.expectEqual(0, container.getElementsByTagName('unknown').length);
const divs = container.getElementsByTagName('div');
testing.expectEqual(true, divs instanceof HTMLCollection);
testing.expectEqual(2, divs.length);
testing.expectEqual('div1', divs[0].id);
testing.expectEqual('div2', divs[1].id);
testing.expectEqual('div1', divs[0].id); // cache test
const ps = container.getElementsByTagName('p');
testing.expectEqual(2, ps.length);
testing.expectEqual('p1', ps[0].id);
testing.expectEqual('p2', ps[1].id);
const spans = container.getElementsByTagName('span');
testing.expectEqual(1, spans.length);
testing.expectEqual('span1', spans[0].id);
}
</script>
<script id=nestedSearch>
{
const nested = $('#nested');
const nestedDivs = nested.getElementsByTagName('div');
testing.expectEqual(3, nestedDivs.length);
testing.expectEqual('outer', nestedDivs[0].id);
testing.expectEqual('inner1', nestedDivs[1].id);
testing.expectEqual('inner2', nestedDivs[2].id);
const outer = $('#outer');
const outerDivs = outer.getElementsByTagName('div');
testing.expectEqual(2, outerDivs.length);
testing.expectEqual('inner1', outerDivs[0].id);
testing.expectEqual('inner2', outerDivs[1].id);
}
</script>
<script id=emptyResult>
{
const empty = $('#empty');
testing.expectEqual(0, empty.getElementsByTagName('div').length);
testing.expectEqual(0, empty.getElementsByTagName('p').length);
testing.expectEqual(0, empty.getElementsByTagName('span').length);
}
</script>
<script id=excludeSelf>
{
// Element itself should NOT be included in results, even if it matches
const divMatch = $('#divMatch');
const divsInDiv = divMatch.getElementsByTagName('div');
testing.expectEqual(0, divsInDiv.length); // divMatch itself is a div but should not be included
const spansInDiv = divMatch.getElementsByTagName('span');
testing.expectEqual(2, spansInDiv.length); // Only the child spans
}
</script>
<script id=caseInsensitive>
{
const container = $('#container');
testing.expectEqual(2, container.getElementsByTagName('DIV').length);
testing.expectEqual(2, container.getElementsByTagName('Div').length);
testing.expectEqual(2, container.getElementsByTagName('div').length);
testing.expectEqual(2, container.getElementsByTagName('P').length);
testing.expectEqual(1, container.getElementsByTagName('SPAN').length);
}
</script>
<script id=item>
{
const container = $('#container');
const divs = container.getElementsByTagName('div');
testing.expectEqual('div1', divs.item(0).id);
testing.expectEqual('div2', divs.item(1).id);
testing.expectEqual(null, divs.item(2));
testing.expectEqual(null, divs.item(-1));
testing.expectEqual(null, divs.item(100));
}
</script>
<script id=namedItem>
{
const container = $('#container');
const divs = container.getElementsByTagName('div');
testing.expectEqual('div1', divs.namedItem('div1').id);
testing.expectEqual('div2', divs.namedItem('div2').id);
testing.expectEqual(null, divs.namedItem('div3'));
testing.expectEqual(null, divs.namedItem('container'));
testing.expectEqual('div1', divs['div1'].id);
testing.expectEqual('div2', divs['div2'].id);
}
</script>
<script id=iterator>
{
const container = $('#container');
const ps = container.getElementsByTagName('p');
let acc = [];
for (let p of ps) {
acc.push(p.id);
}
testing.expectEqual(['p1', 'p2'], acc);
}
</script>
<script id=headings>
{
const headings = $('#headings');
testing.expectEqual(1, headings.getElementsByTagName('h1').length);
testing.expectEqual('Section H1', headings.getElementsByTagName('h1')[0].textContent);
testing.expectEqual(1, headings.getElementsByTagName('h2').length);
testing.expectEqual('Section H2', headings.getElementsByTagName('h2')[0].textContent);
testing.expectEqual(1, headings.getElementsByTagName('h3').length);
testing.expectEqual(1, headings.getElementsByTagName('h4').length);
testing.expectEqual(1, headings.getElementsByTagName('h5').length);
testing.expectEqual(1, headings.getElementsByTagName('h6').length);
// Case insensitive
testing.expectEqual(1, headings.getElementsByTagName('H1').length);
testing.expectEqual('Section H1', headings.getElementsByTagName('H1')[0].textContent);
}
</script>
<script id=arrayIndexing>
{
const container = $('#container');
const divs = container.getElementsByTagName('div');
testing.expectEqual('div1', divs[0].id);
testing.expectEqual('div2', divs[1].id);
testing.expectEqual(undefined, divs[2]);
testing.expectEqual(undefined, divs[-1]);
testing.expectEqual(undefined, divs[100]);
}
</script>

View File

@@ -1,13 +0,0 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<a id=a0></a>
<a href="../anchor1.html" id=a1></a>
<a href="/hello/world/anchor2.html" id=a2></a>
<a href="https://www.openmymind.net/Elixirs-With-Statement/" id=a3></a>
<script id=href>
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/html/anchor.html', $('#a0').href)
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/anchor1.html', $('#a1').href)
testing.expectEqual('http://127.0.0.1:9582/hello/world/anchor2.html', $('#a2').href)
testing.expectEqual('https://www.openmymind.net/Elixirs-With-Statement/', $('#a3').href)
</script>

View File

@@ -1,55 +0,0 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<!-- Button elements -->
<button id="button1">Click me</button>
<button id="button2" disabled>Disabled button</button>
<!-- Form association tests -->
<form id="form1">
<button id="button_in_form">In form</button>
</form>
<form id="form2"></form>
<button id="button_with_form_attr" form="form2">With form attr</button>
<button id="button_no_form">No form</button>
<form id="form3">
<button id="button_invalid_form_attr" form="nonexistent">Invalid form</button>
</form>
<script id="disabled_initial">
testing.expectEqual(false, $('#button1').disabled)
testing.expectEqual(true, $('#button2').disabled)
</script>
<script id="disabled_set">
$('#button1').disabled = true
testing.expectEqual(true, $('#button1').disabled)
$('#button2').disabled = false
testing.expectEqual(false, $('#button2').disabled)
</script>
<script id="form_ancestor">
const buttonInForm = $('#button_in_form')
testing.expectEqual('FORM', buttonInForm.form.tagName)
testing.expectEqual('form1', buttonInForm.form.id)
</script>
<script id="form_attribute">
const buttonWithFormAttr = $('#button_with_form_attr')
testing.expectEqual('FORM', buttonWithFormAttr.form.tagName)
testing.expectEqual('form2', buttonWithFormAttr.form.id)
</script>
<script id="form_null">
const buttonNoForm = $('#button_no_form')
testing.expectEqual(null, buttonNoForm.form)
</script>
<script id="form_invalid_attribute">
const buttonInvalidFormAttr = $('#button_invalid_form_attr')
testing.expectEqual(null, buttonInvalidFormAttr.form)
</script>

View File

@@ -1,246 +0,0 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<input id="text1" type="text" value="initial">
<input id="text2" type="text">
<input id="check1" type="checkbox" checked>
<input id="check2" type="checkbox">
<input id="radio1" type="radio" name="group1" checked>
<input id="radio2" type="radio" name="group1">
<input id="radio3" type="radio" name="group1">
<input id="radio4" type="radio" name="group2">
<input id="disabled1" type="text" disabled>
<input id="disabled2" type="checkbox">
<script id="type">
testing.expectEqual('text', $('#text1').type)
testing.expectEqual('text', $('#text2').type)
testing.expectEqual('checkbox', $('#check1').type)
testing.expectEqual('radio', $('#radio1').type)
</script>
<script id="type_case_insensitive">
{
const input = document.createElement('input')
input.setAttribute('type', 'TEXT')
testing.expectEqual('text', input.type)
input.setAttribute('type', 'ChEcKbOx')
testing.expectEqual('checkbox', input.type)
input.setAttribute('type', 'EMAIL')
testing.expectEqual('email', input.type)
}
</script>
<script id="type_attribute_name_case_insensitive">
{
const input = document.createElement('input')
input.setAttribute('TYPE', 'checkbox')
testing.expectEqual('checkbox', input.type)
testing.expectEqual('checkbox', input.getAttribute('type'))
input.setAttribute('TyPe', 'radio')
testing.expectEqual('radio', input.type)
testing.expectEqual('radio', input.getAttribute('type'))
}
</script>
<script id="type_invalid_defaults_to_text">
{
const input = document.createElement('input')
input.setAttribute('type', 'invalid')
testing.expectEqual('text', input.type)
input.setAttribute('type', 'notavalidtype')
testing.expectEqual('text', input.type)
input.setAttribute('type', 'x'.repeat(50))
testing.expectEqual('text', input.type)
}
</script>
<script id="type_set">
const input = document.createElement('input')
testing.expectEqual('text', input.type)
input.type = 'password'
testing.expectEqual('password', input.type)
input.type = 'EMAIL'
testing.expectEqual('email', input.type)
input.type = 'invalid'
testing.expectEqual('text', input.type)
input.type = 'ChEcKbOx'
testing.expectEqual('checkbox', input.type)
</script>
<script id="value_initial">
testing.expectEqual('initial', $('#text1').value)
testing.expectEqual('', $('#text2').value)
</script>
<script id="value_set">
$('#text1').value = 'changed'
testing.expectEqual('changed', $('#text1').value)
$('#text2').value = 'new value'
testing.expectEqual('new value', $('#text2').value)
</script>
<script id="checked_initial">
testing.expectEqual(true, $('#check1').checked)
testing.expectEqual(false, $('#check2').checked)
testing.expectEqual(true, $('#radio1').checked)
testing.expectEqual(false, $('#radio2').checked)
</script>
<script id="checked_set">
$('#check1').checked = false
testing.expectEqual(false, $('#check1').checked)
$('#check2').checked = true
testing.expectEqual(true, $('#check2').checked)
$('#radio2').checked = true
testing.expectEqual(true, $('#radio2').checked)
</script>
<script id="defaultValue">
testing.expectEqual('initial', $('#text1').defaultValue)
testing.expectEqual('', $('#text2').defaultValue)
$('#text1').value = 'changed'
testing.expectEqual('initial', $('#text1').defaultValue)
</script>
<script id="defaultChecked">
testing.expectEqual(true, $('#check1').defaultChecked)
testing.expectEqual(false, $('#check2').defaultChecked)
testing.expectEqual(true, $('#radio1').defaultChecked)
testing.expectEqual(false, $('#radio2').defaultChecked)
$('#check1').checked = false
testing.expectEqual(true, $('#check1').defaultChecked)
</script>
<script id="disabled_initial">
testing.expectEqual(true, $('#disabled1').disabled)
testing.expectEqual(false, $('#disabled2').disabled)
</script>
<script id="disabled_set">
$('#disabled1').disabled = false
testing.expectEqual(false, $('#disabled1').disabled)
$('#disabled2').disabled = true
testing.expectEqual(true, $('#disabled2').disabled)
</script>
<form id="form1">
<input id="input_in_form" type="text">
</form>
<form id="form2"></form>
<input id="input_with_form_attr" type="text" form="form2">
<input id="input_no_form" type="text">
<form id="form3">
<input id="input_invalid_form_attr" type="text" form="nonexistent">
</form>
<script id="form_ancestor">
const inputInForm = $('#input_in_form')
testing.expectEqual('FORM', inputInForm.form.tagName)
testing.expectEqual('form1', inputInForm.form.id)
</script>
<script id="form_attribute">
const inputWithFormAttr = $('#input_with_form_attr')
testing.expectEqual('FORM', inputWithFormAttr.form.tagName)
testing.expectEqual('form2', inputWithFormAttr.form.id)
</script>
<script id="form_null">
const inputNoForm = $('#input_no_form')
testing.expectEqual(null, inputNoForm.form)
</script>
<script id="form_invalid_attribute">
const inputInvalidFormAttr = $('#input_invalid_form_attr')
testing.expectEqual(null, inputInvalidFormAttr.form)
</script>
<script id="input_clone">
{
const input = document.createElement('input');
input.type = 'image';
const clone = input.cloneNode();
testing.expectEqual('image', clone.type);
}
</script>
<script id="type_reflects_to_attribute">
{
const input = document.createElement('input')
input.type = 'checkbox'
testing.expectEqual('checkbox', input.getAttribute('type'))
testing.expectTrue(input.outerHTML.includes('type="checkbox"'))
input.type = 'radio'
testing.expectEqual('radio', input.getAttribute('type'))
testing.expectTrue(input.outerHTML.includes('type="radio"'))
}
</script>
<script id="disabled_reflects_to_attribute">
{
const input = document.createElement('input')
testing.expectEqual(null, input.getAttribute('disabled'))
testing.expectFalse(input.outerHTML.includes('disabled'))
input.disabled = true
testing.expectEqual('', input.getAttribute('disabled'))
testing.expectTrue(input.outerHTML.includes('disabled'))
input.disabled = false
testing.expectEqual(null, input.getAttribute('disabled'))
testing.expectFalse(input.outerHTML.includes('disabled'))
}
</script>
<script id="value_does_not_reflect_to_attribute">
{
const input = document.createElement('input')
input.setAttribute('value', 'initial')
testing.expectEqual('initial', input.getAttribute('value'))
input.value = 'changed'
testing.expectEqual('changed', input.value)
testing.expectEqual('initial', input.getAttribute('value'))
testing.expectTrue(input.outerHTML.includes('value="initial"'))
testing.expectFalse(input.outerHTML.includes('value="changed"'))
}
</script>
<script id="checked_does_not_reflect_to_attribute">
{
const input = document.createElement('input')
input.type = 'checkbox'
input.setAttribute('checked', '')
testing.expectEqual('', input.getAttribute('checked'))
testing.expectTrue(input.outerHTML.includes('checked'))
input.checked = false
testing.expectEqual(false, input.checked)
testing.expectEqual('', input.getAttribute('checked'))
testing.expectTrue(input.outerHTML.includes('checked'))
}
</script>

View File

@@ -1,140 +0,0 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<input id="radio1" type="radio" name="group1" checked>
<input id="radio2" type="radio" name="group1">
<input id="radio3" type="radio" name="group1">
<input id="radio4" type="radio" name="group2">
<script id="radio_grouping_basic">
testing.expectEqual(true, $('#radio1').checked)
testing.expectEqual(false, $('#radio2').checked)
testing.expectEqual(false, $('#radio3').checked)
$('#radio2').checked = true
testing.expectEqual(false, $('#radio1').checked)
testing.expectEqual(true, $('#radio2').checked)
testing.expectEqual(false, $('#radio3').checked)
$('#radio3').checked = true
testing.expectEqual(false, $('#radio1').checked)
testing.expectEqual(false, $('#radio2').checked)
testing.expectEqual(true, $('#radio3').checked)
</script>
<script id="radio_grouping_different_groups">
$('#radio1').checked = true
$('#radio4').checked = true
testing.expectEqual(true, $('#radio1').checked)
testing.expectEqual(true, $('#radio4').checked)
$('#radio2').checked = true
testing.expectEqual(false, $('#radio1').checked)
testing.expectEqual(true, $('#radio2').checked)
testing.expectEqual(true, $('#radio4').checked)
</script>
<script id="radio_grouping_unchecking">
$('#radio1').checked = true
testing.expectEqual(true, $('#radio1').checked)
$('#radio1').checked = false
testing.expectEqual(false, $('#radio1').checked)
testing.expectEqual(false, $('#radio2').checked)
testing.expectEqual(false, $('#radio3').checked)
</script>
<form id="radio_form1">
<input id="radio_form1_a" type="radio" name="formgroup">
<input id="radio_form1_b" type="radio" name="formgroup">
</form>
<form id="radio_form2">
<input id="radio_form2_a" type="radio" name="formgroup">
<input id="radio_form2_b" type="radio" name="formgroup">
</form>
<input id="radio_no_form_a" type="radio" name="formgroup">
<input id="radio_no_form_b" type="radio" name="formgroup">
<script id="radio_grouping_same_name_different_forms">
$('#radio_form1_a').checked = true
$('#radio_form2_a').checked = true
$('#radio_no_form_a').checked = true
testing.expectEqual(true, $('#radio_form1_a').checked)
testing.expectEqual(true, $('#radio_form2_a').checked)
testing.expectEqual(true, $('#radio_no_form_a').checked)
$('#radio_form1_b').checked = true
testing.expectEqual(false, $('#radio_form1_a').checked)
testing.expectEqual(true, $('#radio_form1_b').checked)
testing.expectEqual(true, $('#radio_form2_a').checked)
testing.expectEqual(true, $('#radio_no_form_a').checked)
$('#radio_no_form_b').checked = true
testing.expectEqual(false, $('#radio_form1_a').checked)
testing.expectEqual(true, $('#radio_form1_b').checked)
testing.expectEqual(true, $('#radio_form2_a').checked)
testing.expectEqual(false, $('#radio_no_form_a').checked)
testing.expectEqual(true, $('#radio_no_form_b').checked)
</script>
<form id="radio_form3"></form>
<input id="radio_form_attr_a" type="radio" name="attrgroup" form="radio_form3">
<input id="radio_form_attr_b" type="radio" name="attrgroup" form="radio_form3">
<input id="radio_form_attr_c" type="radio" name="attrgroup">
<script id="radio_grouping_form_attribute">
$('#radio_form_attr_a').checked = true
$('#radio_form_attr_c').checked = true
testing.expectEqual(true, $('#radio_form_attr_a').checked)
testing.expectEqual(false, $('#radio_form_attr_b').checked)
testing.expectEqual(true, $('#radio_form_attr_c').checked)
$('#radio_form_attr_b').checked = true
testing.expectEqual(false, $('#radio_form_attr_a').checked)
testing.expectEqual(true, $('#radio_form_attr_b').checked)
testing.expectEqual(true, $('#radio_form_attr_c').checked)
</script>
<input id="radio_no_name_a" type="radio">
<input id="radio_no_name_b" type="radio">
<script id="radio_no_name_no_grouping">
$('#radio_no_name_a').checked = true
$('#radio_no_name_b').checked = true
testing.expectEqual(true, $('#radio_no_name_a').checked)
testing.expectEqual(true, $('#radio_no_name_b').checked)
</script>
<input id="radio_empty_name_a" type="radio" name="">
<input id="radio_empty_name_b" type="radio" name="">
<script id="radio_empty_name_no_grouping">
$('#radio_empty_name_a').checked = true
$('#radio_empty_name_b').checked = true
testing.expectEqual(true, $('#radio_empty_name_a').checked)
testing.expectEqual(true, $('#radio_empty_name_b').checked)
</script>
<input id="radio_case_a" type="radio" name="CaseSensitive">
<input id="radio_case_b" type="radio" name="casesensitive">
<input id="radio_case_c" type="radio" name="CaseSensitive">
<script id="radio_name_case_sensitive">
$('#radio_case_a').checked = true
$('#radio_case_b').checked = true
testing.expectEqual(true, $('#radio_case_a').checked)
testing.expectEqual(true, $('#radio_case_b').checked)
$('#radio_case_c').checked = true
testing.expectEqual(false, $('#radio_case_a').checked)
testing.expectEqual(true, $('#radio_case_b').checked)
testing.expectEqual(true, $('#radio_case_c').checked)
</script>

View File

@@ -1,67 +0,0 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<select id="select1">
<option id="opt1" value="val1">Text 1</option>
<option id="opt2" value="val2" selected>Text 2</option>
<option id="opt3">Text 3</option>
<option id="opt4" disabled>Text 4</option>
</select>
<script id="value_with_attribute">
testing.expectEqual('val1', $('#opt1').value)
testing.expectEqual('val2', $('#opt2').value)
</script>
<script id="value_without_attribute">
// When value attribute is not present, value should be text content
testing.expectEqual('Text 3', $('#opt3').value)
</script>
<script id="value_set">
$('#opt1').value = 'changed'
testing.expectEqual('changed', $('#opt1').value)
</script>
<script id="text">
testing.expectEqual('Text 1', $('#opt1').text)
testing.expectEqual('Text 2', $('#opt2').text)
testing.expectEqual('Text 3', $('#opt3').text)
</script>
<script id="selected">
testing.expectEqual(false, $('#opt1').selected)
testing.expectEqual(true, $('#opt2').selected)
testing.expectEqual(false, $('#opt3').selected)
</script>
<script id="selected_set">
$('#opt1').selected = true
testing.expectEqual(true, $('#opt1').selected)
$('#opt2').selected = false
testing.expectEqual(false, $('#opt2').selected)
</script>
<script id="defaultSelected">
testing.expectEqual(false, $('#opt1').defaultSelected)
testing.expectEqual(true, $('#opt2').defaultSelected)
testing.expectEqual(false, $('#opt3').defaultSelected)
// Setting selected shouldn't change defaultSelected
$('#opt1').selected = true
testing.expectEqual(false, $('#opt1').defaultSelected)
</script>
<script id="disabled">
testing.expectEqual(false, $('#opt1').disabled)
testing.expectEqual(true, $('#opt4').disabled)
</script>
<script id="disabled_set">
$('#opt1').disabled = true
testing.expectEqual(true, $('#opt1').disabled)
$('#opt4').disabled = false
testing.expectEqual(false, $('#opt4').disabled)
</script>

View File

@@ -1,43 +0,0 @@
<!DOCTYPE html>
<head></head>
<script src="../../../testing.js"></script>
<script id=append_before_src>
loaded1 = 0;
const script1 = document.createElement('script');
script1.src = "dynamic1.js";
document.getElementsByTagName('head')[0].appendChild(script1);
testing.eventually(() => {
testing.expectEqual(1, loaded1);
});
</script>
<script id=no_double_execute>
document.getElementsByTagName('head')[0].appendChild(script1);
testing.eventually(() => {
testing.expectEqual(1, loaded1);
});
</script>
<script id=append_before_src>
loaded2 = 0;
const script2a = document.createElement('script');
script2a.src = "dynamic2.js";
document.getElementsByTagName('head')[0].appendChild(script2a);
testing.eventually(() => {
testing.expectEqual(2, loaded2);
});
</script>
<script>
const script2b = document.createElement('script');
document.getElementsByTagName('head')[0].appendChild(script2b);
script2b.src = "dynamic2.js";
script2b.src = "dynamic2.js";
</script>
<script id=src_after_append>
testing.eventually(() => {
testing.expectEqual(2, loaded2);
});
</script>

View File

@@ -1 +0,0 @@
loaded1 += 1;

View File

@@ -1 +0,0 @@
loaded2 += 1;

View File

@@ -1,83 +0,0 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<select id="select1">
<option value="val1">Option 1</option>
<option value="val2" selected>Option 2</option>
<option value="val3">Option 3</option>
</select>
<select id="select2">
<option value="a">A</option>
<option value="b">B</option>
</select>
<select id="select3" disabled>
<option value="x">X</option>
</select>
<form id="form1">
<select id="select_in_form">
<option value="f1">Form option</option>
</select>
</form>
<form id="form2"></form>
<select id="select_with_form_attr" form="form2">
<option value="f2">Form attr option</option>
</select>
<select id="select_no_form">
<option value="nf">No form option</option>
</select>
<script id="value_initial">
// Should return value of selected option
testing.expectEqual('val2', $('#select1').value)
// If no option is explicitly selected, first option is implicitly selected
testing.expectEqual('a', $('#select2').value)
</script>
<script id="value_set">
$('#select1').value = 'val3'
testing.expectEqual('val3', $('#select1').value)
// The option should now be selected
const opt3 = $('#select1').querySelector('option[value="val3"]')
testing.expectEqual(true, opt3.selected)
// Other options should be unselected
const opt2 = $('#select1').querySelector('option[value="val2"]')
testing.expectEqual(false, opt2.selected)
</script>
<script id="disabled_initial">
testing.expectEqual(false, $('#select1').disabled)
testing.expectEqual(true, $('#select3').disabled)
</script>
<script id="disabled_set">
$('#select1').disabled = true
testing.expectEqual(true, $('#select1').disabled)
$('#select3').disabled = false
testing.expectEqual(false, $('#select3').disabled)
</script>
<script id="form_ancestor">
const selectInForm = $('#select_in_form')
testing.expectEqual('FORM', selectInForm.form.tagName)
testing.expectEqual('form1', selectInForm.form.id)
</script>
<script id="form_attribute">
const selectWithFormAttr = $('#select_with_form_attr')
testing.expectEqual('FORM', selectWithFormAttr.form.tagName)
testing.expectEqual('form2', selectWithFormAttr.form.id)
</script>
<script id="form_null">
const selectNoForm = $('#select_no_form')
testing.expectEqual(null, selectNoForm.form)
</script>

View File

@@ -1,78 +0,0 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<!-- TextArea elements -->
<textarea id="textarea1">initial text</textarea>
<textarea id="textarea2"></textarea>
<textarea id="textarea3" disabled>disabled text</textarea>
<!-- Form association tests -->
<form id="form1">
<textarea id="textarea_in_form">in form</textarea>
</form>
<form id="form2"></form>
<textarea id="textarea_with_form_attr" form="form2">with form attr</textarea>
<textarea id="textarea_no_form">no form</textarea>
<form id="form3">
<textarea id="textarea_invalid_form_attr" form="nonexistent">invalid form</textarea>
</form>
<script id="value_initial">
testing.expectEqual('initial text', $('#textarea1').value)
testing.expectEqual('', $('#textarea2').value)
</script>
<script id="value_set">
$('#textarea1').value = 'changed'
testing.expectEqual('changed', $('#textarea1').value)
$('#textarea2').value = 'new value'
testing.expectEqual('new value', $('#textarea2').value)
</script>
<script id="defaultValue">
testing.expectEqual('initial text', $('#textarea1').defaultValue)
testing.expectEqual('', $('#textarea2').defaultValue)
// Setting value shouldn't change defaultValue
$('#textarea1').value = 'changed'
testing.expectEqual('initial text', $('#textarea1').defaultValue)
</script>
<script id="disabled_initial">
testing.expectEqual(false, $('#textarea1').disabled)
testing.expectEqual(true, $('#textarea3').disabled)
</script>
<script id="disabled_set">
$('#textarea1').disabled = true
testing.expectEqual(true, $('#textarea1').disabled)
$('#textarea3').disabled = false
testing.expectEqual(false, $('#textarea3').disabled)
</script>
<script id="form_ancestor">
const textareaInForm = $('#textarea_in_form')
testing.expectEqual('FORM', textareaInForm.form.tagName)
testing.expectEqual('form1', textareaInForm.form.id)
</script>
<script id="form_attribute">
const textareaWithFormAttr = $('#textarea_with_form_attr')
testing.expectEqual('FORM', textareaWithFormAttr.form.tagName)
testing.expectEqual('form2', textareaWithFormAttr.form.id)
</script>
<script id="form_null">
const textareaNoForm = $('#textarea_no_form')
testing.expectEqual(null, textareaNoForm.form)
</script>
<script id="form_invalid_attribute">
const textareaInvalidFormAttr = $('#textarea_invalid_form_attr')
testing.expectEqual(null, textareaInvalidFormAttr.form)
</script>

View File

@@ -1,131 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id=d1>hello <em>world</em></div>
<script id=innerHTML>
const d1 = $('#d1');
testing.expectEqual('hello <em>world</em>', d1.innerHTML);
d1.innerHTML = 'only text';
testing.expectEqual('only text', d1.innerHTML);
d1.innerHTML = 'hello <div>world</div><b>!!</b>';
testing.expectEqual('hello <div>world</div><b>!!</b>', d1.innerHTML);
for (let child of d1.childNodes) {
testing.expectEqual(d1, child.parentNode);
}
// doesn't run JavaScript, important!
let inner_loaded = false;
d1.innerHTML = '<script src=inner.js>';
testing.expectEqual('<script src="inner.js"><\/script>', d1.innerHTML);
testing.expectEqual(false, inner_loaded);
</script>
<script id=ids>
d1.innerHTML = '<a id=link>home</a>';
testing.expectEqual('home', $('#link').innerText);
d1.innerHTML = '';
testing.expectEqual(null, $('#link'));
d1.innerHTML = '<div><p><a id=link>hi</a></p></div>';
testing.expectEqual('hi', $('#link').innerText);
d1.innerHTML = '';
testing.todo(null, $('#link'));
</script>
<script id=attributeSerialization>
d1.innerHTML = '<div class="test">text</div>';
testing.expectEqual('<div class="test">text</div>', d1.innerHTML);
d1.innerHTML = '<div id=simple>text</div>';
testing.expectEqual('<div id="simple">text</div>', d1.innerHTML);
d1.innerHTML = '<a href="/?foo=1&bar=2">link</a>';
testing.expectEqual('<a href="/?foo=1&amp;bar=2">link</a>', d1.innerHTML);
d1.innerHTML = '<div title="She said &quot;hello&quot;">text</div>';
testing.expectEqual('<div title="She said &quot;hello&quot;">text</div>', d1.innerHTML);
d1.innerHTML = '<div data-value="<tag>">text</div>';
testing.expectEqual('<div data-value="&lt;tag&gt;">text</div>', d1.innerHTML);
const div1 = document.createElement('div');
div1.setAttribute('title', 'line1\nline2');
d1.innerHTML = '';
d1.appendChild(div1);
testing.expectEqual('<div title="line1\nline2"></div>', d1.innerHTML);
const div2 = document.createElement('div');
div2.setAttribute('title', 'line1\rline2');
d1.innerHTML = '';
d1.appendChild(div2);
testing.expectEqual('<div title="line1\rline2"></div>', d1.innerHTML);
const div3 = document.createElement('div');
div3.setAttribute('title', 'col1\tcol2');
d1.innerHTML = '';
d1.appendChild(div3);
testing.expectEqual('<div title="col1\tcol2"></div>', d1.innerHTML);
d1.innerHTML = '<div data-test="&quot;&amp;&lt;&gt;">text</div>';
testing.expectEqual('<div data-test="&quot;&amp;&lt;&gt;">text</div>', d1.innerHTML);
d1.innerHTML = '<input type="text" value="">';
testing.expectEqual('<input type="text" value="">', d1.innerHTML);
d1.innerHTML = '<div class="foo bar">text</div>';
testing.expectEqual('<div class="foo bar">text</div>', d1.innerHTML);
d1.innerHTML = '<div data-expr="x=5">text</div>';
testing.expectEqual('<div data-expr="x=5">text</div>', d1.innerHTML);
const div4 = document.createElement('div');
div4.setAttribute('title', "it's working");
d1.innerHTML = '';
d1.appendChild(div4);
testing.expectEqual('<div title="it\'s working"></div>', d1.innerHTML);
const div5 = document.createElement('div');
div5.setAttribute('data-code', '`template`');
d1.innerHTML = '';
d1.appendChild(div5);
testing.expectEqual('<div data-code="`template`"></div>', d1.innerHTML);
d1.innerHTML = '<a href="/search?q=test&lang=en" title="Search: &quot;test&quot;">Search</a>';
testing.expectEqual('<a href="/search?q=test&amp;lang=en" title="Search: &quot;test&quot;">Search</a>', d1.innerHTML);
d1.innerHTML = '<div data-start="&start">text</div>';
testing.expectEqual('<div data-start="&amp;start">text</div>', d1.innerHTML);
d1.innerHTML = '<div data-end="end&">text</div>';
testing.expectEqual('<div data-end="end&amp;">text</div>', d1.innerHTML);
d1.innerHTML = '<div data-middle="mid&dle">text</div>';
testing.expectEqual('<div data-middle="mid&amp;dle">text</div>', d1.innerHTML);
</script>
<script id=voidElements>
d1.innerHTML = '<br>';
testing.expectEqual('<br>', d1.innerHTML);
d1.innerHTML = '<hr>';
testing.expectEqual('<hr>', d1.innerHTML);
d1.innerHTML = '<img src="test.png" alt="test">';
testing.expectEqual('<img src="test.png" alt="test">', d1.innerHTML);
d1.innerHTML = '<input type="text" name="field">';
testing.expectEqual('<input type="text" name="field">', d1.innerHTML);
d1.innerHTML = '<link rel="stylesheet" href="style.css">';
testing.expectEqual('<link rel="stylesheet" href="style.css">', d1.innerHTML);
d1.innerHTML = '<meta charset="utf-8">';
testing.expectEqual('<meta charset="utf-8">', d1.innerHTML);
d1.innerHTML = '<div><br><hr><img src="x.png"></div>';
testing.expectEqual('<div><br><hr><img src="x.png"></div>', d1.innerHTML);
</script>

View File

@@ -1 +0,0 @@
inner_loaded = true;

View File

@@ -1,65 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id=p1>
<div id=c2>
<div id=c3></div>
</div>
</div>
<div id=other></div>
<!-- <script id=querySelector">
const p1 = $('#p1');
testing.expectError("Syntax Error", () => p1.querySelector(''));
testing.withError((err) => {
testing.expectEqual(12, err.code);
testing.expectEqual("SyntaxError", err.name);
testing.expectEqual("Syntax Error", err.message);
}, () => p1.querySelector(''));
testing.expectEqual($('#c2'), p1.querySelector('#c2'));
testing.expectEqual($('#c3'), p1.querySelector('#c3'));
testing.expectEqual(null, p1.querySelector('#nope'));
testing.expectEqual(null, p1.querySelector('#other'));
testing.expectEqual($('#c2'), p1.querySelector('*'));
testing.expectEqual($('#c3'), p1.querySelector('*#c3'));
</script> -->
<div id="desc-container">
<p class="text">Direct child paragraph</p>
<div class="nested">
<p class="text">Nested paragraph</p>
<span class="wrapper">
<p class="deep">Deeply nested paragraph</p>
</span>
</div>
<article>
<p class="text">Article paragraph</p>
</article>
</div>
<div id="outer-div">
<div class="level1">
<div class="level2">
<span id="deep-span">Deep span</span>
</div>
</div>
</div>
<script id=descendantSelectors>
{
const container = $('#desc-container');
// testing.expectEqual('Nested paragraph', container.querySelector('div p').textContent);
// testing.expectEqual('Nested paragraph', container.querySelector('div.nested p').textContent);
testing.expectEqual('Deeply nested paragraph', container.querySelector('div span p').textContent);
// testing.expectEqual('Nested paragraph', container.querySelector('.nested .text').textContent);
// testing.expectEqual(null, container.querySelector('article div p'));
// const outerDiv = $('#outer-div');
// testing.expectEqual('deep-span', outerDiv.querySelector('div div span').id);
// testing.expectEqual('deep-span', outerDiv.querySelector('.level1 span').id);
// testing.expectEqual('deep-span', outerDiv.querySelector('.level1 .level2 span').id);
}
</script>

View File

@@ -1,188 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id=root>
<div class="item">Item 1</div>
<span class="item">Item 2</span>
<div class="item special">Item 3</div>
<div id=nested>
<div class="item">Item 4</div>
<span class="special">Item 5</span>
</div>
</div>
<div id=outside class="item">Outside</div>
<script>
function assertList(expected, result) {
testing.expectEqual(expected.length, result.length);
testing.expectEqual(expected, Array.from(result).map((e) => e.textContent));
testing.expectEqual(expected, Array.from(result.values()).map((e) => e.textContent));
testing.expectEqual(expected.map((e, i) => i), Array.from(result.keys()));
}
</script>
<script id=errors>
{
const root = $('#root');
testing.expectError("Syntax Error", () => root.querySelectorAll(''));
testing.withError((err) => {
testing.expectEqual(12, err.code);
testing.expectEqual("SyntaxError", err.name);
testing.expectEqual("Syntax Error", err.message);
}, () => root.querySelectorAll(''));
}
</script>
<script id=byId>
{
const root = $('#root');
const nested = root.querySelectorAll('#nested');
testing.expectEqual(true, nested instanceof NodeList);
testing.expectEqual(1, nested.length);
testing.expectEqual('nested', nested[0].getAttribute('id'));
assertList([], root.querySelectorAll('#outside'));
assertList([], root.querySelectorAll('#nope'));
}
</script>
<script id=byClass>
{
const root = $('#root');
assertList(['Item 1', 'Item 2', 'Item 3', 'Item 4'], root.querySelectorAll('.item'));
assertList(['Item 3', 'Item 5'], root.querySelectorAll('.special'));
assertList([], root.querySelectorAll('.nope'));
}
</script>
<script id=byTag>
{
const root = $('#root');
const divs = root.querySelectorAll('div');
testing.expectEqual(4, divs.length);
assertList(['Item 2', 'Item 5'], root.querySelectorAll('span'));
assertList([], root.querySelectorAll('article'));
}
</script>
<script id=compound>
{
const root = $('#root');
const divItems = root.querySelectorAll('div.item');
testing.expectEqual(3, divItems.length);
assertList(['Item 2'], root.querySelectorAll('span.item'));
assertList(['Item 3'], root.querySelectorAll('div.item.special'));
assertList(['Item 3'], root.querySelectorAll('.item.special'));
assertList(['Item 3'], root.querySelectorAll('.special.item'));
}
</script>
<script id=universal>
{
const root = $('#root');
const all = root.querySelectorAll('*');
testing.expectEqual(true, all.length >= 6);
testing.expectEqual('Item 1', all[0].textContent);
const items = root.querySelectorAll('*.item');
testing.expectEqual(4, items.length);
assertList(['Item 3', 'Item 5'], root.querySelectorAll('*.special'));
}
</script>
<script id=nested>
{
const nested = $('#nested');
assertList(['Item 4'], nested.querySelectorAll('.item'));
assertList(['Item 5'], nested.querySelectorAll('.special'));
assertList(['Item 4'], nested.querySelectorAll('div'));
assertList(['Item 5'], nested.querySelectorAll('span'));
}
</script>
<script id=iterators>
{
const root = $('#root');
const list = root.querySelectorAll('.special');
const entries = Array.from(list.entries());
testing.expectEqual(2, entries.length);
testing.expectEqual(0, entries[0][0]);
testing.expectEqual('Item 3', entries[0][1].textContent);
testing.expectEqual(1, entries[1][0]);
testing.expectEqual('Item 5', entries[1][1].textContent);
const keys = Array.from(list.keys());
testing.expectEqual(2, keys.length);
testing.expectEqual(0, keys[0]);
testing.expectEqual(1, keys[1]);
const values = Array.from(list.values());
testing.expectEqual(['Item 3', 'Item 5'], values.map((e) => e.textContent));
const defaultIter = Array.from(list);
testing.expectEqual(['Item 3', 'Item 5'], defaultIter.map((e) => e.textContent));
}
</script>
<div id="desc-root">
<p class="text">Direct child paragraph</p>
<div class="nested">
<p class="text">Nested paragraph</p>
<span class="wrapper">
<p class="deep">Deeply nested paragraph</p>
</span>
</div>
<article>
<p class="text">Article paragraph</p>
<div class="inner">
<span>Article span</span>
</div>
</article>
</div>
<div id="multi-level">
<div class="level1">
<div class="level2">
<span class="target">Target 1</span>
</div>
</div>
<div class="level1">
<span class="target">Target 2</span>
</div>
</div>
<script id=descendantSelectors>
{
const root = $('#desc-root');
assertList(['Direct child paragraph', 'Nested paragraph', 'Deeply nested paragraph', 'Article paragraph'], root.querySelectorAll('div p'));
assertList(['Direct child paragraph', 'Nested paragraph', 'Article paragraph'], root.querySelectorAll('div .text'));
assertList(['Nested paragraph', 'Deeply nested paragraph'], root.querySelectorAll('.nested p'));
assertList(['Deeply nested paragraph'], root.querySelectorAll('div span p'));
assertList(['Nested paragraph'], root.querySelectorAll('div.nested p.text'));
assertList([], root.querySelectorAll('article div p'));
assertList(['Article span'], root.querySelectorAll('article span'));
assertList(['Article span'], root.querySelectorAll('article div span'));
}
</script>
<script id=multiLevelDescendant>
{
const root = $('#multi-level');
assertList(['Target 1', 'Target 2'], root.querySelectorAll('div span'));
assertList(['Target 1', 'Target 2'], root.querySelectorAll('div div span'));
assertList(['Target 1'], root.querySelectorAll('.level1 .level2 .target'));
assertList(['Target 1', 'Target 2'], root.querySelectorAll('.level1 .target'));
}
</script>
<script id=descendantWithWhitespace>
{
const root = $('#desc-root');
assertList(['Direct child paragraph', 'Nested paragraph', 'Deeply nested paragraph', 'Article paragraph'], root.querySelectorAll(' div p '));
assertList(['Nested paragraph'], root.querySelectorAll(' div.nested p.text '));
}
</script>

View File

@@ -1,26 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id=d1>
<div id=d2>
<div id=d3>
<div id=d4>
<div id=d5>
</div>
</div>
</div>
</div>
</div>
<script id=remove>
testing.expectEqual(1, $('#d4').childElementCount);
$('#d5').remove();
testing.expectEqual(null, document.getElementById('#d5'));
testing.expectEqual(0, $('#d4').childElementCount);
testing.expectEqual(1, $('#d1').childElementCount);
$('#d2').remove();
testing.expectEqual(null, document.getElementById('#d2'));
testing.expectEqual(null, document.getElementById('#d3'));
testing.expectEqual(null, document.getElementById('#d4'));
testing.expectEqual(0, $('#d1').childElementCount);
</script>

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