129 Commits

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
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
Halil Durak
4cf61d101c initial dummy canvas 2025-11-05 11:50:57 +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
64 changed files with 2115 additions and 757 deletions

View File

@@ -2,10 +2,6 @@ name: "Browsercore install"
description: "Install deps for the project browsercore"
inputs:
zig:
description: 'Zig version to install'
required: false
default: '0.15.2'
arch:
description: 'CPU arch used to select the v8 lib'
required: false
@@ -17,7 +13,7 @@ inputs:
zig-v8:
description: 'zig v8 version to install'
required: false
default: 'v0.1.33'
default: 'v0.1.35'
v8:
description: 'v8 version to install'
required: false
@@ -38,9 +34,8 @@ runs:
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
# Zig version used from the `minimum_zig_version` field in build.zig.zon
- uses: mlugg/setup-zig@v2
with:
version: ${{ inputs.zig }}
- name: Cache v8
id: cache-v8
@@ -61,11 +56,8 @@ runs:
- name: install v8
shell: bash
run: |
mkdir -p v8/out/${{ inputs.os }}/debug/obj/zig/
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/out/${{ inputs.os }}/debug/obj/zig/libc_v8.a
mkdir -p v8/out/${{ inputs.os }}/release/obj/zig/
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/out/${{ inputs.os }}/release/obj/zig/libc_v8.a
mkdir -p v8
ln -s ${{ inputs.cache-dir }}/v8/libc_v8.a v8/libc_v8.a
- name: Cache libiconv
id: cache-libiconv

View File

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

View File

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

View File

@@ -1,8 +1,5 @@
name: zig-fmt
env:
ZIG_VERSION: 0.15.2
on:
pull_request:
@@ -32,14 +29,13 @@ jobs:
timeout-minutes: 15
steps:
- uses: mlugg/setup-zig@v2
with:
version: ${{ env.ZIG_VERSION }}
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
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
id: fmt
run: |
@@ -58,6 +54,7 @@ jobs:
fi
echo "${delimiter}" >> "${GITHUB_OUTPUT}"
- name: Fail the job
if: steps.fmt.outputs.zig_fmt_errs != ''
run: exit 1

View File

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

1
.gitignore vendored
View File

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

3
.gitmodules vendored
View File

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

View File

@@ -1,10 +1,9 @@
FROM debian:stable
FROM debian:stable-slim
ARG MINISIG=0.12
ARG ZIG=0.15.2
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
ARG V8=14.0.365.4
ARG ZIG_V8=v0.1.33
ARG ZIG_V8=v0.1.34
ARG TARGETPLATFORM
RUN apt-get update -yq && \
@@ -17,25 +16,25 @@ RUN apt-get update -yq && \
# install minisig
RUN curl --fail -L -O https://github.com/jedisct1/minisign/releases/download/${MINISIG}/minisign-${MINISIG}-linux.tar.gz && \
tar xvzf minisign-${MINISIG}-linux.tar.gz
# install zig
RUN case $TARGETPLATFORM in \
"linux/arm64") ARCH="aarch64" ;; \
*) ARCH="x86_64" ;; \
esac && \
curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-${ARCH}-linux-${ZIG}.tar.xz && \
curl --fail -L -O https://ziglang.org/download/${ZIG}/zig-${ARCH}-linux-${ZIG}.tar.xz.minisig && \
minisign-linux/${ARCH}/minisign -Vm zig-${ARCH}-linux-${ZIG}.tar.xz -P ${ZIG_MINISIG} && \
tar xvf zig-${ARCH}-linux-${ZIG}.tar.xz && \
mv zig-${ARCH}-linux-${ZIG} /usr/local/lib && \
ln -s /usr/local/lib/zig-${ARCH}-linux-${ZIG}/zig /usr/local/bin/zig
tar xvzf minisign-${MINISIG}-linux.tar.gz -C /
# clone lightpanda
RUN git clone https://github.com/lightpanda-io/browser.git
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
RUN git submodule init && \
git submodule update --recursive
@@ -50,11 +49,16 @@ RUN case $TARGETPLATFORM in \
*) ARCH="x86_64" ;; \
esac && \
curl --fail -L -o libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${ZIG_V8}/libc_v8_${V8}_linux_${ARCH}.a && \
mkdir -p v8/out/linux/release/obj/zig/ && \
mv libc_v8.a v8/out/linux/release/obj/zig/libc_v8.a
mkdir -p v8/ && \
mv libc_v8.a v8/libc_v8.a
# 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
@@ -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 /browser/zig-out/bin/lightpanda /bin/lightpanda
COPY --from=1 /usr/bin/tini /usr/bin/tini
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
help:
@printf "\e[36m%-35s %s\e[0m\n" "Command" "Usage"
@printf "\033[36m%-35s %s\033[0m\n" "Command" "Usage"
@sed -n -e '/^## /{'\
-e 's/## //g;'\
-e 'h;'\
@@ -47,54 +47,43 @@ help:
# $(ZIG) commands
# ------------
.PHONY: build build-dev run run-release shell test bench download-zig wpt data get-v8 build-v8 build-v8-dev
.PHONY: end2end
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"
.PHONY: build build-dev run run-release shell test bench wpt data end2end
## Build in release-safe mode
build:
@printf "\e[36mBuilding (release safe)...\e[0m\n"
$(ZIG) build -Doptimize=ReleaseSafe -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@printf "\e[33mBuild OK\e[0m\n"
@printf "\033[36mBuilding (release safe)...\033[0m\n"
$(ZIG) build -Doptimize=ReleaseSafe -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
@printf "\033[33mBuild OK\033[0m\n"
## Build in debug mode
build-dev:
@printf "\e[36mBuilding (debug)...\e[0m\n"
@$(ZIG) build -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@printf "\e[33mBuild OK\e[0m\n"
@printf "\033[36mBuilding (debug)...\033[0m\n"
@$(ZIG) build -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
@printf "\033[33mBuild OK\033[0m\n"
## Run the server in release mode
run: build
@printf "\e[36mRunning...\e[0m\n"
@./zig-out/bin/lightpanda || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
@printf "\033[36mRunning...\033[0m\n"
@./zig-out/bin/lightpanda || (printf "\033[33mRun ERROR\033[0m\n"; exit 1;)
## Run the server in debug mode
run-debug: build-dev
@printf "\e[36mRunning...\e[0m\n"
@./zig-out/bin/lightpanda || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
@printf "\033[36mRunning...\033[0m\n"
@./zig-out/bin/lightpanda || (printf "\033[33mRun ERROR\033[0m\n"; exit 1;)
## Run a JS shell in debug mode
shell:
@printf "\e[36mBuilding shell...\e[0m\n"
@$(ZIG) build shell || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@printf "\033[36mBuilding shell...\033[0m\n"
@$(ZIG) build shell || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
## Run WPT tests
wpt:
@printf "\e[36mBuilding wpt...\e[0m\n"
@$(ZIG) build wpt -- $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@printf "\033[36mBuilding wpt...\033[0m\n"
@$(ZIG) build wpt -- $(filter-out $@,$(MAKECMDGOALS)) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
wpt-summary:
@printf "\e[36mBuilding wpt...\e[0m\n"
@$(ZIG) build wpt -- --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@printf "\033[36mBuilding wpt...\033[0m\n"
@$(ZIG) build wpt -- --summary $(filter-out $@,$(MAKECMDGOALS)) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
## Test - `grep` is used to filter out the huge compile command on build
ifeq ($(OS), macos)
@@ -112,19 +101,6 @@ end2end:
@test -d ../demo
cd ../demo && go run runner/main.go
## v8
get-v8:
@printf "\e[36mGetting v8 source...\e[0m\n"
@$(ZIG) build get-v8
build-v8-dev:
@printf "\e[36mBuilding v8 (dev)...\e[0m\n"
@$(ZIG) build build-v8
build-v8:
@printf "\e[36mBuilding v8...\e[0m\n"
@$(ZIG) build -Doptimize=ReleaseSafe build-v8
# Install and build required dependencies commands
# ------------
.PHONY: install-submodule
@@ -151,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
# and stick to a specif version. Maybe build from source. Anyway not now.
_install-netsurf: clean-netsurf
@printf "\e[36mInstalling NetSurf...\e[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;) && \
@printf "\033[36mInstalling NetSurf...\033[0m\n" && \
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) && \
cp -R vendor/netsurf/share $(BC_NS) && \
export PREFIX=$(BC_NS) && \
export OPTLDFLAGS="-L$(ICONV)/lib" && \
export OPTCFLAGS="$(OPTCFLAGS) -I$(ICONV)/include" && \
printf "\e[33mInstalling libwapcaplet...\e[0m\n" && \
printf "\033[33mInstalling libwapcaplet...\033[0m\n" && \
cd vendor/netsurf/libwapcaplet && \
BUILDDIR=$(BC_NS)/build/libwapcaplet make install && \
cd ../libparserutils && \
printf "\e[33mInstalling libparserutils...\e[0m\n" && \
printf "\033[33mInstalling libparserutils...\033[0m\n" && \
BUILDDIR=$(BC_NS)/build/libparserutils make install && \
cd ../libhubbub && \
printf "\e[33mInstalling libhubbub...\e[0m\n" && \
printf "\033[33mInstalling libhubbub...\033[0m\n" && \
BUILDDIR=$(BC_NS)/build/libhubbub make install && \
rm src/treebuilder/autogenerated-element-type.c && \
cd ../libdom && \
printf "\e[33mInstalling libdom...\e[0m\n" && \
printf "\033[33mInstalling libdom...\033[0m\n" && \
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 && \
$(ZIG) cc \
-I$(ICONV)/include \
@@ -188,14 +164,14 @@ _install-netsurf: clean-netsurf
$(ICONV)/lib/libiconv.a && \
./a.out > /dev/null && \
rm a.out && \
printf "\e[36mDone NetSurf $(OS)\e[0m\n"
printf "\033[36mDone NetSurf $(OS)\033[0m\n"
clean-netsurf:
@printf "\e[36mCleaning NetSurf build...\e[0m\n" && \
@printf "\033[36mCleaning NetSurf build...\033[0m\n" && \
rm -Rf $(BC_NS)
test-netsurf:
@printf "\e[36mTesting NetSurf...\e[0m\n" && \
@printf "\033[36mTesting NetSurf...\033[0m\n" && \
export PREFIX=$(BC_NS) && \
export LDFLAGS="-L$(ICONV)/lib -L$(BC_NS)/lib" && \
export CFLAGS="-I$(ICONV)/include -I$(BC_NS)/include" && \

View File

@@ -158,8 +158,6 @@ 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.
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
### Prerequisites
@@ -168,12 +166,13 @@ 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.
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/),
[Brotli](https://github.com/google/brotli),
[Netsurf libs](https://www.netsurf-browser.org/) and
[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:
@@ -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
[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
### Unit Tests

140
build.zig
View File

@@ -21,36 +21,21 @@ const builtin = @import("builtin");
const Build = std.Build;
/// Do not rename this constant. It is scanned by some scripts to determine
/// which zig version to install.
const recommended_zig_version = "0.15.2";
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 optimize = b.standardOptimizeOption(.{});
// We're still using llvm because the new x86 backend seems to crash
// with v8. This can be reproduced in zig-v8-fork.
const manifest = Manifest.init(b);
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", .{
.root_source_file = b.path("src/main.zig"),
@@ -59,7 +44,7 @@ pub fn build(b: *Build) !void {
.link_libc = true,
.link_libcpp = true,
});
try addDependencies(b, lightpanda_module, opts);
try addDependencies(b, lightpanda_module, opts, prebuilt_v8_path);
{
// browser
@@ -113,7 +98,7 @@ pub fn build(b: *Build) !void {
.target = target,
.optimize = optimize,
});
try addDependencies(b, wpt_module, opts);
try addDependencies(b, wpt_module, opts, prebuilt_v8_path);
// compile and install
const wpt = b.addExecutable(.{
@@ -131,27 +116,9 @@ pub fn build(b: *Build) !void {
const wpt_step = b.step("wpt", "WPT tests");
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);
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 = .{
.target = target,
.optimize = mod.optimize.?,
.prebuilt_v8_path = prebuilt_v8_path,
.cache_root = b.pathFromRoot(".lp-cache"),
};
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");
v8_mod.addOptions("default_exports", v8_opts);
mod.addImport("v8", v8_mod);
const release_dir = if (mod.optimize.? == .Debug) "debug" else "release";
const os = switch (target.result.os.tag) {
.linux => "linux",
.macos => "macos",
else => return error.UnsupportedPlatform,
};
var lib_path = try std.fmt.allocPrint(
mod.owner.allocator,
"v8/out/{s}/{s}/obj/zig/libc_v8.a",
.{ os, release_dir },
);
std.fs.cwd().access(lib_path, .{}) catch {
// legacy path
lib_path = try std.fmt.allocPrint(
mod.owner.allocator,
"v8/out/{s}/obj/zig/libc_v8.a",
.{release_dir},
);
};
mod.addObjectFile(mod.owner.path(lib_path));
switch (target.result.os.tag) {
.macos => {
// v8 has a dependency, abseil-cpp, which, on Mac, uses CoreFoundation
mod.addSystemFrameworkPath(.{ .cwd_relative = "/System/Library/Frameworks" });
mod.linkFramework("CoreFoundation", .{});
},
else => {},
}
}
{
@@ -374,14 +313,27 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo
mod.addCMacro("STDC_HEADERS", "1");
mod.addCMacro("TIME_WITH_SYS_TIME", "1");
mod.addCMacro("USE_NGHTTP2", "1");
mod.addCMacro("USE_MBEDTLS", "1");
mod.addCMacro("USE_OPENSSL", "1");
mod.addCMacro("OPENSSL_IS_BORINGSSL", "1");
mod.addCMacro("USE_THREADS_POSIX", "1");
mod.addCMacro("USE_UNIX_SOCKETS", "1");
}
try buildZlib(b, mod);
try buildBrotli(b, mod);
try buildMbedtls(b, mod);
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 buildCurl(b, mod);
try buildAda(b, mod);
@@ -842,8 +794,9 @@ fn buildCurl(b: *Build, m: *Build.Module) !void {
root ++ "lib/vauth/spnego_sspi.c",
root ++ "lib/vauth/vauth.c",
root ++ "lib/vtls/cipher_suite.c",
root ++ "lib/vtls/mbedtls.c",
root ++ "lib/vtls/mbedtls_threadlock.c",
root ++ "lib/vtls/openssl.c",
root ++ "lib/vtls/hostcheck.c",
root ++ "lib/vtls/keylog.c",
root ++ "lib/vtls/vtls.c",
root ++ "lib/vtls/vtls_scache.c",
root ++ "lib/vtls/x509asn1.c",
@@ -881,3 +834,28 @@ pub fn buildAda(b: *Build, m: *Build.Module) !void {
// 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,17 +1,22 @@
.{
.name = .browser,
.paths = .{""},
.version = "0.0.0",
.fingerprint = 0xda130f3af836cea0,
.fingerprint = 0xda130f3af836cea0, // Changing this has security and trust implications.
.minimum_zig_version = "0.15.2",
.dependencies = .{
.v8 = .{
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/84cdca7cd9065f67c7933388f2091810fc485bc6.tar.gz",
.hash = "v8-0.0.0-xddH67vcAwCuN2gBsAO8TBzEw523KMroIKGrdZwc-Q-y",
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/e047d2a4d5af5783763f0f6a652fab8982a08603.tar.gz",
.hash = "v8-0.0.0-xddH65gMBACRBQMM7EwmVgfi94FJyyX-0jpe5KhXYhfv",
},
//.v8 = .{ .path = "../zig-v8-fork" }
// .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",
},
},
.paths = .{""},
}

View File

@@ -100,6 +100,11 @@ fn getContentType(file_path: []const u8) []const u8 {
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")) {
return "text/html";
}

View File

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

View File

@@ -392,6 +392,15 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
.stack = self.page.js.stackTrace() catch "???",
});
// It's possible, but unlikely, for client.request to immediately finish
// a request, thus calling our callback. We generally don't want a call
// from v8 (which is why we're here), to result in a new script evaluation.
// So we block even the slightest change that `client.request` immediately
// executes a callback.
const was_evaluating = self.is_evaluating;
self.is_evaluating = true;
defer self.is_evaluating = was_evaluating;
try self.client.request(.{
.url = url,
.method = .GET,

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

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

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

@@ -319,9 +319,67 @@ pub const Document = struct {
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");
test "Browser: DOM.Document" {
try testing.htmlRunner("dom/document.html");
}
test "Browser: DOM.Document.write" {
try testing.htmlRunner("dom/document_write.html");
}

View File

@@ -95,6 +95,7 @@ pub const EventTarget = struct {
// --------
pub fn constructor(page: *Page) !*parser.EventTarget {
const et = try page.arena.create(EventTarget);
et.* = .{};
return @ptrCast(&et.base);
}

View File

@@ -390,7 +390,23 @@ pub const Node = struct {
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 {
// 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;
var list: NodeList = .{};

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

@@ -40,6 +40,7 @@ const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
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
pub const Interfaces = .{
@@ -53,6 +54,7 @@ pub const Interfaces = .{
PopStateEvent,
CompositionEvent,
NavigationCurrentEntryChangeEvent,
PageTransitionEvent,
};
pub const Union = generate.Union(Interfaces);
@@ -85,6 +87,7 @@ pub const Event = struct {
.navigation_current_entry_change_event => .{
.NavigationCurrentEntryChangeEvent = @as(*NavigationCurrentEntryChangeEvent, @ptrCast(evt)).*,
},
.page_transition_event => .{ .PageTransitionEvent = @as(*PageTransitionEvent, @ptrCast(evt)).* },
};
}

View File

@@ -254,17 +254,13 @@ pub fn _json(self: *Response, page: *Page) !js.Promise {
self.body_used = true;
if (self.body) |body| {
const p = std.json.parseFromSliceLeaky(
std.json.Value,
page.call_arena,
body,
.{},
) catch |e| {
const value = js.Value.fromJson(page.js, body) catch |e| {
log.info(.browser, "invalid json", .{ .err = e, .source = "Request" });
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);
}

View File

@@ -179,17 +179,13 @@ pub fn _json(self: *Response, page: *Page) !js.Promise {
if (self.body) |body| {
self.body_used = true;
const p = std.json.parseFromSliceLeaky(
std.json.Value,
page.call_arena,
body,
.{},
) catch |e| {
const value = js.Value.fromJson(page.js, body) catch |e| {
log.info(.browser, "invalid json", .{ .err = e, .source = "Response" });
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);
}

View File

@@ -48,7 +48,7 @@ pub fn set_scrollRestoration(self: *History, mode: ScrollRestorationMode) void {
}
pub fn get_state(_: *History, page: *Page) !?js.Value {
if (page.session.navigation.currentEntry().state) |state| {
if (page.session.navigation.currentEntry().state.value) |state| {
const value = try js.Value.fromJson(page.js, state);
return value;
} else {
@@ -61,18 +61,15 @@ pub fn _pushState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
const json = state.toJson(arena) catch return error.DataClone;
_ = try page.session.navigation.pushEntry(url, json, page, true);
_ = try page.session.navigation.pushEntry(url, .{ .source = .history, .value = json }, page, true);
}
pub fn _replaceState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
const arena = page.session.arena;
const entry = page.session.navigation.currentEntry();
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.state = json;
entry.url = url;
const json = try state.toJson(arena);
_ = try page.session.navigation.replaceEntry(url, .{ .source = .history, .value = json }, page, true);
}
pub fn go(_: *const History, delta: i32, page: *Page) !void {
@@ -89,7 +86,7 @@ pub fn go(_: *const History, delta: i32, page: *Page) !void {
if (entry.url) |url| {
if (try page.isSameOrigin(url)) {
PopStateEvent.dispatch(entry.state, page);
PopStateEvent.dispatch(entry.state.value, page);
}
}

View File

@@ -32,6 +32,10 @@ const DataSet = @import("DataSet.zig");
const StyleSheet = @import("../cssom/StyleSheet.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
pub const Interfaces = .{
@@ -487,6 +491,29 @@ pub const HTMLCanvasElement = struct {
pub const Self = parser.Canvas;
pub const prototype = *HTMLElement;
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 {
@@ -1200,11 +1227,22 @@ pub const HTMLTemplateElement = struct {
pub const subtype = .node;
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| {
return tc;
}
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;
return tc;
}
@@ -1356,3 +1394,7 @@ test "Browser: HTML.HtmlScriptElement" {
test "Browser: HTML.HtmlSlotElement" {
try testing.htmlRunner("html/slot.html");
}
test "Browser: HTML.HTMLCanvasElement" {
try testing.htmlRunner("html/canvas.html");
}

View File

@@ -56,7 +56,7 @@ pub const Location = struct {
break :blk try std.fmt.allocPrint(page.arena, "#{s}", .{hash});
};
return page.navigateFromWebAPI(normalized_hash, .{ .reason = .script }, .replace);
return page.navigateFromWebAPI(normalized_hash, .{ .reason = .script }, .{ .replace = null });
}
pub fn get_protocol(self: *Location) []const u8 {
@@ -96,7 +96,7 @@ pub const Location = struct {
}
pub fn _replace(_: *const Location, url: []const u8, page: *Page) !void {
return page.navigateFromWebAPI(url, .{ .reason = .script }, .replace);
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .replace = null });
}
pub fn _reload(_: *const Location, page: *Page) !void {

View File

@@ -14,6 +14,7 @@ const types = @import("types.zig");
const Caller = @import("Caller.zig");
const NamedFunction = Caller.NamedFunction;
const PersistentObject = v8.Persistent(v8.Object);
const PersistentValue = v8.Persistent(v8.Value);
const PersistentModule = v8.Persistent(v8.Module);
const PersistentPromise = v8.Persistent(v8.Promise);
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.
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
// require for this PromiseResolver to be valid for a lifetime longer than
// the function that resolves/rejects them.
@@ -149,6 +153,10 @@ pub fn deinit(self: *Context) void {
p.deinit();
}
for (self.js_value_list.items) |*p| {
p.deinit();
}
for (self.persisted_promise_resolvers.items) |*p| {
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) {
if (cacheable) {
if (self.module_cache.get(url)) |entry| {
// The dynamic import will create an entry without the
// module to prevent multiple calls from asynchronously
// loading the same module. If we're here, without the
// module, then it's time to load it.
if (entry.module != null) {
return if (comptime want_result) entry else {};
const mod, const owned_url = blk: {
const arena = self.arena;
// gop will _always_ initiated if cacheable == true
var gop: std.StringHashMapUnmanaged(ModuleEntry).GetOrPutResult = undefined;
if (cacheable) {
gop = try self.module_cache.getOrPut(arena, url);
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;
const owned_url = try arena.dupe(u8, url);
if (cacheable) {
// 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);
errdefer _ = self.module_identifier.remove(m.getIdentityHash());
gop.value_ptr.module = PersistentModule.init(self.isolate, m);
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;
{
// 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.?.preloadImport(owned_specifier, url);
}
}
}
if (try m.instantiate(v8_context, resolveModuleCallback) == false) {
if (try mod.instantiate(v8_context, resolveModuleCallback) == false) {
return error.ModuleInstantiationError;
}
const evaluated = m.evaluate(v8_context) catch {
std.debug.assert(m.getStatus() == .kErrored);
const evaluated = mod.evaluate(v8_context) catch {
std.debug.assert(mod.getStatus() == .kErrored);
// Some module-loading errors aren't handled by TryCatch. We need to
// get the error from the module itself.
log.warn(.js, "evaluate module", .{
.specifier = owned_url,
.message = self.valueToString(m.getException(), .{}) catch "???",
.message = self.valueToString(mod.getException(), .{}) catch "???",
});
return error.EvaluationError;
};
@@ -301,28 +300,46 @@ pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url:
// be cached
std.debug.assert(cacheable);
const persisted_module = PersistentModule.init(self.isolate, m);
const persisted_promise = PersistentPromise.init(self.isolate, .{ .handle = evaluated.handle });
// entry has to have been created atop this function
const entry = self.module_cache.getPtr(owned_url).?;
var gop = try self.module_cache.getOrPut(arena, owned_url);
if (gop.found_existing) {
// If we're here, it's because we had a cache entry, but no
// 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);
// and the module must have been set after we compiled it
std.debug.assert(entry.module != null);
std.debug.assert(entry.module_promise == null);
gop.value_ptr.module = persisted_module;
gop.value_ptr.module_promise = persisted_promise;
} else {
gop.value_ptr.* = ModuleEntry{
.module = persisted_module,
.module_promise = persisted_promise,
.resolver_promise = null,
};
entry.module_promise = PersistentPromise.init(self.isolate, .{ .handle = evaluated.handle });
return if (comptime want_result) entry.* else {};
}
// After we compile a module, whether it's a top-level one, or a nested one,
// we always want to track its identity (so that, if this module imports other
// 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 ==
@@ -400,9 +417,8 @@ pub fn zigValueToJs(self: *Context, value: anytype) !v8.Value {
},
.pointer => |ptr| switch (ptr.size) {
.one => {
const type_name = @typeName(ptr.child);
if (@hasField(types.Lookup, type_name)) {
const template = self.templates[@field(types.LOOKUP, type_name)];
if (types.has(ptr.child)) {
const template = self.templates[types.getId(ptr.child)];
const js_obj = try self.mapZigInstanceToJs(template, value);
return js_obj.toValue();
}
@@ -436,9 +452,8 @@ pub fn zigValueToJs(self: *Context, value: anytype) !v8.Value {
else => {},
},
.@"struct" => |s| {
const type_name = @typeName(T);
if (@hasField(types.Lookup, type_name)) {
const template = self.templates[@field(types.LOOKUP, type_name)];
if (types.has(T)) {
const template = self.templates[types.getId(T)];
const js_obj = try self.mapZigInstanceToJs(template, value);
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.
// See the TaggedAnyOpaque struct for more details.
const tao = try arena.create(TaggedAnyOpaque);
const meta_index = @field(types.LOOKUP, @typeName(ptr.child));
const meta = self.meta_lookup[meta_index];
const meta = self.meta_lookup[types.getId(ptr.child)];
tao.* = .{
.ptr = value,
@@ -655,7 +669,7 @@ pub fn jsValueToZig(self: *Context, comptime named_function: NamedFunction, comp
if (!js_value.isObject()) {
return error.InvalidArgument;
}
if (@hasField(types.Lookup, @typeName(ptr.child))) {
if (types.has(ptr.child)) {
const js_obj = js_value.castTo(v8.Object);
return self.typeTaggedAnyOpaque(named_function, *types.Receiver(ptr.child), js_obj);
}
@@ -771,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
// 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 {
if (T == js.Function) {
if (!js_value.isFunction()) {
return null;
}
return try self.createFunction(js_value);
}
return switch (T) {
js.Function => {
if (!js_value.isFunction()) {
return null;
}
if (@hasDecl(T, "_TYPED_ARRAY_ID_KLUDGE")) {
const VT = @typeInfo(std.meta.fieldInfo(T, .values).type).pointer.child;
const arr = (try self.jsValueToTypedArray(VT, js_value)) orelse return null;
return .{ .values = arr };
}
if (T == js.String) {
return .{ .string = try self.valueToString(js_value, .{ .allocator = self.arena }) };
}
const js_obj = js_value.castTo(v8.Object);
if (comptime T == js.Object) {
return try self.createFunction(js_value);
},
// zig fmt: off
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),
// zig fmt: on
=> {
const ValueType = @typeInfo(std.meta.fieldInfo(T, .values).type).pointer.child;
const slice = (try self.jsValueToTypedArray(ValueType, js_value)) orelse return null;
return .{ .values = slice };
},
js.String => .{ .string = try self.valueToString(js_value, .{ .allocator = self.arena }) },
// Caller wants an opaque js.Object. Probably a parameter
// that it needs to pass back into a callback
return js.Object{
.js_obj = js_obj,
// that it needs to pass back into a callback.
js.Object => js.Object{
.js_obj = js_value.castTo(v8.Object),
.context = self,
};
}
},
else => {
const js_obj = js_value.castTo(v8.Object);
if (!js_value.isObject()) {
return null;
}
if (!js_value.isObject()) {
return null;
}
const v8_context = self.v8_context;
const isolate = self.isolate;
var value: T = undefined;
inline for (@typeInfo(T).@"struct".fields) |field| {
const name = field.name;
const key = v8.String.initUtf8(isolate, name);
if (js_obj.has(v8_context, key.toValue())) {
@field(value, name) = try self.jsValueToZig(named_function, field.type, try js_obj.getValue(v8_context, key));
} else if (@typeInfo(field.type) == .optional) {
@field(value, name) = null;
} else {
const dflt = field.defaultValue() orelse return null;
@field(value, name) = dflt;
}
}
return value;
const v8_context = self.v8_context;
const isolate = self.isolate;
var value: T = undefined;
inline for (@typeInfo(T).@"struct".fields) |field| {
const name = field.name;
const key = v8.String.initUtf8(isolate, name);
if (js_obj.has(v8_context, key.toValue())) {
@field(value, name) = try self.jsValueToZig(named_function, field.type, try js_obj.getValue(v8_context, key));
} else if (@typeInfo(field.type) == .optional) {
@field(value, name) = null;
} else {
const dflt = field.defaultValue() orelse return null;
@field(value, name) = dflt;
}
}
return value;
},
};
}
fn jsValueToTypedArray(_: *Context, comptime T: type, js_value: v8.Value) !?[]T {
@@ -1189,31 +1203,14 @@ fn _resolveModuleCallback(self: *Context, referrer: v8.Module, specifier: []cons
};
const normalized_specifier = try self.script_manager.?.resolveSpecifier(
self.arena, // might need to survive until the module is loaded
self.arena,
specifier,
referrer_path,
);
const gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
if (gop.found_existing) {
if (gop.value_ptr.module) |m| {
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.?.preloadImport(normalized_specifier, referrer_path);
const entry = self.module_cache.getPtr(normalized_specifier).?;
if (entry.module) |m| {
return m.castToModule().handle;
}
var source = try self.script_manager.?.waitForImport(normalized_specifier);
@@ -1223,26 +1220,10 @@ fn _resolveModuleCallback(self: *Context, referrer: v8.Module, specifier: []cons
try_catch.init(self);
defer try_catch.deinit();
const entry = self.module(true, source.src(), normalized_specifier, true) catch |err| {
switch (err) {
error.EvaluationError => {
// This is a sentinel value telling us that the error was already
// 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;
const mod = try compileModule(self.isolate, source.src(), normalized_specifier);
try self.postCompileModule(mod, normalized_specifier);
entry.module = PersistentModule.init(self.isolate, mod);
return entry.module.?.castToModule().handle;
}
// Will get passed to ScriptManager and then passed back to us when
@@ -1317,7 +1298,32 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c
// `dynamicModuleSourceCallback`, but we can skip some steps
// since the module is alrady loaded,
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
// tries to load this module, it can just return our promise
@@ -1454,14 +1460,13 @@ pub fn typeTaggedAnyOpaque(self: *const Context, comptime named_function: NamedF
return error.InvalidArgument;
}
const type_name = @typeName(T);
if (@hasField(types.Lookup, type_name) == false) {
if (!types.has(T)) {
@compileError(named_function.full_name ++ "has an unknown Zig type: " ++ @typeName(R));
}
const op = js_obj.getInternalField(0).castTo(v8.External).get();
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;
if (type_index == expected_type_index) {
@@ -1489,7 +1494,7 @@ pub fn typeTaggedAnyOpaque(self: *const Context, comptime named_function: NamedF
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) {
return @ptrFromInt(base_ptr + total_offset);
}
@@ -1582,7 +1587,7 @@ fn probeJsValueToZig(self: *Context, comptime named_function: NamedFunction, com
if (!js_value.isObject()) {
return .{ .invalid = {} };
}
if (@hasField(types.Lookup, @typeName(ptr.child))) {
if (types.has(ptr.child)) {
const js_obj = js_value.castTo(v8.Object);
// There's a bit of overhead in doing this, so instead
// 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().?;
if (@hasDecl(Struct, "prototype")) {
const TI = @typeInfo(Struct.prototype);
const proto_name = @typeName(types.Receiver(TI.pointer.child));
if (@hasField(types.Lookup, proto_name) == false) {
@compileError(std.fmt.comptimePrint("Prototype '{s}' for '{s}' is undefined", .{ proto_name, @typeName(Struct) }));
const ProtoType = types.Receiver(TI.pointer.child);
if (!types.has(ProtoType)) {
@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
// template index.
const proto_index = @field(types.LOOKUP, proto_name);
templates[i].inherit(templates[proto_index]);
templates[i].inherit(templates[types.getId(ProtoType)]);
}
// 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
// specific instance of the the Window.
if (@hasDecl(Global, "prototype")) {
const proto_type = types.Receiver(@typeInfo(Global.prototype).pointer.child);
const proto_name = @typeName(proto_type);
const proto_index = @field(types.LOOKUP, proto_name);
js_global.inherit(templates[proto_index]);
const ProtoType = types.Receiver(@typeInfo(Global.prototype).pointer.child);
js_global.inherit(templates[types.getId(ProtoType)]);
}
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().?;
if (@hasDecl(Struct, "prototype")) {
const proto_type = types.Receiver(@typeInfo(Struct.prototype).pointer.child);
const proto_name = @typeName(proto_type);
if (@hasField(types.Lookup, proto_name) == false) {
@compileError("Type '" ++ @typeName(Struct) ++ "' defines an unknown prototype: " ++ proto_name);
const ProtoType = types.Receiver(@typeInfo(Struct.prototype).pointer.child);
if (!types.has(ProtoType)) {
@compileError("Type '" ++ @typeName(Struct) ++ "' defines an unknown prototype: " ++ @typeName(ProtoType));
}
const proto_index = @field(types.LOOKUP, proto_name);
const proto_obj = templates[proto_index].getFunction(v8_context).toObject();
const proto_obj = templates[types.getId(ProtoType)].getFunction(v8_context).toObject();
const self_obj = templates[i].getFunction(v8_context).toObject();
_ = self_obj.setPrototype(v8_context, proto_obj);

View File

@@ -48,8 +48,6 @@ const NamedFunction = Context.NamedFunction;
// Env.JsObject. Want a TypedArray? Env.TypedArray.
pub fn TypedArray(comptime T: type) type {
return struct {
pub const _TYPED_ARRAY_ID_KLUDGE = true;
values: []const T,
pub fn dupe(self: TypedArray(T), allocator: Allocator) !TypedArray(T) {
@@ -150,6 +148,8 @@ pub const Exception = struct {
};
pub const Value = struct {
const PersistentValue = v8.Persistent(v8.Value);
value: v8.Value,
context: *const Context,
@@ -163,6 +163,15 @@ pub const Value = struct {
const value = try v8.Json.parse(ctx.v8_context, json_string);
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 {
@@ -327,68 +336,73 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo
return v8.initNull(isolate).toValue();
},
.@"struct" => {
const T = @TypeOf(value);
if (T == ArrayBuffer) {
const values = value.values;
const len = values.len;
var array_buffer: v8.ArrayBuffer = undefined;
const backing_store = v8.BackingStore.init(isolate, len);
const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData()));
@memcpy(data[0..len], @as([]const u8, @ptrCast(values))[0..len]);
array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr());
return .{ .handle = array_buffer.handle };
}
if (@hasDecl(T, "_TYPED_ARRAY_ID_KLUDGE")) {
const values = value.values;
const value_type = @typeInfo(@TypeOf(values)).pointer.child;
const len = values.len;
const bits = switch (@typeInfo(value_type)) {
.int => |n| n.bits,
.float => |f| f.bits,
else => @compileError("Invalid TypeArray type: " ++ @typeName(value_type)),
};
var array_buffer: v8.ArrayBuffer = undefined;
if (len == 0) {
array_buffer = v8.ArrayBuffer.init(isolate, 0);
} else {
const buffer_len = len * bits / 8;
const backing_store = v8.BackingStore.init(isolate, buffer_len);
switch (@TypeOf(value)) {
ArrayBuffer => {
const values = value.values;
const len = values.len;
var array_buffer: v8.ArrayBuffer = undefined;
const backing_store = v8.BackingStore.init(isolate, len);
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());
}
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(),
return .{ .handle = array_buffer.handle };
},
// zig fmt: off
TypedArray(u8), TypedArray(u16), TypedArray(u32), TypedArray(u64),
TypedArray(i8), TypedArray(i16), TypedArray(i32), TypedArray(i64),
TypedArray(f32), TypedArray(f64),
// 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 => {},
},
.signed => switch (n.bits) {
8 => return v8.Int8Array.init(array_buffer, 0, len).toValue(),
16 => return v8.Int16Array.init(array_buffer, 0, len).toValue(),
32 => return v8.Int32Array.init(array_buffer, 0, len).toValue(),
64 => return v8.BigInt64Array.init(array_buffer, 0, len).toValue(),
else => {},
},
},
.float => |f| switch (f.bits) {
32 => return v8.Float32Array.init(array_buffer, 0, len).toValue(),
64 => return v8.Float64Array.init(array_buffer, 0, len).toValue(),
else => {},
},
else => {},
}
// We normally don't fail in this function unless fail == true
// but this can never be valid.
@compileError("Invalid TypeArray type: " ++ @typeName(value_type));
}
// We normally don't fail in this function unless fail == true
// but this can never be valid.
@compileError("Invalid TypedArray type: " ++ @typeName(value_type));
},
else => {},
}
},
.@"union" => return simpleZigValueToJs(isolate, std.meta.activeTag(value), fail),

View File

@@ -18,6 +18,7 @@ const Interfaces = generate.Tuple(.{
@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("../xmlserializer/xmlserializer.zig").Interfaces,
@import("../fetch/fetch.zig").Interfaces,
@@ -26,104 +27,127 @@ const Interfaces = generate.Tuple(.{
pub const Types = @typeInfo(Interfaces).@"struct".fields;
// Imagine we have a type Cat which has a getter:
//
// fn get_owner(self: *Cat) *Owner {
// return self.owner;
// }
//
// When we execute caller.getter, we'll end up doing something like:
// const res = @call(.auto, Cat.get_owner, .{cat_instance});
//
// How do we turn `res`, which is an *Owner, into something we can return
// to v8? We need the ObjectTemplate associated with Owner. How do we
// get that? Well, we store all the ObjectTemplates in an array that's
// tied to env. So we do something like:
//
// env.templates[index_of_owner].initInstance(...);
//
// But how do we get that `index_of_owner`? `Lookup` is a struct
// that looks like:
//
// const Lookup = struct {
// comptime cat: usize = 0,
// comptime owner: usize = 1,
// ...
// }
//
// So to get the template index of `owner`, we can do:
//
// const index_id = @field(type_lookup, @typeName(@TypeOf(res));
//
pub const Lookup = blk: {
var fields: [Types.len]std.builtin.Type.StructField = undefined;
/// 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));
/// Imagine we have a type `Cat` which has a getter:
///
/// fn get_owner(self: *Cat) *Owner {
/// return self.owner;
/// }
///
/// When we execute `caller.getter`, we'll end up doing something like:
///
/// const res = @call(.auto, Cat.get_owner, .{cat_instance});
///
/// How do we turn `res`, which is an *Owner, into something we can return
/// to v8? We need the ObjectTemplate associated with Owner. How do we
/// get that? Well, we store all the ObjectTemplates in an array that's
/// tied to env. So we do something like:
///
/// env.templates[index_of_owner].initInstance(...);
///
/// But how do we get that `index_of_owner`? `Index` is an enum
/// that looks like:
///
/// pub const Index = enum(BackingInt) {
/// cat = 0,
/// owner = 1,
/// ...
/// }
///
/// (`BackingInt` is calculated at comptime regarding to interfaces we have)
/// 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| {
// 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().?;
if (@hasDecl(Struct, "prototype") and @typeInfo(Struct.prototype) != .pointer) {
@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,
};
fields[i] = .{ .name = @typeName(Receiver(Struct)), .value = i };
}
break :blk @Type(.{ .@"struct" = .{
.layout = .auto,
.decls = &.{},
.is_tuple = false,
.fields = &fields,
} });
break :blk @Type(.{
.@"enum" = .{
.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
// const Animal = struct{};
// const Cat = struct{
// pub const prototype = *Animal;
// };
//
// Would create an array: [0, 0]
// 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 0.
//
// 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 PROTOTYPE_TABLE to see if the TaggedAnyType should be
// cast to a prototype.
pub const PROTOTYPE_TABLE = blk: {
var table: [Types.len]u16 = undefined;
/// Returns the `Index` for the given type.
pub inline fn getIndex(t: type) Index {
return @field(Index, @typeName(t));
}
/// Returns the ID for the given type.
pub inline fn getId(t: type) BackingInt {
return @intFromEnum(getIndex(t));
}
/// Creates a list where the index of a type contains its prototype index.
/// const Animal = struct{};
/// const Cat = struct{
/// pub const prototype = *Animal;
/// };
///
/// 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| {
var prototype_index = i;
const Struct = s.defaultValue().?;
if (@hasDecl(Struct, "prototype")) {
const TI = @typeInfo(Struct.prototype);
const proto_name = @typeName(Receiver(TI.pointer.child));
prototype_index = @field(LOOKUP, proto_name);
}
table[i] = prototype_index;
table[i] = proto_index: {
if (@hasDecl(Struct, "prototype")) {
const prototype_field = @field(Struct, "prototype");
// 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.
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;
};
// This is essentially meta data for each type. Each is stored in env.meta_lookup
// The index for a type can be retrieved via:
// const index = @field(TYPE_LOOKUP, @typeName(Receiver(Struct)));
// const meta = env.meta_lookup[index];
/// This is essentially meta data for each type. Each is stored in `env.meta_lookup`.
/// The index for a type can be retrieved via:
/// const index = types.getIndex(Receiver(Struct));
/// const meta = env.meta_lookup[@intFromEnum(index)];
///
/// Or:
/// const id = types.getId(Receiver(Struct));
/// const meta = env.meta_lookup[id];
pub const Meta = struct {
// Every type is given a unique index. That index is used to lookup various
// 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
// 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.
// We keep 41 for null-termination since HTML parser expects in this format.
charset: [41]u8 = default_charset,
charset_len: usize = 5,
/// String "UTF-8" continued by null characters.
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 },
};
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.
pub fn charsetString(mime: *const Mime) [:0]const u8 {
return @ptrCast(&mime.charset);
pub fn charsetStringZ(mime: *const Mime) [:0]const u8 {
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.
@@ -99,6 +116,7 @@ pub const Mime = struct {
const params = trimLeft(normalized[type_len..]);
var charset: [41]u8 = undefined;
var charset_len: usize = undefined;
var it = std.mem.splitScalar(u8, params, ';');
while (it.next()) |attr| {
@@ -124,6 +142,7 @@ pub const Mime = struct {
@memcpy(charset[0..attribute_value.len], attribute_value);
// Null-terminate right after attribute value.
charset[attribute_value.len] = 0;
charset_len = attribute_value.len;
},
}
}
@@ -131,6 +150,7 @@ pub const Mime = struct {
return .{
.params = params,
.charset = charset,
.charset_len = charset_len,
.content_type = content_type,
};
}
@@ -511,9 +531,9 @@ fn expect(expected: Expectation, input: []const u8) !void {
if (expected.charset) |ec| {
// 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 {
const m: Mime = .unknown;
try testing.expectEqual(m.charsetString(), actual.charsetString());
try testing.expectEqual(m.charsetStringZ(), actual.charsetStringZ());
}
}

View File

@@ -35,6 +35,7 @@ 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");
@@ -110,10 +111,10 @@ pub fn _forward(self: *Navigation, page: *Page) !NavigationReturn {
pub fn updateEntries(self: *Navigation, url: []const u8, kind: NavigationKind, page: *Page, dispatch: bool) !void {
switch (kind) {
.replace => {
_ = try self.replaceEntry(url, null, page, dispatch);
_ = try self.replaceEntry(url, .{ .source = .navigation, .value = null }, page, dispatch);
},
.push => |state| {
_ = try self.pushEntry(url, state, page, dispatch);
_ = try self.pushEntry(url, .{ .source = .navigation, .value = state }, page, dispatch);
},
.traverse => |index| {
self.index = index;
@@ -132,7 +133,13 @@ pub fn processNavigation(self: *Navigation, page: *Page) !void {
/// 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: ?[]const u8, page: *Page, dispatch: bool) !*NavigationHistoryEntry {
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);
@@ -160,18 +167,24 @@ pub fn pushEntry(self: *Navigation, _url: []const u8, state: ?[]const u8, page:
// 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);
}
}
self.index = index;
return entry;
}
pub fn replaceEntry(self: *Navigation, _url: []const u8, state: ?[]const u8, page: *Page, dispatch: bool) !*NavigationHistoryEntry {
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);
@@ -184,7 +197,7 @@ pub fn replaceEntry(self: *Navigation, _url: []const u8, state: ?[]const u8, pag
const entry = try arena.create(NavigationHistoryEntry);
entry.* = NavigationHistoryEntry{
.id = id_str,
.key = id_str,
.key = previous.key,
.url = url,
.state = state,
};
@@ -242,7 +255,20 @@ pub fn navigate(
// todo: Fire navigate event
try finished.resolve({});
_ = try self.pushEntry(url, state, page, true);
_ = 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);
}
@@ -263,7 +289,6 @@ pub fn navigate(
.reload => {
try page.navigateFromWebAPI(url, .{ .reason = .navigation }, kind);
},
else => unreachable,
}
return .{
@@ -275,7 +300,13 @@ pub fn navigate(
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;
return try self.navigate(_url, .{ .push = json }, page);
const kind: NavigationKind = switch (opts.history) {
.replace => .{ .replace = json },
.push, .auto => .{ .push = json },
};
return try self.navigate(_url, kind, page);
}
pub const ReloadOptions = struct {
@@ -290,7 +321,7 @@ pub fn _reload(self: *Navigation, _opts: ?ReloadOptions, page: *Page) !Navigatio
const entry = self.currentEntry();
if (opts.state) |state| {
const previous = entry;
entry.state = state.toJson(arena) catch return error.DataClone;
entry.state = .{ .source = .navigation, .value = state.toJson(arena) catch return error.DataClone };
NavigationCurrentEntryChangeEvent.dispatch(self, previous, .reload);
}
@@ -323,6 +354,6 @@ pub fn _updateCurrentEntry(self: *Navigation, options: UpdateCurrentEntryOptions
const arena = page.session.arena;
const previous = self.currentEntry();
self.currentEntry().state = options.state.toJson(arena) catch return error.DataClone;
self.currentEntry().state = .{ .source = .navigation, .value = options.state.toJson(arena) catch return error.DataClone };
NavigationCurrentEntryChangeEvent.dispatch(self, previous, null);
}

View File

@@ -51,11 +51,16 @@ pub const NavigationType = enum {
pub const NavigationKind = union(NavigationType) {
push: ?[]const u8,
replace,
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;
@@ -64,7 +69,7 @@ pub const NavigationHistoryEntry = struct {
id: []const u8,
key: []const u8,
url: ?[]const u8,
state: ?[]const u8,
state: NavigationState,
pub fn get_id(self: *const NavigationHistoryEntry) []const u8 {
return self.id;
@@ -95,12 +100,16 @@ pub const NavigationHistoryEntry = struct {
return self.url;
}
pub fn _getState(self: *const NavigationHistoryEntry, page: *Page) !?js.Value {
if (self.state) |state| {
return try js.Value.fromJson(page.js, state);
} else {
return null;
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;
}
};

View File

@@ -561,6 +561,7 @@ pub const EventType = enum(u8) {
pop_state = 9,
composition_event = 10,
navigation_current_entry_change_event = 11,
page_transition_event = 12,
};
pub const MutationEvent = c.dom_mutation_event;

View File

@@ -37,6 +37,7 @@ 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 URL = @import("../url.zig").URL;
@@ -82,6 +83,8 @@ pub const Page = struct {
// indicates intention to navigate to another page on the next loop execution.
delayed_navigation: bool = false,
req_id: ?usize = null,
navigated_options: ?NavigatedOpts = null,
state_pool: *std.heap.MemoryPool(State),
@@ -102,6 +105,10 @@ pub const Page = struct {
notified_network_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) {
pre: void,
err: anyerror,
@@ -168,6 +175,9 @@ pub const Page = struct {
self.http_client.abort();
self.script_manager.reset();
parser.deinit();
parser.init();
self.load_state = .parsing;
self.mode = .{ .pre = {} };
_ = self.session.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
@@ -545,11 +555,14 @@ pub const Page = struct {
try self.reset();
}
const req_id = self.http_client.nextReqId();
log.info(.http, "navigate", .{
.url = request_url,
.method = opts.method,
.reason = opts.reason,
.body = opts.body != null,
.req_id = req_id,
});
// if the url is about:blank, we load an empty HTML document in the
@@ -567,22 +580,39 @@ pub const Page = struct {
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;
}
const owned_url = try self.arena.dupeZ(u8, request_url);
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();
if (opts.header) |hdr| try headers.add(hdr);
try self.requestCookie(.{ .is_navigation = true }).headersForRequest(self.arena, owned_url, &headers);
@@ -590,6 +620,7 @@ pub const Page = struct {
// We dispatch page_navigate event before sending the request.
// It ensures the event page_navigated is not dispatched before this one.
self.session.browser.notification.dispatch(.page_navigate, &.{
.req_id = req_id,
.opts = opts,
.url = owned_url,
.timestamp = timestamp(),
@@ -655,13 +686,20 @@ pub const Page = struct {
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, &.{
.req_id = self.req_id.?,
.opts = self.navigated_options.?,
.url = self.url.raw,
.timestamp = timestamp(),
});
}
fn _documentIsComplete(self: *Page) !void {
self.session.browser.runMicrotasks();
self.session.browser.runMessageLoop();
try HTMLDocument.documentIsComplete(self.window.document, self);
// dispatch window.load event
@@ -674,6 +712,8 @@ pub const Page = struct {
parser.toEventTarget(Window, &self.window),
loadevt,
);
PageTransitionEvent.dispatch(&self.window, .show, false);
}
fn pageHeaderDoneCallback(transfer: *Http.Transfer) !void {
@@ -707,14 +747,14 @@ pub const Page = struct {
log.debug(.http, "navigate first chunk", .{ .content_type = mime.content_type, .len = data.len });
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,
.text_javascript,
.text_css,
.text_plain,
=> 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>");
break :blk .{ .text = p };
},
@@ -756,6 +796,9 @@ pub const Page = struct {
var self: *Page = @ptrCast(@alignCast(ctx));
self.clearTransferArena();
// We need to handle different navigation types differently.
try self.session.navigation.processNavigation(self);
switch (self.mode) {
.pre => {
// Received a response without a body like: https://httpbin.io/status/200
@@ -834,9 +877,6 @@ pub const Page = struct {
unreachable;
},
}
// We need to handle different navigation types differently.
try self.session.navigation.processNavigation(self);
}
fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
@@ -914,7 +954,7 @@ pub const Page = struct {
fn windowClicked(node: *parser.EventNode, event: *parser.Event) void {
const self: *Page = @fieldParentPtr("window_clicked_event_node", node);
self._windowClicked(event) catch |err| {
log.err(.browser, "click handler error", .{ .err = err });
log.err(.input, "click handler error", .{ .err = err });
};
}
@@ -926,18 +966,22 @@ pub const Page = struct {
.a => {
const element: *parser.Element = @ptrCast(node);
const href = (try parser.elementGetAttribute(element, "href")) orelse return;
log.debug(.input, "window click on link", .{ .tag = tag, .href = href });
try self.navigateFromWebAPI(href, .{}, .{ .push = null });
return;
},
.input => {
const element: *parser.Element = @ptrCast(node);
const input_type = try parser.inputGetType(@ptrCast(element));
if (std.ascii.eqlIgnoreCase(input_type, "submit")) {
log.debug(.input, "window click on submit input", .{ .tag = tag });
return self.elementSubmitForm(element);
}
},
.button => {
const element: *parser.Element = @ptrCast(node);
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")) {
return self.elementSubmitForm(element);
}
@@ -949,6 +993,12 @@ pub const Page = struct {
},
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 {
@@ -991,7 +1041,7 @@ pub const Page = struct {
fn keydownCallback(node: *parser.EventNode, event: *parser.Event) void {
const self: *Page = @fieldParentPtr("keydown_event_node", node);
self._keydownCallback(event) catch |err| {
log.err(.browser, "keydown handler error", .{ .err = err });
log.err(.input, "keydown handler error", .{ .err = err });
};
}
@@ -1005,23 +1055,29 @@ pub const Page = struct {
if (std.mem.eql(u8, new_key, "Dead")) {
return;
}
switch (tag) {
.input => {
const element: *parser.Element = @ptrCast(node);
const input_type = try parser.inputGetType(@ptrCast(element));
if (std.mem.eql(u8, input_type, "text")) {
if (std.mem.eql(u8, new_key, "Enter")) {
const form = (try self.formForElement(element)) orelse return;
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);
log.debug(.input, "key down on input", .{ .tag = tag, .key = new_key, .input_type = input_type });
if (std.mem.eql(u8, new_key, "Enter")) {
const form = (try self.formForElement(element)) orelse return;
return self.submitForm(@ptrCast(form), null);
}
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 => {
log.debug(.input, "key down on textarea", .{ .tag = tag, .key = new_key });
const value = try parser.textareaGetValue(@ptrCast(node));
if (std.mem.eql(u8, new_key, "Enter")) {
new_key = "\n";
@@ -1029,7 +1085,9 @@ pub const Page = struct {
const new_value = try std.mem.concat(self.arena, u8, &.{ value, new_key });
try parser.textareaSetValue(@ptrCast(node), new_value);
},
else => {},
else => {
log.debug(.input, "key down event", .{ .tag = tag, .key = new_key });
},
}
}
@@ -1216,6 +1274,10 @@ pub const Page = struct {
const current_origin = try self.origin(self.call_arena);
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 {
@@ -1236,6 +1298,12 @@ pub const NavigateOpts = struct {
force: bool = false,
};
pub const NavigatedOpts = struct {
cdp_id: ?i64 = null,
reason: NavigateReason = .address_bar,
method: Http.Method = .GET,
};
const IdleNotification = union(enum) {
// hasn't started yet.
init,

View File

@@ -31,6 +31,7 @@ const Mime = @import("../mime.zig").Mime;
const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page;
const Http = @import("../../http/Http.zig");
const js = @import("../js/js.zig");
// XHR interfaces
// https://xhr.spec.whatwg.org/#interface-xmlhttprequest
@@ -128,21 +129,19 @@ pub const XMLHttpRequest = struct {
JSON,
};
const JSONValue = std.json.Value;
const Response = union(ResponseType) {
Empty: void,
Text: []const u8,
ArrayBuffer: void,
Blob: void,
Document: *parser.Document,
JSON: JSONValue,
JSON: js.Value,
};
const ResponseObj = union(enum) {
Document: *parser.Document,
Failure: void,
JSON: JSONValue,
JSON: js.Value,
fn deinit(self: ResponseObj) void {
switch (self) {
@@ -605,7 +604,7 @@ pub const XMLHttpRequest = struct {
}
// 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.state == .loading or self.state == .done) {
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
// on thiss received bytes. If that threw an exception, then return
// null.
self.setResponseObjJSON();
self.setResponseObjJSON(page);
}
if (self.response_obj) |obj| {
@@ -678,7 +677,7 @@ pub const XMLHttpRequest = struct {
}
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 = {} };
return;
};
@@ -691,22 +690,24 @@ pub const XMLHttpRequest = struct {
};
}
// setResponseObjJSON parses the received bytes as a std.json.Value.
fn setResponseObjJSON(self: *XMLHttpRequest) void {
// TODO should we use parseFromSliceLeaky if we expect the allocator is
// already an arena?
const p = std.json.parseFromSliceLeaky(
JSONValue,
self.arena,
// setResponseObjJSON parses the received bytes as a js.Value.
fn setResponseObjJSON(self: *XMLHttpRequest, page: *Page) void {
const value = js.Value.fromJson(
page.js,
self.response_bytes.items,
.{},
) catch |e| {
log.warn(.http, "invalid json", .{ .err = e, .url = self.url, .source = "xhr" });
self.response_obj = .{ .Failure = {} };
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 {

View File

@@ -230,6 +230,11 @@ pub fn CDPT(comptime TypeProvider: type) type {
asUint(u88, "Performance") => return @import("domains/performance.zig").processMessage(command),
else => {},
},
13 => switch (@as(u104, @bitCast(domain[0..13].*))) {
asUint(u104, "Accessibility") => return @import("domains/accessibility.zig").processMessage(command),
else => {},
},
else => {},
}
@@ -468,6 +473,14 @@ pub fn BrowserContext(comptime CDP_T: type) type {
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 {
try self.cdp.browser.notification.register(.http_request_fail, self, onHttpRequestFail);
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 {
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 {

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

@@ -44,6 +44,7 @@ pub fn processMessage(cmd: anytype) !void {
grantPermissions,
getWindowForTarget,
setDownloadBehavior,
close,
}, cmd.input.action) orelse return error.UnknownMethod;
switch (action) {
@@ -54,6 +55,7 @@ pub fn processMessage(cmd: anytype) !void {
.grantPermissions => return grantPermissions(cmd),
.getWindowForTarget => return getWindowForTarget(cmd),
.setDownloadBehavior => return setDownloadBehavior(cmd),
.close => return cmd.sendResult(null, .{}),
}
}

View File

@@ -24,6 +24,7 @@ const css = @import("../../browser/dom/css.zig");
const parser = @import("../../browser/netsurf.zig");
const dom_node = @import("../../browser/dom/node.zig");
const Element = @import("../../browser/dom/element.zig").Element;
const dump = @import("../../browser/dump.zig");
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
@@ -41,6 +42,8 @@ pub fn processMessage(cmd: anytype) !void {
getBoxModel,
requestChildNodes,
getFrameOwner,
getOuterHTML,
requestNode,
}, cmd.input.action) orelse return error.UnknownMethod;
switch (action) {
@@ -58,6 +61,8 @@ pub fn processMessage(cmd: anytype) !void {
.getBoxModel => return getBoxModel(cmd),
.requestChildNodes => return requestChildNodes(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 }, .{});
}
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");
test "cdp.dom: getSearchResults unknown search id" {

View File

@@ -22,6 +22,7 @@ const Allocator = std.mem.Allocator;
const CdpStorage = @import("storage.zig");
const Transfer = @import("../../http/Client.zig").Transfer;
const Notification = @import("../../notification.zig").Notification;
const Mime = @import("../../browser/mime.zig").Mime;
pub fn processMessage(cmd: anytype) !void {
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 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
try bc.cdp.sendEvent("Network.requestWillBeSent", .{
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}),
.requestId = loader_id,
.frameId = target_id,
.loaderId = bc.loader_id,
.documentUrl = DocumentUrlWriter.init(&page.url.uri),
.loaderId = loader_id,
.type = msg.transfer.req.resource_type.string(),
.documentURL = DocumentUrlWriter.init(&page.url.uri),
.request = TransferAsRequestWriter.init(transfer),
.initiator = .{ .type = "other" },
.redirectHasExtraInfo = false, // TODO change after adding Network.requestWillBeSentExtraInfo
.hasUserGesture = false,
}, .{ .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 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
try bc.cdp.sendEvent("Network.responseReceived", .{
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{msg.transfer.id}),
.loaderId = bc.loader_id,
.requestId = loader_id,
.frameId = target_id,
.loaderId = loader_id,
.response = TransferAsResponseWriter.init(arena, msg.transfer),
.hasExtraInfo = false, // TODO change after adding Network.responseReceivedExtraInfo
}, .{ .session_id = session_id });
}
@@ -392,6 +401,20 @@ const TransferAsResponseWriter = struct {
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
// common to get these from a server (e.g. for Cache-Control), but

View File

@@ -20,6 +20,7 @@ const std = @import("std");
const Page = @import("../../browser/page.zig").Page;
const timestampF = @import("../../datetime.zig").timestamp;
const Notification = @import("../../notification.zig").Notification;
const log = @import("../../log.zig");
const Allocator = std.mem.Allocator;
@@ -32,6 +33,7 @@ pub fn processMessage(cmd: anytype) !void {
createIsolatedWorld,
navigate,
stopLoading,
close,
}, cmd.input.action) orelse return error.UnknownMethod;
switch (action) {
@@ -42,6 +44,7 @@ pub fn processMessage(cmd: anytype) !void {
.createIsolatedWorld => return createIsolatedWorld(cmd),
.navigate => return navigate(cmd),
.stopLoading => return cmd.sendResult(null, .{}),
.close => return close(cmd),
}
}
@@ -129,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 {
const params = (try cmd.params(struct {
frameId: []const u8,
worldName: []const u8,
grantUniveralAccess: bool,
grantUniveralAccess: bool = false,
})) orelse return error.InvalidParams;
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
// or otherwise access a DOM or other JS Object from another context that should fail.
}
@@ -175,7 +215,6 @@ fn navigate(cmd: anytype) !void {
}
var page = bc.session.currentPage() orelse return error.PageNotLoaded;
bc.loader_id = bc.cdp.loader_id_gen.next();
try page.navigate(params.url, .{
.reason = .address_bar,
@@ -188,8 +227,7 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
// things, but no session.
const session_id = bc.session_id orelse return;
bc.loader_id = bc.cdp.loader_id_gen.next();
const loader_id = bc.loader_id;
const loader_id = try std.fmt.allocPrint(arena, "REQ-{d}", .{event.req_id});
const target_id = bc.target_id orelse unreachable;
bc.reset();
@@ -233,6 +271,30 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
try cdp.sendEvent("Page.frameStartedLoading", .{
.frameId = target_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.
// The result for the Page.navigate seems like it _must_ come after
@@ -259,6 +321,17 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
}, .{ .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) {
try cdp.sendEvent("Page.frameClearedScheduledNavigation", .{
.frameId = target_id,
@@ -292,37 +365,14 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
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
try cdp.sendEvent("Page.frameNavigated", .{
.type = "Navigation",
.frame = Frame{
.id = target_id,
.url = event.url,
.loaderId = bc.loader_id,
.loaderId = loader_id,
.securityOrigin = bc.security_origin,
.secureContextType = bc.secure_context_type,
},

View File

@@ -24,6 +24,7 @@ const LOADER_ID = "LOADERID42AA389647D702B4D805F49A";
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
getTargets,
attachToTarget,
closeTarget,
createBrowserContext,
@@ -38,6 +39,7 @@ pub fn processMessage(cmd: anytype) !void {
}, cmd.input.action) orelse return error.UnknownMethod;
switch (action) {
.getTargets => return getTargets(cmd),
.attachToTarget => return attachToTarget(cmd),
.closeTarget => return closeTarget(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 {
var browser_context_ids: []const []const u8 = undefined;
if (cmd.browser_context) |bc| {
@@ -167,7 +194,7 @@ fn createTarget(cmd: anytype) !void {
.targetInfo = TargetInfo{
.attached = false,
.targetId = target_id,
.title = params.url,
.title = "about:blank",
.browserContextId = bc.id,
.url = "about:blank",
},
@@ -178,9 +205,11 @@ fn createTarget(cmd: anytype) !void {
try doAttachtoTarget(cmd, target_id);
}
try page.navigate(params.url, .{
.reason = .address_bar,
});
if (!std.mem.eql(u8, "about:blank", params.url)) {
try page.navigate(params.url, .{
.reason = .address_bar,
});
}
try cmd.sendResult(.{
.targetId = target_id,
@@ -199,12 +228,10 @@ fn attachToTarget(cmd: anytype) !void {
return error.UnknownTargetId;
}
if (bc.session_id != null) {
return error.SessionAlreadyLoaded;
if (bc.session_id == null) {
try doAttachtoTarget(cmd, target_id);
}
try doAttachtoTarget(cmd, target_id);
return cmd.sendResult(
.{ .sessionId = bc.session_id },
.{ .include_session_id = false },
@@ -269,8 +296,8 @@ fn getTargetInfo(cmd: anytype) !void {
.targetInfo = TargetInfo{
.targetId = target_id,
.type = "page",
.title = "",
.url = "",
.title = bc.getTitle() orelse "about:blank",
.url = bc.getURL() orelse "about:blank",
.attached = true,
.canAccessOpener = false,
},
@@ -281,8 +308,8 @@ fn getTargetInfo(cmd: anytype) !void {
.targetInfo = TargetInfo{
.targetId = "TID-STARTUP-B",
.type = "browser",
.title = "",
.url = "",
.title = "about:blank",
.url = "about:blank",
.attached = true,
.canAccessOpener = false,
},
@@ -628,8 +655,8 @@ test "cdp.target: getTargetInfo" {
try ctx.expectSentResult(.{
.targetInfo = .{
.type = "browser",
.title = "",
.url = "",
.title = "about:blank",
.url = "about:blank",
.attached = true,
.canAccessOpener = false,
},
@@ -662,7 +689,7 @@ test "cdp.target: getTargetInfo" {
.targetId = "TID-A",
.type = "page",
.title = "",
.url = "",
.url = "about:blank",
.attached = true,
.canAccessOpener = false,
},

View File

@@ -261,6 +261,16 @@ pub fn fulfillTransfer(self: *Client, transfer: *Transfer, status: u16, headers:
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 {
errdefer req.headers.deinit();
@@ -273,8 +283,7 @@ fn makeTransfer(self: *Client, req: Request) !*Transfer {
const transfer = try self.transfer_pool.create();
errdefer self.transfer_pool.destroy(transfer);
const id = self.next_request_id + 1;
self.next_request_id = id;
const id = self.incrReqId();
transfer.* = .{
.arena = ArenaAllocator.init(self.allocator),
.id = id,
@@ -679,6 +688,19 @@ pub const Request = struct {
xhr,
script,
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",
};
}
};
};

View File

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

View File

@@ -23,67 +23,42 @@ const Allocator = std.mem.Allocator;
const log = @import("log.zig");
const App = @import("app.zig").App;
const Server = @import("server.zig").Server;
const SigHandler = @import("sighandler.zig").SigHandler;
const Browser = @import("browser/browser.zig").Browser;
const DumpStripMode = @import("browser/dump.zig").Opts.StripMode;
const build_config = @import("build_config");
var _app: ?*App = null;
var _server: ?Server = null;
pub fn main() !void {
// allocator
// - in Debug mode we use the General Purpose Allocator to detect memory leaks
// - in Release mode we use the c allocator
var gpa: std.heap.DebugAllocator(.{}) = .init;
const alloc = if (builtin.mode == .Debug) gpa.allocator() else std.heap.c_allocator;
var gpa_instance: std.heap.DebugAllocator(.{}) = .init;
const gpa = if (builtin.mode == .Debug) gpa_instance.allocator() else std.heap.c_allocator;
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
// the args_arena is gone. We need to set it to something that's not
// invalid. (We should just move the args_arena up to main)
// the arena is gone. We need to set it to something that's not
// invalid. (We should just move the arena up to main)
log.opts.filter_scopes = &.{};
log.fatal(.app, "exit", .{ .err = err });
std.posix.exit(1);
};
}
// Handle app shutdown gracefuly on signals.
fn shutdown() void {
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());
fn run(gpa: Allocator, arena: Allocator, sighandler: *SigHandler) !void {
const args = try parseArgs(arena);
switch (args.mode) {
.help => {
@@ -110,13 +85,13 @@ fn run(alloc: Allocator) !void {
const user_agent = blk: {
const USER_AGENT = "User-Agent: Lightpanda/1.0";
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;
};
// _app is global to handle graceful shutdown.
_app = try App.init(alloc, .{
var app = try App.init(gpa, .{
.run_mode = args.mode,
.http_proxy = args.httpProxy(),
.proxy_bearer_token = args.proxyBearerToken(),
@@ -127,24 +102,23 @@ fn run(alloc: Allocator) !void {
.http_max_concurrent = args.httpMaxConcurrent(),
.user_agent = user_agent,
});
const app = _app.?;
defer app.deinit();
app.telemetry.record(.{ .run = {} });
switch (args.mode) {
.serve => |opts| {
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 });
return args.printUsageAndExit(false);
};
// _server is global to handle graceful shutdown.
_server = try Server.init(app, address);
const server = &_server.?;
var server = try Server.init(app, address);
defer server.deinit();
try sighandler.on(Server.stop, .{&server});
// max timeout of 1 week.
const timeout = if (opts.timeout > 604_800) 604_800_000 else @as(i32, opts.timeout) * 1000;
server.run(address, timeout) catch |err| {
@@ -888,7 +862,7 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void {
}
if (std.mem.eql(u8, path, "/xhr/json")) {
return req.respond("{\"over\":\"9000!!!\"}", .{
return req.respond("{\"over\":\"9000!!!\",\"updated_at\":1765867200000}", .{
.extra_headers = &.{
.{ .name = "Content-Type", .value = "application/json" },
},

View File

@@ -90,14 +90,17 @@ pub const Notification = struct {
pub const PageRemove = struct {};
pub const PageNavigate = struct {
req_id: usize,
timestamp: u32,
url: []const u8,
opts: page.NavigateOpts,
};
pub const PageNavigated = struct {
req_id: usize,
timestamp: u32,
url: []const u8,
opts: page.NavigatedOpts,
};
pub const PageNetworkIdle = struct {
@@ -296,6 +299,7 @@ test "Notification" {
// noop
notifier.dispatch(.page_navigate, &.{
.req_id = 1,
.timestamp = 4,
.url = undefined,
.opts = .{},
@@ -305,6 +309,7 @@ test "Notification" {
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
notifier.dispatch(.page_navigate, &.{
.req_id = 1,
.timestamp = 4,
.url = undefined,
.opts = .{},
@@ -313,6 +318,7 @@ test "Notification" {
notifier.unregisterAll(&tc);
notifier.dispatch(.page_navigate, &.{
.req_id = 1,
.timestamp = 10,
.url = undefined,
.opts = .{},
@@ -322,21 +328,23 @@ test "Notification" {
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
notifier.dispatch(.page_navigate, &.{
.req_id = 1,
.timestamp = 10,
.url = undefined,
.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(6, tc.page_navigated);
notifier.unregisterAll(&tc);
notifier.dispatch(.page_navigate, &.{
.req_id = 1,
.timestamp = 100,
.url = undefined,
.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(6, tc.page_navigated);
@@ -344,27 +352,27 @@ test "Notification" {
// unregister
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
try testing.expectEqual(114, tc.page_navigate);
try testing.expectEqual(1006, tc.page_navigated);
notifier.unregister(.page_navigate, &tc);
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
try testing.expectEqual(114, tc.page_navigate);
try testing.expectEqual(2006, tc.page_navigated);
notifier.unregister(.page_navigated, &tc);
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
try testing.expectEqual(114, tc.page_navigate);
try testing.expectEqual(2006, tc.page_navigated);
// already unregistered, try anyways
notifier.unregister(.page_navigated, &tc);
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} });
try testing.expectEqual(114, tc.page_navigate);
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 {
app: *App,
shutdown: bool,
shutdown: bool = false,
allocator: Allocator,
client: ?posix.socket_t,
listener: ?posix.socket_t,
@@ -53,16 +53,36 @@ pub const Server = struct {
.app = app,
.client = null,
.listener = null,
.shutdown = false,
.allocator = allocator,
.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 {
self.shutdown = true;
if (self.listener) |listener| {
posix.close(listener);
self.listener = null;
}
// *if* server.run is running, we should really wait for it to return
// before existing from here.
@@ -83,14 +103,19 @@ pub const Server = struct {
try posix.listen(listener, 1);
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| {
if (self.shutdown) {
return;
switch (err) {
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;

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

@@ -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

@@ -113,4 +113,13 @@
// doesn't crash on null receiver
content.addEventListener('he2', null);
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>

View File

@@ -12,7 +12,8 @@
});
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>
@@ -29,6 +30,7 @@
});
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>

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

@@ -20,3 +20,19 @@
testing.expectEqual('P', t.content.childNodes[1].tagName);
testing.expectEqual('9000!', t.content.childNodes[1].innerHTML);
</script>
<template id="hello"><p>hello, world</p></template>
<script id=template_parsing>
const tt = document.getElementById('hello');
testing.expectEqual('<p>hello, world</p>', tt.innerHTML);
// > 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
testing.expectEqual(0, tt.childNodes.length);
let out = document.createElement('div');
out.appendChild(tt.content.cloneNode(true));
testing.expectEqual('<p>hello, world</p>', out.innerHTML);
</script>

View File

@@ -65,6 +65,8 @@
testing.expectEqual(200, req3.status);
testing.expectEqual('OK', req3.statusText);
testing.expectEqual('9000!!!', req3.response.over);
testing.expectEqual("number", typeof req3.response.updated_at);
testing.expectEqual(1765867200000, req3.response.updated_at);
});
</script>

1
vendor/mbedtls vendored

Submodule vendor/mbedtls deleted from c765c831e5