mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 07:03:29 +00:00
Compare commits
229 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ec22ca6b8 | ||
|
|
d8fe029f80 | ||
|
|
c9cfb1ecba | ||
|
|
94e47f4f35 | ||
|
|
12111d4cdf | ||
|
|
00e8f13f62 | ||
|
|
e1e501e14a | ||
|
|
32015eae3c | ||
|
|
ab31cc0a18 | ||
|
|
1924f136c6 | ||
|
|
0481676be5 | ||
|
|
0da1955b52 | ||
|
|
522b293149 | ||
|
|
0a50b586fd | ||
|
|
11a63b776e | ||
|
|
78467ff209 | ||
|
|
6e5f67cd66 | ||
|
|
17a86cc1a6 | ||
|
|
f6e080bdfd | ||
|
|
3744dc1e58 | ||
|
|
9cdf1f5762 | ||
|
|
25ee34e65d | ||
|
|
720d3f4df9 | ||
|
|
dcca5e60e3 | ||
|
|
f2a406d224 | ||
|
|
68c8372493 | ||
|
|
ef364f83c8 | ||
|
|
33c92776f0 | ||
|
|
f5a2c8d303 | ||
|
|
c555c325e9 | ||
|
|
9310b91ad5 | ||
|
|
a708bc7d0f | ||
|
|
4ba4ce0f7c | ||
|
|
49f20adbab | ||
|
|
6724004a49 | ||
|
|
33ec300947 | ||
|
|
0c2f0b78aa | ||
|
|
152a4e5e7f | ||
|
|
e6f4b9cc66 | ||
|
|
67c8647e25 | ||
|
|
9fe56c5889 | ||
|
|
77bf332f13 | ||
|
|
b4f445183c | ||
|
|
38d48d7515 | ||
|
|
c5776f0ae6 | ||
|
|
7a6d929f08 | ||
|
|
11f7fc4550 | ||
|
|
d50f761c8f | ||
|
|
23dafb0f12 | ||
|
|
9ac46ea0cb | ||
|
|
00d75584db | ||
|
|
c2e64c131a | ||
|
|
840aea9013 | ||
|
|
bf522937e1 | ||
|
|
7d91f7992c | ||
|
|
b2df0c1541 | ||
|
|
d823eebce5 | ||
|
|
55b80ecd15 | ||
|
|
14e1c44eb0 | ||
|
|
eef2fa94d0 | ||
|
|
49ee5e4e68 | ||
|
|
b7f589ee1a | ||
|
|
e18d04a799 | ||
|
|
c1b73dfdc2 | ||
|
|
63e3f8d48a | ||
|
|
194328f2de | ||
|
|
0636240a58 | ||
|
|
7ec5a8d15b | ||
|
|
ead08557bf | ||
|
|
6d808d89b0 | ||
|
|
6b42b5abdd | ||
|
|
e12d6e85f0 | ||
|
|
7da440e9d3 | ||
|
|
d93a065db9 | ||
|
|
8d6ee42096 | ||
|
|
df6a905683 | ||
|
|
88c9875664 | ||
|
|
5fbbf1b59f | ||
|
|
f040f422e4 | ||
|
|
986e69f45d | ||
|
|
deed0546cc | ||
|
|
2a3a243d1c | ||
|
|
eba1a715b1 | ||
|
|
973ebcb7b5 | ||
|
|
e1aec8bc07 | ||
|
|
806b6c0c1e | ||
|
|
c6754d6a6e | ||
|
|
1be9470942 | ||
|
|
edd0c7d0a3 | ||
|
|
d0c741f3bb | ||
|
|
a9842fd790 | ||
|
|
f7040153cd | ||
|
|
e42b03acd8 | ||
|
|
28a87c2a47 | ||
|
|
e2cd983851 | ||
|
|
df82d25e91 | ||
|
|
bcf4083f9c | ||
|
|
4444d8a008 | ||
|
|
d193b73ee3 | ||
|
|
89bfc8ccdc | ||
|
|
7c06067991 | ||
|
|
8b47d72079 | ||
|
|
a2a0db7bc4 | ||
|
|
5f6e5d57c0 | ||
|
|
61357ee7e0 | ||
|
|
88106f8449 | ||
|
|
6f2f0af0ef | ||
|
|
eb829f4d36 | ||
|
|
d155421a40 | ||
|
|
9f2bad7498 | ||
|
|
3c5d601622 | ||
|
|
2a94e5a69e | ||
|
|
8e96ee337d | ||
|
|
304a28a79d | ||
|
|
a3e91debea | ||
|
|
545bcc403a | ||
|
|
69b5a3db15 | ||
|
|
53a5326248 | ||
|
|
3834ebcfa4 | ||
|
|
9363acf4ec | ||
|
|
dad51a4179 | ||
|
|
59b2954ff4 | ||
|
|
5e9d31b053 | ||
|
|
76c88d049f | ||
|
|
f0773a3ca2 | ||
|
|
f9cff763d8 | ||
|
|
8d606d5dc5 | ||
|
|
7347e1d414 | ||
|
|
b65e0e8d77 | ||
|
|
4da25fafd4 | ||
|
|
d8f21e3c67 | ||
|
|
fe8b6e3060 | ||
|
|
8b03c0c651 | ||
|
|
ffbcfc18f1 | ||
|
|
e3f487a7f1 | ||
|
|
309b6370f7 | ||
|
|
6a560fd20c | ||
|
|
c8abbf411b | ||
|
|
c2f17cb216 | ||
|
|
25332fd095 | ||
|
|
e2a8a74906 | ||
|
|
cca6e363c7 | ||
|
|
cb2b488d27 | ||
|
|
a9e2569a1b | ||
|
|
44271cac1a | ||
|
|
762dfe8f31 | ||
|
|
37350b0701 | ||
|
|
e3f7504572 | ||
|
|
deb8490991 | ||
|
|
82c5019a44 | ||
|
|
55c747ad45 | ||
|
|
d080dde361 | ||
|
|
32349e472c | ||
|
|
49e3d569de | ||
|
|
d58045c330 | ||
|
|
c80ef7ca96 | ||
|
|
9db39e4165 | ||
|
|
1e263cfc1b | ||
|
|
5c804f2c3d | ||
|
|
29ce31f2fd | ||
|
|
6e8398be96 | ||
|
|
0af69fee6d | ||
|
|
20f25fc352 | ||
|
|
a2eee9a278 | ||
|
|
b59618120f | ||
|
|
ff0b7ed6bf | ||
|
|
18d14f8c0c | ||
|
|
22459edccc | ||
|
|
52d3f3e966 | ||
|
|
17b20e1ad0 | ||
|
|
6b621fe5ab | ||
|
|
8eb4de9ccb | ||
|
|
4d5f6d42fa | ||
|
|
0fa49b99bf | ||
|
|
4c50b2af1a | ||
|
|
4e61a50946 | ||
|
|
2c7650cdb1 | ||
|
|
8a91840783 | ||
|
|
dcc7e51556 | ||
|
|
565d612abb | ||
|
|
e7738744cb | ||
|
|
de9d253dc9 | ||
|
|
2671cda98f | ||
|
|
bd899111d5 | ||
|
|
db5d933285 | ||
|
|
9c997ec86d | ||
|
|
75e80a47e6 | ||
|
|
d0dbbacd69 | ||
|
|
a2e747002b | ||
|
|
5e8ec4532d | ||
|
|
d64fffc5b3 | ||
|
|
4629e8a9eb | ||
|
|
7839f466ea | ||
|
|
954a693586 | ||
|
|
b59fd9b1fb | ||
|
|
a131e96ed5 | ||
|
|
d9c76aa13e | ||
|
|
6cf805360d | ||
|
|
97c8053010 | ||
|
|
621ffc5db7 | ||
|
|
a7efadabf5 | ||
|
|
a81e10f093 | ||
|
|
886c9daa47 | ||
|
|
500da5bfd8 | ||
|
|
fec212ab94 | ||
|
|
9221c810a6 | ||
|
|
a1af89b6a0 | ||
|
|
b8bf09c8e5 | ||
|
|
026a6c0caf | ||
|
|
da763bf17d | ||
|
|
6777ab9f3d | ||
|
|
45172461c7 | ||
|
|
b4da2abff2 | ||
|
|
63e19c7704 | ||
|
|
399c7def51 | ||
|
|
25bc2d5e75 | ||
|
|
1c77d998c6 | ||
|
|
810bd11a5b | ||
|
|
08e2365d75 | ||
|
|
c0e2377e16 | ||
|
|
f7c0bcceae | ||
|
|
37f4a9c72c | ||
|
|
64ce07340b | ||
|
|
5a70db1322 | ||
|
|
d4104883ef | ||
|
|
4f51f28734 | ||
|
|
65e8b56db4 | ||
|
|
5439a37d25 | ||
|
|
e222d72b46 |
28
.github/actions/install/action.yml
vendored
Normal file
28
.github/actions/install/action.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: "Browsercore install"
|
||||
description: "Install deps for the project browsercore"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
|
||||
steps:
|
||||
- name: install v8
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p vendor/zig-js-runtime/vendor/v8/${{env.ARCH}}/debug
|
||||
ln -s /usr/local/lib/libc_v8.a vendor/zig-js-runtime/vendor/v8/${{env.ARCH}}/debug/libc_v8.a
|
||||
|
||||
mkdir -p vendor/zig-js-runtime/vendor/v8/${{env.ARCH}}/release
|
||||
ln -s /usr/local/lib/libc_v8.a vendor/zig-js-runtime/vendor/v8/${{env.ARCH}}/release/libc_v8.a
|
||||
|
||||
- name: libiconv
|
||||
shell: bash
|
||||
run: |
|
||||
ln -s /usr/local/lib/libiconv vendor/libiconv
|
||||
|
||||
- name: build mimalloc
|
||||
shell: bash
|
||||
run: make install-mimalloc
|
||||
|
||||
- name: build netsurf
|
||||
shell: bash
|
||||
run: make install-netsurf
|
||||
60
.github/workflows/build-deps.yml
vendored
60
.github/workflows/build-deps.yml
vendored
@@ -1,60 +0,0 @@
|
||||
name: build-deps
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
paths:
|
||||
- "vendor/lexbor-src"
|
||||
- "vendor/netsurf/**"
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: lightpanda-io/browsercore-deps
|
||||
ZIG_DOCKER_VERSION: 0.12.0-dev.1773-8a8fd47d2
|
||||
|
||||
jobs:
|
||||
build-deps:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
build_arch: amd64
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GH_CI_PAT }}
|
||||
submodules: true
|
||||
|
||||
- name: Docker connect
|
||||
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
- name: Docker build
|
||||
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.deps
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{env.ZIG_DOCKER_VERSION}}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
ZIG_DOCKER_VERSION=${{ env.ZIG_DOCKER_VERSION }}
|
||||
platforms: ${{matrix.os}}/${{matrix.build_arch}}
|
||||
38
.github/workflows/wpt.yml
vendored
38
.github/workflows/wpt.yml
vendored
@@ -12,10 +12,12 @@ on:
|
||||
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.
|
||||
@@ -26,10 +28,13 @@ on:
|
||||
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:
|
||||
|
||||
@@ -42,36 +47,25 @@ jobs:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/lightpanda-io/zig-browsercore:0.12.0-dev.1773-8a8fd47d2
|
||||
image: ghcr.io/lightpanda-io/zig-browsercore:0.12.1
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# docker blocks io_uring syscalls by default now.
|
||||
# see https://github.com/tigerbeetle/tigerbeetle/pull/1995
|
||||
# see https://github.com/moby/moby/pull/46762
|
||||
options: "--security-opt seccomp=unconfined"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GH_CI_PAT }}
|
||||
# fetch submodules recusively, to get jsruntime-lib submodules also.
|
||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
||||
submodules: recursive
|
||||
|
||||
- name: install v8
|
||||
run: |
|
||||
mkdir -p vendor/jsruntime-lib/vendor/v8/${{env.ARCH}}/debug
|
||||
ln -s /usr/local/lib/libc_v8.a vendor/jsruntime-lib/vendor/v8/${{env.ARCH}}/debug/libc_v8.a
|
||||
|
||||
mkdir -p vendor/jsruntime-lib/vendor/v8/${{env.ARCH}}/release
|
||||
ln -s /usr/local/lib/libc_v8.a vendor/jsruntime-lib/vendor/v8/${{env.ARCH}}/release/libc_v8.a
|
||||
|
||||
- name: install deps
|
||||
run: |
|
||||
ln -s /usr/local/lib/lexbor vendor/lexbor
|
||||
|
||||
ln -s /usr/local/lib/libiconv vendor/libiconv
|
||||
|
||||
ln -s /usr/local/lib/netsurf/build vendor/netsurf/build
|
||||
ln -s /usr/local/lib/netsurf/lib vendor/netsurf/lib
|
||||
ln -s /usr/local/lib/netsurf/include vendor/netsurf/include
|
||||
- uses: ./.github/actions/install
|
||||
|
||||
- run: zig build wpt -Dengine=v8 -- --safe --summary
|
||||
|
||||
@@ -88,7 +82,7 @@ jobs:
|
||||
echo "${{github.sha}}" > commit.txt
|
||||
|
||||
- name: upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: wpt-results
|
||||
path: |
|
||||
@@ -112,7 +106,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: download artifact
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: wpt-results
|
||||
|
||||
|
||||
6
.github/workflows/zig-fmt.yml
vendored
6
.github/workflows/zig-fmt.yml
vendored
@@ -11,6 +11,8 @@ on:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
paths:
|
||||
- ".github/**"
|
||||
- "build.zig"
|
||||
- "src/**/*.zig"
|
||||
- "src/*.zig"
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
@@ -25,7 +27,7 @@ jobs:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/lightpanda-io/zig:0.12.0-dev.1773-8a8fd47d2
|
||||
image: ghcr.io/lightpanda-io/zig:0.12.1
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -33,7 +35,7 @@ jobs:
|
||||
zig_fmt_errs: ${{ steps.fmt.outputs.zig_fmt_errs }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
146
.github/workflows/zig-test.yml
vendored
146
.github/workflows/zig-test.yml
vendored
@@ -2,15 +2,21 @@ name: zig-test
|
||||
|
||||
env:
|
||||
ARCH: x86_64-linux
|
||||
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 }}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "build.zig"
|
||||
- "src/**/*.zig"
|
||||
- "src/*.zig"
|
||||
- "vendor/jsruntime-lib"
|
||||
- "vendor/zig-js-runtime"
|
||||
- ".github/**"
|
||||
pull_request:
|
||||
|
||||
# By default GH trigger on types opened, synchronize and reopened.
|
||||
@@ -21,13 +27,68 @@ on:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
paths:
|
||||
- ".github/**"
|
||||
- "build.zig"
|
||||
- "src/**/*.zig"
|
||||
- "src/*.zig"
|
||||
- "vendor/**"
|
||||
- ".github/**"
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
zig-build-dev:
|
||||
name: zig build dev
|
||||
|
||||
# Don't run the CI with draft PR.
|
||||
if: github.event.pull_request.draft == false
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/lightpanda-io/zig-browsercore:0.12.1
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
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
|
||||
|
||||
- name: zig build debug
|
||||
run: zig build -Dengine=v8
|
||||
|
||||
zig-build-release:
|
||||
name: zig build release
|
||||
|
||||
# Don't run the CI on PR
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/lightpanda-io/zig-browsercore:0.12.1
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
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
|
||||
|
||||
- name: zig build release
|
||||
run: zig build -Doptimize=ReleaseSafe -Dengine=v8
|
||||
|
||||
zig-test:
|
||||
name: zig test
|
||||
|
||||
@@ -36,42 +97,61 @@ jobs:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/lightpanda-io/zig-browsercore:0.12.0-dev.1773-8a8fd47d2
|
||||
image: ghcr.io/lightpanda-io/zig-browsercore:0.12.1
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# docker blocks io_uring syscalls by default now.
|
||||
# see https://github.com/tigerbeetle/tigerbeetle/pull/1995
|
||||
# see https://github.com/moby/moby/pull/46762
|
||||
options: "--security-opt seccomp=unconfined"
|
||||
|
||||
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
|
||||
|
||||
- name: zig build test
|
||||
run: zig build test -Dengine=v8 -- --json > bench.json
|
||||
|
||||
- name: write commit
|
||||
run: |
|
||||
echo "${{github.sha}}" > commit.txt
|
||||
|
||||
- name: upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bench-results
|
||||
path: |
|
||||
bench.json
|
||||
commit.txt
|
||||
retention-days: 10
|
||||
|
||||
bench-fmt:
|
||||
name: perf-fmt
|
||||
needs: zig-test
|
||||
|
||||
# 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:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GH_CI_PAT }}
|
||||
# fetch submodules recusively, to get jsruntime-lib submodules also.
|
||||
submodules: recursive
|
||||
name: bench-results
|
||||
|
||||
- name: install v8
|
||||
run: |
|
||||
mkdir -p vendor/jsruntime-lib/vendor/v8/${{env.ARCH}}/debug
|
||||
ln -s /usr/local/lib/libc_v8.a vendor/jsruntime-lib/vendor/v8/${{env.ARCH}}/debug/libc_v8.a
|
||||
|
||||
mkdir -p vendor/jsruntime-lib/vendor/v8/${{env.ARCH}}/release
|
||||
ln -s /usr/local/lib/libc_v8.a vendor/jsruntime-lib/vendor/v8/${{env.ARCH}}/release/libc_v8.a
|
||||
|
||||
- name: install deps
|
||||
run: |
|
||||
ln -s /usr/local/lib/lexbor vendor/lexbor
|
||||
|
||||
ln -s /usr/local/lib/libiconv vendor/libiconv
|
||||
|
||||
ln -s /usr/local/lib/netsurf/build vendor/netsurf/build
|
||||
ln -s /usr/local/lib/netsurf/lib vendor/netsurf/lib
|
||||
ln -s /usr/local/lib/netsurf/include vendor/netsurf/include
|
||||
|
||||
- name: zig build debug
|
||||
run: zig build -Dengine=v8
|
||||
|
||||
- name: zig build test
|
||||
run: zig build test -Dengine=v8
|
||||
|
||||
- name: zig build release
|
||||
run: zig build -Doptimize=ReleaseSafe -Dengine=v8
|
||||
- name: format and send json result
|
||||
run: /perf-fmt bench-browser ${{ github.sha }} bench.json
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,5 @@
|
||||
zig-cache
|
||||
zig-out
|
||||
/vendor/lexbor/
|
||||
/vendor/netsurf/build/
|
||||
/vendor/netsurf/lib/
|
||||
/vendor/netsurf/include/
|
||||
|
||||
18
.gitmodules
vendored
18
.gitmodules
vendored
@@ -1,15 +1,12 @@
|
||||
[submodule "vendor/jsruntime-lib"]
|
||||
path = vendor/jsruntime-lib
|
||||
url = git@github.com:lightpanda-io/jsruntime-lib.git
|
||||
[submodule "vendor/lexbor-src"]
|
||||
path = vendor/lexbor-src
|
||||
url = https://github.com/lexbor/lexbor
|
||||
[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 = https://source.netsurf-browser.org/libwapcaplet.git
|
||||
url = git@github.com:lightpanda-io/libwapcaplet.git
|
||||
[submodule "vendor/netsurf/libparserutils"]
|
||||
path = vendor/netsurf/libparserutils
|
||||
url = https://source.netsurf-browser.org/libparserutils.git
|
||||
url = git@github.com:lightpanda-io/libparserutils.git
|
||||
[submodule "vendor/netsurf/libdom"]
|
||||
path = vendor/netsurf/libdom
|
||||
url = git@github.com:lightpanda-io/libdom.git
|
||||
@@ -18,7 +15,10 @@
|
||||
url = https://source.netsurf-browser.org/buildsystem.git
|
||||
[submodule "vendor/netsurf/libhubbub"]
|
||||
path = vendor/netsurf/libhubbub
|
||||
url = https://source.netsurf-browser.org/libhubbub.git
|
||||
url = git@github.com:lightpanda-io/libhubbub.git
|
||||
[submodule "tests/wpt"]
|
||||
path = tests/wpt
|
||||
url = https://github.com/lightpanda-io/wpt
|
||||
[submodule "vendor/mimalloc"]
|
||||
path = vendor/mimalloc
|
||||
url = git@github.com:microsoft/mimalloc.git
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
# This dockerfile is used to build browsercore vendor dependencies except
|
||||
# jsruntime-lib v8.
|
||||
# jsruntime-lib v8 is built via zig-v8-fork/Dockerfile.
|
||||
ARG ZIG_DOCKER_VERSION=0.11.0
|
||||
FROM ghcr.io/lightpanda-io/zig:${ZIG_DOCKER_VERSION} as build
|
||||
|
||||
# Install required dependencies
|
||||
RUN apt update && \
|
||||
apt install -y git curl bash xz-utils python3 ca-certificates pkg-config \
|
||||
libglib2.0-dev gperf libexpat1-dev cmake build-essential
|
||||
|
||||
COPY ./Makefile /src/
|
||||
WORKDIR /src
|
||||
|
||||
# build lexbor
|
||||
ADD ./vendor/lexbor-src /src/vendor/lexbor-src
|
||||
RUN make install-lexbor
|
||||
|
||||
# build libiconv
|
||||
RUN make install-libiconv
|
||||
|
||||
# build netsurf
|
||||
ADD ./vendor/netsurf /src/vendor/netsurf
|
||||
RUN make install-netsurf
|
||||
|
||||
FROM scratch as artifact
|
||||
|
||||
COPY --from=build /src/vendor/libiconv /usr/local/lib/libiconv
|
||||
|
||||
COPY --from=build /src/vendor/lexbor /usr/local/lib/lexbor
|
||||
|
||||
COPY --from=build /src/vendor/netsurf/build /usr/local/lib/netsurf/build
|
||||
COPY --from=build /src/vendor/netsurf/lib /usr/local/lib/netsurf/lib
|
||||
COPY --from=build /src/vendor/netsurf/include /usr/local/lib/netsurf/include
|
||||
661
LICENSE
Normal file
661
LICENSE
Normal file
@@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
62
Makefile
62
Makefile
@@ -23,9 +23,9 @@ help:
|
||||
|
||||
# $(ZIG) commands
|
||||
# ------------
|
||||
.PHONY: build build-release run run-release shell test bench download-zig wpt
|
||||
.PHONY: build build-dev run run-release shell test bench download-zig wpt
|
||||
|
||||
zig_version = $(shell grep 'recommended_zig_version = "' "vendor/jsruntime-lib/build.zig" | cut -d'"' -f2)
|
||||
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
|
||||
@@ -54,23 +54,24 @@ endif
|
||||
@curl -o "$(dest)" -L "$(url)" || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||
@printf "\e[33mDownloaded $(dest)\e[0m\n"
|
||||
|
||||
## Build in debug mode
|
||||
## Build in release-safe mode
|
||||
build:
|
||||
@printf "\e[36mBuilding (debug)...\e[0m\n"
|
||||
@$(ZIG) build -Dengine=v8 || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||
@printf "\e[33mBuild OK\e[0m\n"
|
||||
|
||||
build-release:
|
||||
@printf "\e[36mBuilding (release safe)...\e[0m\n"
|
||||
@$(ZIG) build -Doptimize=ReleaseSafe -Dengine=v8 || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||
@printf "\e[33mBuild OK\e[0m\n"
|
||||
|
||||
## Run the server
|
||||
## Build in debug mode
|
||||
build-dev:
|
||||
@printf "\e[36mBuilding (debug)...\e[0m\n"
|
||||
@$(ZIG) build -Dengine=v8 || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||
@printf "\e[33mBuild OK\e[0m\n"
|
||||
|
||||
## Run the server in debug mode
|
||||
run: build
|
||||
@printf "\e[36mRunning...\e[0m\n"
|
||||
@./zig-out/bin/browsercore || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
|
||||
|
||||
## Run a JS shell in release-safe mode
|
||||
## Run a JS shell in debug mode
|
||||
shell:
|
||||
@printf "\e[36mBuilding shell...\e[0m\n"
|
||||
@$(ZIG) build shell -Dengine=v8 || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
|
||||
@@ -93,15 +94,16 @@ test:
|
||||
# Install and build required dependencies commands
|
||||
# ------------
|
||||
.PHONY: install-submodule
|
||||
.PHONY: install-lexbor install-jsruntime install-jsruntime-dev install-libiconv
|
||||
.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-lexbor install-jsruntime install-netsurf
|
||||
install: install-submodule install-zig-js-runtime install-netsurf install-mimalloc
|
||||
|
||||
## Install and build dependencies for dev
|
||||
install-dev: install-submodule install-lexbor install-jsruntime-dev install-netsurf-dev
|
||||
install-dev: install-submodule install-zig-js-runtime-dev install-netsurf-dev install-mimalloc-dev
|
||||
|
||||
install-netsurf-dev: _install-netsurf
|
||||
install-netsurf-dev: OPTCFLAGS := -O0 -g -DNDEBUG
|
||||
@@ -177,20 +179,34 @@ ifeq ("$(wildcard vendor/libiconv/lib/libiconv.a)","")
|
||||
make && make install
|
||||
endif
|
||||
|
||||
install-lexbor:
|
||||
@mkdir -p vendor/lexbor
|
||||
@cd vendor/lexbor && \
|
||||
cmake ../lexbor-src -DLEXBOR_BUILD_SHARED=OFF && \
|
||||
make
|
||||
|
||||
install-jsruntime-dev:
|
||||
@cd vendor/jsruntime-lib && \
|
||||
install-zig-js-runtime-dev:
|
||||
@cd vendor/zig-js-runtime && \
|
||||
make install-dev
|
||||
|
||||
install-jsruntime:
|
||||
@cd vendor/jsruntime-lib && \
|
||||
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:
|
||||
@git submodule init && \
|
||||
|
||||
179
README.md
179
README.md
@@ -1,19 +1,93 @@
|
||||
# Browsercore
|
||||
<p align="center">
|
||||
<a href="https://lightpanda.io"><img src="https://cdn.lightpanda.io/assets/images/logo/lpd-logo.png" alt="Logo" height=170></a>
|
||||
</p>
|
||||
|
||||
## Build
|
||||
<h1 align="center">Lightpanda</h1>
|
||||
|
||||
<div align="center">
|
||||
<br />
|
||||
</div>
|
||||
|
||||
Lightpanda is the open-source browser made for headless usage:
|
||||
|
||||
- Javascript execution
|
||||
- Support of the Web APIs (partial, WIP)
|
||||
- Compatible with Playwright, Puppeteer through CDP (WIP)
|
||||
|
||||
Fast scraping and web automation with minimal memory footprint:
|
||||
|
||||
- Ultra-low memory footprint (12x less than Chrome)
|
||||
- Blazingly fast & instant startup (64x faster than Chrome)
|
||||
|
||||
<img width=500px src="https://cdn.lightpanda.io/assets/images/benchmark2.png">
|
||||
|
||||
See [benchmark details](https://github.com/lightpanda-io/demo).
|
||||
|
||||
## Why?
|
||||
|
||||
### Javascript execution is mandatory for the modern web
|
||||
|
||||
Back in the good old times, grabbing a webpage was as easy as making an HTTP request, cURL-like. It’s 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
|
||||
|
||||
So if we need Javascript, why not use a real web browser. Let’s 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 it’s 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, 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 that’s we did:
|
||||
|
||||
- Not based on Chromium, Blink or WebKit
|
||||
- Low-level system programming language (Zig) with optimisations in mind
|
||||
- Opinionated, no rendering
|
||||
|
||||
## Status
|
||||
|
||||
Lightpanda is still a work in progress and is currently at the Alpha stage.
|
||||
|
||||
Here are the key features we want to implement before releasing a Beta version:
|
||||
|
||||
- [x] Loader
|
||||
- [x] HTML parser and DOM tree
|
||||
- [x] Javascript support
|
||||
- [x] Basic DOM APIs
|
||||
- [x] Ajax
|
||||
- [x] XHR API
|
||||
- [ ] Fetch API
|
||||
- [x] DOM dump
|
||||
- [ ] Basic CDP server
|
||||
|
||||
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
|
||||
|
||||
Browsercore is written with [Zig](https://ziglang.org/) `0.11.0`. You have to
|
||||
Lightpanda is written with [Zig](https://ziglang.org/) `0.12.1`. You have to
|
||||
install it with the right version in order to build the project.
|
||||
|
||||
Browsercore also depends on
|
||||
[js-runtimelib](https://github.com/francisbouvier/jsruntime-lib/) and
|
||||
[lexbor](https://github.com/lexbor/lexbor) libs.
|
||||
Lightpanda also depends on
|
||||
[zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime/) (with v8),
|
||||
[Netsurf libs](https://www.netsurf-browser.org/) and
|
||||
[Mimalloc](https://microsoft.github.io/mimalloc).
|
||||
|
||||
To be able to build the v8 engine for js-runtimelib, you have to install some libs:
|
||||
To be able to build the v8 engine for zig-js-runtime, you have to install some libs:
|
||||
|
||||
For Debian/Ubuntu based Linux:
|
||||
|
||||
```
|
||||
sudo apt install xz-utils \
|
||||
python3 ca-certificates git \
|
||||
@@ -22,94 +96,103 @@ sudo apt install xz-utils \
|
||||
cmake clang
|
||||
```
|
||||
|
||||
For MacOS, you only need Python 3 and cmake.
|
||||
For MacOS, you only need cmake:
|
||||
|
||||
To be able to build lexbor, you need to install also `cmake`.
|
||||
```
|
||||
brew install cmake
|
||||
```
|
||||
|
||||
### Install and build dependencies
|
||||
|
||||
The project uses git submodule for dependencies.
|
||||
The `make install-submodule` will init and update the submodules in the `vendor/`
|
||||
directory.
|
||||
#### All in one build
|
||||
|
||||
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 dependancies, including the v8 Javascript engine.
|
||||
|
||||
#### Step by step build dependancy
|
||||
|
||||
The project uses git submodules for dependencies.
|
||||
|
||||
To init or update the submodules in the `vendor/` directory:
|
||||
|
||||
```
|
||||
make install-submodule
|
||||
```
|
||||
|
||||
### Build netsurf
|
||||
**Netsurf libs**
|
||||
|
||||
Netsurf libs are used for HTML parsing and DOM tree generation.
|
||||
|
||||
The command `make install-netsurf` will build netsurf libs used by browsercore.
|
||||
```
|
||||
make install-netsurf
|
||||
```
|
||||
|
||||
### Build lexbor
|
||||
For dev env, use `make install-netsurf-dev`.
|
||||
|
||||
The command `make install-lexbor` will build lexbor lib used by browsercore.
|
||||
```
|
||||
make install-lexbor
|
||||
```
|
||||
**Mimalloc**
|
||||
|
||||
### Build jsruntime-lib
|
||||
|
||||
The command `make install-jsruntime-dev` uses jsruntime-lib's `zig-v8` dependency to build v8 engine lib.
|
||||
Be aware the build task is very long and cpu consuming.
|
||||
|
||||
Build v8 engine for debug/dev version, it creates
|
||||
`vendor/jsruntime-lib/vendor/v8/$ARCH/debug/libc_v8.a` file.
|
||||
Mimalloc is used as a C memory allocator.
|
||||
|
||||
```
|
||||
make install-jsruntime-dev
|
||||
make install-mimalloc
|
||||
```
|
||||
|
||||
You should also build a release vesion of v8 with:
|
||||
For dev env, use `make install-mimalloc-dev`.
|
||||
|
||||
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).
|
||||
|
||||
**zig-js-runtime**
|
||||
|
||||
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 install-jsruntime
|
||||
make install-zig-js-runtime
|
||||
```
|
||||
|
||||
### All in one build
|
||||
|
||||
You can run `make intall` and `make install-dev` to install deps all in one.
|
||||
For dev env, use `make iinstall-zig-js-runtime-dev`.
|
||||
|
||||
## Test
|
||||
|
||||
### Unit Tests
|
||||
|
||||
You can test browsercore by running `make test`.
|
||||
You can test Lightpanda by running `make test`.
|
||||
|
||||
### Web Platform Tests
|
||||
|
||||
Browsercore is tested against the standardized [Web Platform
|
||||
Lightpanda is tested against the standardized [Web Platform
|
||||
Tests](https://web-platform-tests.org/).
|
||||
|
||||
The relevant tests cases for Browsercore are commit with the project.
|
||||
All the tests cases executed are located in `tests/wpt` dir and come from an
|
||||
external repository: https://github.com/lightpanda-io/wpt
|
||||
The relevant tests cases are committed in a [dedicated repository](https://github.com/lightpanda-io/wpt) which is fetched by the `make install-submodule` command.
|
||||
|
||||
All the tests cases executed are located in the `tests/wpt` sub-directory.
|
||||
|
||||
For reference, you can easily execute a WPT test case with your browser via
|
||||
[wpt.live](https://wpt.live).
|
||||
|
||||
*Run WPT test suite*
|
||||
#### Run WPT test suite
|
||||
|
||||
To run all the tests:
|
||||
|
||||
You can run all the test.
|
||||
The runner execute all the tests ending with `.html`.
|
||||
```
|
||||
make wpt
|
||||
```
|
||||
|
||||
Or one specific test by using a suffix.
|
||||
Or one specific test:
|
||||
|
||||
```
|
||||
make wpt Node-childNodes.html
|
||||
```
|
||||
|
||||
*Add a new WPT test case*
|
||||
#### Add a new WPT test case
|
||||
|
||||
We add new tests cases files with implemented changes in Browsercore.
|
||||
We add new relevant tests cases files when we implemented changes in Lightpanda.
|
||||
|
||||
Copy the test case you want to add from the [WPT
|
||||
repo](https://github.com/web-platform-tests/wpt) into `tests/wpt` dir, commit
|
||||
the files in the https://github.com/lightpanda-io/wpt repository and update the
|
||||
git submodule in browsercore.
|
||||
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 into `tests/wpt`.
|
||||
:warning: Please keep the original directory tree structure of `tests/wpt`.
|
||||
|
||||
126
build.zig
126
build.zig
@@ -1,16 +1,34 @@
|
||||
// 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 jsruntime_path = "vendor/jsruntime-lib/";
|
||||
const jsruntime = @import("vendor/jsruntime-lib/build.zig");
|
||||
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 = jsruntime.recommended_zig_version;
|
||||
|
||||
pub fn build(b: *std.build.Builder) !void {
|
||||
pub fn build(b: *std.Build) !void {
|
||||
switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) {
|
||||
.eq => {},
|
||||
.lt => {
|
||||
@@ -35,16 +53,15 @@ pub fn build(b: *std.build.Builder) !void {
|
||||
// compile and install
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "browsercore",
|
||||
.root_source_file = .{ .path = "src/main.zig" },
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = mode,
|
||||
});
|
||||
try common(exe, options);
|
||||
try common(b, exe, options);
|
||||
b.installArtifact(exe);
|
||||
|
||||
// run
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
if (b.args) |args| {
|
||||
run_cmd.addArgs(args);
|
||||
}
|
||||
@@ -59,18 +76,15 @@ pub fn build(b: *std.build.Builder) !void {
|
||||
// compile and install
|
||||
const shell = b.addExecutable(.{
|
||||
.name = "browsercore-shell",
|
||||
.root_source_file = .{ .path = "src/main_shell.zig" },
|
||||
.root_source_file = b.path("src/main_shell.zig"),
|
||||
.target = target,
|
||||
.optimize = mode,
|
||||
});
|
||||
try common(shell, options);
|
||||
try common(b, shell, options);
|
||||
try jsruntime_pkgs.add_shell(shell);
|
||||
// do not install shell binary
|
||||
b.installArtifact(shell);
|
||||
|
||||
// run
|
||||
const shell_cmd = b.addRunArtifact(shell);
|
||||
shell_cmd.step.dependOn(b.getInstallStep());
|
||||
if (b.args) |args| {
|
||||
shell_cmd.addArgs(args);
|
||||
}
|
||||
@@ -83,11 +97,23 @@ pub fn build(b: *std.build.Builder) !void {
|
||||
// ----
|
||||
|
||||
// compile
|
||||
const tests = b.addTest(.{ .root_source_file = .{ .path = "src/run_tests.zig" } });
|
||||
try common(tests, options);
|
||||
tests.single_threaded = true;
|
||||
tests.test_runner = "src/test_runner.zig";
|
||||
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,
|
||||
});
|
||||
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");
|
||||
@@ -99,16 +125,14 @@ pub fn build(b: *std.build.Builder) !void {
|
||||
// compile and install
|
||||
const wpt = b.addExecutable(.{
|
||||
.name = "browsercore-wpt",
|
||||
.root_source_file = .{ .path = "src/main_wpt.zig" },
|
||||
.root_source_file = b.path("src/main_wpt.zig"),
|
||||
.target = target,
|
||||
.optimize = mode,
|
||||
});
|
||||
try common(wpt, options);
|
||||
b.installArtifact(wpt);
|
||||
try common(b, wpt, options);
|
||||
|
||||
// run
|
||||
const wpt_cmd = b.addRunArtifact(wpt);
|
||||
wpt_cmd.step.dependOn(b.getInstallStep());
|
||||
if (b.args) |args| {
|
||||
wpt_cmd.addArgs(args);
|
||||
}
|
||||
@@ -122,16 +146,15 @@ pub fn build(b: *std.build.Builder) !void {
|
||||
// compile and install
|
||||
const get = b.addExecutable(.{
|
||||
.name = "browsercore-get",
|
||||
.root_source_file = .{ .path = "src/main_get.zig" },
|
||||
.root_source_file = b.path("src/main_get.zig"),
|
||||
.target = target,
|
||||
.optimize = mode,
|
||||
});
|
||||
try common(get, options);
|
||||
try common(b, get, options);
|
||||
b.installArtifact(get);
|
||||
|
||||
// run
|
||||
const get_cmd = b.addRunArtifact(get);
|
||||
get_cmd.step.dependOn(b.getInstallStep());
|
||||
if (b.args) |args| {
|
||||
get_cmd.addArgs(args);
|
||||
}
|
||||
@@ -141,29 +164,38 @@ pub fn build(b: *std.build.Builder) !void {
|
||||
}
|
||||
|
||||
fn common(
|
||||
step: *std.Build.CompileStep,
|
||||
b: *std.Build,
|
||||
step: *std.Build.Step.Compile,
|
||||
options: jsruntime.Options,
|
||||
) !void {
|
||||
try jsruntime_pkgs.add(step, options);
|
||||
linkLexbor(step);
|
||||
linkNetSurf(step);
|
||||
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);
|
||||
}
|
||||
|
||||
fn linkLexbor(step: *std.build.LibExeObjStep) void {
|
||||
// cmake . -DLEXBOR_BUILD_SHARED=OFF
|
||||
const lib_path = "vendor/lexbor/liblexbor_static.a";
|
||||
step.addObjectFile(.{ .path = lib_path });
|
||||
step.addIncludePath(.{ .path = "vendor/lexbor-src/source" });
|
||||
}
|
||||
|
||||
fn linkNetSurf(step: *std.build.LibExeObjStep) void {
|
||||
|
||||
fn moduleNetSurf(b: *std.Build) *std.Build.Module {
|
||||
const mod = b.addModule("netsurf", .{
|
||||
.root_source_file = b.path("src/netsurf/netsurf.zig"),
|
||||
});
|
||||
// iconv
|
||||
step.addObjectFile(.{ .path = "vendor/libiconv/lib/libiconv.a" });
|
||||
step.addIncludePath(.{ .path = "vendor/libiconv/include" });
|
||||
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/";
|
||||
const ns = "vendor/netsurf";
|
||||
mod.addIncludePath(b.path(ns ++ "/include"));
|
||||
|
||||
const libs: [4][]const u8 = .{
|
||||
"libdom",
|
||||
"libhubbub",
|
||||
@@ -171,8 +203,20 @@ fn linkNetSurf(step: *std.build.LibExeObjStep) void {
|
||||
"libwapcaplet",
|
||||
};
|
||||
inline for (libs) |lib| {
|
||||
step.addObjectFile(.{ .path = ns ++ "/lib/" ++ lib ++ ".a" });
|
||||
step.addIncludePath(.{ .path = ns ++ lib ++ "/src" });
|
||||
mod.addObjectFile(b.path(ns ++ "/lib/" ++ lib ++ ".a"));
|
||||
mod.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src"));
|
||||
}
|
||||
step.addIncludePath(.{ .path = ns ++ "/include" });
|
||||
|
||||
return mod;
|
||||
}
|
||||
|
||||
fn moduleMimalloc(b: *std.Build) *std.Build.Module {
|
||||
const mod = b.addModule("mimalloc", .{
|
||||
.root_source_file = b.path("src/mimalloc/mimalloc.zig"),
|
||||
});
|
||||
|
||||
mod.addObjectFile(b.path("vendor/mimalloc/out/libmimalloc.a"));
|
||||
mod.addIncludePath(b.path("vendor/mimalloc/out/include"));
|
||||
|
||||
return mod;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
// 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;
|
||||
@@ -6,6 +24,8 @@ 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;
|
||||
|
||||
@@ -16,4 +36,8 @@ pub const Interfaces = generate.Tuple(.{
|
||||
Events.Interfaces,
|
||||
HTML.Interfaces,
|
||||
XHR.Interfaces,
|
||||
Storage.Interfaces,
|
||||
URL.Interfaces,
|
||||
});
|
||||
|
||||
pub const UserContext = @import("user_context.zig").UserContext;
|
||||
|
||||
1409
src/async/Client.zig
1409
src/async/Client.zig
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,24 @@
|
||||
// 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 os = std.os;
|
||||
const posix = std.posix;
|
||||
const io = std.io;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
@@ -10,15 +28,15 @@ pub const Stream = struct {
|
||||
alloc: std.mem.Allocator,
|
||||
conn: *tcp.Conn,
|
||||
|
||||
handle: std.os.socket_t,
|
||||
handle: posix.socket_t,
|
||||
|
||||
pub fn close(self: Stream) void {
|
||||
os.closeSocket(self.handle);
|
||||
posix.close(self.handle);
|
||||
self.alloc.destroy(self.conn);
|
||||
}
|
||||
|
||||
pub const ReadError = os.ReadError;
|
||||
pub const WriteError = os.WriteError;
|
||||
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);
|
||||
@@ -37,8 +55,8 @@ pub const Stream = struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn readv(s: Stream, iovecs: []const os.iovec) ReadError!usize {
|
||||
return os.readv(s.handle, iovecs);
|
||||
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
|
||||
@@ -87,7 +105,7 @@ pub const Stream = struct {
|
||||
|
||||
/// See https://github.com/ziglang/zig/issues/7699
|
||||
/// See equivalent function: `std.fs.File.writev`.
|
||||
pub fn writev(self: Stream, iovecs: []const os.iovec_const) WriteError!usize {
|
||||
pub fn writev(self: Stream, iovecs: []const posix.iovec_const) WriteError!usize {
|
||||
if (iovecs.len == 0) return 0;
|
||||
const first_buffer = iovecs[0].iov_base[0..iovecs[0].iov_len];
|
||||
return try self.write(first_buffer);
|
||||
@@ -97,7 +115,7 @@ pub const Stream = struct {
|
||||
/// 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: []os.iovec_const) WriteError!void {
|
||||
pub fn writevAll(self: Stream, iovecs: []posix.iovec_const) WriteError!void {
|
||||
if (iovecs.len == 0) return;
|
||||
|
||||
var i: usize = 0;
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
// 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;
|
||||
@@ -41,19 +59,19 @@ pub const Conn = struct {
|
||||
|
||||
loop: *Loop,
|
||||
|
||||
pub fn connect(self: *Conn, socket: std.os.socket_t, address: std.net.Address) !void {
|
||||
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.os.socket_t, buffer: []const u8) !usize {
|
||||
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.os.socket_t, buffer: []u8) !usize {
|
||||
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();
|
||||
@@ -75,12 +93,12 @@ pub fn tcpConnectToHost(alloc: std.mem.Allocator, loop: *Loop, name: []const u8,
|
||||
else => return err,
|
||||
};
|
||||
}
|
||||
return std.os.ConnectError.ConnectionRefused;
|
||||
return std.posix.ConnectError.ConnectionRefused;
|
||||
}
|
||||
|
||||
pub fn tcpConnectToAddress(alloc: std.mem.Allocator, loop: *Loop, addr: net.Address) !Stream {
|
||||
const sockfd = try std.os.socket(addr.any.family, std.os.SOCK.STREAM, std.os.IPPROTO.TCP);
|
||||
errdefer std.os.closeSocket(sockfd);
|
||||
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 };
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
// 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");
|
||||
@@ -22,11 +40,9 @@ test "blocking mode fetch API" {
|
||||
// force client's CA cert scan from system.
|
||||
try client.ca_bundle.rescan(client.allocator);
|
||||
|
||||
var res = try client.fetch(alloc, .{
|
||||
const res = try client.fetch(.{
|
||||
.location = .{ .uri = try std.Uri.parse(url) },
|
||||
.payload = .none,
|
||||
});
|
||||
defer res.deinit();
|
||||
|
||||
try std.testing.expect(res.status == .ok);
|
||||
}
|
||||
@@ -46,13 +62,13 @@ test "blocking mode open/send/wait API" {
|
||||
// force client's CA cert scan from system.
|
||||
try client.ca_bundle.rescan(client.allocator);
|
||||
|
||||
var headers = try std.http.Headers.initList(alloc, &[_]std.http.Field{});
|
||||
defer headers.deinit();
|
||||
|
||||
var req = try client.open(.GET, try std.Uri.parse(url), headers, .{});
|
||||
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.send();
|
||||
try req.finish();
|
||||
try req.wait();
|
||||
|
||||
@@ -69,7 +85,6 @@ const AsyncClient = struct {
|
||||
|
||||
cli: *Client,
|
||||
uri: std.Uri,
|
||||
headers: std.http.Headers,
|
||||
|
||||
req: ?Request = undefined,
|
||||
state: State = .new,
|
||||
@@ -77,9 +92,10 @@ const AsyncClient = struct {
|
||||
impl: YieldImpl,
|
||||
err: ?anyerror = null,
|
||||
|
||||
buf: [2014]u8 = undefined,
|
||||
|
||||
pub fn deinit(self: *AsyncRequest) void {
|
||||
if (self.req) |*r| r.deinit();
|
||||
self.headers.deinit();
|
||||
}
|
||||
|
||||
pub fn fetch(self: *AsyncRequest) void {
|
||||
@@ -98,11 +114,13 @@ const AsyncClient = struct {
|
||||
switch (self.state) {
|
||||
.new => {
|
||||
self.state = .open;
|
||||
self.req = self.cli.open(.GET, self.uri, self.headers, .{}) catch |e| return self.onerr(e);
|
||||
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);
|
||||
self.req.?.send() catch |e| return self.onerr(e);
|
||||
},
|
||||
.send => {
|
||||
self.state = .finish;
|
||||
@@ -146,7 +164,6 @@ const AsyncClient = struct {
|
||||
.impl = YieldImpl.init(self.cli.loop),
|
||||
.cli = &self.cli,
|
||||
.uri = uri,
|
||||
.headers = .{ .allocator = self.cli.allocator, .owned = false },
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,8 +1,27 @@
|
||||
// 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.zig");
|
||||
const parser = @import("netsurf");
|
||||
const Loader = @import("loader.zig").Loader;
|
||||
const Dump = @import("dump.zig");
|
||||
const Mime = @import("mime.zig");
|
||||
@@ -16,8 +35,13 @@ 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 = std.http.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.
|
||||
@@ -68,6 +92,10 @@ pub const Session = struct {
|
||||
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,
|
||||
|
||||
@@ -80,20 +108,27 @@ pub const Session = struct {
|
||||
.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);
|
||||
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.loader.deinit();
|
||||
self.loop.deinit();
|
||||
self.storageShed.deinit();
|
||||
self.httpClient.deinit();
|
||||
self.alloc.destroy(self);
|
||||
}
|
||||
|
||||
@@ -115,17 +150,21 @@ pub const Page = struct {
|
||||
// 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 {
|
||||
return Page{
|
||||
) !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.
|
||||
@@ -133,11 +172,15 @@ pub const Page = struct {
|
||||
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.
|
||||
@@ -151,7 +194,25 @@ pub const Page = struct {
|
||||
}
|
||||
|
||||
// if the page has a pointer to a document, dumps the HTML.
|
||||
try Dump.htmlFile(self.doc.?, out);
|
||||
try Dump.writeHTML(self.doc.?, out);
|
||||
}
|
||||
|
||||
pub fn wait(self: *Page) !void {
|
||||
const alloc = self.arena.allocator();
|
||||
var res = try self.session.env.waitTryCatch(alloc);
|
||||
defer res.deinit(alloc);
|
||||
|
||||
if (res.success) {
|
||||
log.debug("wait: {s}", .{res.result});
|
||||
} else {
|
||||
if (builtin.mode == .Debug and res.stack != null) {
|
||||
log.info("wait: {s}", .{res.stack.?});
|
||||
} else {
|
||||
log.info("wait: {s}", .{res.result});
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// spec reference: https://html.spec.whatwg.org/#document-lifecycle
|
||||
@@ -163,7 +224,16 @@ pub const Page = struct {
|
||||
// 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.parseWithoutScheme(self.rawuri.?);
|
||||
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.
|
||||
|
||||
@@ -177,29 +247,39 @@ pub const Page = struct {
|
||||
|
||||
// TODO handle redirection
|
||||
if (req.response.status != .ok) {
|
||||
log.debug("{?} {d} {s}\n{any}", .{
|
||||
log.debug("{?} {d} {s}", .{
|
||||
req.response.version,
|
||||
req.response.status,
|
||||
req.response.reason,
|
||||
req.response.headers,
|
||||
// TODO log headers
|
||||
});
|
||||
return error.BadStatusCode;
|
||||
}
|
||||
|
||||
// TODO handle charset
|
||||
// https://html.spec.whatwg.org/#content-type
|
||||
const ct = req.response.headers.getFirstValue("Content-Type") orelse {
|
||||
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;
|
||||
};
|
||||
log.debug("header content-type: {s}", .{ct});
|
||||
const mime = try Mime.parse(ct);
|
||||
}
|
||||
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});
|
||||
log.info("non-HTML document: {s}", .{ct.?});
|
||||
|
||||
// save the body into the page.
|
||||
self.raw_data = try req.reader().readAllAlloc(alloc, 16 * 1024 * 1024);
|
||||
@@ -210,6 +290,9 @@ pub const Page = struct {
|
||||
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);
|
||||
@@ -224,10 +307,15 @@ pub const Page = struct {
|
||||
// TODO set document.readyState to interactive
|
||||
// https://html.spec.whatwg.org/#reporting-document-loading-status
|
||||
|
||||
// TODO inject the URL to the document including the fragment.
|
||||
// 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(doc);
|
||||
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
|
||||
|
||||
@@ -236,11 +324,15 @@ pub const Page = struct {
|
||||
log.debug("start js env", .{});
|
||||
try self.session.env.start(alloc);
|
||||
|
||||
// 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.addObject(self.session.window, "window");
|
||||
try self.session.env.addObject(self.session.window, "self");
|
||||
try self.session.env.addObject(html_doc, "document");
|
||||
try self.session.env.bindGlobal(&self.session.window);
|
||||
|
||||
// browse the DOM tree to retrieve scripts
|
||||
// TODO execute the synchronous scripts during the HTL parsing.
|
||||
@@ -326,6 +418,8 @@ pub const Page = struct {
|
||||
// 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);
|
||||
|
||||
@@ -340,8 +434,13 @@ pub const Page = struct {
|
||||
|
||||
// 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);
|
||||
_ = try parser.eventTargetDispatchEvent(
|
||||
parser.toEventTarget(Window, &self.session.window),
|
||||
loadevt,
|
||||
);
|
||||
}
|
||||
|
||||
// evalScript evaluates the src in priority.
|
||||
@@ -377,14 +476,17 @@ pub const Page = struct {
|
||||
const opt_text = try parser.nodeTextContent(parser.elementToNode(e));
|
||||
if (opt_text) |text| {
|
||||
// TODO handle charset attribute
|
||||
var res = jsruntime.JSResult{};
|
||||
try self.session.env.run(alloc, text, "", &res, null);
|
||||
var res = try self.session.env.execTryCatch(alloc, text, "");
|
||||
defer res.deinit(alloc);
|
||||
|
||||
if (res.success) {
|
||||
log.debug("eval inline: {s}", .{res.result});
|
||||
} else {
|
||||
log.info("eval inline: {s}", .{res.result});
|
||||
if (builtin.mode == .Debug and res.stack != null) {
|
||||
log.info("eval inline: {s}", .{res.stack.?});
|
||||
} else {
|
||||
log.info("eval inline: {s}", .{res.result});
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -408,29 +510,37 @@ pub const Page = struct {
|
||||
|
||||
log.debug("starting fetch script {s}", .{src});
|
||||
|
||||
const u = std.Uri.parse(src) catch try std.Uri.parseWithoutScheme(src);
|
||||
const ru = try std.Uri.resolve(self.uri, u, false, alloc);
|
||||
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.fetch(alloc, ru);
|
||||
var fetchres = try self.session.loader.get(alloc, u);
|
||||
defer fetchres.deinit();
|
||||
|
||||
log.info("fech script {any}: {d}", .{ ru, fetchres.status });
|
||||
const resp = fetchres.req.response;
|
||||
|
||||
if (fetchres.status != .ok) return FetchError.BadStatusCode;
|
||||
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 (fetchres.body == null) return FetchError.NoBody;
|
||||
if (body.len == 0) return FetchError.NoBody;
|
||||
|
||||
var res = jsruntime.JSResult{};
|
||||
try self.session.env.run(alloc, fetchres.body.?, src, &res, null);
|
||||
var res = try self.session.env.execTryCatch(alloc, body, src);
|
||||
defer res.deinit(alloc);
|
||||
|
||||
if (res.success) {
|
||||
log.debug("eval remote {s}: {s}", .{ src, res.result });
|
||||
} else {
|
||||
log.info("eval remote {s}: {s}", .{ src, res.result });
|
||||
if (builtin.mode == .Debug and res.stack != null) {
|
||||
log.info("eval remote {s}: {s}", .{ src, res.stack.? });
|
||||
} else {
|
||||
log.info("eval remote {s}: {s}", .{ src, res.result });
|
||||
}
|
||||
return FetchError.JsErr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,36 @@
|
||||
// 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 File = std.fs.File;
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
const Walker = @import("../dom/walker.zig").WalkerChildren;
|
||||
|
||||
pub fn htmlFile(doc: *parser.Document, out: File) !void {
|
||||
try out.writeAll("<!DOCTYPE html>\n");
|
||||
try nodeFile(parser.documentToNode(doc), out);
|
||||
try out.writeAll("\n");
|
||||
// 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");
|
||||
}
|
||||
|
||||
fn nodeFile(root: *parser.Node, out: File) !void {
|
||||
// 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) {
|
||||
@@ -19,8 +39,8 @@ fn nodeFile(root: *parser.Node, out: File) !void {
|
||||
.element => {
|
||||
// open the tag
|
||||
const tag = try parser.nodeLocalName(next.?);
|
||||
try out.writeAll("<");
|
||||
try out.writeAll(tag);
|
||||
try writer.writeAll("<");
|
||||
try writer.writeAll(tag);
|
||||
|
||||
// write the attributes
|
||||
const map = try parser.nodeGetAttributes(next.?);
|
||||
@@ -28,40 +48,43 @@ fn nodeFile(root: *parser.Node, out: File) !void {
|
||||
var i: u32 = 0;
|
||||
while (i < ln) {
|
||||
const attr = try parser.namedNodeMapItem(map, i) orelse break;
|
||||
try out.writeAll(" ");
|
||||
try out.writeAll(try parser.attributeGetName(attr));
|
||||
try out.writeAll("=\"");
|
||||
try out.writeAll(try parser.attributeGetValue(attr) orelse "");
|
||||
try out.writeAll("\"");
|
||||
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 out.writeAll(">");
|
||||
try writer.writeAll(">");
|
||||
|
||||
// void elements can't have any content.
|
||||
if (try isVoid(parser.nodeToElement(next.?))) continue;
|
||||
|
||||
// write the children
|
||||
// TODO avoid recursion
|
||||
try nodeFile(next.?, out);
|
||||
try writeNode(next.?, writer);
|
||||
|
||||
// close the tag
|
||||
try out.writeAll("</");
|
||||
try out.writeAll(tag);
|
||||
try out.writeAll(">");
|
||||
try writer.writeAll("</");
|
||||
try writer.writeAll(tag);
|
||||
try writer.writeAll(">");
|
||||
},
|
||||
.text => {
|
||||
const v = try parser.nodeValue(next.?) orelse continue;
|
||||
try out.writeAll(v);
|
||||
try writer.writeAll(v);
|
||||
},
|
||||
.cdata_section => {
|
||||
const v = try parser.nodeValue(next.?) orelse continue;
|
||||
try out.writeAll("<![CDATA[");
|
||||
try out.writeAll(v);
|
||||
try out.writeAll("]]>");
|
||||
try writer.writeAll("<![CDATA[");
|
||||
try writer.writeAll(v);
|
||||
try writer.writeAll("]]>");
|
||||
},
|
||||
.comment => {
|
||||
const v = try parser.nodeValue(next.?) orelse continue;
|
||||
try out.writeAll("<!--");
|
||||
try out.writeAll(v);
|
||||
try out.writeAll("-->");
|
||||
try writer.writeAll("<!--");
|
||||
try writer.writeAll(v);
|
||||
try writer.writeAll("-->");
|
||||
},
|
||||
// TODO handle processing instruction dump
|
||||
.processing_instruction => continue,
|
||||
@@ -81,8 +104,21 @@ fn nodeFile(root: *parser.Node, out: File) !void {
|
||||
}
|
||||
}
|
||||
|
||||
// HTMLFileTestFn is run by run_tests.zig
|
||||
pub fn HTMLFileTestFn(out: File) !void {
|
||||
// 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();
|
||||
|
||||
@@ -92,5 +128,5 @@ pub fn HTMLFileTestFn(out: File) !void {
|
||||
|
||||
const doc = parser.documentHTMLToDocument(doc_html);
|
||||
|
||||
try htmlFile(doc, out);
|
||||
try writeHTML(doc, out);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,29 @@
|
||||
// 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 user_agent = "Lightpanda.io/1.0";
|
||||
|
||||
pub const Loader = struct {
|
||||
client: std.http.Client,
|
||||
// use 16KB for headers buffer size.
|
||||
server_header_buffer: [1024 * 16]u8 = undefined,
|
||||
|
||||
pub const Response = struct {
|
||||
alloc: std.mem.Allocator,
|
||||
@@ -27,46 +47,30 @@ pub const Loader = struct {
|
||||
self.client.deinit();
|
||||
}
|
||||
|
||||
// the caller must deinit the FetchResult.
|
||||
pub fn fetch(self: *Loader, alloc: std.mem.Allocator, uri: std.Uri) !std.http.Client.FetchResult {
|
||||
var headers = try std.http.Headers.initList(alloc, &[_]std.http.Field{
|
||||
.{ .name = "User-Agent", .value = user_agent },
|
||||
.{ .name = "Accept", .value = "*/*" },
|
||||
.{ .name = "Accept-Language", .value = "en-US,en;q=0.5" },
|
||||
});
|
||||
defer headers.deinit();
|
||||
|
||||
return try self.client.fetch(alloc, .{
|
||||
.location = .{ .uri = uri },
|
||||
.headers = headers,
|
||||
.payload = .none,
|
||||
});
|
||||
}
|
||||
|
||||
// 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 headers = try std.http.Headers.initList(alloc, &[_]std.http.Field{
|
||||
.{ .name = "User-Agent", .value = user_agent },
|
||||
.{ .name = "Accept", .value = "*/*" },
|
||||
.{ .name = "Accept-Language", .value = "en-US,en;q=0.5" },
|
||||
});
|
||||
defer headers.deinit();
|
||||
|
||||
var resp = Response{
|
||||
.alloc = alloc,
|
||||
.req = try alloc.create(std.http.Client.Request),
|
||||
};
|
||||
errdefer alloc.destroy(resp.req);
|
||||
|
||||
resp.req.* = try self.client.open(.GET, uri, headers, .{
|
||||
.handle_redirects = true, // TODO handle redirects manually
|
||||
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.send();
|
||||
try resp.req.finish();
|
||||
try resp.req.wait();
|
||||
|
||||
@@ -74,13 +78,13 @@ pub const Loader = struct {
|
||||
}
|
||||
};
|
||||
|
||||
test "basic url fetch" {
|
||||
test "basic url get" {
|
||||
const alloc = std.testing.allocator;
|
||||
var loader = Loader.init(alloc);
|
||||
defer loader.deinit();
|
||||
|
||||
var result = try loader.fetch(alloc, "https://en.wikipedia.org/wiki/Main_Page");
|
||||
var result = try loader.get(alloc, "https://en.wikipedia.org/wiki/Main_Page");
|
||||
defer result.deinit();
|
||||
|
||||
try std.testing.expect(result.status == std.http.Status.ok);
|
||||
try std.testing.expect(result.req.response.status == std.http.Status.ok);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,28 @@
|
||||
// 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{
|
||||
@@ -21,91 +43,6 @@ pub const Empty = Self{ .mtype = "", .msubtype = "" };
|
||||
pub const HTML = Self{ .mtype = "text", .msubtype = "html" };
|
||||
pub const Javascript = Self{ .mtype = "application", .msubtype = "javascript" };
|
||||
|
||||
const reader = struct {
|
||||
s: []const u8,
|
||||
i: usize = 0,
|
||||
|
||||
fn until(self: *reader, c: u8) []const u8 {
|
||||
const ln = self.s.len;
|
||||
const start = self.i;
|
||||
while (self.i < ln) {
|
||||
if (c == self.s[self.i]) return self.s[start..self.i];
|
||||
self.i += 1;
|
||||
}
|
||||
|
||||
return self.s[start..self.i];
|
||||
}
|
||||
|
||||
fn tail(self: *reader) []const u8 {
|
||||
if (self.i > self.s.len) return "";
|
||||
defer self.i = self.s.len;
|
||||
return self.s[self.i..];
|
||||
}
|
||||
|
||||
fn skip(self: *reader) bool {
|
||||
if (self.i >= self.s.len) return false;
|
||||
self.i += 1;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
test "reader.skip" {
|
||||
var r = reader{ .s = "foo" };
|
||||
try testing.expect(r.skip());
|
||||
try testing.expect(r.skip());
|
||||
try testing.expect(r.skip());
|
||||
try testing.expect(!r.skip());
|
||||
try testing.expect(!r.skip());
|
||||
}
|
||||
|
||||
test "reader.tail" {
|
||||
var r = reader{ .s = "foo" };
|
||||
try testing.expectEqualStrings("foo", r.tail());
|
||||
try testing.expectEqualStrings("", r.tail());
|
||||
}
|
||||
|
||||
test "reader.until" {
|
||||
var r = reader{ .s = "foo.bar.baz" };
|
||||
try testing.expectEqualStrings("foo", r.until('.'));
|
||||
_ = r.skip();
|
||||
try testing.expectEqualStrings("bar", r.until('.'));
|
||||
_ = r.skip();
|
||||
try testing.expectEqualStrings("baz", r.until('.'));
|
||||
|
||||
r = reader{ .s = "foo" };
|
||||
try testing.expectEqualStrings("foo", r.until('.'));
|
||||
|
||||
r = reader{ .s = "" };
|
||||
try testing.expectEqualStrings("", r.until('.'));
|
||||
}
|
||||
|
||||
fn trim(s: []const u8) []const u8 {
|
||||
const ln = s.len;
|
||||
if (ln == 0) {
|
||||
return "";
|
||||
}
|
||||
var start: usize = 0;
|
||||
while (start < ln) {
|
||||
if (!std.ascii.isWhitespace(s[start])) break;
|
||||
start += 1;
|
||||
}
|
||||
|
||||
var end: usize = ln;
|
||||
while (end > 0) {
|
||||
if (!std.ascii.isWhitespace(s[end - 1])) break;
|
||||
end -= 1;
|
||||
}
|
||||
|
||||
return s[start..end];
|
||||
}
|
||||
|
||||
test "trim" {
|
||||
try testing.expectEqualStrings("", trim(""));
|
||||
try testing.expectEqualStrings("foo", trim("foo"));
|
||||
try testing.expectEqualStrings("foo", trim(" \n\tfoo"));
|
||||
try testing.expectEqualStrings("foo", trim("foo \n\t"));
|
||||
}
|
||||
|
||||
// https://mimesniff.spec.whatwg.org/#http-token-code-point
|
||||
fn isHTTPCodePoint(c: u8) bool {
|
||||
return switch (c) {
|
||||
@@ -133,7 +70,7 @@ pub fn parse(s: []const u8) Self.MimeError!Self {
|
||||
if (ln > 255) return MimeError.TooBig;
|
||||
|
||||
var res = Self{ .mtype = "", .msubtype = "" };
|
||||
var r = reader{ .s = s };
|
||||
var r = Reader{ .s = s };
|
||||
|
||||
res.mtype = trim(r.until('/'));
|
||||
if (res.mtype.len == 0) return MimeError.Invalid;
|
||||
@@ -150,7 +87,7 @@ pub fn parse(s: []const u8) Self.MimeError!Self {
|
||||
|
||||
// parse well known parameters.
|
||||
// don't check invalid parameter format.
|
||||
var rp = reader{ .s = res.params };
|
||||
var rp = Reader{ .s = res.params };
|
||||
while (true) {
|
||||
const name = trim(rp.until('='));
|
||||
if (!rp.skip()) return res;
|
||||
|
||||
218
src/css/README.md
Normal file
218
src/css/README.md
Normal file
@@ -0,0 +1,218 @@
|
||||
# css
|
||||
|
||||
Lightpanda css implements CSS selectors parsing and matching in Zig.
|
||||
This package is a port of the Go lib [andybalholm/cascadia](https://github.com/andybalholm/cascadia).
|
||||
|
||||
## Usage
|
||||
|
||||
### Query parser
|
||||
|
||||
```zig
|
||||
const css = @import("css.zig");
|
||||
|
||||
const selector = try css.parse(alloc, "h1", .{});
|
||||
defer selector.deinit(alloc);
|
||||
```
|
||||
|
||||
### DOM tree match
|
||||
|
||||
The lib expects a `Node` interface implementation to match your DOM tree.
|
||||
|
||||
```zig
|
||||
pub const Node = struct {
|
||||
pub fn firstChild(_: Node) !?Node {
|
||||
return error.TODO;
|
||||
}
|
||||
|
||||
pub fn lastChild(_: Node) !?Node {
|
||||
return error.TODO;
|
||||
}
|
||||
|
||||
pub fn nextSibling(_: Node) !?Node {
|
||||
return error.TODO;
|
||||
}
|
||||
|
||||
pub fn prevSibling(_: Node) !?Node {
|
||||
return error.TODO;
|
||||
}
|
||||
|
||||
pub fn parent(_: Node) !?Node {
|
||||
return error.TODO;
|
||||
}
|
||||
|
||||
pub fn isElement(_: Node) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn isDocument(_: Node) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn isComment(_: Node) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn isText(_: Node) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn isEmptyText(_: Node) !bool {
|
||||
return error.TODO;
|
||||
}
|
||||
|
||||
pub fn tag(_: Node) ![]const u8 {
|
||||
return error.TODO;
|
||||
}
|
||||
|
||||
pub fn attr(_: Node, _: []const u8) !?[]const u8 {
|
||||
return error.TODO;
|
||||
}
|
||||
|
||||
pub fn eql(_: Node, _: Node) bool {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
You also need do define a `Matcher` implementing a `match` function to
|
||||
accumulate the results.
|
||||
|
||||
```zig
|
||||
const Matcher = struct {
|
||||
const Nodes = std.ArrayList(Node);
|
||||
|
||||
nodes: Nodes,
|
||||
|
||||
fn init(alloc: std.mem.Allocator) Matcher {
|
||||
return .{ .nodes = Nodes.init(alloc) };
|
||||
}
|
||||
|
||||
fn deinit(m: *Matcher) void {
|
||||
m.nodes.deinit();
|
||||
}
|
||||
|
||||
pub fn match(m: *Matcher, n: Node) !void {
|
||||
try m.nodes.append(n);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Then you can use the lib itself.
|
||||
|
||||
```zig
|
||||
var matcher = Matcher.init(alloc);
|
||||
defer matcher.deinit();
|
||||
|
||||
try css.matchAll(selector, node, &matcher);
|
||||
_ = try css.matchFirst(selector, node, &matcher); // returns true if a node matched.
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
* [x] parse query selector
|
||||
* [x] `matchAll`
|
||||
* [x] `matchFirst`
|
||||
* [ ] specificity
|
||||
|
||||
### Selectors implemented
|
||||
|
||||
#### Selectors
|
||||
|
||||
* [x] Class selectors
|
||||
* [x] Id selectors
|
||||
* [x] Type selectors
|
||||
* [x] Universal selectors
|
||||
* [ ] Nesting selectors
|
||||
|
||||
#### Combinators
|
||||
|
||||
* [x] Child combinator
|
||||
* [ ] Column combinator
|
||||
* [x] Descendant combinator
|
||||
* [ ] Namespace combinator
|
||||
* [x] Next-sibling combinator
|
||||
* [x] Selector list combinator
|
||||
* [x] Subsequent-sibling combinator
|
||||
|
||||
#### Attribute
|
||||
|
||||
* [x] `[attr]`
|
||||
* [x] `[attr=value]`
|
||||
* [x] `[attr|=value]`
|
||||
* [x] `[attr^=value]`
|
||||
* [x] `[attr$=value]`
|
||||
* [ ] `[attr*=value]`
|
||||
* [x] `[attr operator value i]`
|
||||
* [ ] `[attr operator value s]`
|
||||
|
||||
#### Pseudo classes
|
||||
|
||||
* [ ] `:active`
|
||||
* [ ] `:any-link`
|
||||
* [ ] `:autofill`
|
||||
* [ ] `:blank Experimental`
|
||||
* [x] `:checked`
|
||||
* [ ] `:current Experimental`
|
||||
* [ ] `:default`
|
||||
* [ ] `:defined`
|
||||
* [ ] `:dir() Experimental`
|
||||
* [x] `:disabled`
|
||||
* [x] `:empty`
|
||||
* [x] `:enabled`
|
||||
* [ ] `:first`
|
||||
* [x] `:first-child`
|
||||
* [x] `:first-of-type`
|
||||
* [ ] `:focus`
|
||||
* [ ] `:focus-visible`
|
||||
* [ ] `:focus-within`
|
||||
* [ ] `:fullscreen`
|
||||
* [ ] `:future Experimental`
|
||||
* [x] `:has() Experimental`
|
||||
* [ ] `:host`
|
||||
* [ ] `:host()`
|
||||
* [ ] `:host-context() Experimental`
|
||||
* [ ] `:hover`
|
||||
* [ ] `:indeterminate`
|
||||
* [ ] `:in-range`
|
||||
* [ ] `:invalid`
|
||||
* [ ] `:is()`
|
||||
* [x] `:lang()`
|
||||
* [x] `:last-child`
|
||||
* [x] `:last-of-type`
|
||||
* [ ] `:left`
|
||||
* [x] `:link`
|
||||
* [ ] `:local-link Experimental`
|
||||
* [ ] `:modal`
|
||||
* [x] `:not()`
|
||||
* [x] `:nth-child()`
|
||||
* [x] `:nth-last-child()`
|
||||
* [x] `:nth-last-of-type()`
|
||||
* [x] `:nth-of-type()`
|
||||
* [x] `:only-child`
|
||||
* [x] `:only-of-type`
|
||||
* [ ] `:optional`
|
||||
* [ ] `:out-of-range`
|
||||
* [ ] `:past Experimental`
|
||||
* [ ] `:paused`
|
||||
* [ ] `:picture-in-picture`
|
||||
* [ ] `:placeholder-shown`
|
||||
* [ ] `:playing`
|
||||
* [ ] `:read-only`
|
||||
* [ ] `:read-write`
|
||||
* [ ] `:required`
|
||||
* [ ] `:right`
|
||||
* [x] `:root`
|
||||
* [ ] `:scope`
|
||||
* [ ] `:state() Experimental`
|
||||
* [ ] `:target`
|
||||
* [ ] `:target-within Experimental`
|
||||
* [ ] `:user-invalid Experimental`
|
||||
* [ ] `:valid`
|
||||
* [ ] `:visited`
|
||||
* [ ] `:where()`
|
||||
* [ ] `:contains()`
|
||||
* [ ] `:containsown()`
|
||||
* [ ] `:matched()`
|
||||
* [ ] `:matchesown()`
|
||||
* [x] `:root`
|
||||
|
||||
176
src/css/css.zig
Normal file
176
src/css/css.zig
Normal file
@@ -0,0 +1,176 @@
|
||||
// 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/>.
|
||||
|
||||
// CSS Selector parser and query
|
||||
// This package is a rewrite in Zig of Cascadia CSS Selector parser.
|
||||
// see https://github.com/andybalholm/cascadia
|
||||
const std = @import("std");
|
||||
const Selector = @import("selector.zig").Selector;
|
||||
const parser = @import("parser.zig");
|
||||
|
||||
// parse parse a selector string and returns the parsed result or an error.
|
||||
pub fn parse(alloc: std.mem.Allocator, s: []const u8, opts: parser.ParseOptions) parser.ParseError!Selector {
|
||||
var p = parser.Parser{ .s = s, .i = 0, .opts = opts };
|
||||
return p.parse(alloc);
|
||||
}
|
||||
|
||||
// matchFirst call m.match with the first node that matches the selector s, from the
|
||||
// descendants of n and returns true. If none matches, it returns false.
|
||||
pub fn matchFirst(s: Selector, node: anytype, m: anytype) !bool {
|
||||
var c = try node.firstChild();
|
||||
while (true) {
|
||||
if (c == null) break;
|
||||
|
||||
if (try s.match(c.?)) {
|
||||
try m.match(c.?);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (try matchFirst(s, c.?, m)) return true;
|
||||
c = try c.?.nextSibling();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// matchAll call m.match with the all the nodes that matches the selector s, from the
|
||||
// descendants of n.
|
||||
pub fn matchAll(s: Selector, node: anytype, m: anytype) !void {
|
||||
var c = try node.firstChild();
|
||||
while (true) {
|
||||
if (c == null) break;
|
||||
|
||||
if (try s.match(c.?)) try m.match(c.?);
|
||||
try matchAll(s, c.?, m);
|
||||
c = try c.?.nextSibling();
|
||||
}
|
||||
}
|
||||
|
||||
test "parse" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
const testcases = [_][]const u8{
|
||||
"address",
|
||||
"*",
|
||||
"#foo",
|
||||
"li#t1",
|
||||
"*#t4",
|
||||
".t1",
|
||||
"p.t1",
|
||||
"div.teST",
|
||||
".t1.fail",
|
||||
"p.t1.t2",
|
||||
"p.--t1",
|
||||
"p.--t1.--t2",
|
||||
"p[title]",
|
||||
"div[class=\"red\" i]",
|
||||
"address[title=\"foo\"]",
|
||||
"address[title=\"FoOIgnoRECaSe\" i]",
|
||||
"address[title!=\"foo\"]",
|
||||
"address[title!=\"foo\" i]",
|
||||
"p[title!=\"FooBarUFoo\" i]",
|
||||
"[ \t title ~= foo ]",
|
||||
"p[title~=\"FOO\" i]",
|
||||
"p[title~=toofoo i]",
|
||||
"[title~=\"hello world\"]",
|
||||
"[title~=\"hello\" i]",
|
||||
"[title~=\"hello\" I]",
|
||||
"[lang|=\"en\"]",
|
||||
"[lang|=\"EN\" i]",
|
||||
"[lang|=\"EN\" i]",
|
||||
"[title^=\"foo\"]",
|
||||
"[title^=\"foo\" i]",
|
||||
"[title$=\"bar\"]",
|
||||
"[title$=\"BAR\" i]",
|
||||
"[title*=\"bar\"]",
|
||||
"[title*=\"BaRu\" i]",
|
||||
"[title*=\"BaRu\" I]",
|
||||
"p[class$=\" \"]",
|
||||
"p[class$=\"\"]",
|
||||
"p[class^=\" \"]",
|
||||
"p[class^=\"\"]",
|
||||
"p[class*=\" \"]",
|
||||
"p[class*=\"\"]",
|
||||
"input[name=Sex][value=F]",
|
||||
"table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]",
|
||||
".t1:not(.t2)",
|
||||
"div:not(.t1)",
|
||||
"div:not([class=\"t2\"])",
|
||||
"li:nth-child(odd)",
|
||||
"li:nth-child(even)",
|
||||
"li:nth-child(-n+2)",
|
||||
"li:nth-child(3n+1)",
|
||||
"li:nth-last-child(odd)",
|
||||
"li:nth-last-child(even)",
|
||||
"li:nth-last-child(-n+2)",
|
||||
"li:nth-last-child(3n+1)",
|
||||
"span:first-child",
|
||||
"span:last-child",
|
||||
"p:nth-of-type(2)",
|
||||
"p:nth-last-of-type(2)",
|
||||
"p:last-of-type",
|
||||
"p:first-of-type",
|
||||
"p:only-child",
|
||||
"p:only-of-type",
|
||||
":empty",
|
||||
"div p",
|
||||
"div table p",
|
||||
"div > p",
|
||||
"p ~ p",
|
||||
"p + p",
|
||||
"li, p",
|
||||
"p +/*This is a comment*/ p",
|
||||
"p:contains(\"that wraps\")",
|
||||
"p:containsOwn(\"that wraps\")",
|
||||
":containsOwn(\"inner\")",
|
||||
"p:containsOwn(\"block\")",
|
||||
"div:has(#p1)",
|
||||
"div:has(:containsOwn(\"2\"))",
|
||||
"body :has(:containsOwn(\"2\"))",
|
||||
"body :haschild(:containsOwn(\"2\"))",
|
||||
"p:matches([\\d])",
|
||||
"p:matches([a-z])",
|
||||
"p:matches([a-zA-Z])",
|
||||
"p:matches([^\\d])",
|
||||
"p:matches(^(0|a))",
|
||||
"p:matches(^\\d+$)",
|
||||
"p:not(:matches(^\\d+$))",
|
||||
"div :matchesOwn(^\\d+$)",
|
||||
"[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])",
|
||||
"[href#=(^https:\\/\\/[^\\/]*\\/?news)]",
|
||||
":input",
|
||||
":root",
|
||||
"*:root",
|
||||
"html:nth-child(1)",
|
||||
"*:root:first-child",
|
||||
"*:root:nth-child(1)",
|
||||
"a:not(:root)",
|
||||
"body > *:nth-child(3n+2)",
|
||||
"input:disabled",
|
||||
":disabled",
|
||||
":enabled",
|
||||
"div.class1, div.class2",
|
||||
};
|
||||
|
||||
for (testcases) |tc| {
|
||||
const s = parse(alloc, tc, .{}) catch |e| {
|
||||
std.debug.print("query {s}", .{tc});
|
||||
return e;
|
||||
};
|
||||
defer s.deinit(alloc);
|
||||
}
|
||||
}
|
||||
102
src/css/libdom.zig
Normal file
102
src/css/libdom.zig
Normal file
@@ -0,0 +1,102 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
|
||||
// Node implementation with Netsurf Libdom C lib.
|
||||
pub const Node = struct {
|
||||
node: *parser.Node,
|
||||
|
||||
pub fn firstChild(n: Node) !?Node {
|
||||
const c = try parser.nodeFirstChild(n.node);
|
||||
if (c) |cc| return .{ .node = cc };
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn lastChild(n: Node) !?Node {
|
||||
const c = try parser.nodeLastChild(n.node);
|
||||
if (c) |cc| return .{ .node = cc };
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn nextSibling(n: Node) !?Node {
|
||||
const c = try parser.nodeNextSibling(n.node);
|
||||
if (c) |cc| return .{ .node = cc };
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn prevSibling(n: Node) !?Node {
|
||||
const c = try parser.nodePreviousSibling(n.node);
|
||||
if (c) |cc| return .{ .node = cc };
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn parent(n: Node) !?Node {
|
||||
const c = try parser.nodeParentNode(n.node);
|
||||
if (c) |cc| return .{ .node = cc };
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn isElement(n: Node) bool {
|
||||
const t = parser.nodeType(n.node) catch return false;
|
||||
return t == .element;
|
||||
}
|
||||
|
||||
pub fn isDocument(n: Node) bool {
|
||||
const t = parser.nodeType(n.node) catch return false;
|
||||
return t == .document;
|
||||
}
|
||||
|
||||
pub fn isComment(n: Node) bool {
|
||||
const t = parser.nodeType(n.node) catch return false;
|
||||
return t == .comment;
|
||||
}
|
||||
|
||||
pub fn isText(n: Node) bool {
|
||||
const t = parser.nodeType(n.node) catch return false;
|
||||
return t == .text;
|
||||
}
|
||||
|
||||
pub fn isEmptyText(n: Node) !bool {
|
||||
const data = try parser.nodeTextContent(n.node);
|
||||
if (data == null) return true;
|
||||
if (data.?.len == 0) return true;
|
||||
|
||||
return std.mem.trim(u8, data.?, &std.ascii.whitespace).len == 0;
|
||||
}
|
||||
|
||||
pub fn tag(n: Node) ![]const u8 {
|
||||
return try parser.nodeName(n.node);
|
||||
}
|
||||
|
||||
pub fn attr(n: Node, key: []const u8) !?[]const u8 {
|
||||
if (!n.isElement()) return null;
|
||||
return try parser.elementGetAttribute(parser.nodeToElement(n.node), key);
|
||||
}
|
||||
|
||||
pub fn eql(a: Node, b: Node) bool {
|
||||
return a.node == b.node;
|
||||
}
|
||||
};
|
||||
325
src/css/libdom_test.zig
Normal file
325
src/css/libdom_test.zig
Normal file
@@ -0,0 +1,325 @@
|
||||
// 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 css = @import("css.zig");
|
||||
const Node = @import("libdom.zig").Node;
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const Matcher = struct {
|
||||
const Nodes = std.ArrayList(Node);
|
||||
|
||||
nodes: Nodes,
|
||||
|
||||
fn init(alloc: std.mem.Allocator) Matcher {
|
||||
return .{ .nodes = Nodes.init(alloc) };
|
||||
}
|
||||
|
||||
fn deinit(m: *Matcher) void {
|
||||
m.nodes.deinit();
|
||||
}
|
||||
|
||||
fn reset(m: *Matcher) void {
|
||||
m.nodes.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
pub fn match(m: *Matcher, n: Node) !void {
|
||||
try m.nodes.append(n);
|
||||
}
|
||||
};
|
||||
|
||||
test "matchFirst" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
var matcher = Matcher.init(alloc);
|
||||
defer matcher.deinit();
|
||||
|
||||
const testcases = [_]struct {
|
||||
q: []const u8,
|
||||
html: []const u8,
|
||||
exp: usize,
|
||||
}{
|
||||
.{ .q = "address", .html = "<body><address>This address...</address></body>", .exp = 1 },
|
||||
.{ .q = "*", .html = "<!-- comment --><html><head></head><body>text</body></html>", .exp = 1 },
|
||||
.{ .q = "*", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "#foo", .html = "<p id=\"foo\"><p id=\"bar\">", .exp = 1 },
|
||||
.{ .q = "li#t1", .html = "<ul><li id=\"t1\"><p id=\"t1\">", .exp = 1 },
|
||||
.{ .q = ".t3", .html = "<ul><li class=\"t1\"><li class=\"t2 t3\">", .exp = 1 },
|
||||
.{ .q = "*#t4", .html = "<ol><li id=\"t4\"><li id=\"t44\">", .exp = 1 },
|
||||
.{ .q = ".t1", .html = "<ul><li class=\"t1\"><li class=\"t2\">", .exp = 1 },
|
||||
.{ .q = "p.t1", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
||||
.{ .q = "div.teST", .html = "<div class=\"test\">", .exp = 0 },
|
||||
.{ .q = ".t1.fail", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
||||
.{ .q = "p.t1.t2", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
||||
.{ .q = "p.--t1", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
||||
.{ .q = "p.--t1.--t2", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
||||
.{ .q = "p[title]", .html = "<p><p title=\"title\">", .exp = 1 },
|
||||
.{ .q = "div[class=\"red\" i]", .html = "<div><div class=\"Red\">", .exp = 1 },
|
||||
.{ .q = "address[title=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
||||
.{ .q = "address[title=\"FoOIgnoRECaSe\" i]", .html = "<address><address title=\"fooIgnoreCase\"><address title=\"bar\">", .exp = 1 },
|
||||
.{ .q = "address[title!=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
||||
.{ .q = "address[title!=\"foo\" i]", .html = "<address><address title=\"FOO\"><address title=\"bar\">", .exp = 1 },
|
||||
.{ .q = "p[title!=\"FooBarUFoo\" i]", .html = "<p title=\"fooBARuFOO\"><p title=\"varfoo\">", .exp = 1 },
|
||||
.{ .q = "[ title ~= foo ]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
||||
.{ .q = "p[title~=\"FOO\" i]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
||||
.{ .q = "p[title~=toofoo i]", .html = "<p title=\"tot foo bar\">", .exp = 0 },
|
||||
.{ .q = "[title~=\"hello world\"]", .html = "<p title=\"hello world\">", .exp = 0 },
|
||||
.{ .q = "[title~=\"hello\" i]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
||||
.{ .q = "[title~=\"hello\" I]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
||||
.{ .q = "[lang|=\"en\"]", .html = "<p lang=\"en\"><p lang=\"en-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
||||
.{ .q = "[title^=\"foo\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||
.{ .q = "[title^=\"foo\" i]", .html = "<p title=\"FooBAR\"><p title=\"barfoo\">", .exp = 1 },
|
||||
.{ .q = "[title$=\"bar\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||
.{ .q = "[title$=\"BAR\" i]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||
.{ .q = "[title*=\"bar\"]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||
.{ .q = "[title*=\"BaRu\" i]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||
.{ .q = "[title*=\"BaRu\" I]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||
.{ .q = "p[class$=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class$=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class^=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class^=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class*=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class*=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "input[name=Sex][value=F]", .html = "<input type=\"radio\" name=\"Sex\" value=\"F\"/>", .exp = 1 },
|
||||
.{ .q = "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]", .html = "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout: fixed; width: 100%; border: 0 dashed; border-color: #FFFFFF\"><tr style=\"height:64px\">aaa</tr></table>", .exp = 1 },
|
||||
.{ .q = ".t1:not(.t2)", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
||||
.{ .q = "div:not(.t1)", .html = "<div class=\"t3\">", .exp = 1 },
|
||||
.{ .q = "div:not([class=\"t2\"])", .html = "<div><div class=\"t2\"><div class=\"t3\">", .exp = 1 },
|
||||
.{ .q = "li:nth-child(odd)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-child(even)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-last-child(odd)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-last-child(even)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-last-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-last-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
||||
.{ .q = "span:first-child", .html = "<p>some text <span id=\"1\">and a span</span><span id=\"2\"> and another</span></p>", .exp = 1 },
|
||||
.{ .q = "span:last-child", .html = "<span>a span</span> and some text", .exp = 1 },
|
||||
.{ .q = "p:nth-of-type(2)", .html = "<address></address><p id=1><p id=2>", .exp = 1 },
|
||||
.{ .q = "p:nth-last-of-type(2)", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||
.{ .q = "p:last-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||
.{ .q = "p:first-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||
.{ .q = "p:only-child", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p></div>", .exp = 1 },
|
||||
.{ .q = "p:only-of-type", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p><p id=\"3\"></p></div>", .exp = 1 },
|
||||
.{ .q = ":empty", .html = "<p id=\"1\"><!-- --><p id=\"2\">Hello<p id=\"3\"><span>", .exp = 1 },
|
||||
.{ .q = "div p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
||||
.{ .q = "div table p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
||||
.{ .q = "div > p", .html = "<div><p id=\"1\"><div><p id=\"2\"></div><table><tr><td><p id=\"3\"></table></div>", .exp = 1 },
|
||||
.{ .q = "p ~ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
||||
.{ .q = "p + p", .html = "<p id=\"1\"></p> <!--comment--> <p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
||||
.{ .q = "li, p", .html = "<ul><li></li><li></li></ul><p>", .exp = 1 },
|
||||
.{ .q = "p +/*This is a comment*/ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
||||
// .{ .q = "p:contains(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||
// .{ .q = "p:containsOwn(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
|
||||
// .{ .q = ":containsOwn(\"inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||
// .{ .q = "p:containsOwn(\"block\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||
// .{ .q = "div:has(#p1)", .html = "<div id=\"d1\"><p id=\"p1\"><span>text content</span></p></div><div id=\"d2\"/>", .exp = 1 },
|
||||
// .{ .q = "div:has(:containsOwn(\"2\"))", .html = "<div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p>contents <em>2</em></p></div>", .exp = 1 },
|
||||
// .{ .q = "body :has(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
||||
// .{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
||||
// .{ .q = "p:matches([\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "p:matches([a-z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "p:matches([a-zA-Z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "p:matches([^\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "p:matches(^(0|a))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "p:matches(^\\d+$)", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "p:not(:matches(^\\d+$))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "div :matchesOwn(^\\d+$)", .html = "<div><p id=\"p1\">01234<em>567</em>89</p><div>", .exp = 1 },
|
||||
// .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"></a> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"></a> <li><a id=\"a2\" href=\"http://finance.untrusted.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"/> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
||||
// .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"/> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"></a> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
||||
.{ .q = ":input", .html = "<form> <label>Username <input type=\"text\" name=\"username\" /></label> <label>Password <input type=\"password\" name=\"password\" /></label> <label>Country <select name=\"country\"> <option value=\"ca\">Canada</option> <option value=\"us\">United States</option> </select> </label> <label>Bio <textarea name=\"bio\"></textarea></label> <button>Sign up</button> </form>", .exp = 1 },
|
||||
.{ .q = ":root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "*:root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "html:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "*:root:first-child", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "*:root:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "a:not(:root)", .html = "<html><head></head><body><a href=\"http://www.foo.com\"></a></body></html>", .exp = 1 },
|
||||
.{ .q = "body > *:nth-child(3n+2)", .html = "<html><head></head><body><p></p><div></div><span></span><a></a><form></form></body></html>", .exp = 1 },
|
||||
.{ .q = "input:disabled", .html = "<html><head></head><body><fieldset disabled><legend id=\"1\"><input id=\"i1\"/></legend><legend id=\"2\"><input id=\"i2\"/></legend></fieldset></body></html>", .exp = 1 },
|
||||
.{ .q = ":disabled", .html = "<html><head></head><body><fieldset disabled></fieldset></body></html>", .exp = 1 },
|
||||
.{ .q = ":enabled", .html = "<html><head></head><body><fieldset></fieldset></body></html>", .exp = 1 },
|
||||
.{ .q = "div.class1, div.class2", .html = "<div class=class1></div><div class=class2></div><div class=class3></div>", .exp = 1 },
|
||||
};
|
||||
|
||||
for (testcases) |tc| {
|
||||
matcher.reset();
|
||||
|
||||
const doc = try parser.documentHTMLParseFromStr(tc.html);
|
||||
defer parser.documentHTMLClose(doc) catch {};
|
||||
|
||||
const s = css.parse(alloc, tc.q, .{}) catch |e| {
|
||||
std.debug.print("parse, query: {s}\n", .{tc.q});
|
||||
return e;
|
||||
};
|
||||
|
||||
defer s.deinit(alloc);
|
||||
|
||||
const node = Node{ .node = parser.documentHTMLToNode(doc) };
|
||||
|
||||
_ = css.matchFirst(s, node, &matcher) catch |e| {
|
||||
std.debug.print("match, query: {s}\n", .{tc.q});
|
||||
return e;
|
||||
};
|
||||
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
|
||||
std.debug.print("expectation, query: {s}\n", .{tc.q});
|
||||
return e;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test "matchAll" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
var matcher = Matcher.init(alloc);
|
||||
defer matcher.deinit();
|
||||
|
||||
const testcases = [_]struct {
|
||||
q: []const u8,
|
||||
html: []const u8,
|
||||
exp: usize,
|
||||
}{
|
||||
.{ .q = "address", .html = "<body><address>This address...</address></body>", .exp = 1 },
|
||||
.{ .q = "*", .html = "<!-- comment --><html><head></head><body>text</body></html>", .exp = 3 },
|
||||
.{ .q = "*", .html = "<html><head></head><body></body></html>", .exp = 3 },
|
||||
.{ .q = "#foo", .html = "<p id=\"foo\"><p id=\"bar\">", .exp = 1 },
|
||||
.{ .q = "li#t1", .html = "<ul><li id=\"t1\"><p id=\"t1\">", .exp = 1 },
|
||||
.{ .q = ".t3", .html = "<ul><li class=\"t1\"><li class=\"t2 t3\">", .exp = 1 },
|
||||
.{ .q = "*#t4", .html = "<ol><li id=\"t4\"><li id=\"t44\">", .exp = 1 },
|
||||
.{ .q = ".t1", .html = "<ul><li class=\"t1\"><li class=\"t2\">", .exp = 1 },
|
||||
.{ .q = "p.t1", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
||||
.{ .q = "div.teST", .html = "<div class=\"test\">", .exp = 0 },
|
||||
.{ .q = ".t1.fail", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
||||
.{ .q = "p.t1.t2", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
||||
.{ .q = "p.--t1", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
||||
.{ .q = "p.--t1.--t2", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
||||
.{ .q = "p[title]", .html = "<p><p title=\"title\">", .exp = 1 },
|
||||
.{ .q = "div[class=\"red\" i]", .html = "<div><div class=\"Red\">", .exp = 1 },
|
||||
.{ .q = "address[title=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
||||
.{ .q = "address[title=\"FoOIgnoRECaSe\" i]", .html = "<address><address title=\"fooIgnoreCase\"><address title=\"bar\">", .exp = 1 },
|
||||
.{ .q = "address[title!=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 2 },
|
||||
.{ .q = "address[title!=\"foo\" i]", .html = "<address><address title=\"FOO\"><address title=\"bar\">", .exp = 2 },
|
||||
.{ .q = "p[title!=\"FooBarUFoo\" i]", .html = "<p title=\"fooBARuFOO\"><p title=\"varfoo\">", .exp = 1 },
|
||||
.{ .q = "[ title ~= foo ]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
||||
.{ .q = "p[title~=\"FOO\" i]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
||||
.{ .q = "p[title~=toofoo i]", .html = "<p title=\"tot foo bar\">", .exp = 0 },
|
||||
.{ .q = "[title~=\"hello world\"]", .html = "<p title=\"hello world\">", .exp = 0 },
|
||||
.{ .q = "[title~=\"hello\" i]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
||||
.{ .q = "[title~=\"hello\" I]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
||||
.{ .q = "[lang|=\"en\"]", .html = "<p lang=\"en\"><p lang=\"en-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
||||
.{ .q = "[title^=\"foo\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||
.{ .q = "[title^=\"foo\" i]", .html = "<p title=\"FooBAR\"><p title=\"barfoo\">", .exp = 1 },
|
||||
.{ .q = "[title$=\"bar\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||
.{ .q = "[title$=\"BAR\" i]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||
.{ .q = "[title*=\"bar\"]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||
.{ .q = "[title*=\"BaRu\" i]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||
.{ .q = "[title*=\"BaRu\" I]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||
.{ .q = "p[class$=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class$=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class^=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class^=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class*=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class*=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "input[name=Sex][value=F]", .html = "<input type=\"radio\" name=\"Sex\" value=\"F\"/>", .exp = 1 },
|
||||
.{ .q = "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]", .html = "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout: fixed; width: 100%; border: 0 dashed; border-color: #FFFFFF\"><tr style=\"height:64px\">aaa</tr></table>", .exp = 1 },
|
||||
.{ .q = ".t1:not(.t2)", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
||||
.{ .q = "div:not(.t1)", .html = "<div class=\"t3\">", .exp = 1 },
|
||||
.{ .q = "div:not([class=\"t2\"])", .html = "<div><div class=\"t2\"><div class=\"t3\">", .exp = 2 },
|
||||
.{ .q = "li:nth-child(odd)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 2 },
|
||||
.{ .q = "li:nth-child(even)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 2 },
|
||||
.{ .q = "li:nth-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-last-child(odd)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
||||
.{ .q = "li:nth-last-child(even)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
||||
.{ .q = "li:nth-last-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
||||
.{ .q = "li:nth-last-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
||||
.{ .q = "span:first-child", .html = "<p>some text <span id=\"1\">and a span</span><span id=\"2\"> and another</span></p>", .exp = 1 },
|
||||
.{ .q = "span:last-child", .html = "<span>a span</span> and some text", .exp = 1 },
|
||||
.{ .q = "p:nth-of-type(2)", .html = "<address></address><p id=1><p id=2>", .exp = 1 },
|
||||
.{ .q = "p:nth-last-of-type(2)", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||
.{ .q = "p:last-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||
.{ .q = "p:first-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||
.{ .q = "p:only-child", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p></div>", .exp = 1 },
|
||||
.{ .q = "p:only-of-type", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p><p id=\"3\"></p></div>", .exp = 1 },
|
||||
.{ .q = ":empty", .html = "<p id=\"1\"><!-- --><p id=\"2\">Hello<p id=\"3\"><span>", .exp = 3 },
|
||||
.{ .q = "div p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 2 },
|
||||
.{ .q = "div table p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
||||
.{ .q = "div > p", .html = "<div><p id=\"1\"><div><p id=\"2\"></div><table><tr><td><p id=\"3\"></table></div>", .exp = 2 },
|
||||
.{ .q = "p ~ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 2 },
|
||||
.{ .q = "p + p", .html = "<p id=\"1\"></p> <!--comment--> <p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
||||
.{ .q = "li, p", .html = "<ul><li></li><li></li></ul><p>", .exp = 3 },
|
||||
.{ .q = "p +/*This is a comment*/ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
||||
// .{ .q = "p:contains(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||
// .{ .q = "p:containsOwn(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
|
||||
// .{ .q = ":containsOwn(\"inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||
// .{ .q = "p:containsOwn(\"block\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||
.{ .q = "div:has(#p1)", .html = "<div id=\"d1\"><p id=\"p1\"><span>text content</span></p></div><div id=\"d2\"/>", .exp = 1 },
|
||||
// .{ .q = "div:has(:containsOwn(\"2\"))", .html = "<div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p>contents <em>2</em></p></div>", .exp = 1 },
|
||||
// .{ .q = "body :has(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 2 },
|
||||
// .{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
||||
// .{ .q = "p:matches([\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
||||
// .{ .q = "p:matches([a-z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "p:matches([a-zA-Z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
||||
// .{ .q = "p:matches([^\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
||||
// .{ .q = "p:matches(^(0|a))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 3 },
|
||||
// .{ .q = "p:matches(^\\d+$)", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "p:not(:matches(^\\d+$))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
||||
// .{ .q = "div :matchesOwn(^\\d+$)", .html = "<div><p id=\"p1\">01234<em>567</em>89</p><div>", .exp = 2 },
|
||||
// .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"></a> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"></a> <li><a id=\"a2\" href=\"http://finance.untrusted.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"/> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 2 },
|
||||
// .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"/> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"></a> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
||||
.{ .q = ":input", .html = "<form> <label>Username <input type=\"text\" name=\"username\" /></label> <label>Password <input type=\"password\" name=\"password\" /></label> <label>Country <select name=\"country\"> <option value=\"ca\">Canada</option> <option value=\"us\">United States</option> </select> </label> <label>Bio <textarea name=\"bio\"></textarea></label> <button>Sign up</button> </form>", .exp = 5 },
|
||||
.{ .q = ":root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "*:root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "html:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "*:root:first-child", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "*:root:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "a:not(:root)", .html = "<html><head></head><body><a href=\"http://www.foo.com\"></a></body></html>", .exp = 1 },
|
||||
.{ .q = "body > *:nth-child(3n+2)", .html = "<html><head></head><body><p></p><div></div><span></span><a></a><form></form></body></html>", .exp = 2 },
|
||||
.{ .q = "input:disabled", .html = "<html><head></head><body><fieldset disabled><legend id=\"1\"><input id=\"i1\"/></legend><legend id=\"2\"><input id=\"i2\"/></legend></fieldset></body></html>", .exp = 1 },
|
||||
.{ .q = ":disabled", .html = "<html><head></head><body><fieldset disabled></fieldset></body></html>", .exp = 1 },
|
||||
.{ .q = ":enabled", .html = "<html><head></head><body><fieldset></fieldset></body></html>", .exp = 1 },
|
||||
.{ .q = "div.class1, div.class2", .html = "<div class=class1></div><div class=class2></div><div class=class3></div>", .exp = 2 },
|
||||
};
|
||||
|
||||
for (testcases) |tc| {
|
||||
matcher.reset();
|
||||
|
||||
const doc = try parser.documentHTMLParseFromStr(tc.html);
|
||||
defer parser.documentHTMLClose(doc) catch {};
|
||||
|
||||
const s = css.parse(alloc, tc.q, .{}) catch |e| {
|
||||
std.debug.print("parse, query: {s}\n", .{tc.q});
|
||||
return e;
|
||||
};
|
||||
defer s.deinit(alloc);
|
||||
|
||||
const node = Node{ .node = parser.documentHTMLToNode(doc) };
|
||||
|
||||
_ = css.matchAll(s, node, &matcher) catch |e| {
|
||||
std.debug.print("match, query: {s}\n", .{tc.q});
|
||||
return e;
|
||||
};
|
||||
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
|
||||
std.debug.print("expectation, query: {s}\n", .{tc.q});
|
||||
return e;
|
||||
};
|
||||
}
|
||||
}
|
||||
587
src/css/match_test.zig
Normal file
587
src/css/match_test.zig
Normal file
@@ -0,0 +1,587 @@
|
||||
// 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 css = @import("css.zig");
|
||||
|
||||
// Node mock implementation for test only.
|
||||
pub const Node = struct {
|
||||
child: ?*const Node = null,
|
||||
last: ?*const Node = null,
|
||||
sibling: ?*const Node = null,
|
||||
prev: ?*const Node = null,
|
||||
par: ?*const Node = null,
|
||||
|
||||
name: []const u8 = "",
|
||||
att: ?[]const u8 = null,
|
||||
|
||||
pub fn firstChild(n: *const Node) !?*const Node {
|
||||
return n.child;
|
||||
}
|
||||
|
||||
pub fn lastChild(n: *const Node) !?*const Node {
|
||||
return n.last;
|
||||
}
|
||||
|
||||
pub fn nextSibling(n: *const Node) !?*const Node {
|
||||
return n.sibling;
|
||||
}
|
||||
|
||||
pub fn prevSibling(n: *const Node) !?*const Node {
|
||||
return n.prev;
|
||||
}
|
||||
|
||||
pub fn parent(n: *const Node) !?*const Node {
|
||||
return n.par;
|
||||
}
|
||||
|
||||
pub fn isElement(_: *const Node) bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn isDocument(_: *const Node) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn isComment(_: *const Node) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn isText(_: *const Node) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn isEmptyText(_: *const Node) !bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn tag(n: *const Node) ![]const u8 {
|
||||
return n.name;
|
||||
}
|
||||
|
||||
pub fn attr(n: *const Node, _: []const u8) !?[]const u8 {
|
||||
return n.att;
|
||||
}
|
||||
|
||||
pub fn eql(a: *const Node, b: *const Node) bool {
|
||||
return a == b;
|
||||
}
|
||||
};
|
||||
|
||||
const Matcher = struct {
|
||||
const Nodes = std.ArrayList(*const Node);
|
||||
|
||||
nodes: Nodes,
|
||||
|
||||
fn init(alloc: std.mem.Allocator) Matcher {
|
||||
return .{ .nodes = Nodes.init(alloc) };
|
||||
}
|
||||
|
||||
fn deinit(m: *Matcher) void {
|
||||
m.nodes.deinit();
|
||||
}
|
||||
|
||||
fn reset(m: *Matcher) void {
|
||||
m.nodes.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
pub fn match(m: *Matcher, n: *const Node) !void {
|
||||
try m.nodes.append(n);
|
||||
}
|
||||
};
|
||||
|
||||
test "matchFirst" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
var matcher = Matcher.init(alloc);
|
||||
defer matcher.deinit();
|
||||
|
||||
const testcases = [_]struct {
|
||||
q: []const u8,
|
||||
n: Node,
|
||||
exp: usize,
|
||||
}{
|
||||
.{
|
||||
.q = "address",
|
||||
.n = .{ .child = &.{ .name = "body", .child = &.{ .name = "address" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "#foo",
|
||||
.n = .{ .child = &.{ .name = "p", .att = "foo", .child = &.{ .name = "p" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = ".t1",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "t1" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = ".t1",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "foo t1" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "[foo]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo=baz]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "[foo!=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo!=baz]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo~=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "baz bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo~=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "[foo^=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo$=baz]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo*=rb]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo|=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo|=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar-baz" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo|=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "strong, a",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "p a",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .name = "p" } }, .sibling = &.{ .name = "a" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "p a",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "span", .child = &.{
|
||||
.name = "a",
|
||||
.par = &.{ .name = "span", .par = &.{ .name = "p" } },
|
||||
} } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = ":not(p)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "p:has(a)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "p:has(strong)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "p:haschild(a)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "p:haschild(strong)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "p:lang(en)",
|
||||
.n = .{ .child = &.{ .name = "p", .att = "en-US", .child = &.{ .name = "a" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "a:lang(en)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .att = "en-US" } } } },
|
||||
.exp = 1,
|
||||
},
|
||||
};
|
||||
|
||||
for (testcases) |tc| {
|
||||
matcher.reset();
|
||||
|
||||
const s = try css.parse(alloc, tc.q, .{});
|
||||
defer s.deinit(alloc);
|
||||
|
||||
_ = css.matchFirst(s, &tc.n, &matcher) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
|
||||
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test "matchAll" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
var matcher = Matcher.init(alloc);
|
||||
defer matcher.deinit();
|
||||
|
||||
const testcases = [_]struct {
|
||||
q: []const u8,
|
||||
n: Node,
|
||||
exp: usize,
|
||||
}{
|
||||
.{
|
||||
.q = "address",
|
||||
.n = .{ .child = &.{ .name = "body", .child = &.{ .name = "address" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "#foo",
|
||||
.n = .{ .child = &.{ .name = "p", .att = "foo", .child = &.{ .name = "p" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = ".t1",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "t1" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = ".t1",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "foo t1" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "[foo]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo=baz]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "[foo!=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo!=baz]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 2,
|
||||
},
|
||||
.{
|
||||
.q = "[foo~=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "baz bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo~=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "[foo^=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo$=baz]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo*=rb]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo|=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo|=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar-baz" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo|=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "strong, a",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 2,
|
||||
},
|
||||
.{
|
||||
.q = "p a",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .name = "p" } }, .sibling = &.{ .name = "a" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "p a",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "span", .child = &.{
|
||||
.name = "a",
|
||||
.par = &.{ .name = "span", .par = &.{ .name = "p" } },
|
||||
} } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = ":not(p)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 2,
|
||||
},
|
||||
.{
|
||||
.q = "p:has(a)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "p:has(strong)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "p:haschild(a)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "p:haschild(strong)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "p:lang(en)",
|
||||
.n = .{ .child = &.{ .name = "p", .att = "en-US", .child = &.{ .name = "a" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "a:lang(en)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .att = "en-US" } } } },
|
||||
.exp = 1,
|
||||
},
|
||||
};
|
||||
|
||||
for (testcases) |tc| {
|
||||
matcher.reset();
|
||||
|
||||
const s = try css.parse(alloc, tc.q, .{});
|
||||
defer s.deinit(alloc);
|
||||
|
||||
css.matchAll(s, &tc.n, &matcher) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
|
||||
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test "pseudo class" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
var matcher = Matcher.init(alloc);
|
||||
defer matcher.deinit();
|
||||
|
||||
var p1: Node = .{ .name = "p" };
|
||||
var p2: Node = .{ .name = "p" };
|
||||
var a1: Node = .{ .name = "a" };
|
||||
|
||||
p1.sibling = &p2;
|
||||
p2.prev = &p1;
|
||||
|
||||
p2.sibling = &a1;
|
||||
a1.prev = &p2;
|
||||
|
||||
var root: Node = .{ .child = &p1, .last = &a1 };
|
||||
p1.par = &root;
|
||||
p2.par = &root;
|
||||
a1.par = &root;
|
||||
|
||||
const testcases = [_]struct {
|
||||
q: []const u8,
|
||||
n: Node,
|
||||
exp: ?*const Node,
|
||||
}{
|
||||
.{ .q = "p:only-child", .n = root, .exp = null },
|
||||
.{ .q = "a:only-of-type", .n = root, .exp = &a1 },
|
||||
};
|
||||
|
||||
for (testcases) |tc| {
|
||||
matcher.reset();
|
||||
|
||||
const s = try css.parse(alloc, tc.q, .{});
|
||||
defer s.deinit(alloc);
|
||||
|
||||
css.matchAll(s, &tc.n, &matcher) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
|
||||
if (tc.exp) |exp_n| {
|
||||
const exp: usize = 1;
|
||||
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
|
||||
std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const exp: usize = 0;
|
||||
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test "nth pseudo class" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
var matcher = Matcher.init(alloc);
|
||||
defer matcher.deinit();
|
||||
|
||||
var p1: Node = .{ .name = "p" };
|
||||
var p2: Node = .{ .name = "p" };
|
||||
|
||||
p1.sibling = &p2;
|
||||
p2.prev = &p1;
|
||||
|
||||
var root: Node = .{ .child = &p1, .last = &p2 };
|
||||
p1.par = &root;
|
||||
p2.par = &root;
|
||||
|
||||
const testcases = [_]struct {
|
||||
q: []const u8,
|
||||
n: Node,
|
||||
exp: ?*const Node,
|
||||
}{
|
||||
.{ .q = "a:nth-of-type(1)", .n = root, .exp = null },
|
||||
.{ .q = "p:nth-of-type(1)", .n = root, .exp = &p1 },
|
||||
.{ .q = "p:nth-of-type(2)", .n = root, .exp = &p2 },
|
||||
.{ .q = "p:nth-of-type(0)", .n = root, .exp = null },
|
||||
.{ .q = "p:nth-of-type(2n)", .n = root, .exp = &p2 },
|
||||
.{ .q = "p:nth-last-child(1)", .n = root, .exp = &p2 },
|
||||
.{ .q = "p:nth-last-child(2)", .n = root, .exp = &p1 },
|
||||
.{ .q = "p:nth-child(1)", .n = root, .exp = &p1 },
|
||||
.{ .q = "p:nth-child(2)", .n = root, .exp = &p2 },
|
||||
.{ .q = "p:nth-child(odd)", .n = root, .exp = &p1 },
|
||||
.{ .q = "p:nth-child(even)", .n = root, .exp = &p2 },
|
||||
.{ .q = "p:nth-child(n+2)", .n = root, .exp = &p2 },
|
||||
};
|
||||
|
||||
for (testcases) |tc| {
|
||||
matcher.reset();
|
||||
|
||||
const s = try css.parse(alloc, tc.q, .{});
|
||||
defer s.deinit(alloc);
|
||||
|
||||
css.matchAll(s, &tc.n, &matcher) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
|
||||
if (tc.exp) |exp_n| {
|
||||
const exp: usize = 1;
|
||||
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
|
||||
std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const exp: usize = 0;
|
||||
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
}
|
||||
}
|
||||
917
src/css/parser.zig
Normal file
917
src/css/parser.zig
Normal file
@@ -0,0 +1,917 @@
|
||||
// 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/>.
|
||||
|
||||
// CSS Selector parser
|
||||
// This file is a rewrite in Zig of Cascadia CSS Selector parser.
|
||||
// see https://github.com/andybalholm/cascadia
|
||||
// see https://github.com/andybalholm/cascadia/blob/master/parser.go
|
||||
const std = @import("std");
|
||||
const ascii = std.ascii;
|
||||
|
||||
const selector = @import("selector.zig");
|
||||
const Selector = selector.Selector;
|
||||
const PseudoClass = selector.PseudoClass;
|
||||
const AttributeOP = selector.AttributeOP;
|
||||
const Combinator = selector.Combinator;
|
||||
|
||||
pub const ParseError = error{
|
||||
ExpectedSelector,
|
||||
ExpectedIdentifier,
|
||||
ExpectedName,
|
||||
ExpectedIDSelector,
|
||||
ExpectedClassSelector,
|
||||
ExpectedAttributeSelector,
|
||||
ExpectedString,
|
||||
ExpectedRegexp,
|
||||
ExpectedPseudoClassSelector,
|
||||
ExpectedParenthesis,
|
||||
ExpectedParenthesisClose,
|
||||
ExpectedNthExpression,
|
||||
ExpectedInteger,
|
||||
InvalidEscape,
|
||||
EscapeLineEndingOutsideString,
|
||||
InvalidUnicode,
|
||||
UnicodeIsNotHandled,
|
||||
WriteError,
|
||||
PseudoElementNotAtSelectorEnd,
|
||||
PseudoElementNotUnique,
|
||||
PseudoElementDisabled,
|
||||
InvalidAttributeOperator,
|
||||
InvalidAttributeSelector,
|
||||
InvalidString,
|
||||
InvalidRegexp,
|
||||
InvalidPseudoClassSelector,
|
||||
EmptyPseudoClassSelector,
|
||||
InvalidPseudoClass,
|
||||
InvalidPseudoElement,
|
||||
UnmatchParenthesis,
|
||||
NotHandled,
|
||||
UnknownPseudoSelector,
|
||||
InvalidNthExpression,
|
||||
} || PseudoClass.Error || Combinator.Error || std.mem.Allocator.Error;
|
||||
|
||||
pub const ParseOptions = struct {
|
||||
accept_pseudo_elts: bool = true,
|
||||
};
|
||||
|
||||
pub const Parser = struct {
|
||||
s: []const u8, // string to parse
|
||||
i: usize = 0, // current position
|
||||
|
||||
opts: ParseOptions,
|
||||
|
||||
pub fn parse(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
return p.parseSelectorGroup(alloc);
|
||||
}
|
||||
|
||||
// skipWhitespace consumes whitespace characters and comments.
|
||||
// It returns true if there was actually anything to skip.
|
||||
fn skipWhitespace(p: *Parser) bool {
|
||||
var i = p.i;
|
||||
while (i < p.s.len) {
|
||||
const c = p.s[i];
|
||||
// Whitespaces.
|
||||
if (ascii.isWhitespace(c)) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Comments.
|
||||
if (c == '/') {
|
||||
if (std.mem.startsWith(u8, p.s[i..], "/*")) {
|
||||
if (std.mem.indexOf(u8, p.s[i..], "*/")) |end| {
|
||||
i += end + "*/".len;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (i > p.i) {
|
||||
p.i = i;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// parseSimpleSelectorSequence parses a selector sequence that applies to
|
||||
// a single element.
|
||||
fn parseSimpleSelectorSequence(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
if (p.i >= p.s.len) {
|
||||
return ParseError.ExpectedSelector;
|
||||
}
|
||||
|
||||
var buf = std.ArrayList(Selector).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
switch (p.s[p.i]) {
|
||||
'*' => {
|
||||
// It's the universal selector. Just skip over it, since it
|
||||
// doesn't affect the meaning.
|
||||
p.i += 1;
|
||||
|
||||
// other version of universal selector
|
||||
if (p.i + 2 < p.s.len and std.mem.eql(u8, "|*", p.s[p.i .. p.i + 2])) {
|
||||
p.i += 2;
|
||||
}
|
||||
},
|
||||
'#', '.', '[', ':' => {
|
||||
// There's no type selector. Wait to process the other till the
|
||||
// main loop.
|
||||
},
|
||||
else => try buf.append(try p.parseTypeSelector(alloc)),
|
||||
}
|
||||
|
||||
var pseudo_elt: ?PseudoClass = null;
|
||||
|
||||
loop: while (p.i < p.s.len) {
|
||||
var ns: Selector = switch (p.s[p.i]) {
|
||||
'#' => try p.parseIDSelector(alloc),
|
||||
'.' => try p.parseClassSelector(alloc),
|
||||
'[' => try p.parseAttributeSelector(alloc),
|
||||
':' => try p.parsePseudoclassSelector(alloc),
|
||||
else => break :loop,
|
||||
};
|
||||
errdefer ns.deinit(alloc);
|
||||
|
||||
// From https://drafts.csswg.org/selectors-3/#pseudo-elements :
|
||||
// "Only one pseudo-element may appear per selector, and if present
|
||||
// it must appear after the sequence of simple selectors that
|
||||
// represents the subjects of the selector.""
|
||||
switch (ns) {
|
||||
.pseudo_element => |e| {
|
||||
// We found a pseudo-element.
|
||||
// Only one pseudo-element is accepted per selector.
|
||||
if (pseudo_elt != null) return ParseError.PseudoElementNotUnique;
|
||||
if (!p.opts.accept_pseudo_elts) return ParseError.PseudoElementDisabled;
|
||||
|
||||
pseudo_elt = e;
|
||||
ns.deinit(alloc);
|
||||
},
|
||||
else => {
|
||||
if (pseudo_elt != null) return ParseError.PseudoElementNotAtSelectorEnd;
|
||||
try buf.append(ns);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// no need wrap the selectors in compoundSelector
|
||||
if (buf.items.len == 1 and pseudo_elt == null) return buf.items[0];
|
||||
|
||||
return .{ .compound = .{ .selectors = try buf.toOwnedSlice(), .pseudo_elt = pseudo_elt } };
|
||||
}
|
||||
|
||||
// parseTypeSelector parses a type selector (one that matches by tag name).
|
||||
fn parseTypeSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
try p.parseIdentifier(buf.writer());
|
||||
|
||||
return .{ .tag = try buf.toOwnedSlice() };
|
||||
}
|
||||
|
||||
// parseIdentifier parses an identifier.
|
||||
fn parseIdentifier(p: *Parser, w: anytype) ParseError!void {
|
||||
const prefix = '-';
|
||||
var numPrefix: usize = 0;
|
||||
|
||||
while (p.s.len > p.i and p.s[p.i] == prefix) {
|
||||
p.i += 1;
|
||||
numPrefix += 1;
|
||||
}
|
||||
|
||||
if (p.s.len <= p.i) {
|
||||
return ParseError.ExpectedSelector;
|
||||
}
|
||||
|
||||
const c = p.s[p.i];
|
||||
if (!nameStart(c) or c == '\\') {
|
||||
return ParseError.ExpectedSelector;
|
||||
}
|
||||
|
||||
var ii: usize = 0;
|
||||
while (ii < numPrefix) {
|
||||
w.writeByte(prefix) catch return ParseError.WriteError;
|
||||
ii += 1;
|
||||
}
|
||||
try parseName(p, w);
|
||||
}
|
||||
|
||||
// parseName parses a name (which is like an identifier, but doesn't have
|
||||
// extra restrictions on the first character).
|
||||
fn parseName(p: *Parser, w: anytype) ParseError!void {
|
||||
var i = p.i;
|
||||
var ok = false;
|
||||
|
||||
while (i < p.s.len) {
|
||||
const c = p.s[i];
|
||||
|
||||
if (nameChar(c)) {
|
||||
const start = i;
|
||||
while (i < p.s.len and nameChar(p.s[i])) i += 1;
|
||||
w.writeAll(p.s[start..i]) catch return ParseError.WriteError;
|
||||
ok = true;
|
||||
} else if (c == '\\') {
|
||||
p.i = i;
|
||||
try p.parseEscape(w);
|
||||
i = p.i;
|
||||
ok = true;
|
||||
} else {
|
||||
// default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ok) return ParseError.ExpectedName;
|
||||
p.i = i;
|
||||
}
|
||||
|
||||
// parseEscape parses a backslash escape.
|
||||
// The returned string is owned by the caller.
|
||||
fn parseEscape(p: *Parser, w: anytype) ParseError!void {
|
||||
if (p.s.len < p.i + 2 or p.s[p.i] != '\\') {
|
||||
return ParseError.InvalidEscape;
|
||||
}
|
||||
|
||||
const start = p.i + 1;
|
||||
const c = p.s[start];
|
||||
if (ascii.isWhitespace(c)) return ParseError.EscapeLineEndingOutsideString;
|
||||
|
||||
// unicode escape (hex)
|
||||
if (ascii.isHex(c)) {
|
||||
var i: usize = start;
|
||||
while (i < start + 6 and i < p.s.len and ascii.isHex(p.s[i])) {
|
||||
i += 1;
|
||||
}
|
||||
const v = std.fmt.parseUnsigned(u21, p.s[start..i], 16) catch return ParseError.InvalidUnicode;
|
||||
if (p.s.len > i) {
|
||||
switch (p.s[i]) {
|
||||
'\r' => {
|
||||
i += 1;
|
||||
if (p.s.len > i and p.s[i] == '\n') i += 1;
|
||||
},
|
||||
' ', '\t', '\n', std.ascii.control_code.ff => i += 1,
|
||||
else => {},
|
||||
}
|
||||
p.i = i;
|
||||
var buf: [4]u8 = undefined;
|
||||
const ln = std.unicode.utf8Encode(v, &buf) catch return ParseError.InvalidUnicode;
|
||||
w.writeAll(buf[0..ln]) catch return ParseError.WriteError;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the literal character after the backslash.
|
||||
p.i += 2;
|
||||
w.writeAll(p.s[start .. start + 1]) catch return ParseError.WriteError;
|
||||
}
|
||||
|
||||
// parseIDSelector parses a selector that matches by id attribute.
|
||||
fn parseIDSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedIDSelector;
|
||||
if (p.s[p.i] != '#') return ParseError.ExpectedIDSelector;
|
||||
|
||||
p.i += 1;
|
||||
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
try p.parseName(buf.writer());
|
||||
return .{ .id = try buf.toOwnedSlice() };
|
||||
}
|
||||
|
||||
// parseClassSelector parses a selector that matches by class attribute.
|
||||
fn parseClassSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedClassSelector;
|
||||
if (p.s[p.i] != '.') return ParseError.ExpectedClassSelector;
|
||||
|
||||
p.i += 1;
|
||||
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
try p.parseIdentifier(buf.writer());
|
||||
return .{ .class = try buf.toOwnedSlice() };
|
||||
}
|
||||
|
||||
// parseAttributeSelector parses a selector that matches by attribute value.
|
||||
fn parseAttributeSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedAttributeSelector;
|
||||
if (p.s[p.i] != '[') return ParseError.ExpectedAttributeSelector;
|
||||
|
||||
p.i += 1;
|
||||
_ = p.skipWhitespace();
|
||||
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
try p.parseIdentifier(buf.writer());
|
||||
const key = try buf.toOwnedSlice();
|
||||
errdefer alloc.free(key);
|
||||
|
||||
lowerstr(key);
|
||||
|
||||
_ = p.skipWhitespace();
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedAttributeSelector;
|
||||
if (p.s[p.i] == ']') {
|
||||
p.i += 1;
|
||||
return .{ .attribute = .{ .key = key } };
|
||||
}
|
||||
|
||||
if (p.i + 2 >= p.s.len) return ParseError.ExpectedAttributeSelector;
|
||||
|
||||
const op = try parseAttributeOP(p.s[p.i .. p.i + 2]);
|
||||
p.i += op.len();
|
||||
|
||||
_ = p.skipWhitespace();
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedAttributeSelector;
|
||||
|
||||
buf.clearRetainingCapacity();
|
||||
var is_val: bool = undefined;
|
||||
if (op == .regexp) {
|
||||
is_val = false;
|
||||
try p.parseRegex(buf.writer());
|
||||
} else {
|
||||
is_val = true;
|
||||
switch (p.s[p.i]) {
|
||||
'\'', '"' => try p.parseString(buf.writer()),
|
||||
else => try p.parseIdentifier(buf.writer()),
|
||||
}
|
||||
}
|
||||
|
||||
_ = p.skipWhitespace();
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedAttributeSelector;
|
||||
|
||||
// check if the attribute contains an ignore case flag
|
||||
var ci = false;
|
||||
if (p.s[p.i] == 'i' or p.s[p.i] == 'I') {
|
||||
ci = true;
|
||||
p.i += 1;
|
||||
}
|
||||
|
||||
_ = p.skipWhitespace();
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedAttributeSelector;
|
||||
|
||||
if (p.s[p.i] != ']') return ParseError.InvalidAttributeSelector;
|
||||
p.i += 1;
|
||||
|
||||
return .{ .attribute = .{
|
||||
.key = key,
|
||||
.val = if (is_val) try buf.toOwnedSlice() else null,
|
||||
.regexp = if (!is_val) try buf.toOwnedSlice() else null,
|
||||
.op = op,
|
||||
.ci = ci,
|
||||
} };
|
||||
}
|
||||
|
||||
// parseString parses a single- or double-quoted string.
|
||||
fn parseString(p: *Parser, writer: anytype) ParseError!void {
|
||||
var i = p.i;
|
||||
if (p.s.len < i + 2) return ParseError.ExpectedString;
|
||||
|
||||
const quote = p.s[i];
|
||||
i += 1;
|
||||
|
||||
loop: while (i < p.s.len) {
|
||||
switch (p.s[i]) {
|
||||
'\\' => {
|
||||
if (p.s.len > i + 1) {
|
||||
const c = p.s[i + 1];
|
||||
switch (c) {
|
||||
'\r' => {
|
||||
if (p.s.len > i + 2 and p.s[i + 2] == '\n') {
|
||||
i += 3;
|
||||
continue :loop;
|
||||
}
|
||||
i += 2;
|
||||
continue :loop;
|
||||
},
|
||||
'\n', std.ascii.control_code.ff => {
|
||||
i += 2;
|
||||
continue :loop;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
p.i = i;
|
||||
try p.parseEscape(writer);
|
||||
i = p.i;
|
||||
},
|
||||
'\r', '\n', std.ascii.control_code.ff => return ParseError.InvalidString,
|
||||
else => |c| {
|
||||
if (c == quote) break :loop;
|
||||
const start = i;
|
||||
while (i < p.s.len) {
|
||||
const cc = p.s[i];
|
||||
if (cc == quote or cc == '\\' or c == '\r' or c == '\n' or c == std.ascii.control_code.ff) break;
|
||||
i += 1;
|
||||
}
|
||||
writer.writeAll(p.s[start..i]) catch return ParseError.WriteError;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (i >= p.s.len) return ParseError.InvalidString;
|
||||
|
||||
// Consume the final quote.
|
||||
i += 1;
|
||||
p.i = i;
|
||||
}
|
||||
|
||||
// parseRegex parses a regular expression; the end is defined by encountering an
|
||||
// unmatched closing ')' or ']' which is not consumed
|
||||
fn parseRegex(p: *Parser, writer: anytype) ParseError!void {
|
||||
var i = p.i;
|
||||
if (p.s.len < i + 2) return ParseError.ExpectedRegexp;
|
||||
|
||||
// number of open parens or brackets;
|
||||
// when it becomes negative, finished parsing regex
|
||||
var open: isize = 0;
|
||||
|
||||
loop: while (i < p.s.len) {
|
||||
switch (p.s[i]) {
|
||||
'(', '[' => open += 1,
|
||||
')', ']' => {
|
||||
open -= 1;
|
||||
if (open < 0) break :loop;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if (i >= p.s.len) return ParseError.InvalidRegexp;
|
||||
writer.writeAll(p.s[p.i..i]) catch return ParseError.WriteError;
|
||||
p.i = i;
|
||||
}
|
||||
|
||||
// parsePseudoclassSelector parses a pseudoclass selector like :not(p) or a pseudo-element
|
||||
// For backwards compatibility, both ':' and '::' prefix are allowed for pseudo-elements.
|
||||
// https://drafts.csswg.org/selectors-3/#pseudo-elements
|
||||
fn parsePseudoclassSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedPseudoClassSelector;
|
||||
if (p.s[p.i] != ':') return ParseError.ExpectedPseudoClassSelector;
|
||||
|
||||
p.i += 1;
|
||||
|
||||
var must_pseudo_elt: bool = false;
|
||||
if (p.i >= p.s.len) return ParseError.EmptyPseudoClassSelector;
|
||||
if (p.s[p.i] == ':') { // we found a pseudo-element
|
||||
must_pseudo_elt = true;
|
||||
p.i += 1;
|
||||
}
|
||||
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
try p.parseIdentifier(buf.writer());
|
||||
|
||||
const pseudo_class = try PseudoClass.parse(buf.items);
|
||||
|
||||
// reset the buffer to reuse it.
|
||||
buf.clearRetainingCapacity();
|
||||
|
||||
if (must_pseudo_elt and !pseudo_class.isPseudoElement()) return ParseError.InvalidPseudoElement;
|
||||
|
||||
switch (pseudo_class) {
|
||||
.not, .has, .haschild => {
|
||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
||||
|
||||
const sel = try p.parseSelectorGroup(alloc);
|
||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
||||
|
||||
const s = try alloc.create(Selector);
|
||||
errdefer alloc.destroy(s);
|
||||
s.* = sel;
|
||||
|
||||
return .{ .pseudo_class_relative = .{ .pseudo_class = pseudo_class, .match = s } };
|
||||
},
|
||||
.contains, .containsown => {
|
||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
||||
if (p.i == p.s.len) return ParseError.UnmatchParenthesis;
|
||||
|
||||
switch (p.s[p.i]) {
|
||||
'\'', '"' => try p.parseString(buf.writer()),
|
||||
else => try p.parseString(buf.writer()),
|
||||
}
|
||||
|
||||
_ = p.skipWhitespace();
|
||||
if (p.i >= p.s.len) return ParseError.InvalidPseudoClass;
|
||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
||||
|
||||
const val = try buf.toOwnedSlice();
|
||||
errdefer alloc.free(val);
|
||||
|
||||
lowerstr(val);
|
||||
|
||||
return .{ .pseudo_class_contains = .{ .own = pseudo_class == .containsown, .val = val } };
|
||||
},
|
||||
.matches, .matchesown => {
|
||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
||||
|
||||
try p.parseRegex(buf.writer());
|
||||
if (p.i >= p.s.len) return ParseError.InvalidPseudoClassSelector;
|
||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
||||
|
||||
return .{ .pseudo_class_regexp = .{ .own = pseudo_class == .matchesown, .regexp = try buf.toOwnedSlice() } };
|
||||
},
|
||||
.nth_child, .nth_last_child, .nth_of_type, .nth_last_of_type => {
|
||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
||||
const nth = try p.parseNth(alloc);
|
||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
||||
|
||||
const last = pseudo_class == .nth_last_child or pseudo_class == .nth_last_of_type;
|
||||
const of_type = pseudo_class == .nth_of_type or pseudo_class == .nth_last_of_type;
|
||||
return .{ .pseudo_class_nth = .{ .a = nth[0], .b = nth[1], .of_type = of_type, .last = last } };
|
||||
},
|
||||
.first_child => return .{ .pseudo_class_nth = .{ .a = 0, .b = 1, .of_type = false, .last = false } },
|
||||
.last_child => return .{ .pseudo_class_nth = .{ .a = 0, .b = 1, .of_type = false, .last = true } },
|
||||
.first_of_type => return .{ .pseudo_class_nth = .{ .a = 0, .b = 1, .of_type = true, .last = false } },
|
||||
.last_of_type => return .{ .pseudo_class_nth = .{ .a = 0, .b = 1, .of_type = true, .last = true } },
|
||||
.only_child => return .{ .pseudo_class_only_child = false },
|
||||
.only_of_type => return .{ .pseudo_class_only_child = true },
|
||||
.input, .empty, .root, .link => return .{ .pseudo_class = pseudo_class },
|
||||
.enabled, .disabled, .checked => return .{ .pseudo_class = pseudo_class },
|
||||
.lang => {
|
||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
||||
if (p.i == p.s.len) return ParseError.UnmatchParenthesis;
|
||||
|
||||
try p.parseIdentifier(buf.writer());
|
||||
|
||||
_ = p.skipWhitespace();
|
||||
if (p.i >= p.s.len) return ParseError.InvalidPseudoClass;
|
||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
||||
|
||||
const val = try buf.toOwnedSlice();
|
||||
errdefer alloc.free(val);
|
||||
lowerstr(val);
|
||||
|
||||
return .{ .pseudo_class_lang = val };
|
||||
},
|
||||
.visited, .hover, .active, .focus, .target => {
|
||||
// Not applicable in a static context: never match.
|
||||
return .{ .never_match = pseudo_class };
|
||||
},
|
||||
.after, .backdrop, .before, .cue, .first_letter => return .{ .pseudo_element = pseudo_class },
|
||||
.first_line, .grammar_error, .marker, .placeholder => return .{ .pseudo_element = pseudo_class },
|
||||
.selection, .spelling_error => return .{ .pseudo_element = pseudo_class },
|
||||
}
|
||||
}
|
||||
|
||||
// consumeParenthesis consumes an opening parenthesis and any following
|
||||
// whitespace. It returns true if there was actually a parenthesis to skip.
|
||||
fn consumeParenthesis(p: *Parser) bool {
|
||||
if (p.i < p.s.len and p.s[p.i] == '(') {
|
||||
p.i += 1;
|
||||
_ = p.skipWhitespace();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// parseSelectorGroup parses a group of selectors, separated by commas.
|
||||
fn parseSelectorGroup(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
const s = try p.parseSelector(alloc);
|
||||
|
||||
var buf = std.ArrayList(Selector).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
try buf.append(s);
|
||||
|
||||
while (p.i < p.s.len) {
|
||||
if (p.s[p.i] != ',') break;
|
||||
p.i += 1;
|
||||
const ss = try p.parseSelector(alloc);
|
||||
try buf.append(ss);
|
||||
}
|
||||
|
||||
if (buf.items.len == 1) return buf.items[0];
|
||||
|
||||
return .{ .group = try buf.toOwnedSlice() };
|
||||
}
|
||||
|
||||
// parseSelector parses a selector that may include combinators.
|
||||
fn parseSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
_ = p.skipWhitespace();
|
||||
var s = try p.parseSimpleSelectorSequence(alloc);
|
||||
|
||||
while (true) {
|
||||
var combinator: Combinator = .empty;
|
||||
if (p.skipWhitespace()) {
|
||||
combinator = .descendant;
|
||||
}
|
||||
if (p.i >= p.s.len) {
|
||||
return s;
|
||||
}
|
||||
|
||||
switch (p.s[p.i]) {
|
||||
'+', '>', '~' => {
|
||||
combinator = try Combinator.parse(p.s[p.i]);
|
||||
p.i += 1;
|
||||
_ = p.skipWhitespace();
|
||||
},
|
||||
// These characters can't begin a selector, but they can legally occur after one.
|
||||
',', ')' => {
|
||||
return s;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
if (combinator == .empty) {
|
||||
return s;
|
||||
}
|
||||
|
||||
const c = try p.parseSimpleSelectorSequence(alloc);
|
||||
|
||||
const first = try alloc.create(Selector);
|
||||
errdefer alloc.destroy(first);
|
||||
first.* = s;
|
||||
|
||||
const second = try alloc.create(Selector);
|
||||
errdefer alloc.destroy(second);
|
||||
second.* = c;
|
||||
|
||||
s = Selector{ .combined = .{ .first = first, .second = second, .combinator = combinator } };
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
// consumeClosingParenthesis consumes a closing parenthesis and any preceding
|
||||
// whitespace. It returns true if there was actually a parenthesis to skip.
|
||||
fn consumeClosingParenthesis(p: *Parser) bool {
|
||||
const i = p.i;
|
||||
_ = p.skipWhitespace();
|
||||
if (p.i < p.s.len and p.s[p.i] == ')') {
|
||||
p.i += 1;
|
||||
return true;
|
||||
}
|
||||
p.i = i;
|
||||
return false;
|
||||
}
|
||||
|
||||
// parseInteger parses a decimal integer.
|
||||
fn parseInteger(p: *Parser) ParseError!isize {
|
||||
var i = p.i;
|
||||
const start = i;
|
||||
while (i < p.s.len and '0' <= p.s[i] and p.s[i] <= '9') i += 1;
|
||||
if (i == start) return ParseError.ExpectedInteger;
|
||||
p.i = i;
|
||||
|
||||
return std.fmt.parseUnsigned(isize, p.s[start..i], 10) catch ParseError.ExpectedInteger;
|
||||
}
|
||||
|
||||
fn parseNthReadN(p: *Parser, a: isize) ParseError![2]isize {
|
||||
_ = p.skipWhitespace();
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedNthExpression;
|
||||
|
||||
return switch (p.s[p.i]) {
|
||||
'+' => {
|
||||
p.i += 1;
|
||||
_ = p.skipWhitespace();
|
||||
const b = try p.parseInteger();
|
||||
return .{ a, b };
|
||||
},
|
||||
'-' => {
|
||||
p.i += 1;
|
||||
_ = p.skipWhitespace();
|
||||
const b = try p.parseInteger();
|
||||
return .{ a, -b };
|
||||
},
|
||||
else => .{ a, 0 },
|
||||
};
|
||||
}
|
||||
|
||||
fn parseNthReadA(p: *Parser, a: isize) ParseError![2]isize {
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedNthExpression;
|
||||
return switch (p.s[p.i]) {
|
||||
'n', 'N' => {
|
||||
p.i += 1;
|
||||
return p.parseNthReadN(a);
|
||||
},
|
||||
else => .{ 0, a },
|
||||
};
|
||||
}
|
||||
|
||||
fn parseNthNegativeA(p: *Parser) ParseError![2]isize {
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedNthExpression;
|
||||
const c = p.s[p.i];
|
||||
if (std.ascii.isDigit(c)) {
|
||||
const a = try p.parseInteger() * -1;
|
||||
return p.parseNthReadA(a);
|
||||
}
|
||||
if (c == 'n' or c == 'N') {
|
||||
p.i += 1;
|
||||
return p.parseNthReadN(-1);
|
||||
}
|
||||
|
||||
return ParseError.InvalidNthExpression;
|
||||
}
|
||||
|
||||
fn parseNthPositiveA(p: *Parser) ParseError![2]isize {
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedNthExpression;
|
||||
const c = p.s[p.i];
|
||||
if (std.ascii.isDigit(c)) {
|
||||
const a = try p.parseInteger();
|
||||
return p.parseNthReadA(a);
|
||||
}
|
||||
if (c == 'n' or c == 'N') {
|
||||
p.i += 1;
|
||||
return p.parseNthReadN(1);
|
||||
}
|
||||
|
||||
return ParseError.InvalidNthExpression;
|
||||
}
|
||||
|
||||
// parseNth parses the argument for :nth-child (normally of the form an+b).
|
||||
fn parseNth(p: *Parser, alloc: std.mem.Allocator) ParseError![2]isize {
|
||||
// initial state
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedNthExpression;
|
||||
return switch (p.s[p.i]) {
|
||||
'-' => {
|
||||
p.i += 1;
|
||||
return p.parseNthNegativeA();
|
||||
},
|
||||
'+' => {
|
||||
p.i += 1;
|
||||
return p.parseNthPositiveA();
|
||||
},
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => p.parseNthPositiveA(),
|
||||
'n', 'N' => {
|
||||
p.i += 1;
|
||||
return p.parseNthReadN(1);
|
||||
},
|
||||
'o', 'O', 'e', 'E' => {
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
try p.parseName(buf.writer());
|
||||
|
||||
if (std.ascii.eqlIgnoreCase("odd", buf.items)) return .{ 2, 1 };
|
||||
if (std.ascii.eqlIgnoreCase("even", buf.items)) return .{ 2, 0 };
|
||||
|
||||
return ParseError.InvalidNthExpression;
|
||||
},
|
||||
else => ParseError.InvalidNthExpression,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// nameStart returns whether c can be the first character of an identifier
|
||||
// (not counting an initial hyphen, or an escape sequence).
|
||||
fn nameStart(c: u8) bool {
|
||||
return 'a' <= c and c <= 'z' or 'A' <= c and c <= 'Z' or c == '_' or c > 127;
|
||||
}
|
||||
|
||||
// nameChar returns whether c can be a character within an identifier
|
||||
// (not counting an escape sequence).
|
||||
fn nameChar(c: u8) bool {
|
||||
return 'a' <= c and c <= 'z' or 'A' <= c and c <= 'Z' or c == '_' or c > 127 or
|
||||
c == '-' or '0' <= c and c <= '9';
|
||||
}
|
||||
|
||||
fn lowerstr(str: []u8) void {
|
||||
for (str, 0..) |c, i| {
|
||||
str[i] = std.ascii.toLower(c);
|
||||
}
|
||||
}
|
||||
|
||||
// parseAttributeOP parses an AttributeOP from a string of 1 or 2 bytes.
|
||||
fn parseAttributeOP(s: []const u8) ParseError!AttributeOP {
|
||||
if (s.len < 1 or s.len > 2) return ParseError.InvalidAttributeOperator;
|
||||
|
||||
// if the first sign is equal, we don't check anything else.
|
||||
if (s[0] == '=') return .eql;
|
||||
|
||||
if (s.len != 2 or s[1] != '=') return ParseError.InvalidAttributeOperator;
|
||||
|
||||
return switch (s[0]) {
|
||||
'=' => .eql,
|
||||
'!' => .not_eql,
|
||||
'~' => .one_of,
|
||||
'|' => .prefix_hyphen,
|
||||
'^' => .prefix,
|
||||
'$' => .suffix,
|
||||
'*' => .contains,
|
||||
'#' => .regexp,
|
||||
else => ParseError.InvalidAttributeOperator,
|
||||
};
|
||||
}
|
||||
|
||||
test "parser.skipWhitespace" {
|
||||
const testcases = [_]struct {
|
||||
s: []const u8,
|
||||
i: usize,
|
||||
r: bool,
|
||||
}{
|
||||
.{ .s = "", .i = 0, .r = false },
|
||||
.{ .s = "foo", .i = 0, .r = false },
|
||||
.{ .s = " ", .i = 1, .r = true },
|
||||
.{ .s = " foo", .i = 1, .r = true },
|
||||
.{ .s = "/* foo */ bar", .i = 10, .r = true },
|
||||
.{ .s = "/* foo", .i = 0, .r = false },
|
||||
};
|
||||
|
||||
for (testcases) |tc| {
|
||||
var p = Parser{ .s = tc.s, .opts = .{} };
|
||||
const res = p.skipWhitespace();
|
||||
try std.testing.expectEqual(tc.r, res);
|
||||
try std.testing.expectEqual(tc.i, p.i);
|
||||
}
|
||||
}
|
||||
|
||||
test "parser.parseIdentifier" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
const testcases = [_]struct {
|
||||
s: []const u8, // given value
|
||||
exp: []const u8, // expected value
|
||||
err: bool = false,
|
||||
}{
|
||||
.{ .s = "x", .exp = "x" },
|
||||
.{ .s = "96", .exp = "", .err = true },
|
||||
.{ .s = "-x", .exp = "-x" },
|
||||
.{ .s = "r\\e9 sumé", .exp = "résumé" },
|
||||
.{ .s = "r\\0000e9 sumé", .exp = "résumé" },
|
||||
.{ .s = "r\\0000e9sumé", .exp = "résumé" },
|
||||
.{ .s = "a\\\"b", .exp = "a\"b" },
|
||||
};
|
||||
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
for (testcases) |tc| {
|
||||
buf.clearRetainingCapacity();
|
||||
|
||||
var p = Parser{ .s = tc.s, .opts = .{} };
|
||||
p.parseIdentifier(buf.writer()) catch |e| {
|
||||
// if error was expected, continue.
|
||||
if (tc.err) continue;
|
||||
|
||||
std.debug.print("test case {s}\n", .{tc.s});
|
||||
return e;
|
||||
};
|
||||
std.testing.expectEqualDeep(tc.exp, buf.items) catch |e| {
|
||||
std.debug.print("test case {s} : {s}\n", .{ tc.s, buf.items });
|
||||
return e;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test "parser.parseString" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
const testcases = [_]struct {
|
||||
s: []const u8, // given value
|
||||
exp: []const u8, // expected value
|
||||
err: bool = false,
|
||||
}{
|
||||
.{ .s = "\"x\"", .exp = "x" },
|
||||
.{ .s = "'x'", .exp = "x" },
|
||||
.{ .s = "'x", .exp = "", .err = true },
|
||||
.{ .s = "'x\\\r\nx'", .exp = "xx" },
|
||||
.{ .s = "\"r\\e9 sumé\"", .exp = "résumé" },
|
||||
.{ .s = "\"r\\0000e9 sumé\"", .exp = "résumé" },
|
||||
.{ .s = "\"r\\0000e9sumé\"", .exp = "résumé" },
|
||||
.{ .s = "\"a\\\"b\"", .exp = "a\"b" },
|
||||
.{ .s = "\"\\\n\"", .exp = "" },
|
||||
.{ .s = "\"hello world\"", .exp = "hello world" },
|
||||
};
|
||||
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
for (testcases) |tc| {
|
||||
buf.clearRetainingCapacity();
|
||||
|
||||
var p = Parser{ .s = tc.s, .opts = .{} };
|
||||
p.parseString(buf.writer()) catch |e| {
|
||||
// if error was expected, continue.
|
||||
if (tc.err) continue;
|
||||
|
||||
std.debug.print("test case {s}\n", .{tc.s});
|
||||
return e;
|
||||
};
|
||||
std.testing.expectEqualDeep(tc.exp, buf.items) catch |e| {
|
||||
std.debug.print("test case {s} : {s}\n", .{ tc.s, buf.items });
|
||||
return e;
|
||||
};
|
||||
}
|
||||
}
|
||||
767
src/css/selector.zig
Normal file
767
src/css/selector.zig
Normal file
@@ -0,0 +1,767 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
pub const AttributeOP = enum {
|
||||
eql, // =
|
||||
not_eql, // !=
|
||||
one_of, // ~=
|
||||
prefix_hyphen, // |=
|
||||
prefix, // ^=
|
||||
suffix, // $=
|
||||
contains, // *=
|
||||
regexp, // #=
|
||||
|
||||
pub fn len(op: AttributeOP) u2 {
|
||||
if (op == .eql) return 1;
|
||||
return 2;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Combinator = enum {
|
||||
empty,
|
||||
descendant, // space
|
||||
child, // >
|
||||
next_sibling, // +
|
||||
subsequent_sibling, // ~
|
||||
|
||||
pub const Error = error{
|
||||
InvalidCombinator,
|
||||
};
|
||||
|
||||
pub fn parse(c: u8) Error!Combinator {
|
||||
return switch (c) {
|
||||
' ' => .descendant,
|
||||
'>' => .child,
|
||||
'+' => .next_sibling,
|
||||
'~' => .subsequent_sibling,
|
||||
else => Error.InvalidCombinator,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const PseudoClass = enum {
|
||||
not,
|
||||
has,
|
||||
haschild,
|
||||
contains,
|
||||
containsown,
|
||||
matches,
|
||||
matchesown,
|
||||
nth_child,
|
||||
nth_last_child,
|
||||
nth_of_type,
|
||||
nth_last_of_type,
|
||||
first_child,
|
||||
last_child,
|
||||
first_of_type,
|
||||
last_of_type,
|
||||
only_child,
|
||||
only_of_type,
|
||||
input,
|
||||
empty,
|
||||
root,
|
||||
link,
|
||||
lang,
|
||||
enabled,
|
||||
disabled,
|
||||
checked,
|
||||
visited,
|
||||
hover,
|
||||
active,
|
||||
focus,
|
||||
target,
|
||||
after,
|
||||
backdrop,
|
||||
before,
|
||||
cue,
|
||||
first_letter,
|
||||
first_line,
|
||||
grammar_error,
|
||||
marker,
|
||||
placeholder,
|
||||
selection,
|
||||
spelling_error,
|
||||
|
||||
pub const Error = error{
|
||||
InvalidPseudoClass,
|
||||
};
|
||||
|
||||
pub fn isPseudoElement(pc: PseudoClass) bool {
|
||||
return switch (pc) {
|
||||
.after, .backdrop, .before, .cue, .first_letter => true,
|
||||
.first_line, .grammar_error, .marker, .placeholder => true,
|
||||
.selection, .spelling_error => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn parse(s: []const u8) Error!PseudoClass {
|
||||
if (std.ascii.eqlIgnoreCase(s, "not")) return .not;
|
||||
if (std.ascii.eqlIgnoreCase(s, "has")) return .has;
|
||||
if (std.ascii.eqlIgnoreCase(s, "haschild")) return .haschild;
|
||||
if (std.ascii.eqlIgnoreCase(s, "contains")) return .contains;
|
||||
if (std.ascii.eqlIgnoreCase(s, "containsown")) return .containsown;
|
||||
if (std.ascii.eqlIgnoreCase(s, "matches")) return .matches;
|
||||
if (std.ascii.eqlIgnoreCase(s, "matchesown")) return .matchesown;
|
||||
if (std.ascii.eqlIgnoreCase(s, "nth-child")) return .nth_child;
|
||||
if (std.ascii.eqlIgnoreCase(s, "nth-last-child")) return .nth_last_child;
|
||||
if (std.ascii.eqlIgnoreCase(s, "nth-of-type")) return .nth_of_type;
|
||||
if (std.ascii.eqlIgnoreCase(s, "nth-last-of-type")) return .nth_last_of_type;
|
||||
if (std.ascii.eqlIgnoreCase(s, "first-child")) return .first_child;
|
||||
if (std.ascii.eqlIgnoreCase(s, "last-child")) return .last_child;
|
||||
if (std.ascii.eqlIgnoreCase(s, "first-of-type")) return .first_of_type;
|
||||
if (std.ascii.eqlIgnoreCase(s, "last-of-type")) return .last_of_type;
|
||||
if (std.ascii.eqlIgnoreCase(s, "only-child")) return .only_child;
|
||||
if (std.ascii.eqlIgnoreCase(s, "only-of-type")) return .only_of_type;
|
||||
if (std.ascii.eqlIgnoreCase(s, "input")) return .input;
|
||||
if (std.ascii.eqlIgnoreCase(s, "empty")) return .empty;
|
||||
if (std.ascii.eqlIgnoreCase(s, "root")) return .root;
|
||||
if (std.ascii.eqlIgnoreCase(s, "link")) return .link;
|
||||
if (std.ascii.eqlIgnoreCase(s, "lang")) return .lang;
|
||||
if (std.ascii.eqlIgnoreCase(s, "enabled")) return .enabled;
|
||||
if (std.ascii.eqlIgnoreCase(s, "disabled")) return .disabled;
|
||||
if (std.ascii.eqlIgnoreCase(s, "checked")) return .checked;
|
||||
if (std.ascii.eqlIgnoreCase(s, "visited")) return .visited;
|
||||
if (std.ascii.eqlIgnoreCase(s, "hover")) return .hover;
|
||||
if (std.ascii.eqlIgnoreCase(s, "active")) return .active;
|
||||
if (std.ascii.eqlIgnoreCase(s, "focus")) return .focus;
|
||||
if (std.ascii.eqlIgnoreCase(s, "target")) return .target;
|
||||
if (std.ascii.eqlIgnoreCase(s, "after")) return .after;
|
||||
if (std.ascii.eqlIgnoreCase(s, "backdrop")) return .backdrop;
|
||||
if (std.ascii.eqlIgnoreCase(s, "before")) return .before;
|
||||
if (std.ascii.eqlIgnoreCase(s, "cue")) return .cue;
|
||||
if (std.ascii.eqlIgnoreCase(s, "first-letter")) return .first_letter;
|
||||
if (std.ascii.eqlIgnoreCase(s, "first-line")) return .first_line;
|
||||
if (std.ascii.eqlIgnoreCase(s, "grammar-error")) return .grammar_error;
|
||||
if (std.ascii.eqlIgnoreCase(s, "marker")) return .marker;
|
||||
if (std.ascii.eqlIgnoreCase(s, "placeholder")) return .placeholder;
|
||||
if (std.ascii.eqlIgnoreCase(s, "selection")) return .selection;
|
||||
if (std.ascii.eqlIgnoreCase(s, "spelling-error")) return .spelling_error;
|
||||
return Error.InvalidPseudoClass;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Selector = union(enum) {
|
||||
pub const Error = error{
|
||||
UnknownCombinedCombinator,
|
||||
UnsupportedRelativePseudoClass,
|
||||
UnsupportedContainsPseudoClass,
|
||||
UnsupportedPseudoClass,
|
||||
UnsupportedPseudoElement,
|
||||
UnsupportedRegexpPseudoClass,
|
||||
UnsupportedAttrRegexpOperator,
|
||||
};
|
||||
|
||||
compound: struct {
|
||||
selectors: []Selector,
|
||||
pseudo_elt: ?PseudoClass,
|
||||
},
|
||||
group: []Selector,
|
||||
tag: []const u8,
|
||||
id: []const u8,
|
||||
class: []const u8,
|
||||
attribute: struct {
|
||||
key: []const u8,
|
||||
val: ?[]const u8 = null,
|
||||
op: ?AttributeOP = null,
|
||||
regexp: ?[]const u8 = null,
|
||||
ci: bool = false,
|
||||
},
|
||||
combined: struct {
|
||||
first: *Selector,
|
||||
second: *Selector,
|
||||
combinator: Combinator,
|
||||
},
|
||||
|
||||
never_match: PseudoClass,
|
||||
|
||||
pseudo_class: PseudoClass,
|
||||
pseudo_class_only_child: bool,
|
||||
pseudo_class_lang: []const u8,
|
||||
pseudo_class_relative: struct {
|
||||
pseudo_class: PseudoClass,
|
||||
match: *Selector,
|
||||
},
|
||||
pseudo_class_contains: struct {
|
||||
own: bool,
|
||||
val: []const u8,
|
||||
},
|
||||
pseudo_class_regexp: struct {
|
||||
own: bool,
|
||||
regexp: []const u8,
|
||||
},
|
||||
pseudo_class_nth: struct {
|
||||
a: isize,
|
||||
b: isize,
|
||||
of_type: bool,
|
||||
last: bool,
|
||||
},
|
||||
pseudo_element: PseudoClass,
|
||||
|
||||
// returns true if s is a whitespace-separated list that includes val.
|
||||
fn word(haystack: []const u8, needle: []const u8, ci: bool) bool {
|
||||
if (haystack.len == 0) return false;
|
||||
var it = std.mem.splitAny(u8, haystack, " \t\r\n"); // TODO add \f
|
||||
while (it.next()) |part| {
|
||||
if (eql(part, needle, ci)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn eql(a: []const u8, b: []const u8, ci: bool) bool {
|
||||
if (ci) return std.ascii.eqlIgnoreCase(a, b);
|
||||
return std.mem.eql(u8, a, b);
|
||||
}
|
||||
|
||||
fn starts(haystack: []const u8, needle: []const u8, ci: bool) bool {
|
||||
if (ci) return std.ascii.startsWithIgnoreCase(haystack, needle);
|
||||
return std.mem.startsWith(u8, haystack, needle);
|
||||
}
|
||||
|
||||
fn ends(haystack: []const u8, needle: []const u8, ci: bool) bool {
|
||||
if (ci) return std.ascii.endsWithIgnoreCase(haystack, needle);
|
||||
return std.mem.endsWith(u8, haystack, needle);
|
||||
}
|
||||
|
||||
fn contains(haystack: []const u8, needle: []const u8, ci: bool) bool {
|
||||
if (ci) return std.ascii.indexOfIgnoreCase(haystack, needle) != null;
|
||||
return std.mem.indexOf(u8, haystack, needle) != null;
|
||||
}
|
||||
|
||||
// match returns true if the node matches the selector query.
|
||||
pub fn match(s: Selector, n: anytype) !bool {
|
||||
return switch (s) {
|
||||
.tag => |v| n.isElement() and std.ascii.eqlIgnoreCase(v, try n.tag()),
|
||||
.id => |v| return n.isElement() and std.mem.eql(u8, v, try n.attr("id") orelse return false),
|
||||
.class => |v| return n.isElement() and word(try n.attr("class") orelse return false, v, false),
|
||||
.group => |v| {
|
||||
for (v) |sel| {
|
||||
if (try sel.match(n)) return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
.compound => |v| {
|
||||
if (v.selectors.len == 0) return n.isElement();
|
||||
|
||||
for (v.selectors) |sel| {
|
||||
if (!try sel.match(n)) return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
.combined => |v| {
|
||||
return switch (v.combinator) {
|
||||
.empty => try v.first.match(n),
|
||||
.descendant => {
|
||||
if (!try v.second.match(n)) return false;
|
||||
|
||||
// The first must match a ascendent.
|
||||
var p = try n.parent();
|
||||
while (p != null) {
|
||||
if (try v.first.match(p.?)) {
|
||||
return true;
|
||||
}
|
||||
p = try p.?.parent();
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
.child => {
|
||||
const p = try n.parent();
|
||||
if (p == null) return false;
|
||||
|
||||
return try v.second.match(n) and try v.first.match(p.?);
|
||||
},
|
||||
.next_sibling => {
|
||||
if (!try v.second.match(n)) return false;
|
||||
var c = try n.prevSibling();
|
||||
while (c != null) {
|
||||
if (c.?.isText() or c.?.isComment()) {
|
||||
c = try c.?.prevSibling();
|
||||
continue;
|
||||
}
|
||||
return try v.first.match(c.?);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
.subsequent_sibling => {
|
||||
if (!try v.second.match(n)) return false;
|
||||
|
||||
var c = try n.prevSibling();
|
||||
while (c != null) {
|
||||
if (try v.first.match(c.?)) return true;
|
||||
c = try c.?.prevSibling();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
},
|
||||
.attribute => |v| {
|
||||
var attr = try n.attr(v.key);
|
||||
|
||||
if (v.op == null) return attr != null;
|
||||
if (v.val == null or v.val.?.len == 0) return false;
|
||||
|
||||
const val = v.val.?;
|
||||
|
||||
return switch (v.op.?) {
|
||||
.eql => attr != null and eql(attr.?, val, v.ci),
|
||||
.not_eql => attr == null or !eql(attr.?, val, v.ci),
|
||||
.one_of => attr != null and word(attr.?, val, v.ci),
|
||||
.prefix => {
|
||||
if (attr == null) return false;
|
||||
attr.? = std.mem.trim(u8, attr.?, &std.ascii.whitespace);
|
||||
|
||||
if (attr.?.len == 0) return false;
|
||||
|
||||
return starts(attr.?, val, v.ci);
|
||||
},
|
||||
.suffix => {
|
||||
if (attr == null) return false;
|
||||
attr.? = std.mem.trim(u8, attr.?, &std.ascii.whitespace);
|
||||
|
||||
if (attr.?.len == 0) return false;
|
||||
|
||||
return ends(attr.?, val, v.ci);
|
||||
},
|
||||
.contains => {
|
||||
if (attr == null) return false;
|
||||
attr.? = std.mem.trim(u8, attr.?, &std.ascii.whitespace);
|
||||
|
||||
if (attr.?.len == 0) return false;
|
||||
|
||||
return contains(attr.?, val, v.ci);
|
||||
},
|
||||
.prefix_hyphen => {
|
||||
if (attr == null) return false;
|
||||
if (eql(attr.?, val, v.ci)) return true;
|
||||
|
||||
if (attr.?.len <= val.len) return false;
|
||||
|
||||
if (!starts(attr.?, val, v.ci)) return false;
|
||||
|
||||
return attr.?[val.len] == '-';
|
||||
},
|
||||
.regexp => return Error.UnsupportedAttrRegexpOperator, // TODO handle regexp attribute operator.
|
||||
};
|
||||
},
|
||||
.never_match => return false,
|
||||
.pseudo_class_relative => |v| {
|
||||
if (!n.isElement()) return false;
|
||||
|
||||
return switch (v.pseudo_class) {
|
||||
.not => !try v.match.match(n),
|
||||
.has => try hasDescendantMatch(v.match, n),
|
||||
.haschild => try hasChildMatch(v.match, n),
|
||||
else => Error.UnsupportedRelativePseudoClass,
|
||||
};
|
||||
},
|
||||
.pseudo_class_contains => return Error.UnsupportedContainsPseudoClass, // TODO, need mem allocation.
|
||||
.pseudo_class_regexp => return Error.UnsupportedRegexpPseudoClass, // TODO need mem allocation.
|
||||
.pseudo_class_nth => |v| {
|
||||
if (v.a == 0) {
|
||||
if (v.last) {
|
||||
return simpleNthLastChildMatch(v.b, v.of_type, n);
|
||||
}
|
||||
return simpleNthChildMatch(v.b, v.of_type, n);
|
||||
}
|
||||
return nthChildMatch(v.a, v.b, v.last, v.of_type, n);
|
||||
},
|
||||
.pseudo_class => |v| {
|
||||
return switch (v) {
|
||||
.input => {
|
||||
if (!n.isElement()) return false;
|
||||
const ntag = try n.tag();
|
||||
|
||||
return std.ascii.eqlIgnoreCase("input", ntag) or
|
||||
std.ascii.eqlIgnoreCase("select", ntag) or
|
||||
std.ascii.eqlIgnoreCase("button", ntag) or
|
||||
std.ascii.eqlIgnoreCase("textarea", ntag);
|
||||
},
|
||||
.empty => {
|
||||
if (!n.isElement()) return false;
|
||||
|
||||
var c = try n.firstChild();
|
||||
while (c != null) {
|
||||
if (c.?.isElement()) return false;
|
||||
|
||||
if (c.?.isText()) {
|
||||
if (try c.?.isEmptyText()) continue;
|
||||
return false;
|
||||
}
|
||||
|
||||
c = try c.?.nextSibling();
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
.root => {
|
||||
if (!n.isElement()) return false;
|
||||
|
||||
const p = try n.parent();
|
||||
return (p != null and p.?.isDocument());
|
||||
},
|
||||
.link => {
|
||||
const ntag = try n.tag();
|
||||
|
||||
return std.ascii.eqlIgnoreCase("a", ntag) or
|
||||
std.ascii.eqlIgnoreCase("area", ntag) or
|
||||
std.ascii.eqlIgnoreCase("link", ntag);
|
||||
},
|
||||
.enabled => {
|
||||
if (!n.isElement()) return false;
|
||||
|
||||
const ntag = try n.tag();
|
||||
|
||||
if (std.ascii.eqlIgnoreCase("a", ntag) or
|
||||
std.ascii.eqlIgnoreCase("area", ntag) or
|
||||
std.ascii.eqlIgnoreCase("link", ntag))
|
||||
{
|
||||
return try n.attr("href") != null;
|
||||
}
|
||||
|
||||
if (std.ascii.eqlIgnoreCase("optgroup", ntag) or
|
||||
std.ascii.eqlIgnoreCase("menuitem", ntag) or
|
||||
std.ascii.eqlIgnoreCase("fieldset", ntag))
|
||||
{
|
||||
return try n.attr("disabled") == null;
|
||||
}
|
||||
|
||||
if (std.ascii.eqlIgnoreCase("input", ntag) or
|
||||
std.ascii.eqlIgnoreCase("button", ntag) or
|
||||
std.ascii.eqlIgnoreCase("select", ntag) or
|
||||
std.ascii.eqlIgnoreCase("textarea", ntag) or
|
||||
std.ascii.eqlIgnoreCase("option", ntag))
|
||||
{
|
||||
return try n.attr("disabled") == null and
|
||||
!try inDisabledFieldset(n);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
.disabled => {
|
||||
if (!n.isElement()) return false;
|
||||
|
||||
const ntag = try n.tag();
|
||||
|
||||
if (std.ascii.eqlIgnoreCase("optgroup", ntag) or
|
||||
std.ascii.eqlIgnoreCase("menuitem", ntag) or
|
||||
std.ascii.eqlIgnoreCase("fieldset", ntag))
|
||||
{
|
||||
return try n.attr("disabled") != null;
|
||||
}
|
||||
|
||||
if (std.ascii.eqlIgnoreCase("input", ntag) or
|
||||
std.ascii.eqlIgnoreCase("button", ntag) or
|
||||
std.ascii.eqlIgnoreCase("select", ntag) or
|
||||
std.ascii.eqlIgnoreCase("textarea", ntag) or
|
||||
std.ascii.eqlIgnoreCase("option", ntag))
|
||||
{
|
||||
return try n.attr("disabled") != null or
|
||||
try inDisabledFieldset(n);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
.checked => {
|
||||
if (!n.isElement()) return false;
|
||||
|
||||
const ntag = try n.tag();
|
||||
|
||||
if (std.ascii.eqlIgnoreCase("intput", ntag)) {
|
||||
const ntype = try n.attr("type");
|
||||
if (ntype == null) return false;
|
||||
|
||||
if (std.mem.eql(u8, ntype.?, "checkbox") or
|
||||
std.mem.eql(u8, ntype.?, "radio"))
|
||||
{
|
||||
return try n.attr("checked") != null;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
if (std.ascii.eqlIgnoreCase("option", ntag)) {
|
||||
return try n.attr("selected") != null;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
.visited => return false,
|
||||
.hover => return false,
|
||||
.active => return false,
|
||||
.focus => return false,
|
||||
// TODO implement using the url fragment.
|
||||
// see https://developer.mozilla.org/en-US/docs/Web/CSS/:target
|
||||
.target => return false,
|
||||
|
||||
// all others pseudo class are handled by specialized
|
||||
// pseudo_class_X selectors.
|
||||
else => return Error.UnsupportedPseudoClass,
|
||||
};
|
||||
},
|
||||
.pseudo_class_only_child => |v| onlyChildMatch(v, n),
|
||||
.pseudo_class_lang => |v| langMatch(v, n),
|
||||
|
||||
// pseudo elements doesn't make sense in the matching process.
|
||||
// > A CSS pseudo-element is a keyword added to a selector that
|
||||
// > lets you style a specific part of the selected element(s).
|
||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements
|
||||
.pseudo_element => return Error.UnsupportedPseudoElement,
|
||||
};
|
||||
}
|
||||
|
||||
fn hasLegendInPreviousSiblings(n: anytype) anyerror!bool {
|
||||
var c = try n.prevSibling();
|
||||
while (c != null) {
|
||||
const ctag = try c.?.tag();
|
||||
if (std.ascii.eqlIgnoreCase("legend", ctag)) return true;
|
||||
c = try c.?.prevSibling();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn inDisabledFieldset(n: anytype) anyerror!bool {
|
||||
const p = try n.parent();
|
||||
if (p == null) return false;
|
||||
|
||||
const ntag = try n.tag();
|
||||
const ptag = try p.?.tag();
|
||||
|
||||
if (std.ascii.eqlIgnoreCase("fieldset", ptag) and
|
||||
try p.?.attr("disabled") != null and
|
||||
(!std.ascii.eqlIgnoreCase("legend", ntag) or try hasLegendInPreviousSiblings(n)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO should we handle legend like cascadia does?
|
||||
// The implemention below looks suspicious, I didn't find a test case
|
||||
// in cascadia and I didn't find the reference about legend in the
|
||||
// specs. For now I do prefer ignoring this part.
|
||||
//
|
||||
// ```
|
||||
// (n.DataAtom != atom.Legend || hasLegendInPreviousSiblings(n)) {
|
||||
// ```
|
||||
// https://github.com/andybalholm/cascadia/blob/master/pseudo_classes.go#L434
|
||||
|
||||
return try inDisabledFieldset(p.?);
|
||||
}
|
||||
|
||||
fn langMatch(lang: []const u8, n: anytype) anyerror!bool {
|
||||
if (try n.attr("lang")) |own| {
|
||||
if (std.mem.eql(u8, own, lang)) return true;
|
||||
|
||||
// check if the lang attr starts with lang+'-'
|
||||
if (std.mem.startsWith(u8, own, lang)) {
|
||||
if (own.len > lang.len and own[lang.len] == '-') return true;
|
||||
}
|
||||
}
|
||||
|
||||
// if the tag doesn't match, try the parent.
|
||||
const p = try n.parent();
|
||||
if (p == null) return false;
|
||||
|
||||
return langMatch(lang, p.?);
|
||||
}
|
||||
|
||||
// onlyChildMatch implements :only-child
|
||||
// If `ofType` is true, it implements :only-of-type instead.
|
||||
fn onlyChildMatch(of_type: bool, n: anytype) anyerror!bool {
|
||||
if (!n.isElement()) return false;
|
||||
|
||||
const p = try n.parent();
|
||||
if (p == null) return false;
|
||||
|
||||
const ntag = try n.tag();
|
||||
|
||||
var count: usize = 0;
|
||||
var c = try p.?.firstChild();
|
||||
// loop hover all n siblings.
|
||||
while (c != null) {
|
||||
// ignore non elements or others tags if of-type is true.
|
||||
if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
|
||||
c = try c.?.nextSibling();
|
||||
continue;
|
||||
}
|
||||
|
||||
count += 1;
|
||||
if (count > 1) return false;
|
||||
|
||||
c = try c.?.nextSibling();
|
||||
}
|
||||
|
||||
return count == 1;
|
||||
}
|
||||
|
||||
// simpleNthLastChildMatch implements :nth-last-child(b).
|
||||
// If ofType is true, implements :nth-last-of-type instead.
|
||||
fn simpleNthLastChildMatch(b: isize, of_type: bool, n: anytype) anyerror!bool {
|
||||
if (!n.isElement()) return false;
|
||||
|
||||
const p = try n.parent();
|
||||
if (p == null) return false;
|
||||
|
||||
const ntag = try n.tag();
|
||||
|
||||
var count: isize = 0;
|
||||
var c = try p.?.lastChild();
|
||||
// loop hover all n siblings.
|
||||
while (c != null) {
|
||||
// ignore non elements or others tags if of-type is true.
|
||||
if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
|
||||
c = try c.?.prevSibling();
|
||||
continue;
|
||||
}
|
||||
|
||||
count += 1;
|
||||
|
||||
if (n.eql(c.?)) return count == b;
|
||||
if (count >= b) return false;
|
||||
|
||||
c = try c.?.prevSibling();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// simpleNthChildMatch implements :nth-child(b).
|
||||
// If ofType is true, implements :nth-of-type instead.
|
||||
fn simpleNthChildMatch(b: isize, of_type: bool, n: anytype) anyerror!bool {
|
||||
if (!n.isElement()) return false;
|
||||
|
||||
const p = try n.parent();
|
||||
if (p == null) return false;
|
||||
|
||||
const ntag = try n.tag();
|
||||
|
||||
var count: isize = 0;
|
||||
var c = try p.?.firstChild();
|
||||
// loop hover all n siblings.
|
||||
while (c != null) {
|
||||
// ignore non elements or others tags if of-type is true.
|
||||
if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
|
||||
c = try c.?.nextSibling();
|
||||
continue;
|
||||
}
|
||||
|
||||
count += 1;
|
||||
|
||||
if (n.eql(c.?)) return count == b;
|
||||
if (count >= b) return false;
|
||||
|
||||
c = try c.?.nextSibling();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// nthChildMatch implements :nth-child(an+b).
|
||||
// If last is true, implements :nth-last-child instead.
|
||||
// If ofType is true, implements :nth-of-type instead.
|
||||
fn nthChildMatch(a: isize, b: isize, last: bool, of_type: bool, n: anytype) anyerror!bool {
|
||||
if (!n.isElement()) return false;
|
||||
|
||||
const p = try n.parent();
|
||||
if (p == null) return false;
|
||||
|
||||
const ntag = try n.tag();
|
||||
|
||||
var i: isize = -1;
|
||||
var count: isize = 0;
|
||||
var c = try p.?.firstChild();
|
||||
// loop hover all n siblings.
|
||||
while (c != null) {
|
||||
// ignore non elements or others tags if of-type is true.
|
||||
if (!c.?.isElement() or (of_type and !std.mem.eql(u8, ntag, try c.?.tag()))) {
|
||||
c = try c.?.nextSibling();
|
||||
continue;
|
||||
}
|
||||
count += 1;
|
||||
|
||||
if (n.eql(c.?)) {
|
||||
i = count;
|
||||
if (!last) break;
|
||||
}
|
||||
|
||||
c = try c.?.nextSibling();
|
||||
}
|
||||
|
||||
if (i == -1) return false;
|
||||
|
||||
if (last) i = count - i + 1;
|
||||
|
||||
i -= b;
|
||||
if (a == 0) return i == 0;
|
||||
return @mod(i, a) == 0 and @divTrunc(i, a) >= 0;
|
||||
}
|
||||
|
||||
fn hasDescendantMatch(s: *const Selector, n: anytype) anyerror!bool {
|
||||
var c = try n.firstChild();
|
||||
while (c != null) {
|
||||
if (try s.match(c.?)) return true;
|
||||
if (c.?.isElement() and try hasDescendantMatch(s, c.?)) return true;
|
||||
c = try c.?.nextSibling();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
fn hasChildMatch(s: *const Selector, n: anytype) anyerror!bool {
|
||||
var c = try n.firstChild();
|
||||
while (c != null) {
|
||||
if (try s.match(c.?)) return true;
|
||||
c = try c.?.nextSibling();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn deinit(sel: Selector, alloc: std.mem.Allocator) void {
|
||||
switch (sel) {
|
||||
.group => |v| {
|
||||
for (v) |vv| vv.deinit(alloc);
|
||||
alloc.free(v);
|
||||
},
|
||||
.compound => |v| {
|
||||
for (v.selectors) |vv| vv.deinit(alloc);
|
||||
alloc.free(v.selectors);
|
||||
},
|
||||
.tag, .id, .class, .pseudo_class_lang => |v| alloc.free(v),
|
||||
.attribute => |att| {
|
||||
alloc.free(att.key);
|
||||
if (att.val) |v| alloc.free(v);
|
||||
if (att.regexp) |v| alloc.free(v);
|
||||
},
|
||||
.combined => |c| {
|
||||
c.first.deinit(alloc);
|
||||
alloc.destroy(c.first);
|
||||
c.second.deinit(alloc);
|
||||
alloc.destroy(c.second);
|
||||
},
|
||||
.pseudo_class_relative => |v| {
|
||||
v.match.deinit(alloc);
|
||||
alloc.destroy(v.match);
|
||||
},
|
||||
.pseudo_class_contains => |v| alloc.free(v.val),
|
||||
.pseudo_class_regexp => |v| alloc.free(v.regexp),
|
||||
.pseudo_class, .pseudo_element, .never_match => {},
|
||||
.pseudo_class_nth, .pseudo_class_only_child => {},
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,10 +1,28 @@
|
||||
// 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 jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const Node = @import("node.zig").Node;
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const Text = @import("text.zig").Text;
|
||||
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
// 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 jsruntime = @import("jsruntime");
|
||||
@@ -5,7 +23,7 @@ const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
const generate = @import("../generate.zig");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const Node = @import("node.zig").Node;
|
||||
const Comment = @import("comment.zig").Comment;
|
||||
|
||||
@@ -1,9 +1,59 @@
|
||||
const parser = @import("../netsurf.zig");
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
const CharacterData = @import("character_data.zig").CharacterData;
|
||||
|
||||
const UserContext = @import("../user_context.zig").UserContext;
|
||||
|
||||
// https://dom.spec.whatwg.org/#interface-comment
|
||||
pub const Comment = struct {
|
||||
pub const Self = parser.Comment;
|
||||
pub const prototype = *CharacterData;
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
pub fn constructor(userctx: UserContext, data: ?[]const u8) !*parser.Comment {
|
||||
return parser.documentCreateComment(
|
||||
parser.documentHTMLToDocument(userctx.document),
|
||||
data orelse "",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var constructor = [_]Case{
|
||||
.{ .src = "let comment = new Comment('foo')", .ex = "undefined" },
|
||||
.{ .src = "comment.data", .ex = "foo" },
|
||||
|
||||
.{ .src = "let emptycomment = new Comment()", .ex = "undefined" },
|
||||
.{ .src = "emptycomment.data", .ex = "" },
|
||||
};
|
||||
try checkCases(js_env, &constructor);
|
||||
}
|
||||
|
||||
79
src/dom/css.zig
Normal file
79
src/dom/css.zig
Normal file
@@ -0,0 +1,79 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const css = @import("../css/css.zig");
|
||||
const Node = @import("../css/libdom.zig").Node;
|
||||
const NodeList = @import("nodelist.zig").NodeList;
|
||||
|
||||
const MatchFirst = struct {
|
||||
n: ?*parser.Node = null,
|
||||
|
||||
pub fn match(m: *MatchFirst, n: Node) !void {
|
||||
m.n = n.node;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn querySelector(alloc: std.mem.Allocator, n: *parser.Node, selector: []const u8) !?*parser.Node {
|
||||
const ps = try css.parse(alloc, selector, .{ .accept_pseudo_elts = true });
|
||||
defer ps.deinit(alloc);
|
||||
|
||||
var m = MatchFirst{};
|
||||
|
||||
_ = try css.matchFirst(ps, Node{ .node = n }, &m);
|
||||
return m.n;
|
||||
}
|
||||
|
||||
const MatchAll = struct {
|
||||
alloc: std.mem.Allocator,
|
||||
nl: NodeList,
|
||||
|
||||
fn init(alloc: std.mem.Allocator) MatchAll {
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
.nl = NodeList.init(),
|
||||
};
|
||||
}
|
||||
|
||||
fn deinit(m: *MatchAll) void {
|
||||
m.nl.deinit(m.alloc);
|
||||
}
|
||||
|
||||
pub fn match(m: *MatchAll, n: Node) !void {
|
||||
try m.nl.append(m.alloc, n.node);
|
||||
}
|
||||
|
||||
fn toOwnedList(m: *MatchAll) NodeList {
|
||||
defer m.nl = NodeList.init();
|
||||
return m.nl;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn querySelectorAll(alloc: std.mem.Allocator, n: *parser.Node, selector: []const u8) !NodeList {
|
||||
const ps = try css.parse(alloc, selector, .{ .accept_pseudo_elts = true });
|
||||
defer ps.deinit(alloc);
|
||||
|
||||
var m = MatchAll.init(alloc);
|
||||
defer m.deinit();
|
||||
|
||||
try css.matchAll(ps, Node{ .node = n }, &m);
|
||||
return m.toOwnedList();
|
||||
}
|
||||
@@ -1,6 +1,24 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
@@ -13,6 +31,7 @@ const NodeUnion = @import("node.zig").Union;
|
||||
|
||||
const Walker = @import("walker.zig").WalkerDepthFirst;
|
||||
const collection = @import("html_collection.zig");
|
||||
const css = @import("css.zig");
|
||||
|
||||
const Element = @import("element.zig").Element;
|
||||
const ElementUnion = @import("element.zig").Union;
|
||||
@@ -21,14 +40,26 @@ const DocumentType = @import("document_type.zig").DocumentType;
|
||||
const DocumentFragment = @import("document_fragment.zig").DocumentFragment;
|
||||
const DOMImplementation = @import("implementation.zig").DOMImplementation;
|
||||
|
||||
const UserContext = @import("../user_context.zig").UserContext;
|
||||
|
||||
// WEB IDL https://dom.spec.whatwg.org/#document
|
||||
pub const Document = struct {
|
||||
pub const Self = parser.Document;
|
||||
pub const prototype = *Node;
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
pub fn constructor() !*parser.Document {
|
||||
return try parser.domImplementationCreateHTMLDocument(null);
|
||||
pub fn constructor(userctx: UserContext) !*parser.DocumentHTML {
|
||||
const doc = try parser.documentCreateDocument(
|
||||
try parser.documentHTMLGetTitle(userctx.document),
|
||||
);
|
||||
|
||||
// we have to work w/ document instead of html document.
|
||||
const ddoc = parser.documentHTMLToDocument(doc);
|
||||
const ccur = parser.documentHTMLToDocument(userctx.document);
|
||||
try parser.documentSetDocumentURI(ddoc, try parser.documentGetDocumentURI(ccur));
|
||||
try parser.documentSetInputEncoding(ddoc, try parser.documentGetInputEncoding(ccur));
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
// JS funcs
|
||||
@@ -188,54 +219,18 @@ pub const Document = struct {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// TODO netsurf doesn't handle query selectors. We have to implement a
|
||||
// solution by ourselves.
|
||||
// For now we handle only * and single id selector like `#foo`.
|
||||
pub fn _querySelector(self: *parser.Document, selectors: []const u8) !?ElementUnion {
|
||||
if (selectors.len == 0) return null;
|
||||
pub fn _querySelector(self: *parser.Document, alloc: std.mem.Allocator, selector: []const u8) !?ElementUnion {
|
||||
if (selector.len == 0) return null;
|
||||
|
||||
// catch-all, return the firstElementChild
|
||||
if (selectors[0] == '*') return try get_firstElementChild(self);
|
||||
const n = try css.querySelector(alloc, parser.documentToNode(self), selector);
|
||||
|
||||
// support only simple id selector.
|
||||
if (selectors[0] != '#' or std.mem.indexOf(u8, selectors, " ") != null) return null;
|
||||
if (n == null) return null;
|
||||
|
||||
return try _getElementById(self, selectors[1..]);
|
||||
return try Element.toInterface(parser.nodeToElement(n.?));
|
||||
}
|
||||
|
||||
// TODO netsurf doesn't handle query selectors. We have to implement a
|
||||
// solution by ourselves.
|
||||
// We handle only * and single id selector like `#foo`.
|
||||
pub fn _querySelectorAll(self: *parser.Document, alloc: std.mem.Allocator, selectors: []const u8) !NodeList {
|
||||
var list = try NodeList.init();
|
||||
errdefer list.deinit(alloc);
|
||||
|
||||
if (selectors.len == 0) return list;
|
||||
|
||||
// catch-all, return all elements
|
||||
if (selectors[0] == '*') {
|
||||
// walk over the node tree fo find the node by id.
|
||||
const root = parser.documentToNode(self);
|
||||
const walker = Walker{};
|
||||
var next: ?*parser.Node = null;
|
||||
while (true) {
|
||||
next = try walker.get_next(root, next) orelse return list;
|
||||
// ignore non-element nodes.
|
||||
if (try parser.nodeType(next.?) != .element) {
|
||||
continue;
|
||||
}
|
||||
try list.append(alloc, next.?);
|
||||
}
|
||||
}
|
||||
|
||||
// support only simple id selector.
|
||||
if (selectors[0] != '#' or std.mem.indexOf(u8, selectors, " ") != null) return list;
|
||||
|
||||
// walk over the node tree fo find the node by id.
|
||||
const e = try parser.documentGetElementById(self, selectors[1..]) orelse return list;
|
||||
try list.append(alloc, parser.elementToNode(e));
|
||||
|
||||
return list;
|
||||
pub fn _querySelectorAll(self: *parser.Document, alloc: std.mem.Allocator, selector: []const u8) !NodeList {
|
||||
return css.querySelectorAll(alloc, parser.documentToNode(self), selector);
|
||||
}
|
||||
|
||||
// TODO according with https://dom.spec.whatwg.org/#parentnode, the
|
||||
@@ -279,6 +274,13 @@ pub fn testExecFn(
|
||||
.{ .src = "newdoc.children.length", .ex = "0" },
|
||||
.{ .src = "newdoc.getElementsByTagName('*').length", .ex = "0" },
|
||||
.{ .src = "newdoc.getElementsByTagName('*').item(0)", .ex = "null" },
|
||||
.{ .src = "newdoc.inputEncoding === document.inputEncoding", .ex = "true" },
|
||||
.{ .src = "newdoc.documentURI === document.documentURI", .ex = "true" },
|
||||
.{ .src = "newdoc.URL === document.URL", .ex = "true" },
|
||||
.{ .src = "newdoc.compatMode === document.compatMode", .ex = "true" },
|
||||
.{ .src = "newdoc.characterSet === document.characterSet", .ex = "true" },
|
||||
.{ .src = "newdoc.charset === document.charset", .ex = "true" },
|
||||
.{ .src = "newdoc.contentType === document.contentType", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &constructor);
|
||||
|
||||
@@ -377,12 +379,14 @@ pub fn testExecFn(
|
||||
var createComment = [_]Case{
|
||||
.{ .src = "var v = document.createComment('foo')", .ex = "undefined" },
|
||||
.{ .src = "v.nodeName", .ex = "#comment" },
|
||||
.{ .src = "let v2 = v.cloneNode()", .ex = "undefined" },
|
||||
};
|
||||
try checkCases(js_env, &createComment);
|
||||
|
||||
var createProcessingInstruction = [_]Case{
|
||||
.{ .src = "let pi = document.createProcessingInstruction('foo', 'bar')", .ex = "undefined" },
|
||||
.{ .src = "pi.target", .ex = "foo" },
|
||||
.{ .src = "let pi2 = pi.cloneNode()", .ex = "undefined" },
|
||||
};
|
||||
try checkCases(js_env, &createProcessingInstruction);
|
||||
|
||||
@@ -426,6 +430,12 @@ pub fn testExecFn(
|
||||
.{ .src = "document.querySelector('*').nodeName", .ex = "HTML" },
|
||||
.{ .src = "document.querySelector('#content').id", .ex = "content" },
|
||||
.{ .src = "document.querySelector('#para').id", .ex = "para" },
|
||||
.{ .src = "document.querySelector('.ok').id", .ex = "link" },
|
||||
.{ .src = "document.querySelector('a ~ p').id", .ex = "para-empty" },
|
||||
.{ .src = "document.querySelector(':root').nodeName", .ex = "HTML" },
|
||||
|
||||
.{ .src = "document.querySelectorAll('p').length", .ex = "2" },
|
||||
.{ .src = "document.querySelectorAll('.ok').item(0).id", .ex = "link" },
|
||||
};
|
||||
try checkCases(js_env, &querySelector);
|
||||
|
||||
@@ -439,7 +449,7 @@ pub fn testExecFn(
|
||||
try checkCases(js_env, &adoptNode);
|
||||
|
||||
const tags = comptime parser.Tag.all();
|
||||
comptime var createElements: [(tags.len) * 2]Case = undefined;
|
||||
var createElements: [(tags.len) * 2]Case = undefined;
|
||||
inline for (tags, 0..) |tag, i| {
|
||||
const tag_name = @tagName(tag);
|
||||
createElements[i * 2] = Case{
|
||||
|
||||
@@ -1,21 +1,56 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
const Node = @import("node.zig").Node;
|
||||
|
||||
const UserContext = @import("../user_context.zig").UserContext;
|
||||
|
||||
// WEB IDL https://dom.spec.whatwg.org/#documentfragment
|
||||
pub const DocumentFragment = struct {
|
||||
pub const Self = parser.DocumentFragment;
|
||||
pub const prototype = *Node;
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
// TODO add constructor, but I need to associate the new DocumentFragment
|
||||
// with the current document global object...
|
||||
// > The new DocumentFragment() constructor steps are to set this’s node
|
||||
// > document to current global object’s associated Document.
|
||||
// https://dom.spec.whatwg.org/#dom-documentfragment-documentfragment
|
||||
pub fn constructor() !*parser.DocumentFragment {
|
||||
return error.NotImplemented;
|
||||
pub fn constructor(userctx: UserContext) !*parser.DocumentFragment {
|
||||
return parser.documentCreateDocumentFragment(
|
||||
parser.documentHTMLToDocument(userctx.document),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var constructor = [_]Case{
|
||||
.{ .src = "const dc = new DocumentFragment()", .ex = "undefined" },
|
||||
.{ .src = "dc.constructor.name", .ex = "DocumentFragment" },
|
||||
};
|
||||
try checkCases(js_env, &constructor);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const Node = @import("node.zig").Node;
|
||||
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
// 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 DOMException = @import("exceptions.zig").DOMException;
|
||||
@@ -7,6 +25,7 @@ const NamedNodeMap = @import("namednodemap.zig").NamedNodeMap;
|
||||
const DOMTokenList = @import("token_list.zig").DOMTokenList;
|
||||
const NodeList = @import("nodelist.zig").NodeList;
|
||||
const Nod = @import("node.zig");
|
||||
const MutationObserver = @import("mutation_observer.zig");
|
||||
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
DOMException,
|
||||
@@ -17,4 +36,5 @@ pub const Interfaces = generate.Tuple(.{
|
||||
NodeList,
|
||||
Nod.Node,
|
||||
Nod.Interfaces,
|
||||
MutationObserver.Interfaces,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
@@ -8,6 +26,8 @@ const checkCases = jsruntime.test_utils.checkCases;
|
||||
const Variadic = jsruntime.Variadic;
|
||||
|
||||
const collection = @import("html_collection.zig");
|
||||
const writeNode = @import("../browser/dump.zig").writeNode;
|
||||
const css = @import("css.zig");
|
||||
|
||||
const Node = @import("node.zig").Node;
|
||||
const Walker = @import("walker.zig").WalkerDepthFirst;
|
||||
@@ -78,6 +98,38 @@ pub const Element = struct {
|
||||
return try parser.nodeGetAttributes(parser.elementToNode(self));
|
||||
}
|
||||
|
||||
pub fn get_innerHTML(self: *parser.Element, alloc: std.mem.Allocator) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
try writeNode(parser.elementToNode(self), buf.writer());
|
||||
// TODO express the caller owned the slice.
|
||||
// https://github.com/lightpanda-io/jsruntime-lib/issues/195
|
||||
return buf.toOwnedSlice();
|
||||
}
|
||||
|
||||
pub fn set_innerHTML(self: *parser.Element, str: []const u8) !void {
|
||||
const node = parser.elementToNode(self);
|
||||
const doc = try parser.nodeOwnerDocument(node) orelse return parser.DOMError.WrongDocument;
|
||||
// parse the fragment
|
||||
const fragment = try parser.documentParseFragmentFromStr(doc, str);
|
||||
|
||||
// remove existing children
|
||||
try Node.removeChildren(node);
|
||||
|
||||
// get fragment body children
|
||||
const children = try parser.documentFragmentBodyChildren(fragment) orelse return;
|
||||
|
||||
// append children to the node
|
||||
const ln = try parser.nodeListLength(children);
|
||||
var i: u32 = 0;
|
||||
while (i < ln) {
|
||||
defer i += 1;
|
||||
const child = try parser.nodeListItem(children, i) orelse continue;
|
||||
_ = try parser.nodeAppendChild(node, child);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _hasAttributes(self: *parser.Element) !bool {
|
||||
return try parser.nodeHasAttributes(parser.elementToNode(self));
|
||||
}
|
||||
@@ -86,14 +138,26 @@ pub const Element = struct {
|
||||
return try parser.elementGetAttribute(self, qname);
|
||||
}
|
||||
|
||||
pub fn _getAttributeNS(self: *parser.Element, ns: []const u8, qname: []const u8) !?[]const u8 {
|
||||
return try parser.elementGetAttributeNS(self, ns, qname);
|
||||
}
|
||||
|
||||
pub fn _setAttribute(self: *parser.Element, qname: []const u8, value: []const u8) !void {
|
||||
return try parser.elementSetAttribute(self, qname, value);
|
||||
}
|
||||
|
||||
pub fn _setAttributeNS(self: *parser.Element, ns: []const u8, qname: []const u8, value: []const u8) !void {
|
||||
return try parser.elementSetAttributeNS(self, ns, qname, value);
|
||||
}
|
||||
|
||||
pub fn _removeAttribute(self: *parser.Element, qname: []const u8) !void {
|
||||
return try parser.elementRemoveAttribute(self, qname);
|
||||
}
|
||||
|
||||
pub fn _removeAttributeNS(self: *parser.Element, ns: []const u8, qname: []const u8) !void {
|
||||
return try parser.elementRemoveAttributeNS(self, ns, qname);
|
||||
}
|
||||
|
||||
pub fn _hasAttribute(self: *parser.Element, qname: []const u8) !bool {
|
||||
return try parser.elementHasAttribute(self, qname);
|
||||
}
|
||||
@@ -230,56 +294,18 @@ pub const Element = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO netsurf doesn't handle query selectors. We have to implement a
|
||||
// solution by ourselves.
|
||||
// We handle only * and single id selector like `#foo`.
|
||||
pub fn _querySelector(self: *parser.Element, selectors: []const u8) !?Union {
|
||||
if (selectors.len == 0) return null;
|
||||
pub fn _querySelector(self: *parser.Element, alloc: std.mem.Allocator, selector: []const u8) !?Union {
|
||||
if (selector.len == 0) return null;
|
||||
|
||||
// catch-all, return the firstElementChild
|
||||
if (selectors[0] == '*') return try get_firstElementChild(self);
|
||||
const n = try css.querySelector(alloc, parser.elementToNode(self), selector);
|
||||
|
||||
// support only simple id selector.
|
||||
if (selectors[0] != '#' or std.mem.indexOf(u8, selectors, " ") != null) return null;
|
||||
if (n == null) return null;
|
||||
|
||||
// walk over the node tree fo find the node by id.
|
||||
const n = try getElementById(self, selectors[1..]) orelse return null;
|
||||
return try toInterface(parser.nodeToElement(n));
|
||||
return try toInterface(parser.nodeToElement(n.?));
|
||||
}
|
||||
|
||||
// TODO netsurf doesn't handle query selectors. We have to implement a
|
||||
// solution by ourselves.
|
||||
// We handle only * and single id selector like `#foo`.
|
||||
pub fn _querySelectorAll(self: *parser.Element, alloc: std.mem.Allocator, selectors: []const u8) !NodeList {
|
||||
var list = try NodeList.init();
|
||||
errdefer list.deinit(alloc);
|
||||
|
||||
if (selectors.len == 0) return list;
|
||||
|
||||
// catch-all, return all elements
|
||||
if (selectors[0] == '*') {
|
||||
// walk over the node tree fo find the node by id.
|
||||
const root = parser.elementToNode(self);
|
||||
const walker = Walker{};
|
||||
var next: ?*parser.Node = null;
|
||||
while (true) {
|
||||
next = try walker.get_next(root, next) orelse return list;
|
||||
// ignore non-element nodes.
|
||||
if (try parser.nodeType(next.?) != .element) {
|
||||
continue;
|
||||
}
|
||||
try list.append(alloc, next.?);
|
||||
}
|
||||
}
|
||||
|
||||
// support only simple id selector.
|
||||
if (selectors[0] != '#' or std.mem.indexOf(u8, selectors, " ") != null) return list;
|
||||
|
||||
// walk over the node tree fo find the node by id.
|
||||
const n = try getElementById(self, selectors[1..]) orelse return list;
|
||||
try list.append(alloc, n);
|
||||
|
||||
return list;
|
||||
pub fn _querySelectorAll(self: *parser.Element, alloc: std.mem.Allocator, selector: []const u8) !NodeList {
|
||||
return css.querySelectorAll(alloc, parser.elementToNode(self), selector);
|
||||
}
|
||||
|
||||
// TODO according with https://dom.spec.whatwg.org/#parentnode, the
|
||||
@@ -400,6 +426,12 @@ pub fn testExecFn(
|
||||
.{ .src = "e.querySelector('#link').id", .ex = "link" },
|
||||
.{ .src = "e.querySelector('#para').id", .ex = "para" },
|
||||
.{ .src = "e.querySelector('*').id", .ex = "link" },
|
||||
.{ .src = "e.querySelector('')", .ex = "null" },
|
||||
.{ .src = "e.querySelector('*').id", .ex = "link" },
|
||||
.{ .src = "e.querySelector('#content')", .ex = "null" },
|
||||
.{ .src = "e.querySelector('#para').id", .ex = "para" },
|
||||
.{ .src = "e.querySelector('.ok').id", .ex = "link" },
|
||||
.{ .src = "e.querySelector('a ~ p').id", .ex = "para-empty" },
|
||||
|
||||
.{ .src = "e.querySelectorAll('foo').length", .ex = "0" },
|
||||
.{ .src = "e.querySelectorAll('#foo').length", .ex = "0" },
|
||||
@@ -408,6 +440,8 @@ pub fn testExecFn(
|
||||
.{ .src = "e.querySelectorAll('#para').length", .ex = "1" },
|
||||
.{ .src = "e.querySelectorAll('#para').item(0).id", .ex = "para" },
|
||||
.{ .src = "e.querySelectorAll('*').length", .ex = "4" },
|
||||
.{ .src = "e.querySelectorAll('p').length", .ex = "2" },
|
||||
.{ .src = "e.querySelectorAll('.ok').item(0).id", .ex = "link" },
|
||||
};
|
||||
try checkCases(js_env, &querySelector);
|
||||
|
||||
@@ -420,4 +454,20 @@ pub fn testExecFn(
|
||||
.{ .src = "f.getAttributeNode('bar')", .ex = "null" },
|
||||
};
|
||||
try checkCases(js_env, &attrNode);
|
||||
|
||||
var innerHTML = [_]Case{
|
||||
.{ .src = "document.getElementById('para').innerHTML", .ex = " And" },
|
||||
.{ .src = "document.getElementById('para-empty').innerHTML.trim()", .ex = "<span id=\"para-empty-child\"></span>" },
|
||||
|
||||
.{ .src = "let h = document.getElementById('para-empty')", .ex = "undefined" },
|
||||
.{ .src = "const prev = h.innerHTML", .ex = "undefined" },
|
||||
.{ .src = "h.innerHTML = '<p id=\"hello\">hello world</p>'", .ex = "<p id=\"hello\">hello world</p>" },
|
||||
.{ .src = "h.innerHTML", .ex = "<p id=\"hello\">hello world</p>" },
|
||||
.{ .src = "h.firstChild.nodeName", .ex = "P" },
|
||||
.{ .src = "h.firstChild.id", .ex = "hello" },
|
||||
.{ .src = "h.firstChild.textContent", .ex = "hello world" },
|
||||
.{ .src = "h.innerHTML = prev; true", .ex = "true" },
|
||||
.{ .src = "document.getElementById('para-empty').innerHTML.trim()", .ex = "<span id=\"para-empty-child\"></span>" },
|
||||
};
|
||||
try checkCases(js_env, &innerHTML);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
// 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 jsruntime = @import("jsruntime");
|
||||
@@ -6,7 +24,8 @@ const JSObjectID = jsruntime.JSObjectID;
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
const EventHandler = @import("../events/event.zig").EventHandler;
|
||||
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
const Nod = @import("node.zig");
|
||||
@@ -54,7 +73,8 @@ pub const EventTarget = struct {
|
||||
self,
|
||||
alloc,
|
||||
eventType,
|
||||
cbk,
|
||||
EventHandler,
|
||||
.{ .cbk = cbk },
|
||||
capture orelse false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
// 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 allocPrint = std.fmt.allocPrint;
|
||||
|
||||
@@ -5,7 +23,7 @@ const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
|
||||
// https://webidl.spec.whatwg.org/#idl-DOMException
|
||||
pub const DOMException = struct {
|
||||
@@ -91,6 +109,12 @@ pub const DOMException = struct {
|
||||
error.InvalidNodeType => "InvalidNodeTypeError",
|
||||
error.DataClone => "DataCloneError",
|
||||
error.NoError => unreachable,
|
||||
|
||||
// custom netsurf error
|
||||
error.UnspecifiedEventType => "UnspecifiedEventTypeError",
|
||||
error.DispatchRequest => "DispatchRequestError",
|
||||
error.NoMemory => "NoMemoryError",
|
||||
error.AttributeWrongType => "AttributeWrongTypeError",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -124,6 +148,12 @@ pub const DOMException = struct {
|
||||
error.InvalidNodeType => 24,
|
||||
error.DataClone => 25,
|
||||
error.NoError => unreachable,
|
||||
|
||||
// custom netsurf error
|
||||
error.UnspecifiedEventType => 128,
|
||||
error.DispatchRequest => 129,
|
||||
error.NoMemory => 130,
|
||||
error.AttributeWrongType => 131,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
@@ -57,7 +75,7 @@ pub const DOMImplementation = struct {
|
||||
return try parser.domImplementationCreateDocument(cnamespace, cqname, doctype);
|
||||
}
|
||||
|
||||
pub fn _createHTMLDocument(_: *DOMImplementation, title: ?[]const u8) !*parser.Document {
|
||||
pub fn _createHTMLDocument(_: *DOMImplementation, title: ?[]const u8) !*parser.DocumentHTML {
|
||||
return try parser.domImplementationCreateHTMLDocument(title);
|
||||
}
|
||||
|
||||
@@ -77,7 +95,8 @@ pub fn testExecFn(
|
||||
) anyerror!void {
|
||||
var getImplementation = [_]Case{
|
||||
.{ .src = "let impl = document.implementation", .ex = "undefined" },
|
||||
.{ .src = "impl.createHTMLDocument();", .ex = "[object Document]" },
|
||||
.{ .src = "impl.createHTMLDocument();", .ex = "[object HTMLDocument]" },
|
||||
.{ .src = "impl.createHTMLDocument('foo');", .ex = "[object HTMLDocument]" },
|
||||
.{ .src = "impl.createDocument(null, 'foo');", .ex = "[object Document]" },
|
||||
.{ .src = "impl.createDocumentType('foo', 'bar', 'baz')", .ex = "[object DocumentType]" },
|
||||
.{ .src = "impl.hasFeature()", .ex = "true" },
|
||||
|
||||
407
src/dom/mutation_observer.zig
Normal file
407
src/dom/mutation_observer.zig
Normal file
@@ -0,0 +1,407 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Callback = jsruntime.Callback;
|
||||
const CallbackResult = jsruntime.CallbackResult;
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
const generate = @import("../generate.zig");
|
||||
|
||||
const NodeList = @import("nodelist.zig").NodeList;
|
||||
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
MutationObserver,
|
||||
MutationRecord,
|
||||
MutationRecords,
|
||||
});
|
||||
|
||||
const Walker = @import("../dom/walker.zig").WalkerChildren;
|
||||
|
||||
const log = std.log.scoped(.events);
|
||||
|
||||
// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
|
||||
pub const MutationObserver = struct {
|
||||
cbk: Callback,
|
||||
observers: Observers,
|
||||
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
const Observer = struct {
|
||||
node: *parser.Node,
|
||||
options: MutationObserverInit,
|
||||
};
|
||||
|
||||
const deinitFunc = struct {
|
||||
fn deinit(ctx: ?*anyopaque, alloc: std.mem.Allocator) void {
|
||||
const o: *Observer = @ptrCast(@alignCast(ctx));
|
||||
alloc.destroy(o);
|
||||
}
|
||||
}.deinit;
|
||||
|
||||
const Observers = std.ArrayListUnmanaged(*Observer);
|
||||
|
||||
pub const MutationObserverInit = struct {
|
||||
childList: bool = false,
|
||||
attributes: bool = false,
|
||||
characterData: bool = false,
|
||||
subtree: bool = false,
|
||||
attributeOldValue: bool = false,
|
||||
characterDataOldValue: bool = false,
|
||||
// TODO
|
||||
// attributeFilter: [][]const u8,
|
||||
|
||||
fn attr(self: MutationObserverInit) bool {
|
||||
return self.attributes or self.attributeOldValue;
|
||||
}
|
||||
|
||||
fn cdata(self: MutationObserverInit) bool {
|
||||
return self.characterData or self.characterDataOldValue;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn constructor(cbk: Callback) !MutationObserver {
|
||||
return MutationObserver{
|
||||
.cbk = cbk,
|
||||
.observers = .{},
|
||||
};
|
||||
}
|
||||
|
||||
// TODO
|
||||
fn resolveOptions(opt: ?MutationObserverInit) MutationObserverInit {
|
||||
return opt orelse .{};
|
||||
}
|
||||
|
||||
pub fn _observe(self: *MutationObserver, alloc: std.mem.Allocator, node: *parser.Node, options: ?MutationObserverInit) !void {
|
||||
const o = try alloc.create(Observer);
|
||||
o.* = .{
|
||||
.node = node,
|
||||
.options = resolveOptions(options),
|
||||
};
|
||||
errdefer alloc.destroy(o);
|
||||
|
||||
// register the new observer.
|
||||
try self.observers.append(alloc, o);
|
||||
|
||||
// register node's events.
|
||||
if (o.options.childList or o.options.subtree) {
|
||||
try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Node, node),
|
||||
alloc,
|
||||
"DOMNodeInserted",
|
||||
EventHandler,
|
||||
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
|
||||
false,
|
||||
);
|
||||
try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Node, node),
|
||||
alloc,
|
||||
"DOMNodeRemoved",
|
||||
EventHandler,
|
||||
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
|
||||
false,
|
||||
);
|
||||
}
|
||||
if (o.options.attr()) {
|
||||
try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Node, node),
|
||||
alloc,
|
||||
"DOMAttrModified",
|
||||
EventHandler,
|
||||
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
|
||||
false,
|
||||
);
|
||||
}
|
||||
if (o.options.cdata()) {
|
||||
try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Node, node),
|
||||
alloc,
|
||||
"DOMCharacterDataModified",
|
||||
EventHandler,
|
||||
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
|
||||
false,
|
||||
);
|
||||
}
|
||||
if (o.options.subtree) {
|
||||
try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Node, node),
|
||||
alloc,
|
||||
"DOMSubtreeModified",
|
||||
EventHandler,
|
||||
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
pub fn _disconnect(_: *MutationObserver) !void {
|
||||
// TODO unregister listeners.
|
||||
}
|
||||
|
||||
pub fn deinit(self: *MutationObserver, alloc: std.mem.Allocator) void {
|
||||
// TODO unregister listeners.
|
||||
for (self.observers.items) |o| alloc.destroy(o);
|
||||
self.observers.deinit(alloc);
|
||||
}
|
||||
|
||||
// TODO
|
||||
pub fn _takeRecords(_: MutationObserver) ?[]const u8 {
|
||||
return &[_]u8{};
|
||||
}
|
||||
};
|
||||
|
||||
// Handle multiple record?
|
||||
pub const MutationRecords = struct {
|
||||
first: ?MutationRecord = null,
|
||||
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
pub fn get_length(self: *MutationRecords) u32 {
|
||||
if (self.first == null) return 0;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
pub fn postAttach(self: *MutationRecords, js_obj: jsruntime.JSObject) !void {
|
||||
if (self.first) |mr| {
|
||||
try js_obj.set("0", mr);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub const MutationRecord = struct {
|
||||
type: []const u8,
|
||||
target: *parser.Node,
|
||||
addedNodes: NodeList = NodeList.init(),
|
||||
removedNodes: NodeList = NodeList.init(),
|
||||
previousSibling: ?*parser.Node = null,
|
||||
nextSibling: ?*parser.Node = null,
|
||||
attributeName: ?[]const u8 = null,
|
||||
attributeNamespace: ?[]const u8 = null,
|
||||
oldValue: ?[]const u8 = null,
|
||||
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
pub fn get_type(self: MutationRecord) []const u8 {
|
||||
return self.type;
|
||||
}
|
||||
|
||||
pub fn get_addedNodes(self: MutationRecord) NodeList {
|
||||
return self.addedNodes;
|
||||
}
|
||||
|
||||
pub fn get_removedNodes(self: MutationRecord) NodeList {
|
||||
return self.addedNodes;
|
||||
}
|
||||
|
||||
pub fn get_target(self: MutationRecord) *parser.Node {
|
||||
return self.target;
|
||||
}
|
||||
|
||||
pub fn get_attributeName(self: MutationRecord) ?[]const u8 {
|
||||
return self.attributeName;
|
||||
}
|
||||
|
||||
pub fn get_attributeNamespace(self: MutationRecord) ?[]const u8 {
|
||||
return self.attributeNamespace;
|
||||
}
|
||||
|
||||
pub fn get_previousSibling(self: MutationRecord) ?*parser.Node {
|
||||
return self.previousSibling;
|
||||
}
|
||||
|
||||
pub fn get_nextSibling(self: MutationRecord) ?*parser.Node {
|
||||
return self.nextSibling;
|
||||
}
|
||||
|
||||
pub fn get_oldValue(self: MutationRecord) ?[]const u8 {
|
||||
return self.oldValue;
|
||||
}
|
||||
};
|
||||
|
||||
// EventHandler dedicated to mutation events.
|
||||
const EventHandler = struct {
|
||||
fn apply(o: *MutationObserver.Observer, target: *parser.Node) bool {
|
||||
// mutation on any target is always ok.
|
||||
if (o.options.subtree) return true;
|
||||
// if target equals node, alway ok.
|
||||
if (target == o.node) return true;
|
||||
|
||||
// no subtree, no same target and no childlist, always noky.
|
||||
if (!o.options.childList) return false;
|
||||
|
||||
// target must be a child of o.node
|
||||
const walker = Walker{};
|
||||
var next: ?*parser.Node = null;
|
||||
while (true) {
|
||||
next = walker.get_next(o.node, next) catch break orelse break;
|
||||
if (next.? == target) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
fn handle(evt: ?*parser.Event, data: parser.EventHandlerData) void {
|
||||
if (evt == null) return;
|
||||
|
||||
var mrs: MutationRecords = .{};
|
||||
|
||||
const t = parser.eventType(evt.?) catch |e| {
|
||||
log.err("mutation observer event type: {any}", .{e});
|
||||
return;
|
||||
};
|
||||
const et = parser.eventTarget(evt.?) catch |e| {
|
||||
log.err("mutation observer event target: {any}", .{e});
|
||||
return;
|
||||
} orelse return;
|
||||
const node = parser.eventTargetToNode(et);
|
||||
|
||||
// retrieve the observer from the data.
|
||||
const o: *MutationObserver.Observer = @ptrCast(@alignCast(data.data));
|
||||
|
||||
if (!apply(o, node)) return;
|
||||
|
||||
const muevt = parser.eventToMutationEvent(evt.?);
|
||||
|
||||
// TODO get the allocator by another way?
|
||||
const alloc = data.cbk.nat_ctx.alloc;
|
||||
|
||||
if (std.mem.eql(u8, t, "DOMAttrModified")) {
|
||||
mrs.first = .{
|
||||
.type = "attributes",
|
||||
.target = o.node,
|
||||
.attributeName = parser.mutationEventAttributeName(muevt) catch null,
|
||||
};
|
||||
|
||||
// record old value if required.
|
||||
if (o.options.attributeOldValue) {
|
||||
mrs.first.?.oldValue = parser.mutationEventPrevValue(muevt) catch null;
|
||||
}
|
||||
} else if (std.mem.eql(u8, t, "DOMCharacterDataModified")) {
|
||||
mrs.first = .{
|
||||
.type = "characterData",
|
||||
.target = o.node,
|
||||
};
|
||||
|
||||
// record old value if required.
|
||||
if (o.options.characterDataOldValue) {
|
||||
mrs.first.?.oldValue = parser.mutationEventPrevValue(muevt) catch null;
|
||||
}
|
||||
} else if (std.mem.eql(u8, t, "DOMNodeInserted")) {
|
||||
mrs.first = .{
|
||||
.type = "childList",
|
||||
.target = o.node,
|
||||
.addedNodes = NodeList.init(),
|
||||
.removedNodes = NodeList.init(),
|
||||
};
|
||||
|
||||
const rn = parser.mutationEventRelatedNode(muevt) catch null;
|
||||
if (rn) |n| {
|
||||
mrs.first.?.addedNodes.append(alloc, n) catch |e| {
|
||||
log.err("mutation event handler error: {any}", .{e});
|
||||
return;
|
||||
};
|
||||
}
|
||||
} else if (std.mem.eql(u8, t, "DOMNodeRemoved")) {
|
||||
mrs.first = .{
|
||||
.type = "childList",
|
||||
.target = o.node,
|
||||
.addedNodes = NodeList.init(),
|
||||
.removedNodes = NodeList.init(),
|
||||
};
|
||||
|
||||
const rn = parser.mutationEventRelatedNode(muevt) catch null;
|
||||
if (rn) |n| {
|
||||
mrs.first.?.removedNodes.append(alloc, n) catch |e| {
|
||||
log.err("mutation event handler error: {any}", .{e});
|
||||
return;
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
var res = CallbackResult.init(alloc);
|
||||
defer res.deinit();
|
||||
|
||||
// TODO pass MutationRecords and MutationObserver
|
||||
data.cbk.trycall(.{mrs}, &res) catch |e| log.err("mutation event handler error: {any}", .{e});
|
||||
|
||||
// in case of function error, we log the result and the trace.
|
||||
if (!res.success) {
|
||||
log.info("mutation observer event handler error: {s}", .{res.result orelse "unknown"});
|
||||
log.debug("{s}", .{res.stack orelse "no stack trace"});
|
||||
}
|
||||
}
|
||||
}.handle;
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var constructor = [_]Case{
|
||||
.{ .src = "new MutationObserver(() => {}).observe(document, { childList: true });", .ex = "undefined" },
|
||||
};
|
||||
try checkCases(js_env, &constructor);
|
||||
|
||||
var attr = [_]Case{
|
||||
.{ .src =
|
||||
\\var nb = 0;
|
||||
\\var mrs;
|
||||
\\new MutationObserver((mu) => {
|
||||
\\ mrs = mu;
|
||||
\\ nb++;
|
||||
\\}).observe(document.firstElementChild, { attributes: true, attributeOldValue: true });
|
||||
\\document.firstElementChild.setAttribute("foo", "bar");
|
||||
\\// ignored b/c it's about another target.
|
||||
\\document.firstElementChild.firstChild.setAttribute("foo", "bar");
|
||||
\\nb;
|
||||
, .ex = "1" },
|
||||
.{ .src = "mrs[0].type", .ex = "attributes" },
|
||||
.{ .src = "mrs[0].target == document.firstElementChild", .ex = "true" },
|
||||
.{ .src = "mrs[0].target.getAttribute('foo')", .ex = "bar" },
|
||||
.{ .src = "mrs[0].attributeName", .ex = "foo" },
|
||||
.{ .src = "mrs[0].oldValue", .ex = "null" },
|
||||
};
|
||||
try checkCases(js_env, &attr);
|
||||
|
||||
var cdata = [_]Case{
|
||||
.{ .src =
|
||||
\\var node = document.getElementById("para").firstChild;
|
||||
\\var nb2 = 0;
|
||||
\\var mrs2;
|
||||
\\new MutationObserver((mu) => {
|
||||
\\ mrs2 = mu;
|
||||
\\ nb2++;
|
||||
\\}).observe(node, { characterData: true, characterDataOldValue: true });
|
||||
\\node.data = "foo";
|
||||
\\nb2;
|
||||
, .ex = "1" },
|
||||
.{ .src = "mrs2[0].type", .ex = "characterData" },
|
||||
.{ .src = "mrs2[0].target == node", .ex = "true" },
|
||||
.{ .src = "mrs2[0].target.data", .ex = "foo" },
|
||||
.{ .src = "mrs2[0].oldValue", .ex = " And" },
|
||||
};
|
||||
try checkCases(js_env, &cdata);
|
||||
}
|
||||
@@ -1,6 +1,24 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
// 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 jsruntime = @import("jsruntime");
|
||||
@@ -8,7 +26,7 @@ const Variadic = jsruntime.Variadic;
|
||||
|
||||
const generate = @import("../generate.zig");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const EventTarget = @import("event_target.zig").EventTarget;
|
||||
|
||||
@@ -199,7 +217,7 @@ pub const Node = struct {
|
||||
}
|
||||
|
||||
pub fn get_childNodes(self: *parser.Node, alloc: std.mem.Allocator) !NodeList {
|
||||
var list = try NodeList.init();
|
||||
var list = NodeList.init();
|
||||
errdefer list.deinit(alloc);
|
||||
|
||||
var n = try parser.nodeFirstChild(self) orelse return list;
|
||||
@@ -259,14 +277,30 @@ pub const Node = struct {
|
||||
return try Node.toInterface(res);
|
||||
}
|
||||
|
||||
// Check if the hierarchy node tree constraints are respected.
|
||||
// For now, it checks only if new nodes are not self.
|
||||
// TODO implements the others contraints.
|
||||
// see https://dom.spec.whatwg.org/#concept-node-tree
|
||||
pub fn hierarchy(self: *parser.Node, nodes: ?Variadic(*parser.Node)) !bool {
|
||||
if (nodes == null) return true;
|
||||
if (nodes.?.slice.len == 0) return true;
|
||||
|
||||
for (nodes.?.slice) |node| if (self == node) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO according with https://dom.spec.whatwg.org/#parentnode, the
|
||||
// function must accept either node or string.
|
||||
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/114
|
||||
pub fn prepend(self: *parser.Node, nodes: ?Variadic(*parser.Node)) !void {
|
||||
if (nodes == null) return;
|
||||
if (nodes.?.slice.len == 0) return;
|
||||
const first = try parser.nodeFirstChild(self);
|
||||
|
||||
// check hierarchy
|
||||
if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest;
|
||||
|
||||
const first = try parser.nodeFirstChild(self);
|
||||
if (first == null) {
|
||||
for (nodes.?.slice) |node| {
|
||||
_ = try parser.nodeAppendChild(self, node);
|
||||
@@ -285,6 +319,10 @@ pub const Node = struct {
|
||||
pub fn append(self: *parser.Node, nodes: ?Variadic(*parser.Node)) !void {
|
||||
if (nodes == null) return;
|
||||
if (nodes.?.slice.len == 0) return;
|
||||
|
||||
// check hierarchy
|
||||
if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest;
|
||||
|
||||
for (nodes.?.slice) |node| {
|
||||
_ = try parser.nodeAppendChild(self, node);
|
||||
}
|
||||
@@ -297,17 +335,11 @@ pub const Node = struct {
|
||||
if (nodes == null) return;
|
||||
if (nodes.?.slice.len == 0) return;
|
||||
|
||||
// check hierarchy
|
||||
if (!try hierarchy(self, nodes)) return parser.DOMError.HierarchyRequest;
|
||||
|
||||
// remove existing children
|
||||
if (try parser.nodeHasChildNodes(self)) {
|
||||
const children = try parser.nodeGetChildNodes(self);
|
||||
const ln = try parser.nodeListLength(children);
|
||||
var i: u32 = 0;
|
||||
while (i < ln) {
|
||||
defer i += 1;
|
||||
const child = try parser.nodeListItem(children, i) orelse continue;
|
||||
_ = try parser.nodeRemoveChild(self, child);
|
||||
}
|
||||
}
|
||||
try removeChildren(self);
|
||||
|
||||
// add new children
|
||||
for (nodes.?.slice) |node| {
|
||||
@@ -315,6 +347,21 @@ pub const Node = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn removeChildren(self: *parser.Node) !void {
|
||||
if (!try parser.nodeHasChildNodes(self)) return;
|
||||
|
||||
const children = try parser.nodeGetChildNodes(self);
|
||||
const ln = try parser.nodeListLength(children);
|
||||
var i: u32 = 0;
|
||||
while (i < ln) {
|
||||
defer i += 1;
|
||||
// we always retrieve the 0 index child on purpose: libdom nodelist
|
||||
// are dynamic. So the next child to remove is always as pos 0.
|
||||
const child = try parser.nodeListItem(children, 0) orelse continue;
|
||||
_ = try parser.nodeRemoveChild(self, child);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(_: *parser.Node, _: std.mem.Allocator) void {}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
@@ -26,7 +44,7 @@ pub const NodeList = struct {
|
||||
|
||||
nodes: NodesArrayList,
|
||||
|
||||
pub fn init() !NodeList {
|
||||
pub fn init() NodeList {
|
||||
return NodeList{
|
||||
.nodes = NodesArrayList{},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,29 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const Node = @import("node.zig").Node;
|
||||
|
||||
// https://dom.spec.whatwg.org/#processinginstruction
|
||||
pub const ProcessingInstruction = struct {
|
||||
@@ -8,18 +31,39 @@ pub const ProcessingInstruction = struct {
|
||||
|
||||
// TODO for libdom processing instruction inherit from node.
|
||||
// But the spec says it must inherit from CDATA.
|
||||
// Moreover, inherit from Node causes also a crash with cloneNode.
|
||||
// https://github.com/lightpanda-io/browsercore/issues/123
|
||||
//
|
||||
// In consequence, for now, we don't implement all these func for
|
||||
// ProcessingInstruction.
|
||||
//
|
||||
//pub const prototype = *CharacterData;
|
||||
|
||||
pub const prototype = *Node;
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
pub fn get_target(self: *parser.ProcessingInstruction) ![]const u8 {
|
||||
// libdom stores the ProcessingInstruction target in the node's name.
|
||||
return try parser.nodeName(@as(*parser.Node, @ptrCast(self)));
|
||||
return try parser.nodeName(parser.processingInstructionToNode(self));
|
||||
}
|
||||
|
||||
pub fn _cloneNode(self: *parser.ProcessingInstruction, _: ?bool) !*parser.ProcessingInstruction {
|
||||
return try parser.processInstructionCopy(self);
|
||||
}
|
||||
|
||||
pub fn get_data(self: *parser.ProcessingInstruction) !?[]const u8 {
|
||||
return try parser.nodeValue(parser.processingInstructionToNode(self));
|
||||
}
|
||||
|
||||
pub fn set_data(self: *parser.ProcessingInstruction, data: []u8) !void {
|
||||
try parser.nodeSetValue(parser.processingInstructionToNode(self), data);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var createProcessingInstruction = [_]Case{
|
||||
.{ .src = "let pi = document.createProcessingInstruction('foo', 'bar')", .ex = "undefined" },
|
||||
.{ .src = "pi.target", .ex = "foo" },
|
||||
.{ .src = "pi.data", .ex = "bar" },
|
||||
.{ .src = "pi.data = 'foo'", .ex = "foo" },
|
||||
.{ .src = "pi.data", .ex = "foo" },
|
||||
|
||||
.{ .src = "let pi2 = pi.cloneNode()", .ex = "undefined" },
|
||||
};
|
||||
try checkCases(js_env, &createProcessingInstruction);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
// 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 jsruntime = @import("jsruntime");
|
||||
@@ -5,11 +23,13 @@ const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
const generate = @import("../generate.zig");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const CharacterData = @import("character_data.zig").CharacterData;
|
||||
const CDATASection = @import("cdata_section.zig").CDATASection;
|
||||
|
||||
const UserContext = @import("../user_context.zig").UserContext;
|
||||
|
||||
// Text interfaces
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
CDATASection,
|
||||
@@ -20,6 +40,13 @@ pub const Text = struct {
|
||||
pub const prototype = *CharacterData;
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
pub fn constructor(userctx: UserContext, data: ?[]const u8) !*parser.Text {
|
||||
return parser.documentCreateTextNode(
|
||||
parser.documentHTMLToDocument(userctx.document),
|
||||
data orelse "",
|
||||
);
|
||||
}
|
||||
|
||||
// JS funcs
|
||||
// --------
|
||||
|
||||
@@ -44,6 +71,15 @@ pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var constructor = [_]Case{
|
||||
.{ .src = "let t = new Text('foo')", .ex = "undefined" },
|
||||
.{ .src = "t.data", .ex = "foo" },
|
||||
|
||||
.{ .src = "let emptyt = new Text()", .ex = "undefined" },
|
||||
.{ .src = "emptyt.data", .ex = "" },
|
||||
};
|
||||
try checkCases(js_env, &constructor);
|
||||
|
||||
var get_whole_text = [_]Case{
|
||||
.{ .src = "let text = document.getElementById('link').firstChild", .ex = "undefined" },
|
||||
.{ .src = "text.wholeText === 'OK'", .ex = "true" },
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
|
||||
pub const Walker = union(enum) {
|
||||
walkerDepthFirst: WalkerDepthFirst,
|
||||
|
||||
@@ -1,13 +1,32 @@
|
||||
// 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 generate = @import("../generate.zig");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Callback = jsruntime.Callback;
|
||||
const CallbackResult = jsruntime.CallbackResult;
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const DOMException = @import("../dom/exceptions.zig").DOMException;
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
@@ -15,6 +34,8 @@ const EventTargetUnion = @import("../dom/event_target.zig").Union;
|
||||
|
||||
const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
|
||||
|
||||
const log = std.log.scoped(.events);
|
||||
|
||||
// Event interfaces
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
Event,
|
||||
@@ -218,3 +239,25 @@ pub fn testExecFn(
|
||||
};
|
||||
try checkCases(js_env, &remove);
|
||||
}
|
||||
|
||||
pub const EventHandler = struct {
|
||||
fn handle(event: ?*parser.Event, data: parser.EventHandlerData) void {
|
||||
// TODO get the allocator by another way?
|
||||
var res = CallbackResult.init(data.cbk.nat_ctx.alloc);
|
||||
defer res.deinit();
|
||||
|
||||
if (event) |evt| {
|
||||
data.cbk.trycall(.{
|
||||
Event.toInterface(evt) catch unreachable,
|
||||
}, &res) catch |e| log.err("event handler error: {any}", .{e});
|
||||
} else {
|
||||
data.cbk.trycall(.{event}, &res) catch |e| log.err("event handler error: {any}", .{e});
|
||||
}
|
||||
|
||||
// in case of function error, we log the result and the trace.
|
||||
if (!res.success) {
|
||||
log.info("event handler error: {s}", .{res.result orelse "unknown"});
|
||||
log.debug("{s}", .{res.stack orelse "no stack trace"});
|
||||
}
|
||||
}
|
||||
}.handle;
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
// 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");
|
||||
|
||||
@@ -10,6 +28,8 @@ fn itoa(comptime i: u8) ![]const u8 {
|
||||
len = 1;
|
||||
} else if (i < 100) {
|
||||
len = 2;
|
||||
} else if (i < 1000) {
|
||||
len = 3;
|
||||
} else {
|
||||
return error.GenerateTooMuchMembers;
|
||||
}
|
||||
@@ -17,9 +37,9 @@ fn itoa(comptime i: u8) ![]const u8 {
|
||||
return try std.fmt.bufPrint(buf[0..], "{d}", .{i});
|
||||
}
|
||||
|
||||
fn fmtName(comptime T: type) []const u8 {
|
||||
fn fmtName(comptime T: type) [:0]const u8 {
|
||||
var it = std.mem.splitBackwards(u8, @typeName(T), ".");
|
||||
return it.first();
|
||||
return it.first() ++ "";
|
||||
}
|
||||
|
||||
// Union
|
||||
@@ -150,7 +170,11 @@ pub const Union = struct {
|
||||
T = *T;
|
||||
}
|
||||
union_fields[done] = .{
|
||||
.name = fmtName(member_T),
|
||||
// UnionField.name expect a null terminated string.
|
||||
// concatenate the `[]const u8` string with an empty string
|
||||
// literal (`name ++ ""`) to explicitly coerce it to `[:0]const
|
||||
// u8`.
|
||||
.name = fmtName(member_T) ++ "",
|
||||
.type = T,
|
||||
.alignment = @alignOf(T),
|
||||
};
|
||||
@@ -158,7 +182,7 @@ pub const Union = struct {
|
||||
}
|
||||
}
|
||||
const union_info = std.builtin.Type.Union{
|
||||
.layout = .Auto,
|
||||
.layout = .auto,
|
||||
.tag_type = enum_T,
|
||||
.fields = &union_fields,
|
||||
.decls = &decls,
|
||||
@@ -268,7 +292,11 @@ fn TupleT(comptime tuple: anytype) type {
|
||||
continue;
|
||||
}
|
||||
fields[done] = .{
|
||||
.name = try itoa(done),
|
||||
// StructField.name expect a null terminated string.
|
||||
// concatenate the `[]const u8` string with an empty string
|
||||
// literal (`name ++ ""`) to explicitly coerce it to `[:0]const
|
||||
// u8`.
|
||||
.name = try itoa(done) ++ "",
|
||||
.type = type,
|
||||
.default_value = null,
|
||||
.is_comptime = false,
|
||||
@@ -278,7 +306,7 @@ fn TupleT(comptime tuple: anytype) type {
|
||||
}
|
||||
const decls: [0]std.builtin.Type.Declaration = undefined;
|
||||
const info = std.builtin.Type.Struct{
|
||||
.layout = .Auto,
|
||||
.layout = .auto,
|
||||
.fields = &fields,
|
||||
.decls = &decls,
|
||||
.is_tuple = true,
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
@@ -80,7 +98,7 @@ pub const HTMLDocument = struct {
|
||||
}
|
||||
|
||||
pub fn _getElementsByName(self: *parser.DocumentHTML, alloc: std.mem.Allocator, name: []const u8) !NodeList {
|
||||
var list = try NodeList.init();
|
||||
var list = NodeList.init();
|
||||
errdefer list.deinit(alloc);
|
||||
|
||||
if (name.len == 0) return list;
|
||||
|
||||
@@ -1,7 +1,31 @@
|
||||
const parser = @import("../netsurf.zig");
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
const generate = @import("../generate.zig");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
const Element = @import("../dom/element.zig").Element;
|
||||
const URL = @import("../url/url.zig").URL;
|
||||
|
||||
// HTMLElement interfaces
|
||||
pub const Interfaces = .{
|
||||
@@ -108,10 +132,286 @@ pub const HTMLUnknownElement = struct {
|
||||
pub const mem_guarantied = true;
|
||||
};
|
||||
|
||||
// https://html.spec.whatwg.org/#the-a-element
|
||||
pub const HTMLAnchorElement = struct {
|
||||
pub const Self = parser.Anchor;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
pub fn get_target(self: *parser.Anchor) ![]const u8 {
|
||||
return try parser.anchorGetTarget(self);
|
||||
}
|
||||
|
||||
pub fn set_target(self: *parser.Anchor, href: []const u8) !void {
|
||||
return try parser.anchorSetTarget(self, href);
|
||||
}
|
||||
|
||||
pub fn get_download(_: *parser.Anchor) ![]const u8 {
|
||||
return ""; // TODO
|
||||
}
|
||||
|
||||
pub fn get_href(self: *parser.Anchor) ![]const u8 {
|
||||
return try parser.anchorGetHref(self);
|
||||
}
|
||||
|
||||
pub fn set_href(self: *parser.Anchor, href: []const u8) !void {
|
||||
return try parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
pub fn get_hreflang(self: *parser.Anchor) ![]const u8 {
|
||||
return try parser.anchorGetHrefLang(self);
|
||||
}
|
||||
|
||||
pub fn set_hreflang(self: *parser.Anchor, href: []const u8) !void {
|
||||
return try parser.anchorSetHrefLang(self, href);
|
||||
}
|
||||
|
||||
pub fn get_type(self: *parser.Anchor) ![]const u8 {
|
||||
return try parser.anchorGetType(self);
|
||||
}
|
||||
|
||||
pub fn set_type(self: *parser.Anchor, t: []const u8) !void {
|
||||
return try parser.anchorSetType(self, t);
|
||||
}
|
||||
|
||||
pub fn get_rel(self: *parser.Anchor) ![]const u8 {
|
||||
return try parser.anchorGetRel(self);
|
||||
}
|
||||
|
||||
pub fn set_rel(self: *parser.Anchor, t: []const u8) !void {
|
||||
return try parser.anchorSetRel(self, t);
|
||||
}
|
||||
|
||||
pub fn get_text(self: *parser.Anchor) !?[]const u8 {
|
||||
return try parser.nodeTextContent(parser.anchorToNode(self));
|
||||
}
|
||||
|
||||
pub fn set_text(self: *parser.Anchor, v: []const u8) !void {
|
||||
return try parser.nodeSetTextContent(parser.anchorToNode(self), v);
|
||||
}
|
||||
|
||||
inline fn url(self: *parser.Anchor, alloc: std.mem.Allocator) !URL {
|
||||
const href = try parser.anchorGetHref(self);
|
||||
return URL.constructor(alloc, href, null); // TODO inject base url
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_origin(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
return try u.get_origin(alloc);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_protocol(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
return u.get_protocol(alloc);
|
||||
}
|
||||
|
||||
pub fn set_protocol(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
u.uri.scheme = v;
|
||||
const href = try u.format(alloc);
|
||||
defer alloc.free(href);
|
||||
|
||||
try parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_host(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
return try u.get_host(alloc);
|
||||
}
|
||||
|
||||
pub fn set_host(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void {
|
||||
// search : separator
|
||||
var p: ?u16 = null;
|
||||
var h: []const u8 = undefined;
|
||||
for (v, 0..) |c, i| {
|
||||
if (c == ':') {
|
||||
h = v[0..i];
|
||||
p = try std.fmt.parseInt(u16, v[i + 1 ..], 10);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
if (p) |pp| {
|
||||
u.uri.host = .{ .raw = h };
|
||||
u.uri.port = pp;
|
||||
} else {
|
||||
u.uri.host = .{ .raw = v };
|
||||
u.uri.port = null;
|
||||
}
|
||||
|
||||
const href = try u.format(alloc);
|
||||
defer alloc.free(href);
|
||||
|
||||
try parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_hostname(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
return try alloc.dupe(u8, u.get_hostname());
|
||||
}
|
||||
|
||||
pub fn set_hostname(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
u.uri.host = .{ .raw = v };
|
||||
const href = try u.format(alloc);
|
||||
try parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_port(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
return try u.get_port(alloc);
|
||||
}
|
||||
|
||||
pub fn set_port(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
if (v != null and v.?.len > 0) {
|
||||
u.uri.port = try std.fmt.parseInt(u16, v.?, 10);
|
||||
} else {
|
||||
u.uri.port = null;
|
||||
}
|
||||
|
||||
const href = try u.format(alloc);
|
||||
defer alloc.free(href);
|
||||
|
||||
try parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_username(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
return try alloc.dupe(u8, u.get_username());
|
||||
}
|
||||
|
||||
pub fn set_username(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
if (v) |vv| {
|
||||
u.uri.user = .{ .raw = vv };
|
||||
} else {
|
||||
u.uri.user = null;
|
||||
}
|
||||
const href = try u.format(alloc);
|
||||
defer alloc.free(href);
|
||||
|
||||
try parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_password(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
return try alloc.dupe(u8, u.get_password());
|
||||
}
|
||||
|
||||
pub fn set_password(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
if (v) |vv| {
|
||||
u.uri.password = .{ .raw = vv };
|
||||
} else {
|
||||
u.uri.password = null;
|
||||
}
|
||||
const href = try u.format(alloc);
|
||||
defer alloc.free(href);
|
||||
|
||||
try parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_pathname(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
return try alloc.dupe(u8, u.get_pathname());
|
||||
}
|
||||
|
||||
pub fn set_pathname(self: *parser.Anchor, alloc: std.mem.Allocator, v: []const u8) !void {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
u.uri.path = .{ .raw = v };
|
||||
const href = try u.format(alloc);
|
||||
defer alloc.free(href);
|
||||
|
||||
try parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_search(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
return try u.get_search(alloc);
|
||||
}
|
||||
|
||||
pub fn set_search(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
if (v) |vv| {
|
||||
u.uri.query = .{ .raw = vv };
|
||||
} else {
|
||||
u.uri.query = null;
|
||||
}
|
||||
const href = try u.format(alloc);
|
||||
defer alloc.free(href);
|
||||
|
||||
try parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
pub fn get_hash(self: *parser.Anchor, alloc: std.mem.Allocator) ![]const u8 {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
return try u.get_hash(alloc);
|
||||
}
|
||||
|
||||
pub fn set_hash(self: *parser.Anchor, alloc: std.mem.Allocator, v: ?[]const u8) !void {
|
||||
var u = try url(self, alloc);
|
||||
defer u.deinit(alloc);
|
||||
|
||||
if (v) |vv| {
|
||||
u.uri.fragment = .{ .raw = vv };
|
||||
} else {
|
||||
u.uri.fragment = null;
|
||||
}
|
||||
const href = try u.format(alloc);
|
||||
defer alloc.free(href);
|
||||
|
||||
try parser.anchorSetHref(self, href);
|
||||
}
|
||||
|
||||
pub fn deinit(_: *parser.Anchor, _: std.mem.Allocator) void {}
|
||||
};
|
||||
|
||||
pub const HTMLAppletElement = struct {
|
||||
@@ -390,10 +690,120 @@ pub const HTMLQuoteElement = struct {
|
||||
pub const mem_guarantied = true;
|
||||
};
|
||||
|
||||
// https://html.spec.whatwg.org/#the-script-element
|
||||
pub const HTMLScriptElement = struct {
|
||||
pub const Self = parser.Script;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
pub fn get_src(self: *parser.Script) !?[]const u8 {
|
||||
return try parser.elementGetAttribute(
|
||||
parser.scriptToElt(self),
|
||||
"src",
|
||||
) orelse "";
|
||||
}
|
||||
|
||||
pub fn set_src(self: *parser.Script, v: []const u8) !void {
|
||||
return try parser.elementSetAttribute(
|
||||
parser.scriptToElt(self),
|
||||
"src",
|
||||
v,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn get_type(self: *parser.Script) !?[]const u8 {
|
||||
return try parser.elementGetAttribute(
|
||||
parser.scriptToElt(self),
|
||||
"type",
|
||||
) orelse "";
|
||||
}
|
||||
|
||||
pub fn set_type(self: *parser.Script, v: []const u8) !void {
|
||||
return try parser.elementSetAttribute(
|
||||
parser.scriptToElt(self),
|
||||
"type",
|
||||
v,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn get_text(self: *parser.Script) !?[]const u8 {
|
||||
return try parser.elementGetAttribute(
|
||||
parser.scriptToElt(self),
|
||||
"text",
|
||||
) orelse "";
|
||||
}
|
||||
|
||||
pub fn set_text(self: *parser.Script, v: []const u8) !void {
|
||||
return try parser.elementSetAttribute(
|
||||
parser.scriptToElt(self),
|
||||
"text",
|
||||
v,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn get_integrity(self: *parser.Script) !?[]const u8 {
|
||||
return try parser.elementGetAttribute(
|
||||
parser.scriptToElt(self),
|
||||
"integrity",
|
||||
) orelse "";
|
||||
}
|
||||
|
||||
pub fn set_integrity(self: *parser.Script, v: []const u8) !void {
|
||||
return try parser.elementSetAttribute(
|
||||
parser.scriptToElt(self),
|
||||
"integrity",
|
||||
v,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn get_async(self: *parser.Script) !bool {
|
||||
_ = try parser.elementGetAttribute(
|
||||
parser.scriptToElt(self),
|
||||
"async",
|
||||
) orelse return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn set_async(self: *parser.Script, v: bool) !void {
|
||||
if (v) {
|
||||
return try parser.elementSetAttribute(parser.scriptToElt(self), "async", "");
|
||||
}
|
||||
|
||||
return try parser.elementRemoveAttribute(parser.scriptToElt(self), "async");
|
||||
}
|
||||
|
||||
pub fn get_defer(self: *parser.Script) !bool {
|
||||
_ = try parser.elementGetAttribute(
|
||||
parser.scriptToElt(self),
|
||||
"defer",
|
||||
) orelse false;
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn set_defer(self: *parser.Script, v: bool) !void {
|
||||
if (v) {
|
||||
return try parser.elementSetAttribute(parser.scriptToElt(self), "defer", "");
|
||||
}
|
||||
|
||||
return try parser.elementRemoveAttribute(parser.scriptToElt(self), "defer");
|
||||
}
|
||||
|
||||
pub fn get_noModule(self: *parser.Script) !bool {
|
||||
_ = try parser.elementGetAttribute(
|
||||
parser.scriptToElt(self),
|
||||
"nomodule",
|
||||
) orelse false;
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn set_noModule(self: *parser.Script, v: bool) !void {
|
||||
if (v) {
|
||||
return try parser.elementSetAttribute(parser.scriptToElt(self), "nomodule", "");
|
||||
}
|
||||
|
||||
return try parser.elementRemoveAttribute(parser.scriptToElt(self), "nomodule");
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLSelectElement = struct {
|
||||
@@ -571,3 +981,82 @@ pub fn toInterface(comptime T: type, e: *parser.Element) !T {
|
||||
.undef => .{ .HTMLUnknownElement = @as(*parser.Unknown, @ptrCast(elem)) },
|
||||
};
|
||||
}
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var anchor = [_]Case{
|
||||
.{ .src = "let a = document.getElementById('link')", .ex = "undefined" },
|
||||
.{ .src = "a.target", .ex = "" },
|
||||
.{ .src = "a.target = '_blank'", .ex = "_blank" },
|
||||
.{ .src = "a.target", .ex = "_blank" },
|
||||
.{ .src = "a.target = ''", .ex = "" },
|
||||
|
||||
.{ .src = "a.href", .ex = "foo" },
|
||||
.{ .src = "a.href = 'https://lightpanda.io/'", .ex = "https://lightpanda.io/" },
|
||||
.{ .src = "a.href", .ex = "https://lightpanda.io/" },
|
||||
|
||||
.{ .src = "a.origin", .ex = "https://lightpanda.io" },
|
||||
|
||||
.{ .src = "a.host = 'lightpanda.io:443'", .ex = "lightpanda.io:443" },
|
||||
.{ .src = "a.host", .ex = "lightpanda.io:443" },
|
||||
.{ .src = "a.port", .ex = "443" },
|
||||
.{ .src = "a.hostname", .ex = "lightpanda.io" },
|
||||
|
||||
.{ .src = "a.host = 'lightpanda.io'", .ex = "lightpanda.io" },
|
||||
.{ .src = "a.host", .ex = "lightpanda.io" },
|
||||
.{ .src = "a.port", .ex = "" },
|
||||
.{ .src = "a.hostname", .ex = "lightpanda.io" },
|
||||
|
||||
.{ .src = "a.host", .ex = "lightpanda.io" },
|
||||
.{ .src = "a.hostname", .ex = "lightpanda.io" },
|
||||
.{ .src = "a.hostname = 'foo.bar'", .ex = "foo.bar" },
|
||||
.{ .src = "a.href", .ex = "https://foo.bar/" },
|
||||
|
||||
.{ .src = "a.search", .ex = "" },
|
||||
.{ .src = "a.search = 'q=bar'", .ex = "q=bar" },
|
||||
.{ .src = "a.search", .ex = "?q=bar" },
|
||||
.{ .src = "a.href", .ex = "https://foo.bar/?q=bar" },
|
||||
|
||||
.{ .src = "a.hash", .ex = "" },
|
||||
.{ .src = "a.hash = 'frag'", .ex = "frag" },
|
||||
.{ .src = "a.hash", .ex = "#frag" },
|
||||
.{ .src = "a.href", .ex = "https://foo.bar/?q=bar#frag" },
|
||||
|
||||
.{ .src = "a.port", .ex = "" },
|
||||
.{ .src = "a.port = '443'", .ex = "443" },
|
||||
.{ .src = "a.host", .ex = "foo.bar:443" },
|
||||
.{ .src = "a.hostname", .ex = "foo.bar" },
|
||||
.{ .src = "a.href", .ex = "https://foo.bar:443/?q=bar#frag" },
|
||||
.{ .src = "a.port = null", .ex = "null" },
|
||||
.{ .src = "a.href", .ex = "https://foo.bar/?q=bar#frag" },
|
||||
|
||||
.{ .src = "a.href = 'foo'", .ex = "foo" },
|
||||
|
||||
.{ .src = "a.type", .ex = "" },
|
||||
.{ .src = "a.type = 'text/html'", .ex = "text/html" },
|
||||
.{ .src = "a.type", .ex = "text/html" },
|
||||
.{ .src = "a.type = ''", .ex = "" },
|
||||
|
||||
.{ .src = "a.text", .ex = "OK" },
|
||||
.{ .src = "a.text = 'foo'", .ex = "foo" },
|
||||
.{ .src = "a.text", .ex = "foo" },
|
||||
.{ .src = "a.text = 'OK'", .ex = "OK" },
|
||||
};
|
||||
try checkCases(js_env, &anchor);
|
||||
|
||||
var script = [_]Case{
|
||||
.{ .src = "let script = document.createElement('script')", .ex = "undefined" },
|
||||
.{ .src = "script.src = 'foo.bar'", .ex = "foo.bar" },
|
||||
|
||||
.{ .src = "script.async = true", .ex = "true" },
|
||||
.{ .src = "script.async", .ex = "true" },
|
||||
.{ .src = "script.async = false", .ex = "false" },
|
||||
.{ .src = "script.async", .ex = "false" },
|
||||
};
|
||||
try checkCases(js_env, &script);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
// 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 HTMLDocument = @import("document.zig").HTMLDocument;
|
||||
|
||||
@@ -1,31 +1,58 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
|
||||
const storage = @import("../storage/storage.zig");
|
||||
|
||||
// https://dom.spec.whatwg.org/#interface-window-extensions
|
||||
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
|
||||
pub const Window = struct {
|
||||
pub const prototype = *EventTarget;
|
||||
pub const mem_guarantied = true;
|
||||
pub const global_type = true;
|
||||
|
||||
// Extend libdom event target for pure zig struct.
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{},
|
||||
|
||||
document: ?*parser.Document = null,
|
||||
document: ?*parser.DocumentHTML = null,
|
||||
target: []const u8,
|
||||
|
||||
storageShelf: ?*storage.Shelf = null,
|
||||
|
||||
pub fn create(target: ?[]const u8) Window {
|
||||
return Window{
|
||||
.target = target orelse "",
|
||||
};
|
||||
}
|
||||
|
||||
pub fn replaceDocument(self: *Window, doc: *parser.Document) void {
|
||||
pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) void {
|
||||
self.document = doc;
|
||||
}
|
||||
|
||||
pub fn setStorageShelf(self: *Window, shelf: *storage.Shelf) void {
|
||||
self.storageShelf = shelf;
|
||||
}
|
||||
|
||||
pub fn get_window(self: *Window) *Window {
|
||||
return self;
|
||||
}
|
||||
@@ -38,11 +65,21 @@ pub const Window = struct {
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn get_document(self: *Window) ?*parser.Document {
|
||||
pub fn get_document(self: *Window) ?*parser.DocumentHTML {
|
||||
return self.document;
|
||||
}
|
||||
|
||||
pub fn get_name(self: *Window) []const u8 {
|
||||
return self.target;
|
||||
}
|
||||
|
||||
pub fn get_localStorage(self: *Window) !*storage.Bottle {
|
||||
if (self.storageShelf == null) return parser.DOMError.NotSupported;
|
||||
return &self.storageShelf.?.bucket.local;
|
||||
}
|
||||
|
||||
pub fn get_sessionStorage(self: *Window) !*storage.Bottle {
|
||||
if (self.storageShelf == null) return parser.DOMError.NotSupported;
|
||||
return &self.storageShelf.?.bucket.session;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
pub const html: []const u8 =
|
||||
\\<main id='content'>
|
||||
\\<a href='foo'>OK</a>
|
||||
|
||||
458
src/lexbor.zig
458
src/lexbor.zig
@@ -1,458 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
const c = @cImport({
|
||||
@cInclude("lexbor/html/html.h");
|
||||
});
|
||||
|
||||
// Public API
|
||||
// ----------
|
||||
|
||||
// Tag
|
||||
|
||||
pub const Tag = enum(u8) {
|
||||
a = c.LXB_TAG_A,
|
||||
area = c.LXB_TAG_AREA,
|
||||
audio = c.LXB_TAG_AUDIO,
|
||||
br = c.LXB_TAG_BR,
|
||||
base = c.LXB_TAG_BASE,
|
||||
body = c.LXB_TAG_BODY,
|
||||
button = c.LXB_TAG_BUTTON,
|
||||
canvas = c.LXB_TAG_CANVAS,
|
||||
dl = c.LXB_TAG_DL,
|
||||
dialog = c.LXB_TAG_DIALOG,
|
||||
data = c.LXB_TAG_DATA,
|
||||
div = c.LXB_TAG_DIV,
|
||||
embed = c.LXB_TAG_EMBED,
|
||||
fieldset = c.LXB_TAG_FIELDSET,
|
||||
form = c.LXB_TAG_FORM,
|
||||
frameset = c.LXB_TAG_FRAMESET,
|
||||
hr = c.LXB_TAG_HR,
|
||||
head = c.LXB_TAG_HEAD,
|
||||
h1 = c.LXB_TAG_H1,
|
||||
h2 = c.LXB_TAG_H2,
|
||||
h3 = c.LXB_TAG_H3,
|
||||
h4 = c.LXB_TAG_H4,
|
||||
h5 = c.LXB_TAG_H5,
|
||||
h6 = c.LXB_TAG_H6,
|
||||
html = c.LXB_TAG_HTML,
|
||||
iframe = c.LXB_TAG_IFRAME,
|
||||
img = c.LXB_TAG_IMG,
|
||||
input = c.LXB_TAG_INPUT,
|
||||
li = c.LXB_TAG_LI,
|
||||
label = c.LXB_TAG_LABEL,
|
||||
legend = c.LXB_TAG_LEGEND,
|
||||
link = c.LXB_TAG_LINK,
|
||||
map = c.LXB_TAG_MAP,
|
||||
meta = c.LXB_TAG_META,
|
||||
meter = c.LXB_TAG_METER,
|
||||
ins = c.LXB_TAG_INS,
|
||||
del = c.LXB_TAG_DEL,
|
||||
ol = c.LXB_TAG_OL,
|
||||
object = c.LXB_TAG_OBJECT,
|
||||
optgroup = c.LXB_TAG_OPTGROUP,
|
||||
option = c.LXB_TAG_OPTION,
|
||||
output = c.LXB_TAG_OUTPUT,
|
||||
p = c.LXB_TAG_P,
|
||||
picture = c.LXB_TAG_PICTURE,
|
||||
pre = c.LXB_TAG_PRE,
|
||||
progress = c.LXB_TAG_PROGRESS,
|
||||
blockquote = c.LXB_TAG_BLOCKQUOTE,
|
||||
q = c.LXB_TAG_Q,
|
||||
script = c.LXB_TAG_SCRIPT,
|
||||
select = c.LXB_TAG_SELECT,
|
||||
source = c.LXB_TAG_SOURCE,
|
||||
span = c.LXB_TAG_SPAN,
|
||||
style = c.LXB_TAG_STYLE,
|
||||
table = c.LXB_TAG_TABLE,
|
||||
caption = c.LXB_TAG_CAPTION,
|
||||
th = c.LXB_TAG_TH,
|
||||
td = c.LXB_TAG_TD,
|
||||
col = c.LXB_TAG_COL,
|
||||
tr = c.LXB_TAG_TR,
|
||||
thead = c.LXB_TAG_THEAD,
|
||||
tbody = c.LXB_TAG_TBODY,
|
||||
tfoot = c.LXB_TAG_TFOOT,
|
||||
template = c.LXB_TAG_TEMPLATE,
|
||||
textarea = c.LXB_TAG_TEXTAREA,
|
||||
time = c.LXB_TAG_TIME,
|
||||
title = c.LXB_TAG_TITLE,
|
||||
track = c.LXB_TAG_TRACK,
|
||||
ul = c.LXB_TAG_UL,
|
||||
video = c.LXB_TAG_VIDEO,
|
||||
undef = c.LXB_TAG__UNDEF,
|
||||
|
||||
pub fn all() []Tag {
|
||||
comptime {
|
||||
const info = @typeInfo(Tag).Enum;
|
||||
comptime var l: [info.fields.len]Tag = undefined;
|
||||
inline for (info.fields, 0..) |field, i| {
|
||||
l[i] = @as(Tag, @enumFromInt(field.value));
|
||||
}
|
||||
return &l;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn allElements() [][]const u8 {
|
||||
comptime {
|
||||
const tags = all();
|
||||
var names: [tags.len][]const u8 = undefined;
|
||||
inline for (tags, 0..) |tag, i| {
|
||||
names[i] = tag.elementName();
|
||||
}
|
||||
return &names;
|
||||
}
|
||||
}
|
||||
|
||||
fn upperName(comptime name: []const u8) []const u8 {
|
||||
comptime {
|
||||
var upper_name: [name.len]u8 = undefined;
|
||||
for (name, 0..) |char, i| {
|
||||
var to_upper = false;
|
||||
if (i == 0) {
|
||||
to_upper = true;
|
||||
} else if (i == 1 and name.len == 2) {
|
||||
to_upper = true;
|
||||
}
|
||||
if (to_upper) {
|
||||
upper_name[i] = std.ascii.toUpper(char);
|
||||
} else {
|
||||
upper_name[i] = char;
|
||||
}
|
||||
}
|
||||
return &upper_name;
|
||||
}
|
||||
}
|
||||
|
||||
fn elementName(comptime tag: Tag) []const u8 {
|
||||
return switch (tag) {
|
||||
.a => "Anchor",
|
||||
.dl => "DList",
|
||||
.fieldset => "FieldSet",
|
||||
.frameset => "FrameSet",
|
||||
.h1, .h2, .h3, .h4, .h5, .h6 => "Heading",
|
||||
.iframe => "IFrame",
|
||||
.img => "Image",
|
||||
.ins, .del => "Mod",
|
||||
.ol => "OList",
|
||||
.optgroup => "OptGroup",
|
||||
.p => "Paragraph",
|
||||
.blockquote, .q => "Quote",
|
||||
.caption => "TableCaption",
|
||||
.th, .td => "TableCell",
|
||||
.col => "TableCol",
|
||||
.tr => "TableRow",
|
||||
.thead, .tbody, .tfoot => "TableSection",
|
||||
.textarea => "TextArea",
|
||||
.ul => "UList",
|
||||
.undef => "Unknown",
|
||||
else => upperName(@tagName(tag)),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// EventTarget
|
||||
|
||||
pub const EventTarget = c.lxb_dom_event_target_t;
|
||||
|
||||
// Node
|
||||
|
||||
pub const Node = c.lxb_dom_node_t;
|
||||
|
||||
pub const NodeType = enum(u4) {
|
||||
undef,
|
||||
element,
|
||||
attribute,
|
||||
text,
|
||||
cdata_section,
|
||||
entity_reference,
|
||||
entity,
|
||||
processing_instruction,
|
||||
comment,
|
||||
document,
|
||||
document_type,
|
||||
document_fragment,
|
||||
notation,
|
||||
last_entry,
|
||||
};
|
||||
|
||||
pub inline fn nodeEventTarget(node: *Node) *EventTarget {
|
||||
return c.lxb_dom_interface_event_target(node);
|
||||
}
|
||||
|
||||
pub inline fn nodeTag(node: *Node) Tag {
|
||||
// FIXME: lxb_dom_node_tag_id returns a big number if element is unknwon
|
||||
// while it should return 0 (value of enum LXB_TAG__UNDEF).
|
||||
// This fix the problem by assuming that a value greater than an u8 (the basis
|
||||
// of Tag enum) is 0.
|
||||
var val = c.lxb_dom_node_tag_id(node);
|
||||
if (val > 256) {
|
||||
val = 0;
|
||||
}
|
||||
return @as(Tag, @enumFromInt(val));
|
||||
}
|
||||
|
||||
pub const nodeWalker = (fn (node: ?*Node, _: ?*anyopaque) callconv(.C) Action);
|
||||
|
||||
pub inline fn nodeName(node: *Node) [*c]const u8 {
|
||||
var s: usize = undefined;
|
||||
return c.lxb_dom_node_name(node, &s);
|
||||
}
|
||||
|
||||
pub inline fn nodeType(node: *Node) NodeType {
|
||||
return @as(NodeType, @enumFromInt(node.*.type));
|
||||
}
|
||||
|
||||
pub inline fn nodeWalk(node: *Node, comptime walker: nodeWalker) !void {
|
||||
c.lxb_dom_node_simple_walk(node, walker, null);
|
||||
}
|
||||
|
||||
// Element
|
||||
|
||||
pub const Element = c.lxb_dom_element_t;
|
||||
|
||||
pub inline fn elementNode(element: *Element) *Node {
|
||||
return c.lxb_dom_interface_node(element);
|
||||
}
|
||||
|
||||
pub inline fn elementLocalName(element: *Element) []const u8 {
|
||||
var size: usize = undefined;
|
||||
const local_name = c.lxb_dom_element_local_name(element, &size);
|
||||
return std.mem.sliceTo(local_name, 0);
|
||||
}
|
||||
|
||||
pub inline fn elementsByAttr(
|
||||
element: *Element,
|
||||
collection: *Collection,
|
||||
attr: []const u8,
|
||||
value: []const u8,
|
||||
case_sensitve: bool,
|
||||
) !void {
|
||||
const status = c.lxb_dom_elements_by_attr(
|
||||
element,
|
||||
collection,
|
||||
attr.ptr,
|
||||
attr.len,
|
||||
value.ptr,
|
||||
value.len,
|
||||
case_sensitve,
|
||||
);
|
||||
if (status != 0) {
|
||||
return error.ElementsByAttr;
|
||||
}
|
||||
}
|
||||
|
||||
// DocumentHTML
|
||||
|
||||
pub const DocumentHTML = c.lxb_html_document_t;
|
||||
|
||||
pub inline fn documentHTMLInit() *DocumentHTML {
|
||||
return c.lxb_html_document_create();
|
||||
}
|
||||
|
||||
pub inline fn documentHTMLDeinit(document_html: *DocumentHTML) void {
|
||||
_ = c.lxb_html_document_destroy(document_html);
|
||||
}
|
||||
|
||||
pub inline fn documentHTMLParse(document_html: *DocumentHTML, html: []const u8) !void {
|
||||
const status = c.lxb_html_document_parse(document_html, html.ptr, html.len - 1);
|
||||
if (status != 0) {
|
||||
return error.DocumentHTMLParse;
|
||||
}
|
||||
}
|
||||
|
||||
pub inline fn documentHTMLToNode(document_html: *DocumentHTML) *Node {
|
||||
return c.lxb_dom_interface_node(document_html);
|
||||
}
|
||||
|
||||
pub inline fn documentHTMLToDocument(document_html: *DocumentHTML) *Document {
|
||||
return &document_html.dom_document;
|
||||
}
|
||||
|
||||
pub inline fn documentHTMLBody(document_html: *DocumentHTML) *Body {
|
||||
return document_html.body;
|
||||
}
|
||||
|
||||
// Document
|
||||
|
||||
pub const Document = c.lxb_dom_document_t;
|
||||
|
||||
pub inline fn documentCreateElement(document: *Document, tag_name: []const u8) *Element {
|
||||
return c.lxb_dom_document_create_element(document, tag_name.ptr, tag_name.len, null);
|
||||
}
|
||||
|
||||
// Collection
|
||||
|
||||
pub const Collection = c.lxb_dom_collection_t;
|
||||
|
||||
pub inline fn collectionInit(document: *Document, size: usize) *Collection {
|
||||
return c.lxb_dom_collection_make(document, size);
|
||||
}
|
||||
|
||||
pub inline fn collectionDeinit(collection: *Collection) void {
|
||||
_ = c.lxb_dom_collection_destroy(collection, true);
|
||||
}
|
||||
|
||||
pub inline fn collectionElement(collection: *Collection, index: usize) *Element {
|
||||
return c.lxb_dom_collection_element(collection, index);
|
||||
}
|
||||
|
||||
// HTML Elements
|
||||
|
||||
pub const HTMLElement = c.lxb_html_element_t;
|
||||
pub const MediaElement = c.lxb_html_media_element_t;
|
||||
|
||||
pub const Unknown = c.lxb_html_unknown_element_t;
|
||||
pub const Anchor = c.lxb_html_anchor_element_t;
|
||||
pub const Area = c.lxb_html_area_element_t;
|
||||
pub const Audio = c.lxb_html_audio_element_t;
|
||||
pub const BR = c.lxb_html_br_element_t;
|
||||
pub const Base = c.lxb_html_base_element_t;
|
||||
pub const Body = c.lxb_html_body_element_t;
|
||||
pub const Button = c.lxb_html_button_element_t;
|
||||
pub const Canvas = c.lxb_html_canvas_element_t;
|
||||
pub const DList = c.lxb_html_d_list_element_t;
|
||||
pub const Data = c.lxb_html_data_element_t;
|
||||
pub const Dialog = c.lxb_html_dialog_element_t;
|
||||
pub const Div = c.lxb_html_div_element_t;
|
||||
pub const Embed = c.lxb_html_embed_element_t;
|
||||
pub const FieldSet = c.lxb_html_field_set_element_t;
|
||||
pub const Form = c.lxb_html_form_element_t;
|
||||
pub const FrameSet = c.lxb_html_frame_set_element_t;
|
||||
pub const HR = c.lxb_html_hr_element_t;
|
||||
pub const Head = c.lxb_html_head_element_t;
|
||||
pub const Heading = c.lxb_html_heading_element_t;
|
||||
pub const Html = c.lxb_html_html_element_t;
|
||||
pub const IFrame = c.lxb_html_iframe_element_t;
|
||||
pub const Image = c.lxb_html_image_element_t;
|
||||
pub const Input = c.lxb_html_input_element_t;
|
||||
pub const LI = c.lxb_html_li_element_t;
|
||||
pub const Label = c.lxb_html_label_element_t;
|
||||
pub const Legend = c.lxb_html_legend_element_t;
|
||||
pub const Link = c.lxb_html_link_element_t;
|
||||
pub const Map = c.lxb_html_map_element_t;
|
||||
pub const Meta = c.lxb_html_meta_element_t;
|
||||
pub const Meter = c.lxb_html_meter_element_t;
|
||||
pub const Mod = c.lxb_html_mod_element_t;
|
||||
pub const OList = c.lxb_html_o_list_element_t;
|
||||
pub const Object = c.lxb_html_object_element_t;
|
||||
pub const OptGroup = c.lxb_html_opt_group_element_t;
|
||||
pub const Option = c.lxb_html_option_element_t;
|
||||
pub const Output = c.lxb_html_output_element_t;
|
||||
pub const Paragraph = c.lxb_html_paragraph_element_t;
|
||||
pub const Picture = c.lxb_html_picture_element_t;
|
||||
pub const Pre = c.lxb_html_pre_element_t;
|
||||
pub const Progress = c.lxb_html_progress_element_t;
|
||||
pub const Quote = c.lxb_html_quote_element_t;
|
||||
pub const Script = c.lxb_html_script_element_t;
|
||||
pub const Select = c.lxb_html_select_element_t;
|
||||
pub const Source = c.lxb_html_source_element_t;
|
||||
pub const Span = c.lxb_html_span_element_t;
|
||||
pub const Style = c.lxb_html_style_element_t;
|
||||
pub const Table = c.lxb_html_table_element_t;
|
||||
pub const TableCaption = c.lxb_html_table_caption_element_t;
|
||||
pub const TableCell = c.lxb_html_table_cell_element_t;
|
||||
pub const TableCol = c.lxb_html_table_col_element_t;
|
||||
pub const TableRow = c.lxb_html_table_row_element_t;
|
||||
pub const TableSection = c.lxb_html_table_section_element_t;
|
||||
pub const Template = c.lxb_html_template_element_t;
|
||||
pub const TextArea = c.lxb_html_text_area_element_t;
|
||||
pub const Time = c.lxb_html_time_element_t;
|
||||
pub const Title = c.lxb_html_title_element_t;
|
||||
pub const Track = c.lxb_html_track_element_t;
|
||||
pub const UList = c.lxb_html_u_list_element_t;
|
||||
pub const Video = c.lxb_html_video_element_t;
|
||||
|
||||
// Base
|
||||
|
||||
pub const Action = c.lexbor_action_t;
|
||||
|
||||
// TODO: use enum?
|
||||
pub const ActionStop = c.LEXBOR_ACTION_STOP;
|
||||
pub const ActionNext = c.LEXBOR_ACTION_NEXT;
|
||||
pub const ActionOk = c.LEXBOR_ACTION_OK;
|
||||
|
||||
// Playground
|
||||
// ----------
|
||||
|
||||
fn serialize_callback(_: [*c]const u8, _: usize, _: ?*anyopaque) callconv(.C) c_uint {
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn walker_play(nn: ?*c.lxb_dom_node_t, _: ?*anyopaque) callconv(.C) c.lexbor_action_t {
|
||||
if (nn == null) {
|
||||
return c.LEXBOR_ACTION_STOP;
|
||||
}
|
||||
const n = nn.?;
|
||||
|
||||
var s: usize = undefined;
|
||||
const name = c.lxb_dom_node_name(n, &s);
|
||||
|
||||
std.debug.print("type: {d}, name: {s}\n", .{ n.*.type, name });
|
||||
if (n.*.local_name == c.LXB_TAG_A) {
|
||||
const element = c.lxb_dom_interface_element(n);
|
||||
const attr = element.*.first_attr;
|
||||
std.debug.print("link, attr: {any}\n", .{attr.*.upper_name});
|
||||
}
|
||||
return c.LEXBOR_ACTION_OK;
|
||||
}
|
||||
|
||||
pub fn parse_document() void {
|
||||
const html = "<div><a href='foo'>OK</a><p>blah-blah-blah</p></div>";
|
||||
const html_len = html.len - 1;
|
||||
|
||||
// parse
|
||||
const doc = c.lxb_html_document_create();
|
||||
const status_parse = c.lxb_html_document_parse(doc, html, html_len);
|
||||
std.debug.print("status parse: {any}\n", .{status_parse});
|
||||
|
||||
// tree
|
||||
const document_node = c.lxb_dom_interface_node(doc);
|
||||
std.debug.print("document node is empty: {any}\n", .{c.lxb_dom_node_is_empty(document_node)});
|
||||
std.debug.print("document node type: {any}\n", .{document_node.*.type});
|
||||
std.debug.print("document node name: {any}\n", .{document_node.*.local_name});
|
||||
|
||||
c.lxb_dom_node_simple_walk(document_node, walker_play, null);
|
||||
|
||||
const first_child = c.lxb_dom_node_last_child(document_node);
|
||||
if (first_child == null) {
|
||||
std.debug.print("hummm is null\n", .{});
|
||||
}
|
||||
std.debug.print("first child type: {any}\n", .{first_child.*.type});
|
||||
std.debug.print("first child name: {any}\n", .{first_child.*.local_name});
|
||||
|
||||
const tt = c.lxb_dom_node_first_child(first_child);
|
||||
std.debug.print("tt type: {any}\n", .{tt.*.type});
|
||||
std.debug.print("tt name: {any}\n", .{tt.*.local_name});
|
||||
std.debug.print("{any}\n", .{c.LXB_DOM_NODE_TYPE_TEXT});
|
||||
|
||||
var s: usize = undefined;
|
||||
const tt_name = c.lxb_dom_node_name(tt, &s);
|
||||
std.debug.print("tt name: {s}\n", .{tt_name});
|
||||
|
||||
const nn = tt.*.first_child;
|
||||
if (nn == null) {
|
||||
std.debug.print("is null\n", .{});
|
||||
}
|
||||
|
||||
// text
|
||||
var text_len: usize = undefined;
|
||||
var text = c.lxb_dom_node_text_content(tt, &text_len);
|
||||
std.debug.print("size: {d}\n", .{text_len});
|
||||
std.debug.print("text: {s}\n", .{text});
|
||||
|
||||
// serialize
|
||||
const status_serialize = c.lxb_html_serialize_pretty_tree_cb(
|
||||
document_node,
|
||||
c.LXB_HTML_SERIALIZE_OPT_UNDEF,
|
||||
0,
|
||||
serialize_callback,
|
||||
null,
|
||||
);
|
||||
std.debug.print("status serialize: {any}\n", .{status_serialize});
|
||||
|
||||
// destroy
|
||||
_ = c.lxb_html_document_destroy(doc);
|
||||
// _ = c.lxb_dom_document_destroy_text(first_child.*.owner_document, &text);
|
||||
// _ = c.lxb_dom_document_destroy_text(c.lxb_dom_interface_document(document), text);
|
||||
std.debug.print("text2: {s}\n", .{text}); // should not work
|
||||
}
|
||||
43
src/main.zig
43
src/main.zig
@@ -1,32 +1,49 @@
|
||||
// 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 jsruntime = @import("jsruntime");
|
||||
|
||||
const parser = @import("netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
const apiweb = @import("apiweb.zig");
|
||||
const Window = @import("html/window.zig").Window;
|
||||
|
||||
pub const Types = jsruntime.reflect(apiweb.Interfaces);
|
||||
pub const UserContext = apiweb.UserContext;
|
||||
|
||||
const socket_path = "/tmp/browsercore-server.sock";
|
||||
|
||||
var doc: *parser.DocumentHTML = undefined;
|
||||
var server: std.net.StreamServer = undefined;
|
||||
var server: std.net.Server = undefined;
|
||||
|
||||
fn execJS(
|
||||
alloc: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
|
||||
// start JS env
|
||||
try js_env.start(alloc);
|
||||
defer js_env.stop();
|
||||
|
||||
// alias global as self and window
|
||||
try js_env.attachObject(try js_env.getGlobal(), "self", null);
|
||||
try js_env.attachObject(try js_env.getGlobal(), "window", null);
|
||||
|
||||
// add document object
|
||||
try js_env.addObject(doc, "document");
|
||||
var window = Window.create(null);
|
||||
window.replaceDocument(doc);
|
||||
try js_env.bindGlobal(window);
|
||||
|
||||
while (true) {
|
||||
|
||||
@@ -58,6 +75,9 @@ pub fn main() !void {
|
||||
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
try parser.init();
|
||||
defer parser.deinit();
|
||||
|
||||
// document
|
||||
const file = try std.fs.cwd().openFile("test.html", .{});
|
||||
defer file.close();
|
||||
@@ -71,7 +91,7 @@ pub fn main() !void {
|
||||
// reuse_address (SO_REUSEADDR flag) does not seems to work on unix socket
|
||||
// see: https://gavv.net/articles/unix-socket-reuse/
|
||||
// TODO: use a lock file instead
|
||||
std.os.unlink(socket_path) catch |err| {
|
||||
std.posix.unlink(socket_path) catch |err| {
|
||||
if (err != error.FileNotFound) {
|
||||
return err;
|
||||
}
|
||||
@@ -79,10 +99,9 @@ pub fn main() !void {
|
||||
|
||||
// server
|
||||
const addr = try std.net.Address.initUnix(socket_path);
|
||||
server = std.net.StreamServer.init(.{});
|
||||
server = try addr.listen(.{});
|
||||
defer server.deinit();
|
||||
try server.listen(addr);
|
||||
std.debug.print("Listening on: {s}...\n", .{socket_path});
|
||||
|
||||
try jsruntime.loadEnv(&arena, execJS);
|
||||
try jsruntime.loadEnv(&arena, null, execJS);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,32 @@
|
||||
// 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 Browser = @import("browser/browser.zig").Browser;
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const apiweb = @import("apiweb.zig");
|
||||
pub const Types = jsruntime.reflect(apiweb.Interfaces);
|
||||
|
||||
pub const std_options = struct {
|
||||
pub const log_level = .debug;
|
||||
pub const Types = jsruntime.reflect(apiweb.Interfaces);
|
||||
pub const UserContext = apiweb.UserContext;
|
||||
|
||||
pub const std_options = std.Options{
|
||||
.log_level = .debug,
|
||||
};
|
||||
|
||||
const usage =
|
||||
@@ -38,7 +58,7 @@ pub fn main() !void {
|
||||
while (args.next()) |arg| {
|
||||
if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "--help", arg)) {
|
||||
try std.io.getStdErr().writer().print(usage, .{execname});
|
||||
std.os.exit(0);
|
||||
std.posix.exit(0);
|
||||
}
|
||||
if (std.mem.eql(u8, "--dump", arg)) {
|
||||
dump = true;
|
||||
@@ -47,14 +67,14 @@ pub fn main() !void {
|
||||
// allow only one url
|
||||
if (url.len != 0) {
|
||||
try std.io.getStdErr().writer().print(usage, .{execname});
|
||||
std.os.exit(1);
|
||||
std.posix.exit(1);
|
||||
}
|
||||
url = arg;
|
||||
}
|
||||
|
||||
if (url.len == 0) {
|
||||
try std.io.getStdErr().writer().print(usage, .{execname});
|
||||
std.os.exit(1);
|
||||
std.posix.exit(1);
|
||||
}
|
||||
|
||||
const vm = jsruntime.VM.init();
|
||||
@@ -64,8 +84,12 @@ pub fn main() !void {
|
||||
defer browser.deinit();
|
||||
|
||||
var page = try browser.currentSession().createPage();
|
||||
defer page.end();
|
||||
defer page.deinit();
|
||||
|
||||
try page.navigate(url);
|
||||
defer page.end();
|
||||
|
||||
try page.wait();
|
||||
|
||||
if (dump) {
|
||||
try page.dump(std.io.getStdOut());
|
||||
|
||||
@@ -1,13 +1,35 @@
|
||||
// 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 jsruntime = @import("jsruntime");
|
||||
|
||||
const parser = @import("netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
const apiweb = @import("apiweb.zig");
|
||||
const Window = @import("html/window.zig").Window;
|
||||
const storage = @import("storage/storage.zig");
|
||||
|
||||
const html_test = @import("html_test.zig").html;
|
||||
|
||||
pub const Types = jsruntime.reflect(apiweb.Interfaces);
|
||||
pub const UserContext = apiweb.UserContext;
|
||||
const Client = @import("async/Client.zig");
|
||||
|
||||
var doc: *parser.DocumentHTML = undefined;
|
||||
|
||||
@@ -15,17 +37,26 @@ fn execJS(
|
||||
alloc: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
|
||||
// start JS env
|
||||
try js_env.start(alloc);
|
||||
defer js_env.stop();
|
||||
|
||||
// alias global as self and window
|
||||
try js_env.attachObject(try js_env.getGlobal(), "self", null);
|
||||
try js_env.attachObject(try js_env.getGlobal(), "window", null);
|
||||
var cli = Client{ .allocator = alloc, .loop = js_env.nat_ctx.loop };
|
||||
defer cli.deinit();
|
||||
|
||||
// add document object
|
||||
try js_env.addObject(doc, "document");
|
||||
try js_env.setUserContext(UserContext{
|
||||
.document = doc,
|
||||
.httpClient = &cli,
|
||||
});
|
||||
|
||||
var storageShelf = storage.Shelf.init(alloc);
|
||||
defer storageShelf.deinit();
|
||||
|
||||
// alias global as self and window
|
||||
var window = Window.create(null);
|
||||
window.replaceDocument(doc);
|
||||
window.setStorageShelf(&storageShelf);
|
||||
try js_env.bindGlobal(window);
|
||||
|
||||
// launch shellExec
|
||||
try jsruntime.shellExec(alloc, js_env);
|
||||
@@ -39,6 +70,9 @@ pub fn main() !void {
|
||||
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
|
||||
defer arena.deinit();
|
||||
|
||||
try parser.init();
|
||||
defer parser.deinit();
|
||||
|
||||
// document
|
||||
const file = try std.fs.cwd().openFile("test.html", .{});
|
||||
defer file.close();
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
// 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 jsruntime = @import("jsruntime");
|
||||
@@ -30,6 +48,8 @@ const Out = enum {
|
||||
};
|
||||
|
||||
pub const Types = jsruntime.reflect(apiweb.Interfaces);
|
||||
pub const GlobalType = apiweb.GlobalType;
|
||||
pub const UserContext = apiweb.UserContext;
|
||||
|
||||
// TODO For now the WPT tests run is specific to WPT.
|
||||
// It manually load js framwork libs, and run the first script w/ js content in
|
||||
@@ -56,7 +76,7 @@ pub fn main() !void {
|
||||
while (args.next()) |arg| {
|
||||
if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "--help", arg)) {
|
||||
try std.io.getStdErr().writer().print(usage, .{execname});
|
||||
std.os.exit(0);
|
||||
std.posix.exit(0);
|
||||
}
|
||||
if (std.mem.eql(u8, "--json", arg)) {
|
||||
out = .json;
|
||||
@@ -194,12 +214,12 @@ pub fn main() !void {
|
||||
}
|
||||
|
||||
try std.json.stringify(output.items, .{ .whitespace = .indent_2 }, std.io.getStdOut().writer());
|
||||
std.os.exit(0);
|
||||
std.posix.exit(0);
|
||||
}
|
||||
|
||||
if (out == .text and failures > 0) {
|
||||
std.debug.print("{d}/{d} tests suites failures\n", .{ failures, run });
|
||||
std.os.exit(1);
|
||||
std.posix.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,7 +323,7 @@ fn runSafe(
|
||||
if (c.pass) pass += 1;
|
||||
}
|
||||
}
|
||||
const status = if (pass == all) "Pass" else "Fail";
|
||||
const status = if (all > 0 and pass == all) "Pass" else "Fail";
|
||||
std.debug.print("{s} {d}/{d}", .{ status, pass, all });
|
||||
|
||||
continue;
|
||||
@@ -346,7 +366,8 @@ fn runSafe(
|
||||
if (c.pass) pass += 1;
|
||||
}
|
||||
}
|
||||
std.debug.print("{d}/{d}\n\n", .{ pass, all });
|
||||
const status = if (all > 0 and pass == all) "Pass" else "Fail";
|
||||
std.debug.print("{s} {d}/{d}\n\n", .{ status, pass, all });
|
||||
}
|
||||
|
||||
if (out == .json) {
|
||||
|
||||
76
src/mimalloc/mimalloc.zig
Normal file
76
src/mimalloc/mimalloc.zig
Normal file
@@ -0,0 +1,76 @@
|
||||
// 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/>.
|
||||
|
||||
// This file makes the glue between mimalloc heap allocation and libdom memory
|
||||
// management.
|
||||
// We replace the libdom default usage of allocations with mimalloc heap
|
||||
// allocation to be able to free all memory used at once, like an arena usage.
|
||||
|
||||
const std = @import("std");
|
||||
const c = @cImport({
|
||||
@cInclude("mimalloc.h");
|
||||
});
|
||||
|
||||
const Error = error{
|
||||
HeapNotNull,
|
||||
HeapNull,
|
||||
};
|
||||
|
||||
var heap: ?*c.mi_heap_t = null;
|
||||
|
||||
pub fn create() Error!void {
|
||||
if (heap != null) return Error.HeapNotNull;
|
||||
heap = c.mi_heap_new();
|
||||
if (heap == null) return Error.HeapNull;
|
||||
}
|
||||
|
||||
pub fn destroy() void {
|
||||
if (heap == null) return;
|
||||
c.mi_heap_destroy(heap.?);
|
||||
heap = null;
|
||||
}
|
||||
|
||||
pub export fn m_alloc(size: usize) callconv(.C) ?*anyopaque {
|
||||
if (heap == null) return null;
|
||||
return c.mi_heap_malloc(heap.?, size);
|
||||
}
|
||||
|
||||
pub export fn re_alloc(ptr: ?*anyopaque, size: usize) callconv(.C) ?*anyopaque {
|
||||
if (heap == null) return null;
|
||||
return c.mi_heap_realloc(heap.?, ptr, size);
|
||||
}
|
||||
|
||||
pub export fn c_alloc(nmemb: usize, size: usize) callconv(.C) ?*anyopaque {
|
||||
if (heap == null) return null;
|
||||
return c.mi_heap_calloc(heap.?, nmemb, size);
|
||||
}
|
||||
|
||||
pub export fn str_dup(s: [*c]const u8) callconv(.C) [*c]u8 {
|
||||
if (heap == null) return null;
|
||||
return c.mi_heap_strdup(heap.?, s);
|
||||
}
|
||||
|
||||
pub export fn strn_dup(s: [*c]const u8, size: usize) callconv(.C) [*c]u8 {
|
||||
if (heap == null) return null;
|
||||
return c.mi_heap_strndup(heap.?, s, size);
|
||||
}
|
||||
|
||||
// NOOP, use destroy to clear all the memory allocated at once.
|
||||
pub export fn f_ree(_: ?*anyopaque) callconv(.C) void {
|
||||
return;
|
||||
}
|
||||
@@ -1,14 +1,52 @@
|
||||
// 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 c = @cImport({
|
||||
@cInclude("dom/dom.h");
|
||||
@cInclude("core/pi.h");
|
||||
@cInclude("dom/bindings/hubbub/parser.h");
|
||||
@cInclude("events/event_target.h");
|
||||
@cInclude("events/event.h");
|
||||
});
|
||||
|
||||
const mimalloc = @import("mimalloc");
|
||||
|
||||
const Callback = @import("jsruntime").Callback;
|
||||
const EventToInterface = @import("events/event.zig").Event.toInterface;
|
||||
|
||||
// init initializes netsurf lib.
|
||||
// init starts a mimalloc heap arena for the netsurf session. The caller must
|
||||
// call deinit() to free the arena memory.
|
||||
pub fn init() !void {
|
||||
try mimalloc.create();
|
||||
}
|
||||
|
||||
// deinit frees the mimalloc heap arena memory.
|
||||
// It also clean dom namespaces and lwc strings.
|
||||
pub fn deinit() void {
|
||||
_ = c.dom_namespace_finalise();
|
||||
|
||||
// destroy all lwc strings.
|
||||
c.lwc_deinit_strings();
|
||||
|
||||
mimalloc.destroy();
|
||||
}
|
||||
|
||||
// Vtable
|
||||
// ------
|
||||
@@ -223,8 +261,8 @@ pub const Tag = enum(u8) {
|
||||
pub fn all() []Tag {
|
||||
comptime {
|
||||
const info = @typeInfo(Tag).Enum;
|
||||
comptime var l: [info.fields.len]Tag = undefined;
|
||||
inline for (info.fields, 0..) |field, i| {
|
||||
var l: [info.fields.len]Tag = undefined;
|
||||
for (info.fields, 0..) |field, i| {
|
||||
l[i] = @as(Tag, @enumFromInt(field.value));
|
||||
}
|
||||
return &l;
|
||||
@@ -235,7 +273,7 @@ pub const Tag = enum(u8) {
|
||||
comptime {
|
||||
const tags = all();
|
||||
var names: [tags.len][]const u8 = undefined;
|
||||
inline for (tags, 0..) |tag, i| {
|
||||
for (tags, 0..) |tag, i| {
|
||||
names[i] = tag.elementName();
|
||||
}
|
||||
return &names;
|
||||
@@ -318,6 +356,12 @@ pub const DOMError = error{
|
||||
Timeout,
|
||||
InvalidNodeType,
|
||||
DataClone,
|
||||
|
||||
// custom netsurf error
|
||||
UnspecifiedEventType,
|
||||
DispatchRequest,
|
||||
NoMemory,
|
||||
AttributeWrongType,
|
||||
};
|
||||
|
||||
const DOMException = c.dom_exception;
|
||||
@@ -342,6 +386,13 @@ fn DOMErr(except: DOMException) DOMError!void {
|
||||
c.DOM_INVALID_ACCESS_ERR => DOMError.InvalidAccess,
|
||||
c.DOM_VALIDATION_ERR => DOMError.Validation,
|
||||
c.DOM_TYPE_MISMATCH_ERR => DOMError.TypeMismatch,
|
||||
|
||||
// custom netsurf error
|
||||
c.DOM_UNSPECIFIED_EVENT_TYPE_ERR => DOMError.UnspecifiedEventType,
|
||||
c.DOM_DISPATCH_REQUEST_ERR => DOMError.DispatchRequest,
|
||||
c.DOM_NO_MEM_ERR => DOMError.NoMemory,
|
||||
c.DOM_ATTR_WRONG_TYPE_ERR => DOMError.AttributeWrongType,
|
||||
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
@@ -376,6 +427,10 @@ pub fn eventType(evt: *Event) ![]const u8 {
|
||||
var s: ?*String = undefined;
|
||||
const err = c._dom_event_get_type(evt, &s);
|
||||
try DOMErr(err);
|
||||
|
||||
// if the event type is null, return a empty string.
|
||||
if (s == null) return "";
|
||||
|
||||
return strToData(s.?);
|
||||
}
|
||||
|
||||
@@ -467,30 +522,34 @@ pub const EventType = enum(u8) {
|
||||
progress_event = 1,
|
||||
};
|
||||
|
||||
// EventHandler
|
||||
fn event_handler_cbk(data: *anyopaque) *Callback {
|
||||
const ptr: *align(@alignOf(*Callback)) anyopaque = @alignCast(data);
|
||||
return @as(*Callback, @ptrCast(ptr));
|
||||
pub const MutationEvent = c.dom_mutation_event;
|
||||
|
||||
pub fn eventToMutationEvent(evt: *Event) *MutationEvent {
|
||||
return @as(*MutationEvent, @ptrCast(evt));
|
||||
}
|
||||
|
||||
const event_handler = struct {
|
||||
fn handle(event: ?*Event, data: ?*anyopaque) callconv(.C) void {
|
||||
if (data) |d| {
|
||||
const func = event_handler_cbk(d);
|
||||
pub fn mutationEventAttributeName(evt: *MutationEvent) ![]const u8 {
|
||||
var s: ?*String = undefined;
|
||||
const err = c._dom_mutation_event_get_attr_name(evt, &s);
|
||||
try DOMErr(err);
|
||||
return strToData(s.?);
|
||||
}
|
||||
|
||||
if (event) |evt| {
|
||||
func.call(.{
|
||||
EventToInterface(evt) catch unreachable,
|
||||
}) catch unreachable;
|
||||
} else {
|
||||
func.call(.{event}) catch unreachable;
|
||||
}
|
||||
// NOTE: we can not call func.deinit here
|
||||
// b/c the handler can be called several times
|
||||
// either on this dispatch event or in anoter one
|
||||
}
|
||||
}
|
||||
}.handle;
|
||||
pub fn mutationEventPrevValue(evt: *MutationEvent) !?[]const u8 {
|
||||
var s: ?*String = undefined;
|
||||
const err = c._dom_mutation_event_get_prev_value(evt, &s);
|
||||
try DOMErr(err);
|
||||
if (s == null) return null;
|
||||
return strToData(s.?);
|
||||
}
|
||||
|
||||
pub fn mutationEventRelatedNode(evt: *MutationEvent) !?*Node {
|
||||
var n: NodeExternal = undefined;
|
||||
const err = c._dom_mutation_event_get_related_node(evt, &n);
|
||||
try DOMErr(err);
|
||||
if (n == null) return null;
|
||||
return @as(*Node, @ptrCast(n));
|
||||
}
|
||||
|
||||
// EventListener
|
||||
pub const EventListener = c.dom_event_listener;
|
||||
@@ -503,6 +562,10 @@ fn eventListenerGetData(lst: *EventListener) ?*anyopaque {
|
||||
// EventTarget
|
||||
pub const EventTarget = c.dom_event_target;
|
||||
|
||||
pub fn eventTargetToNode(et: *EventTarget) *Node {
|
||||
return @as(*Node, @ptrCast(et));
|
||||
}
|
||||
|
||||
fn eventTargetVtable(et: *EventTarget) c.dom_event_target_vtable {
|
||||
// retrieve the vtable
|
||||
const vtable = et.*.vtable.?;
|
||||
@@ -551,10 +614,9 @@ pub fn eventTargetHasListener(
|
||||
// and capture property,
|
||||
// let's check if the callback handler is the same
|
||||
defer c.dom_event_listener_unref(listener);
|
||||
const data = eventListenerGetData(listener);
|
||||
if (data) |d| {
|
||||
const cbk = event_handler_cbk(d);
|
||||
if (cbk_id == cbk.id()) {
|
||||
const ehd = EventHandlerDataInternal.fromListener(listener);
|
||||
if (ehd) |d| {
|
||||
if (cbk_id == d.data.cbk.id()) {
|
||||
return lst;
|
||||
}
|
||||
}
|
||||
@@ -572,21 +634,99 @@ pub fn eventTargetHasListener(
|
||||
return null;
|
||||
}
|
||||
|
||||
// EventHandlerFunc is a zig function called when the event is dispatched to a
|
||||
// listener.
|
||||
// The EventHandlerFunc is responsible to call the callback included into the
|
||||
// EventHandlerData.
|
||||
pub const EventHandlerFunc = *const fn (event: ?*Event, data: EventHandlerData) void;
|
||||
|
||||
// EventHandler implements the function exposed in C and called by libdom.
|
||||
// It retrieves the EventHandlerInternalData and call the EventHandlerFunc with
|
||||
// the EventHandlerData in parameter.
|
||||
const EventHandler = struct {
|
||||
fn handle(event: ?*Event, data: ?*anyopaque) callconv(.C) void {
|
||||
if (data) |d| {
|
||||
const ehd = EventHandlerDataInternal.get(d);
|
||||
ehd.handler(event, ehd.data);
|
||||
|
||||
// NOTE: we can not call func.deinit here
|
||||
// b/c the handler can be called several times
|
||||
// either on this dispatch event or in anoter one
|
||||
}
|
||||
}
|
||||
}.handle;
|
||||
|
||||
// EventHandlerData contains a JS callback and the data associated to the
|
||||
// handler.
|
||||
// If given, deinitFunc is called with the data pointer to allow the creator to
|
||||
// clean memory.
|
||||
// The callback is deinit by EventHandlerDataInternal. It must NOT be deinit
|
||||
// into deinitFunc.
|
||||
pub const EventHandlerData = struct {
|
||||
cbk: Callback,
|
||||
data: ?*anyopaque = null,
|
||||
// deinitFunc implements the data deinitialization.
|
||||
deinitFunc: ?DeinitFunc = null,
|
||||
|
||||
pub const DeinitFunc = *const fn (data: ?*anyopaque, alloc: std.mem.Allocator) void;
|
||||
};
|
||||
|
||||
// EventHandlerDataInternal groups the EventHandlerFunc and the EventHandlerData.
|
||||
const EventHandlerDataInternal = struct {
|
||||
data: EventHandlerData,
|
||||
handler: EventHandlerFunc,
|
||||
|
||||
fn init(alloc: std.mem.Allocator, handler: EventHandlerFunc, data: EventHandlerData) !*EventHandlerDataInternal {
|
||||
const ptr = try alloc.create(EventHandlerDataInternal);
|
||||
ptr.* = .{
|
||||
.data = data,
|
||||
.handler = handler,
|
||||
};
|
||||
return ptr;
|
||||
}
|
||||
|
||||
fn deinit(self: *EventHandlerDataInternal, alloc: std.mem.Allocator) void {
|
||||
if (self.data.deinitFunc) |d| d(self.data.data, alloc);
|
||||
self.data.cbk.deinit(alloc);
|
||||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
fn get(data: *anyopaque) *EventHandlerDataInternal {
|
||||
const ptr: *align(@alignOf(*EventHandlerDataInternal)) anyopaque = @alignCast(data);
|
||||
return @as(*EventHandlerDataInternal, @ptrCast(ptr));
|
||||
}
|
||||
|
||||
// retrieve a EventHandlerDataInternal from a listener.
|
||||
fn fromListener(lst: *EventListener) ?*EventHandlerDataInternal {
|
||||
const data = eventListenerGetData(lst);
|
||||
// free cbk allocation made on eventTargetAddEventListener
|
||||
if (data == null) return null;
|
||||
|
||||
return get(data.?);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn eventTargetAddEventListener(
|
||||
et: *EventTarget,
|
||||
alloc: std.mem.Allocator,
|
||||
typ: []const u8,
|
||||
cbk: Callback,
|
||||
handlerFunc: EventHandlerFunc,
|
||||
data: EventHandlerData,
|
||||
capture: bool,
|
||||
) !void {
|
||||
// this allocation will be removed either on
|
||||
// eventTargetRemoveEventListener or eventTargetRemoveAllEventListeners
|
||||
const cbk_ptr = try alloc.create(Callback);
|
||||
cbk_ptr.* = cbk;
|
||||
const ehd = try EventHandlerDataInternal.init(alloc, handlerFunc, data);
|
||||
errdefer ehd.deinit(alloc);
|
||||
|
||||
const ctx = @as(*anyopaque, @ptrCast(cbk_ptr));
|
||||
// When a function is used as an event handler, its this parameter is bound
|
||||
// to the DOM element on which the listener is placed.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#this_in_dom_event_handlers
|
||||
try ehd.data.cbk.setThisArg(et);
|
||||
|
||||
const ctx = @as(*anyopaque, @ptrCast(ehd));
|
||||
var listener: ?*EventListener = undefined;
|
||||
const errLst = c.dom_event_listener_create(event_handler, ctx, &listener);
|
||||
const errLst = c.dom_event_listener_create(EventHandler, ctx, &listener);
|
||||
try DOMErr(errLst);
|
||||
defer c.dom_event_listener_unref(listener);
|
||||
|
||||
@@ -602,13 +742,9 @@ pub fn eventTargetRemoveEventListener(
|
||||
lst: *EventListener,
|
||||
capture: bool,
|
||||
) !void {
|
||||
const data = eventListenerGetData(lst);
|
||||
// free cbk allocation made on eventTargetAddEventListener
|
||||
if (data) |d| {
|
||||
const cbk_ptr = event_handler_cbk(d);
|
||||
cbk_ptr.deinit(alloc);
|
||||
alloc.destroy(cbk_ptr);
|
||||
}
|
||||
// free data allocation made on eventTargetAddEventListener
|
||||
const ehd = EventHandlerDataInternal.fromListener(lst);
|
||||
if (ehd) |d| d.deinit(alloc);
|
||||
|
||||
const s = try strFromData(typ);
|
||||
const err = eventTargetVtable(et).remove_event_listener.?(et, s, lst, capture);
|
||||
@@ -636,13 +772,10 @@ pub fn eventTargetRemoveAllEventListeners(
|
||||
|
||||
if (lst) |listener| {
|
||||
defer c.dom_event_listener_unref(listener);
|
||||
const data = eventListenerGetData(listener);
|
||||
if (data) |d| {
|
||||
// free cbk allocation made on eventTargetAddEventListener
|
||||
const cbk = event_handler_cbk(d);
|
||||
cbk.deinit(alloc);
|
||||
alloc.destroy(cbk);
|
||||
}
|
||||
|
||||
const ehd = EventHandlerDataInternal.fromListener(listener);
|
||||
if (ehd) |d| d.deinit(alloc);
|
||||
|
||||
const err = eventTargetVtable(et).remove_event_listener.?(
|
||||
et,
|
||||
null,
|
||||
@@ -1235,6 +1368,18 @@ pub const Comment = c.dom_comment;
|
||||
// ProcessingInstruction
|
||||
pub const ProcessingInstruction = c.dom_processing_instruction;
|
||||
|
||||
// processingInstructionToNode is an helper to convert an ProcessingInstruction to a node.
|
||||
pub inline fn processingInstructionToNode(pi: *ProcessingInstruction) *Node {
|
||||
return @as(*Node, @ptrCast(pi));
|
||||
}
|
||||
|
||||
pub fn processInstructionCopy(pi: *ProcessingInstruction) !*ProcessingInstruction {
|
||||
var res: ?*Node = undefined;
|
||||
const err = c._dom_pi_copy(processingInstructionToNode(pi), &res);
|
||||
try DOMErr(err);
|
||||
return @as(*ProcessingInstruction, @ptrCast(res.?));
|
||||
}
|
||||
|
||||
// Attribute
|
||||
pub const Attribute = c.dom_attr;
|
||||
|
||||
@@ -1299,6 +1444,20 @@ pub fn elementGetAttribute(elem: *Element, name: []const u8) !?[]const u8 {
|
||||
return strToData(s.?);
|
||||
}
|
||||
|
||||
pub fn elementGetAttributeNS(elem: *Element, ns: []const u8, name: []const u8) !?[]const u8 {
|
||||
var s: ?*String = undefined;
|
||||
const err = elementVtable(elem).dom_element_get_attribute_ns.?(
|
||||
elem,
|
||||
try strFromData(ns),
|
||||
try strFromData(name),
|
||||
&s,
|
||||
);
|
||||
try DOMErr(err);
|
||||
if (s == null) return null;
|
||||
|
||||
return strToData(s.?);
|
||||
}
|
||||
|
||||
pub fn elementSetAttribute(elem: *Element, qname: []const u8, value: []const u8) !void {
|
||||
const err = elementVtable(elem).dom_element_set_attribute.?(
|
||||
elem,
|
||||
@@ -1308,11 +1467,35 @@ pub fn elementSetAttribute(elem: *Element, qname: []const u8, value: []const u8)
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn elementSetAttributeNS(
|
||||
elem: *Element,
|
||||
ns: []const u8,
|
||||
qname: []const u8,
|
||||
value: []const u8,
|
||||
) !void {
|
||||
const err = elementVtable(elem).dom_element_set_attribute_ns.?(
|
||||
elem,
|
||||
try strFromData(ns),
|
||||
try strFromData(qname),
|
||||
try strFromData(value),
|
||||
);
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn elementRemoveAttribute(elem: *Element, qname: []const u8) !void {
|
||||
const err = elementVtable(elem).dom_element_remove_attribute.?(elem, try strFromData(qname));
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn elementRemoveAttributeNS(elem: *Element, ns: []const u8, qname: []const u8) !void {
|
||||
const err = elementVtable(elem).dom_element_remove_attribute_ns.?(
|
||||
elem,
|
||||
try strFromData(ns),
|
||||
try strFromData(qname),
|
||||
);
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn elementHasAttribute(elem: *Element, qname: []const u8) !bool {
|
||||
var res: bool = undefined;
|
||||
const err = elementVtable(elem).dom_element_has_attribute.?(elem, try strFromData(qname), &res);
|
||||
@@ -1440,6 +1623,85 @@ pub fn elementHTMLGetTagType(elem_html: *ElementHTML) !Tag {
|
||||
return @as(Tag, @enumFromInt(tag_type));
|
||||
}
|
||||
|
||||
// HTMLScriptElement
|
||||
|
||||
// scriptToElt is an helper to convert an script to an element.
|
||||
pub inline fn scriptToElt(s: *Script) *Element {
|
||||
return @as(*Element, @ptrCast(s));
|
||||
}
|
||||
|
||||
// HTMLAnchorElement
|
||||
|
||||
// anchorToNode is an helper to convert an anchor to a node.
|
||||
pub inline fn anchorToNode(a: *Anchor) *Node {
|
||||
return @as(*Node, @ptrCast(a));
|
||||
}
|
||||
|
||||
pub fn anchorGetTarget(a: *Anchor) ![]const u8 {
|
||||
var res: ?*String = undefined;
|
||||
const err = c.dom_html_anchor_element_get_target(a, &res);
|
||||
try DOMErr(err);
|
||||
if (res == null) return "";
|
||||
return strToData(res.?);
|
||||
}
|
||||
|
||||
pub fn anchorSetTarget(a: *Anchor, target: []const u8) !void {
|
||||
const err = c.dom_html_anchor_element_set_target(a, try strFromData(target));
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn anchorGetHref(a: *Anchor) ![]const u8 {
|
||||
var res: ?*String = undefined;
|
||||
const err = c.dom_html_anchor_element_get_href(a, &res);
|
||||
try DOMErr(err);
|
||||
if (res == null) return "";
|
||||
return strToData(res.?);
|
||||
}
|
||||
|
||||
pub fn anchorSetHref(a: *Anchor, href: []const u8) !void {
|
||||
const err = c.dom_html_anchor_element_set_href(a, try strFromData(href));
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn anchorGetHrefLang(a: *Anchor) ![]const u8 {
|
||||
var res: ?*String = undefined;
|
||||
const err = c.dom_html_anchor_element_get_hreflang(a, &res);
|
||||
try DOMErr(err);
|
||||
if (res == null) return "";
|
||||
return strToData(res.?);
|
||||
}
|
||||
|
||||
pub fn anchorSetHrefLang(a: *Anchor, href: []const u8) !void {
|
||||
const err = c.dom_html_anchor_element_set_hreflang(a, try strFromData(href));
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn anchorGetType(a: *Anchor) ![]const u8 {
|
||||
var res: ?*String = undefined;
|
||||
const err = c.dom_html_anchor_element_get_type(a, &res);
|
||||
try DOMErr(err);
|
||||
if (res == null) return "";
|
||||
return strToData(res.?);
|
||||
}
|
||||
|
||||
pub fn anchorSetType(a: *Anchor, t: []const u8) !void {
|
||||
const err = c.dom_html_anchor_element_set_type(a, try strFromData(t));
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn anchorGetRel(a: *Anchor) ![]const u8 {
|
||||
var res: ?*String = undefined;
|
||||
const err = c.dom_html_anchor_element_get_rel(a, &res);
|
||||
try DOMErr(err);
|
||||
if (res == null) return "";
|
||||
return strToData(res.?);
|
||||
}
|
||||
|
||||
pub fn anchorSetRel(a: *Anchor, rel: []const u8) !void {
|
||||
const err = c.dom_html_anchor_element_set_rel(a, try strFromData(rel));
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
// ElementsHTML
|
||||
|
||||
pub const MediaElement = struct { base: *c.dom_html_element };
|
||||
@@ -1514,6 +1776,22 @@ pub const Video = struct { base: *c.dom_html_element };
|
||||
// Document Fragment
|
||||
pub const DocumentFragment = c.dom_document_fragment;
|
||||
|
||||
pub inline fn documentFragmentToNode(doc: *DocumentFragment) *Node {
|
||||
return @as(*Node, @ptrCast(doc));
|
||||
}
|
||||
|
||||
pub fn documentFragmentBodyChildren(doc: *DocumentFragment) !?*NodeList {
|
||||
const node = documentFragmentToNode(doc);
|
||||
const html = try nodeFirstChild(node) orelse return null;
|
||||
// TODO unref
|
||||
const head = try nodeFirstChild(html) orelse return null;
|
||||
// TODO unref
|
||||
const body = try nodeNextSibling(head) orelse return null;
|
||||
// TODO unref
|
||||
|
||||
return try nodeGetChildNodes(body);
|
||||
}
|
||||
|
||||
// Document Position
|
||||
|
||||
pub const DocumentPosition = enum(u2) {
|
||||
@@ -1595,21 +1873,29 @@ pub inline fn domImplementationCreateDocumentType(
|
||||
return dt.?;
|
||||
}
|
||||
|
||||
pub inline fn domImplementationCreateHTMLDocument(title: ?[]const u8) !*Document {
|
||||
var doc: ?*Document = undefined;
|
||||
const err = c.dom_implementation_create_document(
|
||||
c.DOM_IMPLEMENTATION_HTML,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
&doc,
|
||||
);
|
||||
try DOMErr(err);
|
||||
// TODO set title
|
||||
_ = title;
|
||||
return doc.?;
|
||||
pub inline fn domImplementationCreateHTMLDocument(title: ?[]const u8) !*DocumentHTML {
|
||||
const doc_html = try documentCreateDocument(title);
|
||||
const doc = documentHTMLToDocument(doc_html);
|
||||
|
||||
// add hierarchy: html, head, body.
|
||||
const html = try documentCreateElement(doc, "html");
|
||||
_ = try nodeAppendChild(documentToNode(doc), elementToNode(html));
|
||||
|
||||
const head = try documentCreateElement(doc, "head");
|
||||
_ = try nodeAppendChild(elementToNode(html), elementToNode(head));
|
||||
|
||||
if (title) |t| {
|
||||
try documentHTMLSetTitle(doc_html, t);
|
||||
const htitle = try documentCreateElement(doc, "title");
|
||||
const txt = try documentCreateTextNode(doc, t);
|
||||
_ = try nodeAppendChild(elementToNode(htitle), @as(*Node, @ptrCast(txt)));
|
||||
_ = try nodeAppendChild(elementToNode(head), elementToNode(htitle));
|
||||
}
|
||||
|
||||
const body = try documentCreateElement(doc, "body");
|
||||
_ = try nodeAppendChild(elementToNode(html), elementToNode(body));
|
||||
|
||||
return doc_html;
|
||||
}
|
||||
|
||||
// Document
|
||||
@@ -1653,6 +1939,11 @@ pub inline fn documentGetDocumentURI(doc: *Document) ![]const u8 {
|
||||
return strToData(s.?);
|
||||
}
|
||||
|
||||
pub fn documentSetDocumentURI(doc: *Document, uri: []const u8) !void {
|
||||
const err = documentVtable(doc).dom_document_set_uri.?(doc, try strFromData(uri));
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub inline fn documentGetInputEncoding(doc: *Document) ![]const u8 {
|
||||
var s: ?*String = undefined;
|
||||
const err = documentVtable(doc).dom_document_get_input_encoding.?(doc, &s);
|
||||
@@ -1660,6 +1951,28 @@ pub inline fn documentGetInputEncoding(doc: *Document) ![]const u8 {
|
||||
return strToData(s.?);
|
||||
}
|
||||
|
||||
pub inline fn documentSetInputEncoding(doc: *Document, enc: []const u8) !void {
|
||||
const err = documentVtable(doc).dom_document_set_input_encoding.?(doc, try strFromData(enc));
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub inline fn documentCreateDocument(title: ?[]const u8) !*DocumentHTML {
|
||||
var doc: ?*Document = undefined;
|
||||
const err = c.dom_implementation_create_document(
|
||||
c.DOM_IMPLEMENTATION_HTML,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
&doc,
|
||||
);
|
||||
try DOMErr(err);
|
||||
const doc_html = @as(*DocumentHTML, @ptrCast(doc.?));
|
||||
if (title) |t| try documentHTMLSetTitle(doc_html, t);
|
||||
return doc_html;
|
||||
}
|
||||
|
||||
pub inline fn documentCreateElement(doc: *Document, tag_name: []const u8) !*Element {
|
||||
var elem: ?*Element = undefined;
|
||||
const err = documentVtable(doc).dom_document_create_element.?(doc, try strFromData(tag_name), &elem);
|
||||
@@ -1821,9 +2134,40 @@ pub fn documentHTMLParse(reader: anytype, enc: ?[:0]const u8) !*DocumentHTML {
|
||||
var parser: ?*c.dom_hubbub_parser = undefined;
|
||||
var doc: ?*c.dom_document = undefined;
|
||||
var err: c.hubbub_error = undefined;
|
||||
var params = parseParams(enc);
|
||||
|
||||
var params = c.dom_hubbub_parser_params{
|
||||
.enc = null,
|
||||
err = c.dom_hubbub_parser_create(¶ms, &parser, &doc);
|
||||
try parserErr(err);
|
||||
defer c.dom_hubbub_parser_destroy(parser);
|
||||
|
||||
try parseData(parser.?, reader);
|
||||
|
||||
return @as(*DocumentHTML, @ptrCast(doc.?));
|
||||
}
|
||||
|
||||
pub fn documentParseFragmentFromStr(self: *Document, str: []const u8) !*DocumentFragment {
|
||||
var fbs = std.io.fixedBufferStream(str);
|
||||
return try documentParseFragment(self, fbs.reader(), "UTF-8");
|
||||
}
|
||||
|
||||
pub fn documentParseFragment(self: *Document, reader: anytype, enc: ?[:0]const u8) !*DocumentFragment {
|
||||
var parser: ?*c.dom_hubbub_parser = undefined;
|
||||
var fragment: ?*c.dom_document_fragment = undefined;
|
||||
var err: c.hubbub_error = undefined;
|
||||
var params = parseParams(enc);
|
||||
|
||||
err = c.dom_hubbub_fragment_parser_create(¶ms, self, &parser, &fragment);
|
||||
try parserErr(err);
|
||||
defer c.dom_hubbub_parser_destroy(parser);
|
||||
|
||||
try parseData(parser.?, reader);
|
||||
|
||||
return @as(*DocumentFragment, @ptrCast(fragment.?));
|
||||
}
|
||||
|
||||
fn parseParams(enc: ?[:0]const u8) c.dom_hubbub_parser_params {
|
||||
return .{
|
||||
.enc = enc orelse null,
|
||||
.fix_enc = true,
|
||||
.msg = null,
|
||||
.script = null,
|
||||
@@ -1831,13 +2175,10 @@ pub fn documentHTMLParse(reader: anytype, enc: ?[:0]const u8) !*DocumentHTML {
|
||||
.ctx = null,
|
||||
.daf = null,
|
||||
};
|
||||
}
|
||||
|
||||
if (enc) |e| params.enc = e;
|
||||
|
||||
err = c.dom_hubbub_parser_create(¶ms, &parser, &doc);
|
||||
try parserErr(err);
|
||||
defer c.dom_hubbub_parser_destroy(parser);
|
||||
|
||||
fn parseData(parser: *c.dom_hubbub_parser, reader: anytype) !void {
|
||||
var err: c.hubbub_error = undefined;
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var ln = buffer.len;
|
||||
while (ln > 0) {
|
||||
@@ -1855,8 +2196,6 @@ pub fn documentHTMLParse(reader: anytype, enc: ?[:0]const u8) !*DocumentHTML {
|
||||
|
||||
err = c.dom_hubbub_parser_completed(parser);
|
||||
try parserErr(err);
|
||||
|
||||
return @as(*DocumentHTML, @ptrCast(doc.?));
|
||||
}
|
||||
|
||||
// documentHTMLClose closes the document.
|
||||
@@ -1,13 +1,36 @@
|
||||
// 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 jsruntime = @import("jsruntime");
|
||||
const generate = @import("generate.zig");
|
||||
const pretty = @import("pretty");
|
||||
|
||||
const parser = @import("netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
const apiweb = @import("apiweb.zig");
|
||||
const Window = @import("html/window.zig").Window;
|
||||
const xhr = @import("xhr/xhr.zig");
|
||||
const storage = @import("storage/storage.zig");
|
||||
const url = @import("url/url.zig");
|
||||
const urlquery = @import("url/query.zig");
|
||||
const Client = @import("async/Client.zig");
|
||||
|
||||
const documentTestExecFn = @import("dom/document.zig").testExecFn;
|
||||
const HTMLDocumentTestExecFn = @import("html/document.zig").testExecFn;
|
||||
@@ -23,11 +46,19 @@ const DOMTokenListExecFn = @import("dom/token_list.zig").testExecFn;
|
||||
const NodeListTestExecFn = @import("dom/nodelist.zig").testExecFn;
|
||||
const AttrTestExecFn = @import("dom/attribute.zig").testExecFn;
|
||||
const EventTargetTestExecFn = @import("dom/event_target.zig").testExecFn;
|
||||
const ProcessingInstructionTestExecFn = @import("dom/processing_instruction.zig").testExecFn;
|
||||
const CommentTestExecFn = @import("dom/comment.zig").testExecFn;
|
||||
const DocumentFragmentTestExecFn = @import("dom/document_fragment.zig").testExecFn;
|
||||
const EventTestExecFn = @import("events/event.zig").testExecFn;
|
||||
const XHRTestExecFn = xhr.testExecFn;
|
||||
const ProgressEventTestExecFn = @import("xhr/progress_event.zig").testExecFn;
|
||||
const StorageTestExecFn = storage.testExecFn;
|
||||
const URLTestExecFn = url.testExecFn;
|
||||
const HTMLElementTestExecFn = @import("html/elements.zig").testExecFn;
|
||||
const MutationObserverTestExecFn = @import("dom/mutation_observer.zig").testExecFn;
|
||||
|
||||
pub const Types = jsruntime.reflect(apiweb.Interfaces);
|
||||
pub const UserContext = @import("user_context.zig").UserContext;
|
||||
|
||||
var doc: *parser.DocumentHTML = undefined;
|
||||
|
||||
@@ -36,14 +67,15 @@ fn testExecFn(
|
||||
js_env: *jsruntime.Env,
|
||||
comptime execFn: jsruntime.ContextExecFn,
|
||||
) anyerror!void {
|
||||
try parser.init();
|
||||
defer parser.deinit();
|
||||
|
||||
// start JS env
|
||||
try js_env.start(alloc);
|
||||
defer js_env.stop();
|
||||
|
||||
// alias global as self and window
|
||||
try js_env.attachObject(try js_env.getGlobal(), "self", null);
|
||||
try js_env.attachObject(try js_env.getGlobal(), "window", null);
|
||||
var storageShelf = storage.Shelf.init(alloc);
|
||||
defer storageShelf.deinit();
|
||||
|
||||
// document
|
||||
const file = try std.fs.cwd().openFile("test.html", .{});
|
||||
@@ -54,8 +86,21 @@ fn testExecFn(
|
||||
std.debug.print("documentHTMLClose error: {s}\n", .{@errorName(err)});
|
||||
};
|
||||
|
||||
// add document object
|
||||
try js_env.addObject(doc, "document");
|
||||
var cli = Client{ .allocator = alloc, .loop = js_env.nat_ctx.loop };
|
||||
defer cli.deinit();
|
||||
|
||||
try js_env.setUserContext(.{
|
||||
.document = doc,
|
||||
.httpClient = &cli,
|
||||
});
|
||||
|
||||
// alias global as self and window
|
||||
var window = Window.create(null);
|
||||
|
||||
window.replaceDocument(doc);
|
||||
window.setStorageShelf(&storageShelf);
|
||||
|
||||
try js_env.bindGlobal(window);
|
||||
|
||||
// run test
|
||||
try execFn(alloc, js_env);
|
||||
@@ -79,10 +124,17 @@ fn testsAllExecFn(
|
||||
DOMTokenListExecFn,
|
||||
NodeListTestExecFn,
|
||||
AttrTestExecFn,
|
||||
CommentTestExecFn,
|
||||
DocumentFragmentTestExecFn,
|
||||
EventTargetTestExecFn,
|
||||
EventTestExecFn,
|
||||
XHRTestExecFn,
|
||||
ProgressEventTestExecFn,
|
||||
ProcessingInstructionTestExecFn,
|
||||
StorageTestExecFn,
|
||||
URLTestExecFn,
|
||||
HTMLElementTestExecFn,
|
||||
MutationObserverTestExecFn,
|
||||
};
|
||||
|
||||
inline for (testFns) |testFn| {
|
||||
@@ -90,20 +142,185 @@ fn testsAllExecFn(
|
||||
}
|
||||
}
|
||||
|
||||
const usage =
|
||||
\\usage: test [options]
|
||||
\\ Run the tests. By default the command will run both js and unit tests.
|
||||
\\
|
||||
\\ -h, --help Print this help message and exit.
|
||||
\\ --browser run only browser js tests
|
||||
\\ --unit run only js unit tests
|
||||
\\ --json bench result is formatted in JSON.
|
||||
\\ only browser tests are benchmarked.
|
||||
\\
|
||||
;
|
||||
|
||||
// Out list all the ouputs handled by benchmark result and written on stdout.
|
||||
const Out = enum {
|
||||
text,
|
||||
json,
|
||||
};
|
||||
|
||||
// Which tests must be run.
|
||||
const Run = enum {
|
||||
all,
|
||||
browser,
|
||||
unit,
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
std.debug.print("\n", .{});
|
||||
for (builtin.test_functions) |test_fn| {
|
||||
try test_fn.func();
|
||||
std.debug.print("{s}\tOK\n", .{test_fn.name});
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const gpa_alloc = gpa.allocator();
|
||||
|
||||
var args = try std.process.argsWithAllocator(gpa_alloc);
|
||||
defer args.deinit();
|
||||
|
||||
// ignore the exec name.
|
||||
_ = args.next().?;
|
||||
|
||||
var out: Out = .text;
|
||||
var run: Run = .all;
|
||||
|
||||
while (args.next()) |arg| {
|
||||
if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "--help", arg)) {
|
||||
try std.io.getStdErr().writer().print(usage, .{});
|
||||
std.posix.exit(0);
|
||||
}
|
||||
if (std.mem.eql(u8, "--json", arg)) {
|
||||
out = .json;
|
||||
continue;
|
||||
}
|
||||
if (std.mem.eql(u8, "--browser", arg)) {
|
||||
run = .browser;
|
||||
continue;
|
||||
}
|
||||
if (std.mem.eql(u8, "--unit", arg)) {
|
||||
run = .unit;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// run js tests
|
||||
if (run == .all or run == .browser) try run_js(out);
|
||||
|
||||
// run standard unit tests.
|
||||
if (run == .all or run == .unit) {
|
||||
std.debug.print("\n", .{});
|
||||
for (builtin.test_functions) |test_fn| {
|
||||
try parser.init();
|
||||
defer parser.deinit();
|
||||
|
||||
try test_fn.func();
|
||||
std.debug.print("{s}\tOK\n", .{test_fn.name});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
const TestAsync = @import("async/test.zig");
|
||||
std.testing.refAllDecls(TestAsync);
|
||||
// Run js test and display the output depending of the output parameter.
|
||||
fn run_js(out: Out) !void {
|
||||
var bench_alloc = jsruntime.bench_allocator(std.testing.allocator);
|
||||
|
||||
const start = try std.time.Instant.now();
|
||||
|
||||
// run js exectuion tests
|
||||
try testJSRuntime(bench_alloc.allocator());
|
||||
|
||||
const duration = std.time.Instant.since(try std.time.Instant.now(), start);
|
||||
const stats = bench_alloc.stats();
|
||||
|
||||
// get and display the results
|
||||
if (out == .json) {
|
||||
const res = [_]struct {
|
||||
name: []const u8,
|
||||
bench: struct {
|
||||
duration: u64,
|
||||
|
||||
alloc_nb: usize,
|
||||
realloc_nb: usize,
|
||||
alloc_size: usize,
|
||||
},
|
||||
}{
|
||||
.{ .name = "browser", .bench = .{
|
||||
.duration = duration,
|
||||
.alloc_nb = stats.alloc_nb,
|
||||
.realloc_nb = stats.realloc_nb,
|
||||
.alloc_size = stats.alloc_size,
|
||||
} },
|
||||
// TODO get libdom bench info.
|
||||
.{ .name = "libdom", .bench = .{
|
||||
.duration = duration,
|
||||
.alloc_nb = 0,
|
||||
.realloc_nb = 0,
|
||||
.alloc_size = 0,
|
||||
} },
|
||||
// TODO get v8 bench info.
|
||||
.{ .name = "v8", .bench = .{
|
||||
.duration = duration,
|
||||
.alloc_nb = 0,
|
||||
.realloc_nb = 0,
|
||||
.alloc_size = 0,
|
||||
} },
|
||||
// TODO get main bench info.
|
||||
.{ .name = "main", .bench = .{
|
||||
.duration = duration,
|
||||
.alloc_nb = 0,
|
||||
.realloc_nb = 0,
|
||||
.alloc_size = 0,
|
||||
} },
|
||||
};
|
||||
|
||||
try std.json.stringify(res, .{ .whitespace = .indent_2 }, std.io.getStdOut().writer());
|
||||
return;
|
||||
}
|
||||
|
||||
// display console result by default
|
||||
const dur = pretty.Measure{ .unit = "ms", .value = duration / ms };
|
||||
const size = pretty.Measure{ .unit = "kb", .value = stats.alloc_size / kb };
|
||||
|
||||
const zerosize = pretty.Measure{ .unit = "kb", .value = 0 };
|
||||
|
||||
// benchmark table
|
||||
const row_shape = .{ []const u8, pretty.Measure, u64, u64, pretty.Measure };
|
||||
const table = try pretty.GenerateTable(4, row_shape, pretty.TableConf{ .margin_left = " " });
|
||||
const header = .{ "FUNCTION", "DURATION", "ALLOCATIONS (nb)", "RE-ALLOCATIONS (nb)", "HEAP SIZE" };
|
||||
var t = table.init("Benchmark browsercore 🚀", header);
|
||||
try t.addRow(.{ "browser", dur, stats.alloc_nb, stats.realloc_nb, size });
|
||||
try t.addRow(.{ "libdom", dur, 0, 0, zerosize }); // TODO get libdom bench info.
|
||||
try t.addRow(.{ "v8", dur, 0, 0, zerosize }); // TODO get v8 bench info.
|
||||
try t.addRow(.{ "main", dur, 0, 0, zerosize }); // TODO get main bench info.
|
||||
try t.render(std.io.getStdOut().writer());
|
||||
}
|
||||
|
||||
test "jsruntime" {
|
||||
const kb = 1024;
|
||||
const ms = std.time.ns_per_ms;
|
||||
|
||||
test {
|
||||
const asyncTest = @import("async/test.zig");
|
||||
std.testing.refAllDecls(asyncTest);
|
||||
|
||||
const dumpTest = @import("browser/dump.zig");
|
||||
std.testing.refAllDecls(dumpTest);
|
||||
|
||||
const mimeTest = @import("browser/mime.zig");
|
||||
std.testing.refAllDecls(mimeTest);
|
||||
|
||||
const cssTest = @import("css/css.zig");
|
||||
std.testing.refAllDecls(cssTest);
|
||||
|
||||
const cssParserTest = @import("css/parser.zig");
|
||||
std.testing.refAllDecls(cssParserTest);
|
||||
|
||||
const cssMatchTest = @import("css/match_test.zig");
|
||||
std.testing.refAllDecls(cssMatchTest);
|
||||
|
||||
const cssLibdomTest = @import("css/libdom_test.zig");
|
||||
std.testing.refAllDecls(cssLibdomTest);
|
||||
|
||||
const queryTest = @import("url/query.zig");
|
||||
std.testing.refAllDecls(queryTest);
|
||||
}
|
||||
|
||||
fn testJSRuntime(alloc: std.mem.Allocator) !void {
|
||||
// generate tests
|
||||
try generate.tests();
|
||||
|
||||
@@ -111,11 +328,10 @@ test "jsruntime" {
|
||||
const vm = jsruntime.VM.init();
|
||||
defer vm.deinit();
|
||||
|
||||
var bench_alloc = jsruntime.bench_allocator(std.testing.allocator);
|
||||
var arena_alloc = std.heap.ArenaAllocator.init(bench_alloc.allocator());
|
||||
var arena_alloc = std.heap.ArenaAllocator.init(alloc);
|
||||
defer arena_alloc.deinit();
|
||||
|
||||
try jsruntime.loadEnv(&arena_alloc, testsAllExecFn);
|
||||
try jsruntime.loadEnv(&arena_alloc, null, testsAllExecFn);
|
||||
}
|
||||
|
||||
test "DocumentHTMLParseFromStr" {
|
||||
@@ -138,15 +354,6 @@ test "bug document html parsing #4" {
|
||||
parser.documentHTMLClose(doc) catch {};
|
||||
}
|
||||
|
||||
const dump = @import("browser/dump.zig");
|
||||
test "run browser tests" {
|
||||
// const out = std.io.getStdOut();
|
||||
const out = try std.fs.openFileAbsolute("/dev/null", .{ .mode = .write_only });
|
||||
defer out.close();
|
||||
|
||||
try dump.HTMLFileTestFn(out);
|
||||
}
|
||||
|
||||
test "Window is a libdom event target" {
|
||||
var window = Window.create(null);
|
||||
|
||||
|
||||
250
src/storage/storage.zig
Normal file
250
src/storage/storage.zig
Normal file
@@ -0,0 +1,250 @@
|
||||
// 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 jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
const generate = @import("../generate.zig");
|
||||
|
||||
const DOMError = @import("netsurf").DOMError;
|
||||
|
||||
const log = std.log.scoped(.storage);
|
||||
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
Bottle,
|
||||
});
|
||||
|
||||
// See https://storage.spec.whatwg.org/#model for storage hierarchy.
|
||||
// A Shed contains map of Shelves. The key is the document's origin.
|
||||
// A Shelf contains on default Bucket (it could contain many in the future).
|
||||
// A Bucket contains a local and a session Bottle.
|
||||
// A Bottle stores a map of strings and is exposed to the JS.
|
||||
|
||||
pub const Shed = struct {
|
||||
const Map = std.StringHashMapUnmanaged(Shelf);
|
||||
|
||||
alloc: std.mem.Allocator,
|
||||
map: Map,
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator) Shed {
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
.map = .{},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Shed) void {
|
||||
// loop hover each KV and free the memory.
|
||||
var it = self.map.iterator();
|
||||
while (it.next()) |entry| {
|
||||
entry.value_ptr.deinit();
|
||||
self.alloc.free(entry.key_ptr.*);
|
||||
}
|
||||
self.map.deinit(self.alloc);
|
||||
}
|
||||
|
||||
pub fn getOrPut(self: *Shed, origin: []const u8) !*Shelf {
|
||||
const shelf = self.map.getPtr(origin);
|
||||
if (shelf) |s| return s;
|
||||
|
||||
const oorigin = try self.alloc.dupe(u8, origin);
|
||||
try self.map.put(self.alloc, oorigin, Shelf.init(self.alloc));
|
||||
return self.map.getPtr(origin).?;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Shelf = struct {
|
||||
bucket: Bucket,
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator) Shelf {
|
||||
return .{ .bucket = Bucket.init(alloc) };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Shelf) void {
|
||||
self.bucket.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
pub const Bucket = struct {
|
||||
local: Bottle,
|
||||
session: Bottle,
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator) Bucket {
|
||||
return .{
|
||||
.local = Bottle.init(alloc),
|
||||
.session = Bottle.init(alloc),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Bucket) void {
|
||||
self.local.deinit();
|
||||
self.session.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/webstorage.html#the-storage-interface
|
||||
pub const Bottle = struct {
|
||||
pub const mem_guarantied = true;
|
||||
const Map = std.StringHashMapUnmanaged([]const u8);
|
||||
|
||||
// allocator is stored. we don't use the JS env allocator b/c the storage
|
||||
// data could exists longer than a js env lifetime.
|
||||
alloc: std.mem.Allocator,
|
||||
map: Map,
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator) Bottle {
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
.map = .{},
|
||||
};
|
||||
}
|
||||
|
||||
// loop hover each KV and free the memory.
|
||||
fn free(self: *Bottle) void {
|
||||
var it = self.map.iterator();
|
||||
while (it.next()) |entry| {
|
||||
self.alloc.free(entry.key_ptr.*);
|
||||
self.alloc.free(entry.value_ptr.*);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Bottle) void {
|
||||
self.free();
|
||||
self.map.deinit(self.alloc);
|
||||
}
|
||||
|
||||
pub fn get_length(self: *Bottle) u32 {
|
||||
return @intCast(self.map.count());
|
||||
}
|
||||
|
||||
pub fn _key(self: *Bottle, idx: u32) ?[]const u8 {
|
||||
if (idx >= self.map.count()) return null;
|
||||
|
||||
var it = self.map.valueIterator();
|
||||
var i: u32 = 0;
|
||||
while (it.next()) |v| {
|
||||
if (i == idx) return v.*;
|
||||
i += 1;
|
||||
}
|
||||
unreachable;
|
||||
}
|
||||
|
||||
pub fn _getItem(self: *Bottle, k: []const u8) ?[]const u8 {
|
||||
return self.map.get(k);
|
||||
}
|
||||
|
||||
pub fn _setItem(self: *Bottle, k: []const u8, v: []const u8) !void {
|
||||
const old = self.map.get(k);
|
||||
if (old != null and std.mem.eql(u8, v, old.?)) return;
|
||||
|
||||
// owns k and v by copying them.
|
||||
const kk = try self.alloc.dupe(u8, k);
|
||||
errdefer self.alloc.free(kk);
|
||||
const vv = try self.alloc.dupe(u8, v);
|
||||
errdefer self.alloc.free(vv);
|
||||
|
||||
self.map.put(self.alloc, kk, vv) catch |e| {
|
||||
log.debug("set item: {any}", .{e});
|
||||
return DOMError.QuotaExceeded;
|
||||
};
|
||||
|
||||
// > Broadcast this with key, oldValue, and value.
|
||||
// https://html.spec.whatwg.org/multipage/webstorage.html#the-storageevent-interface
|
||||
//
|
||||
// > The storage event of the Window interface fires when a storage
|
||||
// > area (localStorage or sessionStorage) has been modified in the
|
||||
// > context of another document.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event
|
||||
//
|
||||
// So for now, we won't impement the feature.
|
||||
}
|
||||
|
||||
pub fn _removeItem(self: *Bottle, k: []const u8) !void {
|
||||
const old = self.map.fetchRemove(k);
|
||||
if (old == null) return;
|
||||
|
||||
// > Broadcast this with key, oldValue, and null.
|
||||
// https://html.spec.whatwg.org/multipage/webstorage.html#the-storageevent-interface
|
||||
//
|
||||
// > The storage event of the Window interface fires when a storage
|
||||
// > area (localStorage or sessionStorage) has been modified in the
|
||||
// > context of another document.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event
|
||||
//
|
||||
// So for now, we won't impement the feature.
|
||||
}
|
||||
|
||||
pub fn _clear(self: *Bottle) void {
|
||||
self.free();
|
||||
self.map.clearRetainingCapacity();
|
||||
|
||||
// > Broadcast this with null, null, and null.
|
||||
// https://html.spec.whatwg.org/multipage/webstorage.html#the-storageevent-interface
|
||||
//
|
||||
// > The storage event of the Window interface fires when a storage
|
||||
// > area (localStorage or sessionStorage) has been modified in the
|
||||
// > context of another document.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event
|
||||
//
|
||||
// So for now, we won't impement the feature.
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var storage = [_]Case{
|
||||
.{ .src = "localStorage.length", .ex = "0" },
|
||||
|
||||
.{ .src = "localStorage.setItem('foo', 'bar')", .ex = "undefined" },
|
||||
.{ .src = "localStorage.length", .ex = "1" },
|
||||
.{ .src = "localStorage.getItem('foo')", .ex = "bar" },
|
||||
.{ .src = "localStorage.removeItem('foo')", .ex = "undefined" },
|
||||
.{ .src = "localStorage.length", .ex = "0" },
|
||||
|
||||
// .{ .src = "localStorage['foo'] = 'bar'", .ex = "undefined" },
|
||||
// .{ .src = "localStorage['foo']", .ex = "bar" },
|
||||
// .{ .src = "localStorage.length", .ex = "1" },
|
||||
|
||||
.{ .src = "localStorage.clear()", .ex = "undefined" },
|
||||
.{ .src = "localStorage.length", .ex = "0" },
|
||||
};
|
||||
try checkCases(js_env, &storage);
|
||||
}
|
||||
|
||||
test "storage bottle" {
|
||||
var bottle = Bottle.init(std.testing.allocator);
|
||||
defer bottle.deinit();
|
||||
|
||||
try std.testing.expect(0 == bottle.get_length());
|
||||
try std.testing.expect(null == bottle._getItem("foo"));
|
||||
|
||||
try bottle._setItem("foo", "bar");
|
||||
try std.testing.expect(std.mem.eql(u8, "bar", bottle._getItem("foo").?));
|
||||
|
||||
try bottle._removeItem("foo");
|
||||
|
||||
try std.testing.expect(0 == bottle.get_length());
|
||||
try std.testing.expect(null == bottle._getItem("foo"));
|
||||
}
|
||||
106
src/str/parser.zig
Normal file
106
src/str/parser.zig
Normal file
@@ -0,0 +1,106 @@
|
||||
// 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/>.
|
||||
|
||||
// some utils to parser strings.
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
|
||||
pub const Reader = struct {
|
||||
s: []const u8,
|
||||
i: usize = 0,
|
||||
|
||||
pub fn until(self: *Reader, c: u8) []const u8 {
|
||||
const ln = self.s.len;
|
||||
const start = self.i;
|
||||
while (self.i < ln) {
|
||||
if (c == self.s[self.i]) return self.s[start..self.i];
|
||||
self.i += 1;
|
||||
}
|
||||
|
||||
return self.s[start..self.i];
|
||||
}
|
||||
|
||||
pub fn tail(self: *Reader) []const u8 {
|
||||
if (self.i > self.s.len) return "";
|
||||
defer self.i = self.s.len;
|
||||
return self.s[self.i..];
|
||||
}
|
||||
|
||||
pub fn skip(self: *Reader) bool {
|
||||
if (self.i >= self.s.len) return false;
|
||||
self.i += 1;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
test "Reader.skip" {
|
||||
var r = Reader{ .s = "foo" };
|
||||
try testing.expect(r.skip());
|
||||
try testing.expect(r.skip());
|
||||
try testing.expect(r.skip());
|
||||
try testing.expect(!r.skip());
|
||||
try testing.expect(!r.skip());
|
||||
}
|
||||
|
||||
test "Reader.tail" {
|
||||
var r = Reader{ .s = "foo" };
|
||||
try testing.expectEqualStrings("foo", r.tail());
|
||||
try testing.expectEqualStrings("", r.tail());
|
||||
}
|
||||
|
||||
test "Reader.until" {
|
||||
var r = Reader{ .s = "foo.bar.baz" };
|
||||
try testing.expectEqualStrings("foo", r.until('.'));
|
||||
_ = r.skip();
|
||||
try testing.expectEqualStrings("bar", r.until('.'));
|
||||
_ = r.skip();
|
||||
try testing.expectEqualStrings("baz", r.until('.'));
|
||||
|
||||
r = Reader{ .s = "foo" };
|
||||
try testing.expectEqualStrings("foo", r.until('.'));
|
||||
|
||||
r = Reader{ .s = "" };
|
||||
try testing.expectEqualStrings("", r.until('.'));
|
||||
}
|
||||
|
||||
pub fn trim(s: []const u8) []const u8 {
|
||||
const ln = s.len;
|
||||
if (ln == 0) {
|
||||
return "";
|
||||
}
|
||||
var start: usize = 0;
|
||||
while (start < ln) {
|
||||
if (!std.ascii.isWhitespace(s[start])) break;
|
||||
start += 1;
|
||||
}
|
||||
|
||||
var end: usize = ln;
|
||||
while (end > 0) {
|
||||
if (!std.ascii.isWhitespace(s[end - 1])) break;
|
||||
end -= 1;
|
||||
}
|
||||
|
||||
return s[start..end];
|
||||
}
|
||||
|
||||
test "trim" {
|
||||
try testing.expectEqualStrings("", trim(""));
|
||||
try testing.expectEqualStrings("foo", trim("foo"));
|
||||
try testing.expectEqualStrings("foo", trim(" \n\tfoo"));
|
||||
try testing.expectEqualStrings("foo", trim("foo \n\t"));
|
||||
}
|
||||
@@ -1,8 +1,27 @@
|
||||
// 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 tests = @import("run_tests.zig");
|
||||
|
||||
pub const Types = tests.Types;
|
||||
pub const UserContext = tests.UserContext;
|
||||
|
||||
pub fn main() !void {
|
||||
try tests.main();
|
||||
|
||||
283
src/url/query.zig
Normal file
283
src/url/query.zig
Normal file
@@ -0,0 +1,283 @@
|
||||
// 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 Reader = @import("../str/parser.zig").Reader;
|
||||
|
||||
// Values is a map with string key of string values.
|
||||
pub const Values = struct {
|
||||
alloc: std.mem.Allocator,
|
||||
map: std.StringArrayHashMapUnmanaged(List),
|
||||
|
||||
const List = std.ArrayListUnmanaged([]const u8);
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator) Values {
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
.map = .{},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Values) void {
|
||||
var it = self.map.iterator();
|
||||
while (it.next()) |entry| {
|
||||
for (entry.value_ptr.items) |v| self.alloc.free(v);
|
||||
entry.value_ptr.deinit(self.alloc);
|
||||
self.alloc.free(entry.key_ptr.*);
|
||||
}
|
||||
self.map.deinit(self.alloc);
|
||||
}
|
||||
|
||||
// add the key value couple to the values.
|
||||
// the key and the value are duplicated.
|
||||
pub fn append(self: *Values, k: []const u8, v: []const u8) !void {
|
||||
const vv = try self.alloc.dupe(u8, v);
|
||||
|
||||
if (self.map.getPtr(k)) |list| {
|
||||
return try list.append(self.alloc, vv);
|
||||
}
|
||||
|
||||
const kk = try self.alloc.dupe(u8, k);
|
||||
var list = List{};
|
||||
try list.append(self.alloc, vv);
|
||||
try self.map.put(self.alloc, kk, list);
|
||||
}
|
||||
|
||||
// append by taking the ownership of the key and the value
|
||||
fn appendOwned(self: *Values, k: []const u8, v: []const u8) !void {
|
||||
if (self.map.getPtr(k)) |list| {
|
||||
return try list.append(self.alloc, v);
|
||||
}
|
||||
|
||||
var list = List{};
|
||||
try list.append(self.alloc, v);
|
||||
try self.map.put(self.alloc, k, list);
|
||||
}
|
||||
|
||||
pub fn get(self: *Values, k: []const u8) [][]const u8 {
|
||||
if (self.map.get(k)) |list| {
|
||||
return list.items;
|
||||
}
|
||||
|
||||
return &[_][]const u8{};
|
||||
}
|
||||
|
||||
pub fn first(self: *Values, k: []const u8) []const u8 {
|
||||
if (self.map.getPtr(k)) |list| {
|
||||
if (list.items.len == 0) return "";
|
||||
return list.items[0];
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn delete(self: *Values, k: []const u8) void {
|
||||
if (self.map.getPtr(k)) |list| {
|
||||
list.deinit(self.alloc);
|
||||
_ = self.map.fetchSwapRemove(k);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deleteValue(self: *Values, k: []const u8, v: []const u8) void {
|
||||
const list = self.map.getPtr(k) orelse return;
|
||||
|
||||
for (list.items, 0..) |vv, i| {
|
||||
if (std.mem.eql(u8, v, vv)) {
|
||||
_ = list.swapRemove(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn count(self: *Values) usize {
|
||||
return self.map.count();
|
||||
}
|
||||
|
||||
// the caller owned the returned string.
|
||||
pub fn encode(self: *Values, writer: anytype) !void {
|
||||
var i: usize = 0;
|
||||
var it = self.map.iterator();
|
||||
while (it.next()) |entry| {
|
||||
defer i += 1;
|
||||
if (i > 0) try writer.writeByte('&');
|
||||
|
||||
if (entry.value_ptr.items.len == 0) {
|
||||
try escape(writer, entry.key_ptr.*);
|
||||
continue;
|
||||
}
|
||||
|
||||
const start = i;
|
||||
for (entry.value_ptr.items) |v| {
|
||||
defer i += 1;
|
||||
if (start < i) try writer.writeByte('&');
|
||||
|
||||
try escape(writer, entry.key_ptr.*);
|
||||
if (v.len > 0) try writer.writeByte('=');
|
||||
try escape(writer, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn unhex(c: u8) u8 {
|
||||
if ('0' <= c and c <= '9') return c - '0';
|
||||
if ('a' <= c and c <= 'f') return c - 'a' + 10;
|
||||
if ('A' <= c and c <= 'F') return c - 'A' + 10;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// unescape decodes a percent encoded string.
|
||||
// The caller owned the returned string.
|
||||
pub fn unescape(alloc: std.mem.Allocator, s: []const u8) ![]const u8 {
|
||||
var buf: std.ArrayListUnmanaged(u8) = .{};
|
||||
defer buf.deinit(alloc);
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < s.len) {
|
||||
defer i += 1;
|
||||
|
||||
switch (s[i]) {
|
||||
'%' => {
|
||||
if (i + 2 > s.len) return error.EscapeError;
|
||||
if (!std.ascii.isHex(s[i + 1])) return error.EscapeError;
|
||||
if (!std.ascii.isHex(s[i + 2])) return error.EscapeError;
|
||||
|
||||
try buf.append(alloc, unhex(s[i + 1]) << 4 | unhex(s[i + 2]));
|
||||
i += 2;
|
||||
},
|
||||
'+' => try buf.append(alloc, ' '), // TODO should we decode or keep as it?
|
||||
else => try buf.append(alloc, s[i]),
|
||||
}
|
||||
}
|
||||
|
||||
return try buf.toOwnedSlice(alloc);
|
||||
}
|
||||
|
||||
test "unescape" {
|
||||
var v: []const u8 = undefined;
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
v = try unescape(alloc, "%7E");
|
||||
try std.testing.expect(std.mem.eql(u8, "~", v));
|
||||
alloc.free(v);
|
||||
}
|
||||
|
||||
pub fn escape(writer: anytype, raw: []const u8) !void {
|
||||
var start: usize = 0;
|
||||
for (raw, 0..) |char, index| {
|
||||
if ('a' <= char and char <= 'z' or 'A' <= char and char <= 'Z' or '0' <= char and char <= '9') {
|
||||
continue;
|
||||
}
|
||||
|
||||
try writer.print("{s}%{X:0>2}", .{ raw[start..index], char });
|
||||
start = index + 1;
|
||||
}
|
||||
try writer.writeAll(raw[start..]);
|
||||
}
|
||||
|
||||
// Parse the given query.
|
||||
pub fn parseQuery(alloc: std.mem.Allocator, s: []const u8) !Values {
|
||||
var values = Values.init(alloc);
|
||||
errdefer values.deinit();
|
||||
|
||||
const ln = s.len;
|
||||
if (ln == 0) return values;
|
||||
|
||||
var r = Reader{ .s = s };
|
||||
while (true) {
|
||||
const param = r.until('&');
|
||||
if (param.len == 0) break;
|
||||
|
||||
var rr = Reader{ .s = param };
|
||||
const k = rr.until('=');
|
||||
if (k.len == 0) continue;
|
||||
|
||||
_ = rr.skip();
|
||||
const v = rr.tail();
|
||||
|
||||
// decode k and v
|
||||
const kk = try unescape(alloc, k);
|
||||
const vv = try unescape(alloc, v);
|
||||
|
||||
try values.appendOwned(kk, vv);
|
||||
|
||||
if (!r.skip()) break;
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
test "parse empty query" {
|
||||
var values = try parseQuery(std.testing.allocator, "");
|
||||
defer values.deinit();
|
||||
|
||||
try std.testing.expect(values.count() == 0);
|
||||
}
|
||||
|
||||
test "parse empty query &" {
|
||||
var values = try parseQuery(std.testing.allocator, "&");
|
||||
defer values.deinit();
|
||||
|
||||
try std.testing.expect(values.count() == 0);
|
||||
}
|
||||
|
||||
test "parse query" {
|
||||
var values = try parseQuery(std.testing.allocator, "a=b&b=c");
|
||||
defer values.deinit();
|
||||
|
||||
try std.testing.expect(values.count() == 2);
|
||||
try std.testing.expect(values.get("a").len == 1);
|
||||
try std.testing.expect(std.mem.eql(u8, values.get("a")[0], "b"));
|
||||
try std.testing.expect(std.mem.eql(u8, values.first("a"), "b"));
|
||||
|
||||
try std.testing.expect(values.get("b").len == 1);
|
||||
try std.testing.expect(std.mem.eql(u8, values.get("b")[0], "c"));
|
||||
try std.testing.expect(std.mem.eql(u8, values.first("b"), "c"));
|
||||
}
|
||||
|
||||
test "parse query no value" {
|
||||
var values = try parseQuery(std.testing.allocator, "a");
|
||||
defer values.deinit();
|
||||
|
||||
try std.testing.expect(values.count() == 1);
|
||||
try std.testing.expect(std.mem.eql(u8, values.first("a"), ""));
|
||||
}
|
||||
|
||||
test "parse query dup" {
|
||||
var values = try parseQuery(std.testing.allocator, "a=b&a=c");
|
||||
defer values.deinit();
|
||||
|
||||
try std.testing.expect(values.count() == 1);
|
||||
try std.testing.expect(std.mem.eql(u8, values.first("a"), "b"));
|
||||
try std.testing.expect(values.get("a").len == 2);
|
||||
}
|
||||
|
||||
test "encode query" {
|
||||
var values = try parseQuery(std.testing.allocator, "a=b&b=c");
|
||||
defer values.deinit();
|
||||
|
||||
try values.append("a", "~");
|
||||
|
||||
var buf: std.ArrayListUnmanaged(u8) = .{};
|
||||
defer buf.deinit(std.testing.allocator);
|
||||
|
||||
try values.encode(buf.writer(std.testing.allocator));
|
||||
|
||||
try std.testing.expect(std.mem.eql(u8, buf.items, "a=b&a=%7E&b=c"));
|
||||
}
|
||||
318
src/url/url.zig
Normal file
318
src/url/url.zig
Normal file
@@ -0,0 +1,318 @@
|
||||
// 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 jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
const generate = @import("../generate.zig");
|
||||
|
||||
const query = @import("query.zig");
|
||||
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
URL,
|
||||
URLSearchParams,
|
||||
});
|
||||
|
||||
// https://url.spec.whatwg.org/#url
|
||||
//
|
||||
// TODO we could avoid many of these getter string allocation in two differents
|
||||
// way:
|
||||
//
|
||||
// 1. We can eventually get the slice of scheme *with* the following char in
|
||||
// the underlying string. But I don't know if it's possible and how to do that.
|
||||
// I mean, if the rawuri contains `https://foo.bar`, uri.scheme is a slice
|
||||
// containing only `https`. I want `https:` so, in theory, I don't need to
|
||||
// allocate data, I should be able to retrieve the scheme + the following `:`
|
||||
// from rawuri.
|
||||
//
|
||||
// 2. The other way would bu to copy the `std.Uri` code to ahve a dedicated
|
||||
// parser including the characters we want for the web API.
|
||||
pub const URL = struct {
|
||||
rawuri: []const u8,
|
||||
uri: std.Uri,
|
||||
search_params: URLSearchParams,
|
||||
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
pub fn constructor(alloc: std.mem.Allocator, url: []const u8, base: ?[]const u8) !URL {
|
||||
const raw = try std.mem.concat(alloc, u8, &[_][]const u8{ url, base orelse "" });
|
||||
errdefer alloc.free(raw);
|
||||
|
||||
const uri = std.Uri.parse(raw) catch {
|
||||
return error.TypeError;
|
||||
};
|
||||
|
||||
return .{
|
||||
.rawuri = raw,
|
||||
.uri = uri,
|
||||
.search_params = try URLSearchParams.constructor(
|
||||
alloc,
|
||||
uriComponentNullStr(uri.query),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *URL, alloc: std.mem.Allocator) void {
|
||||
self.search_params.deinit(alloc);
|
||||
alloc.free(self.rawuri);
|
||||
}
|
||||
|
||||
// the caller must free the returned string.
|
||||
// TODO return a disposable string
|
||||
// https://github.com/lightpanda-io/jsruntime-lib/issues/195
|
||||
pub fn get_origin(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
try self.uri.writeToStream(.{
|
||||
.scheme = true,
|
||||
.authentication = false,
|
||||
.authority = true,
|
||||
.path = false,
|
||||
.query = false,
|
||||
.fragment = false,
|
||||
}, buf.writer());
|
||||
return try buf.toOwnedSlice();
|
||||
}
|
||||
|
||||
// get_href returns the URL by writing all its components.
|
||||
// The query is replaced by a dump of search params.
|
||||
//
|
||||
// the caller must free the returned string.
|
||||
// TODO return a disposable string
|
||||
// https://github.com/lightpanda-io/jsruntime-lib/issues/195
|
||||
pub fn get_href(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
|
||||
// retrieve the query search from search_params.
|
||||
const cur = self.uri.query;
|
||||
defer self.uri.query = cur;
|
||||
var q = std.ArrayList(u8).init(alloc);
|
||||
defer q.deinit();
|
||||
try self.search_params.values.encode(q.writer());
|
||||
self.uri.query = .{ .percent_encoded = q.items };
|
||||
|
||||
return try self.format(alloc);
|
||||
}
|
||||
|
||||
// format the url with all its components.
|
||||
pub fn format(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
try self.uri.writeToStream(.{
|
||||
.scheme = true,
|
||||
.authentication = true,
|
||||
.authority = true,
|
||||
.path = uriComponentNullStr(self.uri.path).len > 0,
|
||||
.query = uriComponentNullStr(self.uri.query).len > 0,
|
||||
.fragment = uriComponentNullStr(self.uri.fragment).len > 0,
|
||||
}, buf.writer());
|
||||
return try buf.toOwnedSlice();
|
||||
}
|
||||
|
||||
// the caller must free the returned string.
|
||||
// TODO return a disposable string
|
||||
// https://github.com/lightpanda-io/jsruntime-lib/issues/195
|
||||
pub fn get_protocol(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
|
||||
return try std.mem.concat(alloc, u8, &[_][]const u8{ self.uri.scheme, ":" });
|
||||
}
|
||||
|
||||
pub fn get_username(self: *URL) []const u8 {
|
||||
return uriComponentNullStr(self.uri.user);
|
||||
}
|
||||
|
||||
pub fn get_password(self: *URL) []const u8 {
|
||||
return uriComponentNullStr(self.uri.password);
|
||||
}
|
||||
|
||||
// the caller must free the returned string.
|
||||
// TODO return a disposable string
|
||||
// https://github.com/lightpanda-io/jsruntime-lib/issues/195
|
||||
pub fn get_host(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
try self.uri.writeToStream(.{
|
||||
.scheme = false,
|
||||
.authentication = false,
|
||||
.authority = true,
|
||||
.path = false,
|
||||
.query = false,
|
||||
.fragment = false,
|
||||
}, buf.writer());
|
||||
return try buf.toOwnedSlice();
|
||||
}
|
||||
|
||||
pub fn get_hostname(self: *URL) []const u8 {
|
||||
return uriComponentNullStr(self.uri.host);
|
||||
}
|
||||
|
||||
// the caller must free the returned string.
|
||||
// TODO return a disposable string
|
||||
// https://github.com/lightpanda-io/jsruntime-lib/issues/195
|
||||
pub fn get_port(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
|
||||
if (self.uri.port == null) return try alloc.dupe(u8, "");
|
||||
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
|
||||
try std.fmt.formatInt(self.uri.port.?, 10, .lower, .{}, buf.writer());
|
||||
return try buf.toOwnedSlice();
|
||||
}
|
||||
|
||||
pub fn get_pathname(self: *URL) []const u8 {
|
||||
if (uriComponentStr(self.uri.path).len == 0) return "/";
|
||||
return uriComponentStr(self.uri.path);
|
||||
}
|
||||
|
||||
// the caller must free the returned string.
|
||||
// TODO return a disposable string
|
||||
// https://github.com/lightpanda-io/jsruntime-lib/issues/195
|
||||
pub fn get_search(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
|
||||
if (self.search_params.get_size() == 0) return try alloc.dupe(u8, "");
|
||||
|
||||
var buf: std.ArrayListUnmanaged(u8) = .{};
|
||||
defer buf.deinit(alloc);
|
||||
|
||||
try buf.append(alloc, '?');
|
||||
try self.search_params.values.encode(buf.writer(alloc));
|
||||
return buf.toOwnedSlice(alloc);
|
||||
}
|
||||
|
||||
// the caller must free the returned string.
|
||||
// TODO return a disposable string
|
||||
// https://github.com/lightpanda-io/jsruntime-lib/issues/195
|
||||
pub fn get_hash(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
|
||||
if (self.uri.fragment == null) return try alloc.dupe(u8, "");
|
||||
|
||||
return try std.mem.concat(alloc, u8, &[_][]const u8{ "#", uriComponentNullStr(self.uri.fragment) });
|
||||
}
|
||||
|
||||
pub fn get_searchParams(self: *URL) *URLSearchParams {
|
||||
return &self.search_params;
|
||||
}
|
||||
|
||||
pub fn _toJSON(self: *URL, alloc: std.mem.Allocator) ![]const u8 {
|
||||
return try self.get_href(alloc);
|
||||
}
|
||||
};
|
||||
|
||||
// uriComponentNullStr converts an optional std.Uri.Component to string value.
|
||||
// The string value can be undecoded.
|
||||
fn uriComponentNullStr(c: ?std.Uri.Component) []const u8 {
|
||||
if (c == null) return "";
|
||||
|
||||
return uriComponentStr(c.?);
|
||||
}
|
||||
|
||||
fn uriComponentStr(c: std.Uri.Component) []const u8 {
|
||||
return switch (c) {
|
||||
.raw => |v| v,
|
||||
.percent_encoded => |v| v,
|
||||
};
|
||||
}
|
||||
|
||||
// https://url.spec.whatwg.org/#interface-urlsearchparams
|
||||
// TODO array like
|
||||
pub const URLSearchParams = struct {
|
||||
values: query.Values,
|
||||
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
pub fn constructor(alloc: std.mem.Allocator, init: ?[]const u8) !URLSearchParams {
|
||||
return .{
|
||||
.values = try query.parseQuery(alloc, init orelse ""),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *URLSearchParams, _: std.mem.Allocator) void {
|
||||
self.values.deinit();
|
||||
}
|
||||
|
||||
pub fn get_size(self: *URLSearchParams) u32 {
|
||||
return @intCast(self.values.count());
|
||||
}
|
||||
|
||||
pub fn _append(self: *URLSearchParams, name: []const u8, value: []const u8) !void {
|
||||
try self.values.append(name, value);
|
||||
}
|
||||
|
||||
pub fn _delete(self: *URLSearchParams, name: []const u8, value: ?[]const u8) !void {
|
||||
if (value) |v| return self.values.deleteValue(name, v);
|
||||
|
||||
self.values.delete(name);
|
||||
}
|
||||
|
||||
pub fn _get(self: *URLSearchParams, name: []const u8) ?[]const u8 {
|
||||
return self.values.first(name);
|
||||
}
|
||||
|
||||
// TODO return generates an error: caught unexpected error 'TypeLookup'
|
||||
// pub fn _getAll(self: *URLSearchParams, name: []const u8) [][]const u8 {
|
||||
// try self.values.get(name);
|
||||
// }
|
||||
|
||||
// TODO
|
||||
pub fn _sort(_: *URLSearchParams) void {}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var url = [_]Case{
|
||||
.{ .src = "var url = new URL('https://foo.bar/path?query#fragment')", .ex = "undefined" },
|
||||
.{ .src = "url.origin", .ex = "https://foo.bar" },
|
||||
.{ .src = "url.href", .ex = "https://foo.bar/path?query#fragment" },
|
||||
.{ .src = "url.protocol", .ex = "https:" },
|
||||
.{ .src = "url.username", .ex = "" },
|
||||
.{ .src = "url.password", .ex = "" },
|
||||
.{ .src = "url.host", .ex = "foo.bar" },
|
||||
.{ .src = "url.hostname", .ex = "foo.bar" },
|
||||
.{ .src = "url.port", .ex = "" },
|
||||
.{ .src = "url.pathname", .ex = "/path" },
|
||||
.{ .src = "url.search", .ex = "?query" },
|
||||
.{ .src = "url.hash", .ex = "#fragment" },
|
||||
.{ .src = "url.searchParams.get('query')", .ex = "" },
|
||||
};
|
||||
try checkCases(js_env, &url);
|
||||
|
||||
var qs = [_]Case{
|
||||
.{ .src = "var url = new URL('https://foo.bar/path?a=~&b=%7E#fragment')", .ex = "undefined" },
|
||||
.{ .src = "url.searchParams.get('a')", .ex = "~" },
|
||||
.{ .src = "url.searchParams.get('b')", .ex = "~" },
|
||||
.{ .src = "url.searchParams.append('c', 'foo')", .ex = "undefined" },
|
||||
.{ .src = "url.searchParams.get('c')", .ex = "foo" },
|
||||
.{ .src = "url.searchParams.size", .ex = "3" },
|
||||
|
||||
// search is dynamic
|
||||
.{ .src = "url.search", .ex = "?a=%7E&b=%7E&c=foo" },
|
||||
// href is dynamic
|
||||
.{ .src = "url.href", .ex = "https://foo.bar/path?a=%7E&b=%7E&c=foo#fragment" },
|
||||
|
||||
.{ .src = "url.searchParams.delete('c', 'foo')", .ex = "undefined" },
|
||||
.{ .src = "url.searchParams.get('c')", .ex = "" },
|
||||
.{ .src = "url.searchParams.delete('a')", .ex = "undefined" },
|
||||
.{ .src = "url.searchParams.get('a')", .ex = "" },
|
||||
};
|
||||
try checkCases(js_env, &qs);
|
||||
}
|
||||
8
src/user_context.zig
Normal file
8
src/user_context.zig
Normal file
@@ -0,0 +1,8 @@
|
||||
const std = @import("std");
|
||||
const parser = @import("netsurf");
|
||||
const Client = @import("async/Client.zig");
|
||||
|
||||
pub const UserContext = struct {
|
||||
document: *parser.DocumentHTML,
|
||||
httpClient: *Client,
|
||||
};
|
||||
@@ -1,3 +1,21 @@
|
||||
// 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 fspath = std.fs.path;
|
||||
|
||||
|
||||
@@ -1,37 +1,70 @@
|
||||
// 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 fspath = std.fs.path;
|
||||
|
||||
const FileLoader = @import("fileloader.zig").FileLoader;
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Loop = jsruntime.Loop;
|
||||
const Env = jsruntime.Env;
|
||||
const Window = @import("../html/window.zig").Window;
|
||||
const storage = @import("../storage/storage.zig");
|
||||
|
||||
const Types = @import("../main_wpt.zig").Types;
|
||||
const UserContext = @import("../main_wpt.zig").UserContext;
|
||||
const Client = @import("../async/Client.zig");
|
||||
|
||||
// runWPT parses the given HTML file, starts a js env and run the first script
|
||||
// tags containing javascript sources.
|
||||
// It loads first the js libs files.
|
||||
pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const u8, loader: *FileLoader) !jsruntime.JSResult {
|
||||
const alloc = arena.allocator();
|
||||
try parser.init();
|
||||
defer parser.deinit();
|
||||
|
||||
// document
|
||||
const file = try std.fs.cwd().openFile(f, .{});
|
||||
defer file.close();
|
||||
|
||||
const html_doc = try parser.documentHTMLParse(file.reader(), "UTF-8");
|
||||
const doc = parser.documentHTMLToDocument(html_doc);
|
||||
|
||||
const dirname = fspath.dirname(f[dir.len..]) orelse unreachable;
|
||||
|
||||
// create JS env
|
||||
var loop = try Loop.init(alloc);
|
||||
defer loop.deinit();
|
||||
var js_env = try Env.init(alloc, &loop);
|
||||
|
||||
var cli = Client{ .allocator = alloc, .loop = &loop };
|
||||
defer cli.deinit();
|
||||
|
||||
var js_env = try Env.init(alloc, &loop, UserContext{
|
||||
.document = html_doc,
|
||||
.httpClient = &cli,
|
||||
});
|
||||
defer js_env.deinit();
|
||||
|
||||
var storageShelf = storage.Shelf.init(alloc);
|
||||
defer storageShelf.deinit();
|
||||
|
||||
// load user-defined types in JS env
|
||||
var js_types: [Types.len]usize = undefined;
|
||||
try js_env.load(&js_types);
|
||||
@@ -50,42 +83,31 @@ pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const
|
||||
}
|
||||
|
||||
// setup global env vars.
|
||||
try js_env.attachObject(try js_env.getGlobal(), "self", null);
|
||||
try js_env.attachObject(try js_env.getGlobal(), "window", null);
|
||||
try js_env.addObject(html_doc, "document");
|
||||
var window = Window.create(null);
|
||||
window.replaceDocument(html_doc);
|
||||
window.setStorageShelf(&storageShelf);
|
||||
try js_env.bindGlobal(&window);
|
||||
|
||||
// thanks to the arena, we don't need to deinit res.
|
||||
var res: jsruntime.JSResult = undefined;
|
||||
|
||||
const init =
|
||||
\\window.listeners = [];
|
||||
\\window.document = document;
|
||||
\\window.parent = window;
|
||||
\\window.addEventListener = function (type, listener, options) {
|
||||
\\ window.listeners.push({type: type, listener: listener, options: options});
|
||||
\\};
|
||||
\\window.dispatchEvent = function (event) {
|
||||
\\ len = window.listeners.length;
|
||||
\\ for (var i = 0; i < len; i++) {
|
||||
\\ if (window.listeners[i].type == event.target) {
|
||||
\\ window.listeners[i].listener(event);
|
||||
\\ }
|
||||
\\ }
|
||||
\\ return true;
|
||||
\\};
|
||||
\\window.removeEventListener = function () {};
|
||||
\\
|
||||
\\console = [];
|
||||
\\console.log = function () {
|
||||
\\ console.push(...arguments);
|
||||
\\};
|
||||
\\console.debug = function () {
|
||||
\\ console.push("debug", ...arguments);
|
||||
\\};
|
||||
;
|
||||
res = try evalJS(js_env, alloc, init, "init");
|
||||
if (!res.success) {
|
||||
return res;
|
||||
}
|
||||
res.deinit(alloc);
|
||||
|
||||
// loop hover the scripts.
|
||||
const doc = parser.documentHTMLToDocument(html_doc);
|
||||
const scripts = try parser.documentGetElementsByTagName(doc, "script");
|
||||
const slen = try parser.nodeListLength(scripts);
|
||||
for (0..slen) |i| {
|
||||
@@ -103,6 +125,7 @@ pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const
|
||||
if (!res.success) {
|
||||
return res;
|
||||
}
|
||||
res.deinit(alloc);
|
||||
}
|
||||
|
||||
// If the script as a source text, execute it.
|
||||
@@ -113,28 +136,39 @@ pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const
|
||||
if (!res.success) {
|
||||
return res;
|
||||
}
|
||||
res.deinit(alloc);
|
||||
}
|
||||
|
||||
// Mark tests as ready to run.
|
||||
res = try evalJS(js_env, alloc, "window.dispatchEvent({target: 'load'});", "ready");
|
||||
const loadevt = try parser.eventCreate();
|
||||
defer parser.eventDestroy(loadevt);
|
||||
|
||||
try parser.eventInit(loadevt, "load", .{});
|
||||
_ = try parser.eventTargetDispatchEvent(
|
||||
parser.toEventTarget(Window, &window),
|
||||
loadevt,
|
||||
);
|
||||
|
||||
// wait for all async executions
|
||||
res = try js_env.waitTryCatch(alloc);
|
||||
if (!res.success) {
|
||||
return res;
|
||||
}
|
||||
res.deinit(alloc);
|
||||
|
||||
// Check the final test status.
|
||||
res = try evalJS(js_env, alloc, "report.status;", "teststatus");
|
||||
if (!res.success) {
|
||||
return res;
|
||||
}
|
||||
res.deinit(alloc);
|
||||
|
||||
// return the detailed result.
|
||||
return try evalJS(js_env, alloc, "report.log", "teststatus");
|
||||
}
|
||||
|
||||
fn evalJS(env: jsruntime.Env, alloc: std.mem.Allocator, script: []const u8, name: ?[]const u8) !jsruntime.JSResult {
|
||||
var res = jsruntime.JSResult{};
|
||||
try env.run(alloc, script, name, &res, null);
|
||||
return res;
|
||||
return try env.execTryCatch(alloc, script, name);
|
||||
}
|
||||
|
||||
// browse the path to find the tests list.
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
// 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;
|
||||
|
||||
@@ -119,6 +137,16 @@ pub const Suite = struct {
|
||||
try cases.append(case);
|
||||
}
|
||||
|
||||
if (cases.items.len == 0) {
|
||||
// no test case, create a failed one.
|
||||
suite.pass = false;
|
||||
try cases.append(.{
|
||||
.pass = false,
|
||||
.name = "no test case",
|
||||
.message = "no test case",
|
||||
});
|
||||
}
|
||||
|
||||
suite.cases = try cases.toOwnedSlice();
|
||||
|
||||
return suite;
|
||||
|
||||
@@ -1,11 +1,30 @@
|
||||
// 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 jsruntime = @import("jsruntime");
|
||||
const Callback = jsruntime.Callback;
|
||||
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const EventHandler = @import("../events/event.zig").EventHandler;
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const log = std.log.scoped(.xhr);
|
||||
|
||||
@@ -23,8 +42,20 @@ pub const XMLHttpRequestEventTarget = struct {
|
||||
ontimeout_cbk: ?Callback = null,
|
||||
onloadend_cbk: ?Callback = null,
|
||||
|
||||
fn register(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, typ: []const u8, cbk: Callback) !void {
|
||||
try parser.eventTargetAddEventListener(@as(*parser.EventTarget, @ptrCast(self)), alloc, typ, cbk, false);
|
||||
fn register(
|
||||
self: *XMLHttpRequestEventTarget,
|
||||
alloc: std.mem.Allocator,
|
||||
typ: []const u8,
|
||||
cbk: Callback,
|
||||
) !void {
|
||||
try parser.eventTargetAddEventListener(
|
||||
@as(*parser.EventTarget, @ptrCast(self)),
|
||||
alloc,
|
||||
typ,
|
||||
EventHandler,
|
||||
.{ .cbk = cbk },
|
||||
false,
|
||||
);
|
||||
}
|
||||
fn unregister(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, typ: []const u8, cbk: Callback) !void {
|
||||
const et = @as(*parser.EventTarget, @ptrCast(self));
|
||||
|
||||
@@ -1,10 +1,28 @@
|
||||
// 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 jsruntime = @import("jsruntime");
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
const Event = @import("../events/event.zig").Event;
|
||||
|
||||
const DOMException = @import("../dom/exceptions.zig").DOMException;
|
||||
|
||||
289
src/xhr/xhr.zig
289
src/xhr/xhr.zig
@@ -1,3 +1,21 @@
|
||||
// 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 jsruntime = @import("jsruntime");
|
||||
@@ -5,7 +23,7 @@ const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
const generate = @import("../generate.zig");
|
||||
|
||||
const DOMError = @import("../netsurf.zig").DOMError;
|
||||
const DOMError = @import("netsurf").DOMError;
|
||||
const DOMException = @import("../dom/exceptions.zig").DOMException;
|
||||
|
||||
const ProgressEvent = @import("progress_event.zig").ProgressEvent;
|
||||
@@ -17,7 +35,9 @@ const Loop = jsruntime.Loop;
|
||||
const YieldImpl = Loop.Yield(XMLHttpRequest);
|
||||
const Client = @import("../async/Client.zig");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const UserContext = @import("../user_context.zig").UserContext;
|
||||
|
||||
const log = std.log.scoped(.xhr);
|
||||
|
||||
@@ -75,6 +95,53 @@ pub const XMLHttpRequestBodyInit = union(XMLHttpRequestBodyInitTag) {
|
||||
};
|
||||
|
||||
pub const XMLHttpRequest = struct {
|
||||
proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{},
|
||||
alloc: std.mem.Allocator,
|
||||
cli: *Client,
|
||||
impl: YieldImpl,
|
||||
|
||||
priv_state: PrivState = .new,
|
||||
req: ?Client.Request = null,
|
||||
|
||||
method: std.http.Method,
|
||||
state: u16,
|
||||
url: ?[]const u8,
|
||||
uri: std.Uri,
|
||||
// request headers
|
||||
headers: Headers,
|
||||
sync: bool = true,
|
||||
err: ?anyerror = null,
|
||||
|
||||
// TODO uncomment this field causes casting issue with
|
||||
// XMLHttpRequestEventTarget. I think it's dueto an alignement issue, but
|
||||
// not sure. see
|
||||
// https://lightpanda.slack.com/archives/C05TRU6RBM1/p1707819010681019
|
||||
// upload: ?XMLHttpRequestUpload = null,
|
||||
|
||||
// TODO uncomment this field causes casting issue with
|
||||
// XMLHttpRequestEventTarget. I think it's dueto an alignement issue, but
|
||||
// not sure. see
|
||||
// https://lightpanda.slack.com/archives/C05TRU6RBM1/p1707819010681019
|
||||
// timeout: u32 = 0,
|
||||
|
||||
withCredentials: bool = false,
|
||||
// TODO: response readonly attribute any response;
|
||||
response_bytes: ?[]const u8 = null,
|
||||
response_type: ResponseType = .Empty,
|
||||
response_headers: Headers,
|
||||
|
||||
// used by zig client to parse response headers.
|
||||
// use 16KB for headers buffer size.
|
||||
response_header_buffer: [1024 * 16]u8 = undefined,
|
||||
|
||||
response_status: u10 = 0,
|
||||
response_override_mime_type: ?[]const u8 = null,
|
||||
response_mime: Mime = undefined,
|
||||
response_obj: ?ResponseObj = null,
|
||||
send_flag: bool = false,
|
||||
|
||||
payload: ?[]const u8 = null,
|
||||
|
||||
pub const prototype = *XMLHttpRequestEventTarget;
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
@@ -94,10 +161,92 @@ pub const XMLHttpRequest = struct {
|
||||
JSON,
|
||||
};
|
||||
|
||||
// TODO use std.json.Value instead, but it causes comptime error.
|
||||
// blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/204
|
||||
// const JSONValue = std.json.Value;
|
||||
const JSONValue = u8;
|
||||
const JSONValue = std.json.Value;
|
||||
|
||||
const Headers = struct {
|
||||
alloc: std.mem.Allocator,
|
||||
list: List,
|
||||
|
||||
const List = std.ArrayListUnmanaged(std.http.Header);
|
||||
|
||||
fn init(alloc: std.mem.Allocator) Headers {
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
.list = List{},
|
||||
};
|
||||
}
|
||||
|
||||
fn deinit(self: *Headers) void {
|
||||
self.free();
|
||||
self.list.deinit(self.alloc);
|
||||
}
|
||||
|
||||
fn append(self: *Headers, k: []const u8, v: []const u8) !void {
|
||||
// duplicate strings
|
||||
const kk = try self.alloc.dupe(u8, k);
|
||||
const vv = try self.alloc.dupe(u8, v);
|
||||
try self.list.append(self.alloc, .{ .name = kk, .value = vv });
|
||||
}
|
||||
|
||||
// free all strings allocated.
|
||||
fn free(self: *Headers) void {
|
||||
for (self.list.items) |h| {
|
||||
self.alloc.free(h.name);
|
||||
self.alloc.free(h.value);
|
||||
}
|
||||
}
|
||||
|
||||
fn clearAndFree(self: *Headers) void {
|
||||
self.free();
|
||||
self.list.clearAndFree(self.alloc);
|
||||
}
|
||||
|
||||
fn has(self: Headers, k: []const u8) bool {
|
||||
for (self.list.items) |h| {
|
||||
if (std.ascii.eqlIgnoreCase(k, h.name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
fn getFirstValue(self: Headers, k: []const u8) ?[]const u8 {
|
||||
for (self.list.items) |h| {
|
||||
if (std.ascii.eqlIgnoreCase(k, h.name)) {
|
||||
return h.value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// replace any existing header with the same key
|
||||
fn set(self: *Headers, k: []const u8, v: []const u8) !void {
|
||||
for (self.list.items, 0..) |h, i| {
|
||||
if (std.ascii.eqlIgnoreCase(k, h.name)) {
|
||||
const hh = self.list.swapRemove(i);
|
||||
self.alloc.free(hh.name);
|
||||
self.alloc.free(hh.value);
|
||||
}
|
||||
}
|
||||
self.append(k, v);
|
||||
}
|
||||
|
||||
// TODO
|
||||
fn sort(_: *Headers) void {}
|
||||
|
||||
fn all(self: Headers) []std.http.Header {
|
||||
return self.list.items;
|
||||
}
|
||||
|
||||
fn load(self: *Headers, it: *std.http.HeaderIterator) !void {
|
||||
while (true) {
|
||||
const h = it.next() orelse break;
|
||||
_ = try self.append(h.name, h.value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const Response = union(ResponseType) {
|
||||
Empty: void,
|
||||
@@ -132,54 +281,19 @@ pub const XMLHttpRequest = struct {
|
||||
|
||||
const PrivState = enum { new, open, send, write, finish, wait, done };
|
||||
|
||||
proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{},
|
||||
alloc: std.mem.Allocator,
|
||||
cli: Client,
|
||||
impl: YieldImpl,
|
||||
const min_delay: u64 = 50000000; // 50ms
|
||||
|
||||
priv_state: PrivState = .new,
|
||||
req: ?Client.Request = null,
|
||||
|
||||
method: std.http.Method,
|
||||
state: u16,
|
||||
url: ?[]const u8,
|
||||
uri: std.Uri,
|
||||
headers: std.http.Headers,
|
||||
sync: bool = true,
|
||||
err: ?anyerror = null,
|
||||
|
||||
// TODO uncomment this field causes casting issue with
|
||||
// XMLHttpRequestEventTarget. I think it's dueto an alignement issue, but
|
||||
// not sure. see
|
||||
// https://lightpanda.slack.com/archives/C05TRU6RBM1/p1707819010681019
|
||||
// upload: ?XMLHttpRequestUpload = null,
|
||||
|
||||
timeout: u32 = 0,
|
||||
withCredentials: bool = false,
|
||||
// TODO: response readonly attribute any response;
|
||||
response_bytes: ?[]const u8 = null,
|
||||
response_type: ResponseType = .Empty,
|
||||
response_headers: std.http.Headers,
|
||||
response_status: u10 = 0,
|
||||
response_override_mime_type: ?[]const u8 = null,
|
||||
response_mime: Mime = undefined,
|
||||
response_obj: ?ResponseObj = null,
|
||||
send_flag: bool = false,
|
||||
|
||||
payload: ?[]const u8 = null,
|
||||
|
||||
pub fn constructor(alloc: std.mem.Allocator, loop: *Loop) !XMLHttpRequest {
|
||||
pub fn constructor(alloc: std.mem.Allocator, loop: *Loop, userctx: UserContext) !XMLHttpRequest {
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
.headers = .{ .allocator = alloc, .owned = true },
|
||||
.response_headers = .{ .allocator = alloc, .owned = true },
|
||||
.headers = Headers.init(alloc),
|
||||
.response_headers = Headers.init(alloc),
|
||||
.impl = YieldImpl.init(loop),
|
||||
.method = undefined,
|
||||
.url = null,
|
||||
.uri = undefined,
|
||||
.state = UNSENT,
|
||||
// TODO retrieve the HTTP client globally to reuse existing connections.
|
||||
.cli = .{ .allocator = alloc, .loop = loop },
|
||||
.cli = userctx.httpClient,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -218,25 +332,22 @@ pub const XMLHttpRequest = struct {
|
||||
self.response_headers.deinit();
|
||||
|
||||
self.proto.deinit(alloc);
|
||||
|
||||
// TODO the client must be shared between requests.
|
||||
self.cli.deinit();
|
||||
}
|
||||
|
||||
pub fn get_readyState(self: *XMLHttpRequest) u16 {
|
||||
return self.state;
|
||||
}
|
||||
|
||||
pub fn get_timeout(self: *XMLHttpRequest) u32 {
|
||||
return self.timeout;
|
||||
pub fn get_timeout(_: *XMLHttpRequest) u32 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
pub fn set_timeout(self: *XMLHttpRequest, timeout: u32) !void {
|
||||
// TODO, the value is ignored for now.
|
||||
pub fn set_timeout(_: *XMLHttpRequest, _: u32) !void {
|
||||
// TODO If the current global object is a Window object and this’s
|
||||
// synchronous flag is set, then throw an "InvalidAccessError"
|
||||
// DOMException.
|
||||
// https://xhr.spec.whatwg.org/#dom-xmlhttprequest-timeout
|
||||
self.timeout = timeout;
|
||||
}
|
||||
|
||||
pub fn get_withCredentials(self: *XMLHttpRequest) bool {
|
||||
@@ -301,6 +412,7 @@ pub const XMLHttpRequest = struct {
|
||||
typ: []const u8,
|
||||
opts: ProgressEvent.EventInit,
|
||||
) void {
|
||||
log.debug("dispatch progress event: {s}", .{typ});
|
||||
var evt = ProgressEvent.constructor(typ, .{
|
||||
// https://xhr.spec.whatwg.org/#firing-events-using-the-progressevent-interface
|
||||
.lengthComputable = opts.total > 0,
|
||||
@@ -369,7 +481,7 @@ pub const XMLHttpRequest = struct {
|
||||
const body_init = XMLHttpRequestBodyInit{ .String = body.? };
|
||||
|
||||
// keep the user content type from request headers.
|
||||
if (self.headers.getFirstEntry("Content-Type") == null) {
|
||||
if (self.headers.has("Content-Type")) {
|
||||
// https://fetch.spec.whatwg.org/#bodyinit-safely-extract
|
||||
try self.headers.append("Content-Type", try body_init.contentType());
|
||||
}
|
||||
@@ -395,14 +507,17 @@ pub const XMLHttpRequest = struct {
|
||||
switch (self.priv_state) {
|
||||
.new => {
|
||||
self.priv_state = .open;
|
||||
self.req = self.cli.open(self.method, self.uri, self.headers, .{}) catch |e| return self.onErr(e);
|
||||
self.req = self.cli.open(self.method, self.uri, .{
|
||||
.server_header_buffer = &self.response_header_buffer,
|
||||
.extra_headers = self.headers.all(),
|
||||
}) catch |e| return self.onErr(e);
|
||||
},
|
||||
.open => {
|
||||
// prepare payload transfert.
|
||||
if (self.payload) |v| self.req.?.transfer_encoding = .{ .content_length = v.len };
|
||||
|
||||
self.priv_state = .send;
|
||||
self.req.?.send(.{}) catch |e| return self.onErr(e);
|
||||
self.req.?.send() catch |e| return self.onErr(e);
|
||||
},
|
||||
.send => {
|
||||
if (self.payload) |payload| {
|
||||
@@ -425,7 +540,8 @@ pub const XMLHttpRequest = struct {
|
||||
log.info("{any} {any} {d}", .{ self.method, self.uri, self.req.?.response.status });
|
||||
|
||||
self.priv_state = .done;
|
||||
self.response_headers = self.req.?.response.headers.clone(self.response_headers.allocator) catch |e| return self.onErr(e);
|
||||
var it = self.req.?.response.iterateHeaders();
|
||||
self.response_headers.load(&it) catch |e| return self.onErr(e);
|
||||
|
||||
// extract a mime type from headers.
|
||||
const ct = self.response_headers.getFirstValue("Content-Type") orelse "text/xml";
|
||||
@@ -450,6 +566,7 @@ pub const XMLHttpRequest = struct {
|
||||
const reader = self.req.?.reader();
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var ln = buffer.len;
|
||||
var prev_dispatch: ?std.time.Instant = null;
|
||||
while (ln > 0) {
|
||||
ln = reader.read(&buffer) catch |e| {
|
||||
buf.deinit(self.alloc);
|
||||
@@ -461,7 +578,13 @@ pub const XMLHttpRequest = struct {
|
||||
};
|
||||
loaded = loaded + ln;
|
||||
|
||||
// TODO dispatch only if 50ms have passed.
|
||||
// Dispatch only if 50ms have passed.
|
||||
const now = std.time.Instant.now() catch |e| {
|
||||
buf.deinit(self.alloc);
|
||||
return self.onErr(e);
|
||||
};
|
||||
if (prev_dispatch != null and now.since(prev_dispatch.?) < min_delay) continue;
|
||||
defer prev_dispatch = now;
|
||||
|
||||
self.state = LOADING;
|
||||
self.dispatchEvt("readystatechange");
|
||||
@@ -803,31 +926,43 @@ pub fn testExecFn(
|
||||
};
|
||||
try checkCases(js_env, &document);
|
||||
|
||||
// var json = [_]Case{
|
||||
// .{ .src = "const req3 = new XMLHttpRequest()", .ex = "undefined" },
|
||||
// .{ .src = "req3.open('GET', 'http://httpbin.io/json')", .ex = "undefined" },
|
||||
// .{ .src = "req3.responseType = 'json'", .ex = "json" },
|
||||
|
||||
// .{ .src = "req3.send()", .ex = "undefined" },
|
||||
|
||||
// // Each case executed waits for all loop callaback calls.
|
||||
// // So the url has been retrieved.
|
||||
// .{ .src = "req3.status", .ex = "200" },
|
||||
// .{ .src = "req3.statusText", .ex = "OK" },
|
||||
// .{ .src = "req3.response", .ex = "" },
|
||||
// };
|
||||
// try checkCases(js_env, &json);
|
||||
//
|
||||
var post = [_]Case{
|
||||
var json = [_]Case{
|
||||
.{ .src = "const req3 = new XMLHttpRequest()", .ex = "undefined" },
|
||||
.{ .src = "req3.open('POST', 'http://httpbin.io/post')", .ex = "undefined" },
|
||||
.{ .src = "req3.send('foo')", .ex = "undefined" },
|
||||
.{ .src = "req3.open('GET', 'http://httpbin.io/json')", .ex = "undefined" },
|
||||
.{ .src = "req3.responseType = 'json'", .ex = "json" },
|
||||
|
||||
.{ .src = "req3.send()", .ex = "undefined" },
|
||||
|
||||
// Each case executed waits for all loop callaback calls.
|
||||
// So the url has been retrieved.
|
||||
.{ .src = "req3.status", .ex = "200" },
|
||||
.{ .src = "req3.statusText", .ex = "OK" },
|
||||
.{ .src = "req3.responseText.length > 64", .ex = "true" },
|
||||
.{ .src = "req3.response.slideshow.author", .ex = "Yours Truly" },
|
||||
};
|
||||
try checkCases(js_env, &json);
|
||||
|
||||
var post = [_]Case{
|
||||
.{ .src = "const req4 = new XMLHttpRequest()", .ex = "undefined" },
|
||||
.{ .src = "req4.open('POST', 'http://httpbin.io/post')", .ex = "undefined" },
|
||||
.{ .src = "req4.send('foo')", .ex = "undefined" },
|
||||
|
||||
// Each case executed waits for all loop callaback calls.
|
||||
// So the url has been retrieved.
|
||||
.{ .src = "req4.status", .ex = "200" },
|
||||
.{ .src = "req4.statusText", .ex = "OK" },
|
||||
.{ .src = "req4.responseText.length > 64", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &post);
|
||||
|
||||
var cbk = [_]Case{
|
||||
.{ .src = "const req5 = new XMLHttpRequest()", .ex = "undefined" },
|
||||
.{ .src = "req5.open('GET', 'http://httpbin.io/json')", .ex = "undefined" },
|
||||
.{ .src = "var status = 0; req5.onload = function () { status = this.status };", .ex = "function () { status = this.status }" },
|
||||
.{ .src = "req5.send()", .ex = "undefined" },
|
||||
|
||||
// Each case executed waits for all loop callaback calls.
|
||||
// So the url has been retrieved.
|
||||
.{ .src = "status", .ex = "200" },
|
||||
};
|
||||
try checkCases(js_env, &cbk);
|
||||
}
|
||||
|
||||
Submodule tests/wpt updated: a2c7f5a24d...e0a721a3b8
1
vendor/jsruntime-lib
vendored
1
vendor/jsruntime-lib
vendored
Submodule vendor/jsruntime-lib deleted from 2d7b816f48
1
vendor/lexbor-src
vendored
1
vendor/lexbor-src
vendored
Submodule vendor/lexbor-src deleted from b2c0a617f3
1
vendor/mimalloc
vendored
Submodule
1
vendor/mimalloc
vendored
Submodule
Submodule vendor/mimalloc added at 8f7d1e9a41
2
vendor/netsurf/libdom
vendored
2
vendor/netsurf/libdom
vendored
Submodule vendor/netsurf/libdom updated: 122f010dbc...3677430b15
2
vendor/netsurf/libhubbub
vendored
2
vendor/netsurf/libhubbub
vendored
Submodule vendor/netsurf/libhubbub updated: 873ed6e236...6f102212c8
2
vendor/netsurf/libparserutils
vendored
2
vendor/netsurf/libparserutils
vendored
Submodule vendor/netsurf/libparserutils updated: 96cdd0ff11...094dc22e2b
2
vendor/netsurf/libwapcaplet
vendored
2
vendor/netsurf/libwapcaplet
vendored
Submodule vendor/netsurf/libwapcaplet updated: b5e42b1221...74f1e01173
1
vendor/zig-js-runtime
vendored
Submodule
1
vendor/zig-js-runtime
vendored
Submodule
Submodule vendor/zig-js-runtime added at a820cf4387
Reference in New Issue
Block a user