mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 07:03:29 +00:00
Compare commits
436 Commits
get-cached
...
fetch_lazy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afa0d5ba12 | ||
|
|
4d1e416299 | ||
|
|
3badcdbdbd | ||
|
|
fcd82b2c14 | ||
|
|
d0621510cc | ||
|
|
2a7a8bc2a6 | ||
|
|
af916dea1d | ||
|
|
31335fc4fb | ||
|
|
c84634093d | ||
|
|
37d8d2642d | ||
|
|
0423a178e9 | ||
|
|
7acf67d668 | ||
|
|
ef1fece40c | ||
|
|
ebb590250f | ||
|
|
03130a95d8 | ||
|
|
e133717f7f | ||
|
|
968c695da1 | ||
|
|
707116a030 | ||
|
|
01966f41ff | ||
|
|
141d17dd55 | ||
|
|
a3c2daf306 | ||
|
|
dc60fac90d | ||
|
|
a5e2e8ea15 | ||
|
|
8295c2abe5 | ||
|
|
5997be89f6 | ||
|
|
1c89cfe5d4 | ||
|
|
b5021bd9fa | ||
|
|
4fd365b520 | ||
|
|
479cd5ab1a | ||
|
|
8285cbcaa9 | ||
|
|
545d97b5c0 | ||
|
|
11016abdd3 | ||
|
|
066df87dd4 | ||
|
|
91899912d8 | ||
|
|
4ceca6b90b | ||
|
|
ec936417c6 | ||
|
|
4b75b33eb3 | ||
|
|
1d7e731034 | ||
|
|
ab60f64452 | ||
|
|
9757ea7b0f | ||
|
|
855583874f | ||
|
|
9efc27c2bb | ||
|
|
cab5117d85 | ||
|
|
b7d26cf0d5 | ||
|
|
59b4033ab2 | ||
|
|
13a7219dbd | ||
|
|
eae8a90a89 | ||
|
|
a87f4abd5f | ||
|
|
1b73691c69 | ||
|
|
e00066466b | ||
|
|
b87a8ba97d | ||
|
|
57aa270032 | ||
|
|
90a96fd8a7 | ||
|
|
c05470515f | ||
|
|
81ed4f3699 | ||
|
|
c9ac1eab11 | ||
|
|
1ba542fb3b | ||
|
|
4f127c9de3 | ||
|
|
16656f6c13 | ||
|
|
0f13e062fe | ||
|
|
2e68407fbe | ||
|
|
974f350f27 | ||
|
|
27ffea9052 | ||
|
|
9b2b35e8a2 | ||
|
|
3b51ca3947 | ||
|
|
62a2d08b53 | ||
|
|
e790bde717 | ||
|
|
0ab6b15292 | ||
|
|
2aeeb14c21 | ||
|
|
e5e57ab3bd | ||
|
|
f3ce5dcfbd | ||
|
|
bc341e98fc | ||
|
|
80851f4861 | ||
|
|
22b4456bce | ||
|
|
8d67502997 | ||
|
|
8f31fd778b | ||
|
|
f79f25bcf4 | ||
|
|
68e237eec5 | ||
|
|
8895c70c7f | ||
|
|
3964f8649d | ||
|
|
b7fb0ef1d3 | ||
|
|
66e403c5b4 | ||
|
|
0913abe806 | ||
|
|
6d3065c4c6 | ||
|
|
9092d1f8eb | ||
|
|
1bd1f123a3 | ||
|
|
44c072dcbb | ||
|
|
45c59e2990 | ||
|
|
75f0cd6e62 | ||
|
|
80f758018c | ||
|
|
b5e2c62fdd | ||
|
|
ede35718ae | ||
|
|
31fe2807aa | ||
|
|
f77693d768 | ||
|
|
96e3c16cca | ||
|
|
edd41b37f0 | ||
|
|
139d0038f2 | ||
|
|
d25fc64d7a | ||
|
|
9c83b268b9 | ||
|
|
42092ac16a | ||
|
|
e4860d5bae | ||
|
|
a5d9b658fb | ||
|
|
f464e89415 | ||
|
|
cdc439c4ef | ||
|
|
746168f9ed | ||
|
|
5ad4885102 | ||
|
|
7eb53ca2bc | ||
|
|
10fc056184 | ||
|
|
7517937155 | ||
|
|
a86fa8cc37 | ||
|
|
e1c765e78a | ||
|
|
56b08bddd8 | ||
|
|
2ed8a1c0ec | ||
|
|
389dff7031 | ||
|
|
123d69e595 | ||
|
|
4ab7fe26fc | ||
|
|
0aa1e0200f | ||
|
|
575f827958 | ||
|
|
7136851893 | ||
|
|
67935b11c9 | ||
|
|
85f60cbc7b | ||
|
|
9c35f8a24e | ||
|
|
9971de2ccd | ||
|
|
1ca8dc0ac0 | ||
|
|
85d148822e | ||
|
|
1e738dcf79 | ||
|
|
b5ffd8d046 | ||
|
|
21e354d252 | ||
|
|
15628d9b07 | ||
|
|
950182986a | ||
|
|
bc82023878 | ||
|
|
d5363e5993 | ||
|
|
80adee8558 | ||
|
|
37fe6a661b | ||
|
|
eb453f471b | ||
|
|
afd278ca4e | ||
|
|
ca8877da2d | ||
|
|
42828c64fb | ||
|
|
6600626f4f | ||
|
|
ac10d5b2a3 | ||
|
|
9f040025e7 | ||
|
|
2522e7fe9c | ||
|
|
dd22c55d23 | ||
|
|
a6efa9e9b2 | ||
|
|
5087b8004a | ||
|
|
d4c40242d0 | ||
|
|
5af55f1d5d | ||
|
|
55ef0a5e9e | ||
|
|
5dda86bf4a | ||
|
|
d81377b10d | ||
|
|
da128f5d49 | ||
|
|
6e5fe8e4a2 | ||
|
|
b3d350d41e | ||
|
|
7c6870f8eb | ||
|
|
327b4e4e37 | ||
|
|
7fdc857326 | ||
|
|
0382c2775e | ||
|
|
a0374133cd | ||
|
|
055f697340 | ||
|
|
ec8a9862c7 | ||
|
|
f393eb7b7d | ||
|
|
78285d7b1e | ||
|
|
b6137b03cd | ||
|
|
e237e709b6 | ||
|
|
2ac9b2088a | ||
|
|
a791212d89 | ||
|
|
5cc5f45ef3 | ||
|
|
a11e50c087 | ||
|
|
4dc09360a1 | ||
|
|
3a5528cc4d | ||
|
|
de533755e5 | ||
|
|
024b69ee3e | ||
|
|
d7e7832e9f | ||
|
|
8d4d72bf15 | ||
|
|
86a82d55fa | ||
|
|
5a15066da3 | ||
|
|
81766c8517 | ||
|
|
e486f28a41 | ||
|
|
8a9cbaf413 | ||
|
|
3a0a930b79 | ||
|
|
c40704d2f3 | ||
|
|
c0f0630e17 | ||
|
|
19dbb3a778 | ||
|
|
d4fc6f1b35 | ||
|
|
7c82942912 | ||
|
|
87d48b028b | ||
|
|
d6640f4d15 | ||
|
|
785a8da623 | ||
|
|
57dc303d90 | ||
|
|
ce08cc9659 | ||
|
|
866393743c | ||
|
|
ba255aa653 | ||
|
|
7d46e8fe80 | ||
|
|
6c41245c73 | ||
|
|
2a8e51c2d2 | ||
|
|
b2cf5df612 | ||
|
|
ada9ddd5b8 | ||
|
|
f66f4d9aeb | ||
|
|
d02ba777f2 | ||
|
|
aef614823b | ||
|
|
431db85ecb | ||
|
|
1ebac06f4b | ||
|
|
c7c5af4708 | ||
|
|
0b6a9d3a0b | ||
|
|
23d6362058 | ||
|
|
1443f38e5f | ||
|
|
94960cc842 | ||
|
|
efc983b009 | ||
|
|
74d90f2892 | ||
|
|
56f1b6cc19 | ||
|
|
fa2cd9dfd9 | ||
|
|
687f09d952 | ||
|
|
67b479b5c8 | ||
|
|
eac2140693 | ||
|
|
7a3f5de9c2 | ||
|
|
7005bf2481 | ||
|
|
b80ee3342c | ||
|
|
4c7b7b1e60 | ||
|
|
1a4a3608c8 | ||
|
|
6800d50339 | ||
|
|
036f808ec6 | ||
|
|
7647ce9e6d | ||
|
|
545d3f81ce | ||
|
|
455615b9c1 | ||
|
|
d0e2a03da5 | ||
|
|
fa408e644c | ||
|
|
a22416584d | ||
|
|
b8fc60df45 | ||
|
|
c6455cf02e | ||
|
|
2ac1d39367 | ||
|
|
041e014d68 | ||
|
|
5defb5c442 | ||
|
|
520a572bb4 | ||
|
|
4c602256da | ||
|
|
5a40cbd655 | ||
|
|
a75f9dd48d | ||
|
|
6b47aa2446 | ||
|
|
a847a1faae | ||
|
|
bb381e522c | ||
|
|
6962cfb91a | ||
|
|
302c50a90e | ||
|
|
e2d47e1c86 | ||
|
|
7d51da1efb | ||
|
|
c7674926c3 | ||
|
|
f0ca9728ae | ||
|
|
5fa8567801 | ||
|
|
e5b1acb6e1 | ||
|
|
8fdbaef4c7 | ||
|
|
7869159657 | ||
|
|
7046e18d7e | ||
|
|
a7516061d0 | ||
|
|
e61d787ff0 | ||
|
|
25ad420f85 | ||
|
|
fcd49c000f | ||
|
|
e2320ebe66 | ||
|
|
5e78a26e3d | ||
|
|
159bd06a56 | ||
|
|
bc7e1e07f4 | ||
|
|
ccc9618102 | ||
|
|
0ad09cca9d | ||
|
|
0959eea677 | ||
|
|
3316f2fcf4 | ||
|
|
390a21e4aa | ||
|
|
70ce54a5cd | ||
|
|
087e42a641 | ||
|
|
e26d4afce2 | ||
|
|
b9ae4c6077 | ||
|
|
11485d24f5 | ||
|
|
ce14f0b380 | ||
|
|
8bb2158a2a | ||
|
|
1a9d4af565 | ||
|
|
a6f37633a1 | ||
|
|
3182a47858 | ||
|
|
7335b1d0a4 | ||
|
|
cd33e9ad0e | ||
|
|
557f8444b2 | ||
|
|
65088b8644 | ||
|
|
7cc9521cbb | ||
|
|
4ad19fc4d8 | ||
|
|
ec71f8e2d9 | ||
|
|
ff8a847795 | ||
|
|
6b001c50a4 | ||
|
|
5759c88932 | ||
|
|
00c11d9bd4 | ||
|
|
ed99acebfe | ||
|
|
bade412d30 | ||
|
|
191e9ba073 | ||
|
|
b21688a0ac | ||
|
|
a4d4da4d96 | ||
|
|
16c85c5b8a | ||
|
|
2c7b39927a | ||
|
|
7f47692ad4 | ||
|
|
af4066da87 | ||
|
|
4de4e7504d | ||
|
|
b46c181b07 | ||
|
|
e4f89092b3 | ||
|
|
4fbedf5b20 | ||
|
|
d51a03f1b6 | ||
|
|
f7eee0d461 | ||
|
|
39178d8d2b | ||
|
|
7795916c08 | ||
|
|
0e2a3d8009 | ||
|
|
38a0b6905e | ||
|
|
8797549369 | ||
|
|
f5ec74252d | ||
|
|
211012d367 | ||
|
|
c1319d1f27 | ||
|
|
d4d8773fd1 | ||
|
|
01223601f2 | ||
|
|
d9ed4cfca8 | ||
|
|
7d0e4b6270 | ||
|
|
b2f645a5ce | ||
|
|
6a29d6711c | ||
|
|
5b2806a784 | ||
|
|
a2f15ce0b2 | ||
|
|
68400f3bcf | ||
|
|
31f3c2771a | ||
|
|
f9352e26cb | ||
|
|
4fa542bc38 | ||
|
|
a707e10af7 | ||
|
|
1e095fede5 | ||
|
|
96b10f4b85 | ||
|
|
5100e06f38 | ||
|
|
35e2fa5058 | ||
|
|
8d2d4ffdd2 | ||
|
|
7d05712f40 | ||
|
|
c0106a238b | ||
|
|
f6c68e4580 | ||
|
|
3c8065fdee | ||
|
|
9bd8b2fc43 | ||
|
|
5a3d5f5512 | ||
|
|
ca9e850ac7 | ||
|
|
2dc09c799f | ||
|
|
a49154acf4 | ||
|
|
77eee7f087 | ||
|
|
03694b54f0 | ||
|
|
bed320204d | ||
|
|
971524fa3b | ||
|
|
4758456069 | ||
|
|
3ef4ba6b8b | ||
|
|
a504f051e7 | ||
|
|
ea0bbaf332 | ||
|
|
19c908035b | ||
|
|
05192b6850 | ||
|
|
079ce5e9de | ||
|
|
ff742c0169 | ||
|
|
332e264437 | ||
|
|
3554634c1c | ||
|
|
c96fb3c2f2 | ||
|
|
1e612e4166 | ||
|
|
06984ace21 | ||
|
|
cabd4fa718 | ||
|
|
ddb549cb45 | ||
|
|
c7484c69c0 | ||
|
|
9876d79680 | ||
|
|
32566ccc80 | ||
|
|
7f9e309ae8 | ||
|
|
7831aabe5a | ||
|
|
74b40b97ec | ||
|
|
f45726d61f | ||
|
|
3c0d027306 | ||
|
|
dc83765808 | ||
|
|
4244b572d1 | ||
|
|
77475ca5e4 | ||
|
|
3555680335 | ||
|
|
f65a39a3e3 | ||
|
|
94e8964f69 | ||
|
|
254d22e2cc | ||
|
|
54ab1326e5 | ||
|
|
b0fe5d60ab | ||
|
|
4b1eb2794f | ||
|
|
6a2dd1111c | ||
|
|
f5da89b50b | ||
|
|
bede244598 | ||
|
|
4df48c9695 | ||
|
|
05ad77ffbe | ||
|
|
dc23a74e7b | ||
|
|
f463cb16da | ||
|
|
b785884cd8 | ||
|
|
f09caec09a | ||
|
|
5e30a3997e | ||
|
|
8552a5797c | ||
|
|
a0d528981e | ||
|
|
7ffdee0d7f | ||
|
|
3d0928a449 | ||
|
|
ea1bca05c7 | ||
|
|
df292a2103 | ||
|
|
7f2c360f33 | ||
|
|
fbd40a6514 | ||
|
|
9dd02ec67d | ||
|
|
8e55082d4e | ||
|
|
29378c57ea | ||
|
|
16c74cf3b4 | ||
|
|
b199925f91 | ||
|
|
28397bf9d0 | ||
|
|
1b7abf9972 | ||
|
|
b98bdeaae7 | ||
|
|
221274b473 | ||
|
|
cc6d443113 | ||
|
|
b3c81c9e55 | ||
|
|
84d07f3f18 | ||
|
|
0fee2bbf28 | ||
|
|
ea38845622 | ||
|
|
81a0e95916 | ||
|
|
2a9feee476 | ||
|
|
c38c1fa93a | ||
|
|
8d7c35d034 | ||
|
|
425f62607b | ||
|
|
c1752ae5eb | ||
|
|
d61e91b949 | ||
|
|
090c0f8857 | ||
|
|
c453dd2b3c | ||
|
|
b2b2e97edc | ||
|
|
bd9e4dbc79 | ||
|
|
0c19070800 | ||
|
|
07e37b257f | ||
|
|
0a5f060d1b | ||
|
|
6fcfcb630d | ||
|
|
7aff90aec7 | ||
|
|
f1e513443b | ||
|
|
c533b10e19 | ||
|
|
b4014e8c24 | ||
|
|
478f3a5308 | ||
|
|
b98edf3d76 | ||
|
|
02fe46de58 | ||
|
|
ab2fd0ad36 | ||
|
|
88655d877b | ||
|
|
6e94affea6 | ||
|
|
f7f382275a | ||
|
|
23f3bf43c2 | ||
|
|
8a0c4909b9 | ||
|
|
2aeaf02d05 | ||
|
|
f4a6e34713 | ||
|
|
3eb85da02c | ||
|
|
6533456472 | ||
|
|
ca574df3be |
24
.github/actions/install/action.yml
vendored
24
.github/actions/install/action.yml
vendored
@@ -5,7 +5,7 @@ inputs:
|
||||
zig:
|
||||
description: 'Zig version to install'
|
||||
required: false
|
||||
default: '0.14.1'
|
||||
default: '0.15.1'
|
||||
arch:
|
||||
description: 'CPU arch used to select the v8 lib'
|
||||
required: false
|
||||
@@ -17,11 +17,11 @@ inputs:
|
||||
zig-v8:
|
||||
description: 'zig v8 version to install'
|
||||
required: false
|
||||
default: 'v0.1.28'
|
||||
default: 'v0.1.30'
|
||||
v8:
|
||||
description: 'v8 version to install'
|
||||
required: false
|
||||
default: '13.6.233.8'
|
||||
default: '14.0.365.4'
|
||||
cache-dir:
|
||||
description: 'cache dir to use'
|
||||
required: false
|
||||
@@ -67,9 +67,23 @@ runs:
|
||||
mkdir -p v8/out/${{ inputs.os }}/release/obj/zig/
|
||||
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/out/${{ inputs.os }}/release/obj/zig/libc_v8.a
|
||||
|
||||
- name: libiconv
|
||||
- name: Cache libiconv
|
||||
id: cache-libiconv
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-libiconv
|
||||
with:
|
||||
path: ${{ inputs.cache-dir }}/libiconv
|
||||
key: vendor/libiconv/libiconv-1.17
|
||||
|
||||
- name: download libiconv
|
||||
if: ${{ steps.cache-libiconv.outputs.cache-hit != 'true' }}
|
||||
shell: bash
|
||||
run: make install-libiconv
|
||||
run: make download-libiconv
|
||||
|
||||
- name: build libiconv
|
||||
shell: bash
|
||||
run: make build-libiconv
|
||||
|
||||
- name: build mimalloc
|
||||
shell: bash
|
||||
|
||||
23
.github/workflows/e2e-test.yml
vendored
23
.github/workflows/e2e-test.yml
vendored
@@ -93,9 +93,30 @@ jobs:
|
||||
- name: run end to end tests
|
||||
run: |
|
||||
./lightpanda serve & echo $! > LPD.pid
|
||||
go run runner/main.go --verbose
|
||||
go run runner/main.go
|
||||
kill `cat LPD.pid`
|
||||
|
||||
- name: build proxy
|
||||
run: |
|
||||
cd proxy
|
||||
go build
|
||||
|
||||
- name: run end to end tests through proxy
|
||||
run: |
|
||||
./proxy/proxy & echo $! > PROXY.id
|
||||
./lightpanda serve --http_proxy 'http://127.0.0.1:3000' & echo $! > LPD.pid
|
||||
go run runner/main.go
|
||||
kill `cat LPD.pid` `cat PROXY.id`
|
||||
|
||||
- name: run request interception through proxy
|
||||
run: |
|
||||
export PROXY_USERNAME=username PROXY_PASSWORD=password
|
||||
./proxy/proxy & echo $! > PROXY.id
|
||||
./lightpanda serve & echo $! > LPD.pid
|
||||
URL=https://demo-browser.lightpanda.io/campfire-commerce/ node puppeteer/proxy_auth.js
|
||||
BASE_URL=https://demo-browser.lightpanda.io/ node playwright/proxy_auth.js
|
||||
kill `cat LPD.pid` `cat PROXY.id`
|
||||
|
||||
cdp-and-hyperfine-bench:
|
||||
name: cdp-and-hyperfine-bench
|
||||
needs: zig-build-release
|
||||
|
||||
3
.github/workflows/wpt.yml
vendored
3
.github/workflows/wpt.yml
vendored
@@ -5,6 +5,7 @@ env:
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
|
||||
AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
|
||||
LIGHTPANDA_DISABLE_TELEMETRY: true
|
||||
|
||||
on:
|
||||
schedule:
|
||||
@@ -30,7 +31,7 @@ jobs:
|
||||
- uses: ./.github/actions/install
|
||||
|
||||
- name: json output
|
||||
run: zig build wpt -- --json > wpt.json
|
||||
run: zig build -Doptimize=ReleaseFast wpt -- --json > wpt.json
|
||||
|
||||
- name: write commit
|
||||
run: |
|
||||
|
||||
2
.github/workflows/zig-fmt.yml
vendored
2
.github/workflows/zig-fmt.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: zig-fmt
|
||||
|
||||
env:
|
||||
ZIG_VERSION: 0.14.1
|
||||
ZIG_VERSION: 0.15.1
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
14
.gitmodules
vendored
14
.gitmodules
vendored
@@ -9,7 +9,7 @@
|
||||
url = https://github.com/lightpanda-io/libdom.git/
|
||||
[submodule "vendor/netsurf/share/netsurf-buildsystem"]
|
||||
path = vendor/netsurf/share/netsurf-buildsystem
|
||||
url = https://source.netsurf-browser.org/buildsystem.git
|
||||
url = https://github.com/lightpanda-io/netsurf-buildsystem.git
|
||||
[submodule "vendor/netsurf/libhubbub"]
|
||||
path = vendor/netsurf/libhubbub
|
||||
url = https://github.com/lightpanda-io/libhubbub.git/
|
||||
@@ -19,3 +19,15 @@
|
||||
[submodule "vendor/mimalloc"]
|
||||
path = vendor/mimalloc
|
||||
url = https://github.com/microsoft/mimalloc.git/
|
||||
[submodule "vendor/nghttp2"]
|
||||
path = vendor/nghttp2
|
||||
url = https://github.com/nghttp2/nghttp2.git
|
||||
[submodule "vendor/mbedtls"]
|
||||
path = vendor/mbedtls
|
||||
url = https://github.com/Mbed-TLS/mbedtls.git
|
||||
[submodule "vendor/zlib"]
|
||||
path = vendor/zlib
|
||||
url = https://github.com/madler/zlib.git
|
||||
[submodule "vendor/curl"]
|
||||
path = vendor/curl
|
||||
url = https://github.com/curl/curl.git
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
FROM debian:stable
|
||||
|
||||
ARG MINISIG=0.12
|
||||
ARG ZIG=0.14.1
|
||||
ARG ZIG=0.15.1
|
||||
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
||||
ARG V8=13.6.233.8
|
||||
ARG ZIG_V8=v0.1.28
|
||||
ARG V8=14.0.365.4
|
||||
ARG ZIG_V8=v0.1.30
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
RUN apt-get update -yq && \
|
||||
|
||||
6
Makefile
6
Makefile
@@ -199,14 +199,16 @@ download-libiconv:
|
||||
ifeq ("$(wildcard vendor/libiconv/libiconv-1.17)","")
|
||||
@mkdir -p vendor/libiconv
|
||||
@cd vendor/libiconv && \
|
||||
curl https://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.17.tar.gz | tar -xvzf -
|
||||
curl -L https://github.com/lightpanda-io/libiconv/releases/download/1.17/libiconv-1.17.tar.gz | tar -xvzf -
|
||||
endif
|
||||
|
||||
install-libiconv: download-libiconv clean-libiconv
|
||||
build-libiconv: clean-libiconv
|
||||
@cd vendor/libiconv/libiconv-1.17 && \
|
||||
./configure --prefix=$(ICONV) --enable-static && \
|
||||
make && make install
|
||||
|
||||
install-libiconv: download-libiconv build-libiconv
|
||||
|
||||
clean-libiconv:
|
||||
ifneq ("$(wildcard vendor/libiconv/libiconv-1.17/Makefile)","")
|
||||
@cd vendor/libiconv/libiconv-1.17 && \
|
||||
|
||||
15
README.md
15
README.md
@@ -41,7 +41,7 @@ Due to the nature of Playwright, a script that works with the current version of
|
||||
|
||||
## Quick start
|
||||
|
||||
### Install
|
||||
### Install
|
||||
**Install from the nightly builds**
|
||||
|
||||
You can download the last binary from the [nightly
|
||||
@@ -71,9 +71,8 @@ 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
|
||||
docker run -d --name lightpanda -p 9222:9222 lightpanda/browser:nightly
|
||||
```
|
||||
|
||||
### Dump a URL
|
||||
@@ -141,8 +140,7 @@ You may still encounter errors or crashes. Please open an issue with specifics i
|
||||
|
||||
Here are the key features we have implemented:
|
||||
|
||||
- [x] HTTP loader
|
||||
- [x] HTTP loader
|
||||
- [x] HTTP loader (based on Libcurl)
|
||||
- [x] HTML parser and DOM tree (based on Netsurf libs)
|
||||
- [x] Javascript support (v8)
|
||||
- [x] DOM APIs
|
||||
@@ -155,8 +153,8 @@ Here are the key features we have implemented:
|
||||
- [x] Input form
|
||||
- [x] Cookies
|
||||
- [x] Custom HTTP headers
|
||||
- [ ] Proxy support
|
||||
- [ ] Network interception
|
||||
- [x] Proxy support
|
||||
- [x] 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.
|
||||
|
||||
@@ -166,11 +164,12 @@ You can also follow the progress of our Javascript support in our dedicated [zig
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Lightpanda is written with [Zig](https://ziglang.org/) `0.14.1`. You have to
|
||||
Lightpanda is written with [Zig](https://ziglang.org/) `0.15.1`. You have to
|
||||
install it with the right version in order to build the project.
|
||||
|
||||
Lightpanda also depends on
|
||||
[zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime/) (with v8),
|
||||
[Libcurl](https://curl.se/libcurl/),
|
||||
[Netsurf libs](https://www.netsurf-browser.org/) and
|
||||
[Mimalloc](https://microsoft.github.io/mimalloc).
|
||||
|
||||
|
||||
737
build.zig
737
build.zig
@@ -19,11 +19,13 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const Build = std.Build;
|
||||
|
||||
/// Do not rename this constant. It is scanned by some scripts to determine
|
||||
/// which zig version to install.
|
||||
const recommended_zig_version = "0.14.1";
|
||||
const recommended_zig_version = "0.15.1";
|
||||
|
||||
pub fn build(b: *std.Build) !void {
|
||||
pub fn build(b: *Build) !void {
|
||||
switch (comptime builtin.zig_version.order(std.SemanticVersion.parse(recommended_zig_version) catch unreachable)) {
|
||||
.eq => {},
|
||||
.lt => {
|
||||
@@ -47,6 +49,18 @@ pub fn build(b: *std.Build) !void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
// We're still using llvm because the new x86 backend seems to crash
|
||||
// with v8. This can be reproduced in zig-v8-fork.
|
||||
|
||||
const lightpanda_module = b.addModule("lightpanda", .{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.link_libc = true,
|
||||
.link_libcpp = true,
|
||||
});
|
||||
try addDependencies(b, lightpanda_module, opts);
|
||||
|
||||
{
|
||||
// browser
|
||||
// -------
|
||||
@@ -54,12 +68,9 @@ pub fn build(b: *std.Build) !void {
|
||||
// compile and install
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "lightpanda",
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.use_llvm = true,
|
||||
.root_module = lightpanda_module,
|
||||
});
|
||||
|
||||
try common(b, opts, exe);
|
||||
b.installArtifact(exe);
|
||||
|
||||
// run
|
||||
@@ -73,6 +84,54 @@ pub fn build(b: *std.Build) !void {
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
}
|
||||
|
||||
{
|
||||
// tests
|
||||
// ----
|
||||
|
||||
// compile
|
||||
const tests = b.addTest(.{
|
||||
.root_module = lightpanda_module,
|
||||
.use_llvm = true,
|
||||
.test_runner = .{ .path = b.path("src/test_runner.zig"), .mode = .simple },
|
||||
});
|
||||
|
||||
const run_tests = b.addRunArtifact(tests);
|
||||
if (b.args) |args| {
|
||||
run_tests.addArgs(args);
|
||||
}
|
||||
|
||||
// step
|
||||
const tests_step = b.step("test", "Run unit tests");
|
||||
tests_step.dependOn(&run_tests.step);
|
||||
}
|
||||
|
||||
{
|
||||
// wpt
|
||||
// -----
|
||||
const wpt_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main_wpt.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
try addDependencies(b, wpt_module, opts);
|
||||
|
||||
// compile and install
|
||||
const wpt = b.addExecutable(.{
|
||||
.name = "lightpanda-wpt",
|
||||
.use_llvm = true,
|
||||
.root_module = wpt_module,
|
||||
});
|
||||
|
||||
// run
|
||||
const wpt_cmd = b.addRunArtifact(wpt);
|
||||
if (b.args) |args| {
|
||||
wpt_cmd.addArgs(args);
|
||||
}
|
||||
// step
|
||||
const wpt_step = b.step("wpt", "WPT tests");
|
||||
wpt_step.dependOn(&wpt_cmd.step);
|
||||
}
|
||||
|
||||
{
|
||||
// get v8
|
||||
// -------
|
||||
@@ -90,63 +149,19 @@ pub fn build(b: *std.Build) !void {
|
||||
const build_step = b.step("build-v8", "Build v8");
|
||||
build_step.dependOn(&build_v8.step);
|
||||
}
|
||||
|
||||
{
|
||||
// tests
|
||||
// ----
|
||||
|
||||
// compile
|
||||
const tests = b.addTest(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.test_runner = .{ .path = b.path("src/test_runner.zig"), .mode = .simple },
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
try common(b, opts, tests);
|
||||
|
||||
const run_tests = b.addRunArtifact(tests);
|
||||
if (b.args) |args| {
|
||||
run_tests.addArgs(args);
|
||||
}
|
||||
|
||||
// step
|
||||
const tests_step = b.step("test", "Run unit tests");
|
||||
tests_step.dependOn(&run_tests.step);
|
||||
}
|
||||
|
||||
{
|
||||
// wpt
|
||||
// -----
|
||||
|
||||
// compile and install
|
||||
const wpt = b.addExecutable(.{
|
||||
.name = "lightpanda-wpt",
|
||||
.root_source_file = b.path("src/main_wpt.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
try common(b, opts, wpt);
|
||||
|
||||
// run
|
||||
const wpt_cmd = b.addRunArtifact(wpt);
|
||||
if (b.args) |args| {
|
||||
wpt_cmd.addArgs(args);
|
||||
}
|
||||
// step
|
||||
const wpt_step = b.step("wpt", "WPT tests");
|
||||
wpt_step.dependOn(&wpt_cmd.step);
|
||||
}
|
||||
}
|
||||
|
||||
fn common(b: *std.Build, opts: *std.Build.Step.Options, step: *std.Build.Step.Compile) !void {
|
||||
const mod = step.root_module;
|
||||
const target = mod.resolved_target.?;
|
||||
const optimize = mod.optimize.?;
|
||||
const dep_opts = .{ .target = target, .optimize = optimize };
|
||||
fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !void {
|
||||
try moduleNetSurf(b, mod);
|
||||
mod.addImport("build_config", opts.createModule());
|
||||
|
||||
try moduleNetSurf(b, step, target);
|
||||
mod.addImport("tls", b.dependency("tls", dep_opts).module("tls"));
|
||||
mod.addImport("tigerbeetle-io", b.dependency("tigerbeetle_io", .{}).module("tigerbeetle_io"));
|
||||
const target = mod.resolved_target.?;
|
||||
const dep_opts = .{
|
||||
.target = target,
|
||||
.optimize = mod.optimize.?,
|
||||
};
|
||||
|
||||
mod.addIncludePath(b.path("vendor/lightpanda"));
|
||||
|
||||
{
|
||||
// v8
|
||||
@@ -156,11 +171,7 @@ fn common(b: *std.Build, opts: *std.Build.Step.Options, step: *std.Build.Step.Co
|
||||
const v8_mod = b.dependency("v8", dep_opts).module("v8");
|
||||
v8_mod.addOptions("default_exports", v8_opts);
|
||||
mod.addImport("v8", v8_mod);
|
||||
}
|
||||
|
||||
mod.link_libcpp = true;
|
||||
|
||||
{
|
||||
const release_dir = if (mod.optimize.? == .Debug) "debug" else "release";
|
||||
const os = switch (target.result.os.tag) {
|
||||
.linux => "linux",
|
||||
@@ -181,21 +192,211 @@ fn common(b: *std.Build, opts: *std.Build.Step.Options, step: *std.Build.Step.Co
|
||||
);
|
||||
};
|
||||
mod.addObjectFile(mod.owner.path(lib_path));
|
||||
|
||||
switch (target.result.os.tag) {
|
||||
.macos => {
|
||||
// v8 has a dependency, abseil-cpp, which, on Mac, uses CoreFoundation
|
||||
mod.addSystemFrameworkPath(.{ .cwd_relative = "/System/Library/Frameworks" });
|
||||
mod.linkFramework("CoreFoundation", .{});
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
switch (target.result.os.tag) {
|
||||
.macos => {
|
||||
// v8 has a dependency, abseil-cpp, which, on Mac, uses CoreFoundation
|
||||
mod.addSystemFrameworkPath(.{ .cwd_relative = "/System/Library/Frameworks" });
|
||||
mod.linkFramework("CoreFoundation", .{});
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
{
|
||||
//curl
|
||||
{
|
||||
const is_linux = target.result.os.tag == .linux;
|
||||
if (is_linux) {
|
||||
mod.addCMacro("HAVE_LINUX_TCP_H", "1");
|
||||
mod.addCMacro("HAVE_MSG_NOSIGNAL", "1");
|
||||
mod.addCMacro("HAVE_GETHOSTBYNAME_R", "1");
|
||||
}
|
||||
mod.addCMacro("_FILE_OFFSET_BITS", "64");
|
||||
mod.addCMacro("BUILDING_LIBCURL", "1");
|
||||
mod.addCMacro("CURL_DISABLE_AWS", "1");
|
||||
mod.addCMacro("CURL_DISABLE_DICT", "1");
|
||||
mod.addCMacro("CURL_DISABLE_DOH", "1");
|
||||
mod.addCMacro("CURL_DISABLE_FILE", "1");
|
||||
mod.addCMacro("CURL_DISABLE_FTP", "1");
|
||||
mod.addCMacro("CURL_DISABLE_GOPHER", "1");
|
||||
mod.addCMacro("CURL_DISABLE_KERBEROS", "1");
|
||||
mod.addCMacro("CURL_DISABLE_IMAP", "1");
|
||||
mod.addCMacro("CURL_DISABLE_IPFS", "1");
|
||||
mod.addCMacro("CURL_DISABLE_LDAP", "1");
|
||||
mod.addCMacro("CURL_DISABLE_LDAPS", "1");
|
||||
mod.addCMacro("CURL_DISABLE_MQTT", "1");
|
||||
mod.addCMacro("CURL_DISABLE_NTLM", "1");
|
||||
mod.addCMacro("CURL_DISABLE_PROGRESS_METER", "1");
|
||||
mod.addCMacro("CURL_DISABLE_POP3", "1");
|
||||
mod.addCMacro("CURL_DISABLE_RTSP", "1");
|
||||
mod.addCMacro("CURL_DISABLE_SMB", "1");
|
||||
mod.addCMacro("CURL_DISABLE_SMTP", "1");
|
||||
mod.addCMacro("CURL_DISABLE_TELNET", "1");
|
||||
mod.addCMacro("CURL_DISABLE_TFTP", "1");
|
||||
mod.addCMacro("CURL_EXTERN_SYMBOL", "__attribute__ ((__visibility__ (\"default\"))");
|
||||
mod.addCMacro("CURL_OS", if (is_linux) "\"Linux\"" else "\"mac\"");
|
||||
mod.addCMacro("CURL_STATICLIB", "1");
|
||||
mod.addCMacro("ENABLE_IPV6", "1");
|
||||
mod.addCMacro("HAVE_ALARM", "1");
|
||||
mod.addCMacro("HAVE_ALLOCA_H", "1");
|
||||
mod.addCMacro("HAVE_ARPA_INET_H", "1");
|
||||
mod.addCMacro("HAVE_ARPA_TFTP_H", "1");
|
||||
mod.addCMacro("HAVE_ASSERT_H", "1");
|
||||
mod.addCMacro("HAVE_BASENAME", "1");
|
||||
mod.addCMacro("HAVE_BOOL_T", "1");
|
||||
mod.addCMacro("HAVE_BUILTIN_AVAILABLE", "1");
|
||||
mod.addCMacro("HAVE_CLOCK_GETTIME_MONOTONIC", "1");
|
||||
mod.addCMacro("HAVE_DLFCN_H", "1");
|
||||
mod.addCMacro("HAVE_ERRNO_H", "1");
|
||||
mod.addCMacro("HAVE_FCNTL", "1");
|
||||
mod.addCMacro("HAVE_FCNTL_H", "1");
|
||||
mod.addCMacro("HAVE_FCNTL_O_NONBLOCK", "1");
|
||||
mod.addCMacro("HAVE_FREEADDRINFO", "1");
|
||||
mod.addCMacro("HAVE_FSETXATTR", "1");
|
||||
mod.addCMacro("HAVE_FSETXATTR_5", "1");
|
||||
mod.addCMacro("HAVE_FTRUNCATE", "1");
|
||||
mod.addCMacro("HAVE_GETADDRINFO", "1");
|
||||
mod.addCMacro("HAVE_GETEUID", "1");
|
||||
mod.addCMacro("HAVE_GETHOSTBYNAME", "1");
|
||||
mod.addCMacro("HAVE_GETHOSTBYNAME_R_6", "1");
|
||||
mod.addCMacro("HAVE_GETHOSTNAME", "1");
|
||||
mod.addCMacro("HAVE_GETPEERNAME", "1");
|
||||
mod.addCMacro("HAVE_GETPPID", "1");
|
||||
mod.addCMacro("HAVE_GETPPID", "1");
|
||||
mod.addCMacro("HAVE_GETPROTOBYNAME", "1");
|
||||
mod.addCMacro("HAVE_GETPWUID", "1");
|
||||
mod.addCMacro("HAVE_GETPWUID_R", "1");
|
||||
mod.addCMacro("HAVE_GETRLIMIT", "1");
|
||||
mod.addCMacro("HAVE_GETSOCKNAME", "1");
|
||||
mod.addCMacro("HAVE_GETTIMEOFDAY", "1");
|
||||
mod.addCMacro("HAVE_GMTIME_R", "1");
|
||||
mod.addCMacro("HAVE_IDN2_H", "1");
|
||||
mod.addCMacro("HAVE_IF_NAMETOINDEX", "1");
|
||||
mod.addCMacro("HAVE_IFADDRS_H", "1");
|
||||
mod.addCMacro("HAVE_INET_ADDR", "1");
|
||||
mod.addCMacro("HAVE_INET_PTON", "1");
|
||||
mod.addCMacro("HAVE_INTTYPES_H", "1");
|
||||
mod.addCMacro("HAVE_IOCTL", "1");
|
||||
mod.addCMacro("HAVE_IOCTL_FIONBIO", "1");
|
||||
mod.addCMacro("HAVE_IOCTL_SIOCGIFADDR", "1");
|
||||
mod.addCMacro("HAVE_LDAP_URL_PARSE", "1");
|
||||
mod.addCMacro("HAVE_LIBGEN_H", "1");
|
||||
mod.addCMacro("HAVE_LIBZ", "1");
|
||||
mod.addCMacro("HAVE_LL", "1");
|
||||
mod.addCMacro("HAVE_LOCALE_H", "1");
|
||||
mod.addCMacro("HAVE_LOCALTIME_R", "1");
|
||||
mod.addCMacro("HAVE_LONGLONG", "1");
|
||||
mod.addCMacro("HAVE_MALLOC_H", "1");
|
||||
mod.addCMacro("HAVE_MEMORY_H", "1");
|
||||
mod.addCMacro("HAVE_NET_IF_H", "1");
|
||||
mod.addCMacro("HAVE_NETDB_H", "1");
|
||||
mod.addCMacro("HAVE_NETINET_IN_H", "1");
|
||||
mod.addCMacro("HAVE_NETINET_TCP_H", "1");
|
||||
mod.addCMacro("HAVE_PIPE", "1");
|
||||
mod.addCMacro("HAVE_POLL", "1");
|
||||
mod.addCMacro("HAVE_POLL_FINE", "1");
|
||||
mod.addCMacro("HAVE_POLL_H", "1");
|
||||
mod.addCMacro("HAVE_POSIX_STRERROR_R", "1");
|
||||
mod.addCMacro("HAVE_PTHREAD_H", "1");
|
||||
mod.addCMacro("HAVE_PWD_H", "1");
|
||||
mod.addCMacro("HAVE_RECV", "1");
|
||||
mod.addCMacro("HAVE_SA_FAMILY_T", "1");
|
||||
mod.addCMacro("HAVE_SELECT", "1");
|
||||
mod.addCMacro("HAVE_SEND", "1");
|
||||
mod.addCMacro("HAVE_SETJMP_H", "1");
|
||||
mod.addCMacro("HAVE_SETLOCALE", "1");
|
||||
mod.addCMacro("HAVE_SETRLIMIT", "1");
|
||||
mod.addCMacro("HAVE_SETSOCKOPT", "1");
|
||||
mod.addCMacro("HAVE_SIGACTION", "1");
|
||||
mod.addCMacro("HAVE_SIGINTERRUPT", "1");
|
||||
mod.addCMacro("HAVE_SIGNAL", "1");
|
||||
mod.addCMacro("HAVE_SIGNAL_H", "1");
|
||||
mod.addCMacro("HAVE_SIGSETJMP", "1");
|
||||
mod.addCMacro("HAVE_SOCKADDR_IN6_SIN6_SCOPE_ID", "1");
|
||||
mod.addCMacro("HAVE_SOCKET", "1");
|
||||
mod.addCMacro("HAVE_STDBOOL_H", "1");
|
||||
mod.addCMacro("HAVE_STDINT_H", "1");
|
||||
mod.addCMacro("HAVE_STDIO_H", "1");
|
||||
mod.addCMacro("HAVE_STDLIB_H", "1");
|
||||
mod.addCMacro("HAVE_STRCASECMP", "1");
|
||||
mod.addCMacro("HAVE_STRDUP", "1");
|
||||
mod.addCMacro("HAVE_STRERROR_R", "1");
|
||||
mod.addCMacro("HAVE_STRING_H", "1");
|
||||
mod.addCMacro("HAVE_STRINGS_H", "1");
|
||||
mod.addCMacro("HAVE_STRSTR", "1");
|
||||
mod.addCMacro("HAVE_STRTOK_R", "1");
|
||||
mod.addCMacro("HAVE_STRTOLL", "1");
|
||||
mod.addCMacro("HAVE_STRUCT_SOCKADDR_STORAGE", "1");
|
||||
mod.addCMacro("HAVE_STRUCT_TIMEVAL", "1");
|
||||
mod.addCMacro("HAVE_SYS_IOCTL_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_PARAM_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_POLL_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_RESOURCE_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_SELECT_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_SOCKET_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_STAT_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_TIME_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_TYPES_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_UIO_H", "1");
|
||||
mod.addCMacro("HAVE_SYS_UN_H", "1");
|
||||
mod.addCMacro("HAVE_TERMIO_H", "1");
|
||||
mod.addCMacro("HAVE_TERMIOS_H", "1");
|
||||
mod.addCMacro("HAVE_TIME_H", "1");
|
||||
mod.addCMacro("HAVE_UNAME", "1");
|
||||
mod.addCMacro("HAVE_UNISTD_H", "1");
|
||||
mod.addCMacro("HAVE_UTIME", "1");
|
||||
mod.addCMacro("HAVE_UTIME_H", "1");
|
||||
mod.addCMacro("HAVE_UTIMES", "1");
|
||||
mod.addCMacro("HAVE_VARIADIC_MACROS_C99", "1");
|
||||
mod.addCMacro("HAVE_VARIADIC_MACROS_GCC", "1");
|
||||
mod.addCMacro("HAVE_ZLIB_H", "1");
|
||||
mod.addCMacro("RANDOM_FILE", "\"/dev/urandom\"");
|
||||
mod.addCMacro("RECV_TYPE_ARG1", "int");
|
||||
mod.addCMacro("RECV_TYPE_ARG2", "void *");
|
||||
mod.addCMacro("RECV_TYPE_ARG3", "size_t");
|
||||
mod.addCMacro("RECV_TYPE_ARG4", "int");
|
||||
mod.addCMacro("RECV_TYPE_RETV", "ssize_t");
|
||||
mod.addCMacro("SEND_QUAL_ARG2", "const");
|
||||
mod.addCMacro("SEND_TYPE_ARG1", "int");
|
||||
mod.addCMacro("SEND_TYPE_ARG2", "void *");
|
||||
mod.addCMacro("SEND_TYPE_ARG3", "size_t");
|
||||
mod.addCMacro("SEND_TYPE_ARG4", "int");
|
||||
mod.addCMacro("SEND_TYPE_RETV", "ssize_t");
|
||||
mod.addCMacro("SIZEOF_CURL_OFF_T", "8");
|
||||
mod.addCMacro("SIZEOF_INT", "4");
|
||||
mod.addCMacro("SIZEOF_LONG", "8");
|
||||
mod.addCMacro("SIZEOF_OFF_T", "8");
|
||||
mod.addCMacro("SIZEOF_SHORT", "2");
|
||||
mod.addCMacro("SIZEOF_SIZE_T", "8");
|
||||
mod.addCMacro("SIZEOF_TIME_T", "8");
|
||||
mod.addCMacro("STDC_HEADERS", "1");
|
||||
mod.addCMacro("TIME_WITH_SYS_TIME", "1");
|
||||
mod.addCMacro("USE_NGHTTP2", "1");
|
||||
mod.addCMacro("USE_MBEDTLS", "1");
|
||||
mod.addCMacro("USE_THREADS_POSIX", "1");
|
||||
mod.addCMacro("USE_UNIX_SOCKETS", "1");
|
||||
}
|
||||
|
||||
mod.addImport("build_config", opts.createModule());
|
||||
try buildZlib(b, mod);
|
||||
try buildMbedtls(b, mod);
|
||||
try buildNghttp2(b, mod);
|
||||
try buildCurl(b, mod);
|
||||
|
||||
switch (target.result.os.tag) {
|
||||
.macos => {
|
||||
// needed for proxying on mac
|
||||
mod.addSystemFrameworkPath(.{ .cwd_relative = "/System/Library/Frameworks" });
|
||||
mod.linkFramework("CoreFoundation", .{});
|
||||
mod.linkFramework("SystemConfiguration", .{});
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn moduleNetSurf(b: *std.Build, step: *std.Build.Step.Compile, target: std.Build.ResolvedTarget) !void {
|
||||
fn moduleNetSurf(b: *Build, mod: *Build.Module) !void {
|
||||
const target = mod.resolved_target.?;
|
||||
const os = target.result.os.tag;
|
||||
const arch = target.result.cpu.arch;
|
||||
|
||||
@@ -210,8 +411,8 @@ fn moduleNetSurf(b: *std.Build, step: *std.Build.Step.Compile, target: std.Build
|
||||
"vendor/libiconv/out/{s}-{s}/lib/libiconv.a",
|
||||
.{ @tagName(os), @tagName(arch) },
|
||||
);
|
||||
step.addObjectFile(b.path(libiconv_lib_path));
|
||||
step.addIncludePath(b.path(libiconv_include_path));
|
||||
mod.addObjectFile(b.path(libiconv_lib_path));
|
||||
mod.addIncludePath(b.path(libiconv_include_path));
|
||||
|
||||
{
|
||||
// mimalloc
|
||||
@@ -221,8 +422,8 @@ fn moduleNetSurf(b: *std.Build, step: *std.Build.Step.Compile, target: std.Build
|
||||
mimalloc ++ "/out/{s}-{s}/lib/libmimalloc.a",
|
||||
.{ @tagName(os), @tagName(arch) },
|
||||
);
|
||||
step.addObjectFile(b.path(lib_path));
|
||||
step.addIncludePath(b.path(mimalloc ++ "/include"));
|
||||
mod.addObjectFile(b.path(lib_path));
|
||||
mod.addIncludePath(b.path(mimalloc ++ "/include"));
|
||||
}
|
||||
|
||||
// netsurf libs
|
||||
@@ -232,7 +433,7 @@ fn moduleNetSurf(b: *std.Build, step: *std.Build.Step.Compile, target: std.Build
|
||||
ns ++ "/out/{s}-{s}/include",
|
||||
.{ @tagName(os), @tagName(arch) },
|
||||
);
|
||||
step.addIncludePath(b.path(ns_include_path));
|
||||
mod.addIncludePath(b.path(ns_include_path));
|
||||
|
||||
const libs: [4][]const u8 = .{
|
||||
"libdom",
|
||||
@@ -246,7 +447,379 @@ fn moduleNetSurf(b: *std.Build, step: *std.Build.Step.Compile, target: std.Build
|
||||
ns ++ "/out/{s}-{s}/lib/" ++ lib ++ ".a",
|
||||
.{ @tagName(os), @tagName(arch) },
|
||||
);
|
||||
step.addObjectFile(b.path(ns_lib_path));
|
||||
step.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src"));
|
||||
mod.addObjectFile(b.path(ns_lib_path));
|
||||
mod.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src"));
|
||||
}
|
||||
}
|
||||
|
||||
fn buildZlib(b: *Build, m: *Build.Module) !void {
|
||||
const zlib = b.addLibrary(.{
|
||||
.name = "zlib",
|
||||
.root_module = m,
|
||||
});
|
||||
|
||||
const root = "vendor/zlib/";
|
||||
zlib.installHeader(b.path(root ++ "zlib.h"), "zlib.h");
|
||||
zlib.installHeader(b.path(root ++ "zconf.h"), "zconf.h");
|
||||
zlib.addCSourceFiles(.{ .flags = &.{
|
||||
"-DHAVE_SYS_TYPES_H",
|
||||
"-DHAVE_STDINT_H",
|
||||
"-DHAVE_STDDEF_H",
|
||||
}, .files = &.{
|
||||
root ++ "adler32.c",
|
||||
root ++ "compress.c",
|
||||
root ++ "crc32.c",
|
||||
root ++ "deflate.c",
|
||||
root ++ "gzclose.c",
|
||||
root ++ "gzlib.c",
|
||||
root ++ "gzread.c",
|
||||
root ++ "gzwrite.c",
|
||||
root ++ "inflate.c",
|
||||
root ++ "infback.c",
|
||||
root ++ "inftrees.c",
|
||||
root ++ "inffast.c",
|
||||
root ++ "trees.c",
|
||||
root ++ "uncompr.c",
|
||||
root ++ "zutil.c",
|
||||
} });
|
||||
}
|
||||
|
||||
fn buildMbedtls(b: *Build, m: *Build.Module) !void {
|
||||
const mbedtls = b.addLibrary(.{
|
||||
.name = "mbedtls",
|
||||
.root_module = m,
|
||||
});
|
||||
|
||||
const root = "vendor/mbedtls/";
|
||||
mbedtls.addIncludePath(b.path(root ++ "include"));
|
||||
mbedtls.addIncludePath(b.path(root ++ "library"));
|
||||
|
||||
mbedtls.addCSourceFiles(.{ .flags = &.{}, .files = &.{
|
||||
root ++ "library/aes.c",
|
||||
root ++ "library/aesni.c",
|
||||
root ++ "library/aesce.c",
|
||||
root ++ "library/aria.c",
|
||||
root ++ "library/asn1parse.c",
|
||||
root ++ "library/asn1write.c",
|
||||
root ++ "library/base64.c",
|
||||
root ++ "library/bignum.c",
|
||||
root ++ "library/bignum_core.c",
|
||||
root ++ "library/bignum_mod.c",
|
||||
root ++ "library/bignum_mod_raw.c",
|
||||
root ++ "library/camellia.c",
|
||||
root ++ "library/ccm.c",
|
||||
root ++ "library/chacha20.c",
|
||||
root ++ "library/chachapoly.c",
|
||||
root ++ "library/cipher.c",
|
||||
root ++ "library/cipher_wrap.c",
|
||||
root ++ "library/constant_time.c",
|
||||
root ++ "library/cmac.c",
|
||||
root ++ "library/ctr_drbg.c",
|
||||
root ++ "library/des.c",
|
||||
root ++ "library/dhm.c",
|
||||
root ++ "library/ecdh.c",
|
||||
root ++ "library/ecdsa.c",
|
||||
root ++ "library/ecjpake.c",
|
||||
root ++ "library/ecp.c",
|
||||
root ++ "library/ecp_curves.c",
|
||||
root ++ "library/entropy.c",
|
||||
root ++ "library/entropy_poll.c",
|
||||
root ++ "library/error.c",
|
||||
root ++ "library/gcm.c",
|
||||
root ++ "library/hkdf.c",
|
||||
root ++ "library/hmac_drbg.c",
|
||||
root ++ "library/lmots.c",
|
||||
root ++ "library/lms.c",
|
||||
root ++ "library/md.c",
|
||||
root ++ "library/md5.c",
|
||||
root ++ "library/memory_buffer_alloc.c",
|
||||
root ++ "library/nist_kw.c",
|
||||
root ++ "library/oid.c",
|
||||
root ++ "library/padlock.c",
|
||||
root ++ "library/pem.c",
|
||||
root ++ "library/pk.c",
|
||||
root ++ "library/pk_ecc.c",
|
||||
root ++ "library/pk_wrap.c",
|
||||
root ++ "library/pkcs12.c",
|
||||
root ++ "library/pkcs5.c",
|
||||
root ++ "library/pkparse.c",
|
||||
root ++ "library/pkwrite.c",
|
||||
root ++ "library/platform.c",
|
||||
root ++ "library/platform_util.c",
|
||||
root ++ "library/poly1305.c",
|
||||
root ++ "library/psa_crypto.c",
|
||||
root ++ "library/psa_crypto_aead.c",
|
||||
root ++ "library/psa_crypto_cipher.c",
|
||||
root ++ "library/psa_crypto_client.c",
|
||||
root ++ "library/psa_crypto_ffdh.c",
|
||||
root ++ "library/psa_crypto_driver_wrappers_no_static.c",
|
||||
root ++ "library/psa_crypto_ecp.c",
|
||||
root ++ "library/psa_crypto_hash.c",
|
||||
root ++ "library/psa_crypto_mac.c",
|
||||
root ++ "library/psa_crypto_pake.c",
|
||||
root ++ "library/psa_crypto_rsa.c",
|
||||
root ++ "library/psa_crypto_se.c",
|
||||
root ++ "library/psa_crypto_slot_management.c",
|
||||
root ++ "library/psa_crypto_storage.c",
|
||||
root ++ "library/psa_its_file.c",
|
||||
root ++ "library/psa_util.c",
|
||||
root ++ "library/ripemd160.c",
|
||||
root ++ "library/rsa.c",
|
||||
root ++ "library/rsa_alt_helpers.c",
|
||||
root ++ "library/sha1.c",
|
||||
root ++ "library/sha3.c",
|
||||
root ++ "library/sha256.c",
|
||||
root ++ "library/sha512.c",
|
||||
root ++ "library/threading.c",
|
||||
root ++ "library/timing.c",
|
||||
root ++ "library/version.c",
|
||||
root ++ "library/version_features.c",
|
||||
root ++ "library/pkcs7.c",
|
||||
root ++ "library/x509.c",
|
||||
root ++ "library/x509_create.c",
|
||||
root ++ "library/x509_crl.c",
|
||||
root ++ "library/x509_crt.c",
|
||||
root ++ "library/x509_csr.c",
|
||||
root ++ "library/x509write.c",
|
||||
root ++ "library/x509write_crt.c",
|
||||
root ++ "library/x509write_csr.c",
|
||||
root ++ "library/debug.c",
|
||||
root ++ "library/mps_reader.c",
|
||||
root ++ "library/mps_trace.c",
|
||||
root ++ "library/net_sockets.c",
|
||||
root ++ "library/ssl_cache.c",
|
||||
root ++ "library/ssl_ciphersuites.c",
|
||||
root ++ "library/ssl_client.c",
|
||||
root ++ "library/ssl_cookie.c",
|
||||
root ++ "library/ssl_debug_helpers_generated.c",
|
||||
root ++ "library/ssl_msg.c",
|
||||
root ++ "library/ssl_ticket.c",
|
||||
root ++ "library/ssl_tls.c",
|
||||
root ++ "library/ssl_tls12_client.c",
|
||||
root ++ "library/ssl_tls12_server.c",
|
||||
root ++ "library/ssl_tls13_keys.c",
|
||||
root ++ "library/ssl_tls13_server.c",
|
||||
root ++ "library/ssl_tls13_client.c",
|
||||
root ++ "library/ssl_tls13_generic.c",
|
||||
} });
|
||||
}
|
||||
|
||||
fn buildNghttp2(b: *Build, m: *Build.Module) !void {
|
||||
const nghttp2 = b.addLibrary(.{
|
||||
.name = "nghttp2",
|
||||
.root_module = m,
|
||||
});
|
||||
|
||||
const root = "vendor/nghttp2/";
|
||||
nghttp2.addIncludePath(b.path(root ++ "lib"));
|
||||
nghttp2.addIncludePath(b.path(root ++ "lib/includes"));
|
||||
nghttp2.addCSourceFiles(.{ .flags = &.{
|
||||
"-DNGHTTP2_STATICLIB",
|
||||
"-DHAVE_NETINET_IN",
|
||||
"-DHAVE_TIME_H",
|
||||
}, .files = &.{
|
||||
root ++ "lib/sfparse.c",
|
||||
root ++ "lib/nghttp2_alpn.c",
|
||||
root ++ "lib/nghttp2_buf.c",
|
||||
root ++ "lib/nghttp2_callbacks.c",
|
||||
root ++ "lib/nghttp2_debug.c",
|
||||
root ++ "lib/nghttp2_extpri.c",
|
||||
root ++ "lib/nghttp2_frame.c",
|
||||
root ++ "lib/nghttp2_hd.c",
|
||||
root ++ "lib/nghttp2_hd_huffman.c",
|
||||
root ++ "lib/nghttp2_hd_huffman_data.c",
|
||||
root ++ "lib/nghttp2_helper.c",
|
||||
root ++ "lib/nghttp2_http.c",
|
||||
root ++ "lib/nghttp2_map.c",
|
||||
root ++ "lib/nghttp2_mem.c",
|
||||
root ++ "lib/nghttp2_option.c",
|
||||
root ++ "lib/nghttp2_outbound_item.c",
|
||||
root ++ "lib/nghttp2_pq.c",
|
||||
root ++ "lib/nghttp2_priority_spec.c",
|
||||
root ++ "lib/nghttp2_queue.c",
|
||||
root ++ "lib/nghttp2_rcbuf.c",
|
||||
root ++ "lib/nghttp2_session.c",
|
||||
root ++ "lib/nghttp2_stream.c",
|
||||
root ++ "lib/nghttp2_submit.c",
|
||||
root ++ "lib/nghttp2_version.c",
|
||||
root ++ "lib/nghttp2_ratelim.c",
|
||||
root ++ "lib/nghttp2_time.c",
|
||||
} });
|
||||
}
|
||||
|
||||
fn buildCurl(b: *Build, m: *Build.Module) !void {
|
||||
const curl = b.addLibrary(.{
|
||||
.name = "curl",
|
||||
.root_module = m,
|
||||
});
|
||||
|
||||
const root = "vendor/curl/";
|
||||
|
||||
curl.addIncludePath(b.path(root ++ "lib"));
|
||||
curl.addIncludePath(b.path(root ++ "include"));
|
||||
curl.addCSourceFiles(.{
|
||||
.flags = &.{},
|
||||
.files = &.{
|
||||
root ++ "lib/altsvc.c",
|
||||
root ++ "lib/amigaos.c",
|
||||
root ++ "lib/asyn-ares.c",
|
||||
root ++ "lib/asyn-base.c",
|
||||
root ++ "lib/asyn-thrdd.c",
|
||||
root ++ "lib/bufq.c",
|
||||
root ++ "lib/bufref.c",
|
||||
root ++ "lib/cf-h1-proxy.c",
|
||||
root ++ "lib/cf-h2-proxy.c",
|
||||
root ++ "lib/cf-haproxy.c",
|
||||
root ++ "lib/cf-https-connect.c",
|
||||
root ++ "lib/cf-socket.c",
|
||||
root ++ "lib/cfilters.c",
|
||||
root ++ "lib/conncache.c",
|
||||
root ++ "lib/connect.c",
|
||||
root ++ "lib/content_encoding.c",
|
||||
root ++ "lib/cookie.c",
|
||||
root ++ "lib/cshutdn.c",
|
||||
root ++ "lib/curl_addrinfo.c",
|
||||
root ++ "lib/curl_des.c",
|
||||
root ++ "lib/curl_endian.c",
|
||||
root ++ "lib/curl_fnmatch.c",
|
||||
root ++ "lib/curl_get_line.c",
|
||||
root ++ "lib/curl_gethostname.c",
|
||||
root ++ "lib/curl_gssapi.c",
|
||||
root ++ "lib/curl_memrchr.c",
|
||||
root ++ "lib/curl_ntlm_core.c",
|
||||
root ++ "lib/curl_range.c",
|
||||
root ++ "lib/curl_rtmp.c",
|
||||
root ++ "lib/curl_sasl.c",
|
||||
root ++ "lib/curl_sha512_256.c",
|
||||
root ++ "lib/curl_sspi.c",
|
||||
root ++ "lib/curl_threads.c",
|
||||
root ++ "lib/curl_trc.c",
|
||||
root ++ "lib/cw-out.c",
|
||||
root ++ "lib/cw-pause.c",
|
||||
root ++ "lib/dict.c",
|
||||
root ++ "lib/doh.c",
|
||||
root ++ "lib/dynhds.c",
|
||||
root ++ "lib/easy.c",
|
||||
root ++ "lib/easygetopt.c",
|
||||
root ++ "lib/easyoptions.c",
|
||||
root ++ "lib/escape.c",
|
||||
root ++ "lib/fake_addrinfo.c",
|
||||
root ++ "lib/file.c",
|
||||
root ++ "lib/fileinfo.c",
|
||||
root ++ "lib/fopen.c",
|
||||
root ++ "lib/formdata.c",
|
||||
root ++ "lib/ftp.c",
|
||||
root ++ "lib/ftplistparser.c",
|
||||
root ++ "lib/getenv.c",
|
||||
root ++ "lib/getinfo.c",
|
||||
root ++ "lib/gopher.c",
|
||||
root ++ "lib/hash.c",
|
||||
root ++ "lib/headers.c",
|
||||
root ++ "lib/hmac.c",
|
||||
root ++ "lib/hostip.c",
|
||||
root ++ "lib/hostip4.c",
|
||||
root ++ "lib/hostip6.c",
|
||||
root ++ "lib/hsts.c",
|
||||
root ++ "lib/http.c",
|
||||
root ++ "lib/http1.c",
|
||||
root ++ "lib/http2.c",
|
||||
root ++ "lib/http_aws_sigv4.c",
|
||||
root ++ "lib/http_chunks.c",
|
||||
root ++ "lib/http_digest.c",
|
||||
root ++ "lib/http_negotiate.c",
|
||||
root ++ "lib/http_ntlm.c",
|
||||
root ++ "lib/http_proxy.c",
|
||||
root ++ "lib/httpsrr.c",
|
||||
root ++ "lib/idn.c",
|
||||
root ++ "lib/if2ip.c",
|
||||
root ++ "lib/imap.c",
|
||||
root ++ "lib/krb5.c",
|
||||
root ++ "lib/ldap.c",
|
||||
root ++ "lib/llist.c",
|
||||
root ++ "lib/macos.c",
|
||||
root ++ "lib/md4.c",
|
||||
root ++ "lib/md5.c",
|
||||
root ++ "lib/memdebug.c",
|
||||
root ++ "lib/mime.c",
|
||||
root ++ "lib/mprintf.c",
|
||||
root ++ "lib/mqtt.c",
|
||||
root ++ "lib/multi.c",
|
||||
root ++ "lib/multi_ev.c",
|
||||
root ++ "lib/netrc.c",
|
||||
root ++ "lib/noproxy.c",
|
||||
root ++ "lib/openldap.c",
|
||||
root ++ "lib/parsedate.c",
|
||||
root ++ "lib/pingpong.c",
|
||||
root ++ "lib/pop3.c",
|
||||
root ++ "lib/progress.c",
|
||||
root ++ "lib/psl.c",
|
||||
root ++ "lib/rand.c",
|
||||
root ++ "lib/rename.c",
|
||||
root ++ "lib/request.c",
|
||||
root ++ "lib/rtsp.c",
|
||||
root ++ "lib/select.c",
|
||||
root ++ "lib/sendf.c",
|
||||
root ++ "lib/setopt.c",
|
||||
root ++ "lib/sha256.c",
|
||||
root ++ "lib/share.c",
|
||||
root ++ "lib/slist.c",
|
||||
root ++ "lib/smb.c",
|
||||
root ++ "lib/smtp.c",
|
||||
root ++ "lib/socketpair.c",
|
||||
root ++ "lib/socks.c",
|
||||
root ++ "lib/socks_gssapi.c",
|
||||
root ++ "lib/socks_sspi.c",
|
||||
root ++ "lib/speedcheck.c",
|
||||
root ++ "lib/splay.c",
|
||||
root ++ "lib/strcase.c",
|
||||
root ++ "lib/strdup.c",
|
||||
root ++ "lib/strequal.c",
|
||||
root ++ "lib/strerror.c",
|
||||
root ++ "lib/system_win32.c",
|
||||
root ++ "lib/telnet.c",
|
||||
root ++ "lib/tftp.c",
|
||||
root ++ "lib/transfer.c",
|
||||
root ++ "lib/uint-bset.c",
|
||||
root ++ "lib/uint-hash.c",
|
||||
root ++ "lib/uint-spbset.c",
|
||||
root ++ "lib/uint-table.c",
|
||||
root ++ "lib/url.c",
|
||||
root ++ "lib/urlapi.c",
|
||||
root ++ "lib/version.c",
|
||||
root ++ "lib/ws.c",
|
||||
root ++ "lib/curlx/base64.c",
|
||||
root ++ "lib/curlx/dynbuf.c",
|
||||
root ++ "lib/curlx/inet_ntop.c",
|
||||
root ++ "lib/curlx/nonblock.c",
|
||||
root ++ "lib/curlx/strparse.c",
|
||||
root ++ "lib/curlx/timediff.c",
|
||||
root ++ "lib/curlx/timeval.c",
|
||||
root ++ "lib/curlx/wait.c",
|
||||
root ++ "lib/curlx/warnless.c",
|
||||
root ++ "lib/vquic/curl_ngtcp2.c",
|
||||
root ++ "lib/vquic/curl_osslq.c",
|
||||
root ++ "lib/vquic/curl_quiche.c",
|
||||
root ++ "lib/vquic/vquic.c",
|
||||
root ++ "lib/vquic/vquic-tls.c",
|
||||
root ++ "lib/vauth/cleartext.c",
|
||||
root ++ "lib/vauth/cram.c",
|
||||
root ++ "lib/vauth/digest.c",
|
||||
root ++ "lib/vauth/digest_sspi.c",
|
||||
root ++ "lib/vauth/gsasl.c",
|
||||
root ++ "lib/vauth/krb5_gssapi.c",
|
||||
root ++ "lib/vauth/krb5_sspi.c",
|
||||
root ++ "lib/vauth/ntlm.c",
|
||||
root ++ "lib/vauth/ntlm_sspi.c",
|
||||
root ++ "lib/vauth/oauth2.c",
|
||||
root ++ "lib/vauth/spnego_gssapi.c",
|
||||
root ++ "lib/vauth/spnego_sspi.c",
|
||||
root ++ "lib/vauth/vauth.c",
|
||||
root ++ "lib/vtls/cipher_suite.c",
|
||||
root ++ "lib/vtls/mbedtls.c",
|
||||
root ++ "lib/vtls/mbedtls_threadlock.c",
|
||||
root ++ "lib/vtls/vtls.c",
|
||||
root ++ "lib/vtls/vtls_scache.c",
|
||||
root ++ "lib/vtls/x509asn1.c",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,19 +4,10 @@
|
||||
.version = "0.0.0",
|
||||
.fingerprint = 0xda130f3af836cea0,
|
||||
.dependencies = .{
|
||||
.tls = .{
|
||||
.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/b22911e02e4884a76acf52aa9aff2ba169d05b40.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH69zCAwAzm1u5cQVa-uG5ib2y6PpENXCl8yEYdUYk",
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/7177ee1ae267a44751a0e7e012e257177699a375.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH63TCAwC1D1hEiOtbEnLBbtz9ZPHrdiGWLcBcYQB7",
|
||||
},
|
||||
//.v8 = .{ .path = "../zig-v8-fork" },
|
||||
//.tigerbeetle_io = .{ .path = "../tigerbeetle-io" },
|
||||
// .v8 = .{ .path = "../zig-v8-fork" }
|
||||
},
|
||||
}
|
||||
|
||||
127
flake.lock
generated
127
flake.lock
generated
@@ -1,5 +1,21 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
@@ -18,13 +34,52 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1705309234,
|
||||
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"zlsPkg",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709087332,
|
||||
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1748964450,
|
||||
"narHash": "sha256-ZouDiXkUk8mkMnah10QcoQ9Nu6UW6AFAHLScS3En6aI=",
|
||||
"lastModified": 1756822655,
|
||||
"narHash": "sha256-xQAk8xLy7srAkR5NMZFsQFioL02iTHuuEIs3ohGpgdk=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9ff500cd9e123f46c55855eca64beccead29b152",
|
||||
"rev": "4bdac60bfe32c41103ae500ddf894c258291dd61",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -37,7 +92,9 @@
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
"nixpkgs": "nixpkgs",
|
||||
"zigPkgs": "zigPkgs",
|
||||
"zlsPkg": "zlsPkg"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
@@ -54,6 +111,68 @@
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"zigPkgs": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1756555914,
|
||||
"narHash": "sha256-7yoSPIVEuL+3Wzf6e7NHuW3zmruHizRrYhGerjRHTLI=",
|
||||
"owner": "mitchellh",
|
||||
"repo": "zig-overlay",
|
||||
"rev": "d0df3a2fd0f11134409d6d5ea0e510e5e477f7d6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "mitchellh",
|
||||
"repo": "zig-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"zlsPkg": {
|
||||
"inputs": {
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"zig-overlay": [
|
||||
"zigPkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1756048867,
|
||||
"narHash": "sha256-GFzSHUljcxy7sM1PaabbkQUdUnLwpherekPWJFxXtnk=",
|
||||
"owner": "zigtools",
|
||||
"repo": "zls",
|
||||
"rev": "ce6c8f02c78e622421cfc2405c67c5222819ec03",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "zigtools",
|
||||
"ref": "0.15.0",
|
||||
"repo": "zls",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
||||
22
flake.nix
22
flake.nix
@@ -3,20 +3,37 @@
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/release-25.05";
|
||||
|
||||
zigPkgs.url = "github:mitchellh/zig-overlay";
|
||||
zigPkgs.inputs.nixpkgs.follows = "nixpkgs";
|
||||
|
||||
zlsPkg.url = "github:zigtools/zls/0.15.0";
|
||||
zlsPkg.inputs.zig-overlay.follows = "zigPkgs";
|
||||
zlsPkg.inputs.nixpkgs.follows = "nixpkgs";
|
||||
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
nixpkgs,
|
||||
zigPkgs,
|
||||
zlsPkg,
|
||||
flake-utils,
|
||||
...
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
overlays = [
|
||||
(final: prev: {
|
||||
zigpkgs = zigPkgs.packages.${prev.system};
|
||||
zls = zlsPkg.packages.${prev.system}.default;
|
||||
})
|
||||
];
|
||||
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
inherit system overlays;
|
||||
};
|
||||
|
||||
# We need crtbeginS.o for building.
|
||||
@@ -32,7 +49,7 @@
|
||||
targetPkgs =
|
||||
pkgs: with pkgs; [
|
||||
# Build Tools
|
||||
zig
|
||||
zigpkgs."0.15.1"
|
||||
zls
|
||||
python3
|
||||
pkg-config
|
||||
@@ -49,6 +66,7 @@
|
||||
glib.dev
|
||||
glibc.dev
|
||||
zlib
|
||||
zlib.dev
|
||||
];
|
||||
};
|
||||
in
|
||||
|
||||
118
src/TestHTTPServer.zig
Normal file
118
src/TestHTTPServer.zig
Normal file
@@ -0,0 +1,118 @@
|
||||
const std = @import("std");
|
||||
|
||||
const TestHTTPServer = @This();
|
||||
|
||||
shutdown: bool,
|
||||
listener: ?std.net.Server,
|
||||
handler: Handler,
|
||||
|
||||
const Handler = *const fn (req: *std.http.Server.Request) anyerror!void;
|
||||
|
||||
pub fn init(handler: Handler) TestHTTPServer {
|
||||
return .{
|
||||
.shutdown = true,
|
||||
.listener = null,
|
||||
.handler = handler,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *TestHTTPServer) void {
|
||||
self.shutdown = true;
|
||||
if (self.listener) |*listener| {
|
||||
listener.deinit();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(self: *TestHTTPServer, wg: *std.Thread.WaitGroup) !void {
|
||||
const address = try std.net.Address.parseIp("127.0.0.1", 9582);
|
||||
|
||||
self.listener = try address.listen(.{ .reuse_address = true });
|
||||
var listener = &self.listener.?;
|
||||
|
||||
wg.finish();
|
||||
|
||||
while (true) {
|
||||
const conn = listener.accept() catch |err| {
|
||||
if (self.shutdown) {
|
||||
return;
|
||||
}
|
||||
return err;
|
||||
};
|
||||
const thrd = try std.Thread.spawn(.{}, handleConnection, .{ self, conn });
|
||||
thrd.detach();
|
||||
}
|
||||
}
|
||||
|
||||
fn handleConnection(self: *TestHTTPServer, conn: std.net.Server.Connection) !void {
|
||||
defer conn.stream.close();
|
||||
|
||||
var req_buf: [2048]u8 = undefined;
|
||||
var conn_reader = conn.stream.reader(&req_buf);
|
||||
var conn_writer = conn.stream.writer(&req_buf);
|
||||
|
||||
var http_server = std.http.Server.init(conn_reader.interface(), &conn_writer.interface);
|
||||
|
||||
while (true) {
|
||||
var req = http_server.receiveHead() catch |err| switch (err) {
|
||||
error.ReadFailed => continue,
|
||||
error.HttpConnectionClosing => continue,
|
||||
else => {
|
||||
std.debug.print("Test HTTP Server error: {}\n", .{err});
|
||||
return err;
|
||||
},
|
||||
};
|
||||
self.handler(&req) catch |err| {
|
||||
std.debug.print("test http error '{s}': {}\n", .{ req.head.target, err });
|
||||
try req.respond("server error", .{ .status = .internal_server_error });
|
||||
return;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sendFile(req: *std.http.Server.Request, file_path: []const u8) !void {
|
||||
var file = std.fs.cwd().openFile(file_path, .{}) catch |err| switch (err) {
|
||||
error.FileNotFound => return req.respond("server error", .{ .status = .not_found }),
|
||||
else => return err,
|
||||
};
|
||||
defer file.close();
|
||||
|
||||
const stat = try file.stat();
|
||||
var send_buffer: [4096]u8 = undefined;
|
||||
|
||||
var res = try req.respondStreaming(&send_buffer, .{
|
||||
.content_length = stat.size,
|
||||
.respond_options = .{
|
||||
.extra_headers = &.{
|
||||
.{ .name = "content-type", .value = getContentType(file_path) },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
var read_buffer: [4096]u8 = undefined;
|
||||
var reader = file.reader(&read_buffer);
|
||||
_ = try res.writer.sendFileAll(&reader, .unlimited);
|
||||
try res.writer.flush();
|
||||
try res.end();
|
||||
}
|
||||
|
||||
fn getContentType(file_path: []const u8) []const u8 {
|
||||
if (std.mem.endsWith(u8, file_path, ".js")) {
|
||||
return "application/json";
|
||||
}
|
||||
|
||||
if (std.mem.endsWith(u8, file_path, ".html")) {
|
||||
return "text/html";
|
||||
}
|
||||
|
||||
if (std.mem.endsWith(u8, file_path, ".htm")) {
|
||||
return "text/html";
|
||||
}
|
||||
|
||||
if (std.mem.endsWith(u8, file_path, ".xml")) {
|
||||
// some wpt tests do this
|
||||
return "text/xml";
|
||||
}
|
||||
|
||||
std.debug.print("TestHTTPServer asked to serve an unknown file type: {s}\n", .{file_path});
|
||||
return "text/html";
|
||||
}
|
||||
60
src/app.zig
60
src/app.zig
@@ -1,9 +1,9 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const log = @import("log.zig");
|
||||
const Loop = @import("runtime/loop.zig").Loop;
|
||||
const http = @import("http/client.zig");
|
||||
const Http = @import("http/Http.zig");
|
||||
const Platform = @import("runtime/js.zig").Platform;
|
||||
|
||||
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
|
||||
@@ -12,12 +12,11 @@ const Notification = @import("notification.zig").Notification;
|
||||
// Container for global state / objects that various parts of the system
|
||||
// might need.
|
||||
pub const App = struct {
|
||||
loop: *Loop,
|
||||
http: Http,
|
||||
config: Config,
|
||||
platform: ?*const Platform,
|
||||
platform: Platform,
|
||||
allocator: Allocator,
|
||||
telemetry: Telemetry,
|
||||
http_client: http.Client,
|
||||
app_dir_path: ?[]const u8,
|
||||
notification: *Notification,
|
||||
|
||||
@@ -30,45 +29,51 @@ 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,
|
||||
http_proxy: ?[:0]const u8 = null,
|
||||
proxy_bearer_token: ?[:0]const u8 = null,
|
||||
http_timeout_ms: ?u31 = null,
|
||||
http_connect_timeout_ms: ?u31 = null,
|
||||
http_max_host_open: ?u8 = null,
|
||||
http_max_concurrent: ?u8 = null,
|
||||
};
|
||||
|
||||
pub fn init(allocator: Allocator, config: Config) !*App {
|
||||
const app = try allocator.create(App);
|
||||
errdefer allocator.destroy(app);
|
||||
|
||||
const loop = try allocator.create(Loop);
|
||||
errdefer allocator.destroy(loop);
|
||||
|
||||
loop.* = try Loop.init(allocator);
|
||||
errdefer loop.deinit();
|
||||
|
||||
const notification = try Notification.init(allocator, null);
|
||||
errdefer notification.deinit();
|
||||
|
||||
var http = try Http.init(allocator, .{
|
||||
.max_host_open = config.http_max_host_open orelse 4,
|
||||
.max_concurrent = config.http_max_concurrent orelse 10,
|
||||
.timeout_ms = config.http_timeout_ms orelse 5000,
|
||||
.connect_timeout_ms = config.http_connect_timeout_ms orelse 0,
|
||||
.http_proxy = config.http_proxy,
|
||||
.tls_verify_host = config.tls_verify_host,
|
||||
.proxy_bearer_token = config.proxy_bearer_token,
|
||||
});
|
||||
errdefer http.deinit();
|
||||
|
||||
const platform = try Platform.init();
|
||||
errdefer platform.deinit();
|
||||
|
||||
const app_dir_path = getAndMakeAppDir(allocator);
|
||||
|
||||
app.* = .{
|
||||
.loop = loop,
|
||||
.http = http,
|
||||
.allocator = allocator,
|
||||
.telemetry = undefined,
|
||||
.platform = config.platform,
|
||||
.platform = platform,
|
||||
.app_dir_path = app_dir_path,
|
||||
.notification = notification,
|
||||
.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,
|
||||
};
|
||||
app.telemetry = Telemetry.init(app, config.run_mode);
|
||||
|
||||
app.telemetry = try Telemetry.init(app, config.run_mode);
|
||||
errdefer app.telemetry.deinit();
|
||||
|
||||
try app.telemetry.register(app.notification);
|
||||
|
||||
return app;
|
||||
@@ -80,10 +85,9 @@ pub const App = struct {
|
||||
allocator.free(app_dir_path);
|
||||
}
|
||||
self.telemetry.deinit();
|
||||
self.loop.deinit();
|
||||
allocator.destroy(self.loop);
|
||||
self.http_client.deinit();
|
||||
self.notification.deinit();
|
||||
self.http.deinit();
|
||||
self.platform.deinit();
|
||||
allocator.destroy(self);
|
||||
}
|
||||
};
|
||||
|
||||
52
src/browser/DataURI.zig
Normal file
52
src/browser/DataURI.zig
Normal file
@@ -0,0 +1,52 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
// Parses data:[<media-type>][;base64],<data>
|
||||
pub fn parse(allocator: Allocator, src: []const u8) !?[]const u8 {
|
||||
if (!std.mem.startsWith(u8, src, "data:")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uri = src[5..];
|
||||
const data_starts = std.mem.indexOfScalar(u8, uri, ',') orelse return null;
|
||||
|
||||
var data = uri[data_starts + 1 ..];
|
||||
|
||||
// Extract the encoding.
|
||||
const metadata = uri[0..data_starts];
|
||||
if (std.mem.endsWith(u8, metadata, ";base64")) {
|
||||
const decoder = std.base64.standard.Decoder;
|
||||
const decoded_size = try decoder.calcSizeForSlice(data);
|
||||
|
||||
const buffer = try allocator.alloc(u8, decoded_size);
|
||||
errdefer allocator.free(buffer);
|
||||
|
||||
try decoder.decode(buffer, data);
|
||||
data = buffer;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
test "DataURI: parse valid" {
|
||||
try test_valid("data:text/javascript; charset=utf-8;base64,Zm9v", "foo");
|
||||
try test_valid("data:text/javascript; charset=utf-8;,foo", "foo");
|
||||
try test_valid("data:,foo", "foo");
|
||||
}
|
||||
|
||||
test "DataURI: parse invalid" {
|
||||
try test_cannot_parse("atad:,foo");
|
||||
try test_cannot_parse("data:foo");
|
||||
try test_cannot_parse("data:");
|
||||
}
|
||||
|
||||
fn test_valid(uri: []const u8, expected: []const u8) !void {
|
||||
defer testing.reset();
|
||||
const data_uri = try parse(testing.arena_allocator, uri) orelse return error.TestFailed;
|
||||
try testing.expectEqual(expected, data_uri);
|
||||
}
|
||||
|
||||
fn test_cannot_parse(uri: []const u8) !void {
|
||||
try testing.expectEqual(null, parse(undefined, uri));
|
||||
}
|
||||
162
src/browser/Scheduler.zig
Normal file
162
src/browser/Scheduler.zig
Normal file
@@ -0,0 +1,162 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const log = @import("../log.zig");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Scheduler = @This();
|
||||
|
||||
high_priority: Queue,
|
||||
|
||||
// For repeating tasks. We only want to run these if there are other things to
|
||||
// do. We don't, for example, want a window.setInterval or the page.runMicrotasks
|
||||
// to block the page.wait.
|
||||
low_priority: Queue,
|
||||
|
||||
// we expect allocator to be the page arena, hence we never call high_priority.deinit
|
||||
pub fn init(allocator: Allocator) Scheduler {
|
||||
return .{
|
||||
.high_priority = Queue.init(allocator, {}),
|
||||
.low_priority = Queue.init(allocator, {}),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn reset(self: *Scheduler) void {
|
||||
self.high_priority.clearRetainingCapacity();
|
||||
self.low_priority.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
const AddOpts = struct {
|
||||
name: []const u8 = "",
|
||||
low_priority: bool = false,
|
||||
};
|
||||
pub fn add(self: *Scheduler, ctx: *anyopaque, func: Task.Func, ms: u32, opts: AddOpts) !void {
|
||||
var low_priority = opts.low_priority;
|
||||
if (ms > 5_000) {
|
||||
// we don't want tasks in the far future to block page.wait from
|
||||
// completing. However, if page.wait is called multiple times (maybe
|
||||
// a CDP driver is wait for something to happen), then we do want
|
||||
// to [eventually] run these when their time is up.
|
||||
low_priority = true;
|
||||
}
|
||||
|
||||
var q = if (low_priority) &self.low_priority else &self.high_priority;
|
||||
return q.add(.{
|
||||
.ms = std.time.milliTimestamp() + ms,
|
||||
.ctx = ctx,
|
||||
.func = func,
|
||||
.name = opts.name,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn run(self: *Scheduler) !?i32 {
|
||||
_ = try self.runQueue(&self.low_priority);
|
||||
return self.runQueue(&self.high_priority);
|
||||
}
|
||||
|
||||
fn runQueue(self: *Scheduler, queue: *Queue) !?i32 {
|
||||
// this is O(1)
|
||||
if (queue.count() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = std.time.milliTimestamp();
|
||||
|
||||
var next = queue.peek();
|
||||
while (next) |task| {
|
||||
const time_to_next = task.ms - now;
|
||||
if (time_to_next > 0) {
|
||||
// @intCast is petty safe since we limit tasks to just 5 seconds
|
||||
// in the future
|
||||
return @intCast(time_to_next);
|
||||
}
|
||||
|
||||
if (task.func(task.ctx)) |repeat_delay| {
|
||||
// if we do (now + 0) then our WHILE loop will run endlessly.
|
||||
// no task should ever return 0
|
||||
std.debug.assert(repeat_delay != 0);
|
||||
|
||||
var copy = task;
|
||||
copy.ms = now + repeat_delay;
|
||||
try self.low_priority.add(copy);
|
||||
}
|
||||
_ = queue.remove();
|
||||
next = queue.peek();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const Task = struct {
|
||||
ms: i64,
|
||||
func: Func,
|
||||
ctx: *anyopaque,
|
||||
name: []const u8,
|
||||
|
||||
const Func = *const fn (ctx: *anyopaque) ?u32;
|
||||
};
|
||||
|
||||
const Queue = std.PriorityQueue(Task, void, struct {
|
||||
fn compare(_: void, a: Task, b: Task) std.math.Order {
|
||||
return std.math.order(a.ms, b.ms);
|
||||
}
|
||||
}.compare);
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
test "Scheduler" {
|
||||
defer testing.reset();
|
||||
|
||||
var task = TestTask{ .allocator = testing.arena_allocator };
|
||||
|
||||
var s = Scheduler.init(testing.arena_allocator);
|
||||
try testing.expectEqual(null, s.run());
|
||||
try testing.expectEqual(0, task.calls.items.len);
|
||||
|
||||
try s.add(&task, TestTask.run1, 3, .{});
|
||||
|
||||
try testing.expectDelta(3, try s.run(), 1);
|
||||
try testing.expectEqual(0, task.calls.items.len);
|
||||
|
||||
std.Thread.sleep(std.time.ns_per_ms * 5);
|
||||
try testing.expectEqual(null, s.run());
|
||||
try testing.expectEqualSlices(u32, &.{1}, task.calls.items);
|
||||
|
||||
try s.add(&task, TestTask.run2, 3, .{});
|
||||
try s.add(&task, TestTask.run1, 2, .{});
|
||||
|
||||
std.Thread.sleep(std.time.ns_per_ms * 5);
|
||||
try testing.expectDelta(null, try s.run(), 1);
|
||||
try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items);
|
||||
}
|
||||
|
||||
const TestTask = struct {
|
||||
allocator: Allocator,
|
||||
calls: std.ArrayListUnmanaged(u32) = .{},
|
||||
|
||||
fn run1(ctx: *anyopaque) ?u32 {
|
||||
var self: *TestTask = @ptrCast(@alignCast(ctx));
|
||||
self.calls.append(self.allocator, 1) catch unreachable;
|
||||
return null;
|
||||
}
|
||||
|
||||
fn run2(ctx: *anyopaque) ?u32 {
|
||||
var self: *TestTask = @ptrCast(@alignCast(ctx));
|
||||
self.calls.append(self.allocator, 2) catch unreachable;
|
||||
return 2;
|
||||
}
|
||||
};
|
||||
832
src/browser/ScriptManager.zig
Normal file
832
src/browser/ScriptManager.zig
Normal file
@@ -0,0 +1,832 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const log = @import("../log.zig");
|
||||
const parser = @import("netsurf.zig");
|
||||
|
||||
const Env = @import("env.zig").Env;
|
||||
const Page = @import("page.zig").Page;
|
||||
const DataURI = @import("DataURI.zig");
|
||||
const Http = @import("../http/Http.zig");
|
||||
const Browser = @import("browser.zig").Browser;
|
||||
const URL = @import("../url.zig").URL;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArrayListUnmanaged = std.ArrayListUnmanaged;
|
||||
|
||||
const ScriptManager = @This();
|
||||
|
||||
page: *Page,
|
||||
|
||||
// used to prevent recursive evalutaion
|
||||
is_evaluating: bool,
|
||||
|
||||
// used to prevent executing scripts while we're doing a blocking load
|
||||
is_blocking: bool = false,
|
||||
|
||||
// Only once this is true can deferred scripts be run
|
||||
static_scripts_done: bool,
|
||||
|
||||
// List of async scripts. We don't care about the execution order of these, but
|
||||
// on shutdown/abort, we need to cleanup any pending ones.
|
||||
asyncs: OrderList,
|
||||
|
||||
// When an async script is ready to be evaluated, it's moved from asyncs to
|
||||
// this list. You might think we can evaluate an async script as soon as it's
|
||||
// done, but we can only evaluate scripts when `is_blocking == false`. So this
|
||||
// becomes a list of scripts to execute on the next evaluate().
|
||||
asyncs_ready: OrderList,
|
||||
|
||||
// Normal scripts (non-deferred & non-async). These must be executed in order
|
||||
scripts: OrderList,
|
||||
|
||||
// List of deferred scripts. These must be executed in order, but only once
|
||||
// dom_loaded == true,
|
||||
deferreds: OrderList,
|
||||
|
||||
shutdown: bool = false,
|
||||
|
||||
client: *Http.Client,
|
||||
allocator: Allocator,
|
||||
buffer_pool: BufferPool,
|
||||
script_pool: std.heap.MemoryPool(PendingScript),
|
||||
|
||||
const OrderList = std.DoublyLinkedList;
|
||||
|
||||
pub fn init(browser: *Browser, page: *Page) ScriptManager {
|
||||
// page isn't fully initialized, we can setup our reference, but that's it.
|
||||
const allocator = browser.allocator;
|
||||
return .{
|
||||
.page = page,
|
||||
.asyncs = .{},
|
||||
.scripts = .{},
|
||||
.deferreds = .{},
|
||||
.asyncs_ready = .{},
|
||||
.is_evaluating = false,
|
||||
.allocator = allocator,
|
||||
.client = browser.http_client,
|
||||
.static_scripts_done = false,
|
||||
.buffer_pool = BufferPool.init(allocator, 5),
|
||||
.script_pool = std.heap.MemoryPool(PendingScript).init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *ScriptManager) void {
|
||||
self.reset();
|
||||
self.buffer_pool.deinit();
|
||||
self.script_pool.deinit();
|
||||
}
|
||||
|
||||
pub fn reset(self: *ScriptManager) void {
|
||||
self.clearList(&self.asyncs);
|
||||
self.clearList(&self.scripts);
|
||||
self.clearList(&self.deferreds);
|
||||
self.clearList(&self.asyncs_ready);
|
||||
self.static_scripts_done = false;
|
||||
}
|
||||
|
||||
fn clearList(_: *const ScriptManager, list: *OrderList) void {
|
||||
while (list.first) |node| {
|
||||
const pending_script: *PendingScript = @fieldParentPtr("node", node);
|
||||
// this removes it from the list
|
||||
pending_script.deinit();
|
||||
}
|
||||
std.debug.assert(list.first == null);
|
||||
}
|
||||
|
||||
pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
|
||||
if (try parser.elementGetAttribute(element, "nomodule") != null) {
|
||||
// these scripts should only be loaded if we don't support modules
|
||||
// but since we do support modules, we can just skip them.
|
||||
return;
|
||||
}
|
||||
|
||||
// If a script tag gets dynamically created and added to the dom:
|
||||
// document.getElementsByTagName('head')[0].appendChild(script)
|
||||
// that script tag will immediately get executed by our scriptAddedCallback.
|
||||
// However, if the location where the script tag is inserted happens to be
|
||||
// below where processHTMLDoc currently is, then we'll re-run that same script
|
||||
// again in processHTMLDoc. This flag is used to let us know if a specific
|
||||
// <script> has already been processed.
|
||||
if (try parser.scriptGetProcessed(@ptrCast(element))) {
|
||||
return;
|
||||
}
|
||||
try parser.scriptSetProcessed(@ptrCast(element), true);
|
||||
|
||||
const kind: Script.Kind = blk: {
|
||||
const script_type = try parser.elementGetAttribute(element, "type") orelse break :blk .javascript;
|
||||
if (script_type.len == 0) {
|
||||
break :blk .javascript;
|
||||
}
|
||||
if (std.ascii.eqlIgnoreCase(script_type, "application/javascript")) {
|
||||
break :blk .javascript;
|
||||
}
|
||||
if (std.ascii.eqlIgnoreCase(script_type, "text/javascript")) {
|
||||
break :blk .javascript;
|
||||
}
|
||||
if (std.ascii.eqlIgnoreCase(script_type, "module")) {
|
||||
break :blk .module;
|
||||
}
|
||||
|
||||
// "type" could be anything, but only the above are ones we need to process.
|
||||
// Common other ones are application/json, application/ld+json, text/template
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
const page = self.page;
|
||||
var source: Script.Source = undefined;
|
||||
var remote_url: ?[:0]const u8 = null;
|
||||
if (try parser.elementGetAttribute(element, "src")) |src| {
|
||||
if (try DataURI.parse(page.arena, src)) |data_uri| {
|
||||
source = .{ .@"inline" = data_uri };
|
||||
}
|
||||
remote_url = try URL.stitch(page.arena, src, page.url.raw, .{ .null_terminated = true });
|
||||
source = .{ .remote = .{} };
|
||||
} else {
|
||||
const inline_source = try parser.nodeTextContent(@ptrCast(element)) orelse return;
|
||||
source = .{ .@"inline" = inline_source };
|
||||
}
|
||||
|
||||
var script = Script{
|
||||
.kind = kind,
|
||||
.element = element,
|
||||
.source = source,
|
||||
.url = remote_url orelse page.url.raw,
|
||||
.is_defer = if (remote_url == null) false else try parser.elementGetAttribute(element, "defer") != null,
|
||||
.is_async = if (remote_url == null) false else try parser.elementGetAttribute(element, "async") != null,
|
||||
};
|
||||
|
||||
if (source == .@"inline" and self.scripts.first == null) {
|
||||
// inline script with no pending scripts, execute it immediately.
|
||||
// (if there is a pending script, then we cannot execute this immediately
|
||||
// as it needs to best executed in order)
|
||||
return script.eval(page);
|
||||
}
|
||||
|
||||
const pending_script = try self.script_pool.create();
|
||||
errdefer self.script_pool.destroy(pending_script);
|
||||
pending_script.* = .{
|
||||
.script = script,
|
||||
.complete = false,
|
||||
.manager = self,
|
||||
.node = .{},
|
||||
};
|
||||
|
||||
if (source == .@"inline") {
|
||||
// if we're here, it means that we have pending scripts (i.e. self.scripts
|
||||
// is not empty). Because the script is inline, it's complete/ready, but
|
||||
// we need to process them in order
|
||||
pending_script.complete = true;
|
||||
self.scripts.append(&pending_script.node);
|
||||
return;
|
||||
} else {
|
||||
log.debug(.http, "script queue", .{ .url = remote_url.? });
|
||||
}
|
||||
|
||||
pending_script.getList().append(&pending_script.node);
|
||||
|
||||
errdefer pending_script.deinit();
|
||||
|
||||
var headers = try Http.Headers.init();
|
||||
try page.requestCookie(.{}).headersForRequest(page.arena, remote_url.?, &headers);
|
||||
|
||||
try self.client.request(.{
|
||||
.url = remote_url.?,
|
||||
.ctx = pending_script,
|
||||
.method = .GET,
|
||||
.headers = headers,
|
||||
.cookie_jar = page.cookie_jar,
|
||||
.resource_type = .script,
|
||||
.start_callback = if (log.enabled(.http, .debug)) startCallback else null,
|
||||
.header_callback = headerCallback,
|
||||
.data_callback = dataCallback,
|
||||
.done_callback = doneCallback,
|
||||
.error_callback = errorCallback,
|
||||
});
|
||||
}
|
||||
|
||||
// @TODO: Improving this would have the simplest biggest performance improvement
|
||||
// for most sites.
|
||||
//
|
||||
// For JS imports (both static and dynamic), we currently block to get the
|
||||
// result (the content of the file).
|
||||
//
|
||||
// For static imports, this is necessary, since v8 is expecting the compiled module
|
||||
// as part of the function return. (we should try to pre-load the JavaScript
|
||||
// source via module.GetModuleRequests(), but that's for a later time).
|
||||
//
|
||||
// For dynamic dynamic imports, this is not strictly necessary since the v8
|
||||
// call returns a Promise; we could make this a normal get call, associated with
|
||||
// the promise, and when done, resolve the promise.
|
||||
//
|
||||
// In both cases, for now at least, we just issue a "blocking" request. We block
|
||||
// by ticking the http client until the script is complete.
|
||||
//
|
||||
// This uses the client.blockingRequest call which has a dedicated handle for
|
||||
// these blocking requests. Because they are blocking, we're guaranteed to have
|
||||
// only 1 at a time, thus the 1 reserved handle.
|
||||
//
|
||||
// You almost don't need the http client's blocking handle. In most cases, you
|
||||
// should always have 1 free handle whenever you get here, because we always
|
||||
// release the handle before executing the doneCallback. So, if a module does:
|
||||
// import * as x from 'blah'
|
||||
// And we need to load 'blah', there should always be 1 free handle - the handle
|
||||
// of the http GET we just completed before executing the module.
|
||||
// The exception to this, and the reason we need a special blocking handle, is
|
||||
// for inline modules within the HTML page itself:
|
||||
// <script type=module>import ....</script>
|
||||
// Unlike external modules which can only ever be executed after releasing an
|
||||
// http handle, these are executed without there necessarily being a free handle.
|
||||
// Thus, Http/Client.zig maintains a dedicated handle for these calls.
|
||||
pub fn blockingGet(self: *ScriptManager, url: [:0]const u8) !BlockingResult {
|
||||
std.debug.assert(self.is_blocking == false);
|
||||
|
||||
self.is_blocking = true;
|
||||
defer {
|
||||
self.is_blocking = false;
|
||||
|
||||
// we blocked evaluation while loading this script, there could be
|
||||
// scripts ready to process.
|
||||
self.evaluate();
|
||||
}
|
||||
|
||||
var blocking = Blocking{
|
||||
.allocator = self.allocator,
|
||||
.buffer_pool = &self.buffer_pool,
|
||||
};
|
||||
|
||||
var headers = try Http.Headers.init();
|
||||
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
|
||||
|
||||
var client = self.client;
|
||||
try client.blockingRequest(.{
|
||||
.url = url,
|
||||
.method = .GET,
|
||||
.headers = headers,
|
||||
.cookie_jar = self.page.cookie_jar,
|
||||
.ctx = &blocking,
|
||||
.resource_type = .script,
|
||||
.start_callback = if (log.enabled(.http, .debug)) Blocking.startCallback else null,
|
||||
.header_callback = Blocking.headerCallback,
|
||||
.data_callback = Blocking.dataCallback,
|
||||
.done_callback = Blocking.doneCallback,
|
||||
.error_callback = Blocking.errorCallback,
|
||||
});
|
||||
|
||||
// rely on http's timeout settings to avoid an endless/long loop.
|
||||
while (true) {
|
||||
_ = try client.tick(200);
|
||||
switch (blocking.state) {
|
||||
.running => {},
|
||||
.done => |result| return result,
|
||||
.err => |err| return err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn staticScriptsDone(self: *ScriptManager) void {
|
||||
std.debug.assert(self.static_scripts_done == false);
|
||||
self.static_scripts_done = true;
|
||||
}
|
||||
|
||||
// try to evaluate completed scripts (in order). This is called whenever a script
|
||||
// is completed.
|
||||
fn evaluate(self: *ScriptManager) void {
|
||||
if (self.is_evaluating) {
|
||||
// It's possible for a script.eval to cause evaluate to be called again.
|
||||
// This is particularly true with blockingGet, but even without this,
|
||||
// it's theoretically possible (but unlikely). We could make this work
|
||||
// but there's little reason to support the complexity.
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.is_blocking) {
|
||||
// Cannot evaluate scripts while a blocking-load is in progress. Not
|
||||
// only could that result in incorrect evaluation order, it could
|
||||
// trigger another blocking request, while we're doing a blocking request.
|
||||
return;
|
||||
}
|
||||
|
||||
const page = self.page;
|
||||
self.is_evaluating = true;
|
||||
defer self.is_evaluating = false;
|
||||
|
||||
// every script in asyncs_ready is ready to be evaluated.
|
||||
while (self.asyncs_ready.first) |n| {
|
||||
var pending_script: *PendingScript = @fieldParentPtr("node", n);
|
||||
defer pending_script.deinit();
|
||||
pending_script.script.eval(page);
|
||||
}
|
||||
|
||||
while (self.scripts.first) |n| {
|
||||
var pending_script: *PendingScript = @fieldParentPtr("node", n);
|
||||
if (pending_script.complete == false) {
|
||||
return;
|
||||
}
|
||||
defer pending_script.deinit();
|
||||
pending_script.script.eval(page);
|
||||
}
|
||||
|
||||
if (self.static_scripts_done == false) {
|
||||
// We can only execute deferred scripts if
|
||||
// 1 - all the normal scripts are done
|
||||
// 2 - we've finished parsing the HTML and at least queued all the scripts
|
||||
// The last one isn't obvious, but it's possible for self.scripts to
|
||||
// be empty not because we're done executing all the normal scripts
|
||||
// but because we're done executing some (or maybe none), but we're still
|
||||
// parsing the HTML.
|
||||
return;
|
||||
}
|
||||
|
||||
while (self.deferreds.first) |n| {
|
||||
var pending_script: *PendingScript = @fieldParentPtr("node", n);
|
||||
if (pending_script.complete == false) {
|
||||
return;
|
||||
}
|
||||
defer pending_script.deinit();
|
||||
pending_script.script.eval(page);
|
||||
}
|
||||
|
||||
// When all scripts (normal and deferred) are done loading, the document
|
||||
// state changes (this ultimately triggers the DOMContentLoaded event)
|
||||
page.documentIsLoaded();
|
||||
|
||||
if (self.asyncs.first == null) {
|
||||
// 1 - there are no async scripts pending
|
||||
// 2 - we checkecked static_scripts_done == true above
|
||||
// 3 - we drained self.scripts above
|
||||
// 4 - we drained self.deferred above
|
||||
page.documentIsComplete();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn isDone(self: *const ScriptManager) bool {
|
||||
return self.asyncs.first == null and // there are no more async scripts
|
||||
self.static_scripts_done and // and we've finished parsing the HTML to queue all <scripts>
|
||||
self.scripts.first == null and // and there are no more <script src=> to wait for
|
||||
self.deferreds.first == null; // and there are no more <script defer src=> to wait for
|
||||
}
|
||||
|
||||
fn startCallback(transfer: *Http.Transfer) !void {
|
||||
const script: *PendingScript = @ptrCast(@alignCast(transfer.ctx));
|
||||
script.startCallback(transfer) catch |err| {
|
||||
log.err(.http, "SM.startCallback", .{ .err = err, .transfer = transfer });
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
fn headerCallback(transfer: *Http.Transfer) !void {
|
||||
const script: *PendingScript = @ptrCast(@alignCast(transfer.ctx));
|
||||
try script.headerCallback(transfer);
|
||||
}
|
||||
|
||||
fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void {
|
||||
const script: *PendingScript = @ptrCast(@alignCast(transfer.ctx));
|
||||
script.dataCallback(transfer, data) catch |err| {
|
||||
log.err(.http, "SM.dataCallback", .{ .err = err, .transfer = transfer, .len = data.len });
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
fn doneCallback(ctx: *anyopaque) !void {
|
||||
const script: *PendingScript = @ptrCast(@alignCast(ctx));
|
||||
script.doneCallback();
|
||||
}
|
||||
|
||||
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||
const script: *PendingScript = @ptrCast(@alignCast(ctx));
|
||||
script.errorCallback(err);
|
||||
}
|
||||
|
||||
// A script which is pending execution.
|
||||
// It could be pending because:
|
||||
// (a) we're still downloading its content or
|
||||
// (b) this is a non-async script that has to be executed in order
|
||||
pub const PendingScript = struct {
|
||||
script: Script,
|
||||
complete: bool,
|
||||
node: OrderList.Node,
|
||||
manager: *ScriptManager,
|
||||
|
||||
fn deinit(self: *PendingScript) void {
|
||||
const script = &self.script;
|
||||
const manager = self.manager;
|
||||
|
||||
if (script.source == .remote) {
|
||||
manager.buffer_pool.release(script.source.remote);
|
||||
}
|
||||
self.getList().remove(&self.node);
|
||||
}
|
||||
|
||||
fn remove(self: *PendingScript) void {
|
||||
if (self.node) |*node| {
|
||||
self.getList().remove(node);
|
||||
self.node = null;
|
||||
}
|
||||
}
|
||||
|
||||
fn startCallback(self: *PendingScript, transfer: *Http.Transfer) !void {
|
||||
_ = self;
|
||||
log.debug(.http, "script fetch start", .{ .req = transfer });
|
||||
}
|
||||
|
||||
fn headerCallback(self: *PendingScript, transfer: *Http.Transfer) !void {
|
||||
const header = &transfer.response_header.?;
|
||||
if (header.status != 200) {
|
||||
log.info(.http, "script header", .{
|
||||
.req = transfer,
|
||||
.status = header.status,
|
||||
.content_type = header.contentType(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug(.http, "script header", .{
|
||||
.req = transfer,
|
||||
.status = header.status,
|
||||
.content_type = header.contentType(),
|
||||
});
|
||||
|
||||
// If this isn't true, then we'll likely leak memory. If you don't
|
||||
// set `CURLOPT_SUPPRESS_CONNECT_HEADERS` and CONNECT to a proxy, this
|
||||
// will fail. This assertion exists to catch incorrect assumptions about
|
||||
// how libcurl works, or about how we've configured it.
|
||||
std.debug.assert(self.script.source.remote.capacity == 0);
|
||||
var buffer = self.manager.buffer_pool.get();
|
||||
if (transfer.getContentLength()) |cl| {
|
||||
try buffer.ensureTotalCapacity(self.manager.allocator, cl);
|
||||
}
|
||||
self.script.source = .{ .remote = buffer };
|
||||
}
|
||||
|
||||
fn dataCallback(self: *PendingScript, transfer: *Http.Transfer, data: []const u8) !void {
|
||||
_ = transfer;
|
||||
// too verbose
|
||||
// log.debug(.http, "script data chunk", .{
|
||||
// .req = transfer,
|
||||
// .len = data.len,
|
||||
// });
|
||||
|
||||
try self.script.source.remote.appendSlice(self.manager.allocator, data);
|
||||
}
|
||||
|
||||
fn doneCallback(self: *PendingScript) void {
|
||||
log.debug(.http, "script fetch complete", .{ .req = self.script.url });
|
||||
|
||||
const manager = self.manager;
|
||||
self.complete = true;
|
||||
if (self.script.is_async) {
|
||||
manager.asyncs.remove(&self.node);
|
||||
manager.asyncs_ready.append(&self.node);
|
||||
}
|
||||
manager.evaluate();
|
||||
}
|
||||
|
||||
fn errorCallback(self: *PendingScript, err: anyerror) void {
|
||||
log.warn(.http, "script fetch error", .{ .req = self.script.url, .err = err });
|
||||
|
||||
const manager = self.manager;
|
||||
|
||||
self.deinit();
|
||||
|
||||
if (manager.shutdown) {
|
||||
return;
|
||||
}
|
||||
|
||||
manager.evaluate();
|
||||
}
|
||||
|
||||
fn getList(self: *const PendingScript) *OrderList {
|
||||
// When a script has both the async and defer flag set, it should be
|
||||
// treated as async. Async is newer, so some websites use both so that
|
||||
// if async isn't known, it'll fallback to defer.
|
||||
|
||||
const script = &self.script;
|
||||
if (script.is_async) {
|
||||
return if (self.complete) &self.manager.asyncs_ready else &self.manager.asyncs;
|
||||
}
|
||||
|
||||
if (script.is_defer) {
|
||||
return &self.manager.deferreds;
|
||||
}
|
||||
|
||||
return &self.manager.scripts;
|
||||
}
|
||||
};
|
||||
|
||||
const Script = struct {
|
||||
kind: Kind,
|
||||
url: []const u8,
|
||||
is_async: bool,
|
||||
is_defer: bool,
|
||||
source: Source,
|
||||
element: *parser.Element,
|
||||
|
||||
const Kind = enum {
|
||||
module,
|
||||
javascript,
|
||||
};
|
||||
|
||||
const Callback = union(enum) {
|
||||
string: []const u8,
|
||||
function: Env.Function,
|
||||
};
|
||||
|
||||
const Source = union(enum) {
|
||||
@"inline": []const u8,
|
||||
remote: std.ArrayListUnmanaged(u8),
|
||||
|
||||
fn content(self: Source) []const u8 {
|
||||
return switch (self) {
|
||||
.remote => |buf| buf.items,
|
||||
.@"inline" => |c| c,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
fn eval(self: *Script, page: *Page) void {
|
||||
page.setCurrentScript(@ptrCast(self.element)) catch |err| {
|
||||
log.err(.browser, "set document script", .{ .err = err });
|
||||
return;
|
||||
};
|
||||
|
||||
defer page.setCurrentScript(null) catch |err| {
|
||||
log.err(.browser, "clear document script", .{ .err = err });
|
||||
};
|
||||
|
||||
// inline scripts aren't cached. remote ones are.
|
||||
const cacheable = self.source == .remote;
|
||||
|
||||
const url = self.url;
|
||||
|
||||
log.info(.browser, "executing script", .{
|
||||
.src = url,
|
||||
.kind = self.kind,
|
||||
.cacheable = cacheable,
|
||||
});
|
||||
|
||||
const js_context = page.main_context;
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(js_context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const success = blk: {
|
||||
const content = self.source.content();
|
||||
switch (self.kind) {
|
||||
.javascript => _ = js_context.eval(content, url) catch break :blk false,
|
||||
.module => {
|
||||
// We don't care about waiting for the evaluation here.
|
||||
_ = js_context.module(content, url, cacheable) catch break :blk false;
|
||||
},
|
||||
}
|
||||
break :blk true;
|
||||
};
|
||||
|
||||
if (success) {
|
||||
self.executeCallback("onload", page);
|
||||
return;
|
||||
}
|
||||
|
||||
if (page.delayed_navigation) {
|
||||
// If we're navigating to another page, an error is expected
|
||||
// since we probably terminated the script forcefully.
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = try_catch.err(page.arena) catch |err| @errorName(err) orelse "unknown";
|
||||
log.warn(.user_script, "eval script", .{
|
||||
.url = url,
|
||||
.err = msg,
|
||||
.cacheable = cacheable,
|
||||
});
|
||||
|
||||
self.executeCallback("onerror", page);
|
||||
}
|
||||
|
||||
fn executeCallback(self: *const Script, comptime typ: []const u8, page: *Page) void {
|
||||
const callback = self.getCallback(typ, page) orelse return;
|
||||
|
||||
switch (callback) {
|
||||
.string => |str| {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(page.main_context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
_ = page.main_context.exec(str, typ) catch |err| {
|
||||
const msg = try_catch.err(page.arena) catch @errorName(err) orelse "unknown";
|
||||
log.warn(.user_script, "script callback", .{
|
||||
.url = self.url,
|
||||
.err = msg,
|
||||
.type = typ,
|
||||
.@"inline" = true,
|
||||
});
|
||||
};
|
||||
},
|
||||
.function => |f| {
|
||||
const Event = @import("events/event.zig").Event;
|
||||
const loadevt = parser.eventCreate() catch |err| {
|
||||
log.err(.browser, "SM event creation", .{ .err = err });
|
||||
return;
|
||||
};
|
||||
defer parser.eventDestroy(loadevt);
|
||||
|
||||
var result: Env.Function.Result = undefined;
|
||||
const iface = Event.toInterface(loadevt);
|
||||
f.tryCall(void, .{iface}, &result) catch {
|
||||
log.warn(.user_script, "script callback", .{
|
||||
.url = self.url,
|
||||
.type = typ,
|
||||
.err = result.exception,
|
||||
.stack = result.stack,
|
||||
.@"inline" = false,
|
||||
});
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn getCallback(self: *const Script, comptime typ: []const u8, page: *Page) ?Callback {
|
||||
const element = self.element;
|
||||
// first we check if there was an el.onload set directly on the
|
||||
// element in JavaScript (if so, it'd be stored in the node state)
|
||||
if (page.getNodeState(@ptrCast(element))) |se| {
|
||||
if (@field(se, typ)) |function| {
|
||||
return .{ .function = function };
|
||||
}
|
||||
}
|
||||
// if we have no node state, or if the node state has no onload/onerror
|
||||
// then check for the onload/onerror attribute
|
||||
if (parser.elementGetAttribute(element, typ) catch null) |string| {
|
||||
return .{ .string = string };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const BufferPool = struct {
|
||||
count: usize,
|
||||
available: List = .{},
|
||||
allocator: Allocator,
|
||||
max_concurrent_transfers: u8,
|
||||
mem_pool: std.heap.MemoryPool(Container),
|
||||
|
||||
const List = std.DoublyLinkedList;
|
||||
|
||||
const Container = struct {
|
||||
node: List.Node,
|
||||
buf: std.ArrayListUnmanaged(u8),
|
||||
};
|
||||
|
||||
fn init(allocator: Allocator, max_concurrent_transfers: u8) BufferPool {
|
||||
return .{
|
||||
.available = .{},
|
||||
.count = 0,
|
||||
.allocator = allocator,
|
||||
.max_concurrent_transfers = max_concurrent_transfers,
|
||||
.mem_pool = std.heap.MemoryPool(Container).init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
fn deinit(self: *BufferPool) void {
|
||||
const allocator = self.allocator;
|
||||
|
||||
var node = self.available.first;
|
||||
while (node) |n| {
|
||||
const container: *Container = @fieldParentPtr("node", n);
|
||||
container.buf.deinit(allocator);
|
||||
node = n.next;
|
||||
}
|
||||
self.mem_pool.deinit();
|
||||
}
|
||||
|
||||
fn get(self: *BufferPool) std.ArrayListUnmanaged(u8) {
|
||||
const node = self.available.popFirst() orelse {
|
||||
// return a new buffer
|
||||
return .{};
|
||||
};
|
||||
|
||||
self.count -= 1;
|
||||
const container: *Container = @fieldParentPtr("node", node);
|
||||
defer self.mem_pool.destroy(container);
|
||||
return container.buf;
|
||||
}
|
||||
|
||||
fn release(self: *BufferPool, buffer: ArrayListUnmanaged(u8)) void {
|
||||
// create mutable copy
|
||||
var b = buffer;
|
||||
|
||||
if (self.count == self.max_concurrent_transfers) {
|
||||
b.deinit(self.allocator);
|
||||
return;
|
||||
}
|
||||
|
||||
const container = self.mem_pool.create() catch |err| {
|
||||
b.deinit(self.allocator);
|
||||
log.err(.http, "SM BufferPool release", .{ .err = err });
|
||||
return;
|
||||
};
|
||||
|
||||
b.clearRetainingCapacity();
|
||||
container.* = .{ .buf = b, .node = .{} };
|
||||
self.count += 1;
|
||||
self.available.append(&container.node);
|
||||
}
|
||||
};
|
||||
|
||||
const Blocking = struct {
|
||||
allocator: Allocator,
|
||||
buffer_pool: *BufferPool,
|
||||
state: State = .{ .running = {} },
|
||||
buffer: std.ArrayListUnmanaged(u8) = .{},
|
||||
|
||||
const State = union(enum) {
|
||||
running: void,
|
||||
err: anyerror,
|
||||
done: BlockingResult,
|
||||
};
|
||||
|
||||
fn startCallback(transfer: *Http.Transfer) !void {
|
||||
log.debug(.http, "script fetch start", .{ .req = transfer, .blocking = true });
|
||||
}
|
||||
|
||||
fn headerCallback(transfer: *Http.Transfer) !void {
|
||||
const header = &transfer.response_header.?;
|
||||
log.debug(.http, "script header", .{
|
||||
.req = transfer,
|
||||
.blocking = true,
|
||||
.status = header.status,
|
||||
.content_type = header.contentType(),
|
||||
});
|
||||
|
||||
if (header.status != 200) {
|
||||
return error.InvalidStatusCode;
|
||||
}
|
||||
|
||||
var self: *Blocking = @ptrCast(@alignCast(transfer.ctx));
|
||||
self.buffer = self.buffer_pool.get();
|
||||
}
|
||||
|
||||
fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void {
|
||||
// too verbose
|
||||
// log.debug(.http, "script data chunk", .{
|
||||
// .req = transfer,
|
||||
// .blocking = true,
|
||||
// });
|
||||
|
||||
var self: *Blocking = @ptrCast(@alignCast(transfer.ctx));
|
||||
self.buffer.appendSlice(self.allocator, data) catch |err| {
|
||||
log.err(.http, "SM.dataCallback", .{
|
||||
.err = err,
|
||||
.len = data.len,
|
||||
.blocking = true,
|
||||
.transfer = transfer,
|
||||
});
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
fn doneCallback(ctx: *anyopaque) !void {
|
||||
var self: *Blocking = @ptrCast(@alignCast(ctx));
|
||||
self.state = .{ .done = .{
|
||||
.buffer = self.buffer,
|
||||
.buffer_pool = self.buffer_pool,
|
||||
} };
|
||||
}
|
||||
|
||||
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||
var self: *Blocking = @ptrCast(@alignCast(ctx));
|
||||
self.state = .{ .err = err };
|
||||
self.buffer_pool.release(self.buffer);
|
||||
}
|
||||
};
|
||||
|
||||
pub const BlockingResult = struct {
|
||||
buffer: std.ArrayListUnmanaged(u8),
|
||||
buffer_pool: *BufferPool,
|
||||
|
||||
pub fn deinit(self: *BlockingResult) void {
|
||||
self.buffer_pool.release(self.buffer);
|
||||
}
|
||||
|
||||
pub fn src(self: *const BlockingResult) []const u8 {
|
||||
return self.buffer.items;
|
||||
}
|
||||
};
|
||||
@@ -30,8 +30,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;
|
||||
const StyleSheet = @import("cssom/StyleSheet.zig");
|
||||
const CSSStyleSheet = @import("cssom/CSSStyleSheet.zig");
|
||||
const CSSStyleDeclaration = @import("cssom/CSSStyleDeclaration.zig");
|
||||
|
||||
// for HTMLScript (but probably needs to be added to more)
|
||||
onload: ?Env.Function = null,
|
||||
@@ -53,6 +54,7 @@ style_sheet: ?*StyleSheet = null,
|
||||
|
||||
// for dom/document
|
||||
active_element: ?*parser.Element = null,
|
||||
adopted_style_sheets: ?Env.JsObject = null,
|
||||
|
||||
// for HTMLSelectElement
|
||||
// By default, if no option is explicitly selected, the first option should
|
||||
|
||||
@@ -28,8 +28,7 @@ const Session = @import("session.zig").Session;
|
||||
const Notification = @import("../notification.zig").Notification;
|
||||
|
||||
const log = @import("../log.zig");
|
||||
|
||||
const http = @import("../http/client.zig");
|
||||
const HttpClient = @import("../http/Client.zig");
|
||||
|
||||
// Browser is an instance of the browser.
|
||||
// You can create multiple browser instances.
|
||||
@@ -39,7 +38,7 @@ pub const Browser = struct {
|
||||
app: *App,
|
||||
session: ?Session,
|
||||
allocator: Allocator,
|
||||
http_client: *http.Client,
|
||||
http_client: *HttpClient,
|
||||
page_arena: ArenaAllocator,
|
||||
session_arena: ArenaAllocator,
|
||||
transfer_arena: ArenaAllocator,
|
||||
@@ -49,10 +48,12 @@ pub const Browser = struct {
|
||||
pub fn init(app: *App) !Browser {
|
||||
const allocator = app.allocator;
|
||||
|
||||
const env = try Env.init(allocator, app.platform, .{});
|
||||
const env = try Env.init(allocator, &app.platform, .{});
|
||||
errdefer env.deinit();
|
||||
|
||||
const notification = try Notification.init(allocator, app.notification);
|
||||
app.http.client.notification = notification;
|
||||
app.http.client.next_request_id = 0; // Should we track ids in CDP only?
|
||||
errdefer notification.deinit();
|
||||
|
||||
return .{
|
||||
@@ -61,7 +62,7 @@ pub const Browser = struct {
|
||||
.session = null,
|
||||
.allocator = allocator,
|
||||
.notification = notification,
|
||||
.http_client = &app.http_client,
|
||||
.http_client = app.http.client,
|
||||
.page_arena = ArenaAllocator.init(allocator),
|
||||
.session_arena = ArenaAllocator.init(allocator),
|
||||
.transfer_arena = ArenaAllocator.init(allocator),
|
||||
@@ -75,6 +76,7 @@ pub const Browser = struct {
|
||||
self.page_arena.deinit();
|
||||
self.session_arena.deinit();
|
||||
self.transfer_arena.deinit();
|
||||
self.http_client.notification = null;
|
||||
self.notification.deinit();
|
||||
self.state_pool.deinit();
|
||||
}
|
||||
@@ -110,11 +112,5 @@ pub const Browser = struct {
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
test "Browser" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
// this will crash if ICU isn't properly configured / ininitialized
|
||||
try runner.testCases(&.{
|
||||
.{ "new Intl.DateTimeFormat()", "[object Intl.DateTimeFormat]" },
|
||||
}, .{});
|
||||
try testing.htmlRunner("browser.html");
|
||||
}
|
||||
|
||||
@@ -18,13 +18,12 @@
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const JsObject = @import("../env.zig").Env.JsObject;
|
||||
|
||||
const log = if (builtin.is_test) &test_capture else @import("../../log.zig");
|
||||
|
||||
pub const Console = struct {
|
||||
// TODO: configurable writer
|
||||
timers: std.StringHashMapUnmanaged(u32) = .{},
|
||||
@@ -67,7 +66,7 @@ pub const Console = struct {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(.console, "error", .{
|
||||
log.warn(.console, "error", .{
|
||||
.args = try serializeValues(values, page),
|
||||
.stack = page.stackTrace() catch "???",
|
||||
});
|
||||
@@ -165,165 +164,5 @@ pub const Console = struct {
|
||||
};
|
||||
|
||||
fn timestamp() u32 {
|
||||
const ts = std.posix.clock_gettime(std.posix.CLOCK.MONOTONIC) catch unreachable;
|
||||
return @intCast(ts.sec);
|
||||
return @import("../../datetime.zig").timestamp();
|
||||
}
|
||||
|
||||
var test_capture = TestCapture{};
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.Console" {
|
||||
defer testing.reset();
|
||||
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
{
|
||||
try runner.testCases(&.{
|
||||
.{ "console.log('a')", "undefined" },
|
||||
.{ "console.warn('hello world', 23, true, new Object())", "undefined" },
|
||||
}, .{});
|
||||
|
||||
const captured = test_capture.captured.items;
|
||||
try testing.expectEqual("[info] args= 1: a", captured[0]);
|
||||
try testing.expectEqual("[warn] args= 1: hello world 2: 23 3: true 4: #<Object>", captured[1]);
|
||||
}
|
||||
|
||||
{
|
||||
test_capture.reset();
|
||||
try runner.testCases(&.{
|
||||
.{ "console.countReset()", "undefined" },
|
||||
.{ "console.count()", "undefined" },
|
||||
.{ "console.count('teg')", "undefined" },
|
||||
.{ "console.count('teg')", "undefined" },
|
||||
.{ "console.count('teg')", "undefined" },
|
||||
.{ "console.count()", "undefined" },
|
||||
.{ "console.countReset('teg')", "undefined" },
|
||||
.{ "console.countReset()", "undefined" },
|
||||
.{ "console.count()", "undefined" },
|
||||
}, .{});
|
||||
|
||||
const captured = test_capture.captured.items;
|
||||
try testing.expectEqual("[invalid counter] label=default", captured[0]);
|
||||
try testing.expectEqual("[count] label=default count=1", captured[1]);
|
||||
try testing.expectEqual("[count] label=teg count=1", captured[2]);
|
||||
try testing.expectEqual("[count] label=teg count=2", captured[3]);
|
||||
try testing.expectEqual("[count] label=teg count=3", captured[4]);
|
||||
try testing.expectEqual("[count] label=default count=2", captured[5]);
|
||||
try testing.expectEqual("[count reset] label=teg count=3", captured[6]);
|
||||
try testing.expectEqual("[count reset] label=default count=2", captured[7]);
|
||||
try testing.expectEqual("[count] label=default count=1", captured[8]);
|
||||
}
|
||||
|
||||
{
|
||||
test_capture.reset();
|
||||
try runner.testCases(&.{
|
||||
.{ "console.assert(true)", "undefined" },
|
||||
.{ "console.assert('a', 2, 3, 4)", "undefined" },
|
||||
.{ "console.assert('')", "undefined" },
|
||||
.{ "console.assert('', 'x', true)", "undefined" },
|
||||
.{ "console.assert(false, 'x')", "undefined" },
|
||||
}, .{});
|
||||
|
||||
const captured = test_capture.captured.items;
|
||||
try testing.expectEqual("[assertion failed] values=", captured[0]);
|
||||
try testing.expectEqual("[assertion failed] values= 1: x 2: true", captured[1]);
|
||||
try testing.expectEqual("[assertion failed] values= 1: x", captured[2]);
|
||||
}
|
||||
|
||||
{
|
||||
test_capture.reset();
|
||||
try runner.testCases(&.{
|
||||
.{ "[1].forEach(console.log)", null },
|
||||
}, .{});
|
||||
|
||||
const captured = test_capture.captured.items;
|
||||
try testing.expectEqual("[info] args= 1: 1 2: 0 3: [1]", captured[0]);
|
||||
}
|
||||
}
|
||||
|
||||
const TestCapture = struct {
|
||||
captured: std.ArrayListUnmanaged([]const u8) = .{},
|
||||
|
||||
fn separator(_: *const TestCapture) []const u8 {
|
||||
return " ";
|
||||
}
|
||||
|
||||
fn reset(self: *TestCapture) void {
|
||||
self.captured = .{};
|
||||
}
|
||||
|
||||
fn debug(
|
||||
self: *TestCapture,
|
||||
comptime scope: @Type(.enum_literal),
|
||||
comptime msg: []const u8,
|
||||
args: anytype,
|
||||
) void {
|
||||
self.capture(scope, msg, args);
|
||||
}
|
||||
|
||||
fn info(
|
||||
self: *TestCapture,
|
||||
comptime scope: @Type(.enum_literal),
|
||||
comptime msg: []const u8,
|
||||
args: anytype,
|
||||
) void {
|
||||
self.capture(scope, msg, args);
|
||||
}
|
||||
|
||||
fn warn(
|
||||
self: *TestCapture,
|
||||
comptime scope: @Type(.enum_literal),
|
||||
comptime msg: []const u8,
|
||||
args: anytype,
|
||||
) void {
|
||||
self.capture(scope, msg, args);
|
||||
}
|
||||
|
||||
fn err(
|
||||
self: *TestCapture,
|
||||
comptime scope: @Type(.enum_literal),
|
||||
comptime msg: []const u8,
|
||||
args: anytype,
|
||||
) void {
|
||||
self.capture(scope, msg, args);
|
||||
}
|
||||
|
||||
fn fatal(
|
||||
self: *TestCapture,
|
||||
comptime scope: @Type(.enum_literal),
|
||||
comptime msg: []const u8,
|
||||
args: anytype,
|
||||
) void {
|
||||
self.capture(scope, msg, args);
|
||||
}
|
||||
|
||||
fn capture(
|
||||
self: *TestCapture,
|
||||
comptime scope: @Type(.enum_literal),
|
||||
comptime msg: []const u8,
|
||||
args: anytype,
|
||||
) void {
|
||||
self._capture(scope, msg, args) catch unreachable;
|
||||
}
|
||||
|
||||
fn _capture(
|
||||
self: *TestCapture,
|
||||
comptime scope: @Type(.enum_literal),
|
||||
comptime msg: []const u8,
|
||||
args: anytype,
|
||||
) !void {
|
||||
std.debug.assert(scope == .console);
|
||||
|
||||
const allocator = testing.arena_allocator;
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
try buf.appendSlice(allocator, "[" ++ msg ++ "] ");
|
||||
|
||||
inline for (@typeInfo(@TypeOf(args)).@"struct".fields) |f| {
|
||||
try buf.appendSlice(allocator, f.name);
|
||||
try buf.append(allocator, '=');
|
||||
try @import("../../log.zig").writeValue(.pretty, @field(args, f.name), buf.writer(allocator));
|
||||
try buf.append(allocator, ' ');
|
||||
}
|
||||
self.captured.append(testing.arena_allocator, std.mem.trimRight(u8, buf.items, " ")) catch unreachable;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -66,32 +66,6 @@ const RandomValues = union(enum) {
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.Crypto" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const a = crypto.randomUUID();", "undefined" },
|
||||
.{ "const b = crypto.randomUUID();", "undefined" },
|
||||
.{ "a.length;", "36" },
|
||||
.{ "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" },
|
||||
.{ "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" },
|
||||
}, .{});
|
||||
test "Browser: Crypto" {
|
||||
try testing.htmlRunner("crypto.html");
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ pub fn parse(alloc: std.mem.Allocator, s: []const u8, opts: parser.ParseOptions)
|
||||
|
||||
// matchFirst call m.match with the first node that matches the selector s, from the
|
||||
// descendants of n and returns true. If none matches, it returns false.
|
||||
pub fn matchFirst(s: Selector, node: anytype, m: anytype) !bool {
|
||||
pub fn matchFirst(s: *const Selector, node: anytype, m: anytype) !bool {
|
||||
var c = try node.firstChild();
|
||||
while (true) {
|
||||
if (c == null) break;
|
||||
@@ -63,7 +63,7 @@ pub fn matchFirst(s: Selector, node: anytype, m: anytype) !bool {
|
||||
|
||||
// matchAll call m.match with the all the nodes that matches the selector s, from the
|
||||
// descendants of n.
|
||||
pub fn matchAll(s: Selector, node: anytype, m: anytype) !void {
|
||||
pub fn matchAll(s: *const Selector, node: anytype, m: anytype) !void {
|
||||
var c = try node.firstChild();
|
||||
while (true) {
|
||||
if (c == null) break;
|
||||
@@ -190,12 +190,6 @@ test "parse" {
|
||||
}
|
||||
|
||||
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" },
|
||||
}, .{});
|
||||
test "Browser: CSS" {
|
||||
try testing.htmlRunner("css.html");
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const css = @import("css.zig");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
// Node implementation with Netsurf Libdom C lib.
|
||||
pub const Node = struct {
|
||||
@@ -79,6 +81,14 @@ pub const Node = struct {
|
||||
return t == .text;
|
||||
}
|
||||
|
||||
pub fn text(n: Node) !?[]const u8 {
|
||||
const data = try parser.nodeTextContent(n.node);
|
||||
if (data == null) return null;
|
||||
if (data.?.len == 0) return null;
|
||||
|
||||
return std.mem.trim(u8, data.?, &std.ascii.whitespace);
|
||||
}
|
||||
|
||||
pub fn isEmptyText(n: Node) !bool {
|
||||
const data = try parser.nodeTextContent(n.node);
|
||||
if (data == null) return true;
|
||||
@@ -100,3 +110,318 @@ pub const Node = struct {
|
||||
return a.node == b.node;
|
||||
}
|
||||
};
|
||||
|
||||
const MatcherTest = struct {
|
||||
const Nodes = std.ArrayListUnmanaged(Node);
|
||||
|
||||
nodes: Nodes,
|
||||
allocator: Allocator,
|
||||
|
||||
fn init(allocator: Allocator) MatcherTest {
|
||||
return .{
|
||||
.nodes = .empty,
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
fn deinit(m: *MatcherTest) void {
|
||||
m.nodes.deinit(m.allocator);
|
||||
}
|
||||
|
||||
fn reset(m: *MatcherTest) void {
|
||||
m.nodes.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
pub fn match(m: *MatcherTest, n: Node) !void {
|
||||
try m.nodes.append(m.allocator, n);
|
||||
}
|
||||
};
|
||||
|
||||
test "Browser.CSS.Libdom: matchFirst" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
try parser.init();
|
||||
defer parser.deinit();
|
||||
|
||||
var matcher = MatcherTest.init(alloc);
|
||||
defer matcher.deinit();
|
||||
|
||||
const testcases = [_]struct {
|
||||
q: []const u8,
|
||||
html: []const u8,
|
||||
exp: usize,
|
||||
}{
|
||||
.{ .q = "address", .html = "<body><address>This address...</address></body>", .exp = 1 },
|
||||
.{ .q = "*", .html = "<!-- comment --><html><head></head><body>text</body></html>", .exp = 1 },
|
||||
.{ .q = "*", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "#foo", .html = "<p id=\"foo\"><p id=\"bar\">", .exp = 1 },
|
||||
.{ .q = "li#t1", .html = "<ul><li id=\"t1\"><p id=\"t1\">", .exp = 1 },
|
||||
.{ .q = ".t3", .html = "<ul><li class=\"t1\"><li class=\"t2 t3\">", .exp = 1 },
|
||||
.{ .q = "*#t4", .html = "<ol><li id=\"t4\"><li id=\"t44\">", .exp = 1 },
|
||||
.{ .q = ".t1", .html = "<ul><li class=\"t1\"><li class=\"t2\">", .exp = 1 },
|
||||
.{ .q = "p.t1", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
||||
.{ .q = "div.teST", .html = "<div class=\"test\">", .exp = 0 },
|
||||
.{ .q = ".t1.fail", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
||||
.{ .q = "p.t1.t2", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
||||
.{ .q = "p.--t1", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
||||
.{ .q = "p.--t1.--t2", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
||||
.{ .q = "p[title]", .html = "<p><p title=\"title\">", .exp = 1 },
|
||||
.{ .q = "div[class=\"red\" i]", .html = "<div><div class=\"Red\">", .exp = 1 },
|
||||
.{ .q = "address[title=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
||||
.{ .q = "address[title=\"FoOIgnoRECaSe\" i]", .html = "<address><address title=\"fooIgnoreCase\"><address title=\"bar\">", .exp = 1 },
|
||||
.{ .q = "address[title!=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
||||
.{ .q = "address[title!=\"foo\" i]", .html = "<address><address title=\"FOO\"><address title=\"bar\">", .exp = 1 },
|
||||
.{ .q = "p[title!=\"FooBarUFoo\" i]", .html = "<p title=\"fooBARuFOO\"><p title=\"varfoo\">", .exp = 1 },
|
||||
.{ .q = "[ title ~= foo ]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
||||
.{ .q = "p[title~=\"FOO\" i]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
||||
.{ .q = "p[title~=toofoo i]", .html = "<p title=\"tot foo bar\">", .exp = 0 },
|
||||
.{ .q = "[title~=\"hello world\"]", .html = "<p title=\"hello world\">", .exp = 0 },
|
||||
.{ .q = "[title~=\"hello\" i]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
||||
.{ .q = "[title~=\"hello\" I]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
||||
.{ .q = "[lang|=\"en\"]", .html = "<p lang=\"en\"><p lang=\"en-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
||||
.{ .q = "[title^=\"foo\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||
.{ .q = "[title^=\"foo\" i]", .html = "<p title=\"FooBAR\"><p title=\"barfoo\">", .exp = 1 },
|
||||
.{ .q = "[title$=\"bar\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||
.{ .q = "[title$=\"BAR\" i]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||
.{ .q = "[title*=\"bar\"]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||
.{ .q = "[title*=\"BaRu\" i]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||
.{ .q = "[title*=\"BaRu\" I]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||
.{ .q = "p[class$=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class$=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class^=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class^=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class*=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class*=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "input[name=Sex][value=F]", .html = "<input type=\"radio\" name=\"Sex\" value=\"F\"/>", .exp = 1 },
|
||||
.{ .q = "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]", .html = "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout: fixed; width: 100%; border: 0 dashed; border-color: #FFFFFF\"><tr style=\"height:64px\">aaa</tr></table>", .exp = 1 },
|
||||
.{ .q = ".t1:not(.t2)", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
||||
.{ .q = "div:not(.t1)", .html = "<div class=\"t3\">", .exp = 1 },
|
||||
.{ .q = "div:not([class=\"t2\"])", .html = "<div><div class=\"t2\"><div class=\"t3\">", .exp = 1 },
|
||||
.{ .q = "li:nth-child(odd)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-child(even)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-last-child(odd)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-last-child(even)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-last-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-last-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
||||
.{ .q = "span:first-child", .html = "<p>some text <span id=\"1\">and a span</span><span id=\"2\"> and another</span></p>", .exp = 1 },
|
||||
.{ .q = "span:last-child", .html = "<span>a span</span> and some text", .exp = 1 },
|
||||
.{ .q = "p:nth-of-type(2)", .html = "<address></address><p id=1><p id=2>", .exp = 1 },
|
||||
.{ .q = "p:nth-last-of-type(2)", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||
.{ .q = "p:last-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||
.{ .q = "p:first-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||
.{ .q = "p:only-child", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p></div>", .exp = 1 },
|
||||
.{ .q = "p:only-of-type", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p><p id=\"3\"></p></div>", .exp = 1 },
|
||||
.{ .q = ":empty", .html = "<p id=\"1\"><!-- --><p id=\"2\">Hello<p id=\"3\"><span>", .exp = 1 },
|
||||
.{ .q = "div p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
||||
.{ .q = "div table p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
||||
.{ .q = "div > p", .html = "<div><p id=\"1\"><div><p id=\"2\"></div><table><tr><td><p id=\"3\"></table></div>", .exp = 1 },
|
||||
.{ .q = "p ~ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
||||
.{ .q = "p + p", .html = "<p id=\"1\"></p> <!--comment--> <p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
||||
.{ .q = "li, p", .html = "<ul><li></li><li></li></ul><p>", .exp = 1 },
|
||||
.{ .q = "p +/*This is a comment*/ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
||||
// .{ .q = "p:contains(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||
.{ .q = "p:containsOwn(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
|
||||
.{ .q = ":containsOwn(\"inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||
.{ .q = ":containsOwn(\"Inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
|
||||
.{ .q = "p:containsOwn(\"block\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||
// .{ .q = "div:has(#p1)", .html = "<div id=\"d1\"><p id=\"p1\"><span>text content</span></p></div><div id=\"d2\"/>", .exp = 1 },
|
||||
.{ .q = "div:has(:containsOwn(\"2\"))", .html = "<div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p>contents <em>2</em></p></div>", .exp = 1 },
|
||||
.{ .q = "body :has(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
||||
.{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
||||
// .{ .q = "p:matches([\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "p:matches([a-z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "p:matches([a-zA-Z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "p:matches([^\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "p:matches(^(0|a))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "p:matches(^\\d+$)", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "p:not(:matches(^\\d+$))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "div :matchesOwn(^\\d+$)", .html = "<div><p id=\"p1\">01234<em>567</em>89</p><div>", .exp = 1 },
|
||||
// .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"></a> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"></a> <li><a id=\"a2\" href=\"http://finance.untrusted.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"/> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
||||
// .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"/> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"></a> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
||||
.{ .q = ":input", .html = "<form> <label>Username <input type=\"text\" name=\"username\" /></label> <label>Password <input type=\"password\" name=\"password\" /></label> <label>Country <select name=\"country\"> <option value=\"ca\">Canada</option> <option value=\"us\">United States</option> </select> </label> <label>Bio <textarea name=\"bio\"></textarea></label> <button>Sign up</button> </form>", .exp = 1 },
|
||||
.{ .q = ":root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "*:root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "html:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "*:root:first-child", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "*:root:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "a:not(:root)", .html = "<html><head></head><body><a href=\"http://www.foo.com\"></a></body></html>", .exp = 1 },
|
||||
.{ .q = "body > *:nth-child(3n+2)", .html = "<html><head></head><body><p></p><div></div><span></span><a></a><form></form></body></html>", .exp = 1 },
|
||||
.{ .q = "input:disabled", .html = "<html><head></head><body><fieldset disabled><legend id=\"1\"><input id=\"i1\"/></legend><legend id=\"2\"><input id=\"i2\"/></legend></fieldset></body></html>", .exp = 1 },
|
||||
.{ .q = ":disabled", .html = "<html><head></head><body><fieldset disabled></fieldset></body></html>", .exp = 1 },
|
||||
.{ .q = ":enabled", .html = "<html><head></head><body><fieldset></fieldset></body></html>", .exp = 1 },
|
||||
.{ .q = "div.class1, div.class2", .html = "<div class=class1></div><div class=class2></div><div class=class3></div>", .exp = 1 },
|
||||
};
|
||||
|
||||
for (testcases) |tc| {
|
||||
matcher.reset();
|
||||
|
||||
const doc = try parser.documentHTMLParseFromStr(tc.html);
|
||||
defer parser.documentHTMLClose(doc) catch {};
|
||||
|
||||
const s = css.parse(alloc, tc.q, .{}) catch |e| {
|
||||
std.debug.print("parse, query: {s}\n", .{tc.q});
|
||||
return e;
|
||||
};
|
||||
|
||||
defer s.deinit(alloc);
|
||||
|
||||
const node = Node{ .node = parser.documentHTMLToNode(doc) };
|
||||
|
||||
_ = css.matchFirst(&s, node, &matcher) catch |e| {
|
||||
std.debug.print("match, query: {s}\n", .{tc.q});
|
||||
return e;
|
||||
};
|
||||
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
|
||||
std.debug.print("expectation, query: {s}\n", .{tc.q});
|
||||
return e;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test "Browser.CSS.Libdom: matchAll" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
try parser.init();
|
||||
defer parser.deinit();
|
||||
|
||||
var matcher = MatcherTest.init(alloc);
|
||||
defer matcher.deinit();
|
||||
|
||||
const testcases = [_]struct {
|
||||
q: []const u8,
|
||||
html: []const u8,
|
||||
exp: usize,
|
||||
}{
|
||||
.{ .q = "address", .html = "<body><address>This address...</address></body>", .exp = 1 },
|
||||
.{ .q = "*", .html = "<!-- comment --><html><head></head><body>text</body></html>", .exp = 3 },
|
||||
.{ .q = "*", .html = "<html><head></head><body></body></html>", .exp = 3 },
|
||||
.{ .q = "#foo", .html = "<p id=\"foo\"><p id=\"bar\">", .exp = 1 },
|
||||
.{ .q = "li#t1", .html = "<ul><li id=\"t1\"><p id=\"t1\">", .exp = 1 },
|
||||
.{ .q = ".t3", .html = "<ul><li class=\"t1\"><li class=\"t2 t3\">", .exp = 1 },
|
||||
.{ .q = "*#t4", .html = "<ol><li id=\"t4\"><li id=\"t44\">", .exp = 1 },
|
||||
.{ .q = ".t1", .html = "<ul><li class=\"t1\"><li class=\"t2\">", .exp = 1 },
|
||||
.{ .q = "p.t1", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
||||
.{ .q = "div.teST", .html = "<div class=\"test\">", .exp = 0 },
|
||||
.{ .q = ".t1.fail", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
||||
.{ .q = "p.t1.t2", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
||||
.{ .q = "p.--t1", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
||||
.{ .q = "p.--t1.--t2", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
||||
.{ .q = "p[title]", .html = "<p><p title=\"title\">", .exp = 1 },
|
||||
.{ .q = "div[class=\"red\" i]", .html = "<div><div class=\"Red\">", .exp = 1 },
|
||||
.{ .q = "address[title=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
||||
.{ .q = "address[title=\"FoOIgnoRECaSe\" i]", .html = "<address><address title=\"fooIgnoreCase\"><address title=\"bar\">", .exp = 1 },
|
||||
.{ .q = "address[title!=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 2 },
|
||||
.{ .q = "address[title!=\"foo\" i]", .html = "<address><address title=\"FOO\"><address title=\"bar\">", .exp = 2 },
|
||||
.{ .q = "p[title!=\"FooBarUFoo\" i]", .html = "<p title=\"fooBARuFOO\"><p title=\"varfoo\">", .exp = 1 },
|
||||
.{ .q = "[ title ~= foo ]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
||||
.{ .q = "p[title~=\"FOO\" i]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
||||
.{ .q = "p[title~=toofoo i]", .html = "<p title=\"tot foo bar\">", .exp = 0 },
|
||||
.{ .q = "[title~=\"hello world\"]", .html = "<p title=\"hello world\">", .exp = 0 },
|
||||
.{ .q = "[title~=\"hello\" i]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
||||
.{ .q = "[title~=\"hello\" I]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
||||
.{ .q = "[lang|=\"en\"]", .html = "<p lang=\"en\"><p lang=\"en-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
||||
.{ .q = "[title^=\"foo\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||
.{ .q = "[title^=\"foo\" i]", .html = "<p title=\"FooBAR\"><p title=\"barfoo\">", .exp = 1 },
|
||||
.{ .q = "[title$=\"bar\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||
.{ .q = "[title$=\"BAR\" i]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||
.{ .q = "[title*=\"bar\"]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||
.{ .q = "[title*=\"BaRu\" i]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||
.{ .q = "[title*=\"BaRu\" I]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||
.{ .q = "p[class$=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class$=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class^=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class^=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class*=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class*=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "input[name=Sex][value=F]", .html = "<input type=\"radio\" name=\"Sex\" value=\"F\"/>", .exp = 1 },
|
||||
.{ .q = "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]", .html = "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout: fixed; width: 100%; border: 0 dashed; border-color: #FFFFFF\"><tr style=\"height:64px\">aaa</tr></table>", .exp = 1 },
|
||||
.{ .q = ".t1:not(.t2)", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
||||
.{ .q = "div:not(.t1)", .html = "<div class=\"t3\">", .exp = 1 },
|
||||
.{ .q = "div:not([class=\"t2\"])", .html = "<div><div class=\"t2\"><div class=\"t3\">", .exp = 2 },
|
||||
.{ .q = "li:nth-child(odd)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 2 },
|
||||
.{ .q = "li:nth-child(even)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 2 },
|
||||
.{ .q = "li:nth-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-last-child(odd)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
||||
.{ .q = "li:nth-last-child(even)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
||||
.{ .q = "li:nth-last-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
||||
.{ .q = "li:nth-last-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
||||
.{ .q = "span:first-child", .html = "<p>some text <span id=\"1\">and a span</span><span id=\"2\"> and another</span></p>", .exp = 1 },
|
||||
.{ .q = "span:last-child", .html = "<span>a span</span> and some text", .exp = 1 },
|
||||
.{ .q = "p:nth-of-type(2)", .html = "<address></address><p id=1><p id=2>", .exp = 1 },
|
||||
.{ .q = "p:nth-last-of-type(2)", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||
.{ .q = "p:last-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||
.{ .q = "p:first-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||
.{ .q = "p:only-child", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p></div>", .exp = 1 },
|
||||
.{ .q = "p:only-of-type", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p><p id=\"3\"></p></div>", .exp = 1 },
|
||||
.{ .q = ":empty", .html = "<p id=\"1\"><!-- --><p id=\"2\">Hello<p id=\"3\"><span>", .exp = 3 },
|
||||
.{ .q = "div p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 2 },
|
||||
.{ .q = "div table p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
||||
.{ .q = "div > p", .html = "<div><p id=\"1\"><div><p id=\"2\"></div><table><tr><td><p id=\"3\"></table></div>", .exp = 2 },
|
||||
.{ .q = "p ~ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 2 },
|
||||
.{ .q = "p + p", .html = "<p id=\"1\"></p> <!--comment--> <p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
||||
.{ .q = "li, p", .html = "<ul><li></li><li></li></ul><p>", .exp = 3 },
|
||||
.{ .q = "p +/*This is a comment*/ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
||||
// .{ .q = "p:contains(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||
.{ .q = "p:containsOwn(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
|
||||
.{ .q = ":containsOwn(\"inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||
.{ .q = ":containsOwn(\"Inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
|
||||
.{ .q = "p:containsOwn(\"block\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||
.{ .q = "div:has(#p1)", .html = "<div id=\"d1\"><p id=\"p1\"><span>text content</span></p></div><div id=\"d2\"/>", .exp = 1 },
|
||||
.{ .q = "div:has(:containsOwn(\"2\"))", .html = "<div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p>contents <em>2</em></p></div>", .exp = 1 },
|
||||
.{ .q = "body :has(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 2 },
|
||||
.{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
||||
// .{ .q = "p:matches([\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
||||
// .{ .q = "p:matches([a-z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "p:matches([a-zA-Z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
||||
// .{ .q = "p:matches([^\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
||||
// .{ .q = "p:matches(^(0|a))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 3 },
|
||||
// .{ .q = "p:matches(^\\d+$)", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "p:not(:matches(^\\d+$))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
||||
// .{ .q = "div :matchesOwn(^\\d+$)", .html = "<div><p id=\"p1\">01234<em>567</em>89</p><div>", .exp = 2 },
|
||||
// .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"></a> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"></a> <li><a id=\"a2\" href=\"http://finance.untrusted.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"/> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 2 },
|
||||
// .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"/> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"></a> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
||||
.{ .q = ":input", .html = "<form> <label>Username <input type=\"text\" name=\"username\" /></label> <label>Password <input type=\"password\" name=\"password\" /></label> <label>Country <select name=\"country\"> <option value=\"ca\">Canada</option> <option value=\"us\">United States</option> </select> </label> <label>Bio <textarea name=\"bio\"></textarea></label> <button>Sign up</button> </form>", .exp = 5 },
|
||||
.{ .q = ":root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "*:root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "html:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "*:root:first-child", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "*:root:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "a:not(:root)", .html = "<html><head></head><body><a href=\"http://www.foo.com\"></a></body></html>", .exp = 1 },
|
||||
.{ .q = "body > *:nth-child(3n+2)", .html = "<html><head></head><body><p></p><div></div><span></span><a></a><form></form></body></html>", .exp = 2 },
|
||||
.{ .q = "input:disabled", .html = "<html><head></head><body><fieldset disabled><legend id=\"1\"><input id=\"i1\"/></legend><legend id=\"2\"><input id=\"i2\"/></legend></fieldset></body></html>", .exp = 1 },
|
||||
.{ .q = ":disabled", .html = "<html><head></head><body><fieldset disabled></fieldset></body></html>", .exp = 1 },
|
||||
.{ .q = ":enabled", .html = "<html><head></head><body><fieldset></fieldset></body></html>", .exp = 1 },
|
||||
.{ .q = "div.class1, div.class2", .html = "<div class=class1></div><div class=class2></div><div class=class3></div>", .exp = 2 },
|
||||
};
|
||||
|
||||
for (testcases) |tc| {
|
||||
matcher.reset();
|
||||
|
||||
const doc = try parser.documentHTMLParseFromStr(tc.html);
|
||||
defer parser.documentHTMLClose(doc) catch {};
|
||||
|
||||
const s = css.parse(alloc, tc.q, .{}) catch |e| {
|
||||
std.debug.print("parse, query: {s}\n", .{tc.q});
|
||||
return e;
|
||||
};
|
||||
defer s.deinit(alloc);
|
||||
|
||||
const node = Node{ .node = parser.documentHTMLToNode(doc) };
|
||||
|
||||
_ = css.matchAll(&s, node, &matcher) catch |e| {
|
||||
std.debug.print("match, query: {s}\n", .{tc.q});
|
||||
return e;
|
||||
};
|
||||
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
|
||||
std.debug.print("expectation, query: {s}\n", .{tc.q});
|
||||
return e;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,325 +0,0 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const css = @import("css.zig");
|
||||
const Node = @import("libdom.zig").Node;
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const Matcher = struct {
|
||||
const Nodes = std.ArrayList(Node);
|
||||
|
||||
nodes: Nodes,
|
||||
|
||||
fn init(alloc: std.mem.Allocator) Matcher {
|
||||
return .{ .nodes = Nodes.init(alloc) };
|
||||
}
|
||||
|
||||
fn deinit(m: *Matcher) void {
|
||||
m.nodes.deinit();
|
||||
}
|
||||
|
||||
fn reset(m: *Matcher) void {
|
||||
m.nodes.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
pub fn match(m: *Matcher, n: Node) !void {
|
||||
try m.nodes.append(n);
|
||||
}
|
||||
};
|
||||
|
||||
test "matchFirst" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
var matcher = Matcher.init(alloc);
|
||||
defer matcher.deinit();
|
||||
|
||||
const testcases = [_]struct {
|
||||
q: []const u8,
|
||||
html: []const u8,
|
||||
exp: usize,
|
||||
}{
|
||||
.{ .q = "address", .html = "<body><address>This address...</address></body>", .exp = 1 },
|
||||
.{ .q = "*", .html = "<!-- comment --><html><head></head><body>text</body></html>", .exp = 1 },
|
||||
.{ .q = "*", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "#foo", .html = "<p id=\"foo\"><p id=\"bar\">", .exp = 1 },
|
||||
.{ .q = "li#t1", .html = "<ul><li id=\"t1\"><p id=\"t1\">", .exp = 1 },
|
||||
.{ .q = ".t3", .html = "<ul><li class=\"t1\"><li class=\"t2 t3\">", .exp = 1 },
|
||||
.{ .q = "*#t4", .html = "<ol><li id=\"t4\"><li id=\"t44\">", .exp = 1 },
|
||||
.{ .q = ".t1", .html = "<ul><li class=\"t1\"><li class=\"t2\">", .exp = 1 },
|
||||
.{ .q = "p.t1", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
||||
.{ .q = "div.teST", .html = "<div class=\"test\">", .exp = 0 },
|
||||
.{ .q = ".t1.fail", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
||||
.{ .q = "p.t1.t2", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
||||
.{ .q = "p.--t1", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
||||
.{ .q = "p.--t1.--t2", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
||||
.{ .q = "p[title]", .html = "<p><p title=\"title\">", .exp = 1 },
|
||||
.{ .q = "div[class=\"red\" i]", .html = "<div><div class=\"Red\">", .exp = 1 },
|
||||
.{ .q = "address[title=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
||||
.{ .q = "address[title=\"FoOIgnoRECaSe\" i]", .html = "<address><address title=\"fooIgnoreCase\"><address title=\"bar\">", .exp = 1 },
|
||||
.{ .q = "address[title!=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
||||
.{ .q = "address[title!=\"foo\" i]", .html = "<address><address title=\"FOO\"><address title=\"bar\">", .exp = 1 },
|
||||
.{ .q = "p[title!=\"FooBarUFoo\" i]", .html = "<p title=\"fooBARuFOO\"><p title=\"varfoo\">", .exp = 1 },
|
||||
.{ .q = "[ title ~= foo ]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
||||
.{ .q = "p[title~=\"FOO\" i]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
||||
.{ .q = "p[title~=toofoo i]", .html = "<p title=\"tot foo bar\">", .exp = 0 },
|
||||
.{ .q = "[title~=\"hello world\"]", .html = "<p title=\"hello world\">", .exp = 0 },
|
||||
.{ .q = "[title~=\"hello\" i]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
||||
.{ .q = "[title~=\"hello\" I]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
||||
.{ .q = "[lang|=\"en\"]", .html = "<p lang=\"en\"><p lang=\"en-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 1 },
|
||||
.{ .q = "[title^=\"foo\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||
.{ .q = "[title^=\"foo\" i]", .html = "<p title=\"FooBAR\"><p title=\"barfoo\">", .exp = 1 },
|
||||
.{ .q = "[title$=\"bar\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||
.{ .q = "[title$=\"BAR\" i]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||
.{ .q = "[title*=\"bar\"]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||
.{ .q = "[title*=\"BaRu\" i]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||
.{ .q = "[title*=\"BaRu\" I]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||
.{ .q = "p[class$=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class$=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class^=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class^=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class*=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class*=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "input[name=Sex][value=F]", .html = "<input type=\"radio\" name=\"Sex\" value=\"F\"/>", .exp = 1 },
|
||||
.{ .q = "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]", .html = "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout: fixed; width: 100%; border: 0 dashed; border-color: #FFFFFF\"><tr style=\"height:64px\">aaa</tr></table>", .exp = 1 },
|
||||
.{ .q = ".t1:not(.t2)", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
||||
.{ .q = "div:not(.t1)", .html = "<div class=\"t3\">", .exp = 1 },
|
||||
.{ .q = "div:not([class=\"t2\"])", .html = "<div><div class=\"t2\"><div class=\"t3\">", .exp = 1 },
|
||||
.{ .q = "li:nth-child(odd)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-child(even)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-last-child(odd)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-last-child(even)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-last-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-last-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 1 },
|
||||
.{ .q = "span:first-child", .html = "<p>some text <span id=\"1\">and a span</span><span id=\"2\"> and another</span></p>", .exp = 1 },
|
||||
.{ .q = "span:last-child", .html = "<span>a span</span> and some text", .exp = 1 },
|
||||
.{ .q = "p:nth-of-type(2)", .html = "<address></address><p id=1><p id=2>", .exp = 1 },
|
||||
.{ .q = "p:nth-last-of-type(2)", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||
.{ .q = "p:last-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||
.{ .q = "p:first-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||
.{ .q = "p:only-child", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p></div>", .exp = 1 },
|
||||
.{ .q = "p:only-of-type", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p><p id=\"3\"></p></div>", .exp = 1 },
|
||||
.{ .q = ":empty", .html = "<p id=\"1\"><!-- --><p id=\"2\">Hello<p id=\"3\"><span>", .exp = 1 },
|
||||
.{ .q = "div p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
||||
.{ .q = "div table p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
||||
.{ .q = "div > p", .html = "<div><p id=\"1\"><div><p id=\"2\"></div><table><tr><td><p id=\"3\"></table></div>", .exp = 1 },
|
||||
.{ .q = "p ~ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
||||
.{ .q = "p + p", .html = "<p id=\"1\"></p> <!--comment--> <p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
||||
.{ .q = "li, p", .html = "<ul><li></li><li></li></ul><p>", .exp = 1 },
|
||||
.{ .q = "p +/*This is a comment*/ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
||||
// .{ .q = "p:contains(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||
// .{ .q = "p:containsOwn(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
|
||||
// .{ .q = ":containsOwn(\"inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||
// .{ .q = "p:containsOwn(\"block\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||
// .{ .q = "div:has(#p1)", .html = "<div id=\"d1\"><p id=\"p1\"><span>text content</span></p></div><div id=\"d2\"/>", .exp = 1 },
|
||||
// .{ .q = "div:has(:containsOwn(\"2\"))", .html = "<div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p>contents <em>2</em></p></div>", .exp = 1 },
|
||||
// .{ .q = "body :has(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
||||
// .{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
||||
// .{ .q = "p:matches([\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "p:matches([a-z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "p:matches([a-zA-Z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "p:matches([^\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "p:matches(^(0|a))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "p:matches(^\\d+$)", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "p:not(:matches(^\\d+$))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "div :matchesOwn(^\\d+$)", .html = "<div><p id=\"p1\">01234<em>567</em>89</p><div>", .exp = 1 },
|
||||
// .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"></a> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"></a> <li><a id=\"a2\" href=\"http://finance.untrusted.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"/> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
||||
// .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"/> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"></a> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
||||
.{ .q = ":input", .html = "<form> <label>Username <input type=\"text\" name=\"username\" /></label> <label>Password <input type=\"password\" name=\"password\" /></label> <label>Country <select name=\"country\"> <option value=\"ca\">Canada</option> <option value=\"us\">United States</option> </select> </label> <label>Bio <textarea name=\"bio\"></textarea></label> <button>Sign up</button> </form>", .exp = 1 },
|
||||
.{ .q = ":root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "*:root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "html:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "*:root:first-child", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "*:root:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "a:not(:root)", .html = "<html><head></head><body><a href=\"http://www.foo.com\"></a></body></html>", .exp = 1 },
|
||||
.{ .q = "body > *:nth-child(3n+2)", .html = "<html><head></head><body><p></p><div></div><span></span><a></a><form></form></body></html>", .exp = 1 },
|
||||
.{ .q = "input:disabled", .html = "<html><head></head><body><fieldset disabled><legend id=\"1\"><input id=\"i1\"/></legend><legend id=\"2\"><input id=\"i2\"/></legend></fieldset></body></html>", .exp = 1 },
|
||||
.{ .q = ":disabled", .html = "<html><head></head><body><fieldset disabled></fieldset></body></html>", .exp = 1 },
|
||||
.{ .q = ":enabled", .html = "<html><head></head><body><fieldset></fieldset></body></html>", .exp = 1 },
|
||||
.{ .q = "div.class1, div.class2", .html = "<div class=class1></div><div class=class2></div><div class=class3></div>", .exp = 1 },
|
||||
};
|
||||
|
||||
for (testcases) |tc| {
|
||||
matcher.reset();
|
||||
|
||||
const doc = try parser.documentHTMLParseFromStr(tc.html);
|
||||
defer parser.documentHTMLClose(doc) catch {};
|
||||
|
||||
const s = css.parse(alloc, tc.q, .{}) catch |e| {
|
||||
std.debug.print("parse, query: {s}\n", .{tc.q});
|
||||
return e;
|
||||
};
|
||||
|
||||
defer s.deinit(alloc);
|
||||
|
||||
const node = Node{ .node = parser.documentHTMLToNode(doc) };
|
||||
|
||||
_ = css.matchFirst(s, node, &matcher) catch |e| {
|
||||
std.debug.print("match, query: {s}\n", .{tc.q});
|
||||
return e;
|
||||
};
|
||||
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
|
||||
std.debug.print("expectation, query: {s}\n", .{tc.q});
|
||||
return e;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test "matchAll" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
var matcher = Matcher.init(alloc);
|
||||
defer matcher.deinit();
|
||||
|
||||
const testcases = [_]struct {
|
||||
q: []const u8,
|
||||
html: []const u8,
|
||||
exp: usize,
|
||||
}{
|
||||
.{ .q = "address", .html = "<body><address>This address...</address></body>", .exp = 1 },
|
||||
.{ .q = "*", .html = "<!-- comment --><html><head></head><body>text</body></html>", .exp = 3 },
|
||||
.{ .q = "*", .html = "<html><head></head><body></body></html>", .exp = 3 },
|
||||
.{ .q = "#foo", .html = "<p id=\"foo\"><p id=\"bar\">", .exp = 1 },
|
||||
.{ .q = "li#t1", .html = "<ul><li id=\"t1\"><p id=\"t1\">", .exp = 1 },
|
||||
.{ .q = ".t3", .html = "<ul><li class=\"t1\"><li class=\"t2 t3\">", .exp = 1 },
|
||||
.{ .q = "*#t4", .html = "<ol><li id=\"t4\"><li id=\"t44\">", .exp = 1 },
|
||||
.{ .q = ".t1", .html = "<ul><li class=\"t1\"><li class=\"t2\">", .exp = 1 },
|
||||
.{ .q = "p.t1", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
||||
.{ .q = "div.teST", .html = "<div class=\"test\">", .exp = 0 },
|
||||
.{ .q = ".t1.fail", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
||||
.{ .q = "p.t1.t2", .html = "<p class=\"t1 t2\">", .exp = 1 },
|
||||
.{ .q = "p.--t1", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
||||
.{ .q = "p.--t1.--t2", .html = "<p class=\"--t1 --t2\">", .exp = 1 },
|
||||
.{ .q = "p[title]", .html = "<p><p title=\"title\">", .exp = 1 },
|
||||
.{ .q = "div[class=\"red\" i]", .html = "<div><div class=\"Red\">", .exp = 1 },
|
||||
.{ .q = "address[title=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 1 },
|
||||
.{ .q = "address[title=\"FoOIgnoRECaSe\" i]", .html = "<address><address title=\"fooIgnoreCase\"><address title=\"bar\">", .exp = 1 },
|
||||
.{ .q = "address[title!=\"foo\"]", .html = "<address><address title=\"foo\"><address title=\"bar\">", .exp = 2 },
|
||||
.{ .q = "address[title!=\"foo\" i]", .html = "<address><address title=\"FOO\"><address title=\"bar\">", .exp = 2 },
|
||||
.{ .q = "p[title!=\"FooBarUFoo\" i]", .html = "<p title=\"fooBARuFOO\"><p title=\"varfoo\">", .exp = 1 },
|
||||
.{ .q = "[ title ~= foo ]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
||||
.{ .q = "p[title~=\"FOO\" i]", .html = "<p title=\"tot foo bar\">", .exp = 1 },
|
||||
.{ .q = "p[title~=toofoo i]", .html = "<p title=\"tot foo bar\">", .exp = 0 },
|
||||
.{ .q = "[title~=\"hello world\"]", .html = "<p title=\"hello world\">", .exp = 0 },
|
||||
.{ .q = "[title~=\"hello\" i]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
||||
.{ .q = "[title~=\"hello\" I]", .html = "<p title=\"HELLO world\">", .exp = 1 },
|
||||
.{ .q = "[lang|=\"en\"]", .html = "<p lang=\"en\"><p lang=\"en-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
||||
.{ .q = "[lang|=\"EN\" i]", .html = "<p lang=\"en\"><p lang=\"En-gb\"><p lang=\"enough\"><p lang=\"fr-en\">", .exp = 2 },
|
||||
.{ .q = "[title^=\"foo\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||
.{ .q = "[title^=\"foo\" i]", .html = "<p title=\"FooBAR\"><p title=\"barfoo\">", .exp = 1 },
|
||||
.{ .q = "[title$=\"bar\"]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||
.{ .q = "[title$=\"BAR\" i]", .html = "<p title=\"foobar\"><p title=\"barfoo\">", .exp = 1 },
|
||||
.{ .q = "[title*=\"bar\"]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||
.{ .q = "[title*=\"BaRu\" i]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||
.{ .q = "[title*=\"BaRu\" I]", .html = "<p title=\"foobarufoo\">", .exp = 1 },
|
||||
.{ .q = "p[class$=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class$=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class^=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class^=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class*=\" \"]", .html = "<p class=\" \">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "p[class*=\"\"]", .html = "<p class=\"\">This text should be green.</p><p>This text should be green.</p>", .exp = 0 },
|
||||
.{ .q = "input[name=Sex][value=F]", .html = "<input type=\"radio\" name=\"Sex\" value=\"F\"/>", .exp = 1 },
|
||||
.{ .q = "table[border=\"0\"][cellpadding=\"0\"][cellspacing=\"0\"]", .html = "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"table-layout: fixed; width: 100%; border: 0 dashed; border-color: #FFFFFF\"><tr style=\"height:64px\">aaa</tr></table>", .exp = 1 },
|
||||
.{ .q = ".t1:not(.t2)", .html = "<p class=\"t1 t2\">", .exp = 0 },
|
||||
.{ .q = "div:not(.t1)", .html = "<div class=\"t3\">", .exp = 1 },
|
||||
.{ .q = "div:not([class=\"t2\"])", .html = "<div><div class=\"t2\"><div class=\"t3\">", .exp = 2 },
|
||||
.{ .q = "li:nth-child(odd)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 2 },
|
||||
.{ .q = "li:nth-child(even)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 2 },
|
||||
.{ .q = "li:nth-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3></ol>", .exp = 1 },
|
||||
.{ .q = "li:nth-last-child(odd)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
||||
.{ .q = "li:nth-last-child(even)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
||||
.{ .q = "li:nth-last-child(-n+2)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
||||
.{ .q = "li:nth-last-child(3n+1)", .html = "<ol><li id=1><li id=2><li id=3><li id=4></ol>", .exp = 2 },
|
||||
.{ .q = "span:first-child", .html = "<p>some text <span id=\"1\">and a span</span><span id=\"2\"> and another</span></p>", .exp = 1 },
|
||||
.{ .q = "span:last-child", .html = "<span>a span</span> and some text", .exp = 1 },
|
||||
.{ .q = "p:nth-of-type(2)", .html = "<address></address><p id=1><p id=2>", .exp = 1 },
|
||||
.{ .q = "p:nth-last-of-type(2)", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||
.{ .q = "p:last-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||
.{ .q = "p:first-of-type", .html = "<address></address><p id=1><p id=2></p><a>", .exp = 1 },
|
||||
.{ .q = "p:only-child", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p></div>", .exp = 1 },
|
||||
.{ .q = "p:only-of-type", .html = "<div><p id=\"1\"></p><a></a></div><div><p id=\"2\"></p><p id=\"3\"></p></div>", .exp = 1 },
|
||||
.{ .q = ":empty", .html = "<p id=\"1\"><!-- --><p id=\"2\">Hello<p id=\"3\"><span>", .exp = 3 },
|
||||
.{ .q = "div p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 2 },
|
||||
.{ .q = "div table p", .html = "<div><p id=\"1\"><table><tr><td><p id=\"2\"></table></div><p id=\"3\">", .exp = 1 },
|
||||
.{ .q = "div > p", .html = "<div><p id=\"1\"><div><p id=\"2\"></div><table><tr><td><p id=\"3\"></table></div>", .exp = 2 },
|
||||
.{ .q = "p ~ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 2 },
|
||||
.{ .q = "p + p", .html = "<p id=\"1\"></p> <!--comment--> <p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
||||
.{ .q = "li, p", .html = "<ul><li></li><li></li></ul><p>", .exp = 3 },
|
||||
.{ .q = "p +/*This is a comment*/ p", .html = "<p id=\"1\"><p id=\"2\"></p><address></address><p id=\"3\">", .exp = 1 },
|
||||
// .{ .q = "p:contains(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||
// .{ .q = "p:containsOwn(\"that wraps\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 0 },
|
||||
// .{ .q = ":containsOwn(\"inner\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||
// .{ .q = "p:containsOwn(\"block\")", .html = "<p>Text block that <span>wraps inner text</span> and continues</p>", .exp = 1 },
|
||||
.{ .q = "div:has(#p1)", .html = "<div id=\"d1\"><p id=\"p1\"><span>text content</span></p></div><div id=\"d2\"/>", .exp = 1 },
|
||||
// .{ .q = "div:has(:containsOwn(\"2\"))", .html = "<div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p>contents <em>2</em></p></div>", .exp = 1 },
|
||||
// .{ .q = "body :has(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 2 },
|
||||
// .{ .q = "body :haschild(:containsOwn(\"2\"))", .html = "<body><div id=\"d1\"><p id=\"p1\"><span>contents 1</span></p></div> <div id=\"d2\"><p id=\"p2\">contents <em>2</em></p></div></body>", .exp = 1 },
|
||||
// .{ .q = "p:matches([\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
||||
// .{ .q = "p:matches([a-z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "p:matches([a-zA-Z])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
||||
// .{ .q = "p:matches([^\\d])", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
||||
// .{ .q = "p:matches(^(0|a))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 3 },
|
||||
// .{ .q = "p:matches(^\\d+$)", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 1 },
|
||||
// .{ .q = "p:not(:matches(^\\d+$))", .html = "<p id=\"p1\">0123456789</p><p id=\"p2\">abcdef</p><p id=\"p3\">0123ABCD</p>", .exp = 2 },
|
||||
// .{ .q = "div :matchesOwn(^\\d+$)", .html = "<div><p id=\"p1\">01234<em>567</em>89</p><div>", .exp = 2 },
|
||||
// .{ .q = "[href#=(fina)]:not([href#=(\\/\\/[^\\/]+untrusted)])", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"></a> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"></a> <li><a id=\"a2\" href=\"http://finance.untrusted.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"/> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 2 },
|
||||
// .{ .q = "[href#=(^https:\\/\\/[^\\/]*\\/?news)]", .html = "<ul> <li><a id=\"a1\" href=\"http://www.google.com/finance\"/> <li><a id=\"a2\" href=\"http://finance.yahoo.com/\"/> <li><a id=\"a3\" href=\"https://www.google.com/news\"></a> <li><a id=\"a4\" href=\"http://news.yahoo.com\"/> </ul>", .exp = 1 },
|
||||
.{ .q = ":input", .html = "<form> <label>Username <input type=\"text\" name=\"username\" /></label> <label>Password <input type=\"password\" name=\"password\" /></label> <label>Country <select name=\"country\"> <option value=\"ca\">Canada</option> <option value=\"us\">United States</option> </select> </label> <label>Bio <textarea name=\"bio\"></textarea></label> <button>Sign up</button> </form>", .exp = 5 },
|
||||
.{ .q = ":root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "*:root", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "html:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "*:root:first-child", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "*:root:nth-child(1)", .html = "<html><head></head><body></body></html>", .exp = 1 },
|
||||
.{ .q = "a:not(:root)", .html = "<html><head></head><body><a href=\"http://www.foo.com\"></a></body></html>", .exp = 1 },
|
||||
.{ .q = "body > *:nth-child(3n+2)", .html = "<html><head></head><body><p></p><div></div><span></span><a></a><form></form></body></html>", .exp = 2 },
|
||||
.{ .q = "input:disabled", .html = "<html><head></head><body><fieldset disabled><legend id=\"1\"><input id=\"i1\"/></legend><legend id=\"2\"><input id=\"i2\"/></legend></fieldset></body></html>", .exp = 1 },
|
||||
.{ .q = ":disabled", .html = "<html><head></head><body><fieldset disabled></fieldset></body></html>", .exp = 1 },
|
||||
.{ .q = ":enabled", .html = "<html><head></head><body><fieldset></fieldset></body></html>", .exp = 1 },
|
||||
.{ .q = "div.class1, div.class2", .html = "<div class=class1></div><div class=class2></div><div class=class3></div>", .exp = 2 },
|
||||
};
|
||||
|
||||
for (testcases) |tc| {
|
||||
matcher.reset();
|
||||
|
||||
const doc = try parser.documentHTMLParseFromStr(tc.html);
|
||||
defer parser.documentHTMLClose(doc) catch {};
|
||||
|
||||
const s = css.parse(alloc, tc.q, .{}) catch |e| {
|
||||
std.debug.print("parse, query: {s}\n", .{tc.q});
|
||||
return e;
|
||||
};
|
||||
defer s.deinit(alloc);
|
||||
|
||||
const node = Node{ .node = parser.documentHTMLToNode(doc) };
|
||||
|
||||
_ = css.matchAll(s, node, &matcher) catch |e| {
|
||||
std.debug.print("match, query: {s}\n", .{tc.q});
|
||||
return e;
|
||||
};
|
||||
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
|
||||
std.debug.print("expectation, query: {s}\n", .{tc.q});
|
||||
return e;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,587 +0,0 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const css = @import("css.zig");
|
||||
|
||||
// Node mock implementation for test only.
|
||||
pub const Node = struct {
|
||||
child: ?*const Node = null,
|
||||
last: ?*const Node = null,
|
||||
sibling: ?*const Node = null,
|
||||
prev: ?*const Node = null,
|
||||
par: ?*const Node = null,
|
||||
|
||||
name: []const u8 = "",
|
||||
att: ?[]const u8 = null,
|
||||
|
||||
pub fn firstChild(n: *const Node) !?*const Node {
|
||||
return n.child;
|
||||
}
|
||||
|
||||
pub fn lastChild(n: *const Node) !?*const Node {
|
||||
return n.last;
|
||||
}
|
||||
|
||||
pub fn nextSibling(n: *const Node) !?*const Node {
|
||||
return n.sibling;
|
||||
}
|
||||
|
||||
pub fn prevSibling(n: *const Node) !?*const Node {
|
||||
return n.prev;
|
||||
}
|
||||
|
||||
pub fn parent(n: *const Node) !?*const Node {
|
||||
return n.par;
|
||||
}
|
||||
|
||||
pub fn isElement(_: *const Node) bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn isDocument(_: *const Node) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn isComment(_: *const Node) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn isText(_: *const Node) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn isEmptyText(_: *const Node) !bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn tag(n: *const Node) ![]const u8 {
|
||||
return n.name;
|
||||
}
|
||||
|
||||
pub fn attr(n: *const Node, _: []const u8) !?[]const u8 {
|
||||
return n.att;
|
||||
}
|
||||
|
||||
pub fn eql(a: *const Node, b: *const Node) bool {
|
||||
return a == b;
|
||||
}
|
||||
};
|
||||
|
||||
const Matcher = struct {
|
||||
const Nodes = std.ArrayList(*const Node);
|
||||
|
||||
nodes: Nodes,
|
||||
|
||||
fn init(alloc: std.mem.Allocator) Matcher {
|
||||
return .{ .nodes = Nodes.init(alloc) };
|
||||
}
|
||||
|
||||
fn deinit(m: *Matcher) void {
|
||||
m.nodes.deinit();
|
||||
}
|
||||
|
||||
fn reset(m: *Matcher) void {
|
||||
m.nodes.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
pub fn match(m: *Matcher, n: *const Node) !void {
|
||||
try m.nodes.append(n);
|
||||
}
|
||||
};
|
||||
|
||||
test "matchFirst" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
var matcher = Matcher.init(alloc);
|
||||
defer matcher.deinit();
|
||||
|
||||
const testcases = [_]struct {
|
||||
q: []const u8,
|
||||
n: Node,
|
||||
exp: usize,
|
||||
}{
|
||||
.{
|
||||
.q = "address",
|
||||
.n = .{ .child = &.{ .name = "body", .child = &.{ .name = "address" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "#foo",
|
||||
.n = .{ .child = &.{ .name = "p", .att = "foo", .child = &.{ .name = "p" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = ".t1",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "t1" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = ".t1",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "foo t1" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "[foo]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo=baz]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "[foo!=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo!=baz]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo~=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "baz bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo~=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "[foo^=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo$=baz]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo*=rb]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo|=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo|=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar-baz" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo|=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "strong, a",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "p a",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .name = "p" } }, .sibling = &.{ .name = "a" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "p a",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "span", .child = &.{
|
||||
.name = "a",
|
||||
.par = &.{ .name = "span", .par = &.{ .name = "p" } },
|
||||
} } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = ":not(p)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "p:has(a)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "p:has(strong)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "p:haschild(a)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "p:haschild(strong)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "p:lang(en)",
|
||||
.n = .{ .child = &.{ .name = "p", .att = "en-US", .child = &.{ .name = "a" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "a:lang(en)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .att = "en-US" } } } },
|
||||
.exp = 1,
|
||||
},
|
||||
};
|
||||
|
||||
for (testcases) |tc| {
|
||||
matcher.reset();
|
||||
|
||||
const s = try css.parse(alloc, tc.q, .{});
|
||||
defer s.deinit(alloc);
|
||||
|
||||
_ = css.matchFirst(s, &tc.n, &matcher) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
|
||||
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test "matchAll" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
var matcher = Matcher.init(alloc);
|
||||
defer matcher.deinit();
|
||||
|
||||
const testcases = [_]struct {
|
||||
q: []const u8,
|
||||
n: Node,
|
||||
exp: usize,
|
||||
}{
|
||||
.{
|
||||
.q = "address",
|
||||
.n = .{ .child = &.{ .name = "body", .child = &.{ .name = "address" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "#foo",
|
||||
.n = .{ .child = &.{ .name = "p", .att = "foo", .child = &.{ .name = "p" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = ".t1",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "t1" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = ".t1",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "foo t1" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "[foo]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo=baz]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "[foo!=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo!=baz]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 2,
|
||||
},
|
||||
.{
|
||||
.q = "[foo~=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "baz bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo~=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "[foo^=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo$=baz]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo*=rb]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo|=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo|=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar-baz" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo|=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "strong, a",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 2,
|
||||
},
|
||||
.{
|
||||
.q = "p a",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .name = "p" } }, .sibling = &.{ .name = "a" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "p a",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "span", .child = &.{
|
||||
.name = "a",
|
||||
.par = &.{ .name = "span", .par = &.{ .name = "p" } },
|
||||
} } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = ":not(p)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 2,
|
||||
},
|
||||
.{
|
||||
.q = "p:has(a)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "p:has(strong)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "p:haschild(a)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "p:haschild(strong)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "p:lang(en)",
|
||||
.n = .{ .child = &.{ .name = "p", .att = "en-US", .child = &.{ .name = "a" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "a:lang(en)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .att = "en-US" } } } },
|
||||
.exp = 1,
|
||||
},
|
||||
};
|
||||
|
||||
for (testcases) |tc| {
|
||||
matcher.reset();
|
||||
|
||||
const s = try css.parse(alloc, tc.q, .{});
|
||||
defer s.deinit(alloc);
|
||||
|
||||
css.matchAll(s, &tc.n, &matcher) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
|
||||
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test "pseudo class" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
var matcher = Matcher.init(alloc);
|
||||
defer matcher.deinit();
|
||||
|
||||
var p1: Node = .{ .name = "p" };
|
||||
var p2: Node = .{ .name = "p" };
|
||||
var a1: Node = .{ .name = "a" };
|
||||
|
||||
p1.sibling = &p2;
|
||||
p2.prev = &p1;
|
||||
|
||||
p2.sibling = &a1;
|
||||
a1.prev = &p2;
|
||||
|
||||
var root: Node = .{ .child = &p1, .last = &a1 };
|
||||
p1.par = &root;
|
||||
p2.par = &root;
|
||||
a1.par = &root;
|
||||
|
||||
const testcases = [_]struct {
|
||||
q: []const u8,
|
||||
n: Node,
|
||||
exp: ?*const Node,
|
||||
}{
|
||||
.{ .q = "p:only-child", .n = root, .exp = null },
|
||||
.{ .q = "a:only-of-type", .n = root, .exp = &a1 },
|
||||
};
|
||||
|
||||
for (testcases) |tc| {
|
||||
matcher.reset();
|
||||
|
||||
const s = try css.parse(alloc, tc.q, .{});
|
||||
defer s.deinit(alloc);
|
||||
|
||||
css.matchAll(s, &tc.n, &matcher) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
|
||||
if (tc.exp) |exp_n| {
|
||||
const exp: usize = 1;
|
||||
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
|
||||
std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const exp: usize = 0;
|
||||
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test "nth pseudo class" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
var matcher = Matcher.init(alloc);
|
||||
defer matcher.deinit();
|
||||
|
||||
var p1: Node = .{ .name = "p" };
|
||||
var p2: Node = .{ .name = "p" };
|
||||
|
||||
p1.sibling = &p2;
|
||||
p2.prev = &p1;
|
||||
|
||||
var root: Node = .{ .child = &p1, .last = &p2 };
|
||||
p1.par = &root;
|
||||
p2.par = &root;
|
||||
|
||||
const testcases = [_]struct {
|
||||
q: []const u8,
|
||||
n: Node,
|
||||
exp: ?*const Node,
|
||||
}{
|
||||
.{ .q = "a:nth-of-type(1)", .n = root, .exp = null },
|
||||
.{ .q = "p:nth-of-type(1)", .n = root, .exp = &p1 },
|
||||
.{ .q = "p:nth-of-type(2)", .n = root, .exp = &p2 },
|
||||
.{ .q = "p:nth-of-type(0)", .n = root, .exp = null },
|
||||
.{ .q = "p:nth-of-type(2n)", .n = root, .exp = &p2 },
|
||||
.{ .q = "p:nth-last-child(1)", .n = root, .exp = &p2 },
|
||||
.{ .q = "p:nth-last-child(2)", .n = root, .exp = &p1 },
|
||||
.{ .q = "p:nth-child(1)", .n = root, .exp = &p1 },
|
||||
.{ .q = "p:nth-child(2)", .n = root, .exp = &p2 },
|
||||
.{ .q = "p:nth-child(odd)", .n = root, .exp = &p1 },
|
||||
.{ .q = "p:nth-child(even)", .n = root, .exp = &p2 },
|
||||
.{ .q = "p:nth-child(n+2)", .n = root, .exp = &p2 },
|
||||
};
|
||||
|
||||
for (testcases) |tc| {
|
||||
matcher.reset();
|
||||
|
||||
const s = try css.parse(alloc, tc.q, .{});
|
||||
defer s.deinit(alloc);
|
||||
|
||||
css.matchAll(s, &tc.n, &matcher) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
|
||||
if (tc.exp) |exp_n| {
|
||||
const exp: usize = 1;
|
||||
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
|
||||
std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const exp: usize = 0;
|
||||
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
// see https://github.com/andybalholm/cascadia/blob/master/parser.go
|
||||
const std = @import("std");
|
||||
const ascii = std.ascii;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const selector = @import("selector.zig");
|
||||
const Selector = selector.Selector;
|
||||
@@ -77,8 +78,8 @@ pub const Parser = struct {
|
||||
|
||||
opts: ParseOptions,
|
||||
|
||||
pub fn parse(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
return p.parseSelectorGroup(alloc);
|
||||
pub fn parse(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
return p.parseSelectorGroup(allocator);
|
||||
}
|
||||
|
||||
// skipWhitespace consumes whitespace characters and comments.
|
||||
@@ -115,13 +116,13 @@ pub const Parser = struct {
|
||||
|
||||
// parseSimpleSelectorSequence parses a selector sequence that applies to
|
||||
// a single element.
|
||||
fn parseSimpleSelectorSequence(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
fn parseSimpleSelectorSequence(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
if (p.i >= p.s.len) {
|
||||
return ParseError.ExpectedSelector;
|
||||
}
|
||||
|
||||
var buf = std.ArrayList(Selector).init(alloc);
|
||||
defer buf.deinit();
|
||||
var buf: std.ArrayListUnmanaged(Selector) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
|
||||
switch (p.s[p.i]) {
|
||||
'*' => {
|
||||
@@ -138,20 +139,20 @@ pub const Parser = struct {
|
||||
// There's no type selector. Wait to process the other till the
|
||||
// main loop.
|
||||
},
|
||||
else => try buf.append(try p.parseTypeSelector(alloc)),
|
||||
else => try buf.append(allocator, try p.parseTypeSelector(allocator)),
|
||||
}
|
||||
|
||||
var pseudo_elt: ?PseudoClass = null;
|
||||
|
||||
loop: while (p.i < p.s.len) {
|
||||
var ns: Selector = switch (p.s[p.i]) {
|
||||
'#' => try p.parseIDSelector(alloc),
|
||||
'.' => try p.parseClassSelector(alloc),
|
||||
'[' => try p.parseAttributeSelector(alloc),
|
||||
':' => try p.parsePseudoclassSelector(alloc),
|
||||
'#' => try p.parseIDSelector(allocator),
|
||||
'.' => try p.parseClassSelector(allocator),
|
||||
'[' => try p.parseAttributeSelector(allocator),
|
||||
':' => try p.parsePseudoclassSelector(allocator),
|
||||
else => break :loop,
|
||||
};
|
||||
errdefer ns.deinit(alloc);
|
||||
errdefer ns.deinit(allocator);
|
||||
|
||||
// From https://drafts.csswg.org/selectors-3/#pseudo-elements :
|
||||
// "Only one pseudo-element may appear per selector, and if present
|
||||
@@ -165,28 +166,32 @@ pub const Parser = struct {
|
||||
if (!p.opts.accept_pseudo_elts) return ParseError.PseudoElementDisabled;
|
||||
|
||||
pseudo_elt = e;
|
||||
ns.deinit(alloc);
|
||||
ns.deinit(allocator);
|
||||
},
|
||||
else => {
|
||||
if (pseudo_elt != null) return ParseError.PseudoElementNotAtSelectorEnd;
|
||||
try buf.append(ns);
|
||||
try buf.append(allocator, ns);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// no need wrap the selectors in compoundSelector
|
||||
if (buf.items.len == 1 and pseudo_elt == null) return buf.items[0];
|
||||
if (buf.items.len == 1 and pseudo_elt == null) {
|
||||
return buf.items[0];
|
||||
}
|
||||
|
||||
return .{ .compound = .{ .selectors = try buf.toOwnedSlice(), .pseudo_elt = pseudo_elt } };
|
||||
return .{
|
||||
.compound = .{ .selectors = try buf.toOwnedSlice(allocator), .pseudo_elt = pseudo_elt },
|
||||
};
|
||||
}
|
||||
|
||||
// parseTypeSelector parses a type selector (one that matches by tag name).
|
||||
fn parseTypeSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
try p.parseIdentifier(buf.writer());
|
||||
fn parseTypeSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
try p.parseIdentifier(buf.writer(allocator));
|
||||
|
||||
return .{ .tag = try buf.toOwnedSlice() };
|
||||
return .{ .tag = try buf.toOwnedSlice(allocator) };
|
||||
}
|
||||
|
||||
// parseIdentifier parses an identifier.
|
||||
@@ -314,47 +319,47 @@ pub const Parser = struct {
|
||||
}
|
||||
|
||||
// parseIDSelector parses a selector that matches by id attribute.
|
||||
fn parseIDSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
fn parseIDSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedIDSelector;
|
||||
if (p.s[p.i] != '#') return ParseError.ExpectedIDSelector;
|
||||
|
||||
p.i += 1;
|
||||
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
|
||||
try p.parseName(buf.writer());
|
||||
return .{ .id = try buf.toOwnedSlice() };
|
||||
try p.parseName(buf.writer(allocator));
|
||||
return .{ .id = try buf.toOwnedSlice(allocator) };
|
||||
}
|
||||
|
||||
// parseClassSelector parses a selector that matches by class attribute.
|
||||
fn parseClassSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
fn parseClassSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedClassSelector;
|
||||
if (p.s[p.i] != '.') return ParseError.ExpectedClassSelector;
|
||||
|
||||
p.i += 1;
|
||||
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
|
||||
try p.parseIdentifier(buf.writer());
|
||||
return .{ .class = try buf.toOwnedSlice() };
|
||||
try p.parseIdentifier(buf.writer(allocator));
|
||||
return .{ .class = try buf.toOwnedSlice(allocator) };
|
||||
}
|
||||
|
||||
// parseAttributeSelector parses a selector that matches by attribute value.
|
||||
fn parseAttributeSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
fn parseAttributeSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedAttributeSelector;
|
||||
if (p.s[p.i] != '[') return ParseError.ExpectedAttributeSelector;
|
||||
|
||||
p.i += 1;
|
||||
_ = p.skipWhitespace();
|
||||
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
|
||||
try p.parseIdentifier(buf.writer());
|
||||
const key = try buf.toOwnedSlice();
|
||||
errdefer alloc.free(key);
|
||||
try p.parseIdentifier(buf.writer(allocator));
|
||||
const key = try buf.toOwnedSlice(allocator);
|
||||
errdefer allocator.free(key);
|
||||
|
||||
lowerstr(key);
|
||||
|
||||
@@ -377,12 +382,12 @@ pub const Parser = struct {
|
||||
var is_val: bool = undefined;
|
||||
if (op == .regexp) {
|
||||
is_val = false;
|
||||
try p.parseRegex(buf.writer());
|
||||
try p.parseRegex(buf.writer(allocator));
|
||||
} else {
|
||||
is_val = true;
|
||||
switch (p.s[p.i]) {
|
||||
'\'', '"' => try p.parseString(buf.writer()),
|
||||
else => try p.parseIdentifier(buf.writer()),
|
||||
'\'', '"' => try p.parseString(buf.writer(allocator)),
|
||||
else => try p.parseIdentifier(buf.writer(allocator)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,8 +409,8 @@ pub const Parser = struct {
|
||||
|
||||
return .{ .attribute = .{
|
||||
.key = key,
|
||||
.val = if (is_val) try buf.toOwnedSlice() else null,
|
||||
.regexp = if (!is_val) try buf.toOwnedSlice() else null,
|
||||
.val = if (is_val) try buf.toOwnedSlice(allocator) else null,
|
||||
.regexp = if (!is_val) try buf.toOwnedSlice(allocator) else null,
|
||||
.op = op,
|
||||
.ci = ci,
|
||||
} };
|
||||
@@ -498,7 +503,7 @@ pub const Parser = struct {
|
||||
// parsePseudoclassSelector parses a pseudoclass selector like :not(p) or a pseudo-element
|
||||
// For backwards compatibility, both ':' and '::' prefix are allowed for pseudo-elements.
|
||||
// https://drafts.csswg.org/selectors-3/#pseudo-elements
|
||||
fn parsePseudoclassSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
fn parsePseudoclassSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedPseudoClassSelector;
|
||||
if (p.s[p.i] != ':') return ParseError.ExpectedPseudoClassSelector;
|
||||
|
||||
@@ -511,10 +516,10 @@ pub const Parser = struct {
|
||||
p.i += 1;
|
||||
}
|
||||
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
|
||||
try p.parseIdentifier(buf.writer());
|
||||
try p.parseIdentifier(buf.writer(allocator));
|
||||
|
||||
const pseudo_class = try PseudoClass.parse(buf.items);
|
||||
|
||||
@@ -527,11 +532,11 @@ pub const Parser = struct {
|
||||
.not, .has, .haschild => {
|
||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
||||
|
||||
const sel = try p.parseSelectorGroup(alloc);
|
||||
const sel = try p.parseSelectorGroup(allocator);
|
||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
||||
|
||||
const s = try alloc.create(Selector);
|
||||
errdefer alloc.destroy(s);
|
||||
const s = try allocator.create(Selector);
|
||||
errdefer allocator.destroy(s);
|
||||
s.* = sel;
|
||||
|
||||
return .{ .pseudo_class_relative = .{ .pseudo_class = pseudo_class, .match = s } };
|
||||
@@ -541,33 +546,31 @@ pub const Parser = struct {
|
||||
if (p.i == p.s.len) return ParseError.UnmatchParenthesis;
|
||||
|
||||
switch (p.s[p.i]) {
|
||||
'\'', '"' => try p.parseString(buf.writer()),
|
||||
else => try p.parseString(buf.writer()),
|
||||
'\'', '"' => try p.parseString(buf.writer(allocator)),
|
||||
else => try p.parseString(buf.writer(allocator)),
|
||||
}
|
||||
|
||||
_ = p.skipWhitespace();
|
||||
if (p.i >= p.s.len) return ParseError.InvalidPseudoClass;
|
||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
||||
|
||||
const val = try buf.toOwnedSlice();
|
||||
errdefer alloc.free(val);
|
||||
|
||||
lowerstr(val);
|
||||
const val = try buf.toOwnedSlice(allocator);
|
||||
errdefer allocator.free(val);
|
||||
|
||||
return .{ .pseudo_class_contains = .{ .own = pseudo_class == .containsown, .val = val } };
|
||||
},
|
||||
.matches, .matchesown => {
|
||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
||||
|
||||
try p.parseRegex(buf.writer());
|
||||
try p.parseRegex(buf.writer(allocator));
|
||||
if (p.i >= p.s.len) return ParseError.InvalidPseudoClassSelector;
|
||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
||||
|
||||
return .{ .pseudo_class_regexp = .{ .own = pseudo_class == .matchesown, .regexp = try buf.toOwnedSlice() } };
|
||||
return .{ .pseudo_class_regexp = .{ .own = pseudo_class == .matchesown, .regexp = try buf.toOwnedSlice(allocator) } };
|
||||
},
|
||||
.nth_child, .nth_last_child, .nth_of_type, .nth_last_of_type => {
|
||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
||||
const nth = try p.parseNth(alloc);
|
||||
const nth = try p.parseNth(allocator);
|
||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
||||
|
||||
const last = pseudo_class == .nth_last_child or pseudo_class == .nth_last_of_type;
|
||||
@@ -587,14 +590,14 @@ pub const Parser = struct {
|
||||
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
|
||||
if (p.i == p.s.len) return ParseError.UnmatchParenthesis;
|
||||
|
||||
try p.parseIdentifier(buf.writer());
|
||||
try p.parseIdentifier(buf.writer(allocator));
|
||||
|
||||
_ = p.skipWhitespace();
|
||||
if (p.i >= p.s.len) return ParseError.InvalidPseudoClass;
|
||||
if (!p.consumeClosingParenthesis()) return ParseError.ExpectedParenthesisClose;
|
||||
|
||||
const val = try buf.toOwnedSlice();
|
||||
errdefer alloc.free(val);
|
||||
const val = try buf.toOwnedSlice(allocator);
|
||||
errdefer allocator.free(val);
|
||||
lowerstr(val);
|
||||
|
||||
return .{ .pseudo_class_lang = val };
|
||||
@@ -622,30 +625,32 @@ pub const Parser = struct {
|
||||
}
|
||||
|
||||
// parseSelectorGroup parses a group of selectors, separated by commas.
|
||||
fn parseSelectorGroup(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
const s = try p.parseSelector(alloc);
|
||||
fn parseSelectorGroup(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
const s = try p.parseSelector(allocator);
|
||||
|
||||
var buf = std.ArrayList(Selector).init(alloc);
|
||||
defer buf.deinit();
|
||||
var buf: std.ArrayListUnmanaged(Selector) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
|
||||
try buf.append(s);
|
||||
try buf.append(allocator, s);
|
||||
|
||||
while (p.i < p.s.len) {
|
||||
if (p.s[p.i] != ',') break;
|
||||
p.i += 1;
|
||||
const ss = try p.parseSelector(alloc);
|
||||
try buf.append(ss);
|
||||
const ss = try p.parseSelector(allocator);
|
||||
try buf.append(allocator, ss);
|
||||
}
|
||||
|
||||
if (buf.items.len == 1) return buf.items[0];
|
||||
if (buf.items.len == 1) {
|
||||
return buf.items[0];
|
||||
}
|
||||
|
||||
return .{ .group = try buf.toOwnedSlice() };
|
||||
return .{ .group = try buf.toOwnedSlice(allocator) };
|
||||
}
|
||||
|
||||
// parseSelector parses a selector that may include combinators.
|
||||
fn parseSelector(p: *Parser, alloc: std.mem.Allocator) ParseError!Selector {
|
||||
fn parseSelector(p: *Parser, allocator: Allocator) ParseError!Selector {
|
||||
_ = p.skipWhitespace();
|
||||
var s = try p.parseSimpleSelectorSequence(alloc);
|
||||
var s = try p.parseSimpleSelectorSequence(allocator);
|
||||
|
||||
while (true) {
|
||||
var combinator: Combinator = .empty;
|
||||
@@ -673,17 +678,21 @@ pub const Parser = struct {
|
||||
return s;
|
||||
}
|
||||
|
||||
const c = try p.parseSimpleSelectorSequence(alloc);
|
||||
const c = try p.parseSimpleSelectorSequence(allocator);
|
||||
|
||||
const first = try alloc.create(Selector);
|
||||
errdefer alloc.destroy(first);
|
||||
const first = try allocator.create(Selector);
|
||||
errdefer allocator.destroy(first);
|
||||
first.* = s;
|
||||
|
||||
const second = try alloc.create(Selector);
|
||||
errdefer alloc.destroy(second);
|
||||
const second = try allocator.create(Selector);
|
||||
errdefer allocator.destroy(second);
|
||||
second.* = c;
|
||||
|
||||
s = Selector{ .combined = .{ .first = first, .second = second, .combinator = combinator } };
|
||||
s = Selector{ .combined = .{
|
||||
.first = first,
|
||||
.second = second,
|
||||
.combinator = combinator,
|
||||
} };
|
||||
}
|
||||
|
||||
return s;
|
||||
@@ -776,7 +785,7 @@ pub const Parser = struct {
|
||||
}
|
||||
|
||||
// parseNth parses the argument for :nth-child (normally of the form an+b).
|
||||
fn parseNth(p: *Parser, alloc: std.mem.Allocator) ParseError![2]isize {
|
||||
fn parseNth(p: *Parser, allocator: Allocator) ParseError![2]isize {
|
||||
// initial state
|
||||
if (p.i >= p.s.len) return ParseError.ExpectedNthExpression;
|
||||
return switch (p.s[p.i]) {
|
||||
@@ -794,10 +803,10 @@ pub const Parser = struct {
|
||||
return p.parseNthReadN(1);
|
||||
},
|
||||
'o', 'O', 'e', 'E' => {
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
|
||||
try p.parseName(buf.writer());
|
||||
try p.parseName(buf.writer(allocator));
|
||||
|
||||
if (std.ascii.eqlIgnoreCase("odd", buf.items)) return .{ 2, 1 };
|
||||
if (std.ascii.eqlIgnoreCase("even", buf.items)) return .{ 2, 0 };
|
||||
@@ -873,7 +882,7 @@ test "parser.skipWhitespace" {
|
||||
}
|
||||
|
||||
test "parser.parseIdentifier" {
|
||||
const alloc = std.testing.allocator;
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const testcases = [_]struct {
|
||||
s: []const u8, // given value
|
||||
@@ -889,14 +898,14 @@ test "parser.parseIdentifier" {
|
||||
.{ .s = "a\\\"b", .exp = "a\"b" },
|
||||
};
|
||||
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
|
||||
for (testcases) |tc| {
|
||||
buf.clearRetainingCapacity();
|
||||
|
||||
var p = Parser{ .s = tc.s, .opts = .{} };
|
||||
p.parseIdentifier(buf.writer()) catch |e| {
|
||||
p.parseIdentifier(buf.writer(allocator)) catch |e| {
|
||||
// if error was expected, continue.
|
||||
if (tc.err) continue;
|
||||
|
||||
@@ -911,7 +920,7 @@ test "parser.parseIdentifier" {
|
||||
}
|
||||
|
||||
test "parser.parseString" {
|
||||
const alloc = std.testing.allocator;
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const testcases = [_]struct {
|
||||
s: []const u8, // given value
|
||||
@@ -930,14 +939,14 @@ test "parser.parseString" {
|
||||
.{ .s = "\"hello world\"", .exp = "hello world" },
|
||||
};
|
||||
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
defer buf.deinit();
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer buf.deinit(allocator);
|
||||
|
||||
for (testcases) |tc| {
|
||||
buf.clearRetainingCapacity();
|
||||
|
||||
var p = Parser{ .s = tc.s, .opts = .{} };
|
||||
p.parseString(buf.writer()) catch |e| {
|
||||
p.parseString(buf.writer(allocator)) catch |e| {
|
||||
// if error was expected, continue.
|
||||
if (tc.err) continue;
|
||||
|
||||
@@ -954,7 +963,7 @@ test "parser.parseString" {
|
||||
test "parser.parse" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
const allocator = arena.allocator();
|
||||
|
||||
const testcases = [_]struct {
|
||||
s: []const u8, // given value
|
||||
@@ -970,7 +979,7 @@ test "parser.parse" {
|
||||
|
||||
for (testcases) |tc| {
|
||||
var p = Parser{ .s = tc.s, .opts = .{} };
|
||||
const sel = p.parse(alloc) catch |e| {
|
||||
const sel = p.parse(allocator) catch |e| {
|
||||
// if error was expected, continue.
|
||||
if (tc.err) continue;
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const css = @import("css.zig");
|
||||
|
||||
pub const AttributeOP = enum {
|
||||
eql, // =
|
||||
@@ -306,8 +308,8 @@ pub const Selector = union(enum) {
|
||||
}
|
||||
|
||||
// match returns true if the node matches the selector query.
|
||||
pub fn match(s: Selector, n: anytype) !bool {
|
||||
return switch (s) {
|
||||
pub fn match(s: *const Selector, n: anytype) !bool {
|
||||
return switch (s.*) {
|
||||
.tag => |v| n.isElement() and std.ascii.eqlIgnoreCase(v, try n.tag()),
|
||||
.id => |v| return n.isElement() and std.mem.eql(u8, v, try n.attr("id") orelse return false),
|
||||
.class => |v| return n.isElement() and word(try n.attr("class") orelse return false, v, false),
|
||||
@@ -432,7 +434,25 @@ pub const Selector = union(enum) {
|
||||
else => Error.UnsupportedRelativePseudoClass,
|
||||
};
|
||||
},
|
||||
.pseudo_class_contains => return Error.UnsupportedContainsPseudoClass, // TODO, need mem allocation.
|
||||
.pseudo_class_contains => |v| {
|
||||
// Only containsOwn is implemented.
|
||||
if (v.own == false) return Error.UnsupportedContainsPseudoClass;
|
||||
|
||||
var c = try n.firstChild();
|
||||
while (c != null) {
|
||||
if (c.?.isText()) {
|
||||
const text = try c.?.text();
|
||||
if (text) |_text| {
|
||||
if (contains(_text, v.val, false)) { // we are case sensitive. Is this correct behavior?
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c = try c.?.nextSibling();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
.pseudo_class_regexp => return Error.UnsupportedRegexpPseudoClass, // TODO need mem allocation.
|
||||
.pseudo_class_nth => |v| {
|
||||
if (v.a == 0) {
|
||||
@@ -794,8 +814,8 @@ pub const Selector = union(enum) {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn deinit(sel: Selector, alloc: std.mem.Allocator) void {
|
||||
switch (sel) {
|
||||
pub fn deinit(sel: *const Selector, alloc: std.mem.Allocator) void {
|
||||
switch (sel.*) {
|
||||
.group => |v| {
|
||||
for (v) |vv| vv.deinit(alloc);
|
||||
alloc.free(v);
|
||||
@@ -827,3 +847,578 @@ pub const Selector = union(enum) {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// NodeTest mock implementation for test only.
|
||||
pub const NodeTest = struct {
|
||||
child: ?*const NodeTest = null,
|
||||
last: ?*const NodeTest = null,
|
||||
sibling: ?*const NodeTest = null,
|
||||
prev: ?*const NodeTest = null,
|
||||
par: ?*const NodeTest = null,
|
||||
|
||||
name: []const u8 = "",
|
||||
att: ?[]const u8 = null,
|
||||
|
||||
pub fn firstChild(n: *const NodeTest) !?*const NodeTest {
|
||||
return n.child;
|
||||
}
|
||||
|
||||
pub fn lastChild(n: *const NodeTest) !?*const NodeTest {
|
||||
return n.last;
|
||||
}
|
||||
|
||||
pub fn nextSibling(n: *const NodeTest) !?*const NodeTest {
|
||||
return n.sibling;
|
||||
}
|
||||
|
||||
pub fn prevSibling(n: *const NodeTest) !?*const NodeTest {
|
||||
return n.prev;
|
||||
}
|
||||
|
||||
pub fn parent(n: *const NodeTest) !?*const NodeTest {
|
||||
return n.par;
|
||||
}
|
||||
|
||||
pub fn isElement(_: *const NodeTest) bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn isDocument(_: *const NodeTest) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn isComment(_: *const NodeTest) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn text(_: *const NodeTest) !?[]const u8 {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn isText(_: *const NodeTest) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn isEmptyText(_: *const NodeTest) !bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn tag(n: *const NodeTest) ![]const u8 {
|
||||
return n.name;
|
||||
}
|
||||
|
||||
pub fn attr(n: *const NodeTest, _: []const u8) !?[]const u8 {
|
||||
return n.att;
|
||||
}
|
||||
|
||||
pub fn eql(a: *const NodeTest, b: *const NodeTest) bool {
|
||||
return a == b;
|
||||
}
|
||||
};
|
||||
|
||||
const MatcherTest = struct {
|
||||
const NodeTests = std.ArrayListUnmanaged(*const NodeTest);
|
||||
|
||||
nodes: NodeTests,
|
||||
allocator: Allocator,
|
||||
|
||||
fn init(allocator: Allocator) MatcherTest {
|
||||
return .{
|
||||
.nodes = .empty,
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
fn deinit(m: *MatcherTest) void {
|
||||
m.nodes.deinit(m.allocator);
|
||||
}
|
||||
|
||||
fn reset(m: *MatcherTest) void {
|
||||
m.nodes.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
pub fn match(m: *MatcherTest, n: *const NodeTest) !void {
|
||||
try m.nodes.append(m.allocator, n);
|
||||
}
|
||||
};
|
||||
|
||||
test "Browser.CSS.Selector: matchFirst" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
var matcher = MatcherTest.init(alloc);
|
||||
defer matcher.deinit();
|
||||
|
||||
const testcases = [_]struct {
|
||||
q: []const u8,
|
||||
n: NodeTest,
|
||||
exp: usize,
|
||||
}{
|
||||
.{
|
||||
.q = "address",
|
||||
.n = .{ .child = &.{ .name = "body", .child = &.{ .name = "address" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "#foo",
|
||||
.n = .{ .child = &.{ .name = "p", .att = "foo", .child = &.{ .name = "p" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = ".t1",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "t1" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = ".t1",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "foo t1" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "[foo]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo=baz]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "[foo!=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo!=baz]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo~=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "baz bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo~=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "[foo^=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo$=baz]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo*=rb]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo|=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo|=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar-baz" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo|=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "strong, a",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "p a",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .name = "p" } }, .sibling = &.{ .name = "a" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "p a",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "span", .child = &.{
|
||||
.name = "a",
|
||||
.par = &.{ .name = "span", .par = &.{ .name = "p" } },
|
||||
} } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = ":not(p)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "p:has(a)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "p:has(strong)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "p:haschild(a)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "p:haschild(strong)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "p:lang(en)",
|
||||
.n = .{ .child = &.{ .name = "p", .att = "en-US", .child = &.{ .name = "a" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "a:lang(en)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .att = "en-US" } } } },
|
||||
.exp = 1,
|
||||
},
|
||||
};
|
||||
|
||||
for (testcases) |tc| {
|
||||
matcher.reset();
|
||||
|
||||
const s = try css.parse(alloc, tc.q, .{});
|
||||
defer s.deinit(alloc);
|
||||
|
||||
_ = css.matchFirst(&s, &tc.n, &matcher) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
|
||||
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test "Browser.CSS.Selector: matchAll" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
var matcher = MatcherTest.init(alloc);
|
||||
defer matcher.deinit();
|
||||
|
||||
const testcases = [_]struct {
|
||||
q: []const u8,
|
||||
n: NodeTest,
|
||||
exp: usize,
|
||||
}{
|
||||
.{
|
||||
.q = "address",
|
||||
.n = .{ .child = &.{ .name = "body", .child = &.{ .name = "address" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "#foo",
|
||||
.n = .{ .child = &.{ .name = "p", .att = "foo", .child = &.{ .name = "p" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = ".t1",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "t1" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = ".t1",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "foo t1" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "[foo]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo=baz]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "[foo!=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo!=baz]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 2,
|
||||
},
|
||||
.{
|
||||
.q = "[foo~=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "baz bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo~=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "[foo^=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo$=baz]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo*=rb]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "barbaz" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo|=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo|=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar-baz" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "[foo|=bar]",
|
||||
.n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "ba" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "strong, a",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 2,
|
||||
},
|
||||
.{
|
||||
.q = "p a",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .name = "p" } }, .sibling = &.{ .name = "a" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "p a",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "span", .child = &.{
|
||||
.name = "a",
|
||||
.par = &.{ .name = "span", .par = &.{ .name = "p" } },
|
||||
} } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = ":not(p)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 2,
|
||||
},
|
||||
.{
|
||||
.q = "p:has(a)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "p:has(strong)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "p:haschild(a)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "p:haschild(strong)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a" }, .sibling = &.{ .name = "strong" } } },
|
||||
.exp = 0,
|
||||
},
|
||||
.{
|
||||
.q = "p:lang(en)",
|
||||
.n = .{ .child = &.{ .name = "p", .att = "en-US", .child = &.{ .name = "a" } } },
|
||||
.exp = 1,
|
||||
},
|
||||
.{
|
||||
.q = "a:lang(en)",
|
||||
.n = .{ .child = &.{ .name = "p", .child = &.{ .name = "a", .par = &.{ .att = "en-US" } } } },
|
||||
.exp = 1,
|
||||
},
|
||||
};
|
||||
|
||||
for (testcases) |tc| {
|
||||
matcher.reset();
|
||||
|
||||
const s = try css.parse(alloc, tc.q, .{});
|
||||
defer s.deinit(alloc);
|
||||
|
||||
css.matchAll(&s, &tc.n, &matcher) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
|
||||
std.testing.expectEqual(tc.exp, matcher.nodes.items.len) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test "Browser.CSS.Selector: pseudo class" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
var matcher = MatcherTest.init(alloc);
|
||||
defer matcher.deinit();
|
||||
|
||||
var p1: NodeTest = .{ .name = "p" };
|
||||
var p2: NodeTest = .{ .name = "p" };
|
||||
var a1: NodeTest = .{ .name = "a" };
|
||||
|
||||
p1.sibling = &p2;
|
||||
p2.prev = &p1;
|
||||
|
||||
p2.sibling = &a1;
|
||||
a1.prev = &p2;
|
||||
|
||||
var root: NodeTest = .{ .child = &p1, .last = &a1 };
|
||||
p1.par = &root;
|
||||
p2.par = &root;
|
||||
a1.par = &root;
|
||||
|
||||
const testcases = [_]struct {
|
||||
q: []const u8,
|
||||
n: NodeTest,
|
||||
exp: ?*const NodeTest,
|
||||
}{
|
||||
.{ .q = "p:only-child", .n = root, .exp = null },
|
||||
.{ .q = "a:only-of-type", .n = root, .exp = &a1 },
|
||||
};
|
||||
|
||||
for (testcases) |tc| {
|
||||
matcher.reset();
|
||||
|
||||
const s = try css.parse(alloc, tc.q, .{});
|
||||
defer s.deinit(alloc);
|
||||
|
||||
css.matchAll(&s, &tc.n, &matcher) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
|
||||
if (tc.exp) |exp_n| {
|
||||
const exp: usize = 1;
|
||||
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
|
||||
std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const exp: usize = 0;
|
||||
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test "Browser.CSS.Selector: nth pseudo class" {
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
var matcher = MatcherTest.init(alloc);
|
||||
defer matcher.deinit();
|
||||
|
||||
var p1: NodeTest = .{ .name = "p" };
|
||||
var p2: NodeTest = .{ .name = "p" };
|
||||
|
||||
p1.sibling = &p2;
|
||||
p2.prev = &p1;
|
||||
|
||||
var root: NodeTest = .{ .child = &p1, .last = &p2 };
|
||||
p1.par = &root;
|
||||
p2.par = &root;
|
||||
|
||||
const testcases = [_]struct {
|
||||
q: []const u8,
|
||||
n: NodeTest,
|
||||
exp: ?*const NodeTest,
|
||||
}{
|
||||
.{ .q = "a:nth-of-type(1)", .n = root, .exp = null },
|
||||
.{ .q = "p:nth-of-type(1)", .n = root, .exp = &p1 },
|
||||
.{ .q = "p:nth-of-type(2)", .n = root, .exp = &p2 },
|
||||
.{ .q = "p:nth-of-type(0)", .n = root, .exp = null },
|
||||
.{ .q = "p:nth-of-type(2n)", .n = root, .exp = &p2 },
|
||||
.{ .q = "p:nth-last-child(1)", .n = root, .exp = &p2 },
|
||||
.{ .q = "p:nth-last-child(2)", .n = root, .exp = &p1 },
|
||||
.{ .q = "p:nth-child(1)", .n = root, .exp = &p1 },
|
||||
.{ .q = "p:nth-child(2)", .n = root, .exp = &p2 },
|
||||
.{ .q = "p:nth-child(odd)", .n = root, .exp = &p1 },
|
||||
.{ .q = "p:nth-child(even)", .n = root, .exp = &p2 },
|
||||
.{ .q = "p:nth-child(n+2)", .n = root, .exp = &p2 },
|
||||
};
|
||||
|
||||
for (testcases) |tc| {
|
||||
matcher.reset();
|
||||
|
||||
const s = try css.parse(alloc, tc.q, .{});
|
||||
defer s.deinit(alloc);
|
||||
|
||||
css.matchAll(&s, &tc.n, &matcher) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
|
||||
if (tc.exp) |exp_n| {
|
||||
const exp: usize = 1;
|
||||
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
|
||||
std.testing.expectEqual(exp_n, matcher.nodes.items[0]) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const exp: usize = 0;
|
||||
std.testing.expectEqual(exp, matcher.nodes.items.len) catch |e| {
|
||||
std.debug.print("query: {s}, parsed selector: {any}\n", .{ tc.q, s });
|
||||
return e;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
289
src/browser/cssom/CSSParser.zig
Normal file
289
src/browser/cssom/CSSParser.zig
Normal file
@@ -0,0 +1,289 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const CSSConstants = struct {
|
||||
const IMPORTANT = "!important";
|
||||
const URL_PREFIX = "url(";
|
||||
};
|
||||
|
||||
const CSSParserState = enum {
|
||||
seek_name,
|
||||
in_name,
|
||||
seek_colon,
|
||||
seek_value,
|
||||
in_value,
|
||||
in_quoted_value,
|
||||
in_single_quoted_value,
|
||||
in_url,
|
||||
in_important,
|
||||
};
|
||||
|
||||
const CSSDeclaration = struct {
|
||||
name: []const u8,
|
||||
value: []const u8,
|
||||
is_important: bool,
|
||||
};
|
||||
|
||||
const CSSParser = @This();
|
||||
state: CSSParserState,
|
||||
name_start: usize,
|
||||
name_end: usize,
|
||||
value_start: usize,
|
||||
position: usize,
|
||||
paren_depth: usize,
|
||||
escape_next: bool,
|
||||
|
||||
pub fn init() CSSParser {
|
||||
return .{
|
||||
.state = .seek_name,
|
||||
.name_start = 0,
|
||||
.name_end = 0,
|
||||
.value_start = 0,
|
||||
.position = 0,
|
||||
.paren_depth = 0,
|
||||
.escape_next = false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn parseDeclarations(arena: Allocator, text: []const u8) ![]CSSDeclaration {
|
||||
var parser = init();
|
||||
var declarations: std.ArrayListUnmanaged(CSSDeclaration) = .empty;
|
||||
|
||||
while (parser.position < text.len) {
|
||||
const c = text[parser.position];
|
||||
|
||||
switch (parser.state) {
|
||||
.seek_name => {
|
||||
if (!std.ascii.isWhitespace(c)) {
|
||||
parser.name_start = parser.position;
|
||||
parser.state = .in_name;
|
||||
continue;
|
||||
}
|
||||
},
|
||||
.in_name => {
|
||||
if (c == ':') {
|
||||
parser.name_end = parser.position;
|
||||
parser.state = .seek_value;
|
||||
} else if (std.ascii.isWhitespace(c)) {
|
||||
parser.name_end = parser.position;
|
||||
parser.state = .seek_colon;
|
||||
}
|
||||
},
|
||||
.seek_colon => {
|
||||
if (c == ':') {
|
||||
parser.state = .seek_value;
|
||||
} else if (!std.ascii.isWhitespace(c)) {
|
||||
parser.state = .seek_name;
|
||||
continue;
|
||||
}
|
||||
},
|
||||
.seek_value => {
|
||||
if (!std.ascii.isWhitespace(c)) {
|
||||
parser.value_start = parser.position;
|
||||
if (c == '"') {
|
||||
parser.state = .in_quoted_value;
|
||||
} else if (c == '\'') {
|
||||
parser.state = .in_single_quoted_value;
|
||||
} else if (c == 'u' and parser.position + CSSConstants.URL_PREFIX.len <= text.len and std.mem.startsWith(u8, text[parser.position..], CSSConstants.URL_PREFIX)) {
|
||||
parser.state = .in_url;
|
||||
parser.paren_depth = 1;
|
||||
parser.position += 3;
|
||||
} else {
|
||||
parser.state = .in_value;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
},
|
||||
.in_value => {
|
||||
if (parser.escape_next) {
|
||||
parser.escape_next = false;
|
||||
} else if (c == '\\') {
|
||||
parser.escape_next = true;
|
||||
} else if (c == '(') {
|
||||
parser.paren_depth += 1;
|
||||
} else if (c == ')' and parser.paren_depth > 0) {
|
||||
parser.paren_depth -= 1;
|
||||
} else if (c == ';' and parser.paren_depth == 0) {
|
||||
try parser.finishDeclaration(arena, &declarations, text);
|
||||
parser.state = .seek_name;
|
||||
}
|
||||
},
|
||||
.in_quoted_value => {
|
||||
if (parser.escape_next) {
|
||||
parser.escape_next = false;
|
||||
} else if (c == '\\') {
|
||||
parser.escape_next = true;
|
||||
} else if (c == '"') {
|
||||
parser.state = .in_value;
|
||||
}
|
||||
},
|
||||
.in_single_quoted_value => {
|
||||
if (parser.escape_next) {
|
||||
parser.escape_next = false;
|
||||
} else if (c == '\\') {
|
||||
parser.escape_next = true;
|
||||
} else if (c == '\'') {
|
||||
parser.state = .in_value;
|
||||
}
|
||||
},
|
||||
.in_url => {
|
||||
if (parser.escape_next) {
|
||||
parser.escape_next = false;
|
||||
} else if (c == '\\') {
|
||||
parser.escape_next = true;
|
||||
} else if (c == '(') {
|
||||
parser.paren_depth += 1;
|
||||
} else if (c == ')') {
|
||||
parser.paren_depth -= 1;
|
||||
if (parser.paren_depth == 0) {
|
||||
parser.state = .in_value;
|
||||
}
|
||||
}
|
||||
},
|
||||
.in_important => {},
|
||||
}
|
||||
|
||||
parser.position += 1;
|
||||
}
|
||||
|
||||
try parser.finalize(arena, &declarations, text);
|
||||
|
||||
return declarations.items;
|
||||
}
|
||||
|
||||
fn finishDeclaration(self: *CSSParser, arena: Allocator, declarations: *std.ArrayListUnmanaged(CSSDeclaration), text: []const u8) !void {
|
||||
const name = std.mem.trim(u8, text[self.name_start..self.name_end], &std.ascii.whitespace);
|
||||
if (name.len == 0) return;
|
||||
|
||||
const raw_value = text[self.value_start..self.position];
|
||||
const value = std.mem.trim(u8, raw_value, &std.ascii.whitespace);
|
||||
|
||||
var final_value = value;
|
||||
var is_important = false;
|
||||
|
||||
if (std.mem.endsWith(u8, value, CSSConstants.IMPORTANT)) {
|
||||
is_important = true;
|
||||
final_value = std.mem.trimRight(u8, value[0 .. value.len - CSSConstants.IMPORTANT.len], &std.ascii.whitespace);
|
||||
}
|
||||
|
||||
try declarations.append(arena, .{
|
||||
.name = name,
|
||||
.value = final_value,
|
||||
.is_important = is_important,
|
||||
});
|
||||
}
|
||||
|
||||
fn finalize(self: *CSSParser, arena: Allocator, declarations: *std.ArrayListUnmanaged(CSSDeclaration), text: []const u8) !void {
|
||||
if (self.state != .in_value) {
|
||||
return;
|
||||
}
|
||||
return self.finishDeclaration(arena, declarations, text);
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: CSS.Parser - Simple property" {
|
||||
defer testing.reset();
|
||||
|
||||
const text = "color: red;";
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
||||
|
||||
try testing.expectEqual(1, declarations.len);
|
||||
try testing.expectEqual("color", declarations[0].name);
|
||||
try testing.expectEqual("red", declarations[0].value);
|
||||
try testing.expectEqual(false, declarations[0].is_important);
|
||||
}
|
||||
|
||||
test "Browser: CSS.Parser - Property with !important" {
|
||||
defer testing.reset();
|
||||
const text = "margin: 10px !important;";
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
||||
|
||||
try testing.expectEqual(1, declarations.len);
|
||||
try testing.expectEqual("margin", declarations[0].name);
|
||||
try testing.expectEqual("10px", declarations[0].value);
|
||||
try testing.expectEqual(true, declarations[0].is_important);
|
||||
}
|
||||
|
||||
test "Browser: CSS.Parser - Multiple properties" {
|
||||
defer testing.reset();
|
||||
const text = "color: red; font-size: 12px; margin: 5px !important;";
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
||||
|
||||
try testing.expect(declarations.len == 3);
|
||||
|
||||
try testing.expectEqual("color", declarations[0].name);
|
||||
try testing.expectEqual("red", declarations[0].value);
|
||||
try testing.expectEqual(false, declarations[0].is_important);
|
||||
|
||||
try testing.expectEqual("font-size", declarations[1].name);
|
||||
try testing.expectEqual("12px", declarations[1].value);
|
||||
try testing.expectEqual(false, declarations[1].is_important);
|
||||
|
||||
try testing.expectEqual("margin", declarations[2].name);
|
||||
try testing.expectEqual("5px", declarations[2].value);
|
||||
try testing.expectEqual(true, declarations[2].is_important);
|
||||
}
|
||||
|
||||
test "Browser: CSS.Parser - Quoted value with semicolon" {
|
||||
defer testing.reset();
|
||||
const text = "content: \"Hello; world!\";";
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
||||
|
||||
try testing.expectEqual(1, declarations.len);
|
||||
try testing.expectEqual("content", declarations[0].name);
|
||||
try testing.expectEqual("\"Hello; world!\"", declarations[0].value);
|
||||
try testing.expectEqual(false, declarations[0].is_important);
|
||||
}
|
||||
|
||||
test "Browser: CSS.Parser - URL value" {
|
||||
defer testing.reset();
|
||||
const text = "background-image: url(\"test.png\");";
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
||||
|
||||
try testing.expectEqual(1, declarations.len);
|
||||
try testing.expectEqual("background-image", declarations[0].name);
|
||||
try testing.expectEqual("url(\"test.png\")", declarations[0].value);
|
||||
try testing.expectEqual(false, declarations[0].is_important);
|
||||
}
|
||||
|
||||
test "Browser: CSS.Parser - Whitespace handling" {
|
||||
defer testing.reset();
|
||||
const text = " color : purple ; margin : 10px ; ";
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
||||
|
||||
try testing.expectEqual(2, declarations.len);
|
||||
try testing.expectEqual("color", declarations[0].name);
|
||||
try testing.expectEqual("purple", declarations[0].value);
|
||||
try testing.expectEqual("margin", declarations[1].name);
|
||||
try testing.expectEqual("10px", declarations[1].value);
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const CSSStyleSheet = @import("css_stylesheet.zig").CSSStyleSheet;
|
||||
const CSSStyleSheet = @import("CSSStyleSheet.zig");
|
||||
|
||||
pub const Interfaces = .{
|
||||
CSSRule,
|
||||
@@ -26,11 +26,10 @@ pub const Interfaces = .{
|
||||
};
|
||||
|
||||
// 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,
|
||||
};
|
||||
const CSSRule = @This();
|
||||
css_text: []const u8,
|
||||
parent_rule: ?*CSSRule = null,
|
||||
parent_stylesheet: ?*CSSStyleSheet = null,
|
||||
|
||||
pub const CSSImportRule = struct {
|
||||
pub const prototype = *CSSRule;
|
||||
52
src/browser/cssom/CSSRuleList.zig
Normal file
52
src/browser/cssom/CSSRuleList.zig
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const CSSRule = @import("CSSRule.zig");
|
||||
const StyleSheet = @import("StyleSheet.zig").StyleSheet;
|
||||
|
||||
const CSSImportRule = CSSRule.CSSImportRule;
|
||||
|
||||
const CSSRuleList = @This();
|
||||
list: std.ArrayListUnmanaged([]const u8),
|
||||
|
||||
pub fn constructor() CSSRuleList {
|
||||
return .{ .list = .empty };
|
||||
}
|
||||
|
||||
pub fn _item(self: *CSSRuleList, _index: u32) ?CSSRule {
|
||||
const index: usize = @intCast(_index);
|
||||
|
||||
if (index > self.list.items.len) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// todo: for now, just return null.
|
||||
// this depends on properly parsing CSSRule
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn get_length(self: *CSSRuleList) u32 {
|
||||
return @intCast(self.list.items.len);
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: CSS.CSSRuleList" {
|
||||
try testing.htmlRunner("cssom/css_rule_list.html");
|
||||
}
|
||||
958
src/browser/cssom/CSSStyleDeclaration.zig
Normal file
958
src/browser/cssom/CSSStyleDeclaration.zig
Normal file
@@ -0,0 +1,958 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const Page = @import("../page.zig").Page;
|
||||
const CSSRule = @import("CSSRule.zig");
|
||||
const CSSParser = @import("CSSParser.zig");
|
||||
|
||||
const Property = struct {
|
||||
value: []const u8,
|
||||
priority: bool,
|
||||
};
|
||||
|
||||
const CSSStyleDeclaration = @This();
|
||||
|
||||
properties: std.StringArrayHashMapUnmanaged(Property),
|
||||
|
||||
pub const empty: CSSStyleDeclaration = .{
|
||||
.properties = .empty,
|
||||
};
|
||||
|
||||
pub fn get_cssFloat(self: *const CSSStyleDeclaration) []const u8 {
|
||||
return self._getPropertyValue("float");
|
||||
}
|
||||
|
||||
pub fn set_cssFloat(self: *CSSStyleDeclaration, value: ?[]const u8, page: *Page) !void {
|
||||
const final_value = value orelse "";
|
||||
return self._setProperty("float", final_value, null, page);
|
||||
}
|
||||
|
||||
pub fn get_cssText(self: *const CSSStyleDeclaration, page: *Page) ![]const u8 {
|
||||
var buffer: std.ArrayListUnmanaged(u8) = .empty;
|
||||
const writer = buffer.writer(page.call_arena);
|
||||
var it = self.properties.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const name = entry.key_ptr.*;
|
||||
const property = entry.value_ptr;
|
||||
const escaped = try escapeCSSValue(page.call_arena, property.value);
|
||||
try writer.print("{s}: {s}", .{ name, escaped });
|
||||
if (property.priority) {
|
||||
try writer.writeAll(" !important; ");
|
||||
} else {
|
||||
try writer.writeAll("; ");
|
||||
}
|
||||
}
|
||||
return buffer.items;
|
||||
}
|
||||
|
||||
// TODO Propagate also upward to parent node
|
||||
pub fn set_cssText(self: *CSSStyleDeclaration, text: []const u8, page: *Page) !void {
|
||||
self.properties.clearRetainingCapacity();
|
||||
|
||||
// call_arena is safe here, because _setProperty will dupe the name
|
||||
// using the page's longer-living arena.
|
||||
const declarations = try CSSParser.parseDeclarations(page.call_arena, text);
|
||||
|
||||
for (declarations) |decl| {
|
||||
if (!isValidPropertyName(decl.name)) {
|
||||
continue;
|
||||
}
|
||||
const priority: ?[]const u8 = if (decl.is_important) "important" else null;
|
||||
try self._setProperty(decl.name, decl.value, priority, page);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_length(self: *const CSSStyleDeclaration) usize {
|
||||
return self.properties.count();
|
||||
}
|
||||
|
||||
pub fn get_parentRule(_: *const CSSStyleDeclaration) ?CSSRule {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn _getPropertyPriority(self: *const CSSStyleDeclaration, name: []const u8) []const u8 {
|
||||
const property = self.properties.getPtr(name) orelse return "";
|
||||
return if (property.priority) "important" else "";
|
||||
}
|
||||
|
||||
// TODO should handle properly shorthand properties and canonical forms
|
||||
pub fn _getPropertyValue(self: *const CSSStyleDeclaration, name: []const u8) []const u8 {
|
||||
if (self.properties.getPtr(name)) |property| {
|
||||
return property.value;
|
||||
}
|
||||
|
||||
// default to everything being visible (unless it's been explicitly set)
|
||||
if (std.mem.eql(u8, name, "visibility")) {
|
||||
return "visible";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn _item(self: *const CSSStyleDeclaration, index: usize) []const u8 {
|
||||
const values = self.properties.entries.items(.key);
|
||||
if (index >= values.len) {
|
||||
return "";
|
||||
}
|
||||
return values[index];
|
||||
}
|
||||
|
||||
pub fn _removeProperty(self: *CSSStyleDeclaration, name: []const u8) ![]const u8 {
|
||||
const property = self.properties.fetchOrderedRemove(name) orelse return "";
|
||||
return property.value.value;
|
||||
}
|
||||
|
||||
pub fn _setProperty(self: *CSSStyleDeclaration, name: []const u8, value: []const u8, priority: ?[]const u8, page: *Page) !void {
|
||||
const gop = try self.properties.getOrPut(page.arena, name);
|
||||
if (!gop.found_existing) {
|
||||
const owned_name = try page.arena.dupe(u8, name);
|
||||
gop.key_ptr.* = owned_name;
|
||||
}
|
||||
|
||||
const owned_value = try page.arena.dupe(u8, value);
|
||||
const is_important = priority != null and std.ascii.eqlIgnoreCase(priority.?, "important");
|
||||
gop.value_ptr.* = .{ .value = owned_value, .priority = is_important };
|
||||
}
|
||||
|
||||
pub fn named_get(self: *const CSSStyleDeclaration, name: []const u8, _: *bool) []const u8 {
|
||||
return self._getPropertyValue(name);
|
||||
}
|
||||
|
||||
pub fn named_set(self: *CSSStyleDeclaration, name: []const u8, value: []const u8, _: *bool, page: *Page) !void {
|
||||
return self._setProperty(name, value, null, page);
|
||||
}
|
||||
|
||||
fn isNumericWithUnit(value: []const u8) bool {
|
||||
if (value.len == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const first = value[0];
|
||||
|
||||
if (!std.ascii.isDigit(first) and first != '+' and first != '-' and first != '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
var i: usize = 0;
|
||||
var has_digit = false;
|
||||
var decimal_point = false;
|
||||
|
||||
while (i < value.len) : (i += 1) {
|
||||
const c = value[i];
|
||||
if (std.ascii.isDigit(c)) {
|
||||
has_digit = true;
|
||||
} else if (c == '.' and !decimal_point) {
|
||||
decimal_point = true;
|
||||
} else if ((c == 'e' or c == 'E') and has_digit) {
|
||||
if (i + 1 >= value.len) return false;
|
||||
if (value[i + 1] != '+' and value[i + 1] != '-' and !std.ascii.isDigit(value[i + 1])) break;
|
||||
i += 1;
|
||||
if (value[i] == '+' or value[i] == '-') {
|
||||
i += 1;
|
||||
}
|
||||
var has_exp_digits = false;
|
||||
while (i < value.len and std.ascii.isDigit(value[i])) : (i += 1) {
|
||||
has_exp_digits = true;
|
||||
}
|
||||
if (!has_exp_digits) return false;
|
||||
break;
|
||||
} else if (c != '-' and c != '+') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_digit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (i == value.len) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const unit = value[i..];
|
||||
return CSSKeywords.isValidUnit(unit);
|
||||
}
|
||||
|
||||
fn isHexColor(value: []const u8) bool {
|
||||
if (value.len == 0) {
|
||||
return false;
|
||||
}
|
||||
if (value[0] != '#') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hex_part = value[1..];
|
||||
if (hex_part.len != 3 and hex_part.len != 6 and hex_part.len != 8) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (hex_part) |c| {
|
||||
if (!std.ascii.isHex(c)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fn isMultiValueProperty(value: []const u8) bool {
|
||||
var parts = std.mem.splitAny(u8, value, " ");
|
||||
var multi_value_parts: usize = 0;
|
||||
var all_parts_valid = true;
|
||||
|
||||
while (parts.next()) |part| {
|
||||
if (part.len == 0) continue;
|
||||
multi_value_parts += 1;
|
||||
|
||||
if (isNumericWithUnit(part)) {
|
||||
continue;
|
||||
}
|
||||
if (isHexColor(part)) {
|
||||
continue;
|
||||
}
|
||||
if (CSSKeywords.isKnownKeyword(part)) {
|
||||
continue;
|
||||
}
|
||||
if (CSSKeywords.startsWithFunction(part)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
all_parts_valid = false;
|
||||
break;
|
||||
}
|
||||
|
||||
return multi_value_parts >= 2 and all_parts_valid;
|
||||
}
|
||||
|
||||
fn isAlreadyQuoted(value: []const u8) bool {
|
||||
return value.len >= 2 and ((value[0] == '"' and value[value.len - 1] == '"') or
|
||||
(value[0] == '\'' and value[value.len - 1] == '\''));
|
||||
}
|
||||
|
||||
fn isValidPropertyName(name: []const u8) bool {
|
||||
if (name.len == 0) return false;
|
||||
|
||||
if (std.mem.startsWith(u8, name, "--")) {
|
||||
if (name.len == 2) return false;
|
||||
for (name[2..]) |c| {
|
||||
if (!std.ascii.isAlphanumeric(c) and c != '-' and c != '_') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const first_char = name[0];
|
||||
if (!std.ascii.isAlphabetic(first_char) and first_char != '-') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (first_char == '-') {
|
||||
if (name.len < 2) return false;
|
||||
|
||||
if (!std.ascii.isAlphabetic(name[1])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (name[2..]) |c| {
|
||||
if (!std.ascii.isAlphanumeric(c) and c != '-') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (name[1..]) |c| {
|
||||
if (!std.ascii.isAlphanumeric(c) and c != '-') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fn extractImportant(value: []const u8) struct { value: []const u8, is_important: bool } {
|
||||
const trimmed = std.mem.trim(u8, value, &std.ascii.whitespace);
|
||||
|
||||
if (std.mem.endsWith(u8, trimmed, "!important")) {
|
||||
const clean_value = std.mem.trimRight(u8, trimmed[0 .. trimmed.len - 10], &std.ascii.whitespace);
|
||||
return .{ .value = clean_value, .is_important = true };
|
||||
}
|
||||
|
||||
return .{ .value = trimmed, .is_important = false };
|
||||
}
|
||||
|
||||
fn needsQuotes(value: []const u8) bool {
|
||||
if (value.len == 0) return true;
|
||||
if (isAlreadyQuoted(value)) return false;
|
||||
|
||||
if (CSSKeywords.containsSpecialChar(value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.indexOfScalar(u8, value, ' ') == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const is_url = std.mem.startsWith(u8, value, "url(");
|
||||
const is_function = CSSKeywords.startsWithFunction(value);
|
||||
|
||||
return !isMultiValueProperty(value) and
|
||||
!is_url and
|
||||
!is_function;
|
||||
}
|
||||
|
||||
fn escapeCSSValue(arena: std.mem.Allocator, value: []const u8) ![]const u8 {
|
||||
if (!needsQuotes(value)) {
|
||||
return value;
|
||||
}
|
||||
var out: std.ArrayListUnmanaged(u8) = .empty;
|
||||
|
||||
// We'll need at least this much space, +2 for the quotes
|
||||
try out.ensureTotalCapacity(arena, value.len + 2);
|
||||
const writer = out.writer(arena);
|
||||
|
||||
try writer.writeByte('"');
|
||||
|
||||
for (value, 0..) |c, i| {
|
||||
switch (c) {
|
||||
'"' => try writer.writeAll("\\\""),
|
||||
'\\' => try writer.writeAll("\\\\"),
|
||||
'\n' => try writer.writeAll("\\A "),
|
||||
'\r' => try writer.writeAll("\\D "),
|
||||
'\t' => try writer.writeAll("\\9 "),
|
||||
0...8, 11, 12, 14...31, 127 => {
|
||||
try writer.print("\\{x}", .{c});
|
||||
if (i + 1 < value.len and std.ascii.isHex(value[i + 1])) {
|
||||
try writer.writeByte(' ');
|
||||
}
|
||||
},
|
||||
else => try writer.writeByte(c),
|
||||
}
|
||||
}
|
||||
|
||||
try writer.writeByte('"');
|
||||
return out.items;
|
||||
}
|
||||
|
||||
fn isKnownKeyword(value: []const u8) bool {
|
||||
return CSSKeywords.isKnownKeyword(value);
|
||||
}
|
||||
|
||||
fn containsSpecialChar(value: []const u8) bool {
|
||||
return CSSKeywords.containsSpecialChar(value);
|
||||
}
|
||||
|
||||
const CSSKeywords = struct {
|
||||
const BORDER_STYLES = [_][]const u8{
|
||||
"none", "solid", "dotted", "dashed", "double", "groove", "ridge", "inset", "outset",
|
||||
};
|
||||
|
||||
const COLOR_NAMES = [_][]const u8{
|
||||
"black", "white", "red", "green", "blue", "yellow", "purple", "gray", "transparent",
|
||||
"currentColor", "inherit",
|
||||
};
|
||||
|
||||
const POSITION_KEYWORDS = [_][]const u8{
|
||||
"auto", "center", "left", "right", "top", "bottom",
|
||||
};
|
||||
|
||||
const BACKGROUND_REPEAT = [_][]const u8{
|
||||
"repeat", "no-repeat", "repeat-x", "repeat-y", "space", "round",
|
||||
};
|
||||
|
||||
const FONT_STYLES = [_][]const u8{
|
||||
"normal", "italic", "oblique", "bold", "bolder", "lighter",
|
||||
};
|
||||
|
||||
const FONT_SIZES = [_][]const u8{
|
||||
"xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large",
|
||||
"smaller", "larger",
|
||||
};
|
||||
|
||||
const FONT_FAMILIES = [_][]const u8{
|
||||
"serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui",
|
||||
};
|
||||
|
||||
const CSS_GLOBAL = [_][]const u8{
|
||||
"initial", "inherit", "unset", "revert",
|
||||
};
|
||||
|
||||
const DISPLAY_VALUES = [_][]const u8{
|
||||
"block", "inline", "inline-block", "flex", "grid", "none",
|
||||
};
|
||||
|
||||
const UNITS = [_][]const u8{
|
||||
// LENGTH
|
||||
"px", "em", "rem", "vw", "vh", "vmin", "vmax", "%", "pt", "pc", "in", "cm", "mm",
|
||||
"ex", "ch", "fr",
|
||||
|
||||
// ANGLE
|
||||
"deg", "rad", "grad", "turn",
|
||||
|
||||
// TIME
|
||||
"s", "ms",
|
||||
|
||||
// FREQUENCY
|
||||
"hz", "khz",
|
||||
|
||||
// RESOLUTION
|
||||
"dpi", "dpcm",
|
||||
"dppx",
|
||||
};
|
||||
|
||||
const SPECIAL_CHARS = [_]u8{
|
||||
'"', '\'', ';', '{', '}', '\\', '<', '>', '/', '\n', '\t', '\r', '\x00', '\x7F',
|
||||
};
|
||||
|
||||
const FUNCTIONS = [_][]const u8{
|
||||
"rgb(", "rgba(", "hsl(", "hsla(", "url(", "calc(", "var(", "attr(",
|
||||
"linear-gradient(", "radial-gradient(", "conic-gradient(", "translate(", "rotate(", "scale(", "skew(", "matrix(",
|
||||
};
|
||||
|
||||
const KEYWORDS = BORDER_STYLES ++ COLOR_NAMES ++ POSITION_KEYWORDS ++
|
||||
BACKGROUND_REPEAT ++ FONT_STYLES ++ FONT_SIZES ++ FONT_FAMILIES ++
|
||||
CSS_GLOBAL ++ DISPLAY_VALUES;
|
||||
|
||||
const MAX_KEYWORD_LEN = lengthOfLongestValue(&KEYWORDS);
|
||||
|
||||
pub fn isKnownKeyword(value: []const u8) bool {
|
||||
if (value.len > MAX_KEYWORD_LEN) {
|
||||
return false;
|
||||
}
|
||||
var buf: [MAX_KEYWORD_LEN]u8 = undefined;
|
||||
const normalized = std.ascii.lowerString(&buf, value);
|
||||
|
||||
for (KEYWORDS) |keyword| {
|
||||
if (std.ascii.eqlIgnoreCase(normalized, keyword)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn containsSpecialChar(value: []const u8) bool {
|
||||
return std.mem.indexOfAny(u8, value, &SPECIAL_CHARS) != null;
|
||||
}
|
||||
|
||||
const MAX_UNIT_LEN = lengthOfLongestValue(&UNITS);
|
||||
|
||||
pub fn isValidUnit(unit: []const u8) bool {
|
||||
if (unit.len > MAX_UNIT_LEN) {
|
||||
return false;
|
||||
}
|
||||
var buf: [MAX_UNIT_LEN]u8 = undefined;
|
||||
const normalized = std.ascii.lowerString(&buf, unit);
|
||||
|
||||
for (UNITS) |u| {
|
||||
if (std.mem.eql(u8, normalized, u)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn startsWithFunction(value: []const u8) bool {
|
||||
const pos = std.mem.indexOfScalar(u8, value, '(') orelse return false;
|
||||
if (pos == 0) return false;
|
||||
|
||||
if (std.mem.indexOfScalarPos(u8, value, pos, ')') == null) {
|
||||
return false;
|
||||
}
|
||||
const function_name = value[0..pos];
|
||||
return isValidFunctionName(function_name);
|
||||
}
|
||||
|
||||
fn isValidFunctionName(name: []const u8) bool {
|
||||
if (name.len == 0) return false;
|
||||
|
||||
const first = name[0];
|
||||
if (!std.ascii.isAlphabetic(first) and first != '_' and first != '-') {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (name[1..]) |c| {
|
||||
if (!std.ascii.isAlphanumeric(c) and c != '_' and c != '-') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
fn lengthOfLongestValue(values: []const []const u8) usize {
|
||||
var max: usize = 0;
|
||||
for (values) |v| {
|
||||
max = @max(v.len, max);
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: CSS.StyleDeclaration" {
|
||||
try testing.htmlRunner("cssom/css_style_declaration.html");
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isNumericWithUnit - valid numbers with units" {
|
||||
try testing.expect(isNumericWithUnit("10px"));
|
||||
try testing.expect(isNumericWithUnit("3.14em"));
|
||||
try testing.expect(isNumericWithUnit("-5rem"));
|
||||
try testing.expect(isNumericWithUnit("+12.5%"));
|
||||
try testing.expect(isNumericWithUnit("0vh"));
|
||||
try testing.expect(isNumericWithUnit(".5vw"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isNumericWithUnit - scientific notation" {
|
||||
try testing.expect(isNumericWithUnit("1e5px"));
|
||||
try testing.expect(isNumericWithUnit("2.5E-3em"));
|
||||
try testing.expect(isNumericWithUnit("1e+2rem"));
|
||||
try testing.expect(isNumericWithUnit("-3.14e10px"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isNumericWithUnit - edge cases and invalid inputs" {
|
||||
try testing.expect(!isNumericWithUnit(""));
|
||||
|
||||
try testing.expect(!isNumericWithUnit("px"));
|
||||
try testing.expect(!isNumericWithUnit("--px"));
|
||||
try testing.expect(!isNumericWithUnit(".px"));
|
||||
|
||||
try testing.expect(!isNumericWithUnit("1e"));
|
||||
try testing.expect(!isNumericWithUnit("1epx"));
|
||||
try testing.expect(!isNumericWithUnit("1e+"));
|
||||
try testing.expect(!isNumericWithUnit("1e+px"));
|
||||
|
||||
try testing.expect(!isNumericWithUnit("1.2.3px"));
|
||||
|
||||
try testing.expect(!isNumericWithUnit("10xyz"));
|
||||
try testing.expect(!isNumericWithUnit("5invalid"));
|
||||
|
||||
try testing.expect(isNumericWithUnit("10"));
|
||||
try testing.expect(isNumericWithUnit("3.14"));
|
||||
try testing.expect(isNumericWithUnit("-5"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isHexColor - valid hex colors" {
|
||||
try testing.expect(isHexColor("#000"));
|
||||
try testing.expect(isHexColor("#fff"));
|
||||
try testing.expect(isHexColor("#123456"));
|
||||
try testing.expect(isHexColor("#abcdef"));
|
||||
try testing.expect(isHexColor("#ABCDEF"));
|
||||
try testing.expect(isHexColor("#12345678"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isHexColor - invalid hex colors" {
|
||||
try testing.expect(!isHexColor(""));
|
||||
try testing.expect(!isHexColor("#"));
|
||||
try testing.expect(!isHexColor("000"));
|
||||
try testing.expect(!isHexColor("#00"));
|
||||
try testing.expect(!isHexColor("#0000"));
|
||||
try testing.expect(!isHexColor("#00000"));
|
||||
try testing.expect(!isHexColor("#0000000"));
|
||||
try testing.expect(!isHexColor("#000000000"));
|
||||
try testing.expect(!isHexColor("#gggggg"));
|
||||
try testing.expect(!isHexColor("#123xyz"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isMultiValueProperty - valid multi-value properties" {
|
||||
try testing.expect(isMultiValueProperty("10px 20px"));
|
||||
try testing.expect(isMultiValueProperty("solid red"));
|
||||
try testing.expect(isMultiValueProperty("#fff black"));
|
||||
try testing.expect(isMultiValueProperty("1em 2em 3em 4em"));
|
||||
try testing.expect(isMultiValueProperty("rgb(255,0,0) solid"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isMultiValueProperty - invalid multi-value properties" {
|
||||
try testing.expect(!isMultiValueProperty(""));
|
||||
try testing.expect(!isMultiValueProperty("10px"));
|
||||
try testing.expect(!isMultiValueProperty("invalid unknown"));
|
||||
try testing.expect(!isMultiValueProperty("10px invalid"));
|
||||
try testing.expect(!isMultiValueProperty(" "));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isAlreadyQuoted - various quoting scenarios" {
|
||||
try testing.expect(isAlreadyQuoted("\"hello\""));
|
||||
try testing.expect(isAlreadyQuoted("'world'"));
|
||||
try testing.expect(isAlreadyQuoted("\"\""));
|
||||
try testing.expect(isAlreadyQuoted("''"));
|
||||
|
||||
try testing.expect(!isAlreadyQuoted(""));
|
||||
try testing.expect(!isAlreadyQuoted("hello"));
|
||||
try testing.expect(!isAlreadyQuoted("\""));
|
||||
try testing.expect(!isAlreadyQuoted("'"));
|
||||
try testing.expect(!isAlreadyQuoted("\"hello'"));
|
||||
try testing.expect(!isAlreadyQuoted("'hello\""));
|
||||
try testing.expect(!isAlreadyQuoted("\"hello"));
|
||||
try testing.expect(!isAlreadyQuoted("hello\""));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isValidPropertyName - valid property names" {
|
||||
try testing.expect(isValidPropertyName("color"));
|
||||
try testing.expect(isValidPropertyName("background-color"));
|
||||
try testing.expect(isValidPropertyName("-webkit-transform"));
|
||||
try testing.expect(isValidPropertyName("font-size"));
|
||||
try testing.expect(isValidPropertyName("margin-top"));
|
||||
try testing.expect(isValidPropertyName("z-index"));
|
||||
try testing.expect(isValidPropertyName("line-height"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isValidPropertyName - invalid property names" {
|
||||
try testing.expect(!isValidPropertyName(""));
|
||||
try testing.expect(!isValidPropertyName("123color"));
|
||||
try testing.expect(!isValidPropertyName("color!"));
|
||||
try testing.expect(!isValidPropertyName("color space"));
|
||||
try testing.expect(!isValidPropertyName("@color"));
|
||||
try testing.expect(!isValidPropertyName("color.test"));
|
||||
try testing.expect(!isValidPropertyName("color_test"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: extractImportant - with and without !important" {
|
||||
var result = extractImportant("red !important");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("red", result.value);
|
||||
|
||||
result = extractImportant("blue");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("blue", result.value);
|
||||
|
||||
result = extractImportant(" green !important ");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("green", result.value);
|
||||
|
||||
result = extractImportant("!important");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("", result.value);
|
||||
|
||||
result = extractImportant("important");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("important", result.value);
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: needsQuotes - various scenarios" {
|
||||
try testing.expect(needsQuotes(""));
|
||||
try testing.expect(needsQuotes("hello world"));
|
||||
try testing.expect(needsQuotes("test;"));
|
||||
try testing.expect(needsQuotes("a{b}"));
|
||||
try testing.expect(needsQuotes("test\"quote"));
|
||||
|
||||
try testing.expect(!needsQuotes("\"already quoted\""));
|
||||
try testing.expect(!needsQuotes("'already quoted'"));
|
||||
try testing.expect(!needsQuotes("url(image.png)"));
|
||||
try testing.expect(!needsQuotes("rgb(255, 0, 0)"));
|
||||
try testing.expect(!needsQuotes("10px 20px"));
|
||||
try testing.expect(!needsQuotes("simple"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: escapeCSSValue - escaping various characters" {
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
var result = try escapeCSSValue(allocator, "simple");
|
||||
try testing.expectEqual("simple", result);
|
||||
|
||||
result = try escapeCSSValue(allocator, "\"already quoted\"");
|
||||
try testing.expectEqual("\"already quoted\"", result);
|
||||
|
||||
result = try escapeCSSValue(allocator, "test\"quote");
|
||||
try testing.expectEqual("\"test\\\"quote\"", result);
|
||||
|
||||
result = try escapeCSSValue(allocator, "test\nline");
|
||||
try testing.expectEqual("\"test\\A line\"", result);
|
||||
|
||||
result = try escapeCSSValue(allocator, "test\\back");
|
||||
try testing.expectEqual("\"test\\\\back\"", result);
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: CSSKeywords.isKnownKeyword - case sensitivity" {
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("red"));
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("solid"));
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("center"));
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("inherit"));
|
||||
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("RED"));
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("Red"));
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("SOLID"));
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("Center"));
|
||||
|
||||
try testing.expect(!CSSKeywords.isKnownKeyword("invalid"));
|
||||
try testing.expect(!CSSKeywords.isKnownKeyword("unknown"));
|
||||
try testing.expect(!CSSKeywords.isKnownKeyword(""));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: CSSKeywords.containsSpecialChar - various special characters" {
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test\"quote"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test'quote"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test;end"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test{brace"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test}brace"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test\\back"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test<angle"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test>angle"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test/slash"));
|
||||
|
||||
try testing.expect(!CSSKeywords.containsSpecialChar("normal-text"));
|
||||
try testing.expect(!CSSKeywords.containsSpecialChar("text123"));
|
||||
try testing.expect(!CSSKeywords.containsSpecialChar(""));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: CSSKeywords.isValidUnit - various units" {
|
||||
try testing.expect(CSSKeywords.isValidUnit("px"));
|
||||
try testing.expect(CSSKeywords.isValidUnit("em"));
|
||||
try testing.expect(CSSKeywords.isValidUnit("rem"));
|
||||
try testing.expect(CSSKeywords.isValidUnit("%"));
|
||||
|
||||
try testing.expect(CSSKeywords.isValidUnit("deg"));
|
||||
try testing.expect(CSSKeywords.isValidUnit("rad"));
|
||||
|
||||
try testing.expect(CSSKeywords.isValidUnit("s"));
|
||||
try testing.expect(CSSKeywords.isValidUnit("ms"));
|
||||
|
||||
try testing.expect(CSSKeywords.isValidUnit("PX"));
|
||||
|
||||
try testing.expect(!CSSKeywords.isValidUnit("invalid"));
|
||||
try testing.expect(!CSSKeywords.isValidUnit(""));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: CSSKeywords.startsWithFunction - function detection" {
|
||||
try testing.expect(CSSKeywords.startsWithFunction("rgb(255, 0, 0)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("rgba(255, 0, 0, 0.5)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("url(image.png)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("calc(100% - 20px)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("var(--custom-property)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("linear-gradient(to right, red, blue)"));
|
||||
|
||||
try testing.expect(CSSKeywords.startsWithFunction("custom-function(args)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("unknown(test)"));
|
||||
|
||||
try testing.expect(!CSSKeywords.startsWithFunction("not-a-function"));
|
||||
try testing.expect(!CSSKeywords.startsWithFunction("missing-paren)"));
|
||||
try testing.expect(!CSSKeywords.startsWithFunction("missing-close("));
|
||||
try testing.expect(!CSSKeywords.startsWithFunction(""));
|
||||
try testing.expect(!CSSKeywords.startsWithFunction("rgb"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isNumericWithUnit - whitespace handling" {
|
||||
try testing.expect(!isNumericWithUnit(" 10px"));
|
||||
try testing.expect(!isNumericWithUnit("10 px"));
|
||||
try testing.expect(!isNumericWithUnit("10px "));
|
||||
try testing.expect(!isNumericWithUnit(" 10 px "));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: extractImportant - whitespace edge cases" {
|
||||
var result = extractImportant(" ");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("", result.value);
|
||||
|
||||
result = extractImportant("\t\n\r !important\t\n");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("", result.value);
|
||||
|
||||
result = extractImportant("red\t!important");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("red", result.value);
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isHexColor - mixed case handling" {
|
||||
try testing.expect(isHexColor("#AbC"));
|
||||
try testing.expect(isHexColor("#123aBc"));
|
||||
try testing.expect(isHexColor("#FFffFF"));
|
||||
try testing.expect(isHexColor("#000FFF"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: edge case - very long inputs" {
|
||||
const long_valid = "a" ** 1000 ++ "px";
|
||||
try testing.expect(!isNumericWithUnit(long_valid)); // not numeric
|
||||
|
||||
const long_property = "a-" ** 100 ++ "property";
|
||||
try testing.expect(isValidPropertyName(long_property));
|
||||
|
||||
const long_hex = "#" ++ "a" ** 20;
|
||||
try testing.expect(!isHexColor(long_hex));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: boundary conditions - numeric parsing" {
|
||||
try testing.expect(isNumericWithUnit("0px"));
|
||||
try testing.expect(isNumericWithUnit("0.0px"));
|
||||
try testing.expect(isNumericWithUnit(".0px"));
|
||||
try testing.expect(isNumericWithUnit("0.px"));
|
||||
|
||||
try testing.expect(isNumericWithUnit("999999999px"));
|
||||
try testing.expect(isNumericWithUnit("1.7976931348623157e+308px"));
|
||||
|
||||
try testing.expect(isNumericWithUnit("0.000000001px"));
|
||||
try testing.expect(isNumericWithUnit("1e-100px"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: extractImportant - malformed important declarations" {
|
||||
var result = extractImportant("red ! important");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("red ! important", result.value);
|
||||
|
||||
result = extractImportant("red !Important");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("red !Important", result.value);
|
||||
|
||||
result = extractImportant("red !IMPORTANT");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("red !IMPORTANT", result.value);
|
||||
|
||||
result = extractImportant("!importantred");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("!importantred", result.value);
|
||||
|
||||
result = extractImportant("red !important !important");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("red !important", result.value);
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isMultiValueProperty - complex spacing scenarios" {
|
||||
try testing.expect(isMultiValueProperty("10px 20px"));
|
||||
try testing.expect(isMultiValueProperty("solid red"));
|
||||
|
||||
try testing.expect(isMultiValueProperty(" 10px 20px "));
|
||||
|
||||
try testing.expect(!isMultiValueProperty("10px\t20px"));
|
||||
try testing.expect(!isMultiValueProperty("10px\n20px"));
|
||||
|
||||
try testing.expect(isMultiValueProperty("10px 20px 30px"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isAlreadyQuoted - edge cases with quotes" {
|
||||
try testing.expect(isAlreadyQuoted("\"'hello'\""));
|
||||
try testing.expect(isAlreadyQuoted("'\"hello\"'"));
|
||||
|
||||
try testing.expect(isAlreadyQuoted("\"hello\\\"world\""));
|
||||
try testing.expect(isAlreadyQuoted("'hello\\'world'"));
|
||||
|
||||
try testing.expect(!isAlreadyQuoted("\"hello"));
|
||||
try testing.expect(!isAlreadyQuoted("hello\""));
|
||||
try testing.expect(!isAlreadyQuoted("'hello"));
|
||||
try testing.expect(!isAlreadyQuoted("hello'"));
|
||||
|
||||
try testing.expect(isAlreadyQuoted("\"a\""));
|
||||
try testing.expect(isAlreadyQuoted("'b'"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: needsQuotes - function and URL edge cases" {
|
||||
try testing.expect(!needsQuotes("rgb(255, 0, 0)"));
|
||||
try testing.expect(!needsQuotes("calc(100% - 20px)"));
|
||||
|
||||
try testing.expect(!needsQuotes("url(path with spaces.jpg)"));
|
||||
|
||||
try testing.expect(!needsQuotes("linear-gradient(to right, red, blue)"));
|
||||
|
||||
try testing.expect(needsQuotes("rgb(255, 0, 0"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: escapeCSSValue - control characters and Unicode" {
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
var result = try escapeCSSValue(allocator, "test\ttab");
|
||||
try testing.expectEqual("\"test\\9 tab\"", result);
|
||||
|
||||
result = try escapeCSSValue(allocator, "test\rreturn");
|
||||
try testing.expectEqual("\"test\\D return\"", result);
|
||||
|
||||
result = try escapeCSSValue(allocator, "test\x00null");
|
||||
try testing.expectEqual("\"test\\0null\"", result);
|
||||
|
||||
result = try escapeCSSValue(allocator, "test\x7Fdel");
|
||||
try testing.expectEqual("\"test\\7f del\"", result);
|
||||
|
||||
result = try escapeCSSValue(allocator, "test\"quote\nline\\back");
|
||||
try testing.expectEqual("\"test\\\"quote\\A line\\\\back\"", result);
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isValidPropertyName - CSS custom properties and vendor prefixes" {
|
||||
try testing.expect(isValidPropertyName("--custom-color"));
|
||||
try testing.expect(isValidPropertyName("--my-variable"));
|
||||
try testing.expect(isValidPropertyName("--123"));
|
||||
|
||||
try testing.expect(isValidPropertyName("-webkit-transform"));
|
||||
try testing.expect(isValidPropertyName("-moz-border-radius"));
|
||||
try testing.expect(isValidPropertyName("-ms-filter"));
|
||||
try testing.expect(isValidPropertyName("-o-transition"));
|
||||
|
||||
try testing.expect(!isValidPropertyName("-123invalid"));
|
||||
try testing.expect(!isValidPropertyName("--"));
|
||||
try testing.expect(!isValidPropertyName("-"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: startsWithFunction - case sensitivity and partial matches" {
|
||||
try testing.expect(CSSKeywords.startsWithFunction("RGB(255, 0, 0)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("Rgb(255, 0, 0)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("URL(image.png)"));
|
||||
|
||||
try testing.expect(CSSKeywords.startsWithFunction("rg(something)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("ur(something)"));
|
||||
|
||||
try testing.expect(CSSKeywords.startsWithFunction("rgb(1,2,3)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("rgba(1,2,3,4)"));
|
||||
|
||||
try testing.expect(CSSKeywords.startsWithFunction("my-custom-function(args)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("function-with-dashes(test)"));
|
||||
|
||||
try testing.expect(!CSSKeywords.startsWithFunction("123function(test)"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: isHexColor - Unicode and invalid characters" {
|
||||
try testing.expect(!isHexColor("#ghijkl"));
|
||||
try testing.expect(!isHexColor("#12345g"));
|
||||
try testing.expect(!isHexColor("#xyz"));
|
||||
|
||||
try testing.expect(!isHexColor("#АВС"));
|
||||
|
||||
try testing.expect(!isHexColor("#1234567g"));
|
||||
try testing.expect(!isHexColor("#g2345678"));
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: complex integration scenarios" {
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
try testing.expect(isMultiValueProperty("rgb(255,0,0) url(bg.jpg)"));
|
||||
|
||||
try testing.expect(!needsQuotes("calc(100% - 20px)"));
|
||||
|
||||
const result = try escapeCSSValue(allocator, "fake(function with spaces");
|
||||
try testing.expectEqual("\"fake(function with spaces\"", result);
|
||||
|
||||
const important_result = extractImportant("rgb(255,0,0) !important");
|
||||
try testing.expect(important_result.is_important);
|
||||
try testing.expectEqual("rgb(255,0,0)", important_result.value);
|
||||
}
|
||||
|
||||
test "Browser: CSS.StyleDeclaration: performance edge cases - empty and minimal inputs" {
|
||||
try testing.expect(!isNumericWithUnit(""));
|
||||
try testing.expect(!isHexColor(""));
|
||||
try testing.expect(!isMultiValueProperty(""));
|
||||
try testing.expect(!isAlreadyQuoted(""));
|
||||
try testing.expect(!isValidPropertyName(""));
|
||||
try testing.expect(needsQuotes(""));
|
||||
try testing.expect(!CSSKeywords.isKnownKeyword(""));
|
||||
try testing.expect(!CSSKeywords.containsSpecialChar(""));
|
||||
try testing.expect(!CSSKeywords.isValidUnit(""));
|
||||
try testing.expect(!CSSKeywords.startsWithFunction(""));
|
||||
|
||||
try testing.expect(!isNumericWithUnit("a"));
|
||||
try testing.expect(!isHexColor("a"));
|
||||
try testing.expect(!isMultiValueProperty("a"));
|
||||
try testing.expect(!isAlreadyQuoted("a"));
|
||||
try testing.expect(isValidPropertyName("a"));
|
||||
try testing.expect(!needsQuotes("a"));
|
||||
}
|
||||
97
src/browser/cssom/CSSStyleSheet.zig
Normal file
97
src/browser/cssom/CSSStyleSheet.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 Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const StyleSheet = @import("StyleSheet.zig");
|
||||
const CSSRuleList = @import("CSSRuleList.zig");
|
||||
const CSSImportRule = @import("CSSRule.zig").CSSImportRule;
|
||||
|
||||
const CSSStyleSheet = @This();
|
||||
pub const prototype = *StyleSheet;
|
||||
|
||||
proto: StyleSheet,
|
||||
css_rules: CSSRuleList,
|
||||
owner_rule: ?*CSSImportRule,
|
||||
|
||||
const CSSStyleSheetOpts = struct {
|
||||
base_url: ?[]const u8 = null,
|
||||
// TODO: Suupport media
|
||||
disabled: bool = false,
|
||||
};
|
||||
|
||||
pub fn constructor(_opts: ?CSSStyleSheetOpts) !CSSStyleSheet {
|
||||
const opts = _opts orelse CSSStyleSheetOpts{};
|
||||
return .{
|
||||
.proto = .{ .disabled = opts.disabled },
|
||||
.css_rules = .constructor(),
|
||||
.owner_rule = null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_ownerRule(_: *CSSStyleSheet) ?*CSSImportRule {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn get_cssRules(self: *CSSStyleSheet) *CSSRuleList {
|
||||
return &self.css_rules;
|
||||
}
|
||||
|
||||
pub fn _insertRule(self: *CSSStyleSheet, rule: []const u8, _index: ?usize, page: *Page) !usize {
|
||||
const index = _index orelse 0;
|
||||
if (index > self.css_rules.list.items.len) {
|
||||
return error.IndexSize;
|
||||
}
|
||||
|
||||
const arena = page.arena;
|
||||
try self.css_rules.list.insert(arena, index, try arena.dupe(u8, rule));
|
||||
return index;
|
||||
}
|
||||
|
||||
pub fn _deleteRule(self: *CSSStyleSheet, index: usize) !void {
|
||||
if (index > self.css_rules.list.items.len) {
|
||||
return error.IndexSize;
|
||||
}
|
||||
|
||||
_ = self.css_rules.list.orderedRemove(index);
|
||||
}
|
||||
|
||||
pub fn _replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !Env.Promise {
|
||||
_ = self;
|
||||
_ = text;
|
||||
// TODO: clear self.css_rules
|
||||
// parse text and re-populate self.css_rules
|
||||
|
||||
const resolver = page.main_context.createPromiseResolver();
|
||||
try resolver.resolve({});
|
||||
return resolver.promise();
|
||||
}
|
||||
|
||||
pub fn _replaceSync(self: *CSSStyleSheet, text: []const u8) !void {
|
||||
_ = self;
|
||||
_ = text;
|
||||
// TODO: clear self.css_rules
|
||||
// parse text and re-populate self.css_rules
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: CSS.StyleSheet" {
|
||||
try testing.htmlRunner("cssom/css_stylesheet.html");
|
||||
}
|
||||
@@ -19,37 +19,37 @@
|
||||
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",
|
||||
const StyleSheet = @This();
|
||||
|
||||
pub fn get_disabled(self: *const StyleSheet) bool {
|
||||
return self.disabled;
|
||||
}
|
||||
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_href(self: *const StyleSheet) []const u8 {
|
||||
return self.href;
|
||||
}
|
||||
pub fn get_disabled(self: *const StyleSheet) bool {
|
||||
return self.disabled;
|
||||
}
|
||||
|
||||
// TODO: media
|
||||
pub fn get_href(self: *const StyleSheet) []const u8 {
|
||||
return self.href;
|
||||
}
|
||||
|
||||
pub fn get_ownerNode(self: *const StyleSheet) ?*parser.Node {
|
||||
return self.owner_node;
|
||||
}
|
||||
// TODO: media
|
||||
|
||||
pub fn get_parentStyleSheet(self: *const StyleSheet) ?*StyleSheet {
|
||||
return self.parent_stylesheet;
|
||||
}
|
||||
pub fn get_ownerNode(self: *const StyleSheet) ?*parser.Node {
|
||||
return self.owner_node;
|
||||
}
|
||||
|
||||
pub fn get_title(self: *const StyleSheet) []const u8 {
|
||||
return self.title;
|
||||
}
|
||||
pub fn get_parentStyleSheet(self: *const StyleSheet) ?*StyleSheet {
|
||||
return self.parent_stylesheet;
|
||||
}
|
||||
|
||||
pub fn get_type(self: *const StyleSheet) []const u8 {
|
||||
return self.type;
|
||||
}
|
||||
};
|
||||
pub fn get_title(self: *const StyleSheet) []const u8 {
|
||||
return self.title;
|
||||
}
|
||||
|
||||
pub fn get_type(self: *const StyleSheet) []const u8 {
|
||||
return self.type;
|
||||
}
|
||||
@@ -1,291 +0,0 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const CSSConstants = struct {
|
||||
const IMPORTANT = "!important";
|
||||
const URL_PREFIX = "url(";
|
||||
};
|
||||
|
||||
pub const CSSParserState = enum {
|
||||
seek_name,
|
||||
in_name,
|
||||
seek_colon,
|
||||
seek_value,
|
||||
in_value,
|
||||
in_quoted_value,
|
||||
in_single_quoted_value,
|
||||
in_url,
|
||||
in_important,
|
||||
};
|
||||
|
||||
pub const CSSDeclaration = struct {
|
||||
name: []const u8,
|
||||
value: []const u8,
|
||||
is_important: bool,
|
||||
};
|
||||
|
||||
pub const CSSParser = struct {
|
||||
state: CSSParserState,
|
||||
name_start: usize,
|
||||
name_end: usize,
|
||||
value_start: usize,
|
||||
position: usize,
|
||||
paren_depth: usize,
|
||||
escape_next: bool,
|
||||
|
||||
pub fn init() CSSParser {
|
||||
return .{
|
||||
.state = .seek_name,
|
||||
.name_start = 0,
|
||||
.name_end = 0,
|
||||
.value_start = 0,
|
||||
.position = 0,
|
||||
.paren_depth = 0,
|
||||
.escape_next = false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn parseDeclarations(arena: Allocator, text: []const u8) ![]CSSDeclaration {
|
||||
var parser = init();
|
||||
var declarations: std.ArrayListUnmanaged(CSSDeclaration) = .empty;
|
||||
|
||||
while (parser.position < text.len) {
|
||||
const c = text[parser.position];
|
||||
|
||||
switch (parser.state) {
|
||||
.seek_name => {
|
||||
if (!std.ascii.isWhitespace(c)) {
|
||||
parser.name_start = parser.position;
|
||||
parser.state = .in_name;
|
||||
continue;
|
||||
}
|
||||
},
|
||||
.in_name => {
|
||||
if (c == ':') {
|
||||
parser.name_end = parser.position;
|
||||
parser.state = .seek_value;
|
||||
} else if (std.ascii.isWhitespace(c)) {
|
||||
parser.name_end = parser.position;
|
||||
parser.state = .seek_colon;
|
||||
}
|
||||
},
|
||||
.seek_colon => {
|
||||
if (c == ':') {
|
||||
parser.state = .seek_value;
|
||||
} else if (!std.ascii.isWhitespace(c)) {
|
||||
parser.state = .seek_name;
|
||||
continue;
|
||||
}
|
||||
},
|
||||
.seek_value => {
|
||||
if (!std.ascii.isWhitespace(c)) {
|
||||
parser.value_start = parser.position;
|
||||
if (c == '"') {
|
||||
parser.state = .in_quoted_value;
|
||||
} else if (c == '\'') {
|
||||
parser.state = .in_single_quoted_value;
|
||||
} else if (c == 'u' and parser.position + CSSConstants.URL_PREFIX.len <= text.len and std.mem.startsWith(u8, text[parser.position..], CSSConstants.URL_PREFIX)) {
|
||||
parser.state = .in_url;
|
||||
parser.paren_depth = 1;
|
||||
parser.position += 3;
|
||||
} else {
|
||||
parser.state = .in_value;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
},
|
||||
.in_value => {
|
||||
if (parser.escape_next) {
|
||||
parser.escape_next = false;
|
||||
} else if (c == '\\') {
|
||||
parser.escape_next = true;
|
||||
} else if (c == '(') {
|
||||
parser.paren_depth += 1;
|
||||
} else if (c == ')' and parser.paren_depth > 0) {
|
||||
parser.paren_depth -= 1;
|
||||
} else if (c == ';' and parser.paren_depth == 0) {
|
||||
try parser.finishDeclaration(arena, &declarations, text);
|
||||
parser.state = .seek_name;
|
||||
}
|
||||
},
|
||||
.in_quoted_value => {
|
||||
if (parser.escape_next) {
|
||||
parser.escape_next = false;
|
||||
} else if (c == '\\') {
|
||||
parser.escape_next = true;
|
||||
} else if (c == '"') {
|
||||
parser.state = .in_value;
|
||||
}
|
||||
},
|
||||
.in_single_quoted_value => {
|
||||
if (parser.escape_next) {
|
||||
parser.escape_next = false;
|
||||
} else if (c == '\\') {
|
||||
parser.escape_next = true;
|
||||
} else if (c == '\'') {
|
||||
parser.state = .in_value;
|
||||
}
|
||||
},
|
||||
.in_url => {
|
||||
if (parser.escape_next) {
|
||||
parser.escape_next = false;
|
||||
} else if (c == '\\') {
|
||||
parser.escape_next = true;
|
||||
} else if (c == '(') {
|
||||
parser.paren_depth += 1;
|
||||
} else if (c == ')') {
|
||||
parser.paren_depth -= 1;
|
||||
if (parser.paren_depth == 0) {
|
||||
parser.state = .in_value;
|
||||
}
|
||||
}
|
||||
},
|
||||
.in_important => {},
|
||||
}
|
||||
|
||||
parser.position += 1;
|
||||
}
|
||||
|
||||
try parser.finalize(arena, &declarations, text);
|
||||
|
||||
return declarations.items;
|
||||
}
|
||||
|
||||
fn finishDeclaration(self: *CSSParser, arena: Allocator, declarations: *std.ArrayListUnmanaged(CSSDeclaration), text: []const u8) !void {
|
||||
const name = std.mem.trim(u8, text[self.name_start..self.name_end], &std.ascii.whitespace);
|
||||
if (name.len == 0) return;
|
||||
|
||||
const raw_value = text[self.value_start..self.position];
|
||||
const value = std.mem.trim(u8, raw_value, &std.ascii.whitespace);
|
||||
|
||||
var final_value = value;
|
||||
var is_important = false;
|
||||
|
||||
if (std.mem.endsWith(u8, value, CSSConstants.IMPORTANT)) {
|
||||
is_important = true;
|
||||
final_value = std.mem.trimRight(u8, value[0 .. value.len - CSSConstants.IMPORTANT.len], &std.ascii.whitespace);
|
||||
}
|
||||
|
||||
try declarations.append(arena, .{
|
||||
.name = name,
|
||||
.value = final_value,
|
||||
.is_important = is_important,
|
||||
});
|
||||
}
|
||||
|
||||
fn finalize(self: *CSSParser, arena: Allocator, declarations: *std.ArrayListUnmanaged(CSSDeclaration), text: []const u8) !void {
|
||||
if (self.state != .in_value) {
|
||||
return;
|
||||
}
|
||||
return self.finishDeclaration(arena, declarations, text);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
test "CSSParser - Simple property" {
|
||||
defer testing.reset();
|
||||
|
||||
const text = "color: red;";
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
||||
|
||||
try testing.expectEqual(1, declarations.len);
|
||||
try testing.expectEqual("color", declarations[0].name);
|
||||
try testing.expectEqual("red", declarations[0].value);
|
||||
try testing.expectEqual(false, declarations[0].is_important);
|
||||
}
|
||||
|
||||
test "CSSParser - Property with !important" {
|
||||
defer testing.reset();
|
||||
const text = "margin: 10px !important;";
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
||||
|
||||
try testing.expectEqual(1, declarations.len);
|
||||
try testing.expectEqual("margin", declarations[0].name);
|
||||
try testing.expectEqual("10px", declarations[0].value);
|
||||
try testing.expectEqual(true, declarations[0].is_important);
|
||||
}
|
||||
|
||||
test "CSSParser - Multiple properties" {
|
||||
defer testing.reset();
|
||||
const text = "color: red; font-size: 12px; margin: 5px !important;";
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
||||
|
||||
try testing.expect(declarations.len == 3);
|
||||
|
||||
try testing.expectEqual("color", declarations[0].name);
|
||||
try testing.expectEqual("red", declarations[0].value);
|
||||
try testing.expectEqual(false, declarations[0].is_important);
|
||||
|
||||
try testing.expectEqual("font-size", declarations[1].name);
|
||||
try testing.expectEqual("12px", declarations[1].value);
|
||||
try testing.expectEqual(false, declarations[1].is_important);
|
||||
|
||||
try testing.expectEqual("margin", declarations[2].name);
|
||||
try testing.expectEqual("5px", declarations[2].value);
|
||||
try testing.expectEqual(true, declarations[2].is_important);
|
||||
}
|
||||
|
||||
test "CSSParser - Quoted value with semicolon" {
|
||||
defer testing.reset();
|
||||
const text = "content: \"Hello; world!\";";
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
||||
|
||||
try testing.expectEqual(1, declarations.len);
|
||||
try testing.expectEqual("content", declarations[0].name);
|
||||
try testing.expectEqual("\"Hello; world!\"", declarations[0].value);
|
||||
try testing.expectEqual(false, declarations[0].is_important);
|
||||
}
|
||||
|
||||
test "CSSParser - URL value" {
|
||||
defer testing.reset();
|
||||
const text = "background-image: url(\"test.png\");";
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
||||
|
||||
try testing.expectEqual(1, declarations.len);
|
||||
try testing.expectEqual("background-image", declarations[0].name);
|
||||
try testing.expectEqual("url(\"test.png\")", declarations[0].value);
|
||||
try testing.expectEqual(false, declarations[0].is_important);
|
||||
}
|
||||
|
||||
test "CSSParser - Whitespace handling" {
|
||||
defer testing.reset();
|
||||
const text = " color : purple ; margin : 10px ; ";
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
const declarations = try CSSParser.parseDeclarations(allocator, text);
|
||||
|
||||
try testing.expectEqual(2, declarations.len);
|
||||
try testing.expectEqual("color", declarations[0].name);
|
||||
try testing.expectEqual("purple", declarations[0].value);
|
||||
try testing.expectEqual("margin", declarations[1].name);
|
||||
try testing.expectEqual("10px", declarations[1].value);
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const 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" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const 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 CSSStyleDeclaration = struct {
|
||||
store: std.StringHashMapUnmanaged(Property),
|
||||
order: std.ArrayListUnmanaged([]const u8),
|
||||
|
||||
pub const empty: CSSStyleDeclaration = .{
|
||||
.store = .empty,
|
||||
.order = .empty,
|
||||
};
|
||||
|
||||
const Property = struct {
|
||||
value: []const u8,
|
||||
priority: bool,
|
||||
};
|
||||
|
||||
pub fn get_cssFloat(self: *const CSSStyleDeclaration) []const u8 {
|
||||
return self._getPropertyValue("float");
|
||||
}
|
||||
|
||||
pub fn set_cssFloat(self: *CSSStyleDeclaration, value: ?[]const u8, page: *Page) !void {
|
||||
const final_value = value orelse "";
|
||||
return self._setProperty("float", final_value, null, page);
|
||||
}
|
||||
|
||||
pub fn get_cssText(self: *const CSSStyleDeclaration, page: *Page) ![]const u8 {
|
||||
var buffer: std.ArrayListUnmanaged(u8) = .empty;
|
||||
const writer = buffer.writer(page.call_arena);
|
||||
for (self.order.items) |name| {
|
||||
const prop = self.store.get(name).?;
|
||||
const escaped = try CSSValueAnalyzer.escapeCSSValue(page.call_arena, prop.value);
|
||||
try writer.print("{s}: {s}", .{ name, escaped });
|
||||
if (prop.priority) try writer.writeAll(" !important");
|
||||
try writer.writeAll("; ");
|
||||
}
|
||||
return buffer.items;
|
||||
}
|
||||
|
||||
// TODO Propagate also upward to parent node
|
||||
pub fn set_cssText(self: *CSSStyleDeclaration, text: []const u8, page: *Page) !void {
|
||||
self.store.clearRetainingCapacity();
|
||||
self.order.clearRetainingCapacity();
|
||||
|
||||
// call_arena is safe here, because _setProperty will dupe the name
|
||||
// using the page's longer-living arena.
|
||||
const declarations = try CSSParser.parseDeclarations(page.call_arena, text);
|
||||
|
||||
for (declarations) |decl| {
|
||||
if (!CSSValueAnalyzer.isValidPropertyName(decl.name)) continue;
|
||||
const priority: ?[]const u8 = if (decl.is_important) "important" else null;
|
||||
try self._setProperty(decl.name, decl.value, priority, page);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_length(self: *const CSSStyleDeclaration) usize {
|
||||
return self.order.items.len;
|
||||
}
|
||||
|
||||
pub fn get_parentRule(_: *const CSSStyleDeclaration) ?CSSRule {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn _getPropertyPriority(self: *const CSSStyleDeclaration, name: []const u8) []const u8 {
|
||||
return if (self.store.get(name)) |prop| (if (prop.priority) "important" else "") else "";
|
||||
}
|
||||
|
||||
// TODO should handle properly shorthand properties and canonical forms
|
||||
pub fn _getPropertyValue(self: *const CSSStyleDeclaration, name: []const u8) []const u8 {
|
||||
if (self.store.get(name)) |prop| {
|
||||
return prop.value;
|
||||
}
|
||||
|
||||
// default to everything being visible (unless it's been explicitly set)
|
||||
if (std.mem.eql(u8, name, "visibility")) {
|
||||
return "visible";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn _item(self: *const CSSStyleDeclaration, index: usize) []const u8 {
|
||||
return if (index < self.order.items.len) self.order.items[index] else "";
|
||||
}
|
||||
|
||||
pub fn _removeProperty(self: *CSSStyleDeclaration, name: []const u8) ![]const u8 {
|
||||
const prop = self.store.fetchRemove(name) orelse return "";
|
||||
for (self.order.items, 0..) |item, i| {
|
||||
if (std.mem.eql(u8, item, name)) {
|
||||
_ = self.order.orderedRemove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// safe to return, since it's in our page.arena
|
||||
return prop.value.value;
|
||||
}
|
||||
|
||||
pub fn _setProperty(self: *CSSStyleDeclaration, name: []const u8, value: []const u8, priority: ?[]const u8, page: *Page) !void {
|
||||
const owned_value = try page.arena.dupe(u8, value);
|
||||
const is_important = priority != null and std.ascii.eqlIgnoreCase(priority.?, "important");
|
||||
|
||||
const gop = try self.store.getOrPut(page.arena, name);
|
||||
if (!gop.found_existing) {
|
||||
const owned_name = try page.arena.dupe(u8, name);
|
||||
gop.key_ptr.* = owned_name;
|
||||
try self.order.append(page.arena, owned_name);
|
||||
}
|
||||
|
||||
gop.value_ptr.* = .{ .value = owned_value, .priority = is_important };
|
||||
}
|
||||
|
||||
pub fn named_get(self: *const CSSStyleDeclaration, name: []const u8, _: *bool) []const u8 {
|
||||
return self._getPropertyValue(name);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
test "CSSOM.CSSStyleDeclaration" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let style = document.getElementById('content').style", "undefined" },
|
||||
.{ "style.cssText = 'color: red; font-size: 12px; margin: 5px !important;'", "color: red; font-size: 12px; margin: 5px !important;" },
|
||||
.{ "style.length", "3" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "style.getPropertyValue('color')", "red" },
|
||||
.{ "style.getPropertyValue('font-size')", "12px" },
|
||||
.{ "style.getPropertyValue('unknown-property')", "" },
|
||||
|
||||
.{ "style.getPropertyPriority('margin')", "important" },
|
||||
.{ "style.getPropertyPriority('color')", "" },
|
||||
.{ "style.getPropertyPriority('unknown-property')", "" },
|
||||
|
||||
.{ "style.item(0)", "color" },
|
||||
.{ "style.item(1)", "font-size" },
|
||||
.{ "style.item(2)", "margin" },
|
||||
.{ "style.item(3)", "" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "style.setProperty('background-color', 'blue')", "undefined" },
|
||||
.{ "style.getPropertyValue('background-color')", "blue" },
|
||||
.{ "style.length", "4" },
|
||||
|
||||
.{ "style.setProperty('color', 'green')", "undefined" },
|
||||
.{ "style.getPropertyValue('color')", "green" },
|
||||
.{ "style.length", "4" },
|
||||
.{ "style.color", "green" },
|
||||
|
||||
.{ "style.setProperty('padding', '10px', 'important')", "undefined" },
|
||||
.{ "style.getPropertyValue('padding')", "10px" },
|
||||
.{ "style.getPropertyPriority('padding')", "important" },
|
||||
|
||||
.{ "style.setProperty('border', '1px solid black', 'IMPORTANT')", "undefined" },
|
||||
.{ "style.getPropertyPriority('border')", "important" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "style.removeProperty('color')", "green" },
|
||||
.{ "style.getPropertyValue('color')", "" },
|
||||
.{ "style.length", "5" },
|
||||
|
||||
.{ "style.removeProperty('unknown-property')", "" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "style.cssText.includes('font-size: 12px;')", "true" },
|
||||
.{ "style.cssText.includes('margin: 5px !important;')", "true" },
|
||||
.{ "style.cssText.includes('padding: 10px !important;')", "true" },
|
||||
.{ "style.cssText.includes('border: 1px solid black !important;')", "true" },
|
||||
|
||||
.{ "style.cssText = 'color: purple; text-align: center;'", "color: purple; text-align: center;" },
|
||||
.{ "style.length", "2" },
|
||||
.{ "style.getPropertyValue('color')", "purple" },
|
||||
.{ "style.getPropertyValue('text-align')", "center" },
|
||||
.{ "style.getPropertyValue('font-size')", "" },
|
||||
|
||||
.{ "style.setProperty('cont', 'Hello; world!')", "undefined" },
|
||||
.{ "style.getPropertyValue('cont')", "Hello; world!" },
|
||||
|
||||
.{ "style.cssText = 'content: \"Hello; world!\"; background-image: url(\"test.png\");'", "content: \"Hello; world!\"; background-image: url(\"test.png\");" },
|
||||
.{ "style.getPropertyValue('content')", "\"Hello; world!\"" },
|
||||
.{ "style.getPropertyValue('background-image')", "url(\"test.png\")" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "style.cssFloat", "" },
|
||||
.{ "style.cssFloat = 'left'", "left" },
|
||||
.{ "style.cssFloat", "left" },
|
||||
.{ "style.getPropertyValue('float')", "left" },
|
||||
|
||||
.{ "style.cssFloat = 'right'", "right" },
|
||||
.{ "style.cssFloat", "right" },
|
||||
|
||||
.{ "style.cssFloat = null", "null" },
|
||||
.{ "style.cssFloat", "" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "style.setProperty('display', '')", "undefined" },
|
||||
.{ "style.getPropertyValue('display')", "" },
|
||||
|
||||
.{ "style.cssText = ' color : purple ; margin : 10px ; '", " color : purple ; margin : 10px ; " },
|
||||
.{ "style.getPropertyValue('color')", "purple" },
|
||||
.{ "style.getPropertyValue('margin')", "10px" },
|
||||
|
||||
.{ "style.setProperty('border-bottom-left-radius', '5px')", "undefined" },
|
||||
.{ "style.getPropertyValue('border-bottom-left-radius')", "5px" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "style.visibility", "visible" },
|
||||
.{ "style.getPropertyValue('visibility')", "visible" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const Page = @import("../page.zig").Page;
|
||||
const 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" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -1,811 +0,0 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
pub const CSSValueAnalyzer = struct {
|
||||
pub fn isNumericWithUnit(value: []const u8) bool {
|
||||
if (value.len == 0) return false;
|
||||
|
||||
if (!std.ascii.isDigit(value[0]) and
|
||||
value[0] != '+' and value[0] != '-' and value[0] != '.')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var i: usize = 0;
|
||||
var has_digit = false;
|
||||
var decimal_point = false;
|
||||
|
||||
while (i < value.len) : (i += 1) {
|
||||
const c = value[i];
|
||||
if (std.ascii.isDigit(c)) {
|
||||
has_digit = true;
|
||||
} else if (c == '.' and !decimal_point) {
|
||||
decimal_point = true;
|
||||
} else if ((c == 'e' or c == 'E') and has_digit) {
|
||||
if (i + 1 >= value.len) return false;
|
||||
if (value[i + 1] != '+' and value[i + 1] != '-' and !std.ascii.isDigit(value[i + 1])) break;
|
||||
i += 1;
|
||||
if (value[i] == '+' or value[i] == '-') {
|
||||
i += 1;
|
||||
}
|
||||
var has_exp_digits = false;
|
||||
while (i < value.len and std.ascii.isDigit(value[i])) : (i += 1) {
|
||||
has_exp_digits = true;
|
||||
}
|
||||
if (!has_exp_digits) return false;
|
||||
break;
|
||||
} else if (c != '-' and c != '+') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_digit) return false;
|
||||
|
||||
if (i == value.len) return true;
|
||||
|
||||
const unit = value[i..];
|
||||
return CSSKeywords.isValidUnit(unit);
|
||||
}
|
||||
|
||||
pub fn isHexColor(value: []const u8) bool {
|
||||
if (!std.mem.startsWith(u8, value, "#")) return false;
|
||||
|
||||
const hex_part = value[1..];
|
||||
if (hex_part.len != 3 and hex_part.len != 6 and hex_part.len != 8) return false;
|
||||
|
||||
for (hex_part) |c| {
|
||||
if (!std.ascii.isHex(c)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn isMultiValueProperty(value: []const u8) bool {
|
||||
var parts = std.mem.splitAny(u8, value, " ");
|
||||
var multi_value_parts: usize = 0;
|
||||
var all_parts_valid = true;
|
||||
|
||||
while (parts.next()) |part| {
|
||||
if (part.len == 0) continue;
|
||||
multi_value_parts += 1;
|
||||
|
||||
const is_numeric = isNumericWithUnit(part);
|
||||
const is_hex_color = isHexColor(part);
|
||||
const is_known_keyword = CSSKeywords.isKnownKeyword(part);
|
||||
const is_function = CSSKeywords.startsWithFunction(part);
|
||||
|
||||
if (!is_numeric and !is_hex_color and !is_known_keyword and !is_function) {
|
||||
all_parts_valid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return multi_value_parts >= 2 and all_parts_valid;
|
||||
}
|
||||
|
||||
pub fn isAlreadyQuoted(value: []const u8) bool {
|
||||
return value.len >= 2 and ((value[0] == '"' and value[value.len - 1] == '"') or
|
||||
(value[0] == '\'' and value[value.len - 1] == '\''));
|
||||
}
|
||||
|
||||
pub fn isValidPropertyName(name: []const u8) bool {
|
||||
if (name.len == 0) return false;
|
||||
|
||||
if (std.mem.startsWith(u8, name, "--")) {
|
||||
if (name.len == 2) return false;
|
||||
for (name[2..]) |c| {
|
||||
if (!std.ascii.isAlphanumeric(c) and c != '-' and c != '_') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const first_char = name[0];
|
||||
if (!std.ascii.isAlphabetic(first_char) and first_char != '-') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (first_char == '-') {
|
||||
if (name.len < 2) return false;
|
||||
|
||||
if (!std.ascii.isAlphabetic(name[1])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (name[2..]) |c| {
|
||||
if (!std.ascii.isAlphanumeric(c) and c != '-') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (name[1..]) |c| {
|
||||
if (!std.ascii.isAlphanumeric(c) and c != '-') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn extractImportant(value: []const u8) struct { value: []const u8, is_important: bool } {
|
||||
const trimmed = std.mem.trim(u8, value, &std.ascii.whitespace);
|
||||
|
||||
if (std.mem.endsWith(u8, trimmed, "!important")) {
|
||||
const clean_value = std.mem.trimRight(u8, trimmed[0 .. trimmed.len - 10], &std.ascii.whitespace);
|
||||
return .{ .value = clean_value, .is_important = true };
|
||||
}
|
||||
|
||||
return .{ .value = trimmed, .is_important = false };
|
||||
}
|
||||
|
||||
pub fn needsQuotes(value: []const u8) bool {
|
||||
if (value.len == 0) return true;
|
||||
if (isAlreadyQuoted(value)) return false;
|
||||
|
||||
if (CSSKeywords.containsSpecialChar(value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std.mem.indexOfScalar(u8, value, ' ') == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const is_url = std.mem.startsWith(u8, value, "url(");
|
||||
const is_function = CSSKeywords.startsWithFunction(value);
|
||||
|
||||
return !isMultiValueProperty(value) and
|
||||
!is_url and
|
||||
!is_function;
|
||||
}
|
||||
|
||||
pub fn escapeCSSValue(arena: std.mem.Allocator, value: []const u8) ![]const u8 {
|
||||
if (!needsQuotes(value)) {
|
||||
return value;
|
||||
}
|
||||
var out: std.ArrayListUnmanaged(u8) = .empty;
|
||||
|
||||
// We'll need at least this much space, +2 for the quotes
|
||||
try out.ensureTotalCapacity(arena, value.len + 2);
|
||||
const writer = out.writer(arena);
|
||||
|
||||
try writer.writeByte('"');
|
||||
|
||||
for (value, 0..) |c, i| {
|
||||
switch (c) {
|
||||
'"' => try writer.writeAll("\\\""),
|
||||
'\\' => try writer.writeAll("\\\\"),
|
||||
'\n' => try writer.writeAll("\\A "),
|
||||
'\r' => try writer.writeAll("\\D "),
|
||||
'\t' => try writer.writeAll("\\9 "),
|
||||
0...8, 11, 12, 14...31, 127 => {
|
||||
try writer.print("\\{x}", .{c});
|
||||
if (i + 1 < value.len and std.ascii.isHex(value[i + 1])) {
|
||||
try writer.writeByte(' ');
|
||||
}
|
||||
},
|
||||
else => try writer.writeByte(c),
|
||||
}
|
||||
}
|
||||
|
||||
try writer.writeByte('"');
|
||||
return out.items;
|
||||
}
|
||||
|
||||
pub fn isKnownKeyword(value: []const u8) bool {
|
||||
return CSSKeywords.isKnownKeyword(value);
|
||||
}
|
||||
|
||||
pub fn containsSpecialChar(value: []const u8) bool {
|
||||
return CSSKeywords.containsSpecialChar(value);
|
||||
}
|
||||
};
|
||||
|
||||
const CSSKeywords = struct {
|
||||
const border_styles = [_][]const u8{
|
||||
"none", "solid", "dotted", "dashed", "double", "groove", "ridge", "inset", "outset",
|
||||
};
|
||||
|
||||
const color_names = [_][]const u8{
|
||||
"black", "white", "red", "green", "blue", "yellow", "purple", "gray", "transparent",
|
||||
"currentColor", "inherit",
|
||||
};
|
||||
|
||||
const position_keywords = [_][]const u8{
|
||||
"auto", "center", "left", "right", "top", "bottom",
|
||||
};
|
||||
|
||||
const background_repeat = [_][]const u8{
|
||||
"repeat", "no-repeat", "repeat-x", "repeat-y", "space", "round",
|
||||
};
|
||||
|
||||
const font_styles = [_][]const u8{
|
||||
"normal", "italic", "oblique", "bold", "bolder", "lighter",
|
||||
};
|
||||
|
||||
const font_sizes = [_][]const u8{
|
||||
"xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large",
|
||||
"smaller", "larger",
|
||||
};
|
||||
|
||||
const font_families = [_][]const u8{
|
||||
"serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui",
|
||||
};
|
||||
|
||||
const css_global = [_][]const u8{
|
||||
"initial", "inherit", "unset", "revert",
|
||||
};
|
||||
|
||||
const display_values = [_][]const u8{
|
||||
"block", "inline", "inline-block", "flex", "grid", "none",
|
||||
};
|
||||
|
||||
const length_units = [_][]const u8{
|
||||
"px", "em", "rem", "vw", "vh", "vmin", "vmax", "%", "pt", "pc", "in", "cm", "mm",
|
||||
"ex", "ch", "fr",
|
||||
};
|
||||
|
||||
const angle_units = [_][]const u8{
|
||||
"deg", "rad", "grad", "turn",
|
||||
};
|
||||
|
||||
const time_units = [_][]const u8{
|
||||
"s", "ms",
|
||||
};
|
||||
|
||||
const frequency_units = [_][]const u8{
|
||||
"Hz", "kHz",
|
||||
};
|
||||
|
||||
const resolution_units = [_][]const u8{
|
||||
"dpi", "dpcm", "dppx",
|
||||
};
|
||||
|
||||
const special_chars = [_]u8{
|
||||
'"', '\'', ';', '{', '}', '\\', '<', '>', '/', '\n', '\t', '\r', '\x00', '\x7F',
|
||||
};
|
||||
|
||||
const functions = [_][]const u8{
|
||||
"rgb(", "rgba(", "hsl(", "hsla(", "url(", "calc(", "var(", "attr(",
|
||||
"linear-gradient(", "radial-gradient(", "conic-gradient(", "translate(", "rotate(", "scale(", "skew(", "matrix(",
|
||||
};
|
||||
|
||||
pub fn isKnownKeyword(value: []const u8) bool {
|
||||
const all_categories = [_][]const []const u8{
|
||||
&border_styles, &color_names, &position_keywords, &background_repeat,
|
||||
&font_styles, &font_sizes, &font_families, &css_global,
|
||||
&display_values,
|
||||
};
|
||||
|
||||
for (all_categories) |category| {
|
||||
for (category) |keyword| {
|
||||
if (std.ascii.eqlIgnoreCase(value, keyword)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn containsSpecialChar(value: []const u8) bool {
|
||||
for (value) |c| {
|
||||
for (special_chars) |special| {
|
||||
if (c == special) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn isValidUnit(unit: []const u8) bool {
|
||||
const all_units = [_][]const []const u8{
|
||||
&length_units, &angle_units, &time_units, &frequency_units, &resolution_units,
|
||||
};
|
||||
|
||||
for (all_units) |category| {
|
||||
for (category) |valid_unit| {
|
||||
if (std.ascii.eqlIgnoreCase(unit, valid_unit)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn startsWithFunction(value: []const u8) bool {
|
||||
const pos = std.mem.indexOfScalar(u8, value, '(') orelse return false;
|
||||
if (pos == 0) return false;
|
||||
|
||||
if (std.mem.indexOfScalarPos(u8, value, pos, ')') == null) {
|
||||
return false;
|
||||
}
|
||||
const function_name = value[0..pos];
|
||||
return isValidFunctionName(function_name);
|
||||
}
|
||||
|
||||
fn isValidFunctionName(name: []const u8) bool {
|
||||
if (name.len == 0) return false;
|
||||
|
||||
const first = name[0];
|
||||
if (!std.ascii.isAlphabetic(first) and first != '_' and first != '-') {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (name[1..]) |c| {
|
||||
if (!std.ascii.isAlphanumeric(c) and c != '_' and c != '-') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
test "CSSValueAnalyzer: isNumericWithUnit - valid numbers with units" {
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("10px"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("3.14em"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("-5rem"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("+12.5%"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("0vh"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit(".5vw"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isNumericWithUnit - scientific notation" {
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("1e5px"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("2.5E-3em"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("1e+2rem"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("-3.14e10px"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isNumericWithUnit - edge cases and invalid inputs" {
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(""));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("px"));
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("--px"));
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(".px"));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("1e"));
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("1epx"));
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("1e+"));
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("1e+px"));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("1.2.3px"));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("10xyz"));
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("5invalid"));
|
||||
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("10"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("3.14"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("-5"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isHexColor - valid hex colors" {
|
||||
try testing.expect(CSSValueAnalyzer.isHexColor("#000"));
|
||||
try testing.expect(CSSValueAnalyzer.isHexColor("#fff"));
|
||||
try testing.expect(CSSValueAnalyzer.isHexColor("#123456"));
|
||||
try testing.expect(CSSValueAnalyzer.isHexColor("#abcdef"));
|
||||
try testing.expect(CSSValueAnalyzer.isHexColor("#ABCDEF"));
|
||||
try testing.expect(CSSValueAnalyzer.isHexColor("#12345678"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isHexColor - invalid hex colors" {
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor(""));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#"));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("000"));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#00"));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#0000"));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#00000"));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#0000000"));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#000000000"));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#gggggg"));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#123xyz"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isMultiValueProperty - valid multi-value properties" {
|
||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("10px 20px"));
|
||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("solid red"));
|
||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("#fff black"));
|
||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("1em 2em 3em 4em"));
|
||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("rgb(255,0,0) solid"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isMultiValueProperty - invalid multi-value properties" {
|
||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty(""));
|
||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("10px"));
|
||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("invalid unknown"));
|
||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("10px invalid"));
|
||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty(" "));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isAlreadyQuoted - various quoting scenarios" {
|
||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("\"hello\""));
|
||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("'world'"));
|
||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("\"\""));
|
||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("''"));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted(""));
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("hello"));
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("\""));
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("'"));
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("\"hello'"));
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("'hello\""));
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("\"hello"));
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("hello\""));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isValidPropertyName - valid property names" {
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("color"));
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("background-color"));
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("-webkit-transform"));
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("font-size"));
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("margin-top"));
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("z-index"));
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("line-height"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isValidPropertyName - invalid property names" {
|
||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName(""));
|
||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("123color"));
|
||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("color!"));
|
||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("color space"));
|
||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("@color"));
|
||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("color.test"));
|
||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("color_test"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: extractImportant - with and without !important" {
|
||||
var result = CSSValueAnalyzer.extractImportant("red !important");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("red", result.value);
|
||||
|
||||
result = CSSValueAnalyzer.extractImportant("blue");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("blue", result.value);
|
||||
|
||||
result = CSSValueAnalyzer.extractImportant(" green !important ");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("green", result.value);
|
||||
|
||||
result = CSSValueAnalyzer.extractImportant("!important");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("", result.value);
|
||||
|
||||
result = CSSValueAnalyzer.extractImportant("important");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("important", result.value);
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: needsQuotes - various scenarios" {
|
||||
try testing.expect(CSSValueAnalyzer.needsQuotes(""));
|
||||
try testing.expect(CSSValueAnalyzer.needsQuotes("hello world"));
|
||||
try testing.expect(CSSValueAnalyzer.needsQuotes("test;"));
|
||||
try testing.expect(CSSValueAnalyzer.needsQuotes("a{b}"));
|
||||
try testing.expect(CSSValueAnalyzer.needsQuotes("test\"quote"));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("\"already quoted\""));
|
||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("'already quoted'"));
|
||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("url(image.png)"));
|
||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("rgb(255, 0, 0)"));
|
||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("10px 20px"));
|
||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("simple"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: escapeCSSValue - escaping various characters" {
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
var result = try CSSValueAnalyzer.escapeCSSValue(allocator, "simple");
|
||||
try testing.expectEqual("simple", result);
|
||||
|
||||
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "\"already quoted\"");
|
||||
try testing.expectEqual("\"already quoted\"", result);
|
||||
|
||||
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\"quote");
|
||||
try testing.expectEqual("\"test\\\"quote\"", result);
|
||||
|
||||
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\nline");
|
||||
try testing.expectEqual("\"test\\A line\"", result);
|
||||
|
||||
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\\back");
|
||||
try testing.expectEqual("\"test\\\\back\"", result);
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: CSSKeywords.isKnownKeyword - case sensitivity" {
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("red"));
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("solid"));
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("center"));
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("inherit"));
|
||||
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("RED"));
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("Red"));
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("SOLID"));
|
||||
try testing.expect(CSSKeywords.isKnownKeyword("Center"));
|
||||
|
||||
try testing.expect(!CSSKeywords.isKnownKeyword("invalid"));
|
||||
try testing.expect(!CSSKeywords.isKnownKeyword("unknown"));
|
||||
try testing.expect(!CSSKeywords.isKnownKeyword(""));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: CSSKeywords.containsSpecialChar - various special characters" {
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test\"quote"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test'quote"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test;end"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test{brace"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test}brace"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test\\back"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test<angle"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test>angle"));
|
||||
try testing.expect(CSSKeywords.containsSpecialChar("test/slash"));
|
||||
|
||||
try testing.expect(!CSSKeywords.containsSpecialChar("normal-text"));
|
||||
try testing.expect(!CSSKeywords.containsSpecialChar("text123"));
|
||||
try testing.expect(!CSSKeywords.containsSpecialChar(""));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: CSSKeywords.isValidUnit - various units" {
|
||||
try testing.expect(CSSKeywords.isValidUnit("px"));
|
||||
try testing.expect(CSSKeywords.isValidUnit("em"));
|
||||
try testing.expect(CSSKeywords.isValidUnit("rem"));
|
||||
try testing.expect(CSSKeywords.isValidUnit("%"));
|
||||
|
||||
try testing.expect(CSSKeywords.isValidUnit("deg"));
|
||||
try testing.expect(CSSKeywords.isValidUnit("rad"));
|
||||
|
||||
try testing.expect(CSSKeywords.isValidUnit("s"));
|
||||
try testing.expect(CSSKeywords.isValidUnit("ms"));
|
||||
|
||||
try testing.expect(CSSKeywords.isValidUnit("PX"));
|
||||
|
||||
try testing.expect(!CSSKeywords.isValidUnit("invalid"));
|
||||
try testing.expect(!CSSKeywords.isValidUnit(""));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: CSSKeywords.startsWithFunction - function detection" {
|
||||
try testing.expect(CSSKeywords.startsWithFunction("rgb(255, 0, 0)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("rgba(255, 0, 0, 0.5)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("url(image.png)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("calc(100% - 20px)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("var(--custom-property)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("linear-gradient(to right, red, blue)"));
|
||||
|
||||
try testing.expect(CSSKeywords.startsWithFunction("custom-function(args)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("unknown(test)"));
|
||||
|
||||
try testing.expect(!CSSKeywords.startsWithFunction("not-a-function"));
|
||||
try testing.expect(!CSSKeywords.startsWithFunction("missing-paren)"));
|
||||
try testing.expect(!CSSKeywords.startsWithFunction("missing-close("));
|
||||
try testing.expect(!CSSKeywords.startsWithFunction(""));
|
||||
try testing.expect(!CSSKeywords.startsWithFunction("rgb"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isNumericWithUnit - whitespace handling" {
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(" 10px"));
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("10 px"));
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("10px "));
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(" 10 px "));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: extractImportant - whitespace edge cases" {
|
||||
var result = CSSValueAnalyzer.extractImportant(" ");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("", result.value);
|
||||
|
||||
result = CSSValueAnalyzer.extractImportant("\t\n\r !important\t\n");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("", result.value);
|
||||
|
||||
result = CSSValueAnalyzer.extractImportant("red\t!important");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("red", result.value);
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isHexColor - mixed case handling" {
|
||||
try testing.expect(CSSValueAnalyzer.isHexColor("#AbC"));
|
||||
try testing.expect(CSSValueAnalyzer.isHexColor("#123aBc"));
|
||||
try testing.expect(CSSValueAnalyzer.isHexColor("#FFffFF"));
|
||||
try testing.expect(CSSValueAnalyzer.isHexColor("#000FFF"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: edge case - very long inputs" {
|
||||
const long_valid = "a" ** 1000 ++ "px";
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(long_valid)); // not numeric
|
||||
|
||||
const long_property = "a-" ** 100 ++ "property";
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName(long_property));
|
||||
|
||||
const long_hex = "#" ++ "a" ** 20;
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor(long_hex));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: boundary conditions - numeric parsing" {
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("0px"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("0.0px"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit(".0px"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("0.px"));
|
||||
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("999999999px"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("1.7976931348623157e+308px"));
|
||||
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("0.000000001px"));
|
||||
try testing.expect(CSSValueAnalyzer.isNumericWithUnit("1e-100px"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: extractImportant - malformed important declarations" {
|
||||
var result = CSSValueAnalyzer.extractImportant("red ! important");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("red ! important", result.value);
|
||||
|
||||
result = CSSValueAnalyzer.extractImportant("red !Important");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("red !Important", result.value);
|
||||
|
||||
result = CSSValueAnalyzer.extractImportant("red !IMPORTANT");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("red !IMPORTANT", result.value);
|
||||
|
||||
result = CSSValueAnalyzer.extractImportant("!importantred");
|
||||
try testing.expect(!result.is_important);
|
||||
try testing.expectEqual("!importantred", result.value);
|
||||
|
||||
result = CSSValueAnalyzer.extractImportant("red !important !important");
|
||||
try testing.expect(result.is_important);
|
||||
try testing.expectEqual("red !important", result.value);
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isMultiValueProperty - complex spacing scenarios" {
|
||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("10px 20px"));
|
||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("solid red"));
|
||||
|
||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty(" 10px 20px "));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("10px\t20px"));
|
||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("10px\n20px"));
|
||||
|
||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("10px 20px 30px"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isAlreadyQuoted - edge cases with quotes" {
|
||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("\"'hello'\""));
|
||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("'\"hello\"'"));
|
||||
|
||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("\"hello\\\"world\""));
|
||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("'hello\\'world'"));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("\"hello"));
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("hello\""));
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("'hello"));
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("hello'"));
|
||||
|
||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("\"a\""));
|
||||
try testing.expect(CSSValueAnalyzer.isAlreadyQuoted("'b'"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: needsQuotes - function and URL edge cases" {
|
||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("rgb(255, 0, 0)"));
|
||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("calc(100% - 20px)"));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("url(path with spaces.jpg)"));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("linear-gradient(to right, red, blue)"));
|
||||
|
||||
try testing.expect(CSSValueAnalyzer.needsQuotes("rgb(255, 0, 0"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: escapeCSSValue - control characters and Unicode" {
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
var result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\ttab");
|
||||
try testing.expectEqual("\"test\\9 tab\"", result);
|
||||
|
||||
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\rreturn");
|
||||
try testing.expectEqual("\"test\\D return\"", result);
|
||||
|
||||
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\x00null");
|
||||
try testing.expectEqual("\"test\\0null\"", result);
|
||||
|
||||
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\x7Fdel");
|
||||
try testing.expectEqual("\"test\\7f del\"", result);
|
||||
|
||||
result = try CSSValueAnalyzer.escapeCSSValue(allocator, "test\"quote\nline\\back");
|
||||
try testing.expectEqual("\"test\\\"quote\\A line\\\\back\"", result);
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isValidPropertyName - CSS custom properties and vendor prefixes" {
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("--custom-color"));
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("--my-variable"));
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("--123"));
|
||||
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("-webkit-transform"));
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("-moz-border-radius"));
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("-ms-filter"));
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("-o-transition"));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("-123invalid"));
|
||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("--"));
|
||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName("-"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: startsWithFunction - case sensitivity and partial matches" {
|
||||
try testing.expect(CSSKeywords.startsWithFunction("RGB(255, 0, 0)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("Rgb(255, 0, 0)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("URL(image.png)"));
|
||||
|
||||
try testing.expect(CSSKeywords.startsWithFunction("rg(something)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("ur(something)"));
|
||||
|
||||
try testing.expect(CSSKeywords.startsWithFunction("rgb(1,2,3)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("rgba(1,2,3,4)"));
|
||||
|
||||
try testing.expect(CSSKeywords.startsWithFunction("my-custom-function(args)"));
|
||||
try testing.expect(CSSKeywords.startsWithFunction("function-with-dashes(test)"));
|
||||
|
||||
try testing.expect(!CSSKeywords.startsWithFunction("123function(test)"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: isHexColor - Unicode and invalid characters" {
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#ghijkl"));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#12345g"));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#xyz"));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#АВС"));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#1234567g"));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("#g2345678"));
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: complex integration scenarios" {
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
try testing.expect(CSSValueAnalyzer.isMultiValueProperty("rgb(255,0,0) url(bg.jpg)"));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("calc(100% - 20px)"));
|
||||
|
||||
const result = try CSSValueAnalyzer.escapeCSSValue(allocator, "fake(function with spaces");
|
||||
try testing.expectEqual("\"fake(function with spaces\"", result);
|
||||
|
||||
const important_result = CSSValueAnalyzer.extractImportant("rgb(255,0,0) !important");
|
||||
try testing.expect(important_result.is_important);
|
||||
try testing.expectEqual("rgb(255,0,0)", important_result.value);
|
||||
}
|
||||
|
||||
test "CSSValueAnalyzer: performance edge cases - empty and minimal inputs" {
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit(""));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor(""));
|
||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty(""));
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted(""));
|
||||
try testing.expect(!CSSValueAnalyzer.isValidPropertyName(""));
|
||||
try testing.expect(CSSValueAnalyzer.needsQuotes(""));
|
||||
try testing.expect(!CSSKeywords.isKnownKeyword(""));
|
||||
try testing.expect(!CSSKeywords.containsSpecialChar(""));
|
||||
try testing.expect(!CSSKeywords.isValidUnit(""));
|
||||
try testing.expect(!CSSKeywords.startsWithFunction(""));
|
||||
|
||||
try testing.expect(!CSSValueAnalyzer.isNumericWithUnit("a"));
|
||||
try testing.expect(!CSSValueAnalyzer.isHexColor("a"));
|
||||
try testing.expect(!CSSValueAnalyzer.isMultiValueProperty("a"));
|
||||
try testing.expect(!CSSValueAnalyzer.isAlreadyQuoted("a"));
|
||||
try testing.expect(CSSValueAnalyzer.isValidPropertyName("a"));
|
||||
try testing.expect(!CSSValueAnalyzer.needsQuotes("a"));
|
||||
}
|
||||
@@ -16,15 +16,10 @@
|
||||
// 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,
|
||||
@import("StyleSheet.zig"),
|
||||
@import("CSSStyleSheet.zig"),
|
||||
@import("CSSStyleDeclaration.zig"),
|
||||
@import("CSSRuleList.zig"),
|
||||
@import("CSSRule.zig").Interfaces,
|
||||
};
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
// Represents https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data
|
||||
pub const DataURI = struct {
|
||||
was_base64_encoded: bool,
|
||||
// The contents in the uri. It will be base64 decoded but not prepared in
|
||||
// any way for mime.charset.
|
||||
data: []const u8,
|
||||
|
||||
// Parses data:[<media-type>][;base64],<data>
|
||||
pub fn parse(allocator: Allocator, src: []const u8) !?DataURI {
|
||||
if (!std.mem.startsWith(u8, src, "data:")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uri = src[5..];
|
||||
const data_starts = std.mem.indexOfScalar(u8, uri, ',') orelse return null;
|
||||
|
||||
// Extract the encoding.
|
||||
var metadata = uri[0..data_starts];
|
||||
var base64_encoded = false;
|
||||
if (std.mem.endsWith(u8, metadata, ";base64")) {
|
||||
base64_encoded = true;
|
||||
metadata = metadata[0 .. metadata.len - 7];
|
||||
}
|
||||
|
||||
// TODO: Extract mime type. This not trivial because Mime.parse requires
|
||||
// a []u8 and might mutate the src. And, the DataURI.parse references atm
|
||||
// do not have deinit calls.
|
||||
|
||||
// Prepare the data.
|
||||
var data = uri[data_starts + 1 ..];
|
||||
if (base64_encoded) {
|
||||
const decoder = std.base64.standard.Decoder;
|
||||
const decoded_size = try decoder.calcSizeForSlice(data);
|
||||
|
||||
const buffer = try allocator.alloc(u8, decoded_size);
|
||||
errdefer allocator.free(buffer);
|
||||
|
||||
try decoder.decode(buffer, data);
|
||||
data = buffer;
|
||||
}
|
||||
|
||||
return .{
|
||||
.was_base64_encoded = base64_encoded,
|
||||
.data = data,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const DataURI, allocator: Allocator) void {
|
||||
if (self.was_base64_encoded) {
|
||||
allocator.free(self.data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const testing = std.testing;
|
||||
test "DataURI: parse valid" {
|
||||
try test_valid("data:text/javascript; charset=utf-8;base64,Zm9v", "foo");
|
||||
try test_valid("data:text/javascript; charset=utf-8;,foo", "foo");
|
||||
try test_valid("data:,foo", "foo");
|
||||
}
|
||||
|
||||
test "DataURI: parse invalid" {
|
||||
try test_cannot_parse("atad:,foo");
|
||||
try test_cannot_parse("data:foo");
|
||||
try test_cannot_parse("data:");
|
||||
}
|
||||
|
||||
fn test_valid(uri: []const u8, expected: []const u8) !void {
|
||||
const data_uri = try DataURI.parse(std.testing.allocator, uri) orelse return error.TestFailed;
|
||||
defer data_uri.deinit(testing.allocator);
|
||||
try testing.expectEqualStrings(expected, data_uri.data);
|
||||
}
|
||||
|
||||
fn test_cannot_parse(uri: []const u8) !void {
|
||||
try testing.expectEqual(null, DataURI.parse(std.testing.allocator, uri));
|
||||
}
|
||||
@@ -104,23 +104,6 @@ pub fn _reverse(self: *const Animation) void {
|
||||
}
|
||||
|
||||
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" },
|
||||
}, .{});
|
||||
test "Browser: DOM.Animation" {
|
||||
try testing.htmlRunner("dom/animation.html");
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ pub const MessageEvent = struct {
|
||||
const event = try parser.eventCreate();
|
||||
defer parser.eventDestroy(event);
|
||||
try parser.eventInit(event, "message", .{});
|
||||
try parser.eventSetInternalType(event, .message_event);
|
||||
parser.eventSetInternalType(event, .message_event);
|
||||
|
||||
return .{
|
||||
.proto = event.*,
|
||||
@@ -286,76 +286,6 @@ pub const MessageEvent = struct {
|
||||
};
|
||||
|
||||
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" },
|
||||
}, .{});
|
||||
test "Browser: DOM.MessageChannel" {
|
||||
try testing.htmlRunner("dom/message_channel.html");
|
||||
}
|
||||
|
||||
@@ -70,32 +70,6 @@ pub const Attr = struct {
|
||||
// -----
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.Attribute" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let a = document.createAttributeNS('foo', 'bar')", "undefined" },
|
||||
.{ "a.namespaceURI", "foo" },
|
||||
.{ "a.prefix", "null" },
|
||||
.{ "a.localName", "bar" },
|
||||
.{ "a.name", "bar" },
|
||||
.{ "a.value", "" },
|
||||
// TODO: libdom has a bug here: the created attr has no parent, it
|
||||
// causes a panic w/ libdom when setting the value.
|
||||
//.{ "a.value = 'nok'", "nok" },
|
||||
.{ "a.ownerElement", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let b = document.getElementById('link').getAttributeNode('class')", "undefined" },
|
||||
.{ "b.name", "class" },
|
||||
.{ "b.value", "ok" },
|
||||
.{ "b.value = 'nok'", "nok" },
|
||||
.{ "b.value", "nok" },
|
||||
.{ "b.value = null", "null" },
|
||||
.{ "b.value", "null" },
|
||||
.{ "b.value = 'ok'", "ok" },
|
||||
.{ "b.ownerElement.id", "link" },
|
||||
}, .{});
|
||||
test "Browser: DOM.Attribute" {
|
||||
try testing.htmlRunner("dom/attribute.html");
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@ const Node = @import("node.zig").Node;
|
||||
const Comment = @import("comment.zig").Comment;
|
||||
const Text = @import("text.zig");
|
||||
const ProcessingInstruction = @import("processing_instruction.zig").ProcessingInstruction;
|
||||
const HTMLElem = @import("../html/elements.zig");
|
||||
const Element = @import("element.zig").Element;
|
||||
const ElementUnion = @import("element.zig").Union;
|
||||
|
||||
// CharacterData interfaces
|
||||
pub const Interfaces = .{
|
||||
@@ -49,20 +50,20 @@ pub const CharacterData = struct {
|
||||
return try parser.characterDataLength(self);
|
||||
}
|
||||
|
||||
pub fn get_nextElementSibling(self: *parser.CharacterData) !?HTMLElem.Union {
|
||||
pub fn get_nextElementSibling(self: *parser.CharacterData) !?ElementUnion {
|
||||
const res = try parser.nodeNextElementSibling(parser.characterDataToNode(self));
|
||||
if (res == null) {
|
||||
return null;
|
||||
}
|
||||
return try HTMLElem.toInterface(HTMLElem.Union, res.?);
|
||||
return try Element.toInterface(res.?);
|
||||
}
|
||||
|
||||
pub fn get_previousElementSibling(self: *parser.CharacterData) !?HTMLElem.Union {
|
||||
pub fn get_previousElementSibling(self: *parser.CharacterData) !?ElementUnion {
|
||||
const res = try parser.nodePreviousElementSibling(parser.characterDataToNode(self));
|
||||
if (res == null) {
|
||||
return null;
|
||||
}
|
||||
return try HTMLElem.toInterface(HTMLElem.Union, res.?);
|
||||
return try Element.toInterface(res.?);
|
||||
}
|
||||
|
||||
// Read/Write attributes
|
||||
@@ -101,7 +102,7 @@ pub const CharacterData = struct {
|
||||
// netsurf's CharacterData (text, comment) doesn't implement the
|
||||
// dom_node_get_attributes and thus will crash if we try to call nodeIsEqualNode.
|
||||
pub fn _isEqualNode(self: *parser.CharacterData, other_node: *parser.Node) !bool {
|
||||
if (try parser.nodeType(@alignCast(@ptrCast(self))) != try parser.nodeType(other_node)) {
|
||||
if (try parser.nodeType(@ptrCast(@alignCast(self))) != try parser.nodeType(other_node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -128,69 +129,6 @@ pub const CharacterData = struct {
|
||||
// -----
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.CharacterData" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let link = document.getElementById('link')", "undefined" },
|
||||
.{ "let cdata = link.firstChild", "undefined" },
|
||||
.{ "cdata.data", "OK" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.data = 'OK modified'", "OK modified" },
|
||||
.{ "cdata.data === 'OK modified'", "true" },
|
||||
.{ "cdata.data = 'OK'", "OK" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.length === 2", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.nextElementSibling === null", "true" },
|
||||
// create a next element
|
||||
.{ "let next = document.createElement('a')", "undefined" },
|
||||
.{ "link.appendChild(next, cdata) !== undefined", "true" },
|
||||
.{ "cdata.nextElementSibling.localName === 'a' ", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.previousElementSibling === null", "true" },
|
||||
// create a prev element
|
||||
.{ "let prev = document.createElement('div')", "undefined" },
|
||||
.{ "link.insertBefore(prev, cdata) !== undefined", "true" },
|
||||
.{ "cdata.previousElementSibling.localName === 'div' ", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.appendData(' modified')", "undefined" },
|
||||
.{ "cdata.data === 'OK modified' ", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.deleteData('OK'.length, ' modified'.length)", "undefined" },
|
||||
.{ "cdata.data == 'OK'", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.insertData('OK'.length-1, 'modified')", "undefined" },
|
||||
.{ "cdata.data == 'OmodifiedK'", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.replaceData('OK'.length-1, 'modified'.length, 'replaced')", "undefined" },
|
||||
.{ "cdata.data == 'OreplacedK'", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.substringData('OK'.length-1, 'replaced'.length) == 'replaced'", "true" },
|
||||
.{ "cdata.substringData('OK'.length-1, 0) == ''", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "cdata.substringData('OK'.length-1, 'replaced'.length) == 'replaced'", "true" },
|
||||
.{ "cdata.substringData('OK'.length-1, 0) == ''", "true" },
|
||||
}, .{});
|
||||
test "Browser: DOM.CharacterData" {
|
||||
try testing.htmlRunner("dom/character_data.html");
|
||||
}
|
||||
|
||||
@@ -40,15 +40,6 @@ pub const Comment = struct {
|
||||
// -----
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.Comment" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let comment = new Comment('foo')", "undefined" },
|
||||
.{ "comment.data", "foo" },
|
||||
|
||||
.{ "let emptycomment = new Comment()", "undefined" },
|
||||
.{ "emptycomment.data", "" },
|
||||
}, .{});
|
||||
test "Browser: DOM.Comment" {
|
||||
try testing.htmlRunner("dom/comment.html");
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ pub fn querySelector(alloc: std.mem.Allocator, n: *parser.Node, selector: []cons
|
||||
|
||||
var m = MatchFirst{};
|
||||
|
||||
_ = try css.matchFirst(ps, Node{ .node = n }, &m);
|
||||
_ = try css.matchFirst(&ps, Node{ .node = n }, &m);
|
||||
return m.n;
|
||||
}
|
||||
|
||||
@@ -75,6 +75,6 @@ pub fn querySelectorAll(alloc: std.mem.Allocator, n: *parser.Node, selector: []c
|
||||
var m = MatchAll.init(alloc);
|
||||
defer m.deinit();
|
||||
|
||||
try css.matchAll(ps, Node{ .node = n }, &m);
|
||||
try css.matchAll(&ps, Node{ .node = n }, &m);
|
||||
return m.toOwnedList();
|
||||
}
|
||||
|
||||
@@ -32,10 +32,12 @@ 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 CSSStyleSheet = @import("../cssom/CSSStyleSheet.zig");
|
||||
const NodeIterator = @import("node_iterator.zig").NodeIterator;
|
||||
const Range = @import("range.zig").Range;
|
||||
|
||||
const CustomEvent = @import("../events/custom_event.zig").CustomEvent;
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
|
||||
const DOMImplementation = @import("implementation.zig").DOMImplementation;
|
||||
@@ -110,13 +112,23 @@ pub const Document = struct {
|
||||
return try parser.documentGetDoctype(self);
|
||||
}
|
||||
|
||||
pub fn _createEvent(_: *parser.Document, eventCstr: []const u8) !*parser.Event {
|
||||
// TODO: for now only "Event" constructor is supported
|
||||
// see table on https://dom.spec.whatwg.org/#dom-document-createevent $2
|
||||
if (std.ascii.eqlIgnoreCase(eventCstr, "Event") or std.ascii.eqlIgnoreCase(eventCstr, "Events")) {
|
||||
return try parser.eventCreate();
|
||||
pub fn _createEvent(_: *parser.Document, eventCstr: []const u8) !union(enum) {
|
||||
base: *parser.Event,
|
||||
custom: CustomEvent,
|
||||
} {
|
||||
const eqlIgnoreCase = std.ascii.eqlIgnoreCase;
|
||||
|
||||
if (eqlIgnoreCase(eventCstr, "Event") or eqlIgnoreCase(eventCstr, "Events") or eqlIgnoreCase(eventCstr, "HTMLEvents")) {
|
||||
return .{ .base = try parser.eventCreate() };
|
||||
}
|
||||
return parser.DOMError.NotSupported;
|
||||
|
||||
// Not documented in MDN but supported in Chrome.
|
||||
// This is actually both instance of `Event` and `CustomEvent`.
|
||||
if (std.ascii.eqlIgnoreCase(eventCstr, "CustomEvent")) {
|
||||
return .{ .custom = try CustomEvent.constructor(eventCstr, null) };
|
||||
}
|
||||
|
||||
return error.NotSupported;
|
||||
}
|
||||
|
||||
pub fn _getElementById(self: *parser.Document, id: []const u8) !?ElementUnion {
|
||||
@@ -149,7 +161,9 @@ pub const Document = struct {
|
||||
tag_name: []const u8,
|
||||
page: *Page,
|
||||
) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentToNode(self), tag_name, true);
|
||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentToNode(self), tag_name, .{
|
||||
.include_root = true,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn _getElementsByClassName(
|
||||
@@ -157,7 +171,9 @@ pub const Document = struct {
|
||||
classNames: []const u8,
|
||||
page: *Page,
|
||||
) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByClassName(page.arena, parser.documentToNode(self), classNames, true);
|
||||
return try collection.HTMLCollectionByClassName(page.arena, parser.documentToNode(self), classNames, .{
|
||||
.include_root = true,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn _createDocumentFragment(self: *parser.Document) !*parser.DocumentFragment {
|
||||
@@ -201,7 +217,9 @@ pub const Document = struct {
|
||||
// ParentNode
|
||||
// https://dom.spec.whatwg.org/#parentnode
|
||||
pub fn get_children(self: *parser.Document) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionChildren(parser.documentToNode(self), false);
|
||||
return collection.HTMLCollectionChildren(parser.documentToNode(self), .{
|
||||
.include_root = false,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_firstElementChild(self: *parser.Document) !?ElementUnion {
|
||||
@@ -245,23 +263,23 @@ pub const Document = struct {
|
||||
return Node.replaceChildren(parser.documentToNode(self), nodes);
|
||||
}
|
||||
|
||||
pub fn _createTreeWalker(_: *parser.Document, root: *parser.Node, what_to_show: ?u32, filter: ?TreeWalker.TreeWalkerOpts) !TreeWalker {
|
||||
return try TreeWalker.init(root, what_to_show, filter);
|
||||
pub fn _createTreeWalker(_: *parser.Document, root: *parser.Node, what_to_show: ?TreeWalker.WhatToShow, filter: ?TreeWalker.TreeWalkerOpts) !TreeWalker {
|
||||
return TreeWalker.init(root, what_to_show, filter);
|
||||
}
|
||||
|
||||
pub fn _createNodeIterator(_: *parser.Document, root: *parser.Node, what_to_show: ?u32, filter: ?NodeIterator.NodeIteratorOpts) !NodeIterator {
|
||||
return try NodeIterator.init(root, what_to_show, filter);
|
||||
pub fn _createNodeIterator(_: *parser.Document, root: *parser.Node, what_to_show: ?NodeIterator.WhatToShow, filter: ?NodeIterator.NodeIteratorOpts) !NodeIterator {
|
||||
return NodeIterator.init(root, what_to_show, filter);
|
||||
}
|
||||
|
||||
pub fn getActiveElement(self: *parser.Document, page: *Page) !?*parser.Element {
|
||||
if (page.getNodeState(@alignCast(@ptrCast(self)))) |state| {
|
||||
if (page.getNodeState(@ptrCast(@alignCast(self)))) |state| {
|
||||
if (state.active_element) |ae| {
|
||||
return ae;
|
||||
}
|
||||
}
|
||||
|
||||
if (try parser.documentHTMLBody(page.window.document)) |body| {
|
||||
return @alignCast(@ptrCast(body));
|
||||
return @ptrCast(@alignCast(body));
|
||||
}
|
||||
|
||||
return try parser.documentGetDocumentElement(self);
|
||||
@@ -277,7 +295,7 @@ pub const Document = struct {
|
||||
// we could look for the "disabled" attribute, but that's only meaningful
|
||||
// on certain types, and libdom's vtable doesn't seem to expose this.
|
||||
pub fn setFocus(self: *parser.Document, e: *parser.ElementHTML, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
state.active_element = @ptrCast(e);
|
||||
}
|
||||
|
||||
@@ -289,210 +307,25 @@ pub const Document = struct {
|
||||
pub fn get_styleSheets(_: *parser.Document) []CSSStyleSheet {
|
||||
return &.{};
|
||||
}
|
||||
|
||||
pub fn get_adoptedStyleSheets(self: *parser.Document, page: *Page) !Env.JsObject {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
if (state.adopted_style_sheets) |obj| {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const obj = try page.main_context.newArray(0).persist();
|
||||
state.adopted_style_sheets = obj;
|
||||
return obj;
|
||||
}
|
||||
|
||||
pub fn set_adoptedStyleSheets(self: *parser.Document, sheets: Env.JsObject, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
state.adopted_style_sheets = try sheets.persist();
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.Document" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{
|
||||
.url = "about:blank",
|
||||
});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.__proto__.__proto__.constructor.name", "Document" },
|
||||
.{ "document.__proto__.__proto__.__proto__.constructor.name", "Node" },
|
||||
.{ "document.__proto__.__proto__.__proto__.__proto__.constructor.name", "EventTarget" },
|
||||
|
||||
.{ "let newdoc = new Document()", "undefined" },
|
||||
.{ "newdoc.documentElement", "null" },
|
||||
.{ "newdoc.children.length", "0" },
|
||||
.{ "newdoc.getElementsByTagName('*').length", "0" },
|
||||
.{ "newdoc.getElementsByTagName('*').item(0)", "null" },
|
||||
.{ "newdoc.inputEncoding === document.inputEncoding", "true" },
|
||||
.{ "newdoc.documentURI === document.documentURI", "true" },
|
||||
.{ "newdoc.URL === document.URL", "true" },
|
||||
.{ "newdoc.compatMode === document.compatMode", "true" },
|
||||
.{ "newdoc.characterSet === document.characterSet", "true" },
|
||||
.{ "newdoc.charset === document.charset", "true" },
|
||||
.{ "newdoc.contentType === document.contentType", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let getElementById = document.getElementById('content')", "undefined" },
|
||||
.{ "getElementById.constructor.name", "HTMLDivElement" },
|
||||
.{ "getElementById.localName", "div" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let getElementsByTagName = document.getElementsByTagName('p')", "undefined" },
|
||||
.{ "getElementsByTagName.length", "2" },
|
||||
.{ "getElementsByTagName.item(0).localName", "p" },
|
||||
.{ "getElementsByTagName.item(1).localName", "p" },
|
||||
.{ "let getElementsByTagNameAll = document.getElementsByTagName('*')", "undefined" },
|
||||
.{ "getElementsByTagNameAll.length", "8" },
|
||||
.{ "getElementsByTagNameAll.item(0).localName", "html" },
|
||||
.{ "getElementsByTagNameAll.item(7).localName", "p" },
|
||||
.{ "getElementsByTagNameAll.namedItem('para-empty-child').localName", "span" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let ok = document.getElementsByClassName('ok')", "undefined" },
|
||||
.{ "ok.length", "2" },
|
||||
.{ "let empty = document.getElementsByClassName('empty')", "undefined" },
|
||||
.{ "empty.length", "1" },
|
||||
.{ "let emptyok = document.getElementsByClassName('empty ok')", "undefined" },
|
||||
.{ "emptyok.length", "1" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let e = document.documentElement", "undefined" },
|
||||
.{ "e.localName", "html" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.characterSet", "UTF-8" },
|
||||
.{ "document.charset", "UTF-8" },
|
||||
.{ "document.inputEncoding", "UTF-8" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.compatMode", "CSS1Compat" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.contentType", "text/html" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.documentURI", "about:blank" },
|
||||
.{ "document.URL", "about:blank" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let impl = document.implementation", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let d = new Document()", "undefined" },
|
||||
.{ "d.characterSet", "UTF-8" },
|
||||
.{ "d.URL", "about:blank" },
|
||||
.{ "d.documentURI", "about:blank" },
|
||||
.{ "d.compatMode", "CSS1Compat" },
|
||||
.{ "d.contentType", "text/html" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var v = document.createDocumentFragment()", "undefined" },
|
||||
.{ "v.nodeName", "#document-fragment" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var v = document.createTextNode('foo')", "undefined" },
|
||||
.{ "v.nodeName", "#text" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var v = document.createCDATASection('foo')", "undefined" },
|
||||
.{ "v.nodeName", "#cdata-section" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var v = document.createComment('foo')", "undefined" },
|
||||
.{ "v.nodeName", "#comment" },
|
||||
.{ "let v2 = v.cloneNode()", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let pi = document.createProcessingInstruction('foo', 'bar')", "undefined" },
|
||||
.{ "pi.target", "foo" },
|
||||
.{ "let pi2 = pi.cloneNode()", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let nimp = document.getElementById('content')", "undefined" },
|
||||
.{ "var v = document.importNode(nimp)", "undefined" },
|
||||
.{ "v.nodeName", "DIV" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var v = document.createAttribute('foo')", "undefined" },
|
||||
.{ "v.nodeName", "foo" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.children.length", "1" },
|
||||
.{ "document.children.item(0).nodeName", "HTML" },
|
||||
.{ "document.firstElementChild.nodeName", "HTML" },
|
||||
.{ "document.lastElementChild.nodeName", "HTML" },
|
||||
.{ "document.childElementCount", "1" },
|
||||
|
||||
.{ "let nd = new Document()", "undefined" },
|
||||
.{ "nd.children.length", "0" },
|
||||
.{ "nd.children.item(0)", "null" },
|
||||
.{ "nd.firstElementChild", "null" },
|
||||
.{ "nd.lastElementChild", "null" },
|
||||
.{ "nd.childElementCount", "0" },
|
||||
|
||||
.{ "let emptydoc = document.createElement('html')", "undefined" },
|
||||
.{ "emptydoc.prepend(document.createElement('html'))", "undefined" },
|
||||
|
||||
.{ "let emptydoc2 = document.createElement('html')", "undefined" },
|
||||
.{ "emptydoc2.append(document.createElement('html'))", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.querySelector('')", "null" },
|
||||
.{ "document.querySelector('*').nodeName", "HTML" },
|
||||
.{ "document.querySelector('#content').id", "content" },
|
||||
.{ "document.querySelector('#para').id", "para" },
|
||||
.{ "document.querySelector('.ok').id", "link" },
|
||||
.{ "document.querySelector('a ~ p').id", "para-empty" },
|
||||
.{ "document.querySelector(':root').nodeName", "HTML" },
|
||||
|
||||
.{ "document.querySelectorAll('p').length", "2" },
|
||||
.{
|
||||
\\ Array.from(document.querySelectorAll('#content > p#para-empty'))
|
||||
\\ .map(row => row.querySelector('span').textContent)
|
||||
\\ .length;
|
||||
,
|
||||
"1",
|
||||
},
|
||||
|
||||
.{ "document.querySelectorAll('.\\\\:popover-open').length", "0" },
|
||||
.{ "document.querySelectorAll('.foo\\\\:bar').length", "0" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.activeElement === document.body", "true" },
|
||||
.{ "document.getElementById('link').focus()", "undefined" },
|
||||
.{ "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(&.{
|
||||
.{ "let nadop = document.getElementById('content')", "undefined" },
|
||||
.{ "var v = document.adoptNode(nadop)", "undefined" },
|
||||
.{ "v.nodeName", "DIV" },
|
||||
}, .{});
|
||||
|
||||
const Case = testing.JsRunner.Case;
|
||||
const tags = comptime parser.Tag.all();
|
||||
var createElements: [(tags.len) * 2]Case = undefined;
|
||||
inline for (tags, 0..) |tag, i| {
|
||||
const tag_name = @tagName(tag);
|
||||
createElements[i * 2] = Case{
|
||||
"var " ++ tag_name ++ "Elem = document.createElement('" ++ tag_name ++ "')",
|
||||
"undefined",
|
||||
};
|
||||
createElements[(i * 2) + 1] = Case{
|
||||
tag_name ++ "Elem.localName",
|
||||
tag_name,
|
||||
};
|
||||
}
|
||||
try runner.testCases(&createElements, .{});
|
||||
test "Browser: DOM.Document" {
|
||||
try testing.htmlRunner("dom/document.html");
|
||||
}
|
||||
|
||||
@@ -79,46 +79,18 @@ pub const DocumentFragment = struct {
|
||||
}
|
||||
|
||||
pub fn get_children(self: *parser.DocumentFragment) !collection.HTMLCollection {
|
||||
return collection.HTMLCollectionChildren(parser.documentFragmentToNode(self), false);
|
||||
return collection.HTMLCollectionChildren(parser.documentFragmentToNode(self), .{
|
||||
.include_root = false,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn _getElementById(self: *parser.DocumentFragment, id: []const u8) !?ElementUnion {
|
||||
const e = try parser.nodeGetElementById(@ptrCast(@alignCast(self)), id) orelse return null;
|
||||
return try Element.toInterface(e);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.DocumentFragment" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const dc = new DocumentFragment()", "undefined" },
|
||||
.{ "dc.constructor.name", "DocumentFragment" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const dc1 = new DocumentFragment()", "undefined" },
|
||||
.{ "const dc2 = new DocumentFragment()", "undefined" },
|
||||
.{ "dc1.isEqualNode(dc1)", "true" },
|
||||
.{ "dc1.isEqualNode(dc2)", "true" },
|
||||
}, .{});
|
||||
|
||||
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" },
|
||||
}, .{});
|
||||
test "Browser: DOM.DocumentFragment" {
|
||||
try testing.htmlRunner("dom/document_fragment.html");
|
||||
}
|
||||
|
||||
@@ -62,19 +62,6 @@ pub const DocumentType = struct {
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.DocumentType" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let dt1 = document.implementation.createDocumentType('qname1', 'pid1', 'sys1');", "undefined" },
|
||||
.{ "let dt2 = document.implementation.createDocumentType('qname2', 'pid2', 'sys2');", "undefined" },
|
||||
.{ "let dt3 = document.implementation.createDocumentType('qname1', 'pid1', 'sys1');", "undefined" },
|
||||
.{ "dt1.isEqualNode(dt1)", "true" },
|
||||
.{ "dt1.isEqualNode(dt3)", "true" },
|
||||
.{ "dt1.isEqualNode(dt2)", "false" },
|
||||
.{ "dt2.isEqualNode(dt3)", "false" },
|
||||
.{ "dt1.isEqualNode(document)", "false" },
|
||||
.{ "document.isEqualNode(dt1)", "false" },
|
||||
}, .{});
|
||||
test "Browser: DOM.DocumentType" {
|
||||
try testing.htmlRunner("dom/document_type.html");
|
||||
}
|
||||
|
||||
@@ -36,12 +36,6 @@ pub const DOMParser = struct {
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.DOMParser" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const dp = new DOMParser()", "undefined" },
|
||||
.{ "dp.parseFromString('<div>abc</div>', 'text/html')", "[object HTMLDocument]" },
|
||||
}, .{});
|
||||
test "Browser: DOM.Parser" {
|
||||
try testing.htmlRunner("dom/dom_parser.html");
|
||||
}
|
||||
|
||||
@@ -55,8 +55,33 @@ pub const Element = struct {
|
||||
};
|
||||
|
||||
pub fn toInterface(e: *parser.Element) !Union {
|
||||
return try HTMLElem.toInterface(Union, e);
|
||||
// SVGElement and MathML are not supported yet.
|
||||
return toInterfaceT(Union, e);
|
||||
}
|
||||
|
||||
pub fn toInterfaceT(comptime T: type, e: *parser.Element) !T {
|
||||
const tagname = try parser.elementGetTagName(e) orelse {
|
||||
// If the owner's document is HTML, assume we have an HTMLElement.
|
||||
const doc = try parser.nodeOwnerDocument(parser.elementToNode(e));
|
||||
if (doc != null and !doc.?.is_html) {
|
||||
return .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(e)) };
|
||||
}
|
||||
|
||||
return .{ .Element = e };
|
||||
};
|
||||
|
||||
// TODO SVGElement and MathML are not supported yet.
|
||||
|
||||
const tag = parser.Tag.fromString(tagname) catch {
|
||||
// If the owner's document is HTML, assume we have an HTMLElement.
|
||||
const doc = try parser.nodeOwnerDocument(parser.elementToNode(e));
|
||||
if (doc != null and doc.?.is_html) {
|
||||
return .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(e)) };
|
||||
}
|
||||
|
||||
return .{ .Element = e };
|
||||
};
|
||||
|
||||
return HTMLElem.toInterfaceFromTag(T, e, tag);
|
||||
}
|
||||
|
||||
// JS funcs
|
||||
@@ -112,18 +137,18 @@ 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());
|
||||
return buf.items;
|
||||
var aw = std.Io.Writer.Allocating.init(page.call_arena);
|
||||
try dump.writeChildren(parser.elementToNode(self), .{}, &aw.writer);
|
||||
return aw.written();
|
||||
}
|
||||
|
||||
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());
|
||||
return buf.items;
|
||||
var aw = std.Io.Writer.Allocating.init(page.call_arena);
|
||||
try dump.writeNode(parser.elementToNode(self), .{}, &aw.writer);
|
||||
return aw.written();
|
||||
}
|
||||
|
||||
pub fn set_innerHTML(self: *parser.Element, str: []const u8) !void {
|
||||
pub fn set_innerHTML(self: *parser.Element, str: []const u8, page: *Page) !void {
|
||||
const node = parser.elementToNode(self);
|
||||
const doc = try parser.nodeOwnerDocument(node) orelse return parser.DOMError.WrongDocument;
|
||||
// parse the fragment
|
||||
@@ -132,6 +157,8 @@ pub const Element = struct {
|
||||
// remove existing children
|
||||
try Node.removeChildren(node);
|
||||
|
||||
const fragment_node = parser.documentFragmentToNode(fragment);
|
||||
|
||||
// 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
|
||||
@@ -141,9 +168,32 @@ pub const Element = struct {
|
||||
// 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;
|
||||
const body = try parser.nodeNextSibling(head) orelse return;
|
||||
|
||||
if (try parser.elementTag(self) == .template) {
|
||||
// HTMLElementTemplate is special. We don't append these as children
|
||||
// of the template, but instead set its content as the body of the
|
||||
// fragment. Simpler to do this by copying the body children into
|
||||
// a new fragment
|
||||
const clean = try parser.documentCreateDocumentFragment(doc);
|
||||
const children = try parser.nodeGetChildNodes(body);
|
||||
const ln = try parser.nodeListLength(children);
|
||||
for (0..ln) |_| {
|
||||
// always index 0, because nodeAppendChild moves the node out of
|
||||
// the nodeList and into the new tree
|
||||
const child = try parser.nodeListItem(children, 0) orelse continue;
|
||||
_ = try parser.nodeAppendChild(@ptrCast(@alignCast(clean)), child);
|
||||
}
|
||||
|
||||
const state = try page.getOrCreateNodeState(node);
|
||||
state.template_content = clean;
|
||||
return;
|
||||
}
|
||||
|
||||
// For any node other than a template, we copy the head and body elements
|
||||
// as child nodes of the element
|
||||
{
|
||||
// First, copy some of the head element
|
||||
const children = try parser.nodeGetChildNodes(head);
|
||||
@@ -157,7 +207,6 @@ pub const Element = struct {
|
||||
}
|
||||
|
||||
{
|
||||
const body = try parser.nodeNextSibling(head) orelse return;
|
||||
const children = try parser.nodeGetChildNodes(body);
|
||||
const ln = try parser.nodeListLength(children);
|
||||
for (0..ln) |_| {
|
||||
@@ -265,6 +314,22 @@ pub const Element = struct {
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn _getAttributeNames(self: *parser.Element, page: *Page) ![]const []const u8 {
|
||||
const attributes = try parser.nodeGetAttributes(@ptrCast(self)) orelse return &.{};
|
||||
const ln = try parser.namedNodeMapGetLength(attributes);
|
||||
|
||||
const names = try page.call_arena.alloc([]const u8, ln);
|
||||
var at: usize = 0;
|
||||
|
||||
for (0..ln) |i| {
|
||||
const attribute = try parser.namedNodeMapItem(attributes, @intCast(i)) orelse break;
|
||||
names[at] = try parser.attributeGetName(attribute);
|
||||
at += 1;
|
||||
}
|
||||
|
||||
return names[0..at];
|
||||
}
|
||||
|
||||
pub fn _getAttributeNode(self: *parser.Element, name: []const u8) !?*parser.Attribute {
|
||||
return try parser.elementGetAttributeNode(self, name);
|
||||
}
|
||||
@@ -294,7 +359,7 @@ pub const Element = struct {
|
||||
page.arena,
|
||||
parser.elementToNode(self),
|
||||
tag_name,
|
||||
false,
|
||||
.{ .include_root = false },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -307,14 +372,16 @@ pub const Element = struct {
|
||||
page.arena,
|
||||
parser.elementToNode(self),
|
||||
classNames,
|
||||
false,
|
||||
.{ .include_root = false },
|
||||
);
|
||||
}
|
||||
|
||||
// ParentNode
|
||||
// https://dom.spec.whatwg.org/#parentnode
|
||||
pub fn get_children(self: *parser.Element) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionChildren(parser.elementToNode(self), false);
|
||||
return collection.HTMLCollectionChildren(parser.elementToNode(self), .{
|
||||
.include_root = false,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_firstElementChild(self: *parser.Element) !?Union {
|
||||
@@ -342,13 +409,13 @@ pub const Element = struct {
|
||||
pub fn get_previousElementSibling(self: *parser.Element) !?Union {
|
||||
const res = try parser.nodePreviousElementSibling(parser.elementToNode(self));
|
||||
if (res == null) return null;
|
||||
return try HTMLElem.toInterface(HTMLElem.Union, res.?);
|
||||
return try toInterface(res.?);
|
||||
}
|
||||
|
||||
pub fn get_nextElementSibling(self: *parser.Element) !?Union {
|
||||
const res = try parser.nodeNextElementSibling(parser.elementToNode(self));
|
||||
if (res == null) return null;
|
||||
return try HTMLElem.toInterface(HTMLElem.Union, res.?);
|
||||
return try toInterface(res.?);
|
||||
}
|
||||
|
||||
fn getElementById(self: *parser.Element, id: []const u8) !?*parser.Node {
|
||||
@@ -470,14 +537,14 @@ pub const Element = struct {
|
||||
};
|
||||
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)));
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
if (state.shadow_root) |sr| {
|
||||
if (mode != sr.mode) {
|
||||
// this is the behavior per the spec
|
||||
return error.NotSupportedError;
|
||||
}
|
||||
|
||||
try Node.removeChildren(@alignCast(@ptrCast(sr.proto)));
|
||||
try Node.removeChildren(@ptrCast(@alignCast(sr.proto)));
|
||||
return sr;
|
||||
}
|
||||
|
||||
@@ -491,11 +558,24 @@ pub const Element = struct {
|
||||
.proto = fragment,
|
||||
};
|
||||
state.shadow_root = sr;
|
||||
parser.documentFragmentSetHost(sr.proto, @ptrCast(@alignCast(self)));
|
||||
|
||||
// Storing the ShadowRoot on the element makes sense, it's the ShadowRoot's
|
||||
// parent. When we render, we go top-down, so we'll have the element, get
|
||||
// its shadowroot, and go on. that's what the above code does.
|
||||
// But we sometimes need to go bottom-up, e.g when we have a slot element
|
||||
// and want to find the containing parent. Unforatunately , we don't have
|
||||
// that link, so we need to create it. In the DOM, the ShadowRoot is
|
||||
// represented by this DocumentFragment (it's the ShadowRoot's base prototype)
|
||||
// So we can also store the ShadowRoot in the DocumentFragment's state.
|
||||
const fragment_state = try page.getOrCreateNodeState(@ptrCast(@alignCast(fragment)));
|
||||
fragment_state.shadow_root = sr;
|
||||
|
||||
return sr;
|
||||
}
|
||||
|
||||
pub fn get_shadowRoot(self: *parser.Element, page: *Page) ?*ShadowRoot {
|
||||
const state = page.getNodeState(@alignCast(@ptrCast(self))) orelse return null;
|
||||
const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null;
|
||||
const sr = state.shadow_root orelse return null;
|
||||
if (sr.mode == .closed) {
|
||||
return null;
|
||||
@@ -524,266 +604,6 @@ pub const Element = struct {
|
||||
// -----
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.Element" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let g = document.getElementById('content')", "undefined" },
|
||||
.{ "g.namespaceURI", "http://www.w3.org/1999/xhtml" },
|
||||
.{ "g.prefix", "null" },
|
||||
.{ "g.localName", "div" },
|
||||
.{ "g.tagName", "DIV" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let gs = document.getElementById('content')", "undefined" },
|
||||
.{ "gs.id", "content" },
|
||||
.{ "gs.id = 'foo'", "foo" },
|
||||
.{ "gs.id", "foo" },
|
||||
.{ "gs.id = 'content'", "content" },
|
||||
.{ "gs.className", "" },
|
||||
.{ "let gs2 = document.getElementById('para-empty')", "undefined" },
|
||||
.{ "gs2.className", "ok empty" },
|
||||
.{ "gs2.className = 'foo bar baz'", "foo bar baz" },
|
||||
.{ "gs2.className", "foo bar baz" },
|
||||
.{ "gs2.className = 'ok empty'", "ok empty" },
|
||||
.{ "let cl = gs2.classList", "undefined" },
|
||||
.{ "cl.length", "2" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const el2 = document.createElement('div');", "undefined" },
|
||||
.{ "el2.id = 'closest'; el2.className = 'ok';", "ok" },
|
||||
.{ "el2.closest('#closest')", "[object HTMLDivElement]" },
|
||||
.{ "el2.closest('.ok')", "[object HTMLDivElement]" },
|
||||
.{ "el2.closest('#9000')", "null" },
|
||||
.{ "el2.closest('.notok')", "null" },
|
||||
|
||||
.{ "const sp = document.createElement('span');", "undefined" },
|
||||
.{ "el2.appendChild(sp);", "[object HTMLSpanElement]" },
|
||||
.{ "sp.closest('#closest')", "[object HTMLDivElement]" },
|
||||
.{ "sp.closest('#9000')", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let a = document.getElementById('content')", "undefined" },
|
||||
.{ "a.hasAttributes()", "true" },
|
||||
.{ "a.attributes.length", "1" },
|
||||
.{ "a.getAttribute('id')", "content" },
|
||||
.{ "a.attributes['id'].value", "content" },
|
||||
.{
|
||||
\\ let x = '';
|
||||
\\ for (const attr of a.attributes) {
|
||||
\\ x += attr.name + '=' + attr.value;
|
||||
\\ }
|
||||
\\ x;
|
||||
,
|
||||
"id=content",
|
||||
},
|
||||
|
||||
.{ "a.hasAttribute('foo')", "false" },
|
||||
.{ "a.getAttribute('foo')", "null" },
|
||||
|
||||
.{ "a.setAttribute('foo', 'bar')", "undefined" },
|
||||
.{ "a.hasAttribute('foo')", "true" },
|
||||
.{ "a.getAttribute('foo')", "bar" },
|
||||
|
||||
.{ "a.setAttribute('foo', 'baz')", "undefined" },
|
||||
.{ "a.hasAttribute('foo')", "true" },
|
||||
.{ "a.getAttribute('foo')", "baz" },
|
||||
|
||||
.{ "a.removeAttribute('foo')", "undefined" },
|
||||
.{ "a.hasAttribute('foo')", "false" },
|
||||
.{ "a.getAttribute('foo')", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let b = document.getElementById('content')", "undefined" },
|
||||
.{ "b.toggleAttribute('foo')", "true" },
|
||||
.{ "b.hasAttribute('foo')", "true" },
|
||||
.{ "b.getAttribute('foo')", "" },
|
||||
|
||||
.{ "b.toggleAttribute('foo')", "false" },
|
||||
.{ "b.hasAttribute('foo')", "false" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let c = document.getElementById('content')", "undefined" },
|
||||
.{ "c.children.length", "3" },
|
||||
.{ "c.firstElementChild.nodeName", "A" },
|
||||
.{ "c.lastElementChild.nodeName", "P" },
|
||||
.{ "c.childElementCount", "3" },
|
||||
|
||||
.{ "c.prepend(document.createTextNode('foo'))", "undefined" },
|
||||
.{ "c.append(document.createTextNode('bar'))", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let d = document.getElementById('para')", "undefined" },
|
||||
.{ "d.previousElementSibling.nodeName", "P" },
|
||||
.{ "d.nextElementSibling", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let e = document.getElementById('content')", "undefined" },
|
||||
.{ "e.querySelector('foo')", "null" },
|
||||
.{ "e.querySelector('#foo')", "null" },
|
||||
.{ "e.querySelector('#link').id", "link" },
|
||||
.{ "e.querySelector('#para').id", "para" },
|
||||
.{ "e.querySelector('*').id", "link" },
|
||||
.{ "e.querySelector('')", "null" },
|
||||
.{ "e.querySelector('*').id", "link" },
|
||||
.{ "e.querySelector('#content')", "null" },
|
||||
.{ "e.querySelector('#para').id", "para" },
|
||||
.{ "e.querySelector('.ok').id", "link" },
|
||||
.{ "e.querySelector('a ~ p').id", "para-empty" },
|
||||
|
||||
.{ "e.querySelectorAll('foo').length", "0" },
|
||||
.{ "e.querySelectorAll('#foo').length", "0" },
|
||||
.{ "e.querySelectorAll('#link').length", "1" },
|
||||
.{ "e.querySelectorAll('#link').item(0).id", "link" },
|
||||
.{ "e.querySelectorAll('#para').length", "1" },
|
||||
.{ "e.querySelectorAll('#para').item(0).id", "para" },
|
||||
.{ "e.querySelectorAll('*').length", "4" },
|
||||
.{ "e.querySelectorAll('p').length", "2" },
|
||||
.{ "e.querySelectorAll('.ok').item(0).id", "link" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let f = document.getElementById('content')", "undefined" },
|
||||
.{ "let ff = document.createAttribute('foo')", "undefined" },
|
||||
.{ "f.setAttributeNode(ff)", "null" },
|
||||
.{ "f.getAttributeNode('foo').name", "foo" },
|
||||
.{ "f.removeAttributeNode(ff).name", "foo" },
|
||||
.{ "f.getAttributeNode('bar')", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.getElementById('para').innerHTML", " And" },
|
||||
.{ "document.getElementById('para-empty').innerHTML.trim()", "<span id=\"para-empty-child\"></span>" },
|
||||
|
||||
.{ "let h = document.getElementById('para-empty')", "undefined" },
|
||||
.{ "const prev = h.innerHTML", "undefined" },
|
||||
.{ "h.innerHTML = '<p id=\"hello\">hello world</p>'", "<p id=\"hello\">hello world</p>" },
|
||||
.{ "h.innerHTML", "<p id=\"hello\">hello world</p>" },
|
||||
.{ "h.firstChild.nodeName", "P" },
|
||||
.{ "h.firstChild.id", "hello" },
|
||||
.{ "h.firstChild.textContent", "hello world" },
|
||||
.{ "h.innerHTML = prev; true", "true" },
|
||||
.{ "document.getElementById('para-empty').innerHTML.trim()", "<span id=\"para-empty-child\"></span>" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.getElementById('para').outerHTML", "<p id=\"para\"> And</p>" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.getElementById('para').clientWidth", "1" },
|
||||
.{ "document.getElementById('para').clientHeight", "1" },
|
||||
|
||||
.{ "let r1 = document.getElementById('para').getBoundingClientRect()", "undefined" },
|
||||
.{ "r1.x", "0" },
|
||||
.{ "r1.y", "0" },
|
||||
.{ "r1.width", "1" },
|
||||
.{ "r1.height", "1" },
|
||||
|
||||
.{ "let r2 = document.getElementById('content').getBoundingClientRect()", "undefined" },
|
||||
.{ "r2.x", "1" },
|
||||
.{ "r2.y", "0" },
|
||||
.{ "r2.width", "1" },
|
||||
.{ "r2.height", "1" },
|
||||
|
||||
.{ "let r3 = document.getElementById('para').getBoundingClientRect()", "undefined" },
|
||||
.{ "r3.x", "0" },
|
||||
.{ "r3.y", "0" },
|
||||
.{ "r3.width", "1" },
|
||||
.{ "r3.height", "1" },
|
||||
|
||||
.{ "document.getElementById('para').clientWidth", "2" },
|
||||
.{ "document.getElementById('para').clientHeight", "1" },
|
||||
|
||||
.{ "let r4 = document.createElement('div').getBoundingClientRect()", null },
|
||||
.{ "r4.x", "0" },
|
||||
.{ "r4.y", "0" },
|
||||
.{ "r4.width", "0" },
|
||||
.{ "r4.height", "0" },
|
||||
|
||||
// Test setup causes WrongDocument or HierarchyRequest error unlike in chrome/firefox
|
||||
// .{ // An element of another document, even if created from the main document, is not rendered.
|
||||
// \\ let div5 = document.createElement('div');
|
||||
// \\ const newDoc = document.implementation.createHTMLDocument("New Document");
|
||||
// \\ newDoc.body.appendChild(div5);
|
||||
// \\ let r5 = div5.getBoundingClientRect();
|
||||
// ,
|
||||
// null,
|
||||
// },
|
||||
// .{ "r5.x", "0" },
|
||||
// .{ "r5.y", "0" },
|
||||
// .{ "r5.width", "0" },
|
||||
// .{ "r5.height", "0" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const el = document.createElement('div');", "undefined" },
|
||||
.{ "el.id = 'matches'; el.className = 'ok';", "ok" },
|
||||
.{ "el.matches('#matches')", "true" },
|
||||
.{ "el.matches('.ok')", "true" },
|
||||
.{ "el.matches('#9000')", "false" },
|
||||
.{ "el.matches('.notok')", "false" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const el3 = document.createElement('div');", "undefined" },
|
||||
.{ "el3.scrollIntoViewIfNeeded();", "undefined" },
|
||||
.{ "el3.scrollIntoViewIfNeeded(false);", "undefined" },
|
||||
}, .{});
|
||||
|
||||
// before
|
||||
try runner.testCases(&.{
|
||||
.{ "const before_container = document.createElement('div');", "undefined" },
|
||||
.{ "document.append(before_container);", "undefined" },
|
||||
.{ "const b1 = document.createElement('div');", "undefined" },
|
||||
.{ "before_container.append(b1);", "undefined" },
|
||||
|
||||
.{ "const b1_a = document.createElement('p');", "undefined" },
|
||||
.{ "b1.before(b1_a, 'over 9000');", "undefined" },
|
||||
.{ "before_container.innerHTML", "<p></p>over 9000<div></div>" },
|
||||
}, .{});
|
||||
|
||||
// after
|
||||
try runner.testCases(&.{
|
||||
.{ "const after_container = document.createElement('div');", "undefined" },
|
||||
.{ "document.append(after_container);", "undefined" },
|
||||
.{ "const a1 = document.createElement('div');", "undefined" },
|
||||
.{ "after_container.append(a1);", "undefined" },
|
||||
|
||||
.{ "const a1_a = document.createElement('p');", "undefined" },
|
||||
.{ "a1.after('over 9000', a1_a);", "undefined" },
|
||||
.{ "after_container.innerHTML", "<div></div>over 9000<p></p>" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var div1 = document.createElement('div');", null },
|
||||
.{ "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" },
|
||||
}, .{});
|
||||
test "Browser: DOM.Element" {
|
||||
try testing.htmlRunner("dom/element.html");
|
||||
}
|
||||
|
||||
@@ -31,6 +31,10 @@ pub const Union = union(enum) {
|
||||
xhr: *@import("../xhr/xhr.zig").XMLHttpRequest,
|
||||
plain: *parser.EventTarget,
|
||||
message_port: *@import("MessageChannel.zig").MessagePort,
|
||||
screen: *@import("../html/screen.zig").Screen,
|
||||
screen_orientation: *@import("../html/screen.zig").ScreenOrientation,
|
||||
performance: *@import("performance.zig").Performance,
|
||||
media_query_list: *@import("../html/media_query_list.zig").MediaQueryList,
|
||||
};
|
||||
|
||||
// EventTarget implementation
|
||||
@@ -67,7 +71,18 @@ pub const EventTarget = struct {
|
||||
.message_port => {
|
||||
return .{ .message_port = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) };
|
||||
},
|
||||
else => return error.MissingEventTargetType,
|
||||
.screen => {
|
||||
return .{ .screen = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) };
|
||||
},
|
||||
.screen_orientation => {
|
||||
return .{ .screen_orientation = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) };
|
||||
},
|
||||
.performance => {
|
||||
return .{ .performance = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et))) };
|
||||
},
|
||||
.media_query_list => {
|
||||
return .{ .media_query_list = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et))) };
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,140 +148,19 @@ pub const EventTarget = struct {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn _dispatchEvent(self: *parser.EventTarget, event: *parser.Event) !bool {
|
||||
return try parser.eventTargetDispatchEvent(self, event);
|
||||
pub fn _dispatchEvent(self: *parser.EventTarget, event: *parser.Event, page: *Page) !bool {
|
||||
const res = try parser.eventTargetDispatchEvent(self, event);
|
||||
|
||||
if (!parser.eventBubbles(event) or parser.eventIsStopped(event)) {
|
||||
return res;
|
||||
}
|
||||
|
||||
try page.window.dispatchForDocumentTarget(event);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
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" },
|
||||
// NOTE: as some event properties will change during the event dispatching phases
|
||||
// we need to copy thoses values in order to check them afterwards
|
||||
.{
|
||||
\\ var nb = 0; var evt; var phase; var cur;
|
||||
\\ function cbk(event) {
|
||||
\\ evt = event;
|
||||
\\ phase = event.eventPhase;
|
||||
\\ cur = event.currentTarget;
|
||||
\\ nb ++;
|
||||
\\ }
|
||||
,
|
||||
"undefined",
|
||||
},
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "content.addEventListener('basic', cbk)", "undefined" },
|
||||
.{ "content.dispatchEvent(new Event('basic'))", "true" },
|
||||
.{ "nb", "1" },
|
||||
.{ "evt instanceof Event", "true" },
|
||||
.{ "evt.type", "basic" },
|
||||
.{ "phase", "2" },
|
||||
.{ "cur.getAttribute('id')", "content" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
|
||||
.{ "para.dispatchEvent(new Event('basic'))", "true" },
|
||||
.{ "nb", "0" }, // handler is not called, no capture, not the target, no bubbling
|
||||
.{ "evt === undefined", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0", "0" },
|
||||
.{ "content.addEventListener('basic', cbk)", "undefined" },
|
||||
.{ "content.dispatchEvent(new Event('basic'))", "true" },
|
||||
.{ "nb", "1" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0", "0" },
|
||||
.{ "content.addEventListener('basic', cbk, true)", "undefined" },
|
||||
.{ "content.dispatchEvent(new Event('basic'))", "true" },
|
||||
.{ "nb", "2" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0", "0" },
|
||||
.{ "content.removeEventListener('basic', cbk)", "undefined" },
|
||||
.{ "content.dispatchEvent(new Event('basic'))", "true" },
|
||||
.{ "nb", "1" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0", "0" },
|
||||
.{ "content.removeEventListener('basic', cbk, {capture: true})", "undefined" },
|
||||
.{ "content.dispatchEvent(new Event('basic'))", "true" },
|
||||
.{ "nb", "0" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
|
||||
.{ "content.addEventListener('capture', cbk, true)", "undefined" },
|
||||
.{ "content.dispatchEvent(new Event('capture'))", "true" },
|
||||
.{ "nb", "1" },
|
||||
.{ "evt instanceof Event", "true" },
|
||||
.{ "evt.type", "capture" },
|
||||
.{ "phase", "2" },
|
||||
.{ "cur.getAttribute('id')", "content" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
|
||||
.{ "para.dispatchEvent(new Event('capture'))", "true" },
|
||||
.{ "nb", "1" },
|
||||
.{ "evt instanceof Event", "true" },
|
||||
.{ "evt.type", "capture" },
|
||||
.{ "phase", "1" },
|
||||
.{ "cur.getAttribute('id')", "content" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
|
||||
.{ "content.addEventListener('bubbles', cbk)", "undefined" },
|
||||
.{ "content.dispatchEvent(new Event('bubbles', {bubbles: true}))", "true" },
|
||||
.{ "nb", "1" },
|
||||
.{ "evt instanceof Event", "true" },
|
||||
.{ "evt.type", "bubbles" },
|
||||
.{ "evt.bubbles", "true" },
|
||||
.{ "phase", "2" },
|
||||
.{ "cur.getAttribute('id')", "content" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0; evt = undefined; phase = undefined; cur = undefined", "undefined" },
|
||||
.{ "para.dispatchEvent(new Event('bubbles', {bubbles: true}))", "true" },
|
||||
.{ "nb", "1" },
|
||||
.{ "evt instanceof Event", "true" },
|
||||
.{ "evt.type", "bubbles" },
|
||||
.{ "phase", "3" },
|
||||
.{ "cur.getAttribute('id')", "content" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const obj1 = {calls: 0, handleEvent: function() { this.calls += 1; } };", null },
|
||||
.{ "content.addEventListener('he', obj1);", null },
|
||||
.{ "content.dispatchEvent(new Event('he'));", null },
|
||||
.{ "obj1.calls", "1" },
|
||||
|
||||
.{ "content.removeEventListener('he', obj1);", null },
|
||||
.{ "content.dispatchEvent(new Event('he'));", null },
|
||||
.{ "obj1.calls", "1" },
|
||||
}, .{});
|
||||
|
||||
// doesn't crash on null receiver
|
||||
try runner.testCases(&.{
|
||||
.{ "content.addEventListener('he2', null);", null },
|
||||
.{ "content.dispatchEvent(new Event('he2'));", null },
|
||||
}, .{});
|
||||
test "Browser: DOM.EventTarget" {
|
||||
try testing.htmlRunner("dom/event_target.html");
|
||||
}
|
||||
|
||||
@@ -68,23 +68,24 @@ pub const DOMException = struct {
|
||||
}
|
||||
|
||||
// TODO: deinit
|
||||
pub fn init(alloc: std.mem.Allocator, err: anyerror, callerName: []const u8) !DOMException {
|
||||
const errCast = @as(parser.DOMError, @errorCast(err));
|
||||
const errName = DOMException.name(errCast);
|
||||
const str = switch (errCast) {
|
||||
pub fn init(alloc: std.mem.Allocator, err: anyerror, caller_name: []const u8) !DOMException {
|
||||
const dom_error = @as(parser.DOMError, @errorCast(err));
|
||||
const error_name = DOMException.name(dom_error);
|
||||
const str = switch (dom_error) {
|
||||
error.HierarchyRequest => try allocPrint(
|
||||
alloc,
|
||||
"{s}: Failed to execute '{s}' on 'Node': The new child element contains the parent.",
|
||||
.{ errName, callerName },
|
||||
.{ error_name, caller_name },
|
||||
),
|
||||
error.NoError => unreachable,
|
||||
// todo add more custom error messages
|
||||
else => try allocPrint(
|
||||
alloc,
|
||||
"{s}: TODO message", // TODO: implement other messages
|
||||
.{DOMException.name(errCast)},
|
||||
"{s}: Failed to execute '{s}' : {s}",
|
||||
.{ error_name, caller_name, error_name },
|
||||
),
|
||||
error.NoError => unreachable,
|
||||
};
|
||||
return .{ .err = errCast, .str = str };
|
||||
return .{ .err = dom_error, .str = str };
|
||||
}
|
||||
|
||||
fn error_from_str(name_: []const u8) ?parser.DOMError {
|
||||
@@ -218,47 +219,6 @@ pub const DOMException = struct {
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.Exception" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
const err = "Failed to execute 'appendChild' on 'Node': The new child element contains the parent.";
|
||||
try runner.testCases(&.{
|
||||
.{ "let content = document.getElementById('content')", "undefined" },
|
||||
.{ "let link = document.getElementById('link')", "undefined" },
|
||||
// HierarchyRequestError
|
||||
.{
|
||||
\\ var he;
|
||||
\\ try { link.appendChild(content) } catch (error) { he = error}
|
||||
\\ he.name
|
||||
,
|
||||
"HierarchyRequestError",
|
||||
},
|
||||
.{ "he.code", "3" },
|
||||
.{ "he.message", err },
|
||||
.{ "he.toString()", "HierarchyRequestError: " ++ err },
|
||||
.{ "he instanceof DOMException", "true" },
|
||||
.{ "he instanceof Error", "true" },
|
||||
}, .{});
|
||||
|
||||
// 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" },
|
||||
}, .{});
|
||||
test "Browser: DOM.Exceptions" {
|
||||
try testing.htmlRunner("dom/exceptions.html");
|
||||
}
|
||||
|
||||
@@ -72,13 +72,14 @@ pub fn HTMLCollectionByTagName(
|
||||
arena: Allocator,
|
||||
root: ?*parser.Node,
|
||||
tag_name: []const u8,
|
||||
include_root: bool,
|
||||
opts: Opts,
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = .{ .walkerDepthFirst = .{} },
|
||||
.matcher = .{ .matchByTagName = try MatchByTagName.init(arena, tag_name) },
|
||||
.include_root = include_root,
|
||||
.mutable = opts.mutable,
|
||||
.include_root = opts.include_root,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -109,13 +110,14 @@ pub fn HTMLCollectionByClassName(
|
||||
arena: Allocator,
|
||||
root: ?*parser.Node,
|
||||
classNames: []const u8,
|
||||
include_root: bool,
|
||||
opts: Opts,
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = .{ .walkerDepthFirst = .{} },
|
||||
.matcher = .{ .matchByClassName = try MatchByClassName.init(arena, classNames) },
|
||||
.include_root = include_root,
|
||||
.mutable = opts.mutable,
|
||||
.include_root = opts.include_root,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -139,13 +141,14 @@ pub fn HTMLCollectionByName(
|
||||
arena: Allocator,
|
||||
root: ?*parser.Node,
|
||||
name: []const u8,
|
||||
include_root: bool,
|
||||
opts: Opts,
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = .{ .walkerDepthFirst = .{} },
|
||||
.matcher = .{ .matchByName = try MatchByName.init(arena, name) },
|
||||
.include_root = include_root,
|
||||
.mutable = opts.mutable,
|
||||
.include_root = opts.include_root,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -189,13 +192,14 @@ pub const HTMLAllCollection = struct {
|
||||
|
||||
pub fn HTMLCollectionChildren(
|
||||
root: ?*parser.Node,
|
||||
include_root: bool,
|
||||
) !HTMLCollection {
|
||||
opts: Opts,
|
||||
) HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = .{ .walkerChildren = .{} },
|
||||
.matcher = .{ .matchTrue = .{} },
|
||||
.include_root = include_root,
|
||||
.mutable = opts.mutable,
|
||||
.include_root = opts.include_root,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -224,13 +228,14 @@ pub const MatchByLinks = struct {
|
||||
|
||||
pub fn HTMLCollectionByLinks(
|
||||
root: ?*parser.Node,
|
||||
include_root: bool,
|
||||
opts: Opts,
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = .{ .walkerDepthFirst = .{} },
|
||||
.matcher = .{ .matchByLinks = MatchByLinks{} },
|
||||
.include_root = include_root,
|
||||
.mutable = opts.mutable,
|
||||
.include_root = opts.include_root,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -249,13 +254,14 @@ pub const MatchByAnchors = struct {
|
||||
|
||||
pub fn HTMLCollectionByAnchors(
|
||||
root: ?*parser.Node,
|
||||
include_root: bool,
|
||||
opts: Opts,
|
||||
) !HTMLCollection {
|
||||
return HTMLCollection{
|
||||
.root = root,
|
||||
.walker = .{ .walkerDepthFirst = .{} },
|
||||
.matcher = .{ .matchByAnchors = MatchByAnchors{} },
|
||||
.include_root = include_root,
|
||||
.mutable = opts.mutable,
|
||||
.include_root = opts.include_root,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -285,6 +291,11 @@ pub const HTMLCollectionIterator = struct {
|
||||
}
|
||||
};
|
||||
|
||||
const Opts = struct {
|
||||
include_root: bool,
|
||||
mutable: bool = false,
|
||||
};
|
||||
|
||||
// WEB IDL https://dom.spec.whatwg.org/#htmlcollection
|
||||
// HTMLCollection is re implemented in zig here because libdom
|
||||
// dom_html_collection expects a comparison function callback as arguement.
|
||||
@@ -300,6 +311,8 @@ pub const HTMLCollection = struct {
|
||||
// itself.
|
||||
include_root: bool = false,
|
||||
|
||||
mutable: bool = false,
|
||||
|
||||
// save a state for the collection to improve the _item speed.
|
||||
cur_idx: ?u32 = null,
|
||||
cur_node: ?*parser.Node = null,
|
||||
@@ -350,7 +363,7 @@ pub const HTMLCollection = struct {
|
||||
var node: *parser.Node = undefined;
|
||||
|
||||
// Use the current state to improve speed if possible.
|
||||
if (self.cur_idx != null and index >= self.cur_idx.?) {
|
||||
if (self.mutable == false and self.cur_idx != null and index >= self.cur_idx.?) {
|
||||
i = self.cur_idx.?;
|
||||
node = self.cur_node.?;
|
||||
} else {
|
||||
@@ -449,52 +462,6 @@ pub const HTMLCollection = struct {
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.HTMLCollection" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let getElementsByTagName = document.getElementsByTagName('p')", "undefined" },
|
||||
.{ "getElementsByTagName.length", "2" },
|
||||
.{ "let getElementsByTagNameCI = document.getElementsByTagName('P')", "undefined" },
|
||||
.{ "getElementsByTagNameCI.length", "2" },
|
||||
.{ "getElementsByTagName.item(0).localName", "p" },
|
||||
.{ "getElementsByTagName.item(1).localName", "p" },
|
||||
.{ "let getElementsByTagNameAll = document.getElementsByTagName('*')", "undefined" },
|
||||
.{ "getElementsByTagNameAll.length", "8" },
|
||||
.{ "getElementsByTagNameAll.item(0).localName", "html" },
|
||||
.{ "getElementsByTagNameAll.item(0).localName", "html" },
|
||||
.{ "getElementsByTagNameAll.item(1).localName", "head" },
|
||||
.{ "getElementsByTagNameAll.item(0).localName", "html" },
|
||||
.{ "getElementsByTagNameAll.item(2).localName", "body" },
|
||||
.{ "getElementsByTagNameAll.item(3).localName", "div" },
|
||||
.{ "getElementsByTagNameAll.item(7).localName", "p" },
|
||||
.{ "getElementsByTagNameAll.namedItem('para-empty-child').localName", "span" },
|
||||
|
||||
// array like
|
||||
.{ "getElementsByTagNameAll[0].localName", "html" },
|
||||
.{ "getElementsByTagNameAll[7].localName", "p" },
|
||||
.{ "getElementsByTagNameAll[8]", "undefined" },
|
||||
.{ "getElementsByTagNameAll['para-empty-child'].localName", "span" },
|
||||
.{ "getElementsByTagNameAll['foo']", "undefined" },
|
||||
|
||||
.{ "document.getElementById('content').getElementsByTagName('*').length", "4" },
|
||||
.{ "document.getElementById('content').getElementsByTagName('p').length", "2" },
|
||||
.{ "document.getElementById('content').getElementsByTagName('div').length", "0" },
|
||||
|
||||
.{ "document.children.length", "1" },
|
||||
.{ "document.getElementById('content').children.length", "3" },
|
||||
|
||||
// check liveness
|
||||
.{ "let content = document.getElementById('content')", "undefined" },
|
||||
.{ "let pe = document.getElementById('para-empty')", "undefined" },
|
||||
.{ "let p = document.createElement('p')", "undefined" },
|
||||
.{ "p.textContent = 'OK live'", "OK live" },
|
||||
.{ "getElementsByTagName.item(1).textContent", " And" },
|
||||
.{ "content.appendChild(p) != undefined", "true" },
|
||||
.{ "getElementsByTagName.length", "3" },
|
||||
.{ "getElementsByTagName.item(2).textContent", "OK live" },
|
||||
.{ "content.insertBefore(p, pe) != undefined", "true" },
|
||||
.{ "getElementsByTagName.item(0).textContent", "OK live" },
|
||||
}, .{});
|
||||
test "Browser: DOM.HTMLCollection" {
|
||||
try testing.htmlRunner("dom/html_collection.html");
|
||||
}
|
||||
|
||||
@@ -50,23 +50,7 @@ pub const DOMImplementation = struct {
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.Implementation" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let impl = document.implementation", "undefined" },
|
||||
.{ "impl.createHTMLDocument();", "[object HTMLDocument]" },
|
||||
.{ "const doc = impl.createHTMLDocument('foo');", "undefined" },
|
||||
.{ "doc", "[object HTMLDocument]" },
|
||||
.{ "doc.title", "foo" },
|
||||
.{ "doc.body", "[object HTMLBodyElement]" },
|
||||
.{ "impl.createDocument(null, 'foo');", "[object Document]" },
|
||||
.{ "impl.createDocumentType('foo', 'bar', 'baz')", "[object DocumentType]" },
|
||||
.{ "impl.hasFeature()", "true" },
|
||||
}, .{});
|
||||
test "Browser: DOM.Implementation" {
|
||||
try testing.htmlRunner("dom/implementation.html");
|
||||
}
|
||||
|
||||
@@ -181,110 +181,6 @@ pub const IntersectionObserverEntry = struct {
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.IntersectionObserver" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "new IntersectionObserver(() => {}).observe(document.documentElement);", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let count_a = 0;", "undefined" },
|
||||
.{ "const a1 = document.createElement('div');", "undefined" },
|
||||
.{ "new IntersectionObserver(entries => {count_a += 1;}).observe(a1);", "undefined" },
|
||||
.{ "count_a;", "1" },
|
||||
}, .{});
|
||||
|
||||
// This test is documenting current behavior, not correct behavior.
|
||||
// Currently every time observe is called, the callback is called with all entries.
|
||||
try runner.testCases(&.{
|
||||
.{ "let count_b = 0;", "undefined" },
|
||||
.{ "let observer_b = new IntersectionObserver(entries => {count_b = entries.length;});", "undefined" },
|
||||
.{ "const b1 = document.createElement('div');", "undefined" },
|
||||
.{ "observer_b.observe(b1);", "undefined" },
|
||||
.{ "count_b;", "1" },
|
||||
.{ "const b2 = document.createElement('div');", "undefined" },
|
||||
.{ "observer_b.observe(b2);", "undefined" },
|
||||
.{ "count_b;", "2" },
|
||||
}, .{});
|
||||
|
||||
// Re-observing is a no-op
|
||||
try runner.testCases(&.{
|
||||
.{ "let count_bb = 0;", "undefined" },
|
||||
.{ "let observer_bb = new IntersectionObserver(entries => {count_bb = entries.length;});", "undefined" },
|
||||
.{ "const bb1 = document.createElement('div');", "undefined" },
|
||||
.{ "observer_bb.observe(bb1);", "undefined" },
|
||||
.{ "count_bb;", "1" },
|
||||
.{ "observer_bb.observe(bb1);", "undefined" },
|
||||
.{ "count_bb;", "1" }, // Still 1, not 2
|
||||
}, .{});
|
||||
|
||||
// Unobserve
|
||||
try runner.testCases(&.{
|
||||
.{ "let count_c = 0;", "undefined" },
|
||||
.{ "let observer_c = new IntersectionObserver(entries => { count_c = entries.length;});", "undefined" },
|
||||
.{ "const c1 = document.createElement('div');", "undefined" },
|
||||
.{ "observer_c.observe(c1);", "undefined" },
|
||||
.{ "count_c;", "1" },
|
||||
.{ "observer_c.unobserve(c1);", "undefined" },
|
||||
.{ "const c2 = document.createElement('div');", "undefined" },
|
||||
.{ "observer_c.observe(c2);", "undefined" },
|
||||
.{ "count_c;", "1" },
|
||||
}, .{});
|
||||
|
||||
// Disconnect
|
||||
try runner.testCases(&.{
|
||||
.{ "let observer_d = new IntersectionObserver(entries => {});", "undefined" },
|
||||
.{ "let d1 = document.createElement('div');", "undefined" },
|
||||
.{ "observer_d.observe(d1);", "undefined" },
|
||||
.{ "observer_d.disconnect();", "undefined" },
|
||||
.{ "observer_d.takeRecords().length;", "0" },
|
||||
}, .{});
|
||||
|
||||
// takeRecords
|
||||
try runner.testCases(&.{
|
||||
.{ "let observer_e = new IntersectionObserver(entries => {});", "undefined" },
|
||||
.{ "let e1 = document.createElement('div');", "undefined" },
|
||||
.{ "observer_e.observe(e1);", "undefined" },
|
||||
.{ "const e2 = document.createElement('div');", "undefined" },
|
||||
.{ "observer_e.observe(e2);", "undefined" },
|
||||
.{ "observer_e.takeRecords().length;", "2" },
|
||||
}, .{});
|
||||
|
||||
// Entry
|
||||
try runner.testCases(&.{
|
||||
.{ "let entry;", "undefined" },
|
||||
.{ "let div1 = document.createElement('div')", null },
|
||||
.{ "document.body.appendChild(div1);", null },
|
||||
.{ "new IntersectionObserver(entries => { entry = entries[0]; }).observe(div1);", null },
|
||||
.{ "entry.boundingClientRect.x;", "0" },
|
||||
.{ "entry.intersectionRatio;", "1" },
|
||||
.{ "entry.intersectionRect.x;", "0" },
|
||||
.{ "entry.intersectionRect.y;", "0" },
|
||||
.{ "entry.intersectionRect.width;", "1" },
|
||||
.{ "entry.intersectionRect.height;", "1" },
|
||||
.{ "entry.isIntersecting;", "true" },
|
||||
.{ "entry.rootBounds.x;", "0" },
|
||||
.{ "entry.rootBounds.y;", "0" },
|
||||
.{ "entry.rootBounds.width;", "1" },
|
||||
.{ "entry.rootBounds.height;", "1" },
|
||||
.{ "entry.target;", "[object HTMLDivElement]" },
|
||||
}, .{});
|
||||
|
||||
// Options
|
||||
try runner.testCases(&.{
|
||||
.{ "const new_root = document.createElement('span');", null },
|
||||
.{ "document.body.appendChild(new_root);", null },
|
||||
.{ "let new_entry;", "undefined" },
|
||||
.{
|
||||
\\ const new_observer = new IntersectionObserver(
|
||||
\\ entries => { new_entry = entries[0]; },
|
||||
\\ {root: new_root, rootMargin: '0px 0px 0px 0px', threshold: [0]});
|
||||
,
|
||||
"undefined",
|
||||
},
|
||||
.{ "new_observer.observe(document.createElement('div'));", "undefined" },
|
||||
.{ "new_entry.rootBounds.x;", "1" },
|
||||
}, .{});
|
||||
test "Browser: DOM.IntersectionObserver" {
|
||||
try testing.htmlRunner("dom/intersection_observer.html");
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ 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;
|
||||
@@ -36,12 +35,10 @@ const Walker = @import("../dom/walker.zig").WalkerChildren;
|
||||
|
||||
// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
|
||||
pub const MutationObserver = struct {
|
||||
loop: *Loop,
|
||||
page: *Page,
|
||||
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.
|
||||
@@ -50,17 +47,15 @@ pub const MutationObserver = struct {
|
||||
pub fn constructor(cbk: Env.Function, page: *Page) !MutationObserver {
|
||||
return .{
|
||||
.cbk = cbk,
|
||||
.loop = page.loop,
|
||||
.page = page,
|
||||
.observed = .{},
|
||||
.connected = true,
|
||||
.scheduled = false,
|
||||
.arena = page.arena,
|
||||
.loop_node = .{ .func = callback },
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _observe(self: *MutationObserver, node: *parser.Node, options_: ?Options) !void {
|
||||
const arena = self.arena;
|
||||
const arena = self.page.arena;
|
||||
var options = options_ orelse Options{};
|
||||
if (options.attributeFilter.len > 0) {
|
||||
options.attributeFilter = try arena.dupe([]const u8, options.attributeFilter);
|
||||
@@ -115,17 +110,17 @@ pub const MutationObserver = struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn callback(node: *Loop.CallbackNode, _: *?u63) void {
|
||||
const self: *MutationObserver = @fieldParentPtr("loop_node", node);
|
||||
fn callback(ctx: *anyopaque) ?u32 {
|
||||
const self: *MutationObserver = @ptrCast(@alignCast(ctx));
|
||||
if (self.connected == false) {
|
||||
self.scheduled = true;
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
self.scheduled = false;
|
||||
|
||||
const records = self.observed.items;
|
||||
if (records.len == 0) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
defer self.observed.clearRetainingCapacity();
|
||||
@@ -138,6 +133,7 @@ pub const MutationObserver = struct {
|
||||
.source = "mutation observer",
|
||||
});
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO
|
||||
@@ -282,7 +278,7 @@ const Observer = struct {
|
||||
var mutation_observer = self.mutation_observer;
|
||||
|
||||
const node = blk: {
|
||||
const event_target = try parser.eventTarget(event) orelse return;
|
||||
const event_target = parser.eventTarget(event) orelse return;
|
||||
break :blk parser.eventTargetToNode(event_target);
|
||||
};
|
||||
|
||||
@@ -301,7 +297,7 @@ const Observer = struct {
|
||||
.type = event_type.recordType(),
|
||||
};
|
||||
|
||||
const arena = mutation_observer.arena;
|
||||
const arena = mutation_observer.page.arena;
|
||||
switch (event_type) {
|
||||
.DOMAttrModified => {
|
||||
record.attribute_name = parser.mutationEventAttributeName(mutation_event) catch null;
|
||||
@@ -330,7 +326,12 @@ const Observer = struct {
|
||||
|
||||
if (mutation_observer.scheduled == false) {
|
||||
mutation_observer.scheduled = true;
|
||||
_ = try mutation_observer.loop.timeout(0, &mutation_observer.loop_node);
|
||||
try mutation_observer.page.scheduler.add(
|
||||
mutation_observer,
|
||||
MutationObserver.callback,
|
||||
0,
|
||||
.{ .name = "mutation_observer" },
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -352,85 +353,6 @@ const MutationEventType = enum {
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.MutationObserver" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "new MutationObserver(() => {}).observe(document, { childList: true });", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ var nb = 0;
|
||||
\\ var mrs;
|
||||
\\ new MutationObserver((mu) => {
|
||||
\\ mrs = mu;
|
||||
\\ nb++;
|
||||
\\ }).observe(document.firstElementChild, { attributes: true, attributeOldValue: true });
|
||||
\\ document.firstElementChild.setAttribute("foo", "bar");
|
||||
\\ // ignored b/c it's about another target.
|
||||
\\ document.firstElementChild.firstChild.setAttribute("foo", "bar");
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "nb", "1" },
|
||||
.{ "mrs[0].type", "attributes" },
|
||||
.{ "mrs[0].target == document.firstElementChild", "true" },
|
||||
.{ "mrs[0].target.getAttribute('foo')", "bar" },
|
||||
.{ "mrs[0].attributeName", "foo" },
|
||||
.{ "mrs[0].oldValue", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ var node = document.getElementById("para").firstChild;
|
||||
\\ var nb2 = 0;
|
||||
\\ var mrs2;
|
||||
\\ new MutationObserver((mu) => {
|
||||
\\ mrs2 = mu;
|
||||
\\ nb2++;
|
||||
\\ }).observe(node, { characterData: true, characterDataOldValue: true });
|
||||
\\ node.data = "foo";
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "nb2", "1" },
|
||||
.{ "mrs2[0].type", "characterData" },
|
||||
.{ "mrs2[0].target == node", "true" },
|
||||
.{ "mrs2[0].target.data", "foo" },
|
||||
.{ "mrs2[0].oldValue", " And" },
|
||||
}, .{});
|
||||
|
||||
// tests that mutation observers that have a callback which trigger the
|
||||
// mutation observer don't crash.
|
||||
// https://github.com/lightpanda-io/browser/issues/550
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ var node = document.getElementById("para");
|
||||
\\ new MutationObserver(() => {
|
||||
\\ node.innerText = 'a';
|
||||
\\ }).observe(document, { subtree:true,childList:true });
|
||||
\\ node.innerText = "2";
|
||||
,
|
||||
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" },
|
||||
}, .{});
|
||||
test "Browser: DOM.MutationObserver" {
|
||||
try testing.htmlRunner("dom/mutation_observer.html");
|
||||
}
|
||||
|
||||
@@ -115,26 +115,7 @@ pub const NamedNodeMapIterator = struct {
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.NamedNodeMap" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let a = document.getElementById('content').attributes", "undefined" },
|
||||
.{ "a.length", "1" },
|
||||
.{ "a.item(0)", "[object Attr]" },
|
||||
.{ "a.item(1)", "null" },
|
||||
.{ "a.getNamedItem('id')", "[object Attr]" },
|
||||
.{ "a.getNamedItem('foo')", "null" },
|
||||
.{ "a.setNamedItem(a.getNamedItem('id'))", "[object Attr]" },
|
||||
.{ "a['id'].name", "id" },
|
||||
.{ "a['id'].value", "content" },
|
||||
.{ "a['other']", "undefined" },
|
||||
.{ "a[0].value = 'abc123'", null },
|
||||
.{ "a[0].value", "abc123" },
|
||||
}, .{});
|
||||
test "Browser: DOM.NamedNodeMap" {
|
||||
try testing.htmlRunner("dom/named_node_map.html");
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ const EventTarget = @import("event_target.zig").EventTarget;
|
||||
const Attr = @import("attribute.zig").Attr;
|
||||
const CData = @import("character_data.zig");
|
||||
const Element = @import("element.zig").Element;
|
||||
const ElementUnion = @import("element.zig").Union;
|
||||
const NodeList = @import("nodelist.zig").NodeList;
|
||||
const Document = @import("document.zig").Document;
|
||||
const DocumentType = @import("document_type.zig").DocumentType;
|
||||
@@ -36,11 +37,11 @@ const DocumentFragment = @import("document_fragment.zig").DocumentFragment;
|
||||
const HTMLCollection = @import("html_collection.zig").HTMLCollection;
|
||||
const HTMLAllCollection = @import("html_collection.zig").HTMLAllCollection;
|
||||
const HTMLCollectionIterator = @import("html_collection.zig").HTMLCollectionIterator;
|
||||
const ShadowRoot = @import("shadow_root.zig").ShadowRoot;
|
||||
const Walker = @import("walker.zig").WalkerDepthFirst;
|
||||
|
||||
// HTML
|
||||
const HTML = @import("../html/html.zig");
|
||||
const HTMLElem = @import("../html/elements.zig");
|
||||
|
||||
// Node interfaces
|
||||
pub const Interfaces = .{
|
||||
@@ -67,7 +68,7 @@ pub const Node = struct {
|
||||
|
||||
pub fn toInterface(node: *parser.Node) !Union {
|
||||
return switch (try parser.nodeType(node)) {
|
||||
.element => try HTMLElem.toInterface(
|
||||
.element => try Element.toInterfaceT(
|
||||
Union,
|
||||
@as(*parser.Element, @ptrCast(node)),
|
||||
),
|
||||
@@ -75,7 +76,14 @@ pub const Node = struct {
|
||||
.text => .{ .Text = @as(*parser.Text, @ptrCast(node)) },
|
||||
.cdata_section => .{ .CDATASection = @as(*parser.CDATASection, @ptrCast(node)) },
|
||||
.processing_instruction => .{ .ProcessingInstruction = @as(*parser.ProcessingInstruction, @ptrCast(node)) },
|
||||
.document => .{ .HTMLDocument = @as(*parser.DocumentHTML, @ptrCast(node)) },
|
||||
.document => blk: {
|
||||
const doc: *parser.Document = @ptrCast(node);
|
||||
if (doc.is_html) {
|
||||
break :blk .{ .HTMLDocument = @as(*parser.DocumentHTML, @ptrCast(node)) };
|
||||
}
|
||||
|
||||
break :blk .{ .Document = doc };
|
||||
},
|
||||
.document_type => .{ .DocumentType = @as(*parser.DocumentType, @ptrCast(node)) },
|
||||
.attribute => .{ .Attr = @as(*parser.Attribute, @ptrCast(node)) },
|
||||
.document_fragment => .{ .DocumentFragment = @as(*parser.DocumentFragment, @ptrCast(node)) },
|
||||
@@ -100,10 +108,20 @@ pub const Node = struct {
|
||||
pub const _ENTITY_NODE = @intFromEnum(parser.NodeType.entity);
|
||||
pub const _NOTATION_NODE = @intFromEnum(parser.NodeType.notation);
|
||||
|
||||
pub const _DOCUMENT_POSITION_DISCONNECTED = @intFromEnum(parser.DocumentPosition.disconnected);
|
||||
pub const _DOCUMENT_POSITION_PRECEDING = @intFromEnum(parser.DocumentPosition.preceding);
|
||||
pub const _DOCUMENT_POSITION_FOLLOWING = @intFromEnum(parser.DocumentPosition.following);
|
||||
pub const _DOCUMENT_POSITION_CONTAINS = @intFromEnum(parser.DocumentPosition.contains);
|
||||
pub const _DOCUMENT_POSITION_CONTAINED_BY = @intFromEnum(parser.DocumentPosition.contained_by);
|
||||
pub const _DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = @intFromEnum(parser.DocumentPosition.implementation_specific);
|
||||
|
||||
// JS funcs
|
||||
// --------
|
||||
|
||||
// Read-only attributes
|
||||
pub fn get_baseURI(_: *parser.Node, page: *Page) ![]const u8 {
|
||||
return page.url.raw;
|
||||
}
|
||||
|
||||
pub fn get_firstChild(self: *parser.Node) !?Union {
|
||||
const res = try parser.nodeFirstChild(self);
|
||||
@@ -145,12 +163,12 @@ pub const Node = struct {
|
||||
return try Node.toInterface(res.?);
|
||||
}
|
||||
|
||||
pub fn get_parentElement(self: *parser.Node) !?HTMLElem.Union {
|
||||
pub fn get_parentElement(self: *parser.Node) !?ElementUnion {
|
||||
const res = try parser.nodeParentElement(self);
|
||||
if (res == null) {
|
||||
return null;
|
||||
}
|
||||
return try HTMLElem.toInterface(HTMLElem.Union, @as(*parser.Element, @ptrCast(res.?)));
|
||||
return try Element.toInterface(res.?);
|
||||
}
|
||||
|
||||
pub fn get_nodeName(self: *parser.Node) ![]const u8 {
|
||||
@@ -170,11 +188,35 @@ pub const Node = struct {
|
||||
}
|
||||
|
||||
pub fn get_isConnected(self: *parser.Node) !bool {
|
||||
// TODO: handle Shadow DOM
|
||||
if (try parser.nodeType(self) == .document) {
|
||||
return true;
|
||||
var node = self;
|
||||
while (true) {
|
||||
const node_type = try parser.nodeType(node);
|
||||
if (node_type == .document) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (try parser.nodeParentNode(node)) |parent| {
|
||||
// didn't find a document, but node has a parent, let's see
|
||||
// if it's connected;
|
||||
node = parent;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node_type != .document_fragment) {
|
||||
// doesn't have a parent and isn't a document_fragment
|
||||
// can't be connected
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parser.documentFragmentGetHost(@ptrCast(node))) |host| {
|
||||
// node doesn't have a parent, but it's a document fragment
|
||||
// with a host. The host is like the parent, but we only want to
|
||||
// traverse up (or down) to it in specific cases, like isConnected.
|
||||
node = host;
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return try Node.get_parentNode(self) != null;
|
||||
}
|
||||
|
||||
// Read/Write attributes
|
||||
@@ -198,6 +240,23 @@ pub const Node = struct {
|
||||
// Methods
|
||||
|
||||
pub fn _appendChild(self: *parser.Node, child: *parser.Node) !Union {
|
||||
const self_owner = try parser.nodeOwnerDocument(self);
|
||||
const child_owner = try parser.nodeOwnerDocument(child);
|
||||
|
||||
// If the node to be inserted has a different ownerDocument than the parent node,
|
||||
// modern browsers automatically adopt the node and its descendants into
|
||||
// the parent's ownerDocument.
|
||||
// This process is known as adoption.
|
||||
// (7.1) https://dom.spec.whatwg.org/#concept-node-insert
|
||||
if (child_owner == null or (self_owner != null and child_owner.? != self_owner.?)) {
|
||||
const w = Walker{};
|
||||
var current = child;
|
||||
while (true) {
|
||||
current.owner = self_owner;
|
||||
current = try w.get_next(child, current) orelse break;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: DocumentFragment special case
|
||||
const res = try parser.nodeAppendChild(self, child);
|
||||
return try Node.toInterface(res);
|
||||
@@ -209,14 +268,43 @@ pub const Node = struct {
|
||||
}
|
||||
|
||||
pub fn _compareDocumentPosition(self: *parser.Node, other: *parser.Node) !u32 {
|
||||
if (self == other) return 0;
|
||||
if (self == other) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const docself = try parser.nodeOwnerDocument(self);
|
||||
const docother = try parser.nodeOwnerDocument(other);
|
||||
const docself = try parser.nodeOwnerDocument(self) orelse blk: {
|
||||
if (try parser.nodeType(self) == .document) {
|
||||
break :blk @as(*parser.Document, @ptrCast(self));
|
||||
}
|
||||
break :blk null;
|
||||
};
|
||||
const docother = try parser.nodeOwnerDocument(other) orelse blk: {
|
||||
if (try parser.nodeType(other) == .document) {
|
||||
break :blk @as(*parser.Document, @ptrCast(other));
|
||||
}
|
||||
break :blk null;
|
||||
};
|
||||
|
||||
// Both are in different document.
|
||||
if (docself == null or docother == null or docother.? != docself.?) {
|
||||
return @intFromEnum(parser.DocumentPosition.disconnected);
|
||||
if (docself == null or docother == null or docself.? != docother.?) {
|
||||
return @intFromEnum(parser.DocumentPosition.disconnected) +
|
||||
@intFromEnum(parser.DocumentPosition.implementation_specific) +
|
||||
@intFromEnum(parser.DocumentPosition.preceding);
|
||||
}
|
||||
|
||||
if (@intFromPtr(self) == @intFromPtr(docself.?)) {
|
||||
// if self is the document, and we already know other is in the
|
||||
// document, then other is contained by and following self.
|
||||
return @intFromEnum(parser.DocumentPosition.following) +
|
||||
@intFromEnum(parser.DocumentPosition.contained_by);
|
||||
}
|
||||
|
||||
const rootself = try parser.nodeGetRootNode(self);
|
||||
const rootother = try parser.nodeGetRootNode(other);
|
||||
if (rootself != rootother) {
|
||||
return @intFromEnum(parser.DocumentPosition.disconnected) +
|
||||
@intFromEnum(parser.DocumentPosition.implementation_specific) +
|
||||
@intFromEnum(parser.DocumentPosition.preceding);
|
||||
}
|
||||
|
||||
// TODO Both are in a different trees in the same document.
|
||||
@@ -267,11 +355,23 @@ pub const Node = struct {
|
||||
// - An Element inside a standard web page will return an HTMLDocument object representing the entire page (or <iframe>).
|
||||
// - An Element inside a shadow DOM will return the associated ShadowRoot.
|
||||
// - An Element that is not attached to a document or a shadow tree will return the root of the DOM tree it belongs to
|
||||
pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }) !Union {
|
||||
const GetRootNodeResult = union(enum) {
|
||||
shadow_root: *ShadowRoot,
|
||||
node: Union,
|
||||
};
|
||||
pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }, page: *Page) !GetRootNodeResult {
|
||||
if (options) |options_| if (options_.composed) {
|
||||
log.warn(.web_api, "not implemented", .{ .feature = "getRootNode composed" });
|
||||
};
|
||||
return try Node.toInterface(try parser.nodeGetRootNode(self));
|
||||
|
||||
const root = try parser.nodeGetRootNode(self);
|
||||
if (page.getNodeState(root)) |state| {
|
||||
if (state.shadow_root) |sr| {
|
||||
return .{ .shadow_root = sr };
|
||||
}
|
||||
}
|
||||
|
||||
return .{ .node = try Node.toInterface(root) };
|
||||
}
|
||||
|
||||
pub fn _hasChildNodes(self: *parser.Node) !bool {
|
||||
@@ -290,10 +390,28 @@ pub const Node = struct {
|
||||
}
|
||||
|
||||
pub fn _insertBefore(self: *parser.Node, new_node: *parser.Node, ref_node_: ?*parser.Node) !Union {
|
||||
if (ref_node_) |ref_node| {
|
||||
return Node.toInterface(try parser.nodeInsertBefore(self, new_node, ref_node));
|
||||
if (ref_node_ == null) {
|
||||
return _appendChild(self, new_node);
|
||||
}
|
||||
return _appendChild(self, new_node);
|
||||
|
||||
const self_owner = try parser.nodeOwnerDocument(self);
|
||||
const new_node_owner = try parser.nodeOwnerDocument(new_node);
|
||||
|
||||
// If the node to be inserted has a different ownerDocument than the parent node,
|
||||
// modern browsers automatically adopt the node and its descendants into
|
||||
// the parent's ownerDocument.
|
||||
// This process is known as adoption.
|
||||
// (7.1) https://dom.spec.whatwg.org/#concept-node-insert
|
||||
if (new_node_owner == null or (self_owner != null and new_node_owner.? != self_owner.?)) {
|
||||
const w = Walker{};
|
||||
var current = new_node;
|
||||
while (true) {
|
||||
current.owner = self_owner;
|
||||
current = try w.get_next(new_node, current) orelse break;
|
||||
}
|
||||
}
|
||||
|
||||
return Node.toInterface(try parser.nodeInsertBefore(self, new_node, ref_node_.?));
|
||||
}
|
||||
|
||||
pub fn _isDefaultNamespace(self: *parser.Node, namespace: ?[]const u8) !bool {
|
||||
@@ -496,7 +614,7 @@ pub const Node = struct {
|
||||
fn toNode(self: NodeOrText, doc: *parser.Document) !*parser.Node {
|
||||
return switch (self) {
|
||||
.node => |n| n,
|
||||
.text => |txt| @alignCast(@ptrCast(try parser.documentCreateTextNode(doc, txt))),
|
||||
.text => |txt| @ptrCast(@alignCast(try parser.documentCreateTextNode(doc, txt))),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -513,210 +631,7 @@ pub const Node = struct {
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.node" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
{
|
||||
var err_out: ?[]const u8 = null;
|
||||
try runner.exec(
|
||||
\\ function trimAndReplace(str) {
|
||||
\\ str = str.replace(/(\r\n|\n|\r)/gm,'');
|
||||
\\ str = str.replace(/\s+/g, ' ');
|
||||
\\ str = str.trim();
|
||||
\\ return str;
|
||||
\\ }
|
||||
, "trimAndReplace", &err_out);
|
||||
}
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.body.compareDocumentPosition(document.firstChild); ", "10" },
|
||||
.{ "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"content\"));", "10" },
|
||||
.{ "document.getElementById(\"content\").compareDocumentPosition(document.getElementById(\"para-empty\"));", "20" },
|
||||
.{ "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"link\"));", "0" },
|
||||
.{ "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"link\"));", "2" },
|
||||
.{ "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"para-empty\"));", "4" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.getElementById('content').getRootNode().__proto__.constructor.name", "HTMLDocument" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
// for next test cases
|
||||
.{ "let content = document.getElementById('content')", "undefined" },
|
||||
.{ "let link = document.getElementById('link')", "undefined" },
|
||||
.{ "let first_child = content.firstChild.nextSibling", "undefined" }, // nextSibling because of line return \n
|
||||
|
||||
.{ "let body_first_child = document.body.firstChild", "undefined" },
|
||||
.{ "body_first_child.localName", "div" },
|
||||
.{ "body_first_child.__proto__.constructor.name", "HTMLDivElement" },
|
||||
.{ "document.getElementById('para-empty').firstChild.firstChild", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let last_child = content.lastChild.previousSibling", "undefined" }, // previousSibling because of line return \n
|
||||
.{ "last_child.__proto__.constructor.name", "Comment" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let next_sibling = link.nextSibling.nextSibling", "undefined" },
|
||||
.{ "next_sibling.localName", "p" },
|
||||
.{ "next_sibling.__proto__.constructor.name", "HTMLParagraphElement" },
|
||||
.{ "content.nextSibling.nextSibling", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let prev_sibling = document.getElementById('para-empty').previousSibling.previousSibling", "undefined" },
|
||||
.{ "prev_sibling.localName", "a" },
|
||||
.{ "prev_sibling.__proto__.constructor.name", "HTMLAnchorElement" },
|
||||
.{ "content.previousSibling", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let parent = document.getElementById('para').parentElement", "undefined" },
|
||||
.{ "parent.localName", "div" },
|
||||
.{ "parent.__proto__.constructor.name", "HTMLDivElement" },
|
||||
.{ "let h = content.parentElement.parentElement", "undefined" },
|
||||
.{ "h.parentElement", "null" },
|
||||
.{ "h.parentNode.__proto__.constructor.name", "HTMLDocument" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "first_child.nodeName === 'A'", "true" },
|
||||
.{ "link.firstChild.nodeName === '#text'", "true" },
|
||||
.{ "last_child.nodeName === '#comment'", "true" },
|
||||
.{ "document.nodeName === '#document'", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "first_child.nodeType === 1", "true" },
|
||||
.{ "link.firstChild.nodeType === 3", "true" },
|
||||
.{ "last_child.nodeType === 8", "true" },
|
||||
.{ "document.nodeType === 9", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let owner = content.ownerDocument", "undefined" },
|
||||
.{ "owner.__proto__.constructor.name", "HTMLDocument" },
|
||||
.{ "document.ownerDocument", "null" },
|
||||
.{ "let owner2 = document.createElement('div').ownerDocument", "undefined" },
|
||||
.{ "owner2.__proto__.constructor.name", "HTMLDocument" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "content.isConnected", "true" },
|
||||
.{ "document.isConnected", "true" },
|
||||
.{ "document.createElement('div').isConnected", "false" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "last_child.nodeValue === 'comment'", "true" },
|
||||
.{ "link.nodeValue === null", "true" },
|
||||
.{ "let text = link.firstChild", "undefined" },
|
||||
.{ "text.nodeValue === 'OK'", "true" },
|
||||
.{ "text.nodeValue = 'OK modified'", "OK modified" },
|
||||
.{ "text.nodeValue === 'OK modified'", "true" },
|
||||
.{ "link.nodeValue = 'nothing'", "nothing" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "text.textContent === 'OK modified'", "true" },
|
||||
.{ "trimAndReplace(content.textContent) === 'OK modified And'", "true" },
|
||||
.{ "text.textContent = 'OK'", "OK" },
|
||||
.{ "text.textContent", "OK" },
|
||||
.{ "trimAndReplace(document.getElementById('para-empty').textContent)", "" },
|
||||
.{ "document.getElementById('para-empty').textContent = 'OK'", "OK" },
|
||||
.{ "document.getElementById('para-empty').firstChild.nodeName === '#text'", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let append = document.createElement('h1')", "undefined" },
|
||||
.{ "content.appendChild(append).toString()", "[object HTMLHeadingElement]" },
|
||||
.{ "content.lastChild.__proto__.constructor.name", "HTMLHeadingElement" },
|
||||
.{ "content.appendChild(link).toString()", "[object HTMLAnchorElement]" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let clone = link.cloneNode()", "undefined" },
|
||||
.{ "clone.toString()", "[object HTMLAnchorElement]" },
|
||||
.{ "clone.parentNode === null", "true" },
|
||||
.{ "clone.firstChild === null", "true" },
|
||||
.{ "let clone_deep = link.cloneNode(true)", "undefined" },
|
||||
.{ "clone_deep.firstChild.nodeName === '#text'", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "link.contains(text)", "true" },
|
||||
.{ "text.contains(link)", "false" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "link.hasChildNodes()", "true" },
|
||||
.{ "text.hasChildNodes()", "false" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "link.childNodes.length", "1" },
|
||||
.{ "text.childNodes.length", "0" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let insertBefore = document.createElement('a')", "undefined" },
|
||||
.{ "link.insertBefore(insertBefore, text) !== undefined", "true" },
|
||||
.{ "link.firstChild.localName === 'a'", "true" },
|
||||
|
||||
.{ "let insertBefore2 = document.createElement('b')", null },
|
||||
.{ "link.insertBefore(insertBefore2, null).localName", "b" },
|
||||
.{ "link.childNodes[link.childNodes.length - 1].localName", "b" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
// TODO: does not seems to work
|
||||
// .{ "link.isDefaultNamespace('')", "true" },
|
||||
.{ "link.isDefaultNamespace('false')", "false" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let equal1 = document.createElement('a')", "undefined" },
|
||||
.{ "let equal2 = document.createElement('a')", "undefined" },
|
||||
.{ "equal1.textContent = 'is equal'", "is equal" },
|
||||
.{ "equal2.textContent = 'is equal'", "is equal" },
|
||||
// TODO: does not seems to work
|
||||
// .{ "equal1.isEqualNode(equal2)", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.body.isSameNode(document.body)", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
// TODO: no test
|
||||
.{ "link.normalize()", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "content.removeChild(append) !== undefined", "true" },
|
||||
.{ "last_child.__proto__.constructor.name !== 'HTMLHeadingElement'", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let replace = document.createElement('div')", "undefined" },
|
||||
.{ "link.replaceChild(replace, insertBefore) !== undefined", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "Node.ELEMENT_NODE", "1" },
|
||||
.{ "Node.ATTRIBUTE_NODE", "2" },
|
||||
.{ "Node.TEXT_NODE", "3" },
|
||||
.{ "Node.CDATA_SECTION_NODE", "4" },
|
||||
.{ "Node.PROCESSING_INSTRUCTION_NODE", "7" },
|
||||
.{ "Node.COMMENT_NODE", "8" },
|
||||
.{ "Node.DOCUMENT_NODE", "9" },
|
||||
.{ "Node.DOCUMENT_TYPE_NODE", "10" },
|
||||
.{ "Node.DOCUMENT_FRAGMENT_NODE", "11" },
|
||||
.{ "Node.ENTITY_REFERENCE_NODE", "5" },
|
||||
.{ "Node.ENTITY_NODE", "6" },
|
||||
.{ "Node.NOTATION_NODE", "12" },
|
||||
}, .{});
|
||||
test "Browser: DOM.Node" {
|
||||
try testing.htmlRunner("dom/node.html");
|
||||
try testing.htmlRunner("dom/node_owner.html");
|
||||
}
|
||||
|
||||
@@ -75,15 +75,6 @@ pub fn verify(what_to_show: u32, filter: ?Env.Function, node: *parser.Node) !Ver
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.NodeFilter" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "NodeFilter.FILTER_ACCEPT", "1" },
|
||||
.{ "NodeFilter.FILTER_REJECT", "2" },
|
||||
.{ "NodeFilter.FILTER_SKIP", "3" },
|
||||
.{ "NodeFilter.SHOW_ALL", "4294967295" },
|
||||
.{ "NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT", "129" },
|
||||
}, .{});
|
||||
test "Browser: DOM.NodeFilter" {
|
||||
try testing.htmlRunner("dom/node_filter.html");
|
||||
}
|
||||
|
||||
@@ -17,11 +17,13 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Env = @import("../env.zig").Env;
|
||||
const NodeFilter = @import("node_filter.zig");
|
||||
const Node = @import("node.zig").Node;
|
||||
const NodeUnion = @import("node.zig").Union;
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/NodeIterator
|
||||
// While this is similar to TreeWalker it has its own implementation as there are several subtle differences
|
||||
@@ -29,20 +31,28 @@ const NodeUnion = @import("node.zig").Union;
|
||||
// - nextNode returns the reference node, whereas TreeWalker returns the next node
|
||||
// - Skip and reject are equivalent for NodeIterator, for TreeWalker they are different
|
||||
pub const NodeIterator = struct {
|
||||
pub const Exception = DOMException;
|
||||
|
||||
root: *parser.Node,
|
||||
reference_node: *parser.Node,
|
||||
what_to_show: u32,
|
||||
filter: ?NodeIteratorOpts,
|
||||
filter_func: ?Env.Function,
|
||||
|
||||
pointer_before_current: bool = true,
|
||||
// used to track / block recursive filters
|
||||
is_in_callback: bool = false,
|
||||
|
||||
// One of the few cases where null and undefined resolve to different default.
|
||||
// We need the raw JsObject so that we can probe the tri state:
|
||||
// null, undefined or i32.
|
||||
pub const WhatToShow = Env.JsObject;
|
||||
|
||||
pub const NodeIteratorOpts = union(enum) {
|
||||
function: Env.Function,
|
||||
object: struct { acceptNode: Env.Function },
|
||||
};
|
||||
|
||||
pub fn init(node: *parser.Node, what_to_show: ?u32, filter: ?NodeIteratorOpts) !NodeIterator {
|
||||
pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?NodeIteratorOpts) !NodeIterator {
|
||||
var filter_func: ?Env.Function = null;
|
||||
if (filter) |f| {
|
||||
filter_func = switch (f) {
|
||||
@@ -51,10 +61,21 @@ pub const NodeIterator = struct {
|
||||
};
|
||||
}
|
||||
|
||||
var what_to_show: u32 = undefined;
|
||||
if (what_to_show_) |wts| {
|
||||
switch (try wts.triState(NodeIterator, "what_to_show", u32)) {
|
||||
.null => what_to_show = 0,
|
||||
.undefined => what_to_show = NodeFilter.NodeFilter._SHOW_ALL,
|
||||
.value => |v| what_to_show = v,
|
||||
}
|
||||
} else {
|
||||
what_to_show = NodeFilter.NodeFilter._SHOW_ALL;
|
||||
}
|
||||
|
||||
return .{
|
||||
.root = node,
|
||||
.reference_node = node,
|
||||
.what_to_show = what_to_show orelse NodeFilter.NodeFilter._SHOW_ALL,
|
||||
.what_to_show = what_to_show,
|
||||
.filter = filter,
|
||||
.filter_func = filter_func,
|
||||
};
|
||||
@@ -81,9 +102,13 @@ pub const NodeIterator = struct {
|
||||
}
|
||||
|
||||
pub fn _nextNode(self: *NodeIterator) !?NodeUnion {
|
||||
if (self.pointer_before_current) { // Unlike TreeWalker, NodeIterator starts at the first node
|
||||
self.pointer_before_current = false;
|
||||
try self.callbackStart();
|
||||
defer self.callbackEnd();
|
||||
|
||||
if (self.pointer_before_current) {
|
||||
// Unlike TreeWalker, NodeIterator starts at the first node
|
||||
if (.accept == try NodeFilter.verify(self.what_to_show, self.filter_func, self.reference_node)) {
|
||||
self.pointer_before_current = false;
|
||||
return try Node.toInterface(self.reference_node);
|
||||
}
|
||||
}
|
||||
@@ -107,13 +132,19 @@ pub const NodeIterator = struct {
|
||||
}
|
||||
|
||||
pub fn _previousNode(self: *NodeIterator) !?NodeUnion {
|
||||
try self.callbackStart();
|
||||
defer self.callbackEnd();
|
||||
|
||||
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
|
||||
self.pointer_before_current = true;
|
||||
// Still need to verify as last may be first as well
|
||||
return try Node.toInterface(self.reference_node);
|
||||
}
|
||||
}
|
||||
if (self.reference_node == self.root) return null;
|
||||
if (self.reference_node == self.root) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var current = self.reference_node;
|
||||
while (try parser.nodePreviousSibling(current)) |previous| {
|
||||
@@ -151,6 +182,11 @@ pub const NodeIterator = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn _detach(self: *const NodeIterator) void {
|
||||
// no-op as per spec
|
||||
_ = self;
|
||||
}
|
||||
|
||||
fn firstChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
|
||||
const children = try parser.nodeGetChildNodes(node);
|
||||
const child_count = try parser.nodeListLength(children);
|
||||
@@ -217,74 +253,21 @@ pub const NodeIterator = struct {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn callbackStart(self: *NodeIterator) !void {
|
||||
if (self.is_in_callback) {
|
||||
// this is the correct DOMExeption
|
||||
return error.InvalidState;
|
||||
}
|
||||
self.is_in_callback = true;
|
||||
}
|
||||
|
||||
fn callbackEnd(self: *NodeIterator) void {
|
||||
self.is_in_callback = false;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.NodeFilter" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ const nodeIterator = document.createNodeIterator(
|
||||
\\ document.body,
|
||||
\\ NodeFilter.SHOW_ELEMENT,
|
||||
\\ {
|
||||
\\ acceptNode(node) {
|
||||
\\ return NodeFilter.FILTER_ACCEPT;
|
||||
\\ },
|
||||
\\ },
|
||||
\\ );
|
||||
\\ nodeIterator.nextNode().nodeName;
|
||||
,
|
||||
"BODY",
|
||||
},
|
||||
.{ "nodeIterator.nextNode().nodeName", "DIV" },
|
||||
.{ "nodeIterator.nextNode().nodeName", "A" },
|
||||
.{ "nodeIterator.previousNode().nodeName", "A" }, // pointer_before_current flips
|
||||
.{ "nodeIterator.nextNode().nodeName", "A" }, // pointer_before_current flips
|
||||
.{ "nodeIterator.previousNode().nodeName", "A" }, // pointer_before_current flips
|
||||
.{ "nodeIterator.previousNode().nodeName", "DIV" },
|
||||
.{ "nodeIterator.previousNode().nodeName", "BODY" },
|
||||
.{ "nodeIterator.previousNode()", "null" }, // Not HEAD since body is root
|
||||
.{ "nodeIterator.previousNode()", "null" }, // Keeps returning null
|
||||
.{ "nodeIterator.nextNode().nodeName", "BODY" },
|
||||
|
||||
.{ "nodeIterator.nextNode().nodeName", null },
|
||||
.{ "nodeIterator.nextNode().nodeName", null },
|
||||
.{ "nodeIterator.nextNode().nodeName", null },
|
||||
.{ "nodeIterator.nextNode().nodeName", "SPAN" },
|
||||
.{ "nodeIterator.nextNode().nodeName", "P" },
|
||||
.{ "nodeIterator.nextNode()", "null" }, // Just the last one
|
||||
.{ "nodeIterator.nextNode()", "null" }, // Keeps returning null
|
||||
.{ "nodeIterator.previousNode().nodeName", "P" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ const notationIterator = document.createNodeIterator(
|
||||
\\ document.body,
|
||||
\\ NodeFilter.SHOW_NOTATION,
|
||||
\\ );
|
||||
\\ notationIterator.nextNode();
|
||||
,
|
||||
"null",
|
||||
},
|
||||
.{ "notationIterator.previousNode()", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nodeIterator.filter.acceptNode(document.body)", "1" },
|
||||
.{ "notationIterator.filter", "null" },
|
||||
.{
|
||||
\\ const rejectIterator = document.createNodeIterator(
|
||||
\\ document.body,
|
||||
\\ NodeFilter.SHOW_ALL,
|
||||
\\ (e => { return NodeFilter.FILTER_REJECT}),
|
||||
\\ );
|
||||
\\ rejectIterator.filter(document.body);
|
||||
,
|
||||
"2",
|
||||
},
|
||||
}, .{});
|
||||
test "Browser: DOM.NodeIterator" {
|
||||
try testing.htmlRunner("dom/node_iterator.html");
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ pub const NodeList = struct {
|
||||
try self.nodes.append(alloc, node);
|
||||
}
|
||||
|
||||
pub fn get_length(self: *NodeList) u32 {
|
||||
pub fn get_length(self: *const NodeList) u32 {
|
||||
return @intCast(self.nodes.items.len);
|
||||
}
|
||||
|
||||
@@ -177,22 +177,6 @@ pub const NodeList = struct {
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.NodeList" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let list = document.getElementById('content').childNodes", "undefined" },
|
||||
.{ "list.length", "9" },
|
||||
.{ "list[0].__proto__.constructor.name", "Text" },
|
||||
.{
|
||||
\\ let i = 0;
|
||||
\\ list.forEach(function (n, idx) {
|
||||
\\ i += idx;
|
||||
\\ });
|
||||
\\ i;
|
||||
,
|
||||
"36",
|
||||
},
|
||||
}, .{});
|
||||
test "Browser: DOM.NodeList" {
|
||||
try testing.htmlRunner("dom/node_list.html");
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
const milliTimestamp = @import("../../datetime.zig").milliTimestamp;
|
||||
|
||||
pub const Interfaces = .{
|
||||
Performance,
|
||||
PerformanceEntry,
|
||||
@@ -36,29 +38,27 @@ pub const Performance = struct {
|
||||
// Extend libdom event target for pure zig struct.
|
||||
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .performance },
|
||||
|
||||
time_origin: std.time.Timer,
|
||||
time_origin: u64,
|
||||
// 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,
|
||||
pub fn init() Performance {
|
||||
return .{
|
||||
.time_origin = milliTimestamp(),
|
||||
};
|
||||
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 get_timeOrigin(self: *const Performance) u64 {
|
||||
return self.time_origin;
|
||||
}
|
||||
|
||||
pub fn reset(self: *Performance) void {
|
||||
self.time_origin = milliTimestamp();
|
||||
}
|
||||
|
||||
pub fn _now(self: *const Performance) u64 {
|
||||
return milliTimestamp() - self.time_origin;
|
||||
}
|
||||
|
||||
pub fn _mark(_: *Performance, name: []const u8, _options: ?PerformanceMark.Options, page: *Page) !PerformanceMark {
|
||||
@@ -76,6 +76,19 @@ pub const Performance = struct {
|
||||
pub fn _clearMeasures(_: *Performance, name: ?[]const u8) void {
|
||||
_ = name;
|
||||
}
|
||||
|
||||
// TODO: fn _measures should record the marks in a lookup
|
||||
pub fn _getEntriesByName(_: *Performance, name: []const u8, typ: ?[]const u8) []PerformanceEntry {
|
||||
_ = name;
|
||||
_ = typ;
|
||||
return &.{};
|
||||
}
|
||||
|
||||
// TODO: fn _measures should record the marks in a lookup
|
||||
pub fn _getEntriesByType(_: *Performance, typ: []const u8) []PerformanceEntry {
|
||||
_ = typ;
|
||||
return &.{};
|
||||
}
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry
|
||||
@@ -139,14 +152,14 @@ pub const PerformanceMark = struct {
|
||||
|
||||
const Options = struct {
|
||||
detail: ?Env.JsObject = null,
|
||||
start_time: ?f64 = null,
|
||||
startTime: ?f64 = null,
|
||||
};
|
||||
|
||||
pub fn constructor(name: []const u8, _options: ?Options, page: *Page) !PerformanceMark {
|
||||
const perf = &page.window.performance;
|
||||
|
||||
const options = _options orelse Options{};
|
||||
const start_time = options.start_time orelse perf._now();
|
||||
const start_time = options.startTime orelse @as(f64, @floatFromInt(perf._now()));
|
||||
|
||||
if (start_time < 0.0) {
|
||||
return error.TypeError;
|
||||
@@ -168,16 +181,13 @@ pub const PerformanceMark = struct {
|
||||
const testing = @import("./../../testing.zig");
|
||||
|
||||
test "Performance: get_timeOrigin" {
|
||||
var perf = Performance{ .time_origin = try std.time.Timer.start() };
|
||||
var perf = Performance.init();
|
||||
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() };
|
||||
var perf = Performance.init();
|
||||
|
||||
// Monotonically increasing
|
||||
var now = perf._now();
|
||||
@@ -185,30 +195,14 @@ test "Performance: now" {
|
||||
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" },
|
||||
}, .{});
|
||||
test "Browser: Performance.Mark" {
|
||||
try testing.htmlRunner("dom/performance.html");
|
||||
}
|
||||
|
||||
@@ -53,11 +53,6 @@ const Options = struct {
|
||||
};
|
||||
|
||||
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" },
|
||||
}, .{});
|
||||
test "Browser: DOM.PerformanceObserver" {
|
||||
try testing.htmlRunner("dom/performance_observer.html");
|
||||
}
|
||||
|
||||
@@ -87,30 +87,6 @@ pub const ProcessingInstruction = struct {
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.ProcessingInstruction" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let pi = document.createProcessingInstruction('foo', 'bar')", "undefined" },
|
||||
.{ "pi.target", "foo" },
|
||||
.{ "pi.data", "bar" },
|
||||
.{ "pi.data = 'foo'", "foo" },
|
||||
.{ "pi.data", "foo" },
|
||||
|
||||
.{ "let pi2 = pi.cloneNode()", "undefined" },
|
||||
.{ "pi2.nodeType", "7" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let pi11 = document.createProcessingInstruction('target1', 'data1');", "undefined" },
|
||||
.{ "let pi12 = document.createProcessingInstruction('target2', 'data2');", "undefined" },
|
||||
.{ "let pi13 = document.createProcessingInstruction('target1', 'data1');", "undefined" },
|
||||
.{ "pi11.isEqualNode(pi11)", "true" },
|
||||
.{ "pi11.isEqualNode(pi13)", "true" },
|
||||
.{ "pi11.isEqualNode(pi12)", "false" },
|
||||
.{ "pi12.isEqualNode(pi13)", "false" },
|
||||
.{ "pi11.isEqualNode(document)", "false" },
|
||||
.{ "document.isEqualNode(pi11)", "false" },
|
||||
}, .{});
|
||||
test "Browser: DOM.ProcessingInstruction" {
|
||||
try testing.htmlRunner("dom/processing_instruction.html");
|
||||
}
|
||||
|
||||
@@ -21,8 +21,9 @@ 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;
|
||||
const NodeUnion = @import("node.zig").Union;
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
|
||||
pub const Interfaces = .{
|
||||
AbstractRange,
|
||||
@@ -31,10 +32,10 @@ pub const Interfaces = .{
|
||||
|
||||
pub const AbstractRange = struct {
|
||||
collapsed: bool,
|
||||
end_container: *parser.Node,
|
||||
end_offset: i32,
|
||||
start_container: *parser.Node,
|
||||
start_offset: i32,
|
||||
end_node: *parser.Node,
|
||||
end_offset: u32,
|
||||
start_node: *parser.Node,
|
||||
start_offset: u32,
|
||||
|
||||
pub fn updateCollapsed(self: *AbstractRange) void {
|
||||
// TODO: Eventually, compare properly.
|
||||
@@ -46,54 +47,122 @@ pub const AbstractRange = struct {
|
||||
}
|
||||
|
||||
pub fn get_endContainer(self: *const AbstractRange) !NodeUnion {
|
||||
return Node.toInterface(self.end_container);
|
||||
return Node.toInterface(self.end_node);
|
||||
}
|
||||
|
||||
pub fn get_endOffset(self: *const AbstractRange) i32 {
|
||||
pub fn get_endOffset(self: *const AbstractRange) u32 {
|
||||
return self.end_offset;
|
||||
}
|
||||
|
||||
pub fn get_startContainer(self: *const AbstractRange) !NodeUnion {
|
||||
return Node.toInterface(self.start_container);
|
||||
return Node.toInterface(self.start_node);
|
||||
}
|
||||
|
||||
pub fn get_startOffset(self: *const AbstractRange) i32 {
|
||||
pub fn get_startOffset(self: *const AbstractRange) u32 {
|
||||
return self.start_offset;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Range = struct {
|
||||
pub const Exception = DOMException;
|
||||
pub const prototype = *AbstractRange;
|
||||
|
||||
proto: AbstractRange,
|
||||
|
||||
pub const _START_TO_START = 0;
|
||||
pub const _START_TO_END = 1;
|
||||
pub const _END_TO_END = 2;
|
||||
pub const _END_TO_START = 3;
|
||||
|
||||
// The Range() constructor returns a newly created Range object whose start
|
||||
// and end is the global Document object.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Range/Range
|
||||
pub fn constructor(page: *Page) Range {
|
||||
const proto: AbstractRange = .{
|
||||
.collapsed = true,
|
||||
.end_container = parser.documentHTMLToNode(page.window.document),
|
||||
.end_node = parser.documentHTMLToNode(page.window.document),
|
||||
.end_offset = 0,
|
||||
.start_container = parser.documentHTMLToNode(page.window.document),
|
||||
.start_node = 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;
|
||||
pub fn _setStart(self: *Range, node: *parser.Node, offset_: i32) !void {
|
||||
try ensureValidOffset(node, offset_);
|
||||
const offset: u32 = @intCast(offset_);
|
||||
const position = compare(node, offset, self.proto.start_node, self.proto.start_offset) catch |err| switch (err) {
|
||||
error.WrongDocument => blk: {
|
||||
// allow a node with a different root than the current, or
|
||||
// a disconnected one. Treat it as if it's "after", so that
|
||||
// we also update the end_offset and end_node.
|
||||
break :blk 1;
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
|
||||
if (position == 1) {
|
||||
// if we're setting the node after the current start, the end must
|
||||
// be set too.
|
||||
self.proto.end_offset = offset;
|
||||
self.proto.end_node = node;
|
||||
}
|
||||
self.proto.start_node = node;
|
||||
self.proto.start_offset = offset;
|
||||
self.proto.updateCollapsed();
|
||||
}
|
||||
|
||||
pub fn _setEnd(self: *Range, node: *parser.Node, offset: i32) void {
|
||||
self.proto.end_container = node;
|
||||
pub fn _setStartBefore(self: *Range, node: *parser.Node) !void {
|
||||
const parent, const index = try getParentAndIndex(node);
|
||||
self.proto.start_node = parent;
|
||||
self.proto.start_offset = index;
|
||||
}
|
||||
|
||||
pub fn _setStartAfter(self: *Range, node: *parser.Node) !void {
|
||||
const parent, const index = try getParentAndIndex(node);
|
||||
self.proto.start_node = parent;
|
||||
self.proto.start_offset = index + 1;
|
||||
}
|
||||
|
||||
pub fn _setEnd(self: *Range, node: *parser.Node, offset_: i32) !void {
|
||||
try ensureValidOffset(node, offset_);
|
||||
const offset: u32 = @intCast(offset_);
|
||||
|
||||
const position = compare(node, offset, self.proto.start_node, self.proto.start_offset) catch |err| switch (err) {
|
||||
error.WrongDocument => blk: {
|
||||
// allow a node with a different root than the current, or
|
||||
// a disconnected one. Treat it as if it's "before", so that
|
||||
// we also update the end_offset and end_node.
|
||||
break :blk -1;
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
|
||||
if (position == -1) {
|
||||
// if we're setting the node before the current start, the start
|
||||
// must be set too.
|
||||
self.proto.start_offset = offset;
|
||||
self.proto.start_node = node;
|
||||
}
|
||||
|
||||
self.proto.end_node = node;
|
||||
self.proto.end_offset = offset;
|
||||
self.proto.updateCollapsed();
|
||||
}
|
||||
|
||||
pub fn _setEndBefore(self: *Range, node: *parser.Node) !void {
|
||||
const parent, const index = try getParentAndIndex(node);
|
||||
self.proto.end_node = parent;
|
||||
self.proto.end_offset = index;
|
||||
}
|
||||
|
||||
pub fn _setEndAfter(self: *Range, node: *parser.Node) !void {
|
||||
const parent, const index = try getParentAndIndex(node);
|
||||
self.proto.end_node = parent;
|
||||
self.proto.end_offset = index + 1;
|
||||
}
|
||||
|
||||
pub fn _createContextualFragment(_: *Range, fragment: []const u8, page: *Page) !*parser.DocumentFragment {
|
||||
const document_html = page.window.document;
|
||||
const document = parser.documentHTMLToDocument(document_html);
|
||||
@@ -102,9 +171,9 @@ pub const Range = struct {
|
||||
}
|
||||
|
||||
pub fn _selectNodeContents(self: *Range, node: *parser.Node) !void {
|
||||
self.proto.start_container = node;
|
||||
self.proto.start_node = node;
|
||||
self.proto.start_offset = 0;
|
||||
self.proto.end_container = node;
|
||||
self.proto.end_node = node;
|
||||
|
||||
// Set end_offset
|
||||
switch (try parser.nodeType(node)) {
|
||||
@@ -127,6 +196,89 @@ pub const Range = struct {
|
||||
self.proto.updateCollapsed();
|
||||
}
|
||||
|
||||
// creates a copy
|
||||
pub fn _cloneRange(self: *const Range) Range {
|
||||
return .{
|
||||
.proto = .{
|
||||
.collapsed = self.proto.collapsed,
|
||||
.end_node = self.proto.end_node,
|
||||
.end_offset = self.proto.end_offset,
|
||||
.start_node = self.proto.start_node,
|
||||
.start_offset = self.proto.start_offset,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _comparePoint(self: *const Range, node: *parser.Node, offset_: i32) !i32 {
|
||||
const start = self.proto.start_node;
|
||||
if (try parser.nodeGetRootNode(start) != try parser.nodeGetRootNode(node)) {
|
||||
// WPT really wants this error to be first. Later, when we check
|
||||
// if the relative position is 'disconnected', it'll also catch this
|
||||
// case, but WPT will complain because it sometimes also sends
|
||||
// invalid offsets, and it wants WrongDocument to be raised.
|
||||
return error.WrongDocument;
|
||||
}
|
||||
|
||||
if (try parser.nodeType(node) == .document_type) {
|
||||
return error.InvalidNodeType;
|
||||
}
|
||||
|
||||
try ensureValidOffset(node, offset_);
|
||||
|
||||
const offset: u32 = @intCast(offset_);
|
||||
if (try compare(node, offset, start, self.proto.start_offset) == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (try compare(node, offset, self.proto.end_node, self.proto.end_offset) == 1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
pub fn _isPointInRange(self: *const Range, node: *parser.Node, offset_: i32) !bool {
|
||||
return self._comparePoint(node, offset_) catch |err| switch (err) {
|
||||
error.WrongDocument => return false,
|
||||
else => return err,
|
||||
} == 0;
|
||||
}
|
||||
|
||||
pub fn _intersectsNode(self: *const Range, node: *parser.Node) !bool {
|
||||
const start_root = try parser.nodeGetRootNode(self.proto.start_node);
|
||||
const node_root = try parser.nodeGetRootNode(node);
|
||||
if (start_root != node_root) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parent, const index = getParentAndIndex(node) catch |err| switch (err) {
|
||||
error.InvalidNodeType => return true, // if node has no parent, we return true.
|
||||
else => return err,
|
||||
};
|
||||
|
||||
if (try compare(parent, index + 1, self.proto.start_node, self.proto.start_offset) != 1) {
|
||||
// node isn't after start, can't intersect
|
||||
return false;
|
||||
}
|
||||
|
||||
if (try compare(parent, index, self.proto.end_node, self.proto.end_offset) != -1) {
|
||||
// node isn't before end, can't intersect
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn _compareBoundaryPoints(self: *const Range, how: i32, other: *const Range) !i32 {
|
||||
return switch (how) {
|
||||
_START_TO_START => compare(self.proto.start_node, self.proto.start_offset, other.proto.start_node, other.proto.start_offset),
|
||||
_START_TO_END => compare(self.proto.start_node, self.proto.start_offset, other.proto.end_node, other.proto.end_offset),
|
||||
_END_TO_END => compare(self.proto.end_node, self.proto.end_offset, other.proto.end_node, other.proto.end_offset),
|
||||
_END_TO_START => compare(self.proto.end_node, self.proto.end_offset, other.proto.start_node, other.proto.start_offset),
|
||||
else => error.NotSupported, // this is the correct DOM Exception to return
|
||||
};
|
||||
}
|
||||
|
||||
// The Range.detach() method does nothing. It used to disable the Range
|
||||
// object and enable the browser to release associated resources. The
|
||||
// method has been kept for compatibility.
|
||||
@@ -134,45 +286,105 @@ pub const Range = struct {
|
||||
pub fn _detach(_: *Range) void {}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.Range" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
fn ensureValidOffset(node: *parser.Node, offset: i32) !void {
|
||||
if (offset < 0) {
|
||||
return error.IndexSize;
|
||||
}
|
||||
|
||||
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
|
||||
}, .{});
|
||||
// not >= because 0 seems to represent the node itself.
|
||||
if (offset > try nodeLength(node)) {
|
||||
return error.IndexSize;
|
||||
}
|
||||
}
|
||||
|
||||
fn nodeLength(node: *parser.Node) !usize {
|
||||
switch (try isTextual(node)) {
|
||||
true => return ((try parser.nodeTextContent(node)) orelse "").len,
|
||||
false => {
|
||||
const children = try parser.nodeGetChildNodes(node);
|
||||
return @intCast(try parser.nodeListLength(children));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn isTextual(node: *parser.Node) !bool {
|
||||
return switch (try parser.nodeType(node)) {
|
||||
.text, .comment, .cdata_section => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn getParentAndIndex(child: *parser.Node) !struct { *parser.Node, u32 } {
|
||||
const parent = (try parser.nodeParentNode(child)) orelse return error.InvalidNodeType;
|
||||
const children = try parser.nodeGetChildNodes(parent);
|
||||
const ln = try parser.nodeListLength(children);
|
||||
var i: u32 = 0;
|
||||
while (i < ln) {
|
||||
defer i += 1;
|
||||
const c = try parser.nodeListItem(children, i) orelse continue;
|
||||
if (c == child) {
|
||||
return .{ parent, i };
|
||||
}
|
||||
}
|
||||
|
||||
// should not be possible to reach this point
|
||||
return error.InvalidNodeType;
|
||||
}
|
||||
|
||||
// implementation is largely copied from the WPT helper called getPosition in
|
||||
// the common.js of the dom folder.
|
||||
fn compare(node_a: *parser.Node, offset_a: u32, node_b: *parser.Node, offset_b: u32) !i32 {
|
||||
if (node_a == node_b) {
|
||||
// This is a simple and common case, where the two nodes are the same
|
||||
// We just need to compare their offsets
|
||||
if (offset_a == offset_b) {
|
||||
return 0;
|
||||
}
|
||||
return if (offset_a < offset_b) -1 else 1;
|
||||
}
|
||||
|
||||
// We're probably comparing two different nodes. "Probably", because the
|
||||
// above case on considered the offset if the two nodes were the same
|
||||
// as-is. They could still be the same here, if we first consider the
|
||||
// offset.
|
||||
const position = try Node._compareDocumentPosition(node_b, node_a);
|
||||
if (position & @intFromEnum(parser.DocumentPosition.disconnected) == @intFromEnum(parser.DocumentPosition.disconnected)) {
|
||||
return error.WrongDocument;
|
||||
}
|
||||
|
||||
if (position & @intFromEnum(parser.DocumentPosition.following) == @intFromEnum(parser.DocumentPosition.following)) {
|
||||
return switch (try compare(node_b, offset_b, node_a, offset_a)) {
|
||||
-1 => 1,
|
||||
1 => -1,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
if (position & @intFromEnum(parser.DocumentPosition.contains) == @intFromEnum(parser.DocumentPosition.contains)) {
|
||||
// node_a contains node_b
|
||||
var child = node_b;
|
||||
while (try parser.nodeParentNode(child)) |parent| {
|
||||
if (parent == node_a) {
|
||||
// child.parentNode == node_a
|
||||
break;
|
||||
}
|
||||
child = parent;
|
||||
} else {
|
||||
// this should not happen, because Node._compareDocumentPosition
|
||||
// has told us that node_a contains node_b, so one of node_b's
|
||||
// parent's MUST be node_a. But somehow we do end up here sometimes.
|
||||
return -1;
|
||||
}
|
||||
|
||||
const child_parent, const child_index = try getParentAndIndex(child);
|
||||
std.debug.assert(node_a == child_parent);
|
||||
return if (child_index < offset_a) -1 else 1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: Range" {
|
||||
try testing.htmlRunner("dom/range.html");
|
||||
}
|
||||
|
||||
@@ -17,7 +17,12 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const dump = @import("../dump.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
const Node = @import("node.zig").Node;
|
||||
const Element = @import("element.zig").Element;
|
||||
const ElementUnion = @import("element.zig").Union;
|
||||
|
||||
@@ -29,6 +34,7 @@ pub const ShadowRoot = struct {
|
||||
mode: Mode,
|
||||
host: *parser.Element,
|
||||
proto: *parser.DocumentFragment,
|
||||
adopted_style_sheets: ?Env.JsObject = null,
|
||||
|
||||
pub const Mode = enum {
|
||||
open,
|
||||
@@ -38,36 +44,58 @@ pub const ShadowRoot = struct {
|
||||
pub fn get_host(self: *const ShadowRoot) !ElementUnion {
|
||||
return Element.toInterface(self.host);
|
||||
}
|
||||
|
||||
pub fn get_adoptedStyleSheets(self: *ShadowRoot, page: *Page) !Env.JsObject {
|
||||
if (self.adopted_style_sheets) |obj| {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const obj = try page.main_context.newArray(0).persist();
|
||||
self.adopted_style_sheets = obj;
|
||||
return obj;
|
||||
}
|
||||
|
||||
pub fn set_adoptedStyleSheets(self: *ShadowRoot, sheets: Env.JsObject) !void {
|
||||
self.adopted_style_sheets = try sheets.persist();
|
||||
}
|
||||
|
||||
pub fn get_innerHTML(self: *ShadowRoot, page: *Page) ![]const u8 {
|
||||
var aw = std.Io.Writer.Allocating.init(page.call_arena);
|
||||
try dump.writeChildren(parser.documentFragmentToNode(self.proto), .{}, &aw.writer);
|
||||
return aw.written();
|
||||
}
|
||||
|
||||
pub fn set_innerHTML(self: *ShadowRoot, str_: ?[]const u8) !void {
|
||||
const sr_doc = parser.documentFragmentToNode(self.proto);
|
||||
const doc = try parser.nodeOwnerDocument(sr_doc) orelse return parser.DOMError.WrongDocument;
|
||||
try Node.removeChildren(sr_doc);
|
||||
const str = str_ orelse return;
|
||||
|
||||
const fragment = try parser.documentParseFragmentFromStr(doc, str);
|
||||
const fragment_node = parser.documentFragmentToNode(fragment);
|
||||
|
||||
// Element.set_innerHTML also has some weirdness here. It isn't clear
|
||||
// what should and shouldn't be set. Whatever string you pass to libdom,
|
||||
// it always creates a full HTML document, with an html, head and body
|
||||
// element.
|
||||
// For ShadowRoot, it appears the only the children within the body should
|
||||
// be set.
|
||||
const html = try parser.nodeFirstChild(fragment_node) orelse return;
|
||||
const head = try parser.nodeFirstChild(html) orelse return;
|
||||
const body = try parser.nodeNextSibling(head) orelse return;
|
||||
|
||||
const children = try parser.nodeGetChildNodes(body);
|
||||
const ln = try parser.nodeListLength(children);
|
||||
for (0..ln) |_| {
|
||||
// always index 0, because nodeAppendChild moves the node out of
|
||||
// the nodeList and into the new tree
|
||||
const child = try parser.nodeListItem(children, 0) orelse continue;
|
||||
_ = try parser.nodeAppendChild(sr_doc, child);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.ShadowRoot" {
|
||||
defer testing.reset();
|
||||
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "" });
|
||||
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'
|
||||
}, .{});
|
||||
test "Browser: DOM.ShadowRoot" {
|
||||
try testing.htmlRunner("dom/shadow_root.html");
|
||||
}
|
||||
|
||||
@@ -56,31 +56,7 @@ pub const Text = struct {
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.Text" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let t = new Text('foo')", "undefined" },
|
||||
.{ "t.data", "foo" },
|
||||
|
||||
.{ "let emptyt = new Text()", "undefined" },
|
||||
.{ "emptyt.data", "" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let text = document.getElementById('link').firstChild", "undefined" },
|
||||
.{ "text.wholeText === 'OK'", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "text.data = 'OK modified'", "OK modified" },
|
||||
.{ "let split = text.splitText('OK'.length)", "undefined" },
|
||||
.{ "split.data === ' modified'", "true" },
|
||||
.{ "text.data === 'OK'", "true" },
|
||||
}, .{});
|
||||
test "Browser: DOM.Text" {
|
||||
try testing.htmlRunner("dom/text.html");
|
||||
}
|
||||
|
||||
@@ -169,77 +169,7 @@ pub const Iterator = struct {
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.DOM.TokenList" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let gs = document.getElementById('para-empty')", "undefined" },
|
||||
.{ "let cl = gs.classList", "undefined" },
|
||||
.{ "gs.className", "ok empty" },
|
||||
.{ "cl.value", "ok empty" },
|
||||
.{ "cl.length", "2" },
|
||||
.{ "gs.className = 'foo bar baz'", "foo bar baz" },
|
||||
.{ "gs.className", "foo bar baz" },
|
||||
.{ "cl.length", "3" },
|
||||
.{ "gs.className = 'ok empty'", "ok empty" },
|
||||
.{ "cl.length", "2" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let cl2 = gs.classList", "undefined" },
|
||||
.{ "cl2.length", "2" },
|
||||
.{ "cl2.item(0)", "ok" },
|
||||
.{ "cl2.item(1)", "empty" },
|
||||
.{ "cl2.contains('ok')", "true" },
|
||||
.{ "cl2.contains('nok')", "false" },
|
||||
.{ "cl2.add('foo', 'bar', 'baz')", "undefined" },
|
||||
.{ "cl2.length", "5" },
|
||||
.{ "cl2.remove('foo', 'bar', 'baz')", "undefined" },
|
||||
.{ "cl2.length", "2" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let cl3 = gs.classList", "undefined" },
|
||||
.{ "cl3.toggle('ok')", "false" },
|
||||
.{ "cl3.toggle('ok')", "true" },
|
||||
.{ "cl3.length", "2" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let cl4 = gs.classList", "undefined" },
|
||||
.{ "cl4.replace('ok', 'nok')", "true" },
|
||||
.{ "cl4.value", "empty nok" },
|
||||
.{ "cl4.replace('nok', 'ok')", "true" },
|
||||
.{ "cl4.value", "empty ok" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let cl5 = gs.classList", "undefined" },
|
||||
.{ "let keys = [...cl5.keys()]", "undefined" },
|
||||
.{ "keys.length", "2" },
|
||||
.{ "keys[0]", "0" },
|
||||
.{ "keys[1]", "1" },
|
||||
|
||||
.{ "let values = [...cl5.values()]", "undefined" },
|
||||
.{ "values.length", "2" },
|
||||
.{ "values[0]", "empty" },
|
||||
.{ "values[1]", "ok" },
|
||||
|
||||
.{ "let entries = [...cl5.entries()]", "undefined" },
|
||||
.{ "entries.length", "2" },
|
||||
.{ "entries[0]", "0,empty" },
|
||||
.{ "entries[1]", "1,ok" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let cl6 = gs.classList", "undefined" },
|
||||
.{ "cl6.value = 'a b ccc'", "a b ccc" },
|
||||
.{ "cl6.value", "a b ccc" },
|
||||
.{ "cl6.toString()", "a b ccc" },
|
||||
}, .{});
|
||||
test "Browser: DOM.TokenList" {
|
||||
try testing.htmlRunner("dom/token_list.html");
|
||||
}
|
||||
|
||||
@@ -33,12 +33,17 @@ pub const TreeWalker = struct {
|
||||
filter: ?TreeWalkerOpts,
|
||||
filter_func: ?Env.Function,
|
||||
|
||||
// One of the few cases where null and undefined resolve to different default.
|
||||
// We need the raw JsObject so that we can probe the tri state:
|
||||
// null, undefined or i32.
|
||||
pub const WhatToShow = Env.JsObject;
|
||||
|
||||
pub const TreeWalkerOpts = union(enum) {
|
||||
function: Env.Function,
|
||||
object: struct { acceptNode: Env.Function },
|
||||
};
|
||||
|
||||
pub fn init(node: *parser.Node, what_to_show: ?u32, filter: ?TreeWalkerOpts) !TreeWalker {
|
||||
pub fn init(node: *parser.Node, what_to_show_: ?WhatToShow, filter: ?TreeWalkerOpts) !TreeWalker {
|
||||
var filter_func: ?Env.Function = null;
|
||||
|
||||
if (filter) |f| {
|
||||
@@ -48,10 +53,21 @@ pub const TreeWalker = struct {
|
||||
};
|
||||
}
|
||||
|
||||
var what_to_show: u32 = undefined;
|
||||
if (what_to_show_) |wts| {
|
||||
switch (try wts.triState(TreeWalker, "what_to_show", u32)) {
|
||||
.null => what_to_show = 0,
|
||||
.undefined => what_to_show = NodeFilter.NodeFilter._SHOW_ALL,
|
||||
.value => |v| what_to_show = v,
|
||||
}
|
||||
} else {
|
||||
what_to_show = NodeFilter.NodeFilter._SHOW_ALL;
|
||||
}
|
||||
|
||||
return .{
|
||||
.root = node,
|
||||
.current_node = node,
|
||||
.what_to_show = what_to_show orelse NodeFilter.NodeFilter._SHOW_ALL,
|
||||
.what_to_show = what_to_show,
|
||||
.filter = filter,
|
||||
.filter_func = filter_func,
|
||||
};
|
||||
|
||||
@@ -19,21 +19,25 @@
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("netsurf.zig");
|
||||
const Page = @import("page.zig").Page;
|
||||
const Walker = @import("dom/walker.zig").WalkerChildren;
|
||||
|
||||
pub const Opts = struct {
|
||||
// set to include element shadowroots in the dump
|
||||
page: ?*const Page = null,
|
||||
|
||||
exclude_scripts: bool = false,
|
||||
};
|
||||
|
||||
// writer must be a std.io.Writer
|
||||
pub fn writeHTML(doc: *parser.Document, opts: Opts, writer: anytype) !void {
|
||||
pub fn writeHTML(doc: *parser.Document, opts: Opts, writer: *std.Io.Writer) !void {
|
||||
try writer.writeAll("<!DOCTYPE html>\n");
|
||||
try writeChildren(parser.documentToNode(doc), opts, writer);
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
|
||||
// Spec: https://www.w3.org/TR/xml/#sec-prolog-dtd
|
||||
pub fn writeDocType(doc_type: *parser.DocumentType, writer: anytype) !void {
|
||||
pub fn writeDocType(doc_type: *parser.DocumentType, writer: *std.Io.Writer) !void {
|
||||
try writer.writeAll("<!DOCTYPE ");
|
||||
try writer.writeAll(try parser.documentTypeGetName(doc_type));
|
||||
|
||||
@@ -58,12 +62,12 @@ pub fn writeDocType(doc_type: *parser.DocumentType, writer: anytype) !void {
|
||||
try writer.writeAll(">");
|
||||
}
|
||||
|
||||
pub fn writeNode(node: *parser.Node, opts: Opts, writer: anytype) anyerror!void {
|
||||
pub fn writeNode(node: *parser.Node, opts: Opts, writer: *std.Io.Writer) 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) {
|
||||
const tag_type = try parser.nodeHTMLGetTagType(node) orelse .undef;
|
||||
if (opts.exclude_scripts and try isScriptOrRelated(tag_type, node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -88,6 +92,14 @@ pub fn writeNode(node: *parser.Node, opts: Opts, writer: anytype) anyerror!void
|
||||
|
||||
try writer.writeAll(">");
|
||||
|
||||
if (opts.page) |page| {
|
||||
if (page.getNodeState(node)) |state| {
|
||||
if (state.shadow_root) |sr| {
|
||||
try writeChildren(@ptrCast(@alignCast(sr.proto)), opts, writer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// void elements can't have any content.
|
||||
if (try isVoid(parser.nodeToElement(node))) return;
|
||||
|
||||
@@ -138,7 +150,7 @@ pub fn writeNode(node: *parser.Node, opts: Opts, writer: anytype) anyerror!void
|
||||
}
|
||||
|
||||
// writer must be a std.io.Writer
|
||||
pub fn writeChildren(root: *parser.Node, opts: Opts, writer: anytype) !void {
|
||||
pub fn writeChildren(root: *parser.Node, opts: Opts, writer: *std.Io.Writer) !void {
|
||||
const walker = Walker{};
|
||||
var next: ?*parser.Node = null;
|
||||
while (true) {
|
||||
@@ -147,10 +159,29 @@ pub fn writeChildren(root: *parser.Node, opts: Opts, writer: anytype) !void {
|
||||
}
|
||||
}
|
||||
|
||||
// When `exclude_scripts` is passed to dump, we don't include <script> tags.
|
||||
// We also want to omit <link rel=preload as=ascript>
|
||||
fn isScriptOrRelated(tag_type: parser.Tag, node: *parser.Node) !bool {
|
||||
if (tag_type == .script) {
|
||||
return true;
|
||||
}
|
||||
if (tag_type == .link) {
|
||||
const el = parser.nodeToElement(node);
|
||||
const as = try parser.elementGetAttribute(el, "as") orelse return false;
|
||||
if (!std.ascii.eqlIgnoreCase(as, "script")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rel = try parser.elementGetAttribute(el, "rel") orelse return false;
|
||||
return std.ascii.eqlIgnoreCase(rel, "preload");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr
|
||||
// https://html.spec.whatwg.org/#void-elements
|
||||
fn isVoid(elem: *parser.Element) !bool {
|
||||
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(elem)));
|
||||
const tag = try parser.elementTag(elem);
|
||||
return switch (tag) {
|
||||
.area, .base, .br, .col, .embed, .hr, .img, .input, .link => true,
|
||||
.meta, .source, .track, .wbr => true,
|
||||
@@ -240,13 +271,13 @@ fn testWriteHTML(comptime expected_body: []const u8, src: []const u8) !void {
|
||||
}
|
||||
|
||||
fn testWriteFullHTML(comptime expected: []const u8, src: []const u8) !void {
|
||||
var buf = std.ArrayListUnmanaged(u8){};
|
||||
defer buf.deinit(testing.allocator);
|
||||
var aw = std.Io.Writer.Allocating.init(testing.allocator);
|
||||
defer aw.deinit();
|
||||
|
||||
const doc_html = try parser.documentHTMLParseFromStr(src);
|
||||
defer parser.documentHTMLClose(doc_html) catch {};
|
||||
|
||||
const doc = parser.documentHTMLToDocument(doc_html);
|
||||
try writeHTML(doc, .{}, buf.writer(testing.allocator));
|
||||
try testing.expectEqualStrings(expected, buf.items);
|
||||
try writeHTML(doc, .{}, &aw.writer);
|
||||
try testing.expectEqualStrings(expected, aw.written());
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ const std = @import("std");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
// https://encoding.spec.whatwg.org/#interface-textdecoder
|
||||
const TextDecoder = @This();
|
||||
@@ -37,6 +38,7 @@ const Options = struct {
|
||||
|
||||
fatal: bool,
|
||||
ignore_bom: bool,
|
||||
stream: std.ArrayList(u8),
|
||||
|
||||
pub fn constructor(label_: ?[]const u8, opts_: ?Options) !TextDecoder {
|
||||
if (label_) |l| {
|
||||
@@ -47,6 +49,7 @@ pub fn constructor(label_: ?[]const u8, opts_: ?Options) !TextDecoder {
|
||||
}
|
||||
const opts = opts_ orelse Options{};
|
||||
return .{
|
||||
.stream = .empty,
|
||||
.fatal = opts.fatal,
|
||||
.ignore_bom = opts.ignoreBOM,
|
||||
};
|
||||
@@ -64,44 +67,37 @@ 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)) {
|
||||
const DecodeOptions = struct {
|
||||
stream: bool = false,
|
||||
};
|
||||
pub fn _decode(self: *TextDecoder, input_: ?[]const u8, opts_: ?DecodeOptions, page: *Page) ![]const u8 {
|
||||
var str = input_ orelse return "";
|
||||
const opts: DecodeOptions = opts_ orelse .{};
|
||||
|
||||
if (self.stream.items.len > 0) {
|
||||
try self.stream.appendSlice(page.arena, str);
|
||||
str = self.stream.items;
|
||||
}
|
||||
|
||||
if (self.fatal and !std.unicode.utf8ValidateSlice(str)) {
|
||||
if (opts.stream) {
|
||||
if (self.stream.items.len == 0) {
|
||||
try self.stream.appendSlice(page.arena, str);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return error.InvalidUtf8;
|
||||
}
|
||||
|
||||
if (self.ignore_bom == false and std.mem.startsWith(u8, v, &.{ 0xEF, 0xBB, 0xBF })) {
|
||||
return v[3..];
|
||||
self.stream.clearRetainingCapacity();
|
||||
if (self.ignore_bom == false and std.mem.startsWith(u8, str, &.{ 0xEF, 0xBB, 0xBF })) {
|
||||
return str[3..];
|
||||
}
|
||||
|
||||
return v;
|
||||
return str;
|
||||
}
|
||||
|
||||
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",
|
||||
},
|
||||
}, .{});
|
||||
test "Browser: Encoding.TextDecoder" {
|
||||
try testing.htmlRunner("encoding/decoder.html");
|
||||
}
|
||||
|
||||
@@ -43,20 +43,6 @@ pub fn _encode(_: *const TextEncoder, v: []const u8) !Env.TypedArray(u8) {
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.Encoding.TextEncoder" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{
|
||||
.html = "",
|
||||
});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var encoder = new TextEncoder();", null },
|
||||
.{ "encoder.encoding;", "utf-8" },
|
||||
.{ "encoder.encode('€');", "226,130,172" },
|
||||
|
||||
// Invalid utf-8 sequence.
|
||||
// Result with chrome:
|
||||
// .{ "encoder.encode(new Uint8Array([0xE2,0x28,0xA1]))", "50,50,54,44,52,48,44,49,54,49" },
|
||||
.{ "try {encoder.encode(new Uint8Array([0xE2,0x28,0xA1])) } catch (e) { e };", "Error: InvalidUtf8" },
|
||||
}, .{});
|
||||
test "Browser: Encoding.TextEncoder" {
|
||||
try testing.htmlRunner("encoding/encoder.html");
|
||||
}
|
||||
|
||||
@@ -34,7 +34,10 @@ const WebApis = struct {
|
||||
@import("url/url.zig").Interfaces,
|
||||
@import("xhr/xhr.zig").Interfaces,
|
||||
@import("xhr/form_data.zig").Interfaces,
|
||||
@import("xhr/File.zig"),
|
||||
@import("xmlserializer/xmlserializer.zig").Interfaces,
|
||||
@import("fetch/fetch.zig").Interfaces,
|
||||
@import("streams/streams.zig").Interfaces,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Event = @import("event.zig").Event;
|
||||
const JsObject = @import("../env.zig").JsObject;
|
||||
const netsurf = @import("../netsurf.zig");
|
||||
|
||||
// https://dom.spec.whatwg.org/#interface-customevent
|
||||
pub const CustomEvent = struct {
|
||||
@@ -55,26 +56,30 @@ pub const CustomEvent = struct {
|
||||
pub fn get_detail(self: *CustomEvent) ?JsObject {
|
||||
return self.detail;
|
||||
}
|
||||
|
||||
// Initializes an already created `CustomEvent`.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/initCustomEvent
|
||||
pub fn _initCustomEvent(
|
||||
self: *CustomEvent,
|
||||
event_type: []const u8,
|
||||
can_bubble: bool,
|
||||
cancelable: bool,
|
||||
maybe_detail: ?JsObject,
|
||||
) !void {
|
||||
// This function can only be called after the constructor has called.
|
||||
// So we assume proto is initialized already by constructor.
|
||||
self.proto.type = try netsurf.strFromData(event_type);
|
||||
self.proto.bubble = can_bubble;
|
||||
self.proto.cancelable = cancelable;
|
||||
self.proto.is_initialised = true;
|
||||
// Detail is stored separately.
|
||||
if (maybe_detail) |detail| {
|
||||
self.detail = try detail.persist();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.CustomEvent" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let capture = null", "undefined" },
|
||||
.{ "const el = document.createElement('div');", "undefined" },
|
||||
.{ "el.addEventListener('c1', (e) => { capture = 'c1-' + new String(e.detail)})", "undefined" },
|
||||
.{ "el.addEventListener('c2', (e) => { capture = 'c2-' + new String(e.detail.over)})", "undefined" },
|
||||
|
||||
.{ "el.dispatchEvent(new CustomEvent('c1'));", "true" },
|
||||
.{ "capture", "c1-null" },
|
||||
|
||||
.{ "el.dispatchEvent(new CustomEvent('c1', {detail: '123'}));", "true" },
|
||||
.{ "capture", "c1-123" },
|
||||
|
||||
.{ "el.dispatchEvent(new CustomEvent('c2', {detail: {over: 9000}}));", "true" },
|
||||
.{ "capture", "c2-9000" },
|
||||
}, .{});
|
||||
test "Browser: Events.Custom" {
|
||||
try testing.htmlRunner("events/custom.html");
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ const parser = @import("../netsurf.zig");
|
||||
const generate = @import("../../runtime/generate.zig");
|
||||
|
||||
const Page = @import("../page.zig").Page;
|
||||
const Node = @import("../dom/node.zig").Node;
|
||||
const DOMException = @import("../dom/exceptions.zig").DOMException;
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const EventTargetUnion = @import("../dom/event_target.zig").Union;
|
||||
@@ -32,11 +33,20 @@ 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 KeyboardEvent = @import("keyboard_event.zig").KeyboardEvent;
|
||||
const ErrorEvent = @import("../html/error_event.zig").ErrorEvent;
|
||||
const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
|
||||
|
||||
// Event interfaces
|
||||
pub const Interfaces = .{ Event, CustomEvent, ProgressEvent, MouseEvent, ErrorEvent, MessageEvent };
|
||||
pub const Interfaces = .{
|
||||
Event,
|
||||
CustomEvent,
|
||||
ProgressEvent,
|
||||
MouseEvent,
|
||||
KeyboardEvent,
|
||||
ErrorEvent,
|
||||
MessageEvent,
|
||||
};
|
||||
|
||||
pub const Union = generate.Union(Interfaces);
|
||||
|
||||
@@ -54,14 +64,15 @@ pub const Event = struct {
|
||||
pub const _AT_TARGET = 2;
|
||||
pub const _BUBBLING_PHASE = 3;
|
||||
|
||||
pub fn toInterface(evt: *parser.Event) !Union {
|
||||
return switch (try parser.eventGetInternalType(evt)) {
|
||||
pub fn toInterface(evt: *parser.Event) Union {
|
||||
return switch (parser.eventGetInternalType(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)).* },
|
||||
.keyboard_event => .{ .KeyboardEvent = @as(*parser.KeyboardEvent, @ptrCast(evt)) },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -78,39 +89,42 @@ pub const Event = struct {
|
||||
}
|
||||
|
||||
pub fn get_target(self: *parser.Event, page: *Page) !?EventTargetUnion {
|
||||
const et = try parser.eventTarget(self);
|
||||
const et = parser.eventTarget(self);
|
||||
if (et == null) return null;
|
||||
return try EventTarget.toInterface(et.?, page);
|
||||
}
|
||||
|
||||
pub fn get_currentTarget(self: *parser.Event, page: *Page) !?EventTargetUnion {
|
||||
const et = try parser.eventCurrentTarget(self);
|
||||
const et = parser.eventCurrentTarget(self);
|
||||
if (et == null) return null;
|
||||
return try EventTarget.toInterface(et.?, page);
|
||||
}
|
||||
|
||||
pub fn get_eventPhase(self: *parser.Event) !u8 {
|
||||
return try parser.eventPhase(self);
|
||||
pub fn get_eventPhase(self: *parser.Event) u8 {
|
||||
return parser.eventPhase(self);
|
||||
}
|
||||
|
||||
pub fn get_bubbles(self: *parser.Event) !bool {
|
||||
return try parser.eventBubbles(self);
|
||||
pub fn get_bubbles(self: *parser.Event) bool {
|
||||
return parser.eventBubbles(self);
|
||||
}
|
||||
|
||||
pub fn get_cancelable(self: *parser.Event) !bool {
|
||||
return try parser.eventCancelable(self);
|
||||
pub fn get_cancelable(self: *parser.Event) bool {
|
||||
return parser.eventCancelable(self);
|
||||
}
|
||||
|
||||
pub fn get_defaultPrevented(self: *parser.Event) !bool {
|
||||
return try parser.eventDefaultPrevented(self);
|
||||
pub fn get_defaultPrevented(self: *parser.Event) bool {
|
||||
return parser.eventDefaultPrevented(self);
|
||||
}
|
||||
|
||||
pub fn get_isTrusted(self: *parser.Event) !bool {
|
||||
return try parser.eventIsTrusted(self);
|
||||
pub fn get_isTrusted(self: *parser.Event) bool {
|
||||
return parser.eventIsTrusted(self);
|
||||
}
|
||||
|
||||
pub fn get_timestamp(self: *parser.Event) !u32 {
|
||||
return try parser.eventTimestamp(self);
|
||||
// Even though this is supposed to to provide microsecond resolution, browser
|
||||
// return coarser values to protect against fingerprinting. libdom returns
|
||||
// seconds, which is good enough.
|
||||
pub fn get_timeStamp(self: *parser.Event) u64 {
|
||||
return parser.eventTimestamp(self);
|
||||
}
|
||||
|
||||
// Methods
|
||||
@@ -129,15 +143,73 @@ pub const Event = struct {
|
||||
}
|
||||
|
||||
pub fn _stopPropagation(self: *parser.Event) !void {
|
||||
return try parser.eventStopPropagation(self);
|
||||
return parser.eventStopPropagation(self);
|
||||
}
|
||||
|
||||
pub fn _stopImmediatePropagation(self: *parser.Event) !void {
|
||||
return try parser.eventStopImmediatePropagation(self);
|
||||
return parser.eventStopImmediatePropagation(self);
|
||||
}
|
||||
|
||||
pub fn _preventDefault(self: *parser.Event) !void {
|
||||
return try parser.eventPreventDefault(self);
|
||||
return parser.eventPreventDefault(self);
|
||||
}
|
||||
|
||||
pub fn _composedPath(self: *parser.Event, page: *Page) ![]const EventTargetUnion {
|
||||
const et_ = parser.eventTarget(self);
|
||||
const et = et_ orelse return &.{};
|
||||
|
||||
var node: ?*parser.Node = switch (try parser.eventTargetInternalType(et)) {
|
||||
.libdom_node => @as(*parser.Node, @ptrCast(et)),
|
||||
.plain => parser.eventTargetToNode(et),
|
||||
else => {
|
||||
// Window, XHR, MessagePort, etc...no path beyond the event itself
|
||||
return &.{try EventTarget.toInterface(et, page)};
|
||||
},
|
||||
};
|
||||
|
||||
const arena = page.call_arena;
|
||||
var path: std.ArrayListUnmanaged(EventTargetUnion) = .empty;
|
||||
while (node) |n| {
|
||||
try path.append(arena, .{
|
||||
.node = try Node.toInterface(n),
|
||||
});
|
||||
|
||||
node = try parser.nodeParentNode(n);
|
||||
if (node == null and try parser.nodeType(n) == .document_fragment) {
|
||||
// we have a non-continuous hook from a shadowroot to its host (
|
||||
// it's parent element). libdom doesn't really support ShdowRoots
|
||||
// and, for the most part, that works out well since it naturally
|
||||
// provides isolation. But events don't follow the same
|
||||
// shadowroot isolation as most other things, so, if this is
|
||||
// a parent-less document fragment, we need to check if it has
|
||||
// a host.
|
||||
if (parser.documentFragmentGetHost(@ptrCast(n))) |host| {
|
||||
node = host;
|
||||
|
||||
// If a document fragment has a host, then that host
|
||||
// _has_ to have a state and that state _has_ to have
|
||||
// a shadow_root field. All of this is set in Element._attachShadow
|
||||
if (page.getNodeState(host).?.shadow_root.?.mode == .closed) {
|
||||
// if the shadow root is closed, then the composedPath
|
||||
// starts at the host element.
|
||||
path.clearRetainingCapacity();
|
||||
}
|
||||
} else {
|
||||
// Our document fragement has no parent and no host, we
|
||||
// can break out of the loop.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (path.getLastOrNull()) |last| {
|
||||
// the Window isn't part of the DOM hierarchy, but for events, it
|
||||
// is, so we need to glue it on.
|
||||
if (last.node == .HTMLDocument and last.node.HTMLDocument == page.window.document) {
|
||||
try path.append(arena, .{ .node = .{ .Window = &page.window } });
|
||||
}
|
||||
}
|
||||
return path.items;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -203,7 +275,6 @@ pub const EventHandler = struct {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const callback = (try listener.callback(target)) orelse return null;
|
||||
|
||||
if (signal) |s| {
|
||||
@@ -255,11 +326,7 @@ pub const EventHandler = struct {
|
||||
}
|
||||
|
||||
fn handle(node: *parser.EventNode, event: *parser.Event) void {
|
||||
const ievent = Event.toInterface(event) catch |err| {
|
||||
log.err(.app, "toInterface error", .{ .err = err });
|
||||
return;
|
||||
};
|
||||
|
||||
const ievent = Event.toInterface(event);
|
||||
const self: *EventHandler = @fieldParentPtr("node", node);
|
||||
var result: Function.Result = undefined;
|
||||
self.callback.tryCall(void, .{ievent}, &result) catch {
|
||||
@@ -271,7 +338,7 @@ pub const EventHandler = struct {
|
||||
};
|
||||
|
||||
if (self.once) {
|
||||
const target = (parser.eventTarget(event) catch return).?;
|
||||
const target = parser.eventTarget(event).?;
|
||||
const typ = parser.eventType(event) catch return;
|
||||
parser.eventTargetRemoveEventListener(
|
||||
target,
|
||||
@@ -328,122 +395,6 @@ const SignalCallback = struct {
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.Event" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let content = document.getElementById('content')", "undefined" },
|
||||
.{ "let para = document.getElementById('para')", "undefined" },
|
||||
.{ "var nb = 0; var evt", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ content.addEventListener('target', function(e) {
|
||||
\\ evt = e; nb = nb + 1;
|
||||
\\ e.preventDefault();
|
||||
\\ })
|
||||
,
|
||||
"undefined",
|
||||
},
|
||||
.{ "content.dispatchEvent(new Event('target', {bubbles: true, cancelable: true}))", "false" },
|
||||
.{ "nb", "1" },
|
||||
.{ "evt.target === content", "true" },
|
||||
.{ "evt.bubbles", "true" },
|
||||
.{ "evt.cancelable", "true" },
|
||||
.{ "evt.defaultPrevented", "true" },
|
||||
.{ "evt.isTrusted", "true" },
|
||||
.{ "evt.timestamp > 1704063600", "true" }, // 2024/01/01 00:00
|
||||
// event.type, event.currentTarget, event.phase checked in EventTarget
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0", "0" },
|
||||
.{
|
||||
\\ content.addEventListener('stop',function(e) {
|
||||
\\ e.stopPropagation();
|
||||
\\ nb = nb + 1;
|
||||
\\ }, true)
|
||||
,
|
||||
"undefined",
|
||||
},
|
||||
// the following event listener will not be invoked
|
||||
.{
|
||||
\\ para.addEventListener('stop',function(e) {
|
||||
\\ nb = nb + 1;
|
||||
\\ })
|
||||
,
|
||||
"undefined",
|
||||
},
|
||||
.{ "para.dispatchEvent(new Event('stop'))", "true" },
|
||||
.{ "nb", "1" }, // will be 2 if event was not stopped at content event listener
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0", "0" },
|
||||
.{
|
||||
\\ content.addEventListener('immediate', function(e) {
|
||||
\\ e.stopImmediatePropagation();
|
||||
\\ nb = nb + 1;
|
||||
\\ })
|
||||
,
|
||||
"undefined",
|
||||
},
|
||||
// the following event listener will not be invoked
|
||||
.{
|
||||
\\ content.addEventListener('immediate', function(e) {
|
||||
\\ nb = nb + 1;
|
||||
\\ })
|
||||
,
|
||||
"undefined",
|
||||
},
|
||||
.{ "content.dispatchEvent(new Event('immediate'))", "true" },
|
||||
.{ "nb", "1" }, // will be 2 if event was not stopped at first content event listener
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0", "0" },
|
||||
.{
|
||||
\\ content.addEventListener('legacy', function(e) {
|
||||
\\ evt = e; nb = nb + 1;
|
||||
\\ })
|
||||
,
|
||||
"undefined",
|
||||
},
|
||||
.{ "let evtLegacy = document.createEvent('Event')", "undefined" },
|
||||
.{ "evtLegacy.initEvent('legacy')", "undefined" },
|
||||
.{ "content.dispatchEvent(evtLegacy)", "true" },
|
||||
.{ "nb", "1" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var nb = 0; var evt = null; function cbk(event) { nb ++; evt=event; }", "undefined" },
|
||||
.{ "document.addEventListener('count', cbk)", "undefined" },
|
||||
.{ "document.removeEventListener('count', cbk)", "undefined" },
|
||||
.{ "document.dispatchEvent(new Event('count'))", "true" },
|
||||
.{ "nb", "0" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "nb = 0; function cbk(event) { nb ++; }", null },
|
||||
.{ "document.addEventListener('count', cbk, {once: true})", null },
|
||||
.{ "document.dispatchEvent(new Event('count'))", "true" },
|
||||
.{ "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" },
|
||||
}, .{});
|
||||
test "Browser: Event" {
|
||||
try testing.htmlRunner("events/event.html");
|
||||
}
|
||||
|
||||
160
src/browser/events/keyboard_event.zig
Normal file
160
src/browser/events/keyboard_event.zig
Normal file
@@ -0,0 +1,160 @@
|
||||
// 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 builtin = @import("builtin");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Event = @import("event.zig").Event;
|
||||
const JsObject = @import("../env.zig").JsObject;
|
||||
|
||||
// TODO: We currently don't have a UIEvent interface so we skip it in the prototype chain.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent
|
||||
const UIEvent = Event;
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
|
||||
pub const KeyboardEvent = struct {
|
||||
pub const Self = parser.KeyboardEvent;
|
||||
pub const prototype = *UIEvent;
|
||||
|
||||
pub const ConstructorOptions = struct {
|
||||
key: []const u8 = "",
|
||||
code: []const u8 = "",
|
||||
location: parser.KeyboardEventOpts.LocationCode = .standard,
|
||||
repeat: bool = false,
|
||||
isComposing: bool = false,
|
||||
// Currently not supported but we take as argument.
|
||||
charCode: u32 = 0,
|
||||
// Currently not supported but we take as argument.
|
||||
keyCode: u32 = 0,
|
||||
// Currently not supported but we take as argument.
|
||||
which: u32 = 0,
|
||||
ctrlKey: bool = false,
|
||||
shiftKey: bool = false,
|
||||
altKey: bool = false,
|
||||
metaKey: bool = false,
|
||||
};
|
||||
|
||||
pub fn constructor(event_type: []const u8, maybe_options: ?ConstructorOptions) !*parser.KeyboardEvent {
|
||||
const options: ConstructorOptions = maybe_options orelse .{};
|
||||
|
||||
var event = try parser.keyboardEventCreate();
|
||||
parser.eventSetInternalType(@ptrCast(&event), .keyboard_event);
|
||||
|
||||
try parser.keyboardEventInit(
|
||||
event,
|
||||
event_type,
|
||||
.{
|
||||
.key = options.key,
|
||||
.code = options.code,
|
||||
.location = options.location,
|
||||
.repeat = options.repeat,
|
||||
.is_composing = options.isComposing,
|
||||
.ctrl_key = options.ctrlKey,
|
||||
.shift_key = options.shiftKey,
|
||||
.alt_key = options.altKey,
|
||||
.meta_key = options.metaKey,
|
||||
},
|
||||
);
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
// Returns the modifier state for given modifier key.
|
||||
pub fn _getModifierState(self: *Self, key: []const u8) bool {
|
||||
// Chrome and Firefox do case-sensitive match, here we prefer the same.
|
||||
if (std.mem.eql(u8, key, "Alt")) {
|
||||
return get_altKey(self);
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, key, "AltGraph")) {
|
||||
return (get_altKey(self) and get_ctrlKey(self));
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, key, "Control")) {
|
||||
return get_ctrlKey(self);
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, key, "Shift")) {
|
||||
return get_shiftKey(self);
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, key, "Meta") or std.mem.eql(u8, key, "OS")) {
|
||||
return get_metaKey(self);
|
||||
}
|
||||
|
||||
// Special case for IE.
|
||||
if (comptime builtin.os.tag == .windows) {
|
||||
if (std.mem.eql(u8, key, "Win")) {
|
||||
return get_metaKey(self);
|
||||
}
|
||||
}
|
||||
|
||||
// getModifierState() also accepts a deprecated virtual modifier named "Accel".
|
||||
// event.getModifierState("Accel") returns true when at least one of
|
||||
// KeyboardEvent.ctrlKey or KeyboardEvent.metaKey is true.
|
||||
//
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState#accel_virtual_modifier
|
||||
if (std.mem.eql(u8, key, "Accel")) {
|
||||
return (get_ctrlKey(self) or get_metaKey(self));
|
||||
}
|
||||
|
||||
// TODO: Add support for "CapsLock", "ScrollLock".
|
||||
return false;
|
||||
}
|
||||
|
||||
// Getters.
|
||||
|
||||
pub fn get_altKey(self: *Self) bool {
|
||||
return parser.keyboardEventKeyIsSet(self, .alt);
|
||||
}
|
||||
|
||||
pub fn get_ctrlKey(self: *Self) bool {
|
||||
return parser.keyboardEventKeyIsSet(self, .ctrl);
|
||||
}
|
||||
|
||||
pub fn get_metaKey(self: *Self) bool {
|
||||
return parser.keyboardEventKeyIsSet(self, .meta);
|
||||
}
|
||||
|
||||
pub fn get_shiftKey(self: *Self) bool {
|
||||
return parser.keyboardEventKeyIsSet(self, .shift);
|
||||
}
|
||||
|
||||
pub fn get_isComposing(self: *Self) bool {
|
||||
return self.is_composing;
|
||||
}
|
||||
|
||||
pub fn get_location(self: *Self) u32 {
|
||||
return self.location;
|
||||
}
|
||||
|
||||
pub fn get_key(self: *Self) ![]const u8 {
|
||||
return parser.keyboardEventGetKey(self);
|
||||
}
|
||||
|
||||
pub fn get_repeat(self: *Self) bool {
|
||||
return self.repeat;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser: Events.Keyboard" {
|
||||
try testing.htmlRunner("events/keyboard.html");
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const log = std.log.scoped(.mouse_event);
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Event = @import("event.zig").Event;
|
||||
@@ -56,7 +56,7 @@ pub const MouseEvent = struct {
|
||||
const opts = opts_ orelse MouseEventInit{};
|
||||
|
||||
var mouse_event = try parser.mouseEventCreate();
|
||||
try parser.eventSetInternalType(@ptrCast(&mouse_event), .mouse_event);
|
||||
parser.eventSetInternalType(@ptrCast(&mouse_event), .mouse_event);
|
||||
|
||||
try parser.mouseEventInit(mouse_event, event_type, .{
|
||||
.x = opts.clientX,
|
||||
@@ -69,7 +69,7 @@ pub const MouseEvent = struct {
|
||||
});
|
||||
|
||||
if (!std.mem.eql(u8, event_type, "click")) {
|
||||
log.warn("MouseEvent currently only supports listeners for 'click' events!", .{});
|
||||
log.warn(.mouse_event, "unsupported mouse event", .{ .event = event_type });
|
||||
}
|
||||
|
||||
return mouse_event;
|
||||
@@ -107,34 +107,6 @@ pub const MouseEvent = struct {
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.MouseEvent" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
// Default MouseEvent
|
||||
.{ "let event = new MouseEvent('click')", "undefined" },
|
||||
.{ "event.type", "click" },
|
||||
.{ "event instanceof MouseEvent", "true" },
|
||||
.{ "event instanceof Event", "true" },
|
||||
.{ "event.clientX", "0" },
|
||||
.{ "event.clientY", "0" },
|
||||
.{ "event.screenX", "0" },
|
||||
.{ "event.screenY", "0" },
|
||||
// MouseEvent with parameters
|
||||
.{ "let new_event = new MouseEvent('click', { 'button': 0, 'clientX': 10, 'clientY': 20 })", "undefined" },
|
||||
.{ "new_event.button", "0" },
|
||||
.{ "new_event.x", "10" },
|
||||
.{ "new_event.y", "20" },
|
||||
.{ "new_event.screenX", "10" },
|
||||
.{ "new_event.screenY", "20" },
|
||||
// MouseEvent Listener
|
||||
.{ "let me = new MouseEvent('click')", "undefined" },
|
||||
.{ "me instanceof Event", "true" },
|
||||
.{ "var eevt = null; function ccbk(event) { eevt = event; }", "undefined" },
|
||||
.{ "document.addEventListener('click', ccbk)", "undefined" },
|
||||
.{ "document.dispatchEvent(me)", "true" },
|
||||
.{ "eevt.type", "click" },
|
||||
.{ "eevt instanceof MouseEvent", "true" },
|
||||
}, .{});
|
||||
test "Browser: Events.Mouse" {
|
||||
try testing.htmlRunner("events/mouse.html");
|
||||
}
|
||||
|
||||
227
src/browser/fetch/Headers.zig
Normal file
227
src/browser/fetch/Headers.zig
Normal file
@@ -0,0 +1,227 @@
|
||||
// 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 URL = @import("../../url.zig").URL;
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
const iterator = @import("../iterator/iterator.zig");
|
||||
|
||||
const v8 = @import("v8");
|
||||
const Env = @import("../env.zig").Env;
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Headers
|
||||
const Headers = @This();
|
||||
|
||||
// Case-Insensitive String HashMap.
|
||||
// This allows us to avoid having to allocate lowercase keys all the time.
|
||||
const HeaderHashMap = std.HashMapUnmanaged([]const u8, []const u8, struct {
|
||||
pub fn hash(_: @This(), s: []const u8) u64 {
|
||||
var buf: [64]u8 = undefined;
|
||||
var hasher = std.hash.Wyhash.init(s.len);
|
||||
|
||||
var key = s;
|
||||
while (key.len >= 64) {
|
||||
const lower = std.ascii.lowerString(buf[0..], key[0..64]);
|
||||
hasher.update(lower);
|
||||
key = key[64..];
|
||||
}
|
||||
|
||||
if (key.len > 0) {
|
||||
const lower = std.ascii.lowerString(buf[0..key.len], key);
|
||||
hasher.update(lower);
|
||||
}
|
||||
|
||||
return hasher.final();
|
||||
}
|
||||
|
||||
pub fn eql(_: @This(), a: []const u8, b: []const u8) bool {
|
||||
return std.ascii.eqlIgnoreCase(a, b);
|
||||
}
|
||||
}, 80);
|
||||
|
||||
headers: HeaderHashMap = .empty,
|
||||
|
||||
// They can either be:
|
||||
//
|
||||
// 1. An array of string pairs.
|
||||
// 2. An object with string keys to string values.
|
||||
// 3. Another Headers object.
|
||||
pub const HeadersInit = union(enum) {
|
||||
// List of Pairs of []const u8
|
||||
strings: []const [2][]const u8,
|
||||
// Headers
|
||||
headers: *Headers,
|
||||
// Mappings
|
||||
object: Env.JsObject,
|
||||
};
|
||||
|
||||
pub fn constructor(_init: ?HeadersInit, page: *Page) !Headers {
|
||||
const arena = page.arena;
|
||||
var headers: HeaderHashMap = .empty;
|
||||
|
||||
if (_init) |init| {
|
||||
switch (init) {
|
||||
.strings => |kvs| {
|
||||
for (kvs) |pair| {
|
||||
const key = try arena.dupe(u8, pair[0]);
|
||||
const value = try arena.dupe(u8, pair[1]);
|
||||
|
||||
try headers.put(arena, key, value);
|
||||
}
|
||||
},
|
||||
.headers => |hdrs| {
|
||||
var iter = hdrs.headers.iterator();
|
||||
while (iter.next()) |entry| {
|
||||
try headers.put(arena, entry.key_ptr.*, entry.value_ptr.*);
|
||||
}
|
||||
},
|
||||
.object => |obj| {
|
||||
var iter = obj.nameIterator();
|
||||
while (try iter.next()) |name_value| {
|
||||
const name = try name_value.toString(arena);
|
||||
const value = try obj.get(name);
|
||||
const value_string = try value.toString(arena);
|
||||
|
||||
try headers.put(arena, name, value_string);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return .{
|
||||
.headers = headers,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn append(self: *Headers, name: []const u8, value: []const u8, allocator: std.mem.Allocator) !void {
|
||||
const key = try allocator.dupe(u8, name);
|
||||
const gop = try self.headers.getOrPut(allocator, key);
|
||||
|
||||
if (gop.found_existing) {
|
||||
// If we found it, append the value.
|
||||
const new_value = try std.fmt.allocPrint(allocator, "{s}, {s}", .{ gop.value_ptr.*, value });
|
||||
gop.value_ptr.* = new_value;
|
||||
} else {
|
||||
// Otherwise, we should just put it in.
|
||||
gop.value_ptr.* = try allocator.dupe(u8, value);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _append(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void {
|
||||
const arena = page.arena;
|
||||
try self.append(name, value, arena);
|
||||
}
|
||||
|
||||
pub fn _delete(self: *Headers, name: []const u8) void {
|
||||
_ = self.headers.remove(name);
|
||||
}
|
||||
|
||||
pub const HeadersEntryIterator = struct {
|
||||
slot: [2][]const u8,
|
||||
iter: HeaderHashMap.Iterator,
|
||||
|
||||
// TODO: these SHOULD be in lexigraphical order but I'm not sure how actually
|
||||
// important that is.
|
||||
pub fn _next(self: *HeadersEntryIterator) ?[2][]const u8 {
|
||||
if (self.iter.next()) |entry| {
|
||||
self.slot[0] = entry.key_ptr.*;
|
||||
self.slot[1] = entry.value_ptr.*;
|
||||
return self.slot;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn _entries(self: *const Headers) HeadersEntryIterable {
|
||||
return .{
|
||||
.inner = .{
|
||||
.slot = undefined,
|
||||
.iter = self.headers.iterator(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _forEach(self: *Headers, callback_fn: Env.Function, this_arg: ?Env.JsObject) !void {
|
||||
var iter = self.headers.iterator();
|
||||
|
||||
const cb = if (this_arg) |this| try callback_fn.withThis(this) else callback_fn;
|
||||
|
||||
while (iter.next()) |entry| {
|
||||
try cb.call(void, .{ entry.key_ptr.*, entry.value_ptr.*, self });
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _get(self: *const Headers, name: []const u8) ?[]const u8 {
|
||||
return self.headers.get(name);
|
||||
}
|
||||
|
||||
pub fn _has(self: *const Headers, name: []const u8) bool {
|
||||
return self.headers.contains(name);
|
||||
}
|
||||
|
||||
pub const HeadersKeyIterator = struct {
|
||||
iter: HeaderHashMap.KeyIterator,
|
||||
|
||||
pub fn _next(self: *HeadersKeyIterator) ?[]const u8 {
|
||||
if (self.iter.next()) |key| {
|
||||
return key.*;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn _keys(self: *const Headers) HeadersKeyIterable {
|
||||
return .{ .inner = .{ .iter = self.headers.keyIterator() } };
|
||||
}
|
||||
|
||||
pub fn _set(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void {
|
||||
const arena = page.arena;
|
||||
|
||||
const key = try arena.dupe(u8, name);
|
||||
const gop = try self.headers.getOrPut(arena, key);
|
||||
gop.value_ptr.* = try arena.dupe(u8, value);
|
||||
}
|
||||
|
||||
pub const HeadersValueIterator = struct {
|
||||
iter: HeaderHashMap.ValueIterator,
|
||||
|
||||
pub fn _next(self: *HeadersValueIterator) ?[]const u8 {
|
||||
if (self.iter.next()) |value| {
|
||||
return value.*;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn _values(self: *const Headers) HeadersValueIterable {
|
||||
return .{ .inner = .{ .iter = self.headers.valueIterator() } };
|
||||
}
|
||||
|
||||
pub const HeadersKeyIterable = iterator.Iterable(HeadersKeyIterator, "HeadersKeyIterator");
|
||||
pub const HeadersValueIterable = iterator.Iterable(HeadersValueIterator, "HeadersValueIterator");
|
||||
pub const HeadersEntryIterable = iterator.Iterable(HeadersEntryIterator, "HeadersEntryIterator");
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "fetch: Headers" {
|
||||
try testing.htmlRunner("fetch/headers.html");
|
||||
}
|
||||
266
src/browser/fetch/Request.zig
Normal file
266
src/browser/fetch/Request.zig
Normal file
@@ -0,0 +1,266 @@
|
||||
// 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 URL = @import("../../url.zig").URL;
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
const Response = @import("./Response.zig");
|
||||
const Http = @import("../../http/Http.zig");
|
||||
const ReadableStream = @import("../streams/ReadableStream.zig");
|
||||
|
||||
const v8 = @import("v8");
|
||||
const Env = @import("../env.zig").Env;
|
||||
|
||||
const Headers = @import("Headers.zig");
|
||||
const HeadersInit = @import("Headers.zig").HeadersInit;
|
||||
|
||||
pub const RequestInput = union(enum) {
|
||||
string: []const u8,
|
||||
request: *Request,
|
||||
};
|
||||
|
||||
pub const RequestCache = enum {
|
||||
default,
|
||||
@"no-store",
|
||||
reload,
|
||||
@"no-cache",
|
||||
@"force-cache",
|
||||
@"only-if-cached",
|
||||
|
||||
pub fn fromString(str: []const u8) ?RequestCache {
|
||||
for (std.enums.values(RequestCache)) |cache| {
|
||||
if (std.ascii.eqlIgnoreCase(str, @tagName(cache))) {
|
||||
return cache;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toString(self: RequestCache) []const u8 {
|
||||
return @tagName(self);
|
||||
}
|
||||
};
|
||||
|
||||
pub const RequestCredentials = enum {
|
||||
omit,
|
||||
@"same-origin",
|
||||
include,
|
||||
|
||||
pub fn fromString(str: []const u8) ?RequestCredentials {
|
||||
for (std.enums.values(RequestCredentials)) |cache| {
|
||||
if (std.ascii.eqlIgnoreCase(str, @tagName(cache))) {
|
||||
return cache;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toString(self: RequestCredentials) []const u8 {
|
||||
return @tagName(self);
|
||||
}
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/RequestInit
|
||||
pub const RequestInit = struct {
|
||||
body: ?[]const u8 = null,
|
||||
cache: ?[]const u8 = null,
|
||||
credentials: ?[]const u8 = null,
|
||||
headers: ?HeadersInit = null,
|
||||
integrity: ?[]const u8 = null,
|
||||
method: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Request/Request
|
||||
const Request = @This();
|
||||
|
||||
method: Http.Method,
|
||||
url: [:0]const u8,
|
||||
cache: RequestCache,
|
||||
credentials: RequestCredentials,
|
||||
headers: Headers,
|
||||
body: ?[]const u8,
|
||||
body_used: bool = false,
|
||||
integrity: []const u8,
|
||||
|
||||
pub fn constructor(input: RequestInput, _options: ?RequestInit, page: *Page) !Request {
|
||||
const arena = page.arena;
|
||||
const options: RequestInit = _options orelse .{};
|
||||
|
||||
const url: [:0]const u8 = blk: switch (input) {
|
||||
.string => |str| {
|
||||
break :blk try URL.stitch(arena, str, page.url.raw, .{ .null_terminated = true });
|
||||
},
|
||||
.request => |req| {
|
||||
break :blk try arena.dupeZ(u8, req.url);
|
||||
},
|
||||
};
|
||||
|
||||
const body = if (options.body) |body| try arena.dupe(u8, body) else null;
|
||||
const cache = (if (options.cache) |cache| RequestCache.fromString(cache) else null) orelse RequestCache.default;
|
||||
const credentials = (if (options.credentials) |creds| RequestCredentials.fromString(creds) else null) orelse RequestCredentials.@"same-origin";
|
||||
const integrity = if (options.integrity) |integ| try arena.dupe(u8, integ) else "";
|
||||
const headers: Headers = if (options.headers) |hdrs| try Headers.constructor(hdrs, page) else .{};
|
||||
|
||||
const method: Http.Method = blk: {
|
||||
if (options.method) |given_method| {
|
||||
for (std.enums.values(Http.Method)) |method| {
|
||||
if (std.ascii.eqlIgnoreCase(given_method, @tagName(method))) {
|
||||
break :blk method;
|
||||
}
|
||||
} else {
|
||||
return error.TypeError;
|
||||
}
|
||||
} else {
|
||||
break :blk Http.Method.GET;
|
||||
}
|
||||
};
|
||||
|
||||
return .{
|
||||
.method = method,
|
||||
.url = url,
|
||||
.cache = cache,
|
||||
.credentials = credentials,
|
||||
.headers = headers,
|
||||
.body = body,
|
||||
.integrity = integrity,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_body(self: *const Request, page: *Page) !?*ReadableStream {
|
||||
if (self.body) |body| {
|
||||
const stream = try ReadableStream.constructor(null, null, page);
|
||||
try stream.queue.append(page.arena, body);
|
||||
return stream;
|
||||
} else return null;
|
||||
}
|
||||
|
||||
pub fn get_bodyUsed(self: *const Request) bool {
|
||||
return self.body_used;
|
||||
}
|
||||
|
||||
pub fn get_cache(self: *const Request) RequestCache {
|
||||
return self.cache;
|
||||
}
|
||||
|
||||
pub fn get_credentials(self: *const Request) RequestCredentials {
|
||||
return self.credentials;
|
||||
}
|
||||
|
||||
pub fn get_headers(self: *Request) *Headers {
|
||||
return &self.headers;
|
||||
}
|
||||
|
||||
pub fn get_integrity(self: *const Request) []const u8 {
|
||||
return self.integrity;
|
||||
}
|
||||
|
||||
// TODO: If we ever support the Navigation API, we need isHistoryNavigation
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Request/isHistoryNavigation
|
||||
|
||||
pub fn get_method(self: *const Request) []const u8 {
|
||||
return @tagName(self.method);
|
||||
}
|
||||
|
||||
pub fn get_url(self: *const Request) []const u8 {
|
||||
return self.url;
|
||||
}
|
||||
|
||||
pub fn _clone(self: *Request) !Request {
|
||||
// Not allowed to clone if the body was used.
|
||||
if (self.body_used) {
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
// OK to just return the same fields BECAUSE
|
||||
// all of these fields are read-only and can't be modified.
|
||||
return Request{
|
||||
.body = self.body,
|
||||
.body_used = self.body_used,
|
||||
.cache = self.cache,
|
||||
.credentials = self.credentials,
|
||||
.headers = self.headers,
|
||||
.method = self.method,
|
||||
.integrity = self.integrity,
|
||||
.url = self.url,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _bytes(self: *Response, page: *Page) !Env.Promise {
|
||||
if (self.body_used) {
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
const resolver = Env.PromiseResolver{
|
||||
.js_context = page.main_context,
|
||||
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
|
||||
};
|
||||
|
||||
try resolver.resolve(self.body);
|
||||
self.body_used = true;
|
||||
return resolver.promise();
|
||||
}
|
||||
|
||||
pub fn _json(self: *Response, page: *Page) !Env.Promise {
|
||||
if (self.body_used) {
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
const resolver = Env.PromiseResolver{
|
||||
.js_context = page.main_context,
|
||||
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
|
||||
};
|
||||
|
||||
const p = std.json.parseFromSliceLeaky(
|
||||
std.json.Value,
|
||||
page.call_arena,
|
||||
self.body,
|
||||
.{},
|
||||
) catch |e| {
|
||||
log.info(.browser, "invalid json", .{ .err = e, .source = "Request" });
|
||||
return error.SyntaxError;
|
||||
};
|
||||
|
||||
try resolver.resolve(p);
|
||||
self.body_used = true;
|
||||
return resolver.promise();
|
||||
}
|
||||
|
||||
pub fn _text(self: *Response, page: *Page) !Env.Promise {
|
||||
if (self.body_used) {
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
const resolver = Env.PromiseResolver{
|
||||
.js_context = page.main_context,
|
||||
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
|
||||
};
|
||||
|
||||
try resolver.resolve(self.body);
|
||||
self.body_used = true;
|
||||
return resolver.promise();
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "fetch: Request" {
|
||||
try testing.htmlRunner("fetch/request.html");
|
||||
}
|
||||
196
src/browser/fetch/Response.zig
Normal file
196
src/browser/fetch/Response.zig
Normal file
@@ -0,0 +1,196 @@
|
||||
// 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 v8 = @import("v8");
|
||||
|
||||
const HttpClient = @import("../../http/Client.zig");
|
||||
const Http = @import("../../http/Http.zig");
|
||||
const URL = @import("../../url.zig").URL;
|
||||
|
||||
const ReadableStream = @import("../streams/ReadableStream.zig");
|
||||
const Headers = @import("Headers.zig");
|
||||
const HeadersInit = @import("Headers.zig").HeadersInit;
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Mime = @import("../mime.zig").Mime;
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Response
|
||||
const Response = @This();
|
||||
|
||||
status: u16 = 200,
|
||||
status_text: []const u8 = "",
|
||||
headers: Headers,
|
||||
mime: ?Mime = null,
|
||||
url: []const u8 = "",
|
||||
body: []const u8 = "",
|
||||
body_used: bool = false,
|
||||
redirected: bool = false,
|
||||
|
||||
const ResponseBody = union(enum) {
|
||||
string: []const u8,
|
||||
};
|
||||
|
||||
const ResponseOptions = struct {
|
||||
status: u16 = 200,
|
||||
statusText: ?[]const u8 = null,
|
||||
headers: ?HeadersInit = null,
|
||||
};
|
||||
|
||||
pub fn constructor(_input: ?ResponseBody, _options: ?ResponseOptions, page: *Page) !Response {
|
||||
const arena = page.arena;
|
||||
|
||||
const options: ResponseOptions = _options orelse .{};
|
||||
|
||||
const body = blk: {
|
||||
if (_input) |input| {
|
||||
switch (input) {
|
||||
.string => |str| {
|
||||
break :blk try arena.dupe(u8, str);
|
||||
},
|
||||
}
|
||||
} else {
|
||||
break :blk "";
|
||||
}
|
||||
};
|
||||
|
||||
const headers: Headers = if (options.headers) |hdrs| try Headers.constructor(hdrs, page) else .{};
|
||||
const status_text = if (options.statusText) |st| try arena.dupe(u8, st) else "";
|
||||
|
||||
return .{
|
||||
.body = body,
|
||||
.headers = headers,
|
||||
.status = options.status,
|
||||
.status_text = status_text,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_body(self: *const Response, page: *Page) !*ReadableStream {
|
||||
const stream = try ReadableStream.constructor(null, null, page);
|
||||
try stream.queue.append(page.arena, self.body);
|
||||
return stream;
|
||||
}
|
||||
|
||||
pub fn get_bodyUsed(self: *const Response) bool {
|
||||
return self.body_used;
|
||||
}
|
||||
|
||||
pub fn get_headers(self: *Response) *Headers {
|
||||
return &self.headers;
|
||||
}
|
||||
|
||||
pub fn get_ok(self: *const Response) bool {
|
||||
return self.status >= 200 and self.status <= 299;
|
||||
}
|
||||
|
||||
pub fn get_redirected(self: *const Response) bool {
|
||||
return self.redirected;
|
||||
}
|
||||
|
||||
pub fn get_status(self: *const Response) u16 {
|
||||
return self.status;
|
||||
}
|
||||
|
||||
pub fn get_statusText(self: *const Response) []const u8 {
|
||||
return self.status_text;
|
||||
}
|
||||
|
||||
pub fn get_url(self: *const Response) []const u8 {
|
||||
return self.url;
|
||||
}
|
||||
|
||||
pub fn _clone(self: *const Response) !Response {
|
||||
if (self.body_used) {
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
// OK to just return the same fields BECAUSE
|
||||
// all of these fields are read-only and can't be modified.
|
||||
return Response{
|
||||
.body = self.body,
|
||||
.body_used = self.body_used,
|
||||
.mime = self.mime,
|
||||
.headers = self.headers,
|
||||
.redirected = self.redirected,
|
||||
.status = self.status,
|
||||
.url = self.url,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _bytes(self: *Response, page: *Page) !Env.Promise {
|
||||
if (self.body_used) {
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
const resolver = Env.PromiseResolver{
|
||||
.js_context = page.main_context,
|
||||
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
|
||||
};
|
||||
|
||||
try resolver.resolve(self.body);
|
||||
self.body_used = true;
|
||||
return resolver.promise();
|
||||
}
|
||||
|
||||
pub fn _json(self: *Response, page: *Page) !Env.Promise {
|
||||
if (self.body_used) {
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
const resolver = Env.PromiseResolver{
|
||||
.js_context = page.main_context,
|
||||
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
|
||||
};
|
||||
|
||||
const p = std.json.parseFromSliceLeaky(
|
||||
std.json.Value,
|
||||
page.call_arena,
|
||||
self.body,
|
||||
.{},
|
||||
) catch |e| {
|
||||
log.info(.browser, "invalid json", .{ .err = e, .source = "Response" });
|
||||
return error.SyntaxError;
|
||||
};
|
||||
|
||||
try resolver.resolve(p);
|
||||
self.body_used = true;
|
||||
return resolver.promise();
|
||||
}
|
||||
|
||||
pub fn _text(self: *Response, page: *Page) !Env.Promise {
|
||||
if (self.body_used) {
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
const resolver = Env.PromiseResolver{
|
||||
.js_context = page.main_context,
|
||||
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
|
||||
};
|
||||
|
||||
try resolver.resolve(self.body);
|
||||
self.body_used = true;
|
||||
return resolver.promise();
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "fetch: Response" {
|
||||
try testing.htmlRunner("fetch/response.html");
|
||||
}
|
||||
212
src/browser/fetch/fetch.zig
Normal file
212
src/browser/fetch/fetch.zig
Normal file
@@ -0,0 +1,212 @@
|
||||
// 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 Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
const Http = @import("../../http/Http.zig");
|
||||
const HttpClient = @import("../../http/Client.zig");
|
||||
const Mime = @import("../mime.zig").Mime;
|
||||
|
||||
const Headers = @import("Headers.zig");
|
||||
|
||||
const RequestInput = @import("Request.zig").RequestInput;
|
||||
const RequestInit = @import("Request.zig").RequestInit;
|
||||
const Request = @import("Request.zig");
|
||||
const Response = @import("Response.zig");
|
||||
|
||||
pub const Interfaces = .{
|
||||
@import("Headers.zig"),
|
||||
@import("Headers.zig").HeadersEntryIterable,
|
||||
@import("Headers.zig").HeadersKeyIterable,
|
||||
@import("Headers.zig").HeadersValueIterable,
|
||||
@import("Request.zig"),
|
||||
@import("Response.zig"),
|
||||
};
|
||||
|
||||
pub const FetchContext = struct {
|
||||
arena: std.mem.Allocator,
|
||||
js_ctx: *Env.JsContext,
|
||||
promise_resolver: Env.PersistentPromiseResolver,
|
||||
|
||||
method: Http.Method,
|
||||
url: []const u8,
|
||||
body: std.ArrayListUnmanaged(u8) = .empty,
|
||||
headers: std.ArrayListUnmanaged([]const u8) = .empty,
|
||||
status: u16 = 0,
|
||||
mime: ?Mime = null,
|
||||
transfer: ?*HttpClient.Transfer = null,
|
||||
|
||||
/// This effectively takes ownership of the FetchContext.
|
||||
///
|
||||
/// We just return the underlying slices used for `headers`
|
||||
/// and for `body` here to avoid an allocation.
|
||||
pub fn toResponse(self: *const FetchContext) !Response {
|
||||
var headers: Headers = .{};
|
||||
|
||||
// convert into Headers
|
||||
for (self.headers.items) |hdr| {
|
||||
var iter = std.mem.splitScalar(u8, hdr, ':');
|
||||
const name = iter.next() orelse "";
|
||||
const value = iter.next() orelse "";
|
||||
try headers.append(name, value, self.arena);
|
||||
}
|
||||
|
||||
return Response{
|
||||
.status = self.status,
|
||||
.headers = headers,
|
||||
.mime = self.mime,
|
||||
.body = self.body.items,
|
||||
.url = self.url,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
|
||||
pub fn fetch(input: RequestInput, options: ?RequestInit, page: *Page) !Env.Promise {
|
||||
const arena = page.arena;
|
||||
|
||||
const req = try Request.constructor(input, options, page);
|
||||
var headers = try Http.Headers.init();
|
||||
|
||||
// Copy our headers into the HTTP headers.
|
||||
var header_iter = req.headers.headers.iterator();
|
||||
while (header_iter.next()) |entry| {
|
||||
const combined = try std.fmt.allocPrintSentinel(
|
||||
page.arena,
|
||||
"{s}: {s}",
|
||||
.{ entry.key_ptr.*, entry.value_ptr.* },
|
||||
0,
|
||||
);
|
||||
try headers.add(combined.ptr);
|
||||
}
|
||||
|
||||
try page.requestCookie(.{}).headersForRequest(arena, req.url, &headers);
|
||||
|
||||
const resolver = try page.main_context.createPersistentPromiseResolver();
|
||||
|
||||
const fetch_ctx = try arena.create(FetchContext);
|
||||
fetch_ctx.* = .{
|
||||
.arena = arena,
|
||||
.js_ctx = page.main_context,
|
||||
.promise_resolver = resolver,
|
||||
.method = req.method,
|
||||
.url = req.url,
|
||||
};
|
||||
|
||||
try page.http_client.request(.{
|
||||
.ctx = @ptrCast(fetch_ctx),
|
||||
.url = req.url,
|
||||
.method = req.method,
|
||||
.headers = headers,
|
||||
.body = req.body,
|
||||
.cookie_jar = page.cookie_jar,
|
||||
.resource_type = .fetch,
|
||||
|
||||
.start_callback = struct {
|
||||
fn startCallback(transfer: *HttpClient.Transfer) !void {
|
||||
const self: *FetchContext = @ptrCast(@alignCast(transfer.ctx));
|
||||
log.debug(.fetch, "request start", .{ .method = self.method, .url = self.url, .source = "fetch" });
|
||||
|
||||
self.transfer = transfer;
|
||||
}
|
||||
}.startCallback,
|
||||
.header_callback = struct {
|
||||
fn headerCallback(transfer: *HttpClient.Transfer) !void {
|
||||
const self: *FetchContext = @ptrCast(@alignCast(transfer.ctx));
|
||||
|
||||
const header = &transfer.response_header.?;
|
||||
|
||||
log.debug(.fetch, "request header", .{
|
||||
.source = "fetch",
|
||||
.method = self.method,
|
||||
.url = self.url,
|
||||
.status = header.status,
|
||||
});
|
||||
|
||||
if (header.contentType()) |ct| {
|
||||
self.mime = Mime.parse(ct) catch {
|
||||
return error.MimeParsing;
|
||||
};
|
||||
}
|
||||
|
||||
if (transfer.getContentLength()) |cl| {
|
||||
try self.body.ensureTotalCapacity(self.arena, cl);
|
||||
}
|
||||
|
||||
var it = transfer.responseHeaderIterator();
|
||||
while (it.next()) |hdr| {
|
||||
const joined = try std.fmt.allocPrint(self.arena, "{s}: {s}", .{ hdr.name, hdr.value });
|
||||
try self.headers.append(self.arena, joined);
|
||||
}
|
||||
|
||||
self.status = header.status;
|
||||
}
|
||||
}.headerCallback,
|
||||
.data_callback = struct {
|
||||
fn dataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void {
|
||||
const self: *FetchContext = @ptrCast(@alignCast(transfer.ctx));
|
||||
try self.body.appendSlice(self.arena, data);
|
||||
}
|
||||
}.dataCallback,
|
||||
.done_callback = struct {
|
||||
fn doneCallback(ctx: *anyopaque) !void {
|
||||
const self: *FetchContext = @ptrCast(@alignCast(ctx));
|
||||
self.transfer = null;
|
||||
|
||||
log.info(.fetch, "request complete", .{
|
||||
.source = "fetch",
|
||||
.method = self.method,
|
||||
.url = self.url,
|
||||
.status = self.status,
|
||||
});
|
||||
|
||||
const response = try self.toResponse();
|
||||
try self.promise_resolver.resolve(response);
|
||||
}
|
||||
}.doneCallback,
|
||||
.error_callback = struct {
|
||||
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||
const self: *FetchContext = @ptrCast(@alignCast(ctx));
|
||||
self.transfer = null;
|
||||
|
||||
log.err(.fetch, "error", .{
|
||||
.url = self.url,
|
||||
.err = err,
|
||||
.source = "fetch error",
|
||||
});
|
||||
|
||||
// We throw an Abort error when the page is getting closed so,
|
||||
// in this case, we don't need to reject the promise.
|
||||
if (err != error.Abort) {
|
||||
self.promise_resolver.reject(@errorName(err)) catch unreachable;
|
||||
}
|
||||
}
|
||||
}.errorCallback,
|
||||
});
|
||||
|
||||
return resolver.promise();
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "fetch: fetch" {
|
||||
try testing.htmlRunner("fetch/fetch.html");
|
||||
}
|
||||
@@ -21,7 +21,6 @@ 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 = .{
|
||||
@@ -77,11 +76,9 @@ pub const AbortSignal = struct {
|
||||
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);
|
||||
try page.scheduler.add(callback, TimeoutCallback.run, delay, .{ .name = "abort_signal" });
|
||||
return &callback.signal;
|
||||
}
|
||||
|
||||
@@ -94,7 +91,7 @@ pub const AbortSignal = struct {
|
||||
self.reason = reason_ orelse DEFAULT_REASON;
|
||||
|
||||
const abort_event = try parser.eventCreate();
|
||||
try parser.eventSetInternalType(abort_event, .abort_signal);
|
||||
parser.eventSetInternalType(abort_event, .abort_signal);
|
||||
|
||||
defer parser.eventDestroy(abort_event);
|
||||
try parser.eventInit(abort_event, "abort", .{});
|
||||
@@ -131,57 +128,16 @@ pub const AbortSignal = struct {
|
||||
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);
|
||||
fn run(ctx: *anyopaque) ?u32 {
|
||||
const self: *TimeoutCallback = @ptrCast(@alignCast(ctx));
|
||||
self.signal.abort("TimeoutError") catch |err| {
|
||||
log.warn(.app, "abort signal timeout", .{ .err = err });
|
||||
};
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.AbortController" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var called = 0", null },
|
||||
.{ "var a1 = new AbortController()", null },
|
||||
.{ "var s1 = a1.signal", null },
|
||||
.{ "s1.throwIfAborted()", "undefined" },
|
||||
.{ "s1.reason", "undefined" },
|
||||
.{ "var target;", null },
|
||||
.{
|
||||
\\ s1.addEventListener('abort', (e) => {
|
||||
\\ called += 1;
|
||||
\\ target = e.target;
|
||||
\\
|
||||
\\ });
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "a1.abort()", null },
|
||||
.{ "s1.aborted", "true" },
|
||||
.{ "target == s1", "true" },
|
||||
.{ "s1.reason", "AbortError" },
|
||||
.{ "called", "1" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var s2 = AbortSignal.abort('over 9000')", null },
|
||||
.{ "s2.aborted", "true" },
|
||||
.{ "s2.reason", "over 9000" },
|
||||
.{ "AbortSignal.abort().reason", "AbortError" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "var s3 = AbortSignal.timeout(10)", null },
|
||||
.{ "s3.aborted", "true" },
|
||||
.{ "s3.reason", "TimeoutError" },
|
||||
.{ "try { s3.throwIfAborted() } catch (e) { e }", "Error: TimeoutError" },
|
||||
}, .{});
|
||||
test "Browser: HTML.AbortController" {
|
||||
try testing.htmlRunner("html/abort_controller.html");
|
||||
}
|
||||
|
||||
@@ -76,22 +76,6 @@ fn normalize(allocator: Allocator, name: []const u8) ![]const u8 {
|
||||
}
|
||||
|
||||
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" },
|
||||
}, .{});
|
||||
test "Browser: HTML.DataSet" {
|
||||
try testing.htmlRunner("html/dataset.html");
|
||||
}
|
||||
|
||||
@@ -85,7 +85,10 @@ 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, .is_http = false });
|
||||
try page.cookie_jar.forRequest(&page.url.uri, buf.writer(page.arena), .{
|
||||
.is_http = false,
|
||||
.is_navigation = true,
|
||||
});
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
@@ -118,7 +121,9 @@ pub const HTMLDocument = struct {
|
||||
if (name.len == 0) return list;
|
||||
|
||||
const root = parser.documentHTMLToNode(self);
|
||||
var c = try collection.HTMLCollectionByName(arena, root, name, false);
|
||||
var c = try collection.HTMLCollectionByName(arena, root, name, .{
|
||||
.include_root = false,
|
||||
});
|
||||
|
||||
const ln = try c.get_length();
|
||||
var i: u32 = 0;
|
||||
@@ -132,11 +137,15 @@ pub const HTMLDocument = struct {
|
||||
}
|
||||
|
||||
pub fn get_images(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "img", false);
|
||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "img", .{
|
||||
.include_root = false,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_embeds(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "embed", false);
|
||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "embed", .{
|
||||
.include_root = false,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_plugins(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
||||
@@ -144,11 +153,15 @@ pub const HTMLDocument = struct {
|
||||
}
|
||||
|
||||
pub fn get_forms(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "form", false);
|
||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "form", .{
|
||||
.include_root = false,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_scripts(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "script", false);
|
||||
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "script", .{
|
||||
.include_root = false,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_applets(_: *parser.DocumentHTML) !collection.HTMLCollection {
|
||||
@@ -156,11 +169,15 @@ pub const HTMLDocument = struct {
|
||||
}
|
||||
|
||||
pub fn get_links(self: *parser.DocumentHTML) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByLinks(parser.documentHTMLToNode(self), false);
|
||||
return try collection.HTMLCollectionByLinks(parser.documentHTMLToNode(self), .{
|
||||
.include_root = false,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_anchors(self: *parser.DocumentHTML) !collection.HTMLCollection {
|
||||
return try collection.HTMLCollectionByAnchors(parser.documentHTMLToNode(self), false);
|
||||
return try collection.HTMLCollectionByAnchors(parser.documentHTMLToNode(self), .{
|
||||
.include_root = false,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_all(self: *parser.DocumentHTML) collection.HTMLAllCollection {
|
||||
@@ -192,7 +209,7 @@ pub const HTMLDocument = struct {
|
||||
}
|
||||
|
||||
pub fn get_readyState(self: *parser.DocumentHTML, page: *Page) ![]const u8 {
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
return @tagName(state.ready_state);
|
||||
}
|
||||
|
||||
@@ -275,7 +292,7 @@ pub const HTMLDocument = struct {
|
||||
}
|
||||
|
||||
pub fn documentIsLoaded(self: *parser.DocumentHTML, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
state.ready_state = .interactive;
|
||||
|
||||
log.debug(.script_event, "dispatch event", .{
|
||||
@@ -292,121 +309,12 @@ pub const HTMLDocument = struct {
|
||||
}
|
||||
|
||||
pub fn documentIsComplete(self: *parser.DocumentHTML, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
state.ready_state = .complete;
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
test "Browser.HTML.Document" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.__proto__.constructor.name", "HTMLDocument" },
|
||||
.{ "document.__proto__.__proto__.constructor.name", "Document" },
|
||||
.{ "document.body.localName == 'body'", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.domain", "lightpanda.io" },
|
||||
.{ "document.referrer", "" },
|
||||
.{ "document.title", "" },
|
||||
.{ "document.body.localName", "body" },
|
||||
.{ "document.head.localName", "head" },
|
||||
.{ "document.images.length", "0" },
|
||||
.{ "document.embeds.length", "0" },
|
||||
.{ "document.plugins.length", "0" },
|
||||
.{ "document.scripts.length", "0" },
|
||||
.{ "document.forms.length", "0" },
|
||||
.{ "document.links.length", "1" },
|
||||
.{ "document.applets.length", "0" },
|
||||
.{ "document.anchors.length", "0" },
|
||||
.{ "document.all.length", "8" },
|
||||
.{ "document.currentScript", "null" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.title = 'foo'", "foo" },
|
||||
.{ "document.title", "foo" },
|
||||
.{ "document.title = ''", "" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.getElementById('link').setAttribute('name', 'foo')", "undefined" },
|
||||
.{ "let list = document.getElementsByName('foo')", "undefined" },
|
||||
.{ "list.length", "1" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.cookie", "" },
|
||||
.{ "document.cookie = 'name=Oeschger; SameSite=None; Secure'", "name=Oeschger; SameSite=None; Secure" },
|
||||
.{ "document.cookie = 'favorite_food=tripe; SameSite=None; Secure'", "favorite_food=tripe; SameSite=None; Secure" },
|
||||
.{ "document.cookie", "name=Oeschger; favorite_food=tripe" },
|
||||
.{ "document.cookie = 'IgnoreMy=Ghost; HttpOnly'", null }, // "" should be returned, but the framework overrules it atm
|
||||
.{ "document.cookie", "name=Oeschger; favorite_food=tripe" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.elementFromPoint(0.5, 0.5)", "null" }, // Return null since we only return element s when they have previously been localized
|
||||
.{ "document.elementsFromPoint(0.5, 0.5)", "" },
|
||||
.{
|
||||
\\ let div1 = document.createElement('div');
|
||||
\\ document.body.appendChild(div1);
|
||||
\\ div1.getClientRects();
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "document.elementFromPoint(0.5, 0.5)", "[object HTMLDivElement]" },
|
||||
.{ "let elems = document.elementsFromPoint(0.5, 0.5)", null },
|
||||
.{ "elems.length", "3" },
|
||||
.{ "elems[0]", "[object HTMLDivElement]" },
|
||||
.{ "elems[1]", "[object HTMLBodyElement]" },
|
||||
.{ "elems[2]", "[object HTMLHtmlElement]" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ let a = document.createElement('a');
|
||||
\\ a.href = "https://lightpanda.io";
|
||||
\\ document.body.appendChild(a);
|
||||
\\ a.getClientRects();
|
||||
, // Note this will be placed after the div of previous test
|
||||
null,
|
||||
},
|
||||
.{ "let a_again = document.elementFromPoint(1.5, 0.5)", null },
|
||||
.{ "a_again", "[object HTMLAnchorElement]" },
|
||||
.{ "a_again.href", "https://lightpanda.io" },
|
||||
.{ "let a_agains = document.elementsFromPoint(1.5, 0.5)", null },
|
||||
.{ "a_agains[0].href", "https://lightpanda.io" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "!document.all", "true" },
|
||||
.{ "!!document.all", "false" },
|
||||
.{ "document.all(5)", "[object HTMLParagraphElement]" },
|
||||
.{ "document.all('content')", "[object HTMLDivElement]" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.defaultView.document == document", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "document.readyState", "loading" },
|
||||
}, .{});
|
||||
|
||||
try HTMLDocument.documentIsLoaded(runner.page.window.document, runner.page);
|
||||
try runner.testCases(&.{
|
||||
.{ "document.readyState", "interactive" },
|
||||
}, .{});
|
||||
|
||||
try HTMLDocument.documentIsComplete(runner.page.window.document, runner.page);
|
||||
try runner.testCases(&.{
|
||||
.{ "document.readyState", "complete" },
|
||||
}, .{});
|
||||
test "Browser: HTML.Document" {
|
||||
try testing.htmlRunner("html/document.html");
|
||||
}
|
||||
|
||||
@@ -26,14 +26,16 @@ const Page = @import("../page.zig").Page;
|
||||
const urlStitch = @import("../../url.zig").URL.stitch;
|
||||
const URL = @import("../url/url.zig").URL;
|
||||
const Node = @import("../dom/node.zig").Node;
|
||||
const NodeUnion = @import("../dom/node.zig").Union;
|
||||
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;
|
||||
const StyleSheet = @import("../cssom/StyleSheet.zig");
|
||||
const CSSStyleDeclaration = @import("../cssom/CSSStyleDeclaration.zig");
|
||||
|
||||
// HTMLElement interfaces
|
||||
pub const Interfaces = .{
|
||||
Element,
|
||||
HTMLElement,
|
||||
HTMLUnknownElement,
|
||||
HTMLAnchorElement,
|
||||
@@ -60,7 +62,6 @@ pub const Interfaces = .{
|
||||
HTMLHeadElement,
|
||||
HTMLHeadingElement,
|
||||
HTMLHtmlElement,
|
||||
HTMLIFrameElement,
|
||||
HTMLImageElement,
|
||||
HTMLImageElement.Factory,
|
||||
HTMLInputElement,
|
||||
@@ -75,7 +76,6 @@ pub const Interfaces = .{
|
||||
HTMLOListElement,
|
||||
HTMLObjectElement,
|
||||
HTMLOptGroupElement,
|
||||
HTMLOptionElement,
|
||||
HTMLOutputElement,
|
||||
HTMLParagraphElement,
|
||||
HTMLParamElement,
|
||||
@@ -86,6 +86,7 @@ pub const Interfaces = .{
|
||||
HTMLScriptElement,
|
||||
HTMLSourceElement,
|
||||
HTMLSpanElement,
|
||||
HTMLSlotElement,
|
||||
HTMLStyleElement,
|
||||
HTMLTableElement,
|
||||
HTMLTableCaptionElement,
|
||||
@@ -102,7 +103,8 @@ pub const Interfaces = .{
|
||||
HTMLVideoElement,
|
||||
|
||||
@import("form.zig").HTMLFormElement,
|
||||
@import("select.zig").HTMLSelectElement,
|
||||
@import("iframe.zig").HTMLIFrameElement,
|
||||
@import("select.zig").Interfaces,
|
||||
};
|
||||
|
||||
pub const Union = generate.Union(Interfaces);
|
||||
@@ -145,7 +147,7 @@ pub const HTMLElement = struct {
|
||||
try Node.removeChildren(n);
|
||||
|
||||
// attach the text node.
|
||||
_ = try parser.nodeAppendChild(n, @as(*parser.Node, @alignCast(@ptrCast(t))));
|
||||
_ = try parser.nodeAppendChild(n, @as(*parser.Node, @ptrCast(@alignCast(t))));
|
||||
}
|
||||
|
||||
pub fn _click(e: *parser.ElementHTML) !void {
|
||||
@@ -264,7 +266,7 @@ pub const HTMLAnchorElement = struct {
|
||||
// But
|
||||
// document.createElement('a').host
|
||||
// should not fail, it should return an empty string
|
||||
if (try parser.elementGetAttribute(@alignCast(@ptrCast(self)), "href")) |href| {
|
||||
if (try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "href")) |href| {
|
||||
return URL.constructor(.{ .string = href }, null, page); // TODO inject base url
|
||||
}
|
||||
return .empty;
|
||||
@@ -584,12 +586,6 @@ pub const HTMLHtmlElement = struct {
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLIFrameElement = struct {
|
||||
pub const Self = parser.IFrame;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLImageElement = struct {
|
||||
pub const Self = parser.Image;
|
||||
pub const prototype = *HTMLElement;
|
||||
@@ -813,12 +809,6 @@ pub const HTMLOptGroupElement = struct {
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLOptionElement = struct {
|
||||
pub const Self = parser.Option;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLOutputElement = struct {
|
||||
pub const Self = parser.Output;
|
||||
pub const prototype = *HTMLElement;
|
||||
@@ -874,12 +864,23 @@ pub const HTMLScriptElement = struct {
|
||||
) orelse "";
|
||||
}
|
||||
|
||||
pub fn set_src(self: *parser.Script, v: []const u8) !void {
|
||||
return try parser.elementSetAttribute(
|
||||
pub fn set_src(self: *parser.Script, v: []const u8, page: *Page) !void {
|
||||
try parser.elementSetAttribute(
|
||||
parser.scriptToElt(self),
|
||||
"src",
|
||||
v,
|
||||
);
|
||||
|
||||
if (try Node.get_isConnected(@ptrCast(@alignCast(self)))) {
|
||||
// There are sites which do set the src AFTER appending the script
|
||||
// tag to the document:
|
||||
// const s = document.createElement('script');
|
||||
// document.getElementsByTagName('body')[0].appendChild(s);
|
||||
// s.src = '...';
|
||||
// This should load the script.
|
||||
// addFromElement protects against double execution.
|
||||
try page.script_manager.addFromElement(@ptrCast(@alignCast(self)));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_type(self: *parser.Script) !?[]const u8 {
|
||||
@@ -890,7 +891,7 @@ pub const HTMLScriptElement = struct {
|
||||
}
|
||||
|
||||
pub fn set_type(self: *parser.Script, v: []const u8) !void {
|
||||
return try parser.elementSetAttribute(
|
||||
try parser.elementSetAttribute(
|
||||
parser.scriptToElt(self),
|
||||
"type",
|
||||
v,
|
||||
@@ -905,7 +906,7 @@ pub const HTMLScriptElement = struct {
|
||||
}
|
||||
|
||||
pub fn set_text(self: *parser.Script, v: []const u8) !void {
|
||||
return try parser.elementSetAttribute(
|
||||
try parser.elementSetAttribute(
|
||||
parser.scriptToElt(self),
|
||||
"text",
|
||||
v,
|
||||
@@ -920,7 +921,7 @@ pub const HTMLScriptElement = struct {
|
||||
}
|
||||
|
||||
pub fn set_integrity(self: *parser.Script, v: []const u8) !void {
|
||||
return try parser.elementSetAttribute(
|
||||
try parser.elementSetAttribute(
|
||||
parser.scriptToElt(self),
|
||||
"integrity",
|
||||
v,
|
||||
@@ -976,23 +977,38 @@ pub const HTMLScriptElement = struct {
|
||||
return try parser.elementRemoveAttribute(parser.scriptToElt(self), "nomodule");
|
||||
}
|
||||
|
||||
pub fn get_nonce(self: *parser.Script) !?[]const u8 {
|
||||
return try parser.elementGetAttribute(
|
||||
parser.scriptToElt(self),
|
||||
"nonce",
|
||||
) orelse "";
|
||||
}
|
||||
|
||||
pub fn set_nonce(self: *parser.Script, v: []const u8) !void {
|
||||
try parser.elementSetAttribute(
|
||||
parser.scriptToElt(self),
|
||||
"nonce",
|
||||
v,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn get_onload(self: *parser.Script, page: *Page) !?Env.Function {
|
||||
const state = page.getNodeState(@alignCast(@ptrCast(self))) orelse return null;
|
||||
const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null;
|
||||
return state.onload;
|
||||
}
|
||||
|
||||
pub fn set_onload(self: *parser.Script, function: ?Env.Function, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
state.onload = function;
|
||||
}
|
||||
|
||||
pub fn get_onerror(self: *parser.Script, page: *Page) !?Env.Function {
|
||||
const state = page.getNodeState(@alignCast(@ptrCast(self))) orelse return null;
|
||||
const state = page.getNodeState(@ptrCast(@alignCast(self))) orelse return null;
|
||||
return state.onerror;
|
||||
}
|
||||
|
||||
pub fn set_onerror(self: *parser.Script, function: ?Env.Function, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
state.onerror = function;
|
||||
}
|
||||
};
|
||||
@@ -1009,13 +1025,108 @@ pub const HTMLSpanElement = struct {
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
pub const HTMLSlotElement = struct {
|
||||
pub const Self = parser.Slot;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn get_name(self: *parser.Slot) !?[]const u8 {
|
||||
return (try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "name")) orelse "";
|
||||
}
|
||||
|
||||
pub fn set_name(self: *parser.Slot, value: []const u8) !void {
|
||||
return parser.elementSetAttribute(@ptrCast(@alignCast(self)), "name", value);
|
||||
}
|
||||
|
||||
const AssignedNodesOpts = struct {
|
||||
flatten: bool = false,
|
||||
};
|
||||
pub fn _assignedNodes(self: *parser.Slot, opts_: ?AssignedNodesOpts, page: *Page) ![]NodeUnion {
|
||||
const opts = opts_ orelse AssignedNodesOpts{ .flatten = false };
|
||||
|
||||
if (try findAssignedSlotNodes(self, opts, page)) |nodes| {
|
||||
return nodes;
|
||||
}
|
||||
|
||||
if (!opts.flatten) {
|
||||
return &.{};
|
||||
}
|
||||
|
||||
const node: *parser.Node = @ptrCast(@alignCast(self));
|
||||
const nl = try parser.nodeGetChildNodes(node);
|
||||
const len = try parser.nodeListLength(nl);
|
||||
if (len == 0) {
|
||||
return &.{};
|
||||
}
|
||||
|
||||
var assigned = try page.call_arena.alloc(NodeUnion, len);
|
||||
var i: usize = 0;
|
||||
while (true) : (i += 1) {
|
||||
const child = try parser.nodeListItem(nl, @intCast(i)) orelse break;
|
||||
assigned[i] = try Node.toInterface(child);
|
||||
}
|
||||
return assigned[0..i];
|
||||
}
|
||||
|
||||
fn findAssignedSlotNodes(self: *parser.Slot, opts: AssignedNodesOpts, page: *Page) !?[]NodeUnion {
|
||||
if (opts.flatten) {
|
||||
log.warn(.web_api, "not implemented", .{ .feature = "HTMLSlotElement flatten assignedNodes" });
|
||||
}
|
||||
|
||||
const slot_name = try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "name");
|
||||
const node: *parser.Node = @ptrCast(@alignCast(self));
|
||||
var root = try parser.nodeGetRootNode(node);
|
||||
if (page.getNodeState(root)) |state| {
|
||||
if (state.shadow_root) |sr| {
|
||||
root = @ptrCast(@alignCast(sr.host));
|
||||
}
|
||||
}
|
||||
|
||||
var arr: std.ArrayList(NodeUnion) = .empty;
|
||||
const w = @import("../dom/walker.zig").WalkerChildren{};
|
||||
var next: ?*parser.Node = null;
|
||||
while (true) {
|
||||
next = try w.get_next(root, next) orelse break;
|
||||
if (try parser.nodeType(next.?) != .element) {
|
||||
if (slot_name == null) {
|
||||
// default slot (with no name), takes everything
|
||||
try arr.append(page.call_arena, try Node.toInterface(next.?));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const el: *parser.Element = @ptrCast(@alignCast(next.?));
|
||||
const element_slot = try parser.elementGetAttribute(el, "slot");
|
||||
|
||||
if (nullableStringsAreEqual(slot_name, element_slot)) {
|
||||
// either they're the same string or they are both null
|
||||
try arr.append(page.call_arena, try Node.toInterface(next.?));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return if (arr.items.len == 0) null else arr.items;
|
||||
}
|
||||
|
||||
fn nullableStringsAreEqual(a: ?[]const u8, b: ?[]const u8) bool {
|
||||
if (a == null and b == null) {
|
||||
return true;
|
||||
}
|
||||
if (a) |aa| {
|
||||
const bb = b orelse return false;
|
||||
return std.mem.eql(u8, aa, bb);
|
||||
}
|
||||
|
||||
// a is null, but b isn't (else the first guard clause would have hit)
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
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)));
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
if (state.style_sheet) |ss| {
|
||||
return ss;
|
||||
}
|
||||
@@ -1069,7 +1180,7 @@ pub const HTMLTemplateElement = struct {
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn get_content(self: *parser.Template, page: *Page) !*parser.DocumentFragment {
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
|
||||
if (state.template_content) |tc| {
|
||||
return tc;
|
||||
}
|
||||
@@ -1115,367 +1226,111 @@ pub const HTMLVideoElement = struct {
|
||||
pub const subtype = .node;
|
||||
};
|
||||
|
||||
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)));
|
||||
|
||||
pub fn toInterfaceFromTag(comptime T: type, e: *parser.Element, tag: parser.Tag) !T {
|
||||
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)) },
|
||||
.applet => .{ .HTMLAppletElement = @as(*parser.Applet, @ptrCast(elem)) },
|
||||
.area => .{ .HTMLAreaElement = @as(*parser.Area, @ptrCast(elem)) },
|
||||
.audio => .{ .HTMLAudioElement = @as(*parser.Audio, @ptrCast(elem)) },
|
||||
.base => .{ .HTMLBaseElement = @as(*parser.Base, @ptrCast(elem)) },
|
||||
.body => .{ .HTMLBodyElement = @as(*parser.Body, @ptrCast(elem)) },
|
||||
.br => .{ .HTMLBRElement = @as(*parser.BR, @ptrCast(elem)) },
|
||||
.button => .{ .HTMLButtonElement = @as(*parser.Button, @ptrCast(elem)) },
|
||||
.canvas => .{ .HTMLCanvasElement = @as(*parser.Canvas, @ptrCast(elem)) },
|
||||
.dl => .{ .HTMLDListElement = @as(*parser.DList, @ptrCast(elem)) },
|
||||
.data => .{ .HTMLDataElement = @as(*parser.Data, @ptrCast(elem)) },
|
||||
.datalist => .{ .HTMLDataListElement = @as(*parser.DataList, @ptrCast(elem)) },
|
||||
.dialog => .{ .HTMLDialogElement = @as(*parser.Dialog, @ptrCast(elem)) },
|
||||
.dir => .{ .HTMLDirectoryElement = @as(*parser.Directory, @ptrCast(elem)) },
|
||||
.div => .{ .HTMLDivElement = @as(*parser.Div, @ptrCast(elem)) },
|
||||
.embed => .{ .HTMLEmbedElement = @as(*parser.Embed, @ptrCast(elem)) },
|
||||
.fieldset => .{ .HTMLFieldSetElement = @as(*parser.FieldSet, @ptrCast(elem)) },
|
||||
.font => .{ .HTMLFontElement = @as(*parser.Font, @ptrCast(elem)) },
|
||||
.form => .{ .HTMLFormElement = @as(*parser.Form, @ptrCast(elem)) },
|
||||
.frame => .{ .HTMLFrameElement = @as(*parser.Frame, @ptrCast(elem)) },
|
||||
.frameset => .{ .HTMLFrameSetElement = @as(*parser.FrameSet, @ptrCast(elem)) },
|
||||
.hr => .{ .HTMLHRElement = @as(*parser.HR, @ptrCast(elem)) },
|
||||
.head => .{ .HTMLHeadElement = @as(*parser.Head, @ptrCast(elem)) },
|
||||
.h1, .h2, .h3, .h4, .h5, .h6 => .{ .HTMLHeadingElement = @as(*parser.Heading, @ptrCast(elem)) },
|
||||
.html => .{ .HTMLHtmlElement = @as(*parser.Html, @ptrCast(elem)) },
|
||||
.iframe => .{ .HTMLIFrameElement = @as(*parser.IFrame, @ptrCast(elem)) },
|
||||
.img => .{ .HTMLImageElement = @as(*parser.Image, @ptrCast(elem)) },
|
||||
.input => .{ .HTMLInputElement = @as(*parser.Input, @ptrCast(elem)) },
|
||||
.li => .{ .HTMLLIElement = @as(*parser.LI, @ptrCast(elem)) },
|
||||
.label => .{ .HTMLLabelElement = @as(*parser.Label, @ptrCast(elem)) },
|
||||
.legend => .{ .HTMLLegendElement = @as(*parser.Legend, @ptrCast(elem)) },
|
||||
.link => .{ .HTMLLinkElement = @as(*parser.Link, @ptrCast(elem)) },
|
||||
.map => .{ .HTMLMapElement = @as(*parser.Map, @ptrCast(elem)) },
|
||||
.meta => .{ .HTMLMetaElement = @as(*parser.Meta, @ptrCast(elem)) },
|
||||
.meter => .{ .HTMLMeterElement = @as(*parser.Meter, @ptrCast(elem)) },
|
||||
.ins, .del => .{ .HTMLModElement = @as(*parser.Mod, @ptrCast(elem)) },
|
||||
.ol => .{ .HTMLOListElement = @as(*parser.OList, @ptrCast(elem)) },
|
||||
.object => .{ .HTMLObjectElement = @as(*parser.Object, @ptrCast(elem)) },
|
||||
.optgroup => .{ .HTMLOptGroupElement = @as(*parser.OptGroup, @ptrCast(elem)) },
|
||||
.option => .{ .HTMLOptionElement = @as(*parser.Option, @ptrCast(elem)) },
|
||||
.output => .{ .HTMLOutputElement = @as(*parser.Output, @ptrCast(elem)) },
|
||||
.p => .{ .HTMLParagraphElement = @as(*parser.Paragraph, @ptrCast(elem)) },
|
||||
.param => .{ .HTMLParamElement = @as(*parser.Param, @ptrCast(elem)) },
|
||||
.picture => .{ .HTMLPictureElement = @as(*parser.Picture, @ptrCast(elem)) },
|
||||
.pre => .{ .HTMLPreElement = @as(*parser.Pre, @ptrCast(elem)) },
|
||||
.progress => .{ .HTMLProgressElement = @as(*parser.Progress, @ptrCast(elem)) },
|
||||
.blockquote, .q => .{ .HTMLQuoteElement = @as(*parser.Quote, @ptrCast(elem)) },
|
||||
.script => .{ .HTMLScriptElement = @as(*parser.Script, @ptrCast(elem)) },
|
||||
.select => .{ .HTMLSelectElement = @as(*parser.Select, @ptrCast(elem)) },
|
||||
.source => .{ .HTMLSourceElement = @as(*parser.Source, @ptrCast(elem)) },
|
||||
.span => .{ .HTMLSpanElement = @as(*parser.Span, @ptrCast(elem)) },
|
||||
.style => .{ .HTMLStyleElement = @as(*parser.Style, @ptrCast(elem)) },
|
||||
.table => .{ .HTMLTableElement = @as(*parser.Table, @ptrCast(elem)) },
|
||||
.caption => .{ .HTMLTableCaptionElement = @as(*parser.TableCaption, @ptrCast(elem)) },
|
||||
.th, .td => .{ .HTMLTableCellElement = @as(*parser.TableCell, @ptrCast(elem)) },
|
||||
.col, .colgroup => .{ .HTMLTableColElement = @as(*parser.TableCol, @ptrCast(elem)) },
|
||||
.tr => .{ .HTMLTableRowElement = @as(*parser.TableRow, @ptrCast(elem)) },
|
||||
.thead, .tbody, .tfoot => .{ .HTMLTableSectionElement = @as(*parser.TableSection, @ptrCast(elem)) },
|
||||
.template => .{ .HTMLTemplateElement = @as(*parser.Template, @ptrCast(elem)) },
|
||||
.textarea => .{ .HTMLTextAreaElement = @as(*parser.TextArea, @ptrCast(elem)) },
|
||||
.time => .{ .HTMLTimeElement = @as(*parser.Time, @ptrCast(elem)) },
|
||||
.title => .{ .HTMLTitleElement = @as(*parser.Title, @ptrCast(elem)) },
|
||||
.track => .{ .HTMLTrackElement = @as(*parser.Track, @ptrCast(elem)) },
|
||||
.ul => .{ .HTMLUListElement = @as(*parser.UList, @ptrCast(elem)) },
|
||||
.video => .{ .HTMLVideoElement = @as(*parser.Video, @ptrCast(elem)) },
|
||||
.undef => .{ .HTMLUnknownElement = @as(*parser.Unknown, @ptrCast(elem)) },
|
||||
.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(e)) },
|
||||
.a => .{ .HTMLAnchorElement = @as(*parser.Anchor, @ptrCast(e)) },
|
||||
.applet => .{ .HTMLAppletElement = @as(*parser.Applet, @ptrCast(e)) },
|
||||
.area => .{ .HTMLAreaElement = @as(*parser.Area, @ptrCast(e)) },
|
||||
.audio => .{ .HTMLAudioElement = @as(*parser.Audio, @ptrCast(e)) },
|
||||
.base => .{ .HTMLBaseElement = @as(*parser.Base, @ptrCast(e)) },
|
||||
.body => .{ .HTMLBodyElement = @as(*parser.Body, @ptrCast(e)) },
|
||||
.br => .{ .HTMLBRElement = @as(*parser.BR, @ptrCast(e)) },
|
||||
.button => .{ .HTMLButtonElement = @as(*parser.Button, @ptrCast(e)) },
|
||||
.canvas => .{ .HTMLCanvasElement = @as(*parser.Canvas, @ptrCast(e)) },
|
||||
.dl => .{ .HTMLDListElement = @as(*parser.DList, @ptrCast(e)) },
|
||||
.data => .{ .HTMLDataElement = @as(*parser.Data, @ptrCast(e)) },
|
||||
.datalist => .{ .HTMLDataListElement = @as(*parser.DataList, @ptrCast(e)) },
|
||||
.dialog => .{ .HTMLDialogElement = @as(*parser.Dialog, @ptrCast(e)) },
|
||||
.dir => .{ .HTMLDirectoryElement = @as(*parser.Directory, @ptrCast(e)) },
|
||||
.div => .{ .HTMLDivElement = @as(*parser.Div, @ptrCast(e)) },
|
||||
.embed => .{ .HTMLEmbedElement = @as(*parser.Embed, @ptrCast(e)) },
|
||||
.fieldset => .{ .HTMLFieldSetElement = @as(*parser.FieldSet, @ptrCast(e)) },
|
||||
.font => .{ .HTMLFontElement = @as(*parser.Font, @ptrCast(e)) },
|
||||
.form => .{ .HTMLFormElement = @as(*parser.Form, @ptrCast(e)) },
|
||||
.frame => .{ .HTMLFrameElement = @as(*parser.Frame, @ptrCast(e)) },
|
||||
.frameset => .{ .HTMLFrameSetElement = @as(*parser.FrameSet, @ptrCast(e)) },
|
||||
.hr => .{ .HTMLHRElement = @as(*parser.HR, @ptrCast(e)) },
|
||||
.head => .{ .HTMLHeadElement = @as(*parser.Head, @ptrCast(e)) },
|
||||
.h1, .h2, .h3, .h4, .h5, .h6 => .{ .HTMLHeadingElement = @as(*parser.Heading, @ptrCast(e)) },
|
||||
.html => .{ .HTMLHtmlElement = @as(*parser.Html, @ptrCast(e)) },
|
||||
.iframe => .{ .HTMLIFrameElement = @as(*parser.IFrame, @ptrCast(e)) },
|
||||
.img => .{ .HTMLImageElement = @as(*parser.Image, @ptrCast(e)) },
|
||||
.input => .{ .HTMLInputElement = @as(*parser.Input, @ptrCast(e)) },
|
||||
.li => .{ .HTMLLIElement = @as(*parser.LI, @ptrCast(e)) },
|
||||
.label => .{ .HTMLLabelElement = @as(*parser.Label, @ptrCast(e)) },
|
||||
.legend => .{ .HTMLLegendElement = @as(*parser.Legend, @ptrCast(e)) },
|
||||
.link => .{ .HTMLLinkElement = @as(*parser.Link, @ptrCast(e)) },
|
||||
.map => .{ .HTMLMapElement = @as(*parser.Map, @ptrCast(e)) },
|
||||
.meta => .{ .HTMLMetaElement = @as(*parser.Meta, @ptrCast(e)) },
|
||||
.meter => .{ .HTMLMeterElement = @as(*parser.Meter, @ptrCast(e)) },
|
||||
.ins, .del => .{ .HTMLModElement = @as(*parser.Mod, @ptrCast(e)) },
|
||||
.ol => .{ .HTMLOListElement = @as(*parser.OList, @ptrCast(e)) },
|
||||
.object => .{ .HTMLObjectElement = @as(*parser.Object, @ptrCast(e)) },
|
||||
.optgroup => .{ .HTMLOptGroupElement = @as(*parser.OptGroup, @ptrCast(e)) },
|
||||
.option => .{ .HTMLOptionElement = @as(*parser.Option, @ptrCast(e)) },
|
||||
.output => .{ .HTMLOutputElement = @as(*parser.Output, @ptrCast(e)) },
|
||||
.p => .{ .HTMLParagraphElement = @as(*parser.Paragraph, @ptrCast(e)) },
|
||||
.param => .{ .HTMLParamElement = @as(*parser.Param, @ptrCast(e)) },
|
||||
.picture => .{ .HTMLPictureElement = @as(*parser.Picture, @ptrCast(e)) },
|
||||
.pre => .{ .HTMLPreElement = @as(*parser.Pre, @ptrCast(e)) },
|
||||
.progress => .{ .HTMLProgressElement = @as(*parser.Progress, @ptrCast(e)) },
|
||||
.blockquote, .q => .{ .HTMLQuoteElement = @as(*parser.Quote, @ptrCast(e)) },
|
||||
.script => .{ .HTMLScriptElement = @as(*parser.Script, @ptrCast(e)) },
|
||||
.select => .{ .HTMLSelectElement = @as(*parser.Select, @ptrCast(e)) },
|
||||
.source => .{ .HTMLSourceElement = @as(*parser.Source, @ptrCast(e)) },
|
||||
.span => .{ .HTMLSpanElement = @as(*parser.Span, @ptrCast(e)) },
|
||||
.slot => .{ .HTMLSlotElement = @as(*parser.Slot, @ptrCast(e)) },
|
||||
.style => .{ .HTMLStyleElement = @as(*parser.Style, @ptrCast(e)) },
|
||||
.table => .{ .HTMLTableElement = @as(*parser.Table, @ptrCast(e)) },
|
||||
.caption => .{ .HTMLTableCaptionElement = @as(*parser.TableCaption, @ptrCast(e)) },
|
||||
.th, .td => .{ .HTMLTableCellElement = @as(*parser.TableCell, @ptrCast(e)) },
|
||||
.col, .colgroup => .{ .HTMLTableColElement = @as(*parser.TableCol, @ptrCast(e)) },
|
||||
.tr => .{ .HTMLTableRowElement = @as(*parser.TableRow, @ptrCast(e)) },
|
||||
.thead, .tbody, .tfoot => .{ .HTMLTableSectionElement = @as(*parser.TableSection, @ptrCast(e)) },
|
||||
.template => .{ .HTMLTemplateElement = @as(*parser.Template, @ptrCast(e)) },
|
||||
.textarea => .{ .HTMLTextAreaElement = @as(*parser.TextArea, @ptrCast(e)) },
|
||||
.time => .{ .HTMLTimeElement = @as(*parser.Time, @ptrCast(e)) },
|
||||
.title => .{ .HTMLTitleElement = @as(*parser.Title, @ptrCast(e)) },
|
||||
.track => .{ .HTMLTrackElement = @as(*parser.Track, @ptrCast(e)) },
|
||||
.ul => .{ .HTMLUListElement = @as(*parser.UList, @ptrCast(e)) },
|
||||
.video => .{ .HTMLVideoElement = @as(*parser.Video, @ptrCast(e)) },
|
||||
.undef => .{ .HTMLUnknownElement = @as(*parser.Unknown, @ptrCast(e)) },
|
||||
};
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.Element" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let link = document.getElementById('link')", "undefined" },
|
||||
.{ "link.target", "" },
|
||||
.{ "link.target = '_blank'", "_blank" },
|
||||
.{ "link.target", "_blank" },
|
||||
.{ "link.target = ''", "" },
|
||||
|
||||
.{ "link.href", "foo" },
|
||||
.{ "link.href = 'https://lightpanda.io/'", "https://lightpanda.io/" },
|
||||
.{ "link.href", "https://lightpanda.io/" },
|
||||
|
||||
.{ "link.origin", "https://lightpanda.io" },
|
||||
|
||||
.{ "link.host = 'lightpanda.io:443'", "lightpanda.io:443" },
|
||||
.{ "link.host", "lightpanda.io:443" },
|
||||
.{ "link.port", "443" },
|
||||
.{ "link.hostname", "lightpanda.io" },
|
||||
|
||||
.{ "link.host = 'lightpanda.io'", "lightpanda.io" },
|
||||
.{ "link.host", "lightpanda.io" },
|
||||
.{ "link.port", "" },
|
||||
.{ "link.hostname", "lightpanda.io" },
|
||||
|
||||
.{ "link.host", "lightpanda.io" },
|
||||
.{ "link.hostname", "lightpanda.io" },
|
||||
.{ "link.hostname = 'foo.bar'", "foo.bar" },
|
||||
.{ "link.href", "https://foo.bar/" },
|
||||
|
||||
.{ "link.search", "" },
|
||||
.{ "link.search = 'q=bar'", "q=bar" },
|
||||
.{ "link.search", "?q=bar" },
|
||||
.{ "link.href", "https://foo.bar/?q=bar" },
|
||||
|
||||
.{ "link.hash", "" },
|
||||
.{ "link.hash = 'frag'", "frag" },
|
||||
.{ "link.hash", "#frag" },
|
||||
.{ "link.href", "https://foo.bar/?q=bar#frag" },
|
||||
|
||||
.{ "link.port", "" },
|
||||
.{ "link.port = '443'", "443" },
|
||||
.{ "link.host", "foo.bar:443" },
|
||||
.{ "link.hostname", "foo.bar" },
|
||||
.{ "link.href", "https://foo.bar:443/?q=bar#frag" },
|
||||
.{ "link.port = null", "null" },
|
||||
.{ "link.href", "https://foo.bar/?q=bar#frag" },
|
||||
|
||||
.{ "link.href = 'foo'", "foo" },
|
||||
|
||||
.{ "link.type", "" },
|
||||
.{ "link.type = 'text/html'", "text/html" },
|
||||
.{ "link.type", "text/html" },
|
||||
.{ "link.type = ''", "" },
|
||||
|
||||
.{ "link.text", "OK" },
|
||||
.{ "link.text = 'foo'", "foo" },
|
||||
.{ "link.text", "foo" },
|
||||
.{ "link.text = 'OK'", "OK" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let script = document.createElement('script')", "undefined" },
|
||||
.{ "script.src = 'foo.bar'", "foo.bar" },
|
||||
|
||||
.{ "script.async = true", "true" },
|
||||
.{ "script.async", "true" },
|
||||
.{ "script.async = false", "false" },
|
||||
.{ "script.async", "false" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const backup = document.getElementById('content')", "undefined" },
|
||||
.{ "document.getElementById('content').innerText = 'foo';", "foo" },
|
||||
.{ "document.getElementById('content').innerText", "foo" },
|
||||
.{ "document.getElementById('content').innerHTML = backup; true;", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let click_count = 0;", "undefined" },
|
||||
.{ "let clickCbk = function() { click_count++ }", "undefined" },
|
||||
.{ "document.getElementById('content').addEventListener('click', clickCbk);", "undefined" },
|
||||
.{ "document.getElementById('content').click()", "undefined" },
|
||||
.{ "click_count", "1" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let style = document.getElementById('content').style", "undefined" },
|
||||
.{ "style.cssText = 'color: red; font-size: 12px; margin: 5px !important;'", "color: red; font-size: 12px; margin: 5px !important;" },
|
||||
.{ "style.length", "3" },
|
||||
.{ "style.setProperty('background-color', 'blue')", "undefined" },
|
||||
.{ "style.getPropertyValue('background-color')", "blue" },
|
||||
.{ "style.length", "4" },
|
||||
}, .{});
|
||||
|
||||
// Image
|
||||
try runner.testCases(&.{
|
||||
// Testing constructors
|
||||
.{ "(new Image).width", "0" },
|
||||
.{ "(new Image).height", "0" },
|
||||
.{ "(new Image(4)).width", "4" },
|
||||
.{ "(new Image(4, 6)).height", "6" },
|
||||
|
||||
// Testing ulong property
|
||||
.{ "let fruit = new Image", null },
|
||||
.{ "fruit.width", "0" },
|
||||
.{ "fruit.width = 5", "5" },
|
||||
.{ "fruit.width", "5" },
|
||||
.{ "fruit.width = '15'", "15" },
|
||||
.{ "fruit.width", "15" },
|
||||
.{ "fruit.width = 'apple'", "apple" },
|
||||
.{ "fruit.width;", "0" },
|
||||
|
||||
// Testing string property
|
||||
.{ "let lyric = new Image", null },
|
||||
.{ "lyric.src", "" },
|
||||
.{ "lyric.src = 'okay'", "okay" },
|
||||
.{ "lyric.src", "okay" },
|
||||
.{ "lyric.src = 15", "15" },
|
||||
.{ "lyric.src", "15" },
|
||||
}, .{});
|
||||
|
||||
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" },
|
||||
}, .{});
|
||||
|
||||
// detached node cannot be focused
|
||||
try runner.testCases(&.{
|
||||
.{ "const focused = document.activeElement", null },
|
||||
.{ "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.Element" {
|
||||
try testing.htmlRunner("html/element.html");
|
||||
}
|
||||
|
||||
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.HtmlLinkElement" {
|
||||
try testing.htmlRunner("html/link.html");
|
||||
}
|
||||
|
||||
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);
|
||||
defer alloc.deinit();
|
||||
const arena = alloc.allocator();
|
||||
|
||||
try runner.testCases(&.{.{ "let elem_input = document.createElement('input')", null }}, .{});
|
||||
|
||||
try runner.testCases(&.{.{ "elem_input.form", "null" }}, .{}); // Initial value
|
||||
// Valid input.form is tested separately :Browser.HTML.HtmlInputElement.propeties.form
|
||||
try testProperty(arena, &runner, "elem_input.form", "null", &.{.{ .input = "'foo'" }}); // Invalid
|
||||
|
||||
try runner.testCases(&.{.{ "elem_input.accept", "" }}, .{}); // Initial value
|
||||
try testProperty(arena, &runner, "elem_input.accept", null, &str_valids); // Valid
|
||||
|
||||
try runner.testCases(&.{.{ "elem_input.alt", "" }}, .{}); // Initial value
|
||||
try testProperty(arena, &runner, "elem_input.alt", null, &str_valids); // Valid
|
||||
|
||||
try runner.testCases(&.{.{ "elem_input.disabled", "false" }}, .{}); // Initial value
|
||||
try testProperty(arena, &runner, "elem_input.disabled", null, &bool_valids); // Valid
|
||||
|
||||
try runner.testCases(&.{.{ "elem_input.maxLength", "-1" }}, .{}); // Initial value
|
||||
try testProperty(arena, &runner, "elem_input.maxLength", null, &.{.{ .input = "5" }}); // Valid
|
||||
try testProperty(arena, &runner, "elem_input.maxLength", "0", &.{.{ .input = "'banana'" }}); // Invalid
|
||||
try runner.testCases(&.{.{ "try { elem_input.maxLength = -45 } catch(e) {e}", "Error: NegativeValueNotAllowed" }}, .{}); // Error
|
||||
|
||||
try runner.testCases(&.{.{ "elem_input.name", "" }}, .{}); // Initial value
|
||||
try testProperty(arena, &runner, "elem_input.name", null, &str_valids); // Valid
|
||||
|
||||
try runner.testCases(&.{.{ "elem_input.readOnly", "false" }}, .{}); // Initial value
|
||||
try testProperty(arena, &runner, "elem_input.readOnly", null, &bool_valids); // Valid
|
||||
|
||||
try runner.testCases(&.{.{ "elem_input.size", "20" }}, .{}); // Initial value
|
||||
try testProperty(arena, &runner, "elem_input.size", null, &.{.{ .input = "5" }}); // Valid
|
||||
try testProperty(arena, &runner, "elem_input.size", "20", &.{.{ .input = "-26" }}); // Invalid
|
||||
try runner.testCases(&.{.{ "try { elem_input.size = 0 } catch(e) {e}", "Error: ZeroNotAllowed" }}, .{}); // Error
|
||||
try runner.testCases(&.{.{ "try { elem_input.size = 'banana' } catch(e) {e}", "Error: ZeroNotAllowed" }}, .{}); // Error
|
||||
|
||||
try runner.testCases(&.{.{ "elem_input.src", "" }}, .{}); // Initial value
|
||||
try testProperty(arena, &runner, "elem_input.src", null, &.{
|
||||
.{ .input = "'foo'", .expected = "https://lightpanda.io/foo" }, // TODO stitch should work with spaces -> %20
|
||||
.{ .input = "-3", .expected = "https://lightpanda.io/-3" },
|
||||
.{ .input = "''", .expected = "https://lightpanda.io/noslashattheend" },
|
||||
});
|
||||
|
||||
try runner.testCases(&.{.{ "elem_input.type", "text" }}, .{}); // Initial value
|
||||
try testProperty(arena, &runner, "elem_input.type", null, &.{.{ .input = "'checkbox'", .expected = "checkbox" }}); // Valid
|
||||
try testProperty(arena, &runner, "elem_input.type", "text", &.{.{ .input = "'5'" }}); // Invalid
|
||||
|
||||
// Properties that are related
|
||||
try runner.testCases(&.{
|
||||
.{ "let input_checked = document.createElement('input')", null },
|
||||
.{ "input_checked.defaultChecked", "false" },
|
||||
.{ "input_checked.checked", "false" },
|
||||
|
||||
.{ "input_checked.defaultChecked = true", "true" },
|
||||
.{ "input_checked.defaultChecked", "true" },
|
||||
.{ "input_checked.checked", "true" }, // Also perceived as true
|
||||
|
||||
.{ "input_checked.checked = false", "false" },
|
||||
.{ "input_checked.defaultChecked", "true" },
|
||||
.{ "input_checked.checked", "false" },
|
||||
|
||||
.{ "input_checked.defaultChecked = true", "true" },
|
||||
.{ "input_checked.checked", "false" }, // Still false
|
||||
}, .{});
|
||||
try runner.testCases(&.{
|
||||
.{ "let input_value = document.createElement('input')", null },
|
||||
.{ "input_value.defaultValue", "" },
|
||||
.{ "input_value.value", "" },
|
||||
|
||||
.{ "input_value.defaultValue = 3.1", "3.1" },
|
||||
.{ "input_value.defaultValue", "3.1" },
|
||||
.{ "input_value.value", "3.1" }, // Also perceived as 3.1
|
||||
|
||||
.{ "input_value.value = 'mango'", "mango" },
|
||||
.{ "input_value.defaultValue", "3.1" },
|
||||
.{ "input_value.value", "mango" },
|
||||
|
||||
.{ "input_value.defaultValue = true", "true" },
|
||||
.{ "input_value.value", "mango" }, // Still mango
|
||||
}, .{});
|
||||
test "Browser: HTML.HtmlImageElement" {
|
||||
try testing.htmlRunner("html/image.html");
|
||||
}
|
||||
|
||||
test "Browser.HTML.HtmlInputElement.properties.form" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
|
||||
\\ <form action="test.php" target="_blank">
|
||||
\\ <p>
|
||||
\\ <label>First name: <input type="text" name="first-name" /></label>
|
||||
\\ </p>
|
||||
\\ </form>
|
||||
});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let elem_input = document.querySelector('input')", null },
|
||||
.{ "elem_input.form", "[object HTMLFormElement]" }, // Initial value
|
||||
.{ "elem_input.form = 'foo'", null },
|
||||
.{ "elem_input.form", "[object HTMLFormElement]" }, // Invalid
|
||||
}, .{});
|
||||
test "Browser: HTML.HtmlInputElement" {
|
||||
try testing.htmlRunner("html/input.html");
|
||||
}
|
||||
|
||||
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.HtmlTemplateElement" {
|
||||
try testing.htmlRunner("html/template.html");
|
||||
}
|
||||
|
||||
test "Browser.HTML.HTMLStyleElement" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "" });
|
||||
defer runner.deinit();
|
||||
test "Browser: HTML.HtmlStyleElement" {
|
||||
try testing.htmlRunner("html/style.html");
|
||||
}
|
||||
|
||||
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" },
|
||||
}, .{});
|
||||
test "Browser: HTML.HtmlScriptElement" {
|
||||
try testing.htmlRunner("html/script/script.html");
|
||||
try testing.htmlRunner("html/script/inline_defer.html");
|
||||
}
|
||||
|
||||
test "Browser: HTML.HTMLSlotElement" {
|
||||
try testing.htmlRunner("html/html_slot_element.html");
|
||||
}
|
||||
|
||||
const Check = struct {
|
||||
|
||||
@@ -42,7 +42,7 @@ pub const ErrorEvent = struct {
|
||||
const event = try parser.eventCreate();
|
||||
defer parser.eventDestroy(event);
|
||||
try parser.eventInit(event, event_type, .{});
|
||||
try parser.eventSetInternalType(event, .event);
|
||||
parser.eventSetInternalType(event, .event);
|
||||
|
||||
const o = opts orelse ErrorEventInit{};
|
||||
|
||||
@@ -81,34 +81,6 @@ pub const ErrorEvent = struct {
|
||||
};
|
||||
|
||||
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!" },
|
||||
}, .{});
|
||||
test "Browser: HTML.ErrorEvent" {
|
||||
try testing.htmlRunner("html/error_event.html");
|
||||
}
|
||||
|
||||
@@ -87,34 +87,7 @@ pub const History = struct {
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.History" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "history.scrollRestoration", "auto" },
|
||||
.{ "history.scrollRestoration = 'manual'", "manual" },
|
||||
.{ "history.scrollRestoration = 'foo'", "foo" },
|
||||
.{ "history.scrollRestoration", "manual" },
|
||||
.{ "history.scrollRestoration = 'auto'", "auto" },
|
||||
.{ "history.scrollRestoration", "auto" },
|
||||
|
||||
.{ "history.state", "null" },
|
||||
|
||||
.{ "history.pushState({}, null, '')", "undefined" },
|
||||
|
||||
.{ "history.replaceState({}, null, '')", "undefined" },
|
||||
|
||||
.{ "history.go()", "undefined" },
|
||||
.{ "history.go(1)", "undefined" },
|
||||
.{ "history.go(-1)", "undefined" },
|
||||
|
||||
.{ "history.forward()", "undefined" },
|
||||
|
||||
.{ "history.back()", "undefined" },
|
||||
}, .{});
|
||||
test "Browser: HTML.History" {
|
||||
try testing.htmlRunner("html/history.html");
|
||||
}
|
||||
|
||||
30
src/browser/html/iframe.zig
Normal file
30
src/browser/html/iframe.zig
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
const HTMLElement = @import("elements.zig").HTMLElement;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#htmliframeelement
|
||||
pub const HTMLIFrameElement = struct {
|
||||
pub const Self = parser.IFrame;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
};
|
||||
@@ -87,20 +87,6 @@ pub const Location = struct {
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.Location" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "location.href", "https://lightpanda.io/opensource-browser/" },
|
||||
.{ "document.location.href", "https://lightpanda.io/opensource-browser/" },
|
||||
|
||||
.{ "location.host", "lightpanda.io" },
|
||||
.{ "location.hostname", "lightpanda.io" },
|
||||
.{ "location.origin", "https://lightpanda.io" },
|
||||
.{ "location.pathname", "/opensource-browser/" },
|
||||
.{ "location.hash", "" },
|
||||
.{ "location.port", "" },
|
||||
.{ "location.search", "" },
|
||||
}, .{});
|
||||
test "Browser: HTML.Location" {
|
||||
try testing.htmlRunner("html/location.html");
|
||||
}
|
||||
|
||||
@@ -80,17 +80,7 @@ pub const Navigator = struct {
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.Navigator" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "navigator.userAgent", "Lightpanda/1.0" },
|
||||
.{ "navigator.appVersion", "1.0" },
|
||||
.{ "navigator.language", "en-US" },
|
||||
}, .{});
|
||||
test "Browser: HTML.Navigator" {
|
||||
try testing.htmlRunner("html/navigator.html");
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
|
||||
pub const Interfaces = .{
|
||||
@@ -29,6 +30,8 @@ pub const Interfaces = .{
|
||||
pub const Screen = struct {
|
||||
pub const prototype = *EventTarget;
|
||||
|
||||
proto: parser.EventTargetTBase = .{ .internal_target_type = .screen },
|
||||
|
||||
height: u32 = 1080,
|
||||
width: u32 = 1920,
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Screen/colorDepth
|
||||
@@ -83,6 +86,7 @@ pub const ScreenOrientation = struct {
|
||||
|
||||
angle: u32 = 0,
|
||||
type: ScreenOrientationType,
|
||||
proto: parser.EventTargetTBase = .{ .internal_target_type = .screen_orientation },
|
||||
|
||||
pub fn get_angle(self: *const ScreenOrientation) u32 {
|
||||
return self.angle;
|
||||
@@ -94,16 +98,6 @@ pub const ScreenOrientation = struct {
|
||||
};
|
||||
|
||||
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" },
|
||||
}, .{});
|
||||
test "Browser: HTML.Screen" {
|
||||
try testing.htmlRunner("html/screen.html");
|
||||
}
|
||||
|
||||
@@ -18,8 +18,16 @@
|
||||
const std = @import("std");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const HTMLElement = @import("elements.zig").HTMLElement;
|
||||
const collection = @import("../dom/html_collection.zig");
|
||||
|
||||
const Page = @import("../page.zig").Page;
|
||||
const HTMLElement = @import("elements.zig").HTMLElement;
|
||||
|
||||
pub const Interfaces = .{
|
||||
HTMLSelectElement,
|
||||
HTMLOptionElement,
|
||||
HTMLOptionsCollection,
|
||||
};
|
||||
|
||||
pub const HTMLSelectElement = struct {
|
||||
pub const Self = parser.Select;
|
||||
@@ -56,7 +64,7 @@ pub const HTMLSelectElement = struct {
|
||||
}
|
||||
|
||||
pub fn get_selectedIndex(select: *parser.Select, page: *Page) !i32 {
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(select)));
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(select)));
|
||||
const selected_index = try parser.selectGetSelectedIndex(select);
|
||||
|
||||
// See the explicit_index_set field documentation
|
||||
@@ -75,7 +83,7 @@ pub const HTMLSelectElement = struct {
|
||||
// Libdom's dom_html_select_select_set_selected_index will crash if index
|
||||
// is out of range, and it doesn't properly unset options
|
||||
pub fn set_selectedIndex(select: *parser.Select, index: i32, page: *Page) !void {
|
||||
var state = try page.getOrCreateNodeState(@alignCast(@ptrCast(select)));
|
||||
var state = try page.getOrCreateNodeState(@ptrCast(@alignCast(select)));
|
||||
state.explicit_index_set = true;
|
||||
|
||||
const options = try parser.selectGetOptions(select);
|
||||
@@ -89,56 +97,108 @@ pub const HTMLSelectElement = struct {
|
||||
try parser.optionSetSelected(option, true);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_options(select: *parser.Select) HTMLOptionsCollection {
|
||||
return .{
|
||||
.select = select,
|
||||
.proto = collection.HTMLCollectionChildren(@ptrCast(@alignCast(select)), .{
|
||||
.mutable = true,
|
||||
.include_root = false,
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLOptionElement = struct {
|
||||
pub const Self = parser.Option;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn get_value(self: *parser.Option) ![]const u8 {
|
||||
return parser.optionGetValue(self);
|
||||
}
|
||||
pub fn set_value(self: *parser.Option, value: []const u8) !void {
|
||||
return parser.optionSetValue(self, value);
|
||||
}
|
||||
|
||||
pub fn get_label(self: *parser.Option) ![]const u8 {
|
||||
return parser.optionGetLabel(self);
|
||||
}
|
||||
pub fn set_label(self: *parser.Option, label: []const u8) !void {
|
||||
return parser.optionSetLabel(self, label);
|
||||
}
|
||||
|
||||
pub fn get_selected(self: *parser.Option) !bool {
|
||||
return parser.optionGetSelected(self);
|
||||
}
|
||||
pub fn set_selected(self: *parser.Option, value: bool) !void {
|
||||
return parser.optionSetSelected(self, value);
|
||||
}
|
||||
|
||||
pub fn get_disabled(self: *parser.Option) !bool {
|
||||
return parser.optionGetDisabled(self);
|
||||
}
|
||||
pub fn set_disabled(self: *parser.Option, value: bool) !void {
|
||||
return parser.optionSetDisabled(self, value);
|
||||
}
|
||||
|
||||
pub fn get_text(self: *parser.Option) ![]const u8 {
|
||||
return parser.optionGetText(self);
|
||||
}
|
||||
|
||||
pub fn get_form(self: *parser.Option) !?*parser.Form {
|
||||
return parser.optionGetForm(self);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLOptionsCollection = struct {
|
||||
pub const prototype = *collection.HTMLCollection;
|
||||
|
||||
proto: collection.HTMLCollection,
|
||||
select: *parser.Select,
|
||||
|
||||
pub fn get_selectedIndex(self: *HTMLOptionsCollection, page: *Page) !i32 {
|
||||
return HTMLSelectElement.get_selectedIndex(self.select, page);
|
||||
}
|
||||
|
||||
pub fn set_selectedIndex(self: *HTMLOptionsCollection, index: i32, page: *Page) !void {
|
||||
return HTMLSelectElement.set_selectedIndex(self.select, index, page);
|
||||
}
|
||||
|
||||
const BeforeOpts = union(enum) {
|
||||
index: u32,
|
||||
option: *parser.Option,
|
||||
};
|
||||
pub fn _add(self: *HTMLOptionsCollection, option: *parser.Option, before_: ?BeforeOpts) !void {
|
||||
const Node = @import("../dom/node.zig").Node;
|
||||
const before = before_ orelse {
|
||||
return self.appendOption(option);
|
||||
};
|
||||
|
||||
const insert_before: *parser.Node = switch (before) {
|
||||
.option => |o| @ptrCast(@alignCast(o)),
|
||||
.index => |i| (try self.proto.item(i)) orelse return self.appendOption(option),
|
||||
};
|
||||
return Node.before(insert_before, &.{
|
||||
.{ .node = @ptrCast(@alignCast(option)) },
|
||||
});
|
||||
}
|
||||
|
||||
pub fn _remove(self: *HTMLOptionsCollection, index: u32) !void {
|
||||
const Node = @import("../dom/node.zig").Node;
|
||||
const option = (try self.proto.item(index)) orelse return;
|
||||
_ = try Node._removeChild(@ptrCast(@alignCast(self.select)), option);
|
||||
}
|
||||
|
||||
fn appendOption(self: *HTMLOptionsCollection, option: *parser.Option) !void {
|
||||
const Node = @import("../dom/node.zig").Node;
|
||||
return Node.append(@ptrCast(@alignCast(self.select)), &.{
|
||||
.{ .node = @ptrCast(@alignCast(option)) },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.Select" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html =
|
||||
\\ <form id=f1>
|
||||
\\ <select id=s1 name=s1><option>o1<option>o2</select>
|
||||
\\ </form>
|
||||
\\ <select id=s2></select>
|
||||
});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "const s = document.getElementById('s1');", null },
|
||||
.{ "s.form", "[object HTMLFormElement]" },
|
||||
|
||||
.{ "document.getElementById('s2').form", "null" },
|
||||
|
||||
.{ "s.disabled", "false" },
|
||||
.{ "s.disabled = true", null },
|
||||
.{ "s.disabled", "true" },
|
||||
.{ "s.disabled = false", null },
|
||||
.{ "s.disabled", "false" },
|
||||
|
||||
.{ "s.multiple", "false" },
|
||||
.{ "s.multiple = true", null },
|
||||
.{ "s.multiple", "true" },
|
||||
.{ "s.multiple = false", null },
|
||||
.{ "s.multiple", "false" },
|
||||
|
||||
.{ "s.name;", "s1" },
|
||||
.{ "s.name = 'sel1';", null },
|
||||
.{ "s.name", "sel1" },
|
||||
|
||||
.{ "s.length;", "2" },
|
||||
|
||||
.{ "s.selectedIndex", "0" },
|
||||
.{ "s.selectedIndex = 2", null }, // out of range
|
||||
.{ "s.selectedIndex", "-1" },
|
||||
|
||||
.{ "s.selectedIndex = -1", null },
|
||||
.{ "s.selectedIndex", "-1" },
|
||||
|
||||
.{ "s.selectedIndex = 0", null },
|
||||
.{ "s.selectedIndex", "0" },
|
||||
|
||||
.{ "s.selectedIndex = 1", null },
|
||||
.{ "s.selectedIndex", "1" },
|
||||
|
||||
.{ "s.selectedIndex = -323", null },
|
||||
.{ "s.selectedIndex", "-1" },
|
||||
}, .{});
|
||||
test "Browser: HTML.Select" {
|
||||
try testing.htmlRunner("html/select.html");
|
||||
}
|
||||
|
||||
@@ -31,11 +31,6 @@ pub const SVGElement = struct {
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.SVGElement" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "'AString' instanceof SVGElement", "false" },
|
||||
}, .{});
|
||||
test "Browser: HTML.SVGElement" {
|
||||
try testing.htmlRunner("html/svg.html");
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ 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 Navigator = @import("navigator.zig").Navigator;
|
||||
const History = @import("history.zig").History;
|
||||
@@ -32,13 +31,18 @@ const Console = @import("../console/console.zig").Console;
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const MediaQueryList = @import("media_query_list.zig").MediaQueryList;
|
||||
const Performance = @import("../dom/performance.zig").Performance;
|
||||
const CSSStyleDeclaration = @import("../cssom/css_style_declaration.zig").CSSStyleDeclaration;
|
||||
const CSSStyleDeclaration = @import("../cssom/CSSStyleDeclaration.zig");
|
||||
const Screen = @import("screen.zig").Screen;
|
||||
const domcss = @import("../dom/css.zig");
|
||||
const Css = @import("../css/css.zig").Css;
|
||||
|
||||
const Function = Env.Function;
|
||||
const JsObject = Env.JsObject;
|
||||
|
||||
const v8 = @import("v8");
|
||||
const Request = @import("../fetch/Request.zig");
|
||||
const fetchFn = @import("../fetch/fetch.zig").fetch;
|
||||
|
||||
const storage = @import("../storage/storage.zig");
|
||||
|
||||
// https://dom.spec.whatwg.org/#interface-window-extensions
|
||||
@@ -57,7 +61,7 @@ pub const Window = struct {
|
||||
|
||||
// counter for having unique timer ids
|
||||
timer_id: u30 = 0,
|
||||
timers: std.AutoHashMapUnmanaged(u32, *TimerCallback) = .{},
|
||||
timers: std.AutoHashMapUnmanaged(u32, void) = .{},
|
||||
|
||||
crypto: Crypto = .{},
|
||||
console: Console = .{},
|
||||
@@ -76,7 +80,7 @@ pub const Window = struct {
|
||||
.document = html_doc,
|
||||
.target = target orelse "",
|
||||
.navigator = navigator orelse .{},
|
||||
.performance = .{ .time_origin = try std.time.Timer.start() },
|
||||
.performance = Performance.init(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -86,7 +90,7 @@ pub const Window = struct {
|
||||
}
|
||||
|
||||
pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) !void {
|
||||
self.performance.time_origin.reset(); // When to reset see: https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin
|
||||
self.performance.reset(); // When to reset see: https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin
|
||||
self.document = doc;
|
||||
try parser.documentHTMLSetLocation(Location, doc, &self.location);
|
||||
}
|
||||
@@ -95,6 +99,10 @@ pub const Window = struct {
|
||||
self.storage_shelf = shelf;
|
||||
}
|
||||
|
||||
pub fn _fetch(_: *Window, input: Request.RequestInput, options: ?Request.RequestInit, page: *Page) !Env.Promise {
|
||||
return fetchFn(input, options, page);
|
||||
}
|
||||
|
||||
pub fn get_window(self: *Window) *Window {
|
||||
return self;
|
||||
}
|
||||
@@ -127,7 +135,43 @@ pub const Window = struct {
|
||||
return self;
|
||||
}
|
||||
|
||||
// TODO: frames
|
||||
// frames return the window itself, but accessing it via a pseudo
|
||||
// array returns the Window object corresponding to the given frame or
|
||||
// iframe.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Window/frames
|
||||
pub fn get_frames(self: *Window) *Window {
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn indexed_get(self: *Window, index: u32, has_value: *bool, page: *Page) !*Window {
|
||||
const frames = try domcss.querySelectorAll(
|
||||
page.call_arena,
|
||||
parser.documentHTMLToNode(self.document),
|
||||
"iframe",
|
||||
);
|
||||
|
||||
if (index >= frames.nodes.items.len) {
|
||||
has_value.* = false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
has_value.* = true;
|
||||
// TODO return the correct frame's window
|
||||
// frames.nodes.items[indexed]
|
||||
return error.TODO;
|
||||
}
|
||||
|
||||
// Retrieve the numbre of frames/iframes from the DOM dynamically.
|
||||
pub fn get_length(self: *const Window, page: *Page) !u32 {
|
||||
const frames = try domcss.querySelectorAll(
|
||||
page.call_arena,
|
||||
parser.documentHTMLToNode(self.document),
|
||||
"iframe",
|
||||
);
|
||||
|
||||
return frames.get_length();
|
||||
}
|
||||
|
||||
pub fn get_top(self: *Window) *Window {
|
||||
return self;
|
||||
}
|
||||
@@ -179,34 +223,35 @@ pub const Window = struct {
|
||||
}
|
||||
|
||||
pub fn _requestAnimationFrame(self: *Window, cbk: Function, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, 5, page, .{ .animation_frame = true });
|
||||
return self.createTimeout(cbk, 5, page, .{
|
||||
.animation_frame = true,
|
||||
.name = "animationFrame",
|
||||
.low_priority = true,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn _cancelAnimationFrame(self: *Window, id: u32, page: *Page) !void {
|
||||
const kv = self.timers.fetchRemove(id) orelse return;
|
||||
return page.loop.cancel(kv.value.loop_id);
|
||||
pub fn _cancelAnimationFrame(self: *Window, id: u32) !void {
|
||||
_ = self.timers.remove(id);
|
||||
}
|
||||
|
||||
pub fn _setTimeout(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, delay, page, .{ .args = params });
|
||||
return self.createTimeout(cbk, delay, page, .{ .args = params, .name = "setTimeout" });
|
||||
}
|
||||
|
||||
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 });
|
||||
return self.createTimeout(cbk, delay, page, .{ .repeat = true, .args = params, .name = "setInterval" });
|
||||
}
|
||||
|
||||
pub fn _clearTimeout(self: *Window, id: u32, page: *Page) !void {
|
||||
const kv = self.timers.fetchRemove(id) orelse return;
|
||||
return page.loop.cancel(kv.value.loop_id);
|
||||
pub fn _clearTimeout(self: *Window, id: u32) !void {
|
||||
_ = self.timers.remove(id);
|
||||
}
|
||||
|
||||
pub fn _clearInterval(self: *Window, id: u32, page: *Page) !void {
|
||||
const kv = self.timers.fetchRemove(id) orelse return;
|
||||
return page.loop.cancel(kv.value.loop_id);
|
||||
pub fn _clearInterval(self: *Window, id: u32) !void {
|
||||
_ = self.timers.remove(id);
|
||||
}
|
||||
|
||||
pub fn _queueMicrotask(self: *Window, cbk: Function, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, 0, page, .{});
|
||||
return self.createTimeout(cbk, 0, page, .{ .name = "queueMicrotask" });
|
||||
}
|
||||
|
||||
pub fn _matchMedia(_: *const Window, media: []const u8, page: *Page) !MediaQueryList {
|
||||
@@ -232,20 +277,14 @@ pub const Window = struct {
|
||||
}
|
||||
|
||||
const CreateTimeoutOpts = struct {
|
||||
name: []const u8,
|
||||
args: []Env.JsObject = &.{},
|
||||
repeat: bool = false,
|
||||
animation_frame: bool = false,
|
||||
low_priority: bool = false,
|
||||
};
|
||||
fn createTimeout(self: *Window, cbk: Function, delay_: ?u32, page: *Page, opts: CreateTimeoutOpts) !u32 {
|
||||
const delay = delay_ orelse 0;
|
||||
if (delay > 5000) {
|
||||
log.warn(.user_script, "long timeout ignored", .{ .delay = delay, .interval = opts.repeat });
|
||||
// self.timer_id is u30, so the largest value we can generate is
|
||||
// 1_073_741_824. Returning 2_000_000_000 makes sure that clients
|
||||
// can call cancelTimer/cancelInterval without breaking anything.
|
||||
return 2_000_000_000;
|
||||
}
|
||||
|
||||
if (self.timers.count() > 512) {
|
||||
return error.TooManyTimeout;
|
||||
}
|
||||
@@ -258,6 +297,8 @@ pub const Window = struct {
|
||||
if (gop.found_existing) {
|
||||
// this can only happen if we've created 2^31 timeouts.
|
||||
return error.TooManyTimeout;
|
||||
} else {
|
||||
gop.value_ptr.* = {};
|
||||
}
|
||||
errdefer _ = self.timers.remove(timer_id);
|
||||
|
||||
@@ -270,22 +311,22 @@ pub const Window = struct {
|
||||
}
|
||||
}
|
||||
|
||||
const delay_ms: u63 = @as(u63, delay) * std.time.ns_per_ms;
|
||||
const callback = try arena.create(TimerCallback);
|
||||
|
||||
callback.* = .{
|
||||
.cbk = cbk,
|
||||
.loop_id = 0, // we're going to set this to a real value shortly
|
||||
.window = self,
|
||||
.timer_id = timer_id,
|
||||
.args = persisted_args,
|
||||
.node = .{ .func = TimerCallback.run },
|
||||
.repeat = if (opts.repeat) delay_ms else null,
|
||||
.animation_frame = opts.animation_frame,
|
||||
// setting a repeat time of 0 is illegal, doing + 1 is a simple way to avoid that
|
||||
.repeat = if (opts.repeat) delay + 1 else null,
|
||||
};
|
||||
callback.loop_id = try page.loop.timeout(delay_ms, &callback.node);
|
||||
|
||||
gop.value_ptr.* = callback;
|
||||
try page.scheduler.add(callback, TimerCallback.run, delay, .{
|
||||
.name = opts.name,
|
||||
.low_priority = opts.low_priority,
|
||||
});
|
||||
|
||||
return timer_id;
|
||||
}
|
||||
|
||||
@@ -354,30 +395,32 @@ pub const Window = struct {
|
||||
};
|
||||
|
||||
const TimerCallback = struct {
|
||||
// the internal loop id, need it when cancelling
|
||||
loop_id: usize,
|
||||
|
||||
// the id of our timer (windows.timers key)
|
||||
timer_id: u31,
|
||||
|
||||
// if false, we'll remove the timer_id from the window.timers lookup on run
|
||||
repeat: ?u32,
|
||||
|
||||
// The JavaScript callback to execute
|
||||
cbk: Function,
|
||||
|
||||
// This is the internal data that the event loop tracks. We'll get this
|
||||
// back in run and, from it, can get our TimerCallback instance
|
||||
node: Loop.CallbackNode = undefined,
|
||||
|
||||
// if the event should be repeated
|
||||
repeat: ?u63 = null,
|
||||
|
||||
animation_frame: bool = false,
|
||||
|
||||
window: *Window,
|
||||
|
||||
args: []Env.JsObject = &.{},
|
||||
|
||||
fn run(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
|
||||
const self: *TimerCallback = @fieldParentPtr("node", node);
|
||||
fn run(ctx: *anyopaque) ?u32 {
|
||||
const self: *TimerCallback = @ptrCast(@alignCast(ctx));
|
||||
if (self.repeat != null) {
|
||||
if (self.window.timers.contains(self.timer_id) == false) {
|
||||
// it was called
|
||||
return null;
|
||||
}
|
||||
} else if (self.window.timers.remove(self.timer_id) == false) {
|
||||
// it was cancelled
|
||||
return null;
|
||||
}
|
||||
|
||||
var result: Function.Result = undefined;
|
||||
|
||||
@@ -396,133 +439,12 @@ const TimerCallback = struct {
|
||||
});
|
||||
};
|
||||
|
||||
if (self.repeat) |r| {
|
||||
// setInterval
|
||||
repeat_delay.* = r;
|
||||
return;
|
||||
}
|
||||
|
||||
// setTimeout
|
||||
_ = self.window.timers.remove(self.timer_id);
|
||||
return self.repeat;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.Window" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "window.parent === window", "true" },
|
||||
.{ "window.top === window", "true" },
|
||||
}, .{});
|
||||
|
||||
// requestAnimationFrame should be able to wait by recursively calling itself
|
||||
// Note however that we in this test do not wait as the request is just send to the browser
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ let start = 0;
|
||||
\\ function step(timestamp) {
|
||||
\\ start = timestamp;
|
||||
\\ }
|
||||
,
|
||||
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
|
||||
try runner.testCases(&.{
|
||||
.{ "let request_id = requestAnimationFrame(timestamp => {});", null },
|
||||
.{ "cancelAnimationFrame(request_id);", "undefined" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "innerHeight", "1" },
|
||||
.{ "innerWidth", "1" }, // Width is 1 even if there are no elements
|
||||
.{
|
||||
\\ let div1 = document.createElement('div');
|
||||
\\ document.body.appendChild(div1);
|
||||
\\ div1.getClientRects();
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{
|
||||
\\ let div2 = document.createElement('div');
|
||||
\\ document.body.appendChild(div2);
|
||||
\\ div2.getClientRects();
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "innerHeight", "1" },
|
||||
.{ "innerWidth", "2" },
|
||||
}, .{});
|
||||
|
||||
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" },
|
||||
}, .{});
|
||||
}
|
||||
test "Browser: Window" {
|
||||
try testing.htmlRunner("window/window.html");
|
||||
try testing.htmlRunner("window/frames.html");
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
// management.
|
||||
// We replace the libdom default usage of allocations with mimalloc heap
|
||||
// allocation to be able to free all memory used at once, like an arena usage.
|
||||
const std = @import("std");
|
||||
|
||||
const c = @cImport({
|
||||
@cInclude("mimalloc.h");
|
||||
@@ -33,43 +34,43 @@ const Error = error{
|
||||
var heap: ?*c.mi_heap_t = null;
|
||||
|
||||
pub fn create() Error!void {
|
||||
if (heap != null) return Error.HeapNotNull;
|
||||
std.debug.assert(heap == null);
|
||||
heap = c.mi_heap_new();
|
||||
if (heap == null) return Error.HeapNull;
|
||||
std.debug.assert(heap != null);
|
||||
}
|
||||
|
||||
pub fn destroy() void {
|
||||
if (heap == null) return;
|
||||
std.debug.assert(heap != null);
|
||||
c.mi_heap_destroy(heap.?);
|
||||
heap = null;
|
||||
}
|
||||
|
||||
pub export fn m_alloc(size: usize) callconv(.C) ?*anyopaque {
|
||||
if (heap == null) return null;
|
||||
pub export fn m_alloc(size: usize) callconv(.c) ?*anyopaque {
|
||||
std.debug.assert(heap != null);
|
||||
return c.mi_heap_malloc(heap.?, size);
|
||||
}
|
||||
|
||||
pub export fn re_alloc(ptr: ?*anyopaque, size: usize) callconv(.C) ?*anyopaque {
|
||||
if (heap == null) return null;
|
||||
pub export fn re_alloc(ptr: ?*anyopaque, size: usize) callconv(.c) ?*anyopaque {
|
||||
std.debug.assert(heap != null);
|
||||
return c.mi_heap_realloc(heap.?, ptr, size);
|
||||
}
|
||||
|
||||
pub export fn c_alloc(nmemb: usize, size: usize) callconv(.C) ?*anyopaque {
|
||||
if (heap == null) return null;
|
||||
pub export fn c_alloc(nmemb: usize, size: usize) callconv(.c) ?*anyopaque {
|
||||
std.debug.assert(heap != null);
|
||||
return c.mi_heap_calloc(heap.?, nmemb, size);
|
||||
}
|
||||
|
||||
pub export fn str_dup(s: [*c]const u8) callconv(.C) [*c]u8 {
|
||||
if (heap == null) return null;
|
||||
pub export fn str_dup(s: [*c]const u8) callconv(.c) [*c]u8 {
|
||||
std.debug.assert(heap != null);
|
||||
return c.mi_heap_strdup(heap.?, s);
|
||||
}
|
||||
|
||||
pub export fn strn_dup(s: [*c]const u8, size: usize) callconv(.C) [*c]u8 {
|
||||
if (heap == null) return null;
|
||||
pub export fn strn_dup(s: [*c]const u8, size: usize) callconv(.c) [*c]u8 {
|
||||
std.debug.assert(heap != null);
|
||||
return c.mi_heap_strndup(heap.?, s, size);
|
||||
}
|
||||
|
||||
// NOOP, use destroy to clear all the memory allocated at once.
|
||||
pub export fn f_ree(_: ?*anyopaque) callconv(.C) void {
|
||||
pub export fn f_ree(_: ?*anyopaque) callconv(.c) void {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -22,13 +22,15 @@ const Allocator = std.mem.Allocator;
|
||||
pub const Mime = struct {
|
||||
content_type: ContentType,
|
||||
params: []const u8 = "",
|
||||
charset: ?[]const u8 = null,
|
||||
// IANA defines max. charset value length as 40.
|
||||
// We keep 41 for null-termination since HTML parser expects in this format.
|
||||
charset: [41]u8 = default_charset,
|
||||
|
||||
pub const unknown = Mime{
|
||||
.params = "",
|
||||
.charset = "",
|
||||
.content_type = .{ .unknown = {} },
|
||||
};
|
||||
/// String "UTF-8" continued by null characters.
|
||||
pub const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36;
|
||||
|
||||
/// Mime with unknown Content-Type, empty params and empty charset.
|
||||
pub const unknown = Mime{ .content_type = .{ .unknown = {} } };
|
||||
|
||||
pub const ContentTypeEnum = enum {
|
||||
text_xml,
|
||||
@@ -52,7 +54,35 @@ pub const Mime = struct {
|
||||
other: struct { type: []const u8, sub_type: []const u8 },
|
||||
};
|
||||
|
||||
pub fn parse(arena: Allocator, input: []u8) !Mime {
|
||||
/// Returns the null-terminated charset value.
|
||||
pub inline fn charsetString(mime: *const Mime) [:0]const u8 {
|
||||
return @ptrCast(&mime.charset);
|
||||
}
|
||||
|
||||
/// Removes quotes of value if quotes are given.
|
||||
///
|
||||
/// Currently we don't validate the charset.
|
||||
/// See section 2.3 Naming Requirements:
|
||||
/// https://datatracker.ietf.org/doc/rfc2978/
|
||||
fn parseCharset(value: []const u8) error{ CharsetTooBig, Invalid }![]const u8 {
|
||||
// Cannot be larger than 40.
|
||||
// https://datatracker.ietf.org/doc/rfc2978/
|
||||
if (value.len > 40) return error.CharsetTooBig;
|
||||
|
||||
// If the first char is a quote, look for a pair.
|
||||
if (value[0] == '"') {
|
||||
if (value.len < 3 or value[value.len - 1] != '"') {
|
||||
return error.Invalid;
|
||||
}
|
||||
|
||||
return value[1 .. value.len - 1];
|
||||
}
|
||||
|
||||
// No quotes.
|
||||
return value;
|
||||
}
|
||||
|
||||
pub fn parse(input: []u8) !Mime {
|
||||
if (input.len > 255) {
|
||||
return error.TooBig;
|
||||
}
|
||||
@@ -69,7 +99,7 @@ pub const Mime = struct {
|
||||
|
||||
const params = trimLeft(normalized[type_len..]);
|
||||
|
||||
var charset: ?[]const u8 = null;
|
||||
var charset: [41]u8 = undefined;
|
||||
|
||||
var it = std.mem.splitScalar(u8, params, ';');
|
||||
while (it.next()) |attr| {
|
||||
@@ -86,7 +116,16 @@ pub const Mime = struct {
|
||||
}, name) orelse continue;
|
||||
|
||||
switch (attribute_name) {
|
||||
.charset => charset = try parseAttributeValue(arena, value),
|
||||
.charset => {
|
||||
if (value.len == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
const attribute_value = try parseCharset(value);
|
||||
@memcpy(charset[0..attribute_value.len], attribute_value);
|
||||
// Null-terminate right after attribute value.
|
||||
charset[attribute_value.len] = 0;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,58 +263,6 @@ pub const Mime = struct {
|
||||
break :blk v;
|
||||
};
|
||||
|
||||
fn parseAttributeValue(arena: Allocator, value: []const u8) ![]const u8 {
|
||||
if (value[0] != '"') {
|
||||
// almost certainly referenced from an http.Request which has its
|
||||
// own lifetime.
|
||||
return arena.dupe(u8, value);
|
||||
}
|
||||
|
||||
// 1 to skip the opening quote
|
||||
var value_pos: usize = 1;
|
||||
var unescaped_len: usize = 0;
|
||||
const last = value.len - 1;
|
||||
|
||||
while (value_pos < value.len) {
|
||||
switch (value[value_pos]) {
|
||||
'"' => break,
|
||||
'\\' => {
|
||||
if (value_pos == last) {
|
||||
return error.Invalid;
|
||||
}
|
||||
const next = value[value_pos + 1];
|
||||
if (T_SPECIAL[next] == false) {
|
||||
return error.Invalid;
|
||||
}
|
||||
value_pos += 2;
|
||||
},
|
||||
else => value_pos += 1,
|
||||
}
|
||||
unescaped_len += 1;
|
||||
}
|
||||
|
||||
if (unescaped_len == 0) {
|
||||
return error.Invalid;
|
||||
}
|
||||
|
||||
value_pos = 1;
|
||||
const owned = try arena.alloc(u8, unescaped_len);
|
||||
for (0..unescaped_len) |i| {
|
||||
switch (value[value_pos]) {
|
||||
'"' => break,
|
||||
'\\' => {
|
||||
owned[i] = value[value_pos + 1];
|
||||
value_pos += 2;
|
||||
},
|
||||
else => |c| {
|
||||
owned[i] = c;
|
||||
value_pos += 1;
|
||||
},
|
||||
}
|
||||
}
|
||||
return owned;
|
||||
}
|
||||
|
||||
const VALID_CODEPOINTS = blk: {
|
||||
var v: [256]bool = undefined;
|
||||
for (0..256) |i| {
|
||||
@@ -306,7 +293,7 @@ pub const Mime = struct {
|
||||
};
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
test "Mime: invalid " {
|
||||
test "Mime: invalid" {
|
||||
defer testing.reset();
|
||||
|
||||
const invalids = [_][]const u8{
|
||||
@@ -324,12 +311,11 @@ test "Mime: invalid " {
|
||||
"text/html; charset=\"\"",
|
||||
"text/html; charset=\"",
|
||||
"text/html; charset=\"\\",
|
||||
"text/html; charset=\"\\a\"", // invalid to escape non special characters
|
||||
};
|
||||
|
||||
for (invalids) |invalid| {
|
||||
const mutable_input = try testing.arena_allocator.dupe(u8, invalid);
|
||||
try testing.expectError(error.Invalid, Mime.parse(undefined, mutable_input));
|
||||
try testing.expectError(error.Invalid, Mime.parse(mutable_input));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,13 +380,25 @@ test "Mime: parse charset" {
|
||||
.content_type = .{ .text_xml = {} },
|
||||
.charset = "utf-8",
|
||||
.params = "charset=\"utf-8\"",
|
||||
}, "text/xml;charset=\"utf-8\"");
|
||||
}, "text/xml;charset=\"UTF-8\"");
|
||||
|
||||
try expect(.{
|
||||
.content_type = .{ .text_html = {} },
|
||||
.charset = "iso-8859-1",
|
||||
.params = "charset=\"iso-8859-1\"",
|
||||
}, "text/html; charset=\"iso-8859-1\"");
|
||||
|
||||
try expect(.{
|
||||
.content_type = .{ .text_html = {} },
|
||||
.charset = "iso-8859-1",
|
||||
.params = "charset=\"iso-8859-1\"",
|
||||
}, "text/html; charset=\"ISO-8859-1\"");
|
||||
|
||||
try expect(.{
|
||||
.content_type = .{ .text_xml = {} },
|
||||
.charset = "\\ \" ",
|
||||
.params = "charset=\"\\\\ \\\" \"",
|
||||
}, "text/xml;charset=\"\\\\ \\\" \" ");
|
||||
.charset = "custom-non-standard-charset-value",
|
||||
.params = "charset=\"custom-non-standard-charset-value\"",
|
||||
}, "text/xml;charset=\"custom-non-standard-charset-value\"");
|
||||
}
|
||||
|
||||
test "Mime: isHTML" {
|
||||
@@ -409,7 +407,7 @@ test "Mime: isHTML" {
|
||||
const isHTML = struct {
|
||||
fn isHTML(expected: bool, input: []const u8) !void {
|
||||
const mutable_input = try testing.arena_allocator.dupe(u8, input);
|
||||
var mime = try Mime.parse(testing.arena_allocator, mutable_input);
|
||||
var mime = try Mime.parse(mutable_input);
|
||||
try testing.expectEqual(expected, mime.isHTML());
|
||||
}
|
||||
}.isHTML;
|
||||
@@ -495,7 +493,7 @@ const Expectation = struct {
|
||||
fn expect(expected: Expectation, input: []const u8) !void {
|
||||
const mutable_input = try testing.arena_allocator.dupe(u8, input);
|
||||
|
||||
const actual = try Mime.parse(testing.arena_allocator, mutable_input);
|
||||
const actual = try Mime.parse(mutable_input);
|
||||
try testing.expectEqual(
|
||||
std.meta.activeTag(expected.content_type),
|
||||
std.meta.activeTag(actual.content_type),
|
||||
@@ -513,8 +511,10 @@ fn expect(expected: Expectation, input: []const u8) !void {
|
||||
try testing.expectEqual(expected.params, actual.params);
|
||||
|
||||
if (expected.charset) |ec| {
|
||||
try testing.expectEqual(ec, actual.charset.?);
|
||||
// We remove the null characters for testing purposes here.
|
||||
try testing.expectEqual(ec, actual.charsetString()[0..ec.len]);
|
||||
} else {
|
||||
try testing.expectEqual(null, actual.charset);
|
||||
const m: Mime = .unknown;
|
||||
try testing.expectEqual(m.charsetString(), actual.charsetString());
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1465
src/browser/page.zig
1465
src/browser/page.zig
File diff suppressed because it is too large
Load Diff
@@ -1,671 +0,0 @@
|
||||
// fetch.js code comes from
|
||||
// https://github.com/JakeChampion/fetch/blob/main/fetch.js
|
||||
//
|
||||
// The original code source is available in MIT license.
|
||||
//
|
||||
// The script comes from the built version from npm.
|
||||
// You can get the package with the command:
|
||||
//
|
||||
// wget $(npm view whatwg-fetch dist.tarball)
|
||||
//
|
||||
// The source is the content of `package/dist/fetch.umd.js` file.
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
||||
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
||||
(factory((global.WHATWGFetch = {})));
|
||||
}(this, (function (exports) { 'use strict';
|
||||
|
||||
/* eslint-disable no-prototype-builtins */
|
||||
var g =
|
||||
(typeof globalThis !== 'undefined' && globalThis) ||
|
||||
(typeof self !== 'undefined' && self) ||
|
||||
// eslint-disable-next-line no-undef
|
||||
(typeof global !== 'undefined' && global) ||
|
||||
{};
|
||||
|
||||
var support = {
|
||||
searchParams: 'URLSearchParams' in g,
|
||||
iterable: 'Symbol' in g && 'iterator' in Symbol,
|
||||
blob:
|
||||
'FileReader' in g &&
|
||||
'Blob' in g &&
|
||||
(function() {
|
||||
try {
|
||||
new Blob();
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
})(),
|
||||
formData: 'FormData' in g,
|
||||
|
||||
// Arraybuffer is available but xhr doesn't implement it for now.
|
||||
// arrayBuffer: 'ArrayBuffer' in g
|
||||
arrayBuffer: false
|
||||
};
|
||||
|
||||
function isDataView(obj) {
|
||||
return obj && DataView.prototype.isPrototypeOf(obj)
|
||||
}
|
||||
|
||||
if (support.arrayBuffer) {
|
||||
var viewClasses = [
|
||||
'[object Int8Array]',
|
||||
'[object Uint8Array]',
|
||||
'[object Uint8ClampedArray]',
|
||||
'[object Int16Array]',
|
||||
'[object Uint16Array]',
|
||||
'[object Int32Array]',
|
||||
'[object Uint32Array]',
|
||||
'[object Float32Array]',
|
||||
'[object Float64Array]'
|
||||
];
|
||||
|
||||
var isArrayBufferView =
|
||||
ArrayBuffer.isView ||
|
||||
function(obj) {
|
||||
return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeName(name) {
|
||||
if (typeof name !== 'string') {
|
||||
name = String(name);
|
||||
}
|
||||
if (/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(name) || name === '') {
|
||||
throw new TypeError('Invalid character in header field name: "' + name + '"')
|
||||
}
|
||||
return name.toLowerCase()
|
||||
}
|
||||
|
||||
function normalizeValue(value) {
|
||||
if (typeof value !== 'string') {
|
||||
value = String(value);
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// Build a destructive iterator for the value list
|
||||
function iteratorFor(items) {
|
||||
var iterator = {
|
||||
next: function() {
|
||||
var value = items.shift();
|
||||
return {done: value === undefined, value: value}
|
||||
}
|
||||
};
|
||||
|
||||
if (support.iterable) {
|
||||
iterator[Symbol.iterator] = function() {
|
||||
return iterator
|
||||
};
|
||||
}
|
||||
|
||||
return iterator
|
||||
}
|
||||
|
||||
function Headers(headers) {
|
||||
this.map = {};
|
||||
|
||||
if (headers instanceof Headers) {
|
||||
headers.forEach(function(value, name) {
|
||||
this.append(name, value);
|
||||
}, this);
|
||||
} else if (Array.isArray(headers)) {
|
||||
headers.forEach(function(header) {
|
||||
if (header.length != 2) {
|
||||
throw new TypeError('Headers constructor: expected name/value pair to be length 2, found' + header.length)
|
||||
}
|
||||
this.append(header[0], header[1]);
|
||||
}, this);
|
||||
} else if (headers) {
|
||||
Object.getOwnPropertyNames(headers).forEach(function(name) {
|
||||
this.append(name, headers[name]);
|
||||
}, this);
|
||||
}
|
||||
}
|
||||
|
||||
Headers.prototype.append = function(name, value) {
|
||||
name = normalizeName(name);
|
||||
value = normalizeValue(value);
|
||||
var oldValue = this.map[name];
|
||||
this.map[name] = oldValue ? oldValue + ', ' + value : value;
|
||||
};
|
||||
|
||||
Headers.prototype['delete'] = function(name) {
|
||||
delete this.map[normalizeName(name)];
|
||||
};
|
||||
|
||||
Headers.prototype.get = function(name) {
|
||||
name = normalizeName(name);
|
||||
return this.has(name) ? this.map[name] : null
|
||||
};
|
||||
|
||||
Headers.prototype.has = function(name) {
|
||||
return this.map.hasOwnProperty(normalizeName(name))
|
||||
};
|
||||
|
||||
Headers.prototype.set = function(name, value) {
|
||||
this.map[normalizeName(name)] = normalizeValue(value);
|
||||
};
|
||||
|
||||
Headers.prototype.forEach = function(callback, thisArg) {
|
||||
for (var name in this.map) {
|
||||
if (this.map.hasOwnProperty(name)) {
|
||||
callback.call(thisArg, this.map[name], name, this);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Headers.prototype.keys = function() {
|
||||
var items = [];
|
||||
this.forEach(function(value, name) {
|
||||
items.push(name);
|
||||
});
|
||||
return iteratorFor(items)
|
||||
};
|
||||
|
||||
Headers.prototype.values = function() {
|
||||
var items = [];
|
||||
this.forEach(function(value) {
|
||||
items.push(value);
|
||||
});
|
||||
return iteratorFor(items)
|
||||
};
|
||||
|
||||
Headers.prototype.entries = function() {
|
||||
var items = [];
|
||||
this.forEach(function(value, name) {
|
||||
items.push([name, value]);
|
||||
});
|
||||
return iteratorFor(items)
|
||||
};
|
||||
|
||||
if (support.iterable) {
|
||||
Headers.prototype[Symbol.iterator] = Headers.prototype.entries;
|
||||
}
|
||||
|
||||
function consumed(body) {
|
||||
if (body._noBody) return
|
||||
if (body.bodyUsed) {
|
||||
return Promise.reject(new TypeError('Already read'))
|
||||
}
|
||||
body.bodyUsed = true;
|
||||
}
|
||||
|
||||
function fileReaderReady(reader) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
reader.onload = function() {
|
||||
resolve(reader.result);
|
||||
};
|
||||
reader.onerror = function() {
|
||||
reject(reader.error);
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
function readBlobAsArrayBuffer(blob) {
|
||||
var reader = new FileReader();
|
||||
var promise = fileReaderReady(reader);
|
||||
reader.readAsArrayBuffer(blob);
|
||||
return promise
|
||||
}
|
||||
|
||||
function readBlobAsText(blob) {
|
||||
var reader = new FileReader();
|
||||
var promise = fileReaderReady(reader);
|
||||
var match = /charset=([A-Za-z0-9_-]+)/.exec(blob.type);
|
||||
var encoding = match ? match[1] : 'utf-8';
|
||||
reader.readAsText(blob, encoding);
|
||||
return promise
|
||||
}
|
||||
|
||||
function readArrayBufferAsText(buf) {
|
||||
var view = new Uint8Array(buf);
|
||||
var chars = new Array(view.length);
|
||||
|
||||
for (var i = 0; i < view.length; i++) {
|
||||
chars[i] = String.fromCharCode(view[i]);
|
||||
}
|
||||
return chars.join('')
|
||||
}
|
||||
|
||||
function bufferClone(buf) {
|
||||
if (buf.slice) {
|
||||
return buf.slice(0)
|
||||
} else {
|
||||
var view = new Uint8Array(buf.byteLength);
|
||||
view.set(new Uint8Array(buf));
|
||||
return view.buffer
|
||||
}
|
||||
}
|
||||
|
||||
function Body() {
|
||||
this.bodyUsed = false;
|
||||
|
||||
this._initBody = function(body) {
|
||||
/*
|
||||
fetch-mock wraps the Response object in an ES6 Proxy to
|
||||
provide useful test harness features such as flush. However, on
|
||||
ES5 browsers without fetch or Proxy support pollyfills must be used;
|
||||
the proxy-pollyfill is unable to proxy an attribute unless it exists
|
||||
on the object before the Proxy is created. This change ensures
|
||||
Response.bodyUsed exists on the instance, while maintaining the
|
||||
semantic of setting Request.bodyUsed in the constructor before
|
||||
_initBody is called.
|
||||
*/
|
||||
// eslint-disable-next-line no-self-assign
|
||||
this.bodyUsed = this.bodyUsed;
|
||||
this._bodyInit = body;
|
||||
if (!body) {
|
||||
this._noBody = true;
|
||||
this._bodyText = '';
|
||||
} else if (typeof body === 'string') {
|
||||
this._bodyText = body;
|
||||
} else if (support.blob && Blob.prototype.isPrototypeOf(body)) {
|
||||
this._bodyBlob = body;
|
||||
} else if (support.formData && FormData.prototype.isPrototypeOf(body)) {
|
||||
this._bodyFormData = body;
|
||||
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
|
||||
this._bodyText = body.toString();
|
||||
} else if (support.arrayBuffer && support.blob && isDataView(body)) {
|
||||
this._bodyArrayBuffer = bufferClone(body.buffer);
|
||||
// IE 10-11 can't handle a DataView body.
|
||||
this._bodyInit = new Blob([this._bodyArrayBuffer]);
|
||||
} else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) {
|
||||
this._bodyArrayBuffer = bufferClone(body);
|
||||
} else {
|
||||
this._bodyText = body = Object.prototype.toString.call(body);
|
||||
}
|
||||
|
||||
if (!this.headers.get('content-type')) {
|
||||
if (typeof body === 'string') {
|
||||
this.headers.set('content-type', 'text/plain;charset=UTF-8');
|
||||
} else if (this._bodyBlob && this._bodyBlob.type) {
|
||||
this.headers.set('content-type', this._bodyBlob.type);
|
||||
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
|
||||
this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (support.blob) {
|
||||
this.blob = function() {
|
||||
var rejected = consumed(this);
|
||||
if (rejected) {
|
||||
return rejected
|
||||
}
|
||||
|
||||
if (this._bodyBlob) {
|
||||
return Promise.resolve(this._bodyBlob)
|
||||
} else if (this._bodyArrayBuffer) {
|
||||
return Promise.resolve(new Blob([this._bodyArrayBuffer]))
|
||||
} else if (this._bodyFormData) {
|
||||
throw new Error('could not read FormData body as blob')
|
||||
} else {
|
||||
return Promise.resolve(new Blob([this._bodyText]))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.arrayBuffer = function() {
|
||||
if (this._bodyArrayBuffer) {
|
||||
var isConsumed = consumed(this);
|
||||
if (isConsumed) {
|
||||
return isConsumed
|
||||
} else if (ArrayBuffer.isView(this._bodyArrayBuffer)) {
|
||||
return Promise.resolve(
|
||||
this._bodyArrayBuffer.buffer.slice(
|
||||
this._bodyArrayBuffer.byteOffset,
|
||||
this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength
|
||||
)
|
||||
)
|
||||
} else {
|
||||
return Promise.resolve(this._bodyArrayBuffer)
|
||||
}
|
||||
} else if (support.blob) {
|
||||
return this.blob().then(readBlobAsArrayBuffer)
|
||||
} else {
|
||||
throw new Error('could not read as ArrayBuffer')
|
||||
}
|
||||
};
|
||||
|
||||
this.text = function() {
|
||||
var rejected = consumed(this);
|
||||
if (rejected) {
|
||||
return rejected
|
||||
}
|
||||
|
||||
if (this._bodyBlob) {
|
||||
return readBlobAsText(this._bodyBlob)
|
||||
} else if (this._bodyArrayBuffer) {
|
||||
return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer))
|
||||
} else if (this._bodyFormData) {
|
||||
throw new Error('could not read FormData body as text')
|
||||
} else {
|
||||
return Promise.resolve(this._bodyText)
|
||||
}
|
||||
};
|
||||
|
||||
if (support.formData) {
|
||||
this.formData = function() {
|
||||
return this.text().then(decode)
|
||||
};
|
||||
}
|
||||
|
||||
this.json = function() {
|
||||
return this.text().then(JSON.parse)
|
||||
};
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
// HTTP methods whose capitalization should be normalized
|
||||
var methods = ['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE'];
|
||||
|
||||
function normalizeMethod(method) {
|
||||
var upcased = method.toUpperCase();
|
||||
return methods.indexOf(upcased) > -1 ? upcased : method
|
||||
}
|
||||
|
||||
function Request(input, options) {
|
||||
if (!(this instanceof Request)) {
|
||||
throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.')
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
var body = options.body;
|
||||
|
||||
if (input instanceof Request) {
|
||||
if (input.bodyUsed) {
|
||||
throw new TypeError('Already read')
|
||||
}
|
||||
this.url = input.url;
|
||||
this.credentials = input.credentials;
|
||||
if (!options.headers) {
|
||||
this.headers = new Headers(input.headers);
|
||||
}
|
||||
this.method = input.method;
|
||||
this.mode = input.mode;
|
||||
this.signal = input.signal;
|
||||
if (!body && input._bodyInit != null) {
|
||||
body = input._bodyInit;
|
||||
input.bodyUsed = true;
|
||||
}
|
||||
} else {
|
||||
this.url = String(input);
|
||||
}
|
||||
|
||||
this.credentials = options.credentials || this.credentials || 'same-origin';
|
||||
if (options.headers || !this.headers) {
|
||||
this.headers = new Headers(options.headers);
|
||||
}
|
||||
this.method = normalizeMethod(options.method || this.method || 'GET');
|
||||
this.mode = options.mode || this.mode || null;
|
||||
this.signal = options.signal || this.signal || (function () {
|
||||
if ('AbortController' in g) {
|
||||
var ctrl = new AbortController();
|
||||
return ctrl.signal;
|
||||
}
|
||||
}());
|
||||
this.referrer = null;
|
||||
|
||||
if ((this.method === 'GET' || this.method === 'HEAD') && body) {
|
||||
throw new TypeError('Body not allowed for GET or HEAD requests')
|
||||
}
|
||||
this._initBody(body);
|
||||
|
||||
if (this.method === 'GET' || this.method === 'HEAD') {
|
||||
if (options.cache === 'no-store' || options.cache === 'no-cache') {
|
||||
// Search for a '_' parameter in the query string
|
||||
var reParamSearch = /([?&])_=[^&]*/;
|
||||
if (reParamSearch.test(this.url)) {
|
||||
// If it already exists then set the value with the current time
|
||||
this.url = this.url.replace(reParamSearch, '$1_=' + new Date().getTime());
|
||||
} else {
|
||||
// Otherwise add a new '_' parameter to the end with the current time
|
||||
var reQueryString = /\?/;
|
||||
this.url += (reQueryString.test(this.url) ? '&' : '?') + '_=' + new Date().getTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Request.prototype.clone = function() {
|
||||
return new Request(this, {body: this._bodyInit})
|
||||
};
|
||||
|
||||
function decode(body) {
|
||||
var form = new FormData();
|
||||
body
|
||||
.trim()
|
||||
.split('&')
|
||||
.forEach(function(bytes) {
|
||||
if (bytes) {
|
||||
var split = bytes.split('=');
|
||||
var name = split.shift().replace(/\+/g, ' ');
|
||||
var value = split.join('=').replace(/\+/g, ' ');
|
||||
form.append(decodeURIComponent(name), decodeURIComponent(value));
|
||||
}
|
||||
});
|
||||
return form
|
||||
}
|
||||
|
||||
function parseHeaders(rawHeaders) {
|
||||
var headers = new Headers();
|
||||
// Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space
|
||||
// https://tools.ietf.org/html/rfc7230#section-3.2
|
||||
var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' ');
|
||||
// Avoiding split via regex to work around a common IE11 bug with the core-js 3.6.0 regex polyfill
|
||||
// https://github.com/github/fetch/issues/748
|
||||
// https://github.com/zloirock/core-js/issues/751
|
||||
preProcessedHeaders
|
||||
.split('\r')
|
||||
.map(function(header) {
|
||||
return header.indexOf('\n') === 0 ? header.substr(1, header.length) : header
|
||||
})
|
||||
.forEach(function(line) {
|
||||
var parts = line.split(':');
|
||||
var key = parts.shift().trim();
|
||||
if (key) {
|
||||
var value = parts.join(':').trim();
|
||||
try {
|
||||
headers.append(key, value);
|
||||
} catch (error) {
|
||||
console.warn('Response ' + error.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
return headers
|
||||
}
|
||||
|
||||
Body.call(Request.prototype);
|
||||
|
||||
function Response(bodyInit, options) {
|
||||
if (!(this instanceof Response)) {
|
||||
throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.')
|
||||
}
|
||||
if (!options) {
|
||||
options = {};
|
||||
}
|
||||
|
||||
this.type = 'default';
|
||||
this.status = options.status === undefined ? 200 : options.status;
|
||||
if (this.status < 200 || this.status > 599) {
|
||||
throw new RangeError("Failed to construct 'Response': The status provided (0) is outside the range [200, 599].")
|
||||
}
|
||||
this.ok = this.status >= 200 && this.status < 300;
|
||||
this.statusText = options.statusText === undefined ? '' : '' + options.statusText;
|
||||
this.headers = new Headers(options.headers);
|
||||
this.url = options.url || '';
|
||||
this._initBody(bodyInit);
|
||||
}
|
||||
|
||||
Body.call(Response.prototype);
|
||||
|
||||
Response.prototype.clone = function() {
|
||||
return new Response(this._bodyInit, {
|
||||
status: this.status,
|
||||
statusText: this.statusText,
|
||||
headers: new Headers(this.headers),
|
||||
url: this.url
|
||||
})
|
||||
};
|
||||
|
||||
Response.error = function() {
|
||||
var response = new Response(null, {status: 200, statusText: ''});
|
||||
response.ok = false;
|
||||
response.status = 0;
|
||||
response.type = 'error';
|
||||
return response
|
||||
};
|
||||
|
||||
var redirectStatuses = [301, 302, 303, 307, 308];
|
||||
|
||||
Response.redirect = function(url, status) {
|
||||
if (redirectStatuses.indexOf(status) === -1) {
|
||||
throw new RangeError('Invalid status code')
|
||||
}
|
||||
|
||||
return new Response(null, {status: status, headers: {location: url}})
|
||||
};
|
||||
|
||||
exports.DOMException = g.DOMException;
|
||||
try {
|
||||
new exports.DOMException();
|
||||
} catch (err) {
|
||||
exports.DOMException = function(message, name) {
|
||||
this.message = message;
|
||||
this.name = name;
|
||||
var error = Error(message);
|
||||
this.stack = error.stack;
|
||||
};
|
||||
exports.DOMException.prototype = Object.create(Error.prototype);
|
||||
exports.DOMException.prototype.constructor = exports.DOMException;
|
||||
}
|
||||
|
||||
function fetch(input, init) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var request = new Request(input, init);
|
||||
|
||||
if (request.signal && request.signal.aborted) {
|
||||
return reject(new exports.DOMException('Aborted', 'AbortError'))
|
||||
}
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
|
||||
function abortXhr() {
|
||||
xhr.abort();
|
||||
}
|
||||
|
||||
xhr.onload = function() {
|
||||
var options = {
|
||||
statusText: xhr.statusText,
|
||||
headers: parseHeaders(xhr.getAllResponseHeaders() || '')
|
||||
};
|
||||
// This check if specifically for when a user fetches a file locally from the file system
|
||||
// Only if the status is out of a normal range
|
||||
if (request.url.indexOf('file://') === 0 && (xhr.status < 200 || xhr.status > 599)) {
|
||||
options.status = 200;
|
||||
} else {
|
||||
options.status = xhr.status;
|
||||
}
|
||||
options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL');
|
||||
var body = 'response' in xhr ? xhr.response : xhr.responseText;
|
||||
setTimeout(function() {
|
||||
resolve(new Response(body, options));
|
||||
}, 0);
|
||||
};
|
||||
|
||||
xhr.onerror = function() {
|
||||
setTimeout(function() {
|
||||
reject(new TypeError('Network request failed'));
|
||||
}, 0);
|
||||
};
|
||||
|
||||
xhr.ontimeout = function() {
|
||||
setTimeout(function() {
|
||||
reject(new TypeError('Network request timed out'));
|
||||
}, 0);
|
||||
};
|
||||
|
||||
xhr.onabort = function() {
|
||||
setTimeout(function() {
|
||||
reject(new exports.DOMException('Aborted', 'AbortError'));
|
||||
}, 0);
|
||||
};
|
||||
|
||||
function fixUrl(url) {
|
||||
try {
|
||||
return url === '' && g.location.href ? g.location.href : url
|
||||
} catch (e) {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
xhr.open(request.method, fixUrl(request.url), true);
|
||||
|
||||
if (request.credentials === 'include') {
|
||||
xhr.withCredentials = true;
|
||||
} else if (request.credentials === 'omit') {
|
||||
xhr.withCredentials = false;
|
||||
}
|
||||
|
||||
if ('responseType' in xhr) {
|
||||
if (support.blob) {
|
||||
xhr.responseType = 'blob';
|
||||
} else if (
|
||||
support.arrayBuffer
|
||||
) {
|
||||
xhr.responseType = 'arraybuffer';
|
||||
}
|
||||
}
|
||||
|
||||
if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers || (g.Headers && init.headers instanceof g.Headers))) {
|
||||
var names = [];
|
||||
Object.getOwnPropertyNames(init.headers).forEach(function(name) {
|
||||
names.push(normalizeName(name));
|
||||
xhr.setRequestHeader(name, normalizeValue(init.headers[name]));
|
||||
});
|
||||
request.headers.forEach(function(value, name) {
|
||||
if (names.indexOf(name) === -1) {
|
||||
xhr.setRequestHeader(name, value);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
request.headers.forEach(function(value, name) {
|
||||
xhr.setRequestHeader(name, value);
|
||||
});
|
||||
}
|
||||
|
||||
if (request.signal) {
|
||||
request.signal.addEventListener('abort', abortXhr);
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
// DONE (success or failure)
|
||||
if (xhr.readyState === 4) {
|
||||
request.signal.removeEventListener('abort', abortXhr);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit);
|
||||
})
|
||||
}
|
||||
|
||||
fetch.polyfill = true;
|
||||
|
||||
if (!g.fetch) {
|
||||
g.fetch = fetch;
|
||||
g.Headers = Headers;
|
||||
g.Request = Request;
|
||||
g.Response = Response;
|
||||
}
|
||||
|
||||
exports.Headers = Headers;
|
||||
exports.Request = Request;
|
||||
exports.Response = Response;
|
||||
exports.fetch = fetch;
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
|
||||
})));
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user