mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 15:13:28 +00:00
Compare commits
301 Commits
pandasurf
...
get-cached
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc9f0b961d | ||
|
|
7969e047c7 | ||
|
|
f0d6d9d177 | ||
|
|
0b793d82fe | ||
|
|
f6d51462eb | ||
|
|
5bdacbab61 | ||
|
|
e239cc962b | ||
|
|
6ebd4fcf5b | ||
|
|
ef427fa966 | ||
|
|
f4383a11d7 | ||
|
|
77b6377473 | ||
|
|
7bf3cf999f | ||
|
|
4ab611de0c | ||
|
|
d7745a418f | ||
|
|
058a5a43ba | ||
|
|
878dbd81b1 | ||
|
|
3c64ed1eb2 | ||
|
|
ee50f1238c | ||
|
|
1af2513fc0 | ||
|
|
9c0d26bc84 | ||
|
|
4d9053a83b | ||
|
|
3f7e98c277 | ||
|
|
aebc877e7b | ||
|
|
eef5f3fec2 | ||
|
|
16a1677fde | ||
|
|
f199816fcd | ||
|
|
5e74e17b41 | ||
|
|
98b041e84a | ||
|
|
bba9c8f652 | ||
|
|
1a05fe6ae1 | ||
|
|
16fcbf66ee | ||
|
|
b7fd4e90e2 | ||
|
|
b6341c10cc | ||
|
|
08487b0fcc | ||
|
|
b084dde22a | ||
|
|
5229a7c997 | ||
|
|
e56c56e2fe | ||
|
|
7374f956cf | ||
|
|
287df42994 | ||
|
|
06e514cc2e | ||
|
|
dffd8b5fec | ||
|
|
2a87337875 | ||
|
|
a74f79118f | ||
|
|
a13ed0bec3 | ||
|
|
f8ca45f0f2 | ||
|
|
4bf92a34f6 | ||
|
|
4f1c84004a | ||
|
|
1bd430598d | ||
|
|
3bc654bf97 | ||
|
|
3906acb83d | ||
|
|
cfd62ac137 | ||
|
|
cd540dfae9 | ||
|
|
74ad9ec8bf | ||
|
|
4f8a3fe5b9 | ||
|
|
09ca0e6ef0 | ||
|
|
fae2b5acfa | ||
|
|
d35a3eab6c | ||
|
|
7f7f47497a | ||
|
|
eb14ac3741 | ||
|
|
22334faba3 | ||
|
|
d08fd297e8 | ||
|
|
0dd664bfbf | ||
|
|
1602932d72 | ||
|
|
98c8b7d2b0 | ||
|
|
b9ae24c42d | ||
|
|
b387fd2bd4 | ||
|
|
818f4540fd | ||
|
|
49a97dbb66 | ||
|
|
a8b72c1d5f | ||
|
|
765b8dc97b | ||
|
|
5123697afe | ||
|
|
2a2a9d7941 | ||
|
|
2873aa5f81 | ||
|
|
795c925ba1 | ||
|
|
d6ace3f695 | ||
|
|
dd04759de7 | ||
|
|
10fbde84ba | ||
|
|
2b5652e1e4 | ||
|
|
18796ae44e | ||
|
|
a67692dc29 | ||
|
|
1efd756a55 | ||
|
|
29671acdb6 | ||
|
|
e82240a60e | ||
|
|
72083c8614 | ||
|
|
8c2c1e534c | ||
|
|
bfc01d957b | ||
|
|
4a12d045e4 | ||
|
|
2d78b2c219 | ||
|
|
3049bb0b9f | ||
|
|
34ab8152fb | ||
|
|
fb58c50fb7 | ||
|
|
955f917015 | ||
|
|
12c7df98e4 | ||
|
|
889c29a163 | ||
|
|
886c1370e7 | ||
|
|
febcc0a673 | ||
|
|
98cad6bf8d | ||
|
|
7e5daedc8c | ||
|
|
da3fe6f7ea | ||
|
|
f612ce262f | ||
|
|
24ccfca279 | ||
|
|
34b3c3982b | ||
|
|
7f732c94da | ||
|
|
bdc49a65aa | ||
|
|
73d82dd0ba | ||
|
|
dfa4403c8a | ||
|
|
b8f3b19499 | ||
|
|
448718d112 | ||
|
|
6de55df4bc | ||
|
|
189fe26667 | ||
|
|
7230884116 | ||
|
|
d7fba81f8f | ||
|
|
29ac13185c | ||
|
|
3a49ee83ce | ||
|
|
95cbbc3b45 | ||
|
|
2a5c7d139f | ||
|
|
b74863873b | ||
|
|
7b46fe9cc8 | ||
|
|
afc8c69a82 | ||
|
|
38bbad6e88 | ||
|
|
1df47fd415 | ||
|
|
faf21c5fff | ||
|
|
2aee580795 | ||
|
|
404c027546 | ||
|
|
04e59c6df2 | ||
|
|
835042b794 | ||
|
|
907490e266 | ||
|
|
80fe167646 | ||
|
|
d30631f991 | ||
|
|
8956ab85f9 | ||
|
|
2cdc9e9f5f | ||
|
|
13c623755c | ||
|
|
bdfceec520 | ||
|
|
941dace7f9 | ||
|
|
07693e54af | ||
|
|
b6132f2497 | ||
|
|
b3fe3d02c9 | ||
|
|
e880b18bb1 | ||
|
|
74a299eef7 | ||
|
|
300428ddfb | ||
|
|
1c27f8251e | ||
|
|
92badd3722 | ||
|
|
8a80f0b3dd | ||
|
|
fcc74b63d3 | ||
|
|
d7155e6662 | ||
|
|
42c3841639 | ||
|
|
c331713401 | ||
|
|
002d9c1747 | ||
|
|
2885ceceb1 | ||
|
|
22a644ba01 | ||
|
|
bab120a75d | ||
|
|
7a07c82f06 | ||
|
|
e881d2f6cf | ||
|
|
c8d003a08f | ||
|
|
e2cc404571 | ||
|
|
be71eaae47 | ||
|
|
ed31a452b2 | ||
|
|
f51ee7f3a0 | ||
|
|
9d1dc97766 | ||
|
|
b78729f685 | ||
|
|
44a76e59f9 | ||
|
|
1504e36a68 | ||
|
|
80348ef190 | ||
|
|
a3c14748d3 | ||
|
|
3c0143af92 | ||
|
|
22a93a9c39 | ||
|
|
e8866a6431 | ||
|
|
455ed79872 | ||
|
|
3d17c531d7 | ||
|
|
dfe90243d6 | ||
|
|
bf1db50667 | ||
|
|
a2565a7c83 | ||
|
|
947d01a3c0 | ||
|
|
be11d82c9c | ||
|
|
7a0e7fff13 | ||
|
|
81fb71b7f7 | ||
|
|
b10f5ec99f | ||
|
|
5abe7bdeef | ||
|
|
54be651415 | ||
|
|
cdbf6d7ae7 | ||
|
|
50349edf4d | ||
|
|
da307c1b40 | ||
|
|
b50b96bd1d | ||
|
|
92654fc5aa | ||
|
|
36b2de216b | ||
|
|
8745c1016e | ||
|
|
f5a58c1ff0 | ||
|
|
d9e72049ae | ||
|
|
927ca01161 | ||
|
|
3ea8d0b01c | ||
|
|
c52d33e331 | ||
|
|
fd36606acc | ||
|
|
1c6f4a79e0 | ||
|
|
7896d274a3 | ||
|
|
6937c8ecb4 | ||
|
|
f02b9566c5 | ||
|
|
c9936c2b7e | ||
|
|
bbd9e5e07c | ||
|
|
476fb7ec4e | ||
|
|
7435274be2 | ||
|
|
08d2ea6a10 | ||
|
|
41b7ed6938 | ||
|
|
7a311a181b | ||
|
|
ddcb597710 | ||
|
|
9c75f29875 | ||
|
|
343f3885f7 | ||
|
|
ed7dfeab84 | ||
|
|
8de27b3674 | ||
|
|
f56b0a5f6d | ||
|
|
0a27e1254f | ||
|
|
3f9b256fcb | ||
|
|
9ea9859150 | ||
|
|
03e3f95d2e | ||
|
|
e721b0af92 | ||
|
|
e18c589de3 | ||
|
|
aea34264a9 | ||
|
|
8d3a04235d | ||
|
|
9c4088b24c | ||
|
|
1e7ee4e0a1 | ||
|
|
ec92f110b3 | ||
|
|
2aa5eb85ad | ||
|
|
2815f02382 | ||
|
|
8bd7c8dd41 | ||
|
|
269dcf071f | ||
|
|
997ec7f0bc | ||
|
|
d9c26bb77f | ||
|
|
c0fc3a19c8 | ||
|
|
ce638c39e3 | ||
|
|
6b651cd5e4 | ||
|
|
4560f31010 | ||
|
|
c97a32e24b | ||
|
|
8a005bc5a1 | ||
|
|
20aabee72e | ||
|
|
a00c2345ee | ||
|
|
cb35b3624a | ||
|
|
c6f59a7aa6 | ||
|
|
bf296ad797 | ||
|
|
256540934b | ||
|
|
3c07c0818d | ||
|
|
a01d18ace1 | ||
|
|
55e02f01dc | ||
|
|
fe6ccad485 | ||
|
|
11fe79312d | ||
|
|
bdb2338b5b | ||
|
|
bbafb048d0 | ||
|
|
9fc2fa51bd | ||
|
|
d8ec50345a | ||
|
|
9f1cc09ca8 | ||
|
|
5dcc3db36b | ||
|
|
898b73ffc8 | ||
|
|
c5d49a9d34 | ||
|
|
ef9f828d35 | ||
|
|
c691764205 | ||
|
|
2c940d4fd6 | ||
|
|
54bd55d45d | ||
|
|
0b846b15b1 | ||
|
|
269eb7e154 | ||
|
|
97bc19e4ae | ||
|
|
2656cc7842 | ||
|
|
ba94818415 | ||
|
|
ac759a6eed | ||
|
|
1839b346a6 | ||
|
|
c1ffe7f8e6 | ||
|
|
833b4d10bd | ||
|
|
ce98c336c9 | ||
|
|
d05619990a | ||
|
|
8033e41d4a | ||
|
|
60f4eab759 | ||
|
|
d7656ea985 | ||
|
|
e402998577 | ||
|
|
073f75efa3 | ||
|
|
da414f7eb3 | ||
|
|
270b89830a | ||
|
|
74ce7ca416 | ||
|
|
3f4338cb51 | ||
|
|
30ee41fd0e | ||
|
|
4965fec55c | ||
|
|
18dff8455c | ||
|
|
fe16f06aee | ||
|
|
48c1c05a93 | ||
|
|
38dee1166d | ||
|
|
0c6fc68eae | ||
|
|
223611d89e | ||
|
|
6f5141d5fb | ||
|
|
a6ac7d9c4e | ||
|
|
9b35736be3 | ||
|
|
9f54cb35f4 | ||
|
|
329bffb127 | ||
|
|
e2542f41b5 | ||
|
|
efc7b9d4a5 | ||
|
|
72915760c4 | ||
|
|
e9d7a946c5 | ||
|
|
714e5e0456 | ||
|
|
26e8642aca | ||
|
|
68dfb4ee86 | ||
|
|
f1ff789334 | ||
|
|
1f45d5b8e4 | ||
|
|
c20052f314 | ||
|
|
c28d87d59c | ||
|
|
237ddcba9a | ||
|
|
eadb5b6461 |
2
.github/actions/install/action.yml
vendored
2
.github/actions/install/action.yml
vendored
@@ -17,7 +17,7 @@ inputs:
|
||||
zig-v8:
|
||||
description: 'zig v8 version to install'
|
||||
required: false
|
||||
default: 'v0.1.24'
|
||||
default: 'v0.1.28'
|
||||
v8:
|
||||
description: 'v8 version to install'
|
||||
required: false
|
||||
|
||||
13
.github/workflows/build.yml
vendored
13
.github/workflows/build.yml
vendored
@@ -60,7 +60,7 @@ jobs:
|
||||
ARCH: aarch64
|
||||
OS: linux
|
||||
|
||||
runs-on: ubuntu-24.04-arm
|
||||
runs-on: ubuntu-22.04-arm
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
arch: ${{env.ARCH}}
|
||||
|
||||
- name: zig build
|
||||
run: zig build --release=safe -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||
run: zig build --release=safe -Doptimize=ReleaseSafe -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||
|
||||
- name: Rename binary
|
||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
@@ -98,7 +98,9 @@ jobs:
|
||||
ARCH: aarch64
|
||||
OS: macos
|
||||
|
||||
runs-on: macos-latest
|
||||
# macos-14 runs on arm CPU. see
|
||||
# https://github.com/actions/runner-images?tab=readme-ov-file
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
@@ -136,6 +138,11 @@ jobs:
|
||||
ARCH: x86_64
|
||||
OS: macos
|
||||
|
||||
# macos-13 runs on x86 CPU. see
|
||||
# https://github.com/actions/runner-images?tab=readme-ov-file
|
||||
# If we want to build for macos-14 or superior, we need to switch to
|
||||
# macos-14-large.
|
||||
# No need for now, but maybe we will need it in the short term.
|
||||
runs-on: macos-13
|
||||
timeout-minutes: 15
|
||||
|
||||
|
||||
71
.github/workflows/e2e-test.yml
vendored
71
.github/workflows/e2e-test.yml
vendored
@@ -45,6 +45,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
# Don't run the CI with draft PR.
|
||||
if: github.event.pull_request.draft == false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -65,56 +68,6 @@ jobs:
|
||||
zig-out/bin/lightpanda
|
||||
retention-days: 1
|
||||
|
||||
puppeteer-perf:
|
||||
name: puppeteer-perf
|
||||
needs: zig-build-release
|
||||
|
||||
env:
|
||||
MAX_MEMORY: 30000
|
||||
MAX_AVG_DURATION: 24
|
||||
LIGHTPANDA_DISABLE_TELEMETRY: true
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'lightpanda-io/demo'
|
||||
fetch-depth: 0
|
||||
|
||||
- run: npm install
|
||||
|
||||
- name: download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: lightpanda-build-release
|
||||
|
||||
- run: chmod a+x ./lightpanda
|
||||
|
||||
- name: run puppeteer
|
||||
run: |
|
||||
python3 -m http.server 1234 -d ./public & echo $! > PYTHON.pid
|
||||
./lightpanda serve & echo $! > LPD.pid
|
||||
RUNS=100 npm run bench-puppeteer-cdp > puppeteer.out || exit 1
|
||||
cat /proc/`cat LPD.pid`/status |grep VmHWM|grep -oP '\d+' > LPD.VmHWM
|
||||
kill `cat LPD.pid` `cat PYTHON.pid`
|
||||
|
||||
- name: puppeteer result
|
||||
run: cat puppeteer.out
|
||||
|
||||
- name: memory regression
|
||||
run: |
|
||||
export LPD_VmHWM=`cat LPD.VmHWM`
|
||||
echo "Peak resident set size: $LPD_VmHWM"
|
||||
test "$LPD_VmHWM" -le "$MAX_MEMORY"
|
||||
|
||||
- name: duration regression
|
||||
run: |
|
||||
export PUPPETEER_AVG_DURATION=`cat puppeteer.out|grep 'avg run'|sed 's/avg run duration (ms) //'`
|
||||
echo "puppeteer avg duration: $PUPPETEER_AVG_DURATION"
|
||||
test "$PUPPETEER_AVG_DURATION" -le "$MAX_AVG_DURATION"
|
||||
|
||||
demo-scripts:
|
||||
name: demo-scripts
|
||||
needs: zig-build-release
|
||||
@@ -147,8 +100,10 @@ jobs:
|
||||
name: cdp-and-hyperfine-bench
|
||||
needs: zig-build-release
|
||||
|
||||
# Don't execute on PR
|
||||
if: github.event_name != 'pull_request'
|
||||
env:
|
||||
MAX_MEMORY: 27000
|
||||
MAX_AVG_DURATION: 23
|
||||
LIGHTPANDA_DISABLE_TELEMETRY: true
|
||||
|
||||
# use a self host runner.
|
||||
runs-on: lpd-bench-hetzner
|
||||
@@ -185,6 +140,18 @@ jobs:
|
||||
- name: puppeteer result
|
||||
run: cat puppeteer.out
|
||||
|
||||
- name: memory regression
|
||||
run: |
|
||||
export LPD_VmHWM=`cat LPD.VmHWM`
|
||||
echo "Peak resident set size: $LPD_VmHWM"
|
||||
test "$LPD_VmHWM" -le "$MAX_MEMORY"
|
||||
|
||||
- name: duration regression
|
||||
run: |
|
||||
export PUPPETEER_AVG_DURATION=`cat puppeteer.out|grep 'avg run'|sed 's/avg run duration (ms) //'`
|
||||
echo "puppeteer avg duration: $PUPPETEER_AVG_DURATION"
|
||||
test "$PUPPETEER_AVG_DURATION" -le "$MAX_AVG_DURATION"
|
||||
|
||||
- name: json output
|
||||
run: |
|
||||
export AVG_DURATION=`cat puppeteer.out|grep 'avg run'|sed 's/avg run duration (ms) //'`
|
||||
|
||||
43
Dockerfile
43
Dockerfile
@@ -1,11 +1,11 @@
|
||||
FROM ubuntu:24.04
|
||||
FROM debian:stable
|
||||
|
||||
ARG MINISIG=0.12
|
||||
ARG ZIG=0.14.1
|
||||
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
||||
ARG ARCH=x86_64
|
||||
ARG V8=13.6.233.8
|
||||
ARG ZIG_V8=v0.1.24
|
||||
ARG ZIG_V8=v0.1.28
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
RUN apt-get update -yq && \
|
||||
apt-get install -yq xz-utils \
|
||||
@@ -20,30 +20,19 @@ RUN curl --fail -L -O https://github.com/jedisct1/minisign/releases/download/${M
|
||||
tar xvzf minisign-${MINISIG}-linux.tar.gz
|
||||
|
||||
# install zig
|
||||
RUN curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-${ARCH}-linux-${ZIG}.tar.xz
|
||||
RUN curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-${ARCH}-linux-${ZIG}.tar.xz.minisig
|
||||
|
||||
RUN minisign-linux/${ARCH}/minisign -Vm zig-${ARCH}-linux-${ZIG}.tar.xz -P ${ZIG_MINISIG}
|
||||
|
||||
# clean minisg
|
||||
RUN rm -fr minisign-0.11-linux.tar.gz minisign-linux
|
||||
|
||||
# install zig
|
||||
RUN tar xvf zig-${ARCH}-linux-${ZIG}.tar.xz && \
|
||||
RUN case $TARGETPLATFORM in \
|
||||
"linux/arm64") ARCH="aarch64" ;; \
|
||||
*) ARCH="x86_64" ;; \
|
||||
esac && \
|
||||
curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-${ARCH}-linux-${ZIG}.tar.xz && \
|
||||
curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-${ARCH}-linux-${ZIG}.tar.xz.minisig && \
|
||||
minisign-linux/${ARCH}/minisign -Vm zig-${ARCH}-linux-${ZIG}.tar.xz -P ${ZIG_MINISIG} && \
|
||||
tar xvf zig-${ARCH}-linux-${ZIG}.tar.xz && \
|
||||
mv zig-${ARCH}-linux-${ZIG} /usr/local/lib && \
|
||||
ln -s /usr/local/lib/zig-${ARCH}-linux-${ZIG}/zig /usr/local/bin/zig
|
||||
|
||||
# clean up zig install
|
||||
RUN rm -fr zig-${ARCH}-linux-${ZIG}.tar.xz zig-${ARCH}-linux-${ZIG}.tar.xz.minisig
|
||||
|
||||
# force use of http instead of ssh with github
|
||||
RUN cat <<EOF > /root/.gitconfig
|
||||
[url "https://github.com/"]
|
||||
insteadOf="git@github.com:"
|
||||
EOF
|
||||
|
||||
# clone lightpanda
|
||||
RUN git clone git@github.com:lightpanda-io/browser.git
|
||||
RUN git clone https://github.com/lightpanda-io/browser.git
|
||||
|
||||
WORKDIR /browser
|
||||
|
||||
@@ -56,14 +45,18 @@ RUN make install-libiconv && \
|
||||
make install-mimalloc
|
||||
|
||||
# download and install v8
|
||||
RUN curl --fail -L -o libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${ZIG_V8}/libc_v8_${V8}_linux_${ARCH}.a && \
|
||||
RUN case $TARGETPLATFORM in \
|
||||
"linux/arm64") ARCH="aarch64" ;; \
|
||||
*) ARCH="x86_64" ;; \
|
||||
esac && \
|
||||
curl --fail -L -o libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${ZIG_V8}/libc_v8_${V8}_linux_${ARCH}.a && \
|
||||
mkdir -p v8/out/linux/release/obj/zig/ && \
|
||||
mv libc_v8.a v8/out/linux/release/obj/zig/libc_v8.a
|
||||
|
||||
# build release
|
||||
RUN make build
|
||||
|
||||
FROM ubuntu:24.04
|
||||
FROM debian:stable-slim
|
||||
|
||||
# copy ca certificates
|
||||
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
34
README.md
34
README.md
@@ -18,7 +18,7 @@ Lightpanda is the open-source browser made for headless usage:
|
||||
|
||||
- Javascript execution
|
||||
- Support of Web APIs (partial, WIP)
|
||||
- Compatible with Playwright[^1], Puppeteer through CDP (WIP)
|
||||
- Compatible with Playwright[^1], Puppeteer, chromedp through CDP
|
||||
|
||||
Fast web automation for AI agents, LLM training, scraping and testing:
|
||||
|
||||
@@ -41,7 +41,8 @@ Due to the nature of Playwright, a script that works with the current version of
|
||||
|
||||
## Quick start
|
||||
|
||||
### Install from the nightly builds
|
||||
### Install
|
||||
**Install from the nightly builds**
|
||||
|
||||
You can download the last binary from the [nightly
|
||||
builds](https://github.com/lightpanda-io/browser/releases/tag/nightly) for
|
||||
@@ -64,6 +65,17 @@ chmod a+x ./lightpanda
|
||||
The Lightpanda browser is compatible to run on windows inside WSL. Follow the Linux instruction for installation from a WSL terminal.
|
||||
It is recommended to install clients like Puppeteer on the Windows host.
|
||||
|
||||
**Install from Docker**
|
||||
|
||||
Lightpanda provides [official Docker
|
||||
images](https://hub.docker.com/r/lightpanda/browser) for both Linux amd64 and
|
||||
arm64 architectures.
|
||||
The following command fetches the Docker image and starts a new container exposing Lightpanda's CDP server on port `9222`.
|
||||
The `--privileged` option is required because the browser requires `io_uring` syscalls which are blocked by default by Docker.
|
||||
```console
|
||||
docker run -d --name lightpanda -p 9222:9222 --privileged lightpanda/browser:nightly
|
||||
```
|
||||
|
||||
### Dump a URL
|
||||
|
||||
```console
|
||||
@@ -124,21 +136,27 @@ By default, Lightpanda collects and sends usage telemetry. This can be disabled
|
||||
|
||||
## Status
|
||||
|
||||
Lightpanda is still a work in progress and is currently at a Beta stage.
|
||||
|
||||
:warning: You should expect most websites to fail or crash.
|
||||
Lightpanda is in Beta and currently a work in progress. Stability and coverage are improving and many websites now work.
|
||||
You may still encounter errors or crashes. Please open an issue with specifics if so.
|
||||
|
||||
Here are the key features we have implemented:
|
||||
|
||||
- [x] HTTP loader
|
||||
- [x] HTTP loader
|
||||
- [x] HTML parser and DOM tree (based on Netsurf libs)
|
||||
- [x] Javascript support (v8)
|
||||
- [x] Basic DOM APIs
|
||||
- [x] DOM APIs
|
||||
- [x] Ajax
|
||||
- [x] XHR API
|
||||
- [x] Fetch API
|
||||
- [x] Fetch API (polyfill)
|
||||
- [x] DOM dump
|
||||
- [x] Basic CDP/websockets server
|
||||
- [x] CDP/websockets server
|
||||
- [x] Click
|
||||
- [x] Input form
|
||||
- [x] Cookies
|
||||
- [x] Custom HTTP headers
|
||||
- [ ] Proxy support
|
||||
- [ ] Network interception
|
||||
|
||||
NOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time.
|
||||
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
.fingerprint = 0xda130f3af836cea0,
|
||||
.dependencies = .{
|
||||
.tls = .{
|
||||
.url = "https://github.com/ianic/tls.zig/archive/b29a8b45fc59fc2d202769c4f54509bb9e17d0a2.tar.gz",
|
||||
.hash = "tls-0.1.0-ER2e0uAxBQDm_TmSDdbiiyvAZoh4ejlDD4hW8Fl813xE",
|
||||
.url = "https://github.com/ianic/tls.zig/archive/55845f755d9e2e821458ea55693f85c737cd0c7a.tar.gz",
|
||||
.hash = "tls-0.1.0-ER2e0m43BQAshi8ixj1qf3w2u2lqKtXtkrxUJ4AGZDcl",
|
||||
},
|
||||
.tigerbeetle_io = .{
|
||||
.url = "https://github.com/lightpanda-io/tigerbeetle-io/archive/61d9652f1a957b7f4db723ea6aa0ce9635e840ce.tar.gz",
|
||||
.hash = "tigerbeetle_io-0.0.0-ViLgxpyRBAB5BMfIcj3KMXfbJzwARs9uSl8aRy2OXULd",
|
||||
},
|
||||
.v8 = .{
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/1d25fcf3ced688adca3c7a95a138771e4ebba692.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH61eyAwDICIkLAkfQcxsX4TMCKY80QiSUgNBQqx-u",
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/b22911e02e4884a76acf52aa9aff2ba169d05b40.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH69zCAwAzm1u5cQVa-uG5ib2y6PpENXCl8yEYdUYk",
|
||||
},
|
||||
//.v8 = .{ .path = "../zig-v8-fork" },
|
||||
//.tigerbeetle_io = .{ .path = "../tigerbeetle-io" },
|
||||
|
||||
15
src/app.zig
15
src/app.zig
@@ -3,7 +3,9 @@ const Allocator = std.mem.Allocator;
|
||||
|
||||
const log = @import("log.zig");
|
||||
const Loop = @import("runtime/loop.zig").Loop;
|
||||
const HttpClient = @import("http/client.zig").Client;
|
||||
const http = @import("http/client.zig");
|
||||
const Platform = @import("runtime/js.zig").Platform;
|
||||
|
||||
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
|
||||
const Notification = @import("notification.zig").Notification;
|
||||
|
||||
@@ -12,9 +14,10 @@ const Notification = @import("notification.zig").Notification;
|
||||
pub const App = struct {
|
||||
loop: *Loop,
|
||||
config: Config,
|
||||
platform: ?*const Platform,
|
||||
allocator: Allocator,
|
||||
telemetry: Telemetry,
|
||||
http_client: HttpClient,
|
||||
http_client: http.Client,
|
||||
app_dir_path: ?[]const u8,
|
||||
notification: *Notification,
|
||||
|
||||
@@ -27,8 +30,11 @@ pub const App = struct {
|
||||
|
||||
pub const Config = struct {
|
||||
run_mode: RunMode,
|
||||
platform: ?*const Platform = null,
|
||||
tls_verify_host: bool = true,
|
||||
http_proxy: ?std.Uri = null,
|
||||
proxy_type: ?http.ProxyType = null,
|
||||
proxy_auth: ?http.ProxyAuth = null,
|
||||
};
|
||||
|
||||
pub fn init(allocator: Allocator, config: Config) !*App {
|
||||
@@ -50,11 +56,14 @@ pub const App = struct {
|
||||
.loop = loop,
|
||||
.allocator = allocator,
|
||||
.telemetry = undefined,
|
||||
.platform = config.platform,
|
||||
.app_dir_path = app_dir_path,
|
||||
.notification = notification,
|
||||
.http_client = try HttpClient.init(allocator, .{
|
||||
.http_client = try http.Client.init(allocator, loop, .{
|
||||
.max_concurrent = 3,
|
||||
.http_proxy = config.http_proxy,
|
||||
.proxy_type = config.proxy_type,
|
||||
.proxy_auth = config.proxy_auth,
|
||||
.tls_verify_host = config.tls_verify_host,
|
||||
}),
|
||||
.config = config,
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
|
||||
const Env = @import("env.zig").Env;
|
||||
const parser = @import("netsurf.zig");
|
||||
const DataSet = @import("html/DataSet.zig");
|
||||
const ShadowRoot = @import("dom/shadow_root.zig").ShadowRoot;
|
||||
const StyleSheet = @import("cssom/stylesheet.zig").StyleSheet;
|
||||
const CSSStyleDeclaration = @import("cssom/css_style_declaration.zig").CSSStyleDeclaration;
|
||||
|
||||
// for HTMLScript (but probably needs to be added to more)
|
||||
@@ -36,10 +39,18 @@ onerror: ?Env.Function = null,
|
||||
|
||||
// for HTMLElement
|
||||
style: CSSStyleDeclaration = .empty,
|
||||
dataset: ?DataSet = null,
|
||||
template_content: ?*parser.DocumentFragment = null,
|
||||
|
||||
// For dom/element
|
||||
shadow_root: ?*ShadowRoot = null,
|
||||
|
||||
// for html/document
|
||||
ready_state: ReadyState = .loading,
|
||||
|
||||
// for html/HTMLStyleElement
|
||||
style_sheet: ?*StyleSheet = null,
|
||||
|
||||
// for dom/document
|
||||
active_element: ?*parser.Element = null,
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ const App = @import("../app.zig").App;
|
||||
const Session = @import("session.zig").Session;
|
||||
const Notification = @import("../notification.zig").Notification;
|
||||
|
||||
const log = @import("../log.zig");
|
||||
|
||||
const http = @import("../http/client.zig");
|
||||
|
||||
// Browser is an instance of the browser.
|
||||
@@ -47,7 +49,7 @@ pub const Browser = struct {
|
||||
pub fn init(app: *App) !Browser {
|
||||
const allocator = app.allocator;
|
||||
|
||||
const env = try Env.init(allocator, .{});
|
||||
const env = try Env.init(allocator, app.platform, .{});
|
||||
errdefer env.deinit();
|
||||
|
||||
const notification = try Notification.init(allocator, app.notification);
|
||||
@@ -95,7 +97,14 @@ pub const Browser = struct {
|
||||
}
|
||||
|
||||
pub fn runMicrotasks(self: *const Browser) void {
|
||||
return self.env.runMicrotasks();
|
||||
self.env.runMicrotasks();
|
||||
}
|
||||
|
||||
pub fn runMessageLoop(self: *const Browser) void {
|
||||
while (self.env.pumpMessageLoop()) {
|
||||
log.debug(.browser, "pumpMessageLoop", .{});
|
||||
}
|
||||
self.env.runIdleTasks();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -30,39 +30,39 @@ pub const Console = struct {
|
||||
timers: std.StringHashMapUnmanaged(u32) = .{},
|
||||
counts: std.StringHashMapUnmanaged(u32) = .{},
|
||||
|
||||
pub fn static_lp(values: []JsObject, page: *Page) !void {
|
||||
pub fn _lp(values: []JsObject, page: *Page) !void {
|
||||
if (values.len == 0) {
|
||||
return;
|
||||
}
|
||||
log.fatal(.console, "lightpanda", .{ .args = try serializeValues(values, page) });
|
||||
}
|
||||
|
||||
pub fn static_log(values: []JsObject, page: *Page) !void {
|
||||
pub fn _log(values: []JsObject, page: *Page) !void {
|
||||
if (values.len == 0) {
|
||||
return;
|
||||
}
|
||||
log.info(.console, "info", .{ .args = try serializeValues(values, page) });
|
||||
}
|
||||
|
||||
pub fn static_info(values: []JsObject, page: *Page) !void {
|
||||
return static_log(values, page);
|
||||
pub fn _info(values: []JsObject, page: *Page) !void {
|
||||
return _log(values, page);
|
||||
}
|
||||
|
||||
pub fn static_debug(values: []JsObject, page: *Page) !void {
|
||||
pub fn _debug(values: []JsObject, page: *Page) !void {
|
||||
if (values.len == 0) {
|
||||
return;
|
||||
}
|
||||
log.debug(.console, "debug", .{ .args = try serializeValues(values, page) });
|
||||
}
|
||||
|
||||
pub fn static_warn(values: []JsObject, page: *Page) !void {
|
||||
pub fn _warn(values: []JsObject, page: *Page) !void {
|
||||
if (values.len == 0) {
|
||||
return;
|
||||
}
|
||||
log.warn(.console, "warn", .{ .args = try serializeValues(values, page) });
|
||||
}
|
||||
|
||||
pub fn static_error(values: []JsObject, page: *Page) !void {
|
||||
pub fn _error(values: []JsObject, page: *Page) !void {
|
||||
if (values.len == 0) {
|
||||
return;
|
||||
}
|
||||
@@ -73,7 +73,7 @@ pub const Console = struct {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn static_clear() void {}
|
||||
pub fn _clear() void {}
|
||||
|
||||
pub fn _count(self: *Console, label_: ?[]const u8, page: *Page) !void {
|
||||
const label = label_ orelse "default";
|
||||
@@ -134,7 +134,7 @@ pub const Console = struct {
|
||||
log.warn(.console, "timer stop", .{ .label = label, .elapsed = elapsed - kv.value });
|
||||
}
|
||||
|
||||
pub fn static_assert(assertion: JsObject, values: []JsObject, page: *Page) !void {
|
||||
pub fn _assert(assertion: JsObject, values: []JsObject, page: *Page) !void {
|
||||
if (assertion.isTruthy()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -17,16 +17,21 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const uuidv4 = @import("../../id.zig").uuidv4;
|
||||
|
||||
// https://w3c.github.io/webcrypto/#crypto-interface
|
||||
pub const Crypto = struct {
|
||||
pub fn _getRandomValues(_: *const Crypto, into: RandomValues) !void {
|
||||
_not_empty: bool = true,
|
||||
|
||||
pub fn _getRandomValues(_: *const Crypto, js_obj: Env.JsObject) !Env.JsObject {
|
||||
var into = try js_obj.toZig(Crypto, "getRandomValues", RandomValues);
|
||||
const buf = into.asBuffer();
|
||||
if (buf.len > 65_536) {
|
||||
return error.QuotaExceededError;
|
||||
}
|
||||
std.crypto.random.bytes(buf);
|
||||
return js_obj;
|
||||
}
|
||||
|
||||
pub fn _randomUUID(_: *const Crypto) [36]u8 {
|
||||
@@ -47,16 +52,16 @@ const RandomValues = union(enum) {
|
||||
uint64: []u64,
|
||||
|
||||
fn asBuffer(self: RandomValues) []u8 {
|
||||
switch (self) {
|
||||
.int8 => |b| return (@as([]u8, @ptrCast(b)))[0..b.len],
|
||||
.uint8 => |b| return (@as([]u8, @ptrCast(b)))[0..b.len],
|
||||
.int16 => |b| return (@as([]u8, @ptrCast(b)))[0 .. b.len * 2],
|
||||
.uint16 => |b| return (@as([]u8, @ptrCast(b)))[0 .. b.len * 2],
|
||||
.int32 => |b| return (@as([]u8, @ptrCast(b)))[0 .. b.len * 4],
|
||||
.uint32 => |b| return (@as([]u8, @ptrCast(b)))[0 .. b.len * 4],
|
||||
.int64 => |b| return (@as([]u8, @ptrCast(b)))[0 .. b.len * 8],
|
||||
.uint64 => |b| return (@as([]u8, @ptrCast(b)))[0 .. b.len * 8],
|
||||
}
|
||||
return switch (self) {
|
||||
.int8 => |b| (@as([]u8, @ptrCast(b)))[0..b.len],
|
||||
.uint8 => |b| (@as([]u8, @ptrCast(b)))[0..b.len],
|
||||
.int16 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 2],
|
||||
.uint16 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 2],
|
||||
.int32 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 4],
|
||||
.uint32 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 4],
|
||||
.int64 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 8],
|
||||
.uint64 => |b| (@as([]u8, @ptrCast(b)))[0 .. b.len * 8],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -69,14 +74,24 @@ test "Browser.Crypto" {
|
||||
.{ "const a = crypto.randomUUID();", "undefined" },
|
||||
.{ "const b = crypto.randomUUID();", "undefined" },
|
||||
.{ "a.length;", "36" },
|
||||
.{ "a.length;", "36" },
|
||||
.{ "b.length;", "36" },
|
||||
.{ "a == b;", "false" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "try { crypto.getRandomValues(new BigUint64Array(8193)) } catch(e) { e.message == 'QuotaExceededError' }", "true" },
|
||||
.{ "let r1 = new Int32Array(5)", "undefined" },
|
||||
.{ "crypto.getRandomValues(r1)", "undefined" },
|
||||
.{ "let r2 = crypto.getRandomValues(r1)", "undefined" },
|
||||
.{ "new Set(r1).size", "5" },
|
||||
.{ "new Set(r2).size", "5" },
|
||||
.{ "r1.every((v, i) => v === r2[i])", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var r3 = new Uint8Array(16)", null },
|
||||
.{ "let r4 = crypto.getRandomValues(r3)", "undefined" },
|
||||
.{ "r4[6] = 10", null },
|
||||
.{ "r4[6]", "10" },
|
||||
.{ "r3[6]", "10" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -23,6 +23,20 @@ const std = @import("std");
|
||||
const Selector = @import("selector.zig").Selector;
|
||||
const parser = @import("parser.zig");
|
||||
|
||||
pub const Interfaces = .{
|
||||
Css,
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CSS
|
||||
pub const Css = struct {
|
||||
_not_empty: bool = true,
|
||||
|
||||
pub fn _supports(_: *Css, _: []const u8, _: ?[]const u8) bool {
|
||||
// TODO: Actually respond with which CSS features we support.
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// parse parse a selector string and returns the parsed result or an error.
|
||||
pub fn parse(alloc: std.mem.Allocator, s: []const u8, opts: parser.ParseOptions) parser.ParseError!Selector {
|
||||
var p = parser.Parser{ .s = s, .i = 0, .opts = opts };
|
||||
@@ -174,3 +188,14 @@ test "parse" {
|
||||
defer s.deinit(alloc);
|
||||
}
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.CSS" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "CSS.supports('display: flex')", "true" },
|
||||
.{ "CSS.supports('text-decoration-style', 'blink')", "true" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ pub const Parser = struct {
|
||||
}
|
||||
|
||||
const c = p.s[p.i];
|
||||
if (!nameStart(c) or c == '\\') {
|
||||
if (!(nameStart(c) or c == '\\')) {
|
||||
return ParseError.ExpectedSelector;
|
||||
}
|
||||
|
||||
@@ -582,6 +582,7 @@ pub const Parser = struct {
|
||||
.only_of_type => return .{ .pseudo_class_only_child = true },
|
||||
.input, .empty, .root, .link => return .{ .pseudo_class = pseudo_class },
|
||||
.enabled, .disabled, .checked => return .{ .pseudo_class = pseudo_class },
|
||||
.visible => return .{ .pseudo_class = pseudo_class },
|
||||
.lang => {
|
||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
||||
if (p.i == p.s.len) return ParseError.UnmatchParenthesis;
|
||||
@@ -605,6 +606,7 @@ pub const Parser = struct {
|
||||
.after, .backdrop, .before, .cue, .first_letter => return .{ .pseudo_element = pseudo_class },
|
||||
.first_line, .grammar_error, .marker, .placeholder => return .{ .pseudo_element = pseudo_class },
|
||||
.selection, .spelling_error => return .{ .pseudo_element = pseudo_class },
|
||||
.modal, .popover_open => return .{ .pseudo_element = pseudo_class },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -948,3 +950,36 @@ test "parser.parseString" {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test "parser.parse" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
const testcases = [_]struct {
|
||||
s: []const u8, // given value
|
||||
exp: Selector, // expected value
|
||||
err: bool = false,
|
||||
}{
|
||||
.{ .s = "root", .exp = .{ .tag = "root" } },
|
||||
.{ .s = ".root", .exp = .{ .class = "root" } },
|
||||
.{ .s = ":root", .exp = .{ .pseudo_class = .root } },
|
||||
.{ .s = ".\\:bar", .exp = .{ .class = ":bar" } },
|
||||
.{ .s = ".foo\\:bar", .exp = .{ .class = "foo:bar" } },
|
||||
};
|
||||
|
||||
for (testcases) |tc| {
|
||||
var p = Parser{ .s = tc.s, .opts = .{} };
|
||||
const sel = p.parse(alloc) catch |e| {
|
||||
// if error was expected, continue.
|
||||
if (tc.err) continue;
|
||||
|
||||
std.debug.print("test case {s}\n", .{tc.s});
|
||||
return e;
|
||||
};
|
||||
std.testing.expectEqualDeep(tc.exp, sel) catch |e| {
|
||||
std.debug.print("test case {s} : {}\n", .{ tc.s, sel });
|
||||
return e;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,9 @@ pub const PseudoClass = enum {
|
||||
placeholder,
|
||||
selection,
|
||||
spelling_error,
|
||||
modal,
|
||||
popover_open,
|
||||
visible,
|
||||
|
||||
pub const Error = error{
|
||||
InvalidPseudoClass,
|
||||
@@ -113,51 +116,108 @@ pub const PseudoClass = enum {
|
||||
}
|
||||
|
||||
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;
|
||||
const longest_selector = "nth-last-of-type";
|
||||
if (s.len > longest_selector.len) {
|
||||
return Error.InvalidPseudoClass;
|
||||
}
|
||||
|
||||
var buf: [longest_selector.len]u8 = undefined;
|
||||
const selector = std.ascii.lowerString(&buf, s);
|
||||
|
||||
switch (selector.len) {
|
||||
3 => switch (@as(u24, @bitCast(selector[0..3].*))) {
|
||||
asUint(u24, "cue") => return .cue,
|
||||
asUint(u24, "has") => return .has,
|
||||
asUint(u24, "not") => return .not,
|
||||
else => {},
|
||||
},
|
||||
4 => switch (@as(u32, @bitCast(selector[0..4].*))) {
|
||||
asUint(u32, "lang") => return .lang,
|
||||
asUint(u32, "link") => return .link,
|
||||
asUint(u32, "root") => return .root,
|
||||
else => {},
|
||||
},
|
||||
5 => switch (@as(u40, @bitCast(selector[0..5].*))) {
|
||||
asUint(u40, "after") => return .after,
|
||||
asUint(u40, "empty") => return .empty,
|
||||
asUint(u40, "focus") => return .focus,
|
||||
asUint(u40, "hover") => return .hover,
|
||||
asUint(u40, "input") => return .input,
|
||||
asUint(u40, "modal") => return .modal,
|
||||
else => {},
|
||||
},
|
||||
6 => switch (@as(u48, @bitCast(selector[0..6].*))) {
|
||||
asUint(u48, "active") => return .active,
|
||||
asUint(u48, "before") => return .before,
|
||||
asUint(u48, "marker") => return .marker,
|
||||
asUint(u48, "target") => return .target,
|
||||
else => {},
|
||||
},
|
||||
7 => switch (@as(u56, @bitCast(selector[0..7].*))) {
|
||||
asUint(u56, "checked") => return .checked,
|
||||
asUint(u56, "enabled") => return .enabled,
|
||||
asUint(u56, "matches") => return .matches,
|
||||
asUint(u56, "visited") => return .visited,
|
||||
asUint(u56, "visible") => return .visible,
|
||||
else => {},
|
||||
},
|
||||
8 => switch (@as(u64, @bitCast(selector[0..8].*))) {
|
||||
asUint(u64, "backdrop") => return .backdrop,
|
||||
asUint(u64, "contains") => return .contains,
|
||||
asUint(u64, "disabled") => return .disabled,
|
||||
asUint(u64, "haschild") => return .haschild,
|
||||
else => {},
|
||||
},
|
||||
9 => switch (@as(u72, @bitCast(selector[0..9].*))) {
|
||||
asUint(u72, "nth-child") => return .nth_child,
|
||||
asUint(u72, "selection") => return .selection,
|
||||
else => {},
|
||||
},
|
||||
10 => switch (@as(u80, @bitCast(selector[0..10].*))) {
|
||||
asUint(u80, "first-line") => return .first_line,
|
||||
asUint(u80, "last-child") => return .last_child,
|
||||
asUint(u80, "matchesown") => return .matchesown,
|
||||
asUint(u80, "only-child") => return .only_child,
|
||||
else => {},
|
||||
},
|
||||
11 => switch (@as(u88, @bitCast(selector[0..11].*))) {
|
||||
asUint(u88, "containsown") => return .containsown,
|
||||
asUint(u88, "first-child") => return .first_child,
|
||||
asUint(u88, "nth-of-type") => return .nth_of_type,
|
||||
asUint(u88, "placeholder") => return .placeholder,
|
||||
else => {},
|
||||
},
|
||||
12 => switch (@as(u96, @bitCast(selector[0..12].*))) {
|
||||
asUint(u96, "first-letter") => return .first_letter,
|
||||
asUint(u96, "last-of-type") => return .last_of_type,
|
||||
asUint(u96, "only-of-type") => return .only_of_type,
|
||||
asUint(u96, "popover-open") => return .popover_open,
|
||||
else => {},
|
||||
},
|
||||
13 => switch (@as(u104, @bitCast(selector[0..13].*))) {
|
||||
asUint(u104, "first-of-type") => return .first_of_type,
|
||||
asUint(u104, "grammar-error") => return .grammar_error,
|
||||
else => {},
|
||||
},
|
||||
14 => switch (@as(u112, @bitCast(selector[0..14].*))) {
|
||||
asUint(u112, "nth-last-child") => return .nth_last_child,
|
||||
asUint(u112, "spelling-error") => return .spelling_error,
|
||||
else => {},
|
||||
},
|
||||
16 => switch (@as(u128, @bitCast(selector[0..16].*))) {
|
||||
asUint(u128, "nth-last-of-type") => return .nth_last_of_type,
|
||||
else => {},
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
return Error.InvalidPseudoClass;
|
||||
}
|
||||
};
|
||||
|
||||
fn asUint(comptime T: type, comptime string: []const u8) T {
|
||||
return @bitCast(string[0..string.len].*);
|
||||
}
|
||||
|
||||
pub const Selector = union(enum) {
|
||||
pub const Error = error{
|
||||
UnknownCombinedCombinator,
|
||||
@@ -509,6 +569,8 @@ pub const Selector = union(enum) {
|
||||
// TODO implement using the url fragment.
|
||||
// see https://developer.mozilla.org/en-US/docs/Web/CSS/:target
|
||||
.target => return false,
|
||||
// visible always returns true.
|
||||
.visible => return true,
|
||||
|
||||
// all others pseudo class are handled by specialized
|
||||
// pseudo_class_X selectors.
|
||||
|
||||
42
src/browser/cssom/css_rule.zig
Normal file
42
src/browser/cssom/css_rule.zig
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const CSSStyleSheet = @import("css_stylesheet.zig").CSSStyleSheet;
|
||||
|
||||
pub const Interfaces = .{
|
||||
CSSRule,
|
||||
CSSImportRule,
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CSSRule
|
||||
pub const CSSRule = struct {
|
||||
css_text: []const u8,
|
||||
parent_rule: ?*CSSRule = null,
|
||||
parent_stylesheet: ?*CSSStyleSheet = null,
|
||||
};
|
||||
|
||||
pub const CSSImportRule = struct {
|
||||
pub const prototype = *CSSRule;
|
||||
href: []const u8,
|
||||
layer_name: ?[]const u8,
|
||||
media: void,
|
||||
style_sheet: CSSStyleSheet,
|
||||
supports_text: ?[]const u8,
|
||||
};
|
||||
60
src/browser/cssom/css_rule_list.zig
Normal file
60
src/browser/cssom/css_rule_list.zig
Normal file
@@ -0,0 +1,60 @@
|
||||
// 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 StyleSheet = @import("stylesheet.zig").StyleSheet;
|
||||
const CSSRule = @import("css_rule.zig").CSSRule;
|
||||
const CSSImportRule = @import("css_rule.zig").CSSImportRule;
|
||||
|
||||
pub const CSSRuleList = struct {
|
||||
list: std.ArrayListUnmanaged([]const u8),
|
||||
|
||||
pub fn constructor() CSSRuleList {
|
||||
return .{ .list = .empty };
|
||||
}
|
||||
|
||||
pub fn _item(self: *CSSRuleList, _index: u32) ?CSSRule {
|
||||
const index: usize = @intCast(_index);
|
||||
|
||||
if (index > self.list.items.len) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// todo: for now, just return null.
|
||||
// this depends on properly parsing CSSRule
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn get_length(self: *CSSRuleList) u32 {
|
||||
return @intCast(self.list.items.len);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.CSS.CSSRuleList" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let list = new CSSRuleList()", "undefined" },
|
||||
.{ "list instanceof CSSRuleList", "true" },
|
||||
.{ "list.length", "0" },
|
||||
.{ "list.item(0)", "null" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -20,15 +20,9 @@ const std = @import("std");
|
||||
|
||||
const CSSParser = @import("./css_parser.zig").CSSParser;
|
||||
const CSSValueAnalyzer = @import("./css_value_analyzer.zig").CSSValueAnalyzer;
|
||||
const CSSRule = @import("css_rule.zig").CSSRule;
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
pub const Interfaces = .{
|
||||
CSSStyleDeclaration,
|
||||
CSSRule,
|
||||
};
|
||||
|
||||
const CSSRule = struct {};
|
||||
|
||||
pub const CSSStyleDeclaration = struct {
|
||||
store: std.StringHashMapUnmanaged(Property),
|
||||
order: std.ArrayListUnmanaged([]const u8),
|
||||
@@ -85,7 +79,7 @@ pub const CSSStyleDeclaration = struct {
|
||||
return self.order.items.len;
|
||||
}
|
||||
|
||||
pub fn get_parentRule() ?CSSRule {
|
||||
pub fn get_parentRule(_: *const CSSStyleDeclaration) ?CSSRule {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
91
src/browser/cssom/css_stylesheet.zig
Normal file
91
src/browser/cssom/css_stylesheet.zig
Normal file
@@ -0,0 +1,91 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const Page = @import("../page.zig").Page;
|
||||
const StyleSheet = @import("stylesheet.zig").StyleSheet;
|
||||
|
||||
const CSSRuleList = @import("css_rule_list.zig").CSSRuleList;
|
||||
const CSSImportRule = @import("css_rule.zig").CSSImportRule;
|
||||
|
||||
pub const CSSStyleSheet = struct {
|
||||
pub const prototype = *StyleSheet;
|
||||
|
||||
proto: StyleSheet,
|
||||
css_rules: CSSRuleList,
|
||||
owner_rule: ?*CSSImportRule,
|
||||
|
||||
const CSSStyleSheetOpts = struct {
|
||||
base_url: ?[]const u8 = null,
|
||||
// TODO: Suupport media
|
||||
disabled: bool = false,
|
||||
};
|
||||
|
||||
pub fn constructor(_opts: ?CSSStyleSheetOpts) !CSSStyleSheet {
|
||||
const opts = _opts orelse CSSStyleSheetOpts{};
|
||||
return .{
|
||||
.proto = StyleSheet{ .disabled = opts.disabled },
|
||||
.css_rules = .constructor(),
|
||||
.owner_rule = null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_ownerRule(_: *CSSStyleSheet) ?*CSSImportRule {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn get_cssRules(self: *CSSStyleSheet) *CSSRuleList {
|
||||
return &self.css_rules;
|
||||
}
|
||||
|
||||
pub fn _insertRule(self: *CSSStyleSheet, rule: []const u8, _index: ?usize, page: *Page) !usize {
|
||||
const index = _index orelse 0;
|
||||
if (index > self.css_rules.list.items.len) {
|
||||
return error.IndexSize;
|
||||
}
|
||||
|
||||
const arena = page.arena;
|
||||
try self.css_rules.list.insert(arena, index, try arena.dupe(u8, rule));
|
||||
return index;
|
||||
}
|
||||
|
||||
pub fn _deleteRule(self: *CSSStyleSheet, index: usize) !void {
|
||||
if (index > self.css_rules.list.items.len) {
|
||||
return error.IndexSize;
|
||||
}
|
||||
|
||||
_ = self.css_rules.list.orderedRemove(index);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.CSS.StyleSheet" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let css = new CSSStyleSheet()", "undefined" },
|
||||
.{ "css instanceof CSSStyleSheet", "true" },
|
||||
.{ "css.cssRules.length", "0" },
|
||||
.{ "css.ownerRule", "null" },
|
||||
.{ "let index1 = css.insertRule('body { color: red; }', 0)", "undefined" },
|
||||
.{ "index1", "0" },
|
||||
.{ "css.cssRules.length", "1" },
|
||||
}, .{});
|
||||
}
|
||||
30
src/browser/cssom/cssom.zig
Normal file
30
src/browser/cssom/cssom.zig
Normal file
@@ -0,0 +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/>.
|
||||
|
||||
pub const Stylesheet = @import("stylesheet.zig").StyleSheet;
|
||||
pub const CSSStylesheet = @import("css_stylesheet.zig").CSSStyleSheet;
|
||||
pub const CSSStyleDeclaration = @import("css_style_declaration.zig").CSSStyleDeclaration;
|
||||
pub const CSSRuleList = @import("css_rule_list.zig").CSSRuleList;
|
||||
|
||||
pub const Interfaces = .{
|
||||
Stylesheet,
|
||||
CSSStylesheet,
|
||||
CSSStyleDeclaration,
|
||||
CSSRuleList,
|
||||
@import("css_rule.zig").Interfaces,
|
||||
};
|
||||
55
src/browser/cssom/stylesheet.zig
Normal file
55
src/browser/cssom/stylesheet.zig
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/StyleSheet#specifications
|
||||
pub const StyleSheet = struct {
|
||||
disabled: bool = false,
|
||||
href: []const u8 = "",
|
||||
owner_node: ?*parser.Node = null,
|
||||
parent_stylesheet: ?*StyleSheet = null,
|
||||
title: []const u8 = "",
|
||||
type: []const u8 = "text/css",
|
||||
|
||||
pub fn get_disabled(self: *const StyleSheet) bool {
|
||||
return self.disabled;
|
||||
}
|
||||
|
||||
pub fn get_href(self: *const StyleSheet) []const u8 {
|
||||
return self.href;
|
||||
}
|
||||
|
||||
// TODO: media
|
||||
|
||||
pub fn get_ownerNode(self: *const StyleSheet) ?*parser.Node {
|
||||
return self.owner_node;
|
||||
}
|
||||
|
||||
pub fn get_parentStyleSheet(self: *const StyleSheet) ?*StyleSheet {
|
||||
return self.parent_stylesheet;
|
||||
}
|
||||
|
||||
pub fn get_title(self: *const StyleSheet) []const u8 {
|
||||
return self.title;
|
||||
}
|
||||
|
||||
pub fn get_type(self: *const StyleSheet) []const u8 {
|
||||
return self.type;
|
||||
}
|
||||
};
|
||||
126
src/browser/dom/Animation.zig
Normal file
126
src/browser/dom/Animation.zig
Normal file
@@ -0,0 +1,126 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const Page = @import("../page.zig").Page;
|
||||
const JsObject = @import("../env.zig").JsObject;
|
||||
const Promise = @import("../env.zig").Promise;
|
||||
const PromiseResolver = @import("../env.zig").PromiseResolver;
|
||||
|
||||
const Animation = @This();
|
||||
|
||||
effect: ?JsObject,
|
||||
timeline: ?JsObject,
|
||||
ready_resolver: ?PromiseResolver,
|
||||
finished_resolver: ?PromiseResolver,
|
||||
|
||||
pub fn constructor(effect: ?JsObject, timeline: ?JsObject) !Animation {
|
||||
return .{
|
||||
.effect = if (effect) |eo| try eo.persist() else null,
|
||||
.timeline = if (timeline) |to| try to.persist() else null,
|
||||
.ready_resolver = null,
|
||||
.finished_resolver = null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_playState(self: *const Animation) []const u8 {
|
||||
_ = self;
|
||||
return "finished";
|
||||
}
|
||||
|
||||
pub fn get_pending(self: *const Animation) bool {
|
||||
_ = self;
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn get_finished(self: *Animation, page: *Page) !Promise {
|
||||
if (self.finished_resolver == null) {
|
||||
const resolver = page.main_context.createPromiseResolver();
|
||||
try resolver.resolve(self);
|
||||
self.finished_resolver = resolver;
|
||||
}
|
||||
return self.finished_resolver.?.promise();
|
||||
}
|
||||
|
||||
pub fn get_ready(self: *Animation, page: *Page) !Promise {
|
||||
// never resolved, because we're always "finished"
|
||||
if (self.ready_resolver == null) {
|
||||
const resolver = page.main_context.createPromiseResolver();
|
||||
self.ready_resolver = resolver;
|
||||
}
|
||||
return self.ready_resolver.?.promise();
|
||||
}
|
||||
|
||||
pub fn get_effect(self: *const Animation) ?JsObject {
|
||||
return self.effect;
|
||||
}
|
||||
|
||||
pub fn set_effect(self: *Animation, effect: JsObject) !void {
|
||||
self.effect = try effect.persist();
|
||||
}
|
||||
|
||||
pub fn get_timeline(self: *const Animation) ?JsObject {
|
||||
return self.timeline;
|
||||
}
|
||||
|
||||
pub fn set_timeline(self: *Animation, timeline: JsObject) !void {
|
||||
self.timeline = try timeline.persist();
|
||||
}
|
||||
|
||||
pub fn _play(self: *const Animation) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
pub fn _pause(self: *const Animation) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
pub fn _cancel(self: *const Animation) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
pub fn _finish(self: *const Animation) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
pub fn _reverse(self: *const Animation) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.Animation" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "" });
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let a1 = document.createElement('div').animate(null, null)", null },
|
||||
.{ "a1.playState", "finished" },
|
||||
.{ "let cb = [];", null },
|
||||
.{ "a1.ready.then(() => { cb.push('ready') })", null },
|
||||
.{
|
||||
\\ a1.finished.then((x) => {
|
||||
\\ cb.push('finished');
|
||||
\\ cb.push(x == a1);
|
||||
\\ })
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "cb", "finished,true" },
|
||||
}, .{});
|
||||
}
|
||||
361
src/browser/dom/MessageChannel.zig
Normal file
361
src/browser/dom/MessageChannel.zig
Normal file
@@ -0,0 +1,361 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const EventHandler = @import("../events/event.zig").EventHandler;
|
||||
|
||||
const JsObject = Env.JsObject;
|
||||
const Function = Env.Function;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const MAX_QUEUE_SIZE = 10;
|
||||
|
||||
pub const Interfaces = .{ MessageChannel, MessagePort };
|
||||
|
||||
const MessageChannel = @This();
|
||||
|
||||
port1: *MessagePort,
|
||||
port2: *MessagePort,
|
||||
|
||||
pub fn constructor(page: *Page) !MessageChannel {
|
||||
// Why do we allocate this rather than storing directly in the struct?
|
||||
// https://github.com/lightpanda-io/project/discussions/165
|
||||
const port1 = try page.arena.create(MessagePort);
|
||||
const port2 = try page.arena.create(MessagePort);
|
||||
port1.* = .{
|
||||
.pair = port2,
|
||||
};
|
||||
port2.* = .{
|
||||
.pair = port1,
|
||||
};
|
||||
|
||||
return .{
|
||||
.port1 = port1,
|
||||
.port2 = port2,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_port1(self: *const MessageChannel) *MessagePort {
|
||||
return self.port1;
|
||||
}
|
||||
|
||||
pub fn get_port2(self: *const MessageChannel) *MessagePort {
|
||||
return self.port2;
|
||||
}
|
||||
|
||||
pub const MessagePort = struct {
|
||||
pub const prototype = *EventTarget;
|
||||
|
||||
proto: parser.EventTargetTBase = .{ .internal_target_type = .message_port },
|
||||
|
||||
pair: *MessagePort,
|
||||
closed: bool = false,
|
||||
started: bool = false,
|
||||
onmessage_cbk: ?Function = null,
|
||||
onmessageerror_cbk: ?Function = null,
|
||||
// This is the queue of messages to dispatch to THIS MessagePort when the
|
||||
// MessagePort is started.
|
||||
queue: std.ArrayListUnmanaged(JsObject) = .empty,
|
||||
|
||||
pub const PostMessageOption = union(enum) {
|
||||
transfer: JsObject,
|
||||
options: Opts,
|
||||
|
||||
pub const Opts = struct {
|
||||
transfer: JsObject,
|
||||
};
|
||||
};
|
||||
|
||||
pub fn _postMessage(self: *MessagePort, obj: JsObject, opts_: ?PostMessageOption, page: *Page) !void {
|
||||
if (self.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts_ != null) {
|
||||
log.warn(.web_api, "not implemented", .{ .feature = "MessagePort postMessage options" });
|
||||
return error.NotImplemented;
|
||||
}
|
||||
|
||||
try self.pair.dispatchOrQueue(obj, page.arena);
|
||||
}
|
||||
|
||||
// Start impacts the ability to receive a message.
|
||||
// Given pair1 (started) and pair2 (not started), then:
|
||||
// pair2.postMessage('x'); //will be dispatched to pair1.onmessage
|
||||
// pair1.postMessage('x'); // will be queued until pair2 is started
|
||||
pub fn _start(self: *MessagePort) !void {
|
||||
if (self.started) {
|
||||
return;
|
||||
}
|
||||
self.started = true;
|
||||
for (self.queue.items) |data| {
|
||||
try self.dispatch(data);
|
||||
}
|
||||
// we'll never use this queue again, but it's allocated with an arena
|
||||
// we don't even need to clear it, but it seems a bit safer to do at
|
||||
// least that
|
||||
self.queue.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
// Closing seems to stop both the publishing and receiving of messages,
|
||||
// effectively rendering the channel useless. It cannot be reversed.
|
||||
pub fn _close(self: *MessagePort) void {
|
||||
self.closed = true;
|
||||
self.pair.closed = true;
|
||||
}
|
||||
|
||||
pub fn get_onmessage(self: *MessagePort) ?Function {
|
||||
return self.onmessage_cbk;
|
||||
}
|
||||
pub fn get_onmessageerror(self: *MessagePort) ?Function {
|
||||
return self.onmessageerror_cbk;
|
||||
}
|
||||
|
||||
pub fn set_onmessage(self: *MessagePort, listener: EventHandler.Listener, page: *Page) !void {
|
||||
if (self.onmessage_cbk) |cbk| {
|
||||
try self.unregister("message", cbk.id);
|
||||
}
|
||||
self.onmessage_cbk = try self.register(page.arena, "message", listener);
|
||||
|
||||
// When onmessage is set directly, then it's like start() was called.
|
||||
// If addEventListener('message') is used, the app has to call start()
|
||||
// explicitly.
|
||||
try self._start();
|
||||
}
|
||||
|
||||
pub fn set_onmessageerror(self: *MessagePort, listener: EventHandler.Listener, page: *Page) !void {
|
||||
if (self.onmessageerror_cbk) |cbk| {
|
||||
try self.unregister("messageerror", cbk.id);
|
||||
}
|
||||
self.onmessageerror_cbk = try self.register(page.arena, "messageerror", listener);
|
||||
}
|
||||
|
||||
// called from our pair. If port1.postMessage("x") is called, then this
|
||||
// will be called on port2.
|
||||
fn dispatchOrQueue(self: *MessagePort, obj: JsObject, arena: Allocator) !void {
|
||||
// our pair should have checked this already
|
||||
std.debug.assert(self.closed == false);
|
||||
|
||||
if (self.started) {
|
||||
return self.dispatch(try obj.persist());
|
||||
}
|
||||
|
||||
if (self.queue.items.len > MAX_QUEUE_SIZE) {
|
||||
// This isn't part of the spec, but not putting a limit is reckless
|
||||
return error.MessageQueueLimit;
|
||||
}
|
||||
return self.queue.append(arena, try obj.persist());
|
||||
}
|
||||
|
||||
fn dispatch(self: *MessagePort, obj: JsObject) !void {
|
||||
// obj is already persisted, don't use `MessageEvent.constructor`, but
|
||||
// go directly to `init`, which assumes persisted objects.
|
||||
var evt = try MessageEvent.init(.{ .data = obj });
|
||||
_ = try parser.eventTargetDispatchEvent(
|
||||
parser.toEventTarget(MessagePort, self),
|
||||
@as(*parser.Event, @ptrCast(&evt)),
|
||||
);
|
||||
}
|
||||
|
||||
fn register(
|
||||
self: *MessagePort,
|
||||
alloc: Allocator,
|
||||
typ: []const u8,
|
||||
listener: EventHandler.Listener,
|
||||
) !?Function {
|
||||
const target = @as(*parser.EventTarget, @ptrCast(self));
|
||||
const eh = (try EventHandler.register(alloc, target, typ, listener, null)) orelse unreachable;
|
||||
return eh.callback;
|
||||
}
|
||||
|
||||
fn unregister(self: *MessagePort, typ: []const u8, cbk_id: usize) !void {
|
||||
const et = @as(*parser.EventTarget, @ptrCast(self));
|
||||
const lst = try parser.eventTargetHasListener(et, typ, false, cbk_id);
|
||||
if (lst == null) {
|
||||
return;
|
||||
}
|
||||
try parser.eventTargetRemoveEventListener(et, typ, lst.?, false);
|
||||
}
|
||||
};
|
||||
|
||||
pub const MessageEvent = struct {
|
||||
const Event = @import("../events/event.zig").Event;
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
|
||||
pub const prototype = *Event;
|
||||
pub const Exception = DOMException;
|
||||
pub const union_make_copy = true;
|
||||
|
||||
proto: parser.Event,
|
||||
data: ?JsObject,
|
||||
|
||||
// You would think if port1 sends to port2, the source would be port2
|
||||
// (which is how I read the documentation), but it appears to always be
|
||||
// null. It can always be set explicitly via the constructor;
|
||||
source: ?JsObject,
|
||||
|
||||
origin: []const u8,
|
||||
|
||||
// This is used for Server-Sent events. Appears to always be an empty
|
||||
// string for MessagePort messages.
|
||||
last_event_id: []const u8,
|
||||
|
||||
// This might be related to the "transfer" option of postMessage which
|
||||
// we don't yet support. For "normal" message, it's always an empty array.
|
||||
// Though it could be set explicitly via the constructor
|
||||
ports: []*MessagePort,
|
||||
|
||||
const Options = struct {
|
||||
data: ?JsObject = null,
|
||||
source: ?JsObject = null,
|
||||
origin: []const u8 = "",
|
||||
lastEventId: []const u8 = "",
|
||||
ports: []*MessagePort = &.{},
|
||||
};
|
||||
|
||||
pub fn constructor(opts: Options) !MessageEvent {
|
||||
return init(.{
|
||||
.data = if (opts.data) |obj| try obj.persist() else null,
|
||||
.source = if (opts.source) |obj| try obj.persist() else null,
|
||||
.ports = opts.ports,
|
||||
.origin = opts.origin,
|
||||
.lastEventId = opts.lastEventId,
|
||||
});
|
||||
}
|
||||
|
||||
// This is like "constructor", but it assumes JsObjects have already been
|
||||
// persisted. Necessary because this `new MessageEvent()` can be called
|
||||
// directly from JS OR from a port.postMessage. In the latter case, data
|
||||
// may have already been persisted (as it might need to be queued);
|
||||
fn init(opts: Options) !MessageEvent {
|
||||
const event = try parser.eventCreate();
|
||||
defer parser.eventDestroy(event);
|
||||
try parser.eventInit(event, "message", .{});
|
||||
try parser.eventSetInternalType(event, .message_event);
|
||||
|
||||
return .{
|
||||
.proto = event.*,
|
||||
.data = opts.data,
|
||||
.source = opts.source,
|
||||
.ports = opts.ports,
|
||||
.origin = opts.origin,
|
||||
.last_event_id = opts.lastEventId,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_data(self: *const MessageEvent) !?JsObject {
|
||||
return self.data;
|
||||
}
|
||||
|
||||
pub fn get_origin(self: *const MessageEvent) []const u8 {
|
||||
return self.origin;
|
||||
}
|
||||
|
||||
pub fn get_source(self: *const MessageEvent) ?JsObject {
|
||||
return self.source;
|
||||
}
|
||||
|
||||
pub fn get_ports(self: *const MessageEvent) []*MessagePort {
|
||||
return self.ports;
|
||||
}
|
||||
|
||||
pub fn get_lastEventId(self: *const MessageEvent) []const u8 {
|
||||
return self.last_event_id;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.MessageChannel" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{
|
||||
.html = "",
|
||||
});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const mc1 = new MessageChannel()", null },
|
||||
.{ "mc1.port1 == mc1.port1", "true" },
|
||||
.{ "mc1.port2 == mc1.port2", "true" },
|
||||
.{ "mc1.port1 != mc1.port2", "true" },
|
||||
.{ "mc1.port1.postMessage('msg1');", "undefined" },
|
||||
.{
|
||||
\\ let message = null;
|
||||
\\ let target = null;
|
||||
\\ let currentTarget = null;
|
||||
\\ mc1.port2.onmessage = (e) => {
|
||||
\\ message = e.data;
|
||||
\\ target = e.target;
|
||||
\\ currentTarget = e.currentTarget;
|
||||
\\ };
|
||||
,
|
||||
null,
|
||||
},
|
||||
// as soon as onmessage is called, queued messages are delivered
|
||||
.{ "message", "msg1" },
|
||||
.{ "target == mc1.port2", "true" },
|
||||
.{ "currentTarget == mc1.port2", "true" },
|
||||
|
||||
.{ "mc1.port1.postMessage('msg2');", "undefined" },
|
||||
.{ "message", "msg2" },
|
||||
.{ "target == mc1.port2", "true" },
|
||||
.{ "currentTarget == mc1.port2", "true" },
|
||||
|
||||
.{ "message = null", null },
|
||||
.{ "mc1.port1.close();", null },
|
||||
.{ "mc1.port1.postMessage('msg3');", "undefined" },
|
||||
.{ "message", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const mc2 = new MessageChannel()", null },
|
||||
.{ "mc2.port2.postMessage('msg1');", "undefined" },
|
||||
.{ "mc2.port1.postMessage('msg2');", "undefined" },
|
||||
.{
|
||||
\\ let message1 = null;
|
||||
\\ mc2.port1.addEventListener('message', (e) => {
|
||||
\\ message1 = e.data;
|
||||
\\ });
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{
|
||||
\\ let message2 = null;
|
||||
\\ mc2.port2.addEventListener('message', (e) => {
|
||||
\\ message2 = e.data;
|
||||
\\ });
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "message1", "null" },
|
||||
.{ "message2", "null" },
|
||||
.{ "mc2.port2.start()", null },
|
||||
|
||||
.{ "message1", "null" },
|
||||
.{ "message2", "msg2" },
|
||||
.{ "message2 = null", null },
|
||||
|
||||
.{ "mc2.port1.start()", null },
|
||||
.{ "message1", "msg1" },
|
||||
.{ "message2", "null" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -15,7 +15,6 @@
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const Node = @import("node.zig").Node;
|
||||
@@ -47,7 +46,14 @@ pub const Attr = struct {
|
||||
}
|
||||
|
||||
pub fn set_value(self: *parser.Attribute, v: []const u8) !?[]const u8 {
|
||||
if (try parser.attributeGetOwnerElement(self)) |el| {
|
||||
// if possible, go through the element, as that triggers a
|
||||
// DOMAttrModified event (which MutationObserver cares about)
|
||||
const name = try parser.attributeGetName(self);
|
||||
try parser.elementSetAttribute(el, name, v);
|
||||
} else {
|
||||
try parser.attributeSetValue(self, v);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
@@ -31,6 +32,9 @@ const css = @import("css.zig");
|
||||
const Element = @import("element.zig").Element;
|
||||
const ElementUnion = @import("element.zig").Union;
|
||||
const TreeWalker = @import("tree_walker.zig").TreeWalker;
|
||||
const CSSStyleSheet = @import("../cssom/css_stylesheet.zig").CSSStyleSheet;
|
||||
const NodeIterator = @import("node_iterator.zig").NodeIterator;
|
||||
const Range = @import("range.zig").Range;
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
|
||||
@@ -121,8 +125,10 @@ pub const Document = struct {
|
||||
}
|
||||
|
||||
pub fn _createElement(self: *parser.Document, tag_name: []const u8) !ElementUnion {
|
||||
const e = try parser.documentCreateElement(self, tag_name);
|
||||
return try Element.toInterface(e);
|
||||
// The element’s namespace is the HTML namespace when document is an HTML document
|
||||
// https://dom.spec.whatwg.org/#ref-for-dom-document-createelement%E2%91%A0
|
||||
const e = try parser.documentCreateElementNS(self, "http://www.w3.org/1999/xhtml", tag_name);
|
||||
return Element.toInterface(e);
|
||||
}
|
||||
|
||||
pub fn _createElementNS(self: *parser.Document, ns: []const u8, tag_name: []const u8) !ElementUnion {
|
||||
@@ -216,7 +222,7 @@ pub const Document = struct {
|
||||
pub fn _querySelector(self: *parser.Document, selector: []const u8, page: *Page) !?ElementUnion {
|
||||
if (selector.len == 0) return null;
|
||||
|
||||
const n = try css.querySelector(page.arena, parser.documentToNode(self), selector);
|
||||
const n = try css.querySelector(page.call_arena, parser.documentToNode(self), selector);
|
||||
|
||||
if (n == null) return null;
|
||||
|
||||
@@ -243,6 +249,10 @@ pub const Document = struct {
|
||||
return try TreeWalker.init(root, what_to_show, filter);
|
||||
}
|
||||
|
||||
pub fn _createNodeIterator(_: *parser.Document, root: *parser.Node, what_to_show: ?u32, filter: ?NodeIterator.NodeIteratorOpts) !NodeIterator {
|
||||
return try NodeIterator.init(root, what_to_show, filter);
|
||||
}
|
||||
|
||||
pub fn getActiveElement(self: *parser.Document, page: *Page) !?*parser.Element {
|
||||
if (page.getNodeState(@alignCast(@ptrCast(self)))) |state| {
|
||||
if (state.active_element) |ae| {
|
||||
@@ -270,6 +280,15 @@ pub const Document = struct {
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
state.active_element = @ptrCast(e);
|
||||
}
|
||||
|
||||
pub fn _createRange(_: *parser.Document, page: *Page) Range {
|
||||
return Range.constructor(page);
|
||||
}
|
||||
|
||||
// TODO: dummy implementation
|
||||
pub fn get_styleSheets(_: *parser.Document) []CSSStyleSheet {
|
||||
return &.{};
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
@@ -438,6 +457,9 @@ test "Browser.DOM.Document" {
|
||||
,
|
||||
"1",
|
||||
},
|
||||
|
||||
.{ "document.querySelectorAll('.\\\\:popover-open').length", "0" },
|
||||
.{ "document.querySelectorAll('.foo\\\\:bar').length", "0" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
@@ -446,6 +468,10 @@ test "Browser.DOM.Document" {
|
||||
.{ "document.activeElement === document.getElementById('link')", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.styleSheets.length", "0" },
|
||||
}, .{});
|
||||
|
||||
// this test breaks the doc structure, keep it at the end of the test
|
||||
// suite.
|
||||
try runner.testCases(&.{
|
||||
|
||||
@@ -16,8 +16,13 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const css = @import("css.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
const NodeList = @import("nodelist.zig").NodeList;
|
||||
const Element = @import("element.zig").Element;
|
||||
const ElementUnion = @import("element.zig").Union;
|
||||
const collection = @import("html_collection.zig");
|
||||
|
||||
const Node = @import("node.zig").Node;
|
||||
|
||||
@@ -53,6 +58,29 @@ pub const DocumentFragment = struct {
|
||||
pub fn _replaceChildren(self: *parser.DocumentFragment, nodes: []const Node.NodeOrText) !void {
|
||||
return Node.replaceChildren(parser.documentFragmentToNode(self), nodes);
|
||||
}
|
||||
|
||||
pub fn _querySelector(self: *parser.DocumentFragment, selector: []const u8, page: *Page) !?ElementUnion {
|
||||
if (selector.len == 0) return null;
|
||||
|
||||
const n = try css.querySelector(page.call_arena, parser.documentFragmentToNode(self), selector);
|
||||
|
||||
if (n == null) return null;
|
||||
|
||||
return try Element.toInterface(parser.nodeToElement(n.?));
|
||||
}
|
||||
|
||||
pub fn _querySelectorAll(self: *parser.DocumentFragment, selector: []const u8, page: *Page) !NodeList {
|
||||
return css.querySelectorAll(page.arena, parser.documentFragmentToNode(self), selector);
|
||||
}
|
||||
|
||||
pub fn get_childElementCount(self: *parser.DocumentFragment) !u32 {
|
||||
var children = try get_children(self);
|
||||
return children.get_length();
|
||||
}
|
||||
|
||||
pub fn get_children(self: *parser.DocumentFragment) !collection.HTMLCollection {
|
||||
return collection.HTMLCollectionChildren(parser.documentFragmentToNode(self), false);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
@@ -71,4 +99,26 @@ test "Browser.DOM.DocumentFragment" {
|
||||
.{ "dc1.isEqualNode(dc1)", "true" },
|
||||
.{ "dc1.isEqualNode(dc2)", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let f = document.createDocumentFragment()", null },
|
||||
.{ "let d = document.createElement('div');", null },
|
||||
.{ "d.childElementCount", "0" },
|
||||
|
||||
.{ "d.id = 'x';", null },
|
||||
.{ "document.getElementById('x') == null;", "true" },
|
||||
.{ "f.append(d);", null },
|
||||
.{ "f.childElementCount", "1" },
|
||||
.{ "f.children[0].id", "x" },
|
||||
.{ "document.getElementById('x') == null;", "true" },
|
||||
|
||||
.{ "document.getElementsByTagName('body')[0].append(f.cloneNode(true));", null },
|
||||
.{ "document.getElementById('x') != null;", "true" },
|
||||
|
||||
.{ "document.querySelector('.hello')", "null" },
|
||||
.{ "document.querySelectorAll('.hello').length", "0" },
|
||||
|
||||
.{ "document.querySelector('#x').id", "x" },
|
||||
.{ "document.querySelectorAll('#x')[0].id", "x" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -23,11 +23,14 @@ const NamedNodeMap = @import("namednodemap.zig").NamedNodeMap;
|
||||
const DOMTokenList = @import("token_list.zig");
|
||||
const NodeList = @import("nodelist.zig");
|
||||
const Node = @import("node.zig");
|
||||
const ResizeObserver = @import("resize_observer.zig");
|
||||
const MutationObserver = @import("mutation_observer.zig");
|
||||
const IntersectionObserver = @import("intersection_observer.zig");
|
||||
const DOMParser = @import("dom_parser.zig").DOMParser;
|
||||
const TreeWalker = @import("tree_walker.zig").TreeWalker;
|
||||
const NodeIterator = @import("node_iterator.zig").NodeIterator;
|
||||
const NodeFilter = @import("node_filter.zig").NodeFilter;
|
||||
const PerformanceObserver = @import("performance_observer.zig").PerformanceObserver;
|
||||
|
||||
pub const Interfaces = .{
|
||||
DOMException,
|
||||
@@ -39,9 +42,16 @@ pub const Interfaces = .{
|
||||
NodeList.Interfaces,
|
||||
Node.Node,
|
||||
Node.Interfaces,
|
||||
ResizeObserver.Interfaces,
|
||||
MutationObserver.Interfaces,
|
||||
IntersectionObserver.Interfaces,
|
||||
DOMParser,
|
||||
TreeWalker,
|
||||
NodeIterator,
|
||||
NodeFilter,
|
||||
@import("performance.zig").Interfaces,
|
||||
PerformanceObserver,
|
||||
@import("range.zig").Interfaces,
|
||||
@import("Animation.zig"),
|
||||
@import("MessageChannel.zig").Interfaces,
|
||||
};
|
||||
|
||||
@@ -30,6 +30,11 @@ const Node = @import("node.zig").Node;
|
||||
const Walker = @import("walker.zig").WalkerDepthFirst;
|
||||
const NodeList = @import("nodelist.zig").NodeList;
|
||||
const HTMLElem = @import("../html/elements.zig");
|
||||
const ShadowRoot = @import("../dom/shadow_root.zig").ShadowRoot;
|
||||
|
||||
const Animation = @import("Animation.zig");
|
||||
const JsObject = @import("../env.zig").JsObject;
|
||||
|
||||
pub const Union = @import("../html/elements.zig").Union;
|
||||
|
||||
// WEB IDL https://dom.spec.whatwg.org/#element
|
||||
@@ -43,6 +48,10 @@ pub const Element = struct {
|
||||
y: f64,
|
||||
width: f64,
|
||||
height: f64,
|
||||
bottom: f64,
|
||||
right: f64,
|
||||
top: f64,
|
||||
left: f64,
|
||||
};
|
||||
|
||||
pub fn toInterface(e: *parser.Element) !Union {
|
||||
@@ -104,13 +113,13 @@ pub const Element = struct {
|
||||
|
||||
pub fn get_innerHTML(self: *parser.Element, page: *Page) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).init(page.arena);
|
||||
try dump.writeChildren(parser.elementToNode(self), buf.writer());
|
||||
try dump.writeChildren(parser.elementToNode(self), .{}, buf.writer());
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
pub fn get_outerHTML(self: *parser.Element, page: *Page) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).init(page.arena);
|
||||
try dump.writeNode(parser.elementToNode(self), buf.writer());
|
||||
try dump.writeNode(parser.elementToNode(self), .{}, buf.writer());
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
@@ -123,19 +132,43 @@ pub const Element = struct {
|
||||
// remove existing children
|
||||
try Node.removeChildren(node);
|
||||
|
||||
// get fragment body children
|
||||
const children = try parser.documentFragmentBodyChildren(fragment) orelse return;
|
||||
|
||||
// append children to the node
|
||||
// I'm not sure what the exact behavior is supposed to be. Initially,
|
||||
// we were only copying the body of the document fragment. But it seems
|
||||
// like head elements should be copied too. Specifically, some sites
|
||||
// create script tags via innerHTML, which we need to capture.
|
||||
// If you play with this in a browser, you should notice that the
|
||||
// behavior is different depending on whether you're in a blank page
|
||||
// or an actual document. In a blank page, something like:
|
||||
// x.innerHTML = '<script></script>';
|
||||
// does _not_ create an empty script, but in a real page, it does. Weird.
|
||||
const fragment_node = parser.documentFragmentToNode(fragment);
|
||||
const html = try parser.nodeFirstChild(fragment_node) orelse return;
|
||||
const head = try parser.nodeFirstChild(html) orelse return;
|
||||
{
|
||||
// First, copy some of the head element
|
||||
const children = try parser.nodeGetChildNodes(head);
|
||||
const ln = try parser.nodeListLength(children);
|
||||
for (0..ln) |_| {
|
||||
// always index 0, because ndoeAppendChild moves the node out of
|
||||
// always index 0, because nodeAppendChild moves the node out of
|
||||
// the nodeList and into the new tree
|
||||
const child = try parser.nodeListItem(children, 0) orelse continue;
|
||||
_ = try parser.nodeAppendChild(node, child);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const body = try parser.nodeNextSibling(head) orelse return;
|
||||
const children = try parser.nodeGetChildNodes(body);
|
||||
const ln = try parser.nodeListLength(children);
|
||||
for (0..ln) |_| {
|
||||
// always index 0, because nodeAppendChild moves the node out of
|
||||
// the nodeList and into the new tree
|
||||
const child = try parser.nodeListItem(children, 0) orelse continue;
|
||||
_ = try parser.nodeAppendChild(node, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The closest() method of the Element interface traverses the element and its parents (heading toward the document root) until it finds a node that matches the specified CSS selector.
|
||||
// Returns the closest ancestor Element or itself, which matches the selectors. If there are no such element, null.
|
||||
pub fn _closest(self: *parser.Element, selector: []const u8, page: *Page) !?*parser.Element {
|
||||
@@ -156,8 +189,14 @@ pub const Element = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// don't use parser.nodeHasAttributes(...) because that returns true/false
|
||||
// based on the type, e.g. a node never as attributes, an element always has
|
||||
// attributes. But, Element.hasAttributes is supposed to return true only
|
||||
// if the element has at least 1 attribute.
|
||||
pub fn _hasAttributes(self: *parser.Element) !bool {
|
||||
return try parser.nodeHasAttributes(parser.elementToNode(self));
|
||||
// an element _must_ have at least an empty attribute
|
||||
const node_map = try parser.nodeGetAttributes(parser.elementToNode(self)) orelse unreachable;
|
||||
return try parser.namedNodeMapGetLength(node_map) > 0;
|
||||
}
|
||||
|
||||
pub fn _getAttribute(self: *parser.Element, qname: []const u8) !?[]const u8 {
|
||||
@@ -331,7 +370,7 @@ pub const Element = struct {
|
||||
pub fn _querySelector(self: *parser.Element, selector: []const u8, page: *Page) !?Union {
|
||||
if (selector.len == 0) return null;
|
||||
|
||||
const n = try css.querySelector(page.arena, parser.elementToNode(self), selector);
|
||||
const n = try css.querySelector(page.call_arena, parser.elementToNode(self), selector);
|
||||
|
||||
if (n == null) return null;
|
||||
|
||||
@@ -369,7 +408,16 @@ pub const Element = struct {
|
||||
pub fn _getBoundingClientRect(self: *parser.Element, page: *Page) !DOMRect {
|
||||
// Since we are lazy rendering we need to do this check. We could store the renderer in a viewport such that it could cache these, but it would require tracking changes.
|
||||
if (!try page.isNodeAttached(parser.elementToNode(self))) {
|
||||
return DOMRect{ .x = 0, .y = 0, .width = 0, .height = 0 };
|
||||
return DOMRect{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.width = 0,
|
||||
.height = 0,
|
||||
.bottom = 0,
|
||||
.right = 0,
|
||||
.top = 0,
|
||||
.left = 0,
|
||||
};
|
||||
}
|
||||
return page.renderer.getRect(self);
|
||||
}
|
||||
@@ -416,6 +464,60 @@ pub const Element = struct {
|
||||
_ = opts;
|
||||
return true;
|
||||
}
|
||||
|
||||
const AttachShadowOpts = struct {
|
||||
mode: []const u8, // must be specified
|
||||
};
|
||||
pub fn _attachShadow(self: *parser.Element, opts: AttachShadowOpts, page: *Page) !*ShadowRoot {
|
||||
const mode = std.meta.stringToEnum(ShadowRoot.Mode, opts.mode) orelse return error.InvalidArgument;
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
if (state.shadow_root) |sr| {
|
||||
if (mode != sr.mode) {
|
||||
// this is the behavior per the spec
|
||||
return error.NotSupportedError;
|
||||
}
|
||||
|
||||
try Node.removeChildren(@alignCast(@ptrCast(sr.proto)));
|
||||
return sr;
|
||||
}
|
||||
|
||||
// Not sure what to do if there is no owner document
|
||||
const doc = try parser.nodeOwnerDocument(@ptrCast(self)) orelse return error.InvalidArgument;
|
||||
const fragment = try parser.documentCreateDocumentFragment(doc);
|
||||
const sr = try page.arena.create(ShadowRoot);
|
||||
sr.* = .{
|
||||
.host = self,
|
||||
.mode = mode,
|
||||
.proto = fragment,
|
||||
};
|
||||
state.shadow_root = sr;
|
||||
return sr;
|
||||
}
|
||||
|
||||
pub fn get_shadowRoot(self: *parser.Element, page: *Page) ?*ShadowRoot {
|
||||
const state = page.getNodeState(@alignCast(@ptrCast(self))) orelse return null;
|
||||
const sr = state.shadow_root orelse return null;
|
||||
if (sr.mode == .closed) {
|
||||
return null;
|
||||
}
|
||||
return sr;
|
||||
}
|
||||
|
||||
pub fn _animate(self: *parser.Element, effect: JsObject, opts: JsObject) !Animation {
|
||||
_ = self;
|
||||
_ = opts;
|
||||
return Animation.constructor(effect, null);
|
||||
}
|
||||
|
||||
pub fn _remove(self: *parser.Element) !void {
|
||||
// TODO: This hasn't been tested to make sure all references to this
|
||||
// node are properly updated. A lot of libdom is lazy and will look
|
||||
// for related elements JIT by walking the tree, but there could be
|
||||
// cases in libdom or the Zig WebAPI where this reference is kept
|
||||
const as_node: *parser.Node = @ptrCast(self);
|
||||
const parent = try parser.nodeParentNode(as_node) orelse return;
|
||||
_ = try Node._removeChild(parent, as_node);
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
@@ -666,4 +768,22 @@ test "Browser.DOM.Element" {
|
||||
.{ "div1.innerHTML = \" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>\"", null },
|
||||
.{ "div1.getElementsByTagName('a').length", "1" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.createElement('a').hasAttributes()", "false" },
|
||||
.{ "var fc; (fc = document.createElement('div')).innerHTML = '<script><\\/script>'", null },
|
||||
.{ "fc.outerHTML", "<div><script></script></div>" },
|
||||
|
||||
.{ "fc; (fc = document.createElement('div')).innerHTML = '<script><\\/script><p>hello</p>'", null },
|
||||
.{ "fc.outerHTML", "<div><script></script><p>hello</p></div>" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const rm = document.createElement('div')", null },
|
||||
.{ "rm.id = 'to-remove'", null },
|
||||
.{ "document.getElementsByTagName('body')[0].appendChild(rm)", null },
|
||||
.{ "document.getElementById('to-remove') != null", "true" },
|
||||
.{ "rm.remove()", "undefined" },
|
||||
.{ "document.getElementById('to-remove') != null", "false" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
@@ -23,30 +24,60 @@ const Page = @import("../page.zig").Page;
|
||||
const EventHandler = @import("../events/event.zig").EventHandler;
|
||||
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
const Nod = @import("node.zig");
|
||||
const nod = @import("node.zig");
|
||||
|
||||
// EventTarget interfaces
|
||||
pub const Union = Nod.Union;
|
||||
pub const Union = union(enum) {
|
||||
node: nod.Union,
|
||||
xhr: *@import("../xhr/xhr.zig").XMLHttpRequest,
|
||||
plain: *parser.EventTarget,
|
||||
message_port: *@import("MessageChannel.zig").MessagePort,
|
||||
};
|
||||
|
||||
// EventTarget implementation
|
||||
pub const EventTarget = struct {
|
||||
pub const Self = parser.EventTarget;
|
||||
pub const Exception = DOMException;
|
||||
|
||||
// Extend libdom event target for pure zig struct.
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .plain },
|
||||
|
||||
pub fn toInterface(et: *parser.EventTarget, page: *Page) !Union {
|
||||
// Not all targets are *parser.Nodes. page.zig emits a "load" event
|
||||
// where the target is a Window, which cannot be cast directly to a node.
|
||||
// Ideally, we'd remove this duality. Failing that, we'll need to embed
|
||||
// data into the *parser.EventTarget should we need this for other types.
|
||||
// For now, for the Window, which is a singleton, we can do this:
|
||||
if (@intFromPtr(et) == @intFromPtr(&page.window.base)) {
|
||||
return .{ .Window = &page.window };
|
||||
// libdom assumes that all event targets are libdom nodes. They are not.
|
||||
|
||||
switch (try parser.eventTargetInternalType(et)) {
|
||||
.libdom_node => {
|
||||
return .{ .node = try nod.Node.toInterface(@as(*parser.Node, @ptrCast(et))) };
|
||||
},
|
||||
.plain => return .{ .plain = et },
|
||||
.abort_signal => {
|
||||
// AbortSignal is a special case, it has its own internal type.
|
||||
// We return it as a node, but we need to handle it differently.
|
||||
return .{ .node = .{ .AbortSignal = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) } };
|
||||
},
|
||||
.window => {
|
||||
// The window is a common non-node target, but it's easy to handle as its a singleton.
|
||||
std.debug.assert(@intFromPtr(et) == @intFromPtr(&page.window.base));
|
||||
return .{ .node = .{ .Window = &page.window } };
|
||||
},
|
||||
.xhr => {
|
||||
const XMLHttpRequestEventTarget = @import("../xhr/event_target.zig").XMLHttpRequestEventTarget;
|
||||
const base: *XMLHttpRequestEventTarget = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et)));
|
||||
return .{ .xhr = @fieldParentPtr("proto", base) };
|
||||
},
|
||||
.message_port => {
|
||||
return .{ .message_port = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) };
|
||||
},
|
||||
else => return error.MissingEventTargetType,
|
||||
}
|
||||
return Nod.Node.toInterface(@as(*parser.Node, @ptrCast(et)));
|
||||
}
|
||||
|
||||
// JS funcs
|
||||
// --------
|
||||
pub fn constructor(page: *Page) !*parser.EventTarget {
|
||||
const et = try page.arena.create(EventTarget);
|
||||
return @ptrCast(&et.base);
|
||||
}
|
||||
|
||||
pub fn _addEventListener(
|
||||
self: *parser.EventTarget,
|
||||
typ: []const u8,
|
||||
@@ -112,6 +143,10 @@ test "Browser.DOM.EventTarget" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "new EventTarget()", "[object EventTarget]" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let content = document.getElementById('content')", "undefined" },
|
||||
.{ "let para = document.getElementById('para')", "undefined" },
|
||||
|
||||
@@ -20,10 +20,11 @@ const std = @import("std");
|
||||
const allocPrint = std.fmt.allocPrint;
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
// https://webidl.spec.whatwg.org/#idl-DOMException
|
||||
pub const DOMException = struct {
|
||||
err: parser.DOMError,
|
||||
err: ?parser.DOMError,
|
||||
str: []const u8,
|
||||
|
||||
pub const ErrorSet = parser.DOMError;
|
||||
@@ -55,6 +56,17 @@ pub const DOMException = struct {
|
||||
pub const _INVALID_NODE_TYPE_ERR = 24;
|
||||
pub const _DATA_CLONE_ERR = 25;
|
||||
|
||||
pub fn constructor(message_: ?[]const u8, name_: ?[]const u8, page: *const Page) !DOMException {
|
||||
const message = message_ orelse "";
|
||||
const err = if (name_) |n| error_from_str(n) else null;
|
||||
const fixed_name = name(err);
|
||||
|
||||
if (message.len == 0) return .{ .err = err, .str = fixed_name };
|
||||
|
||||
const str = try allocPrint(page.arena, "{s}: {s}", .{ fixed_name, message });
|
||||
return .{ .err = err, .str = str };
|
||||
}
|
||||
|
||||
// TODO: deinit
|
||||
pub fn init(alloc: std.mem.Allocator, err: anyerror, callerName: []const u8) !DOMException {
|
||||
const errCast = @as(parser.DOMError, @errorCast(err));
|
||||
@@ -75,14 +87,52 @@ pub const DOMException = struct {
|
||||
return .{ .err = errCast, .str = str };
|
||||
}
|
||||
|
||||
fn name(err: parser.DOMError) []const u8 {
|
||||
fn error_from_str(name_: []const u8) ?parser.DOMError {
|
||||
// @speed: Consider length first, left as is for maintainability, awaiting switch on string support
|
||||
if (std.mem.eql(u8, name_, "IndexSizeError")) return error.IndexSize;
|
||||
if (std.mem.eql(u8, name_, "StringSizeError")) return error.StringSize;
|
||||
if (std.mem.eql(u8, name_, "HierarchyRequestError")) return error.HierarchyRequest;
|
||||
if (std.mem.eql(u8, name_, "WrongDocumentError")) return error.WrongDocument;
|
||||
if (std.mem.eql(u8, name_, "InvalidCharacterError")) return error.InvalidCharacter;
|
||||
if (std.mem.eql(u8, name_, "NoDataAllowedError")) return error.NoDataAllowed;
|
||||
if (std.mem.eql(u8, name_, "NoModificationAllowedError")) return error.NoModificationAllowed;
|
||||
if (std.mem.eql(u8, name_, "NotFoundError")) return error.NotFound;
|
||||
if (std.mem.eql(u8, name_, "NotSupportedError")) return error.NotSupported;
|
||||
if (std.mem.eql(u8, name_, "InuseAttributeError")) return error.InuseAttribute;
|
||||
if (std.mem.eql(u8, name_, "InvalidStateError")) return error.InvalidState;
|
||||
if (std.mem.eql(u8, name_, "SyntaxError")) return error.Syntax;
|
||||
if (std.mem.eql(u8, name_, "InvalidModificationError")) return error.InvalidModification;
|
||||
if (std.mem.eql(u8, name_, "NamespaceError")) return error.Namespace;
|
||||
if (std.mem.eql(u8, name_, "InvalidAccessError")) return error.InvalidAccess;
|
||||
if (std.mem.eql(u8, name_, "ValidationError")) return error.Validation;
|
||||
if (std.mem.eql(u8, name_, "TypeMismatchError")) return error.TypeMismatch;
|
||||
if (std.mem.eql(u8, name_, "SecurityError")) return error.Security;
|
||||
if (std.mem.eql(u8, name_, "NetworkError")) return error.Network;
|
||||
if (std.mem.eql(u8, name_, "AbortError")) return error.Abort;
|
||||
if (std.mem.eql(u8, name_, "URLismatchError")) return error.URLismatch;
|
||||
if (std.mem.eql(u8, name_, "QuotaExceededError")) return error.QuotaExceeded;
|
||||
if (std.mem.eql(u8, name_, "TimeoutError")) return error.Timeout;
|
||||
if (std.mem.eql(u8, name_, "InvalidNodeTypeError")) return error.InvalidNodeType;
|
||||
if (std.mem.eql(u8, name_, "DataCloneError")) return error.DataClone;
|
||||
|
||||
// custom netsurf error
|
||||
if (std.mem.eql(u8, name_, "UnspecifiedEventTypeError")) return error.UnspecifiedEventType;
|
||||
if (std.mem.eql(u8, name_, "DispatchRequestError")) return error.DispatchRequest;
|
||||
if (std.mem.eql(u8, name_, "NoMemoryError")) return error.NoMemory;
|
||||
if (std.mem.eql(u8, name_, "AttributeWrongTypeError")) return error.AttributeWrongType;
|
||||
return null;
|
||||
}
|
||||
|
||||
fn name(err_: ?parser.DOMError) []const u8 {
|
||||
const err = err_ orelse return "Error";
|
||||
|
||||
return switch (err) {
|
||||
error.IndexSize => "IndexSizeError",
|
||||
error.StringSize => "StringSizeError",
|
||||
error.StringSize => "StringSizeError", // Legacy: DOMSTRING_SIZE_ERR
|
||||
error.HierarchyRequest => "HierarchyRequestError",
|
||||
error.WrongDocument => "WrongDocumentError",
|
||||
error.InvalidCharacter => "InvalidCharacterError",
|
||||
error.NoDataAllowed => "NoDataAllowedError",
|
||||
error.NoDataAllowed => "NoDataAllowedError", // Legacy: NO_DATA_ALLOWED_ERR
|
||||
error.NoModificationAllowed => "NoModificationAllowedError",
|
||||
error.NotFound => "NotFoundError",
|
||||
error.NotSupported => "NotSupportedError",
|
||||
@@ -92,7 +142,7 @@ pub const DOMException = struct {
|
||||
error.InvalidModification => "InvalidModificationError",
|
||||
error.Namespace => "NamespaceError",
|
||||
error.InvalidAccess => "InvalidAccessError",
|
||||
error.Validation => "ValidationError",
|
||||
error.Validation => "ValidationError", // Legacy: VALIDATION_ERR
|
||||
error.TypeMismatch => "TypeMismatchError",
|
||||
error.Security => "SecurityError",
|
||||
error.Network => "NetworkError",
|
||||
@@ -115,7 +165,8 @@ pub const DOMException = struct {
|
||||
// JS properties and methods
|
||||
|
||||
pub fn get_code(self: *const DOMException) u8 {
|
||||
return switch (self.err) {
|
||||
const err = self.err orelse return 0;
|
||||
return switch (err) {
|
||||
error.IndexSize => 1,
|
||||
error.StringSize => 2,
|
||||
error.HierarchyRequest => 3,
|
||||
@@ -157,7 +208,8 @@ pub const DOMException = struct {
|
||||
|
||||
pub fn get_message(self: *const DOMException) []const u8 {
|
||||
const errName = DOMException.name(self.err);
|
||||
return self.str[errName.len + 2 ..];
|
||||
if (self.str.len <= errName.len + 2) return "";
|
||||
return self.str[errName.len + 2 ..]; // ! Requires str is formatted as "{name}: {message}"
|
||||
}
|
||||
|
||||
pub fn _toString(self: *const DOMException) []const u8 {
|
||||
@@ -188,4 +240,25 @@ test "Browser.DOM.Exception" {
|
||||
.{ "he instanceof DOMException", "true" },
|
||||
.{ "he instanceof Error", "true" },
|
||||
}, .{});
|
||||
|
||||
// Test DOMException constructor
|
||||
try runner.testCases(&.{
|
||||
.{ "let exc0 = new DOMException()", "undefined" },
|
||||
.{ "exc0.name", "Error" },
|
||||
.{ "exc0.code", "0" },
|
||||
.{ "exc0.message", "" },
|
||||
.{ "exc0.toString()", "Error" },
|
||||
|
||||
.{ "let exc1 = new DOMException('Sandwich malfunction')", "undefined" },
|
||||
.{ "exc1.name", "Error" },
|
||||
.{ "exc1.code", "0" },
|
||||
.{ "exc1.message", "Sandwich malfunction" },
|
||||
.{ "exc1.toString()", "Error: Sandwich malfunction" },
|
||||
|
||||
.{ "let exc2 = new DOMException('Caterpillar turned into a butterfly', 'NoModificationAllowedError')", "undefined" },
|
||||
.{ "exc2.name", "NoModificationAllowedError" },
|
||||
.{ "exc2.code", "7" },
|
||||
.{ "exc2.message", "Caterpillar turned into a butterfly" },
|
||||
.{ "exc2.toString()", "NoModificationAllowedError: Caterpillar turned into a butterfly" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ const Allocator = std.mem.Allocator;
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
const Loop = @import("../../runtime/loop.zig").Loop;
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const NodeList = @import("nodelist.zig").NodeList;
|
||||
@@ -35,25 +36,37 @@ const Walker = @import("../dom/walker.zig").WalkerChildren;
|
||||
|
||||
// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
|
||||
pub const MutationObserver = struct {
|
||||
loop: *Loop,
|
||||
cbk: Env.Function,
|
||||
arena: Allocator,
|
||||
connected: bool,
|
||||
scheduled: bool,
|
||||
loop_node: Loop.CallbackNode,
|
||||
|
||||
// List of records which were observed. When the call scope ends, we need to
|
||||
// execute our callback with it.
|
||||
observed: std.ArrayListUnmanaged(*MutationRecord),
|
||||
observed: std.ArrayListUnmanaged(MutationRecord),
|
||||
|
||||
pub fn constructor(cbk: Env.Function, page: *Page) !MutationObserver {
|
||||
return .{
|
||||
.cbk = cbk,
|
||||
.loop = page.loop,
|
||||
.observed = .{},
|
||||
.connected = true,
|
||||
.scheduled = false,
|
||||
.arena = page.arena,
|
||||
.loop_node = .{ .func = callback },
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _observe(self: *MutationObserver, node: *parser.Node, options_: ?MutationObserverInit) !void {
|
||||
const options = options_ orelse MutationObserverInit{};
|
||||
pub fn _observe(self: *MutationObserver, node: *parser.Node, options_: ?Options) !void {
|
||||
const arena = self.arena;
|
||||
var options = options_ orelse Options{};
|
||||
if (options.attributeFilter.len > 0) {
|
||||
options.attributeFilter = try arena.dupe([]const u8, options.attributeFilter);
|
||||
}
|
||||
|
||||
const observer = try self.arena.create(Observer);
|
||||
const observer = try arena.create(Observer);
|
||||
observer.* = .{
|
||||
.node = node,
|
||||
.options = options,
|
||||
@@ -102,16 +115,21 @@ pub const MutationObserver = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn jsCallScopeEnd(self: *MutationObserver) void {
|
||||
const record = self.observed.items;
|
||||
if (record.len == 0) {
|
||||
fn callback(node: *Loop.CallbackNode, _: *?u63) void {
|
||||
const self: *MutationObserver = @fieldParentPtr("loop_node", node);
|
||||
if (self.connected == false) {
|
||||
self.scheduled = true;
|
||||
return;
|
||||
}
|
||||
self.scheduled = false;
|
||||
|
||||
const records = self.observed.items;
|
||||
if (records.len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
defer self.observed.clearRetainingCapacity();
|
||||
|
||||
for (record) |r| {
|
||||
const records = [_]MutationRecord{r.*};
|
||||
var result: Env.Function.Result = undefined;
|
||||
self.cbk.tryCall(void, .{records}, &result) catch {
|
||||
log.debug(.user_script, "callback error", .{
|
||||
@@ -121,11 +139,10 @@ pub const MutationObserver = struct {
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
pub fn _disconnect(_: *MutationObserver) !void {
|
||||
// TODO unregister listeners.
|
||||
pub fn _disconnect(self: *MutationObserver) !void {
|
||||
self.connected = false;
|
||||
}
|
||||
|
||||
// TODO
|
||||
@@ -182,31 +199,27 @@ pub const MutationRecord = struct {
|
||||
}
|
||||
};
|
||||
|
||||
const MutationObserverInit = struct {
|
||||
const Options = struct {
|
||||
childList: bool = false,
|
||||
attributes: bool = false,
|
||||
characterData: bool = false,
|
||||
subtree: bool = false,
|
||||
attributeOldValue: bool = false,
|
||||
characterDataOldValue: bool = false,
|
||||
// TODO
|
||||
// attributeFilter: [][]const u8,
|
||||
attributeFilter: [][]const u8 = &.{},
|
||||
|
||||
fn attr(self: MutationObserverInit) bool {
|
||||
return self.attributes or self.attributeOldValue;
|
||||
fn attr(self: Options) bool {
|
||||
return self.attributes or self.attributeOldValue or self.attributeFilter.len > 0;
|
||||
}
|
||||
|
||||
fn cdata(self: MutationObserverInit) bool {
|
||||
fn cdata(self: Options) bool {
|
||||
return self.characterData or self.characterDataOldValue;
|
||||
}
|
||||
};
|
||||
|
||||
const Observer = struct {
|
||||
node: *parser.Node,
|
||||
options: MutationObserverInit,
|
||||
|
||||
// record of the mutation, all observed changes in 1 call are batched
|
||||
record: ?MutationRecord = null,
|
||||
options: Options,
|
||||
|
||||
// reference back to the MutationObserver so that we can access the arena
|
||||
// and batch the mutation records.
|
||||
@@ -214,19 +227,34 @@ const Observer = struct {
|
||||
|
||||
event_node: parser.EventNode,
|
||||
|
||||
fn appliesTo(o: *const Observer, target: *parser.Node) bool {
|
||||
fn appliesTo(
|
||||
self: *const Observer,
|
||||
target: *parser.Node,
|
||||
event_type: MutationEventType,
|
||||
event: *parser.MutationEvent,
|
||||
) !bool {
|
||||
if (event_type == .DOMAttrModified and self.options.attributeFilter.len > 0) {
|
||||
const attribute_name = try parser.mutationEventAttributeName(event);
|
||||
for (self.options.attributeFilter) |needle| blk: {
|
||||
if (std.mem.eql(u8, attribute_name, needle)) {
|
||||
break :blk;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// mutation on any target is always ok.
|
||||
if (o.options.subtree) {
|
||||
if (self.options.subtree) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if target equals node, alway ok.
|
||||
if (target == o.node) {
|
||||
if (target == self.node) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// no subtree, no same target and no childlist, always noky.
|
||||
if (!o.options.childList) {
|
||||
if (!self.options.childList) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -234,7 +262,7 @@ const Observer = struct {
|
||||
const walker = Walker{};
|
||||
var next: ?*parser.Node = null;
|
||||
while (true) {
|
||||
next = walker.get_next(o.node, next) catch break orelse break;
|
||||
next = walker.get_next(self.node, next) catch break orelse break;
|
||||
if (next.? == target) {
|
||||
return true;
|
||||
}
|
||||
@@ -258,27 +286,22 @@ const Observer = struct {
|
||||
break :blk parser.eventTargetToNode(event_target);
|
||||
};
|
||||
|
||||
if (self.appliesTo(node) == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mutation_event = parser.eventToMutationEvent(event);
|
||||
const event_type = blk: {
|
||||
const t = try parser.eventType(event);
|
||||
break :blk std.meta.stringToEnum(MutationEventType, t) orelse return;
|
||||
};
|
||||
|
||||
const arena = mutation_observer.arena;
|
||||
if (self.record == null) {
|
||||
self.record = .{
|
||||
if (try self.appliesTo(node, event_type, mutation_event) == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
var record = MutationRecord{
|
||||
.target = self.node,
|
||||
.type = event_type.recordType(),
|
||||
};
|
||||
try mutation_observer.observed.append(arena, &self.record.?);
|
||||
}
|
||||
|
||||
var record = &self.record.?;
|
||||
const mutation_event = parser.eventToMutationEvent(event);
|
||||
|
||||
const arena = mutation_observer.arena;
|
||||
switch (event_type) {
|
||||
.DOMAttrModified => {
|
||||
record.attribute_name = parser.mutationEventAttributeName(mutation_event) catch null;
|
||||
@@ -302,6 +325,13 @@ const Observer = struct {
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
try mutation_observer.observed.append(arena, record);
|
||||
|
||||
if (mutation_observer.scheduled == false) {
|
||||
mutation_observer.scheduled = true;
|
||||
_ = try mutation_observer.loop.timeout(0, &mutation_observer.loop_node);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -341,10 +371,10 @@ test "Browser.DOM.MutationObserver" {
|
||||
\\ document.firstElementChild.setAttribute("foo", "bar");
|
||||
\\ // ignored b/c it's about another target.
|
||||
\\ document.firstElementChild.firstChild.setAttribute("foo", "bar");
|
||||
\\ nb;
|
||||
,
|
||||
"1",
|
||||
null,
|
||||
},
|
||||
.{ "nb", "1" },
|
||||
.{ "mrs[0].type", "attributes" },
|
||||
.{ "mrs[0].target == document.firstElementChild", "true" },
|
||||
.{ "mrs[0].target.getAttribute('foo')", "bar" },
|
||||
@@ -362,10 +392,10 @@ test "Browser.DOM.MutationObserver" {
|
||||
\\ nb2++;
|
||||
\\ }).observe(node, { characterData: true, characterDataOldValue: true });
|
||||
\\ node.data = "foo";
|
||||
\\ nb2;
|
||||
,
|
||||
"1",
|
||||
null,
|
||||
},
|
||||
.{ "nb2", "1" },
|
||||
.{ "mrs2[0].type", "characterData" },
|
||||
.{ "mrs2[0].target == node", "true" },
|
||||
.{ "mrs2[0].target.data", "foo" },
|
||||
@@ -383,7 +413,24 @@ test "Browser.DOM.MutationObserver" {
|
||||
\\ }).observe(document, { subtree:true,childList:true });
|
||||
\\ node.innerText = "2";
|
||||
,
|
||||
"2",
|
||||
null,
|
||||
},
|
||||
.{ "node.innerText", "a" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ var node = document.getElementById("para");
|
||||
\\ var attrWatch = 0;
|
||||
\\ new MutationObserver(() => {
|
||||
\\ attrWatch++;
|
||||
\\ }).observe(document, { attributeFilter: ["name"], subtree: true });
|
||||
\\ node.setAttribute("id", "1");
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "attrWatch", "0" },
|
||||
.{ "node.setAttribute('name', 'other');", null },
|
||||
.{ "attrWatch", "1" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -134,5 +134,7 @@ test "Browser.DOM.NamedNodeMap" {
|
||||
.{ "a['id'].name", "id" },
|
||||
.{ "a['id'].value", "content" },
|
||||
.{ "a['other']", "undefined" },
|
||||
.{ "a[0].value = 'abc123'", null },
|
||||
.{ "a[0].value", "abc123" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -17,11 +17,15 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Node = @import("node.zig").Node;
|
||||
|
||||
pub const NodeFilter = struct {
|
||||
pub const _FILTER_ACCEPT: u16 = 1;
|
||||
pub const _FILTER_REJECT: u16 = 2;
|
||||
pub const _FILTER_SKIP: u16 = 3;
|
||||
|
||||
pub const _SHOW_ALL: u32 = std.math.maxInt(u32);
|
||||
pub const _SHOW_ELEMENT: u32 = 0b1;
|
||||
pub const _SHOW_ATTRIBUTE: u32 = 0b10;
|
||||
@@ -37,6 +41,39 @@ pub const NodeFilter = struct {
|
||||
pub const _SHOW_NOTATION: u32 = 0b100000000000;
|
||||
};
|
||||
|
||||
const VerifyResult = enum { accept, skip, reject };
|
||||
|
||||
pub fn verify(what_to_show: u32, filter: ?Env.Function, node: *parser.Node) !VerifyResult {
|
||||
const node_type = try parser.nodeType(node);
|
||||
|
||||
// Verify that we can show this node type.
|
||||
if (!switch (node_type) {
|
||||
.attribute => what_to_show & NodeFilter._SHOW_ATTRIBUTE != 0,
|
||||
.cdata_section => what_to_show & NodeFilter._SHOW_CDATA_SECTION != 0,
|
||||
.comment => what_to_show & NodeFilter._SHOW_COMMENT != 0,
|
||||
.document => what_to_show & NodeFilter._SHOW_DOCUMENT != 0,
|
||||
.document_fragment => what_to_show & NodeFilter._SHOW_DOCUMENT_FRAGMENT != 0,
|
||||
.document_type => what_to_show & NodeFilter._SHOW_DOCUMENT_TYPE != 0,
|
||||
.element => what_to_show & NodeFilter._SHOW_ELEMENT != 0,
|
||||
.entity => what_to_show & NodeFilter._SHOW_ENTITY != 0,
|
||||
.entity_reference => what_to_show & NodeFilter._SHOW_ENTITY_REFERENCE != 0,
|
||||
.notation => what_to_show & NodeFilter._SHOW_NOTATION != 0,
|
||||
.processing_instruction => what_to_show & NodeFilter._SHOW_PROCESSING_INSTRUCTION != 0,
|
||||
.text => what_to_show & NodeFilter._SHOW_TEXT != 0,
|
||||
}) return .reject;
|
||||
|
||||
// Verify that we aren't filtering it out.
|
||||
if (filter) |f| {
|
||||
const acceptance = try f.call(u16, .{try Node.toInterface(node)});
|
||||
return switch (acceptance) {
|
||||
NodeFilter._FILTER_ACCEPT => .accept,
|
||||
NodeFilter._FILTER_REJECT => .reject,
|
||||
NodeFilter._FILTER_SKIP => .skip,
|
||||
else => .reject,
|
||||
};
|
||||
} else return .accept;
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.NodeFilter" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
|
||||
290
src/browser/dom/node_iterator.zig
Normal file
290
src/browser/dom/node_iterator.zig
Normal file
@@ -0,0 +1,290 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const NodeFilter = @import("node_filter.zig");
|
||||
const Node = @import("node.zig").Node;
|
||||
const NodeUnion = @import("node.zig").Union;
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/NodeIterator
|
||||
// While this is similar to TreeWalker it has its own implementation as there are several subtle differences
|
||||
// For example:
|
||||
// - nextNode returns the reference node, whereas TreeWalker returns the next node
|
||||
// - Skip and reject are equivalent for NodeIterator, for TreeWalker they are different
|
||||
pub const NodeIterator = struct {
|
||||
root: *parser.Node,
|
||||
reference_node: *parser.Node,
|
||||
what_to_show: u32,
|
||||
filter: ?NodeIteratorOpts,
|
||||
filter_func: ?Env.Function,
|
||||
|
||||
pointer_before_current: bool = true,
|
||||
|
||||
pub const NodeIteratorOpts = union(enum) {
|
||||
function: Env.Function,
|
||||
object: struct { acceptNode: Env.Function },
|
||||
};
|
||||
|
||||
pub fn init(node: *parser.Node, what_to_show: ?u32, filter: ?NodeIteratorOpts) !NodeIterator {
|
||||
var filter_func: ?Env.Function = null;
|
||||
if (filter) |f| {
|
||||
filter_func = switch (f) {
|
||||
.function => |func| func,
|
||||
.object => |o| o.acceptNode,
|
||||
};
|
||||
}
|
||||
|
||||
return .{
|
||||
.root = node,
|
||||
.reference_node = node,
|
||||
.what_to_show = what_to_show orelse NodeFilter.NodeFilter._SHOW_ALL,
|
||||
.filter = filter,
|
||||
.filter_func = filter_func,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_filter(self: *const NodeIterator) ?NodeIteratorOpts {
|
||||
return self.filter;
|
||||
}
|
||||
|
||||
pub fn get_pointerBeforeReferenceNode(self: *const NodeIterator) bool {
|
||||
return self.pointer_before_current;
|
||||
}
|
||||
|
||||
pub fn get_referenceNode(self: *const NodeIterator) !NodeUnion {
|
||||
return try Node.toInterface(self.reference_node);
|
||||
}
|
||||
|
||||
pub fn get_root(self: *const NodeIterator) !NodeUnion {
|
||||
return try Node.toInterface(self.root);
|
||||
}
|
||||
|
||||
pub fn get_whatToShow(self: *const NodeIterator) u32 {
|
||||
return self.what_to_show;
|
||||
}
|
||||
|
||||
pub fn _nextNode(self: *NodeIterator) !?NodeUnion {
|
||||
if (self.pointer_before_current) { // Unlike TreeWalker, NodeIterator starts at the first node
|
||||
self.pointer_before_current = false;
|
||||
if (.accept == try NodeFilter.verify(self.what_to_show, self.filter_func, self.reference_node)) {
|
||||
return try Node.toInterface(self.reference_node);
|
||||
}
|
||||
}
|
||||
|
||||
if (try self.firstChild(self.reference_node)) |child| {
|
||||
self.reference_node = child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
|
||||
var current = self.reference_node;
|
||||
while (current != self.root) {
|
||||
if (try self.nextSibling(current)) |sibling| {
|
||||
self.reference_node = sibling;
|
||||
return try Node.toInterface(sibling);
|
||||
}
|
||||
|
||||
current = (try parser.nodeParentNode(current)) orelse break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn _previousNode(self: *NodeIterator) !?NodeUnion {
|
||||
if (!self.pointer_before_current) {
|
||||
self.pointer_before_current = true;
|
||||
if (.accept == try NodeFilter.verify(self.what_to_show, self.filter_func, self.reference_node)) {
|
||||
return try Node.toInterface(self.reference_node); // Still need to verify as last may be first as well
|
||||
}
|
||||
}
|
||||
if (self.reference_node == self.root) return null;
|
||||
|
||||
var current = self.reference_node;
|
||||
while (try parser.nodePreviousSibling(current)) |previous| {
|
||||
current = previous;
|
||||
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||
.accept => {
|
||||
// Get last child if it has one.
|
||||
if (try self.lastChild(current)) |child| {
|
||||
self.reference_node = child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
|
||||
// Otherwise, this node is our previous one.
|
||||
self.reference_node = current;
|
||||
return try Node.toInterface(current);
|
||||
},
|
||||
.reject, .skip => {
|
||||
// Get last child if it has one.
|
||||
if (try self.lastChild(current)) |child| {
|
||||
self.reference_node = child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (current != self.root) {
|
||||
if (try self.parentNode(current)) |parent| {
|
||||
self.reference_node = parent;
|
||||
return try Node.toInterface(parent);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn firstChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
|
||||
const children = try parser.nodeGetChildNodes(node);
|
||||
const child_count = try parser.nodeListLength(children);
|
||||
|
||||
for (0..child_count) |i| {
|
||||
const index: u32 = @intCast(i);
|
||||
const child = (try parser.nodeListItem(children, index)) orelse return null;
|
||||
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
|
||||
.accept => return child, // NOTE: Skip and reject are equivalent for NodeIterator, this is different from TreeWalker
|
||||
.reject, .skip => if (try self.firstChild(child)) |gchild| return gchild,
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn lastChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
|
||||
const children = try parser.nodeGetChildNodes(node);
|
||||
const child_count = try parser.nodeListLength(children);
|
||||
|
||||
var index: u32 = child_count;
|
||||
while (index > 0) {
|
||||
index -= 1;
|
||||
const child = (try parser.nodeListItem(children, index)) orelse return null;
|
||||
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
|
||||
.accept => return child, // NOTE: Skip and reject are equivalent for NodeIterator, this is different from TreeWalker
|
||||
.reject, .skip => if (try self.lastChild(child)) |gchild| return gchild,
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// This implementation is actually the same as :TreeWalker
|
||||
fn parentNode(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
|
||||
if (self.root == node) return null;
|
||||
|
||||
var current = node;
|
||||
while (true) {
|
||||
if (current == self.root) return null;
|
||||
current = (try parser.nodeParentNode(current)) orelse return null;
|
||||
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||
.accept => return current,
|
||||
.reject, .skip => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This implementation is actually the same as :TreeWalker
|
||||
fn nextSibling(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
|
||||
var current = node;
|
||||
|
||||
while (true) {
|
||||
current = (try parser.nodeNextSibling(current)) orelse return null;
|
||||
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||
.accept => return current,
|
||||
.skip, .reject => continue,
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.NodeFilter" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ const nodeIterator = document.createNodeIterator(
|
||||
\\ document.body,
|
||||
\\ NodeFilter.SHOW_ELEMENT,
|
||||
\\ {
|
||||
\\ acceptNode(node) {
|
||||
\\ return NodeFilter.FILTER_ACCEPT;
|
||||
\\ },
|
||||
\\ },
|
||||
\\ );
|
||||
\\ nodeIterator.nextNode().nodeName;
|
||||
,
|
||||
"BODY",
|
||||
},
|
||||
.{ "nodeIterator.nextNode().nodeName", "DIV" },
|
||||
.{ "nodeIterator.nextNode().nodeName", "A" },
|
||||
.{ "nodeIterator.previousNode().nodeName", "A" }, // pointer_before_current flips
|
||||
.{ "nodeIterator.nextNode().nodeName", "A" }, // pointer_before_current flips
|
||||
.{ "nodeIterator.previousNode().nodeName", "A" }, // pointer_before_current flips
|
||||
.{ "nodeIterator.previousNode().nodeName", "DIV" },
|
||||
.{ "nodeIterator.previousNode().nodeName", "BODY" },
|
||||
.{ "nodeIterator.previousNode()", "null" }, // Not HEAD since body is root
|
||||
.{ "nodeIterator.previousNode()", "null" }, // Keeps returning null
|
||||
.{ "nodeIterator.nextNode().nodeName", "BODY" },
|
||||
|
||||
.{ "nodeIterator.nextNode().nodeName", null },
|
||||
.{ "nodeIterator.nextNode().nodeName", null },
|
||||
.{ "nodeIterator.nextNode().nodeName", null },
|
||||
.{ "nodeIterator.nextNode().nodeName", "SPAN" },
|
||||
.{ "nodeIterator.nextNode().nodeName", "P" },
|
||||
.{ "nodeIterator.nextNode()", "null" }, // Just the last one
|
||||
.{ "nodeIterator.nextNode()", "null" }, // Keeps returning null
|
||||
.{ "nodeIterator.previousNode().nodeName", "P" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ const notationIterator = document.createNodeIterator(
|
||||
\\ document.body,
|
||||
\\ NodeFilter.SHOW_NOTATION,
|
||||
\\ );
|
||||
\\ notationIterator.nextNode();
|
||||
,
|
||||
"null",
|
||||
},
|
||||
.{ "notationIterator.previousNode()", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nodeIterator.filter.acceptNode(document.body)", "1" },
|
||||
.{ "notationIterator.filter", "null" },
|
||||
.{
|
||||
\\ const rejectIterator = document.createNodeIterator(
|
||||
\\ document.body,
|
||||
\\ NodeFilter.SHOW_ALL,
|
||||
\\ (e => { return NodeFilter.FILTER_REJECT}),
|
||||
\\ );
|
||||
\\ rejectIterator.filter(document.body);
|
||||
,
|
||||
"2",
|
||||
},
|
||||
}, .{});
|
||||
}
|
||||
214
src/browser/dom/performance.zig
Normal file
214
src/browser/dom/performance.zig
Normal file
@@ -0,0 +1,214 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
pub const Interfaces = .{
|
||||
Performance,
|
||||
PerformanceEntry,
|
||||
PerformanceMark,
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Performance
|
||||
pub const Performance = struct {
|
||||
pub const prototype = *EventTarget;
|
||||
|
||||
// Extend libdom event target for pure zig struct.
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .performance },
|
||||
|
||||
time_origin: std.time.Timer,
|
||||
// if (Window.crossOriginIsolated) -> Resolution in isolated contexts: 5 microseconds
|
||||
// else -> Resolution in non-isolated contexts: 100 microseconds
|
||||
const ms_resolution = 100;
|
||||
|
||||
fn limitedResolutionMs(nanoseconds: u64) f64 {
|
||||
const elapsed_at_resolution = ((nanoseconds / std.time.ns_per_us) + ms_resolution / 2) / ms_resolution * ms_resolution;
|
||||
const elapsed = @as(f64, @floatFromInt(elapsed_at_resolution));
|
||||
return elapsed / @as(f64, std.time.us_per_ms);
|
||||
}
|
||||
|
||||
pub fn get_timeOrigin(self: *const Performance) f64 {
|
||||
const is_posix = switch (@import("builtin").os.tag) { // From std.time.zig L125
|
||||
.windows, .uefi, .wasi => false,
|
||||
else => true,
|
||||
};
|
||||
const zero = std.time.Instant{ .timestamp = if (!is_posix) 0 else .{ .sec = 0, .nsec = 0 } };
|
||||
const started = self.time_origin.started.since(zero);
|
||||
return limitedResolutionMs(started);
|
||||
}
|
||||
|
||||
pub fn _now(self: *Performance) f64 {
|
||||
return limitedResolutionMs(self.time_origin.read());
|
||||
}
|
||||
|
||||
pub fn _mark(_: *Performance, name: []const u8, _options: ?PerformanceMark.Options, page: *Page) !PerformanceMark {
|
||||
const mark: PerformanceMark = try .constructor(name, _options, page);
|
||||
// TODO: Should store this in an entries list
|
||||
return mark;
|
||||
}
|
||||
|
||||
// TODO: fn _mark should record the marks in a lookup
|
||||
pub fn _clearMarks(_: *Performance, name: ?[]const u8) void {
|
||||
_ = name;
|
||||
}
|
||||
|
||||
// TODO: fn _measures should record the marks in a lookup
|
||||
pub fn _clearMeasures(_: *Performance, name: ?[]const u8) void {
|
||||
_ = name;
|
||||
}
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry
|
||||
pub const PerformanceEntry = struct {
|
||||
const PerformanceEntryType = enum {
|
||||
element,
|
||||
event,
|
||||
first_input,
|
||||
largest_contentful_paint,
|
||||
layout_shift,
|
||||
long_animation_frame,
|
||||
longtask,
|
||||
mark,
|
||||
measure,
|
||||
navigation,
|
||||
paint,
|
||||
resource,
|
||||
taskattribution,
|
||||
visibility_state,
|
||||
|
||||
pub fn toString(self: PerformanceEntryType) []const u8 {
|
||||
return switch (self) {
|
||||
.first_input => "first-input",
|
||||
.largest_contentful_paint => "largest-contentful-paint",
|
||||
.layout_shift => "layout-shift",
|
||||
.long_animation_frame => "long-animation-frame",
|
||||
.visibility_state => "visibility-state",
|
||||
else => @tagName(self),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
duration: f64 = 0.0,
|
||||
entry_type: PerformanceEntryType,
|
||||
name: []const u8,
|
||||
start_time: f64 = 0.0,
|
||||
|
||||
pub fn get_duration(self: *const PerformanceEntry) f64 {
|
||||
return self.duration;
|
||||
}
|
||||
|
||||
pub fn get_entryType(self: *const PerformanceEntry) PerformanceEntryType {
|
||||
return self.entry_type;
|
||||
}
|
||||
|
||||
pub fn get_name(self: *const PerformanceEntry) []const u8 {
|
||||
return self.name;
|
||||
}
|
||||
|
||||
pub fn get_startTime(self: *const PerformanceEntry) f64 {
|
||||
return self.start_time;
|
||||
}
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceMark
|
||||
pub const PerformanceMark = struct {
|
||||
pub const prototype = *PerformanceEntry;
|
||||
|
||||
proto: PerformanceEntry,
|
||||
detail: ?Env.JsObject,
|
||||
|
||||
const Options = struct {
|
||||
detail: ?Env.JsObject = null,
|
||||
start_time: ?f64 = null,
|
||||
};
|
||||
|
||||
pub fn constructor(name: []const u8, _options: ?Options, page: *Page) !PerformanceMark {
|
||||
const perf = &page.window.performance;
|
||||
|
||||
const options = _options orelse Options{};
|
||||
const start_time = options.start_time orelse perf._now();
|
||||
|
||||
if (start_time < 0.0) {
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
const detail = if (options.detail) |d| try d.persist() else null;
|
||||
|
||||
const duped_name = try page.arena.dupe(u8, name);
|
||||
const proto = PerformanceEntry{ .name = duped_name, .entry_type = .mark, .start_time = start_time };
|
||||
|
||||
return .{ .proto = proto, .detail = detail };
|
||||
}
|
||||
|
||||
pub fn get_detail(self: *const PerformanceMark) ?Env.JsObject {
|
||||
return self.detail;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("./../../testing.zig");
|
||||
|
||||
test "Performance: get_timeOrigin" {
|
||||
var perf = Performance{ .time_origin = try std.time.Timer.start() };
|
||||
const time_origin = perf.get_timeOrigin();
|
||||
try testing.expect(time_origin >= 0);
|
||||
|
||||
// Check resolution
|
||||
try testing.expectDelta(@rem(time_origin * std.time.us_per_ms, 100.0), 0.0, 0.2);
|
||||
}
|
||||
|
||||
test "Performance: now" {
|
||||
var perf = Performance{ .time_origin = try std.time.Timer.start() };
|
||||
|
||||
// Monotonically increasing
|
||||
var now = perf._now();
|
||||
while (now <= 0) { // Loop for now to not be 0
|
||||
try testing.expectEqual(now, 0);
|
||||
now = perf._now();
|
||||
}
|
||||
// Check resolution
|
||||
try testing.expectDelta(@rem(now * std.time.us_per_ms, 100.0), 0.0, 0.1);
|
||||
|
||||
var after = perf._now();
|
||||
while (after <= now) { // Loop untill after > now
|
||||
try testing.expectEqual(after, now);
|
||||
after = perf._now();
|
||||
}
|
||||
// Check resolution
|
||||
try testing.expectDelta(@rem(after * std.time.us_per_ms, 100.0), 0.0, 0.1);
|
||||
}
|
||||
|
||||
test "Browser.Performance.Mark" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let performance = window.performance", "undefined" },
|
||||
.{ "performance instanceof Performance", "true" },
|
||||
.{ "let mark = performance.mark(\"start\")", "undefined" },
|
||||
.{ "mark instanceof PerformanceMark", "true" },
|
||||
.{ "mark.name", "start" },
|
||||
.{ "mark.entryType", "mark" },
|
||||
.{ "mark.duration", "0" },
|
||||
.{ "mark.detail", "null" },
|
||||
}, .{});
|
||||
}
|
||||
63
src/browser/dom/performance_observer.zig
Normal file
63
src/browser/dom/performance_observer.zig
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Env = @import("../env.zig").Env;
|
||||
|
||||
const PerformanceEntry = @import("performance.zig").PerformanceEntry;
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver
|
||||
pub const PerformanceObserver = struct {
|
||||
pub const _supportedEntryTypes = [0][]const u8{};
|
||||
|
||||
pub fn constructor(cbk: Env.Function) PerformanceObserver {
|
||||
_ = cbk;
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn _observe(self: *const PerformanceObserver, options_: ?Options) void {
|
||||
_ = self;
|
||||
_ = options_;
|
||||
return;
|
||||
}
|
||||
|
||||
pub fn _disconnect(self: *PerformanceObserver) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
pub fn _takeRecords(_: *const PerformanceObserver) []PerformanceEntry {
|
||||
return &[_]PerformanceEntry{};
|
||||
}
|
||||
};
|
||||
|
||||
const Options = struct {
|
||||
buffered: ?bool = null,
|
||||
durationThreshold: ?f64 = null,
|
||||
entryTypes: ?[]const []const u8 = null,
|
||||
type: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.PerformanceObserver" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "PerformanceObserver.supportedEntryTypes.length", "0" },
|
||||
}, .{});
|
||||
}
|
||||
178
src/browser/dom/range.zig
Normal file
178
src/browser/dom/range.zig
Normal file
@@ -0,0 +1,178 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
const NodeUnion = @import("node.zig").Union;
|
||||
const Node = @import("node.zig").Node;
|
||||
|
||||
pub const Interfaces = .{
|
||||
AbstractRange,
|
||||
Range,
|
||||
};
|
||||
|
||||
pub const AbstractRange = struct {
|
||||
collapsed: bool,
|
||||
end_container: *parser.Node,
|
||||
end_offset: i32,
|
||||
start_container: *parser.Node,
|
||||
start_offset: i32,
|
||||
|
||||
pub fn updateCollapsed(self: *AbstractRange) void {
|
||||
// TODO: Eventually, compare properly.
|
||||
self.collapsed = false;
|
||||
}
|
||||
|
||||
pub fn get_collapsed(self: *const AbstractRange) bool {
|
||||
return self.collapsed;
|
||||
}
|
||||
|
||||
pub fn get_endContainer(self: *const AbstractRange) !NodeUnion {
|
||||
return Node.toInterface(self.end_container);
|
||||
}
|
||||
|
||||
pub fn get_endOffset(self: *const AbstractRange) i32 {
|
||||
return self.end_offset;
|
||||
}
|
||||
|
||||
pub fn get_startContainer(self: *const AbstractRange) !NodeUnion {
|
||||
return Node.toInterface(self.start_container);
|
||||
}
|
||||
|
||||
pub fn get_startOffset(self: *const AbstractRange) i32 {
|
||||
return self.start_offset;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Range = struct {
|
||||
pub const prototype = *AbstractRange;
|
||||
|
||||
proto: AbstractRange,
|
||||
|
||||
// The Range() constructor returns a newly created Range object whose start
|
||||
// and end is the global Document object.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Range/Range
|
||||
pub fn constructor(page: *Page) Range {
|
||||
const proto: AbstractRange = .{
|
||||
.collapsed = true,
|
||||
.end_container = parser.documentHTMLToNode(page.window.document),
|
||||
.end_offset = 0,
|
||||
.start_container = parser.documentHTMLToNode(page.window.document),
|
||||
.start_offset = 0,
|
||||
};
|
||||
|
||||
return .{ .proto = proto };
|
||||
}
|
||||
|
||||
pub fn _setStart(self: *Range, node: *parser.Node, offset: i32) void {
|
||||
self.proto.start_container = node;
|
||||
self.proto.start_offset = offset;
|
||||
self.proto.updateCollapsed();
|
||||
}
|
||||
|
||||
pub fn _setEnd(self: *Range, node: *parser.Node, offset: i32) void {
|
||||
self.proto.end_container = node;
|
||||
self.proto.end_offset = offset;
|
||||
self.proto.updateCollapsed();
|
||||
}
|
||||
|
||||
pub fn _createContextualFragment(_: *Range, fragment: []const u8, page: *Page) !*parser.DocumentFragment {
|
||||
const document_html = page.window.document;
|
||||
const document = parser.documentHTMLToDocument(document_html);
|
||||
const doc_frag = try parser.documentParseFragmentFromStr(document, fragment);
|
||||
return doc_frag;
|
||||
}
|
||||
|
||||
pub fn _selectNodeContents(self: *Range, node: *parser.Node) !void {
|
||||
self.proto.start_container = node;
|
||||
self.proto.start_offset = 0;
|
||||
self.proto.end_container = node;
|
||||
|
||||
// Set end_offset
|
||||
switch (try parser.nodeType(node)) {
|
||||
.text, .cdata_section, .comment, .processing_instruction => {
|
||||
// For text-like nodes, end_offset should be the length of the text data
|
||||
if (try parser.nodeValue(node)) |text_data| {
|
||||
self.proto.end_offset = @intCast(text_data.len);
|
||||
} else {
|
||||
self.proto.end_offset = 0;
|
||||
}
|
||||
},
|
||||
else => {
|
||||
// For element and other nodes, end_offset is the number of children
|
||||
const child_nodes = try parser.nodeGetChildNodes(node);
|
||||
const child_count = try parser.nodeListLength(child_nodes);
|
||||
self.proto.end_offset = @intCast(child_count);
|
||||
},
|
||||
}
|
||||
|
||||
self.proto.updateCollapsed();
|
||||
}
|
||||
|
||||
// The Range.detach() method does nothing. It used to disable the Range
|
||||
// object and enable the browser to release associated resources. The
|
||||
// method has been kept for compatibility.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Range/detach
|
||||
pub fn _detach(_: *Range) void {}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.Range" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
// Test Range constructor
|
||||
.{ "let range = new Range()", "undefined" },
|
||||
.{ "range instanceof Range", "true" },
|
||||
.{ "range instanceof AbstractRange", "true" },
|
||||
|
||||
// Test initial state - collapsed range
|
||||
.{ "range.collapsed", "true" },
|
||||
.{ "range.startOffset", "0" },
|
||||
.{ "range.endOffset", "0" },
|
||||
.{ "range.startContainer instanceof HTMLDocument", "true" },
|
||||
.{ "range.endContainer instanceof HTMLDocument", "true" },
|
||||
|
||||
// Test document.createRange()
|
||||
.{ "let docRange = document.createRange()", "undefined" },
|
||||
.{ "docRange instanceof Range", "true" },
|
||||
.{ "docRange.collapsed", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const container = document.getElementById('content');", null },
|
||||
|
||||
// Test text range
|
||||
.{ "const commentNode = container.childNodes[7];", null },
|
||||
.{ "commentNode.nodeValue", "comment" },
|
||||
.{ "const textRange = document.createRange();", null },
|
||||
.{ "textRange.selectNodeContents(commentNode)", "undefined" },
|
||||
.{ "textRange.startOffset", "0" },
|
||||
.{ "textRange.endOffset", "7" }, // length of `comment`
|
||||
|
||||
// Test Node range
|
||||
.{ "const nodeRange = document.createRange();", null },
|
||||
.{ "nodeRange.selectNodeContents(container)", "undefined" },
|
||||
.{ "nodeRange.startOffset", "0" },
|
||||
.{ "nodeRange.endOffset", "9" }, // length of container.childNodes
|
||||
}, .{});
|
||||
}
|
||||
54
src/browser/dom/resize_observer.zig
Normal file
54
src/browser/dom/resize_observer.zig
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
pub const Interfaces = .{
|
||||
ResizeObserver,
|
||||
};
|
||||
|
||||
// WEB IDL https://drafts.csswg.org/resize-observer/#resize-observer-interface
|
||||
pub const ResizeObserver = struct {
|
||||
pub fn constructor(cbk: Env.Function) ResizeObserver {
|
||||
_ = cbk;
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn _observe(self: *const ResizeObserver, element: *parser.Element, options_: ?Options) void {
|
||||
_ = self;
|
||||
_ = element;
|
||||
_ = options_;
|
||||
return;
|
||||
}
|
||||
|
||||
pub fn _unobserve(self: *const ResizeObserver, element: *parser.Element) void {
|
||||
_ = self;
|
||||
_ = element;
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO
|
||||
pub fn _disconnect(self: *ResizeObserver) void {
|
||||
_ = self;
|
||||
}
|
||||
};
|
||||
|
||||
const Options = struct {
|
||||
box: []const u8,
|
||||
};
|
||||
73
src/browser/dom/shadow_root.zig
Normal file
73
src/browser/dom/shadow_root.zig
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Element = @import("element.zig").Element;
|
||||
const ElementUnion = @import("element.zig").Union;
|
||||
|
||||
// WEB IDL https://dom.spec.whatwg.org/#interface-shadowroot
|
||||
pub const ShadowRoot = struct {
|
||||
pub const prototype = *parser.DocumentFragment;
|
||||
pub const subtype = .node;
|
||||
|
||||
mode: Mode,
|
||||
host: *parser.Element,
|
||||
proto: *parser.DocumentFragment,
|
||||
|
||||
pub const Mode = enum {
|
||||
open,
|
||||
closed,
|
||||
};
|
||||
|
||||
pub fn get_host(self: *const ShadowRoot) !ElementUnion {
|
||||
return Element.toInterface(self.host);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.ShadowRoot" {
|
||||
defer testing.reset();
|
||||
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "" });
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const div1 = document.createElement('div');", null },
|
||||
.{ "let sr1 = div1.attachShadow({mode: 'open'})", null },
|
||||
.{ "sr1.host == div1", "true" },
|
||||
.{ "div1.attachShadow({mode: 'open'}) == sr1", "true" },
|
||||
.{ "div1.shadowRoot == sr1", "true" },
|
||||
|
||||
.{ "try { div1.attachShadow({mode: 'closed'}) } catch (e) { e }", "Error: NotSupportedError" },
|
||||
|
||||
.{ " sr1.append(document.createElement('div'))", null },
|
||||
.{ " sr1.append(document.createElement('span'))", null },
|
||||
.{ "sr1.childElementCount", "2" },
|
||||
// re-attaching clears it
|
||||
.{ "div1.attachShadow({mode: 'open'}) == sr1", "true" },
|
||||
.{ "sr1.childElementCount", "0" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const div2 = document.createElement('di2');", null },
|
||||
.{ "let sr2 = div2.attachShadow({mode: 'closed'})", null },
|
||||
.{ "sr2.host == div2", "true" },
|
||||
.{ "div2.shadowRoot", "null" }, // null when attached with 'closed'
|
||||
}, .{});
|
||||
}
|
||||
@@ -19,16 +19,19 @@
|
||||
const std = @import("std");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const NodeFilter = @import("node_filter.zig").NodeFilter;
|
||||
const NodeFilter = @import("node_filter.zig");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const Node = @import("node.zig").Node;
|
||||
const NodeUnion = @import("node.zig").Union;
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker
|
||||
pub const TreeWalker = struct {
|
||||
root: *parser.Node,
|
||||
current_node: *parser.Node,
|
||||
what_to_show: u32,
|
||||
filter: ?Env.Function,
|
||||
filter: ?TreeWalkerOpts,
|
||||
filter_func: ?Env.Function,
|
||||
|
||||
pub const TreeWalkerOpts = union(enum) {
|
||||
function: Env.Function,
|
||||
@@ -48,58 +51,25 @@ pub const TreeWalker = struct {
|
||||
return .{
|
||||
.root = node,
|
||||
.current_node = node,
|
||||
.what_to_show = what_to_show orelse NodeFilter._SHOW_ALL,
|
||||
.filter = filter_func,
|
||||
.what_to_show = what_to_show orelse NodeFilter.NodeFilter._SHOW_ALL,
|
||||
.filter = filter,
|
||||
.filter_func = filter_func,
|
||||
};
|
||||
}
|
||||
|
||||
const VerifyResult = enum { accept, skip, reject };
|
||||
|
||||
fn verify(self: *const TreeWalker, node: *parser.Node) !VerifyResult {
|
||||
const node_type = try parser.nodeType(node);
|
||||
const what_to_show = self.what_to_show;
|
||||
|
||||
// Verify that we can show this node type.
|
||||
if (!switch (node_type) {
|
||||
.attribute => what_to_show & NodeFilter._SHOW_ATTRIBUTE != 0,
|
||||
.cdata_section => what_to_show & NodeFilter._SHOW_CDATA_SECTION != 0,
|
||||
.comment => what_to_show & NodeFilter._SHOW_COMMENT != 0,
|
||||
.document => what_to_show & NodeFilter._SHOW_DOCUMENT != 0,
|
||||
.document_fragment => what_to_show & NodeFilter._SHOW_DOCUMENT_FRAGMENT != 0,
|
||||
.document_type => what_to_show & NodeFilter._SHOW_DOCUMENT_TYPE != 0,
|
||||
.element => what_to_show & NodeFilter._SHOW_ELEMENT != 0,
|
||||
.entity => what_to_show & NodeFilter._SHOW_ENTITY != 0,
|
||||
.entity_reference => what_to_show & NodeFilter._SHOW_ENTITY_REFERENCE != 0,
|
||||
.notation => what_to_show & NodeFilter._SHOW_NOTATION != 0,
|
||||
.processing_instruction => what_to_show & NodeFilter._SHOW_PROCESSING_INSTRUCTION != 0,
|
||||
.text => what_to_show & NodeFilter._SHOW_TEXT != 0,
|
||||
}) return .reject;
|
||||
|
||||
// Verify that we aren't filtering it out.
|
||||
if (self.filter) |f| {
|
||||
const filter = try f.call(u32, .{node});
|
||||
return switch (filter) {
|
||||
NodeFilter._FILTER_ACCEPT => .accept,
|
||||
NodeFilter._FILTER_REJECT => .reject,
|
||||
NodeFilter._FILTER_SKIP => .skip,
|
||||
else => .reject,
|
||||
};
|
||||
} else return .accept;
|
||||
pub fn get_root(self: *TreeWalker) !NodeUnion {
|
||||
return try Node.toInterface(self.root);
|
||||
}
|
||||
|
||||
pub fn get_root(self: *TreeWalker) *parser.Node {
|
||||
return self.root;
|
||||
}
|
||||
|
||||
pub fn get_currentNode(self: *TreeWalker) *parser.Node {
|
||||
return self.current_node;
|
||||
pub fn get_currentNode(self: *TreeWalker) !NodeUnion {
|
||||
return try Node.toInterface(self.current_node);
|
||||
}
|
||||
|
||||
pub fn get_whatToShow(self: *TreeWalker) u32 {
|
||||
return self.what_to_show;
|
||||
}
|
||||
|
||||
pub fn get_filter(self: *TreeWalker) ?Env.Function {
|
||||
pub fn get_filter(self: *TreeWalker) ?TreeWalkerOpts {
|
||||
return self.filter;
|
||||
}
|
||||
|
||||
@@ -115,7 +85,7 @@ pub const TreeWalker = struct {
|
||||
const index: u32 = @intCast(i);
|
||||
const child = (try parser.nodeListItem(children, index)) orelse return null;
|
||||
|
||||
switch (try self.verify(child)) {
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
|
||||
.accept => return child,
|
||||
.reject => continue,
|
||||
.skip => if (try self.firstChild(child)) |gchild| return gchild,
|
||||
@@ -134,7 +104,7 @@ pub const TreeWalker = struct {
|
||||
index -= 1;
|
||||
const child = (try parser.nodeListItem(children, index)) orelse return null;
|
||||
|
||||
switch (try self.verify(child)) {
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
|
||||
.accept => return child,
|
||||
.reject => continue,
|
||||
.skip => if (try self.lastChild(child)) |gchild| return gchild,
|
||||
@@ -150,7 +120,7 @@ pub const TreeWalker = struct {
|
||||
while (true) {
|
||||
current = (try parser.nodeNextSibling(current)) orelse return null;
|
||||
|
||||
switch (try self.verify(current)) {
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||
.accept => return current,
|
||||
.skip, .reject => continue,
|
||||
}
|
||||
@@ -165,7 +135,7 @@ pub const TreeWalker = struct {
|
||||
while (true) {
|
||||
current = (try parser.nodePreviousSibling(current)) orelse return null;
|
||||
|
||||
switch (try self.verify(current)) {
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||
.accept => return current,
|
||||
.skip, .reject => continue,
|
||||
}
|
||||
@@ -182,42 +152,42 @@ pub const TreeWalker = struct {
|
||||
if (current == self.root) return null;
|
||||
current = (try parser.nodeParentNode(current)) orelse return null;
|
||||
|
||||
switch (try self.verify(current)) {
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||
.accept => return current,
|
||||
.reject, .skip => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _firstChild(self: *TreeWalker) !?*parser.Node {
|
||||
pub fn _firstChild(self: *TreeWalker) !?NodeUnion {
|
||||
if (try self.firstChild(self.current_node)) |child| {
|
||||
self.current_node = child;
|
||||
return child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn _lastChild(self: *TreeWalker) !?*parser.Node {
|
||||
pub fn _lastChild(self: *TreeWalker) !?NodeUnion {
|
||||
if (try self.lastChild(self.current_node)) |child| {
|
||||
self.current_node = child;
|
||||
return child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn _nextNode(self: *TreeWalker) !?*parser.Node {
|
||||
pub fn _nextNode(self: *TreeWalker) !?NodeUnion {
|
||||
if (try self.firstChild(self.current_node)) |child| {
|
||||
self.current_node = child;
|
||||
return child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
|
||||
var current = self.current_node;
|
||||
while (current != self.root) {
|
||||
if (try self.nextSibling(current)) |sibling| {
|
||||
self.current_node = sibling;
|
||||
return sibling;
|
||||
return try Node.toInterface(sibling);
|
||||
}
|
||||
|
||||
current = (try parser.nodeParentNode(current)) orelse break;
|
||||
@@ -226,47 +196,49 @@ pub const TreeWalker = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn _nextSibling(self: *TreeWalker) !?*parser.Node {
|
||||
pub fn _nextSibling(self: *TreeWalker) !?NodeUnion {
|
||||
if (try self.nextSibling(self.current_node)) |sibling| {
|
||||
self.current_node = sibling;
|
||||
return sibling;
|
||||
return try Node.toInterface(sibling);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn _parentNode(self: *TreeWalker) !?*parser.Node {
|
||||
pub fn _parentNode(self: *TreeWalker) !?NodeUnion {
|
||||
if (try self.parentNode(self.current_node)) |parent| {
|
||||
self.current_node = parent;
|
||||
return parent;
|
||||
return try Node.toInterface(parent);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn _previousNode(self: *TreeWalker) !?*parser.Node {
|
||||
pub fn _previousNode(self: *TreeWalker) !?NodeUnion {
|
||||
if (self.current_node == self.root) return null;
|
||||
|
||||
var current = self.current_node;
|
||||
while (try parser.nodePreviousSibling(current)) |previous| {
|
||||
current = previous;
|
||||
|
||||
switch (try self.verify(current)) {
|
||||
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
|
||||
.accept => {
|
||||
// Get last child if it has one.
|
||||
if (try self.lastChild(current)) |child| {
|
||||
self.current_node = child;
|
||||
return child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
|
||||
// Otherwise, this node is our previous one.
|
||||
self.current_node = current;
|
||||
return current;
|
||||
return try Node.toInterface(current);
|
||||
},
|
||||
.reject => continue,
|
||||
.skip => {
|
||||
// Get last child if it has one.
|
||||
if (try self.lastChild(current)) |child| {
|
||||
self.current_node = child;
|
||||
return child;
|
||||
return try Node.toInterface(child);
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -275,17 +247,17 @@ pub const TreeWalker = struct {
|
||||
if (current != self.root) {
|
||||
if (try self.parentNode(current)) |parent| {
|
||||
self.current_node = parent;
|
||||
return parent;
|
||||
return try Node.toInterface(parent);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn _previousSibling(self: *TreeWalker) !?*parser.Node {
|
||||
pub fn _previousSibling(self: *TreeWalker) !?NodeUnion {
|
||||
if (try self.previousSibling(self.current_node)) |sibling| {
|
||||
self.current_node = sibling;
|
||||
return sibling;
|
||||
return try Node.toInterface(sibling);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -21,10 +21,14 @@ const std = @import("std");
|
||||
const parser = @import("netsurf.zig");
|
||||
const Walker = @import("dom/walker.zig").WalkerChildren;
|
||||
|
||||
pub const Opts = struct {
|
||||
exclude_scripts: bool = false,
|
||||
};
|
||||
|
||||
// writer must be a std.io.Writer
|
||||
pub fn writeHTML(doc: *parser.Document, writer: anytype) !void {
|
||||
pub fn writeHTML(doc: *parser.Document, opts: Opts, writer: anytype) !void {
|
||||
try writer.writeAll("<!DOCTYPE html>\n");
|
||||
try writeChildren(parser.documentToNode(doc), writer);
|
||||
try writeChildren(parser.documentToNode(doc), opts, writer);
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
|
||||
@@ -54,10 +58,15 @@ pub fn writeDocType(doc_type: *parser.DocumentType, writer: anytype) !void {
|
||||
try writer.writeAll(">");
|
||||
}
|
||||
|
||||
pub fn writeNode(node: *parser.Node, writer: anytype) anyerror!void {
|
||||
pub fn writeNode(node: *parser.Node, opts: Opts, writer: anytype) anyerror!void {
|
||||
switch (try parser.nodeType(node)) {
|
||||
.element => {
|
||||
// open the tag
|
||||
const tag_type = try parser.elementHTMLGetTagType(@ptrCast(node));
|
||||
if (tag_type == .script and opts.exclude_scripts) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tag = try parser.nodeLocalName(node);
|
||||
try writer.writeAll("<");
|
||||
try writer.writeAll(tag);
|
||||
@@ -82,9 +91,13 @@ pub fn writeNode(node: *parser.Node, writer: anytype) anyerror!void {
|
||||
// void elements can't have any content.
|
||||
if (try isVoid(parser.nodeToElement(node))) return;
|
||||
|
||||
if (tag_type == .script) {
|
||||
try writer.writeAll(try parser.nodeTextContent(node) orelse "");
|
||||
} else {
|
||||
// write the children
|
||||
// TODO avoid recursion
|
||||
try writeChildren(node, writer);
|
||||
try writeChildren(node, opts, writer);
|
||||
}
|
||||
|
||||
// close the tag
|
||||
try writer.writeAll("</");
|
||||
@@ -125,12 +138,12 @@ pub fn writeNode(node: *parser.Node, writer: anytype) anyerror!void {
|
||||
}
|
||||
|
||||
// writer must be a std.io.Writer
|
||||
pub fn writeChildren(root: *parser.Node, writer: anytype) !void {
|
||||
pub fn writeChildren(root: *parser.Node, opts: Opts, writer: anytype) !void {
|
||||
const walker = Walker{};
|
||||
var next: ?*parser.Node = null;
|
||||
while (true) {
|
||||
next = try walker.get_next(root, next) orelse break;
|
||||
try writeNode(next.?, writer);
|
||||
try writeNode(next.?, opts, writer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,6 +224,11 @@ test "dump.writeHTML" {
|
||||
\\</head><body>9000</body></html>
|
||||
\\
|
||||
, "<html><title>It's over what?</title><meta name=a value=\"b\">\n<body>9000");
|
||||
|
||||
try testWriteHTML(
|
||||
"<p>hi</p><script>alert(power > 9000)</script>",
|
||||
"<p>hi</p><script>alert(power > 9000)</script>",
|
||||
);
|
||||
}
|
||||
|
||||
fn testWriteHTML(comptime expected_body: []const u8, src: []const u8) !void {
|
||||
@@ -229,6 +247,6 @@ fn testWriteFullHTML(comptime expected: []const u8, src: []const u8) !void {
|
||||
defer parser.documentHTMLClose(doc_html) catch {};
|
||||
|
||||
const doc = parser.documentHTMLToDocument(doc_html);
|
||||
try writeHTML(doc, buf.writer(testing.allocator));
|
||||
try writeHTML(doc, .{}, buf.writer(testing.allocator));
|
||||
try testing.expectEqualStrings(expected, buf.items);
|
||||
}
|
||||
|
||||
107
src/browser/encoding/TextDecoder.zig
Normal file
107
src/browser/encoding/TextDecoder.zig
Normal file
@@ -0,0 +1,107 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
|
||||
// https://encoding.spec.whatwg.org/#interface-textdecoder
|
||||
const TextDecoder = @This();
|
||||
|
||||
const SupportedLabels = enum {
|
||||
utf8,
|
||||
@"utf-8",
|
||||
@"unicode-1-1-utf-8",
|
||||
};
|
||||
|
||||
const Options = struct {
|
||||
fatal: bool = false,
|
||||
ignoreBOM: bool = false,
|
||||
};
|
||||
|
||||
fatal: bool,
|
||||
ignore_bom: bool,
|
||||
|
||||
pub fn constructor(label_: ?[]const u8, opts_: ?Options) !TextDecoder {
|
||||
if (label_) |l| {
|
||||
_ = std.meta.stringToEnum(SupportedLabels, l) orelse {
|
||||
log.warn(.web_api, "not implemented", .{ .feature = "TextDecoder label", .label = l });
|
||||
return error.NotImplemented;
|
||||
};
|
||||
}
|
||||
const opts = opts_ orelse Options{};
|
||||
return .{
|
||||
.fatal = opts.fatal,
|
||||
.ignore_bom = opts.ignoreBOM,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_encoding(_: *const TextDecoder) []const u8 {
|
||||
return "utf-8";
|
||||
}
|
||||
|
||||
pub fn get_ignoreBOM(self: *const TextDecoder) bool {
|
||||
return self.ignore_bom;
|
||||
}
|
||||
|
||||
pub fn get_fatal(self: *const TextDecoder) bool {
|
||||
return self.fatal;
|
||||
}
|
||||
|
||||
// TODO: Should accept an ArrayBuffer, TypedArray or DataView
|
||||
// js.zig will currently only map a TypedArray to our []const u8.
|
||||
pub fn _decode(self: *const TextDecoder, v: []const u8) ![]const u8 {
|
||||
if (self.fatal and !std.unicode.utf8ValidateSlice(v)) {
|
||||
return error.InvalidUtf8;
|
||||
}
|
||||
|
||||
if (self.ignore_bom == false and std.mem.startsWith(u8, v, &.{ 0xEF, 0xBB, 0xBF })) {
|
||||
return v[3..];
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.Encoding.TextDecoder" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{
|
||||
.html = "",
|
||||
});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let d1 = new TextDecoder();", null },
|
||||
.{ "d1.encoding;", "utf-8" },
|
||||
.{ "d1.fatal", "false" },
|
||||
.{ "d1.ignoreBOM", "false" },
|
||||
.{ "d1.decode(new Uint8Array([240, 160, 174, 183]))", "𠮷" },
|
||||
.{ "d1.decode(new Uint8Array([0xEF, 0xBB, 0xBF, 240, 160, 174, 183]))", "𠮷" },
|
||||
.{ "d1.decode(new Uint8Array([49, 50]).buffer)", "12" },
|
||||
|
||||
.{ "let d2 = new TextDecoder('utf8', {fatal: true})", null },
|
||||
.{
|
||||
\\ try {
|
||||
\\ let data = new Uint8Array([240, 240, 160, 174, 183]);
|
||||
\\ d2.decode(data);
|
||||
\\ } catch (e) {e}
|
||||
,
|
||||
"Error: InvalidUtf8",
|
||||
},
|
||||
}, .{});
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
@@ -20,12 +20,9 @@ const std = @import("std");
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
|
||||
pub const Interfaces = .{
|
||||
TextEncoder,
|
||||
};
|
||||
|
||||
// https://encoding.spec.whatwg.org/#interface-textencoder
|
||||
pub const TextEncoder = struct {
|
||||
const TextEncoder = @This();
|
||||
|
||||
pub fn constructor() !TextEncoder {
|
||||
return .{};
|
||||
}
|
||||
@@ -44,15 +41,16 @@ pub const TextEncoder = struct {
|
||||
|
||||
return .{ .values = v };
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.Encoding.TextEncoder" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{
|
||||
.html = "",
|
||||
});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var encoder = new TextEncoder();", "undefined" },
|
||||
.{ "var encoder = new TextEncoder();", null },
|
||||
.{ "encoder.encoding;", "utf-8" },
|
||||
.{ "encoder.encode('€');", "226,130,172" },
|
||||
|
||||
22
src/browser/encoding/encoding.zig
Normal file
22
src/browser/encoding/encoding.zig
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
pub const Interfaces = .{
|
||||
@import("TextDecoder.zig"),
|
||||
@import("TextEncoder.zig"),
|
||||
};
|
||||
@@ -22,9 +22,11 @@ const WebApis = struct {
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
@import("crypto/crypto.zig").Crypto,
|
||||
@import("console/console.zig").Console,
|
||||
@import("cssom/css_style_declaration.zig").Interfaces,
|
||||
@import("css/css.zig").Interfaces,
|
||||
@import("cssom/cssom.zig").Interfaces,
|
||||
@import("dom/dom.zig").Interfaces,
|
||||
@import("encoding/text_encoder.zig").Interfaces,
|
||||
@import("dom/shadow_root.zig").ShadowRoot,
|
||||
@import("encoding/encoding.zig").Interfaces,
|
||||
@import("events/event.zig").Interfaces,
|
||||
@import("html/html.zig").Interfaces,
|
||||
@import("iterator/iterator.zig").Interfaces,
|
||||
@@ -39,5 +41,8 @@ const WebApis = struct {
|
||||
pub const JsThis = Env.JsThis;
|
||||
pub const JsObject = Env.JsObject;
|
||||
pub const Function = Env.Function;
|
||||
pub const Promise = Env.Promise;
|
||||
pub const PromiseResolver = Env.PromiseResolver;
|
||||
|
||||
pub const Env = js.Env(*Page, WebApis);
|
||||
pub const Global = @import("html/window.zig").Window;
|
||||
|
||||
@@ -27,18 +27,16 @@ const Page = @import("../page.zig").Page;
|
||||
const DOMException = @import("../dom/exceptions.zig").DOMException;
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const EventTargetUnion = @import("../dom/event_target.zig").Union;
|
||||
const AbortSignal = @import("../html/AbortController.zig").AbortSignal;
|
||||
|
||||
const CustomEvent = @import("custom_event.zig").CustomEvent;
|
||||
const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
|
||||
const MouseEvent = @import("mouse_event.zig").MouseEvent;
|
||||
const ErrorEvent = @import("../html/error_event.zig").ErrorEvent;
|
||||
const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
|
||||
|
||||
// Event interfaces
|
||||
pub const Interfaces = .{
|
||||
Event,
|
||||
CustomEvent,
|
||||
ProgressEvent,
|
||||
MouseEvent,
|
||||
};
|
||||
pub const Interfaces = .{ Event, CustomEvent, ProgressEvent, MouseEvent, ErrorEvent, MessageEvent };
|
||||
|
||||
pub const Union = generate.Union(Interfaces);
|
||||
|
||||
@@ -58,10 +56,12 @@ pub const Event = struct {
|
||||
|
||||
pub fn toInterface(evt: *parser.Event) !Union {
|
||||
return switch (try parser.eventGetInternalType(evt)) {
|
||||
.event => .{ .Event = evt },
|
||||
.event, .abort_signal, .xhr_event => .{ .Event = evt },
|
||||
.custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* },
|
||||
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
|
||||
.mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) },
|
||||
.error_event => .{ .ErrorEvent = @as(*ErrorEvent, @ptrCast(evt)).* },
|
||||
.message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ pub const EventHandler = struct {
|
||||
// that the listener won't call preventDefault() and thus can safely
|
||||
// run the default as needed).
|
||||
passive: ?bool,
|
||||
signal: ?bool, // currently does nothing
|
||||
signal: ?*AbortSignal, // currently does nothing
|
||||
};
|
||||
};
|
||||
|
||||
@@ -191,18 +191,14 @@ pub const EventHandler = struct {
|
||||
) !?*EventHandler {
|
||||
var once = false;
|
||||
var capture = false;
|
||||
var signal: ?*AbortSignal = null;
|
||||
|
||||
if (opts_) |opts| {
|
||||
switch (opts) {
|
||||
.capture => |c| capture = c,
|
||||
.flags => |f| {
|
||||
// Done this way so that, for common cases that _only_ set
|
||||
// capture, i.e. {captrue: true}, it works.
|
||||
// But for any case that sets any of the other flags, we
|
||||
// error. If we don't error, this function call would succeed
|
||||
// but the behavior might be wrong. At this point, it's
|
||||
// better to be explicit and error.
|
||||
if (f.signal orelse false) return error.NotImplemented;
|
||||
once = f.once orelse false;
|
||||
signal = f.signal orelse null;
|
||||
capture = f.capture orelse false;
|
||||
},
|
||||
}
|
||||
@@ -210,6 +206,28 @@ pub const EventHandler = struct {
|
||||
|
||||
const callback = (try listener.callback(target)) orelse return null;
|
||||
|
||||
if (signal) |s| {
|
||||
const signal_target = parser.toEventTarget(AbortSignal, s);
|
||||
|
||||
const scb = try allocator.create(SignalCallback);
|
||||
scb.* = .{
|
||||
.target = target,
|
||||
.capture = capture,
|
||||
.callback_id = callback.id,
|
||||
.typ = try allocator.dupe(u8, typ),
|
||||
.signal_target = signal_target,
|
||||
.signal_listener = undefined,
|
||||
.node = .{ .func = SignalCallback.handle },
|
||||
};
|
||||
|
||||
scb.signal_listener = try parser.eventTargetAddEventListener(
|
||||
signal_target,
|
||||
"abort",
|
||||
&scb.node,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
// check if event target has already this listener
|
||||
if (try parser.eventTargetHasListener(target, typ, capture, callback.id) != null) {
|
||||
return null;
|
||||
@@ -265,6 +283,50 @@ pub const EventHandler = struct {
|
||||
}
|
||||
};
|
||||
|
||||
const SignalCallback = struct {
|
||||
typ: []const u8,
|
||||
capture: bool,
|
||||
callback_id: usize,
|
||||
node: parser.EventNode,
|
||||
target: *parser.EventTarget,
|
||||
signal_target: *parser.EventTarget,
|
||||
signal_listener: *parser.EventListener,
|
||||
|
||||
fn handle(node: *parser.EventNode, _: *parser.Event) void {
|
||||
const self: *SignalCallback = @fieldParentPtr("node", node);
|
||||
self._handle() catch |err| {
|
||||
log.err(.app, "event signal handler", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
fn _handle(self: *SignalCallback) !void {
|
||||
const lst = try parser.eventTargetHasListener(
|
||||
self.target,
|
||||
self.typ,
|
||||
self.capture,
|
||||
self.callback_id,
|
||||
);
|
||||
if (lst == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try parser.eventTargetRemoveEventListener(
|
||||
self.target,
|
||||
self.typ,
|
||||
lst.?,
|
||||
self.capture,
|
||||
);
|
||||
|
||||
// remove the abort signal listener itself
|
||||
try parser.eventTargetRemoveEventListener(
|
||||
self.signal_target,
|
||||
"abort",
|
||||
self.signal_listener,
|
||||
false,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.Event" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
@@ -370,5 +432,18 @@ test "Browser.Event" {
|
||||
.{ "document.dispatchEvent(new Event('count'))", "true" },
|
||||
.{ "document.dispatchEvent(new Event('count'))", "true" },
|
||||
.{ "nb", "1" },
|
||||
.{ "document.removeEventListener('count', cbk)", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0; function cbk(event) { nb ++; }", null },
|
||||
.{ "let ac = new AbortController()", null },
|
||||
.{ "document.addEventListener('count', cbk, {signal: ac.signal})", null },
|
||||
.{ "document.dispatchEvent(new Event('count'))", "true" },
|
||||
.{ "document.dispatchEvent(new Event('count'))", "true" },
|
||||
.{ "ac.abort()", null },
|
||||
.{ "document.dispatchEvent(new Event('count'))", "true" },
|
||||
.{ "nb", "2" },
|
||||
.{ "document.removeEventListener('count', cbk)", "undefined" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
187
src/browser/html/AbortController.zig
Normal file
187
src/browser/html/AbortController.zig
Normal file
@@ -0,0 +1,187 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const Loop = @import("../../runtime/loop.zig").Loop;
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
|
||||
pub const Interfaces = .{
|
||||
AbortController,
|
||||
AbortSignal,
|
||||
};
|
||||
|
||||
const AbortController = @This();
|
||||
|
||||
signal: *AbortSignal,
|
||||
|
||||
pub fn constructor(page: *Page) !AbortController {
|
||||
// Why do we allocate this rather than storing directly in the struct?
|
||||
// https://github.com/lightpanda-io/project/discussions/165
|
||||
const signal = try page.arena.create(AbortSignal);
|
||||
signal.* = .init;
|
||||
|
||||
return .{
|
||||
.signal = signal,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_signal(self: *AbortController) *AbortSignal {
|
||||
return self.signal;
|
||||
}
|
||||
|
||||
pub fn _abort(self: *AbortController, reason_: ?[]const u8) !void {
|
||||
return self.signal.abort(reason_);
|
||||
}
|
||||
|
||||
pub const AbortSignal = struct {
|
||||
const DEFAULT_REASON = "AbortError";
|
||||
|
||||
pub const prototype = *EventTarget;
|
||||
proto: parser.EventTargetTBase = .{ .internal_target_type = .abort_signal },
|
||||
|
||||
aborted: bool,
|
||||
reason: ?[]const u8,
|
||||
|
||||
pub const init: AbortSignal = .{
|
||||
.reason = null,
|
||||
.aborted = false,
|
||||
};
|
||||
|
||||
pub fn static_abort(reason_: ?[]const u8) AbortSignal {
|
||||
return .{
|
||||
.aborted = true,
|
||||
.reason = reason_ orelse DEFAULT_REASON,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn static_timeout(delay: u32, page: *Page) !*AbortSignal {
|
||||
const callback = try page.arena.create(TimeoutCallback);
|
||||
callback.* = .{
|
||||
.signal = .init,
|
||||
.node = .{ .func = TimeoutCallback.run },
|
||||
};
|
||||
|
||||
const delay_ms: u63 = @as(u63, delay) * std.time.ns_per_ms;
|
||||
_ = try page.loop.timeout(delay_ms, &callback.node);
|
||||
return &callback.signal;
|
||||
}
|
||||
|
||||
pub fn get_aborted(self: *const AbortSignal) bool {
|
||||
return self.aborted;
|
||||
}
|
||||
|
||||
fn abort(self: *AbortSignal, reason_: ?[]const u8) !void {
|
||||
self.aborted = true;
|
||||
self.reason = reason_ orelse DEFAULT_REASON;
|
||||
|
||||
const abort_event = try parser.eventCreate();
|
||||
try parser.eventSetInternalType(abort_event, .abort_signal);
|
||||
|
||||
defer parser.eventDestroy(abort_event);
|
||||
try parser.eventInit(abort_event, "abort", .{});
|
||||
_ = try parser.eventTargetDispatchEvent(
|
||||
parser.toEventTarget(AbortSignal, self),
|
||||
abort_event,
|
||||
);
|
||||
}
|
||||
|
||||
const Reason = union(enum) {
|
||||
reason: []const u8,
|
||||
undefined: void,
|
||||
};
|
||||
pub fn get_reason(self: *const AbortSignal) Reason {
|
||||
if (self.reason) |r| {
|
||||
return .{ .reason = r };
|
||||
}
|
||||
return .{ .undefined = {} };
|
||||
}
|
||||
|
||||
const ThrowIfAborted = union(enum) {
|
||||
exception: Env.Exception,
|
||||
undefined: void,
|
||||
};
|
||||
pub fn _throwIfAborted(self: *const AbortSignal, page: *Page) ThrowIfAborted {
|
||||
if (self.aborted) {
|
||||
const ex = page.main_context.throw(self.reason orelse DEFAULT_REASON);
|
||||
return .{ .exception = ex };
|
||||
}
|
||||
return .{ .undefined = {} };
|
||||
}
|
||||
};
|
||||
|
||||
const TimeoutCallback = struct {
|
||||
signal: AbortSignal,
|
||||
|
||||
// This is the internal data that the event loop tracks. We'll get this
|
||||
// back in run and, from it, can get our TimeoutCallback instance
|
||||
node: Loop.CallbackNode = undefined,
|
||||
|
||||
fn run(node: *Loop.CallbackNode, _: *?u63) void {
|
||||
const self: *TimeoutCallback = @fieldParentPtr("node", node);
|
||||
self.signal.abort("TimeoutError") catch |err| {
|
||||
log.warn(.app, "abort signal timeout", .{ .err = err });
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.AbortController" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var called = 0", null },
|
||||
.{ "var a1 = new AbortController()", null },
|
||||
.{ "var s1 = a1.signal", null },
|
||||
.{ "s1.throwIfAborted()", "undefined" },
|
||||
.{ "s1.reason", "undefined" },
|
||||
.{ "var target;", null },
|
||||
.{
|
||||
\\ s1.addEventListener('abort', (e) => {
|
||||
\\ called += 1;
|
||||
\\ target = e.target;
|
||||
\\
|
||||
\\ });
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "a1.abort()", null },
|
||||
.{ "s1.aborted", "true" },
|
||||
.{ "target == s1", "true" },
|
||||
.{ "s1.reason", "AbortError" },
|
||||
.{ "called", "1" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var s2 = AbortSignal.abort('over 9000')", null },
|
||||
.{ "s2.aborted", "true" },
|
||||
.{ "s2.reason", "over 9000" },
|
||||
.{ "AbortSignal.abort().reason", "AbortError" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var s3 = AbortSignal.timeout(10)", null },
|
||||
.{ "s3.aborted", "true" },
|
||||
.{ "s3.reason", "TimeoutError" },
|
||||
.{ "try { s3.throwIfAborted() } catch (e) { e }", "Error: TimeoutError" },
|
||||
}, .{});
|
||||
}
|
||||
97
src/browser/html/DataSet.zig
Normal file
97
src/browser/html/DataSet.zig
Normal file
@@ -0,0 +1,97 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
const std = @import("std");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const DataSet = @This();
|
||||
|
||||
element: *parser.Element,
|
||||
|
||||
pub fn named_get(self: *const DataSet, name: []const u8, _: *bool, page: *Page) !Env.UndefinedOr([]const u8) {
|
||||
const normalized_name = try normalize(page.call_arena, name);
|
||||
if (try parser.elementGetAttribute(self.element, normalized_name)) |value| {
|
||||
return .{ .value = value };
|
||||
}
|
||||
return .undefined;
|
||||
}
|
||||
|
||||
pub fn named_set(self: *DataSet, name: []const u8, value: []const u8, _: *bool, page: *Page) !void {
|
||||
const normalized_name = try normalize(page.call_arena, name);
|
||||
try parser.elementSetAttribute(self.element, normalized_name, value);
|
||||
}
|
||||
|
||||
pub fn named_delete(self: *DataSet, name: []const u8, _: *bool, page: *Page) !void {
|
||||
const normalized_name = try normalize(page.call_arena, name);
|
||||
try parser.elementRemoveAttribute(self.element, normalized_name);
|
||||
}
|
||||
|
||||
fn normalize(allocator: Allocator, name: []const u8) ![]const u8 {
|
||||
var upper_count: usize = 0;
|
||||
for (name) |c| {
|
||||
if (std.ascii.isUpper(c)) {
|
||||
upper_count += 1;
|
||||
}
|
||||
}
|
||||
// for every upper-case letter, we'll probably need a dash before it
|
||||
// and we need the 'data-' prefix
|
||||
var normalized = try allocator.alloc(u8, name.len + upper_count + 5);
|
||||
|
||||
@memcpy(normalized[0..5], "data-");
|
||||
if (upper_count == 0) {
|
||||
@memcpy(normalized[5..], name);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
var pos: usize = 5;
|
||||
for (name) |c| {
|
||||
if (std.ascii.isUpper(c)) {
|
||||
normalized[pos] = '-';
|
||||
pos += 1;
|
||||
normalized[pos] = c + 32;
|
||||
} else {
|
||||
normalized[pos] = c;
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.DataSet" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "" });
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let el1 = document.createElement('div')", null },
|
||||
.{ "el1.dataset.x", "undefined" },
|
||||
.{ "el1.dataset.x = '123'", "123" },
|
||||
.{ "delete el1.dataset.x", "true" },
|
||||
.{ "el1.dataset.x", "undefined" },
|
||||
.{ "delete el1.dataset.other", "true" }, // yes, this is right
|
||||
|
||||
.{ "let ds1 = el1.dataset", null },
|
||||
.{ "ds1.helloWorld = 'yes'", null },
|
||||
.{ "el1.getAttribute('data-hello-world')", "yes" },
|
||||
.{ "el1.setAttribute('data-this-will-work', 'positive')", null },
|
||||
.{ "ds1.thisWillWork", "positive" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -42,8 +42,12 @@ pub const HTMLDocument = struct {
|
||||
// JS funcs
|
||||
// --------
|
||||
|
||||
pub fn get_domain(self: *parser.DocumentHTML) ![]const u8 {
|
||||
return try parser.documentHTMLGetDomain(self);
|
||||
pub fn get_domain(self: *parser.DocumentHTML, page: *Page) ![]const u8 {
|
||||
// libdom's document_html get_domain always returns null, this is
|
||||
// the way MDN recommends getting the domain anyways, since document.domain
|
||||
// is deprecated.
|
||||
const location = try parser.documentHTMLGetLocation(Location, self) orelse return "";
|
||||
return location.get_host(page);
|
||||
}
|
||||
|
||||
pub fn set_domain(_: *parser.DocumentHTML, _: []const u8) ![]const u8 {
|
||||
@@ -81,7 +85,7 @@ pub const HTMLDocument = struct {
|
||||
|
||||
pub fn get_cookie(_: *parser.DocumentHTML, page: *Page) ![]const u8 {
|
||||
var buf: std.ArrayListUnmanaged(u8) = .{};
|
||||
try page.cookie_jar.forRequest(&page.url.uri, buf.writer(page.arena), .{ .navigation = true });
|
||||
try page.cookie_jar.forRequest(&page.url.uri, buf.writer(page.arena), .{ .navigation = true, .is_http = false });
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
@@ -90,6 +94,10 @@ pub const HTMLDocument = struct {
|
||||
// outlives the page's arena.
|
||||
const c = try Cookie.parse(page.cookie_jar.allocator, &page.url.uri, cookie_str);
|
||||
errdefer c.deinit();
|
||||
if (c.http_only) {
|
||||
c.deinit();
|
||||
return ""; // HttpOnly cookies cannot be set from JS
|
||||
}
|
||||
try page.cookie_jar.add(c, std.time.timestamp());
|
||||
return cookie_str;
|
||||
}
|
||||
@@ -229,19 +237,23 @@ pub const HTMLDocument = struct {
|
||||
// Since LightPanda requires the client to know what they are clicking on we do not return the underlying element at this moment
|
||||
// This can currenty only happen if the first pixel is clicked without having rendered any element. This will change when css properties are supported.
|
||||
// This returns an ElementUnion instead of a *Parser.Element in case the element somehow hasn't passed through the js runtime yet.
|
||||
pub fn _elementFromPoint(_: *parser.DocumentHTML, x: f32, y: f32, page: *Page) !?ElementUnion {
|
||||
const ix: i32 = @intFromFloat(@floor(x));
|
||||
const iy: i32 = @intFromFloat(@floor(y));
|
||||
const element = page.renderer.getElementAtPosition(ix, iy) orelse return null;
|
||||
// While x and y should be f32, here we take i32 since that's what our
|
||||
// "renderer" uses. By specifying i32 here, rather than f32 and doing the
|
||||
// conversion ourself, we rely on v8's type conversion which is both more
|
||||
// flexible (e.g. handles NaN) and will be more consistent with a browser.
|
||||
pub fn _elementFromPoint(_: *parser.DocumentHTML, x: i32, y: i32, page: *Page) !?ElementUnion {
|
||||
const element = page.renderer.getElementAtPosition(x, y) orelse return null;
|
||||
// TODO if pointer-events set to none the underlying element should be returned (parser.documentGetDocumentElement(self.document);?)
|
||||
return try Element.toInterface(element);
|
||||
}
|
||||
|
||||
// Returns an array of all elements at the specified coordinates (relative to the viewport). The elements are ordered from the topmost to the bottommost box of the viewport.
|
||||
pub fn _elementsFromPoint(_: *parser.DocumentHTML, x: f32, y: f32, page: *Page) ![]ElementUnion {
|
||||
const ix: i32 = @intFromFloat(@floor(x));
|
||||
const iy: i32 = @intFromFloat(@floor(y));
|
||||
const element = page.renderer.getElementAtPosition(ix, iy) orelse return &.{};
|
||||
// While x and y should be f32, here we take i32 since that's what our
|
||||
// "renderer" uses. By specifying i32 here, rather than f32 and doing the
|
||||
// conversion ourself, we rely on v8's type conversion which is both more
|
||||
// flexible (e.g. handles NaN) and will be more consistent with a browser.
|
||||
pub fn _elementsFromPoint(_: *parser.DocumentHTML, x: i32, y: i32, page: *Page) ![]ElementUnion {
|
||||
const element = page.renderer.getElementAtPosition(x, y) orelse return &.{};
|
||||
// TODO if pointer-events set to none the underlying element should be returned (parser.documentGetDocumentElement(self.document);?)
|
||||
|
||||
var list: std.ArrayListUnmanaged(ElementUnion) = .empty;
|
||||
@@ -266,15 +278,17 @@ pub const HTMLDocument = struct {
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
state.ready_state = .interactive;
|
||||
|
||||
const evt = try parser.eventCreate();
|
||||
defer parser.eventDestroy(evt);
|
||||
|
||||
log.debug(.script_event, "dispatch event", .{
|
||||
.type = "DOMContentLoaded",
|
||||
.source = "document",
|
||||
});
|
||||
|
||||
const evt = try parser.eventCreate();
|
||||
defer parser.eventDestroy(evt);
|
||||
try parser.eventInit(evt, "DOMContentLoaded", .{ .bubbles = true, .cancelable = true });
|
||||
_ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, self), evt);
|
||||
|
||||
try page.window.dispatchForDocumentTarget(evt);
|
||||
}
|
||||
|
||||
pub fn documentIsComplete(self: *parser.DocumentHTML, page: *Page) !void {
|
||||
@@ -299,7 +313,7 @@ test "Browser.HTML.Document" {
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.domain", "" },
|
||||
.{ "document.domain", "lightpanda.io" },
|
||||
.{ "document.referrer", "" },
|
||||
.{ "document.title", "" },
|
||||
.{ "document.body.localName", "body" },
|
||||
@@ -333,6 +347,8 @@ test "Browser.HTML.Document" {
|
||||
.{ "document.cookie = 'name=Oeschger; SameSite=None; Secure'", "name=Oeschger; SameSite=None; Secure" },
|
||||
.{ "document.cookie = 'favorite_food=tripe; SameSite=None; Secure'", "favorite_food=tripe; SameSite=None; Secure" },
|
||||
.{ "document.cookie", "name=Oeschger; favorite_food=tripe" },
|
||||
.{ "document.cookie = 'IgnoreMy=Ghost; HttpOnly'", null }, // "" should be returned, but the framework overrules it atm
|
||||
.{ "document.cookie", "name=Oeschger; favorite_food=tripe" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
const std = @import("std");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const generate = @import("../../runtime/generate.zig");
|
||||
@@ -26,7 +27,9 @@ const urlStitch = @import("../../url.zig").URL.stitch;
|
||||
const URL = @import("../url/url.zig").URL;
|
||||
const Node = @import("../dom/node.zig").Node;
|
||||
const Element = @import("../dom/element.zig").Element;
|
||||
const DataSet = @import("DataSet.zig");
|
||||
|
||||
const StyleSheet = @import("../cssom/stylesheet.zig").StyleSheet;
|
||||
const CSSStyleDeclaration = @import("../cssom/css_style_declaration.zig").CSSStyleDeclaration;
|
||||
|
||||
// HTMLElement interfaces
|
||||
@@ -117,6 +120,15 @@ pub const HTMLElement = struct {
|
||||
return &state.style;
|
||||
}
|
||||
|
||||
pub fn get_dataset(e: *parser.ElementHTML, page: *Page) !*DataSet {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(e));
|
||||
if (state.dataset) |*ds| {
|
||||
return ds;
|
||||
}
|
||||
state.dataset = DataSet{ .element = @ptrCast(e) };
|
||||
return &state.dataset.?;
|
||||
}
|
||||
|
||||
pub fn get_innerText(e: *parser.ElementHTML) ![]const u8 {
|
||||
const n = @as(*parser.Node, @ptrCast(e));
|
||||
return try parser.nodeTextContent(n) orelse "";
|
||||
@@ -244,8 +256,18 @@ pub const HTMLAnchorElement = struct {
|
||||
return try parser.nodeSetTextContent(parser.anchorToNode(self), v);
|
||||
}
|
||||
|
||||
inline fn url(self: *parser.Anchor, page: *Page) !URL {
|
||||
return URL.constructor(.{ .element = @alignCast(@ptrCast(self)) }, null, page); // TODO inject base url
|
||||
fn url(self: *parser.Anchor, page: *Page) !URL {
|
||||
// Although the URL.constructor union accepts an .{.element = X}, we
|
||||
// can't use this here because the behavior is different.
|
||||
// URL.constructor(document.createElement('a')
|
||||
// should fail (a.href isn't a valid URL)
|
||||
// But
|
||||
// document.createElement('a').host
|
||||
// should not fail, it should return an empty string
|
||||
if (try parser.elementGetAttribute(@alignCast(@ptrCast(self)), "href")) |href| {
|
||||
return URL.constructor(.{ .string = href }, null, page); // TODO inject base url
|
||||
}
|
||||
return .empty;
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
@@ -613,6 +635,7 @@ pub const HTMLImageElement = struct {
|
||||
pub const Factory = struct {
|
||||
pub const js_name = "Image";
|
||||
pub const subtype = .node;
|
||||
|
||||
pub const js_legacy_factory = true;
|
||||
pub const prototype = *HTMLImageElement;
|
||||
|
||||
@@ -737,6 +760,15 @@ pub const HTMLLinkElement = struct {
|
||||
pub const Self = parser.Link;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn get_href(self: *parser.Link) ![]const u8 {
|
||||
return try parser.linkGetHref(self);
|
||||
}
|
||||
|
||||
pub fn set_href(self: *parser.Link, href: []const u8, page: *const Page) !void {
|
||||
const full = try urlStitch(page.call_arena, href, page.url.raw, .{});
|
||||
return try parser.linkSetHref(self, full);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLMapElement = struct {
|
||||
@@ -981,6 +1013,18 @@ pub const HTMLStyleElement = struct {
|
||||
pub const Self = parser.Style;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn get_sheet(self: *parser.Style, page: *Page) !*StyleSheet {
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
if (state.style_sheet) |ss| {
|
||||
return ss;
|
||||
}
|
||||
|
||||
const ss = try page.arena.create(StyleSheet);
|
||||
ss.* = .{};
|
||||
state.style_sheet = ss;
|
||||
return ss;
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLTableElement = struct {
|
||||
@@ -1023,6 +1067,16 @@ pub const HTMLTemplateElement = struct {
|
||||
pub const Self = parser.Template;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn get_content(self: *parser.Template, page: *Page) !*parser.DocumentFragment {
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
if (state.template_content) |tc| {
|
||||
return tc;
|
||||
}
|
||||
const tc = try parser.documentCreateDocumentFragment(@ptrCast(page.window.document));
|
||||
state.template_content = tc;
|
||||
return tc;
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLTextAreaElement = struct {
|
||||
@@ -1064,6 +1118,7 @@ pub const HTMLVideoElement = struct {
|
||||
pub fn toInterface(comptime T: type, e: *parser.Element) !T {
|
||||
const elem: *align(@alignOf(*parser.Element)) parser.Element = @alignCast(e);
|
||||
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(elem)));
|
||||
|
||||
return switch (tag) {
|
||||
.abbr, .acronym, .address, .article, .aside, .b, .basefont, .bdi, .bdo, .bgsound, .big, .center, .cite, .code, .dd, .details, .dfn, .dt, .em, .figcaption, .figure, .footer, .header, .hgroup, .i, .isindex, .keygen, .kbd, .main, .mark, .marquee, .menu, .menuitem, .nav, .nobr, .noframes, .noscript, .rp, .rt, .ruby, .s, .samp, .section, .small, .spacer, .strike, .strong, .sub, .summary, .sup, .tt, .u, .wbr, ._var => .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(elem)) },
|
||||
.a => .{ .HTMLAnchorElement = @as(*parser.Anchor, @ptrCast(elem)) },
|
||||
@@ -1262,6 +1317,8 @@ test "Browser.HTML.Element" {
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let a = document.createElement('a');", null },
|
||||
.{ "a.href", "" },
|
||||
.{ "a.host", "" },
|
||||
.{ "a.href = 'about'", null },
|
||||
.{ "a.href", "https://lightpanda.io/opensource-browser/about" },
|
||||
}, .{});
|
||||
@@ -1272,8 +1329,26 @@ test "Browser.HTML.Element" {
|
||||
.{ "document.createElement('a').focus()", null },
|
||||
.{ "document.activeElement === focused", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let l2 = document.createElement('link');", null },
|
||||
.{ "l2.href", "" },
|
||||
.{ "l2.href = 'https://lightpanda.io/opensource-browser/15'", null },
|
||||
.{ "l2.href", "https://lightpanda.io/opensource-browser/15" },
|
||||
|
||||
.{ "l2.href = '/over/9000'", null },
|
||||
.{ "l2.href", "https://lightpanda.io/over/9000" },
|
||||
}, .{});
|
||||
}
|
||||
test "Browser.HTML.HtmlInputElement.propeties" {
|
||||
|
||||
test "Browser.HTML.Element.DataSet" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "<div id=x data-power='over 9000' data-empty data-some-long-key=ok></div>" });
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{ .{ "let div = document.getElementById('x')", null }, .{ "div.dataset.nope", "undefined" }, .{ "div.dataset.power", "over 9000" }, .{ "div.dataset.empty", "" }, .{ "div.dataset.someLongKey", "ok" }, .{ "delete div.dataset.power", "true" }, .{ "div.dataset.power", "undefined" } }, .{});
|
||||
}
|
||||
|
||||
test "Browser.HTML.HtmlInputElement.properties" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .url = "https://lightpanda.io/noslashattheend" });
|
||||
defer runner.deinit();
|
||||
var alloc = std.heap.ArenaAllocator.init(runner.app.allocator);
|
||||
@@ -1357,7 +1432,8 @@ test "Browser.HTML.HtmlInputElement.propeties" {
|
||||
.{ "input_value.value", "mango" }, // Still mango
|
||||
}, .{});
|
||||
}
|
||||
test "Browser.HTML.HtmlInputElement.propeties.form" {
|
||||
|
||||
test "Browser.HTML.HtmlInputElement.properties.form" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
|
||||
\\ <form action="test.php" target="_blank">
|
||||
\\ <p>
|
||||
@@ -1375,6 +1451,33 @@ test "Browser.HTML.HtmlInputElement.propeties.form" {
|
||||
}, .{});
|
||||
}
|
||||
|
||||
test "Browser.HTML.HTMLTemplateElement" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "<div id=c></div>" });
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let t = document.createElement('template')", null },
|
||||
.{ "let d = document.createElement('div')", null },
|
||||
.{ "d.id = 'abc'", null },
|
||||
.{ "t.content.append(d)", null },
|
||||
.{ "document.getElementById('abc')", "null" },
|
||||
.{ "document.getElementById('c').appendChild(t.content.cloneNode(true))", null },
|
||||
.{ "document.getElementById('abc').id", "abc" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
test "Browser.HTML.HTMLStyleElement" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "" });
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let s = document.createElement('style')", null },
|
||||
.{ "s.sheet.type", "text/css" },
|
||||
.{ "s.sheet == s.sheet", "true" },
|
||||
.{ "document.createElement('style').sheet == s.sheet", "false" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
const Check = struct {
|
||||
input: []const u8,
|
||||
expected: ?[]const u8 = null, // Needed when input != expected
|
||||
|
||||
114
src/browser/html/error_event.zig
Normal file
114
src/browser/html/error_event.zig
Normal file
@@ -0,0 +1,114 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
const Env = @import("../env.zig").Env;
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent
|
||||
pub const ErrorEvent = struct {
|
||||
pub const prototype = *parser.Event;
|
||||
pub const union_make_copy = true;
|
||||
|
||||
proto: parser.Event,
|
||||
message: []const u8,
|
||||
filename: []const u8,
|
||||
lineno: i32,
|
||||
colno: i32,
|
||||
@"error": ?Env.JsObject,
|
||||
|
||||
const ErrorEventInit = struct {
|
||||
message: []const u8 = "",
|
||||
filename: []const u8 = "",
|
||||
lineno: i32 = 0,
|
||||
colno: i32 = 0,
|
||||
@"error": ?Env.JsObject = null,
|
||||
};
|
||||
|
||||
pub fn constructor(event_type: []const u8, opts: ?ErrorEventInit) !ErrorEvent {
|
||||
const event = try parser.eventCreate();
|
||||
defer parser.eventDestroy(event);
|
||||
try parser.eventInit(event, event_type, .{});
|
||||
try parser.eventSetInternalType(event, .event);
|
||||
|
||||
const o = opts orelse ErrorEventInit{};
|
||||
|
||||
return .{
|
||||
.proto = event.*,
|
||||
.message = o.message,
|
||||
.filename = o.filename,
|
||||
.lineno = o.lineno,
|
||||
.colno = o.colno,
|
||||
.@"error" = if (o.@"error") |e| try e.persist() else null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_message(self: *const ErrorEvent) []const u8 {
|
||||
return self.message;
|
||||
}
|
||||
|
||||
pub fn get_filename(self: *const ErrorEvent) []const u8 {
|
||||
return self.filename;
|
||||
}
|
||||
|
||||
pub fn get_lineno(self: *const ErrorEvent) i32 {
|
||||
return self.lineno;
|
||||
}
|
||||
|
||||
pub fn get_colno(self: *const ErrorEvent) i32 {
|
||||
return self.colno;
|
||||
}
|
||||
|
||||
pub fn get_error(self: *const ErrorEvent) Env.UndefinedOr(Env.JsObject) {
|
||||
if (self.@"error") |e| {
|
||||
return .{ .value = e };
|
||||
}
|
||||
return .undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.ErrorEvent" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "<div id=c></div>" });
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let e1 = new ErrorEvent('err1')", null },
|
||||
.{ "e1.message", "" },
|
||||
.{ "e1.filename", "" },
|
||||
.{ "e1.lineno", "0" },
|
||||
.{ "e1.colno", "0" },
|
||||
.{ "e1.error", "undefined" },
|
||||
|
||||
.{
|
||||
\\ let e2 = new ErrorEvent('err1', {
|
||||
\\ message: 'm1',
|
||||
\\ filename: 'fx19',
|
||||
\\ lineno: 443,
|
||||
\\ colno: 8999,
|
||||
\\ error: 'under 9000!',
|
||||
\\
|
||||
\\})
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "e2.message", "m1" },
|
||||
.{ "e2.filename", "fx19" },
|
||||
.{ "e2.lineno", "443" },
|
||||
.{ "e2.colno", "8999" },
|
||||
.{ "e2.error", "under 9000!" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -24,7 +24,6 @@ const Navigator = @import("navigator.zig").Navigator;
|
||||
const History = @import("history.zig").History;
|
||||
const Location = @import("location.zig").Location;
|
||||
const MediaQueryList = @import("media_query_list.zig").MediaQueryList;
|
||||
const Performance = @import("performance.zig").Performance;
|
||||
|
||||
pub const Interfaces = .{
|
||||
HTMLDocument,
|
||||
@@ -37,5 +36,8 @@ pub const Interfaces = .{
|
||||
History,
|
||||
Location,
|
||||
MediaQueryList,
|
||||
Performance,
|
||||
@import("DataSet.zig"),
|
||||
@import("screen.zig").Interfaces,
|
||||
@import("error_event.zig").ErrorEvent,
|
||||
@import("AbortController.zig").Interfaces,
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@ pub const MediaQueryList = struct {
|
||||
|
||||
// Extend libdom event target for pure zig struct.
|
||||
// This is not safe as it relies on a structure layout that isn't guaranteed
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{},
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .media_query_list },
|
||||
|
||||
matches: bool,
|
||||
media: []const u8,
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Performance
|
||||
pub const Performance = struct {
|
||||
pub const prototype = *EventTarget;
|
||||
|
||||
// Extend libdom event target for pure zig struct.
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{},
|
||||
|
||||
time_origin: std.time.Timer,
|
||||
// if (Window.crossOriginIsolated) -> Resolution in isolated contexts: 5 microseconds
|
||||
// else -> Resolution in non-isolated contexts: 100 microseconds
|
||||
const ms_resolution = 100;
|
||||
|
||||
fn limitedResolutionMs(nanoseconds: u64) f64 {
|
||||
const elapsed_at_resolution = ((nanoseconds / std.time.ns_per_us) + ms_resolution / 2) / ms_resolution * ms_resolution;
|
||||
const elapsed = @as(f64, @floatFromInt(elapsed_at_resolution));
|
||||
return elapsed / @as(f64, std.time.us_per_ms);
|
||||
}
|
||||
|
||||
pub fn get_timeOrigin(self: *const Performance) f64 {
|
||||
const is_posix = switch (@import("builtin").os.tag) { // From std.time.zig L125
|
||||
.windows, .uefi, .wasi => false,
|
||||
else => true,
|
||||
};
|
||||
const zero = std.time.Instant{ .timestamp = if (!is_posix) 0 else .{ .sec = 0, .nsec = 0 } };
|
||||
const started = self.time_origin.started.since(zero);
|
||||
return limitedResolutionMs(started);
|
||||
}
|
||||
|
||||
pub fn _now(self: *Performance) f64 {
|
||||
return limitedResolutionMs(self.time_origin.read());
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("./../../testing.zig");
|
||||
|
||||
test "Performance: get_timeOrigin" {
|
||||
var perf = Performance{ .time_origin = try std.time.Timer.start() };
|
||||
const time_origin = perf.get_timeOrigin();
|
||||
try testing.expect(time_origin >= 0);
|
||||
|
||||
// Check resolution
|
||||
try testing.expectDelta(@rem(time_origin * std.time.us_per_ms, 100.0), 0.0, 0.1);
|
||||
}
|
||||
|
||||
test "Performance: now" {
|
||||
var perf = Performance{ .time_origin = try std.time.Timer.start() };
|
||||
|
||||
// Monotonically increasing
|
||||
var now = perf._now();
|
||||
while (now <= 0) { // Loop for now to not be 0
|
||||
try testing.expectEqual(now, 0);
|
||||
now = perf._now();
|
||||
}
|
||||
// Check resolution
|
||||
try testing.expectDelta(@rem(now * std.time.us_per_ms, 100.0), 0.0, 0.1);
|
||||
|
||||
var after = perf._now();
|
||||
while (after <= now) { // Loop untill after > now
|
||||
try testing.expectEqual(after, now);
|
||||
after = perf._now();
|
||||
}
|
||||
// Check resolution
|
||||
try testing.expectDelta(@rem(after * std.time.us_per_ms, 100.0), 0.0, 0.1);
|
||||
}
|
||||
109
src/browser/html/screen.zig
Normal file
109
src/browser/html/screen.zig
Normal file
@@ -0,0 +1,109 @@
|
||||
// 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 EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
|
||||
pub const Interfaces = .{
|
||||
Screen,
|
||||
ScreenOrientation,
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Screen
|
||||
pub const Screen = struct {
|
||||
pub const prototype = *EventTarget;
|
||||
|
||||
height: u32 = 1080,
|
||||
width: u32 = 1920,
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Screen/colorDepth
|
||||
color_depth: u32 = 8,
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Screen/pixelDepth
|
||||
pixel_depth: u32 = 8,
|
||||
orientation: ScreenOrientation = .{ .type = .landscape_primary },
|
||||
|
||||
pub fn get_availHeight(self: *const Screen) u32 {
|
||||
return self.height;
|
||||
}
|
||||
|
||||
pub fn get_availWidth(self: *const Screen) u32 {
|
||||
return self.width;
|
||||
}
|
||||
|
||||
pub fn get_height(self: *const Screen) u32 {
|
||||
return self.height;
|
||||
}
|
||||
|
||||
pub fn get_width(self: *const Screen) u32 {
|
||||
return self.width;
|
||||
}
|
||||
|
||||
pub fn get_pixelDepth(self: *const Screen) u32 {
|
||||
return self.pixel_depth;
|
||||
}
|
||||
|
||||
pub fn get_orientation(self: *const Screen) ScreenOrientation {
|
||||
return self.orientation;
|
||||
}
|
||||
};
|
||||
|
||||
const ScreenOrientationType = enum {
|
||||
portrait_primary,
|
||||
portrait_secondary,
|
||||
landscape_primary,
|
||||
landscape_secondary,
|
||||
|
||||
pub fn toString(self: ScreenOrientationType) []const u8 {
|
||||
return switch (self) {
|
||||
.portrait_primary => "portrait-primary",
|
||||
.portrait_secondary => "portrait-secondary",
|
||||
.landscape_primary => "landscape-primary",
|
||||
.landscape_secondary => "landscape-secondary",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const ScreenOrientation = struct {
|
||||
pub const prototype = *EventTarget;
|
||||
|
||||
angle: u32 = 0,
|
||||
type: ScreenOrientationType,
|
||||
|
||||
pub fn get_angle(self: *const ScreenOrientation) u32 {
|
||||
return self.angle;
|
||||
}
|
||||
|
||||
pub fn get_type(self: *const ScreenOrientation) []const u8 {
|
||||
return self.type.toString();
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.Screen" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let screen = window.screen", "undefined" },
|
||||
.{ "screen.width === 1920", "true" },
|
||||
.{ "screen.height === 1080", "true" },
|
||||
.{ "let orientation = screen.orientation", "undefined" },
|
||||
.{ "orientation.angle === 0", "true" },
|
||||
.{ "orientation.type === \"landscape-primary\"", "true" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -20,7 +20,7 @@ const std = @import("std");
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Function = @import("../env.zig").Function;
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const Loop = @import("../../runtime/loop.zig").Loop;
|
||||
|
||||
@@ -31,8 +31,13 @@ const Crypto = @import("../crypto/crypto.zig").Crypto;
|
||||
const Console = @import("../console/console.zig").Console;
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const MediaQueryList = @import("media_query_list.zig").MediaQueryList;
|
||||
const Performance = @import("performance.zig").Performance;
|
||||
const Performance = @import("../dom/performance.zig").Performance;
|
||||
const CSSStyleDeclaration = @import("../cssom/css_style_declaration.zig").CSSStyleDeclaration;
|
||||
const Screen = @import("screen.zig").Screen;
|
||||
const Css = @import("../css/css.zig").Css;
|
||||
|
||||
const Function = Env.Function;
|
||||
const JsObject = Env.JsObject;
|
||||
|
||||
const storage = @import("../storage/storage.zig");
|
||||
|
||||
@@ -42,7 +47,7 @@ pub const Window = struct {
|
||||
pub const prototype = *EventTarget;
|
||||
|
||||
// Extend libdom event target for pure zig struct.
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{},
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .window },
|
||||
|
||||
document: *parser.DocumentHTML,
|
||||
target: []const u8 = "",
|
||||
@@ -58,6 +63,8 @@ pub const Window = struct {
|
||||
console: Console = .{},
|
||||
navigator: Navigator = .{},
|
||||
performance: Performance,
|
||||
screen: Screen = .{},
|
||||
css: Css = .{},
|
||||
|
||||
pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window {
|
||||
var fbs = std.io.fixedBufferStream("");
|
||||
@@ -163,6 +170,14 @@ pub const Window = struct {
|
||||
return &self.performance;
|
||||
}
|
||||
|
||||
pub fn get_screen(self: *Window) *Screen {
|
||||
return &self.screen;
|
||||
}
|
||||
|
||||
pub fn get_CSS(self: *Window) *Css {
|
||||
return &self.css;
|
||||
}
|
||||
|
||||
pub fn _requestAnimationFrame(self: *Window, cbk: Function, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, 5, page, .{ .animation_frame = true });
|
||||
}
|
||||
@@ -172,14 +187,12 @@ pub const Window = struct {
|
||||
return page.loop.cancel(kv.value.loop_id);
|
||||
}
|
||||
|
||||
// TODO handle callback arguments.
|
||||
pub fn _setTimeout(self: *Window, cbk: Function, delay: ?u32, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, delay, page, .{});
|
||||
pub fn _setTimeout(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, delay, page, .{ .args = params });
|
||||
}
|
||||
|
||||
// TODO handle callback arguments.
|
||||
pub fn _setInterval(self: *Window, cbk: Function, delay: ?u32, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, delay, page, .{ .repeat = true });
|
||||
pub fn _setInterval(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, delay, page, .{ .repeat = true, .args = params });
|
||||
}
|
||||
|
||||
pub fn _clearTimeout(self: *Window, id: u32, page: *Page) !void {
|
||||
@@ -192,6 +205,10 @@ pub const Window = struct {
|
||||
return page.loop.cancel(kv.value.loop_id);
|
||||
}
|
||||
|
||||
pub fn _queueMicrotask(self: *Window, cbk: Function, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, 0, page, .{});
|
||||
}
|
||||
|
||||
pub fn _matchMedia(_: *const Window, media: []const u8, page: *Page) !MediaQueryList {
|
||||
return .{
|
||||
.matches = false, // TODO?
|
||||
@@ -199,11 +216,27 @@ pub const Window = struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _btoa(_: *const Window, value: []const u8, page: *Page) ![]const u8 {
|
||||
const Encoder = std.base64.standard.Encoder;
|
||||
const out = try page.call_arena.alloc(u8, Encoder.calcSize(value.len));
|
||||
return Encoder.encode(out, value);
|
||||
}
|
||||
|
||||
pub fn _atob(_: *const Window, value: []const u8, page: *Page) ![]const u8 {
|
||||
const Decoder = std.base64.standard.Decoder;
|
||||
const size = Decoder.calcSizeForSlice(value) catch return error.InvalidCharacterError;
|
||||
|
||||
const out = try page.call_arena.alloc(u8, size);
|
||||
Decoder.decode(out, value) catch return error.InvalidCharacterError;
|
||||
return out;
|
||||
}
|
||||
|
||||
const CreateTimeoutOpts = struct {
|
||||
args: []Env.JsObject = &.{},
|
||||
repeat: bool = false,
|
||||
animation_frame: bool = false,
|
||||
};
|
||||
fn createTimeout(self: *Window, cbk: Function, delay_: ?u32, page: *Page, comptime opts: CreateTimeoutOpts) !u32 {
|
||||
fn createTimeout(self: *Window, cbk: Function, delay_: ?u32, page: *Page, opts: CreateTimeoutOpts) !u32 {
|
||||
const delay = delay_ orelse 0;
|
||||
if (delay > 5000) {
|
||||
log.warn(.user_script, "long timeout ignored", .{ .delay = delay, .interval = opts.repeat });
|
||||
@@ -228,6 +261,15 @@ pub const Window = struct {
|
||||
}
|
||||
errdefer _ = self.timers.remove(timer_id);
|
||||
|
||||
const args = opts.args;
|
||||
var persisted_args: []Env.JsObject = &.{};
|
||||
if (args.len > 0) {
|
||||
persisted_args = try page.arena.alloc(Env.JsObject, args.len);
|
||||
for (args, persisted_args) |a, *ca| {
|
||||
ca.* = try a.persist();
|
||||
}
|
||||
}
|
||||
|
||||
const delay_ms: u63 = @as(u63, delay) * std.time.ns_per_ms;
|
||||
const callback = try arena.create(TimerCallback);
|
||||
|
||||
@@ -236,6 +278,7 @@ pub const Window = struct {
|
||||
.loop_id = 0, // we're going to set this to a real value shortly
|
||||
.window = self,
|
||||
.timer_id = timer_id,
|
||||
.args = persisted_args,
|
||||
.node = .{ .func = TimerCallback.run },
|
||||
.repeat = if (opts.repeat) delay_ms else null,
|
||||
.animation_frame = opts.animation_frame,
|
||||
@@ -265,9 +308,48 @@ pub const Window = struct {
|
||||
behavior: []const u8,
|
||||
};
|
||||
};
|
||||
pub fn _scrollTo(_: *const Window, opts: ScrollToOpts, y: ?u32) void {
|
||||
pub fn _scrollTo(self: *Window, opts: ScrollToOpts, y: ?u32) !void {
|
||||
_ = opts;
|
||||
_ = y;
|
||||
|
||||
{
|
||||
const scroll_event = try parser.eventCreate();
|
||||
defer parser.eventDestroy(scroll_event);
|
||||
|
||||
try parser.eventInit(scroll_event, "scroll", .{});
|
||||
_ = try parser.eventTargetDispatchEvent(
|
||||
parser.toEventTarget(Window, self),
|
||||
scroll_event,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const scroll_end = try parser.eventCreate();
|
||||
defer parser.eventDestroy(scroll_end);
|
||||
|
||||
try parser.eventInit(scroll_end, "scrollend", .{});
|
||||
_ = try parser.eventTargetDispatchEvent(
|
||||
parser.toEventTarget(parser.DocumentHTML, self.document),
|
||||
scroll_end,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// libdom's document doesn't have a parent, which is correct, but
|
||||
// breaks the event bubbling that happens for many events from
|
||||
// document -> window.
|
||||
// We need to force dispatch this event on the window, with the
|
||||
// document target.
|
||||
// In theory, we should do this for a lot of events and might need
|
||||
// to come up with a good way to solve this more generically. But
|
||||
// this specific event, and maybe a few others in the near future,
|
||||
// are blockers.
|
||||
// Worth noting that NetSurf itself appears to do something similar:
|
||||
// https://github.com/netsurf-browser/netsurf/blob/a32e1a03e1c91ee9f0aa211937dbae7a96831149/content/handlers/html/html.c#L380
|
||||
pub fn dispatchForDocumentTarget(self: *Window, evt: *parser.Event) !void {
|
||||
// we assume that this evt has already been dispatched on the document
|
||||
// and thus the target has already been set to the document.
|
||||
return self.base.redispatchEvent(evt);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -292,6 +374,8 @@ const TimerCallback = struct {
|
||||
|
||||
window: *Window,
|
||||
|
||||
args: []Env.JsObject = &.{},
|
||||
|
||||
fn run(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
|
||||
const self: *TimerCallback = @fieldParentPtr("node", node);
|
||||
|
||||
@@ -301,11 +385,11 @@ const TimerCallback = struct {
|
||||
if (self.animation_frame) {
|
||||
call = self.cbk.tryCall(void, .{self.window.performance._now()}, &result);
|
||||
} else {
|
||||
call = self.cbk.tryCall(void, .{}, &result);
|
||||
call = self.cbk.tryCall(void, self.args, &result);
|
||||
}
|
||||
|
||||
call catch {
|
||||
log.debug(.user_script, "callback error", .{
|
||||
log.warn(.user_script, "callback error", .{
|
||||
.err = result.exception,
|
||||
.stack = result.stack,
|
||||
.source = "window timeout",
|
||||
@@ -337,20 +421,15 @@ test "Browser.HTML.Window" {
|
||||
// Note however that we in this test do not wait as the request is just send to the browser
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ let start;
|
||||
\\ let start = 0;
|
||||
\\ function step(timestamp) {
|
||||
\\ if (start === undefined) {
|
||||
\\ start = timestamp;
|
||||
\\ }
|
||||
\\ const elapsed = timestamp - start;
|
||||
\\ if (elapsed < 2000) {
|
||||
\\ requestAnimationFrame(step);
|
||||
\\ }
|
||||
\\ }
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "requestAnimationFrame(step);", null }, // returned id is checked in the next test
|
||||
.{ " start > 0", "true" },
|
||||
}, .{});
|
||||
|
||||
// cancelAnimationFrame should be able to cancel a request with the given id
|
||||
@@ -380,10 +459,70 @@ test "Browser.HTML.Window" {
|
||||
.{ "innerWidth", "2" },
|
||||
}, .{});
|
||||
|
||||
// cancelAnimationFrame should be able to cancel a request with the given id
|
||||
try runner.testCases(&.{
|
||||
.{ "let longCall = false;", null },
|
||||
.{ "window.setTimeout(() => {longCall = true}, 5001);", null },
|
||||
.{ "longCall;", "false" },
|
||||
|
||||
.{ "let wst = 0;", null },
|
||||
.{ "window.setTimeout(() => {wst += 1}, 1)", null },
|
||||
.{ "wst", "1" },
|
||||
|
||||
.{ "window.setTimeout((a, b) => {wst = a + b}, 1, 2, 3)", null },
|
||||
.{ "wst", "5" },
|
||||
}, .{});
|
||||
|
||||
// window event target
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ let called = false;
|
||||
\\ window.addEventListener("ready", (e) => {
|
||||
\\ called = (e.currentTarget == window);
|
||||
\\ }, {capture: false, once: false});
|
||||
\\ const evt = new Event("ready", { bubbles: true, cancelable: false });
|
||||
\\ window.dispatchEvent(evt);
|
||||
\\ called;
|
||||
,
|
||||
"true",
|
||||
},
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const b64 = btoa('https://ziglang.org/documentation/master/std/#std.base64.Base64Decoder')", "undefined" },
|
||||
.{ "b64", "aHR0cHM6Ly96aWdsYW5nLm9yZy9kb2N1bWVudGF0aW9uL21hc3Rlci9zdGQvI3N0ZC5iYXNlNjQuQmFzZTY0RGVjb2Rlcg==" },
|
||||
.{ "const str = atob(b64)", "undefined" },
|
||||
.{ "str", "https://ziglang.org/documentation/master/std/#std.base64.Base64Decoder" },
|
||||
.{ "try { atob('b') } catch (e) { e } ", "Error: InvalidCharacterError" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let scroll = false; let scrolend = false", null },
|
||||
.{ "window.addEventListener('scroll', () => {scroll = true});", null },
|
||||
.{ "document.addEventListener('scrollend', () => {scrollend = true});", null },
|
||||
.{ "window.scrollTo(0)", null },
|
||||
.{ "scroll", "true" },
|
||||
.{ "scrollend", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var qm = false; window.queueMicrotask(() => {qm = true });", null },
|
||||
.{ "qm", "true" },
|
||||
}, .{});
|
||||
|
||||
{
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ let dcl = false;
|
||||
\\ window.addEventListener('DOMContentLoaded', (e) => {
|
||||
\\ dcl = e.target == document;
|
||||
\\ });
|
||||
,
|
||||
null,
|
||||
},
|
||||
}, .{});
|
||||
try runner.dispatchDOMContentLoaded();
|
||||
try runner.testCases(&.{
|
||||
.{ "dcl", "true" },
|
||||
}, .{});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ pub const Mime = struct {
|
||||
text_html,
|
||||
text_javascript,
|
||||
text_plain,
|
||||
text_css,
|
||||
application_json,
|
||||
unknown,
|
||||
other,
|
||||
};
|
||||
@@ -44,6 +46,8 @@ pub const Mime = struct {
|
||||
text_html: void,
|
||||
text_javascript: void,
|
||||
text_plain: void,
|
||||
text_css: void,
|
||||
application_json: void,
|
||||
unknown: void,
|
||||
other: struct { type: []const u8, sub_type: []const u8 },
|
||||
};
|
||||
@@ -174,18 +178,22 @@ pub const Mime = struct {
|
||||
if (std.meta.stringToEnum(enum {
|
||||
@"text/xml",
|
||||
@"text/html",
|
||||
@"text/css",
|
||||
@"text/plain",
|
||||
|
||||
@"text/javascript",
|
||||
@"application/javascript",
|
||||
@"application/x-javascript",
|
||||
|
||||
@"text/plain",
|
||||
@"application/json",
|
||||
}, type_name)) |known_type| {
|
||||
const ct: ContentType = switch (known_type) {
|
||||
.@"text/xml" => .{ .text_xml = {} },
|
||||
.@"text/html" => .{ .text_html = {} },
|
||||
.@"text/javascript", .@"application/javascript", .@"application/x-javascript" => .{ .text_javascript = {} },
|
||||
.@"text/plain" => .{ .text_plain = {} },
|
||||
.@"text/css" => .{ .text_css = {} },
|
||||
.@"application/json" => .{ .application_json = {} },
|
||||
};
|
||||
return .{ ct, attribute_start };
|
||||
}
|
||||
@@ -218,7 +226,9 @@ pub const Mime = struct {
|
||||
|
||||
fn parseAttributeValue(arena: Allocator, value: []const u8) ![]const u8 {
|
||||
if (value[0] != '"') {
|
||||
return value;
|
||||
// almost certainly referenced from an http.Request which has its
|
||||
// own lifetime.
|
||||
return arena.dupe(u8, value);
|
||||
}
|
||||
|
||||
// 1 to skip the opening quote
|
||||
@@ -349,6 +359,9 @@ test "Mime: parse common" {
|
||||
try expect(.{ .content_type = .{ .text_javascript = {} } }, "text/javascript");
|
||||
try expect(.{ .content_type = .{ .text_javascript = {} } }, "Application/JavaScript");
|
||||
try expect(.{ .content_type = .{ .text_javascript = {} } }, "application/x-javascript");
|
||||
|
||||
try expect(.{ .content_type = .{ .application_json = {} } }, "application/json");
|
||||
try expect(.{ .content_type = .{ .text_css = {} } }, "text/css");
|
||||
}
|
||||
|
||||
test "Mime: parse uncommon" {
|
||||
|
||||
@@ -525,6 +525,10 @@ pub const EventType = enum(u8) {
|
||||
progress_event = 1,
|
||||
custom_event = 2,
|
||||
mouse_event = 3,
|
||||
error_event = 4,
|
||||
abort_signal = 5,
|
||||
xhr_event = 6,
|
||||
message_event = 7,
|
||||
};
|
||||
|
||||
pub const MutationEvent = c.dom_mutation_event;
|
||||
@@ -746,6 +750,13 @@ pub fn eventTargetDispatchEvent(et: *EventTarget, event: *Event) !bool {
|
||||
return res;
|
||||
}
|
||||
|
||||
pub fn eventTargetInternalType(et: *EventTarget) !EventTargetTBase.InternalType {
|
||||
var res: u32 = undefined;
|
||||
const err = eventTargetVtable(et).internal_type.?(et, &res);
|
||||
try DOMErr(err);
|
||||
return @enumFromInt(res);
|
||||
}
|
||||
|
||||
pub fn elementDispatchEvent(element: *Element, event: *Event) !bool {
|
||||
const et: *EventTarget = toEventTarget(Element, element);
|
||||
return eventTargetDispatchEvent(et, @ptrCast(event));
|
||||
@@ -768,14 +779,37 @@ pub fn eventTargetTBaseFieldName(comptime T: type) ?[]const u8 {
|
||||
// EventTargetBase is used to implement EventTarget for pure zig struct.
|
||||
pub const EventTargetTBase = extern struct {
|
||||
const Self = @This();
|
||||
const InternalType = enum(u32) {
|
||||
libdom_node = 0,
|
||||
plain = 1,
|
||||
abort_signal = 2,
|
||||
xhr = 3,
|
||||
window = 4,
|
||||
performance = 5,
|
||||
media_query_list = 6,
|
||||
message_port = 7,
|
||||
};
|
||||
|
||||
vtable: ?*const c.struct_dom_event_target_vtable = &c.struct_dom_event_target_vtable{
|
||||
.dispatch_event = dispatch_event,
|
||||
.remove_event_listener = remove_event_listener,
|
||||
.add_event_listener = add_event_listener,
|
||||
.iter_event_listener = iter_event_listener,
|
||||
.internal_type = internal_type,
|
||||
},
|
||||
|
||||
// When we dispatch the event, we need to provide a target. In reality, the
|
||||
// target is the container of this EventTargetTBase. But we can't pass that
|
||||
// to _dom_event_target_dispatch, because it expects a dom_event_target.
|
||||
// If you try to pass an non-event_target, you'll get weird behavior. For
|
||||
// example, libdom might dom_node_ref that memory. Say we passed a *Window
|
||||
// as the target, what happens if libdom calls dom_node_ref(window)? If
|
||||
// you're lucky, you'll crash. If you're unlucky, you'll increment a random
|
||||
// part of the window structure.
|
||||
refcnt: u32 = 0,
|
||||
|
||||
eti: c.dom_event_target_internal = c.dom_event_target_internal{ .listeners = null },
|
||||
internal_target_type: InternalType,
|
||||
|
||||
pub fn add_event_listener(et: [*c]c.dom_event_target, t: [*c]c.dom_string, l: ?*c.struct_dom_event_listener, capture: bool) callconv(.C) c.dom_exception {
|
||||
const self = @as(*Self, @ptrCast(et));
|
||||
@@ -808,6 +842,20 @@ pub const EventTargetTBase = extern struct {
|
||||
const self = @as(*Self, @ptrCast(et));
|
||||
return c._dom_event_target_iter_event_listener(self.eti, t, capture, cur, next, l);
|
||||
}
|
||||
|
||||
pub fn internal_type(et: [*c]c.dom_event_target, internal_type_: [*c]u32) callconv(.C) c.dom_exception {
|
||||
const self = @as(*Self, @ptrCast(et));
|
||||
internal_type_.* = @intFromEnum(self.internal_target_type);
|
||||
return c.DOM_NO_ERR;
|
||||
}
|
||||
|
||||
// Called to simulate bubbling from a libdom node (e.g. the Document) to a
|
||||
// Zig instance (e.g. the Window).
|
||||
pub fn redispatchEvent(self: *EventTargetTBase, evt: *Event) !void {
|
||||
var res: bool = undefined;
|
||||
const err = c._dom_event_target_dispatch(@ptrCast(self), &self.eti, evt, c.DOM_BUBBLING_PHASE, &res);
|
||||
try DOMErr(err);
|
||||
}
|
||||
};
|
||||
|
||||
// MouseEvent
|
||||
@@ -1816,6 +1864,21 @@ pub fn anchorSetRel(a: *Anchor, rel: []const u8) !void {
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
// HTMLLinkElement
|
||||
|
||||
pub fn linkGetHref(link: *Link) ![]const u8 {
|
||||
var res: ?*String = undefined;
|
||||
const err = c.dom_html_link_element_get_href(link, &res);
|
||||
try DOMErr(err);
|
||||
if (res == null) return "";
|
||||
return strToData(res.?);
|
||||
}
|
||||
|
||||
pub fn linkSetHref(link: *Link, href: []const u8) !void {
|
||||
const err = c.dom_html_link_element_set_href(link, try strFromData(href));
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
// ElementsHTML
|
||||
|
||||
pub const MediaElement = struct { base: *c.dom_html_element };
|
||||
@@ -1896,18 +1959,6 @@ pub inline fn documentFragmentToNode(doc: *DocumentFragment) *Node {
|
||||
return @as(*Node, @alignCast(@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(u32) {
|
||||
@@ -2355,14 +2406,6 @@ pub inline fn documentHTMLSetBody(doc_html: *DocumentHTML, elt: ?*ElementHTML) !
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub inline fn documentHTMLGetDomain(doc: *DocumentHTML) ![]const u8 {
|
||||
var s: ?*String = undefined;
|
||||
const err = documentHTMLVtable(doc).get_domain.?(doc, &s);
|
||||
try DOMErr(err);
|
||||
if (s == null) return "";
|
||||
return strToData(s.?);
|
||||
}
|
||||
|
||||
pub inline fn documentHTMLGetReferrer(doc: *DocumentHTML) ![]const u8 {
|
||||
var s: ?*String = undefined;
|
||||
const err = documentHTMLVtable(doc).get_referrer.?(doc, &s);
|
||||
|
||||
@@ -78,7 +78,10 @@ pub const Page = struct {
|
||||
|
||||
renderer: Renderer,
|
||||
|
||||
// run v8 micro tasks
|
||||
microtask_node: Loop.CallbackNode,
|
||||
// run v8 pump message loop and idle tasks
|
||||
messageloop_node: Loop.CallbackNode,
|
||||
|
||||
keydown_event_node: parser.EventNode,
|
||||
window_clicked_event_node: parser.EventNode,
|
||||
@@ -87,18 +90,13 @@ pub const Page = struct {
|
||||
// execute any JavaScript
|
||||
main_context: *Env.JsContext,
|
||||
|
||||
// List of modules currently fetched/loaded.
|
||||
module_map: std.StringHashMapUnmanaged([]const u8),
|
||||
|
||||
// current_script is the script currently evaluated by the page.
|
||||
// current_script could by fetch module to resolve module's url to fetch.
|
||||
current_script: ?*const Script = null,
|
||||
|
||||
// indicates intention to navigate to another page on the next loop execution.
|
||||
delayed_navigation: bool = false,
|
||||
|
||||
state_pool: *std.heap.MemoryPool(State),
|
||||
|
||||
polyfill_loader: polyfill.Loader = .{},
|
||||
|
||||
pub fn init(self: *Page, arena: Allocator, session: *Session) !void {
|
||||
const browser = session.browser;
|
||||
self.* = .{
|
||||
@@ -113,20 +111,22 @@ pub const Page = struct {
|
||||
.state_pool = &browser.state_pool,
|
||||
.cookie_jar = &session.cookie_jar,
|
||||
.microtask_node = .{ .func = microtaskCallback },
|
||||
.messageloop_node = .{ .func = messageLoopCallback },
|
||||
.keydown_event_node = .{ .func = keydownCallback },
|
||||
.window_clicked_event_node = .{ .func = windowClicked },
|
||||
.request_factory = browser.http_client.requestFactory(.{
|
||||
.notification = browser.notification,
|
||||
}),
|
||||
.main_context = undefined,
|
||||
.module_map = .empty,
|
||||
};
|
||||
self.main_context = try session.executor.createJsContext(&self.window, self, self, true);
|
||||
|
||||
// load polyfills
|
||||
try polyfill.load(self.arena, self.main_context);
|
||||
self.main_context = try session.executor.createJsContext(&self.window, self, self, true, Env.GlobalMissingCallback.init(&self.polyfill_loader));
|
||||
try polyfill.preload(self.arena, self.main_context);
|
||||
|
||||
// message loop must run only non-test env
|
||||
if (comptime !builtin.is_test) {
|
||||
_ = try session.browser.app.loop.timeout(1 * std.time.ns_per_ms, &self.microtask_node);
|
||||
_ = try session.browser.app.loop.timeout(100 * std.time.ns_per_ms, &self.messageloop_node);
|
||||
}
|
||||
}
|
||||
|
||||
fn microtaskCallback(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
|
||||
@@ -135,8 +135,14 @@ pub const Page = struct {
|
||||
repeat_delay.* = 1 * std.time.ns_per_ms;
|
||||
}
|
||||
|
||||
fn messageLoopCallback(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
|
||||
const self: *Page = @fieldParentPtr("messageloop_node", node);
|
||||
self.session.browser.runMessageLoop();
|
||||
repeat_delay.* = 100 * std.time.ns_per_ms;
|
||||
}
|
||||
|
||||
// dump writes the page content into the given file.
|
||||
pub fn dump(self: *const Page, out: std.fs.File) !void {
|
||||
pub fn dump(self: *const Page, opts: Dump.Opts, out: std.fs.File) !void {
|
||||
if (self.raw_data) |raw_data| {
|
||||
// raw_data was set if the document was not HTML, dump the data content only.
|
||||
return try out.writeAll(raw_data);
|
||||
@@ -144,32 +150,20 @@ pub const Page = struct {
|
||||
|
||||
// if the page has a pointer to a document, dumps the HTML.
|
||||
const doc = parser.documentHTMLToDocument(self.window.document);
|
||||
try Dump.writeHTML(doc, out);
|
||||
try Dump.writeHTML(doc, opts, out);
|
||||
}
|
||||
|
||||
pub fn fetchModuleSource(ctx: *anyopaque, specifier: []const u8) !?[]const u8 {
|
||||
pub fn fetchModuleSource(ctx: *anyopaque, src: []const u8) !?[]const u8 {
|
||||
const self: *Page = @ptrCast(@alignCast(ctx));
|
||||
const base = if (self.current_script) |s| s.src else null;
|
||||
|
||||
const file_src = blk: {
|
||||
if (base) |_base| {
|
||||
break :blk try URL.stitch(self.arena, specifier, _base, .{});
|
||||
} else break :blk specifier;
|
||||
};
|
||||
|
||||
if (self.module_map.get(file_src)) |module| return module;
|
||||
|
||||
const module = try self.fetchData(specifier, base);
|
||||
if (module) |_module| try self.module_map.putNoClobber(self.arena, file_src, _module);
|
||||
return module;
|
||||
return self.fetchData("module", src);
|
||||
}
|
||||
|
||||
pub fn wait(self: *Page) !void {
|
||||
pub fn wait(self: *Page, wait_ns: usize) !void {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(self.main_context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
try self.session.browser.app.loop.run();
|
||||
try self.session.browser.app.loop.run(wait_ns);
|
||||
|
||||
if (try_catch.hasCaught() == false) {
|
||||
log.debug(.browser, "page wait complete", .{});
|
||||
@@ -217,7 +211,7 @@ pub const Page = struct {
|
||||
{
|
||||
// block exists to limit the lifetime of the request, which holds
|
||||
// onto a connection
|
||||
var request = try self.newHTTPRequest(opts.method, &self.url, .{ .navigation = true });
|
||||
var request = try self.newHTTPRequest(opts.method, &self.url, .{ .navigation = true, .is_http = true });
|
||||
defer request.deinit();
|
||||
|
||||
request.body = opts.body;
|
||||
@@ -258,17 +252,33 @@ pub const Page = struct {
|
||||
.reason = opts.reason,
|
||||
});
|
||||
|
||||
if (!mime.isHTML()) {
|
||||
if (mime.isHTML()) {
|
||||
// the page is an HTML, load it as it.
|
||||
try self.loadHTMLDoc(&response, mime.charset orelse "utf-8");
|
||||
} else {
|
||||
// the page isn't an HTML
|
||||
var arr: std.ArrayListUnmanaged(u8) = .{};
|
||||
while (try response.next()) |data| {
|
||||
try arr.appendSlice(arena, try arena.dupe(u8, data));
|
||||
}
|
||||
// save the body into the page.
|
||||
self.raw_data = arr.items;
|
||||
return;
|
||||
}
|
||||
|
||||
try self.loadHTMLDoc(&response, mime.charset orelse "utf-8");
|
||||
// construct a pseudo HTML containing the response body.
|
||||
var buf: std.ArrayListUnmanaged(u8) = .{};
|
||||
|
||||
switch (mime.content_type) {
|
||||
.application_json, .text_plain, .text_javascript, .text_css => {
|
||||
try buf.appendSlice(arena, "<html><head><meta charset=\"utf-8\"></head><body><pre>");
|
||||
try buf.appendSlice(arena, self.raw_data.?);
|
||||
try buf.appendSlice(arena, "</pre></body></html>\n");
|
||||
},
|
||||
// In other cases, we prefer to not integrate the content into the HTML document page iself.
|
||||
else => {},
|
||||
}
|
||||
var fbs = std.io.fixedBufferStream(buf.items);
|
||||
try self.loadHTMLDoc(fbs.reader(), mime.charset orelse "utf-8");
|
||||
}
|
||||
}
|
||||
|
||||
try self.processHTMLDoc();
|
||||
@@ -352,8 +362,11 @@ pub const Page = struct {
|
||||
continue;
|
||||
}
|
||||
|
||||
const e = parser.nodeToElement(next.?);
|
||||
const current = next.?;
|
||||
|
||||
const e = parser.nodeToElement(current);
|
||||
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(e)));
|
||||
|
||||
if (tag != .script) {
|
||||
// ignore non-js script.
|
||||
continue;
|
||||
@@ -445,26 +458,20 @@ pub const Page = struct {
|
||||
log.err(.browser, "clear document script", .{ .err = err });
|
||||
};
|
||||
|
||||
var script_source: ?[]const u8 = null;
|
||||
if (script.src) |src| {
|
||||
self.current_script = script;
|
||||
defer self.current_script = null;
|
||||
const src = script.src orelse {
|
||||
// source is inline
|
||||
// TODO handle charset attribute
|
||||
const script_source = try parser.nodeTextContent(parser.elementToNode(script.element)) orelse return;
|
||||
return script.eval(self, script_source);
|
||||
};
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
|
||||
script_source = (try self.fetchData(src, null)) orelse {
|
||||
const script_source = (try self.fetchData("script", src)) orelse {
|
||||
// TODO If el's result is null, then fire an event named error at
|
||||
// el, and return
|
||||
return;
|
||||
};
|
||||
} else {
|
||||
// source is inline
|
||||
// TODO handle charset attribute
|
||||
script_source = try parser.nodeTextContent(parser.elementToNode(script.element));
|
||||
}
|
||||
|
||||
if (script_source) |ss| {
|
||||
try script.eval(self, ss);
|
||||
}
|
||||
return script.eval(self, script_source);
|
||||
|
||||
// TODO If el's from an external file is true, then fire an event
|
||||
// named load at el.
|
||||
@@ -474,7 +481,11 @@ pub const Page = struct {
|
||||
// It resolves src using the page's uri.
|
||||
// If a base path is given, src is resolved according to the base first.
|
||||
// the caller owns the returned string
|
||||
fn fetchData(self: *const Page, src: []const u8, base: ?[]const u8) !?[]const u8 {
|
||||
fn fetchData(
|
||||
self: *const Page,
|
||||
comptime reason: []const u8,
|
||||
src: []const u8,
|
||||
) !?[]const u8 {
|
||||
const arena = self.arena;
|
||||
|
||||
// Handle data URIs.
|
||||
@@ -482,22 +493,27 @@ pub const Page = struct {
|
||||
return data_uri.data;
|
||||
}
|
||||
|
||||
var res_src = src;
|
||||
|
||||
// if a base path is given, we resolve src using base.
|
||||
if (base) |_base| {
|
||||
res_src = try URL.stitch(arena, src, _base, .{ .alloc = .if_needed });
|
||||
}
|
||||
|
||||
var origin_url = &self.url;
|
||||
const url = try origin_url.resolve(arena, res_src);
|
||||
const url = try origin_url.resolve(arena, src);
|
||||
|
||||
log.debug(.http, "fetching script", .{ .url = url });
|
||||
errdefer |err| log.err(.http, "fetch error", .{ .err = err, .url = url });
|
||||
var status_code: u16 = 0;
|
||||
log.debug(.http, "fetching script", .{
|
||||
.url = url,
|
||||
.src = src,
|
||||
.reason = reason,
|
||||
});
|
||||
|
||||
errdefer |err| log.err(.http, "fetch error", .{
|
||||
.err = err,
|
||||
.url = url,
|
||||
.reason = reason,
|
||||
.status = status_code,
|
||||
});
|
||||
|
||||
var request = try self.newHTTPRequest(.GET, &url, .{
|
||||
.origin_uri = &origin_url.uri,
|
||||
.navigation = false,
|
||||
.is_http = true,
|
||||
});
|
||||
defer request.deinit();
|
||||
|
||||
@@ -505,7 +521,8 @@ pub const Page = struct {
|
||||
var header = response.header;
|
||||
try self.session.cookie_jar.populateFromResponse(&url.uri, &header);
|
||||
|
||||
if (header.status < 200 or header.status > 299) {
|
||||
status_code = header.status;
|
||||
if (status_code < 200 or status_code > 299) {
|
||||
return error.BadStatusCode;
|
||||
}
|
||||
|
||||
@@ -523,7 +540,8 @@ pub const Page = struct {
|
||||
|
||||
log.info(.http, "fetch complete", .{
|
||||
.url = url,
|
||||
.status = header.status,
|
||||
.reason = reason,
|
||||
.status = status_code,
|
||||
.content_length = arr.items.len,
|
||||
});
|
||||
return arr.items;
|
||||
@@ -985,22 +1003,31 @@ const Script = struct {
|
||||
try_catch.init(page.main_context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const src = self.src orelse "inline";
|
||||
_ = switch (self.kind) {
|
||||
.javascript => page.main_context.exec(body, src),
|
||||
.module => blk: {
|
||||
switch (try page.main_context.module(body, src)) {
|
||||
.value => |v| break :blk v,
|
||||
.exception => |e| {
|
||||
log.warn(.user_script, "eval module", .{
|
||||
const src: []const u8 = blk: {
|
||||
const s = self.src orelse break :blk page.url.raw;
|
||||
break :blk try URL.stitch(page.arena, s, page.url.raw, .{ .alloc = .if_needed });
|
||||
};
|
||||
|
||||
// if self.src is null, then this is an inline script, and it should
|
||||
// not be cached.
|
||||
const cacheable = self.src != null;
|
||||
|
||||
log.debug(.browser, "executing script", .{
|
||||
.src = src,
|
||||
.err = try e.exception(page.arena),
|
||||
.kind = self.kind,
|
||||
.cacheable = cacheable,
|
||||
});
|
||||
return error.JsErr;
|
||||
},
|
||||
|
||||
const failed = blk: {
|
||||
switch (self.kind) {
|
||||
.javascript => _ = page.main_context.eval(body, src) catch break :blk true,
|
||||
// We don't care about waiting for the evaluation here.
|
||||
.module => _ = page.main_context.module(body, src, cacheable) catch break :blk true,
|
||||
}
|
||||
},
|
||||
} catch {
|
||||
break :blk false;
|
||||
};
|
||||
|
||||
if (failed) {
|
||||
if (page.delayed_navigation) {
|
||||
return error.Terminated;
|
||||
}
|
||||
@@ -1009,12 +1036,14 @@ const Script = struct {
|
||||
log.warn(.user_script, "eval script", .{
|
||||
.src = src,
|
||||
.err = msg,
|
||||
.cacheable = cacheable,
|
||||
});
|
||||
}
|
||||
|
||||
try self.executeCallback("onerror", page);
|
||||
return error.JsErr;
|
||||
};
|
||||
}
|
||||
|
||||
try self.executeCallback("onload", page);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ test "Browser.fetch" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try @import("polyfill.zig").load(testing.allocator, runner.page.main_context);
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ var ok = false;
|
||||
|
||||
@@ -23,25 +23,98 @@ const log = @import("../../log.zig");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Env = @import("../env.zig").Env;
|
||||
|
||||
const modules = [_]struct {
|
||||
name: []const u8,
|
||||
source: []const u8,
|
||||
}{
|
||||
.{ .name = "polyfill-fetch", .source = @import("fetch.zig").source },
|
||||
};
|
||||
pub const Loader = struct {
|
||||
state: enum { empty, loading } = .empty,
|
||||
|
||||
pub fn load(allocator: Allocator, js_context: *Env.JsContext) !void {
|
||||
done: struct {
|
||||
fetch: bool = false,
|
||||
webcomponents: bool = false,
|
||||
} = .{},
|
||||
|
||||
fn load(self: *Loader, comptime name: []const u8, source: []const u8, js_context: *Env.JsContext) void {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(js_context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
for (modules) |m| {
|
||||
_ = js_context.exec(m.source, m.name) catch |err| {
|
||||
self.state = .loading;
|
||||
defer self.state = .empty;
|
||||
|
||||
log.debug(.js, "polyfill load", .{ .name = name });
|
||||
_ = js_context.exec(source, name) catch |err| {
|
||||
log.fatal(.app, "polyfill error", .{
|
||||
.name = name,
|
||||
.err = try_catch.err(js_context.call_arena) catch @errorName(err) orelse @errorName(err),
|
||||
});
|
||||
};
|
||||
|
||||
@field(self.done, name) = true;
|
||||
}
|
||||
|
||||
pub fn missing(self: *Loader, name: []const u8, js_context: *Env.JsContext) bool {
|
||||
// Avoid recursive calls during polyfill loading.
|
||||
if (self.state == .loading) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!self.done.fetch and isFetch(name)) {
|
||||
const source = @import("fetch.zig").source;
|
||||
self.load("fetch", source, js_context);
|
||||
|
||||
// We return false here: We want v8 to continue the calling chain
|
||||
// to finally find the polyfill we just inserted. If we want to
|
||||
// return false and stops the call chain, we have to use
|
||||
// `info.GetReturnValue.Set()` function, or `undefined` will be
|
||||
// returned immediately.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!self.done.webcomponents and isWebcomponents(name)) {
|
||||
const source = @import("webcomponents.zig").source;
|
||||
self.load("webcomponents", source, js_context);
|
||||
// We return false here: We want v8 to continue the calling chain
|
||||
// to finally find the polyfill we just inserted. If we want to
|
||||
// return false and stops the call chain, we have to use
|
||||
// `info.GetReturnValue.Set()` function, or `undefined` will be
|
||||
// returned immediately.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (comptime builtin.mode == .Debug) {
|
||||
log.debug(.unknown_prop, "unkown global property", .{
|
||||
.info = "but the property can exist in pure JS",
|
||||
.property = name,
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
fn isFetch(name: []const u8) bool {
|
||||
if (std.mem.eql(u8, name, "fetch")) return true;
|
||||
if (std.mem.eql(u8, name, "Request")) return true;
|
||||
if (std.mem.eql(u8, name, "Response")) return true;
|
||||
if (std.mem.eql(u8, name, "Headers")) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
fn isWebcomponents(name: []const u8) bool {
|
||||
if (std.mem.eql(u8, name, "customElements")) return true;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn preload(allocator: Allocator, js_context: *Env.JsContext) !void {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(js_context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const name = "webcomponents-pre";
|
||||
const source = @import("webcomponents.zig").pre;
|
||||
_ = js_context.exec(source, name) catch |err| {
|
||||
if (try try_catch.err(allocator)) |msg| {
|
||||
defer allocator.free(msg);
|
||||
log.fatal(.app, "polyfill error", .{ .name = m.name, .err = msg });
|
||||
log.fatal(.app, "polyfill error", .{ .name = name, .err = msg });
|
||||
}
|
||||
return err;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
61
src/browser/polyfill/webcomponents.js
Normal file
61
src/browser/polyfill/webcomponents.js
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
@license @nocompile
|
||||
Copyright (c) 2018 The Polymer Project Authors. All rights reserved.
|
||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
||||
Code distributed by Google as part of the polymer project is also
|
||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
||||
*/
|
||||
(function(){/*
|
||||
|
||||
Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
|
||||
This code may only be used under the BSD style license found at
|
||||
http://polymer.github.io/LICENSE.txt The complete set of authors may be found
|
||||
at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
|
||||
be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
|
||||
Google as part of the polymer project is also subject to an additional IP
|
||||
rights grant found at http://polymer.github.io/PATENTS.txt
|
||||
*/
|
||||
'use strict';var n=window.Document.prototype.createElement,p=window.Document.prototype.createElementNS,aa=window.Document.prototype.importNode,ba=window.Document.prototype.prepend,ca=window.Document.prototype.append,da=window.DocumentFragment.prototype.prepend,ea=window.DocumentFragment.prototype.append,q=window.Node.prototype.cloneNode,r=window.Node.prototype.appendChild,t=window.Node.prototype.insertBefore,u=window.Node.prototype.removeChild,v=window.Node.prototype.replaceChild,w=Object.getOwnPropertyDescriptor(window.Node.prototype,
|
||||
"textContent"),y=window.Element.prototype.attachShadow,z=Object.getOwnPropertyDescriptor(window.Element.prototype,"innerHTML"),A=window.Element.prototype.getAttribute,B=window.Element.prototype.setAttribute,C=window.Element.prototype.removeAttribute,D=window.Element.prototype.toggleAttribute,E=window.Element.prototype.getAttributeNS,F=window.Element.prototype.setAttributeNS,G=window.Element.prototype.removeAttributeNS,H=window.Element.prototype.insertAdjacentElement,fa=window.Element.prototype.insertAdjacentHTML,
|
||||
ha=window.Element.prototype.prepend,ia=window.Element.prototype.append,ja=window.Element.prototype.before,ka=window.Element.prototype.after,la=window.Element.prototype.replaceWith,ma=window.Element.prototype.remove,na=window.HTMLElement,I=Object.getOwnPropertyDescriptor(window.HTMLElement.prototype,"innerHTML"),oa=window.HTMLElement.prototype.insertAdjacentElement,pa=window.HTMLElement.prototype.insertAdjacentHTML;var qa=new Set;"annotation-xml color-profile font-face font-face-src font-face-uri font-face-format font-face-name missing-glyph".split(" ").forEach(function(a){return qa.add(a)});function ra(a){var b=qa.has(a);a=/^[a-z][.0-9_a-z]*-[-.0-9_a-z]*$/.test(a);return!b&&a}var sa=document.contains?document.contains.bind(document):document.documentElement.contains.bind(document.documentElement);
|
||||
function J(a){var b=a.isConnected;if(void 0!==b)return b;if(sa(a))return!0;for(;a&&!(a.__CE_isImportDocument||a instanceof Document);)a=a.parentNode||(window.ShadowRoot&&a instanceof ShadowRoot?a.host:void 0);return!(!a||!(a.__CE_isImportDocument||a instanceof Document))}function K(a){var b=a.children;if(b)return Array.prototype.slice.call(b);b=[];for(a=a.firstChild;a;a=a.nextSibling)a.nodeType===Node.ELEMENT_NODE&&b.push(a);return b}
|
||||
function L(a,b){for(;b&&b!==a&&!b.nextSibling;)b=b.parentNode;return b&&b!==a?b.nextSibling:null}
|
||||
function M(a,b,d){for(var f=a;f;){if(f.nodeType===Node.ELEMENT_NODE){var c=f;b(c);var e=c.localName;if("link"===e&&"import"===c.getAttribute("rel")){f=c.import;void 0===d&&(d=new Set);if(f instanceof Node&&!d.has(f))for(d.add(f),f=f.firstChild;f;f=f.nextSibling)M(f,b,d);f=L(a,c);continue}else if("template"===e){f=L(a,c);continue}if(c=c.__CE_shadowRoot)for(c=c.firstChild;c;c=c.nextSibling)M(c,b,d)}f=f.firstChild?f.firstChild:L(a,f)}};function N(){var a=!(null===O||void 0===O||!O.noDocumentConstructionObserver),b=!(null===O||void 0===O||!O.shadyDomFastWalk);this.m=[];this.g=[];this.j=!1;this.shadyDomFastWalk=b;this.I=!a}function P(a,b,d,f){var c=window.ShadyDOM;if(a.shadyDomFastWalk&&c&&c.inUse){if(b.nodeType===Node.ELEMENT_NODE&&d(b),b.querySelectorAll)for(a=c.nativeMethods.querySelectorAll.call(b,"*"),b=0;b<a.length;b++)d(a[b])}else M(b,d,f)}function ta(a,b){a.j=!0;a.m.push(b)}function ua(a,b){a.j=!0;a.g.push(b)}
|
||||
function Q(a,b){a.j&&P(a,b,function(d){return R(a,d)})}function R(a,b){if(a.j&&!b.__CE_patched){b.__CE_patched=!0;for(var d=0;d<a.m.length;d++)a.m[d](b);for(d=0;d<a.g.length;d++)a.g[d](b)}}function S(a,b){var d=[];P(a,b,function(c){return d.push(c)});for(b=0;b<d.length;b++){var f=d[b];1===f.__CE_state?a.connectedCallback(f):T(a,f)}}function U(a,b){var d=[];P(a,b,function(c){return d.push(c)});for(b=0;b<d.length;b++){var f=d[b];1===f.__CE_state&&a.disconnectedCallback(f)}}
|
||||
function V(a,b,d){d=void 0===d?{}:d;var f=d.J,c=d.upgrade||function(g){return T(a,g)},e=[];P(a,b,function(g){a.j&&R(a,g);if("link"===g.localName&&"import"===g.getAttribute("rel")){var h=g.import;h instanceof Node&&(h.__CE_isImportDocument=!0,h.__CE_registry=document.__CE_registry);h&&"complete"===h.readyState?h.__CE_documentLoadHandled=!0:g.addEventListener("load",function(){var k=g.import;if(!k.__CE_documentLoadHandled){k.__CE_documentLoadHandled=!0;var l=new Set;f&&(f.forEach(function(m){return l.add(m)}),
|
||||
l.delete(k));V(a,k,{J:l,upgrade:c})}})}else e.push(g)},f);for(b=0;b<e.length;b++)c(e[b])}
|
||||
function T(a,b){try{var d=b.ownerDocument,f=d.__CE_registry;var c=f&&(d.defaultView||d.__CE_isImportDocument)?W(f,b.localName):void 0;if(c&&void 0===b.__CE_state){c.constructionStack.push(b);try{try{if(new c.constructorFunction!==b)throw Error("The custom element constructor did not produce the element being upgraded.");}finally{c.constructionStack.pop()}}catch(k){throw b.__CE_state=2,k;}b.__CE_state=1;b.__CE_definition=c;if(c.attributeChangedCallback&&b.hasAttributes()){var e=c.observedAttributes;
|
||||
for(c=0;c<e.length;c++){var g=e[c],h=b.getAttribute(g);null!==h&&a.attributeChangedCallback(b,g,null,h,null)}}J(b)&&a.connectedCallback(b)}}catch(k){X(k)}}N.prototype.connectedCallback=function(a){var b=a.__CE_definition;if(b.connectedCallback)try{b.connectedCallback.call(a)}catch(d){X(d)}};N.prototype.disconnectedCallback=function(a){var b=a.__CE_definition;if(b.disconnectedCallback)try{b.disconnectedCallback.call(a)}catch(d){X(d)}};
|
||||
N.prototype.attributeChangedCallback=function(a,b,d,f,c){var e=a.__CE_definition;if(e.attributeChangedCallback&&-1<e.observedAttributes.indexOf(b))try{e.attributeChangedCallback.call(a,b,d,f,c)}catch(g){X(g)}};
|
||||
function va(a,b,d,f){var c=b.__CE_registry;if(c&&(null===f||"http://www.w3.org/1999/xhtml"===f)&&(c=W(c,d)))try{var e=new c.constructorFunction;if(void 0===e.__CE_state||void 0===e.__CE_definition)throw Error("Failed to construct '"+d+"': The returned value was not constructed with the HTMLElement constructor.");if("http://www.w3.org/1999/xhtml"!==e.namespaceURI)throw Error("Failed to construct '"+d+"': The constructed element's namespace must be the HTML namespace.");if(e.hasAttributes())throw Error("Failed to construct '"+
|
||||
d+"': The constructed element must not have any attributes.");if(null!==e.firstChild)throw Error("Failed to construct '"+d+"': The constructed element must not have any children.");if(null!==e.parentNode)throw Error("Failed to construct '"+d+"': The constructed element must not have a parent node.");if(e.ownerDocument!==b)throw Error("Failed to construct '"+d+"': The constructed element's owner document is incorrect.");if(e.localName!==d)throw Error("Failed to construct '"+d+"': The constructed element's local name is incorrect.");
|
||||
return e}catch(g){return X(g),b=null===f?n.call(b,d):p.call(b,f,d),Object.setPrototypeOf(b,HTMLUnknownElement.prototype),b.__CE_state=2,b.__CE_definition=void 0,R(a,b),b}b=null===f?n.call(b,d):p.call(b,f,d);R(a,b);return b}
|
||||
function X(a){var b="",d="",f=0,c=0;a instanceof Error?(b=a.message,d=a.sourceURL||a.fileName||"",f=a.line||a.lineNumber||0,c=a.column||a.columnNumber||0):b="Uncaught "+String(a);var e=void 0;void 0===ErrorEvent.prototype.initErrorEvent?e=new ErrorEvent("error",{cancelable:!0,message:b,filename:d,lineno:f,colno:c,error:a}):(e=document.createEvent("ErrorEvent"),e.initErrorEvent("error",!1,!0,b,d,f),e.preventDefault=function(){Object.defineProperty(this,"defaultPrevented",{configurable:!0,get:function(){return!0}})});
|
||||
void 0===e.error&&Object.defineProperty(e,"error",{configurable:!0,enumerable:!0,get:function(){return a}});window.dispatchEvent(e);e.defaultPrevented||console.error(a)};function wa(){var a=this;this.g=void 0;this.F=new Promise(function(b){a.l=b})}wa.prototype.resolve=function(a){if(this.g)throw Error("Already resolved.");this.g=a;this.l(a)};function xa(a){var b=document;this.l=void 0;this.h=a;this.g=b;V(this.h,this.g);"loading"===this.g.readyState&&(this.l=new MutationObserver(this.G.bind(this)),this.l.observe(this.g,{childList:!0,subtree:!0}))}function ya(a){a.l&&a.l.disconnect()}xa.prototype.G=function(a){var b=this.g.readyState;"interactive"!==b&&"complete"!==b||ya(this);for(b=0;b<a.length;b++)for(var d=a[b].addedNodes,f=0;f<d.length;f++)V(this.h,d[f])};function Y(a){this.s=new Map;this.u=new Map;this.C=new Map;this.A=!1;this.B=new Map;this.o=function(b){return b()};this.i=!1;this.v=[];this.h=a;this.D=a.I?new xa(a):void 0}Y.prototype.H=function(a,b){var d=this;if(!(b instanceof Function))throw new TypeError("Custom element constructor getters must be functions.");za(this,a);this.s.set(a,b);this.v.push(a);this.i||(this.i=!0,this.o(function(){return Aa(d)}))};
|
||||
Y.prototype.define=function(a,b){var d=this;if(!(b instanceof Function))throw new TypeError("Custom element constructors must be functions.");za(this,a);Ba(this,a,b);this.v.push(a);this.i||(this.i=!0,this.o(function(){return Aa(d)}))};function za(a,b){if(!ra(b))throw new SyntaxError("The element name '"+b+"' is not valid.");if(W(a,b))throw Error("A custom element with name '"+(b+"' has already been defined."));if(a.A)throw Error("A custom element is already being defined.");}
|
||||
function Ba(a,b,d){a.A=!0;var f;try{var c=d.prototype;if(!(c instanceof Object))throw new TypeError("The custom element constructor's prototype is not an object.");var e=function(m){var x=c[m];if(void 0!==x&&!(x instanceof Function))throw Error("The '"+m+"' callback must be a function.");return x};var g=e("connectedCallback");var h=e("disconnectedCallback");var k=e("adoptedCallback");var l=(f=e("attributeChangedCallback"))&&d.observedAttributes||[]}catch(m){throw m;}finally{a.A=!1}d={localName:b,
|
||||
constructorFunction:d,connectedCallback:g,disconnectedCallback:h,adoptedCallback:k,attributeChangedCallback:f,observedAttributes:l,constructionStack:[]};a.u.set(b,d);a.C.set(d.constructorFunction,d);return d}Y.prototype.upgrade=function(a){V(this.h,a)};
|
||||
function Aa(a){if(!1!==a.i){a.i=!1;for(var b=[],d=a.v,f=new Map,c=0;c<d.length;c++)f.set(d[c],[]);V(a.h,document,{upgrade:function(k){if(void 0===k.__CE_state){var l=k.localName,m=f.get(l);m?m.push(k):a.u.has(l)&&b.push(k)}}});for(c=0;c<b.length;c++)T(a.h,b[c]);for(c=0;c<d.length;c++){for(var e=d[c],g=f.get(e),h=0;h<g.length;h++)T(a.h,g[h]);(e=a.B.get(e))&&e.resolve(void 0)}d.length=0}}Y.prototype.get=function(a){if(a=W(this,a))return a.constructorFunction};
|
||||
Y.prototype.whenDefined=function(a){if(!ra(a))return Promise.reject(new SyntaxError("'"+a+"' is not a valid custom element name."));var b=this.B.get(a);if(b)return b.F;b=new wa;this.B.set(a,b);var d=this.u.has(a)||this.s.has(a);a=-1===this.v.indexOf(a);d&&a&&b.resolve(void 0);return b.F};Y.prototype.polyfillWrapFlushCallback=function(a){this.D&&ya(this.D);var b=this.o;this.o=function(d){return a(function(){return b(d)})}};
|
||||
function W(a,b){var d=a.u.get(b);if(d)return d;if(d=a.s.get(b)){a.s.delete(b);try{return Ba(a,b,d())}catch(f){X(f)}}}Y.prototype.define=Y.prototype.define;Y.prototype.upgrade=Y.prototype.upgrade;Y.prototype.get=Y.prototype.get;Y.prototype.whenDefined=Y.prototype.whenDefined;Y.prototype.polyfillDefineLazy=Y.prototype.H;Y.prototype.polyfillWrapFlushCallback=Y.prototype.polyfillWrapFlushCallback;function Z(a,b,d){function f(c){return function(e){for(var g=[],h=0;h<arguments.length;++h)g[h]=arguments[h];h=[];for(var k=[],l=0;l<g.length;l++){var m=g[l];m instanceof Element&&J(m)&&k.push(m);if(m instanceof DocumentFragment)for(m=m.firstChild;m;m=m.nextSibling)h.push(m);else h.push(m)}c.apply(this,g);for(g=0;g<k.length;g++)U(a,k[g]);if(J(this))for(g=0;g<h.length;g++)k=h[g],k instanceof Element&&S(a,k)}}void 0!==d.prepend&&(b.prepend=f(d.prepend));void 0!==d.append&&(b.append=f(d.append))};function Ca(a){Document.prototype.createElement=function(b){return va(a,this,b,null)};Document.prototype.importNode=function(b,d){b=aa.call(this,b,!!d);this.__CE_registry?V(a,b):Q(a,b);return b};Document.prototype.createElementNS=function(b,d){return va(a,this,d,b)};Z(a,Document.prototype,{prepend:ba,append:ca})};function Da(a){function b(f){return function(c){for(var e=[],g=0;g<arguments.length;++g)e[g]=arguments[g];g=[];for(var h=[],k=0;k<e.length;k++){var l=e[k];l instanceof Element&&J(l)&&h.push(l);if(l instanceof DocumentFragment)for(l=l.firstChild;l;l=l.nextSibling)g.push(l);else g.push(l)}f.apply(this,e);for(e=0;e<h.length;e++)U(a,h[e]);if(J(this))for(e=0;e<g.length;e++)h=g[e],h instanceof Element&&S(a,h)}}var d=Element.prototype;void 0!==ja&&(d.before=b(ja));void 0!==ka&&(d.after=b(ka));void 0!==la&&
|
||||
(d.replaceWith=function(f){for(var c=[],e=0;e<arguments.length;++e)c[e]=arguments[e];e=[];for(var g=[],h=0;h<c.length;h++){var k=c[h];k instanceof Element&&J(k)&&g.push(k);if(k instanceof DocumentFragment)for(k=k.firstChild;k;k=k.nextSibling)e.push(k);else e.push(k)}h=J(this);la.apply(this,c);for(c=0;c<g.length;c++)U(a,g[c]);if(h)for(U(a,this),c=0;c<e.length;c++)g=e[c],g instanceof Element&&S(a,g)});void 0!==ma&&(d.remove=function(){var f=J(this);ma.call(this);f&&U(a,this)})};function Ea(a){function b(c,e){Object.defineProperty(c,"innerHTML",{enumerable:e.enumerable,configurable:!0,get:e.get,set:function(g){var h=this,k=void 0;J(this)&&(k=[],P(a,this,function(x){x!==h&&k.push(x)}));e.set.call(this,g);if(k)for(var l=0;l<k.length;l++){var m=k[l];1===m.__CE_state&&a.disconnectedCallback(m)}this.ownerDocument.__CE_registry?V(a,this):Q(a,this);return g}})}function d(c,e){c.insertAdjacentElement=function(g,h){var k=J(h);g=e.call(this,g,h);k&&U(a,h);J(g)&&S(a,h);return g}}function f(c,
|
||||
e){function g(h,k){for(var l=[];h!==k;h=h.nextSibling)l.push(h);for(k=0;k<l.length;k++)V(a,l[k])}c.insertAdjacentHTML=function(h,k){h=h.toLowerCase();if("beforebegin"===h){var l=this.previousSibling;e.call(this,h,k);g(l||this.parentNode.firstChild,this)}else if("afterbegin"===h)l=this.firstChild,e.call(this,h,k),g(this.firstChild,l);else if("beforeend"===h)l=this.lastChild,e.call(this,h,k),g(l||this.firstChild,null);else if("afterend"===h)l=this.nextSibling,e.call(this,h,k),g(this.nextSibling,l);
|
||||
else throw new SyntaxError("The value provided ("+String(h)+") is not one of 'beforebegin', 'afterbegin', 'beforeend', or 'afterend'.");}}y&&(Element.prototype.attachShadow=function(c){c=y.call(this,c);if(a.j&&!c.__CE_patched){c.__CE_patched=!0;for(var e=0;e<a.m.length;e++)a.m[e](c)}return this.__CE_shadowRoot=c});z&&z.get?b(Element.prototype,z):I&&I.get?b(HTMLElement.prototype,I):ua(a,function(c){b(c,{enumerable:!0,configurable:!0,get:function(){return q.call(this,!0).innerHTML},set:function(e){var g=
|
||||
"template"===this.localName,h=g?this.content:this,k=p.call(document,this.namespaceURI,this.localName);for(k.innerHTML=e;0<h.childNodes.length;)u.call(h,h.childNodes[0]);for(e=g?k.content:k;0<e.childNodes.length;)r.call(h,e.childNodes[0])}})});Element.prototype.setAttribute=function(c,e){if(1!==this.__CE_state)return B.call(this,c,e);var g=A.call(this,c);B.call(this,c,e);e=A.call(this,c);a.attributeChangedCallback(this,c,g,e,null)};Element.prototype.setAttributeNS=function(c,e,g){if(1!==this.__CE_state)return F.call(this,
|
||||
c,e,g);var h=E.call(this,c,e);F.call(this,c,e,g);g=E.call(this,c,e);a.attributeChangedCallback(this,e,h,g,c)};Element.prototype.removeAttribute=function(c){if(1!==this.__CE_state)return C.call(this,c);var e=A.call(this,c);C.call(this,c);null!==e&&a.attributeChangedCallback(this,c,e,null,null)};D&&(Element.prototype.toggleAttribute=function(c,e){if(1!==this.__CE_state)return D.call(this,c,e);var g=A.call(this,c),h=null!==g;e=D.call(this,c,e);h!==e&&a.attributeChangedCallback(this,c,g,e?"":null,null);
|
||||
return e});Element.prototype.removeAttributeNS=function(c,e){if(1!==this.__CE_state)return G.call(this,c,e);var g=E.call(this,c,e);G.call(this,c,e);var h=E.call(this,c,e);g!==h&&a.attributeChangedCallback(this,e,g,h,c)};oa?d(HTMLElement.prototype,oa):H&&d(Element.prototype,H);pa?f(HTMLElement.prototype,pa):fa&&f(Element.prototype,fa);Z(a,Element.prototype,{prepend:ha,append:ia});Da(a)};var Fa={};function Ga(a){function b(){var d=this.constructor;var f=document.__CE_registry.C.get(d);if(!f)throw Error("Failed to construct a custom element: The constructor was not registered with `customElements`.");var c=f.constructionStack;if(0===c.length)return c=n.call(document,f.localName),Object.setPrototypeOf(c,d.prototype),c.__CE_state=1,c.__CE_definition=f,R(a,c),c;var e=c.length-1,g=c[e];if(g===Fa)throw Error("Failed to construct '"+f.localName+"': This element was already constructed.");c[e]=Fa;
|
||||
Object.setPrototypeOf(g,d.prototype);R(a,g);return g}b.prototype=na.prototype;Object.defineProperty(HTMLElement.prototype,"constructor",{writable:!0,configurable:!0,enumerable:!1,value:b});window.HTMLElement=b};function Ha(a){function b(d,f){Object.defineProperty(d,"textContent",{enumerable:f.enumerable,configurable:!0,get:f.get,set:function(c){if(this.nodeType===Node.TEXT_NODE)f.set.call(this,c);else{var e=void 0;if(this.firstChild){var g=this.childNodes,h=g.length;if(0<h&&J(this)){e=Array(h);for(var k=0;k<h;k++)e[k]=g[k]}}f.set.call(this,c);if(e)for(c=0;c<e.length;c++)U(a,e[c])}}})}Node.prototype.insertBefore=function(d,f){if(d instanceof DocumentFragment){var c=K(d);d=t.call(this,d,f);if(J(this))for(f=
|
||||
0;f<c.length;f++)S(a,c[f]);return d}c=d instanceof Element&&J(d);f=t.call(this,d,f);c&&U(a,d);J(this)&&S(a,d);return f};Node.prototype.appendChild=function(d){if(d instanceof DocumentFragment){var f=K(d);d=r.call(this,d);if(J(this))for(var c=0;c<f.length;c++)S(a,f[c]);return d}f=d instanceof Element&&J(d);c=r.call(this,d);f&&U(a,d);J(this)&&S(a,d);return c};Node.prototype.cloneNode=function(d){d=q.call(this,!!d);this.ownerDocument.__CE_registry?V(a,d):Q(a,d);return d};Node.prototype.removeChild=function(d){var f=
|
||||
d instanceof Element&&J(d),c=u.call(this,d);f&&U(a,d);return c};Node.prototype.replaceChild=function(d,f){if(d instanceof DocumentFragment){var c=K(d);d=v.call(this,d,f);if(J(this))for(U(a,f),f=0;f<c.length;f++)S(a,c[f]);return d}c=d instanceof Element&&J(d);var e=v.call(this,d,f),g=J(this);g&&U(a,f);c&&U(a,d);g&&S(a,d);return e};w&&w.get?b(Node.prototype,w):ta(a,function(d){b(d,{enumerable:!0,configurable:!0,get:function(){for(var f=[],c=this.firstChild;c;c=c.nextSibling)c.nodeType!==Node.COMMENT_NODE&&
|
||||
f.push(c.textContent);return f.join("")},set:function(f){for(;this.firstChild;)u.call(this,this.firstChild);null!=f&&""!==f&&r.call(this,document.createTextNode(f))}})})};var O=window.customElements;function Ia(){var a=new N;Ga(a);Ca(a);Z(a,DocumentFragment.prototype,{prepend:da,append:ea});Ha(a);Ea(a);window.CustomElementRegistry=Y;a=new Y(a);document.__CE_registry=a;Object.defineProperty(window,"customElements",{configurable:!0,enumerable:!0,value:a})}O&&!O.forcePolyfill&&"function"==typeof O.define&&"function"==typeof O.get||Ia();window.__CE_installPolyfill=Ia;/*
|
||||
|
||||
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
||||
Code distributed by Google as part of the polymer project is also
|
||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
||||
*/
|
||||
}).call(this);
|
||||
65
src/browser/polyfill/webcomponents.zig
Normal file
65
src/browser/polyfill/webcomponents.zig
Normal file
@@ -0,0 +1,65 @@
|
||||
// webcomponents.js code comes from
|
||||
// https://github.com/webcomponents/polyfills/tree/master/packages/webcomponentsjs
|
||||
//
|
||||
// The original code source is available in a "BSD style license".
|
||||
//
|
||||
// This is the `webcomponents-ce.js` bundle
|
||||
pub const source = @embedFile("webcomponents.js");
|
||||
|
||||
// The main webcomponents.js is lazilly loaded when window.customElements is
|
||||
// called. But, if you look at the test below, you'll notice that we declare
|
||||
// our custom element (LightPanda) before we call `customElements.define`. We
|
||||
// _have_ to declare it before we can register it.
|
||||
// That causes an issue, because the LightPanda class extends HTMLElement, which
|
||||
// hasn't been monkeypatched by the polyfill yet. If you were to try it as-is
|
||||
// you'd get an "Illegal Constructor", because that's what the Zig HTMLElement
|
||||
// constructor does (and that's correct).
|
||||
// However, once HTMLElement is monkeypatched, it'll work. One simple solution
|
||||
// is to run the webcomponents.js polyfill proactively on each page, ensuring
|
||||
// that HTMLElement is monkeypatched before any other JavaScript is run. But
|
||||
// that adds _a lot_ of overhead.
|
||||
// So instead of always running the [large and intrusive] webcomponents.js
|
||||
// polyfill, we'll always run this little snippet. It wraps the HTMLElement
|
||||
// constructor. When the Lightpanda class is created, it'll extend our little
|
||||
// wrapper. But, unlike the Zig default constructor which throws, our code
|
||||
// calls the "real" constructor. That might seem like the same thing, but by the
|
||||
// time our wrapper is called, the webcomponents.js polyfill will have been
|
||||
// loaded and the "real" constructor will be the monkeypatched version.
|
||||
// TL;DR creates a layer of indirection for the constructor, so that, when it's
|
||||
// actually instantiated, the webcomponents.js polyfill will have been loaded.
|
||||
pub const pre =
|
||||
\\ (() => {
|
||||
\\ const HE = window.HTMLElement;
|
||||
\\ const b = function() { return HE.prototype.constructor.call(this); }
|
||||
\\ b.prototype = HE.prototype;
|
||||
\\ window.HTMLElement = b;
|
||||
\\ })();
|
||||
;
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.webcomponents" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "<div id=main></div>" });
|
||||
defer runner.deinit();
|
||||
|
||||
try @import("polyfill.zig").preload(testing.allocator, runner.page.main_context);
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ class LightPanda extends HTMLElement {
|
||||
\\ constructor() {
|
||||
\\ super();
|
||||
\\ }
|
||||
\\ connectedCallback() {
|
||||
\\ this.append('connected');
|
||||
\\ }
|
||||
\\ }
|
||||
\\ window.customElements.define("lightpanda-test", LightPanda);
|
||||
\\ const main = document.getElementById('main');
|
||||
\\ main.appendChild(document.createElement('lightpanda-test'));
|
||||
,
|
||||
null,
|
||||
},
|
||||
|
||||
.{ "main.innerHTML", "<lightpanda-test>connected</lightpanda-test>" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -62,20 +62,38 @@ const FlatRenderer = struct {
|
||||
gop.value_ptr.* = x;
|
||||
}
|
||||
|
||||
const _x: f64 = @floatFromInt(x);
|
||||
const y: f64 = 0.0;
|
||||
const w: f64 = 1.0;
|
||||
const h: f64 = 1.0;
|
||||
|
||||
return .{
|
||||
.x = @floatFromInt(x),
|
||||
.y = 0.0,
|
||||
.width = 1.0,
|
||||
.height = 1.0,
|
||||
.x = _x,
|
||||
.y = y,
|
||||
.width = w,
|
||||
.height = h,
|
||||
.left = _x,
|
||||
.top = y,
|
||||
.right = _x + w,
|
||||
.bottom = y + h,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn boundingRect(self: *const FlatRenderer) Element.DOMRect {
|
||||
const x: f64 = 0.0;
|
||||
const y: f64 = 0.0;
|
||||
const w: f64 = @floatFromInt(self.width());
|
||||
const h: f64 = @floatFromInt(self.width());
|
||||
|
||||
return .{
|
||||
.x = 0.0,
|
||||
.y = 0.0,
|
||||
.width = @floatFromInt(self.width()),
|
||||
.height = @floatFromInt(self.height()),
|
||||
.x = x,
|
||||
.y = y,
|
||||
.width = w,
|
||||
.height = h,
|
||||
.left = x,
|
||||
.top = y,
|
||||
.right = x + w,
|
||||
.bottom = y + h,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ pub const LookupOpts = struct {
|
||||
request_time: ?i64 = null,
|
||||
origin_uri: ?*const Uri = null,
|
||||
navigation: bool = true,
|
||||
is_http: bool,
|
||||
};
|
||||
|
||||
pub const Jar = struct {
|
||||
@@ -32,6 +33,13 @@ pub const Jar = struct {
|
||||
self.cookies.deinit(self.allocator);
|
||||
}
|
||||
|
||||
pub fn clearRetainingCapacity(self: *Jar) void {
|
||||
for (self.cookies.items) |c| {
|
||||
c.deinit();
|
||||
}
|
||||
self.cookies.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
pub fn add(
|
||||
self: *Jar,
|
||||
cookie: Cookie,
|
||||
@@ -59,87 +67,33 @@ pub const Jar = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn removeExpired(self: *Jar, request_time: ?i64) void {
|
||||
if (self.cookies.items.len == 0) return;
|
||||
const time = request_time orelse std.time.timestamp();
|
||||
var i: usize = self.cookies.items.len - 1;
|
||||
while (i > 0) {
|
||||
defer i -= 1;
|
||||
const cookie = &self.cookies.items[i];
|
||||
if (isCookieExpired(cookie, time)) {
|
||||
self.cookies.swapRemove(i).deinit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn forRequest(self: *Jar, target_uri: *const Uri, writer: anytype, opts: LookupOpts) !void {
|
||||
const target_path = target_uri.path.percent_encoded;
|
||||
const target_host = (target_uri.host orelse return error.InvalidURI).percent_encoded;
|
||||
const target = PreparedUri{
|
||||
.host = (target_uri.host orelse return error.InvalidURI).percent_encoded,
|
||||
.path = target_uri.path.percent_encoded,
|
||||
.secure = std.mem.eql(u8, target_uri.scheme, "https"),
|
||||
};
|
||||
const same_site = try areSameSite(opts.origin_uri, target.host);
|
||||
|
||||
const same_site = try areSameSite(opts.origin_uri, target_host);
|
||||
const is_secure = std.mem.eql(u8, target_uri.scheme, "https");
|
||||
|
||||
var i: usize = 0;
|
||||
var cookies = self.cookies.items;
|
||||
const navigation = opts.navigation;
|
||||
const request_time = opts.request_time orelse std.time.timestamp();
|
||||
removeExpired(self, opts.request_time);
|
||||
|
||||
var first = true;
|
||||
while (i < cookies.len) {
|
||||
const cookie = &cookies[i];
|
||||
for (self.cookies.items) |*cookie| {
|
||||
if (!cookie.appliesTo(&target, same_site, opts.navigation, opts.is_http)) continue;
|
||||
|
||||
if (isCookieExpired(cookie, request_time)) {
|
||||
cookie.deinit();
|
||||
_ = self.cookies.swapRemove(i);
|
||||
// don't increment i !
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
|
||||
if (is_secure == false and cookie.secure) {
|
||||
// secure cookie can only be sent over HTTPs
|
||||
continue;
|
||||
}
|
||||
|
||||
if (same_site == false) {
|
||||
// If we aren't on the "same site" (matching 2nd level domain
|
||||
// taking into account public suffix list), then the cookie
|
||||
// can only be sent if cookie.same_site == .none, or if
|
||||
// we're navigating to (as opposed to, say, loading an image)
|
||||
// and cookie.same_site == .lax
|
||||
switch (cookie.same_site) {
|
||||
.strict => continue,
|
||||
.lax => if (navigation == false) continue,
|
||||
.none => {},
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const domain = cookie.domain;
|
||||
if (domain[0] == '.') {
|
||||
// When a Set-Cookie header has a Domain attribute
|
||||
// Then we will _always_ prefix it with a dot, extending its
|
||||
// availability to all subdomains (yes, setting the Domain
|
||||
// attributes EXPANDS the domains which the cookie will be
|
||||
// sent to, to always include all subdomains).
|
||||
if (std.mem.eql(u8, target_host, domain[1..]) == false and std.mem.endsWith(u8, target_host, domain) == false) {
|
||||
continue;
|
||||
}
|
||||
} else if (std.mem.eql(u8, target_host, domain) == false) {
|
||||
// When the Domain attribute isn't specific, then the cookie
|
||||
// is only sent on an exact match.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const path = cookie.path;
|
||||
if (path[path.len - 1] == '/') {
|
||||
// If our cookie has a trailing slash, we can only match is
|
||||
// the target path is a perfix. I.e., if our path is
|
||||
// /doc/ we can only match /doc/*
|
||||
if (std.mem.startsWith(u8, target_path, path) == false) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Our cookie path is something like /hello
|
||||
if (std.mem.startsWith(u8, target_path, path) == false) {
|
||||
// The target path has to either be /hello (it isn't)
|
||||
continue;
|
||||
} else if (target_path.len < path.len or (target_path.len > path.len and target_path[path.len] != '/')) {
|
||||
// Or it has to be something like /hello/* (it isn't)
|
||||
// it isn't!
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
// we have a match!
|
||||
if (first) {
|
||||
first = false;
|
||||
@@ -173,47 +127,9 @@ pub const Jar = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub const CookieList = struct {
|
||||
_cookies: std.ArrayListUnmanaged(*const Cookie) = .{},
|
||||
|
||||
pub fn deinit(self: *CookieList, allocator: Allocator) void {
|
||||
self._cookies.deinit(allocator);
|
||||
}
|
||||
|
||||
pub fn cookies(self: *const CookieList) []*const Cookie {
|
||||
return self._cookies.items;
|
||||
}
|
||||
|
||||
pub fn len(self: *const CookieList) usize {
|
||||
return self._cookies.items.len;
|
||||
}
|
||||
|
||||
pub fn write(self: *const CookieList, writer: anytype) !void {
|
||||
const all = self._cookies.items;
|
||||
if (all.len == 0) {
|
||||
return;
|
||||
}
|
||||
try writeCookie(all[0], writer);
|
||||
for (all[1..]) |cookie| {
|
||||
try writer.writeAll("; ");
|
||||
try writeCookie(cookie, writer);
|
||||
}
|
||||
}
|
||||
|
||||
fn writeCookie(cookie: *const Cookie, writer: anytype) !void {
|
||||
if (cookie.name.len > 0) {
|
||||
try writer.writeAll(cookie.name);
|
||||
try writer.writeByte('=');
|
||||
}
|
||||
if (cookie.value.len > 0) {
|
||||
try writer.writeAll(cookie.value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn isCookieExpired(cookie: *const Cookie, now: i64) bool {
|
||||
const ce = cookie.expires orelse return false;
|
||||
return ce <= now;
|
||||
return ce <= @as(f64, @floatFromInt(now));
|
||||
}
|
||||
|
||||
fn areCookiesEqual(a: *const Cookie, b: *const Cookie) bool {
|
||||
@@ -256,12 +172,12 @@ pub const Cookie = struct {
|
||||
arena: ArenaAllocator,
|
||||
name: []const u8,
|
||||
value: []const u8,
|
||||
path: []const u8,
|
||||
domain: []const u8,
|
||||
expires: ?i64,
|
||||
secure: bool,
|
||||
http_only: bool,
|
||||
same_site: SameSite,
|
||||
path: []const u8,
|
||||
expires: ?f64,
|
||||
secure: bool = false,
|
||||
http_only: bool = false,
|
||||
same_site: SameSite = .none,
|
||||
|
||||
const SameSite = enum {
|
||||
strict,
|
||||
@@ -292,9 +208,6 @@ pub const Cookie = struct {
|
||||
// this check is necessary, `std.mem.minMax` asserts len > 0
|
||||
return error.Empty;
|
||||
}
|
||||
|
||||
const host = (uri.host orelse return error.InvalidURI).percent_encoded;
|
||||
|
||||
{
|
||||
const min, const max = std.mem.minMax(u8, str);
|
||||
if (min < 32 or max > 126) {
|
||||
@@ -313,7 +226,7 @@ pub const Cookie = struct {
|
||||
var secure: ?bool = null;
|
||||
var max_age: ?i64 = null;
|
||||
var http_only: ?bool = null;
|
||||
var expires: ?DateTime = null;
|
||||
var expires: ?[]const u8 = null;
|
||||
var same_site: ?Cookie.SameSite = null;
|
||||
|
||||
var it = std.mem.splitScalar(u8, rest, ';');
|
||||
@@ -339,37 +252,13 @@ pub const Cookie = struct {
|
||||
samesite,
|
||||
}, std.ascii.lowerString(&scrap, key_string)) orelse continue;
|
||||
|
||||
var value = if (sep == attribute.len) "" else trim(attribute[sep + 1 ..]);
|
||||
const value = if (sep == attribute.len) "" else trim(attribute[sep + 1 ..]);
|
||||
switch (key) {
|
||||
.path => {
|
||||
// path attribute value either begins with a '/' or we
|
||||
// ignore it and use the "default-path" algorithm
|
||||
if (value.len > 0 and value[0] == '/') {
|
||||
path = value;
|
||||
}
|
||||
},
|
||||
.domain => {
|
||||
if (value.len == 0) {
|
||||
continue;
|
||||
}
|
||||
if (value[0] == '.') {
|
||||
// leading dot is ignored
|
||||
value = value[1..];
|
||||
}
|
||||
|
||||
if (std.mem.indexOfScalarPos(u8, value, 0, '.') == null and std.ascii.eqlIgnoreCase("localhost", value) == false) {
|
||||
// can't set a cookie for a TLD
|
||||
return error.InvalidDomain;
|
||||
}
|
||||
|
||||
if (std.mem.endsWith(u8, host, value) == false) {
|
||||
return error.InvalidDomain;
|
||||
}
|
||||
domain = value;
|
||||
},
|
||||
.path => path = value,
|
||||
.domain => domain = value,
|
||||
.secure => secure = true,
|
||||
.@"max-age" => max_age = std.fmt.parseInt(i64, value, 10) catch continue,
|
||||
.expires => expires = DateTime.parse(value, .rfc822) catch continue,
|
||||
.expires => expires = value,
|
||||
.httponly => http_only = true,
|
||||
.samesite => {
|
||||
same_site = std.meta.stringToEnum(Cookie.SameSite, std.ascii.lowerString(&scrap, value)) orelse continue;
|
||||
@@ -386,27 +275,33 @@ pub const Cookie = struct {
|
||||
const aa = arena.allocator();
|
||||
const owned_name = try aa.dupe(u8, cookie_name);
|
||||
const owned_value = try aa.dupe(u8, cookie_value);
|
||||
const owned_path = if (path) |p|
|
||||
try aa.dupe(u8, p)
|
||||
else
|
||||
try defaultPath(aa, uri.path.percent_encoded);
|
||||
const owned_path = try parsePath(aa, uri, path);
|
||||
const owned_domain = try parseDomain(aa, uri, domain);
|
||||
|
||||
const owned_domain = if (domain) |d| blk: {
|
||||
const s = try aa.alloc(u8, d.len + 1);
|
||||
s[0] = '.';
|
||||
@memcpy(s[1..], d);
|
||||
break :blk s;
|
||||
} else blk: {
|
||||
break :blk try aa.dupe(u8, host);
|
||||
};
|
||||
|
||||
var normalized_expires: ?i64 = null;
|
||||
var normalized_expires: ?f64 = null;
|
||||
if (max_age) |ma| {
|
||||
normalized_expires = std.time.timestamp() + ma;
|
||||
normalized_expires = @floatFromInt(std.time.timestamp() + ma);
|
||||
} else {
|
||||
// max age takes priority over expires
|
||||
if (expires) |e| {
|
||||
normalized_expires = e.sub(DateTime.now(), .seconds);
|
||||
if (expires) |expires_| {
|
||||
var exp_dt = DateTime.parse(expires_, .rfc822) catch null;
|
||||
if (exp_dt == null) {
|
||||
if ((expires_.len > 11 and expires_[7] == '-' and expires_[11] == '-')) {
|
||||
// Replace dashes and try again
|
||||
const output = try aa.dupe(u8, expires_);
|
||||
output[7] = ' ';
|
||||
output[11] = ' ';
|
||||
exp_dt = DateTime.parse(output, .rfc822) catch null;
|
||||
}
|
||||
}
|
||||
if (exp_dt) |dt| {
|
||||
normalized_expires = @floatFromInt(dt.unix(.seconds));
|
||||
} else {
|
||||
// Algolia, for example, will call document.setCookie with
|
||||
// an expired value which is literally 'Invalid Date'
|
||||
// (it's trying to do something like: `new Date() + undefined`).
|
||||
log.debug(.web_api, "cookie expires date", .{ .date = expires_ });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,6 +318,100 @@ pub const Cookie = struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn parsePath(arena: Allocator, uri: ?*const std.Uri, explicit_path: ?[]const u8) ![]const u8 {
|
||||
// path attribute value either begins with a '/' or we
|
||||
// ignore it and use the "default-path" algorithm
|
||||
if (explicit_path) |path| {
|
||||
if (path.len > 0 and path[0] == '/') {
|
||||
return try arena.dupe(u8, path);
|
||||
}
|
||||
}
|
||||
|
||||
// default-path
|
||||
const url_path = (uri orelse return "/").path;
|
||||
|
||||
const either = url_path.percent_encoded;
|
||||
if (either.len == 0 or (either.len == 1 and either[0] == '/')) {
|
||||
return "/";
|
||||
}
|
||||
|
||||
var owned_path: []const u8 = try percentEncode(arena, url_path, isPathChar);
|
||||
const last = std.mem.lastIndexOfScalar(u8, owned_path[1..], '/') orelse {
|
||||
return "/";
|
||||
};
|
||||
return try arena.dupe(u8, owned_path[0 .. last + 1]);
|
||||
}
|
||||
|
||||
pub fn parseDomain(arena: Allocator, uri: ?*const std.Uri, explicit_domain: ?[]const u8) ![]const u8 {
|
||||
var encoded_host: ?[]const u8 = null;
|
||||
if (uri) |uri_| {
|
||||
const uri_host = uri_.host orelse return error.InvalidURI;
|
||||
const host = try percentEncode(arena, uri_host, isHostChar);
|
||||
_ = toLower(host);
|
||||
encoded_host = host;
|
||||
}
|
||||
|
||||
if (explicit_domain) |domain| {
|
||||
if (domain.len > 0) {
|
||||
const no_leading_dot = if (domain[0] == '.') domain[1..] else domain;
|
||||
|
||||
var list = std.ArrayList(u8).init(arena);
|
||||
try list.ensureTotalCapacity(no_leading_dot.len + 1); // Expect no precents needed
|
||||
list.appendAssumeCapacity('.');
|
||||
try std.Uri.Component.percentEncode(list.writer(), no_leading_dot, isHostChar);
|
||||
var owned_domain: []u8 = list.items; // @memory retains memory used before growing
|
||||
_ = toLower(owned_domain);
|
||||
|
||||
if (std.mem.indexOfScalarPos(u8, owned_domain, 1, '.') == null and std.mem.eql(u8, "localhost", owned_domain[1..]) == false) {
|
||||
// can't set a cookie for a TLD
|
||||
return error.InvalidDomain;
|
||||
}
|
||||
if (encoded_host) |host| {
|
||||
if (std.mem.endsWith(u8, host, owned_domain[1..]) == false) {
|
||||
return error.InvalidDomain;
|
||||
}
|
||||
}
|
||||
|
||||
return owned_domain;
|
||||
}
|
||||
}
|
||||
|
||||
return encoded_host orelse return error.InvalidDomain; // default-domain
|
||||
}
|
||||
|
||||
pub fn percentEncode(arena: Allocator, component: std.Uri.Component, comptime isValidChar: fn (u8) bool) ![]u8 {
|
||||
switch (component) {
|
||||
.raw => |str| {
|
||||
var list = std.ArrayList(u8).init(arena);
|
||||
try list.ensureTotalCapacity(str.len); // Expect no precents needed
|
||||
try std.Uri.Component.percentEncode(list.writer(), str, isValidChar);
|
||||
return list.items; // @memory retains memory used before growing
|
||||
},
|
||||
.percent_encoded => |str| {
|
||||
return try arena.dupe(u8, str);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn isHostChar(c: u8) bool {
|
||||
return switch (c) {
|
||||
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true,
|
||||
'!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=' => true,
|
||||
':' => true,
|
||||
'[', ']' => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn isPathChar(c: u8) bool {
|
||||
return switch (c) {
|
||||
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true,
|
||||
'!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=' => true,
|
||||
'/', ':', '@' => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn parseNameValue(str: []const u8) !struct { []const u8, []const u8, []const u8 } {
|
||||
const key_value_end = std.mem.indexOfScalarPos(u8, str, 0, ';') orelse str.len;
|
||||
const rest = if (key_value_end == str.len) "" else str[key_value_end + 1 ..];
|
||||
@@ -439,17 +428,77 @@ pub const Cookie = struct {
|
||||
const value = trim(str[sep + 1 .. key_value_end]);
|
||||
return .{ name, value, rest };
|
||||
}
|
||||
|
||||
pub fn appliesTo(self: *const Cookie, url: *const PreparedUri, same_site: bool, navigation: bool, is_http: bool) bool {
|
||||
if (self.http_only and is_http == false) {
|
||||
// http only cookies can be accessed from Javascript
|
||||
return false;
|
||||
}
|
||||
|
||||
if (url.secure == false and self.secure) {
|
||||
// secure cookie can only be sent over HTTPs
|
||||
return false;
|
||||
}
|
||||
|
||||
if (same_site == false) {
|
||||
// If we aren't on the "same site" (matching 2nd level domain
|
||||
// taking into account public suffix list), then the cookie
|
||||
// can only be sent if cookie.same_site == .none, or if
|
||||
// we're navigating to (as opposed to, say, loading an image)
|
||||
// and cookie.same_site == .lax
|
||||
switch (self.same_site) {
|
||||
.strict => return false,
|
||||
.lax => if (navigation == false) return false,
|
||||
.none => {},
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
if (self.domain[0] == '.') {
|
||||
// When a Set-Cookie header has a Domain attribute
|
||||
// Then we will _always_ prefix it with a dot, extending its
|
||||
// availability to all subdomains (yes, setting the Domain
|
||||
// attributes EXPANDS the domains which the cookie will be
|
||||
// sent to, to always include all subdomains).
|
||||
if (std.mem.eql(u8, url.host, self.domain[1..]) == false and std.mem.endsWith(u8, url.host, self.domain) == false) {
|
||||
return false;
|
||||
}
|
||||
} else if (std.mem.eql(u8, url.host, self.domain) == false) {
|
||||
// When the Domain attribute isn't specific, then the cookie
|
||||
// is only sent on an exact match.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
if (self.path[self.path.len - 1] == '/') {
|
||||
// If our cookie has a trailing slash, we can only match is
|
||||
// the target path is a perfix. I.e., if our path is
|
||||
// /doc/ we can only match /doc/*
|
||||
if (std.mem.startsWith(u8, url.path, self.path) == false) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// Our cookie path is something like /hello
|
||||
if (std.mem.startsWith(u8, url.path, self.path) == false) {
|
||||
// The target path has to either be /hello (it isn't)
|
||||
return false;
|
||||
} else if (url.path.len < self.path.len or (url.path.len > self.path.len and url.path[self.path.len] != '/')) {
|
||||
// Or it has to be something like /hello/* (it isn't)
|
||||
// it isn't!
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
fn defaultPath(allocator: Allocator, document_path: []const u8) ![]const u8 {
|
||||
if (document_path.len == 0 or (document_path.len == 1 and document_path[0] == '/')) {
|
||||
return "/";
|
||||
}
|
||||
const last = std.mem.lastIndexOfScalar(u8, document_path[1..], '/') orelse {
|
||||
return "/";
|
||||
pub const PreparedUri = struct {
|
||||
host: []const u8, // Percent encoded, lower case
|
||||
path: []const u8, // Percent encoded
|
||||
secure: bool, // True if scheme is https
|
||||
};
|
||||
return try allocator.dupe(u8, document_path[0 .. last + 1]);
|
||||
}
|
||||
|
||||
fn trim(str: []const u8) []const u8 {
|
||||
return std.mem.trim(u8, str, &std.ascii.whitespace);
|
||||
@@ -463,6 +512,13 @@ fn trimRight(str: []const u8) []const u8 {
|
||||
return std.mem.trimLeft(u8, str, &std.ascii.whitespace);
|
||||
}
|
||||
|
||||
pub fn toLower(str: []u8) []u8 {
|
||||
for (str, 0..) |c, i| {
|
||||
str[i] = std.ascii.toLower(c);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "cookie: findSecondLevelDomain" {
|
||||
const cases = [_]struct { []const u8, []const u8 }{
|
||||
@@ -548,7 +604,7 @@ test "Jar: forRequest" {
|
||||
|
||||
{
|
||||
// test with no cookies
|
||||
try expectCookies("", &jar, test_uri, .{});
|
||||
try expectCookies("", &jar, test_uri, .{ .is_http = true });
|
||||
}
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "global1=1"), now);
|
||||
@@ -562,97 +618,114 @@ test "Jar: forRequest" {
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri_2, "domain1=9;domain=test.lightpanda.io"), now);
|
||||
|
||||
// nothing fancy here
|
||||
try expectCookies("global1=1; global2=2", &jar, test_uri, .{});
|
||||
try expectCookies("global1=1; global2=2", &jar, test_uri, .{ .origin_uri = &test_uri, .navigation = false });
|
||||
try expectCookies("global1=1; global2=2", &jar, test_uri, .{ .is_http = true });
|
||||
try expectCookies("global1=1; global2=2", &jar, test_uri, .{ .origin_uri = &test_uri, .navigation = false, .is_http = true });
|
||||
|
||||
// We have a cookie where Domain=lightpanda.io
|
||||
// This should _not_ match xyxlightpanda.io
|
||||
try expectCookies("", &jar, try std.Uri.parse("http://anothersitelightpanda.io/"), .{
|
||||
.origin_uri = &test_uri,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// matching path without trailing /
|
||||
try expectCookies("global1=1; global2=2; path1=3", &jar, try std.Uri.parse("http://lightpanda.io/about"), .{
|
||||
.origin_uri = &test_uri,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// incomplete prefix path
|
||||
try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/abou"), .{
|
||||
.origin_uri = &test_uri,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// path doesn't match
|
||||
try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/aboutus"), .{
|
||||
.origin_uri = &test_uri,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// path doesn't match cookie directory
|
||||
try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/docs"), .{
|
||||
.origin_uri = &test_uri,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// exact directory match
|
||||
try expectCookies("global1=1; global2=2; path2=4", &jar, try std.Uri.parse("http://lightpanda.io/docs/"), .{
|
||||
.origin_uri = &test_uri,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// sub directory match
|
||||
try expectCookies("global1=1; global2=2; path2=4", &jar, try std.Uri.parse("http://lightpanda.io/docs/more"), .{
|
||||
.origin_uri = &test_uri,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// secure
|
||||
try expectCookies("global1=1; global2=2; secure=5", &jar, try std.Uri.parse("https://lightpanda.io/"), .{
|
||||
.origin_uri = &test_uri,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// navigational cross domain, secure
|
||||
try expectCookies("global1=1; global2=2; secure=5; sitenone=6; sitelax=7", &jar, try std.Uri.parse("https://lightpanda.io/x/"), .{
|
||||
.origin_uri = &(try std.Uri.parse("https://example.com/")),
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// navigational cross domain, insecure
|
||||
try expectCookies("global1=1; global2=2; sitelax=7", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{
|
||||
.origin_uri = &(try std.Uri.parse("https://example.com/")),
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// non-navigational cross domain, insecure
|
||||
try expectCookies("", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{
|
||||
.origin_uri = &(try std.Uri.parse("https://example.com/")),
|
||||
.navigation = false,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// non-navigational cross domain, secure
|
||||
try expectCookies("sitenone=6", &jar, try std.Uri.parse("https://lightpanda.io/x/"), .{
|
||||
.origin_uri = &(try std.Uri.parse("https://example.com/")),
|
||||
.navigation = false,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// non-navigational same origin
|
||||
try expectCookies("global1=1; global2=2; sitelax=7; sitestrict=8", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{
|
||||
.origin_uri = &(try std.Uri.parse("https://lightpanda.io/")),
|
||||
.navigation = false,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// exact domain match + suffix
|
||||
try expectCookies("global2=2; domain1=9", &jar, try std.Uri.parse("http://test.lightpanda.io/"), .{
|
||||
.origin_uri = &test_uri,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// domain suffix match + suffix
|
||||
try expectCookies("global2=2; domain1=9", &jar, try std.Uri.parse("http://1.test.lightpanda.io/"), .{
|
||||
.origin_uri = &test_uri,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// non-matching domain
|
||||
try expectCookies("global2=2", &jar, try std.Uri.parse("http://other.lightpanda.io/"), .{
|
||||
.origin_uri = &test_uri,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
const l = jar.cookies.items.len;
|
||||
try expectCookies("global1=1", &jar, test_uri, .{
|
||||
.request_time = now + 100,
|
||||
.origin_uri = &test_uri,
|
||||
.is_http = true,
|
||||
});
|
||||
try testing.expectEqual(l - 1, jar.cookies.items.len);
|
||||
|
||||
@@ -660,40 +733,6 @@ test "Jar: forRequest" {
|
||||
// the 'global2' cookie
|
||||
}
|
||||
|
||||
test "CookieList: write" {
|
||||
var arr: std.ArrayListUnmanaged(u8) = .{};
|
||||
defer arr.deinit(testing.allocator);
|
||||
|
||||
var cookie_list = CookieList{};
|
||||
defer cookie_list.deinit(testing.allocator);
|
||||
|
||||
const c1 = try Cookie.parse(testing.allocator, &test_uri, "cookie_name=cookie_value");
|
||||
defer c1.deinit();
|
||||
{
|
||||
try cookie_list._cookies.append(testing.allocator, &c1);
|
||||
try cookie_list.write(arr.writer(testing.allocator));
|
||||
try testing.expectEqual("cookie_name=cookie_value", arr.items);
|
||||
}
|
||||
|
||||
const c2 = try Cookie.parse(testing.allocator, &test_uri, "x84");
|
||||
defer c2.deinit();
|
||||
{
|
||||
arr.clearRetainingCapacity();
|
||||
try cookie_list._cookies.append(testing.allocator, &c2);
|
||||
try cookie_list.write(arr.writer(testing.allocator));
|
||||
try testing.expectEqual("cookie_name=cookie_value; x84", arr.items);
|
||||
}
|
||||
|
||||
const c3 = try Cookie.parse(testing.allocator, &test_uri, "nope=");
|
||||
defer c3.deinit();
|
||||
{
|
||||
arr.clearRetainingCapacity();
|
||||
try cookie_list._cookies.append(testing.allocator, &c3);
|
||||
try cookie_list.write(arr.writer(testing.allocator));
|
||||
try testing.expectEqual("cookie_name=cookie_value; x84; nope=", arr.items);
|
||||
}
|
||||
}
|
||||
|
||||
test "Cookie: parse key=value" {
|
||||
try expectError(error.Empty, null, "");
|
||||
try expectError(error.InvalidByteSequence, null, &.{ 'a', 30, '=', 'b' });
|
||||
@@ -816,7 +855,8 @@ test "Cookie: parse expires" {
|
||||
try expectAttribute(.{ .expires = null }, null, "b;expires=13.22");
|
||||
try expectAttribute(.{ .expires = null }, null, "b;expires=33");
|
||||
|
||||
try expectAttribute(.{ .expires = 1918798080 - std.time.timestamp() }, null, "b;expires=Wed, 21 Oct 2030 07:28:00 GMT");
|
||||
try expectAttribute(.{ .expires = 1918798080 }, null, "b;expires=Wed, 21 Oct 2030 07:28:00 GMT");
|
||||
try expectAttribute(.{ .expires = 1784275395 }, null, "b;expires=Fri, 17-Jul-2026 08:03:15 GMT");
|
||||
// max-age has priority over expires
|
||||
try expectAttribute(.{ .expires = std.time.timestamp() + 10 }, null, "b;Max-Age=10; expires=Wed, 21 Oct 2030 07:28:00 GMT");
|
||||
}
|
||||
@@ -836,7 +876,7 @@ test "Cookie: parse all" {
|
||||
.http_only = true,
|
||||
.secure = true,
|
||||
.domain = ".lightpanda.io",
|
||||
.expires = std.time.timestamp() + 30,
|
||||
.expires = @floatFromInt(std.time.timestamp() + 30),
|
||||
}, "https://lightpanda.io/cms/users", "user-id=9000; HttpOnly; Max-Age=30; Secure; path=/; Domain=lightpanda.io");
|
||||
|
||||
try expectCookie(.{
|
||||
@@ -847,7 +887,7 @@ test "Cookie: parse all" {
|
||||
.secure = false,
|
||||
.domain = ".localhost",
|
||||
.same_site = .lax,
|
||||
.expires = std.time.timestamp() + 7200,
|
||||
.expires = @floatFromInt(std.time.timestamp() + 7200),
|
||||
}, "http://localhost:8000/login", "app_session=123; Max-Age=7200; path=/; domain=localhost; httponly; samesite=lax");
|
||||
}
|
||||
|
||||
@@ -874,7 +914,7 @@ const ExpectedCookie = struct {
|
||||
value: []const u8,
|
||||
path: []const u8,
|
||||
domain: []const u8,
|
||||
expires: ?i64 = null,
|
||||
expires: ?f64 = null,
|
||||
secure: bool = false,
|
||||
http_only: bool = false,
|
||||
same_site: Cookie.SameSite = .lax,
|
||||
@@ -893,7 +933,7 @@ fn expectCookie(expected: ExpectedCookie, url: []const u8, set_cookie: []const u
|
||||
try testing.expectEqual(expected.path, cookie.path);
|
||||
try testing.expectEqual(expected.domain, cookie.domain);
|
||||
|
||||
try testing.expectDelta(expected.expires, cookie.expires, 2);
|
||||
try testing.expectDelta(expected.expires, cookie.expires, 2.0);
|
||||
}
|
||||
|
||||
fn expectAttribute(expected: anytype, url: ?[]const u8, set_cookie: []const u8) !void {
|
||||
@@ -903,7 +943,10 @@ fn expectAttribute(expected: anytype, url: ?[]const u8, set_cookie: []const u8)
|
||||
|
||||
inline for (@typeInfo(@TypeOf(expected)).@"struct".fields) |f| {
|
||||
if (comptime std.mem.eql(u8, f.name, "expires")) {
|
||||
try testing.expectDelta(expected.expires, cookie.expires, 1);
|
||||
switch (@typeInfo(@TypeOf(expected.expires))) {
|
||||
.int, .comptime_int => try testing.expectDelta(@as(f64, @floatFromInt(expected.expires)), cookie.expires, 1.0),
|
||||
else => try testing.expectDelta(expected.expires, cookie.expires, 1.0),
|
||||
}
|
||||
} else {
|
||||
try testing.expectEqual(@field(expected, f.name), @field(cookie, f.name));
|
||||
}
|
||||
|
||||
@@ -54,6 +54,11 @@ pub const URL = struct {
|
||||
uri: std.Uri,
|
||||
search_params: URLSearchParams,
|
||||
|
||||
pub const empty = URL{
|
||||
.uri = .{ .scheme = "" },
|
||||
.search_params = .{},
|
||||
};
|
||||
|
||||
const URLArg = union(enum) {
|
||||
url: *URL,
|
||||
element: *parser.ElementHTML,
|
||||
@@ -84,7 +89,17 @@ pub const URL = struct {
|
||||
raw = if (url == .url) url_str else try arena.dupe(u8, url_str);
|
||||
}
|
||||
|
||||
const uri = std.Uri.parse(raw.?) catch return error.TypeError;
|
||||
const uri = std.Uri.parse(raw.?) catch blk: {
|
||||
if (!std.mem.endsWith(u8, raw.?, "://")) {
|
||||
return error.TypeError;
|
||||
}
|
||||
// schema only is valid!
|
||||
break :blk std.Uri{
|
||||
.scheme = raw.?[0 .. raw.?.len - 3],
|
||||
.host = .{ .percent_encoded = "" },
|
||||
};
|
||||
};
|
||||
|
||||
return init(arena, uri);
|
||||
}
|
||||
|
||||
@@ -553,6 +568,14 @@ test "Browser.URL" {
|
||||
.{ "var url = new URL('over?9000', 'https://lightpanda.io')", null },
|
||||
.{ "url.href", "https://lightpanda.io/over?9000" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let sk = new URL('sveltekit-internal://')", null },
|
||||
.{ "sk.protocol", "sveltekit-internal:" },
|
||||
.{ "sk.host", "" },
|
||||
.{ "sk.hostname", "" },
|
||||
.{ "sk.href", "sveltekit-internal://" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
test "Browser.URLSearchParams" {
|
||||
|
||||
93
src/browser/webcomponents/custom_element_registry.zig
Normal file
93
src/browser/webcomponents/custom_element_registry.zig
Normal file
@@ -0,0 +1,93 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Currently not used. Relying on polyfill instead
|
||||
|
||||
const std = @import("std");
|
||||
const log = @import("../../log.zig");
|
||||
const v8 = @import("v8");
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
const Element = @import("../dom/element.zig").Element;
|
||||
|
||||
pub const CustomElementRegistry = struct {
|
||||
// tag_name -> Function
|
||||
lookup: std.StringHashMapUnmanaged(Env.Function) = .empty,
|
||||
|
||||
pub fn _define(self: *CustomElementRegistry, tag_name: []const u8, fun: Env.Function, page: *Page) !void {
|
||||
log.info(.browser, "define custom element", .{ .name = tag_name });
|
||||
|
||||
const arena = page.arena;
|
||||
const gop = try self.lookup.getOrPut(arena, tag_name);
|
||||
if (!gop.found_existing) {
|
||||
errdefer _ = self.lookup.remove(tag_name);
|
||||
const owned_tag_name = try arena.dupe(u8, tag_name);
|
||||
gop.key_ptr.* = owned_tag_name;
|
||||
}
|
||||
gop.value_ptr.* = fun;
|
||||
fun.setName(tag_name);
|
||||
}
|
||||
|
||||
pub fn _get(self: *CustomElementRegistry, name: []const u8) ?Env.Function {
|
||||
return self.lookup.get(name);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
test "Browser.CustomElementRegistry" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
try runner.testCases(&.{
|
||||
// Basic registry access
|
||||
.{ "typeof customElements", "object" },
|
||||
.{ "customElements instanceof CustomElementRegistry", "true" },
|
||||
|
||||
// Define a simple custom element
|
||||
.{
|
||||
\\ class MyElement extends HTMLElement {
|
||||
\\ constructor() {
|
||||
\\ super();
|
||||
\\ this.textContent = 'Hello World';
|
||||
\\ }
|
||||
\\ }
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "customElements.define('my-element', MyElement)", "undefined" },
|
||||
|
||||
// Check if element is defined
|
||||
.{ "customElements.get('my-element') === MyElement", "true" },
|
||||
// .{ "customElements.get('non-existent')", "null" },
|
||||
|
||||
// Create element via document.createElement
|
||||
.{ "let el = document.createElement('my-element')", "undefined" },
|
||||
.{ "el instanceof MyElement", "true" },
|
||||
.{ "el instanceof HTMLElement", "true" },
|
||||
.{ "el.tagName", "MY-ELEMENT" },
|
||||
.{ "el.textContent", "Hello World" },
|
||||
|
||||
// Create element via HTML parsing
|
||||
// .{ "document.body.innerHTML = '<my-element></my-element>'", "undefined" },
|
||||
// .{ "let parsed = document.querySelector('my-element')", "undefined" },
|
||||
// .{ "parsed instanceof MyElement", "true" },
|
||||
// .{ "parsed.textContent", "Hello World" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -31,7 +31,7 @@ pub const XMLHttpRequestEventTarget = struct {
|
||||
pub const prototype = *EventTarget;
|
||||
|
||||
// Extend libdom event target for pure zig struct.
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{},
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .xhr },
|
||||
|
||||
onloadstart_cbk: ?Function = null,
|
||||
onprogress_cbk: ?Function = null,
|
||||
@@ -39,6 +39,7 @@ pub const XMLHttpRequestEventTarget = struct {
|
||||
onload_cbk: ?Function = null,
|
||||
ontimeout_cbk: ?Function = null,
|
||||
onloadend_cbk: ?Function = null,
|
||||
onreadystatechange_cbk: ?Function = null,
|
||||
|
||||
fn register(
|
||||
self: *XMLHttpRequestEventTarget,
|
||||
@@ -86,6 +87,9 @@ pub const XMLHttpRequestEventTarget = struct {
|
||||
pub fn get_onloadend(self: *XMLHttpRequestEventTarget) ?Function {
|
||||
return self.onloadend_cbk;
|
||||
}
|
||||
pub fn get_onreadystatechange(self: *XMLHttpRequestEventTarget) ?Function {
|
||||
return self.onreadystatechange_cbk;
|
||||
}
|
||||
|
||||
pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, listener: EventHandler.Listener, page: *Page) !void {
|
||||
if (self.onloadstart_cbk) |cbk| try self.unregister("loadstart", cbk.id);
|
||||
@@ -111,4 +115,8 @@ pub const XMLHttpRequestEventTarget = struct {
|
||||
if (self.onloadend_cbk) |cbk| try self.unregister("loadend", cbk.id);
|
||||
self.onloadend_cbk = try self.register(page.arena, "loadend", listener);
|
||||
}
|
||||
pub fn set_onreadystatechange(self: *XMLHttpRequestEventTarget, listener: EventHandler.Listener, page: *Page) !void {
|
||||
if (self.onreadystatechange_cbk) |cbk| try self.unregister("readystatechange", cbk.id);
|
||||
self.onreadystatechange_cbk = try self.register(page.arena, "readystatechange", listener);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -115,17 +115,24 @@ const EntryIterable = iterator.Iterable(kv.EntryIterator, "FormDataEntryIterator
|
||||
// TODO: handle disabled fieldsets
|
||||
fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page) !kv.List {
|
||||
const arena = page.arena;
|
||||
const collection = try parser.formGetCollection(form);
|
||||
const len = try parser.htmlCollectionGetLength(collection);
|
||||
|
||||
// Don't use libdom's formGetCollection (aka dom_html_form_element_get_elements)
|
||||
// It doesn't work with dynamically added elements, because their form
|
||||
// property doesn't get set. We should fix that.
|
||||
// However, even once fixed, there are other form-collection features we
|
||||
// probably want to implement (like disabled fieldsets), so we might want
|
||||
// to stick with our own walker even if fix libdom to properly support
|
||||
// dynamically added elements.
|
||||
const node_list = try @import("../dom/css.zig").querySelectorAll(arena, @alignCast(@ptrCast(form)), "input,select,button,textarea");
|
||||
const nodes = node_list.nodes.items;
|
||||
|
||||
var entries: kv.List = .{};
|
||||
try entries.ensureTotalCapacity(arena, len);
|
||||
try entries.ensureTotalCapacity(arena, nodes.len);
|
||||
|
||||
var submitter_included = false;
|
||||
const submitter_name_ = try getSubmitterName(submitter_);
|
||||
|
||||
for (0..len) |i| {
|
||||
const node = try parser.htmlCollectionItem(collection, @intCast(i));
|
||||
for (nodes) |node| {
|
||||
const element = parser.nodeToElement(node);
|
||||
|
||||
// must have a name
|
||||
@@ -181,10 +188,7 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page
|
||||
submitter_included = true;
|
||||
}
|
||||
},
|
||||
else => {
|
||||
log.warn(.web_api, "unsupported form element", .{ .tag = @tagName(tag) });
|
||||
continue;
|
||||
},
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,6 +301,7 @@ test "Browser.FormData" {
|
||||
\\ <input type=submit name=s2 value=s2-v>
|
||||
\\ <input type=image name=i1 value=i1-v>
|
||||
\\ </form>
|
||||
\\ <input type=text name=abc value=123 form=form1>
|
||||
});
|
||||
defer runner.deinit();
|
||||
|
||||
@@ -356,6 +361,8 @@ test "Browser.FormData" {
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let form1 = document.getElementById('form1')", null },
|
||||
.{ "let input = document.createElement('input');", null },
|
||||
.{ "input.name = 'dyn'; input.value= 'dyn-v'; form1.appendChild(input);", null },
|
||||
.{ "let submit1 = document.getElementById('s1')", null },
|
||||
.{ "let f2 = new FormData(form1, submit1)", null },
|
||||
.{ "acc = '';", null },
|
||||
@@ -378,6 +385,7 @@ test "Browser.FormData" {
|
||||
\\mlt-2=water
|
||||
\\mlt-2=tea
|
||||
\\s1=s1-v
|
||||
\\dyn=dyn-v
|
||||
},
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -37,10 +37,10 @@ pub const ProgressEvent = struct {
|
||||
loaded: u64 = 0,
|
||||
total: u64 = 0,
|
||||
|
||||
pub fn constructor(eventType: []const u8, opts: ?EventInit) !ProgressEvent {
|
||||
pub fn constructor(event_type: []const u8, opts: ?EventInit) !ProgressEvent {
|
||||
const event = try parser.eventCreate();
|
||||
defer parser.eventDestroy(event);
|
||||
try parser.eventInit(event, eventType, .{});
|
||||
try parser.eventInit(event, event_type, .{});
|
||||
try parser.eventSetInternalType(event, .progress_event);
|
||||
|
||||
const o = opts orelse EventInit{};
|
||||
|
||||
@@ -138,6 +138,13 @@ pub const XMLHttpRequest = struct {
|
||||
done = 4,
|
||||
};
|
||||
|
||||
// class attributes
|
||||
pub const _UNSENT = @intFromEnum(State.unsent);
|
||||
pub const _OPENED = @intFromEnum(State.opened);
|
||||
pub const _HEADERS_RECEIVED = @intFromEnum(State.headers_received);
|
||||
pub const _LOADING = @intFromEnum(State.loading);
|
||||
pub const _DONE = @intFromEnum(State.done);
|
||||
|
||||
// https://xhr.spec.whatwg.org/#response-type
|
||||
const ResponseType = enum {
|
||||
Empty,
|
||||
@@ -360,6 +367,8 @@ pub const XMLHttpRequest = struct {
|
||||
// We can we defer event destroy once the event is dispatched.
|
||||
defer parser.eventDestroy(evt);
|
||||
|
||||
try parser.eventSetInternalType(evt, .xhr_event);
|
||||
|
||||
try parser.eventInit(evt, typ, .{ .bubbles = true, .cancelable = true });
|
||||
_ = try parser.eventTargetDispatchEvent(@as(*parser.EventTarget, @ptrCast(self)), evt);
|
||||
}
|
||||
@@ -458,7 +467,6 @@ pub const XMLHttpRequest = struct {
|
||||
&self.url.?.uri,
|
||||
self,
|
||||
onHttpRequestReady,
|
||||
self.loop,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -475,6 +483,7 @@ pub const XMLHttpRequest = struct {
|
||||
try self.cookie_jar.forRequest(&self.url.?.uri, arr.writer(self.arena), .{
|
||||
.navigation = false,
|
||||
.origin_uri = &self.origin_url.uri,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
if (arr.items.len > 0) {
|
||||
@@ -493,7 +502,7 @@ pub const XMLHttpRequest = struct {
|
||||
}
|
||||
}
|
||||
|
||||
try request.sendAsync(self.loop, self, .{});
|
||||
try request.sendAsync(self, .{});
|
||||
self.request = request;
|
||||
}
|
||||
|
||||
@@ -579,11 +588,27 @@ pub const XMLHttpRequest = struct {
|
||||
}
|
||||
|
||||
fn onErr(self: *XMLHttpRequest, err: anyerror) void {
|
||||
self.state = .done;
|
||||
self.send_flag = false;
|
||||
|
||||
// capture the state before we change it
|
||||
const s = self.state;
|
||||
|
||||
const is_abort = err == DOMError.Abort;
|
||||
|
||||
if (is_abort) {
|
||||
self.state = .unsent;
|
||||
} else {
|
||||
self.state = .done;
|
||||
self.dispatchEvt("error");
|
||||
}
|
||||
|
||||
if (s != .done or s != .unsent) {
|
||||
self.dispatchEvt("readystatechange");
|
||||
self.dispatchProgressEvent("error", .{});
|
||||
if (is_abort) {
|
||||
self.dispatchProgressEvent("abort", .{});
|
||||
}
|
||||
self.dispatchProgressEvent("loadend", .{});
|
||||
}
|
||||
|
||||
const level: log.Level = if (err == DOMError.Abort) .debug else .err;
|
||||
log.log(.http, level, "error", .{
|
||||
@@ -922,4 +947,26 @@ test "Browser.XHR.XMLHttpRequest" {
|
||||
// So the url has been retrieved.
|
||||
.{ "status", "200" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const req6 = new XMLHttpRequest()", null },
|
||||
.{
|
||||
\\ var readyStates = [];
|
||||
\\ var currentTarget = null;
|
||||
\\ req6.onreadystatechange = (e) => {
|
||||
\\ currentTarget = e.currentTarget;
|
||||
\\ readyStates.push(req6.readyState);
|
||||
\\ }
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "req6.open('GET', 'https://127.0.0.1:9581/xhr')", null },
|
||||
.{ "req6.send()", null },
|
||||
.{ "readyStates.length", "4" },
|
||||
.{ "readyStates[0] === XMLHttpRequest.OPENED", "true" },
|
||||
.{ "readyStates[1] === XMLHttpRequest.HEADERS_RECEIVED", "true" },
|
||||
.{ "readyStates[2] === XMLHttpRequest.LOADING", "true" },
|
||||
.{ "readyStates[3] === XMLHttpRequest.DONE", "true" },
|
||||
.{ "currentTarget == req6", "true" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -36,9 +36,9 @@ pub const XMLSerializer = struct {
|
||||
pub fn _serializeToString(_: *const XMLSerializer, root: *parser.Node, page: *Page) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).init(page.arena);
|
||||
switch (try parser.nodeType(root)) {
|
||||
.document => try dump.writeHTML(@as(*parser.Document, @ptrCast(root)), buf.writer()),
|
||||
.document => try dump.writeHTML(@as(*parser.Document, @ptrCast(root)), .{}, buf.writer()),
|
||||
.document_type => try dump.writeDocType(@as(*parser.DocumentType, @ptrCast(root)), buf.writer()),
|
||||
else => try dump.writeNode(root, buf.writer()),
|
||||
else => try dump.writeNode(root, .{}, buf.writer()),
|
||||
}
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
119
src/cdp/Node.zig
119
src/cdp/Node.zig
@@ -201,49 +201,69 @@ pub const Search = struct {
|
||||
// (For now, we only support direct children)
|
||||
|
||||
pub const Writer = struct {
|
||||
opts: Opts,
|
||||
node: *const Node,
|
||||
depth: i32,
|
||||
exclude_root: bool,
|
||||
root: *const Node,
|
||||
registry: *Registry,
|
||||
|
||||
pub const Opts = struct {};
|
||||
pub const Opts = struct {
|
||||
depth: i32 = 0,
|
||||
exclude_root: bool = false,
|
||||
};
|
||||
|
||||
pub fn jsonStringify(self: *const Writer, w: anytype) !void {
|
||||
self.toJSON(w) catch |err| {
|
||||
if (self.exclude_root) {
|
||||
_ = self.writeChildren(self.root, 1, w) catch |err| {
|
||||
log.err(.cdp, "node writeChildren", .{ .err = err });
|
||||
return error.OutOfMemory;
|
||||
};
|
||||
} else {
|
||||
self.toJSON(self.root, 0, w) catch |err| {
|
||||
// The only error our jsonStringify method can return is
|
||||
// @TypeOf(w).Error. In other words, our code can't return its own
|
||||
// error, we can only return a writer error. Kinda sucks.
|
||||
log.err(.cdp, "json stringify", .{ .err = err });
|
||||
log.err(.cdp, "node toJSON stringify", .{ .err = err });
|
||||
return error.OutOfMemory;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn toJSON(self: *const Writer, w: anytype) !void {
|
||||
fn toJSON(self: *const Writer, node: *const Node, depth: usize, w: anytype) !void {
|
||||
try w.beginObject();
|
||||
try self.writeCommon(self.node, false, w);
|
||||
try self.writeCommon(node, false, w);
|
||||
|
||||
{
|
||||
try w.objectField("children");
|
||||
const child_count = try self.writeChildren(node, depth, w);
|
||||
try w.objectField("childNodeCount");
|
||||
try w.write(child_count);
|
||||
|
||||
try w.endObject();
|
||||
}
|
||||
|
||||
fn writeChildren(self: *const Writer, node: *const Node, depth: usize, w: anytype) anyerror!usize {
|
||||
var registry = self.registry;
|
||||
const child_nodes = try parser.nodeGetChildNodes(self.node._node);
|
||||
const child_nodes = try parser.nodeGetChildNodes(node._node);
|
||||
const child_count = try parser.nodeListLength(child_nodes);
|
||||
const full_child = self.depth < 0 or self.depth < depth;
|
||||
|
||||
var i: usize = 0;
|
||||
try w.objectField("children");
|
||||
try w.beginArray();
|
||||
for (0..child_count) |_| {
|
||||
const child = (try parser.nodeListItem(child_nodes, @intCast(i))) orelse break;
|
||||
const child_node = try registry.register(child);
|
||||
if (full_child) {
|
||||
try self.toJSON(child_node, depth + 1, w);
|
||||
} else {
|
||||
try w.beginObject();
|
||||
try self.writeCommon(child_node, true, w);
|
||||
try w.endObject();
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
try w.endArray();
|
||||
|
||||
try w.objectField("childNodeCount");
|
||||
try w.write(i);
|
||||
}
|
||||
|
||||
try w.endObject();
|
||||
return i;
|
||||
}
|
||||
|
||||
fn writeCommon(self: *const Writer, node: *const Node, include_child_count: bool, w: anytype) !void {
|
||||
@@ -400,14 +420,15 @@ test "cdp Node: Writer" {
|
||||
var registry = Registry.init(testing.allocator);
|
||||
defer registry.deinit();
|
||||
|
||||
var doc = try testing.Document.init("<a id=a1></a><a id=a2></a>");
|
||||
var doc = try testing.Document.init("<a id=a1></a><div id=d2><a id=a2></a></div>");
|
||||
defer doc.deinit();
|
||||
|
||||
{
|
||||
const node = try registry.register(doc.asNode());
|
||||
const json = try std.json.stringifyAlloc(testing.allocator, Writer{
|
||||
.node = node,
|
||||
.opts = .{},
|
||||
.root = node,
|
||||
.depth = 0,
|
||||
.exclude_root = false,
|
||||
.registry = ®istry,
|
||||
}, .{});
|
||||
defer testing.allocator.free(json);
|
||||
@@ -445,8 +466,9 @@ test "cdp Node: Writer" {
|
||||
{
|
||||
const node = registry.lookup_by_id.get(1).?;
|
||||
const json = try std.json.stringifyAlloc(testing.allocator, Writer{
|
||||
.node = node,
|
||||
.opts = .{},
|
||||
.root = node,
|
||||
.depth = 1,
|
||||
.exclude_root = false,
|
||||
.registry = ®istry,
|
||||
}, .{});
|
||||
defer testing.allocator.free(json);
|
||||
@@ -495,4 +517,61 @@ test "cdp Node: Writer" {
|
||||
} },
|
||||
}, json);
|
||||
}
|
||||
|
||||
{
|
||||
const node = registry.lookup_by_id.get(1).?;
|
||||
const json = try std.json.stringifyAlloc(testing.allocator, Writer{
|
||||
.root = node,
|
||||
.depth = -1,
|
||||
.exclude_root = true,
|
||||
.registry = ®istry,
|
||||
}, .{});
|
||||
defer testing.allocator.free(json);
|
||||
|
||||
try testing.expectJson(&.{ .{
|
||||
.nodeId = 2,
|
||||
.backendNodeId = 2,
|
||||
.nodeType = 1,
|
||||
.nodeName = "HEAD",
|
||||
.localName = "head",
|
||||
.nodeValue = "",
|
||||
.childNodeCount = 0,
|
||||
.documentURL = null,
|
||||
.baseURL = null,
|
||||
.xmlVersion = "",
|
||||
.compatibilityMode = "NoQuirksMode",
|
||||
.isScrollable = false,
|
||||
.parentId = 1,
|
||||
}, .{
|
||||
.nodeId = 3,
|
||||
.backendNodeId = 3,
|
||||
.nodeType = 1,
|
||||
.nodeName = "BODY",
|
||||
.localName = "body",
|
||||
.nodeValue = "",
|
||||
.childNodeCount = 2,
|
||||
.documentURL = null,
|
||||
.baseURL = null,
|
||||
.xmlVersion = "",
|
||||
.compatibilityMode = "NoQuirksMode",
|
||||
.isScrollable = false,
|
||||
.children = &.{ .{
|
||||
.nodeId = 4,
|
||||
.localName = "a",
|
||||
.childNodeCount = 0,
|
||||
.parentId = 3,
|
||||
}, .{
|
||||
.nodeId = 5,
|
||||
.localName = "div",
|
||||
.childNodeCount = 1,
|
||||
.parentId = 3,
|
||||
.children = &.{.{
|
||||
.nodeId = 6,
|
||||
.localName = "a",
|
||||
.childNodeCount = 0,
|
||||
.parentId = 5,
|
||||
}},
|
||||
} },
|
||||
} }, json);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ const json = std.json;
|
||||
const log = @import("../log.zig");
|
||||
const App = @import("../app.zig").App;
|
||||
const Env = @import("../browser/env.zig").Env;
|
||||
const asUint = @import("../str/parser.zig").asUint;
|
||||
const Browser = @import("../browser/browser.zig").Browser;
|
||||
const Session = @import("../browser/session.zig").Session;
|
||||
const Page = @import("../browser/page.zig").Page;
|
||||
@@ -31,6 +30,8 @@ const Inspector = @import("../browser/env.zig").Env.Inspector;
|
||||
const Incrementing = @import("../id.zig").Incrementing;
|
||||
const Notification = @import("../notification.zig").Notification;
|
||||
|
||||
const polyfill = @import("../browser/polyfill/polyfill.zig");
|
||||
|
||||
pub const URL_BASE = "chrome://newtab/";
|
||||
pub const LOADER_ID = "LOADERID24DD2FD56CF1EF33C965C79C";
|
||||
|
||||
@@ -182,41 +183,42 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
||||
|
||||
switch (domain.len) {
|
||||
3 => switch (@as(u24, @bitCast(domain[0..3].*))) {
|
||||
asUint("DOM") => return @import("domains/dom.zig").processMessage(command),
|
||||
asUint("Log") => return @import("domains/log.zig").processMessage(command),
|
||||
asUint("CSS") => return @import("domains/css.zig").processMessage(command),
|
||||
asUint(u24, "DOM") => return @import("domains/dom.zig").processMessage(command),
|
||||
asUint(u24, "Log") => return @import("domains/log.zig").processMessage(command),
|
||||
asUint(u24, "CSS") => return @import("domains/css.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
4 => switch (@as(u32, @bitCast(domain[0..4].*))) {
|
||||
asUint("Page") => return @import("domains/page.zig").processMessage(command),
|
||||
asUint(u32, "Page") => return @import("domains/page.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
5 => switch (@as(u40, @bitCast(domain[0..5].*))) {
|
||||
asUint("Fetch") => return @import("domains/fetch.zig").processMessage(command),
|
||||
asUint("Input") => return @import("domains/input.zig").processMessage(command),
|
||||
asUint(u40, "Fetch") => return @import("domains/fetch.zig").processMessage(command),
|
||||
asUint(u40, "Input") => return @import("domains/input.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
6 => switch (@as(u48, @bitCast(domain[0..6].*))) {
|
||||
asUint("Target") => return @import("domains/target.zig").processMessage(command),
|
||||
asUint(u48, "Target") => return @import("domains/target.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
7 => switch (@as(u56, @bitCast(domain[0..7].*))) {
|
||||
asUint("Browser") => return @import("domains/browser.zig").processMessage(command),
|
||||
asUint("Runtime") => return @import("domains/runtime.zig").processMessage(command),
|
||||
asUint("Network") => return @import("domains/network.zig").processMessage(command),
|
||||
asUint(u56, "Browser") => return @import("domains/browser.zig").processMessage(command),
|
||||
asUint(u56, "Runtime") => return @import("domains/runtime.zig").processMessage(command),
|
||||
asUint(u56, "Network") => return @import("domains/network.zig").processMessage(command),
|
||||
asUint(u56, "Storage") => return @import("domains/storage.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
8 => switch (@as(u64, @bitCast(domain[0..8].*))) {
|
||||
asUint("Security") => return @import("domains/security.zig").processMessage(command),
|
||||
asUint(u64, "Security") => return @import("domains/security.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
9 => switch (@as(u72, @bitCast(domain[0..9].*))) {
|
||||
asUint("Emulation") => return @import("domains/emulation.zig").processMessage(command),
|
||||
asUint("Inspector") => return @import("domains/inspector.zig").processMessage(command),
|
||||
asUint(u72, "Emulation") => return @import("domains/emulation.zig").processMessage(command),
|
||||
asUint(u72, "Inspector") => return @import("domains/inspector.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
11 => switch (@as(u88, @bitCast(domain[0..11].*))) {
|
||||
asUint("Performance") => return @import("domains/performance.zig").processMessage(command),
|
||||
asUint(u88, "Performance") => return @import("domains/performance.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
else => {},
|
||||
@@ -321,6 +323,11 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
inspector: Inspector,
|
||||
isolated_world: ?IsolatedWorld,
|
||||
|
||||
// Used to restore the proxy after the CDP session ends. If CDP never over-wrote it, it won't restore it (the first null).
|
||||
// If the CDP is restoring it, but the original value was null, that's the 2nd null.
|
||||
// If you only have 1 null it would be ambiguous, does null mean it shouldn't be restored, or should it be restored to null?
|
||||
http_proxy_before: ??std.Uri = null,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
fn init(self: *Self, id: []const u8, cdp: *CDP_T) !void {
|
||||
@@ -374,6 +381,8 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
self.node_registry.deinit();
|
||||
self.node_search_list.deinit();
|
||||
self.cdp.browser.notification.unregisterAll(self);
|
||||
|
||||
if (self.http_proxy_before) |prev_proxy| self.cdp.browser.http_client.http_proxy = prev_proxy;
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
@@ -397,10 +406,11 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
return &self.isolated_world.?;
|
||||
}
|
||||
|
||||
pub fn nodeWriter(self: *Self, node: *const Node, opts: Node.Writer.Opts) Node.Writer {
|
||||
pub fn nodeWriter(self: *Self, root: *const Node, opts: Node.Writer.Opts) Node.Writer {
|
||||
return .{
|
||||
.node = node,
|
||||
.opts = opts,
|
||||
.root = root,
|
||||
.depth = opts.depth,
|
||||
.exclude_root = opts.exclude_root,
|
||||
.registry = &self.node_registry,
|
||||
};
|
||||
}
|
||||
@@ -551,6 +561,10 @@ const IsolatedWorld = struct {
|
||||
executor: Env.ExecutionWorld,
|
||||
grant_universal_access: bool,
|
||||
|
||||
// Polyfill loader for the isolated world.
|
||||
// We want to load polyfill in the world's context.
|
||||
polyfill_loader: polyfill.Loader = .{},
|
||||
|
||||
pub fn deinit(self: *IsolatedWorld) void {
|
||||
self.executor.deinit();
|
||||
}
|
||||
@@ -566,7 +580,13 @@ const IsolatedWorld = struct {
|
||||
// Currently we have only 1 page/frame and thus also only 1 state in the isolate world.
|
||||
pub fn createContext(self: *IsolatedWorld, page: *Page) !void {
|
||||
if (self.executor.js_context != null) return error.Only1IsolatedContextSupported;
|
||||
_ = try self.executor.createJsContext(&page.window, page, {}, false);
|
||||
_ = try self.executor.createJsContext(
|
||||
&page.window,
|
||||
page,
|
||||
{},
|
||||
false,
|
||||
Env.GlobalMissingCallback.init(&self.polyfill_loader),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -696,6 +716,10 @@ const InputParams = struct {
|
||||
}
|
||||
};
|
||||
|
||||
fn asUint(comptime T: type, comptime string: []const u8) T {
|
||||
return @bitCast(string[0..string.len].*);
|
||||
}
|
||||
|
||||
const testing = @import("testing.zig");
|
||||
test "cdp: invalid json" {
|
||||
var ctx = testing.context();
|
||||
|
||||
@@ -38,6 +38,7 @@ pub fn processMessage(cmd: anytype) !void {
|
||||
scrollIntoViewIfNeeded,
|
||||
getContentQuads,
|
||||
getBoxModel,
|
||||
requestChildNodes,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
@@ -53,22 +54,25 @@ pub fn processMessage(cmd: anytype) !void {
|
||||
.scrollIntoViewIfNeeded => return scrollIntoViewIfNeeded(cmd),
|
||||
.getContentQuads => return getContentQuads(cmd),
|
||||
.getBoxModel => return getBoxModel(cmd),
|
||||
.requestChildNodes => return requestChildNodes(cmd),
|
||||
}
|
||||
}
|
||||
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getDocument
|
||||
fn getDocument(cmd: anytype) !void {
|
||||
// const params = (try cmd.params(struct {
|
||||
// depth: ?u32 = null,
|
||||
// pierce: ?bool = null,
|
||||
// })) orelse return error.InvalidParams;
|
||||
const Params = struct {
|
||||
// CDP documentation implies that 0 isn't valid, but it _does_ work in Chrome
|
||||
depth: i32 = 3,
|
||||
pierce: bool = false,
|
||||
};
|
||||
const params = try cmd.params(Params) orelse Params{};
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
const doc = parser.documentHTMLToDocument(page.window.document);
|
||||
|
||||
const node = try bc.node_registry.register(parser.documentToNode(doc));
|
||||
return cmd.sendResult(.{ .root = bc.nodeWriter(node, .{}) }, .{});
|
||||
return cmd.sendResult(.{ .root = bc.nodeWriter(node, .{ .depth = params.depth }) }, .{});
|
||||
}
|
||||
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-performSearch
|
||||
@@ -433,6 +437,30 @@ fn getBoxModel(cmd: anytype) !void {
|
||||
} }, .{});
|
||||
}
|
||||
|
||||
fn requestChildNodes(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
nodeId: Node.Id,
|
||||
depth: i32 = 1,
|
||||
pierce: bool = false,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
if (params.depth == 0) return error.InvalidParams;
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const session_id = bc.session_id orelse return error.SessionIdNotLoaded;
|
||||
const node = bc.node_registry.lookup_by_id.get(params.nodeId) orelse {
|
||||
return error.InvalidNode;
|
||||
};
|
||||
|
||||
try cmd.sendEvent("DOM.setChildNodes", .{
|
||||
.parentId = node.id,
|
||||
.nodes = bc.nodeWriter(node, .{ .depth = params.depth, .exclude_root = true }),
|
||||
}, .{
|
||||
.session_id = session_id,
|
||||
});
|
||||
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
|
||||
test "cdp.dom: getSearchResults unknown search id" {
|
||||
|
||||
@@ -17,10 +17,11 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Notification = @import("../../notification.zig").Notification;
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const CdpStorage = @import("storage.zig");
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
@@ -28,13 +29,25 @@ pub fn processMessage(cmd: anytype) !void {
|
||||
disable,
|
||||
setCacheDisabled,
|
||||
setExtraHTTPHeaders,
|
||||
setUserAgentOverride,
|
||||
deleteCookies,
|
||||
clearBrowserCookies,
|
||||
setCookie,
|
||||
setCookies,
|
||||
getCookies,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.enable => return enable(cmd),
|
||||
.disable => return disable(cmd),
|
||||
.setCacheDisabled => return cmd.sendResult(null, .{}),
|
||||
.setUserAgentOverride => return cmd.sendResult(null, .{}),
|
||||
.setExtraHTTPHeaders => return setExtraHTTPHeaders(cmd),
|
||||
.deleteCookies => return deleteCookies(cmd),
|
||||
.clearBrowserCookies => return clearBrowserCookies(cmd),
|
||||
.setCookie => return setCookie(cmd),
|
||||
.setCookies => return setCookies(cmd),
|
||||
.getCookies => return getCookies(cmd),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +84,112 @@ fn setExtraHTTPHeaders(cmd: anytype) !void {
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
const Cookie = @import("../../browser/storage/storage.zig").Cookie;
|
||||
|
||||
// Only matches the cookie on provided parameters
|
||||
fn cookieMatches(cookie: *const Cookie, name: []const u8, domain: ?[]const u8, path: ?[]const u8) bool {
|
||||
if (!std.mem.eql(u8, cookie.name, name)) return false;
|
||||
|
||||
if (domain) |domain_| {
|
||||
const c_no_dot = if (std.mem.startsWith(u8, cookie.domain, ".")) cookie.domain[1..] else cookie.domain;
|
||||
const d_no_dot = if (std.mem.startsWith(u8, domain_, ".")) domain_[1..] else domain_;
|
||||
if (!std.mem.eql(u8, c_no_dot, d_no_dot)) return false;
|
||||
}
|
||||
if (path) |path_| {
|
||||
if (!std.mem.eql(u8, cookie.path, path_)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
fn deleteCookies(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
name: []const u8,
|
||||
url: ?[]const u8 = null,
|
||||
domain: ?[]const u8 = null,
|
||||
path: ?[]const u8 = null,
|
||||
partitionKey: ?CdpStorage.CookiePartitionKey = null,
|
||||
})) orelse return error.InvalidParams;
|
||||
if (params.partitionKey != null) return error.NotYetImplementedParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const cookies = &bc.session.cookie_jar.cookies;
|
||||
|
||||
const uri = if (params.url) |url| std.Uri.parse(url) catch return error.InvalidParams else null;
|
||||
const uri_ptr = if (uri) |u| &u else null;
|
||||
|
||||
var index = cookies.items.len;
|
||||
while (index > 0) {
|
||||
index -= 1;
|
||||
const cookie = &cookies.items[index];
|
||||
const domain = try Cookie.parseDomain(cmd.arena, uri_ptr, params.domain);
|
||||
const path = try Cookie.parsePath(cmd.arena, uri_ptr, params.path);
|
||||
|
||||
// We do not want to use Cookie.appliesTo here. As a Cookie with a shorter path would match.
|
||||
// Similar to deduplicating with areCookiesEqual, except domain and path are optional.
|
||||
if (cookieMatches(cookie, params.name, domain, path)) {
|
||||
cookies.swapRemove(index).deinit();
|
||||
}
|
||||
}
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
fn clearBrowserCookies(cmd: anytype) !void {
|
||||
if (try cmd.params(struct {}) != null) return error.InvalidParams;
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
bc.session.cookie_jar.clearRetainingCapacity();
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
fn setCookie(cmd: anytype) !void {
|
||||
const params = (try cmd.params(
|
||||
CdpStorage.CdpCookie,
|
||||
)) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
try CdpStorage.setCdpCookie(&bc.session.cookie_jar, params);
|
||||
|
||||
try cmd.sendResult(.{ .success = true }, .{});
|
||||
}
|
||||
|
||||
fn setCookies(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
cookies: []const CdpStorage.CdpCookie,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
for (params.cookies) |param| {
|
||||
try CdpStorage.setCdpCookie(&bc.session.cookie_jar, param);
|
||||
}
|
||||
|
||||
try cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
const GetCookiesParam = struct { urls: ?[]const []const u8 = null };
|
||||
fn getCookies(cmd: anytype) !void {
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const params = (try cmd.params(GetCookiesParam)) orelse GetCookiesParam{};
|
||||
|
||||
// If not specified, use the URLs of the page and all of its subframes. TODO subframes
|
||||
const page_url = if (bc.session.page) |*page| page.url.raw else null; // @speed: avoid repasing the URL
|
||||
const param_urls = params.urls orelse &[_][]const u8{page_url orelse return error.InvalidParams};
|
||||
|
||||
var urls = try std.ArrayListUnmanaged(CdpStorage.PreparedUri).initCapacity(cmd.arena, param_urls.len);
|
||||
for (param_urls) |url| {
|
||||
const uri = std.Uri.parse(url) catch return error.InvalidParams;
|
||||
|
||||
urls.appendAssumeCapacity(.{
|
||||
.host = try Cookie.parseDomain(cmd.arena, &uri, null),
|
||||
.path = try Cookie.parsePath(cmd.arena, &uri, null),
|
||||
.secure = std.mem.eql(u8, uri.scheme, "https"),
|
||||
});
|
||||
}
|
||||
|
||||
var jar = &bc.session.cookie_jar;
|
||||
jar.removeExpired(null);
|
||||
const writer = CdpStorage.CookieWriter{ .cookies = jar.cookies.items, .urls = urls.items };
|
||||
try cmd.sendResult(.{ .cookies = writer }, .{});
|
||||
}
|
||||
|
||||
// Upsert a header into the headers array.
|
||||
// returns true if the header was added, false if it was updated
|
||||
fn putAssumeCapacity(headers: *std.ArrayListUnmanaged(std.http.Header), extra: std.http.Header) bool {
|
||||
@@ -235,3 +354,77 @@ test "cdp.network setExtraHTTPHeaders" {
|
||||
try ctx.processMessage(.{ .id = 5, .method = "Target.attachToTarget", .params = .{ .targetId = bc.target_id.? } });
|
||||
try testing.expectEqual(bc.cdp.extra_headers.items.len, 0);
|
||||
}
|
||||
|
||||
test "cdp.Network: cookies" {
|
||||
const ResCookie = CdpStorage.ResCookie;
|
||||
const CdpCookie = CdpStorage.CdpCookie;
|
||||
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
_ = try ctx.loadBrowserContext(.{ .id = "BID-S" });
|
||||
|
||||
// Initially empty
|
||||
try ctx.processMessage(.{
|
||||
.id = 3,
|
||||
.method = "Network.getCookies",
|
||||
.params = .{ .urls = &[_][]const u8{"https://example.com/pancakes"} },
|
||||
});
|
||||
try ctx.expectSentResult(.{ .cookies = &[_]ResCookie{} }, .{ .id = 3 });
|
||||
|
||||
// Has cookies after setting them
|
||||
try ctx.processMessage(.{
|
||||
.id = 4,
|
||||
.method = "Network.setCookie",
|
||||
.params = CdpCookie{ .name = "test3", .value = "valuenot3", .url = "https://car.example.com/defnotpancakes" },
|
||||
});
|
||||
try ctx.expectSentResult(null, .{ .id = 4 });
|
||||
try ctx.processMessage(.{
|
||||
.id = 5,
|
||||
.method = "Network.setCookies",
|
||||
.params = .{
|
||||
.cookies = &[_]CdpCookie{
|
||||
.{ .name = "test3", .value = "value3", .url = "https://car.example.com/pan/cakes" },
|
||||
.{ .name = "test4", .value = "value4", .domain = "example.com", .path = "/mango" },
|
||||
},
|
||||
},
|
||||
});
|
||||
try ctx.expectSentResult(null, .{ .id = 5 });
|
||||
try ctx.processMessage(.{
|
||||
.id = 6,
|
||||
.method = "Network.getCookies",
|
||||
.params = .{ .urls = &[_][]const u8{"https://car.example.com/pan/cakes"} },
|
||||
});
|
||||
try ctx.expectSentResult(.{
|
||||
.cookies = &[_]ResCookie{
|
||||
.{ .name = "test3", .value = "value3", .domain = "car.example.com", .path = "/", .secure = true }, // No Pancakes!
|
||||
},
|
||||
}, .{ .id = 6 });
|
||||
|
||||
// deleteCookies
|
||||
try ctx.processMessage(.{
|
||||
.id = 7,
|
||||
.method = "Network.deleteCookies",
|
||||
.params = .{ .name = "test3", .domain = "car.example.com" },
|
||||
});
|
||||
try ctx.expectSentResult(null, .{ .id = 7 });
|
||||
try ctx.processMessage(.{
|
||||
.id = 8,
|
||||
.method = "Storage.getCookies",
|
||||
.params = .{ .browserContextId = "BID-S" },
|
||||
});
|
||||
// Just the untouched test4 should be in the result
|
||||
try ctx.expectSentResult(.{ .cookies = &[_]ResCookie{.{ .name = "test4", .value = "value4", .domain = ".example.com", .path = "/mango" }} }, .{ .id = 8 });
|
||||
|
||||
// Empty after clearBrowserCookies
|
||||
try ctx.processMessage(.{
|
||||
.id = 9,
|
||||
.method = "Network.clearBrowserCookies",
|
||||
});
|
||||
try ctx.expectSentResult(null, .{ .id = 9 });
|
||||
try ctx.processMessage(.{
|
||||
.id = 10,
|
||||
.method = "Storage.getCookies",
|
||||
.params = .{ .browserContextId = "BID-S" },
|
||||
});
|
||||
try ctx.expectSentResult(.{ .cookies = &[_]ResCookie{} }, .{ .id = 10 });
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ pub fn processMessage(cmd: anytype) !void {
|
||||
addScriptToEvaluateOnNewDocument,
|
||||
createIsolatedWorld,
|
||||
navigate,
|
||||
stopLoading,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
@@ -40,6 +41,7 @@ pub fn processMessage(cmd: anytype) !void {
|
||||
.addScriptToEvaluateOnNewDocument => return addScriptToEvaluateOnNewDocument(cmd),
|
||||
.createIsolatedWorld => return createIsolatedWorld(cmd),
|
||||
.navigate => return navigate(cmd),
|
||||
.stopLoading => return cmd.sendResult(null, .{}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,7 +288,7 @@ pub fn pageCreated(bc: anytype, page: *Page) !void {
|
||||
try isolated_world.createContext(page);
|
||||
|
||||
const polyfill = @import("../../browser/polyfill/polyfill.zig");
|
||||
try polyfill.load(bc.arena, &isolated_world.executor.js_context.?);
|
||||
try polyfill.preload(bc.arena, &isolated_world.executor.js_context.?);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
301
src/cdp/domains/storage.zig
Normal file
301
src/cdp/domains/storage.zig
Normal file
@@ -0,0 +1,301 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const Cookie = @import("../../browser/storage/storage.zig").Cookie;
|
||||
const CookieJar = @import("../../browser/storage/storage.zig").CookieJar;
|
||||
pub const PreparedUri = @import("../../browser/storage/cookie.zig").PreparedUri;
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
clearCookies,
|
||||
setCookies,
|
||||
getCookies,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.clearCookies => return clearCookies(cmd),
|
||||
.getCookies => return getCookies(cmd),
|
||||
.setCookies => return setCookies(cmd),
|
||||
}
|
||||
}
|
||||
|
||||
const BrowserContextParam = struct { browserContextId: ?[]const u8 = null };
|
||||
|
||||
fn clearCookies(cmd: anytype) !void {
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const params = (try cmd.params(BrowserContextParam)) orelse BrowserContextParam{};
|
||||
|
||||
if (params.browserContextId) |browser_context_id| {
|
||||
if (std.mem.eql(u8, browser_context_id, bc.id) == false) {
|
||||
return error.UnknownBrowserContextId;
|
||||
}
|
||||
}
|
||||
|
||||
bc.session.cookie_jar.clearRetainingCapacity();
|
||||
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
fn getCookies(cmd: anytype) !void {
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const params = (try cmd.params(BrowserContextParam)) orelse BrowserContextParam{};
|
||||
|
||||
if (params.browserContextId) |browser_context_id| {
|
||||
if (std.mem.eql(u8, browser_context_id, bc.id) == false) {
|
||||
return error.UnknownBrowserContextId;
|
||||
}
|
||||
}
|
||||
bc.session.cookie_jar.removeExpired(null);
|
||||
const writer = CookieWriter{ .cookies = bc.session.cookie_jar.cookies.items };
|
||||
try cmd.sendResult(.{ .cookies = writer }, .{});
|
||||
}
|
||||
|
||||
fn setCookies(cmd: anytype) !void {
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const params = (try cmd.params(struct {
|
||||
cookies: []const CdpCookie,
|
||||
browserContextId: ?[]const u8 = null,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
if (params.browserContextId) |browser_context_id| {
|
||||
if (std.mem.eql(u8, browser_context_id, bc.id) == false) {
|
||||
return error.UnknownBrowserContextId;
|
||||
}
|
||||
}
|
||||
|
||||
for (params.cookies) |param| {
|
||||
try setCdpCookie(&bc.session.cookie_jar, param);
|
||||
}
|
||||
|
||||
try cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
pub const SameSite = enum {
|
||||
Strict,
|
||||
Lax,
|
||||
None,
|
||||
};
|
||||
pub const CookiePriority = enum {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
};
|
||||
pub const CookieSourceScheme = enum {
|
||||
Unset,
|
||||
NonSecure,
|
||||
Secure,
|
||||
};
|
||||
|
||||
pub const CookiePartitionKey = struct {
|
||||
topLevelSite: []const u8,
|
||||
hasCrossSiteAncestor: bool,
|
||||
};
|
||||
|
||||
pub const CdpCookie = struct {
|
||||
name: []const u8,
|
||||
value: []const u8,
|
||||
url: ?[]const u8 = null,
|
||||
domain: ?[]const u8 = null,
|
||||
path: ?[]const u8 = null,
|
||||
secure: ?bool = null, // default: https://www.rfc-editor.org/rfc/rfc6265#section-5.3
|
||||
httpOnly: bool = false, // default: https://www.rfc-editor.org/rfc/rfc6265#section-5.3
|
||||
sameSite: SameSite = .None, // default: https://datatracker.ietf.org/doc/html/draft-west-first-party-cookies
|
||||
expires: ?f64 = null, // -1? says google
|
||||
priority: CookiePriority = .Medium, // default: https://datatracker.ietf.org/doc/html/draft-west-cookie-priority-00
|
||||
sameParty: ?bool = null,
|
||||
sourceScheme: ?CookieSourceScheme = null,
|
||||
// sourcePort: Temporary ability and it will be removed from CDP
|
||||
partitionKey: ?CookiePartitionKey = null,
|
||||
};
|
||||
|
||||
pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void {
|
||||
if (param.priority != .Medium or param.sameParty != null or param.sourceScheme != null or param.partitionKey != null) {
|
||||
return error.NotYetImplementedParams;
|
||||
}
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(cookie_jar.allocator);
|
||||
errdefer arena.deinit();
|
||||
const a = arena.allocator();
|
||||
|
||||
// NOTE: The param.url can affect the default domain, (NOT path), secure, source port, and source scheme.
|
||||
const uri = if (param.url) |url| std.Uri.parse(url) catch return error.InvalidParams else null;
|
||||
const uri_ptr = if (uri) |*u| u else null;
|
||||
const domain = try Cookie.parseDomain(a, uri_ptr, param.domain);
|
||||
const path = if (param.path == null) "/" else try Cookie.parsePath(a, null, param.path);
|
||||
|
||||
const secure = if (param.secure) |s| s else if (uri) |uri_| std.mem.eql(u8, uri_.scheme, "https") else false;
|
||||
|
||||
const cookie = Cookie{
|
||||
.arena = arena,
|
||||
.name = try a.dupe(u8, param.name),
|
||||
.value = try a.dupe(u8, param.value),
|
||||
.path = path,
|
||||
.domain = domain,
|
||||
.expires = param.expires,
|
||||
.secure = secure,
|
||||
.http_only = param.httpOnly,
|
||||
.same_site = switch (param.sameSite) {
|
||||
.Strict => .strict,
|
||||
.Lax => .lax,
|
||||
.None => .none,
|
||||
},
|
||||
};
|
||||
try cookie_jar.add(cookie, std.time.timestamp());
|
||||
}
|
||||
|
||||
pub const CookieWriter = struct {
|
||||
cookies: []const Cookie,
|
||||
urls: ?[]const PreparedUri = null,
|
||||
|
||||
pub fn jsonStringify(self: *const CookieWriter, w: anytype) !void {
|
||||
self.writeCookies(w) catch |err| {
|
||||
// The only error our jsonStringify method can return is @TypeOf(w).Error.
|
||||
log.err(.cdp, "json stringify", .{ .err = err });
|
||||
return error.OutOfMemory;
|
||||
};
|
||||
}
|
||||
|
||||
fn writeCookies(self: CookieWriter, w: anytype) !void {
|
||||
try w.beginArray();
|
||||
if (self.urls) |urls| {
|
||||
for (self.cookies) |*cookie| {
|
||||
for (urls) |*url| {
|
||||
if (cookie.appliesTo(url, true, true, true)) { // TBD same_site, should we compare to the pages url?
|
||||
try writeCookie(cookie, w);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (self.cookies) |*cookie| {
|
||||
try writeCookie(cookie, w);
|
||||
}
|
||||
}
|
||||
try w.endArray();
|
||||
}
|
||||
};
|
||||
pub fn writeCookie(cookie: *const Cookie, w: anytype) !void {
|
||||
try w.beginObject();
|
||||
{
|
||||
try w.objectField("name");
|
||||
try w.write(cookie.name);
|
||||
|
||||
try w.objectField("value");
|
||||
try w.write(cookie.value);
|
||||
|
||||
try w.objectField("domain");
|
||||
try w.write(cookie.domain); // Should we hide a leading dot?
|
||||
|
||||
try w.objectField("path");
|
||||
try w.write(cookie.path);
|
||||
|
||||
try w.objectField("expires");
|
||||
try w.write(cookie.expires orelse -1);
|
||||
|
||||
// TODO size
|
||||
|
||||
try w.objectField("httpOnly");
|
||||
try w.write(cookie.http_only);
|
||||
|
||||
try w.objectField("secure");
|
||||
try w.write(cookie.secure);
|
||||
|
||||
try w.objectField("session");
|
||||
try w.write(cookie.expires == null);
|
||||
|
||||
try w.objectField("sameSite");
|
||||
switch (cookie.same_site) {
|
||||
.none => try w.write("None"),
|
||||
.lax => try w.write("Lax"),
|
||||
.strict => try w.write("Strict"),
|
||||
}
|
||||
|
||||
// TODO experimentals
|
||||
}
|
||||
try w.endObject();
|
||||
}
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
|
||||
test "cdp.Storage: cookies" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
_ = try ctx.loadBrowserContext(.{ .id = "BID-S" });
|
||||
|
||||
// Initially empty
|
||||
try ctx.processMessage(.{
|
||||
.id = 3,
|
||||
.method = "Storage.getCookies",
|
||||
.params = .{ .browserContextId = "BID-S" },
|
||||
});
|
||||
try ctx.expectSentResult(.{ .cookies = &[_]ResCookie{} }, .{ .id = 3 });
|
||||
|
||||
// Has cookies after setting them
|
||||
try ctx.processMessage(.{
|
||||
.id = 4,
|
||||
.method = "Storage.setCookies",
|
||||
.params = .{
|
||||
.cookies = &[_]CdpCookie{
|
||||
.{ .name = "test", .value = "value", .domain = "example.com", .path = "/mango" },
|
||||
.{ .name = "test2", .value = "value2", .url = "https://car.example.com/pancakes" },
|
||||
},
|
||||
.browserContextId = "BID-S",
|
||||
},
|
||||
});
|
||||
try ctx.expectSentResult(null, .{ .id = 4 });
|
||||
try ctx.processMessage(.{
|
||||
.id = 5,
|
||||
.method = "Storage.getCookies",
|
||||
.params = .{ .browserContextId = "BID-S" },
|
||||
});
|
||||
try ctx.expectSentResult(.{
|
||||
.cookies = &[_]ResCookie{
|
||||
.{ .name = "test", .value = "value", .domain = ".example.com", .path = "/mango" },
|
||||
.{ .name = "test2", .value = "value2", .domain = "car.example.com", .path = "/", .secure = true }, // No Pancakes!
|
||||
},
|
||||
}, .{ .id = 5 });
|
||||
|
||||
// Empty after clearing cookies
|
||||
try ctx.processMessage(.{
|
||||
.id = 6,
|
||||
.method = "Storage.clearCookies",
|
||||
.params = .{ .browserContextId = "BID-S" },
|
||||
});
|
||||
try ctx.expectSentResult(null, .{ .id = 6 });
|
||||
try ctx.processMessage(.{
|
||||
.id = 7,
|
||||
.method = "Storage.getCookies",
|
||||
.params = .{ .browserContextId = "BID-S" },
|
||||
});
|
||||
try ctx.expectSentResult(.{ .cookies = &[_]ResCookie{} }, .{ .id = 7 });
|
||||
}
|
||||
|
||||
pub const ResCookie = struct {
|
||||
name: []const u8,
|
||||
value: []const u8,
|
||||
domain: []const u8,
|
||||
path: []const u8 = "/",
|
||||
expires: f64 = -1,
|
||||
httpOnly: bool = false,
|
||||
secure: bool = false,
|
||||
sameSite: []const u8 = "None",
|
||||
};
|
||||
@@ -66,11 +66,30 @@ fn getBrowserContexts(cmd: anytype) !void {
|
||||
}
|
||||
|
||||
fn createBrowserContext(cmd: anytype) !void {
|
||||
const params = try cmd.params(struct {
|
||||
disposeOnDetach: bool = false,
|
||||
proxyServer: ?[]const u8 = null,
|
||||
proxyBypassList: ?[]const u8 = null,
|
||||
originsWithUniversalNetworkAccess: ?[]const []const u8 = null,
|
||||
});
|
||||
if (params) |p| {
|
||||
if (p.disposeOnDetach or p.proxyBypassList != null or p.originsWithUniversalNetworkAccess != null) std.debug.print("Target.createBrowserContext: Not implemented param set\n", .{});
|
||||
}
|
||||
|
||||
const bc = cmd.createBrowserContext() catch |err| switch (err) {
|
||||
error.AlreadyExists => return cmd.sendError(-32000, "Cannot have more than one browser context at a time"),
|
||||
else => return err,
|
||||
};
|
||||
|
||||
if (params) |p| {
|
||||
if (p.proxyServer) |proxy| {
|
||||
// For now the http client is not in the browser context so we assume there is just 1.
|
||||
bc.http_proxy_before = cmd.cdp.browser.http_client.http_proxy;
|
||||
const proxy_cp = try cmd.cdp.browser.http_client.allocator.dupe(u8, proxy);
|
||||
cmd.cdp.browser.http_client.http_proxy = try std.Uri.parse(proxy_cp);
|
||||
}
|
||||
}
|
||||
|
||||
return cmd.sendResult(.{
|
||||
.browserContextId = bc.id,
|
||||
}, .{});
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
pub fn lookup(value: []const u8) bool {
|
||||
return public_suffix_list.has(value);
|
||||
}
|
||||
|
||||
const public_suffix_list = std.StaticStringMap(void).initComptime([_]struct { []const u8, void }{
|
||||
const public_suffix_list = std.StaticStringMap(void).initComptime(entries);
|
||||
|
||||
const entries: []const struct { []const u8, void } =
|
||||
if (builtin.is_test) &.{
|
||||
.{ "api.gov.uk", {} },
|
||||
.{ "gov.uk", {} },
|
||||
} else &.{
|
||||
.{ "ac", {} },
|
||||
.{ "com.ac", {} },
|
||||
.{ "edu.ac", {} },
|
||||
@@ -9739,4 +9746,4 @@ const public_suffix_list = std.StaticStringMap(void).initComptime([_]struct { []
|
||||
.{ "basicserver.io", {} },
|
||||
.{ "virtualserver.io", {} },
|
||||
.{ "enterprisecloud.nu", {} },
|
||||
});
|
||||
};
|
||||
|
||||
1068
src/http/client.zig
1068
src/http/client.zig
File diff suppressed because it is too large
Load Diff
30
src/log.zig
30
src/log.zig
@@ -39,12 +39,13 @@ pub const Scope = enum {
|
||||
unknown_prop,
|
||||
web_api,
|
||||
xhr,
|
||||
polyfill,
|
||||
};
|
||||
|
||||
const Opts = struct {
|
||||
format: Format = if (is_debug) .pretty else .logfmt,
|
||||
level: Level = if (is_debug) .info else .warn,
|
||||
filter_scopes: []const Scope = &.{.unknown_prop},
|
||||
filter_scopes: []const Scope = &.{},
|
||||
};
|
||||
|
||||
pub var opts = Opts{};
|
||||
@@ -146,6 +147,16 @@ fn logTo(comptime scope: Scope, level: Level, comptime msg: []const u8, data: an
|
||||
}
|
||||
|
||||
fn logLogfmt(comptime scope: Scope, level: Level, comptime msg: []const u8, data: anytype, writer: anytype) !void {
|
||||
try logLogFmtPrefix(scope, level, msg, writer);
|
||||
inline for (@typeInfo(@TypeOf(data)).@"struct".fields) |f| {
|
||||
const key = " " ++ f.name ++ "=";
|
||||
try writer.writeAll(key);
|
||||
try writeValue(.logfmt, @field(data, f.name), writer);
|
||||
}
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
|
||||
fn logLogFmtPrefix(comptime scope: Scope, level: Level, comptime msg: []const u8, writer: anytype) !void {
|
||||
try writer.writeAll("$time=");
|
||||
try writer.print("{d}", .{timestamp()});
|
||||
|
||||
@@ -164,15 +175,20 @@ fn logLogfmt(comptime scope: Scope, level: Level, comptime msg: []const u8, data
|
||||
break :blk prefix ++ "\"" ++ msg ++ "\"";
|
||||
};
|
||||
try writer.writeAll(full_msg);
|
||||
}
|
||||
|
||||
fn logPretty(comptime scope: Scope, level: Level, comptime msg: []const u8, data: anytype, writer: anytype) !void {
|
||||
try logPrettyPrefix(scope, level, msg, writer);
|
||||
inline for (@typeInfo(@TypeOf(data)).@"struct".fields) |f| {
|
||||
const key = " " ++ f.name ++ " = ";
|
||||
try writer.writeAll(key);
|
||||
try writeValue(.logfmt, @field(data, f.name), writer);
|
||||
try writeValue(.pretty, @field(data, f.name), writer);
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
|
||||
fn logPretty(comptime scope: Scope, level: Level, comptime msg: []const u8, data: anytype, writer: anytype) !void {
|
||||
fn logPrettyPrefix(comptime scope: Scope, level: Level, comptime msg: []const u8, writer: anytype) !void {
|
||||
if (scope == .console and level == .fatal and comptime std.mem.eql(u8, msg, "lightpanda")) {
|
||||
try writer.writeAll("\x1b[0;104mWARN ");
|
||||
} else {
|
||||
@@ -201,14 +217,6 @@ fn logPretty(comptime scope: Scope, level: Level, comptime msg: []const u8, data
|
||||
try writer.print(" \x1b[0m[+{d}ms]", .{elapsed()});
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
|
||||
inline for (@typeInfo(@TypeOf(data)).@"struct".fields) |f| {
|
||||
const key = " " ++ f.name ++ " = ";
|
||||
try writer.writeAll(key);
|
||||
try writeValue(.pretty, @field(data, f.name), writer);
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
|
||||
pub fn writeValue(comptime format: Format, value: anytype, writer: anytype) !void {
|
||||
|
||||
126
src/main.zig
126
src/main.zig
@@ -23,6 +23,7 @@ const Allocator = std.mem.Allocator;
|
||||
const log = @import("log.zig");
|
||||
const server = @import("server.zig");
|
||||
const App = @import("app.zig").App;
|
||||
const http = @import("http/client.zig");
|
||||
const Platform = @import("runtime/js.zig").Platform;
|
||||
const Browser = @import("browser/browser.zig").Browser;
|
||||
|
||||
@@ -82,7 +83,10 @@ fn run(alloc: Allocator) !void {
|
||||
|
||||
var app = try App.init(alloc, .{
|
||||
.run_mode = args.mode,
|
||||
.platform = &platform,
|
||||
.http_proxy = args.httpProxy(),
|
||||
.proxy_type = args.proxyType(),
|
||||
.proxy_auth = args.proxyAuth(),
|
||||
.tls_verify_host = args.tlsVerifyHost(),
|
||||
});
|
||||
defer app.deinit();
|
||||
@@ -126,11 +130,11 @@ fn run(alloc: Allocator) !void {
|
||||
},
|
||||
};
|
||||
|
||||
try page.wait();
|
||||
try page.wait(std.time.ns_per_s * 3);
|
||||
|
||||
// dump
|
||||
if (opts.dump) {
|
||||
try page.dump(std.io.getStdOut());
|
||||
try page.dump(.{ .exclude_scripts = opts.noscript }, std.io.getStdOut());
|
||||
}
|
||||
},
|
||||
else => unreachable,
|
||||
@@ -155,6 +159,20 @@ const Command = struct {
|
||||
};
|
||||
}
|
||||
|
||||
fn proxyType(self: *const Command) ?http.ProxyType {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.proxy_type,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
fn proxyAuth(self: *const Command) ?http.ProxyAuth {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.proxy_auth,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
fn logLevel(self: *const Command) ?log.Level {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch => |opts| opts.common.log_level,
|
||||
@@ -194,10 +212,13 @@ const Command = struct {
|
||||
url: []const u8,
|
||||
dump: bool = false,
|
||||
common: Common,
|
||||
noscript: bool = false,
|
||||
};
|
||||
|
||||
const Common = struct {
|
||||
http_proxy: ?std.Uri = null,
|
||||
proxy_type: ?http.ProxyType = null,
|
||||
proxy_auth: ?http.ProxyAuth = null,
|
||||
tls_verify_host: bool = true,
|
||||
log_level: ?log.Level = null,
|
||||
log_format: ?log.Format = null,
|
||||
@@ -216,6 +237,21 @@ const Command = struct {
|
||||
\\--http_proxy The HTTP proxy to use for all HTTP requests.
|
||||
\\ Defaults to none.
|
||||
\\
|
||||
\\--proxy_type The type of proxy: connect, forward.
|
||||
\\ 'connect' creates a tunnel through the proxy via
|
||||
\\ and initial CONNECT request.
|
||||
\\ 'forward' sends the full URL in the request target
|
||||
\\ and expects the proxy to MITM the request.
|
||||
\\ Defaults to connect when --http_proxy is set.
|
||||
\\
|
||||
\\--proxy_bearer_token
|
||||
\\ The token to send for bearer authentication with the proxy
|
||||
\\ Proxy-Authorization: Bearer <token>
|
||||
\\
|
||||
\\--proxy_basic_auth
|
||||
\\ The user:password to send for basic authentication with the proxy
|
||||
\\ Proxy-Authorization: Basic <base64(user:password)>
|
||||
\\
|
||||
\\--log_level The log level: debug, info, warn, error or fatal.
|
||||
\\ Defaults to
|
||||
++ (if (builtin.mode == .Debug) " info." else "warn.") ++
|
||||
@@ -240,6 +276,7 @@ const Command = struct {
|
||||
\\Options:
|
||||
\\--dump Dumps document to stdout.
|
||||
\\ Defaults to false.
|
||||
\\--noscript Exclude <script> tags in dump. Defaults to false.
|
||||
\\
|
||||
++ common_options ++
|
||||
\\
|
||||
@@ -317,6 +354,9 @@ fn inferMode(opt: []const u8) ?App.RunMode {
|
||||
if (std.mem.eql(u8, opt, "--dump")) {
|
||||
return .fetch;
|
||||
}
|
||||
if (std.mem.eql(u8, opt, "--noscript")) {
|
||||
return .fetch;
|
||||
}
|
||||
if (std.mem.startsWith(u8, opt, "--") == false) {
|
||||
return .fetch;
|
||||
}
|
||||
@@ -402,6 +442,7 @@ fn parseFetchArgs(
|
||||
args: *std.process.ArgIterator,
|
||||
) !Command.Fetch {
|
||||
var dump: bool = false;
|
||||
var noscript: bool = true;
|
||||
var url: ?[]const u8 = null;
|
||||
var common: Command.Common = .{};
|
||||
|
||||
@@ -411,6 +452,11 @@ fn parseFetchArgs(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--noscript", opt)) {
|
||||
noscript = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (try parseCommonArg(allocator, opt, args, &common)) {
|
||||
continue;
|
||||
}
|
||||
@@ -436,6 +482,7 @@ fn parseFetchArgs(
|
||||
.url = url.?,
|
||||
.dump = dump,
|
||||
.common = common,
|
||||
.noscript = noscript,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -456,6 +503,47 @@ fn parseCommonArg(
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
common.http_proxy = try std.Uri.parse(try allocator.dupe(u8, str));
|
||||
if (common.http_proxy.?.host == null) {
|
||||
log.fatal(.app, "invalid http proxy", .{ .arg = "--http_proxy", .hint = "missing scheme?" });
|
||||
return error.InvalidArgument;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--proxy_type", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--proxy_type" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
common.proxy_type = std.meta.stringToEnum(http.ProxyType, str) orelse {
|
||||
log.fatal(.app, "invalid option choice", .{ .arg = "--proxy_type", .value = str });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--proxy_bearer_token", opt)) {
|
||||
if (common.proxy_auth != null) {
|
||||
log.fatal(.app, "proxy auth already set", .{ .arg = "--proxy_bearer_token" });
|
||||
return error.InvalidArgument;
|
||||
}
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--proxy_bearer_token" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
common.proxy_auth = .{ .bearer = .{ .token = str } };
|
||||
return true;
|
||||
}
|
||||
if (std.mem.eql(u8, "--proxy_basic_auth", opt)) {
|
||||
if (common.proxy_auth != null) {
|
||||
log.fatal(.app, "proxy auth already set", .{ .arg = "--proxy_basic_auth" });
|
||||
return error.InvalidArgument;
|
||||
}
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--proxy_basic_auth" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
common.proxy_auth = .{ .basic = .{ .user_pass = str } };
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -505,7 +593,7 @@ fn parseCommonArg(
|
||||
var it = std.mem.splitScalar(u8, str, ',');
|
||||
while (it.next()) |part| {
|
||||
try arr.append(allocator, std.meta.stringToEnum(log.Scope, part) orelse {
|
||||
log.fatal(.app, "invalid option choice", .{ .arg = "--log_scope_filter", .value = part });
|
||||
log.fatal(.app, "invalid option choice", .{ .arg = "--log_filter_scopes", .value = part });
|
||||
return false;
|
||||
});
|
||||
}
|
||||
@@ -527,7 +615,7 @@ test "tests:beforeAll" {
|
||||
log.opts.format = .logfmt;
|
||||
|
||||
test_wg.startMany(3);
|
||||
_ = try Platform.init();
|
||||
const platform = try Platform.init();
|
||||
|
||||
{
|
||||
const address = try std.net.Address.parseIp("127.0.0.1", 9582);
|
||||
@@ -543,7 +631,7 @@ test "tests:beforeAll" {
|
||||
|
||||
{
|
||||
const address = try std.net.Address.parseIp("127.0.0.1", 9583);
|
||||
const thread = try std.Thread.spawn(.{}, serveCDP, .{address});
|
||||
const thread = try std.Thread.spawn(.{}, serveCDP, .{ address, &platform });
|
||||
thread.detach();
|
||||
}
|
||||
|
||||
@@ -573,7 +661,8 @@ fn serveHTTP(address: std.net.Address) !void {
|
||||
var conn = try listener.accept();
|
||||
defer conn.stream.close();
|
||||
var http_server = std.http.Server.init(conn, &read_buffer);
|
||||
|
||||
var connect_headers: std.ArrayListUnmanaged(std.http.Header) = .{};
|
||||
REQUEST: while (true) {
|
||||
var request = http_server.receiveHead() catch |err| switch (err) {
|
||||
error.HttpConnectionClosing => continue :ACCEPT,
|
||||
else => {
|
||||
@@ -582,6 +671,21 @@ fn serveHTTP(address: std.net.Address) !void {
|
||||
},
|
||||
};
|
||||
|
||||
if (request.head.method == .CONNECT) {
|
||||
try request.respond("", .{ .status = .ok });
|
||||
|
||||
// Proxy headers and destination headers are separated in the case of a CONNECT proxy
|
||||
// We store the CONNECT headers, then continue with the request for the destination
|
||||
var it = request.iterateHeaders();
|
||||
while (it.next()) |hdr| {
|
||||
try connect_headers.append(aa, .{
|
||||
.name = try std.fmt.allocPrint(aa, "__{s}", .{hdr.name}),
|
||||
.value = try aa.dupe(u8, hdr.value),
|
||||
});
|
||||
}
|
||||
continue :REQUEST;
|
||||
}
|
||||
|
||||
const path = request.head.target;
|
||||
if (std.mem.eql(u8, path, "/loader")) {
|
||||
try request.respond("Hello!", .{
|
||||
@@ -619,6 +723,11 @@ fn serveHTTP(address: std.net.Address) !void {
|
||||
.value = hdr.value,
|
||||
});
|
||||
}
|
||||
|
||||
if (connect_headers.items.len > 0) {
|
||||
try headers.appendSlice(aa, connect_headers.items);
|
||||
connect_headers.clearRetainingCapacity();
|
||||
}
|
||||
try headers.append(aa, .{ .name = "Connection", .value = "Close" });
|
||||
|
||||
try request.respond("over 9000!", .{
|
||||
@@ -626,6 +735,8 @@ fn serveHTTP(address: std.net.Address) !void {
|
||||
.extra_headers = headers.items,
|
||||
});
|
||||
}
|
||||
continue :ACCEPT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -702,11 +813,12 @@ fn serveHTTPS(address: std.net.Address) !void {
|
||||
}
|
||||
}
|
||||
|
||||
fn serveCDP(address: std.net.Address) !void {
|
||||
fn serveCDP(address: std.net.Address, platform: *const Platform) !void {
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
var app = try App.init(gpa.allocator(), .{
|
||||
.run_mode = .serve,
|
||||
.tls_verify_host = false,
|
||||
.platform = platform,
|
||||
});
|
||||
defer app.deinit();
|
||||
|
||||
|
||||
@@ -70,7 +70,13 @@ pub fn main() !void {
|
||||
defer _ = test_arena.reset(.{ .retain_capacity = {} });
|
||||
|
||||
var err_out: ?[]const u8 = null;
|
||||
const result = run(test_arena.allocator(), test_file, &loader, &err_out) catch |err| blk: {
|
||||
const result = run(
|
||||
test_arena.allocator(),
|
||||
&platform,
|
||||
test_file,
|
||||
&loader,
|
||||
&err_out,
|
||||
) catch |err| blk: {
|
||||
if (err_out == null) {
|
||||
err_out = @errorName(err);
|
||||
}
|
||||
@@ -89,7 +95,13 @@ pub fn main() !void {
|
||||
try writer.finalize();
|
||||
}
|
||||
|
||||
fn run(arena: Allocator, test_file: []const u8, loader: *FileLoader, err_out: *?[]const u8) !?[]const u8 {
|
||||
fn run(
|
||||
arena: Allocator,
|
||||
platform: *const Platform,
|
||||
test_file: []const u8,
|
||||
loader: *FileLoader,
|
||||
err_out: *?[]const u8,
|
||||
) !?[]const u8 {
|
||||
// document
|
||||
const html = blk: {
|
||||
const full_path = try std.fs.path.join(arena, &.{ WPT_DIR, test_file });
|
||||
@@ -110,10 +122,11 @@ fn run(arena: Allocator, test_file: []const u8, loader: *FileLoader, err_out: *?
|
||||
var runner = try @import("testing.zig").jsRunner(arena, .{
|
||||
.url = "http://127.0.0.1",
|
||||
.html = html,
|
||||
.platform = platform,
|
||||
});
|
||||
defer runner.deinit();
|
||||
|
||||
try polyfill.load(arena, runner.page.main_context);
|
||||
try polyfill.preload(arena, runner.page.main_context);
|
||||
|
||||
// loop over the scripts.
|
||||
const doc = parser.documentHTMLToDocument(runner.page.window.document);
|
||||
@@ -157,7 +170,7 @@ fn run(arena: Allocator, test_file: []const u8, loader: *FileLoader, err_out: *?
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(runner.page.main_context);
|
||||
defer try_catch.deinit();
|
||||
try runner.page.loop.run();
|
||||
try runner.page.loop.run(std.time.ns_per_ms * 200);
|
||||
|
||||
if (try_catch.hasCaught()) {
|
||||
err_out.* = (try try_catch.err(arena)) orelse "unknwon error";
|
||||
|
||||
1330
src/runtime/js.zig
1330
src/runtime/js.zig
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,8 @@ const MemoryPool = std.heap.MemoryPool;
|
||||
const log = @import("../log.zig");
|
||||
pub const IO = @import("tigerbeetle-io").IO;
|
||||
|
||||
const RUN_DURATION = 10 * std.time.ns_per_ms;
|
||||
|
||||
// SingleThreaded I/O Loop based on Tigerbeetle io_uring loop.
|
||||
// On Linux it's using io_uring.
|
||||
// On MacOS and Windows it's using kqueue/IOCP with a ring design.
|
||||
@@ -82,7 +84,7 @@ pub const Loop = struct {
|
||||
// run tail events. We do run the tail events to ensure all the
|
||||
// contexts are correcly free.
|
||||
while (self.pending_network_count != 0 or self.pending_timeout_count != 0) {
|
||||
self.io.run_for_ns(std.time.ns_per_ms * 10) catch |err| {
|
||||
self.io.run_for_ns(RUN_DURATION) catch |err| {
|
||||
log.err(.loop, "deinit", .{ .err = err });
|
||||
break;
|
||||
};
|
||||
@@ -102,12 +104,16 @@ pub const Loop = struct {
|
||||
// Stops when there is no more I/O events registered on the loop.
|
||||
// Note that I/O events callbacks might register more I/O events
|
||||
// on the go when they are executed (ie. nested I/O events).
|
||||
pub fn run(self: *Self) !void {
|
||||
pub fn run(self: *Self, wait_ns: usize) !void {
|
||||
// stop repeating / interval timeouts from re-registering
|
||||
self.stopping = true;
|
||||
defer self.stopping = false;
|
||||
|
||||
while (self.pending_network_count != 0 or self.pending_timeout_count != 0) {
|
||||
const max_iterations = wait_ns / (RUN_DURATION);
|
||||
for (0..max_iterations) |_| {
|
||||
if (self.pending_network_count == 0 and self.pending_timeout_count == 0) {
|
||||
break;
|
||||
}
|
||||
self.io.run_for_ns(std.time.ns_per_ms * 10) catch |err| {
|
||||
log.err(.loop, "deinit", .{ .err = err });
|
||||
break;
|
||||
@@ -187,6 +193,11 @@ pub const Loop = struct {
|
||||
}
|
||||
|
||||
pub fn timeout(self: *Self, nanoseconds: u63, callback_node: ?*CallbackNode) !usize {
|
||||
if (self.stopping and nanoseconds > std.time.ns_per_ms * 500) {
|
||||
// we're trying to shutdown, we probably don't want to wait for a new
|
||||
// long timeout
|
||||
return 0;
|
||||
}
|
||||
const completion = try self.alloc.create(Completion);
|
||||
errdefer self.alloc.destroy(completion);
|
||||
completion.* = undefined;
|
||||
|
||||
@@ -77,6 +77,117 @@ pub const MyAPI = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub const Parent = packed struct {
|
||||
parent_id: i32 = 0,
|
||||
|
||||
pub fn get_parent(self: *const Parent) i32 {
|
||||
return self.parent_id;
|
||||
}
|
||||
pub fn set_parent(self: *Parent, id: i32) void {
|
||||
self.parent_id = id;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Middle = struct {
|
||||
pub const prototype = *Parent;
|
||||
|
||||
middle_id: i32 = 0,
|
||||
_padding_1: u8 = 0,
|
||||
_padding_2: u8 = 1,
|
||||
_padding_3: u8 = 2,
|
||||
proto: Parent,
|
||||
|
||||
pub fn constructor() Middle {
|
||||
return .{
|
||||
.middle_id = 0,
|
||||
.proto = .{ .parent_id = 0 },
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_middle(self: *const Middle) i32 {
|
||||
return self.middle_id;
|
||||
}
|
||||
pub fn set_middle(self: *Middle, id: i32) void {
|
||||
self.middle_id = id;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Child = struct {
|
||||
pub const prototype = *Middle;
|
||||
|
||||
child_id: i32 = 0,
|
||||
_padding_1: u8 = 0,
|
||||
proto: Middle,
|
||||
|
||||
pub fn constructor() Child {
|
||||
return .{
|
||||
.child_id = 0,
|
||||
.proto = .{ .middle_id = 0, .proto = .{ .parent_id = 0 } },
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_child(self: *const Child) i32 {
|
||||
return self.child_id;
|
||||
}
|
||||
pub fn set_child(self: *Child, id: i32) void {
|
||||
self.child_id = id;
|
||||
}
|
||||
};
|
||||
|
||||
pub const MiddlePtr = packed struct {
|
||||
pub const prototype = *Parent;
|
||||
|
||||
middle_id: i32 = 0,
|
||||
_padding_1: u8 = 0,
|
||||
_padding_2: u8 = 1,
|
||||
_padding_3: u8 = 2,
|
||||
proto: *Parent,
|
||||
|
||||
pub fn constructor(state: State) !MiddlePtr {
|
||||
const parent = try state.arena.create(Parent);
|
||||
parent.* = .{ .parent_id = 0 };
|
||||
return .{
|
||||
.middle_id = 0,
|
||||
.proto = parent,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_middle(self: *const MiddlePtr) i32 {
|
||||
return self.middle_id;
|
||||
}
|
||||
pub fn set_middle(self: *MiddlePtr, id: i32) void {
|
||||
self.middle_id = id;
|
||||
}
|
||||
};
|
||||
|
||||
pub const ChildPtr = packed struct {
|
||||
pub const prototype = *MiddlePtr;
|
||||
|
||||
child_id: i32 = 0,
|
||||
_padding_1: u8 = 0,
|
||||
_padding_2: u8 = 1,
|
||||
proto: *MiddlePtr,
|
||||
|
||||
pub fn constructor(state: State) !ChildPtr {
|
||||
const parent = try state.arena.create(Parent);
|
||||
const middle = try state.arena.create(MiddlePtr);
|
||||
|
||||
parent.* = .{ .parent_id = 0 };
|
||||
middle.* = .{ .middle_id = 0, .proto = parent };
|
||||
return .{
|
||||
.child_id = 0,
|
||||
.proto = middle,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_child(self: *const ChildPtr) i32 {
|
||||
return self.child_id;
|
||||
}
|
||||
pub fn set_child(self: *ChildPtr, id: i32) void {
|
||||
self.child_id = id;
|
||||
}
|
||||
};
|
||||
|
||||
const State = struct {
|
||||
arena: Allocator,
|
||||
};
|
||||
@@ -90,6 +201,11 @@ test "JS: object types" {
|
||||
Other,
|
||||
MyObject,
|
||||
MyAPI,
|
||||
Parent,
|
||||
Middle,
|
||||
Child,
|
||||
MiddlePtr,
|
||||
ChildPtr,
|
||||
}).init(.{ .arena = arena.allocator() }, {});
|
||||
|
||||
defer runner.deinit();
|
||||
@@ -120,4 +236,40 @@ test "JS: object types" {
|
||||
// check object property
|
||||
.{ "myObjIndirect.a.val()", "4" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let m1 = new Middle();", null },
|
||||
.{ "m1.middle = 2", null },
|
||||
.{ "m1.parent = 3", null },
|
||||
.{ "m1.middle", "2" },
|
||||
.{ "m1.parent", "3" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let c1 = new Child();", null },
|
||||
.{ "c1.child = 1", null },
|
||||
.{ "c1.middle = 2", null },
|
||||
.{ "c1.parent = 3", null },
|
||||
.{ "c1.child", "1" },
|
||||
.{ "c1.middle", "2" },
|
||||
.{ "c1.parent", "3" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let m2 = new MiddlePtr();", null },
|
||||
.{ "m2.middle = 2", null },
|
||||
.{ "m2.parent = 3", null },
|
||||
.{ "m2.middle", "2" },
|
||||
.{ "m2.parent", "3" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let c2 = new ChildPtr();", null },
|
||||
.{ "c2.child = 1", null },
|
||||
.{ "c2.middle = 2", null },
|
||||
.{ "c2.parent = 3", null },
|
||||
.{ "c2.child", "1" },
|
||||
.{ "c2.middle", "2" },
|
||||
.{ "c2.parent", "3" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -336,4 +336,8 @@ test "JS: primitive types" {
|
||||
.{ "p.returnFloat32()", "1.100000023841858,-200.03500366210938,0.0003000000142492354" },
|
||||
.{ "p.returnFloat64()", "8881.22284,-4928.3838122,-0.00004" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "'foo\\\\:bar'", "foo\\:bar" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ pub fn Runner(comptime State: type, comptime Global: type, comptime types: anyty
|
||||
const self = try allocator.create(Self);
|
||||
errdefer allocator.destroy(self);
|
||||
|
||||
self.env = try Env.init(allocator, .{});
|
||||
self.env = try Env.init(allocator, null, .{});
|
||||
errdefer self.env.deinit();
|
||||
|
||||
self.executor = try self.env.newExecutionWorld();
|
||||
@@ -53,6 +53,7 @@ pub fn Runner(comptime State: type, comptime Global: type, comptime types: anyty
|
||||
state,
|
||||
{},
|
||||
true,
|
||||
null,
|
||||
);
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ const CDP = @import("cdp/cdp.zig").CDP;
|
||||
|
||||
const TimeoutCheck = std.time.ns_per_ms * 100;
|
||||
|
||||
const MAX_HTTP_REQUEST_SIZE = 2048;
|
||||
const MAX_HTTP_REQUEST_SIZE = 4096;
|
||||
|
||||
// max message size
|
||||
// +14 for max websocket payload overhead
|
||||
@@ -223,7 +223,7 @@ pub const Client = struct {
|
||||
}
|
||||
|
||||
fn close(self: *Self) void {
|
||||
log.info(.app, "client disconected", .{});
|
||||
log.info(.app, "client disconnected", .{});
|
||||
self.connected = false;
|
||||
// recv only, because we might have pending writes we'd like to get
|
||||
// out (like the HTTP error response)
|
||||
@@ -633,6 +633,10 @@ pub const Client = struct {
|
||||
}
|
||||
|
||||
fn queueSend(self: *Self) void {
|
||||
if (self.connected == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const node = self.send_queue.first orelse {
|
||||
// no more messages to send;
|
||||
return;
|
||||
@@ -1142,7 +1146,7 @@ test "Client: http invalid request" {
|
||||
var c = try createTestClient();
|
||||
defer c.deinit();
|
||||
|
||||
const res = try c.httpRequest("GET /over/9000 HTTP/1.1\r\n" ++ "Header: " ++ ("a" ** 2050) ++ "\r\n\r\n");
|
||||
const res = try c.httpRequest("GET /over/9000 HTTP/1.1\r\n" ++ "Header: " ++ ("a" ** 4100) ++ "\r\n\r\n");
|
||||
try testing.expectEqualStrings("HTTP/1.1 413 \r\n" ++
|
||||
"Connection: Close\r\n" ++
|
||||
"Content-Length: 17\r\n\r\n" ++
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// some utils to parser strings.
|
||||
const std = @import("std");
|
||||
|
||||
pub const Reader = struct {
|
||||
pos: usize = 0,
|
||||
data: []const u8,
|
||||
|
||||
pub fn until(self: *Reader, c: u8) []const u8 {
|
||||
const pos = self.pos;
|
||||
const data = self.data;
|
||||
|
||||
const index = std.mem.indexOfScalarPos(u8, data, pos, c) orelse data.len;
|
||||
self.pos = index;
|
||||
return data[pos..index];
|
||||
}
|
||||
|
||||
pub fn tail(self: *Reader) []const u8 {
|
||||
const pos = self.pos;
|
||||
const data = self.data;
|
||||
if (pos > data.len) {
|
||||
return "";
|
||||
}
|
||||
self.pos = data.len;
|
||||
return data[pos..];
|
||||
}
|
||||
|
||||
pub fn skip(self: *Reader) bool {
|
||||
const pos = self.pos;
|
||||
if (pos >= self.data.len) {
|
||||
return false;
|
||||
}
|
||||
self.pos = pos + 1;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// converts a comptime-known string (i.e. null terminated) to an uint
|
||||
pub fn asUint(comptime string: anytype) AsUintReturn(string) {
|
||||
const byteLength = @bitSizeOf(@TypeOf(string.*)) / 8 - 1;
|
||||
const expectedType = *const [byteLength:0]u8;
|
||||
if (@TypeOf(string) != expectedType) {
|
||||
@compileError("expected : " ++ @typeName(expectedType) ++
|
||||
", got: " ++ @typeName(@TypeOf(string)));
|
||||
}
|
||||
|
||||
return @bitCast(@as(*const [byteLength]u8, string).*);
|
||||
}
|
||||
|
||||
fn AsUintReturn(comptime string: anytype) type {
|
||||
return @Type(.{
|
||||
.int = .{
|
||||
.bits = @bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0
|
||||
.signedness = .unsigned,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const testing = std.testing;
|
||||
test "parser.Reader: skip" {
|
||||
var r = Reader{ .data = "foo" };
|
||||
try testing.expectEqual(true, r.skip());
|
||||
try testing.expectEqual(true, r.skip());
|
||||
try testing.expectEqual(true, r.skip());
|
||||
try testing.expectEqual(false, r.skip());
|
||||
try testing.expectEqual(false, r.skip());
|
||||
}
|
||||
|
||||
test "parser.Reader: tail" {
|
||||
var r = Reader{ .data = "foo" };
|
||||
try testing.expectEqualStrings("foo", r.tail());
|
||||
try testing.expectEqualStrings("", r.tail());
|
||||
try testing.expectEqualStrings("", r.tail());
|
||||
}
|
||||
|
||||
test "parser.Reader: until" {
|
||||
var r = Reader{ .data = "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{ .data = "foo" };
|
||||
try testing.expectEqualStrings("foo", r.until('.'));
|
||||
try testing.expectEqualStrings("", r.tail());
|
||||
|
||||
r = Reader{ .data = "" };
|
||||
try testing.expectEqualStrings("", r.until('.'));
|
||||
try testing.expectEqualStrings("", r.tail());
|
||||
}
|
||||
|
||||
test "parser: asUint" {
|
||||
const ASCII_x = @as(u8, @bitCast([1]u8{'x'}));
|
||||
const ASCII_ab = @as(u16, @bitCast([2]u8{ 'a', 'b' }));
|
||||
const ASCII_xyz = @as(u24, @bitCast([3]u8{ 'x', 'y', 'z' }));
|
||||
const ASCII_abcd = @as(u32, @bitCast([4]u8{ 'a', 'b', 'c', 'd' }));
|
||||
|
||||
try testing.expectEqual(ASCII_x, asUint("x"));
|
||||
try testing.expectEqual(ASCII_ab, asUint("ab"));
|
||||
try testing.expectEqual(ASCII_xyz, asUint("xyz"));
|
||||
try testing.expectEqual(ASCII_abcd, asUint("abcd"));
|
||||
|
||||
try testing.expectEqual(u8, @TypeOf(asUint("x")));
|
||||
try testing.expectEqual(u16, @TypeOf(asUint("ab")));
|
||||
try testing.expectEqual(u24, @TypeOf(asUint("xyz")));
|
||||
try testing.expectEqual(u32, @TypeOf(asUint("abcd")));
|
||||
}
|
||||
@@ -19,6 +19,8 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Platform = @import("runtime/js.zig").Platform;
|
||||
|
||||
pub const allocator = std.testing.allocator;
|
||||
pub const expectError = std.testing.expectError;
|
||||
pub const expect = std.testing.expect;
|
||||
@@ -66,7 +68,10 @@ pub fn expectEqual(expected: anytype, actual: anytype) !void {
|
||||
if (@typeInfo(@TypeOf(expected)) == .null) {
|
||||
return std.testing.expectEqual(null, actual);
|
||||
}
|
||||
return expectEqual(expected, actual.?);
|
||||
if (actual) |_actual| {
|
||||
return expectEqual(expected, _actual);
|
||||
}
|
||||
return std.testing.expectEqual(expected, null);
|
||||
},
|
||||
.@"union" => |union_info| {
|
||||
if (union_info.tag_type == null) {
|
||||
@@ -380,6 +385,7 @@ pub const JsRunner = struct {
|
||||
var app = try App.init(alloc, .{
|
||||
.run_mode = .serve,
|
||||
.tls_verify_host = false,
|
||||
.platform = opts.platform,
|
||||
});
|
||||
errdefer app.deinit();
|
||||
|
||||
@@ -435,7 +441,7 @@ pub const JsRunner = struct {
|
||||
}
|
||||
return err;
|
||||
};
|
||||
try self.page.loop.run();
|
||||
try self.page.loop.run(std.time.ns_per_ms * 200);
|
||||
@import("root").js_runner_duration += std.time.Instant.since(try std.time.Instant.now(), start);
|
||||
|
||||
if (case.@"1") |expected| {
|
||||
@@ -468,9 +474,16 @@ pub const JsRunner = struct {
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
pub fn dispatchDOMContentLoaded(self: *JsRunner) !void {
|
||||
const HTMLDocument = @import("browser/html/document.zig").HTMLDocument;
|
||||
const html_doc = self.page.window.document;
|
||||
try HTMLDocument.documentIsLoaded(html_doc, self.page);
|
||||
}
|
||||
};
|
||||
|
||||
const RunnerOpts = struct {
|
||||
platform: ?*const Platform = null,
|
||||
url: []const u8 = "https://lightpanda.io/opensource-browser/",
|
||||
html: []const u8 =
|
||||
\\ <div id="content">
|
||||
|
||||
268
src/url.zig
268
src/url.zig
@@ -100,41 +100,72 @@ pub const URL = struct {
|
||||
/// For URLs without a path, it will add src as the path.
|
||||
pub fn stitch(
|
||||
allocator: Allocator,
|
||||
src: []const u8,
|
||||
path: []const u8,
|
||||
base: []const u8,
|
||||
opts: StitchOpts,
|
||||
) ![]const u8 {
|
||||
if (base.len == 0 or isURL(src)) {
|
||||
if (base.len == 0 or isComleteHTTPUrl(path)) {
|
||||
if (opts.alloc == .always) {
|
||||
return allocator.dupe(u8, src);
|
||||
return allocator.dupe(u8, path);
|
||||
}
|
||||
return src;
|
||||
return path;
|
||||
}
|
||||
if (src.len == 0) {
|
||||
|
||||
if (path.len == 0) {
|
||||
if (opts.alloc == .always) {
|
||||
return allocator.dupe(u8, base);
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
const protocol_end: usize = blk: {
|
||||
if (std.mem.indexOf(u8, base, "://")) |protocol_index| {
|
||||
break :blk protocol_index + 3;
|
||||
} else {
|
||||
break :blk 0;
|
||||
}
|
||||
};
|
||||
// Quick hack because domains have to be at least 3 characters.
|
||||
// Given https://a.b this will point to 'a'
|
||||
// Given http://a.b this will point '.'
|
||||
// Either way, we just care about this value to find the start of the path
|
||||
const protocol_end: usize = if (isComleteHTTPUrl(base)) 8 else 0;
|
||||
|
||||
const normalized_src = if (src[0] == '/') src[1..] else src;
|
||||
if (path[0] == '/') {
|
||||
const pos = std.mem.indexOfScalarPos(u8, base, protocol_end, '/') orelse base.len;
|
||||
return std.fmt.allocPrint(allocator, "{s}{s}", .{ base[0..pos], path });
|
||||
}
|
||||
|
||||
if (std.mem.lastIndexOfScalar(u8, base[protocol_end..], '/')) |index| {
|
||||
const last_slash_pos = index + protocol_end;
|
||||
if (last_slash_pos == base.len - 1) {
|
||||
return std.fmt.allocPrint(allocator, "{s}{s}", .{ base, normalized_src });
|
||||
var normalized_base = base;
|
||||
if (std.mem.lastIndexOfScalar(u8, base[protocol_end..], '/')) |pos| {
|
||||
normalized_base = base[0 .. pos + protocol_end];
|
||||
}
|
||||
return std.fmt.allocPrint(allocator, "{s}/{s}", .{ base[0..last_slash_pos], normalized_src });
|
||||
|
||||
var out = try std.fmt.allocPrint(allocator, "{s}/{s}", .{
|
||||
normalized_base,
|
||||
path,
|
||||
});
|
||||
|
||||
// Strip out ./ and ../. This is done in-place, because doing so can
|
||||
// only ever make `out` smaller. After this, `out` cannot be freed by
|
||||
// an allocator, which is ok, because we expect allocator to be an arena.
|
||||
var in_i: usize = 0;
|
||||
var out_i: usize = 0;
|
||||
while (in_i < out.len) {
|
||||
if (std.mem.startsWith(u8, out[in_i..], "./")) {
|
||||
in_i += 2;
|
||||
continue;
|
||||
}
|
||||
return std.fmt.allocPrint(allocator, "{s}/{s}", .{ base, normalized_src });
|
||||
if (std.mem.startsWith(u8, out[in_i..], "../")) {
|
||||
std.debug.assert(out[out_i - 1] == '/');
|
||||
|
||||
out_i -= 2;
|
||||
while (out_i > 1 and out[out_i - 1] != '/') {
|
||||
out_i -= 1;
|
||||
}
|
||||
// <= to deal with the hack-ish protocol_end which will be off-by-one between http and https
|
||||
if (out_i <= protocol_end) return error.InvalidURL;
|
||||
in_i += 3;
|
||||
continue;
|
||||
}
|
||||
out[out_i] = out[in_i];
|
||||
in_i += 1;
|
||||
out_i += 1;
|
||||
}
|
||||
return out[0..out_i];
|
||||
}
|
||||
|
||||
pub fn concatQueryString(arena: Allocator, url: []const u8, query_string: []const u8) ![]const u8 {
|
||||
@@ -163,7 +194,7 @@ pub const URL = struct {
|
||||
}
|
||||
};
|
||||
|
||||
fn isURL(url: []const u8) bool {
|
||||
fn isComleteHTTPUrl(url: []const u8) bool {
|
||||
if (std.mem.startsWith(u8, url, "://")) {
|
||||
return true;
|
||||
}
|
||||
@@ -184,17 +215,17 @@ fn isURL(url: []const u8) bool {
|
||||
}
|
||||
|
||||
const testing = @import("testing.zig");
|
||||
test "URL: isURL" {
|
||||
try testing.expectEqual(true, isURL("://lightpanda.io"));
|
||||
try testing.expectEqual(true, isURL("://lightpanda.io/about"));
|
||||
try testing.expectEqual(true, isURL("http://lightpanda.io/about"));
|
||||
try testing.expectEqual(true, isURL("HttP://lightpanda.io/about"));
|
||||
try testing.expectEqual(true, isURL("httpS://lightpanda.io/about"));
|
||||
try testing.expectEqual(true, isURL("HTTPs://lightpanda.io/about"));
|
||||
test "URL: isComleteHTTPUrl" {
|
||||
try testing.expectEqual(true, isComleteHTTPUrl("://lightpanda.io"));
|
||||
try testing.expectEqual(true, isComleteHTTPUrl("://lightpanda.io/about"));
|
||||
try testing.expectEqual(true, isComleteHTTPUrl("http://lightpanda.io/about"));
|
||||
try testing.expectEqual(true, isComleteHTTPUrl("HttP://lightpanda.io/about"));
|
||||
try testing.expectEqual(true, isComleteHTTPUrl("httpS://lightpanda.io/about"));
|
||||
try testing.expectEqual(true, isComleteHTTPUrl("HTTPs://lightpanda.io/about"));
|
||||
|
||||
try testing.expectEqual(false, isURL("/lightpanda.io"));
|
||||
try testing.expectEqual(false, isURL("../../about"));
|
||||
try testing.expectEqual(false, isURL("about"));
|
||||
try testing.expectEqual(false, isComleteHTTPUrl("/lightpanda.io"));
|
||||
try testing.expectEqual(false, isComleteHTTPUrl("../../about"));
|
||||
try testing.expectEqual(false, isComleteHTTPUrl("about"));
|
||||
}
|
||||
|
||||
test "URL: resolve size" {
|
||||
@@ -213,73 +244,122 @@ test "URL: resolve size" {
|
||||
try std.testing.expectEqualStrings(out_url.raw[26..], &url_string);
|
||||
}
|
||||
|
||||
test "URL: Stitching Base & Src URLs (Basic)" {
|
||||
const allocator = testing.allocator;
|
||||
test "URL: stitch" {
|
||||
defer testing.reset();
|
||||
|
||||
const base = "https://www.google.com/xyz/abc/123";
|
||||
const src = "something.js";
|
||||
const result = try URL.stitch(allocator, src, base, .{});
|
||||
defer allocator.free(result);
|
||||
try testing.expectString("https://www.google.com/xyz/abc/something.js", result);
|
||||
const Case = struct {
|
||||
base: []const u8,
|
||||
path: []const u8,
|
||||
expected: []const u8,
|
||||
};
|
||||
|
||||
const cases = [_]Case{
|
||||
.{
|
||||
.base = "https://lightpanda.io/xyz/abc/123",
|
||||
.path = "something.js",
|
||||
.expected = "https://lightpanda.io/xyz/abc/something.js",
|
||||
},
|
||||
.{
|
||||
.base = "https://lightpanda.io/xyz/abc/123",
|
||||
.path = "/something.js",
|
||||
.expected = "https://lightpanda.io/something.js",
|
||||
},
|
||||
.{
|
||||
.base = "https://lightpanda.io/",
|
||||
.path = "something.js",
|
||||
.expected = "https://lightpanda.io/something.js",
|
||||
},
|
||||
.{
|
||||
.base = "https://lightpanda.io/",
|
||||
.path = "/something.js",
|
||||
.expected = "https://lightpanda.io/something.js",
|
||||
},
|
||||
.{
|
||||
.base = "https://lightpanda.io",
|
||||
.path = "something.js",
|
||||
.expected = "https://lightpanda.io/something.js",
|
||||
},
|
||||
.{
|
||||
.base = "https://lightpanda.io",
|
||||
.path = "abc/something.js",
|
||||
.expected = "https://lightpanda.io/abc/something.js",
|
||||
},
|
||||
.{
|
||||
.base = "https://lightpanda.io/nested",
|
||||
.path = "abc/something.js",
|
||||
.expected = "https://lightpanda.io/abc/something.js",
|
||||
},
|
||||
.{
|
||||
.base = "https://lightpanda.io/nested/",
|
||||
.path = "abc/something.js",
|
||||
.expected = "https://lightpanda.io/nested/abc/something.js",
|
||||
},
|
||||
.{
|
||||
.base = "https://lightpanda.io/nested/",
|
||||
.path = "/abc/something.js",
|
||||
.expected = "https://lightpanda.io/abc/something.js",
|
||||
},
|
||||
.{
|
||||
.base = "https://lightpanda.io/nested/",
|
||||
.path = "http://www.github.com/lightpanda-io/",
|
||||
.expected = "http://www.github.com/lightpanda-io/",
|
||||
},
|
||||
.{
|
||||
.base = "https://lightpanda.io/nested/",
|
||||
.path = "",
|
||||
.expected = "https://lightpanda.io/nested/",
|
||||
},
|
||||
.{
|
||||
.base = "https://lightpanda.io/abc/aaa",
|
||||
.path = "./hello/./world",
|
||||
.expected = "https://lightpanda.io/abc/hello/world",
|
||||
},
|
||||
.{
|
||||
.base = "https://lightpanda.io/abc/aaa/",
|
||||
.path = "../hello",
|
||||
.expected = "https://lightpanda.io/abc/hello",
|
||||
},
|
||||
.{
|
||||
.base = "https://lightpanda.io/abc/aaa",
|
||||
.path = "../hello",
|
||||
.expected = "https://lightpanda.io/hello",
|
||||
},
|
||||
.{
|
||||
.base = "https://lightpanda.io/abc/aaa/",
|
||||
.path = "./.././.././hello",
|
||||
.expected = "https://lightpanda.io/hello",
|
||||
},
|
||||
.{
|
||||
.base = "some/page",
|
||||
.path = "hello",
|
||||
.expected = "some/hello",
|
||||
},
|
||||
.{
|
||||
.base = "some/page/",
|
||||
.path = "hello",
|
||||
.expected = "some/page/hello",
|
||||
},
|
||||
|
||||
.{
|
||||
.base = "some/page/other",
|
||||
.path = ".././hello",
|
||||
.expected = "some/hello",
|
||||
},
|
||||
};
|
||||
|
||||
for (cases) |case| {
|
||||
const result = try stitch(testing.arena_allocator, case.path, case.base, .{});
|
||||
try testing.expectString(case.expected, result);
|
||||
}
|
||||
|
||||
test "URL: Stitching Base & Src URLs (Just Ending Slash)" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
const base = "https://www.google.com/";
|
||||
const src = "something.js";
|
||||
const result = try URL.stitch(allocator, src, base, .{});
|
||||
defer allocator.free(result);
|
||||
try testing.expectString("https://www.google.com/something.js", result);
|
||||
}
|
||||
|
||||
test "URL: Stitching Base & Src URLs with leading slash" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
const base = "https://www.google.com/";
|
||||
const src = "/something.js";
|
||||
const result = try URL.stitch(allocator, src, base, .{});
|
||||
defer allocator.free(result);
|
||||
try testing.expectString("https://www.google.com/something.js", result);
|
||||
}
|
||||
|
||||
test "URL: Stitching Base & Src URLs (No Ending Slash)" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
const base = "https://www.google.com";
|
||||
const src = "something.js";
|
||||
const result = try URL.stitch(allocator, src, base, .{});
|
||||
defer allocator.free(result);
|
||||
try testing.expectString("https://www.google.com/something.js", result);
|
||||
}
|
||||
|
||||
test "URL: Stiching Base & Src URLs (Both Local)" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
const base = "./abcdef/123.js";
|
||||
const src = "something.js";
|
||||
const result = try URL.stitch(allocator, src, base, .{});
|
||||
defer allocator.free(result);
|
||||
try testing.expectString("./abcdef/something.js", result);
|
||||
}
|
||||
|
||||
test "URL: Stiching src as full path" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
const base = "https://www.lightpanda.io/";
|
||||
const src = "https://lightpanda.io/something.js";
|
||||
const result = try URL.stitch(allocator, src, base, .{ .alloc = .if_needed });
|
||||
try testing.expectString("https://lightpanda.io/something.js", result);
|
||||
}
|
||||
|
||||
test "URL: Stitching Base & Src URLs (empty src)" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
const base = "https://www.google.com/xyz/abc/123";
|
||||
const src = "";
|
||||
const result = try URL.stitch(allocator, src, base, .{});
|
||||
defer allocator.free(result);
|
||||
try testing.expectString("https://www.google.com/xyz/abc/123", result);
|
||||
try testing.expectError(
|
||||
error.InvalidURL,
|
||||
stitch(testing.arena_allocator, "../hello", "https://lightpanda.io/", .{}),
|
||||
);
|
||||
try testing.expectError(
|
||||
error.InvalidURL,
|
||||
stitch(testing.arena_allocator, "../hello", "http://lightpanda.io/", .{}),
|
||||
);
|
||||
}
|
||||
|
||||
test "URL: concatQueryString" {
|
||||
|
||||
2
vendor/netsurf/libdom
vendored
2
vendor/netsurf/libdom
vendored
Submodule vendor/netsurf/libdom updated: 614187b0aa...84723c5be5
Reference in New Issue
Block a user