264 Commits
v0.2.5 ... v0.1

Author SHA1 Message Date
Pierre Tachoire
4d0126d953 Merge pull request #1337 from lightpanda-io/ci-check
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
e2e-integration-test / zig build release (push) Has been cancelled
e2e-integration-test / demo-integration-scripts (push) Has been cancelled
ci: try to fix tag
2026-01-07 20:38:28 +01:00
Pierre Tachoire
b79193f621 ci: try to fix tag 2026-01-07 20:37:34 +01:00
Pierre Tachoire
1d0f38b29f ci: fix tag detection 2026-01-07 19:11:46 +01:00
Pierre Tachoire
8fcf12f74c Merge pull request #1336 from lightpanda-io/nightly-build-tags
ci: handle release tags and nightly builds
2026-01-07 18:55:09 +01:00
Pierre Tachoire
c26938c333 handle release tags and nightly builds 2026-01-07 18:46:26 +01:00
Pierre Tachoire
c9394fbc43 Merge pull request #1298 from arrufat/fix-makefile-escape-codes
Some checks failed
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
e2e-integration-test / zig build release (push) Has been cancelled
e2e-integration-test / demo-integration-scripts (push) Has been cancelled
build: standardize ansi escape sequences in makefile
2025-12-29 12:54:09 +01:00
Adrià Arrufat
a6cc21b449 build: standardize ansi escape sequences in makefile
Replaces the use of `\e` with the standard octal representation `\033`
for ANSI color codes in all Makefile targets.
2025-12-27 00:31:23 +01:00
Karl Seguin
e072ff3c4a Merge pull request #1293 from lightpanda-io/v8-json-parse
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
e2e-integration-test / zig build release (push) Has been cancelled
e2e-integration-test / demo-integration-scripts (push) Has been cancelled
Use V8 to parse JSON with fetch/xhr
2025-12-25 20:47:55 +08:00
Pierre Tachoire
5e4e4dcbc6 split Value.fromJson into Value.persist 2025-12-25 12:07:32 +01:00
Pierre Tachoire
beef458c3c js: persist value returned by v8 JSON parser 2025-12-24 16:36:24 +01:00
Pierre Tachoire
1dcccef080 use V8 json parser with xhr/fetch webAPIs
The pure zig JSON parser didn't generate the same type of values than JS
JSON.parse command.
Using directly V8's JSON parser gives the assurance to have the right
JS types.
Moreover, it avoid data transformations between Zig and V8.
2025-12-24 15:35:44 +01:00
Pierre Tachoire
66342b35db add test for big json number with fetch/xhr 2025-12-24 15:35:43 +01:00
Karl Seguin
0efab26c7b Merge pull request #1281 from lightpanda-io/page-reset-libdom
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
e2e-integration-test / zig build release (push) Has been cancelled
e2e-integration-test / demo-integration-scripts (push) Has been cancelled
reset libdom memory on page.reset
2025-12-20 17:11:20 +08:00
Pierre Tachoire
85bf8669dd reset libdom memeory on page.reset 2025-12-19 17:32:29 +01:00
Pierre Tachoire
a69efb9d3f Merge pull request #1278 from lightpanda-io/cdp-page-close
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
e2e-integration-test / zig build release (push) Has been cancelled
e2e-integration-test / demo-integration-scripts (push) Has been cancelled
cdp: add page.Close
2025-12-18 13:12:20 +01:00
Pierre Tachoire
e97c9959fa cdp: add page.Close 2025-12-18 10:46:54 +01:00
Karl Seguin
68e9d3b9ea Merge pull request #1275 from lightpanda-io/wpt-mjs
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
e2e-integration-test / zig build release (push) Has been cancelled
e2e-integration-test / demo-integration-scripts (push) Has been cancelled
wpt: allow mjs serve through test web server
2025-12-16 06:49:01 +08:00
Pierre Tachoire
0c1c26462c Merge pull request #1274 from lightpanda-io/document-write
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
implement document.write
2025-12-15 09:03:57 +01:00
Pierre Tachoire
ce85fa53b0 wpt: allow mjs serve through test web server 2025-12-15 08:50:12 +01:00
Pierre Tachoire
d8bbaff506 _open does the page.open test directly 2025-12-15 08:28:20 +01:00
Pierre Tachoire
447ef83e0a Merge pull request #1265 from lightpanda-io/network-event
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
cdp: improve network's events
2025-12-15 08:26:35 +01:00
Pierre Tachoire
6d4966e83d implement document.write 2025-12-13 14:21:45 +01:00
Pierre Tachoire
42440f1503 fix mime.charsetString() 2025-12-12 18:00:20 +01:00
Pierre Tachoire
26827efe34 cdp: use same value for requestId and loaderId
For all events regarding an HTTP request, the values of requestId
and loaderId must be the same.
2025-12-12 17:04:18 +01:00
Pierre Tachoire
e2682ab9fe cdp: dispatch Page.navigate response after navigation 2025-12-11 17:51:17 +01:00
Pierre Tachoire
34518dfa98 cdp: add missing fields to Network.requestWillBeSent 2025-12-10 18:22:44 +01:00
Pierre Tachoire
9579f727b3 cdp: add mimeType and charset to Network.Response 2025-12-10 18:21:32 +01:00
Pierre Tachoire
7c976209cc Merge pull request #1263 from lightpanda-io/nightly-integration
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
e2e-integration-test / zig build release (push) Has been cancelled
e2e-integration-test / demo-integration-scripts (push) Has been cancelled
ci: add nightly integration test
2025-12-09 14:57:16 +01:00
Pierre Tachoire
e76b9936ea ci: add nightly integration test 2025-12-09 14:41:45 +01:00
Pierre Tachoire
b0daf2f96e Merge pull request #1262 from lightpanda-io/cla-allowlist-update
cla: update allow list
2025-12-09 14:28:06 +01:00
Pierre Tachoire
d2e7c41d67 Merge pull request #1261 from lightpanda-io/build-macintel
ci: use macos-14-intel for building macos x86
2025-12-09 14:17:25 +01:00
Pierre Tachoire
2a0c8f01b9 cla: update allow list 2025-12-09 14:16:53 +01:00
Pierre Tachoire
83378a68c8 Merge pull request #1258 from lightpanda-io/wp/mrdimidium/zig-versions
Get rid of copies of the Zig version
2025-12-09 14:15:36 +01:00
Pierre Tachoire
5382e59d71 ci: use macos-14-intel for building macos x86
macos-13 is unsupported. We Have to switch for payed instance.
see https://github.com/actions/runner-images/issues/13046
2025-12-09 14:05:00 +01:00
Nikolay Govorov
bb7da6aafb Get rid of copies of the Zig version 2025-12-09 07:43:06 +00:00
Pierre Tachoire
f7fd68ca3d Merge pull request #1257 from lightpanda-io/update-readme
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
adjust README deps
2025-12-09 08:15:41 +01:00
Pierre Tachoire
1ab6659c04 adjust README deps 2025-12-09 08:14:19 +01:00
Pierre Tachoire
4893a79d37 Merge pull request #1236 from lightpanda-io/v8-build-with-zig-gclient-ci
V8 build with zig gclient ci
2025-12-09 08:10:04 +01:00
Karl Seguin
00d6195590 Merge pull request #1256 from lightpanda-io/docker-again
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
A bit more changes in Dockerfile
2025-12-09 06:49:17 +08:00
Karl Seguin
100b2a6a95 Merge pull request #1254 from lightpanda-io/cdp-request-node
cdp: implement DOM.requestNode
2025-12-09 06:48:48 +08:00
Pierre Tachoire
b317bf7854 docker: enable log level info by default 2025-12-08 18:27:56 +01:00
Pierre Tachoire
dea6156a2b docker: use debian slim for building 2025-12-08 18:27:19 +01:00
Pierre Tachoire
d8d07fb095 docker: copy tini from another base
And avoid having apt data in the final container
2025-12-08 18:26:37 +01:00
Pierre Tachoire
a8437afadd Merge pull request #1255 from lightpanda-io/wp/mrdimidium/init-for-docker
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Add 'tini' as init process for docker
2025-12-08 18:14:26 +01:00
Nikolay Govorov
1fd61ce6a4 Add 'tini' as init process for docker 2025-12-08 14:40:17 +00:00
Pierre Tachoire
ea757407f5 cdp: implement DOM.requestNode 2025-12-08 15:27:34 +01:00
Pierre Tachoire
00e18e24b9 Merge pull request #1251 from axlEscalada/axlescalada/fix-alignment-event-target
fix alignment event target
2025-12-08 14:45:17 +01:00
axl
1927a16089 feat: test for event target 2025-12-07 21:37:24 -03:00
axl
35da652a5d fix: initialize event target 2025-12-07 21:30:04 -03:00
Karl Seguin
ed3a562d84 Merge pull request #1247 from arjunkomath/main
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
feat: support listening on IPv6
2025-12-08 07:20:21 +08:00
Arjun Komath
fd5fbe3ea1 feat: support listening on ipv6 2025-12-06 17:03:34 +11:00
Muki Kiboigo
641c6c3f42 update to new zig-v8-fork 2025-12-05 07:30:57 -08:00
Karl Seguin
cdd7399016 Merge pull request #1243 from lightpanda-io/wp/mrdimidium/graceful-shutdown
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Add a synchronous signal handler for graceful shutdown
2025-12-05 07:34:22 +08:00
Nikolay Govorov
74eee75e47 Add a synchronous signal handler for graceful shutdown 2025-12-04 19:17:32 +00:00
Pierre Tachoire
2e45d547c2 bump zig v8 version 2025-12-04 09:04:38 +01:00
Pierre Tachoire
28e1d6e8c8 bump zig-v8 2025-12-04 09:03:41 +01:00
Muki Kiboigo
8837193643 point zig directly to cached libc_v8.a 2025-12-04 09:01:15 +01:00
Muki Kiboigo
c5ab10cf43 use new version of zig-v8-fork 2025-12-04 09:01:14 +01:00
Pierre Tachoire
90f6495e93 ci: update install workflow according to v8 changes 2025-12-04 09:01:14 +01:00
Pierre Tachoire
4cbd1da749 update v8 to gclient branch 2025-12-04 09:01:14 +01:00
Muki Kiboigo
9477a8be42 wip use local zig-v8-fork 2025-12-04 09:01:13 +01:00
Muki Kiboigo
b0f0df5632 use zig-v8-fork gclient commit 2025-12-04 09:01:13 +01:00
Muki Kiboigo
d2c90486da use prebuilt v8 2025-12-04 09:01:13 +01:00
Muki Kiboigo
3d7801df05 build v8 with zig 2025-12-04 09:01:12 +01:00
Pierre Tachoire
c962858f61 Merge pull request #1231 from lightpanda-io/input-click
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
set focus on input click
2025-11-28 12:39:42 +01:00
Pierre Tachoire
b0d9ebaf3a handle key down for more input types 2025-11-28 11:36:59 +01:00
Pierre Tachoire
9881a4d288 improve key down log 2025-11-28 11:36:59 +01:00
Pierre Tachoire
96e80cc2fc form: enter must send the form for all input 2025-11-28 11:36:58 +01:00
Pierre Tachoire
7887ca6a45 improve input log 2025-11-28 11:36:58 +01:00
Pierre Tachoire
633aee9439 change the focus on click event 2025-11-28 11:36:58 +01:00
Pierre Tachoire
27a85c1241 add .input scope to logs
And Add debug messages for click and key down events callback on page.
2025-11-28 11:36:57 +01:00
Halil Durak
2e4996d6c9 Merge pull request #1237 from lightpanda-io/nikneym/curl-use-boringssl
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Prefer BoringSSL as TLS backend
2025-11-28 10:19:09 +03:00
Halil Durak
3f8ad1ae35 ci: increase e2e-test max memory 2025-11-27 10:53:47 +03:00
Halil Durak
5c71e0f93b wipe Mbed TLS 2025-11-26 16:06:57 +03:00
Halil Durak
a124f5caa9 make BoringSSL the default TLS backend 2025-11-26 12:26:45 +03:00
Halil Durak
96a53c4e97 add an option to build libcurl with BoringSSL 2025-11-26 10:27:25 +03:00
Pierre Tachoire
927cbe7b11 Merge pull request #1227 from lightpanda-io/navigation-process-before-page
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
processNavigation before running page scripts
2025-11-21 10:45:09 +01:00
Karl Seguin
b365ffcc8d Merge pull request #1228 from liveview-native/dynamic-import-fix
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Handle `Evaluating` module status in `_dynamicModuleCallback`
2025-11-21 10:26:58 +08:00
Carson Katri
9d6bc5b615 Fix module evaluation checks 2025-11-20 14:20:33 -05:00
Muki Kiboigo
2b2882c76d processNavigation before running page scripts 2025-11-20 07:55:54 -08:00
Karl Seguin
f058cf0697 Merge pull request #1221 from lightpanda-io/cdp-get-targets
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
cdp: add Target.getTargets
2025-11-20 19:50:17 +08:00
Karl Seguin
346ae14bcd Merge pull request #1222 from lightpanda-io/cdp-multi-attachtotarget
cdp: accept multiple attachToTarget calls
2025-11-20 19:49:18 +08:00
Karl Seguin
c30de2bb32 Merge pull request #1224 from lightpanda-io/accessibility-domain
cdp: add accessibility domain
2025-11-20 19:47:58 +08:00
Karl Seguin
5e43f76a0a Merge pull request #1223 from lightpanda-io/cdp-grantuniversal
cdp: use default value for grantUniveralAccess
2025-11-20 19:47:47 +08:00
muki
2b4409248e Merge pull request #1215 from lightpanda-io/misc-navigation-changes
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Assorted Navigation Changes/Fixes
2025-11-19 07:31:52 -08:00
Halil Durak
21464dfa55 Merge pull request #1219 from lightpanda-io/nikneym/rework-types
Refactor `types.zig`
2025-11-19 18:25:04 +03:00
Pierre Tachoire
cf7bddd887 cdp: add accessibility domain 2025-11-19 16:13:35 +01:00
Pierre Tachoire
455fe5d2ba cdp: use default value for grantUniveralAccess
In createIsolatedWorld, we set  a default value to false for optional
grantUniveralAccess parameter.
2025-11-19 16:12:18 +01:00
Pierre Tachoire
b764a7a0dc cdp: return valid url and title for getTargets 2025-11-19 15:58:52 +01:00
Pierre Tachoire
b776cf1647 cdp: add getTargets 2025-11-19 15:39:44 +01:00
Pierre Tachoire
4c37a8e766 cdp: accept multiple attachToTarget calls 2025-11-19 15:26:09 +01:00
Halil Durak
707db8173f prefer an enum instead of struct declarations for JS API table
Also adds utility functions (namely `has`, `getIndex` and `getId`) to work easily with types.
2025-11-19 13:53:06 +03:00
Pierre Tachoire
1412c5821c Merge pull request #1218 from lightpanda-io/cdp-targetinfo-title
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
cdp: return document's title on targetinfo
2025-11-19 09:50:44 +01:00
Pierre Tachoire
4f236d0b30 cdp: return document's title on targetinfo 2025-11-19 09:11:48 +01:00
Pierre Tachoire
b18ec4dee3 Merge pull request #1216 from lightpanda-io/cdp-createtarget-navigate
cdp: don't navigate for about:blank
2025-11-19 08:23:32 +01:00
Pierre Tachoire
0e3f8c9e42 cdp: don't navigate for about:blank
If the create target url is `about:blank`, don't navigate.
Indeed, Chrome doesn't navigate if the url is blank.
2025-11-18 18:11:57 +01:00
Pierre Tachoire
c4bf37fb5b Merge pull request #1212 from lightpanda-io/cdp-dom-outerhtml
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
cdp: DOM.getouterHTML
2025-11-17 16:57:51 +01:00
Muki Kiboigo
4fc09eccdf proper handling of history opt in navigate 2025-11-17 06:42:56 -08:00
Muki Kiboigo
67f979be77 update navigation index before currenteventchange 2025-11-17 06:42:56 -08:00
Muki Kiboigo
f475f3440e seperate Navigation State and History State 2025-11-17 06:42:56 -08:00
Muki Kiboigo
56e30a9c97 use replaceEntry in History replaceState 2025-11-17 06:42:52 -08:00
Halil Durak
d3522e0e36 Merge pull request #1213 from lightpanda-io/nikneym/remove-kludge
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
2025-11-17 10:01:26 +03:00
Halil Durak
5417a8d9b0 remove _TYPED_ARRAY_ID_KLUDGE hack
This replaces `_TYPED_ARRAY_ID_KLUDGE` usage with actual types we use for `TypedArray`.
2025-11-14 14:59:28 +03:00
Halil Durak
d15a384f9a Merge pull request #1209 from lightpanda-io/nikneym/webgl-rendering-context
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Dummy `WebGLRenderingContext`
2025-11-14 14:18:46 +03:00
Halil Durak
f419f05a5e support WEBGL_lose_context 2025-11-14 12:18:13 +03:00
Pierre Tachoire
c2827a0f16 cdp: add browser.Close but ignore it 2025-11-13 18:29:38 +01:00
Pierre Tachoire
263dab0bdf cdp: add DOM.getOuterHTML 2025-11-13 18:29:13 +01:00
Halil Durak
3c98e4f71e add WEBGL_debug_renderer_info 2025-11-13 15:40:59 +03:00
Halil Durak
73574dce52 prefer std.meta.fieldNames for creating the array 2025-11-13 15:38:48 +03:00
Halil Durak
c459325a5f update CanvasRenderingContext2D test
Adds the missing RGBA and long digit hex format tests.
2025-11-13 14:55:12 +03:00
Halil Durak
37ac465695 add WebGLRenderingContext test 2025-11-13 14:36:07 +03:00
Halil Durak
a8298a0fda support getSupportedExtensions 2025-11-13 14:35:53 +03:00
Halil Durak
7404b20228 initial effort for WebGLRenderingContext 2025-11-13 12:56:18 +03:00
Pierre Tachoire
b782cc6389 Merge pull request #1199 from lightpanda-io/nikneym/dummy-canvas
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Dummy canvas
2025-11-13 08:28:24 +01:00
Pierre Tachoire
4538464df4 Merge pull request #1205 from lightpanda-io/template-content
handle template's original content
2025-11-13 08:27:46 +01:00
Pierre Tachoire
9081a813e7 Merge pull request #1207 from lightpanda-io/pagetransitionevent
add PageTransitionEvent
2025-11-13 08:27:36 +01:00
muki
0dfd5ce940 Merge pull request #1206 from lightpanda-io/microtask-before-load
Run microtasks before `onload`
2025-11-12 09:06:26 -08:00
Muki Kiboigo
2bbbb4662e fire pageshow after load 2025-11-12 09:04:26 -08:00
Muki Kiboigo
a651c0a2d1 add PageTransitionEvent 2025-11-12 09:04:24 -08:00
Muki Kiboigo
5174212183 run microtasks before firing onload 2025-11-12 08:35:31 -08:00
Halil Durak
d48a6619a3 fix failing isHexColor test 2025-11-12 19:00:33 +03:00
Halil Durak
dd079f0c0e update canvas test 2025-11-12 18:49:13 +03:00
Halil Durak
d193ab6dc0 implement basic support for fillStyle 2025-11-12 18:49:06 +03:00
Halil Durak
4872aabc87 make 6 a valid length for hex colors
Also marks `isHexColor` as public function.
2025-11-12 18:47:39 +03:00
Pierre Tachoire
c4380b91f4 handle template's original content
When the document fragment is called via the content method on a
templat, it must contain the original template's HTML nodes.
2025-11-12 11:02:22 +01:00
Pierre Tachoire
3f2f56d603 Merge pull request #1197 from lightpanda-io/module_loading
Module loading
2025-11-12 07:52:15 +01:00
muki
43b210dcf5 Merge pull request #1200 from lightpanda-io/location-set-hash
add `set_hash` to Location
2025-11-11 20:13:40 -08:00
Muki Kiboigo
16e7c0841d handle empty hashes in Location 2025-11-10 06:52:14 -08:00
Halil Durak
0a705b15ce add color representation by RGBA
It seems we can represent most things with RGBA (at least this is what other browsers do) so a universal color API based on RGBA is nice to have, especially for CSS and Canvas.
2025-11-10 16:57:35 +03:00
Pierre Tachoire
2f2870c066 Merge pull request #1201 from lightpanda-io/devtools
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
update zig-v8
2025-11-10 14:51:21 +01:00
Pierre Tachoire
9c277ae26e add debugger placeholders 2025-11-10 09:47:58 +01:00
Muki Kiboigo
19b9ba8601 add hash support to URL stitch 2025-11-05 10:11:12 -08:00
Muki Kiboigo
92ddb5640d new NavigationEventTarget on new page 2025-11-05 10:08:37 -08:00
Muki Kiboigo
38c6a9bd9d changeLocation on nav 2025-11-05 10:08:37 -08:00
Muki Kiboigo
3cc53b579b add location set hash tests 2025-11-05 10:08:37 -08:00
Muki Kiboigo
c009669ec8 properly handle replace navigation case 2025-11-05 10:08:37 -08:00
Muki Kiboigo
0e3f18367a add set_hash to Location 2025-11-05 10:08:37 -08:00
Halil Durak
4cf61d101c initial dummy canvas 2025-11-05 11:50:57 +03:00
Halil Durak
47ceabc43f Merge pull request #1195 from lightpanda-io/nikneym/blob-simd
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
2025-11-04 19:42:20 +03:00
Karl Seguin
dc4927d49e Merge pull request #1191 from lightpanda-io/refactor_script_manager
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Refactor the ScriptManager
2025-11-04 22:35:43 +08:00
Pierre Tachoire
bc29fce41a Merge pull request #1198 from lightpanda-io/nikneym/url-trim
Trim CR and LF characters from both ends
2025-11-04 15:32:45 +01:00
Halil Durak
97c92d7873 replace trimmed_path with path 2025-11-04 17:24:45 +03:00
Karl Seguin
68fbe742eb Update src/browser/ScriptManager.zig
Co-authored-by: Pierre Tachoire <pierre@lightpanda.io>
2025-11-04 22:22:13 +08:00
Karl Seguin
5b08188b93 Merge pull request #1196 from lightpanda-io/nikneym/tiny-changes
Tiny changes
2025-11-04 21:46:14 +08:00
Halil Durak
aa884803e3 trim CR and LF characters from both ends 2025-11-04 16:42:37 +03:00
Karl Seguin
d0d2850458 Improve module loading
This does two changes to module loading. First, for normal imports, it only
instantiates and evaluates the top-level module. This ensures that circular
dependencies can be resolved. This bug was introduced when I tried to
deduplicate code between dynamic and normal modules - but it turns out that
non-top-level normal modules do have a simpler flow (they just need to be
compiled, and we let v8 deal with the rest).

The other change is to handle more edge cases. Code like this should now be ok:

```
<script type=module>
  var a = await import('a.js');
</script>
<script type=module>
  import a from a.js
</script>
```

Previously, the dynamic import of a.js (first block) could interact badly with
the normal import of a.js in the 2nd block.

This change is built on top of https://github.com/lightpanda-io/browser/pull/1191
which also helps reduce the number of cases by ensure that a script isn't
evaluated while we're trying to evaluate a script.
2025-11-04 20:26:12 +08:00
Karl Seguin
f9087d3840 ignore errorCallback on shutdown 2025-11-04 20:11:26 +08:00
Halil Durak
0fab9be5c2 queueMicrotask should not return a timer ID 2025-11-04 14:39:05 +03:00
Pierre Tachoire
53c73c5851 Merge pull request #1189 from lightpanda-io/cdp-browser-permissions
cdp: add browser permissions noop
2025-11-04 12:12:40 +01:00
Halil Durak
996837ab0c return an empty origin and protocol string if url not provided 2025-11-04 13:30:55 +03:00
Halil Durak
74a5438587 update Blob test 2025-11-04 13:07:23 +03:00
Halil Durak
1fd28cef40 add vectorized line endings scanner 2025-11-04 13:07:02 +03:00
Karl Seguin
7c825cbe82 fix segfault on http error callback 2025-11-04 10:40:53 +08:00
muki
40522d8720 Merge pull request #1192 from lightpanda-io/wpt-navigation
Some checks failed
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Update wpt submodule for Navigation
2025-11-03 09:17:21 -08:00
Pierre Tachoire
2446580db9 update zig-v8 2025-11-03 17:47:35 +01:00
Karl Seguin
70e02dcfc7 Refactor the ScriptManager
This PR introduces two major changes to the ScriptManager.

1 - Simplification.
Rather than having a `Script`, `PendingScript`, `AsyncModule` and `SyncModule`,
there is only a `Script`, with an added `mode` union. All of the previous
objects had the same behavior (collect the response in a buffer), up to the
point of execution, which is where the mode comes in.

2 - Correctness
Whether or not the previous version was "incorrect", it was difficult to use
correctly. Specifically, the previous version would execute async scripts and
async modules as soon as they're done. That seems allowed, but it caused issues
with module loading in Context.js. Specifically, between compiling and
instantiating a module, or between instantiation and evaluation, an async script
or module could be evaluated. It isn't clear whether v8 allows that, but if it
does, it introduces a lot of new potential states (specifically, unexpected
changes to the v8.Module's status) that we have to handle.

This version only evaluate scripts in the `evaluate`, which doesn't allow
recursive calls (so a waitForImport, which continues to pump the HTTP loop, can
never result in `evaluate` being called again).

This undoes the change made in https://github.com/lightpanda-io/browser/pull/1158
because I do not think it's possible to have multiple waiters waiting for the
same (or even different) modules. The linked issue points to a crash in
https://www.nytimes.com which doesn't crash with this version.
2025-11-03 20:21:46 +08:00
Muki Kiboigo
235337d1c9 update wpt submodule for Navigation 2025-11-02 20:00:46 -08:00
Karl Seguin
8a867bc9c2 Merge pull request #1190 from lightpanda-io/nikneym/blob
Some checks failed
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
`Blob` support
2025-11-01 19:58:18 +08:00
Halil Durak
7aafab9c0a prefer js.resolvePromise helper for promise returns 2025-11-01 13:29:24 +03:00
Halil Durak
10c2d7dc87 remove unnecessary import and declaration 2025-11-01 13:28:27 +03:00
Halil Durak
9b990da7fa update Blob test 2025-10-31 17:18:33 +03:00
Halil Durak
93542c9756 support Blob.slice 2025-10-31 17:18:22 +03:00
Halil Durak
4be7fa178c support Blob.arrayBuffer
Also adds support for `ArrayBuffer` to js.zig.
2025-10-31 17:18:03 +03:00
Halil Durak
5785c147da support Blob type 2025-10-31 17:17:02 +03:00
Halil Durak
b68675bb94 update Blob test 2025-10-30 22:36:07 +03:00
Halil Durak
3307a664c4 prefer writeVec instead of writeAll + writeByte 2025-10-30 22:35:58 +03:00
Halil Durak
dd43be4818 remove method added for testing
This was only required while testing things, not an actual API.
2025-10-30 22:35:37 +03:00
Halil Durak
c491648941 implement various Blob methods
Support for `stream`, `text` and `bytes`.
2025-10-30 22:34:58 +03:00
Halil Durak
1085950b88 initial Blob support 2025-10-30 16:08:03 +03:00
Pierre Tachoire
1d91d24b12 cdp: add browser permissions noop 2025-10-28 15:02:24 +01:00
Pierre Tachoire
cc83d85542 Merge pull request #1188 from lightpanda-io/script-load-order
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
add a test for script load order
2025-10-28 14:12:31 +01:00
Pierre Tachoire
706a87a458 keep consistent queue for inline 2025-10-28 13:12:18 +01:00
Pierre Tachoire
3ec15ad1f7 add a test for script load order 2025-10-28 13:12:18 +01:00
Karl Seguin
07e603ecda Merge pull request #1186 from lightpanda-io/defer-module
module scripts are deferred by default
2025-10-28 18:36:54 +08:00
Pierre Tachoire
52fc2c365f use getList() to pick the right queue w/ inline scripts 2025-10-28 11:23:29 +01:00
Pierre Tachoire
8f3620adf0 modules are deferred by default 2025-10-28 09:17:57 +01:00
Karl Seguin
f7abf0956f Merge pull request #1184 from lightpanda-io/usage-fix
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
add --log_filter_scopes usage
2025-10-28 10:07:34 +08:00
Karl Seguin
73217f7832 Merge pull request #1185 from lightpanda-io/fix-script-print-wait-analysis
fix printWaitAnalysis with queue name changes
2025-10-28 10:07:13 +08:00
Pierre Tachoire
52fb2010fc fix printWaitAnalysis with queue name changes 2025-10-27 17:50:30 +01:00
Pierre Tachoire
03ffcdb604 add --log_filter_scopes usage 2025-10-27 17:37:05 +01:00
Karl Seguin
20314fccec Merge pull request #1182 from lightpanda-io/navigation-file-fix
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Fix build issues related to Navigation
2025-10-27 23:08:01 +08:00
Muki Kiboigo
018e95bea7 rename navigation.zig to navigation/root.zig 2025-10-27 07:48:22 -07:00
Pierre Tachoire
c9dc4ef57a Merge pull request #1144 from lightpanda-io/readme-mac
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
update mac instructions to build from source
2025-10-24 09:59:27 +02:00
Pierre Tachoire
6c9d013e20 update mac instructions to build from source 2025-10-24 09:58:38 +02:00
Pierre Tachoire
d2d10d5db4 Merge pull request #1175 from lightpanda-io/readme-cdp
README: add CDP link
2025-10-24 09:57:15 +02:00
Pierre Tachoire
37a8a24528 README: add CDP link 2025-10-24 09:53:29 +02:00
Pierre Tachoire
d0b83c674c Merge pull request #1138 from lightpanda-io/navigation
add `Navigation` WebAPI
2025-10-24 09:30:09 +02:00
Pierre Tachoire
b58ff2c869 Merge pull request #1171 from lightpanda-io/cdp-lifecycle
support url on createTarget and send lifecycle events
2025-10-24 08:33:11 +02:00
Pierre Tachoire
b2e41837d9 Merge pull request #1174 from lightpanda-io/nikneym/url-can-parse
Add `URL.canParse`
2025-10-24 08:32:25 +02:00
Halil Durak
2e6ec1e23b add URL.canParse test 2025-10-23 13:31:01 +03:00
Halil Durak
7808d12de2 add URL.canParse static method 2025-10-23 13:30:39 +03:00
Halil Durak
1015fc09ee Merge pull request #1170 from lightpanda-io/nikneym/ada-in-web-apis
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Use ada-url for URL operations in web APIs
2025-10-23 12:13:45 +03:00
Pierre Tachoire
1c37b1c70e Merge pull request #1173 from lightpanda-io/renderer-size
renderer: set a default box size of 5 pixels
2025-10-23 10:17:10 +02:00
Muki Kiboigo
28ec8d4b94 use page arena in get_sameDocument 2025-10-22 08:42:53 -07:00
Muki Kiboigo
6e42df2e71 set oncurrententrychange callback to null 2025-10-22 08:42:26 -07:00
Muki Kiboigo
6b924e8a4c use toEventTarget in NavigationEventTarget 2025-10-22 07:54:17 -07:00
Muki Kiboigo
80ae3c9fc6 not implemented on Navigation traverseTo 2025-10-22 07:54:10 -07:00
Pierre Tachoire
2422c8718c renderer: set a default box size of 5 pixels 2025-10-22 15:54:43 +02:00
Pierre Tachoire
b5ef8418a6 cdp: fix double createTarget response 2025-10-22 14:18:53 +02:00
Halil Durak
8d4cf400ce bring back invalidUrl test with error expectation 2025-10-22 15:03:06 +03:00
Halil Durak
c6a0368c61 add a searchParamsSetHref test according to href setter change 2025-10-22 15:02:48 +03:00
Halil Durak
033eb82ae5 reinitialize search_params too when href set 2025-10-22 15:01:27 +03:00
Halil Durak
2d14452dda remove stale todo comments 2025-10-22 13:40:44 +03:00
Pierre Tachoire
a69164b482 page: fix page mode when loading about:blank 2025-10-22 12:08:27 +02:00
Halil Durak
d4d35670a0 prefer call_arena in web APIs 2025-10-22 11:42:16 +03:00
Muki Kiboigo
b40e7ece91 no nullable url on Navigation pushEntry 2025-10-21 19:25:24 -07:00
Muki Kiboigo
9c4367b26e check query on eqlDocument 2025-10-21 19:23:19 -07:00
Muki Kiboigo
0eb639ac76 fix navigation shortcut URL stitching 2025-10-21 18:31:41 -07:00
Muki Kiboigo
9778eed1ed clean up Navigation test names 2025-10-21 18:31:41 -07:00
Muki Kiboigo
8b4ffeb911 fix NavigationCurrentEntryChange Constructor 2025-10-21 18:31:41 -07:00
Muki Kiboigo
b55b9bba0a functional NavigationCurrentEntryChangeEvent 2025-10-21 18:31:39 -07:00
Muki Kiboigo
82a45253de add direct event handlers 2025-10-21 18:31:09 -07:00
Muki Kiboigo
4c957041e2 add tests for eqlDocument 2025-10-21 18:31:08 -07:00
Muki Kiboigo
b8f9598de3 add NavigationCurrentEntryChangeEvent 2025-10-21 18:31:07 -07:00
Muki Kiboigo
907bd33d87 split NavigationType and NavigationKind 2025-10-21 18:29:28 -07:00
Muki Kiboigo
e9b08f19cf fix navigation and related tests 2025-10-21 18:29:28 -07:00
Muki Kiboigo
f97697535f History as compat layer over Navigation 2025-10-21 18:29:28 -07:00
Muki Kiboigo
e80c8d5bff add functional Navigation 2025-10-21 18:29:28 -07:00
Muki Kiboigo
70a009a52b add eqlDocument comparison 2025-10-21 18:29:28 -07:00
Muki Kiboigo
8ab9364f19 add ENUM_JS_USE_TAG for enums 2025-10-21 18:29:27 -07:00
Muki Kiboigo
186655e614 initial Navigation scaffolding 2025-10-21 18:29:27 -07:00
Pierre Tachoire
43958b81f8 http: remove inflight conn check
chromiumoxide sends the command while connections are in progress and it
doesn't cause issue w/ curl.
2025-10-21 17:50:11 +02:00
Pierre Tachoire
2d8a95946a cdp: dispatch lifecycle events when enable 2025-10-21 17:48:51 +02:00
Pierre Tachoire
a7c3bad9ad cdp: implement url parameter on createTarget 2025-10-21 17:45:19 +02:00
Halil Durak
7d39bc979f remove invalidUrl test in url.html 2025-10-21 16:50:16 +03:00
Halil Durak
d60d3ebaac update link.html test 2025-10-21 16:49:48 +03:00
Halil Durak
ba66b7c5db refactor HTMLAnchorElement regarding to URL changes
This still doesn't use `state` since `state` doesn't allow us to iterate the nodes when releasing the memory and we need to call `URL.destructor` when freeing. In the future, we might omit getter allocations by making such change.
2025-10-21 16:49:12 +03:00
Halil Durak
8342f0c394 omit try keyword when not necessary 2025-10-21 16:46:06 +03:00
Halil Durak
69884b9d8d Location changes regarding to changes in URL 2025-10-21 16:44:29 +03:00
Halil Durak
c568a75599 refactor URL web API 2025-10-21 16:43:09 +03:00
Halil Durak
9deb5249a9 introduce ada-url to build system
Also add ada-url bindings.
2025-10-21 16:42:01 +03:00
Pierre Tachoire
fb6fbffe3f Merge pull request #1169 from lightpanda-io/cdp-security-ignore-cert-err
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
cdp: implement Security.setIgnoreCertificateErrors
2025-10-21 15:15:51 +02:00
Pierre Tachoire
510c61cc20 cdp: add test for setIgnoreCertificateErrors 2025-10-21 14:08:26 +02:00
Pierre Tachoire
6915738e02 cdp: ensure no inflight conns is running before set TLS verify 2025-10-21 14:07:59 +02:00
Pierre Tachoire
4f62cc833b http: fix VERIFY_HOST value 2025-10-21 13:47:09 +02:00
Karl Seguin
46ffb801db Merge pull request #1168 from lightpanda-io/dom_range_fixes
Reverses 2 incorrect comparions
2025-10-21 19:45:32 +08:00
Pierre Tachoire
d2065f713f cdp: implement Security.setIgnoreCertificateErrors 2025-10-21 13:44:29 +02:00
Karl Seguin
6f8c3abb55 Merge pull request #1167 from lightpanda-io/typos
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
correct typos (all in comments)
2025-10-21 18:09:27 +08:00
Karl Seguin
163a0e8b70 Reverses 2 incorrect comparions
A bit obvious when you see the "expected -1 but got 1".

Goal is to bring us over 40K passing :)
2025-10-21 18:08:05 +08:00
Karl Seguin
ca3efb3ad9 correct typos (all in comments) 2025-10-21 16:17:38 +08:00
Karl Seguin
4468932346 Merge pull request #1166 from gootik/patch-1
Fix typo in 'input' selector check
2025-10-21 16:04:03 +08:00
Sasan Hezarkhani
9a03ba61c5 Fix typo in 'input' selector check
Fix a small typo in selector check
2025-10-20 21:30:35 -07:00
Karl Seguin
fe3777041d Merge pull request #1164 from lightpanda-io/nix_0.15.2
Update `flake.lock` for Zig 0.15.2
2025-10-21 08:18:46 +08:00
Muki Kiboigo
1c579a98b4 update flake.lock 2025-10-20 07:20:46 -07:00
Karl Seguin
3e10cf0a64 Merge pull request #1163 from lightpanda-io/zig_0_15_2
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Upgrade to Zig 0.15.2 - no code changes
2025-10-20 17:32:59 +08:00
Karl Seguin
ef9784a7d4 Upgrade to Zig 0.15.2 - no code changes 2025-10-20 16:44:45 +08:00
Karl Seguin
6f1c3c8fd2 Merge pull request #1162 from lightpanda-io/fix_node_iterator_regression
Fixes the regression to node iterator
2025-10-20 16:44:11 +08:00
Karl Seguin
e12c650ea5 Fixes the regression to node iterator
Caused by: https://github.com/lightpanda-io/browser/pull/1149/

WPT go from 727/766 (the pre-regression value) to 744/766.
2025-10-20 16:28:07 +08:00
Karl Seguin
9373cbb440 Merge pull request #1159 from lightpanda-io/make_test_filter_compiler
Filter out the huge compile command when using `make test`
2025-10-20 15:36:16 +08:00
Pierre Tachoire
fd6d038956 Merge pull request #1152 from lightpanda-io/cdp-inserttext
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
cdp: add input.insertText
2025-10-19 18:16:00 +02:00
Karl Seguin
9845392b71 Simplify filter and try to make it work with progressive build info 2025-10-18 11:18:50 +08:00
Karl Seguin
0795b7a583 Filter out the huge compile command when using make test
I couldn't figure out how (or if it's possible) to do this with build.zig
2025-10-18 08:14:07 +08:00
Karl Seguin
29f0e71f10 Merge pull request #1158 from lightpanda-io/concurrent-waitformodule
Some checks failed
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
handle multiple waiters for the same module
2025-10-18 07:46:30 +08:00
Karl Seguin
1a47f7b5a8 Merge pull request #1157 from lightpanda-io/rootNode_composed
support the composed option of getRootNode()
2025-10-18 07:44:38 +08:00
Karl Seguin
6a30ab7a57 Merge pull request #1156 from lightpanda-io/report_error
add window.reportError
2025-10-18 07:44:24 +08:00
Karl Seguin
758f7deb93 Merge pull request #1155 from lightpanda-io/composition_event
add CompositionEvent
2025-10-18 07:44:13 +08:00
Pierre Tachoire
9f4e3bf792 add a shared boolean to GetResult to avoid deinit 2025-10-17 18:02:21 +02:00
Pierre Tachoire
a5dfe8ab28 handle multiple waiters for the same module 2025-10-17 17:49:56 +02:00
Karl Seguin
288379aa7d support the composed option of getRootNode() 2025-10-16 19:08:33 +08:00
Karl Seguin
a9739bf361 add window.reportError 2025-10-16 18:33:18 +08:00
Karl Seguin
c69adcb163 add CompositionEvent 2025-10-16 15:57:37 +08:00
Pierre Tachoire
14a23123c0 add Document.hasFocus placeholder 2025-10-15 15:34:06 +02:00
Pierre Tachoire
09be5e23f1 add input.select placeholder 2025-10-15 15:32:27 +02:00
Pierre Tachoire
0aaed08c1e cdp: add input.insertText 2025-10-15 13:52:21 +02:00
113 changed files with 5092 additions and 1806 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,10 +22,9 @@ jobs:
timeout-minutes: 90 timeout-minutes: 90
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive submodules: recursive
- uses: ./.github/actions/install - uses: ./.github/actions/install

View File

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

View File

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

1
.gitignore vendored
View File

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

3
.gitmodules vendored
View File

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

View File

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

View File

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

View File

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

172
build.zig
View File

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

View File

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

12
flake.lock generated
View File

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

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ pub const App = struct {
telemetry: Telemetry, telemetry: Telemetry,
app_dir_path: ?[]const u8, app_dir_path: ?[]const u8,
notification: *Notification, notification: *Notification,
shutdown: bool = false,
pub const RunMode = enum { pub const RunMode = enum {
help, help,
@@ -82,9 +83,14 @@ pub const App = struct {
} }
pub fn deinit(self: *App) void { pub fn deinit(self: *App) void {
if (@atomicRmw(bool, &self.shutdown, .Xchg, true, .monotonic)) {
return;
}
const allocator = self.allocator; const allocator = self.allocator;
if (self.app_dir_path) |app_dir_path| { if (self.app_dir_path) |app_dir_path| {
allocator.free(app_dir_path); allocator.free(app_dir_path);
self.app_dir_path = null;
} }
self.telemetry.deinit(); self.telemetry.deinit();
self.notification.deinit(); self.notification.deinit();

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -562,7 +562,7 @@ pub const Selector = union(enum) {
const ntag = try n.tag(); const ntag = try n.tag();
if (std.ascii.eqlIgnoreCase("intput", ntag)) { if (std.ascii.eqlIgnoreCase("input", ntag)) {
const ntype = try n.attr("type"); const ntype = try n.attr("type");
if (ntype == null) return false; if (ntype == null) return false;

View File

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

283
src/browser/cssom/color.zig Normal file
View File

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

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const log = @import("../../log.zig");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
@@ -313,9 +314,72 @@ pub const Document = struct {
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self))); const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self)));
state.adopted_style_sheets = try sheets.persist(); state.adopted_style_sheets = try sheets.persist();
} }
pub fn _hasFocus(_: *parser.Document) bool {
log.debug(.web_api, "not implemented", .{ .feature = "Document hasFocus" });
return true;
}
pub fn _open(_: *parser.Document, page: *Page) !*parser.DocumentHTML {
if (page.open) {
return page.window.document;
}
// This implementation is invalid.
// According to MDN, we should cleanup registered listeners.
// So we sould cleanup previous DOM memory.
// But this implementation is more simple for now.
const html_doc = try parser.documentHTMLParseFromStr("");
try page.setDocument(html_doc);
page.open = true;
return page.window.document;
}
pub fn _close(_: *parser.Document, page: *Page) !void {
page.open = false;
}
pub fn _write(self: *parser.Document, str: []const u8, page: *Page) !void {
_ = try _open(self, page);
const document = parser.documentHTMLToDocument(page.window.document);
const fragment = try parser.documentParseFragmentFromStr(document, str);
const fragment_node = parser.documentFragmentToNode(fragment);
const fragment_html = parser.nodeFirstChild(fragment_node) orelse return;
const fragment_head = parser.nodeFirstChild(fragment_html) orelse return;
const fragment_body = parser.nodeNextSibling(fragment_head) orelse return;
const document_node = parser.documentToNode(document);
const document_html = parser.nodeFirstChild(document_node) orelse return;
const document_head = parser.nodeFirstChild(document_html) orelse return;
const document_body = parser.nodeNextSibling(document_head) orelse return;
{
const children = try parser.nodeGetChildNodes(fragment_head);
// always index 0, because nodeAppendChild moves the node out of
// the nodeList and into the new tree
while (parser.nodeListItem(children, 0)) |child| {
_ = try parser.nodeAppendChild(document_head, child);
}
}
{
const children = try parser.nodeGetChildNodes(fragment_body);
// always index 0, because nodeAppendChild moves the node out of
// the nodeList and into the new tree
while (parser.nodeListItem(children, 0)) |child| {
_ = try parser.nodeAppendChild(document_body, child);
}
}
}
}; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: DOM.Document" { test "Browser: DOM.Document" {
try testing.htmlRunner("dom/document.html"); try testing.htmlRunner("dom/document.html");
} }
test "Browser: DOM.Document.write" {
try testing.htmlRunner("dom/document_write.html");
}

View File

@@ -34,6 +34,7 @@ pub const Union = union(enum) {
screen_orientation: *@import("../html/screen.zig").ScreenOrientation, screen_orientation: *@import("../html/screen.zig").ScreenOrientation,
performance: *@import("performance.zig").Performance, performance: *@import("performance.zig").Performance,
media_query_list: *@import("../html/media_query_list.zig").MediaQueryList, media_query_list: *@import("../html/media_query_list.zig").MediaQueryList,
navigation: *@import("../navigation/Navigation.zig"),
}; };
// EventTarget implementation // EventTarget implementation
@@ -82,6 +83,11 @@ pub const EventTarget = struct {
.media_query_list => { .media_query_list => {
return .{ .media_query_list = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et))) }; return .{ .media_query_list = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et))) };
}, },
.navigation => {
const NavigationEventTarget = @import("../navigation/NavigationEventTarget.zig");
const base: *NavigationEventTarget = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et)));
return .{ .navigation = @fieldParentPtr("proto", base) };
},
} }
} }
@@ -89,6 +95,7 @@ pub const EventTarget = struct {
// -------- // --------
pub fn constructor(page: *Page) !*parser.EventTarget { pub fn constructor(page: *Page) !*parser.EventTarget {
const et = try page.arena.create(EventTarget); const et = try page.arena.create(EventTarget);
et.* = .{};
return @ptrCast(&et.base); return @ptrCast(&et.base);
} }

View File

@@ -286,7 +286,7 @@ const Opts = struct {
// WEB IDL https://dom.spec.whatwg.org/#htmlcollection // WEB IDL https://dom.spec.whatwg.org/#htmlcollection
// HTMLCollection is re implemented in zig here because libdom // HTMLCollection is re implemented in zig here because libdom
// dom_html_collection expects a comparison function callback as arguement. // dom_html_collection expects a comparison function callback as argument.
// But we wanted a dynamically comparison here, according to the match tagname. // But we wanted a dynamically comparison here, according to the match tagname.
pub const HTMLCollection = struct { pub const HTMLCollection = struct {
matcher: Matcher, matcher: Matcher,

View File

@@ -360,25 +360,53 @@ pub const Node = struct {
node: Union, node: Union,
}; };
pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }, page: *Page) !GetRootNodeResult { pub fn _getRootNode(self: *parser.Node, options: ?struct { composed: bool = false }, page: *Page) !GetRootNodeResult {
if (options) |options_| if (options_.composed) { const composed = if (options) |opts| opts.composed else false;
log.warn(.web_api, "not implemented", .{ .feature = "getRootNode composed" });
};
const root = parser.nodeGetRootNode(self); var current_root = parser.nodeGetRootNode(self);
if (page.getNodeState(root)) |state| {
if (state.shadow_root) |sr| { while (true) {
return .{ .shadow_root = sr }; const node_type = parser.nodeType(current_root);
if (node_type == .document_fragment) {
if (parser.documentFragmentGetHost(@ptrCast(current_root))) |host| {
if (page.getNodeState(host)) |state| {
if (state.shadow_root) |sr| {
if (!composed) {
return .{ .shadow_root = sr };
}
current_root = parser.nodeGetRootNode(@ptrCast(sr.host));
continue;
}
}
}
} }
break;
} }
return .{ .node = try Node.toInterface(root) }; return .{ .node = try Node.toInterface(current_root) };
} }
pub fn _hasChildNodes(self: *parser.Node) bool { pub fn _hasChildNodes(self: *parser.Node) bool {
return parser.nodeHasChildNodes(self); return parser.nodeHasChildNodes(self);
} }
fn is_template(self: *parser.Node) !bool {
if (parser.nodeType(self) != .element) {
return false;
}
const e = parser.nodeToElement(self);
return try parser.elementTag(e) == .template;
}
pub fn get_childNodes(self: *parser.Node, page: *Page) !NodeList { pub fn get_childNodes(self: *parser.Node, page: *Page) !NodeList {
// special case for template:
// > The Node.childNodes property of the <template> element is always empty
// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/template#usage_notes
if (try is_template(self)) {
return .{};
}
const allocator = page.arena; const allocator = page.arena;
var list: NodeList = .{}; var list: NodeList = .{};
@@ -461,7 +489,7 @@ pub const Node = struct {
// Check if the hierarchy node tree constraints are respected. // Check if the hierarchy node tree constraints are respected.
// For now, it checks only if new nodes are not self. // For now, it checks only if new nodes are not self.
// TODO implements the others contraints. // TODO implements the others constraints.
// see https://dom.spec.whatwg.org/#concept-node-tree // see https://dom.spec.whatwg.org/#concept-node-tree
pub fn hierarchy(self: *parser.Node, nodes: []const NodeOrText) bool { pub fn hierarchy(self: *parser.Node, nodes: []const NodeOrText) bool {
for (nodes) |n| { for (nodes) |n| {

View File

@@ -106,7 +106,6 @@ pub const NodeIterator = struct {
defer self.callbackEnd(); defer self.callbackEnd();
if (self.pointer_before_current) { if (self.pointer_before_current) {
self.pointer_before_current = false;
// Unlike TreeWalker, NodeIterator starts at the first node // Unlike TreeWalker, NodeIterator starts at the first node
if (.accept == try NodeFilter.verify(self.what_to_show, self.filter_func, self.reference_node)) { if (.accept == try NodeFilter.verify(self.what_to_show, self.filter_func, self.reference_node)) {
self.pointer_before_current = false; self.pointer_before_current = false;
@@ -116,6 +115,7 @@ pub const NodeIterator = struct {
if (try self.firstChild(self.reference_node)) |child| { if (try self.firstChild(self.reference_node)) |child| {
self.reference_node = child; self.reference_node = child;
self.pointer_before_current = false;
return try Node.toInterface(child); return try Node.toInterface(child);
} }

View File

@@ -195,7 +195,7 @@ test "Performance: now" {
} }
var after = perf._now(); var after = perf._now();
while (after <= now) { // Loop untill after > now while (after <= now) { // Loop until after > now
try testing.expectEqual(after, now); try testing.expectEqual(after, now);
after = perf._now(); after = perf._now();
} }

View File

@@ -92,7 +92,7 @@ pub const Range = struct {
pub fn _setStart(self: *Range, node: *parser.Node, offset_: i32) !void { pub fn _setStart(self: *Range, node: *parser.Node, offset_: i32) !void {
try ensureValidOffset(node, offset_); try ensureValidOffset(node, offset_);
const offset: u32 = @intCast(offset_); const offset: u32 = @intCast(offset_);
const position = compare(node, offset, self.proto.start_node, self.proto.start_offset) catch |err| switch (err) { const position = compare(node, offset, self.proto.end_node, self.proto.end_offset) catch |err| switch (err) {
error.WrongDocument => blk: { error.WrongDocument => blk: {
// allow a node with a different root than the current, or // allow a node with a different root than the current, or
// a disconnected one. Treat it as if it's "after", so that // a disconnected one. Treat it as if it's "after", so that
@@ -103,7 +103,7 @@ pub const Range = struct {
}; };
if (position == 1) { if (position == 1) {
// if we're setting the node after the current start, the end must // if we're setting the node after the current end, the end must
// be set too. // be set too.
self.proto.end_offset = offset; self.proto.end_offset = offset;
self.proto.end_node = node; self.proto.end_node = node;
@@ -378,7 +378,7 @@ fn compare(node_a: *parser.Node, offset_a: u32, node_b: *parser.Node, offset_b:
const child_parent, const child_index = try getParentAndIndex(child); const child_parent, const child_index = try getParentAndIndex(child);
std.debug.assert(node_a == child_parent); std.debug.assert(node_a == child_parent);
return if (child_index < offset_a) -1 else 1; return if (offset_a <= child_index) -1 else 1;
} }
return -1; return -1;

View File

@@ -227,7 +227,6 @@ pub const TreeWalker = struct {
continue; continue;
}; };
if (!result.should_descend) { if (!result.should_descend) {
// This is an .accept node - return it // This is an .accept node - return it
self.current_node = result.node; self.current_node = result.node;

View File

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

View File

@@ -0,0 +1,57 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("../netsurf.zig");
// https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent
pub const CompositionEvent = struct {
data: []const u8,
proto: parser.Event,
pub const union_make_copy = true;
pub const prototype = *parser.Event;
pub const ConstructorOptions = struct {
data: []const u8 = "",
};
pub fn constructor(event_type: []const u8, options_: ?ConstructorOptions) !CompositionEvent {
const options: ConstructorOptions = options_ orelse .{};
const event = try parser.eventCreate();
defer parser.eventDestroy(event);
try parser.eventInit(event, event_type, .{});
parser.eventSetInternalType(event, .composition_event);
return .{
.proto = event.*,
.data = options.data,
};
}
pub fn get_data(self: *const CompositionEvent) []const u8 {
return self.data;
}
};
const testing = @import("../../testing.zig");
test "Browser: Events.Composition" {
try testing.htmlRunner("events/composition.html");
}

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const log = @import("../../log.zig"); const log = @import("../../log.zig");
@@ -37,6 +38,9 @@ const KeyboardEvent = @import("keyboard_event.zig").KeyboardEvent;
const ErrorEvent = @import("../html/error_event.zig").ErrorEvent; const ErrorEvent = @import("../html/error_event.zig").ErrorEvent;
const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent; const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
const PopStateEvent = @import("../html/History.zig").PopStateEvent; const PopStateEvent = @import("../html/History.zig").PopStateEvent;
const CompositionEvent = @import("composition_event.zig").CompositionEvent;
const NavigationCurrentEntryChangeEvent = @import("../navigation/root.zig").NavigationCurrentEntryChangeEvent;
const PageTransitionEvent = @import("../events/PageTransitionEvent.zig");
// Event interfaces // Event interfaces
pub const Interfaces = .{ pub const Interfaces = .{
@@ -48,6 +52,9 @@ pub const Interfaces = .{
ErrorEvent, ErrorEvent,
MessageEvent, MessageEvent,
PopStateEvent, PopStateEvent,
CompositionEvent,
NavigationCurrentEntryChangeEvent,
PageTransitionEvent,
}; };
pub const Union = generate.Union(Interfaces); pub const Union = generate.Union(Interfaces);
@@ -72,10 +79,15 @@ pub const Event = struct {
.custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* }, .custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* },
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* }, .progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
.mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) }, .mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) },
.error_event => .{ .ErrorEvent = @as(*ErrorEvent, @ptrCast(evt)).* }, .error_event => .{ .ErrorEvent = (@as(*ErrorEvent, @fieldParentPtr("proto", evt))).* },
.message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* }, .message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* },
.keyboard_event => .{ .KeyboardEvent = @as(*parser.KeyboardEvent, @ptrCast(evt)) }, .keyboard_event => .{ .KeyboardEvent = @as(*parser.KeyboardEvent, @ptrCast(evt)) },
.pop_state => .{ .PopStateEvent = @as(*PopStateEvent, @ptrCast(evt)).* }, .pop_state => .{ .PopStateEvent = @as(*PopStateEvent, @ptrCast(evt)).* },
.composition_event => .{ .CompositionEvent = (@as(*CompositionEvent, @fieldParentPtr("proto", evt))).* },
.navigation_current_entry_change_event => .{
.NavigationCurrentEntryChangeEvent = @as(*NavigationCurrentEntryChangeEvent, @ptrCast(evt)).*,
},
.page_transition_event => .{ .PageTransitionEvent = @as(*PageTransitionEvent, @ptrCast(evt)).* },
}; };
} }
@@ -223,8 +235,6 @@ pub const EventHandler = struct {
node: parser.EventNode, node: parser.EventNode,
listener: *parser.EventListener, listener: *parser.EventListener,
const js = @import("../js/js.zig");
pub const Listener = union(enum) { pub const Listener = union(enum) {
function: js.Function, function: js.Function,
object: js.Object, object: js.Object,
@@ -396,6 +406,40 @@ const SignalCallback = struct {
} }
}; };
pub fn DirectEventHandler(
comptime TargetT: type,
target: *TargetT,
event_type: []const u8,
maybe_listener: ?EventHandler.Listener,
cb: *?js.Function,
page_arena: std.mem.Allocator,
) !void {
const event_target = parser.toEventTarget(TargetT, target);
// Check if we have a listener set.
if (cb.*) |callback| {
const listener = try parser.eventTargetHasListener(event_target, event_type, false, callback.id);
std.debug.assert(listener != null);
try parser.eventTargetRemoveEventListener(event_target, event_type, listener.?, false);
}
if (maybe_listener) |listener| {
switch (listener) {
// If an object is given as listener, do nothing.
.object => {},
.function => |callback| {
_ = try EventHandler.register(page_arena, event_target, event_type, listener, null) orelse unreachable;
cb.* = callback;
return;
},
}
}
// Just unset the listener.
cb.* = null;
}
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: Event" { test "Browser: Event" {
try testing.htmlRunner("events/event.html"); try testing.htmlRunner("events/event.html");

View File

@@ -254,17 +254,13 @@ pub fn _json(self: *Response, page: *Page) !js.Promise {
self.body_used = true; self.body_used = true;
if (self.body) |body| { if (self.body) |body| {
const p = std.json.parseFromSliceLeaky( const value = js.Value.fromJson(page.js, body) catch |e| {
std.json.Value,
page.call_arena,
body,
.{},
) catch |e| {
log.info(.browser, "invalid json", .{ .err = e, .source = "Request" }); log.info(.browser, "invalid json", .{ .err = e, .source = "Request" });
return error.SyntaxError; return error.SyntaxError;
}; };
const pvalue = try value.persist(page.js);
return page.js.resolvePromise(p); return page.js.resolvePromise(pvalue);
} }
return page.js.resolvePromise(null); return page.js.resolvePromise(null);
} }

View File

@@ -179,17 +179,13 @@ pub fn _json(self: *Response, page: *Page) !js.Promise {
if (self.body) |body| { if (self.body) |body| {
self.body_used = true; self.body_used = true;
const p = std.json.parseFromSliceLeaky( const value = js.Value.fromJson(page.js, body) catch |e| {
std.json.Value,
page.call_arena,
body,
.{},
) catch |e| {
log.info(.browser, "invalid json", .{ .err = e, .source = "Response" }); log.info(.browser, "invalid json", .{ .err = e, .source = "Response" });
return error.SyntaxError; return error.SyntaxError;
}; };
const pvalue = try value.persist(page.js);
return page.js.resolvePromise(p); return page.js.resolvePromise(pvalue);
} }
return page.js.resolvePromise(null); return page.js.resolvePromise(null);
} }

284
src/browser/file/Blob.zig Normal file
View File

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

View File

@@ -21,14 +21,12 @@ const std = @import("std");
// https://w3c.github.io/FileAPI/#file-section // https://w3c.github.io/FileAPI/#file-section
const File = @This(); const File = @This();
// Very incomplete. The prototype for this is Blob, which we don't have. /// TODO: Implement File API.
// This minimum "implementation" is added because some JavaScript code just
// checks: if (x instanceof File) throw Error(...)
pub fn constructor() File { pub fn constructor() File {
return .{}; return .{};
} }
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: File" { test "Browser: File.File" {
try testing.htmlRunner("xhr/file.html"); try testing.htmlRunner("file/file.html");
} }

View File

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

View File

@@ -21,140 +21,76 @@ const log = @import("../../log.zig");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const Window = @import("window.zig").Window;
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface // https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
const History = @This(); const History = @This();
const HistoryEntry = struct {
url: []const u8,
// This is serialized as JSON because
// History must survive a JsContext.
state: ?[]u8,
};
const ScrollRestorationMode = enum { const ScrollRestorationMode = enum {
pub const ENUM_JS_USE_TAG = true;
auto, auto,
manual, manual,
pub fn fromString(str: []const u8) ?ScrollRestorationMode {
for (std.enums.values(ScrollRestorationMode)) |mode| {
if (std.ascii.eqlIgnoreCase(str, @tagName(mode))) {
return mode;
}
} else {
return null;
}
}
pub fn toString(self: ScrollRestorationMode) []const u8 {
return @tagName(self);
}
}; };
scroll_restoration: ScrollRestorationMode = .auto, scroll_restoration: ScrollRestorationMode = .auto,
stack: std.ArrayListUnmanaged(HistoryEntry) = .empty,
current: ?usize = null,
pub fn get_length(self: *History) u32 { pub fn get_length(_: *History, page: *Page) u32 {
return @intCast(self.stack.items.len); return @intCast(page.session.navigation.entries.items.len);
} }
pub fn get_scrollRestoration(self: *History) ScrollRestorationMode { pub fn get_scrollRestoration(self: *History) ScrollRestorationMode {
return self.scroll_restoration; return self.scroll_restoration;
} }
pub fn set_scrollRestoration(self: *History, mode: []const u8) void { pub fn set_scrollRestoration(self: *History, mode: ScrollRestorationMode) void {
self.scroll_restoration = ScrollRestorationMode.fromString(mode) orelse self.scroll_restoration; self.scroll_restoration = mode;
} }
pub fn get_state(self: *History, page: *Page) !?js.Value { pub fn get_state(_: *History, page: *Page) !?js.Value {
if (self.current) |curr| { if (page.session.navigation.currentEntry().state.value) |state| {
const entry = self.stack.items[curr]; const value = try js.Value.fromJson(page.js, state);
if (entry.state) |state| { return value;
const value = try js.Value.fromJson(page.js, state);
return value;
} else {
return null;
}
} else { } else {
return null; return null;
} }
} }
pub fn pushNavigation(self: *History, _url: []const u8, page: *Page) !void { pub fn _pushState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
const arena = page.session.arena; const arena = page.session.arena;
const url = try arena.dupe(u8, _url); const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
const entry = HistoryEntry{ .state = null, .url = url }; const json = state.toJson(arena) catch return error.DataClone;
try self.stack.append(arena, entry); _ = try page.session.navigation.pushEntry(url, .{ .source = .history, .value = json }, page, true);
self.current = self.stack.items.len - 1;
} }
pub fn dispatchPopStateEvent(state: ?[]const u8, page: *Page) void { pub fn _replaceState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
log.debug(.script_event, "dispatch popstate event", .{
.type = "popstate",
.source = "history",
});
History._dispatchPopStateEvent(state, page) catch |err| {
log.err(.app, "dispatch popstate event error", .{
.err = err,
.type = "popstate",
.source = "history",
});
};
}
fn _dispatchPopStateEvent(state: ?[]const u8, page: *Page) !void {
var evt = try PopStateEvent.constructor("popstate", .{ .state = state });
_ = try parser.eventTargetDispatchEvent(
@as(*parser.EventTarget, @ptrCast(&page.window)),
&evt.proto,
);
}
pub fn _pushState(self: *History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
const arena = page.session.arena; const arena = page.session.arena;
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
const json = try state.toJson(arena); const json = try state.toJson(arena);
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw); _ = try page.session.navigation.replaceEntry(url, .{ .source = .history, .value = json }, page, true);
const entry = HistoryEntry{ .state = json, .url = url };
try self.stack.append(arena, entry);
self.current = self.stack.items.len - 1;
} }
pub fn _replaceState(self: *History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void { pub fn go(_: *const History, delta: i32, page: *Page) !void {
const arena = page.session.arena;
if (self.current) |curr| {
const entry = &self.stack.items[curr];
const json = try state.toJson(arena);
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
entry.* = HistoryEntry{ .state = json, .url = url };
} else {
try self._pushState(state, "", _url, page);
}
}
pub fn go(self: *History, delta: i32, page: *Page) !void {
// 0 behaves the same as no argument, both reloading the page. // 0 behaves the same as no argument, both reloading the page.
// If this is getting called, there SHOULD be an entry, atleast from pushNavigation.
const current = self.current.?;
const current = page.session.navigation.index;
const index_s: i64 = @intCast(@as(i64, @intCast(current)) + @as(i64, @intCast(delta))); const index_s: i64 = @intCast(@as(i64, @intCast(current)) + @as(i64, @intCast(delta)));
if (index_s < 0 or index_s > self.stack.items.len - 1) { if (index_s < 0 or index_s > page.session.navigation.entries.items.len - 1) {
return; return;
} }
const index = @as(usize, @intCast(index_s)); const index = @as(usize, @intCast(index_s));
const entry = self.stack.items[index]; const entry = page.session.navigation.entries.items[index];
self.current = index;
if (try page.isSameOrigin(entry.url)) { if (entry.url) |url| {
History.dispatchPopStateEvent(entry.state, page); if (try page.isSameOrigin(url)) {
PopStateEvent.dispatch(entry.state.value, page);
}
} }
try page.navigateFromWebAPI(entry.url, .{ .reason = .history }); _ = try page.session.navigation.navigate(entry.url, .{ .traverse = index }, page);
} }
pub fn _go(self: *History, _delta: ?i32, page: *Page) !void { pub fn _go(self: *History, _delta: ?i32, page: *Page) !void {
@@ -207,9 +143,38 @@ pub const PopStateEvent = struct {
return null; return null;
} }
} }
pub fn dispatch(state: ?[]const u8, page: *Page) void {
log.debug(.script_event, "dispatch popstate event", .{
.type = "popstate",
.source = "history",
});
var evt = PopStateEvent.constructor("popstate", .{ .state = state }) catch |err| {
log.err(.app, "event constructor error", .{
.err = err,
.type = "popstate",
.source = "history",
});
return;
};
_ = parser.eventTargetDispatchEvent(
parser.toEventTarget(Window, &page.window),
&evt.proto,
) catch |err| {
log.err(.app, "dispatch popstate event error", .{
.err = err,
.type = "popstate",
.source = "history",
});
};
}
}; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser: HTML.History" { test "Browser: HTML.History" {
try testing.htmlRunner("html/history.html"); try testing.htmlRunner("html/history/history.html");
try testing.htmlRunner("html/history/history2.html");
} }

View File

@@ -42,12 +42,12 @@ pub const HTMLDocument = struct {
// JS funcs // JS funcs
// -------- // --------
pub fn get_domain(self: *parser.DocumentHTML, page: *Page) ![]const u8 { pub fn get_domain(self: *parser.DocumentHTML) ![]const u8 {
// libdom's document_html get_domain always returns null, this is // libdom's document_html get_domain always returns null, this is
// the way MDN recommends getting the domain anyways, since document.domain // the way MDN recommends getting the domain anyways, since document.domain
// is deprecated. // is deprecated.
const location = try parser.documentHTMLGetLocation(Location, self) orelse return ""; const location = try parser.documentHTMLGetLocation(Location, self) orelse return "";
return location.get_host(page); return location.get_host();
} }
pub fn set_domain(_: *parser.DocumentHTML, _: []const u8) ![]const u8 { pub fn set_domain(_: *parser.DocumentHTML, _: []const u8) ![]const u8 {
@@ -195,7 +195,7 @@ pub const HTMLDocument = struct {
} }
pub fn set_location(_: *const parser.DocumentHTML, url: []const u8, page: *Page) !void { pub fn set_location(_: *const parser.DocumentHTML, url: []const u8, page: *Page) !void {
return page.navigateFromWebAPI(url, .{ .reason = .script }); return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null });
} }
pub fn get_designMode(_: *parser.DocumentHTML) []const u8 { pub fn get_designMode(_: *parser.DocumentHTML) []const u8 {

View File

@@ -32,6 +32,10 @@ const DataSet = @import("DataSet.zig");
const StyleSheet = @import("../cssom/StyleSheet.zig"); const StyleSheet = @import("../cssom/StyleSheet.zig");
const CSSStyleDeclaration = @import("../cssom/CSSStyleDeclaration.zig"); const CSSStyleDeclaration = @import("../cssom/CSSStyleDeclaration.zig");
const CanvasRenderingContext2D = @import("../canvas/CanvasRenderingContext2D.zig");
const WebGLRenderingContext = @import("../canvas/WebGLRenderingContext.zig");
const WalkerChildren = @import("../dom/walker.zig").WalkerChildren;
// HTMLElement interfaces // HTMLElement interfaces
pub const Interfaces = .{ pub const Interfaces = .{
@@ -218,36 +222,36 @@ pub const HTMLAnchorElement = struct {
} }
pub fn get_href(self: *parser.Anchor) ![]const u8 { pub fn get_href(self: *parser.Anchor) ![]const u8 {
return try parser.anchorGetHref(self); return parser.anchorGetHref(self);
} }
pub fn set_href(self: *parser.Anchor, href: []const u8, page: *const Page) !void { pub fn set_href(self: *parser.Anchor, href: []const u8, page: *const Page) !void {
const full = try urlStitch(page.call_arena, href, page.url.raw, .{}); const full = try urlStitch(page.call_arena, href, page.url.raw, .{});
return try parser.anchorSetHref(self, full); return parser.anchorSetHref(self, full);
} }
pub fn get_hreflang(self: *parser.Anchor) ![]const u8 { pub fn get_hreflang(self: *parser.Anchor) ![]const u8 {
return try parser.anchorGetHrefLang(self); return parser.anchorGetHrefLang(self);
} }
pub fn set_hreflang(self: *parser.Anchor, href: []const u8) !void { pub fn set_hreflang(self: *parser.Anchor, href: []const u8) !void {
return try parser.anchorSetHrefLang(self, href); return parser.anchorSetHrefLang(self, href);
} }
pub fn get_type(self: *parser.Anchor) ![]const u8 { pub fn get_type(self: *parser.Anchor) ![]const u8 {
return try parser.anchorGetType(self); return parser.anchorGetType(self);
} }
pub fn set_type(self: *parser.Anchor, t: []const u8) !void { pub fn set_type(self: *parser.Anchor, t: []const u8) !void {
return try parser.anchorSetType(self, t); return parser.anchorSetType(self, t);
} }
pub fn get_rel(self: *parser.Anchor) ![]const u8 { pub fn get_rel(self: *parser.Anchor) ![]const u8 {
return try parser.anchorGetRel(self); return parser.anchorGetRel(self);
} }
pub fn set_rel(self: *parser.Anchor, t: []const u8) !void { pub fn set_rel(self: *parser.Anchor, t: []const u8) !void {
return try parser.anchorSetRel(self, t); return parser.anchorSetRel(self, t);
} }
pub fn get_text(self: *parser.Anchor) !?[]const u8 { pub fn get_text(self: *parser.Anchor) !?[]const u8 {
@@ -269,182 +273,175 @@ pub const HTMLAnchorElement = struct {
if (try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "href")) |href| { if (try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "href")) |href| {
return URL.constructor(.{ .string = href }, null, page); // TODO inject base url return URL.constructor(.{ .string = href }, null, page); // TODO inject base url
} }
return .empty; return error.NotProvided;
} }
// TODO return a disposable string
pub fn get_origin(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_origin(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); var u = url(self, page) catch return "";
return try u.get_origin(page); defer u.destructor();
return u.get_origin(page);
} }
// TODO return a disposable string
pub fn get_protocol(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_protocol(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); var u = url(self, page) catch return "";
return u.get_protocol(); defer u.destructor();
return page.call_arena.dupe(u8, u.get_protocol());
} }
pub fn set_protocol(self: *parser.Anchor, v: []const u8, page: *Page) !void { pub fn set_protocol(self: *parser.Anchor, protocol: []const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page); var u = try url(self, page);
defer u.destructor();
try u.set_protocol(protocol);
u.uri.scheme = v; const href = try u._toString(page);
const href = try u.toString(arena); return parser.anchorSetHref(self, href);
try parser.anchorSetHref(self, href);
} }
// TODO return a disposable string
pub fn get_host(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_host(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); var u = url(self, page) catch return "";
return try u.get_host(page); defer u.destructor();
return page.call_arena.dupe(u8, u.get_host());
} }
pub fn set_host(self: *parser.Anchor, v: []const u8, page: *Page) !void { pub fn set_host(self: *parser.Anchor, host: []const u8, page: *Page) !void {
// search : separator
var p: ?u16 = null;
var h: []const u8 = undefined;
for (v, 0..) |c, i| {
if (c == ':') {
h = v[0..i];
p = try std.fmt.parseInt(u16, v[i + 1 ..], 10);
break;
}
}
const arena = page.arena;
var u = try url(self, page); var u = try url(self, page);
defer u.destructor();
try u.set_host(host);
if (p) |pp| { const href = try u._toString(page);
u.uri.host = .{ .raw = h }; return parser.anchorSetHref(self, href);
u.uri.port = pp;
} else {
u.uri.host = .{ .raw = v };
u.uri.port = null;
}
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
} }
pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); var u = url(self, page) catch return "";
return u.get_hostname(); defer u.destructor();
return page.call_arena.dupe(u8, u.get_hostname());
} }
pub fn set_hostname(self: *parser.Anchor, v: []const u8, page: *Page) !void { pub fn set_hostname(self: *parser.Anchor, hostname: []const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page); var u = try url(self, page);
u.uri.host = .{ .raw = v }; defer u.destructor();
const href = try u.toString(arena); try u.set_hostname(hostname);
try parser.anchorSetHref(self, href);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
} }
// TODO return a disposable string
pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); var u = url(self, page) catch return "";
return try u.get_port(page); defer u.destructor();
return page.call_arena.dupe(u8, u.get_port());
} }
pub fn set_port(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { pub fn set_port(self: *parser.Anchor, maybe_port: ?[]const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page); var u = try url(self, page);
defer u.destructor();
if (v != null and v.?.len > 0) { if (maybe_port) |port| {
u.uri.port = try std.fmt.parseInt(u16, v.?, 10); try u.set_port(port);
} else { } else {
u.uri.port = null; u.clearPort();
} }
const href = try u.toString(arena); const href = try u._toString(page);
try parser.anchorSetHref(self, href); return parser.anchorSetHref(self, href);
} }
// TODO return a disposable string
pub fn get_username(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_username(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); var u = url(self, page) catch return "";
return u.get_username(); defer u.destructor();
}
pub fn set_username(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { const username = u.get_username();
const arena = page.arena; if (username.len == 0) {
var u = try url(self, page); return "";
if (v) |vv| {
u.uri.user = .{ .raw = vv };
} else {
u.uri.user = null;
} }
const href = try u.toString(arena);
try parser.anchorSetHref(self, href); return page.call_arena.dupe(u8, username);
}
pub fn set_username(self: *parser.Anchor, maybe_username: ?[]const u8, page: *Page) !void {
var u = try url(self, page);
defer u.destructor();
const username = if (maybe_username) |username| username else "";
try u.set_username(username);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
} }
// TODO return a disposable string
pub fn get_password(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_password(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); var u = url(self, page) catch return "";
return try page.arena.dupe(u8, u.get_password()); defer u.destructor();
return page.call_arena.dupe(u8, u.get_password());
} }
pub fn set_password(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { pub fn set_password(self: *parser.Anchor, maybe_password: ?[]const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page); var u = try url(self, page);
defer u.destructor();
if (v) |vv| { const password = if (maybe_password) |password| password else "";
u.uri.password = .{ .raw = vv }; try u.set_password(password);
} else {
u.uri.password = null;
}
const href = try u.toString(arena);
try parser.anchorSetHref(self, href); const href = try u._toString(page);
return parser.anchorSetHref(self, href);
} }
// TODO return a disposable string
pub fn get_pathname(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_pathname(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); var u = url(self, page) catch return "";
return u.get_pathname(); defer u.destructor();
return page.call_arena.dupe(u8, u.get_pathname());
} }
pub fn set_pathname(self: *parser.Anchor, v: []const u8, page: *Page) !void { pub fn set_pathname(self: *parser.Anchor, pathname: []const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page); var u = try url(self, page);
u.uri.path = .{ .raw = v }; defer u.destructor();
const href = try u.toString(arena);
try parser.anchorSetHref(self, href); try u.set_pathname(pathname);
const href = try u._toString(page);
return parser.anchorSetHref(self, href);
} }
pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); var u = url(self, page) catch return "";
return try u.get_search(page); defer u.destructor();
// This allocates in page arena so no need to dupe.
return u.get_search(page);
} }
pub fn set_search(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { pub fn set_search(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void {
var u = try url(self, page); var u = try url(self, page);
defer u.destructor();
try u.set_search(v, page); try u.set_search(v, page);
const href = try u.toString(page.call_arena); const href = try u._toString(page);
try parser.anchorSetHref(self, href); return parser.anchorSetHref(self, href);
} }
// TODO return a disposable string
pub fn get_hash(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_hash(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); var u = url(self, page) catch return "";
return try u.get_hash(page); defer u.destructor();
return page.call_arena.dupe(u8, u.get_hash());
} }
pub fn set_hash(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { pub fn set_hash(self: *parser.Anchor, maybe_hash: ?[]const u8, page: *Page) !void {
const arena = page.arena;
var u = try url(self, page); var u = try url(self, page);
defer u.destructor();
if (v) |vv| { if (maybe_hash) |hash| {
u.uri.fragment = .{ .raw = vv }; try u.set_hash(hash);
} else { } else {
u.uri.fragment = null; u.clearHash();
} }
const href = try u.toString(arena);
try parser.anchorSetHref(self, href); const href = try u._toString(page);
return parser.anchorSetHref(self, href);
} }
}; };
@@ -494,6 +491,29 @@ pub const HTMLCanvasElement = struct {
pub const Self = parser.Canvas; pub const Self = parser.Canvas;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;
pub const subtype = .node; pub const subtype = .node;
/// This should be a union once we support other context types.
const ContextAttributes = struct {
alpha: bool,
color_space: []const u8 = "srgb",
};
/// Returns a drawing context on the canvas, or null if the context identifier
/// is not supported, or the canvas has already been set to a different context mode.
pub fn _getContext(
ctx_type: []const u8,
_: ?ContextAttributes,
) ?union(enum) { @"2d": CanvasRenderingContext2D, webgl: WebGLRenderingContext } {
if (std.mem.eql(u8, ctx_type, "2d")) {
return .{ .@"2d" = .{} };
}
if (std.mem.eql(u8, ctx_type, "webgl") or std.mem.eql(u8, ctx_type, "experimental-webgl")) {
return .{ .webgl = .{} };
}
return null;
}
}; };
pub const HTMLDListElement = struct { pub const HTMLDListElement = struct {
@@ -732,6 +752,9 @@ pub const HTMLInputElement = struct {
pub fn set_value(self: *parser.Input, value: []const u8) !void { pub fn set_value(self: *parser.Input, value: []const u8) !void {
try parser.inputSetValue(self, value); try parser.inputSetValue(self, value);
} }
pub fn _select(_: *parser.Input) void {
log.debug(.web_api, "not implemented", .{ .feature = "HTMLInputElement select" });
}
}; };
pub const HTMLLIElement = struct { pub const HTMLLIElement = struct {
@@ -1204,11 +1227,22 @@ pub const HTMLTemplateElement = struct {
pub const subtype = .node; pub const subtype = .node;
pub fn get_content(self: *parser.Template, page: *Page) !*parser.DocumentFragment { pub fn get_content(self: *parser.Template, page: *Page) !*parser.DocumentFragment {
const state = try page.getOrCreateNodeState(@ptrCast(@alignCast(self))); const n: *parser.Node = @ptrCast(@alignCast(self));
const state = try page.getOrCreateNodeState(n);
if (state.template_content) |tc| { if (state.template_content) |tc| {
return tc; return tc;
} }
const tc = try parser.documentCreateDocumentFragment(@ptrCast(page.window.document)); const tc = try parser.documentCreateDocumentFragment(@ptrCast(page.window.document));
const ntc: *parser.Node = @ptrCast(@alignCast(tc));
// move existing template's childnodes to the fragment.
const walker = WalkerChildren{};
var next: ?*parser.Node = null;
while (true) {
next = try walker.get_next(n, next) orelse break;
_ = try parser.nodeAppendChild(ntc, next.?);
}
state.template_content = tc; state.template_content = tc;
return tc; return tc;
} }
@@ -1354,8 +1388,13 @@ test "Browser: HTML.HtmlScriptElement" {
try testing.htmlRunner("html/script/import.html"); try testing.htmlRunner("html/script/import.html");
try testing.htmlRunner("html/script/dynamic_import.html"); try testing.htmlRunner("html/script/dynamic_import.html");
try testing.htmlRunner("html/script/importmap.html"); try testing.htmlRunner("html/script/importmap.html");
try testing.htmlRunner("html/script/order.html");
} }
test "Browser: HTML.HtmlSlotElement" { test "Browser: HTML.HtmlSlotElement" {
try testing.htmlRunner("html/slot.html"); try testing.htmlRunner("html/slot.html");
} }
test "Browser: HTML.HTMLCanvasElement" {
try testing.htmlRunner("html/canvas.html");
}

View File

@@ -42,7 +42,7 @@ pub const ErrorEvent = struct {
const event = try parser.eventCreate(); const event = try parser.eventCreate();
defer parser.eventDestroy(event); defer parser.eventDestroy(event);
try parser.eventInit(event, event_type, .{}); try parser.eventInit(event, event_type, .{});
parser.eventSetInternalType(event, .event); parser.eventSetInternalType(event, .error_event);
const o = opts orelse ErrorEventInit{}; const o = opts orelse ErrorEventInit{};

View File

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

View File

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

View File

@@ -25,6 +25,7 @@ const Page = @import("../page.zig").Page;
const Navigator = @import("navigator.zig").Navigator; const Navigator = @import("navigator.zig").Navigator;
const History = @import("History.zig"); const History = @import("History.zig");
const Navigation = @import("../navigation/Navigation.zig");
const Location = @import("location.zig").Location; const Location = @import("location.zig").Location;
const Crypto = @import("../crypto/crypto.zig").Crypto; const Crypto = @import("../crypto/crypto.zig").Crypto;
const Console = @import("../console/console.zig").Console; const Console = @import("../console/console.zig").Console;
@@ -41,6 +42,9 @@ const Request = @import("../fetch/Request.zig");
const fetchFn = @import("../fetch/fetch.zig").fetch; const fetchFn = @import("../fetch/fetch.zig").fetch;
const storage = @import("../storage/storage.zig"); const storage = @import("../storage/storage.zig");
const ErrorEvent = @import("error_event.zig").ErrorEvent;
const DirectEventHandler = @import("../events/event.zig").DirectEventHandler;
// https://dom.spec.whatwg.org/#interface-window-extensions // https://dom.spec.whatwg.org/#interface-window-extensions
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window // https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
@@ -52,7 +56,7 @@ pub const Window = struct {
document: *parser.DocumentHTML, document: *parser.DocumentHTML,
target: []const u8 = "", target: []const u8 = "",
location: Location = .default, location: Location,
storage_shelf: ?*storage.Shelf = null, storage_shelf: ?*storage.Shelf = null,
// counter for having unique timer ids // counter for having unique timer ids
@@ -68,6 +72,7 @@ pub const Window = struct {
scroll_x: u32 = 0, scroll_x: u32 = 0,
scroll_y: u32 = 0, scroll_y: u32 = 0,
onload_callback: ?js.Function = null, onload_callback: ?js.Function = null,
onpopstate_callback: ?js.Function = null,
pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window { pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window {
var fbs = std.io.fixedBufferStream(""); var fbs = std.io.fixedBufferStream("");
@@ -78,6 +83,7 @@ pub const Window = struct {
return .{ return .{
.document = html_doc, .document = html_doc,
.target = target orelse "", .target = target orelse "",
.location = try .init("about:blank"),
.navigator = navigator orelse .{}, .navigator = navigator orelse .{},
.performance = Performance.init(), .performance = Performance.init(),
}; };
@@ -88,6 +94,10 @@ pub const Window = struct {
try parser.documentHTMLSetLocation(Location, self.document, &self.location); try parser.documentHTMLSetLocation(Location, self.document, &self.location);
} }
pub fn changeLocation(self: *Window, new_url: []const u8, page: *Page) !void {
return self.location.url.reinit(new_url, page);
}
pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) !void { pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) !void {
self.performance.reset(); // When to reset see: https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin self.performance.reset(); // When to reset see: https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin
self.document = doc; self.document = doc;
@@ -109,31 +119,17 @@ pub const Window = struct {
/// Sets `onload_callback`. /// Sets `onload_callback`.
pub fn set_onload(self: *Window, maybe_listener: ?EventHandler.Listener, page: *Page) !void { pub fn set_onload(self: *Window, maybe_listener: ?EventHandler.Listener, page: *Page) !void {
const event_target = parser.toEventTarget(Window, self); try DirectEventHandler(Window, self, "load", maybe_listener, &self.onload_callback, page.arena);
const event_type = "load"; }
// Check if we have a listener set. /// Returns `onpopstate_callback`.
if (self.onload_callback) |callback| { pub fn get_onpopstate(self: *const Window) ?js.Function {
const listener = try parser.eventTargetHasListener(event_target, event_type, false, callback.id); return self.onpopstate_callback;
std.debug.assert(listener != null); }
try parser.eventTargetRemoveEventListener(event_target, event_type, listener.?, false);
}
if (maybe_listener) |listener| { /// Sets `onpopstate_callback`.
switch (listener) { pub fn set_onpopstate(self: *Window, maybe_listener: ?EventHandler.Listener, page: *Page) !void {
// If an object is given as listener, do nothing. try DirectEventHandler(Window, self, "popstate", maybe_listener, &self.onpopstate_callback, page.arena);
.object => {},
.function => |callback| {
_ = try EventHandler.register(page.arena, event_target, event_type, listener, null) orelse unreachable;
self.onload_callback = callback;
return;
},
}
}
// Just unset the listener.
self.onload_callback = null;
} }
pub fn get_location(self: *Window) *Location { pub fn get_location(self: *Window) *Location {
@@ -141,7 +137,7 @@ pub const Window = struct {
} }
pub fn set_location(_: *const Window, url: []const u8, page: *Page) !void { pub fn set_location(_: *const Window, url: []const u8, page: *Page) !void {
return page.navigateFromWebAPI(url, .{ .reason = .script }); return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null });
} }
// frames return the window itself, but accessing it via a pseudo // frames return the window itself, but accessing it via a pseudo
@@ -189,6 +185,10 @@ pub const Window = struct {
return &page.session.history; return &page.session.history;
} }
pub fn get_navigation(_: *Window, page: *Page) *Navigation {
return &page.session.navigation;
}
// The interior height of the window in pixels, including the height of the horizontal scroll bar, if present. // The interior height of the window in pixels, including the height of the horizontal scroll bar, if present.
pub fn get_innerHeight(_: *Window, page: *Page) u32 { pub fn get_innerHeight(_: *Window, page: *Page) u32 {
// We do not have scrollbars or padding so this is the same as Element.clientHeight // We do not have scrollbars or padding so this is the same as Element.clientHeight
@@ -247,8 +247,8 @@ pub const Window = struct {
_ = self.timers.remove(id); _ = self.timers.remove(id);
} }
pub fn _queueMicrotask(self: *Window, cbk: js.Function, page: *Page) !u32 { pub fn _queueMicrotask(self: *Window, cbk: js.Function, page: *Page) !void {
return self.createTimeout(cbk, 0, page, .{ .name = "queueMicrotask" }); _ = try self.createTimeout(cbk, 0, page, .{ .name = "queueMicrotask" });
} }
pub fn _setImmediate(self: *Window, cbk: js.Function, page: *Page) !u32 { pub fn _setImmediate(self: *Window, cbk: js.Function, page: *Page) !u32 {
@@ -281,6 +281,25 @@ pub const Window = struct {
return out; return out;
} }
pub fn _reportError(self: *Window, err: js.Object, page: *Page) !void {
var error_event = try ErrorEvent.constructor("error", .{
.@"error" = err,
});
_ = try parser.eventTargetDispatchEvent(
parser.toEventTarget(Window, self),
@as(*parser.Event, &error_event.proto),
);
if (parser.eventDefaultPrevented(&error_event.proto) == false) {
const err_string = err.toString() catch "Unknown error";
log.info(.user_script, "error", .{
.err = err_string,
.stack = page.stackTrace() catch "???",
.source = "window.reportError",
});
}
}
const CreateTimeoutOpts = struct { const CreateTimeoutOpts = struct {
name: []const u8, name: []const u8,
args: []js.Object = &.{}, args: []js.Object = &.{},

View File

@@ -14,6 +14,7 @@ const types = @import("types.zig");
const Caller = @import("Caller.zig"); const Caller = @import("Caller.zig");
const NamedFunction = Caller.NamedFunction; const NamedFunction = Caller.NamedFunction;
const PersistentObject = v8.Persistent(v8.Object); const PersistentObject = v8.Persistent(v8.Object);
const PersistentValue = v8.Persistent(v8.Value);
const PersistentModule = v8.Persistent(v8.Module); const PersistentModule = v8.Persistent(v8.Module);
const PersistentPromise = v8.Persistent(v8.Promise); const PersistentPromise = v8.Persistent(v8.Promise);
const PersistentFunction = v8.Persistent(v8.Function); const PersistentFunction = v8.Persistent(v8.Function);
@@ -70,6 +71,9 @@ identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .empty,
// we now simply persist every time persist() is called. // we now simply persist every time persist() is called.
js_object_list: std.ArrayListUnmanaged(PersistentObject) = .empty, js_object_list: std.ArrayListUnmanaged(PersistentObject) = .empty,
// js_value_list tracks persisted js values.
js_value_list: std.ArrayListUnmanaged(PersistentValue) = .empty,
// Various web APIs depend on having a persistent promise resolver. They // Various web APIs depend on having a persistent promise resolver. They
// require for this PromiseResolver to be valid for a lifetime longer than // require for this PromiseResolver to be valid for a lifetime longer than
// the function that resolves/rejects them. // the function that resolves/rejects them.
@@ -149,6 +153,10 @@ pub fn deinit(self: *Context) void {
p.deinit(); p.deinit();
} }
for (self.js_value_list.items) |*p| {
p.deinit();
}
for (self.persisted_promise_resolvers.items) |*p| { for (self.persisted_promise_resolvers.items) |*p| {
p.deinit(); p.deinit();
} }
@@ -222,63 +230,54 @@ pub fn exec(self: *Context, src: []const u8, name: ?[]const u8) !js.Value {
} }
pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url: []const u8, cacheable: bool) !(if (want_result) ModuleEntry else void) { pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url: []const u8, cacheable: bool) !(if (want_result) ModuleEntry else void) {
if (cacheable) { const mod, const owned_url = blk: {
if (self.module_cache.get(url)) |entry| { const arena = self.arena;
// The dynamic import will create an entry without the
// module to prevent multiple calls from asynchronously // gop will _always_ initiated if cacheable == true
// loading the same module. If we're here, without the var gop: std.StringHashMapUnmanaged(ModuleEntry).GetOrPutResult = undefined;
// module, then it's time to load it. if (cacheable) {
if (entry.module != null) { gop = try self.module_cache.getOrPut(arena, url);
return if (comptime want_result) entry else {}; if (gop.found_existing) {
if (gop.value_ptr.module != null) {
return if (comptime want_result) gop.value_ptr.* else {};
}
} else {
// first time seing this
gop.value_ptr.* = .{};
} }
} }
}
const m = try compileModule(self.isolate, src, url); const m = try compileModule(self.isolate, src, url);
const owned_url = try arena.dupeZ(u8, url);
const arena = self.arena; if (cacheable) {
const owned_url = try arena.dupe(u8, url); // compileModule is synchronous - nothing can modify the cache during compilation
std.debug.assert(gop.value_ptr.module == null);
try self.module_identifier.putNoClobber(arena, m.getIdentityHash(), owned_url); gop.value_ptr.module = PersistentModule.init(self.isolate, m);
errdefer _ = self.module_identifier.remove(m.getIdentityHash()); if (!gop.found_existing) {
gop.key_ptr.* = owned_url;
}
}
break :blk .{ m, owned_url };
};
try self.postCompileModule(mod, owned_url);
const v8_context = self.v8_context; const v8_context = self.v8_context;
{ if (try mod.instantiate(v8_context, resolveModuleCallback) == false) {
// Non-async modules are blocking. We can download them in
// parallel, but they need to be processed serially. So we
// want to get the list of dependent modules this module has
// and start downloading them asap.
const requests = m.getModuleRequests();
for (0..requests.length()) |i| {
const req = requests.get(v8_context, @intCast(i)).castTo(v8.ModuleRequest);
const specifier = try self.jsStringToZig(req.getSpecifier(), .{});
const normalized_specifier = try self.script_manager.?.resolveSpecifier(
self.call_arena,
specifier,
owned_url,
);
const gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
if (!gop.found_existing) {
const owned_specifier = try self.arena.dupeZ(u8, normalized_specifier);
gop.key_ptr.* = owned_specifier;
gop.value_ptr.* = .{};
try self.script_manager.?.getModule(owned_specifier, url);
}
}
}
if (try m.instantiate(v8_context, resolveModuleCallback) == false) {
return error.ModuleInstantiationError; return error.ModuleInstantiationError;
} }
const evaluated = m.evaluate(v8_context) catch { const evaluated = mod.evaluate(v8_context) catch {
std.debug.assert(m.getStatus() == .kErrored); std.debug.assert(mod.getStatus() == .kErrored);
// Some module-loading errors aren't handled by TryCatch. We need to // Some module-loading errors aren't handled by TryCatch. We need to
// get the error from the module itself. // get the error from the module itself.
log.warn(.js, "evaluate module", .{ log.warn(.js, "evaluate module", .{
.specifier = owned_url, .specifier = owned_url,
.message = self.valueToString(m.getException(), .{}) catch "???", .message = self.valueToString(mod.getException(), .{}) catch "???",
}); });
return error.EvaluationError; return error.EvaluationError;
}; };
@@ -301,28 +300,46 @@ pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url:
// be cached // be cached
std.debug.assert(cacheable); std.debug.assert(cacheable);
const persisted_module = PersistentModule.init(self.isolate, m); // entry has to have been created atop this function
const persisted_promise = PersistentPromise.init(self.isolate, .{ .handle = evaluated.handle }); const entry = self.module_cache.getPtr(owned_url).?;
var gop = try self.module_cache.getOrPut(arena, owned_url); // and the module must have been set after we compiled it
if (gop.found_existing) { std.debug.assert(entry.module != null);
// If we're here, it's because we had a cache entry, but no std.debug.assert(entry.module_promise == null);
// module. This happens because both our synch and async
// module loaders create the entry to prevent concurrent
// loads of the same resource (like Go's Singleflight).
std.debug.assert(gop.value_ptr.module == null);
std.debug.assert(gop.value_ptr.module_promise == null);
gop.value_ptr.module = persisted_module; entry.module_promise = PersistentPromise.init(self.isolate, .{ .handle = evaluated.handle });
gop.value_ptr.module_promise = persisted_promise; return if (comptime want_result) entry.* else {};
} else { }
gop.value_ptr.* = ModuleEntry{
.module = persisted_module, // After we compile a module, whether it's a top-level one, or a nested one,
.module_promise = persisted_promise, // we always want to track its identity (so that, if this module imports other
.resolver_promise = null, // modules, we can resolve the full URL), and preload any dependent modules.
}; fn postCompileModule(self: *Context, mod: v8.Module, url: [:0]const u8) !void {
try self.module_identifier.putNoClobber(self.arena, mod.getIdentityHash(), url);
const v8_context = self.v8_context;
// Non-async modules are blocking. We can download them in parallel, but
// they need to be processed serially. So we want to get the list of
// dependent modules this module has and start downloading them asap.
const requests = mod.getModuleRequests();
const script_manager = self.script_manager.?;
for (0..requests.length()) |i| {
const req = requests.get(v8_context, @intCast(i)).castTo(v8.ModuleRequest);
const specifier = try self.jsStringToZig(req.getSpecifier(), .{});
const normalized_specifier = try script_manager.resolveSpecifier(
self.call_arena,
specifier,
url,
);
const nested_gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
if (!nested_gop.found_existing) {
const owned_specifier = try self.arena.dupeZ(u8, normalized_specifier);
nested_gop.key_ptr.* = owned_specifier;
nested_gop.value_ptr.* = .{};
try script_manager.preloadImport(owned_specifier, url);
}
} }
return if (comptime want_result) gop.value_ptr.* else {};
} }
// == Creators == // == Creators ==
@@ -400,9 +417,8 @@ pub fn zigValueToJs(self: *Context, value: anytype) !v8.Value {
}, },
.pointer => |ptr| switch (ptr.size) { .pointer => |ptr| switch (ptr.size) {
.one => { .one => {
const type_name = @typeName(ptr.child); if (types.has(ptr.child)) {
if (@hasField(types.Lookup, type_name)) { const template = self.templates[types.getId(ptr.child)];
const template = self.templates[@field(types.LOOKUP, type_name)];
const js_obj = try self.mapZigInstanceToJs(template, value); const js_obj = try self.mapZigInstanceToJs(template, value);
return js_obj.toValue(); return js_obj.toValue();
} }
@@ -436,9 +452,8 @@ pub fn zigValueToJs(self: *Context, value: anytype) !v8.Value {
else => {}, else => {},
}, },
.@"struct" => |s| { .@"struct" => |s| {
const type_name = @typeName(T); if (types.has(T)) {
if (@hasField(types.Lookup, type_name)) { const template = self.templates[types.getId(T)];
const template = self.templates[@field(types.LOOKUP, type_name)];
const js_obj = try self.mapZigInstanceToJs(template, value); const js_obj = try self.mapZigInstanceToJs(template, value);
return js_obj.toValue(); return js_obj.toValue();
} }
@@ -574,8 +589,7 @@ pub fn mapZigInstanceToJs(self: *Context, js_obj_or_template: anytype, value: an
// well as any meta data we'll need to use it later. // well as any meta data we'll need to use it later.
// See the TaggedAnyOpaque struct for more details. // See the TaggedAnyOpaque struct for more details.
const tao = try arena.create(TaggedAnyOpaque); const tao = try arena.create(TaggedAnyOpaque);
const meta_index = @field(types.LOOKUP, @typeName(ptr.child)); const meta = self.meta_lookup[types.getId(ptr.child)];
const meta = self.meta_lookup[meta_index];
tao.* = .{ tao.* = .{
.ptr = value, .ptr = value,
@@ -655,7 +669,7 @@ pub fn jsValueToZig(self: *Context, comptime named_function: NamedFunction, comp
if (!js_value.isObject()) { if (!js_value.isObject()) {
return error.InvalidArgument; return error.InvalidArgument;
} }
if (@hasField(types.Lookup, @typeName(ptr.child))) { if (types.has(ptr.child)) {
const js_obj = js_value.castTo(v8.Object); const js_obj = js_value.castTo(v8.Object);
return self.typeTaggedAnyOpaque(named_function, *types.Receiver(ptr.child), js_obj); return self.typeTaggedAnyOpaque(named_function, *types.Receiver(ptr.child), js_obj);
} }
@@ -750,9 +764,16 @@ pub fn jsValueToZig(self: *Context, comptime named_function: NamedFunction, comp
unreachable; unreachable;
}, },
.@"enum" => |e| { .@"enum" => |e| {
switch (@typeInfo(e.tag_type)) { if (@hasDecl(T, "ENUM_JS_USE_TAG")) {
.int => return std.meta.intToEnum(T, try jsIntToZig(e.tag_type, js_value, self.v8_context)), const str = try self.jsValueToZig(named_function, []const u8, js_value);
else => @compileError(named_function.full_name ++ " has an unsupported enum parameter type: " ++ @typeName(T)), return std.meta.stringToEnum(T, str) orelse return error.InvalidEnumValue;
} else {
switch (@typeInfo(e.tag_type)) {
.int => return std.meta.intToEnum(T, try jsIntToZig(e.tag_type, js_value, self.v8_context)),
else => {
@compileError(named_function.full_name ++ " has an unsupported enum parameter type: " ++ @typeName(T));
},
}
} }
}, },
else => {}, else => {},
@@ -764,55 +785,55 @@ pub fn jsValueToZig(self: *Context, comptime named_function: NamedFunction, comp
// Extracted so that it can be used in both jsValueToZig and in // Extracted so that it can be used in both jsValueToZig and in
// probeJsValueToZig. Avoids having to duplicate this logic when probing. // probeJsValueToZig. Avoids having to duplicate this logic when probing.
fn jsValueToStruct(self: *Context, comptime named_function: NamedFunction, comptime T: type, js_value: v8.Value) !?T { fn jsValueToStruct(self: *Context, comptime named_function: NamedFunction, comptime T: type, js_value: v8.Value) !?T {
if (T == js.Function) { return switch (T) {
if (!js_value.isFunction()) { js.Function => {
return null; if (!js_value.isFunction()) {
} return null;
return try self.createFunction(js_value); }
}
if (@hasDecl(T, "_TYPED_ARRAY_ID_KLUDGE")) { return try self.createFunction(js_value);
const VT = @typeInfo(std.meta.fieldInfo(T, .values).type).pointer.child; },
const arr = (try self.jsValueToTypedArray(VT, js_value)) orelse return null; // zig fmt: off
return .{ .values = arr }; js.TypedArray(u8), js.TypedArray(u16), js.TypedArray(u32), js.TypedArray(u64),
} js.TypedArray(i8), js.TypedArray(i16), js.TypedArray(i32), js.TypedArray(i64),
js.TypedArray(f32), js.TypedArray(f64),
if (T == js.String) { // zig fmt: on
return .{ .string = try self.valueToString(js_value, .{ .allocator = self.arena }) }; => {
} const ValueType = @typeInfo(std.meta.fieldInfo(T, .values).type).pointer.child;
const slice = (try self.jsValueToTypedArray(ValueType, js_value)) orelse return null;
const js_obj = js_value.castTo(v8.Object); return .{ .values = slice };
},
if (comptime T == js.Object) { js.String => .{ .string = try self.valueToString(js_value, .{ .allocator = self.arena }) },
// Caller wants an opaque js.Object. Probably a parameter // Caller wants an opaque js.Object. Probably a parameter
// that it needs to pass back into a callback // that it needs to pass back into a callback.
return js.Object{ js.Object => js.Object{
.js_obj = js_obj, .js_obj = js_value.castTo(v8.Object),
.context = self, .context = self,
}; },
} else => {
const js_obj = js_value.castTo(v8.Object);
if (!js_value.isObject()) {
return null;
}
if (!js_value.isObject()) { const v8_context = self.v8_context;
return null; const isolate = self.isolate;
} var value: T = undefined;
inline for (@typeInfo(T).@"struct".fields) |field| {
const v8_context = self.v8_context; const name = field.name;
const isolate = self.isolate; const key = v8.String.initUtf8(isolate, name);
if (js_obj.has(v8_context, key.toValue())) {
var value: T = undefined; @field(value, name) = try self.jsValueToZig(named_function, field.type, try js_obj.getValue(v8_context, key));
inline for (@typeInfo(T).@"struct".fields) |field| { } else if (@typeInfo(field.type) == .optional) {
const name = field.name; @field(value, name) = null;
const key = v8.String.initUtf8(isolate, name); } else {
if (js_obj.has(v8_context, key.toValue())) { const dflt = field.defaultValue() orelse return null;
@field(value, name) = try self.jsValueToZig(named_function, field.type, try js_obj.getValue(v8_context, key)); @field(value, name) = dflt;
} else if (@typeInfo(field.type) == .optional) { }
@field(value, name) = null; }
} else { return value;
const dflt = field.defaultValue() orelse return null; },
@field(value, name) = dflt; };
}
}
return value;
} }
fn jsValueToTypedArray(_: *Context, comptime T: type, js_value: v8.Value) !?[]T { fn jsValueToTypedArray(_: *Context, comptime T: type, js_value: v8.Value) !?[]T {
@@ -1182,60 +1203,27 @@ fn _resolveModuleCallback(self: *Context, referrer: v8.Module, specifier: []cons
}; };
const normalized_specifier = try self.script_manager.?.resolveSpecifier( const normalized_specifier = try self.script_manager.?.resolveSpecifier(
self.arena, // might need to survive until the module is loaded self.arena,
specifier, specifier,
referrer_path, referrer_path,
); );
const gop = try self.module_cache.getOrPut(self.arena, normalized_specifier); const entry = self.module_cache.getPtr(normalized_specifier).?;
if (gop.found_existing) { if (entry.module) |m| {
if (gop.value_ptr.module) |m| { return m.castToModule().handle;
return m.handle;
}
// We don't have a module, but we do have a cache entry for it
// That means we're already trying to load it. We just have
// to wait for it to be done.
} else {
// I don't think it's possible for us to be here. This is
// only ever called by v8 when we evaluate a module. But
// before evaluating, we should have already started
// downloading all of the module's nested modules. So it
// should be impossible that this is the first time we've
// heard about this module.
// But, I'm not confident enough in that, and ther's little
// harm in handling this case.
@branchHint(.unlikely);
gop.value_ptr.* = .{};
try self.script_manager.?.getModule(normalized_specifier, referrer_path);
} }
var fetch_result = try self.script_manager.?.waitForModule(normalized_specifier); var source = try self.script_manager.?.waitForImport(normalized_specifier);
defer fetch_result.deinit(); defer source.deinit();
var try_catch: js.TryCatch = undefined; var try_catch: js.TryCatch = undefined;
try_catch.init(self); try_catch.init(self);
defer try_catch.deinit(); defer try_catch.deinit();
const entry = self.module(true, fetch_result.src(), normalized_specifier, true) catch |err| { const mod = try compileModule(self.isolate, source.src(), normalized_specifier);
switch (err) { try self.postCompileModule(mod, normalized_specifier);
error.EvaluationError => { entry.module = PersistentModule.init(self.isolate, mod);
// This is a sentinel value telling us that the error was already return entry.module.?.castToModule().handle;
// logged. Some module-loading errors aren't captured by Try/Catch.
// We need to handle those errors differently, where the module
// exists.
},
else => log.warn(.js, "compile resolved module", .{
.specifier = normalized_specifier,
.stack = try_catch.stack(self.call_arena) catch null,
.src = try_catch.sourceLine(self.call_arena) catch "err",
.line = try_catch.sourceLineNumber() orelse 0,
.exception = (try_catch.exception(self.call_arena) catch @errorName(err)) orelse @errorName(err),
}),
}
return null;
};
// entry.module is always set when returning from self.module()
return entry.module.?.handle;
} }
// Will get passed to ScriptManager and then passed back to us when // Will get passed to ScriptManager and then passed back to us when
@@ -1290,7 +1278,7 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c
}; };
// Next, we need to actually load it. // Next, we need to actually load it.
self.script_manager.?.getAsyncModule(specifier, dynamicModuleSourceCallback, state, referrer) catch |err| { self.script_manager.?.getAsyncImport(specifier, dynamicModuleSourceCallback, state, referrer) catch |err| {
const error_msg = v8.String.initUtf8(isolate, @errorName(err)); const error_msg = v8.String.initUtf8(isolate, @errorName(err));
_ = resolver.reject(self.v8_context, error_msg.toValue()); _ = resolver.reject(self.v8_context, error_msg.toValue());
}; };
@@ -1310,7 +1298,32 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c
// `dynamicModuleSourceCallback`, but we can skip some steps // `dynamicModuleSourceCallback`, but we can skip some steps
// since the module is alrady loaded, // since the module is alrady loaded,
std.debug.assert(gop.value_ptr.module != null); std.debug.assert(gop.value_ptr.module != null);
std.debug.assert(gop.value_ptr.module_promise != null);
// If the module hasn't been evaluated yet (it was only instantiated
// as a static import dependency), we need to evaluate it now.
if (gop.value_ptr.module_promise == null) {
const mod = gop.value_ptr.module.?.castToModule();
const status = mod.getStatus();
if (status == .kEvaluated or status == .kEvaluating) {
// Module was already evaluated (shouldn't normally happen, but handle it).
// Create a pre-resolved promise with the module namespace.
const persisted_module_resolver = v8.Persistent(v8.PromiseResolver).init(isolate, v8.PromiseResolver.init(self.v8_context));
try self.persisted_promise_resolvers.append(self.arena, persisted_module_resolver);
var module_resolver = persisted_module_resolver.castToPromiseResolver();
_ = module_resolver.resolve(self.v8_context, mod.getModuleNamespace());
gop.value_ptr.module_promise = PersistentPromise.init(self.isolate, module_resolver.getPromise());
} else {
// the module was loaded, but not evaluated, we _have_ to evaluate it now
const evaluated = mod.evaluate(self.v8_context) catch {
std.debug.assert(status == .kErrored);
const error_msg = v8.String.initUtf8(isolate, "Module evaluation failed");
_ = resolver.reject(self.v8_context, error_msg.toValue());
return promise;
};
std.debug.assert(evaluated.isPromise());
gop.value_ptr.module_promise = PersistentPromise.init(self.isolate, .{ .handle = evaluated.handle });
}
}
// like before, we want to set this up so that if anything else // like before, we want to set this up so that if anything else
// tries to load this module, it can just return our promise // tries to load this module, it can just return our promise
@@ -1323,24 +1336,24 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c
return promise; return promise;
} }
fn dynamicModuleSourceCallback(ctx: *anyopaque, fetch_result_: anyerror!ScriptManager.GetResult) void { fn dynamicModuleSourceCallback(ctx: *anyopaque, module_source_: anyerror!ScriptManager.ModuleSource) void {
const state: *DynamicModuleResolveState = @ptrCast(@alignCast(ctx)); const state: *DynamicModuleResolveState = @ptrCast(@alignCast(ctx));
var self = state.context; var self = state.context;
var fetch_result = fetch_result_ catch |err| { var ms = module_source_ catch |err| {
const error_msg = v8.String.initUtf8(self.isolate, @errorName(err)); const error_msg = v8.String.initUtf8(self.isolate, @errorName(err));
_ = state.resolver.castToPromiseResolver().reject(self.v8_context, error_msg.toValue()); _ = state.resolver.castToPromiseResolver().reject(self.v8_context, error_msg.toValue());
return; return;
}; };
const module_entry = blk: { const module_entry = blk: {
defer fetch_result.deinit(); defer ms.deinit();
var try_catch: js.TryCatch = undefined; var try_catch: js.TryCatch = undefined;
try_catch.init(self); try_catch.init(self);
defer try_catch.deinit(); defer try_catch.deinit();
break :blk self.module(true, fetch_result.src(), state.specifier, true) catch { break :blk self.module(true, ms.src(), state.specifier, true) catch {
const ex = try_catch.exception(self.call_arena) catch |err| @errorName(err) orelse "unknown error"; const ex = try_catch.exception(self.call_arena) catch |err| @errorName(err) orelse "unknown error";
log.err(.js, "module compilation failed", .{ log.err(.js, "module compilation failed", .{
.specifier = state.specifier, .specifier = state.specifier,
@@ -1447,14 +1460,13 @@ pub fn typeTaggedAnyOpaque(self: *const Context, comptime named_function: NamedF
return error.InvalidArgument; return error.InvalidArgument;
} }
const type_name = @typeName(T); if (!types.has(T)) {
if (@hasField(types.Lookup, type_name) == false) {
@compileError(named_function.full_name ++ "has an unknown Zig type: " ++ @typeName(R)); @compileError(named_function.full_name ++ "has an unknown Zig type: " ++ @typeName(R));
} }
const op = js_obj.getInternalField(0).castTo(v8.External).get(); const op = js_obj.getInternalField(0).castTo(v8.External).get();
const tao: *TaggedAnyOpaque = @ptrCast(@alignCast(op)); const tao: *TaggedAnyOpaque = @ptrCast(@alignCast(op));
const expected_type_index = @field(types.LOOKUP, type_name); const expected_type_index = types.getId(T);
var type_index = tao.index; var type_index = tao.index;
if (type_index == expected_type_index) { if (type_index == expected_type_index) {
@@ -1482,7 +1494,7 @@ pub fn typeTaggedAnyOpaque(self: *const Context, comptime named_function: NamedF
total_offset += @intCast(proto_offset); total_offset += @intCast(proto_offset);
} }
const prototype_index = types.PROTOTYPE_TABLE[type_index]; const prototype_index = types.PrototypeTable[type_index];
if (prototype_index == expected_type_index) { if (prototype_index == expected_type_index) {
return @ptrFromInt(base_ptr + total_offset); return @ptrFromInt(base_ptr + total_offset);
} }
@@ -1575,7 +1587,7 @@ fn probeJsValueToZig(self: *Context, comptime named_function: NamedFunction, com
if (!js_value.isObject()) { if (!js_value.isObject()) {
return .{ .invalid = {} }; return .{ .invalid = {} };
} }
if (@hasField(types.Lookup, @typeName(ptr.child))) { if (types.has(ptr.child)) {
const js_obj = js_value.castTo(v8.Object); const js_obj = js_value.castTo(v8.Object);
// There's a bit of overhead in doing this, so instead // There's a bit of overhead in doing this, so instead
// of having a version of typeTaggedAnyOpaque which // of having a version of typeTaggedAnyOpaque which

View File

@@ -111,16 +111,14 @@ pub fn init(allocator: Allocator, platform: *const Platform, _: Opts) !*Env {
const Struct = s.defaultValue().?; const Struct = s.defaultValue().?;
if (@hasDecl(Struct, "prototype")) { if (@hasDecl(Struct, "prototype")) {
const TI = @typeInfo(Struct.prototype); const TI = @typeInfo(Struct.prototype);
const proto_name = @typeName(types.Receiver(TI.pointer.child)); const ProtoType = types.Receiver(TI.pointer.child);
if (@hasField(types.Lookup, proto_name) == false) { if (!types.has(ProtoType)) {
@compileError(std.fmt.comptimePrint("Prototype '{s}' for '{s}' is undefined", .{ proto_name, @typeName(Struct) })); @compileError(std.fmt.comptimePrint("Prototype '{s}' for '{s}' is undefined", .{ @typeName(ProtoType), @typeName(Struct) }));
} }
// Hey, look! This is our first real usage of the types.LOOKUP. // Hey, look! This is our first real usage of the `types.Index`.
// Just like we said above, given a type, we can get its // Just like we said above, given a type, we can get its
// template index. // template index.
templates[i].inherit(templates[types.getId(ProtoType)]);
const proto_index = @field(types.LOOKUP, proto_name);
templates[i].inherit(templates[proto_index]);
} }
// while we're here, let's populate our meta lookup // while we're here, let's populate our meta lookup

View File

@@ -104,10 +104,8 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool, global_cal
// though it's also a Window, we need to set the prototype for this // though it's also a Window, we need to set the prototype for this
// specific instance of the the Window. // specific instance of the the Window.
if (@hasDecl(Global, "prototype")) { if (@hasDecl(Global, "prototype")) {
const proto_type = types.Receiver(@typeInfo(Global.prototype).pointer.child); const ProtoType = types.Receiver(@typeInfo(Global.prototype).pointer.child);
const proto_name = @typeName(proto_type); js_global.inherit(templates[types.getId(ProtoType)]);
const proto_index = @field(types.LOOKUP, proto_name);
js_global.inherit(templates[proto_index]);
} }
const context_local = v8.Context.init(isolate, global_template, null); const context_local = v8.Context.init(isolate, global_template, null);
@@ -123,14 +121,12 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool, global_cal
const Struct = s.defaultValue().?; const Struct = s.defaultValue().?;
if (@hasDecl(Struct, "prototype")) { if (@hasDecl(Struct, "prototype")) {
const proto_type = types.Receiver(@typeInfo(Struct.prototype).pointer.child); const ProtoType = types.Receiver(@typeInfo(Struct.prototype).pointer.child);
const proto_name = @typeName(proto_type); if (!types.has(ProtoType)) {
if (@hasField(types.Lookup, proto_name) == false) { @compileError("Type '" ++ @typeName(Struct) ++ "' defines an unknown prototype: " ++ @typeName(ProtoType));
@compileError("Type '" ++ @typeName(Struct) ++ "' defines an unknown prototype: " ++ proto_name);
} }
const proto_index = @field(types.LOOKUP, proto_name); const proto_obj = templates[types.getId(ProtoType)].getFunction(v8_context).toObject();
const proto_obj = templates[proto_index].getFunction(v8_context).toObject();
const self_obj = templates[i].getFunction(v8_context).toObject(); const self_obj = templates[i].getFunction(v8_context).toObject();
_ = self_obj.setPrototype(v8_context, proto_obj); _ = self_obj.setPrototype(v8_context, proto_obj);

View File

@@ -28,7 +28,14 @@ pub fn init(allocator: Allocator, isolate: v8.Isolate, ctx: anytype) !Inspector
// If necessary, turn a void context into something we can safely ptrCast // If necessary, turn a void context into something we can safely ptrCast
const safe_context: *anyopaque = if (ContextT == void) @ptrCast(@constCast(&{})) else ctx; const safe_context: *anyopaque = if (ContextT == void) @ptrCast(@constCast(&{})) else ctx;
const channel = v8.InspectorChannel.init(safe_context, InspectorContainer.onInspectorResponse, InspectorContainer.onInspectorEvent, isolate); const channel = v8.InspectorChannel.init(
safe_context,
InspectorContainer.onInspectorResponse,
InspectorContainer.onInspectorEvent,
InspectorContainer.onRunMessageLoopOnPause,
InspectorContainer.onQuitMessageLoopOnPause,
isolate,
);
const client = v8.InspectorClient.init(); const client = v8.InspectorClient.init();
@@ -109,6 +116,8 @@ pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []con
const NoopInspector = struct { const NoopInspector = struct {
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {} pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {} pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}
pub fn onRunMessageLoopOnPause(_: *anyopaque, _: u32) void {}
pub fn onQuitMessageLoopOnPause(_: *anyopaque) void {}
}; };
pub fn getTaggedAnyOpaque(value: v8.Value) ?*js.TaggedAnyOpaque { pub fn getTaggedAnyOpaque(value: v8.Value) ?*js.TaggedAnyOpaque {

View File

@@ -48,8 +48,6 @@ const NamedFunction = Context.NamedFunction;
// Env.JsObject. Want a TypedArray? Env.TypedArray. // Env.JsObject. Want a TypedArray? Env.TypedArray.
pub fn TypedArray(comptime T: type) type { pub fn TypedArray(comptime T: type) type {
return struct { return struct {
pub const _TYPED_ARRAY_ID_KLUDGE = true;
values: []const T, values: []const T,
pub fn dupe(self: TypedArray(T), allocator: Allocator) !TypedArray(T) { pub fn dupe(self: TypedArray(T), allocator: Allocator) !TypedArray(T) {
@@ -58,6 +56,10 @@ pub fn TypedArray(comptime T: type) type {
}; };
} }
pub const ArrayBuffer = struct {
values: []const u8,
};
pub const PromiseResolver = struct { pub const PromiseResolver = struct {
context: *Context, context: *Context,
resolver: v8.PromiseResolver, resolver: v8.PromiseResolver,
@@ -146,6 +148,8 @@ pub const Exception = struct {
}; };
pub const Value = struct { pub const Value = struct {
const PersistentValue = v8.Persistent(v8.Value);
value: v8.Value, value: v8.Value,
context: *const Context, context: *const Context,
@@ -159,6 +163,15 @@ pub const Value = struct {
const value = try v8.Json.parse(ctx.v8_context, json_string); const value = try v8.Json.parse(ctx.v8_context, json_string);
return Value{ .context = ctx, .value = value }; return Value{ .context = ctx, .value = value };
} }
pub fn persist(self: Value, context: *Context) !Value {
const js_value = self.value;
const persisted = PersistentValue.init(context.isolate, js_value);
try context.js_value_list.append(context.arena, persisted);
return Value{ .context = context, .value = persisted.toValue() };
}
}; };
pub const ValueIterator = struct { pub const ValueIterator = struct {
@@ -323,63 +336,86 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo
return v8.initNull(isolate).toValue(); return v8.initNull(isolate).toValue();
}, },
.@"struct" => { .@"struct" => {
const T = @TypeOf(value); switch (@TypeOf(value)) {
if (@hasDecl(T, "_TYPED_ARRAY_ID_KLUDGE")) { ArrayBuffer => {
const values = value.values; const values = value.values;
const value_type = @typeInfo(@TypeOf(values)).pointer.child; const len = values.len;
const len = values.len; var array_buffer: v8.ArrayBuffer = undefined;
const bits = switch (@typeInfo(value_type)) { const backing_store = v8.BackingStore.init(isolate, len);
.int => |n| n.bits,
.float => |f| f.bits,
else => @compileError("Invalid TypeArray type: " ++ @typeName(value_type)),
};
var array_buffer: v8.ArrayBuffer = undefined;
if (len == 0) {
array_buffer = v8.ArrayBuffer.init(isolate, 0);
} else {
const buffer_len = len * bits / 8;
const backing_store = v8.BackingStore.init(isolate, buffer_len);
const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData())); const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData()));
@memcpy(data[0..buffer_len], @as([]const u8, @ptrCast(values))[0..buffer_len]); @memcpy(data[0..len], @as([]const u8, @ptrCast(values))[0..len]);
array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr()); array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr());
}
switch (@typeInfo(value_type)) { return .{ .handle = array_buffer.handle };
.int => |n| switch (n.signedness) { },
.unsigned => switch (n.bits) { // zig fmt: off
8 => return v8.Uint8Array.init(array_buffer, 0, len).toValue(), TypedArray(u8), TypedArray(u16), TypedArray(u32), TypedArray(u64),
16 => return v8.Uint16Array.init(array_buffer, 0, len).toValue(), TypedArray(i8), TypedArray(i16), TypedArray(i32), TypedArray(i64),
32 => return v8.Uint32Array.init(array_buffer, 0, len).toValue(), TypedArray(f32), TypedArray(f64),
64 => return v8.BigUint64Array.init(array_buffer, 0, len).toValue(), // zig fmt: on
=> {
const values = value.values;
const value_type = @typeInfo(@TypeOf(values)).pointer.child;
const len = values.len;
const bits = switch (@typeInfo(value_type)) {
.int => |n| n.bits,
.float => |f| f.bits,
else => @compileError("Invalid TypedArray type: " ++ @typeName(value_type)),
};
var array_buffer: v8.ArrayBuffer = undefined;
if (len == 0) {
array_buffer = v8.ArrayBuffer.init(isolate, 0);
} else {
const buffer_len = len * bits / 8;
const backing_store = v8.BackingStore.init(isolate, buffer_len);
const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData()));
@memcpy(data[0..buffer_len], @as([]const u8, @ptrCast(values))[0..buffer_len]);
array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr());
}
switch (@typeInfo(value_type)) {
.int => |n| switch (n.signedness) {
.unsigned => switch (n.bits) {
8 => return v8.Uint8Array.init(array_buffer, 0, len).toValue(),
16 => return v8.Uint16Array.init(array_buffer, 0, len).toValue(),
32 => return v8.Uint32Array.init(array_buffer, 0, len).toValue(),
64 => return v8.BigUint64Array.init(array_buffer, 0, len).toValue(),
else => {},
},
.signed => switch (n.bits) {
8 => return v8.Int8Array.init(array_buffer, 0, len).toValue(),
16 => return v8.Int16Array.init(array_buffer, 0, len).toValue(),
32 => return v8.Int32Array.init(array_buffer, 0, len).toValue(),
64 => return v8.BigInt64Array.init(array_buffer, 0, len).toValue(),
else => {},
},
},
.float => |f| switch (f.bits) {
32 => return v8.Float32Array.init(array_buffer, 0, len).toValue(),
64 => return v8.Float64Array.init(array_buffer, 0, len).toValue(),
else => {}, else => {},
}, },
.signed => switch (n.bits) {
8 => return v8.Int8Array.init(array_buffer, 0, len).toValue(),
16 => return v8.Int16Array.init(array_buffer, 0, len).toValue(),
32 => return v8.Int32Array.init(array_buffer, 0, len).toValue(),
64 => return v8.BigInt64Array.init(array_buffer, 0, len).toValue(),
else => {},
},
},
.float => |f| switch (f.bits) {
32 => return v8.Float32Array.init(array_buffer, 0, len).toValue(),
64 => return v8.Float64Array.init(array_buffer, 0, len).toValue(),
else => {}, else => {},
}, }
else => {}, // We normally don't fail in this function unless fail == true
} // but this can never be valid.
// We normally don't fail in this function unless fail == true @compileError("Invalid TypedArray type: " ++ @typeName(value_type));
// but this can never be valid. },
@compileError("Invalid TypeArray type: " ++ @typeName(value_type)); else => {},
} }
}, },
.@"union" => return simpleZigValueToJs(isolate, std.meta.activeTag(value), fail), .@"union" => return simpleZigValueToJs(isolate, std.meta.activeTag(value), fail),
.@"enum" => { .@"enum" => {
const T = @TypeOf(value); const T = @TypeOf(value);
if (@hasDecl(T, "toString")) { if (@hasDecl(T, "toString")) {
// This should be deprecated in favor of the ENUM_JS_USE_TAG.
return simpleZigValueToJs(isolate, value.toString(), fail); return simpleZigValueToJs(isolate, value.toString(), fail);
} }
if (@hasDecl(T, "ENUM_JS_USE_TAG")) {
return simpleZigValueToJs(isolate, @tagName(value), fail);
}
}, },
else => {}, else => {},
} }

View File

@@ -16,8 +16,10 @@ const Interfaces = generate.Tuple(.{
@import("../storage/storage.zig").Interfaces, @import("../storage/storage.zig").Interfaces,
@import("../url/url.zig").Interfaces, @import("../url/url.zig").Interfaces,
@import("../xhr/xhr.zig").Interfaces, @import("../xhr/xhr.zig").Interfaces,
@import("../navigation/root.zig").Interfaces,
@import("../file/root.zig").Interfaces,
@import("../canvas/root.zig").Interfaces,
@import("../xhr/form_data.zig").Interfaces, @import("../xhr/form_data.zig").Interfaces,
@import("../xhr/File.zig"),
@import("../xmlserializer/xmlserializer.zig").Interfaces, @import("../xmlserializer/xmlserializer.zig").Interfaces,
@import("../fetch/fetch.zig").Interfaces, @import("../fetch/fetch.zig").Interfaces,
@import("../streams/streams.zig").Interfaces, @import("../streams/streams.zig").Interfaces,
@@ -25,104 +27,127 @@ const Interfaces = generate.Tuple(.{
pub const Types = @typeInfo(Interfaces).@"struct".fields; pub const Types = @typeInfo(Interfaces).@"struct".fields;
// Imagine we have a type Cat which has a getter: /// Integer type we use for `Index` enum. Can be u8 at min.
// pub const BackingInt = std.math.IntFittingRange(0, @max(std.math.maxInt(u8), Types.len));
// fn get_owner(self: *Cat) *Owner {
// return self.owner; /// Imagine we have a type `Cat` which has a getter:
// } ///
// /// fn get_owner(self: *Cat) *Owner {
// When we execute caller.getter, we'll end up doing something like: /// return self.owner;
// const res = @call(.auto, Cat.get_owner, .{cat_instance}); /// }
// ///
// How do we turn `res`, which is an *Owner, into something we can return /// When we execute `caller.getter`, we'll end up doing something like:
// to v8? We need the ObjectTemplate associated with Owner. How do we ///
// get that? Well, we store all the ObjectTemplates in an array that's /// const res = @call(.auto, Cat.get_owner, .{cat_instance});
// tied to env. So we do something like: ///
// /// How do we turn `res`, which is an *Owner, into something we can return
// env.templates[index_of_owner].initInstance(...); /// to v8? We need the ObjectTemplate associated with Owner. How do we
// /// get that? Well, we store all the ObjectTemplates in an array that's
// But how do we get that `index_of_owner`? `Lookup` is a struct /// tied to env. So we do something like:
// that looks like: ///
// /// env.templates[index_of_owner].initInstance(...);
// const Lookup = struct { ///
// comptime cat: usize = 0, /// But how do we get that `index_of_owner`? `Index` is an enum
// comptime owner: usize = 1, /// that looks like:
// ... ///
// } /// pub const Index = enum(BackingInt) {
// /// cat = 0,
// So to get the template index of `owner`, we can do: /// owner = 1,
// /// ...
// const index_id = @field(type_lookup, @typeName(@TypeOf(res)); /// }
// ///
pub const Lookup = blk: { /// (`BackingInt` is calculated at comptime regarding to interfaces we have)
var fields: [Types.len]std.builtin.Type.StructField = undefined; /// So to get the template index of `owner`, simply do:
///
/// const index_id = types.getId(@TypeOf(res));
pub const Index = blk: {
var fields: [Types.len]std.builtin.Type.EnumField = undefined;
for (Types, 0..) |s, i| { for (Types, 0..) |s, i| {
// This prototype type check has nothing to do with building our
// Lookup. But we put it here, early, so that the rest of the
// code doesn't have to worry about checking if Struct.prototype is
// a pointer.
const Struct = s.defaultValue().?; const Struct = s.defaultValue().?;
if (@hasDecl(Struct, "prototype") and @typeInfo(Struct.prototype) != .pointer) { fields[i] = .{ .name = @typeName(Receiver(Struct)), .value = i };
@compileError(std.fmt.comptimePrint("Prototype '{s}' for type '{s} must be a pointer", .{ @typeName(Struct.prototype), @typeName(Struct) }));
}
fields[i] = .{
.name = @typeName(Receiver(Struct)),
.type = usize,
.is_comptime = true,
.alignment = @alignOf(usize),
.default_value_ptr = &i,
};
} }
break :blk @Type(.{ .@"struct" = .{
.layout = .auto, break :blk @Type(.{
.decls = &.{}, .@"enum" = .{
.is_tuple = false, .fields = &fields,
.fields = &fields, .tag_type = BackingInt,
} }); .is_exhaustive = true,
.decls = &.{},
},
});
}; };
pub const LOOKUP = Lookup{}; /// Returns a boolean indicating if a type exist in the `Index`.
pub inline fn has(t: type) bool {
return @hasField(Index, @typeName(t));
}
// Creates a list where the index of a type contains its prototype index /// Returns the `Index` for the given type.
// const Animal = struct{}; pub inline fn getIndex(t: type) Index {
// const Cat = struct{ return @field(Index, @typeName(t));
// pub const prototype = *Animal; }
// };
// /// Returns the ID for the given type.
// Would create an array: [0, 0] pub inline fn getId(t: type) BackingInt {
// Animal, at index, 0, has no prototype, so we set it to itself return @intFromEnum(getIndex(t));
// Cat, at index 1, has an Animal prototype, so we set it to 0. }
//
// When we're trying to pass an argument to a Zig function, we'll know the /// Creates a list where the index of a type contains its prototype index.
// target type (the function parameter type), and we'll have a /// const Animal = struct{};
// TaggedAnyOpaque which will have the index of the type of that parameter. /// const Cat = struct{
// We'll use the PROTOTYPE_TABLE to see if the TaggedAnyType should be /// pub const prototype = *Animal;
// cast to a prototype. /// };
pub const PROTOTYPE_TABLE = blk: { ///
var table: [Types.len]u16 = undefined; /// Would create an array of indexes:
/// [Index.Animal, Index.Animal]
///
/// `Animal`, at index, 0, has no prototype, so we set it to itself.
/// `Cat`, at index 1, has an `Animal` prototype, so we set it to `Animal`.
///
/// When we're trying to pass an argument to a Zig function, we'll know the
/// target type (the function parameter type), and we'll have a
/// TaggedAnyOpaque which will have the index of the type of that parameter.
/// We'll use the `PrototypeTable` to see if the TaggedAnyType should be
/// cast to a prototype.
pub const PrototypeTable = blk: {
var table: [Types.len]BackingInt = undefined;
for (Types, 0..) |s, i| { for (Types, 0..) |s, i| {
var prototype_index = i;
const Struct = s.defaultValue().?; const Struct = s.defaultValue().?;
if (@hasDecl(Struct, "prototype")) { table[i] = proto_index: {
const TI = @typeInfo(Struct.prototype); if (@hasDecl(Struct, "prototype")) {
const proto_name = @typeName(Receiver(TI.pointer.child)); const prototype_field = @field(Struct, "prototype");
prototype_index = @field(LOOKUP, proto_name); // This prototype type check has nothing to do with building our
} // Lookup. But we put it here, early, so that the rest of the
table[i] = prototype_index; // code doesn't have to worry about checking if Struct.prototype is
// a pointer.
break :proto_index switch (@typeInfo(prototype_field)) {
.pointer => |pointer| getId(Receiver(pointer.child)),
inline else => @compileError(std.fmt.comptimePrint("Prototype '{s}' for type '{s}' must be a pointer", .{
prototype_field,
@typeName(Struct),
})),
};
}
break :proto_index i;
};
} }
break :blk table; break :blk table;
}; };
// This is essentially meta data for each type. Each is stored in env.meta_lookup /// This is essentially meta data for each type. Each is stored in `env.meta_lookup`.
// The index for a type can be retrieved via: /// The index for a type can be retrieved via:
// const index = @field(TYPE_LOOKUP, @typeName(Receiver(Struct))); /// const index = types.getIndex(Receiver(Struct));
// const meta = env.meta_lookup[index]; /// const meta = env.meta_lookup[@intFromEnum(index)];
///
/// Or:
/// const id = types.getId(Receiver(Struct));
/// const meta = env.meta_lookup[id];
pub const Meta = struct { pub const Meta = struct {
// Every type is given a unique index. That index is used to lookup various // Every type is given a unique index. That index is used to lookup various
// things, i.e. the prototype chain. // things, i.e. the prototype chain.
index: u16, index: BackingInt,
// We store the type's subtype here, so that when we create an instance of // We store the type's subtype here, so that when we create an instance of
// the type, and bind it to JavaScript, we can store the subtype along with // the type, and bind it to JavaScript, we can store the subtype along with

View File

@@ -24,6 +24,7 @@ pub const Mime = struct {
// IANA defines max. charset value length as 40. // IANA defines max. charset value length as 40.
// We keep 41 for null-termination since HTML parser expects in this format. // We keep 41 for null-termination since HTML parser expects in this format.
charset: [41]u8 = default_charset, charset: [41]u8 = default_charset,
charset_len: usize = 5,
/// String "UTF-8" continued by null characters. /// String "UTF-8" continued by null characters.
pub const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36; pub const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36;
@@ -53,9 +54,25 @@ pub const Mime = struct {
other: struct { type: []const u8, sub_type: []const u8 }, other: struct { type: []const u8, sub_type: []const u8 },
}; };
pub fn contentTypeString(mime: *const Mime) []const u8 {
return switch (mime.content_type) {
.text_xml => "text/xml",
.text_html => "text/html",
.text_javascript => "application/javascript",
.text_plain => "text/plain",
.text_css => "text/css",
.application_json => "application/json",
else => "",
};
}
/// Returns the null-terminated charset value. /// Returns the null-terminated charset value.
pub fn charsetString(mime: *const Mime) [:0]const u8 { pub fn charsetStringZ(mime: *const Mime) [:0]const u8 {
return @ptrCast(&mime.charset); return mime.charset[0..mime.charset_len :0];
}
pub fn charsetString(mime: *const Mime) []const u8 {
return mime.charset[0..mime.charset_len];
} }
/// Removes quotes of value if quotes are given. /// Removes quotes of value if quotes are given.
@@ -99,6 +116,7 @@ pub const Mime = struct {
const params = trimLeft(normalized[type_len..]); const params = trimLeft(normalized[type_len..]);
var charset: [41]u8 = undefined; var charset: [41]u8 = undefined;
var charset_len: usize = undefined;
var it = std.mem.splitScalar(u8, params, ';'); var it = std.mem.splitScalar(u8, params, ';');
while (it.next()) |attr| { while (it.next()) |attr| {
@@ -124,6 +142,7 @@ pub const Mime = struct {
@memcpy(charset[0..attribute_value.len], attribute_value); @memcpy(charset[0..attribute_value.len], attribute_value);
// Null-terminate right after attribute value. // Null-terminate right after attribute value.
charset[attribute_value.len] = 0; charset[attribute_value.len] = 0;
charset_len = attribute_value.len;
}, },
} }
} }
@@ -131,6 +150,7 @@ pub const Mime = struct {
return .{ return .{
.params = params, .params = params,
.charset = charset, .charset = charset,
.charset_len = charset_len,
.content_type = content_type, .content_type = content_type,
}; };
} }
@@ -511,9 +531,9 @@ fn expect(expected: Expectation, input: []const u8) !void {
if (expected.charset) |ec| { if (expected.charset) |ec| {
// We remove the null characters for testing purposes here. // We remove the null characters for testing purposes here.
try testing.expectEqual(ec, actual.charsetString()[0..ec.len]); try testing.expectEqual(ec, actual.charsetString());
} else { } else {
const m: Mime = .unknown; const m: Mime = .unknown;
try testing.expectEqual(m.charsetString(), actual.charsetString()); try testing.expectEqual(m.charsetStringZ(), actual.charsetStringZ());
} }
} }

View File

@@ -0,0 +1,359 @@
// 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 js = @import("../js/js.zig");
const Page = @import("../page.zig").Page;
const DirectEventHandler = @import("../events/event.zig").DirectEventHandler;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
const EventHandler = @import("../events/event.zig").EventHandler;
const parser = @import("../netsurf.zig");
// https://developer.mozilla.org/en-US/docs/Web/API/Navigation
const Navigation = @This();
const NavigationKind = @import("root.zig").NavigationKind;
const NavigationHistoryEntry = @import("root.zig").NavigationHistoryEntry;
const NavigationTransition = @import("root.zig").NavigationTransition;
const NavigationState = @import("root.zig").NavigationState;
const NavigationCurrentEntryChangeEvent = @import("root.zig").NavigationCurrentEntryChangeEvent;
const NavigationEventTarget = @import("NavigationEventTarget.zig");
pub const prototype = *NavigationEventTarget;
proto: NavigationEventTarget = NavigationEventTarget{},
index: usize = 0,
// Need to be stable pointers, because Events can reference entries.
entries: std.ArrayListUnmanaged(*NavigationHistoryEntry) = .empty,
next_entry_id: usize = 0,
pub fn resetForNewPage(self: *Navigation) void {
// libdom will automatically clean this up when a new page is made.
// We must create a new target whenever we create a new page.
self.proto = NavigationEventTarget{};
}
pub fn get_canGoBack(self: *const Navigation) bool {
return self.index > 0;
}
pub fn get_canGoForward(self: *const Navigation) bool {
return self.entries.items.len > self.index + 1;
}
pub fn currentEntry(self: *Navigation) *NavigationHistoryEntry {
return self.entries.items[self.index];
}
pub fn get_currentEntry(self: *Navigation) *NavigationHistoryEntry {
return self.currentEntry();
}
pub fn get_transition(_: *const Navigation) ?NavigationTransition {
// For now, all transitions are just considered complete.
return null;
}
const NavigationReturn = struct {
committed: js.Promise,
finished: js.Promise,
};
pub fn _back(self: *Navigation, page: *Page) !NavigationReturn {
if (!self.get_canGoBack()) {
return error.InvalidStateError;
}
const new_index = self.index - 1;
const next_entry = self.entries.items[new_index];
self.index = new_index;
return self.navigate(next_entry.url, .{ .traverse = new_index }, page);
}
pub fn _entries(self: *const Navigation) []*NavigationHistoryEntry {
return self.entries.items;
}
pub fn _forward(self: *Navigation, page: *Page) !NavigationReturn {
if (!self.get_canGoForward()) {
return error.InvalidStateError;
}
const new_index = self.index + 1;
const next_entry = self.entries.items[new_index];
self.index = new_index;
return self.navigate(next_entry.url, .{ .traverse = new_index }, page);
}
pub fn updateEntries(self: *Navigation, url: []const u8, kind: NavigationKind, page: *Page, dispatch: bool) !void {
switch (kind) {
.replace => {
_ = try self.replaceEntry(url, .{ .source = .navigation, .value = null }, page, dispatch);
},
.push => |state| {
_ = try self.pushEntry(url, .{ .source = .navigation, .value = state }, page, dispatch);
},
.traverse => |index| {
self.index = index;
},
.reload => {},
}
}
// This is for after true navigation processing, where we need to ensure that our entries are up to date.
// This is only really safe to run in the `pageDoneCallback` where we can guarantee that the URL and NavigationKind are correct.
pub fn processNavigation(self: *Navigation, page: *Page) !void {
const url = page.url.raw;
const kind: NavigationKind = page.session.navigation_kind orelse .{ .push = null };
try self.updateEntries(url, kind, page, false);
}
/// Pushes an entry into the Navigation stack WITHOUT actually navigating to it.
/// For that, use `navigate`.
pub fn pushEntry(
self: *Navigation,
_url: []const u8,
state: NavigationState,
page: *Page,
dispatch: bool,
) !*NavigationHistoryEntry {
const arena = page.session.arena;
const url = try arena.dupe(u8, _url);
// truncates our history here.
if (self.entries.items.len > self.index + 1) {
self.entries.shrinkRetainingCapacity(self.index + 1);
}
const index = self.entries.items.len;
const id = self.next_entry_id;
self.next_entry_id += 1;
const id_str = try std.fmt.allocPrint(arena, "{d}", .{id});
const entry = try arena.create(NavigationHistoryEntry);
entry.* = NavigationHistoryEntry{
.id = id_str,
.key = id_str,
.url = url,
.state = state,
};
// we don't always have a current entry...
const previous = if (self.entries.items.len > 0) self.currentEntry() else null;
try self.entries.append(arena, entry);
self.index = index;
if (previous) |prev| {
if (dispatch) {
NavigationCurrentEntryChangeEvent.dispatch(self, prev, .push);
}
}
return entry;
}
pub fn replaceEntry(
self: *Navigation,
_url: []const u8,
state: NavigationState,
page: *Page,
dispatch: bool,
) !*NavigationHistoryEntry {
const arena = page.session.arena;
const url = try arena.dupe(u8, _url);
const previous = self.currentEntry();
const id = self.next_entry_id;
self.next_entry_id += 1;
const id_str = try std.fmt.allocPrint(arena, "{d}", .{id});
const entry = try arena.create(NavigationHistoryEntry);
entry.* = NavigationHistoryEntry{
.id = id_str,
.key = previous.key,
.url = url,
.state = state,
};
self.entries.items[self.index] = entry;
if (dispatch) {
NavigationCurrentEntryChangeEvent.dispatch(self, previous, .replace);
}
return entry;
}
const NavigateOptions = struct {
const NavigateOptionsHistory = enum {
pub const ENUM_JS_USE_TAG = true;
auto,
push,
replace,
};
state: ?js.Object = null,
info: ?js.Object = null,
history: NavigateOptionsHistory = .auto,
};
pub fn navigate(
self: *Navigation,
_url: ?[]const u8,
kind: NavigationKind,
page: *Page,
) !NavigationReturn {
const arena = page.session.arena;
const url = _url orelse return error.MissingURL;
// https://github.com/WICG/navigation-api/issues/95
//
// These will only settle on same-origin navigation (mostly intended for SPAs).
// It is fine (and expected) for these to not settle on cross-origin requests :)
const committed = try page.js.createPromiseResolver(.page);
const finished = try page.js.createPromiseResolver(.page);
const new_url_string = try URL.stitch(arena, url, page.url.raw, .{});
const new_url = try URL.parse(new_url_string, null);
const is_same_document = try page.url.eqlDocument(&new_url, arena);
switch (kind) {
.push => |state| {
if (is_same_document) {
page.url = new_url;
try committed.resolve({});
// todo: Fire navigate event
try finished.resolve({});
_ = try self.pushEntry(url, .{ .source = .navigation, .value = state }, page, true);
} else {
try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
}
},
.replace => |state| {
if (is_same_document) {
page.url = new_url;
try committed.resolve({});
// todo: Fire navigate event
try finished.resolve({});
_ = try self.replaceEntry(url, .{ .source = .navigation, .value = state }, page, true);
} else {
try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
}
},
.traverse => |index| {
self.index = index;
if (is_same_document) {
page.url = new_url;
try committed.resolve({});
// todo: Fire navigate event
try finished.resolve({});
} else {
try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
}
},
.reload => {
try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
},
}
return .{
.committed = committed.promise(),
.finished = finished.promise(),
};
}
pub fn _navigate(self: *Navigation, _url: []const u8, _opts: ?NavigateOptions, page: *Page) !NavigationReturn {
const opts = _opts orelse NavigateOptions{};
const json = if (opts.state) |state| state.toJson(page.session.arena) catch return error.DataClone else null;
const kind: NavigationKind = switch (opts.history) {
.replace => .{ .replace = json },
.push, .auto => .{ .push = json },
};
return try self.navigate(_url, kind, page);
}
pub const ReloadOptions = struct {
state: ?js.Object = null,
info: ?js.Object = null,
};
pub fn _reload(self: *Navigation, _opts: ?ReloadOptions, page: *Page) !NavigationReturn {
const arena = page.session.arena;
const opts = _opts orelse ReloadOptions{};
const entry = self.currentEntry();
if (opts.state) |state| {
const previous = entry;
entry.state = .{ .source = .navigation, .value = state.toJson(arena) catch return error.DataClone };
NavigationCurrentEntryChangeEvent.dispatch(self, previous, .reload);
}
return self.navigate(entry.url, .reload, page);
}
pub const TraverseToOptions = struct {
info: ?js.Object = null,
};
pub fn _traverseTo(self: *Navigation, key: []const u8, _opts: ?TraverseToOptions, page: *Page) !NavigationReturn {
if (_opts != null) {
log.debug(.browser, "not implemented", .{ .options = _opts });
}
for (self.entries.items, 0..) |entry, i| {
if (std.mem.eql(u8, key, entry.key)) {
return try self.navigate(entry.url, .{ .traverse = i }, page);
}
}
return error.InvalidStateError;
}
pub const UpdateCurrentEntryOptions = struct {
state: js.Object,
};
pub fn _updateCurrentEntry(self: *Navigation, options: UpdateCurrentEntryOptions, page: *Page) !void {
const arena = page.session.arena;
const previous = self.currentEntry();
self.currentEntry().state = .{ .source = .navigation, .value = options.state.toJson(arena) catch return error.DataClone };
NavigationCurrentEntryChangeEvent.dispatch(self, previous, null);
}

View File

@@ -0,0 +1,58 @@
const std = @import("std");
const js = @import("../js/js.zig");
const Page = @import("../page.zig").Page;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
const EventHandler = @import("../events/event.zig").EventHandler;
const parser = @import("../netsurf.zig");
pub const NavigationEventTarget = @This();
pub const prototype = *EventTarget;
// Extend libdom event target for pure zig struct.
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .navigation },
oncurrententrychange_cbk: ?js.Function = null,
fn register(
self: *NavigationEventTarget,
alloc: std.mem.Allocator,
typ: []const u8,
listener: EventHandler.Listener,
) !?js.Function {
const target = parser.toEventTarget(NavigationEventTarget, self);
// The only time this can return null if the listener is already
// registered. But before calling `register`, all of our functions
// remove any existing listener, so it should be impossible to get null
// from this function call.
const eh = (try EventHandler.register(alloc, target, typ, listener, null)) orelse unreachable;
return eh.callback;
}
fn unregister(self: *NavigationEventTarget, typ: []const u8, cbk_id: usize) !void {
const et = parser.toEventTarget(NavigationEventTarget, self);
// check if event target has already this listener
const lst = try parser.eventTargetHasListener(et, typ, false, cbk_id);
if (lst == null) {
return;
}
// remove listener
try parser.eventTargetRemoveEventListener(et, typ, lst.?, false);
}
pub fn get_oncurrententrychange(self: *NavigationEventTarget) ?js.Function {
return self.oncurrententrychange_cbk;
}
pub fn set_oncurrententrychange(self: *NavigationEventTarget, listener: ?EventHandler.Listener, page: *Page) !void {
if (self.oncurrententrychange_cbk) |cbk| try self.unregister("currententrychange", cbk.id);
if (listener) |listen| {
self.oncurrententrychange_cbk = try self.register(page.arena, "currententrychange", listen);
} else {
self.oncurrententrychange_cbk = null;
}
}

View File

@@ -0,0 +1,224 @@
// 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 js = @import("../js/js.zig");
const Page = @import("../page.zig").Page;
const DirectEventHandler = @import("../events/event.zig").DirectEventHandler;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
const EventHandler = @import("../events/event.zig").EventHandler;
const parser = @import("../netsurf.zig");
const Navigation = @import("Navigation.zig");
const NavigationEventTarget = @import("NavigationEventTarget.zig");
pub const Interfaces = .{
Navigation,
NavigationEventTarget,
NavigationActivation,
NavigationTransition,
NavigationHistoryEntry,
};
pub const NavigationType = enum {
pub const ENUM_JS_USE_TAG = true;
push,
replace,
traverse,
reload,
};
pub const NavigationKind = union(NavigationType) {
push: ?[]const u8,
replace: ?[]const u8,
traverse: usize,
reload,
};
pub const NavigationState = struct {
source: enum { history, navigation },
value: ?[]const u8,
};
// https://developer.mozilla.org/en-US/docs/Web/API/NavigationHistoryEntry
pub const NavigationHistoryEntry = struct {
pub const prototype = *EventTarget;
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .plain },
id: []const u8,
key: []const u8,
url: ?[]const u8,
state: NavigationState,
pub fn get_id(self: *const NavigationHistoryEntry) []const u8 {
return self.id;
}
pub fn get_index(self: *const NavigationHistoryEntry, page: *Page) i32 {
const navigation = page.session.navigation;
for (navigation.entries.items, 0..) |entry, i| {
if (std.mem.eql(u8, entry.id, self.id)) {
return @intCast(i);
}
}
return -1;
}
pub fn get_key(self: *const NavigationHistoryEntry) []const u8 {
return self.key;
}
pub fn get_sameDocument(self: *const NavigationHistoryEntry, page: *Page) !bool {
const _url = self.url orelse return false;
const url = try URL.parse(_url, null);
return page.url.eqlDocument(&url, page.call_arena);
}
pub fn get_url(self: *const NavigationHistoryEntry) ?[]const u8 {
return self.url;
}
pub const StateReturn = union(enum) { value: ?js.Value, undefined: void };
pub fn _getState(self: *const NavigationHistoryEntry, page: *Page) !StateReturn {
if (self.state.source == .navigation) {
if (self.state.value) |value| {
return .{ .value = try js.Value.fromJson(page.js, value) };
}
}
return .undefined;
}
};
// https://developer.mozilla.org/en-US/docs/Web/API/NavigationActivation
pub const NavigationActivation = struct {
const NavigationActivationType = enum {
pub const ENUM_JS_USE_TAG = true;
push,
reload,
replace,
traverse,
};
entry: NavigationHistoryEntry,
from: ?NavigationHistoryEntry = null,
type: NavigationActivationType,
pub fn get_entry(self: *const NavigationActivation) NavigationHistoryEntry {
return self.entry;
}
pub fn get_from(self: *const NavigationActivation) ?NavigationHistoryEntry {
return self.from;
}
pub fn get_navigationType(self: *const NavigationActivation) NavigationActivationType {
return self.type;
}
};
// https://developer.mozilla.org/en-US/docs/Web/API/NavigationTransition
pub const NavigationTransition = struct {
finished: js.Promise,
from: NavigationHistoryEntry,
navigation_type: NavigationActivation.NavigationActivationType,
};
const Event = @import("../events/event.zig").Event;
pub const NavigationCurrentEntryChangeEvent = struct {
pub const prototype = *Event;
pub const union_make_copy = true;
pub const EventInit = struct {
from: *NavigationHistoryEntry,
navigationType: ?NavigationType = null,
};
proto: parser.Event,
from: *NavigationHistoryEntry,
navigation_type: ?NavigationType,
pub fn constructor(event_type: []const u8, opts: EventInit) !NavigationCurrentEntryChangeEvent {
const event = try parser.eventCreate();
defer parser.eventDestroy(event);
try parser.eventInit(event, event_type, .{});
parser.eventSetInternalType(event, .navigation_current_entry_change_event);
return .{
.proto = event.*,
.from = opts.from,
.navigation_type = opts.navigationType,
};
}
pub fn get_from(self: *NavigationCurrentEntryChangeEvent) *NavigationHistoryEntry {
return self.from;
}
pub fn get_navigationType(self: *const NavigationCurrentEntryChangeEvent) ?NavigationType {
return self.navigation_type;
}
pub fn dispatch(navigation: *Navigation, from: *NavigationHistoryEntry, typ: ?NavigationType) void {
log.debug(.script_event, "dispatch event", .{
.type = "currententrychange",
.source = "navigation",
});
var evt = NavigationCurrentEntryChangeEvent.constructor(
"currententrychange",
.{ .from = from, .navigationType = typ },
) catch |err| {
log.err(.app, "event constructor error", .{
.err = err,
.type = "currententrychange",
.source = "navigation",
});
return;
};
_ = parser.eventTargetDispatchEvent(
@as(*parser.EventTarget, @ptrCast(navigation)),
&evt.proto,
) catch |err| {
log.err(.app, "dispatch event error", .{
.err = err,
.type = "currententrychange",
.source = "navigation",
});
};
}
};
const testing = @import("../../testing.zig");
test "Browser: Navigation" {
try testing.htmlRunner("html/navigation/navigation.html");
try testing.htmlRunner("html/navigation/navigation_currententrychange.html");
}

View File

@@ -559,6 +559,9 @@ pub const EventType = enum(u8) {
message_event = 7, message_event = 7,
keyboard_event = 8, keyboard_event = 8,
pop_state = 9, pop_state = 9,
composition_event = 10,
navigation_current_entry_change_event = 11,
page_transition_event = 12,
}; };
pub const MutationEvent = c.dom_mutation_event; pub const MutationEvent = c.dom_mutation_event;
@@ -830,6 +833,7 @@ pub const EventTargetTBase = extern struct {
message_port = 7, message_port = 7,
screen = 8, screen = 8,
screen_orientation = 9, screen_orientation = 9,
navigation = 10,
}; };
vtable: ?*const c.struct_dom_event_target_vtable = &c.struct_dom_event_target_vtable{ vtable: ?*const c.struct_dom_event_target_vtable = &c.struct_dom_event_target_vtable{

View File

@@ -35,6 +35,10 @@ const ScriptManager = @import("ScriptManager.zig");
const SlotChangeMonitor = @import("SlotChangeMonitor.zig"); const SlotChangeMonitor = @import("SlotChangeMonitor.zig");
const HTMLDocument = @import("html/document.zig").HTMLDocument; const HTMLDocument = @import("html/document.zig").HTMLDocument;
const NavigationKind = @import("navigation/root.zig").NavigationKind;
const NavigationCurrentEntryChangeEvent = @import("navigation/root.zig").NavigationCurrentEntryChangeEvent;
const PageTransitionEvent = @import("events/PageTransitionEvent.zig");
const js = @import("js/js.zig"); const js = @import("js/js.zig");
const URL = @import("../url.zig").URL; const URL = @import("../url.zig").URL;
@@ -79,6 +83,8 @@ pub const Page = struct {
// indicates intention to navigate to another page on the next loop execution. // indicates intention to navigate to another page on the next loop execution.
delayed_navigation: bool = false, delayed_navigation: bool = false,
req_id: ?usize = null,
navigated_options: ?NavigatedOpts = null,
state_pool: *std.heap.MemoryPool(State), state_pool: *std.heap.MemoryPool(State),
@@ -99,6 +105,10 @@ pub const Page = struct {
notified_network_idle: IdleNotification = .init, notified_network_idle: IdleNotification = .init,
notified_network_almost_idle: IdleNotification = .init, notified_network_almost_idle: IdleNotification = .init,
// Indicates if the page's document is open or close.
// Relates with https://developer.mozilla.org/en-US/docs/Web/API/Document/open
open: bool = false,
const Mode = union(enum) { const Mode = union(enum) {
pre: void, pre: void,
err: anyerror, err: anyerror,
@@ -165,6 +175,9 @@ pub const Page = struct {
self.http_client.abort(); self.http_client.abort();
self.script_manager.reset(); self.script_manager.reset();
parser.deinit();
parser.init();
self.load_state = .parsing; self.load_state = .parsing;
self.mode = .{ .pre = {} }; self.mode = .{ .pre = {} };
_ = self.session.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 }); _ = self.session.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
@@ -485,16 +498,16 @@ pub const Page = struct {
} }
{ {
std.debug.print("\nprimary schedule: {d}\n", .{self.scheduler.primary.count()}); std.debug.print("\nhigh_priority schedule: {d}\n", .{self.scheduler.high_priority.count()});
var it = self.scheduler.primary.iterator(); var it = self.scheduler.high_priority.iterator();
while (it.next()) |task| { while (it.next()) |task| {
std.debug.print(" - {s} schedule: {d}ms\n", .{ task.name, task.ms - now }); std.debug.print(" - {s} schedule: {d}ms\n", .{ task.name, task.ms - now });
} }
} }
{ {
std.debug.print("\nsecondary schedule: {d}\n", .{self.scheduler.secondary.count()}); std.debug.print("\nlow_priority schedule: {d}\n", .{self.scheduler.low_priority.count()});
var it = self.scheduler.secondary.iterator(); var it = self.scheduler.low_priority.iterator();
while (it.next()) |task| { while (it.next()) |task| {
std.debug.print(" - {s} schedule: {d}ms\n", .{ task.name, task.ms - now }); std.debug.print(" - {s} schedule: {d}ms\n", .{ task.name, task.ms - now });
} }
@@ -542,27 +555,64 @@ pub const Page = struct {
try self.reset(); try self.reset();
} }
const req_id = self.http_client.nextReqId();
log.info(.http, "navigate", .{ log.info(.http, "navigate", .{
.url = request_url, .url = request_url,
.method = opts.method, .method = opts.method,
.reason = opts.reason, .reason = opts.reason,
.body = opts.body != null, .body = opts.body != null,
.req_id = req_id,
}); });
// if the url is about:blank, nothing to do. // if the url is about:blank, we load an empty HTML document in the
// page and dispatch the events.
if (std.mem.eql(u8, "about:blank", request_url)) { if (std.mem.eql(u8, "about:blank", request_url)) {
const html_doc = try parser.documentHTMLParseFromStr(""); const html_doc = try parser.documentHTMLParseFromStr("");
try self.setDocument(html_doc); try self.setDocument(html_doc);
// Assume we parsed the document.
// It's important to force a reset during the following navigation.
self.mode = .parsed;
// We do not processHTMLDoc here as we know we don't have any scripts // We do not processHTMLDoc here as we know we don't have any scripts
// This assumption may be false when CDP Page.addScriptToEvaluateOnNewDocument is implemented // This assumption may be false when CDP Page.addScriptToEvaluateOnNewDocument is implemented
try HTMLDocument.documentIsComplete(self.window.document, self); self.documentIsComplete();
self.session.browser.notification.dispatch(.page_navigate, &.{
.req_id = req_id,
.opts = opts,
.url = request_url,
.timestamp = timestamp(),
});
self.session.browser.notification.dispatch(.page_navigated, &.{
.req_id = req_id,
.opts = .{
.cdp_id = opts.cdp_id,
.reason = opts.reason,
.method = opts.method,
},
.url = request_url,
.timestamp = timestamp(),
});
// force next request id manually b/c we won't create a real req.
_ = self.http_client.incrReqId();
return; return;
} }
const owned_url = try self.arena.dupeZ(u8, request_url); const owned_url = try self.arena.dupeZ(u8, request_url);
self.url = try URL.parse(owned_url, null); self.url = try URL.parse(owned_url, null);
self.req_id = req_id;
self.navigated_options = .{
.cdp_id = opts.cdp_id,
.reason = opts.reason,
.method = opts.method,
};
var headers = try self.http_client.newHeaders(); var headers = try self.http_client.newHeaders();
if (opts.header) |hdr| try headers.add(hdr); if (opts.header) |hdr| try headers.add(hdr);
try self.requestCookie(.{ .is_navigation = true }).headersForRequest(self.arena, owned_url, &headers); try self.requestCookie(.{ .is_navigation = true }).headersForRequest(self.arena, owned_url, &headers);
@@ -570,6 +620,7 @@ pub const Page = struct {
// We dispatch page_navigate event before sending the request. // We dispatch page_navigate event before sending the request.
// It ensures the event page_navigated is not dispatched before this one. // It ensures the event page_navigated is not dispatched before this one.
self.session.browser.notification.dispatch(.page_navigate, &.{ self.session.browser.notification.dispatch(.page_navigate, &.{
.req_id = req_id,
.opts = opts, .opts = opts,
.url = owned_url, .url = owned_url,
.timestamp = timestamp(), .timestamp = timestamp(),
@@ -635,13 +686,20 @@ pub const Page = struct {
log.err(.browser, "document is complete", .{ .err = err }); log.err(.browser, "document is complete", .{ .err = err });
}; };
std.debug.assert(self.req_id != null);
std.debug.assert(self.navigated_options != null);
self.session.browser.notification.dispatch(.page_navigated, &.{ self.session.browser.notification.dispatch(.page_navigated, &.{
.req_id = self.req_id.?,
.opts = self.navigated_options.?,
.url = self.url.raw, .url = self.url.raw,
.timestamp = timestamp(), .timestamp = timestamp(),
}); });
} }
fn _documentIsComplete(self: *Page) !void { fn _documentIsComplete(self: *Page) !void {
self.session.browser.runMicrotasks();
self.session.browser.runMessageLoop();
try HTMLDocument.documentIsComplete(self.window.document, self); try HTMLDocument.documentIsComplete(self.window.document, self);
// dispatch window.load event // dispatch window.load event
@@ -654,6 +712,8 @@ pub const Page = struct {
parser.toEventTarget(Window, &self.window), parser.toEventTarget(Window, &self.window),
loadevt, loadevt,
); );
PageTransitionEvent.dispatch(&self.window, .show, false);
} }
fn pageHeaderDoneCallback(transfer: *Http.Transfer) !void { fn pageHeaderDoneCallback(transfer: *Http.Transfer) !void {
@@ -687,14 +747,14 @@ pub const Page = struct {
log.debug(.http, "navigate first chunk", .{ .content_type = mime.content_type, .len = data.len }); log.debug(.http, "navigate first chunk", .{ .content_type = mime.content_type, .len = data.len });
self.mode = switch (mime.content_type) { self.mode = switch (mime.content_type) {
.text_html => .{ .html = try parser.Parser.init(mime.charsetString()) }, .text_html => .{ .html = try parser.Parser.init(mime.charsetStringZ()) },
.application_json, .application_json,
.text_javascript, .text_javascript,
.text_css, .text_css,
.text_plain, .text_plain,
=> blk: { => blk: {
var p = try parser.Parser.init(mime.charsetString()); var p = try parser.Parser.init(mime.charsetStringZ());
try p.process("<html><head><meta charset=\"utf-8\"></head><body><pre>"); try p.process("<html><head><meta charset=\"utf-8\"></head><body><pre>");
break :blk .{ .text = p }; break :blk .{ .text = p };
}, },
@@ -736,6 +796,9 @@ pub const Page = struct {
var self: *Page = @ptrCast(@alignCast(ctx)); var self: *Page = @ptrCast(@alignCast(ctx));
self.clearTransferArena(); self.clearTransferArena();
// We need to handle different navigation types differently.
try self.session.navigation.processNavigation(self);
switch (self.mode) { switch (self.mode) {
.pre => { .pre => {
// Received a response without a body like: https://httpbin.io/status/200 // Received a response without a body like: https://httpbin.io/status/200
@@ -814,9 +877,6 @@ pub const Page = struct {
unreachable; unreachable;
}, },
} }
// Push the navigation after a successful load.
try self.session.history.pushNavigation(self.url.raw, self);
} }
fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void { fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
@@ -859,7 +919,7 @@ pub const Page = struct {
self.window.setStorageShelf( self.window.setStorageShelf(
try self.session.storage_shed.getOrPut(try self.origin(self.arena)), try self.session.storage_shed.getOrPut(try self.origin(self.arena)),
); );
try self.window.replaceLocation(.{ .url = try self.url.toWebApi(self.arena) }); try self.window.changeLocation(self.url.raw, self);
} }
pub const MouseEvent = struct { pub const MouseEvent = struct {
@@ -894,7 +954,7 @@ pub const Page = struct {
fn windowClicked(node: *parser.EventNode, event: *parser.Event) void { fn windowClicked(node: *parser.EventNode, event: *parser.Event) void {
const self: *Page = @fieldParentPtr("window_clicked_event_node", node); const self: *Page = @fieldParentPtr("window_clicked_event_node", node);
self._windowClicked(event) catch |err| { self._windowClicked(event) catch |err| {
log.err(.browser, "click handler error", .{ .err = err }); log.err(.input, "click handler error", .{ .err = err });
}; };
} }
@@ -906,18 +966,22 @@ pub const Page = struct {
.a => { .a => {
const element: *parser.Element = @ptrCast(node); const element: *parser.Element = @ptrCast(node);
const href = (try parser.elementGetAttribute(element, "href")) orelse return; const href = (try parser.elementGetAttribute(element, "href")) orelse return;
try self.navigateFromWebAPI(href, .{}); log.debug(.input, "window click on link", .{ .tag = tag, .href = href });
try self.navigateFromWebAPI(href, .{}, .{ .push = null });
return;
}, },
.input => { .input => {
const element: *parser.Element = @ptrCast(node); const element: *parser.Element = @ptrCast(node);
const input_type = try parser.inputGetType(@ptrCast(element)); const input_type = try parser.inputGetType(@ptrCast(element));
if (std.ascii.eqlIgnoreCase(input_type, "submit")) { if (std.ascii.eqlIgnoreCase(input_type, "submit")) {
log.debug(.input, "window click on submit input", .{ .tag = tag });
return self.elementSubmitForm(element); return self.elementSubmitForm(element);
} }
}, },
.button => { .button => {
const element: *parser.Element = @ptrCast(node); const element: *parser.Element = @ptrCast(node);
const button_type = try parser.buttonGetType(@ptrCast(element)); const button_type = try parser.buttonGetType(@ptrCast(element));
log.debug(.input, "window click on button", .{ .tag = tag, .button_type = button_type });
if (std.ascii.eqlIgnoreCase(button_type, "submit")) { if (std.ascii.eqlIgnoreCase(button_type, "submit")) {
return self.elementSubmitForm(element); return self.elementSubmitForm(element);
} }
@@ -929,6 +993,12 @@ pub const Page = struct {
}, },
else => {}, else => {},
} }
log.debug(.input, "window click on element", .{ .tag = tag });
// Set the focus on the clicked element.
// Thanks to parser.nodeHTMLGetTagType, we know nod is an element.
// We assume we have a ElementHTML.
const Document = @import("dom/document.zig").Document;
try Document.setFocus(@ptrCast(self.window.document), @as(*parser.ElementHTML, @ptrCast(node)), self);
} }
pub const KeyboardEvent = struct { pub const KeyboardEvent = struct {
@@ -971,7 +1041,7 @@ pub const Page = struct {
fn keydownCallback(node: *parser.EventNode, event: *parser.Event) void { fn keydownCallback(node: *parser.EventNode, event: *parser.Event) void {
const self: *Page = @fieldParentPtr("keydown_event_node", node); const self: *Page = @fieldParentPtr("keydown_event_node", node);
self._keydownCallback(event) catch |err| { self._keydownCallback(event) catch |err| {
log.err(.browser, "keydown handler error", .{ .err = err }); log.err(.input, "keydown handler error", .{ .err = err });
}; };
} }
@@ -985,23 +1055,29 @@ pub const Page = struct {
if (std.mem.eql(u8, new_key, "Dead")) { if (std.mem.eql(u8, new_key, "Dead")) {
return; return;
} }
switch (tag) { switch (tag) {
.input => { .input => {
const element: *parser.Element = @ptrCast(node); const element: *parser.Element = @ptrCast(node);
const input_type = try parser.inputGetType(@ptrCast(element)); const input_type = try parser.inputGetType(@ptrCast(element));
if (std.mem.eql(u8, input_type, "text")) { log.debug(.input, "key down on input", .{ .tag = tag, .key = new_key, .input_type = input_type });
if (std.mem.eql(u8, new_key, "Enter")) { if (std.mem.eql(u8, new_key, "Enter")) {
const form = (try self.formForElement(element)) orelse return; const form = (try self.formForElement(element)) orelse return;
return self.submitForm(@ptrCast(form), null); return self.submitForm(@ptrCast(form), null);
}
const value = try parser.inputGetValue(@ptrCast(element));
const new_value = try std.mem.concat(self.arena, u8, &.{ value, new_key });
try parser.inputSetValue(@ptrCast(element), new_value);
} }
if (std.mem.eql(u8, input_type, "radio")) {
return;
}
if (std.mem.eql(u8, input_type, "checkbox")) {
return;
}
const value = try parser.inputGetValue(@ptrCast(element));
const new_value = try std.mem.concat(self.arena, u8, &.{ value, new_key });
try parser.inputSetValue(@ptrCast(element), new_value);
}, },
.textarea => { .textarea => {
log.debug(.input, "key down on textarea", .{ .tag = tag, .key = new_key });
const value = try parser.textareaGetValue(@ptrCast(node)); const value = try parser.textareaGetValue(@ptrCast(node));
if (std.mem.eql(u8, new_key, "Enter")) { if (std.mem.eql(u8, new_key, "Enter")) {
new_key = "\n"; new_key = "\n";
@@ -1009,6 +1085,33 @@ pub const Page = struct {
const new_value = try std.mem.concat(self.arena, u8, &.{ value, new_key }); const new_value = try std.mem.concat(self.arena, u8, &.{ value, new_key });
try parser.textareaSetValue(@ptrCast(node), new_value); try parser.textareaSetValue(@ptrCast(node), new_value);
}, },
else => {
log.debug(.input, "key down event", .{ .tag = tag, .key = new_key });
},
}
}
// insertText is a shortcut to insert text into the active element.
pub fn insertText(self: *Page, v: []const u8) !void {
const Document = @import("dom/document.zig").Document;
const element = (try Document.getActiveElement(@ptrCast(self.window.document), self)) orelse return;
const node = parser.elementToNode(element);
const tag = (try parser.nodeHTMLGetTagType(node)) orelse return;
switch (tag) {
.input => {
const input_type = try parser.inputGetType(@ptrCast(element));
if (std.mem.eql(u8, input_type, "text")) {
const value = try parser.inputGetValue(@ptrCast(element));
const new_value = try std.mem.concat(self.arena, u8, &.{ value, v });
try parser.inputSetValue(@ptrCast(element), new_value);
}
},
.textarea => {
const value = try parser.textareaGetValue(@ptrCast(node));
const new_value = try std.mem.concat(self.arena, u8, &.{ value, v });
try parser.textareaSetValue(@ptrCast(node), new_value);
},
else => {}, else => {},
} }
} }
@@ -1018,8 +1121,32 @@ pub const Page = struct {
// As such we schedule the function to be called as soon as possible. // As such we schedule the function to be called as soon as possible.
// The page.arena is safe to use here, but the transfer_arena exists // The page.arena is safe to use here, but the transfer_arena exists
// specifically for this type of lifetime. // specifically for this type of lifetime.
pub fn navigateFromWebAPI(self: *Page, url: []const u8, opts: NavigateOpts) !void { pub fn navigateFromWebAPI(self: *Page, url: []const u8, opts: NavigateOpts, kind: NavigationKind) !void {
const session = self.session; const session = self.session;
const stitched_url = try URL.stitch(
session.transfer_arena,
url,
self.url.raw,
.{
.alloc = .always,
.null_terminated = true,
},
);
// Force will force a page load.
// Otherwise, we need to check if this is a true navigation.
if (!opts.force) {
// If we are navigating within the same document, just change URL.
const new_url = try URL.parse(stitched_url, null);
if (try self.url.eqlDocument(&new_url, session.transfer_arena)) {
self.url = new_url;
try self.window.changeLocation(self.url.raw, self);
try session.navigation.updateEntries(stitched_url, kind, self, true);
return;
}
}
if (session.queued_navigation != null) { if (session.queued_navigation != null) {
// It might seem like this should never happen. And it might not, // It might seem like this should never happen. And it might not,
// BUT..consider the case where we have script like: // BUT..consider the case where we have script like:
@@ -1042,9 +1169,11 @@ pub const Page = struct {
session.queued_navigation = .{ session.queued_navigation = .{
.opts = opts, .opts = opts,
.url = try URL.stitch(session.transfer_arena, url, self.url.raw, .{ .alloc = .always }), .url = stitched_url,
}; };
session.navigation_kind = kind;
self.http_client.abort(); self.http_client.abort();
// In v8, this throws an exception which JS code cannot catch. // In v8, this throws an exception which JS code cannot catch.
@@ -1095,7 +1224,7 @@ pub const Page = struct {
} else { } else {
action = try URL.concatQueryString(transfer_arena, action, buf.items); action = try URL.concatQueryString(transfer_arena, action, buf.items);
} }
try self.navigateFromWebAPI(action, opts); try self.navigateFromWebAPI(action, opts, .{ .push = null });
} }
pub fn isNodeAttached(self: *const Page, node: *parser.Node) bool { pub fn isNodeAttached(self: *const Page, node: *parser.Node) bool {
@@ -1145,6 +1274,10 @@ pub const Page = struct {
const current_origin = try self.origin(self.call_arena); const current_origin = try self.origin(self.call_arena);
return std.mem.startsWith(u8, url, current_origin); return std.mem.startsWith(u8, url, current_origin);
} }
pub fn getTitle(self: *const Page) ![]const u8 {
return try parser.documentHTMLGetTitle(self.window.document);
}
}; };
pub const NavigateReason = enum { pub const NavigateReason = enum {
@@ -1153,6 +1286,7 @@ pub const NavigateReason = enum {
form, form,
script, script,
history, history,
navigation,
}; };
pub const NavigateOpts = struct { pub const NavigateOpts = struct {
@@ -1161,6 +1295,13 @@ pub const NavigateOpts = struct {
method: Http.Method = .GET, method: Http.Method = .GET,
body: ?[]const u8 = null, body: ?[]const u8 = null,
header: ?[:0]const u8 = null, header: ?[:0]const u8 = null,
force: bool = false,
};
pub const NavigatedOpts = struct {
cdp_id: ?i64 = null,
reason: NavigateReason = .address_bar,
method: Http.Method = .GET,
}; };
const IdleNotification = union(enum) { const IdleNotification = union(enum) {

View File

@@ -41,6 +41,10 @@ const FlatRenderer = struct {
const Element = @import("dom/element.zig").Element; const Element = @import("dom/element.zig").Element;
// Define the size of each element in the grid.
const default_w = 5;
const default_h = 5;
// we expect allocator to be an arena // we expect allocator to be an arena
pub fn init(allocator: Allocator) FlatRenderer { pub fn init(allocator: Allocator) FlatRenderer {
return .{ return .{
@@ -62,10 +66,10 @@ const FlatRenderer = struct {
gop.value_ptr.* = x; gop.value_ptr.* = x;
} }
const _x: f64 = @floatFromInt(x); const _x: f64 = @floatFromInt(x * default_w);
const y: f64 = 0.0; const y: f64 = 0.0;
const w: f64 = 1.0; const w: f64 = default_w;
const h: f64 = 1.0; const h: f64 = default_h;
return .{ return .{
.x = _x, .x = _x,
@@ -98,18 +102,20 @@ const FlatRenderer = struct {
} }
pub fn width(self: *const FlatRenderer) u32 { pub fn width(self: *const FlatRenderer) u32 {
return @max(@as(u32, @intCast(self.elements.items.len)), 1); // At least 1 pixel even if empty return @max(@as(u32, @intCast(self.elements.items.len * default_w)), default_w); // At least default width pixels even if empty
} }
pub fn height(_: *const FlatRenderer) u32 { pub fn height(_: *const FlatRenderer) u32 {
return 1; return 5;
} }
pub fn getElementAtPosition(self: *const FlatRenderer, x: i32, y: i32) ?*parser.Element { pub fn getElementAtPosition(self: *const FlatRenderer, _x: i32, y: i32) ?*parser.Element {
if (y != 0 or x < 0) { if (y < 0 or y > default_h or _x < 0) {
return null; return null;
} }
const x = @divFloor(_x, default_w);
const elements = self.elements.items; const elements = self.elements.items;
return if (x < elements.len) @ptrFromInt(elements[@intCast(x)]) else null; return if (x < elements.len) @ptrFromInt(elements[@intCast(x)]) else null;
} }

View File

@@ -22,9 +22,11 @@ const Allocator = std.mem.Allocator;
const js = @import("js/js.zig"); const js = @import("js/js.zig");
const Page = @import("page.zig").Page; const Page = @import("page.zig").Page;
const NavigationKind = @import("navigation/root.zig").NavigationKind;
const Browser = @import("browser.zig").Browser; const Browser = @import("browser.zig").Browser;
const NavigateOpts = @import("page.zig").NavigateOpts; const NavigateOpts = @import("page.zig").NavigateOpts;
const History = @import("html/History.zig"); const History = @import("html/History.zig");
const Navigation = @import("navigation/Navigation.zig");
const log = @import("../log.zig"); const log = @import("../log.zig");
const parser = @import("netsurf.zig"); const parser = @import("netsurf.zig");
@@ -57,6 +59,8 @@ pub const Session = struct {
// History is persistent across the "tab". // History is persistent across the "tab".
// https://developer.mozilla.org/en-US/docs/Web/API/History // https://developer.mozilla.org/en-US/docs/Web/API/History
history: History = .{}, history: History = .{},
navigation: Navigation = .{},
navigation_kind: ?NavigationKind = null,
page: ?Page = null, page: ?Page = null,
@@ -100,6 +104,9 @@ pub const Session = struct {
// We need to init this early as JS event handlers may be registered through Runtime.evaluate before the first html doc is loaded // We need to init this early as JS event handlers may be registered through Runtime.evaluate before the first html doc is loaded
parser.init(); parser.init();
// creates a new event target for Navigation
self.navigation.resetForNewPage();
const page_arena = &self.browser.page_arena; const page_arena = &self.browser.page_arena;
_ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 }); _ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
_ = self.browser.state_pool.reset(.{ .retain_with_limit = 4 * 1024 }); _ = self.browser.state_pool.reset(.{ .retain_with_limit = 4 * 1024 });

View File

@@ -18,6 +18,8 @@
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Writer = std.Io.Writer;
const ada = @import("ada");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
@@ -35,182 +37,235 @@ pub const Interfaces = .{
EntryIterable, EntryIterable,
}; };
// https://url.spec.whatwg.org/#url /// https://developer.mozilla.org/en-US/docs/Web/API/URL/URL
//
// TODO we could avoid many of these getter string allocatoration in two differents
// way:
//
// 1. We can eventually get the slice of scheme *with* the following char in
// the underlying string. But I don't know if it's possible and how to do that.
// I mean, if the rawuri contains `https://foo.bar`, uri.scheme is a slice
// containing only `https`. I want `https:` so, in theory, I don't need to
// allocatorate data, I should be able to retrieve the scheme + the following `:`
// from rawuri.
//
// 2. The other way would be to copy the `std.Uri` code to have a dedicated
// parser including the characters we want for the web API.
pub const URL = struct { pub const URL = struct {
uri: std.Uri, internal: ada.URL,
/// We prefer in-house search params solution here;
/// ada's search params impl use more memory.
/// It also offers it's own iterator implementation
/// where we'd like to use ours.
search_params: URLSearchParams, search_params: URLSearchParams,
pub const empty = URL{ pub const empty = URL{
.uri = .{ .scheme = "" }, .internal = null,
.search_params = .{}, .search_params = .{},
}; };
const URLArg = union(enum) { // You can use an existing URL object for either argument, and it will be
url: *URL, // stringified from the object's href property.
element: *parser.ElementHTML, const ConstructorArg = union(enum) {
string: []const u8, string: []const u8,
url: *const URL,
element: *parser.Element,
fn toString(self: URLArg, arena: Allocator) !?[]const u8 { fn toString(self: ConstructorArg, page: *Page) ![]const u8 {
switch (self) { return switch (self) {
.string => |s| return s, .string => |s| s,
.url => |url| return try url.toString(arena), .url => |url| url._toString(page),
.element => |e| return try parser.elementGetAttribute(@ptrCast(e), "href"), .element => |e| {
} const attrib = try parser.elementGetAttribute(@ptrCast(e), "href") orelse {
return error.InvalidArgument;
};
return attrib;
},
};
} }
}; };
pub fn constructor(url: URLArg, base: ?URLArg, page: *Page) !URL { pub fn constructor(url: ConstructorArg, maybe_base: ?ConstructorArg, page: *Page) !URL {
const arena = page.arena; const url_str = try url.toString(page);
const url_str = try url.toString(arena) orelse return error.InvalidArgument;
var raw: ?[]const u8 = null; const internal = try blk: {
if (base) |b| { if (maybe_base) |base| {
if (try b.toString(arena)) |bb| { break :blk ada.parseWithBase(url_str, try base.toString(page));
raw = try @import("../../url.zig").URL.stitch(arena, url_str, bb, .{});
} }
}
if (raw == null) { break :blk ada.parse(url_str);
// if it was a URL, then it's already be owned by the arena
raw = if (url == .url) url_str else try arena.dupe(u8, url_str);
}
const uri = std.Uri.parse(raw.?) catch blk: {
if (!std.mem.endsWith(u8, raw.?, "://")) {
return error.TypeError;
}
// schema only is valid!
break :blk std.Uri{
.scheme = raw.?[0 .. raw.?.len - 3],
.host = .{ .percent_encoded = "" },
};
}; };
return init(arena, uri);
}
pub fn init(arena: Allocator, uri: std.Uri) !URL {
return .{ return .{
.uri = uri, .internal = internal,
.search_params = try URLSearchParams.init( .search_params = try prepareSearchParams(page.arena, internal),
arena,
uriComponentNullStr(uri.query),
),
}; };
} }
pub fn initWithoutSearchParams(uri: std.Uri) URL { pub fn destructor(self: *const URL) void {
return .{ .uri = uri, .search_params = .{} }; // Not tracked by arena.
return ada.free(self.internal);
} }
pub fn get_origin(self: *URL, page: *Page) ![]const u8 { /// Only to be used by `Location` API. `url` MUST NOT provide search params.
var aw = std.Io.Writer.Allocating.init(page.arena); pub fn initForLocation(url: []const u8) !URL {
try self.uri.writeToStream(&aw.writer, .{ return .{ .internal = try ada.parse(url), .search_params = .{} };
.scheme = true,
.authentication = false,
.authority = true,
.path = false,
.query = false,
.fragment = false,
});
return aw.written();
} }
// get_href returns the URL by writing all its components. /// Reinitializes the URL by parsing given `url`. Search params can be provided.
pub fn get_href(self: *URL, page: *Page) ![]const u8 { pub fn reinit(self: *URL, url: []const u8, page: *Page) !void {
return self.toString(page.arena); _ = ada.setHref(self.internal, url);
if (!ada.isValid(self.internal)) return error.Internal;
self.search_params = try prepareSearchParams(page.arena, self.internal);
} }
pub fn _toString(self: *URL, page: *Page) ![]const u8 { /// Prepares a `URLSearchParams` from given `internal`.
return self.toString(page.arena); /// Resets `search` of `internal`.
fn prepareSearchParams(arena: Allocator, internal: ada.URL) !URLSearchParams {
const maybe_search = ada.getSearchNullable(internal);
// Empty.
if (maybe_search.data == null) return .{};
const search = maybe_search.data[0..maybe_search.length];
const search_params = URLSearchParams.initFromString(arena, search);
// After a call to this function, search params are tracked by
// `search_params`. So we reset the internal's search.
ada.clearSearch(internal);
return search_params;
} }
// format the url with all its components. pub fn clearPort(self: *const URL) void {
pub fn toString(self: *const URL, arena: Allocator) ![]const u8 { return ada.clearPort(self.internal);
var aw = std.Io.Writer.Allocating.init(arena); }
try self.uri.writeToStream(&aw.writer, .{
.scheme = true,
.authentication = true,
.authority = true,
.path = uriComponentNullStr(self.uri.path).len > 0,
});
pub fn clearHash(self: *const URL) void {
return ada.clearHash(self.internal);
}
/// Returns a boolean indicating whether or not an absolute URL,
/// or a relative URL combined with a base URL, are parsable and valid.
pub fn static_canParse(url: ConstructorArg, maybe_base: ?ConstructorArg, page: *Page) !bool {
const url_str = try url.toString(page);
if (maybe_base) |base| {
return ada.canParseWithBase(url_str, try base.toString(page));
}
return ada.canParse(url_str);
}
/// Alias to get_href.
pub fn _toString(self: *const URL, page: *Page) ![]const u8 {
return self.get_href(page);
}
// Getters.
pub fn get_searchParams(self: *URL) *URLSearchParams {
return &self.search_params;
}
pub fn get_origin(self: *const URL, page: *Page) ![]const u8 {
// `ada.getOriginNullable` allocates memory in order to find the `origin`.
// We'd like to use our arena allocator for such case;
// so here we allocate the `origin` in page arena and free the original.
const maybe_origin = ada.getOriginNullable(self.internal);
if (maybe_origin.data == null) {
return "";
}
defer ada.freeOwnedString(maybe_origin);
const origin = maybe_origin.data[0..maybe_origin.length];
return page.call_arena.dupe(u8, origin);
}
pub fn get_href(self: *const URL, page: *Page) ![]const u8 {
var w: Writer.Allocating = .init(page.arena);
// If URL is not valid, return immediately.
if (!ada.isValid(self.internal)) {
return "";
}
// Since the earlier check passed, this can't be null.
const str = ada.getHrefNullable(self.internal);
const href = str.data[0..str.length];
// This can't be null either.
const comps = ada.getComponents(self.internal);
// If hash provided, we write it after we fit-in the search params.
const has_hash = comps.hash_start != ada.URLOmitted;
const href_part = if (has_hash) href[0..comps.hash_start] else href;
try w.writer.writeAll(href_part);
// Write search params if provided.
if (self.search_params.get_size() > 0) { if (self.search_params.get_size() > 0) {
try aw.writer.writeByte('?'); try w.writer.writeByte('?');
try self.search_params.write(&aw.writer); try self.search_params.write(&w.writer);
} }
{ // Write hash if provided before.
const fragment = uriComponentNullStr(self.uri.fragment); const hash = self.get_hash();
if (fragment.len > 0) { try w.writer.writeAll(hash);
try aw.writer.writeByte('#');
try aw.writer.writeAll(fragment); return w.written();
} }
pub fn get_username(self: *const URL) []const u8 {
const username = ada.getUsernameNullable(self.internal);
if (username.data == null) {
return "";
} }
return aw.written(); return username.data[0..username.length];
} }
pub fn get_protocol(self: *const URL) []const u8 { pub fn get_password(self: *const URL) []const u8 {
// std.Uri keeps a pointer to "https", "http" (scheme part) so we know const password = ada.getPasswordNullable(self.internal);
// its followed by ':'. if (password.data == null) {
const scheme = self.uri.scheme; return "";
return scheme.ptr[0 .. scheme.len + 1]; }
return password.data[0..password.length];
} }
pub fn get_username(self: *URL) []const u8 { pub fn get_port(self: *const URL) []const u8 {
return uriComponentNullStr(self.uri.user); const port = ada.getPortNullable(self.internal);
if (port.data == null) {
return "";
}
return port.data[0..port.length];
} }
pub fn get_password(self: *URL) []const u8 { pub fn get_hash(self: *const URL) []const u8 {
return uriComponentNullStr(self.uri.password); const hash = ada.getHashNullable(self.internal);
if (hash.data == null) {
return "";
}
return hash.data[0..hash.length];
} }
pub fn get_host(self: *URL, page: *Page) ![]const u8 { pub fn get_host(self: *const URL) []const u8 {
var aw = std.Io.Writer.Allocating.init(page.arena); const host = ada.getHostNullable(self.internal);
try self.uri.writeToStream(&aw.writer, .{ if (host.data == null) {
.scheme = false, return "";
.authentication = false, }
.authority = true,
.path = false, return host.data[0..host.length];
.query = false,
.fragment = false,
});
return aw.written();
} }
pub fn get_hostname(self: *URL) []const u8 { pub fn get_hostname(self: *const URL) []const u8 {
return uriComponentNullStr(self.uri.host); const hostname = ada.getHostnameNullable(self.internal);
if (hostname.data == null) {
return "";
}
return hostname.data[0..hostname.length];
} }
pub fn get_port(self: *URL, page: *Page) ![]const u8 { pub fn get_pathname(self: *const URL) []const u8 {
const arena = page.arena; const path = ada.getPathnameNullable(self.internal);
if (self.uri.port == null) return try arena.dupe(u8, ""); // Return a slash if path is null.
if (path.data == null) {
return "/";
}
var aw = std.Io.Writer.Allocating.init(arena); return path.data[0..path.length];
try aw.writer.printInt(self.uri.port.?, 10, .lower, .{});
return aw.written();
} }
pub fn get_pathname(self: *URL) []const u8 { /// get_search depends on the current state of `search_params`.
if (uriComponentStr(self.uri.path).len == 0) return "/"; pub fn get_search(self: *const URL, page: *Page) ![]const u8 {
return uriComponentStr(self.uri.path);
}
pub fn get_search(self: *URL, page: *Page) ![]const u8 {
const arena = page.arena; const arena = page.arena;
if (self.search_params.get_size() == 0) { if (self.search_params.get_size() == 0) {
@@ -223,72 +278,104 @@ pub const URL = struct {
return buf.items; return buf.items;
} }
pub fn set_search(self: *URL, qs_: ?[]const u8, page: *Page) !void { pub fn get_protocol(self: *const URL) []const u8 {
const protocol = ada.getProtocolNullable(self.internal);
if (protocol.data == null) {
return "";
}
return protocol.data[0..protocol.length];
}
// Setters.
/// Ada-url don't define any errors, so we just prefer one unified
/// `Internal` error for failing cases.
const SetterError = error{Internal};
pub fn set_href(self: *URL, input: []const u8, page: *Page) !void {
_ = ada.setHref(self.internal, input);
if (!ada.isValid(self.internal)) return error.Internal;
// Can't call `get_search` here since it uses `search_params`.
self.search_params = try prepareSearchParams(page.arena, self.internal);
}
pub fn set_host(self: *const URL, input: []const u8) SetterError!void {
_ = ada.setHost(self.internal, input);
if (!ada.isValid(self.internal)) return error.Internal;
}
pub fn set_hostname(self: *const URL, input: []const u8) SetterError!void {
_ = ada.setHostname(self.internal, input);
if (!ada.isValid(self.internal)) return error.Internal;
}
pub fn set_protocol(self: *const URL, input: []const u8) SetterError!void {
_ = ada.setProtocol(self.internal, input);
if (!ada.isValid(self.internal)) return error.Internal;
}
pub fn set_username(self: *const URL, input: []const u8) SetterError!void {
_ = ada.setUsername(self.internal, input);
if (!ada.isValid(self.internal)) return error.Internal;
}
pub fn set_password(self: *const URL, input: []const u8) SetterError!void {
_ = ada.setPassword(self.internal, input);
if (!ada.isValid(self.internal)) return error.Internal;
}
pub fn set_port(self: *const URL, input: []const u8) SetterError!void {
_ = ada.setPort(self.internal, input);
if (!ada.isValid(self.internal)) return error.Internal;
}
pub fn set_pathname(self: *const URL, input: []const u8) SetterError!void {
_ = ada.setPathname(self.internal, input);
if (!ada.isValid(self.internal)) return error.Internal;
}
pub fn set_search(self: *URL, maybe_input: ?[]const u8, page: *Page) !void {
self.search_params = .{}; self.search_params = .{};
if (qs_) |qs| { if (maybe_input) |input| {
self.search_params = try URLSearchParams.init(page.arena, qs); self.search_params = try .initFromString(page.arena, input);
} }
} }
pub fn get_hash(self: *URL, page: *Page) ![]const u8 { pub fn set_hash(self: *const URL, input: []const u8) !void {
const arena = page.arena; ada.setHash(self.internal, input);
if (self.uri.fragment == null) return try arena.dupe(u8, ""); if (!ada.isValid(self.internal)) return error.Internal;
return try std.mem.concat(arena, u8, &[_][]const u8{ "#", uriComponentNullStr(self.uri.fragment) });
}
pub fn get_searchParams(self: *URL) *URLSearchParams {
return &self.search_params;
}
pub fn _toJSON(self: *URL, page: *Page) ![]const u8 {
return self.get_href(page);
} }
}; };
// uriComponentNullStr converts an optional std.Uri.Component to string value.
// The string value can be undecoded.
fn uriComponentNullStr(c: ?std.Uri.Component) []const u8 {
if (c == null) return "";
return uriComponentStr(c.?);
}
fn uriComponentStr(c: std.Uri.Component) []const u8 {
return switch (c) {
.raw => |v| v,
.percent_encoded => |v| v,
};
}
// https://url.spec.whatwg.org/#interface-urlsearchparams
pub const URLSearchParams = struct { pub const URLSearchParams = struct {
entries: kv.List = .{}, entries: kv.List = .{},
const URLSearchParamsOpts = union(enum) { pub const ConstructorOptions = union(enum) {
qs: []const u8, query_string: []const u8,
form_data: *const FormData, form_data: *const FormData,
js_obj: js.Object, object: js.Object,
}; };
pub fn constructor(opts_: ?URLSearchParamsOpts, page: *Page) !URLSearchParams {
const opts = opts_ orelse return .{ .entries = .{} };
return switch (opts) {
.qs => |qs| init(page.arena, qs),
.form_data => |fd| .{ .entries = try fd.entries.clone(page.arena) },
.js_obj => |js_obj| {
const arena = page.arena;
var it = js_obj.nameIterator();
var entries: kv.List = .{}; pub fn constructor(maybe_options: ?ConstructorOptions, page: *Page) !URLSearchParams {
const options = maybe_options orelse return .{};
const arena = page.arena;
return switch (options) {
.query_string => |string| .{ .entries = try parseQuery(arena, string) },
.form_data => |form_data| .{ .entries = try form_data.entries.clone(arena) },
.object => |object| {
var it = object.nameIterator();
var entries = kv.List{};
try entries.ensureTotalCapacity(arena, it.count); try entries.ensureTotalCapacity(arena, it.count);
while (try it.next()) |js_name| { while (try it.next()) |js_name| {
const name = try js_name.toString(arena); const name = try js_name.toString(arena);
const js_val = try js_obj.get(name); const js_value = try object.get(name);
entries.appendOwnedAssumeCapacity( const value = try js_value.toString(arena);
name,
try js_val.toString(arena), entries.appendOwnedAssumeCapacity(name, value);
);
} }
return .{ .entries = entries }; return .{ .entries = entries };
@@ -296,10 +383,9 @@ pub const URLSearchParams = struct {
}; };
} }
pub fn init(arena: Allocator, qs_: ?[]const u8) !URLSearchParams { /// Initializes URLSearchParams from a query string.
return .{ pub fn initFromString(arena: Allocator, query_string: []const u8) !URLSearchParams {
.entries = if (qs_) |qs| try parseQuery(arena, qs) else .{}, return .{ .entries = try parseQuery(arena, query_string) };
};
} }
pub fn get_size(self: *const URLSearchParams) u32 { pub fn get_size(self: *const URLSearchParams) u32 {

View File

@@ -31,6 +31,7 @@ const Mime = @import("../mime.zig").Mime;
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const Http = @import("../../http/Http.zig"); const Http = @import("../../http/Http.zig");
const js = @import("../js/js.zig");
// XHR interfaces // XHR interfaces
// https://xhr.spec.whatwg.org/#interface-xmlhttprequest // https://xhr.spec.whatwg.org/#interface-xmlhttprequest
@@ -128,21 +129,19 @@ pub const XMLHttpRequest = struct {
JSON, JSON,
}; };
const JSONValue = std.json.Value;
const Response = union(ResponseType) { const Response = union(ResponseType) {
Empty: void, Empty: void,
Text: []const u8, Text: []const u8,
ArrayBuffer: void, ArrayBuffer: void,
Blob: void, Blob: void,
Document: *parser.Document, Document: *parser.Document,
JSON: JSONValue, JSON: js.Value,
}; };
const ResponseObj = union(enum) { const ResponseObj = union(enum) {
Document: *parser.Document, Document: *parser.Document,
Failure: void, Failure: void,
JSON: JSONValue, JSON: js.Value,
fn deinit(self: ResponseObj) void { fn deinit(self: ResponseObj) void {
switch (self) { switch (self) {
@@ -605,7 +604,7 @@ pub const XMLHttpRequest = struct {
} }
// https://xhr.spec.whatwg.org/#the-response-attribute // https://xhr.spec.whatwg.org/#the-response-attribute
pub fn get_response(self: *XMLHttpRequest) !?Response { pub fn get_response(self: *XMLHttpRequest, page: *Page) !?Response {
if (self.response_type == .Empty or self.response_type == .Text) { if (self.response_type == .Empty or self.response_type == .Text) {
if (self.state == .loading or self.state == .done) { if (self.state == .loading or self.state == .done) {
return .{ .Text = try self.get_responseText() }; return .{ .Text = try self.get_responseText() };
@@ -652,7 +651,7 @@ pub const XMLHttpRequest = struct {
// TODO Let jsonObject be the result of running parse JSON from bytes // TODO Let jsonObject be the result of running parse JSON from bytes
// on thiss received bytes. If that threw an exception, then return // on thiss received bytes. If that threw an exception, then return
// null. // null.
self.setResponseObjJSON(); self.setResponseObjJSON(page);
} }
if (self.response_obj) |obj| { if (self.response_obj) |obj| {
@@ -678,7 +677,7 @@ pub const XMLHttpRequest = struct {
} }
var fbs = std.io.fixedBufferStream(self.response_bytes.items); var fbs = std.io.fixedBufferStream(self.response_bytes.items);
const doc = parser.documentHTMLParse(fbs.reader(), mime.charsetString()) catch { const doc = parser.documentHTMLParse(fbs.reader(), mime.charsetStringZ()) catch {
self.response_obj = .{ .Failure = {} }; self.response_obj = .{ .Failure = {} };
return; return;
}; };
@@ -691,22 +690,24 @@ pub const XMLHttpRequest = struct {
}; };
} }
// setResponseObjJSON parses the received bytes as a std.json.Value. // setResponseObjJSON parses the received bytes as a js.Value.
fn setResponseObjJSON(self: *XMLHttpRequest) void { fn setResponseObjJSON(self: *XMLHttpRequest, page: *Page) void {
// TODO should we use parseFromSliceLeaky if we expect the allocator is const value = js.Value.fromJson(
// already an arena? page.js,
const p = std.json.parseFromSliceLeaky(
JSONValue,
self.arena,
self.response_bytes.items, self.response_bytes.items,
.{},
) catch |e| { ) catch |e| {
log.warn(.http, "invalid json", .{ .err = e, .url = self.url, .source = "xhr" }); log.warn(.http, "invalid json", .{ .err = e, .url = self.url, .source = "xhr" });
self.response_obj = .{ .Failure = {} }; self.response_obj = .{ .Failure = {} };
return; return;
}; };
self.response_obj = .{ .JSON = p }; const pvalue = value.persist(page.js) catch |e| {
log.warn(.http, "persist v8 json value", .{ .err = e, .url = self.url, .source = "xhr" });
self.response_obj = .{ .Failure = {} };
return;
};
self.response_obj = .{ .JSON = pvalue };
} }
pub fn get_responseText(self: *XMLHttpRequest) ![]const u8 { pub fn get_responseText(self: *XMLHttpRequest) ![]const u8 {

View File

@@ -230,6 +230,11 @@ pub fn CDPT(comptime TypeProvider: type) type {
asUint(u88, "Performance") => return @import("domains/performance.zig").processMessage(command), asUint(u88, "Performance") => return @import("domains/performance.zig").processMessage(command),
else => {}, else => {},
}, },
13 => switch (@as(u104, @bitCast(domain[0..13].*))) {
asUint(u104, "Accessibility") => return @import("domains/accessibility.zig").processMessage(command),
else => {},
},
else => {}, else => {},
} }
@@ -468,6 +473,14 @@ pub fn BrowserContext(comptime CDP_T: type) type {
return if (raw_url.len == 0) null else raw_url; return if (raw_url.len == 0) null else raw_url;
} }
pub fn getTitle(self: *const Self) ?[]const u8 {
const page = self.session.currentPage() orelse return null;
return page.getTitle() catch |err| {
log.err(.cdp, "page title", .{ .err = err });
return null;
};
}
pub fn networkEnable(self: *Self) !void { pub fn networkEnable(self: *Self) !void {
try self.cdp.browser.notification.register(.http_request_fail, self, onHttpRequestFail); try self.cdp.browser.notification.register(.http_request_fail, self, onHttpRequestFail);
try self.cdp.browser.notification.register(.http_request_start, self, onHttpRequestStart); try self.cdp.browser.notification.register(.http_request_start, self, onHttpRequestStart);
@@ -538,7 +551,8 @@ pub fn BrowserContext(comptime CDP_T: type) type {
pub fn onPageNavigated(ctx: *anyopaque, msg: *const Notification.PageNavigated) !void { pub fn onPageNavigated(ctx: *anyopaque, msg: *const Notification.PageNavigated) !void {
const self: *Self = @ptrCast(@alignCast(ctx)); const self: *Self = @ptrCast(@alignCast(ctx));
return @import("domains/page.zig").pageNavigated(self, msg); defer self.resetNotificationArena();
return @import("domains/page.zig").pageNavigated(self.notification_arena, self, msg);
} }
pub fn onPageNetworkIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkIdle) !void { pub fn onPageNetworkIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkIdle) !void {
@@ -632,6 +646,17 @@ pub fn BrowserContext(comptime CDP_T: type) type {
}; };
} }
// debugger events
pub fn onRunMessageLoopOnPause(_: *anyopaque, _: u32) void {
// onRunMessageLoopOnPause is called when a breakpoint is hit.
// Until quit pause, we must continue to run a nested message loop
// to interact with the the debugger ony (ie. Chrome DevTools).
}
pub fn onQuitMessageLoopOnPause(_: *anyopaque) void {
// Quit breakpoint pause.
}
// This is hacky x 2. First, we create the JSON payload by gluing our // This is hacky x 2. First, we create the JSON payload by gluing our
// session_id onto it. Second, we're much more client/websocket aware than // session_id onto it. Second, we're much more client/websocket aware than
// we should be. // we should be.
@@ -702,7 +727,7 @@ const IsolatedWorld = struct {
// The isolate world must share at least some of the state with the related page, specifically the DocumentHTML // The isolate world must share at least some of the state with the related page, specifically the DocumentHTML
// (assuming grantUniveralAccess will be set to True!). // (assuming grantUniveralAccess will be set to True!).
// We just created the world and the page. The page's state lives in the session, but is update on navigation. // We just created the world and the page. The page's state lives in the session, but is update on navigation.
// This also means this pointer becomes invalid after removePage untill a new page is created. // This also means this pointer becomes invalid after removePage until a new page is created.
// Currently we have only 1 page/frame and thus also only 1 state in the isolate world. // Currently we have only 1 page/frame and thus also only 1 state in the isolate world.
pub fn createContext(self: *IsolatedWorld, page: *Page) !void { pub fn createContext(self: *IsolatedWorld, page: *Page) !void {
// if (self.executor.context != null) return error.Only1IsolatedContextSupported; // if (self.executor.context != null) return error.Only1IsolatedContextSupported;

View File

@@ -0,0 +1,38 @@
// 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 fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
enable,
disable,
}, cmd.input.action) orelse return error.UnknownMethod;
switch (action) {
.enable => return enable(cmd),
.disable => return disable(cmd),
}
}
fn enable(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
fn disable(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}

View File

@@ -38,16 +38,24 @@ const DEV_TOOLS_WINDOW_ID = 1923710101;
pub fn processMessage(cmd: anytype) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
getVersion, getVersion,
setDownloadBehavior, setPermission,
getWindowForTarget,
setWindowBounds, setWindowBounds,
resetPermissions,
grantPermissions,
getWindowForTarget,
setDownloadBehavior,
close,
}, cmd.input.action) orelse return error.UnknownMethod; }, cmd.input.action) orelse return error.UnknownMethod;
switch (action) { switch (action) {
.getVersion => return getVersion(cmd), .getVersion => return getVersion(cmd),
.setDownloadBehavior => return setDownloadBehavior(cmd), .setPermission => return setPermission(cmd),
.getWindowForTarget => return getWindowForTarget(cmd),
.setWindowBounds => return setWindowBounds(cmd), .setWindowBounds => return setWindowBounds(cmd),
.resetPermissions => return resetPermissions(cmd),
.grantPermissions => return grantPermissions(cmd),
.getWindowForTarget => return getWindowForTarget(cmd),
.setDownloadBehavior => return setDownloadBehavior(cmd),
.close => return cmd.sendResult(null, .{}),
} }
} }
@@ -89,6 +97,21 @@ fn setWindowBounds(cmd: anytype) !void {
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
// TODO: noop method
fn grantPermissions(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
// TODO: noop method
fn setPermission(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
// TODO: noop method
fn resetPermissions(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
const testing = @import("../testing.zig"); const testing = @import("../testing.zig");
test "cdp.browser: getVersion" { test "cdp.browser: getVersion" {
var ctx = testing.context(); var ctx = testing.context();

View File

@@ -24,6 +24,7 @@ const css = @import("../../browser/dom/css.zig");
const parser = @import("../../browser/netsurf.zig"); const parser = @import("../../browser/netsurf.zig");
const dom_node = @import("../../browser/dom/node.zig"); const dom_node = @import("../../browser/dom/node.zig");
const Element = @import("../../browser/dom/element.zig").Element; const Element = @import("../../browser/dom/element.zig").Element;
const dump = @import("../../browser/dump.zig");
pub fn processMessage(cmd: anytype) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
@@ -41,6 +42,8 @@ pub fn processMessage(cmd: anytype) !void {
getBoxModel, getBoxModel,
requestChildNodes, requestChildNodes,
getFrameOwner, getFrameOwner,
getOuterHTML,
requestNode,
}, cmd.input.action) orelse return error.UnknownMethod; }, cmd.input.action) orelse return error.UnknownMethod;
switch (action) { switch (action) {
@@ -58,6 +61,8 @@ pub fn processMessage(cmd: anytype) !void {
.getBoxModel => return getBoxModel(cmd), .getBoxModel => return getBoxModel(cmd),
.requestChildNodes => return requestChildNodes(cmd), .requestChildNodes => return requestChildNodes(cmd),
.getFrameOwner => return getFrameOwner(cmd), .getFrameOwner => return getFrameOwner(cmd),
.getOuterHTML => return getOuterHTML(cmd),
.requestNode => return requestNode(cmd),
} }
} }
@@ -494,6 +499,38 @@ fn getFrameOwner(cmd: anytype) !void {
return cmd.sendResult(.{ .nodeId = node.id, .backendNodeId = node.id }, .{}); return cmd.sendResult(.{ .nodeId = node.id, .backendNodeId = node.id }, .{});
} }
fn getOuterHTML(cmd: anytype) !void {
const params = (try cmd.params(struct {
nodeId: ?Node.Id = null,
backendNodeId: ?Node.Id = null,
objectId: ?[]const u8 = null,
includeShadowDOM: bool = false,
})) orelse return error.InvalidParams;
if (params.includeShadowDOM) {
log.warn(.cdp, "not implemented", .{ .feature = "DOM.getOuterHTML: Not implemented includeShadowDOM parameter" });
}
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId);
var aw = std.Io.Writer.Allocating.init(cmd.arena);
try dump.writeNode(node._node, .{}, &aw.writer);
return cmd.sendResult(.{ .outerHTML = aw.written() }, .{});
}
fn requestNode(cmd: anytype) !void {
const params = (try cmd.params(struct {
objectId: []const u8,
})) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const node = try getNode(cmd.arena, bc, null, null, params.objectId);
return cmd.sendResult(.{ .nodeId = node.id }, .{});
}
const testing = @import("../testing.zig"); const testing = @import("../testing.zig");
test "cdp.dom: getSearchResults unknown search id" { test "cdp.dom: getSearchResults unknown search id" {
@@ -663,11 +700,11 @@ test "cdp.dom: getBoxModel" {
.params = .{ .nodeId = 6 }, .params = .{ .nodeId = 6 },
}); });
try ctx.expectSentResult(.{ .model = BoxModel{ try ctx.expectSentResult(.{ .model = BoxModel{
.content = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 }, .content = Quad{ 0.0, 0.0, 5.0, 0.0, 5.0, 5.0, 0.0, 5.0 },
.padding = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 }, .padding = Quad{ 0.0, 0.0, 5.0, 0.0, 5.0, 5.0, 0.0, 5.0 },
.border = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 }, .border = Quad{ 0.0, 0.0, 5.0, 0.0, 5.0, 5.0, 0.0, 5.0 },
.margin = Quad{ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 }, .margin = Quad{ 0.0, 0.0, 5.0, 0.0, 5.0, 5.0, 0.0, 5.0 },
.width = 1, .width = 5,
.height = 1, .height = 5,
} }, .{ .id = 5 }); } }, .{ .id = 5 });
} }

View File

@@ -23,11 +23,13 @@ pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
dispatchKeyEvent, dispatchKeyEvent,
dispatchMouseEvent, dispatchMouseEvent,
insertText,
}, cmd.input.action) orelse return error.UnknownMethod; }, cmd.input.action) orelse return error.UnknownMethod;
switch (action) { switch (action) {
.dispatchKeyEvent => return dispatchKeyEvent(cmd), .dispatchKeyEvent => return dispatchKeyEvent(cmd),
.dispatchMouseEvent => return dispatchMouseEvent(cmd), .dispatchMouseEvent => return dispatchMouseEvent(cmd),
.insertText => return insertText(cmd),
} }
} }
@@ -115,6 +117,20 @@ fn dispatchMouseEvent(cmd: anytype) !void {
// result already sent // result already sent
} }
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-insertText
fn insertText(cmd: anytype) !void {
const params = (try cmd.params(struct {
text: []const u8, // The text to insert
})) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return;
const page = bc.session.currentPage() orelse return;
try page.insertText(params.text);
try cmd.sendResult(null, .{});
}
fn clickNavigate(cmd: anytype, uri: std.Uri) !void { fn clickNavigate(cmd: anytype, uri: std.Uri) !void {
const bc = cmd.browser_context.?; const bc = cmd.browser_context.?;

View File

@@ -22,6 +22,7 @@ const Allocator = std.mem.Allocator;
const CdpStorage = @import("storage.zig"); const CdpStorage = @import("storage.zig");
const Transfer = @import("../../http/Client.zig").Transfer; const Transfer = @import("../../http/Client.zig").Transfer;
const Notification = @import("../../notification.zig").Notification; const Notification = @import("../../notification.zig").Notification;
const Mime = @import("../../browser/mime.zig").Mime;
pub fn processMessage(cmd: anytype) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
@@ -242,14 +243,18 @@ pub fn httpRequestStart(arena: Allocator, bc: anytype, msg: *const Notification.
} }
const transfer = msg.transfer; const transfer = msg.transfer;
const loader_id = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id});
// We're missing a bunch of fields, but, for now, this seems like enough // We're missing a bunch of fields, but, for now, this seems like enough
try bc.cdp.sendEvent("Network.requestWillBeSent", .{ try bc.cdp.sendEvent("Network.requestWillBeSent", .{
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}), .requestId = loader_id,
.frameId = target_id, .frameId = target_id,
.loaderId = bc.loader_id, .loaderId = loader_id,
.documentUrl = DocumentUrlWriter.init(&page.url.uri), .type = msg.transfer.req.resource_type.string(),
.documentURL = DocumentUrlWriter.init(&page.url.uri),
.request = TransferAsRequestWriter.init(transfer), .request = TransferAsRequestWriter.init(transfer),
.initiator = .{ .type = "other" }, .initiator = .{ .type = "other" },
.redirectHasExtraInfo = false, // TODO change after adding Network.requestWillBeSentExtraInfo
.hasUserGesture = false,
}, .{ .session_id = session_id }); }, .{ .session_id = session_id });
} }
@@ -259,12 +264,16 @@ pub fn httpResponseHeaderDone(arena: Allocator, bc: anytype, msg: *const Notific
const session_id = bc.session_id orelse return; const session_id = bc.session_id orelse return;
const target_id = bc.target_id orelse unreachable; const target_id = bc.target_id orelse unreachable;
const transfer = msg.transfer;
const loader_id = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id});
// We're missing a bunch of fields, but, for now, this seems like enough // We're missing a bunch of fields, but, for now, this seems like enough
try bc.cdp.sendEvent("Network.responseReceived", .{ try bc.cdp.sendEvent("Network.responseReceived", .{
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{msg.transfer.id}), .requestId = loader_id,
.loaderId = bc.loader_id,
.frameId = target_id, .frameId = target_id,
.loaderId = loader_id,
.response = TransferAsResponseWriter.init(arena, msg.transfer), .response = TransferAsResponseWriter.init(arena, msg.transfer),
.hasExtraInfo = false, // TODO change after adding Network.responseReceivedExtraInfo
}, .{ .session_id = session_id }); }, .{ .session_id = session_id });
} }
@@ -392,6 +401,20 @@ const TransferAsResponseWriter = struct {
try jws.write(@as(std.http.Status, @enumFromInt(status)).phrase() orelse "Unknown"); try jws.write(@as(std.http.Status, @enumFromInt(status)).phrase() orelse "Unknown");
} }
{
const mime: Mime = blk: {
if (transfer.response_header.?.contentType()) |ct| {
break :blk try Mime.parse(ct);
}
break :blk .unknown;
};
try jws.objectField("mimeType");
try jws.write(mime.contentTypeString());
try jws.objectField("charset");
try jws.write(mime.charsetString());
}
{ {
// chromedp doesn't like having duplicate header names. It's pretty // chromedp doesn't like having duplicate header names. It's pretty
// common to get these from a server (e.g. for Cache-Control), but // common to get these from a server (e.g. for Cache-Control), but

View File

@@ -18,7 +18,9 @@
const std = @import("std"); const std = @import("std");
const Page = @import("../../browser/page.zig").Page; const Page = @import("../../browser/page.zig").Page;
const timestampF = @import("../../datetime.zig").timestamp;
const Notification = @import("../../notification.zig").Notification; const Notification = @import("../../notification.zig").Notification;
const log = @import("../../log.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
@@ -31,6 +33,7 @@ pub fn processMessage(cmd: anytype) !void {
createIsolatedWorld, createIsolatedWorld,
navigate, navigate,
stopLoading, stopLoading,
close,
}, cmd.input.action) orelse return error.UnknownMethod; }, cmd.input.action) orelse return error.UnknownMethod;
switch (action) { switch (action) {
@@ -41,6 +44,7 @@ pub fn processMessage(cmd: anytype) !void {
.createIsolatedWorld => return createIsolatedWorld(cmd), .createIsolatedWorld => return createIsolatedWorld(cmd),
.navigate => return navigate(cmd), .navigate => return navigate(cmd),
.stopLoading => return cmd.sendResult(null, .{}), .stopLoading => return cmd.sendResult(null, .{}),
.close => return close(cmd),
} }
} }
@@ -82,11 +86,33 @@ fn setLifecycleEventsEnabled(cmd: anytype) !void {
})) orelse return error.InvalidParams; })) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
if (params.enabled) {
try bc.lifecycleEventsEnable(); if (params.enabled == false) {
} else {
bc.lifecycleEventsDisable(); bc.lifecycleEventsDisable();
return cmd.sendResult(null, .{});
} }
// Enable lifecycle events.
try bc.lifecycleEventsEnable();
// When we enable lifecycle events, we must dispatch events for all
// attached targets.
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
if (page.load_state == .complete) {
try sendPageLifecycle(bc, "DOMContentLoaded", timestampF());
try sendPageLifecycle(bc, "load", timestampF());
const http_active = page.http_client.active;
const total_network_activity = http_active + page.http_client.intercepted;
if (page.notified_network_almost_idle.check(total_network_activity <= 2)) {
try sendPageLifecycle(bc, "networkAlmostIdle", timestampF());
}
if (page.notified_network_idle.check(total_network_activity == 0)) {
try sendPageLifecycle(bc, "networkIdle", timestampF());
}
}
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
@@ -106,14 +132,51 @@ fn addScriptToEvaluateOnNewDocument(cmd: anytype) !void {
}, .{}); }, .{});
} }
fn close(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const target_id = bc.target_id orelse return error.TargetNotLoaded;
// can't be null if we have a target_id
std.debug.assert(bc.session.page != null);
try cmd.sendResult(.{}, .{});
// Following code is similar to target.closeTarget
//
// could be null, created but never attached
if (bc.session_id) |session_id| {
// Inspector.detached event
try cmd.sendEvent("Inspector.detached", .{
.reason = "Render process gone.",
}, .{ .session_id = session_id });
// detachedFromTarget event
try cmd.sendEvent("Target.detachedFromTarget", .{
.targetId = target_id,
.sessionId = session_id,
.reason = "Render process gone.",
}, .{});
bc.session_id = null;
}
bc.session.removePage();
for (bc.isolated_worlds.items) |*world| {
world.deinit();
}
bc.isolated_worlds.clearRetainingCapacity();
bc.target_id = null;
}
fn createIsolatedWorld(cmd: anytype) !void { fn createIsolatedWorld(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
frameId: []const u8, frameId: []const u8,
worldName: []const u8, worldName: []const u8,
grantUniveralAccess: bool, grantUniveralAccess: bool = false,
})) orelse return error.InvalidParams; })) orelse return error.InvalidParams;
if (!params.grantUniveralAccess) { if (!params.grantUniveralAccess) {
std.debug.print("grantUniveralAccess == false is not yet implemented", .{}); log.warn(.cdp, "not implemented", .{ .feature = "grantUniveralAccess == false is not yet implemented" });
// When grantUniveralAccess == false and the client attempts to resolve // When grantUniveralAccess == false and the client attempts to resolve
// or otherwise access a DOM or other JS Object from another context that should fail. // or otherwise access a DOM or other JS Object from another context that should fail.
} }
@@ -152,7 +215,6 @@ fn navigate(cmd: anytype) !void {
} }
var page = bc.session.currentPage() orelse return error.PageNotLoaded; var page = bc.session.currentPage() orelse return error.PageNotLoaded;
bc.loader_id = bc.cdp.loader_id_gen.next();
try page.navigate(params.url, .{ try page.navigate(params.url, .{
.reason = .address_bar, .reason = .address_bar,
@@ -165,8 +227,7 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
// things, but no session. // things, but no session.
const session_id = bc.session_id orelse return; const session_id = bc.session_id orelse return;
bc.loader_id = bc.cdp.loader_id_gen.next(); const loader_id = try std.fmt.allocPrint(arena, "REQ-{d}", .{event.req_id});
const loader_id = bc.loader_id;
const target_id = bc.target_id orelse unreachable; const target_id = bc.target_id orelse unreachable;
bc.reset(); bc.reset();
@@ -174,7 +235,7 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
var cdp = bc.cdp; var cdp = bc.cdp;
const reason_: ?[]const u8 = switch (event.opts.reason) { const reason_: ?[]const u8 = switch (event.opts.reason) {
.anchor => "anchorClick", .anchor => "anchorClick",
.script, .history => "scriptInitiated", .script, .history, .navigation => "scriptInitiated",
.form => switch (event.opts.method) { .form => switch (event.opts.method) {
.GET => "formSubmissionGet", .GET => "formSubmissionGet",
.POST => "formSubmissionPost", .POST => "formSubmissionPost",
@@ -210,6 +271,30 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
try cdp.sendEvent("Page.frameStartedLoading", .{ try cdp.sendEvent("Page.frameStartedLoading", .{
.frameId = target_id, .frameId = target_id,
}, .{ .session_id = session_id }); }, .{ .session_id = session_id });
}
pub fn pageRemove(bc: anytype) !void {
// The main page is going to be removed, we need to remove contexts from other worlds first.
for (bc.isolated_worlds.items) |*isolated_world| {
try isolated_world.removeContext();
}
}
pub fn pageCreated(bc: anytype, page: *Page) !void {
for (bc.isolated_worlds.items) |*isolated_world| {
try isolated_world.createContextAndLoadPolyfills(bc.arena, page);
}
}
pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.PageNavigated) !void {
// detachTarget could be called, in which case, we still have a page doing
// things, but no session.
const session_id = bc.session_id orelse return;
const loader_id = try std.fmt.allocPrint(arena, "REQ-{d}", .{event.req_id});
const target_id = bc.target_id orelse unreachable;
const timestamp = event.timestamp;
var cdp = bc.cdp;
// Drivers are sensitive to the order of events. Some more than others. // Drivers are sensitive to the order of events. Some more than others.
// The result for the Page.navigate seems like it _must_ come after // The result for the Page.navigate seems like it _must_ come after
@@ -236,6 +321,17 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
}, .{ .session_id = session_id }); }, .{ .session_id = session_id });
} }
const reason_: ?[]const u8 = switch (event.opts.reason) {
.anchor => "anchorClick",
.script, .history, .navigation => "scriptInitiated",
.form => switch (event.opts.method) {
.GET => "formSubmissionGet",
.POST => "formSubmissionPost",
else => unreachable,
},
.address_bar => null,
};
if (reason_ != null) { if (reason_ != null) {
try cdp.sendEvent("Page.frameClearedScheduledNavigation", .{ try cdp.sendEvent("Page.frameClearedScheduledNavigation", .{
.frameId = target_id, .frameId = target_id,
@@ -269,37 +365,14 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
false, false,
); );
} }
}
pub fn pageRemove(bc: anytype) !void {
// The main page is going to be removed, we need to remove contexts from other worlds first.
for (bc.isolated_worlds.items) |*isolated_world| {
try isolated_world.removeContext();
}
}
pub fn pageCreated(bc: anytype, page: *Page) !void {
for (bc.isolated_worlds.items) |*isolated_world| {
try isolated_world.createContextAndLoadPolyfills(bc.arena, page);
}
}
pub fn pageNavigated(bc: anytype, event: *const Notification.PageNavigated) !void {
// detachTarget could be called, in which case, we still have a page doing
// things, but no session.
const session_id = bc.session_id orelse return;
const loader_id = bc.loader_id;
const target_id = bc.target_id orelse unreachable;
const timestamp = event.timestamp;
var cdp = bc.cdp;
// frameNavigated event // frameNavigated event
try cdp.sendEvent("Page.frameNavigated", .{ try cdp.sendEvent("Page.frameNavigated", .{
.type = "Navigation", .type = "Navigation",
.frame = Frame{ .frame = Frame{
.id = target_id, .id = target_id,
.url = event.url, .url = event.url,
.loaderId = bc.loader_id, .loaderId = loader_id,
.securityOrigin = bc.security_origin, .securityOrigin = bc.security_origin,
.secureContextType = bc.secure_context_type, .secureContextType = bc.secure_context_type,
}, },

View File

@@ -21,9 +21,48 @@ const std = @import("std");
pub fn processMessage(cmd: anytype) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
enable, enable,
setIgnoreCertificateErrors,
}, cmd.input.action) orelse return error.UnknownMethod; }, cmd.input.action) orelse return error.UnknownMethod;
switch (action) { switch (action) {
.enable => return cmd.sendResult(null, .{}), .enable => return cmd.sendResult(null, .{}),
.setIgnoreCertificateErrors => return setIgnoreCertificateErrors(cmd),
} }
} }
fn setIgnoreCertificateErrors(cmd: anytype) !void {
const params = (try cmd.params(struct {
ignore: bool,
})) orelse return error.InvalidParams;
if (params.ignore) {
try cmd.cdp.browser.http_client.disableTlsVerify();
} else {
try cmd.cdp.browser.http_client.enableTlsVerify();
}
return cmd.sendResult(null, .{});
}
const testing = @import("../testing.zig");
test "cdp.Security: setIgnoreCertificateErrors" {
var ctx = testing.context();
defer ctx.deinit();
_ = try ctx.loadBrowserContext(.{ .id = "BID-9" });
try ctx.processMessage(.{
.id = 8,
.method = "Security.setIgnoreCertificateErrors",
.params = .{ .ignore = true },
});
try ctx.expectSentResult(null, .{ .id = 8 });
try ctx.processMessage(.{
.id = 9,
.method = "Security.setIgnoreCertificateErrors",
.params = .{ .ignore = false },
});
try ctx.expectSentResult(null, .{ .id = 9 });
}

View File

@@ -24,6 +24,7 @@ const LOADER_ID = "LOADERID42AA389647D702B4D805F49A";
pub fn processMessage(cmd: anytype) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
getTargets,
attachToTarget, attachToTarget,
closeTarget, closeTarget,
createBrowserContext, createBrowserContext,
@@ -38,6 +39,7 @@ pub fn processMessage(cmd: anytype) !void {
}, cmd.input.action) orelse return error.UnknownMethod; }, cmd.input.action) orelse return error.UnknownMethod;
switch (action) { switch (action) {
.getTargets => return getTargets(cmd),
.attachToTarget => return attachToTarget(cmd), .attachToTarget => return attachToTarget(cmd),
.closeTarget => return closeTarget(cmd), .closeTarget => return closeTarget(cmd),
.createBrowserContext => return createBrowserContext(cmd), .createBrowserContext => return createBrowserContext(cmd),
@@ -52,6 +54,31 @@ pub fn processMessage(cmd: anytype) !void {
} }
} }
fn getTargets(cmd: anytype) !void {
// Some clients like Stagehand expects to have an existing context.
const bc = cmd.browser_context orelse cmd.createBrowserContext() catch |err| switch (err) {
error.AlreadyExists => unreachable,
else => return err,
};
const target_id = bc.target_id orelse {
return cmd.sendResult(.{
.targetInfos = [_]TargetInfo{},
}, .{ .include_session_id = false });
};
return cmd.sendResult(.{
.targetInfos = [_]TargetInfo{.{
.targetId = target_id,
.type = "page",
.title = bc.getTitle() orelse "about:blank",
.url = bc.getURL() orelse "about:blank",
.attached = true,
.canAccessOpener = false,
}},
}, .{ .include_session_id = false });
}
fn getBrowserContexts(cmd: anytype) !void { fn getBrowserContexts(cmd: anytype) !void {
var browser_context_ids: []const []const u8 = undefined; var browser_context_ids: []const []const u8 = undefined;
if (cmd.browser_context) |bc| { if (cmd.browser_context) |bc| {
@@ -109,7 +136,7 @@ fn disposeBrowserContext(cmd: anytype) !void {
fn createTarget(cmd: anytype) !void { fn createTarget(cmd: anytype) !void {
const params = (try cmd.params(struct { const params = (try cmd.params(struct {
// url: []const u8, url: []const u8 = "about:blank",
// width: ?u64 = null, // width: ?u64 = null,
// height: ?u64 = null, // height: ?u64 = null,
browserContextId: ?[]const u8 = null, browserContextId: ?[]const u8 = null,
@@ -178,6 +205,12 @@ fn createTarget(cmd: anytype) !void {
try doAttachtoTarget(cmd, target_id); try doAttachtoTarget(cmd, target_id);
} }
if (!std.mem.eql(u8, "about:blank", params.url)) {
try page.navigate(params.url, .{
.reason = .address_bar,
});
}
try cmd.sendResult(.{ try cmd.sendResult(.{
.targetId = target_id, .targetId = target_id,
}, .{}); }, .{});
@@ -195,12 +228,10 @@ fn attachToTarget(cmd: anytype) !void {
return error.UnknownTargetId; return error.UnknownTargetId;
} }
if (bc.session_id != null) { if (bc.session_id == null) {
return error.SessionAlreadyLoaded; try doAttachtoTarget(cmd, target_id);
} }
try doAttachtoTarget(cmd, target_id);
return cmd.sendResult( return cmd.sendResult(
.{ .sessionId = bc.session_id }, .{ .sessionId = bc.session_id },
.{ .include_session_id = false }, .{ .include_session_id = false },
@@ -265,8 +296,8 @@ fn getTargetInfo(cmd: anytype) !void {
.targetInfo = TargetInfo{ .targetInfo = TargetInfo{
.targetId = target_id, .targetId = target_id,
.type = "page", .type = "page",
.title = "", .title = bc.getTitle() orelse "about:blank",
.url = "", .url = bc.getURL() orelse "about:blank",
.attached = true, .attached = true,
.canAccessOpener = false, .canAccessOpener = false,
}, },
@@ -277,8 +308,8 @@ fn getTargetInfo(cmd: anytype) !void {
.targetInfo = TargetInfo{ .targetInfo = TargetInfo{
.targetId = "TID-STARTUP-B", .targetId = "TID-STARTUP-B",
.type = "browser", .type = "browser",
.title = "", .title = "about:blank",
.url = "", .url = "about:blank",
.attached = true, .attached = true,
.canAccessOpener = false, .canAccessOpener = false,
}, },
@@ -517,7 +548,7 @@ test "cdp.target: createTarget" {
{ {
var ctx = testing.context(); var ctx = testing.context();
defer ctx.deinit(); defer ctx.deinit();
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .url = "about/blank" } }); try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .url = "about:blank" } });
// should create a browser context // should create a browser context
const bc = ctx.cdp().browser_context.?; const bc = ctx.cdp().browser_context.?;
@@ -529,7 +560,7 @@ test "cdp.target: createTarget" {
defer ctx.deinit(); defer ctx.deinit();
// active auto attach to get the Target.attachedToTarget event. // active auto attach to get the Target.attachedToTarget event.
try ctx.processMessage(.{ .id = 9, .method = "Target.setAutoAttach", .params = .{ .autoAttach = true, .waitForDebuggerOnStart = false } }); try ctx.processMessage(.{ .id = 9, .method = "Target.setAutoAttach", .params = .{ .autoAttach = true, .waitForDebuggerOnStart = false } });
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .url = "about/blank" } }); try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .url = "about:blank" } });
// should create a browser context // should create a browser context
const bc = ctx.cdp().browser_context.?; const bc = ctx.cdp().browser_context.?;
@@ -624,8 +655,8 @@ test "cdp.target: getTargetInfo" {
try ctx.expectSentResult(.{ try ctx.expectSentResult(.{
.targetInfo = .{ .targetInfo = .{
.type = "browser", .type = "browser",
.title = "", .title = "about:blank",
.url = "", .url = "about:blank",
.attached = true, .attached = true,
.canAccessOpener = false, .canAccessOpener = false,
}, },
@@ -658,7 +689,7 @@ test "cdp.target: getTargetInfo" {
.targetId = "TID-A", .targetId = "TID-A",
.type = "page", .type = "page",
.title = "", .title = "",
.url = "", .url = "about:blank",
.attached = true, .attached = true,
.canAccessOpener = false, .canAccessOpener = false,
}, },

View File

@@ -93,6 +93,11 @@ notification: ?*Notification = null,
// restoring, this originally-configured value is what it goes to. // restoring, this originally-configured value is what it goes to.
http_proxy: ?[:0]const u8 = null, http_proxy: ?[:0]const u8 = null,
// track if the client use a proxy for connections.
// We can't use http_proxy because we want also to track proxy configured via
// CDP.
use_proxy: bool,
// The complete user-agent header line // The complete user-agent header line
user_agent: [:0]const u8, user_agent: [:0]const u8,
@@ -126,6 +131,7 @@ pub fn init(allocator: Allocator, ca_blob: ?c.curl_blob, opts: Http.Opts) !*Clie
.handles = handles, .handles = handles,
.allocator = allocator, .allocator = allocator,
.http_proxy = opts.http_proxy, .http_proxy = opts.http_proxy,
.use_proxy = opts.http_proxy != null,
.user_agent = opts.user_agent, .user_agent = opts.user_agent,
.transfer_pool = transfer_pool, .transfer_pool = transfer_pool,
}; };
@@ -255,6 +261,16 @@ pub fn fulfillTransfer(self: *Client, transfer: *Transfer, status: u16, headers:
return transfer.fulfill(status, headers, body); return transfer.fulfill(status, headers, body);
} }
pub fn nextReqId(self: *Client) usize {
return self.next_request_id + 1;
}
pub fn incrReqId(self: *Client) usize {
const id = self.next_request_id + 1;
self.next_request_id = id;
return id;
}
fn makeTransfer(self: *Client, req: Request) !*Transfer { fn makeTransfer(self: *Client, req: Request) !*Transfer {
errdefer req.headers.deinit(); errdefer req.headers.deinit();
@@ -267,8 +283,7 @@ fn makeTransfer(self: *Client, req: Request) !*Transfer {
const transfer = try self.transfer_pool.create(); const transfer = try self.transfer_pool.create();
errdefer self.transfer_pool.destroy(transfer); errdefer self.transfer_pool.destroy(transfer);
const id = self.next_request_id + 1; const id = self.incrReqId();
self.next_request_id = id;
transfer.* = .{ transfer.* = .{
.arena = ArenaAllocator.init(self.allocator), .arena = ArenaAllocator.init(self.allocator),
.id = id, .id = id,
@@ -315,6 +330,7 @@ pub fn changeProxy(self: *Client, proxy: [:0]const u8) !void {
for (self.handles.handles) |*h| { for (self.handles.handles) |*h| {
try errorCheck(c.curl_easy_setopt(h.conn.easy, c.CURLOPT_PROXY, proxy.ptr)); try errorCheck(c.curl_easy_setopt(h.conn.easy, c.CURLOPT_PROXY, proxy.ptr));
} }
self.use_proxy = true;
} }
// Same restriction as changeProxy. Should be ok since this is only called on // Same restriction as changeProxy. Should be ok since this is only called on
@@ -326,6 +342,37 @@ pub fn restoreOriginalProxy(self: *Client) !void {
for (self.handles.handles) |*h| { for (self.handles.handles) |*h| {
try errorCheck(c.curl_easy_setopt(h.conn.easy, c.CURLOPT_PROXY, proxy)); try errorCheck(c.curl_easy_setopt(h.conn.easy, c.CURLOPT_PROXY, proxy));
} }
self.use_proxy = proxy != null;
}
// Enable TLS verification on all connections.
pub fn enableTlsVerify(self: *const Client) !void {
for (self.handles.handles) |*h| {
const easy = h.conn.easy;
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYHOST, @as(c_long, 2)));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYPEER, @as(c_long, 1)));
if (self.use_proxy) {
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYHOST, @as(c_long, 2)));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYPEER, @as(c_long, 1)));
}
}
}
// Disable TLS verification on all connections.
pub fn disableTlsVerify(self: *const Client) !void {
for (self.handles.handles) |*h| {
const easy = h.conn.easy;
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYHOST, @as(c_long, 0)));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYPEER, @as(c_long, 0)));
if (self.use_proxy) {
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYHOST, @as(c_long, 0)));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYPEER, @as(c_long, 0)));
}
}
} }
fn makeRequest(self: *Client, handle: *Handle, transfer: *Transfer) !void { fn makeRequest(self: *Client, handle: *Handle, transfer: *Transfer) !void {
@@ -641,6 +688,19 @@ pub const Request = struct {
xhr, xhr,
script, script,
fetch, fetch,
// Allowed Values: Document, Stylesheet, Image, Media, Font, Script,
// TextTrack, XHR, Fetch, Prefetch, EventSource, WebSocket, Manifest,
// SignedExchange, Ping, CSPViolationReport, Preflight, FedCM, Other
// https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-ResourceType
pub fn string(self: ResourceType) []const u8 {
return switch (self) {
.document => "Document",
.xhr => "XHR",
.script => "Script",
.fetch => "Fetch",
};
}
}; };
}; };
@@ -808,7 +868,7 @@ pub const Transfer = struct {
self.deinit(); self.deinit();
} }
// abortAuthChallenge is called when an auth chanllenge interception is // abortAuthChallenge is called when an auth challenge interception is
// abort. We don't call self.client.endTransfer here b/c it has been done // abort. We don't call self.client.endTransfer here b/c it has been done
// before interception process. // before interception process.
pub fn abortAuthChallenge(self: *Transfer) void { pub fn abortAuthChallenge(self: *Transfer) void {

View File

@@ -40,6 +40,7 @@ pub const Scope = enum {
fetch, fetch,
polyfill, polyfill,
interceptor, interceptor,
input,
}; };
const Opts = struct { const Opts = struct {

View File

@@ -23,67 +23,42 @@ const Allocator = std.mem.Allocator;
const log = @import("log.zig"); const log = @import("log.zig");
const App = @import("app.zig").App; const App = @import("app.zig").App;
const Server = @import("server.zig").Server; const Server = @import("server.zig").Server;
const SigHandler = @import("sighandler.zig").SigHandler;
const Browser = @import("browser/browser.zig").Browser; const Browser = @import("browser/browser.zig").Browser;
const DumpStripMode = @import("browser/dump.zig").Opts.StripMode; const DumpStripMode = @import("browser/dump.zig").Opts.StripMode;
const build_config = @import("build_config"); const build_config = @import("build_config");
var _app: ?*App = null;
var _server: ?Server = null;
pub fn main() !void { pub fn main() !void {
// allocator // allocator
// - in Debug mode we use the General Purpose Allocator to detect memory leaks // - in Debug mode we use the General Purpose Allocator to detect memory leaks
// - in Release mode we use the c allocator // - in Release mode we use the c allocator
var gpa: std.heap.DebugAllocator(.{}) = .init; var gpa_instance: std.heap.DebugAllocator(.{}) = .init;
const alloc = if (builtin.mode == .Debug) gpa.allocator() else std.heap.c_allocator; const gpa = if (builtin.mode == .Debug) gpa_instance.allocator() else std.heap.c_allocator;
defer if (builtin.mode == .Debug) { defer if (builtin.mode == .Debug) {
if (gpa.detectLeaks()) std.posix.exit(1); if (gpa_instance.detectLeaks()) std.posix.exit(1);
}; };
run(alloc) catch |err| { var arena_instance = std.heap.ArenaAllocator.init(gpa);
const arena = arena_instance.allocator();
defer arena_instance.deinit();
var sighandler = SigHandler{ .arena = arena };
try sighandler.install();
run(gpa, arena, &sighandler) catch |err| {
// If explicit filters were set, they won't be valid anymore because // If explicit filters were set, they won't be valid anymore because
// the args_arena is gone. We need to set it to something that's not // the arena is gone. We need to set it to something that's not
// invalid. (We should just move the args_arena up to main) // invalid. (We should just move the arena up to main)
log.opts.filter_scopes = &.{}; log.opts.filter_scopes = &.{};
log.fatal(.app, "exit", .{ .err = err }); log.fatal(.app, "exit", .{ .err = err });
std.posix.exit(1); std.posix.exit(1);
}; };
} }
// Handle app shutdown gracefuly on signals. fn run(gpa: Allocator, arena: Allocator, sighandler: *SigHandler) !void {
fn shutdown() void { const args = try parseArgs(arena);
const sigaction: std.posix.Sigaction = .{
.handler = .{
.handler = struct {
pub fn handler(_: c_int) callconv(.c) void {
// Shutdown service gracefuly.
if (_server) |server| {
server.deinit();
}
if (_app) |app| {
app.deinit();
}
std.posix.exit(0);
}
}.handler,
},
.mask = std.posix.empty_sigset,
.flags = 0,
};
// Exit the program on SIGINT signal. When running the browser in a Docker
// container, sending a CTRL-C (SIGINT) signal is catched but doesn't exit
// the program. Here we force exiting on SIGINT.
std.posix.sigaction(std.posix.SIG.INT, &sigaction, null);
std.posix.sigaction(std.posix.SIG.TERM, &sigaction, null);
std.posix.sigaction(std.posix.SIG.QUIT, &sigaction, null);
}
fn run(alloc: Allocator) !void {
var args_arena = std.heap.ArenaAllocator.init(alloc);
defer args_arena.deinit();
const args = try parseArgs(args_arena.allocator());
switch (args.mode) { switch (args.mode) {
.help => { .help => {
@@ -110,13 +85,13 @@ fn run(alloc: Allocator) !void {
const user_agent = blk: { const user_agent = blk: {
const USER_AGENT = "User-Agent: Lightpanda/1.0"; const USER_AGENT = "User-Agent: Lightpanda/1.0";
if (args.userAgentSuffix()) |suffix| { if (args.userAgentSuffix()) |suffix| {
break :blk try std.fmt.allocPrintSentinel(args_arena.allocator(), "{s} {s}", .{ USER_AGENT, suffix }, 0); break :blk try std.fmt.allocPrintSentinel(arena, "{s} {s}", .{ USER_AGENT, suffix }, 0);
} }
break :blk USER_AGENT; break :blk USER_AGENT;
}; };
// _app is global to handle graceful shutdown. // _app is global to handle graceful shutdown.
_app = try App.init(alloc, .{ var app = try App.init(gpa, .{
.run_mode = args.mode, .run_mode = args.mode,
.http_proxy = args.httpProxy(), .http_proxy = args.httpProxy(),
.proxy_bearer_token = args.proxyBearerToken(), .proxy_bearer_token = args.proxyBearerToken(),
@@ -127,24 +102,23 @@ fn run(alloc: Allocator) !void {
.http_max_concurrent = args.httpMaxConcurrent(), .http_max_concurrent = args.httpMaxConcurrent(),
.user_agent = user_agent, .user_agent = user_agent,
}); });
const app = _app.?;
defer app.deinit(); defer app.deinit();
app.telemetry.record(.{ .run = {} }); app.telemetry.record(.{ .run = {} });
switch (args.mode) { switch (args.mode) {
.serve => |opts| { .serve => |opts| {
log.debug(.app, "startup", .{ .mode = "serve" }); log.debug(.app, "startup", .{ .mode = "serve" });
const address = std.net.Address.parseIp4(opts.host, opts.port) catch |err| { const address = std.net.Address.parseIp(opts.host, opts.port) catch |err| {
log.fatal(.app, "invalid server address", .{ .err = err, .host = opts.host, .port = opts.port }); log.fatal(.app, "invalid server address", .{ .err = err, .host = opts.host, .port = opts.port });
return args.printUsageAndExit(false); return args.printUsageAndExit(false);
}; };
// _server is global to handle graceful shutdown. // _server is global to handle graceful shutdown.
_server = try Server.init(app, address); var server = try Server.init(app, address);
const server = &_server.?;
defer server.deinit(); defer server.deinit();
try sighandler.on(Server.stop, .{&server});
// max timeout of 1 week. // max timeout of 1 week.
const timeout = if (opts.timeout > 604_800) 604_800_000 else @as(i32, opts.timeout) * 1000; const timeout = if (opts.timeout > 604_800) 604_800_000 else @as(i32, opts.timeout) * 1000;
server.run(address, timeout) catch |err| { server.run(address, timeout) catch |err| {
@@ -373,7 +347,11 @@ const Command = struct {
\\ Defaults to \\ Defaults to
++ (if (builtin.mode == .Debug) " pretty." else " logfmt.") ++ ++ (if (builtin.mode == .Debug) " pretty." else " logfmt.") ++
\\ \\
\\ --user_agent_suffix \\--log_filter_scopes
\\ Filter out too verbose logs per scope:
\\ http, unknown_prop, script_event, ...
\\
\\--user_agent_suffix
\\ Suffix to append to the Lightpanda/X.Y User-Agent \\ Suffix to append to the Lightpanda/X.Y User-Agent
\\ \\
; ;
@@ -884,7 +862,7 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void {
} }
if (std.mem.eql(u8, path, "/xhr/json")) { if (std.mem.eql(u8, path, "/xhr/json")) {
return req.respond("{\"over\":\"9000!!!\"}", .{ return req.respond("{\"over\":\"9000!!!\",\"updated_at\":1765867200000}", .{
.extra_headers = &.{ .extra_headers = &.{
.{ .name = "Content-Type", .value = "application/json" }, .{ .name = "Content-Type", .value = "application/json" },
}, },

View File

@@ -90,14 +90,17 @@ pub const Notification = struct {
pub const PageRemove = struct {}; pub const PageRemove = struct {};
pub const PageNavigate = struct { pub const PageNavigate = struct {
req_id: usize,
timestamp: u32, timestamp: u32,
url: []const u8, url: []const u8,
opts: page.NavigateOpts, opts: page.NavigateOpts,
}; };
pub const PageNavigated = struct { pub const PageNavigated = struct {
req_id: usize,
timestamp: u32, timestamp: u32,
url: []const u8, url: []const u8,
opts: page.NavigatedOpts,
}; };
pub const PageNetworkIdle = struct { pub const PageNetworkIdle = struct {
@@ -296,6 +299,7 @@ test "Notification" {
// noop // noop
notifier.dispatch(.page_navigate, &.{ notifier.dispatch(.page_navigate, &.{
.req_id = 1,
.timestamp = 4, .timestamp = 4,
.url = undefined, .url = undefined,
.opts = .{}, .opts = .{},
@@ -305,6 +309,7 @@ test "Notification" {
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate); try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
notifier.dispatch(.page_navigate, &.{ notifier.dispatch(.page_navigate, &.{
.req_id = 1,
.timestamp = 4, .timestamp = 4,
.url = undefined, .url = undefined,
.opts = .{}, .opts = .{},
@@ -313,6 +318,7 @@ test "Notification" {
notifier.unregisterAll(&tc); notifier.unregisterAll(&tc);
notifier.dispatch(.page_navigate, &.{ notifier.dispatch(.page_navigate, &.{
.req_id = 1,
.timestamp = 10, .timestamp = 10,
.url = undefined, .url = undefined,
.opts = .{}, .opts = .{},
@@ -322,21 +328,23 @@ test "Notification" {
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate); try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated); try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
notifier.dispatch(.page_navigate, &.{ notifier.dispatch(.page_navigate, &.{
.req_id = 1,
.timestamp = 10, .timestamp = 10,
.url = undefined, .url = undefined,
.opts = .{}, .opts = .{},
}); });
notifier.dispatch(.page_navigated, &.{ .timestamp = 6, .url = undefined }); notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 6, .url = undefined, .opts = .{} });
try testing.expectEqual(14, tc.page_navigate); try testing.expectEqual(14, tc.page_navigate);
try testing.expectEqual(6, tc.page_navigated); try testing.expectEqual(6, tc.page_navigated);
notifier.unregisterAll(&tc); notifier.unregisterAll(&tc);
notifier.dispatch(.page_navigate, &.{ notifier.dispatch(.page_navigate, &.{
.req_id = 1,
.timestamp = 100, .timestamp = 100,
.url = undefined, .url = undefined,
.opts = .{}, .opts = .{},
}); });
notifier.dispatch(.page_navigated, &.{ .timestamp = 100, .url = undefined }); notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
try testing.expectEqual(14, tc.page_navigate); try testing.expectEqual(14, tc.page_navigate);
try testing.expectEqual(6, tc.page_navigated); try testing.expectEqual(6, tc.page_navigated);
@@ -344,27 +352,27 @@ test "Notification" {
// unregister // unregister
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate); try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated); try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} }); notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined }); notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
try testing.expectEqual(114, tc.page_navigate); try testing.expectEqual(114, tc.page_navigate);
try testing.expectEqual(1006, tc.page_navigated); try testing.expectEqual(1006, tc.page_navigated);
notifier.unregister(.page_navigate, &tc); notifier.unregister(.page_navigate, &tc);
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} }); notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined }); notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
try testing.expectEqual(114, tc.page_navigate); try testing.expectEqual(114, tc.page_navigate);
try testing.expectEqual(2006, tc.page_navigated); try testing.expectEqual(2006, tc.page_navigated);
notifier.unregister(.page_navigated, &tc); notifier.unregister(.page_navigated, &tc);
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} }); notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined }); notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
try testing.expectEqual(114, tc.page_navigate); try testing.expectEqual(114, tc.page_navigate);
try testing.expectEqual(2006, tc.page_navigated); try testing.expectEqual(2006, tc.page_navigated);
// already unregistered, try anyways // already unregistered, try anyways
notifier.unregister(.page_navigated, &tc); notifier.unregister(.page_navigated, &tc);
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} }); notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined }); notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
try testing.expectEqual(114, tc.page_navigate); try testing.expectEqual(114, tc.page_navigate);
try testing.expectEqual(2006, tc.page_navigated); try testing.expectEqual(2006, tc.page_navigated);
} }

View File

@@ -38,7 +38,7 @@ const MAX_MESSAGE_SIZE = 512 * 1024 + 14 + 140;
pub const Server = struct { pub const Server = struct {
app: *App, app: *App,
shutdown: bool, shutdown: bool = false,
allocator: Allocator, allocator: Allocator,
client: ?posix.socket_t, client: ?posix.socket_t,
listener: ?posix.socket_t, listener: ?posix.socket_t,
@@ -53,16 +53,36 @@ pub const Server = struct {
.app = app, .app = app,
.client = null, .client = null,
.listener = null, .listener = null,
.shutdown = false,
.allocator = allocator, .allocator = allocator,
.json_version_response = json_version_response, .json_version_response = json_version_response,
}; };
} }
/// Interrupts the server so that main can complete normally and call all defer handlers.
pub fn stop(self: *Server) void {
if (@atomicRmw(bool, &self.shutdown, .Xchg, true, .monotonic)) {
return;
}
// Linux and BSD/macOS handle canceling a socket blocked on accept differently.
// For Linux, we use std.shutdown, which will cause accept to return error.SocketNotListening (EINVAL).
// For BSD, shutdown will return an error. Instead we call posix.close, which will result with error.ConnectionAborted (BADF).
if (self.listener) |listener| switch (builtin.target.os.tag) {
.linux => posix.shutdown(listener, .recv) catch |err| {
log.warn(.app, "listener shutdown", .{ .err = err });
},
.macos, .freebsd, .netbsd, .openbsd => {
self.listener = null;
posix.close(listener);
},
else => unreachable,
};
}
pub fn deinit(self: *Server) void { pub fn deinit(self: *Server) void {
self.shutdown = true;
if (self.listener) |listener| { if (self.listener) |listener| {
posix.close(listener); posix.close(listener);
self.listener = null;
} }
// *if* server.run is running, we should really wait for it to return // *if* server.run is running, we should really wait for it to return
// before existing from here. // before existing from here.
@@ -83,14 +103,19 @@ pub const Server = struct {
try posix.listen(listener, 1); try posix.listen(listener, 1);
log.info(.app, "server running", .{ .address = address }); log.info(.app, "server running", .{ .address = address });
while (true) { while (!@atomicLoad(bool, &self.shutdown, .monotonic)) {
const socket = posix.accept(listener, null, null, posix.SOCK.NONBLOCK) catch |err| { const socket = posix.accept(listener, null, null, posix.SOCK.NONBLOCK) catch |err| {
if (self.shutdown) { switch (err) {
return; error.SocketNotListening, error.ConnectionAborted => {
log.info(.app, "server stopped", .{});
break;
},
else => {
log.err(.app, "CDP accept", .{ .err = err });
std.Thread.sleep(std.time.ns_per_s);
continue;
},
} }
log.err(.app, "CDP accept", .{ .err = err });
std.Thread.sleep(std.time.ns_per_s);
continue;
}; };
self.client = socket; self.client = socket;
@@ -487,7 +512,7 @@ pub const Client = struct {
} }
// called by CDP // called by CDP
// Websocket frames have a variable lenght header. For server-client, // Websocket frames have a variable length header. For server-client,
// it could be anywhere from 2 to 10 bytes. Our IO.Loop doesn't have // it could be anywhere from 2 to 10 bytes. Our IO.Loop doesn't have
// writev, so we need to get creative. We'll JSON serialize to a // writev, so we need to get creative. We'll JSON serialize to a
// buffer, where the first 10 bytes are reserved. We can then backfill // buffer, where the first 10 bytes are reserved. We can then backfill

88
src/sighandler.zig Normal file
View File

@@ -0,0 +1,88 @@
//! This structure processes operating system signals (SIGINT, SIGTERM)
//! and runs callbacks to clean up the system gracefully.
//!
//! The structure does not clear the memory allocated in the arena,
//! clear the entire arena when exiting the program.
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const log = @import("log.zig");
pub const SigHandler = struct {
arena: Allocator,
sigset: std.posix.sigset_t = undefined,
handle_thread: ?std.Thread = null,
attempt: u32 = 0,
listeners: std.ArrayList(Listener) = .empty,
pub const Listener = struct {
args: []const u8,
start: *const fn (context: *const anyopaque) void,
};
pub fn install(self: *SigHandler) !void {
// Block SIGINT and SIGTERM for the current thread and all created from it
self.sigset = std.posix.sigemptyset();
std.posix.sigaddset(&self.sigset, std.posix.SIG.INT);
std.posix.sigaddset(&self.sigset, std.posix.SIG.TERM);
std.posix.sigaddset(&self.sigset, std.posix.SIG.QUIT);
std.posix.sigprocmask(std.posix.SIG.BLOCK, &self.sigset, null);
self.handle_thread = try std.Thread.spawn(.{ .allocator = self.arena }, SigHandler.sighandle, .{self});
self.handle_thread.?.detach();
}
pub fn on(self: *SigHandler, func: anytype, args: std.meta.ArgsTuple(@TypeOf(func))) !void {
assert(@typeInfo(@TypeOf(func)).@"fn".return_type.? == void);
const Args = @TypeOf(args);
const TypeErased = struct {
fn start(context: *const anyopaque) void {
const args_casted: *const Args = @ptrCast(@alignCast(context));
@call(.auto, func, args_casted.*);
}
};
const buffer = try self.arena.alignedAlloc(u8, .of(Args), @sizeOf(Args));
errdefer self.arena.free(buffer);
const bytes: []const u8 = @ptrCast((&args)[0..1]);
@memcpy(buffer, bytes);
try self.listeners.append(self.arena, .{
.args = buffer,
.start = TypeErased.start,
});
}
fn sighandle(self: *SigHandler) noreturn {
while (true) {
var sig: c_int = 0;
const rc = std.c.sigwait(&self.sigset, &sig);
if (rc != 0) {
log.err(.app, "Unable to process signal {}", .{rc});
std.process.exit(1);
}
switch (sig) {
std.posix.SIG.INT, std.posix.SIG.TERM => {
if (self.attempt > 1) {
std.process.exit(1);
}
self.attempt += 1;
log.info(.app, "Received termination signal...", .{});
for (self.listeners.items) |*item| {
item.start(item.args.ptr);
}
continue;
},
else => continue,
}
}
}
};

View File

@@ -402,19 +402,13 @@ pub fn htmlRunner(file: []const u8) !void {
const url = try std.fmt.allocPrint(arena_allocator, "http://localhost:9582/src/tests/{s}", .{file}); const url = try std.fmt.allocPrint(arena_allocator, "http://localhost:9582/src/tests/{s}", .{file});
try page.navigate(url, .{}); try page.navigate(url, .{});
_ = page.wait(2000); test_session.fetchWait(2000);
// page exits more aggressively in tests. We want to make sure this is called // page exits more aggressively in tests. We want to make sure this is called
// at lease once. // at lease once.
page.session.browser.runMicrotasks(); page.session.browser.runMicrotasks();
page.session.browser.runMessageLoop(); page.session.browser.runMessageLoop();
const needs_second_wait = try js_context.exec("testing._onPageWait.length > 0", "check_onPageWait");
if (needs_second_wait.value.toBool(page.js.isolate)) {
// sets the isSecondWait flag in testing.
_ = js_context.exec("testing._isSecondWait = true", "set_second_wait_flag") catch {};
_ = page.wait(2000);
}
@import("root").js_runner_duration += std.time.Instant.since(try std.time.Instant.now(), start); @import("root").js_runner_duration += std.time.Instant.since(try std.time.Instant.now(), start);
const value = js_context.exec("testing.getStatus()", "testing.getStatus()") catch |err| { const value = js_context.exec("testing.getStatus()", "testing.getStatus()") catch |err| {

View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id="content">
<a id="a1" href="foo" class="ok">OK</a>
<p id="p1" class="ok empty">
<span id="s1"></span>
</p>
<p id="p2"> And</p>
</div>
<script id=document_write>
document.open();
document.write("<p id=ok>Hello world!</p>");
document.write("<p>I am a fish</p>");
document.write("<p>The number is 42</p>");
document.close();
const ok = document.getElementById("ok");
testing.expectEqual('Hello world!', ok.innerText);
const content = document.firstElementChild.innerHTML;
testing.expectEqual('<head></head><body><p id="ok">Hello world!</p><p>I am a fish</p><p>The number is 42</p></body>', content);
</script>

View File

@@ -168,35 +168,35 @@
<script id=dimensions> <script id=dimensions>
const para = document.getElementById('para'); const para = document.getElementById('para');
testing.expectEqual(1, para.clientWidth); testing.expectEqual(5, para.clientWidth);
testing.expectEqual(1, para.clientHeight); testing.expectEqual(5, para.clientHeight);
// let r1 = document.getElementById('para').getBoundingClientRect(); let r1 = document.getElementById('para').getBoundingClientRect();
// testing.expectEqual(0, r1.x); testing.expectEqual(0, r1.x);
// testing.expectEqual(0, r1.y); testing.expectEqual(0, r1.y);
// testing.expectEqual(1, r1.width); testing.expectEqual(5, r1.width);
// testing.expectEqual(2, r1.height); testing.expectEqual(5, r1.height);
// let r2 = document.getElementById('content').getBoundingClientRect(); let r2 = document.getElementById('content').getBoundingClientRect();
// testing.expectEqual(1, r2.x); testing.expectEqual(5, r2.x);
// testing.expectEqual(0, r2.y); testing.expectEqual(0, r2.y);
// testing.expectEqual(1, r2.width); testing.expectEqual(5, r2.width);
// testing.expectEqual(1, r2.height); testing.expectEqual(5, r2.height);
// let r3 = document.getElementById('para').getBoundingClientRect(); let r3 = document.getElementById('para').getBoundingClientRect();
// testing.expectEqual(0, r3.x); testing.expectEqual(0, r3.x);
// testing.expectEqual(0, r3.y); testing.expectEqual(0, r3.y);
// testing.expectEqual(1, r3.width); testing.expectEqual(5, r3.width);
// testing.expectEqual(1, r3.height); testing.expectEqual(5, r3.height);
// testing.expectEqual(1, para.clientWidth); testing.expectEqual(10, para.clientWidth);
// testing.expectEqual(1, para.clientHeight); testing.expectEqual(5, para.clientHeight);
// let r4 = document.createElement('div').getBoundingClientRect(); let r4 = document.createElement('div').getBoundingClientRect();
// testing.expectEqual(0, r4.x); testing.expectEqual(0, r4.x);
// testing.expectEqual(0, r4.y); testing.expectEqual(0, r4.y);
// testing.expectEqual(0, r4.width); testing.expectEqual(0, r4.width);
// testing.expectEqual(0, r4.height); testing.expectEqual(0, r4.height);
</script> </script>
<script id=matches> <script id=matches>

View File

@@ -113,4 +113,13 @@
// doesn't crash on null receiver // doesn't crash on null receiver
content.addEventListener('he2', null); content.addEventListener('he2', null);
content.dispatchEvent(new Event('he2')); content.dispatchEvent(new Event('he2'));
// Test that EventTarget constructor properly initializes vtable
const et = new EventTarget();
testing.expectEqual('[object EventTarget]', et.toString());
let constructorTestCalled = false;
et.addEventListener('test', () => { constructorTestCalled = true; });
et.dispatchEvent(new Event('test'));
testing.expectEqual(true, constructorTestCalled);
</script> </script>

View File

@@ -122,13 +122,13 @@
testing.expectEqual(1, entry.intersectionRatio); testing.expectEqual(1, entry.intersectionRatio);
testing.expectEqual(0, entry.intersectionRect.x); testing.expectEqual(0, entry.intersectionRect.x);
testing.expectEqual(0, entry.intersectionRect.y); testing.expectEqual(0, entry.intersectionRect.y);
testing.expectEqual(1, entry.intersectionRect.width); testing.expectEqual(5, entry.intersectionRect.width);
testing.expectEqual(1, entry.intersectionRect.height); testing.expectEqual(5, entry.intersectionRect.height);
testing.expectEqual(true, entry.isIntersecting); testing.expectEqual(true, entry.isIntersecting);
testing.expectEqual(0, entry.rootBounds.x); testing.expectEqual(0, entry.rootBounds.x);
testing.expectEqual(0, entry.rootBounds.y); testing.expectEqual(0, entry.rootBounds.y);
testing.expectEqual(1, entry.rootBounds.width); testing.expectEqual(5, entry.rootBounds.width);
testing.expectEqual(1, entry.rootBounds.height); testing.expectEqual(5, entry.rootBounds.height);
testing.expectEqual('[object HTMLDivElement]', entry.target.toString()); testing.expectEqual('[object HTMLDivElement]', entry.target.toString());
}); });
} }

View File

@@ -7,6 +7,7 @@
<p id="para"> And</p> <p id="para"> And</p>
<!--comment--> <!--comment-->
</div> </div>
<div id="rootNodeComposed"></div>
</body> </body>
<script src="../testing.js"></script> <script src="../testing.js"></script>
@@ -36,6 +37,26 @@ let first_child = content.firstChild.nextSibling; // nextSibling because of line
testing.expectEqual('HTMLDocument', content.getRootNode().__proto__.constructor.name); testing.expectEqual('HTMLDocument', content.getRootNode().__proto__.constructor.name);
</script> </script>
<script id=getRootNodeComposed>
const testContainer = $('#rootNodeComposed');
const shadowHost = document.createElement('div');
testContainer.appendChild(shadowHost);
const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
const shadowChild = document.createElement('span');
shadowRoot.appendChild(shadowChild);
testing.expectEqual('ShadowRoot', shadowChild.getRootNode().__proto__.constructor.name);
testing.expectEqual('ShadowRoot', shadowChild.getRootNode({ composed: false }).__proto__.constructor.name);
testing.expectEqual('HTMLDocument', shadowChild.getRootNode({ composed: true }).__proto__.constructor.name);
testing.expectEqual('HTMLDocument', shadowHost.getRootNode().__proto__.constructor.name);
const disconnected = document.createElement('div');
const disconnectedChild = document.createElement('span');
disconnected.appendChild(disconnectedChild);
testing.expectEqual('HTMLDivElement', disconnectedChild.getRootNode().__proto__.constructor.name);
testing.expectEqual('HTMLDivElement', disconnectedChild.getRootNode({ composed: true }).__proto__.constructor.name);
</script>
<script id=firstChild> <script id=firstChild>
let body_first_child = document.body.firstChild; let body_first_child = document.body.firstChild;
testing.expectEqual('div', body_first_child.localName); testing.expectEqual('div', body_first_child.localName);

View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script id=noNata>
{
let event = new CompositionEvent("test", {});
testing.expectEqual(true, event instanceof CompositionEvent);
testing.expectEqual(true, event instanceof Event);
testing.expectEqual("test", event.type);
testing.expectEqual("", event.data);
}
</script>
<script id=withData>
{
let event = new CompositionEvent("test2", {data: "over 9000!"});
testing.expectEqual("test2", event.type);
testing.expectEqual("over 9000!", event.data);
}
</script>
<script id=dispatch>
{
let called = 0;
document.addEventListener('CE', (e) => {
testing.expectEqual('test-data', e.data);
testing.expectEqual(true, e instanceof CompositionEvent);
called += 1
});
document.dispatchEvent(new CompositionEvent('CE', {data: 'test-data'}));
testing.expectEqual(1, called);
}
</script>

View File

@@ -12,7 +12,8 @@
}); });
testing.async(promise1, (json) => { testing.async(promise1, (json) => {
testing.expectEqual({over: '9000!!!'}, json); testing.expectEqual("number", typeof json.updated_at);
testing.expectEqual({over: '9000!!!',updated_at:1765867200000}, json);
}); });
</script> </script>
@@ -29,6 +30,7 @@
}); });
testing.async(promise1, (json) => { testing.async(promise1, (json) => {
testing.expectEqual({over: '9000!!!'}, json); testing.expectEqual("number", typeof json.updated_at);
testing.expectEqual({over: '9000!!!',updated_at:1765867200000}, json);
}); });
</script> </script>

125
src/tests/file/blob.html Normal file
View File

@@ -0,0 +1,125 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script id=Blob/Blob.text>
{
const parts = ["\r\nthe quick brown\rfo\rx\r", "\njumps over\r\nthe\nlazy\r", "\ndog"];
// "transparent" ending should not modify the final buffer.
const blob = new Blob(parts, { type: "text/html" });
const expected = parts.join("");
testing.expectEqual(expected.length, blob.size);
testing.expectEqual("text/html", blob.type);
testing.async(blob.text(), result => testing.expectEqual(expected, result));
}
{
const parts = ["\rhello\r", "\nwor\r\nld"];
// "native" ending should modify the final buffer.
const blob = new Blob(parts, { endings: "native" });
const expected = "\nhello\n\nwor\nld";
testing.expectEqual(expected.length, blob.size);
testing.async(blob.text(), result => testing.expectEqual(expected, result));
testing.async(blob.arrayBuffer(), result => testing.expectEqual(true, result instanceof ArrayBuffer));
}
</script>
<script id=Blob.stream>
{
const parts = ["may", "thy", "knife", "chip", "and", "shatter"];
const blob = new Blob(parts);
const reader = blob.stream().getReader();
testing.async(reader.read(), ({ done, value }) => {
const expected = new Uint8Array([109, 97, 121, 116, 104, 121, 107, 110,
105, 102, 101, 99, 104, 105, 112, 97,
110, 100, 115, 104, 97, 116, 116, 101,
114]);
testing.expectEqual(false, done);
testing.expectEqual(true, value instanceof Uint8Array);
testing.expectEqual(expected, value);
});
}
</script>
<script id=Blob.arrayBuffer/Blob.slice>
{
const parts = ["la", "symphonie", "des", "éclairs"];
const blob = new Blob(parts);
testing.async(blob.arrayBuffer(), result => testing.expectEqual(true, result instanceof ArrayBuffer));
let temp = blob.slice(0);
testing.expectEqual(blob.size, temp.size);
testing.async(temp.text(), result => {
testing.expectEqual("lasymphoniedeséclairs", result);
});
temp = blob.slice(-4, -2, "custom");
testing.expectEqual(2, temp.size);
testing.expectEqual("custom", temp.type);
testing.async(temp.text(), result => testing.expectEqual("ai", result));
temp = blob.slice(14);
testing.expectEqual(8, temp.size);
testing.async(temp.text(), result => testing.expectEqual("éclairs", result));
temp = blob.slice(6, -10, "text/eclair");
testing.expectEqual(6, temp.size);
testing.expectEqual("text/eclair", temp.type);
testing.async(temp.text(), result => testing.expectEqual("honied", result));
}
</script>
<!-- Firefox and Safari only -->
<script id=Blob.bytes>
{
const parts = ["light ", "panda ", "rocks ", "!"];
const blob = new Blob(parts);
testing.async(blob.bytes(), result => {
const expected = new Uint8Array([108, 105, 103, 104, 116, 32, 112, 97,
110, 100, 97, 32, 114, 111, 99, 107, 115,
32, 33]);
testing.expectEqual(true, result instanceof Uint8Array);
testing.expectEqual(expected, result);
});
}
// Test for SIMD.
{
const parts = [
"\rThe opened package\r\nof potato\nchi\rps",
"held the\r\nanswer to the\r mystery. Both det\rectives looke\r\rd\r",
"\rat it but failed to realize\nit was\r\nthe\rkey\r\n",
"\r\nto solve the \rcrime.\r"
];
const blob = new Blob(parts, { type: "text/html", endings: "native" });
testing.expectEqual(161, blob.size);
testing.expectEqual("text/html", blob.type);
testing.async(blob.bytes(), result => {
const expected = new Uint8Array([10, 84, 104, 101, 32, 111, 112, 101, 110,
101, 100, 32, 112, 97, 99, 107, 97, 103,
101, 10, 111, 102, 32, 112, 111, 116, 97,
116, 111, 10, 99, 104, 105, 10, 112, 115,
104, 101, 108, 100, 32, 116, 104, 101, 10,
97, 110, 115, 119, 101, 114, 32, 116, 111,
32, 116, 104, 101, 10, 32, 109, 121, 115,
116, 101, 114, 121, 46, 32, 66, 111, 116,
104, 32, 100, 101, 116, 10, 101, 99, 116,
105, 118, 101, 115, 32, 108, 111, 111, 107,
101, 10, 10, 100, 10, 10, 97, 116, 32, 105,
116, 32, 98, 117, 116, 32, 102, 97, 105, 108,
101, 100, 32, 116, 111, 32, 114, 101, 97,
108, 105, 122, 101, 10, 105, 116, 32, 119, 97,
115, 10, 116, 104, 101, 10, 107, 101, 121,
10, 10, 116, 111, 32, 115, 111, 108, 118, 101,
32, 116, 104, 101, 32, 10, 99, 114, 105, 109,
101, 46, 10]);
testing.expectEqual(true, result instanceof Uint8Array);
testing.expectEqual(expected, result);
});
}
</script>

View File

@@ -1,6 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<script src="../testing.js"></script> <script src="../testing.js"></script>
<script id=file> <script id=file>
let f = new File() let f = new File();
testing.expectEqual(true, f instanceof File); testing.expectEqual(true, f instanceof File);
</script> </script>

116
src/tests/html/canvas.html Normal file
View File

@@ -0,0 +1,116 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script id=CanvasRenderingContext2D>
{
const element = document.createElement("canvas");
const ctx = element.getContext("2d");
testing.expectEqual(true, ctx instanceof CanvasRenderingContext2D);
// We can't really test this but let's try to call it at least.
ctx.fillRect(0, 0, 0, 0);
}
</script>
<script id=CanvasRenderingContext2D#fillStyle>
{
const element = document.createElement("canvas");
const ctx = element.getContext("2d");
// Black by default.
testing.expectEqual(ctx.fillStyle, "#000000");
ctx.fillStyle = "red";
testing.expectEqual(ctx.fillStyle, "#ff0000");
ctx.fillStyle = "rebeccapurple";
testing.expectEqual(ctx.fillStyle, "#663399");
// No changes made if color is invalid.
ctx.fillStyle = "invalid-color";
testing.expectEqual(ctx.fillStyle, "#663399");
ctx.fillStyle = "#fc0";
testing.expectEqual(ctx.fillStyle, "#ffcc00");
ctx.fillStyle = "#ff0000";
testing.expectEqual(ctx.fillStyle, "#ff0000");
ctx.fillStyle = "#fF00000F";
testing.expectEqual(ctx.fillStyle, "rgba(255, 0, 0, 0.06)");
}
</script>
<script id=WebGLRenderingContext#getSupportedExtensions>
{
const element = document.createElement("canvas");
const ctx = element.getContext("webgl");
testing.expectEqual(true, ctx instanceof WebGLRenderingContext);
const supportedExtensions = ctx.getSupportedExtensions();
// The order Chrome prefer.
const expectedExtensions = [
"ANGLE_instanced_arrays",
"EXT_blend_minmax",
"EXT_clip_control",
"EXT_color_buffer_half_float",
"EXT_depth_clamp",
"EXT_disjoint_timer_query",
"EXT_float_blend",
"EXT_frag_depth",
"EXT_polygon_offset_clamp",
"EXT_shader_texture_lod",
"EXT_texture_compression_bptc",
"EXT_texture_compression_rgtc",
"EXT_texture_filter_anisotropic",
"EXT_texture_mirror_clamp_to_edge",
"EXT_sRGB",
"KHR_parallel_shader_compile",
"OES_element_index_uint",
"OES_fbo_render_mipmap",
"OES_standard_derivatives",
"OES_texture_float",
"OES_texture_float_linear",
"OES_texture_half_float",
"OES_texture_half_float_linear",
"OES_vertex_array_object",
"WEBGL_blend_func_extended",
"WEBGL_color_buffer_float",
"WEBGL_compressed_texture_astc",
"WEBGL_compressed_texture_etc",
"WEBGL_compressed_texture_etc1",
"WEBGL_compressed_texture_pvrtc",
"WEBGL_compressed_texture_s3tc",
"WEBGL_compressed_texture_s3tc_srgb",
"WEBGL_debug_renderer_info",
"WEBGL_debug_shaders",
"WEBGL_depth_texture",
"WEBGL_draw_buffers",
"WEBGL_lose_context",
"WEBGL_multi_draw",
"WEBGL_polygon_mode"
];
testing.expectEqual(expectedExtensions.length, supportedExtensions.length);
for (let i = 0; i < expectedExtensions.length; i++) {
testing.expectEqual(expectedExtensions[i], supportedExtensions[i]);
}
}
</script>
<script id=WebGLRenderingCanvas#getExtension>
// WEBGL_debug_renderer_info
{
const element = document.createElement("canvas");
const ctx = element.getContext("webgl");
const rendererInfo = ctx.getExtension("WEBGL_debug_renderer_info");
testing.expectEqual(true, rendererInfo instanceof WEBGL_debug_renderer_info);
testing.expectEqual(rendererInfo.UNMASKED_VENDOR_WEBGL, 0x9245);
testing.expectEqual(rendererInfo.UNMASKED_RENDERER_WEBGL, 0x9246);
}
// WEBGL_lose_context
{
const element = document.createElement("canvas");
const ctx = element.getContext("webgl");
const loseContext = ctx.getExtension("WEBGL_lose_context");
testing.expectEqual(true, loseContext instanceof WEBGL_lose_context);
loseContext.loseContext();
loseContext.restoreContext();
}
</script>

View File

@@ -46,15 +46,15 @@
testing.expectEqual('name=Oeschger; favorite_food=tripe', document.cookie); testing.expectEqual('name=Oeschger; favorite_food=tripe', document.cookie);
// Return null since we only return elements when they have previously been localized // Return null since we only return elements when they have previously been localized
testing.expectEqual(null, document.elementFromPoint(0.5, 0.5)); testing.expectEqual(null, document.elementFromPoint(2.5, 2.5));
testing.expectEqual([], document.elementsFromPoint(0.5, 0.5)); testing.expectEqual([], document.elementsFromPoint(2.5, 2.5));
let div1 = document.createElement('div'); let div1 = document.createElement('div');
document.body.appendChild(div1); document.body.appendChild(div1);
div1.getClientRects(); // clal this to position it div1.getClientRects(); // clal this to position it
testing.expectEqual('[object HTMLDivElement]', document.elementFromPoint(0.5, 0.5).toString()); testing.expectEqual('[object HTMLDivElement]', document.elementFromPoint(2.5, 2.5).toString());
let elems = document.elementsFromPoint(0.5, 0.5); let elems = document.elementsFromPoint(2.5, 2.5);
testing.expectEqual(3, elems.length); testing.expectEqual(3, elems.length);
testing.expectEqual('[object HTMLDivElement]', elems[0].toString()); testing.expectEqual('[object HTMLDivElement]', elems[0].toString());
testing.expectEqual('[object HTMLBodyElement]', elems[1].toString()); testing.expectEqual('[object HTMLBodyElement]', elems[1].toString());
@@ -66,11 +66,11 @@
// Note this will be placed after the div of previous test // Note this will be placed after the div of previous test
a.getClientRects(); a.getClientRects();
let a_again = document.elementFromPoint(1.5, 0.5); let a_again = document.elementFromPoint(7.5, 0.5);
testing.expectEqual('[object HTMLAnchorElement]', a_again.toString()); testing.expectEqual('[object HTMLAnchorElement]', a_again.toString());
testing.expectEqual('https://lightpanda.io', a_again.href); testing.expectEqual('https://lightpanda.io', a_again.href);
let a_agains = document.elementsFromPoint(1.5, 0.5); let a_agains = document.elementsFromPoint(7.5, 0.5);
testing.expectEqual('https://lightpanda.io', a_agains[0].href); testing.expectEqual('https://lightpanda.io', a_agains[0].href);

View File

@@ -1,21 +1,22 @@
<!DOCTYPE html> <!DOCTYPE html>
<script src="../testing.js"></script> <script src="../../testing.js"></script>
<script id=history> <script id=history>
testing.expectEqual('auto', history.scrollRestoration); testing.expectEqual('auto', history.scrollRestoration);
history.scrollRestoration = 'manual'; history.scrollRestoration = 'manual';
history.scrollRestoration = 'foo';
testing.expectEqual('manual', history.scrollRestoration); testing.expectEqual('manual', history.scrollRestoration);
history.scrollRestoration = 'auto'; history.scrollRestoration = 'auto';
testing.expectEqual('auto', history.scrollRestoration); testing.expectEqual('auto', history.scrollRestoration);
testing.expectEqual(null, history.state) testing.expectEqual(null, history.state)
history.pushState({ testInProgress: true }, null, 'http://127.0.0.1:9582/xhr/json'); history.pushState({ testInProgress: true }, null, 'http://127.0.0.1:9582/src/tests/html/history/history_after_nav.html');
testing.expectEqual({ testInProgress: true }, history.state); testing.expectEqual({ testInProgress: true }, history.state);
history.pushState({ testInProgress: false }, null, 'http://127.0.0.1:9582/xhr/json');
history.replaceState({ "new": "field", testComplete: true }, null); history.replaceState({ "new": "field", testComplete: true }, null);
let state = { "new": "field", testComplete: true }; let state = { "new": "field", testComplete: true };
testing.expectEqual(state, history.state); testing.expectEqual(state, history.state);
@@ -32,10 +33,5 @@
testing.expectEqual(state, popstateEventState); testing.expectEqual(state, popstateEventState);
}) })
testing.onPageWait(() => { history.back();
testing.expectEqual(true, history.state && history.state.testComplete);
testing.expectEqual(state, history.state);
});
testing.expectEqual(undefined, history.go());
</script> </script>

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<script id=history2>
history.pushState(
{"new": "field", testComplete: true },
null,
'http://127.0.0.1:9582/src/tests/html/history/history_after_nav.html'
);
let popstateEventFired = false;
let popstateEventState = null;
// uses the window event listener.
window.onpopstate = (event) => {
popstateEventFired = true;
popstateEventState = event.state;
};
testing.eventually(() => {
testing.expectEqual(true, popstateEventFired);
testing.expectEqual(true, popstateEventState.testComplete);
})
history.back();
</script>

View File

@@ -0,0 +1,6 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<script id=history2>
testing.expectEqual(true, history.state && history.state.testInProgress);
</script>

View File

@@ -16,8 +16,8 @@
testing.expectEqual('https://lightpanda.io', link.origin); testing.expectEqual('https://lightpanda.io', link.origin);
link.host = 'lightpanda.io:443'; link.host = 'lightpanda.io:443';
testing.expectEqual('lightpanda.io:443', link.host); testing.expectEqual('lightpanda.io', link.host);
testing.expectEqual('443', link.port); testing.expectEqual('', link.port);
testing.expectEqual('lightpanda.io', link.hostname); testing.expectEqual('lightpanda.io', link.hostname);
link.host = 'lightpanda.io'; link.host = 'lightpanda.io';
@@ -42,9 +42,9 @@
testing.expectEqual('', link.port); testing.expectEqual('', link.port);
link.port = '443'; link.port = '443';
testing.expectEqual('foo.bar:443', link.host); testing.expectEqual('foo.bar', link.host);
testing.expectEqual('foo.bar', link.hostname); testing.expectEqual('foo.bar', link.hostname);
testing.expectEqual('https://foo.bar:443/?q=bar#frag', link.href); testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);
link.port = null; link.port = null;
testing.expectEqual('https://foo.bar/?q=bar#frag', link.href); testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);

View File

@@ -13,3 +13,21 @@
testing.expectEqual("9582", location.port); testing.expectEqual("9582", location.port);
testing.expectEqual("", location.search); testing.expectEqual("", location.search);
</script> </script>
<script id=location_hash>
location.hash = "";
testing.expectEqual("", location.hash);
testing.expectEqual('http://localhost:9582/src/tests/html/location.html', location.href);
location.hash = "#abcdef";
testing.expectEqual("#abcdef", location.hash);
testing.expectEqual('http://localhost:9582/src/tests/html/location.html#abcdef', location.href);
location.hash = "xyzxyz";
testing.expectEqual("#xyzxyz", location.hash);
testing.expectEqual('http://localhost:9582/src/tests/html/location.html#xyzxyz', location.href);
location.hash = "";
testing.expectEqual("", location.hash);
testing.expectEqual('http://localhost:9582/src/tests/html/location.html', location.href);
</script>

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<script id=navigation>
testing.expectEqual('object', typeof navigation);
testing.expectEqual('object', typeof navigation.currentEntry);
testing.expectEqual('string', typeof navigation.currentEntry.id);
testing.expectEqual('string', typeof navigation.currentEntry.key);
testing.expectEqual('string', typeof navigation.currentEntry.url);
const currentIndex = navigation.currentEntry.index;
navigation.navigate(
'http://localhost:9582/src/tests/html/navigation/navigation2.html',
{ state: { currentIndex: currentIndex, navTestInProgress: true } }
);
</script>

View File

@@ -0,0 +1,8 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<script id=navigation2>
const state = navigation.currentEntry.getState();
testing.expectEqual(true, state.navTestInProgress);
testing.expectEqual(state.currentIndex + 1, navigation.currentEntry.index);
</script>

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<script id=navigation_currententrychange>
let currentEntryChanged = false;
navigation.addEventListener("currententrychange", () => {
currentEntryChanged = true;
});
// Doesn't fully navigate if same document.
location.href = location.href + "#1";
testing.expectEqual(true, currentEntryChanged);
</script>

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