168 Commits

Author SHA1 Message Date
Karl Seguin
a84708e99d Merge pull request #1359 from lightpanda-io/crash_handler
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
e2e-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
Improve crash handling
2026-01-19 16:50:08 +08:00
Halil Durak
6b6c0e930e Merge pull request #1376 from lightpanda-io/nikneym/attribute-ns
Add simplified `setAttributeNS` and `getAttributeNS`
2026-01-19 11:08:49 +03:00
Halil Durak
926892be01 add not_implemented warnings 2026-01-19 10:57:48 +03:00
Karl Seguin
2894bef9ef Update src/crash_handler.zig
Co-authored-by: Pierre Tachoire <pierre@lightpanda.io>
2026-01-19 15:06:43 +08:00
Karl Seguin
a6e7ecd9e5 Move more asserts to custom asserter.
Deciding what should be an lp.assert, vs an std.debug.assert, vs a debug-only
assert is a little arbitrary.

debug-only asserts, guarded with an `if (comptime IS_DEBUG)` obviously avoid the
check in release and thus have a performance advantage. We also use them at
library boundaries. If libcurl says it will always emit a header line with a
trailing \r\n, is that really a check we need to do in production? I don't think
so. First, that code path is checked _a lot_ in debug. Second, it feels a bit
like we're testing libcurl (in production!)..why? A debug-only assertion should
be good enough to catch any changes in libcurl.
2026-01-19 09:12:16 +08:00
Karl Seguin
9b000a002e Hook v8 crashes into new crash handler 2026-01-19 07:37:10 +08:00
Karl Seguin
0f9c9e2089 Improve crash handling
This adds a crash handler which reports a crash (if telemetry is enabled). On a
crash, this looks for `curl` (using the PATH env), and forks the process to then
call execve. This relies on a new endpoint to be setup to accept the "report".
Also, we include very little data..I figured just knowing about crashes would
be a good place to start.

A panic handler is provided, which override's Zig default handler and hooks
into the crash handler.

An `assert` function is added and hooks into the crash handler. This is
currently only used in one place (Session.zig) to demonstrate its use. In
addition to reporting a failed assert, the assert aborts execution in
ReleaseFast (as opposed to an undefined behavior with std.debug.assert).

I want to hook this into the v8 global error handler, but only after direct_v8
is merged.

Much of this is inspired by bun's code. They have their own assert (1) and
a [more sophisticated] crashHandler (2).
:

(1) beccd01647/src/bun.zig (L2987)
(2) beccd01647/src/crash_handler.zig (L198)
2026-01-19 07:36:46 +08:00
Karl Seguin
393227a786 Merge pull request #1373 from lightpanda-io/explicit_globals
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
e2e-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
Explicit globals
2026-01-19 07:26:04 +08:00
Karl Seguin
c5870353e3 update v8 dep 2026-01-19 07:17:45 +08:00
Karl Seguin
7c9941c629 Make Promise, PromiseResolver and Module have explicit globals.
See bb06900b6f84abaccc7ecfd386af1a9dc0029c50 for an explanation.
2026-01-19 07:15:48 +08:00
Karl Seguin
c7dbb6792d Make js.Object and js.Value have explicit global
See: bb06900b6f84abaccc7ecfd386af1a9dc0029c50 for details on this change.
2026-01-19 07:15:48 +08:00
Karl Seguin
728b2b7089 update v8 dep 2026-01-19 07:15:48 +08:00
Karl Seguin
5def997bed Make Global Function explicit.
This is the first in a series of changes to make globals explicit. The ultimate
goal of having explicit Globals is to move away from the global HandleScope and
to explicit HandleScopes.

Currently, we treat globals and locals interchangeably. In fact, for Global ->
Local, we just ptrCast. This works because we have 1 global HandleScope, which
effectively disables V8's GC and thus nothing ever gets moved.

If we're going to introduce explicit HandleScopes, then we need to first have
correct Globals. Specifically, when we want to act on the global, we need to
get the local value, and that will eventually mean making sure there's a
HandleScope.

While adding explicit globals, we're keeping the global HandleScope so that we
can minimize the change. So, given that we still have the global HandleScope
the change is largely two things:
1 - js.Function.persit() returns a js.Function.Global. Types that persist global
   functions must be updated to js.Function.Global.
2 - To turn js.Function.Global -> js.Function, we need to call .local() on it.

The bridge has been updated to support js.Function.Global for both input and
output parameters. Thus, window.setOnLoad can now directly take a
js.Function.Global, and window.getOnLoad can directly return that
js.Function.Global.
2026-01-19 07:15:48 +08:00
Karl Seguin
a30c65966b Merge pull request #1380 from lightpanda-io/static_accessor_fix
Fix static accessors
2026-01-19 07:15:09 +08:00
Karl Seguin
cd67ed8a27 Fix static accessors
These are called without a self from v8, and should match that in Zig code.
2026-01-19 07:08:58 +08:00
Pierre Tachoire
5400dc783e Merge pull request #1379 from lightpanda-io/textarea_setDefaultValue
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
e2e-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Add TextArea.defaultValue  setter
2026-01-18 17:12:49 +01:00
Pierre Tachoire
2880e9867d Merge pull request #1378 from lightpanda-io/performance_observer_use_after_free
Fix potential use-after-free with PerformanceObserver.
2026-01-18 17:03:36 +01:00
Karl Seguin
58f9469a6f Add TextArea.defaultValue setter 2026-01-18 07:49:58 +08:00
Karl Seguin
30d052db99 Fix potential use-after-free with PerformanceObserver.
TL;DR - use page.arena instead of page.call_arena

This probably comes from copying the implementation of MutationObserver and/or
IntersectionObserver. But those dispatches are different in that they directly
dispatch a slice (e.g. of MutationRecords) which gets mapped to a v8::Array when
doing the callback. The MutationRecords exist on the heap, not in
_pending_records, so the call_arena is fine.

PerformanceObserver returns an Zig object, not a slice. Therefore it gets mapped
to a v8::Object which references the Zig object. The state of that object, the
_entries list, has to exist for the lifetime of that object, not the call_arena.
2026-01-17 15:57:43 +08:00
Karl Seguin
744311f107 Merge pull request #1375 from lightpanda-io/nikneym/audio-constructor
Some checks failed
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
e2e-test / zig build release (push) Has been cancelled
e2e-integration-test / zig build release (push) Has been cancelled
e2e-integration-test / demo-integration-scripts (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
e2e-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Add `Audio` constructor
2026-01-16 23:25:56 +00:00
Karl Seguin
656674a477 Merge pull request #1356 from lightpanda-io/nikneym/subtle-crypto
Initial support for `SubtleCrypto` API
2026-01-16 23:21:35 +00:00
Karl Seguin
0e4aa38aaa Merge pull request #1312 from lightpanda-io/stagehand-zigdom
Stagehand zigdom
2026-01-16 23:10:19 +00:00
Pierre Tachoire
fdc267fa1f Merge pull request #1308 from lightpanda-io/axtree-backport
cdp: add AXTree
2026-01-16 17:56:40 +01:00
Pierre Tachoire
4325b80d64 axnode: small fixes 2026-01-16 17:30:43 +01:00
Pierre Tachoire
fbe07836f9 cdp: return a valide response for Page.getFrameTree on STARTUP
Stagehand expects a valid response for this specific command.
Add also `Target.activateTarget`
2026-01-16 16:27:55 +01:00
Halil Durak
304681bd21 add simplified setAttributeNS and getAttributeNS
This ignores namespaces for now, we have to come up with a solution if it becomes a necessity.
2026-01-16 18:13:43 +03:00
Halil Durak
05a01bb7c4 add Audio constructor 2026-01-16 17:38:54 +03:00
Pierre Tachoire
cbc028b040 cdp: accept multiple attachToTarget calls 2026-01-16 09:10:41 +01:00
Pierre Tachoire
2074c0149f axnode: add aria-labelledby support 2026-01-16 09:01:39 +01:00
Pierre Tachoire
61ed97dd45 axnode: use writeString for content's name 2026-01-16 09:00:57 +01:00
Pierre Tachoire
a358c46b9f axnode: ignore script and style children 2026-01-16 08:28:16 +01:00
Pierre Tachoire
50c1e2472b axnode: encode json string into stripWhitespaces 2026-01-16 08:27:43 +01:00
Halil Durak
ea2fc76d3c don't @panic! 2026-01-15 20:40:53 +03:00
Halil Durak
58634b54ec add tests for implemented bits of SubtleCrypto 2026-01-15 19:10:02 +03:00
Halil Durak
4b4bc1a4d3 don't allocate new SubtleCrypto for each access 2026-01-15 19:10:01 +03:00
Halil Durak
0549e07a90 implement deriveBits for X25519 2026-01-15 19:10:01 +03:00
Halil Durak
42666b1d30 add bindings needed for X25519 deriveBits implementation 2026-01-15 19:10:01 +03:00
Halil Durak
0a8be77233 create public/private key objects out of raw keys
This is needed for `deriveKey()` and `deriveBits()`.
2026-01-15 19:10:01 +03:00
Halil Durak
b26fb0e6c7 add more libcrypto bindings 2026-01-15 19:10:00 +03:00
Halil Durak
1699a92822 support x25519 init
Created a mess in previous commit.
2026-01-15 19:10:00 +03:00
Halil Durak
7ae3e8cb47 code cleanup, support keypairs, init support for X25519 2026-01-15 19:10:00 +03:00
Halil Durak
fd26ae4b5b parse keyUsages properly 2026-01-15 19:10:00 +03:00
Halil Durak
9945a5f9cc implement sign and verify for HMAC 2026-01-15 19:09:59 +03:00
Halil Durak
d5e9ae23ef ground zero SubtleCrypto 2026-01-15 19:09:59 +03:00
Pierre Tachoire
d50e056114 axnode: ignore non-html tags 2026-01-15 16:42:40 +01:00
Pierre Tachoire
d7d956d966 axnode: fix invalid enum 2026-01-15 15:40:52 +01:00
Pierre Tachoire
bd3966bf8d axnode: add focus on webroot 2026-01-15 15:37:49 +01:00
Pierre Tachoire
74578ba274 axnode: implement list marker 2026-01-15 15:37:49 +01:00
Pierre Tachoire
cb89742d2f axnode: add li level 2026-01-15 15:37:48 +01:00
Pierre Tachoire
6d0f991c17 axnode: add hr properties 2026-01-15 15:37:48 +01:00
Pierre Tachoire
d126d2a0f9 axnode: ignore hidden input 2026-01-15 15:37:47 +01:00
Pierre Tachoire
b51cca5617 axnode: use select.getValue 2026-01-15 15:37:47 +01:00
Pierre Tachoire
dc54dad290 axnode: add more attributes for input elements 2026-01-15 15:37:47 +01:00
Pierre Tachoire
7d6ab5a708 axnode: force manual formatting in switches
In order to uses less space and improve the readability.

zig fmt allows only 1 switch case per line or all in one line.
When having a lot of conditions, splitting the line is useful.
2026-01-15 15:37:46 +01:00
Pierre Tachoire
07acb9308d axnode: fallback button name to their tagname 2026-01-15 15:37:46 +01:00
Pierre Tachoire
ef315a46bc axnode: don't extract all text content as name
ignore name extraction for more elements
2026-01-15 15:37:45 +01:00
Pierre Tachoire
eb45bd051c axtree: simpler AXValue 2026-01-15 15:37:45 +01:00
Pierre Tachoire
65102edc98 axtree: remove useless error return 2026-01-15 15:37:44 +01:00
Pierre Tachoire
04eda96416 axtree: reverse writeNode return logic 2026-01-15 15:37:44 +01:00
Pierre Tachoire
f5036bdf5e axtree: use a simpler union switch 2026-01-15 15:37:44 +01:00
Pierre Tachoire
b6df85da7a axtree: add improvements 2026-01-15 15:37:43 +01:00
Pierre Tachoire
9775b39a8d axnode: use absolute urls 2026-01-15 15:37:43 +01:00
Pierre Tachoire
d6d74c5024 first version of AXTree 2026-01-15 15:37:42 +01:00
Pierre Tachoire
e09d15b12a add more generic HTML types 2026-01-15 15:37:35 +01:00
Karl Seguin
6d33d23935 Merge pull request #1371 from lightpanda-io/reject_non_new_constructor
Some checks failed
e2e-test / zig build release (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
e2e-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
Reject constructor calls without new
2026-01-15 12:06:55 +00:00
Karl Seguin
47760e00f7 Reject constructor calls without new
This was previously a fixed bug, but it got lost in the direct_v8 merging.

https://github.com/lightpanda-io/browser/pull/1316
2026-01-15 19:25:43 +08:00
Karl Seguin
72e8421099 Merge pull request #1366 from lightpanda-io/details_are_values
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
e2e-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
use js.Value when input can be a value
2026-01-14 23:25:17 +00:00
Karl Seguin
844b0ed457 Merge pull request #1368 from lightpanda-io/dupe_remove_id
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
e2e-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
Make removeIds lookup own the key
2026-01-14 11:52:06 +00:00
Karl Seguin
7e37db796f Make removeIds lookup own the key
Virtually all string are page-owned (in the page.arena), but because of the
Small String Optimization we use (string.zig), a string could be stack-allocated

The correct solution is probably to change the key to be a string.String. But
I want to give more thought to memory in general, and strings specifically need
to be thought about. So this is a quick fix for crashing.
2026-01-14 18:35:26 +08:00
Karl Seguin
3e5b506675 Merge pull request #1367 from lightpanda-io/readable_stream_cancel_persist
persist the readable stream's cancel callback
2026-01-14 10:20:14 +00:00
Karl Seguin
d356dbfc06 Merge pull request #1365 from lightpanda-io/try_catch_caught
Try catch caught
2026-01-14 09:59:19 +00:00
Karl Seguin
f5aee1f4c0 persist the readable stream's cancel callback 2026-01-14 17:58:41 +08:00
Karl Seguin
de4926d87d fix legacy runner, manual merge 2026-01-14 17:49:27 +08:00
Karl Seguin
56a39e2cc7 Apply tryCatch change to wpt runner 2026-01-14 17:34:08 +08:00
Karl Seguin
8e14dacc32 Improve ergonomics of try catch (and Function's tryCall)
It now returns a Caught struct which contains all information. The Caught struct
can be logged directly, providing more consistent logs for caught errors.
2026-01-14 17:34:02 +08:00
Karl Seguin
05102c673a use js.Value when input can be a value
We previously treated v8::Object and v8::Values interchangeably, and would just
ptrCast one to the other. So, if an API was defined with a js.Object but was
given a non-object value, e.g. 9001, it would still work.

This has since been tightened. If an API takes a js.Object, than the v8 value
must be an object. Passing a non-object will result in a InvalidArgument error.

CustomEvent.detail and PerformanceMark.detail can both be any value, so the
apis/fields have been updated from js.Object -> js.Value.
2026-01-14 15:38:34 +08:00
Karl Seguin
db2ecfe159 Merge pull request #1307 from lightpanda-io/direct_v8
Direct v8
2026-01-14 07:27:42 +00:00
Karl Seguin
640cb0d489 Merge pull request #1364 from lightpanda-io/observer_try_catch
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
e2e-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
add trycatch to Intersection and Performance Observers
2026-01-14 02:10:56 +00:00
Karl Seguin
223a6170d5 Fix use-after free
On CDP.BrowserContext.deinit, clear the isolated world ExecutionContext before
terminating the session. This is important as the isolated_world list is
allocated from the session.arena.

Also, semi-revert 63f1c85964. Before all this
we were running microtasks on ExecutionWorld.removeContext. That didn't seem
right (and I thought it was the original source of the bug). But, for the "real"
Page context, this is critical, since Microtasks can reference the Page object.
Since microTasks are isolation-level, it's possible for a microtasks for Page1
to execute after Page1 goes away (if we create a new page, Page2). This re-adds
the microtask "draining", but only for the Page (i.e. in Page.deinit).
2026-01-14 09:37:10 +08:00
Karl Seguin
63f1c85964 Remove unnecessary microtask run.
This crashes linux in releasesafe without an embedded snapshot. Not sure why,
but it shouldn't be necessary. This was added back when we were executing
microtasks on a schedule, rather than manually at explicit points.
2026-01-13 18:09:58 +08:00
Karl Seguin
c252c8e870 update v8 dep version 2026-01-13 16:12:28 +08:00
Karl Seguin
801c019150 update v8 2026-01-13 16:07:49 +08:00
Karl Seguin
d77a6620f3 merge main 2026-01-13 13:05:16 +08:00
Karl Seguin
4e4a615df8 Move Env's FunctionTemplate from Global -> Eternal
(we'll move more to Eternal's, this is just a first teaser)
2026-01-13 12:58:31 +08:00
Karl Seguin
1b0ea44519 merge main 2026-01-13 12:58:31 +08:00
Karl Seguin
86f4ea108d Store snapshot templates in isolate, not context.
This lets us load the isolate without having to create a temp/dummy context
just to get the templates.

Call ContextDisposedNotification when a context is removed. Supposedly this can
help/hint to the isolate about memory management.
2026-01-13 12:58:30 +08:00
Karl Seguin
2322cb9b83 remove unused code, remove references to v8::Persistent 2026-01-13 12:58:30 +08:00
Karl Seguin
4720268426 Don't dupe StartupData, use what v8 gives us directly. 2026-01-13 12:58:30 +08:00
Karl Seguin
b4f134bff6 Prefer js.Value over js.Object in History/Navigation
Persist function callback in PerformanceObserver
2026-01-13 12:58:30 +08:00
Karl Seguin
f2a9125b99 js.v8 is not equal to js.v8.c
This means the C funtions/types now sit in the root of v8.
2026-01-13 12:58:30 +08:00
Karl Seguin
8438b7d561 remove remaining direct v8 references 2026-01-13 12:58:30 +08:00
Karl Seguin
18c846757b migrate almost all types 2026-01-13 12:58:28 +08:00
Karl Seguin
bc11a48e6b migrate most cases, merge Caller into bridge 2026-01-13 12:57:06 +08:00
Karl Seguin
01ecd725b8 cleanup resolvers 2026-01-13 12:57:06 +08:00
Karl Seguin
e6af7d1bd0 import more types 2026-01-13 12:57:06 +08:00
Karl Seguin
701de08e8a have our js.Context directly hold a js handle 2026-01-13 12:57:06 +08:00
Karl Seguin
363b95bdef Isolate and HandleScope 2026-01-13 12:57:06 +08:00
Karl Seguin
ca5a385b51 Port js.Object
Use js.Value in apis that should take values (not objects), like console.log
and setTimeout and reportError.
2026-01-13 12:57:03 +08:00
Karl Seguin
93f0d24673 port TryCatch 2026-01-13 12:56:07 +08:00
Karl Seguin
a5038893fe port Snapshot 2026-01-13 12:56:07 +08:00
Karl Seguin
3442f99a49 remove unused js.This 2026-01-13 12:56:07 +08:00
Karl Seguin
6ecf52cc03 port Platform and Inspector to use v8's C handles/functions directly 2026-01-13 12:56:07 +08:00
Karl Seguin
8aaef674fe Migrate Function and String 2026-01-13 12:56:07 +08:00
Karl Seguin
3b1cd06615 Make js.Array and js.Value directly contain their v8 handles. 2026-01-13 12:56:06 +08:00
Karl Seguin
4841f8cc8f add trycatch to Intersection and Performance Observers 2026-01-13 12:55:10 +08:00
Karl Seguin
d9d8f68bf8 Merge pull request #1361 from lightpanda-io/mutation-observer-trycall
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
e2e-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
use tryCall for MutationObserver records callback
2026-01-12 22:42:33 +00:00
Pierre Tachoire
cf726d9813 fix double slash in import path 2026-01-12 17:59:49 +01:00
Pierre Tachoire
92be2c45d6 use tryCall for MutationObserver records callback
Instead of `call` to avoid uncaught error
2026-01-12 17:58:40 +01:00
Pierre Tachoire
914092b538 Merge pull request #1355 from lightpanda-io/console_apis
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
e2e-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
add some missing APIs to Console
2026-01-12 15:15:24 +01:00
Pierre Tachoire
a8cd5fc266 Merge pull request #1354 from lightpanda-io/node_document
Node document
2026-01-12 15:14:57 +01:00
Pierre Tachoire
643f07fa10 Merge pull request #1352 from lightpanda-io/mutation_character_data
Mutation character data
2026-01-12 15:13:32 +01:00
Pierre Tachoire
0d77ff661b Merge pull request #1360 from lightpanda-io/wpt-v8
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
e2e-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
ci: fix wpt path
2026-01-12 10:53:51 +01:00
Pierre Tachoire
70d84b2f72 ci: fix wpt build path 2026-01-12 10:38:13 +01:00
Pierre Tachoire
41905ef735 Merge pull request #1358 from lightpanda-io/wpt-v8
ci: move fetch test from integration to e2e
2026-01-12 09:26:55 +01:00
Pierre Tachoire
2a468cc750 ci: split wpt build and run
vv8 build can pollute stdout output.
2026-01-12 09:18:16 +01:00
Pierre Tachoire
32520000c6 ci: use releaseFast mode for wpt 2026-01-12 09:09:36 +01:00
Pierre Tachoire
14db7a8eb3 ci: move fetch test from integration to e2e 2026-01-12 08:53:24 +01:00
Pierre Tachoire
8460e9a385 Merge pull request #1357 from lightpanda-io/wpt-v8
CI changes
2026-01-12 08:42:53 +01:00
Pierre Tachoire
933a93a703 ci: move fetch tests into 2e2 2026-01-12 08:32:52 +01:00
Pierre Tachoire
c2e09d3084 ci: build only run cmd forbuild dev test 2026-01-12 08:28:03 +01:00
Pierre Tachoire
98397401b8 ci: use compiled v8 with wpt tests 2026-01-12 08:11:55 +01:00
Karl Seguin
e042b1105a Merge pull request #1311 from lightpanda-io/nikneym/backport-canvas
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
Backport dummy canvas APIs
2026-01-10 23:26:42 +00:00
Halil Durak
ee4775eb1a prefer underscore on fields 2026-01-10 14:13:41 +03:00
Halil Durak
6ff6232316 move isHexColor to color.zig 2026-01-10 14:13:09 +03:00
Karl Seguin
10035ab2f4 add some missing APIs to Console 2026-01-10 17:45:25 +08:00
Karl Seguin
2679175ae9 make createElement return DOMException on error 2026-01-10 16:00:11 +08:00
Karl Seguin
8d3aa1f3fa validate tag name given to document.createElement 2026-01-10 10:43:43 +08:00
Karl Seguin
75e78795ec Add Document.replaceChildren
Improve correctness (hierarchy validation) of various Document functions.
2026-01-10 10:32:02 +08:00
Karl Seguin
05f0f8901e make Node.isConnected() shadowroot-aware 2026-01-10 08:24:12 +08:00
Karl Seguin
6917aeb47b Walk document for doctype 2026-01-10 08:05:03 +08:00
Karl Seguin
516a86e33f Merge pull request #1331 from lightpanda-io/zigdom-selector-case-insensitive
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
Adds case insensitivity to `Element.querySelector`
2026-01-09 23:20:39 +00:00
Halil Durak
7184a91c95 finalize canvas backport 2026-01-09 19:46:11 +03:00
Halil Durak
83e9d705cf backport dummy canvas APIs 2026-01-09 16:47:19 +03:00
Karl Seguin
bb907f5adb Support range mutation across nodes
Range mutation will trigger MutationObserver

MutationObserver with characterDataOldValue=true implicitly means
characterData=true

For MutationObserver-characterData test.
2026-01-09 20:42:23 +08:00
Karl Seguin
f1b60453bd Add getAttributeNamespace to MutationRecord 2026-01-09 20:25:49 +08:00
Pierre Tachoire
0ef339f12a Merge pull request #1349 from lightpanda-io/build-timeout
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: increase timeout limit for build
2026-01-09 12:34:18 +01:00
Pierre Tachoire
5c0169ee05 ci: force release to be the latest 2026-01-09 11:44:38 +01:00
Pierre Tachoire
daf959ee90 Merge pull request #1347 from lightpanda-io/registerProtocolHandler
Add Navigator.registerProtocolHandler and unregisterProtocolHandler p…
2026-01-09 11:42:49 +01:00
Pierre Tachoire
89b43b6102 Merge pull request #1348 from lightpanda-io/wpt_events
Wpt events
2026-01-09 11:41:41 +01:00
Pierre Tachoire
d3b05201b9 Merge pull request #1350 from lightpanda-io/css_selector_escape_sequence
Support escape sequences in CSS selector for id and class selectors
2026-01-09 11:40:33 +01:00
Karl Seguin
127e53cf3a Merge pull request #1344 from lightpanda-io/indexed_fix
Define the index handler on the instance, not the prototype.
2026-01-09 10:38:54 +00:00
Pierre Tachoire
29281fe3ec Merge pull request #1346 from lightpanda-io/more_cloneNode
Support cloneNode for DocumentType and Attribute
2026-01-09 11:38:18 +01:00
Pierre Tachoire
a0fb55802f Merge pull request #1345 from lightpanda-io/add_more_explicit_types
Adds a number of HTML elements
2026-01-09 11:37:51 +01:00
Pierre Tachoire
90ec068367 Merge pull request #1351 from lightpanda-io/inspector-deinit-handlescope
use temporary handle scope to deinit inspector
2026-01-09 11:37:00 +01:00
Pierre Tachoire
f57cf1be75 use temporary handlescope to deinit inspector 2026-01-09 11:25:34 +01:00
Karl Seguin
3f44dee367 Support escape sequences in CSS selector for id and class selectors
Improves dom/nodes/ParentNode-querySelector-escapes.html from 20/68 -> 64/68.

Previous main had 66/68..but the last 4 are really edge cases that add a lot
of complexity.
2026-01-09 16:42:57 +08:00
Pierre Tachoire
82161ce94c ci: increase timeout limit for build 2026-01-09 09:14:01 +01:00
Karl Seguin
27b8e2a38c fix test 2026-01-09 14:38:47 +08:00
Karl Seguin
e5f2fbdcb2 clear isTrusted on redispatch and prevent redispatching while dispatching 2026-01-09 14:37:34 +08:00
Karl Seguin
cdf0cdd0ea Don't require handleEvent function to be on object event listener
When registering an object as an event listener, the handleEvent function
doesn't have to be defined then and there. The handleEvent function can be added
at any point in the future.
2026-01-09 14:27:00 +08:00
Karl Seguin
f12ff2c7bd Add Navigator.registerProtocolHandler and unregisterProtocolHandler placeholders 2026-01-09 13:40:19 +08:00
Karl Seguin
6c7c507d32 Support cloneNode for DocumentType and Attribute 2026-01-09 11:10:50 +08:00
Karl Seguin
0c97b8238b Adds a number of HTML elements
Instead of being mapped to HTMLUnknownElement, these will all be mapped to the
correct type. This is important for many WPT tests. But it's not impossible that
some script checks `if (x instanceof HTMLBaseElement)` and, without this, that
would error since HTMLBaseElement wouldn't be defined.
2026-01-09 10:56:23 +08:00
Karl Seguin
967a2030e6 Define the index handler on the instance, not the prototype.
While it sorta works if done on the prototype, it's incorrect as these are no
longer "own" properties (which some WPT tests care about). NamedIndexes were
already correctly defined on the instance.
2026-01-09 10:31:01 +08:00
Karl Seguin
78ebd5faf8 Merge pull request #1342 from lightpanda-io/better_namespace_support
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
Improve support for non-HTML namespace
2026-01-09 07:05:36 +08:00
Karl Seguin
9d498fa069 Improve support for non-HTML namespace
This does a better job of tracking the implicit namespace based on the context.
For example, when using DOMParser.parseFromString with an XML namespace, all
subsequent elements will be in the XML namespace.

Adds support for null namespace.

Rather than defaulting to HTML, unknown namespaces now map to a special unknown
type. We don't currently preserve the original namespace, but we're at least
able to properly handle the casing in this case.
2026-01-09 07:03:52 +08:00
Karl Seguin
0db1ceaea7 Merge pull request #1339 from lightpanda-io/remove_className
Remove className function from every type
2026-01-09 06:57:40 +08:00
Karl Seguin
df27aeef6c Merge pull request #1343 from lightpanda-io/navigator-ua-suffix
return the app's user agent on Navigator.userAgent
2026-01-09 06:56:01 +08:00
Karl Seguin
5ae0df53bb Remove className function from every type
The toString symbol is now automatically implemented on any type with a
JsApi.Meta.Name, so className is no longer used.
2026-01-09 06:55:07 +08:00
Karl Seguin
48df6ae159 Merge pull request #1338 from lightpanda-io/HTMLSpanElement
add an explicit HTMLSpanElement
2026-01-09 06:52:17 +08:00
Pierre Tachoire
6cae2fcea7 return the app's user agent on Navigator.userAgent 2026-01-08 15:23:02 +01:00
Pierre Tachoire
d1d4d4894d Merge pull request #1341 from lightpanda-io/test-bench
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
tests: re-enable metrics JSON output
2026-01-08 14:01:56 +01:00
Pierre Tachoire
adfcf7bb2c tests: re-enable metrics JSON output
METRICS=true zig build test
2026-01-08 13:07:27 +01:00
Karl Seguin
c8f75cd266 Merge pull request #1340 from lightpanda-io/nikneym/parse-from-string-return
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
Return `*Document` instead of tagged union in `parseFromString`
2026-01-08 19:16:14 +08:00
Halil Durak
282a9bbf65 return *Document instead of tagged union in parseFromString
Did a detour to XML PR and realized this is simpler.
2026-01-08 12:46:46 +03:00
Karl Seguin
d4c8af2a61 add an explicit HTMLSpanElement 2026-01-08 16:03:50 +08:00
Muki Kiboigo
3930524bbf use tokenizeAny instead of tokenizeScalar in Selector 2026-01-07 06:12:49 -08:00
Muki Kiboigo
622ca3121f add case insensitivity support to selector parsing 2026-01-06 23:31:34 -08:00
190 changed files with 10547 additions and 4015 deletions

View File

@@ -13,7 +13,7 @@ inputs:
zig-v8:
description: 'zig v8 version to install'
required: false
default: 'v0.2.2'
default: 'v0.2.4'
v8:
description: 'v8 version to install'
required: false

View File

@@ -27,7 +27,7 @@ jobs:
OS: linux
runs-on: ubuntu-22.04
timeout-minutes: 15
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
@@ -62,6 +62,7 @@ jobs:
allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: ${{ env.RELEASE }}
makeLatest: true
build-linux-aarch64:
env:
@@ -69,7 +70,7 @@ jobs:
OS: linux
runs-on: ubuntu-22.04-arm
timeout-minutes: 15
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
@@ -104,6 +105,7 @@ jobs:
allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: ${{ env.RELEASE }}
makeLatest: true
build-macos-aarch64:
env:
@@ -113,7 +115,7 @@ jobs:
# macos-14 runs on arm CPU. see
# https://github.com/actions/runner-images?tab=readme-ov-file
runs-on: macos-14
timeout-minutes: 15
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
@@ -148,6 +150,7 @@ jobs:
allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: ${{ env.RELEASE }}
makeLatest: true
build-macos-x86_64:
env:
@@ -155,7 +158,7 @@ jobs:
OS: macos
runs-on: macos-14-large
timeout-minutes: 15
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
@@ -190,3 +193,4 @@ jobs:
allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: ${{ env.RELEASE }}
makeLatest: true

View File

@@ -232,3 +232,19 @@ jobs:
- name: format and send json result
run: /perf-fmt hyperfine ${{ github.sha }} hyperfine.json
browser-fetch:
name: browser fetch
needs: zig-build-release
runs-on: ubuntu-latest
steps:
- name: download artifact
uses: actions/download-artifact@v4
with:
name: lightpanda-build-release
- run: chmod a+x ./lightpanda
- run: ./lightpanda fetch https://demo-browser.lightpanda.io/campfire-commerce/

View File

@@ -30,8 +30,11 @@ jobs:
- uses: ./.github/actions/install
- name: json output
run: zig build wpt -- --json > wpt.json
- name: build wpt
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -- version
- name: run test with json output
run: zig-out/bin/lightpanda-wpt --json > wpt.json
- name: write commit
run: |

View File

@@ -38,52 +38,6 @@ on:
workflow_dispatch:
jobs:
zig-build-dev:
name: zig build dev
# Don't run the CI with draft PR.
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- uses: ./.github/actions/install
- name: zig build debug
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a
- name: upload artifact
uses: actions/upload-artifact@v4
with:
name: lightpanda-build-dev
path: |
zig-out/bin/lightpanda
retention-days: 1
browser-fetch:
name: browser fetch
needs: zig-build-dev
# Don't run the CI with draft PR.
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- name: download artifact
uses: actions/download-artifact@v4
with:
name: lightpanda-build-dev
- run: chmod a+x ./lightpanda
- run: ./lightpanda fetch https://httpbin.io/xhr/get
zig-test:
name: zig test
timeout-minutes: 15
@@ -103,7 +57,7 @@ jobs:
- uses: ./.github/actions/install
- name: zig build test
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a test -- --json > bench.json
run: METRICS=true zig build -Dprebuilt_v8_path=v8/libc_v8.a test > bench.json
- name: write commit
run: |

View File

@@ -3,7 +3,7 @@ FROM debian:stable-slim
ARG MINISIG=0.12
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
ARG V8=14.0.365.4
ARG ZIG_V8=v0.2.2
ARG ZIG_V8=v0.2.4
ARG TARGETPLATFORM
RUN apt-get update -yq && \

View File

@@ -48,6 +48,7 @@ pub fn build(b: *Build) !void {
.sanitize_c = enable_csan,
.sanitize_thread = enable_tsan,
});
mod.addImport("lightpanda", mod); // allow circular "lightpanda" import
try addDependencies(b, mod, opts, prebuilt_v8_path);
@@ -117,7 +118,6 @@ pub fn build(b: *Build) !void {
}
{
// ZIGDOM
// browser
const exe = b.addExecutable(.{
.name = "legacy_test",

View File

@@ -6,10 +6,10 @@
.minimum_zig_version = "0.15.2",
.dependencies = .{
.v8 = .{
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/d6b5f89cfc7feece29359e8c848bb916e8ecfab6.tar.gz",
.hash = "v8-0.0.0-xddH6_0gBABrJc5cL6-P2wGvvweTTCgWdpmClr9r-C-s",
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/v0.2.4.tar.gz",
.hash = "v8-0.0.0-xddH66YvBAD0YI9xr6F0Xgnw9wN30FdZ10FLyuoV3e66",
},
//.v8 = .{ .path = "../zig-v8-fork" },
// .v8 = .{ .path = "../zig-v8-fork" },
.@"boringssl-zig" = .{
.url = "git+https://github.com/Syndica/boringssl-zig.git#c53df00d06b02b755ad88bbf4d1202ed9687b096",
.hash = "boringssl-0.1.0-VtJeWehMAAA4RNnwRnzEvKcS9rjsR1QVRw1uJrwXxmVK",

View File

@@ -86,8 +86,8 @@ pub fn init(allocator: Allocator, config: Config) !*App {
app.platform = try Platform.init();
errdefer app.platform.deinit();
app.snapshot = try Snapshot.load(allocator);
errdefer app.snapshot.deinit(allocator);
app.snapshot = try Snapshot.load();
errdefer app.snapshot.deinit();
app.app_dir_path = getAndMakeAppDir(allocator);
@@ -112,7 +112,7 @@ pub fn deinit(self: *App) void {
self.telemetry.deinit();
self.notification.deinit();
self.http.deinit();
self.snapshot.deinit(allocator);
self.snapshot.deinit();
self.platform.deinit();
allocator.destroy(self);

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const log = @import("log.zig");
const Page = @import("browser/Page.zig");
@@ -241,7 +242,7 @@ pub fn unregister(self: *Notification, comptime event: EventType, receiver: anyt
if (listeners.items.len == 0) {
listeners.deinit(self.allocator);
const removed = self.listeners.remove(@intFromPtr(receiver));
std.debug.assert(removed == true);
lp.assert(removed == true, "Notification.unregister", .{ .type = event });
}
}

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const builtin = @import("builtin");
const net = std.net;
@@ -157,7 +158,7 @@ fn readLoop(self: *Server, socket: posix.socket_t, timeout_ms: u32) !void {
});
defer http.removeCDPClient();
std.debug.assert(client.mode == .http);
lp.assert(client.mode == .http, "Server.readLoop invalid mode", .{});
while (true) {
if (http.poll(timeout_ms) != .cdp_socket) {
log.info(.app, "CDP timeout", .{});
@@ -236,7 +237,7 @@ pub const Client = struct {
const socket_flags = try posix.fcntl(socket, posix.F.GETFL, 0);
const nonblocking = @as(u32, @bitCast(posix.O{ .NONBLOCK = true }));
// we expect the socket to come to us as nonblocking
std.debug.assert(socket_flags & nonblocking == nonblocking);
lp.assert(socket_flags & nonblocking == nonblocking, "Client.init blocking", .{});
var reader = try Reader(true).init(server.allocator);
errdefer reader.deinit();
@@ -311,7 +312,7 @@ pub const Client = struct {
}
fn processHTTPRequest(self: *Client) !bool {
std.debug.assert(self.reader.pos == 0);
lp.assert(self.reader.pos == 0, "Client.HTTP pos", .{ .pos = self.reader.pos });
const request = self.reader.buf[0..self.reader.len];
if (request.len > MAX_HTTP_REQUEST_SIZE) {
@@ -592,8 +593,7 @@ pub const Client = struct {
// blocking and switch it back to non-blocking after the write
// is complete. Doesn't seem particularly efficiently, but
// this should virtually never happen.
std.debug.assert(changed_to_blocking == false);
log.debug(.app, "CDP write would block", .{});
lp.assert(changed_to_blocking == false, "Client.double block", .{});
changed_to_blocking = true;
_ = try posix.fcntl(self.socket, posix.F.SETFL, self.socket_flags & ~@as(u32, @bitCast(posix.O{ .NONBLOCK = true })));
continue :LOOP;
@@ -821,7 +821,7 @@ fn Reader(comptime EXPECT_MASK: bool) type {
const pos = self.pos;
const len = self.len;
std.debug.assert(pos <= len);
lp.assert(pos <= len, "Client.Reader.compact precondition", .{ .pos = pos, .len = len });
// how many (if any) partial bytes do we have
const partial_bytes = len - pos;
@@ -842,7 +842,7 @@ fn Reader(comptime EXPECT_MASK: bool) type {
const next_message_len = length_meta.@"1";
// if this isn't true, then we have a full message and it
// should have been processed.
std.debug.assert(next_message_len > partial_bytes);
lp.assert(pos <= len, "Client.Reader.compact postcondition", .{ .next_len = next_message_len, .partial = partial_bytes });
const missing_bytes = next_message_len - partial_bytes;
@@ -929,7 +929,7 @@ fn fillWebsocketHeader(buf: std.ArrayListUnmanaged(u8)) []const u8 {
// makes the assumption that our caller reserved the first
// 10 bytes for the header
fn websocketHeader(buf: []u8, op_code: OpCode, payload_len: usize) []const u8 {
std.debug.assert(buf.len == 10);
lp.assert(buf.len == 10, "Websocket.Header", .{ .len = buf.len });
const len = payload_len;
buf[0] = 128 | @intFromEnum(op_code); // fin | opcode

View File

@@ -102,8 +102,8 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, call
}
const func = switch (callback) {
.function => |f| Function{ .value = f },
.object => |o| Function{ .object = o },
.function => |f| Function{ .value = try f.persist() },
.object => |o| Function{ .object = try o.persist() },
};
const listener = try self.listener_pool.create();
@@ -368,12 +368,13 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
}
switch (listener.function) {
.value => |value| try value.callWithThis(void, current_target, .{event}),
.value => |value| try value.local().callWithThis(void, current_target, .{event}),
.string => |string| {
const str = try page.call_arena.dupeZ(u8, string.str());
try self.page.js.eval(str, null);
},
.object => |obj| {
.object => |*obj_global| {
const obj = obj_global.local();
if (try obj.getFunction("handleEvent")) |handleEvent| {
try handleEvent.callWithThis(void, obj, .{event});
}
@@ -443,20 +444,20 @@ const Listener = struct {
};
const Function = union(enum) {
value: js.Function,
value: js.Function.Global,
string: String,
object: js.Object,
object: js.Object.Global,
fn eqlFunction(self: Function, func: js.Function) bool {
return switch (self) {
.value => |v| return v.id == func.id,
.value => |v| v.isEqual(func),
else => false,
};
}
fn eqlObject(self: Function, obj: js.Object) bool {
return switch (self) {
.object => |o| return o.getId() == obj.getId(),
.object => |o| return o.isEqual(obj),
else => false,
};
}

View File

@@ -17,10 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const assert = std.debug.assert;
const builtin = @import("builtin");
const reflect = @import("reflect.zig");
const IS_DEBUG = builtin.mode == .Debug;
const log = @import("../log.zig");
const String = @import("../string.zig").String;
@@ -38,6 +36,9 @@ const XMLHttpRequestEventTarget = @import("webapi/net/XMLHttpRequestEventTarget.
const Blob = @import("webapi/Blob.zig");
const AbstractRange = @import("webapi/AbstractRange.zig");
const IS_DEBUG = builtin.mode == .Debug;
const assert = std.debug.assert;
const Factory = @This();
_page: *Page,
_slab: SlabAllocator,

File diff suppressed because it is too large Load Diff

View File

@@ -95,7 +95,9 @@ fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
if (repeat_in_ms) |ms| {
// Task cannot be repeated immediately, and they should know that
std.debug.assert(ms != 0);
if (comptime IS_DEBUG) {
std.debug.assert(ms != 0);
}
task.run_at = now + ms;
try self.low_priority.add(task);
}

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const builtin = @import("builtin");
const js = @import("js/js.zig");
@@ -484,7 +485,7 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
// Called from the Page to let us know it's done parsing the HTML. Necessary that
// we know this so that we know that we can start evaluating deferred scripts.
pub fn staticScriptsDone(self: *ScriptManager) void {
std.debug.assert(self.static_scripts_done == false);
lp.assert(self.static_scripts_done == false, "ScriptManager.staticScriptsDone", .{});
self.static_scripts_done = true;
self.evaluate();
}
@@ -675,7 +676,7 @@ pub const Script = struct {
// set `CURLOPT_SUPPRESS_CONNECT_HEADERS` and CONNECT to a proxy, this
// will fail. This assertion exists to catch incorrect assumptions about
// how libcurl works, or about how we've configured it.
std.debug.assert(self.source.remote.capacity == 0);
lp.assert(self.source.remote.capacity == 0, "ScriptManager.HeaderCallback", .{ .capacity = self.source.remote.capacity });
var buffer = self.manager.buffer_pool.get();
if (transfer.getContentLength()) |cl| {
try buffer.ensureTotalCapacity(self.manager.allocator, cl);
@@ -750,10 +751,12 @@ pub const Script = struct {
fn eval(self: *Script, page: *Page) void {
// never evaluated, source is passed back to v8, via callbacks.
std.debug.assert(self.mode != .import_async);
if (comptime IS_DEBUG) {
std.debug.assert(self.mode != .import_async);
// never evaluated, source is passed back to v8 when asked for it.
std.debug.assert(self.mode != .import);
// never evaluated, source is passed back to v8 when asked for it.
std.debug.assert(self.mode != .import);
}
if (page.isGoingAway()) {
// don't evaluate scripts for a dying page.
@@ -834,20 +837,19 @@ pub const Script = struct {
return;
}
const msg = try_catch.err(page.arena) catch |err| @errorName(err) orelse "unknown";
const caught = try_catch.caughtOrError(page.call_arena, error.Unknown);
log.warn(.js, "eval script", .{
.url = url,
.err = msg,
.stack = try_catch.stack(page.call_arena) catch null,
.line = try_catch.sourceLineNumber() orelse 0,
.caught = caught,
.cacheable = cacheable,
});
self.executeCallback("error", script_element._on_error, page);
}
fn executeCallback(self: *const Script, comptime typ: []const u8, cb_: ?js.Function, page: *Page) void {
const cb = cb_ orelse return;
fn executeCallback(self: *const Script, comptime typ: []const u8, cb_: ?js.Function.Global, page: *Page) void {
const cb_global = cb_ orelse return;
const cb = cb_global.local();
const Event = @import("webapi/Event.zig");
const event = Event.initTrusted(typ, .{}, page) catch |err| {
@@ -859,13 +861,12 @@ pub const Script = struct {
return;
};
var result: js.Function.Result = undefined;
cb.tryCall(void, .{event}, &result) catch {
var caught: js.TryCatch.Caught = undefined;
cb.tryCall(void, .{event}, &caught) catch {
log.warn(.js, "script callback", .{
.url = self.url,
.type = typ,
.err = result.exception,
.stack = result.stack,
.caught = caught,
});
};
}

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const log = @import("../log.zig");
@@ -92,7 +93,7 @@ pub fn deinit(self: *Session) void {
// NOTE: the caller is not the owner of the returned value,
// the pointer on Page is just returned as a convenience
pub fn createPage(self: *Session) !*Page {
std.debug.assert(self.page == null);
lp.assert(self.page == null, "Session.createPage - page not null", .{});
const page_arena = &self.browser.page_arena;
_ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
@@ -116,8 +117,7 @@ pub fn createPage(self: *Session) !*Page {
pub fn removePage(self: *Session) void {
// Inform CDP the page is going to be removed, allowing other worlds to remove themselves before the main one
self.browser.notification.dispatch(.page_remove, .{});
std.debug.assert(self.page != null);
lp.assert(self.page != null, "Session.removePage - page is null", .{});
self.page.?.deinit();
self.page = null;

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const Allocator = std.mem.Allocator;
const ResolveOpts = struct {
@@ -93,7 +94,7 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
}
if (std.mem.startsWith(u8, out[in_i..], "../")) {
std.debug.assert(out[out_i - 1] == '/');
lp.assert(out[out_i - 1] == '/', "URL.resolve", .{ .out = out });
if (out_i > path_marker) {
// go back before the /

298
src/browser/color.zig Normal file
View File

@@ -0,0 +1,298 @@
// 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;
pub fn isHexColor(value: []const u8) bool {
if (value.len == 0) {
return false;
}
if (value[0] != '#') {
return false;
}
const hex_part = value[1..];
switch (hex_part.len) {
3, 4, 6, 8 => for (hex_part) |c| if (!std.ascii.isHex(c)) return false,
else => return false,
}
return true;
}
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

@@ -21,18 +21,48 @@ const js = @import("js.zig");
const v8 = js.v8;
const Array = @This();
js_arr: v8.Array,
context: *js.Context,
ctx: *js.Context,
handle: *const v8.Array,
pub fn len(self: Array) usize {
return @intCast(self.js_arr.length());
return v8.v8__Array__Length(self.handle);
}
pub fn get(self: Array, index: usize) !js.Value {
const idx_key = v8.Integer.initU32(self.context.isolate, @intCast(index));
const js_obj = self.js_arr.castTo(v8.Object);
pub fn get(self: Array, index: u32) !js.Value {
const ctx = self.ctx;
const idx = js.Integer.init(ctx.isolate.handle, index);
const handle = v8.v8__Object__Get(@ptrCast(self.handle), ctx.handle, idx.handle) orelse {
return error.JsException;
};
return .{
.context = self.context,
.js_val = try js_obj.getValue(self.context.v8_context, idx_key.toValue()),
.ctx = self.ctx,
.handle = handle,
};
}
pub fn set(self: Array, index: u32, value: anytype, comptime opts: js.bridge.Caller.CallOpts) !bool {
const ctx = self.ctx;
const js_value = try ctx.zigValueToJs(value, opts);
var out: v8.MaybeBool = undefined;
v8.v8__Object__SetAtIndex(@ptrCast(self.handle), ctx.handle, index, js_value.handle, &out);
return out.has_value;
}
pub fn toObject(self: Array) js.Object {
return .{
.ctx = self.ctx,
.handle = @ptrCast(self.handle),
};
}
pub fn toValue(self: Array) js.Value {
return .{
.ctx = self.ctx,
.handle = @ptrCast(self.handle),
};
}

41
src/browser/js/BigInt.zig Normal file
View File

@@ -0,0 +1,41 @@
// 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 js = @import("js.zig");
const v8 = js.v8;
const BigInt = @This();
handle: *const v8.Integer,
pub fn init(isolate: *v8.Isolate, val: anytype) BigInt {
const handle = switch (@TypeOf(val)) {
i8, i16, i32, i64, isize => v8.v8__BigInt__New(isolate, val).?,
u8, u16, u32, u64, usize => v8.v8__BigInt__NewFromUnsigned(isolate, val).?,
else => |T| @compileError("cannot create v8::BigInt from: " ++ @typeName(T)),
};
return .{ .handle = handle };
}
pub fn getInt64(self: BigInt) i64 {
return v8.v8__BigInt__Int64Value(self.handle, null);
}
pub fn getUint64(self: BigInt) u64 {
return v8.v8__BigInt__Uint64Value(self.handle, null);
}

View File

@@ -1,538 +0,0 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = @import("../../log.zig");
const js = @import("js.zig");
const v8 = js.v8;
const Context = @import("Context.zig");
const Page = @import("../Page.zig");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const CALL_ARENA_RETAIN = 1024 * 16;
// Responsible for calling Zig functions from JS invocations. This could
// probably just contained in ExecutionWorld, but having this specific logic, which
// is somewhat repetitive between constructors, functions, getters, etc contained
// here does feel like it makes it cleaner.
const Caller = @This();
context: *Context,
v8_context: v8.Context,
isolate: v8.Isolate,
call_arena: Allocator,
// info is a v8.PropertyCallbackInfo or a v8.FunctionCallback
// All we really want from it is the isolate.
// executor = Isolate -> getCurrentContext -> getEmbedderData()
pub fn init(info: anytype) Caller {
const isolate = info.getIsolate();
const v8_context = isolate.getCurrentContext();
const context: *Context = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64());
context.call_depth += 1;
return .{
.context = context,
.isolate = isolate,
.v8_context = v8_context,
.call_arena = context.call_arena,
};
}
pub fn deinit(self: *Caller) void {
const context = self.context;
const call_depth = context.call_depth - 1;
// Because of callbacks, calls can be nested. Because of this, we
// can't clear the call_arena after _every_ call. Imagine we have
// arr.forEach((i) => { console.log(i); }
//
// First we call forEach. Inside of our forEach call,
// we call console.log. If we reset the call_arena after this call,
// it'll reset it for the `forEach` call after, which might still
// need the data.
//
// Therefore, we keep a call_depth, and only reset the call_arena
// when a top-level (call_depth == 0) function ends.
if (call_depth == 0) {
const arena: *ArenaAllocator = @ptrCast(@alignCast(context.call_arena.ptr));
_ = arena.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN });
}
context.call_depth = call_depth;
}
pub const CallOpts = struct {
dom_exception: bool = false,
null_as_undefined: bool = false,
as_typed_array: bool = false,
};
pub fn constructor(self: *Caller, comptime T: type, func: anytype, info: v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
if (!info.isConstructCall()) {
self.handleError(T, @TypeOf(func), error.InvalidArgument, info, opts);
return;
}
self._constructor(func, info) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
};
}
pub fn _constructor(self: *Caller, func: anytype, info: v8.FunctionCallbackInfo) !void {
const F = @TypeOf(func);
const args = try self.getArgs(F, 0, info);
const res = @call(.auto, func, args);
const ReturnType = @typeInfo(F).@"fn".return_type orelse {
@compileError(@typeName(F) ++ " has a constructor without a return type");
};
const new_this = info.getThis();
var this = new_this;
if (@typeInfo(ReturnType) == .error_union) {
const non_error_res = res catch |err| return err;
this = (try self.context.mapZigInstanceToJs(this, non_error_res)).castToObject();
} else {
this = (try self.context.mapZigInstanceToJs(this, res)).castToObject();
}
// If we got back a different object (existing wrapper), copy the prototype
// from new object. (this happens when we're upgrading an CustomElement)
if (this.handle != new_this.handle) {
const new_prototype = new_this.getPrototype();
_ = this.setPrototype(self.context.v8_context, new_prototype.castTo(v8.Object));
}
info.getReturnValue().set(this);
}
pub fn method(self: *Caller, comptime T: type, func: anytype, info: v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
self._method(T, func, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
};
}
pub fn _method(self: *Caller, comptime T: type, func: anytype, info: v8.FunctionCallbackInfo, comptime opts: CallOpts) !void {
const F = @TypeOf(func);
var handle_scope: v8.HandleScope = undefined;
handle_scope.init(self.isolate);
defer handle_scope.deinit();
var args = try self.getArgs(F, 1, info);
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
const res = @call(.auto, func, args);
info.getReturnValue().set(try self.context.zigValueToJs(res, opts));
}
pub fn function(self: *Caller, comptime T: type, func: anytype, info: v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
self._function(func, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
};
}
pub fn _function(self: *Caller, func: anytype, info: v8.FunctionCallbackInfo, comptime opts: CallOpts) !void {
const F = @TypeOf(func);
const context = self.context;
const args = try self.getArgs(F, 0, info);
const res = @call(.auto, func, args);
info.getReturnValue().set(try context.zigValueToJs(res, opts));
}
pub fn getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
return self._getIndex(T, func, idx, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
return v8.Intercepted.No;
};
}
pub fn _getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args = try self.getArgs(F, 2, info);
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
@field(args, "1") = idx;
const ret = @call(.auto, func, args);
return self.handleIndexedReturn(T, F, true, ret, info, opts);
}
pub fn getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
return self._getNamedIndex(T, func, name, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
return v8.Intercepted.No;
};
}
pub fn _getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args = try self.getArgs(F, 2, info);
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
@field(args, "1") = try self.nameToString(name);
const ret = @call(.auto, func, args);
return self.handleIndexedReturn(T, F, true, ret, info, opts);
}
pub fn setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, js_value: v8.Value, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
return self._setNamedIndex(T, func, name, js_value, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
return v8.Intercepted.No;
};
}
pub fn _setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, js_value: v8.Value, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args: ParameterTypes(F) = undefined;
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
@field(args, "1") = try self.nameToString(name);
@field(args, "2") = try self.context.jsValueToZig(@TypeOf(@field(args, "2")), js_value);
if (@typeInfo(F).@"fn".params.len == 4) {
@field(args, "3") = self.context.page;
}
const ret = @call(.auto, func, args);
return self.handleIndexedReturn(T, F, false, ret, info, opts);
}
pub fn deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
return self._deleteNamedIndex(T, func, name, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
return v8.Intercepted.No;
};
}
pub fn _deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args: ParameterTypes(F) = undefined;
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
@field(args, "1") = try self.nameToString(name);
if (@typeInfo(F).@"fn".params.len == 3) {
@field(args, "2") = self.context.page;
}
const ret = @call(.auto, func, args);
return self.handleIndexedReturn(T, F, false, ret, info, opts);
}
fn handleIndexedReturn(self: *Caller, comptime T: type, comptime F: type, comptime getter: bool, ret: anytype, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
// need to unwrap this error immediately for when opts.null_as_undefined == true
// and we need to compare it to null;
const non_error_ret = switch (@typeInfo(@TypeOf(ret))) {
.error_union => |eu| blk: {
break :blk ret catch |err| {
// We can't compare err == error.NotHandled if error.NotHandled
// isn't part of the possible error set. So we first need to check
// if error.NotHandled is part of the error set.
if (isInErrorSet(error.NotHandled, eu.error_set)) {
if (err == error.NotHandled) {
return v8.Intercepted.No;
}
}
self.handleError(T, F, err, info, opts);
return v8.Intercepted.No;
};
},
else => ret,
};
if (comptime getter) {
info.getReturnValue().set(try self.context.zigValueToJs(non_error_ret, opts));
}
return v8.Intercepted.Yes;
}
fn isInErrorSet(err: anyerror, comptime T: type) bool {
inline for (@typeInfo(T).error_set.?) |e| {
if (err == @field(anyerror, e.name)) return true;
}
return false;
}
fn namedSetOrDeleteCall(res: anytype, has_value: bool) !u8 {
if (@typeInfo(@TypeOf(res)) == .error_union) {
_ = try res;
}
if (has_value == false) {
return v8.Intercepted.No;
}
return v8.Intercepted.Yes;
}
fn nameToString(self: *Caller, name: v8.Name) ![]const u8 {
return self.context.valueToString(.{ .handle = name.handle }, .{});
}
fn isSelfReceiver(comptime T: type, comptime F: type) bool {
return checkSelfReceiver(T, F, false);
}
fn assertSelfReceiver(comptime T: type, comptime F: type) void {
_ = checkSelfReceiver(T, F, true);
}
fn checkSelfReceiver(comptime T: type, comptime F: type, comptime fail: bool) bool {
const params = @typeInfo(F).@"fn".params;
if (params.len == 0) {
if (fail) {
@compileError(@typeName(F) ++ " must have a self parameter");
}
return false;
}
const first_param = params[0].type.?;
if (first_param != *T and first_param != *const T) {
if (fail) {
@compileError(std.fmt.comptimePrint("The first parameter to {s} must be a *{s} or *const {s}. Got: {s}", .{
@typeName(F),
@typeName(T),
@typeName(T),
@typeName(first_param),
}));
}
return false;
}
return true;
}
fn assertIsPageArg(comptime T: type, comptime F: type, index: comptime_int) void {
const param = @typeInfo(F).@"fn".params[index].type.?;
if (isPage(param)) {
return;
}
@compileError(std.fmt.comptimePrint("The {d} parameter of {s}.{s} must be a *Page or *const Page. Got: {s}", .{ index, @typeName(T), @typeName(F), @typeName(param) }));
}
fn handleError(self: *Caller, comptime T: type, comptime F: type, err: anyerror, info: anytype, comptime opts: CallOpts) void {
const isolate = self.isolate;
if (comptime @import("builtin").mode == .Debug and @hasDecl(@TypeOf(info), "length")) {
if (log.enabled(.js, .warn)) {
self.logFunctionCallError(@typeName(T), @typeName(F), err, info);
}
}
var js_err: ?v8.Value = switch (err) {
error.InvalidArgument => createTypeException(isolate, "invalid argument"),
error.OutOfMemory => js._createException(isolate, "out of memory"),
error.IllegalConstructor => js._createException(isolate, "Illegal Contructor"),
else => blk: {
if (!comptime opts.dom_exception) {
break :blk null;
}
const DOMException = @import("../webapi/DOMException.zig");
const ex = DOMException.fromError(err) orelse break :blk null;
break :blk self.context.zigValueToJs(ex, .{}) catch js._createException(isolate, "internal error");
},
};
if (js_err == null) {
js_err = js._createException(isolate, @errorName(err));
}
const js_exception = isolate.throwException(js_err.?);
info.getReturnValue().setValueHandle(js_exception.handle);
}
// If we call a method in javascript: cat.lives('nine');
//
// Then we'd expect a Zig function with 2 parameters: a self and the string.
// In this case, offset == 1. Offset is always 1 for setters or methods.
//
// Offset is always 0 for constructors.
//
// For constructors, setters and methods, we can further increase offset + 1
// if the first parameter is an instance of Page.
//
// Finally, if the JS function is called with _more_ parameters and
// the last parameter in Zig is an array, we'll try to slurp the additional
// parameters into the array.
fn getArgs(self: *const Caller, comptime F: type, comptime offset: usize, info: anytype) !ParameterTypes(F) {
const context = self.context;
var args: ParameterTypes(F) = undefined;
const params = @typeInfo(F).@"fn".params[offset..];
// Except for the constructor, the first parameter is always `self`
// This isn't something we'll bind from JS, so skip it.
const params_to_map = blk: {
if (params.len == 0) {
return args;
}
// If the last parameter is the Page, set it, and exclude it
// from our params slice, because we don't want to bind it to
// a JS argument
if (comptime isPage(params[params.len - 1].type.?)) {
@field(args, tupleFieldName(params.len - 1 + offset)) = self.context.page;
break :blk params[0 .. params.len - 1];
}
// If the last parameter is a special JsThis, set it, and exclude it
// from our params slice, because we don't want to bind it to
// a JS argument
if (comptime params[params.len - 1].type.? == js.This) {
@field(args, tupleFieldName(params.len - 1 + offset)) = .{ .obj = .{
.context = context,
.js_obj = info.getThis(),
} };
// AND the 2nd last parameter is state
if (params.len > 1 and comptime isPage(params[params.len - 2].type.?)) {
@field(args, tupleFieldName(params.len - 2 + offset)) = self.context.page;
break :blk params[0 .. params.len - 2];
}
break :blk params[0 .. params.len - 1];
}
// we have neither a Page nor a JsObject. All params must be
// bound to a JavaScript value.
break :blk params;
};
if (params_to_map.len == 0) {
return args;
}
const js_parameter_count = info.length();
const last_js_parameter = params_to_map.len - 1;
var is_variadic = false;
{
// This is going to get complicated. If the last Zig parameter
// is a slice AND the corresponding javascript parameter is
// NOT an an array, then we'll treat it as a variadic.
const last_parameter_type = params_to_map[params_to_map.len - 1].type.?;
const last_parameter_type_info = @typeInfo(last_parameter_type);
if (last_parameter_type_info == .pointer and last_parameter_type_info.pointer.size == .slice) {
const slice_type = last_parameter_type_info.pointer.child;
const corresponding_js_value = info.getArg(@as(u32, @intCast(last_js_parameter)));
if (corresponding_js_value.isArray() == false and corresponding_js_value.isTypedArray() == false and slice_type != u8) {
is_variadic = true;
if (js_parameter_count == 0) {
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
} else if (js_parameter_count >= params_to_map.len) {
const arr = try self.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);
for (arr, last_js_parameter..) |*a, i| {
const js_value = info.getArg(@as(u32, @intCast(i)));
a.* = try context.jsValueToZig(slice_type, js_value);
}
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = arr;
} else {
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
}
}
}
}
inline for (params_to_map, 0..) |param, i| {
const field_index = comptime i + offset;
if (comptime i == params_to_map.len - 1) {
if (is_variadic) {
break;
}
}
if (comptime isPage(param.type.?)) {
@compileError("Page must be the last parameter (or 2nd last if there's a JsThis): " ++ @typeName(F));
} else if (comptime param.type.? == js.This) {
@compileError("JsThis must be the last parameter: " ++ @typeName(F));
} else if (i >= js_parameter_count) {
if (@typeInfo(param.type.?) != .optional) {
return error.InvalidArgument;
}
@field(args, tupleFieldName(field_index)) = null;
} else {
const js_value = info.getArg(@as(u32, @intCast(i)));
@field(args, tupleFieldName(field_index)) = context.jsValueToZig(param.type.?, js_value) catch {
return error.InvalidArgument;
};
}
}
return args;
}
// This is extracted to speed up compilation. When left inlined in handleError,
// this can add as much as 10 seconds of compilation time.
fn logFunctionCallError(self: *Caller, type_name: []const u8, func: []const u8, err: anyerror, info: v8.FunctionCallbackInfo) void {
const args_dump = self.serializeFunctionArgs(info) catch "failed to serialize args";
log.info(.js, "function call error", .{
.type = type_name,
.func = func,
.err = err,
.args = args_dump,
.stack = self.context.stackTrace() catch |err1| @errorName(err1),
});
}
fn serializeFunctionArgs(self: *Caller, info: v8.FunctionCallbackInfo) ![]const u8 {
const context = self.context;
var buf = std.Io.Writer.Allocating.init(context.call_arena);
const separator = log.separator();
for (0..info.length()) |i| {
try buf.writer.print("{s}{d} - ", .{ separator, i + 1 });
try context.debugValue(info.getArg(@intCast(i)), &buf.writer);
}
return buf.written();
}
// Takes a function, and returns a tuple for its argument. Used when we
// @call a function
fn ParameterTypes(comptime F: type) type {
const params = @typeInfo(F).@"fn".params;
var fields: [params.len]std.builtin.Type.StructField = undefined;
inline for (params, 0..) |param, i| {
fields[i] = .{
.name = tupleFieldName(i),
.type = param.type.?,
.default_value_ptr = null,
.is_comptime = false,
.alignment = @alignOf(param.type.?),
};
}
return @Type(.{ .@"struct" = .{
.layout = .auto,
.decls = &.{},
.fields = &fields,
.is_tuple = true,
} });
}
fn tupleFieldName(comptime i: usize) [:0]const u8 {
return switch (i) {
0 => "0",
1 => "1",
2 => "2",
3 => "3",
4 => "4",
5 => "5",
6 => "6",
7 => "7",
8 => "8",
9 => "9",
else => std.fmt.comptimePrint("{d}", .{i}),
};
}
fn isPage(comptime T: type) bool {
return T == *Page or T == *const Page;
}
fn createTypeException(isolate: v8.Isolate, msg: []const u8) v8.Value {
return v8.Exception.initTypeError(v8.String.initUtf8(isolate, msg));
}

File diff suppressed because it is too large Load Diff

View File

@@ -46,58 +46,66 @@ allocator: Allocator,
platform: *const Platform,
// the global isolate
isolate: v8.Isolate,
isolate: js.Isolate,
// just kept around because we need to free it on deinit
isolate_params: *v8.CreateParams,
context_id: usize,
// Global handles that need to be freed on deinit
eternal_function_templates: []v8.Eternal,
// Dynamic slice to avoid circular dependency on JsApis.len at comptime
templates: []v8.FunctionTemplate,
templates: []*const v8.FunctionTemplate,
pub fn init(allocator: Allocator, platform: *const Platform, snapshot: *Snapshot) !Env {
var params = try allocator.create(v8.CreateParams);
errdefer allocator.destroy(params);
v8.c.v8__Isolate__CreateParams__CONSTRUCT(params);
v8.v8__Isolate__CreateParams__CONSTRUCT(params);
params.snapshot_blob = @ptrCast(&snapshot.startup_data);
params.array_buffer_allocator = v8.createDefaultArrayBufferAllocator();
errdefer v8.destroyArrayBufferAllocator(params.array_buffer_allocator.?);
params.array_buffer_allocator = v8.v8__ArrayBuffer__Allocator__NewDefaultAllocator().?;
errdefer v8.v8__ArrayBuffer__Allocator__DELETE(params.array_buffer_allocator.?);
params.external_references = &snapshot.external_references;
var isolate = v8.Isolate.init(params);
var isolate = js.Isolate.init(params);
errdefer isolate.deinit();
// This is the callback that runs whenever a module is dynamically imported.
isolate.setHostImportModuleDynamicallyCallback(Context.dynamicModuleCallback);
isolate.setPromiseRejectCallback(promiseRejectCallback);
isolate.setMicrotasksPolicy(v8.c.kExplicit);
v8.v8__Isolate__SetHostImportModuleDynamicallyCallback(isolate.handle, Context.dynamicModuleCallback);
v8.v8__Isolate__SetPromiseRejectCallback(isolate.handle, promiseRejectCallback);
v8.v8__Isolate__SetMicrotasksPolicy(isolate.handle, v8.kExplicit);
v8.v8__Isolate__SetFatalErrorHandler(isolate.handle, fatalCallback);
v8.v8__Isolate__SetOOMErrorHandler(isolate.handle, oomCallback);
isolate.enter();
errdefer isolate.exit();
isolate.setHostInitializeImportMetaObjectCallback(Context.metaObjectCallback);
v8.v8__Isolate__SetHostInitializeImportMetaObjectCallback(isolate.handle, Context.metaObjectCallback);
// Allocate templates array dynamically to avoid comptime dependency on JsApis.len
const templates = try allocator.alloc(v8.FunctionTemplate, JsApis.len);
// Allocate arrays dynamically to avoid comptime dependency on JsApis.len
const eternal_function_templates = try allocator.alloc(v8.Eternal, JsApis.len);
errdefer allocator.free(eternal_function_templates);
const templates = try allocator.alloc(*const v8.FunctionTemplate, JsApis.len);
errdefer allocator.free(templates);
{
var temp_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&temp_scope, isolate);
var temp_scope: js.HandleScope = undefined;
temp_scope.init(isolate);
defer temp_scope.deinit();
const context = v8.Context.init(isolate, null, null);
context.enter();
defer context.exit();
inline for (JsApis, 0..) |JsApi, i| {
JsApi.Meta.class_id = i;
const data = context.getDataFromSnapshotOnce(snapshot.data_start + i);
const function = v8.FunctionTemplate{ .handle = @ptrCast(data) };
templates[i] = v8.Persistent(v8.FunctionTemplate).init(isolate, function).castToFunctionTemplate();
const data = v8.v8__Isolate__GetDataFromSnapshotOnce(isolate.handle, snapshot.data_start + i);
const function_handle: *const v8.FunctionTemplate = @ptrCast(data);
// Make function template eternal
v8.v8__Eternal__New(isolate.handle, @ptrCast(function_handle), &eternal_function_templates[i]);
// Extract the local handle from the global for easy access
const eternal_ptr = v8.v8__Eternal__Get(&eternal_function_templates[i], isolate.handle);
templates[i] = @ptrCast(@alignCast(eternal_ptr.?));
}
}
@@ -108,19 +116,24 @@ pub fn init(allocator: Allocator, platform: *const Platform, snapshot: *Snapshot
.allocator = allocator,
.templates = templates,
.isolate_params = params,
.eternal_function_templates = eternal_function_templates,
};
}
pub fn deinit(self: *Env) void {
self.allocator.free(self.templates);
self.allocator.free(self.eternal_function_templates);
self.isolate.exit();
self.isolate.deinit();
v8.destroyArrayBufferAllocator(self.isolate_params.array_buffer_allocator.?);
v8.v8__ArrayBuffer__Allocator__DELETE(self.isolate_params.array_buffer_allocator.?);
self.allocator.destroy(self.isolate_params);
self.allocator.free(self.templates);
}
pub fn newInspector(self: *Env, arena: Allocator, ctx: anytype) !Inspector {
return Inspector.init(arena, self.isolate, ctx);
pub fn newInspector(self: *Env, arena: Allocator, ctx: anytype) !*Inspector {
const inspector = try arena.create(Inspector);
try Inspector.init(inspector, self.isolate.handle, ctx);
return inspector;
}
pub fn runMicrotasks(self: *const Env) void {
@@ -128,11 +141,11 @@ pub fn runMicrotasks(self: *const Env) void {
}
pub fn pumpMessageLoop(self: *const Env) bool {
return self.platform.inner.pumpMessageLoop(self.isolate, false);
return v8.v8__Platform__PumpMessageLoop(self.platform.handle, self.isolate.handle, false);
}
pub fn runIdleTasks(self: *const Env) void {
return self.platform.inner.runIdleTasks(self.isolate, 1);
v8.v8__Platform__RunIdleTasks(self.platform.handle, self.isolate.handle, 1);
}
pub fn newExecutionWorld(self: *Env) !ExecutionWorld {
return .{
@@ -147,8 +160,8 @@ pub fn newExecutionWorld(self: *Env) !ExecutionWorld {
// `lowMemoryNotification` call on the isolate to encourage v8 to free
// any contexts which have been freed.
pub fn lowMemoryNotification(self: *Env) void {
var handle_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&handle_scope, self.isolate);
var handle_scope: js.HandleScope = undefined;
handle_scope.init(self.isolate);
defer handle_scope.deinit();
self.isolate.lowMemoryNotification();
}
@@ -174,14 +187,15 @@ pub fn dumpMemoryStats(self: *Env) void {
, .{ stats.total_heap_size, stats.total_heap_size_executable, stats.total_physical_size, stats.total_available_size, stats.used_heap_size, stats.heap_size_limit, stats.malloced_memory, stats.external_memory, stats.peak_malloced_memory, stats.number_of_native_contexts, stats.number_of_detached_contexts, stats.total_global_handles_size, stats.used_global_handles_size, stats.does_zap_garbage });
}
fn promiseRejectCallback(v8_msg: v8.C_PromiseRejectMessage) callconv(.c) void {
const msg = v8.PromiseRejectMessage.initFromC(v8_msg);
const isolate = msg.getPromise().toObject().getIsolate();
const context = Context.fromIsolate(isolate);
fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) void {
const promise_handle = v8.v8__PromiseRejectMessage__GetPromise(&message_handle).?;
const isolate_handle = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?;
const js_isolate = js.Isolate{ .handle = isolate_handle };
const context = Context.fromIsolate(js_isolate);
const value =
if (msg.getValue()) |v8_value|
context.valueToString(v8_value, .{}) catch |err| @errorName(err)
if (v8.v8__PromiseRejectMessage__GetValue(&message_handle)) |v8_value|
context.valueToString(.{ .ctx = context, .handle = v8_value }, .{}) catch |err| @errorName(err)
else
"no value";
@@ -191,3 +205,17 @@ fn promiseRejectCallback(v8_msg: v8.C_PromiseRejectMessage) callconv(.c) void {
.note = "This should be updated to call window.unhandledrejection",
});
}
fn fatalCallback(c_location: [*c]const u8, c_message: [*c]const u8) callconv(.c) void {
const location = std.mem.span(c_location);
const message = std.mem.span(c_message);
log.fatal(.app, "V8 fatal callback", .{ .location = location, .message = message });
@import("../../crash_handler.zig").crash("Fatal V8 Error", .{ .location = location, .message = message }, @returnAddress());
}
fn oomCallback(c_location: [*c]const u8, details: ?*const v8.OOMDetails) callconv(.c) void {
const location = std.mem.span(c_location);
const detail = if (details) |d| std.mem.span(d.detail) else "";
log.fatal(.app, "V8 OOM", .{ .location = location, .detail = detail });
@import("../../crash_handler.zig").crash("V8 OOM", .{ .location = location, .detail = detail }, @returnAddress());
}

View File

@@ -17,19 +17,20 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const IS_DEBUG = @import("builtin").mode == .Debug;
const lp = @import("lightpanda");
const log = @import("../../log.zig");
const Page = @import("../Page.zig");
const js = @import("js.zig");
const v8 = js.v8;
const Env = @import("Env.zig");
const bridge = @import("bridge.zig");
const Context = @import("Context.zig");
const Page = @import("../Page.zig");
const ArenaAllocator = std.heap.ArenaAllocator;
const IS_DEBUG = @import("builtin").mode == .Debug;
const CONTEXT_ARENA_RETAIN = 1024 * 64;
@@ -52,6 +53,7 @@ context_arena: ArenaAllocator,
// does all the work, but having all page-specific data structures
// grouped together helps keep things clean.
context: ?Context = null,
persisted_context: ?js.Global(Context) = null,
// no init, must be initialized via env.newExecutionWorld()
@@ -59,53 +61,58 @@ pub fn deinit(self: *ExecutionWorld) void {
if (self.context != null) {
self.removeContext();
}
self.context_arena.deinit();
}
// Only the top Context in the Main ExecutionWorld should hold a handle_scope.
// A v8.HandleScope is like an arena. Once created, any "Local" that
// A js.HandleScope is like an arena. Once created, any "Local" that
// v8 creates will be released (or at least, releasable by the v8 GC)
// when the handle_scope is freed.
// We also maintain our own "context_arena" which allows us to have
// all page related memory easily managed.
pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context {
std.debug.assert(self.context == null);
lp.assert(self.context == null, "ExecptionWorld.createContext has context", .{});
const env = self.env;
const isolate = env.isolate;
const arena = self.context_arena.allocator();
var v8_context: v8.Context = blk: {
var temp_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&temp_scope, isolate);
const persisted_context: js.Global(Context) = blk: {
var temp_scope: js.HandleScope = undefined;
temp_scope.init(isolate);
defer temp_scope.deinit();
// Creates a global template that inherits from Window.
const global_template = @import("Snapshot.zig").createGlobalTemplate(isolate, env.templates);
// Getting this into the snapshot is tricky (anything involving the
// global is tricky). Easier to do here
const global_template = @import("Snapshot.zig").createGlobalTemplate(isolate.handle, env.templates);
v8.v8__ObjectTemplate__SetNamedHandler(global_template, &.{
.getter = bridge.unknownPropertyCallback,
.setter = null,
.query = null,
.deleter = null,
.enumerator = null,
.definer = null,
.descriptor = null,
.data = null,
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
});
// Add the named property handler
global_template.setNamedProperty(v8.NamedPropertyHandlerConfiguration{
.getter = unknownPropertyCallback,
.flags = v8.PropertyHandlerFlags.NonMasking | v8.PropertyHandlerFlags.OnlyInterceptStrings,
}, null);
const context_local = v8.Context.init(isolate, global_template, null);
const v8_context = v8.Persistent(v8.Context).init(isolate, context_local).castToContext();
break :blk v8_context;
const context_handle = v8.v8__Context__New(isolate.handle, global_template, null).?;
break :blk js.Global(Context).init(isolate.handle, context_handle);
};
// For a Page we only create one HandleScope, it is stored in the main World (enter==true). A page can have multple contexts, 1 for each World.
// The main Context that enters and holds the HandleScope should therefore always be created first. Following other worlds for this page
// like isolated Worlds, will thereby place their objects on the main page's HandleScope. Note: In the furure the number of context will multiply multiple frames support
var handle_scope: ?v8.HandleScope = null;
const v8_context = persisted_context.local();
var handle_scope: ?js.HandleScope = null;
if (enter) {
handle_scope = @as(v8.HandleScope, undefined);
v8.HandleScope.init(&handle_scope.?, isolate);
v8_context.enter();
handle_scope = @as(js.HandleScope, undefined);
handle_scope.?.init(isolate);
v8.v8__Context__Enter(v8_context);
}
errdefer if (enter) {
v8_context.exit();
v8.v8__Context__Exit(v8_context);
handle_scope.?.deinit();
};
@@ -116,33 +123,34 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context
.page = page,
.id = context_id,
.isolate = isolate,
.v8_context = v8_context,
.handle = v8_context,
.templates = env.templates,
.handle_scope = handle_scope,
.script_manager = &page._script_manager,
.call_arena = page.call_arena,
.arena = arena,
};
self.persisted_context = persisted_context;
var context = &self.context.?;
// Store a pointer to our context inside the v8 context so that, given
// a v8 context, we can get our context out
const data = isolate.initBigIntU64(@intCast(@intFromPtr(context)));
v8_context.setEmbedderData(1, data);
const data = isolate.initBigInt(@intFromPtr(context));
v8.v8__Context__SetEmbedderData(context.handle, 1, @ptrCast(data.handle));
try context.setupGlobal();
return context;
}
pub fn removeContext(self: *ExecutionWorld) void {
// Force running the micro task to drain the queue before reseting the
// context arena.
// Tasks in the queue are relying to the arena memory could be present in
// the queue. Running them later could lead to invalid memory accesses.
self.env.runMicrotasks();
self.context.?.deinit();
var context = &(self.context orelse return);
context.deinit();
self.context = null;
self.persisted_context.?.deinit();
self.persisted_context = null;
self.env.isolate.notifyContextDisposed();
_ = self.context_arena.reset(.{ .retain_with_limit = CONTEXT_ARENA_RETAIN });
}
@@ -153,56 +161,3 @@ pub fn terminateExecution(self: *const ExecutionWorld) void {
pub fn resumeExecution(self: *const ExecutionWorld) void {
self.env.isolate.cancelTerminateExecution();
}
pub fn unknownPropertyCallback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
const context = Context.fromIsolate(info.getIsolate());
const maybe_property: ?[]u8 = context.valueToString(.{ .handle = c_name.? }, .{}) catch null;
const ignored = std.StaticStringMap(void).initComptime(.{
.{ "process", {} },
.{ "ShadyDOM", {} },
.{ "ShadyCSS", {} },
.{ "litNonce", {} },
.{ "litHtmlVersions", {} },
.{ "litElementVersions", {} },
.{ "litHtmlPolyfillSupport", {} },
.{ "litElementHydrateSupport", {} },
.{ "litElementPolyfillSupport", {} },
.{ "reactiveElementVersions", {} },
.{ "recaptcha", {} },
.{ "grecaptcha", {} },
.{ "___grecaptcha_cfg", {} },
.{ "__recaptcha_api", {} },
.{ "__google_recaptcha_client", {} },
.{ "CLOSURE_FLAGS", {} },
});
if (maybe_property) |prop| {
if (!ignored.has(prop)) {
const page = context.page;
const document = page.document;
if (document.getElementById(prop, page)) |el| {
const js_value = context.zigValueToJs(el, .{}) catch {
return v8.Intercepted.No;
};
info.getReturnValue().set(js_value);
return v8.Intercepted.Yes;
}
log.debug(.unknown_prop, "unknown global property", .{
.info = "but the property can exist in pure JS",
.stack = context.stackTrace() catch "???",
.property = prop,
});
}
}
return v8.Intercepted.No;
}

View File

@@ -20,70 +20,49 @@ const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const PersistentFunction = v8.Persistent(v8.Function);
const Allocator = std.mem.Allocator;
const Function = @This();
id: usize,
context: *js.Context,
this: ?v8.Object = null,
func: PersistentFunction,
ctx: *js.Context,
this: ?*const v8.Object = null,
handle: *const v8.Function,
pub const Result = struct {
stack: ?[]const u8,
exception: []const u8,
};
pub fn getName(self: *const Function, allocator: Allocator) ![]const u8 {
const name = self.func.castToFunction().getName();
return self.context.valueToString(name, .{ .allocator = allocator });
}
pub fn setName(self: *const Function, name: []const u8) void {
const v8_name = v8.String.initUtf8(self.context.isolate, name);
self.func.castToFunction().setName(v8_name);
}
pub fn withThis(self: *const Function, value: anytype) !Function {
const this_obj = if (@TypeOf(value) == js.Object)
value.js_obj
value.handle
else
(try self.context.zigValueToJs(value, .{})).castTo(v8.Object);
(try self.ctx.zigValueToJs(value, .{})).handle;
return .{
.id = self.id,
.ctx = self.ctx,
.this = this_obj,
.func = self.func,
.context = self.context,
.handle = self.handle,
};
}
pub fn newInstance(self: *const Function, result: *Result) !js.Object {
const context = self.context;
pub fn newInstance(self: *const Function, caught: *js.TryCatch.Caught) !js.Object {
const ctx = self.ctx;
var try_catch: js.TryCatch = undefined;
try_catch.init(context);
try_catch.init(ctx);
defer try_catch.deinit();
// This creates a new instance using this Function as a constructor.
// This returns a generic Object
const js_obj = self.func.castToFunction().initInstance(context.v8_context, &.{}) orelse {
if (try_catch.hasCaught()) {
const allocator = context.call_arena;
result.stack = try_catch.stack(allocator) catch null;
result.exception = (try_catch.exception(allocator) catch "???") orelse "???";
} else {
result.stack = null;
result.exception = "???";
}
// const c_args = @as(?[*]const ?*c.Value, @ptrCast(&.{}));
const handle = v8.v8__Function__NewInstance(self.handle, ctx.handle, 0, null) orelse {
caught.* = try_catch.caughtOrError(ctx.call_arena, error.Unknown);
return error.JsConstructorFailed;
};
return .{
.context = context,
.js_obj = js_obj,
.ctx = ctx,
.handle = handle,
};
}
@@ -91,30 +70,24 @@ pub fn call(self: *const Function, comptime T: type, args: anytype) !T {
return self.callWithThis(T, self.getThis(), args);
}
pub fn tryCall(self: *const Function, comptime T: type, args: anytype, result: *Result) !T {
return self.tryCallWithThis(T, self.getThis(), args, result);
pub fn tryCall(self: *const Function, comptime T: type, args: anytype, caught: *js.TryCatch.Caught) !T {
return self.tryCallWithThis(T, self.getThis(), args, caught);
}
pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, result: *Result) !T {
pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught) !T {
var try_catch: js.TryCatch = undefined;
try_catch.init(self.context);
try_catch.init(self.ctx);
defer try_catch.deinit();
return self.callWithThis(T, this, args) catch |err| {
if (try_catch.hasCaught()) {
const allocator = self.context.call_arena;
result.stack = try_catch.stack(allocator) catch null;
result.exception = (try_catch.exception(allocator) catch @errorName(err)) orelse @errorName(err);
} else {
result.stack = null;
result.exception = @errorName(err);
}
caught.* = try_catch.caughtOrError(self.ctx.call_arena, err);
return err;
};
}
pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T {
const context = self.context;
const ctx = self.ctx;
// When we're calling a function from within JavaScript itself, this isn't
// necessary. We're within a Caller instantiation, which will already have
@@ -125,65 +98,111 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args
// need to increase the call_depth so that the call_arena remains valid for
// the duration of the function call. If we don't do this, the call_arena
// will be reset after each statement of the function which executes Zig code.
const call_depth = context.call_depth;
context.call_depth = call_depth + 1;
defer context.call_depth = call_depth;
const call_depth = ctx.call_depth;
ctx.call_depth = call_depth + 1;
defer ctx.call_depth = call_depth;
const js_this = blk: {
if (@TypeOf(this) == v8.Object) {
if (@TypeOf(this) == js.Object) {
break :blk this;
}
if (@TypeOf(this) == js.Object) {
break :blk this.js_obj;
}
break :blk try context.zigValueToJs(this, .{});
break :blk try ctx.zigValueToJs(this, .{});
};
const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args;
const js_args: []const v8.Value = switch (@typeInfo(@TypeOf(aargs))) {
const js_args: []const *const v8.Value = switch (@typeInfo(@TypeOf(aargs))) {
.@"struct" => |s| blk: {
const fields = s.fields;
var js_args: [fields.len]v8.Value = undefined;
var js_args: [fields.len]*const v8.Value = undefined;
inline for (fields, 0..) |f, i| {
js_args[i] = try context.zigValueToJs(@field(aargs, f.name), .{});
js_args[i] = (try ctx.zigValueToJs(@field(aargs, f.name), .{})).handle;
}
const cargs: [fields.len]v8.Value = js_args;
const cargs: [fields.len]*const v8.Value = js_args;
break :blk &cargs;
},
.pointer => blk: {
var values = try context.call_arena.alloc(v8.Value, args.len);
var values = try ctx.call_arena.alloc(*const v8.Value, args.len);
for (args, 0..) |a, i| {
values[i] = try context.zigValueToJs(a, .{});
values[i] = (try ctx.zigValueToJs(a, .{})).handle;
}
break :blk values;
},
else => @compileError("JS Function called with invalid paremter type"),
};
const result = self.func.castToFunction().call(context.v8_context, js_this, js_args);
if (result == null) {
const c_args = @as(?[*]const ?*v8.Value, @ptrCast(js_args.ptr));
const handle = v8.v8__Function__Call(self.handle, ctx.handle, js_this.handle, @as(c_int, @intCast(js_args.len)), c_args) orelse {
// std.debug.print("CB ERR: {s}\n", .{self.src() catch "???"});
return error.JSExecCallback;
}
};
if (@typeInfo(T) == .void) return {};
return context.jsValueToZig(T, result.?);
if (@typeInfo(T) == .void) {
return {};
}
return ctx.jsValueToZig(T, .{ .ctx = ctx, .handle = handle });
}
fn getThis(self: *const Function) v8.Object {
return self.this orelse self.context.v8_context.getGlobal();
fn getThis(self: *const Function) js.Object {
const handle = if (self.this) |t| t else v8.v8__Context__Global(self.ctx.handle).?;
return .{
.ctx = self.ctx,
.handle = handle,
};
}
pub fn src(self: *const Function) ![]const u8 {
const value = self.func.castToFunction().toValue();
return self.context.valueToString(value, .{});
return self.context.valueToString(.{ .handle = @ptrCast(self.handle) }, .{});
}
pub fn getPropertyValue(self: *const Function, name: []const u8) !?js.Value {
const func_obj = self.func.castToFunction().toObject();
const key = v8.String.initUtf8(self.context.isolate, name);
const value = func_obj.getValue(self.context.v8_context, key) catch return null;
return self.context.createValue(value);
const ctx = self.ctx;
const key = ctx.isolate.initStringHandle(name);
const handle = v8.v8__Object__Get(self.handle, ctx.handle, key) orelse {
return error.JsException;
};
return .{
.ctx = ctx,
.handle = handle,
};
}
pub fn persist(self: *const Function) !Global {
var ctx = self.ctx;
var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
try ctx.global_functions.append(ctx.arena, global);
return .{
.handle = global,
.ctx = ctx,
};
}
pub fn persistWithThis(self: *const Function, value: anytype) !Global {
const with_this = try self.withThis(value);
return with_this.persist();
}
pub const Global = struct {
handle: v8.Global,
ctx: *js.Context,
pub fn deinit(self: *Global) void {
v8.v8__Global__Reset(&self.handle);
}
pub fn local(self: *const Global) Function {
return .{
.ctx = self.ctx,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)),
};
}
pub fn isEqual(self: *const Global, other: Function) bool {
return v8.v8__Global__IsEqual(&self.handle, other.handle);
}
};

View File

@@ -0,0 +1,36 @@
// 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 js = @import("js.zig");
const v8 = js.v8;
const HandleScope = @This();
handle: v8.HandleScope,
// V8 takes an address of the value that's passed in, so it needs to be stable.
// We can't create the v8.HandleScope here, pass it to v8 and then return the
// value, as v8 will then have taken the address of the function-scopped (and no
// longer valid) local.
pub fn init(self: *HandleScope, isolate: js.Isolate) void {
v8.v8__HandleScope__CONSTRUCT(&self.handle, isolate.handle);
}
pub fn deinit(self: *HandleScope) void {
v8.v8__HandleScope__DESTRUCT(&self.handle);
}

View File

@@ -23,48 +23,87 @@ const v8 = js.v8;
const Context = @import("Context.zig");
const Allocator = std.mem.Allocator;
const RndGen = std.Random.DefaultPrng;
const CONTEXT_GROUP_ID = 1;
const CLIENT_TRUST_LEVEL = 1;
const Inspector = @This();
pub const RemoteObject = v8.RemoteObject;
isolate: v8.Isolate,
inner: *v8.Inspector,
session: v8.InspectorSession,
handle: *v8.Inspector,
isolate: *v8.Isolate,
client: Client,
channel: Channel,
session: Session,
rnd: RndGen = RndGen.init(0),
default_context: ?*const v8.Context = null,
// We expect allocator to be an arena
pub fn init(allocator: Allocator, isolate: v8.Isolate, ctx: anytype) !Inspector {
// Note: This initializes the pre-allocated inspector in-place
pub fn init(self: *Inspector, isolate: *v8.Isolate, ctx: anytype) !void {
const ContextT = @TypeOf(ctx);
const InspectorContainer = switch (@typeInfo(ContextT)) {
const Container = switch (@typeInfo(ContextT)) {
.@"struct" => ContextT,
.pointer => |ptr| ptr.child,
.void => NoopInspector,
else => @compileError("invalid context type"),
};
// If necessary, turn a void context into something we can safely ptrCast
const safe_context: *anyopaque = if (ContextT == void) @ptrCast(@constCast(&{})) else ctx;
const channel = v8.InspectorChannel.init(
// Initialize the fields that callbacks need first
self.* = .{
.handle = undefined,
.isolate = isolate,
.client = undefined,
.channel = undefined,
.rnd = RndGen.init(0),
.default_context = null,
.session = undefined,
};
// Create client and set inspector data BEFORE creating the inspector
// because V8 will call generateUniqueId during inspector creation
const client = Client.init();
self.client = client;
client.setInspector(self);
// Now create the inspector - generateUniqueId will work because data is set
const handle = v8.v8_inspector__Inspector__Create(isolate, client.handle).?;
self.handle = handle;
// Create the channel
const channel = Channel.init(
safe_context,
InspectorContainer.onInspectorResponse,
InspectorContainer.onInspectorEvent,
InspectorContainer.onRunMessageLoopOnPause,
InspectorContainer.onQuitMessageLoopOnPause,
Container.onInspectorResponse,
Container.onInspectorEvent,
Container.onRunMessageLoopOnPause,
Container.onQuitMessageLoopOnPause,
isolate,
);
self.channel = channel;
channel.setInspector(self);
const client = v8.InspectorClient.init();
const inner = try allocator.create(v8.Inspector);
v8.Inspector.init(inner, client, channel, isolate);
return .{ .inner = inner, .isolate = isolate, .session = inner.connect() };
// Create the session
const session_handle = v8.v8_inspector__Inspector__Connect(
handle,
CONTEXT_GROUP_ID,
channel.handle,
CLIENT_TRUST_LEVEL,
).?;
self.session = .{ .handle = session_handle };
}
pub fn deinit(self: *const Inspector) void {
var temp_scope: v8.HandleScope = undefined;
v8.v8__HandleScope__CONSTRUCT(&temp_scope, self.isolate);
defer v8.v8__HandleScope__DESTRUCT(&temp_scope);
self.session.deinit();
self.inner.deinit();
self.client.deinit();
self.channel.deinit();
v8.v8_inspector__Inspector__DELETE(self.handle);
}
pub fn send(self: *const Inspector, msg: []const u8) void {
@@ -73,8 +112,8 @@ pub fn send(self: *const Inspector, msg: []const u8) void {
// comes and goes, but CDP can keep sending messages.
const isolate = self.isolate;
var temp_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&temp_scope, isolate);
defer temp_scope.deinit();
v8.v8__HandleScope__CONSTRUCT(&temp_scope, isolate);
defer v8.v8__HandleScope__DESTRUCT(&temp_scope);
self.session.dispatchProtocolMessage(isolate, msg);
}
@@ -88,20 +127,34 @@ pub fn send(self: *const Inspector, msg: []const u8) void {
// {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}
// - is_default_context: Whether the execution context is default, should match the auxData
pub fn contextCreated(
self: *const Inspector,
self: *Inspector,
context: *const Context,
name: []const u8,
origin: []const u8,
aux_data: ?[]const u8,
aux_data: []const u8,
is_default_context: bool,
) void {
self.inner.contextCreated(context.v8_context, name, origin, aux_data, is_default_context);
v8.v8_inspector__Inspector__ContextCreated(
self.handle,
name.ptr,
name.len,
origin.ptr,
origin.len,
aux_data.ptr,
aux_data.len,
CONTEXT_GROUP_ID,
context.handle,
);
if (is_default_context) {
self.default_context = context.handle;
}
}
// Retrieves the RemoteObject for a given value.
// The value is loaded through the ExecutionWorld's mapZigInstanceToJs function,
// just like a method return value. Therefore, if we've mapped this
// value before, we'll get the existing JS PersistedObject and if not
// value before, we'll get the existing js.Global(js.Object) and if not
// we'll create it and track it for cleanup when the context ends.
pub fn getRemoteObject(
self: *const Inspector,
@@ -114,9 +167,9 @@ pub fn getRemoteObject(
// We do not want to expose this as a parameter for now
const generate_preview = false;
return self.session.wrapObject(
context.isolate,
context.v8_context,
js_value,
context.isolate.handle,
context.handle,
js_value.handle,
group,
generate_preview,
);
@@ -132,15 +185,209 @@ pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []con
const unwrapped = try self.session.unwrapObject(allocator, object_id);
// The values context and groupId are not used here
const js_val = unwrapped.value;
if (js_val.isObject() == false) {
if (!v8.v8__Value__IsObject(js_val)) {
return error.ObjectIdIsNotANode;
}
const Node = @import("../webapi/Node.zig");
return Context.typeTaggedAnyOpaque(*Node, js_val.castTo(v8.Object)) catch {
// Cast to *const v8.Object for typeTaggedAnyOpaque
return Context.typeTaggedAnyOpaque(*Node, @ptrCast(js_val)) catch {
return error.ObjectIdIsNotANode;
};
}
pub const RemoteObject = struct {
handle: *v8.RemoteObject,
pub fn deinit(self: RemoteObject) void {
v8.v8_inspector__RemoteObject__DELETE(self.handle);
}
pub fn getType(self: RemoteObject, allocator: Allocator) ![]const u8 {
var ctype_: v8.CZigString = .{ .ptr = null, .len = 0 };
if (!v8.v8_inspector__RemoteObject__getType(self.handle, &allocator, &ctype_)) return error.V8AllocFailed;
return cZigStringToString(ctype_) orelse return error.InvalidType;
}
pub fn getSubtype(self: RemoteObject, allocator: Allocator) !?[]const u8 {
if (!v8.v8_inspector__RemoteObject__hasSubtype(self.handle)) return null;
var csubtype: v8.CZigString = .{ .ptr = null, .len = 0 };
if (!v8.v8_inspector__RemoteObject__getSubtype(self.handle, &allocator, &csubtype)) return error.V8AllocFailed;
return cZigStringToString(csubtype);
}
pub fn getClassName(self: RemoteObject, allocator: Allocator) !?[]const u8 {
if (!v8.v8_inspector__RemoteObject__hasClassName(self.handle)) return null;
var cclass_name: v8.CZigString = .{ .ptr = null, .len = 0 };
if (!v8.v8_inspector__RemoteObject__getClassName(self.handle, &allocator, &cclass_name)) return error.V8AllocFailed;
return cZigStringToString(cclass_name);
}
pub fn getDescription(self: RemoteObject, allocator: Allocator) !?[]const u8 {
if (!v8.v8_inspector__RemoteObject__hasDescription(self.handle)) return null;
var description: v8.CZigString = .{ .ptr = null, .len = 0 };
if (!v8.v8_inspector__RemoteObject__getDescription(self.handle, &allocator, &description)) return error.V8AllocFailed;
return cZigStringToString(description);
}
pub fn getObjectId(self: RemoteObject, allocator: Allocator) !?[]const u8 {
if (!v8.v8_inspector__RemoteObject__hasObjectId(self.handle)) return null;
var cobject_id: v8.CZigString = .{ .ptr = null, .len = 0 };
if (!v8.v8_inspector__RemoteObject__getObjectId(self.handle, &allocator, &cobject_id)) return error.V8AllocFailed;
return cZigStringToString(cobject_id);
}
};
const Session = struct {
handle: *v8.InspectorSession,
fn deinit(self: Session) void {
v8.v8_inspector__Session__DELETE(self.handle);
}
fn dispatchProtocolMessage(self: Session, isolate: *v8.Isolate, msg: []const u8) void {
v8.v8_inspector__Session__dispatchProtocolMessage(
self.handle,
isolate,
msg.ptr,
msg.len,
);
}
fn wrapObject(
self: Session,
isolate: *v8.Isolate,
ctx: *const v8.Context,
val: *const v8.Value,
grpname: []const u8,
generatepreview: bool,
) !RemoteObject {
const remote_object = v8.v8_inspector__Session__wrapObject(
self.handle,
isolate,
ctx,
val,
grpname.ptr,
grpname.len,
generatepreview,
).?;
return .{ .handle = remote_object };
}
fn unwrapObject(
self: Session,
allocator: Allocator,
object_id: []const u8,
) !UnwrappedObject {
const in_object_id = v8.CZigString{
.ptr = object_id.ptr,
.len = object_id.len,
};
var out_error: v8.CZigString = .{ .ptr = null, .len = 0 };
var out_value_handle: ?*v8.Value = null;
var out_context_handle: ?*v8.Context = null;
var out_object_group: v8.CZigString = .{ .ptr = null, .len = 0 };
const result = v8.v8_inspector__Session__unwrapObject(
self.handle,
&allocator,
&out_error,
in_object_id,
&out_value_handle,
&out_context_handle,
&out_object_group,
);
if (!result) {
const error_str = cZigStringToString(out_error) orelse return error.UnwrapFailed;
std.log.err("unwrapObject failed: {s}", .{error_str});
return error.UnwrapFailed;
}
return .{
.value = out_value_handle.?,
.context = out_context_handle.?,
.object_group = cZigStringToString(out_object_group),
};
}
};
const UnwrappedObject = struct {
value: *const v8.Value,
context: *const v8.Context,
object_group: ?[]const u8,
};
const Channel = struct {
handle: *v8.InspectorChannelImpl,
// callbacks
ctx: *anyopaque,
onNotif: onNotifFn = undefined,
onResp: onRespFn = undefined,
onRunMessageLoopOnPause: onRunMessageLoopOnPauseFn = undefined,
onQuitMessageLoopOnPause: onQuitMessageLoopOnPauseFn = undefined,
pub const onNotifFn = *const fn (ctx: *anyopaque, msg: []const u8) void;
pub const onRespFn = *const fn (ctx: *anyopaque, call_id: u32, msg: []const u8) void;
pub const onRunMessageLoopOnPauseFn = *const fn (ctx: *anyopaque, context_group_id: u32) void;
pub const onQuitMessageLoopOnPauseFn = *const fn (ctx: *anyopaque) void;
fn init(
ctx: *anyopaque,
onResp: onRespFn,
onNotif: onNotifFn,
onRunMessageLoopOnPause: onRunMessageLoopOnPauseFn,
onQuitMessageLoopOnPause: onQuitMessageLoopOnPauseFn,
isolate: *v8.Isolate,
) Channel {
const handle = v8.v8_inspector__Channel__IMPL__CREATE(isolate);
return .{
.handle = handle,
.ctx = ctx,
.onResp = onResp,
.onNotif = onNotif,
.onRunMessageLoopOnPause = onRunMessageLoopOnPause,
.onQuitMessageLoopOnPause = onQuitMessageLoopOnPause,
};
}
fn deinit(self: Channel) void {
v8.v8_inspector__Channel__IMPL__DELETE(self.handle);
}
fn setInspector(self: Channel, inspector: *anyopaque) void {
v8.v8_inspector__Channel__IMPL__SET_DATA(self.handle, inspector);
}
fn resp(self: Channel, call_id: u32, msg: []const u8) void {
self.onResp(self.ctx, call_id, msg);
}
fn notif(self: Channel, msg: []const u8) void {
self.onNotif(self.ctx, msg);
}
};
const Client = struct {
handle: *v8.InspectorClientImpl,
fn init() Client {
return .{ .handle = v8.v8_inspector__Client__IMPL__CREATE() };
}
fn deinit(self: Client) void {
v8.v8_inspector__Client__IMPL__DELETE(self.handle);
}
fn setInspector(self: Client, inspector: *anyopaque) void {
v8.v8_inspector__Client__IMPL__SET_DATA(self.handle, inspector);
}
};
const NoopInspector = struct {
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}
@@ -148,15 +395,107 @@ const NoopInspector = struct {
pub fn onQuitMessageLoopOnPause(_: *anyopaque) void {}
};
pub fn getTaggedAnyOpaque(value: v8.Value) ?*js.TaggedAnyOpaque {
if (value.isObject() == false) {
fn fromData(data: *anyopaque) *Inspector {
return @ptrCast(@alignCast(data));
}
pub fn getTaggedAnyOpaque(value: *const v8.Value) ?*js.TaggedAnyOpaque {
if (!v8.v8__Value__IsObject(value)) {
return null;
}
const obj = value.castTo(v8.Object);
if (obj.internalFieldCount() == 0) {
const internal_field_count = v8.v8__Object__InternalFieldCount(value);
if (internal_field_count == 0) {
return null;
}
const external_data = obj.getInternalField(0).castTo(v8.External).get().?;
const external_value = v8.v8__Object__GetInternalField(value, 0).?;
const external_data = v8.v8__External__Value(external_value).?;
return @ptrCast(@alignCast(external_data));
}
fn cZigStringToString(s: v8.CZigString) ?[]const u8 {
if (s.ptr == null) return null;
return s.ptr[0..s.len];
}
// C export functions for Inspector callbacks
pub export fn v8_inspector__Client__IMPL__generateUniqueId(
_: *v8.InspectorClientImpl,
data: *anyopaque,
) callconv(.c) i64 {
const inspector: *Inspector = @ptrCast(@alignCast(data));
return inspector.rnd.random().int(i64);
}
pub export fn v8_inspector__Client__IMPL__runMessageLoopOnPause(
_: *v8.InspectorClientImpl,
data: *anyopaque,
ctx_group_id: c_int,
) callconv(.c) void {
const inspector: *Inspector = @ptrCast(@alignCast(data));
inspector.channel.onRunMessageLoopOnPause(inspector.channel.ctx, @intCast(ctx_group_id));
}
pub export fn v8_inspector__Client__IMPL__quitMessageLoopOnPause(
_: *v8.InspectorClientImpl,
data: *anyopaque,
) callconv(.c) void {
const inspector: *Inspector = @ptrCast(@alignCast(data));
inspector.channel.onQuitMessageLoopOnPause(inspector.channel.ctx);
}
pub export fn v8_inspector__Client__IMPL__runIfWaitingForDebugger(
_: *v8.InspectorClientImpl,
_: *anyopaque,
_: c_int,
) callconv(.c) void {
// TODO
}
pub export fn v8_inspector__Client__IMPL__consoleAPIMessage(
_: *v8.InspectorClientImpl,
_: *anyopaque,
_: c_int,
_: v8.MessageErrorLevel,
_: *v8.StringView,
_: *v8.StringView,
_: c_uint,
_: c_uint,
_: *v8.StackTrace,
) callconv(.c) void {}
pub export fn v8_inspector__Client__IMPL__ensureDefaultContextInGroup(
_: *v8.InspectorClientImpl,
data: *anyopaque,
) callconv(.c) ?*const v8.Context {
const inspector: *Inspector = @ptrCast(@alignCast(data));
return inspector.default_context;
}
pub export fn v8_inspector__Channel__IMPL__sendResponse(
_: *v8.InspectorChannelImpl,
data: *anyopaque,
call_id: c_int,
msg: [*c]u8,
length: usize,
) callconv(.c) void {
const inspector: *Inspector = @ptrCast(@alignCast(data));
inspector.channel.resp(@as(u32, @intCast(call_id)), msg[0..length]);
}
pub export fn v8_inspector__Channel__IMPL__sendNotification(
_: *v8.InspectorChannelImpl,
data: *anyopaque,
msg: [*c]u8,
length: usize,
) callconv(.c) void {
const inspector: *Inspector = @ptrCast(@alignCast(data));
inspector.channel.notif(msg[0..length]);
}
pub export fn v8_inspector__Channel__IMPL__flushProtocolNotifications(
_: *v8.InspectorChannelImpl,
_: *anyopaque,
) callconv(.c) void {
// TODO
}

View File

@@ -19,22 +19,17 @@
const std = @import("std");
const js = @import("js.zig");
// This only exists so that we know whether a function wants the opaque
// JS argument (js.Object), or if it wants the receiver as an opaque
// value.
// js.Object is normally used when a method wants an opaque JS object
// that it'll pass into a callback.
// This is used when the function wants to do advanced manipulation
// of the v8.Object bound to the instance. For example, postAttach is an
// example of using This.
const v8 = js.v8;
const This = @This();
obj: js.Object,
const Integer = @This();
pub fn setIndex(self: This, index: u32, value: anytype, opts: js.Object.SetOpts) !void {
return self.obj.setIndex(index, value, opts);
}
pub fn set(self: This, key: []const u8, value: anytype, opts: js.Object.SetOpts) !void {
return self.obj.set(key, value, opts);
handle: *const v8.Integer,
pub fn init(isolate: *v8.Isolate, value: anytype) Integer {
const handle = switch (@TypeOf(value)) {
i8, i16, i32 => v8.v8__Integer__New(isolate, value).?,
u8, u16, u32 => v8.v8__Integer__NewFromUnsigned(isolate, value).?,
else => |T| @compileError("cannot create v8::Integer from: " ++ @typeName(T)),
};
return .{ .handle = handle };
}

118
src/browser/js/Isolate.zig Normal file
View File

@@ -0,0 +1,118 @@
// 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 js = @import("js.zig");
const v8 = js.v8;
const Isolate = @This();
handle: *v8.Isolate,
pub fn init(params: *v8.CreateParams) Isolate {
return .{
.handle = v8.v8__Isolate__New(params).?,
};
}
pub fn deinit(self: Isolate) void {
v8.v8__Isolate__Dispose(self.handle);
}
pub fn enter(self: Isolate) void {
v8.v8__Isolate__Enter(self.handle);
}
pub fn exit(self: Isolate) void {
v8.v8__Isolate__Exit(self.handle);
}
pub fn performMicrotasksCheckpoint(self: Isolate) void {
v8.v8__Isolate__PerformMicrotaskCheckpoint(self.handle);
}
pub fn enqueueMicrotask(self: Isolate, callback: anytype, data: anytype) void {
v8.v8__Isolate__EnqueueMicrotask(self.handle, callback, data);
}
pub fn enqueueMicrotaskFunc(self: Isolate, function: js.Function) void {
v8.v8__Isolate__EnqueueMicrotaskFunc(self.handle, function.handle);
}
pub fn lowMemoryNotification(self: Isolate) void {
v8.v8__Isolate__LowMemoryNotification(self.handle);
}
pub fn notifyContextDisposed(self: Isolate) void {
_ = v8.v8__Isolate__ContextDisposedNotification(self.handle);
}
pub fn getHeapStatistics(self: Isolate) v8.HeapStatistics {
var res: v8.HeapStatistics = undefined;
v8.v8__Isolate__GetHeapStatistics(self.handle, &res);
return res;
}
pub fn throwException(self: Isolate, value: *const v8.Value) *const v8.Value {
return v8.v8__Isolate__ThrowException(self.handle, value).?;
}
pub fn initStringHandle(self: Isolate, str: []const u8) *const v8.String {
return v8.v8__String__NewFromUtf8(self.handle, str.ptr, v8.kNormal, @as(c_int, @intCast(str.len))).?;
}
pub fn createError(self: Isolate, msg: []const u8) *const v8.Value {
const message = self.initStringHandle(msg);
return v8.v8__Exception__Error(message).?;
}
pub fn createTypeError(self: Isolate, msg: []const u8) *const v8.Value {
const message = self.initStringHandle(msg);
return v8.v8__Exception__TypeError(message).?;
}
pub fn initNull(self: Isolate) *const v8.Value {
return v8.v8__Null(self.handle).?;
}
pub fn initUndefined(self: Isolate) *const v8.Value {
return v8.v8__Undefined(self.handle).?;
}
pub fn initFalse(self: Isolate) *const v8.Value {
return v8.v8__False(self.handle).?;
}
pub fn initTrue(self: Isolate) *const v8.Value {
return v8.v8__True(self.handle).?;
}
pub fn initInteger(self: Isolate, val: anytype) js.Integer {
return js.Integer.init(self.handle, val);
}
pub fn initBigInt(self: Isolate, val: anytype) js.BigInt {
return js.BigInt.init(self.handle, val);
}
pub fn initNumber(self: Isolate, val: anytype) js.Number {
return js.Number.init(self.handle, val);
}
pub fn createExternal(self: Isolate, val: *anyopaque) *const v8.External {
return v8.v8__External__New(self.handle, val).?;
}

142
src/browser/js/Module.zig Normal file
View File

@@ -0,0 +1,142 @@
// 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 js = @import("js.zig");
const v8 = js.v8;
const Module = @This();
ctx: *js.Context,
handle: *const v8.Module,
pub const Status = enum(u32) {
kUninstantiated = v8.kUninstantiated,
kInstantiating = v8.kInstantiating,
kInstantiated = v8.kInstantiated,
kEvaluating = v8.kEvaluating,
kEvaluated = v8.kEvaluated,
kErrored = v8.kErrored,
};
pub fn getStatus(self: Module) Status {
return @enumFromInt(v8.v8__Module__GetStatus(self.handle));
}
pub fn getException(self: Module) js.Value {
return .{
.ctx = self.ctx,
.handle = v8.v8__Module__GetException(self.handle).?,
};
}
pub fn getModuleRequests(self: Module) Requests {
return .{
.ctx = self.ctx.handle,
.handle = v8.v8__Module__GetModuleRequests(self.handle).?,
};
}
pub fn instantiate(self: Module, cb: v8.ResolveModuleCallback) !bool {
var out: v8.MaybeBool = undefined;
v8.v8__Module__InstantiateModule(self.handle, self.ctx.handle, cb, &out);
if (out.has_value) {
return out.value;
}
return error.JsException;
}
pub fn evaluate(self: Module) !js.Value {
const ctx = self.ctx;
const res = v8.v8__Module__Evaluate(self.handle, ctx.handle) orelse return error.JsException;
if (self.getStatus() == .kErrored) {
return error.JsException;
}
return .{
.ctx = ctx,
.handle = res,
};
}
pub fn getIdentityHash(self: Module) u32 {
return @bitCast(v8.v8__Module__GetIdentityHash(self.handle));
}
pub fn getModuleNamespace(self: Module) js.Value {
return .{
.ctx = self.ctx,
.handle = v8.v8__Module__GetModuleNamespace(self.handle).?,
};
}
pub fn getScriptId(self: Module) u32 {
return @intCast(v8.v8__Module__ScriptId(self.handle));
}
pub fn persist(self: Module) !Global {
var ctx = self.ctx;
var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
try ctx.global_modules.append(ctx.arena, global);
return .{
.handle = global,
.ctx = ctx,
};
}
pub const Global = struct {
handle: v8.Global,
ctx: *js.Context,
pub fn deinit(self: *Global) void {
v8.v8__Global__Reset(&self.handle);
}
pub fn local(self: *const Global) Module {
return .{
.ctx = self.ctx,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)),
};
}
pub fn isEqual(self: *const Global, other: Module) bool {
return v8.v8__Global__IsEqual(&self.handle, other.handle);
}
};
const Requests = struct {
ctx: *const v8.Context,
handle: *const v8.FixedArray,
pub fn len(self: Requests) usize {
return @intCast(v8.v8__FixedArray__Length(self.handle));
}
pub fn get(self: Requests, idx: usize) Request {
return .{ .handle = v8.v8__FixedArray__Get(self.handle, self.ctx, @intCast(idx)).? };
}
};
const Request = struct {
handle: *const v8.ModuleRequest,
pub fn specifier(self: Request) *const v8.String {
return v8.v8__ModuleRequest__GetSpecifier(self.handle).?;
}
};

24
src/browser/js/Name.zig Normal file
View File

@@ -0,0 +1,24 @@
// 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 js = @import("js.zig");
const v8 = js.v8;
const Name = @This();
handle: *const v8.Name,

31
src/browser/js/Number.zig Normal file
View File

@@ -0,0 +1,31 @@
// 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 js = @import("js.zig");
const v8 = js.v8;
const Number = @This();
handle: *const v8.Number,
pub fn init(isolate: *v8.Isolate, value: anytype) Number {
const handle = v8.v8__Number__New(isolate, value).?;
return .{ .handle = handle };
}

View File

@@ -23,86 +23,97 @@ const v8 = js.v8;
const IS_DEBUG = @import("builtin").mode == .Debug;
const Context = @import("Context.zig");
const PersistentObject = v8.Persistent(v8.Object);
const Allocator = std.mem.Allocator;
const Object = @This();
js_obj: v8.Object,
context: *js.Context,
ctx: *js.Context,
handle: *const v8.Object,
pub fn getId(self: Object) u32 {
return self.js_obj.getIdentityHash();
return @bitCast(v8.v8__Object__GetIdentityHash(self.handle));
}
pub const SetOpts = packed struct(u32) {
READ_ONLY: bool = false,
DONT_ENUM: bool = false,
DONT_DELETE: bool = false,
_: u29 = 0,
};
pub fn setIndex(self: Object, index: u32, value: anytype, opts: SetOpts) !void {
@setEvalBranchQuota(10000);
const key = switch (index) {
inline 0...20 => |i| std.fmt.comptimePrint("{d}", .{i}),
else => try std.fmt.allocPrint(self.context.arena, "{d}", .{index}),
pub fn has(self: Object, key: anytype) bool {
const ctx = self.ctx;
const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key);
var out: v8.MaybeBool = undefined;
v8.v8__Object__Has(self.handle, self.ctx.handle, key_handle, &out);
if (out.has_value) {
return out.value;
}
return false;
}
pub fn get(self: Object, key: anytype) !js.Value {
const ctx = self.ctx;
const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key);
const js_val_handle = v8.v8__Object__Get(self.handle, ctx.handle, key_handle) orelse return error.JsException;
return .{
.ctx = ctx,
.handle = js_val_handle,
};
return self.set(key, value, opts);
}
pub fn set(self: Object, key: []const u8, value: anytype, opts: SetOpts) error{ FailedToSet, OutOfMemory }!void {
const context = self.context;
pub fn set(self: Object, key: anytype, value: anytype, comptime opts: js.bridge.Caller.CallOpts) !bool {
const ctx = self.ctx;
const js_key = v8.String.initUtf8(context.isolate, key);
const js_value = try context.zigValueToJs(value, .{});
const js_value = try ctx.zigValueToJs(value, opts);
const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key);
const res = self.js_obj.defineOwnProperty(context.v8_context, js_key.toName(), js_value, @bitCast(opts)) orelse false;
if (!res) {
return error.FailedToSet;
var out: v8.MaybeBool = undefined;
v8.v8__Object__Set(self.handle, ctx.handle, key_handle, js_value.handle, &out);
return out.has_value;
}
pub fn defineOwnProperty(self: Object, name: []const u8, value: js.Value, attr: v8.PropertyAttribute) ?bool {
const ctx = self.ctx;
const name_handle = ctx.isolate.initStringHandle(name);
var out: v8.MaybeBool = undefined;
v8.v8__Object__DefineOwnProperty(self.handle, ctx.handle, @ptrCast(name_handle), value.handle, attr, &out);
if (out.has_value) {
return out.value;
} else {
return null;
}
}
pub fn get(self: Object, key: []const u8) !js.Value {
const context = self.context;
const js_key = v8.String.initUtf8(context.isolate, key);
const js_val = try self.js_obj.getValue(context.v8_context, js_key);
return context.createValue(js_val);
}
pub fn isTruthy(self: Object) bool {
const js_value = self.js_obj.toValue();
return js_value.toBool(self.context.isolate);
}
pub fn toString(self: Object) ![]const u8 {
const js_value = self.js_obj.toValue();
return self.context.valueToString(js_value, .{});
return self.ctx.valueToString(self.toValue(), .{});
}
pub fn toValue(self: Object) js.Value {
return .{
.ctx = self.ctx,
.handle = @ptrCast(self.handle),
};
}
pub fn format(self: Object, writer: *std.Io.Writer) !void {
if (comptime IS_DEBUG) {
return self.context.debugValue(self.js_obj.toValue(), writer);
return self.ctx.debugValue(self.toValue(), writer);
}
const str = self.toString() catch return error.WriteFailed;
return writer.writeAll(str);
}
pub fn toJson(self: Object, allocator: Allocator) ![]u8 {
const json_string = try v8.Json.stringify(self.context.v8_context, self.js_obj.toValue(), null);
const str = try self.context.jsStringToZig(json_string, .{ .allocator = allocator });
return str;
}
pub fn persist(self: Object) !Global {
var ctx = self.ctx;
pub fn persist(self: Object) !Object {
var context = self.context;
const js_obj = self.js_obj;
var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
const persisted = PersistentObject.init(context.isolate, js_obj);
try context.js_object_list.append(context.arena, persisted);
try ctx.global_objects.append(ctx.arena, global);
return .{
.context = context,
.js_obj = persisted.castToObject(),
.handle = global,
.ctx = ctx,
};
}
@@ -110,15 +121,18 @@ pub fn getFunction(self: Object, name: []const u8) !?js.Function {
if (self.isNullOrUndefined()) {
return null;
}
const context = self.context;
const ctx = self.ctx;
const js_name = v8.String.initUtf8(context.isolate, name);
const js_name = ctx.isolate.initStringHandle(name);
const js_val_handle = v8.v8__Object__Get(self.handle, ctx.handle, js_name) orelse return error.JsException;
const js_value = try self.js_obj.getValue(context.v8_context, js_name.toName());
if (!js_value.isFunction()) {
if (v8.v8__Value__IsFunction(js_val_handle) == false) {
return null;
}
return try context.createFunction(js_value);
return .{
.ctx = ctx,
.handle = @ptrCast(js_val_handle),
};
}
pub fn callMethod(self: Object, comptime T: type, method_name: []const u8, args: anytype) !T {
@@ -126,41 +140,69 @@ pub fn callMethod(self: Object, comptime T: type, method_name: []const u8, args:
return func.callWithThis(T, self, args);
}
pub fn isNull(self: Object) bool {
return self.js_obj.toValue().isNull();
}
pub fn isUndefined(self: Object) bool {
return self.js_obj.toValue().isUndefined();
}
pub fn isNullOrUndefined(self: Object) bool {
return self.js_obj.toValue().isNullOrUndefined();
return v8.v8__Value__IsNullOrUndefined(@ptrCast(self.handle));
}
pub fn getOwnPropertyNames(self: Object) js.Array {
const handle = v8.v8__Object__GetOwnPropertyNames(self.handle, self.ctx.handle).?;
return .{
.ctx = self.ctx,
.handle = handle,
};
}
pub fn getPropertyNames(self: Object) js.Array {
const handle = v8.v8__Object__GetPropertyNames(self.handle, self.ctx.handle).?;
return .{
.ctx = self.ctx,
.handle = handle,
};
}
pub fn nameIterator(self: Object) NameIterator {
const context = self.context;
const js_obj = self.js_obj;
const ctx = self.ctx;
const array = js_obj.getPropertyNames(context.v8_context);
const count = array.length();
const handle = v8.v8__Object__GetPropertyNames(self.handle, ctx.handle).?;
const count = v8.v8__Array__Length(handle);
return .{
.ctx = ctx,
.handle = handle,
.count = count,
.context = context,
.js_obj = array.castTo(v8.Object),
};
}
pub fn toZig(self: Object, comptime T: type) !T {
return self.context.jsValueToZig(T, self.js_obj.toValue());
const js_value = js.Value{ .ctx = self.ctx, .handle = @ptrCast(self.handle) };
return self.ctx.jsValueToZig(T, js_value);
}
pub const Global = struct {
handle: v8.Global,
ctx: *js.Context,
pub fn deinit(self: *Global) void {
v8.v8__Global__Reset(&self.handle);
}
pub fn local(self: *const Global) Object {
return .{
.ctx = self.ctx,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)),
};
}
pub fn isEqual(self: *const Global, other: Object) bool {
return v8.v8__Global__IsEqual(&self.handle, other.handle);
}
};
pub const NameIterator = struct {
count: u32,
idx: u32 = 0,
js_obj: v8.Object,
context: *const Context,
ctx: *Context,
handle: *const v8.Array,
pub fn next(self: *NameIterator) !?[]const u8 {
const idx = self.idx;
@@ -169,8 +211,8 @@ pub const NameIterator = struct {
}
self.idx += 1;
const context = self.context;
const js_val = try self.js_obj.getAtIndex(context.v8_context, idx);
return try context.valueToString(js_val, .{});
const js_val_handle = v8.v8__Object__GetIndex(@ptrCast(self.handle), self.ctx.handle, idx) orelse return error.JsException;
const js_val = js.Value{ .ctx = self.ctx, .handle = js_val_handle };
return try self.ctx.valueToString(js_val, .{});
}
};

View File

@@ -20,20 +20,22 @@ const js = @import("js.zig");
const v8 = js.v8;
const Platform = @This();
inner: v8.Platform,
handle: *v8.Platform,
pub fn init() !Platform {
if (v8.initV8ICU() == false) {
if (v8.v8__V8__InitializeICU() == false) {
return error.FailedToInitializeICU;
}
const platform = v8.Platform.initDefault(0, true);
v8.initV8Platform(platform);
v8.initV8();
return .{ .inner = platform };
// 0 - threadpool size, 0 == let v8 decide
// 1 - idle_task_support, 1 == enabled
const handle = v8.v8__Platform__NewDefaultPlatform(0, 1).?;
v8.v8__V8__InitializePlatform(handle);
v8.v8__V8__Initialize();
return .{ .handle = handle };
}
pub fn deinit(self: Platform) void {
_ = v8.deinitV8();
v8.deinitV8Platform();
self.inner.deinit();
_ = v8.v8__V8__Dispose();
v8.v8__V8__DisposePlatform();
v8.v8__Platform__DELETE(self.handle);
}

View File

@@ -0,0 +1,83 @@
// 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 js = @import("js.zig");
const v8 = js.v8;
const Promise = @This();
ctx: *js.Context,
handle: *const v8.Promise,
pub fn toObject(self: Promise) js.Object {
return .{
.ctx = self.ctx,
.handle = @ptrCast(self.handle),
};
}
pub fn toValue(self: Promise) js.Value {
return .{
.ctx = self.ctx,
.handle = @ptrCast(self.handle),
};
}
pub fn thenAndCatch(self: Promise, on_fulfilled: js.Function, on_rejected: js.Function) !Promise {
if (v8.v8__Promise__Then2(self.handle, self.ctx.handle, on_fulfilled.handle, on_rejected.handle)) |handle| {
return .{
.ctx = self.ctx,
.handle = handle,
};
}
return error.PromiseChainFailed;
}
pub fn persist(self: Promise) !Global {
var ctx = self.ctx;
var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
try ctx.global_promises.append(ctx.arena, global);
return .{
.handle = global,
.ctx = ctx,
};
}
pub const Global = struct {
handle: v8.Global,
ctx: *js.Context,
pub fn deinit(self: *Global) void {
v8.v8__Global__Reset(&self.handle);
}
pub fn local(self: *const Global) Promise {
return .{
.ctx = self.ctx,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)),
};
}
pub fn isEqual(self: *const Global, other: Promise) bool {
return v8.v8__Global__IsEqual(&self.handle, other.handle);
}
pub fn promise(self: *const Global) Promise {
return self.local();
}
};

View File

@@ -0,0 +1,107 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("js.zig");
const v8 = js.v8;
const log = @import("../../log.zig");
const PromiseResolver = @This();
ctx: *js.Context,
handle: *const v8.PromiseResolver,
pub fn init(ctx: *js.Context) PromiseResolver {
return .{
.ctx = ctx,
.handle = v8.v8__Promise__Resolver__New(ctx.handle).?,
};
}
pub fn promise(self: PromiseResolver) js.Promise {
return .{
.ctx = self.ctx,
.handle = v8.v8__Promise__Resolver__GetPromise(self.handle).?,
};
}
pub fn resolve(self: PromiseResolver, comptime source: []const u8, value: anytype) void {
self._resolve(value) catch |err| {
log.err(.bug, "resolve", .{ .source = source, .err = err, .persistent = false });
};
}
fn _resolve(self: PromiseResolver, value: anytype) !void {
const ctx: *js.Context = @constCast(self.ctx);
const js_value = try ctx.zigValueToJs(value, .{});
var out: v8.MaybeBool = undefined;
v8.v8__Promise__Resolver__Resolve(self.handle, self.ctx.handle, js_value.handle, &out);
if (!out.has_value or !out.value) {
return error.FailedToResolvePromise;
}
ctx.runMicrotasks();
}
pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype) void {
self._reject(value) catch |err| {
log.err(.bug, "reject", .{ .source = source, .err = err, .persistent = false });
};
}
fn _reject(self: PromiseResolver, value: anytype) !void {
const ctx = self.ctx;
const js_value = try ctx.zigValueToJs(value, .{});
var out: v8.MaybeBool = undefined;
v8.v8__Promise__Resolver__Reject(self.handle, ctx.handle, js_value.handle, &out);
if (!out.has_value or !out.value) {
return error.FailedToRejectPromise;
}
ctx.runMicrotasks();
}
pub fn persist(self: PromiseResolver) !Global {
var ctx = self.ctx;
var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
try ctx.global_promise_resolvers.append(ctx.arena, global);
return .{
.handle = global,
.ctx = ctx,
};
}
pub const Global = struct {
handle: v8.Global,
ctx: *js.Context,
pub fn deinit(self: *Global) void {
v8.v8__Global__Reset(&self.handle);
}
pub fn local(self: *const Global) PromiseResolver {
return .{
.ctx = self.ctx,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)),
};
}
pub fn isEqual(self: *const Global, other: PromiseResolver) bool {
return v8.v8__Global__IsEqual(&self.handle, other.handle);
}
};

View File

@@ -53,14 +53,14 @@ startup_data: v8.StartupData,
external_references: [countExternalReferences()]isize,
// Track whether this snapshot owns its data (was created in-process)
// If false, the data points into embedded_snapshot_blob and should not be freed
// If false, the data points into embedded_snapshot_blob and will not be freed
owns_data: bool = false,
pub fn load(allocator: Allocator) !Snapshot {
pub fn load() !Snapshot {
if (loadEmbedded()) |snapshot| {
return snapshot;
}
return create(allocator);
return create();
}
fn loadEmbedded() ?Snapshot {
@@ -75,7 +75,7 @@ fn loadEmbedded() ?Snapshot {
const blob = embedded_snapshot_blob[@sizeOf(usize)..];
const startup_data = v8.StartupData{ .data = blob.ptr, .raw_size = @intCast(blob.len) };
if (!v8.SnapshotCreator.startupDataIsValid(startup_data)) {
if (!v8.v8__StartupData__IsValid(startup_data)) {
return null;
}
@@ -87,10 +87,11 @@ fn loadEmbedded() ?Snapshot {
};
}
pub fn deinit(self: Snapshot, allocator: Allocator) void {
pub fn deinit(self: Snapshot) void {
// Only free if we own the data (was created in-process)
if (self.owns_data) {
allocator.free(self.startup_data.data[0..@intCast(self.startup_data.raw_size)]);
// V8 allocated this with `new char[]`, so we need to use the C++ delete[] operator
v8.v8__StartupData__DELETE(self.startup_data.data);
}
}
@@ -105,50 +106,53 @@ pub fn write(self: Snapshot, writer: *std.Io.Writer) !void {
pub fn fromEmbedded(self: Snapshot) bool {
// if the snapshot comes from the embedFile, then it'll be flagged as not
// owneing (aka, not needing to free) the data.
// owning (aka, not needing to free) the data.
return self.owns_data == false;
}
fn isValid(self: Snapshot) bool {
return v8.SnapshotCreator.startupDataIsValid(self.startup_data);
return v8.v8__StartupData__IsValid(self.startup_data);
}
pub fn createGlobalTemplate(isolate: v8.Isolate, templates: []const v8.FunctionTemplate) v8.ObjectTemplate {
pub fn createGlobalTemplate(isolate: *v8.Isolate, templates: anytype) *const v8.ObjectTemplate {
// Set up the global template to inherit from Window's template
// This way the global object gets all Window properties through inheritance
const js_global = v8.FunctionTemplate.initDefault(isolate);
js_global.setClassName(v8.String.initUtf8(isolate, "Window"));
const js_global = v8.v8__FunctionTemplate__New__DEFAULT(isolate);
const window_name = v8.v8__String__NewFromUtf8(isolate, "Window", v8.kNormal, 6);
v8.v8__FunctionTemplate__SetClassName(js_global, window_name);
// Find Window in JsApis by name (avoids circular import)
const window_index = comptime bridge.JsApiLookup.getId(Window.JsApi);
js_global.inherit(templates[window_index]);
return js_global.getInstanceTemplate();
v8.v8__FunctionTemplate__Inherit(js_global, templates[window_index]);
return v8.v8__FunctionTemplate__InstanceTemplate(js_global).?;
}
pub fn create(allocator: Allocator) !Snapshot {
pub fn create() !Snapshot {
var external_references = collectExternalReferences();
var params = v8.initCreateParams();
params.array_buffer_allocator = v8.createDefaultArrayBufferAllocator();
defer v8.destroyArrayBufferAllocator(params.array_buffer_allocator.?);
var params: v8.CreateParams = undefined;
v8.v8__Isolate__CreateParams__CONSTRUCT(&params);
params.array_buffer_allocator = v8.v8__ArrayBuffer__Allocator__NewDefaultAllocator();
defer v8.v8__ArrayBuffer__Allocator__DELETE(params.array_buffer_allocator.?);
params.external_references = @ptrCast(&external_references);
var snapshot_creator: v8.SnapshotCreator = undefined;
v8.SnapshotCreator.init(&snapshot_creator, &params);
defer snapshot_creator.deinit();
const snapshot_creator = v8.v8__SnapshotCreator__CREATE(&params);
defer v8.v8__SnapshotCreator__DESTRUCT(snapshot_creator);
var data_start: usize = 0;
const isolate = snapshot_creator.getIsolate();
const isolate = v8.v8__SnapshotCreator__getIsolate(snapshot_creator).?;
{
// CreateBlob, which we'll call once everything is setup, MUST NOT
// be called from an active HandleScope. Hence we have this scope to
// clean it up before we call CreateBlob
var handle_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&handle_scope, isolate);
defer handle_scope.deinit();
v8.v8__HandleScope__CONSTRUCT(&handle_scope, isolate);
defer v8.v8__HandleScope__DESTRUCT(&handle_scope);
// Create templates (constructors only) FIRST
var templates: [JsApis.len]v8.FunctionTemplate = undefined;
var templates: [JsApis.len]*v8.FunctionTemplate = undefined;
inline for (JsApis, 0..) |JsApi, i| {
@setEvalBranchQuota(10_000);
templates[i] = generateConstructor(JsApi, isolate);
@@ -159,23 +163,22 @@ pub fn create(allocator: Allocator) !Snapshot {
// This must come before attachClass so inheritance is set up first
inline for (JsApis, 0..) |JsApi, i| {
if (comptime protoIndexLookup(JsApi)) |proto_index| {
templates[i].inherit(templates[proto_index]);
v8.v8__FunctionTemplate__Inherit(templates[i], templates[proto_index]);
}
}
// Set up the global template to inherit from Window's template
// This way the global object gets all Window properties through inheritance
const global_template = createGlobalTemplate(isolate, templates[0..]);
const context = v8.Context.init(isolate, global_template, null);
context.enter();
defer context.exit();
const context = v8.v8__Context__New(isolate, global_template, null);
v8.v8__Context__Enter(context);
defer v8.v8__Context__Exit(context);
// Add templates to context snapshot
var last_data_index: usize = 0;
inline for (JsApis, 0..) |_, i| {
@setEvalBranchQuota(10_000);
const data_index = snapshot_creator.addDataWithContext(context, @ptrCast(templates[i].handle));
const data_index = v8.v8__SnapshotCreator__AddData(snapshot_creator, @ptrCast(templates[i]));
if (i == 0) {
data_start = data_index;
last_data_index = data_index;
@@ -193,16 +196,18 @@ pub fn create(allocator: Allocator) !Snapshot {
}
// Realize all templates by getting their functions and attaching to global
const global_obj = context.getGlobal();
const global_obj = v8.v8__Context__Global(context);
inline for (JsApis, 0..) |JsApi, i| {
const func = templates[i].getFunction(context);
const func = v8.v8__FunctionTemplate__GetFunction(templates[i], context);
// Attach to global if it has a name
if (@hasDecl(JsApi.Meta, "name")) {
if (@hasDecl(JsApi.Meta, "constructor_alias")) {
const v8_class_name = v8.String.initUtf8(isolate, JsApi.Meta.constructor_alias);
_ = global_obj.setValue(context, v8_class_name, func);
const alias = JsApi.Meta.constructor_alias;
const v8_class_name = v8.v8__String__NewFromUtf8(isolate, alias.ptr, v8.kNormal, @intCast(alias.len));
var maybe_result: v8.MaybeBool = undefined;
v8.v8__Object__Set(global_obj, context, v8_class_name, func, &maybe_result);
// @TODO: This is wrong. This name should be registered with the
// illegalConstructorCallback. I.e. new Image() is OK, but
@@ -210,11 +215,15 @@ pub fn create(allocator: Allocator) !Snapshot {
// But we _have_ to register the name, i.e. HTMLImageElement
// has to be registered so, for now, instead of creating another
// template, we just hook it into the constructor.
const illegal_class_name = v8.String.initUtf8(isolate, JsApi.Meta.name);
_ = global_obj.setValue(context, illegal_class_name, func);
const name = JsApi.Meta.name;
const illegal_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
var maybe_result2: v8.MaybeBool = undefined;
v8.v8__Object__Set(global_obj, context, illegal_class_name, func, &maybe_result2);
} else {
const v8_class_name = v8.String.initUtf8(isolate, JsApi.Meta.name);
_ = global_obj.setValue(context, v8_class_name, func);
const name = JsApi.Meta.name;
const v8_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
var maybe_result: v8.MaybeBool = undefined;
v8.v8__Object__Set(global_obj, context, v8_class_name, func, &maybe_result);
}
}
}
@@ -222,8 +231,10 @@ pub fn create(allocator: Allocator) !Snapshot {
{
// If we want to overwrite the built-in console, we have to
// delete the built-in one.
const console_key = v8.String.initUtf8(isolate, "console");
if (global_obj.deleteValue(context, console_key) == false) {
const console_key = v8.v8__String__NewFromUtf8(isolate, "console", v8.kNormal, 7);
var maybe_deleted: v8.MaybeBool = undefined;
v8.v8__Object__Delete(global_obj, context, console_key, &maybe_deleted);
if (maybe_deleted.value == false) {
return error.ConsoleDeleteError;
}
}
@@ -233,30 +244,36 @@ pub fn create(allocator: Allocator) !Snapshot {
// TODO: see if newer V8 engines have a way around this.
inline for (JsApis, 0..) |JsApi, i| {
if (comptime protoIndexLookup(JsApi)) |proto_index| {
const proto_obj = templates[proto_index].getFunction(context).toObject();
const self_obj = templates[i].getFunction(context).toObject();
_ = self_obj.setPrototype(context, proto_obj);
const proto_func = v8.v8__FunctionTemplate__GetFunction(templates[proto_index], context);
const proto_obj: *const v8.Object = @ptrCast(proto_func);
const self_func = v8.v8__FunctionTemplate__GetFunction(templates[i], context);
const self_obj: *const v8.Object = @ptrCast(self_func);
var maybe_result: v8.MaybeBool = undefined;
v8.v8__Object__SetPrototype(self_obj, context, proto_obj, &maybe_result);
}
}
{
// Custom exception
// TODO: this is an horrible hack, I can't figure out how to do this cleanly.
const code = v8.String.initUtf8(isolate, "DOMException.prototype.__proto__ = Error.prototype");
_ = try (try v8.Script.compile(context, code, null)).run(context);
const code_str = "DOMException.prototype.__proto__ = Error.prototype";
const code = v8.v8__String__NewFromUtf8(isolate, code_str.ptr, v8.kNormal, @intCast(code_str.len));
const script = v8.v8__Script__Compile(context, code, null) orelse return error.ScriptCompileFailed;
_ = v8.v8__Script__Run(script, context) orelse return error.ScriptRunFailed;
}
snapshot_creator.setDefaultContext(context);
v8.v8__SnapshotCreator__setDefaultContext(snapshot_creator, context);
}
const blob = snapshot_creator.createBlob(v8.FunctionCodeHandling.kKeep);
const owned = try allocator.dupe(u8, blob.data[0..@intCast(blob.raw_size)]);
const blob = v8.v8__SnapshotCreator__createBlob(snapshot_creator, v8.kKeep);
return .{
.owns_data = true,
.data_start = data_start,
.external_references = external_references,
.startup_data = .{ .data = owned.ptr, .raw_size = @intCast(owned.len) },
.startup_data = blob,
};
}
@@ -365,7 +382,7 @@ fn collectExternalReferences() [countExternalReferences()]isize {
// via `new ClassName()` - but they could, for example, be created in
// Zig and returned from a function call, which is why we need the
// FunctionTemplate.
fn generateConstructor(comptime JsApi: type, isolate: v8.Isolate) v8.FunctionTemplate {
fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *v8.FunctionTemplate {
const callback = blk: {
if (@hasDecl(JsApi, "constructor")) {
break :blk JsApi.constructor.func;
@@ -375,19 +392,24 @@ fn generateConstructor(comptime JsApi: type, isolate: v8.Isolate) v8.FunctionTem
break :blk illegalConstructorCallback;
};
const template = v8.FunctionTemplate.initCallback(isolate, callback);
const template = @constCast(v8.v8__FunctionTemplate__New__DEFAULT2(isolate, callback).?);
if (!@hasDecl(JsApi.Meta, "empty_with_no_proto")) {
template.getInstanceTemplate().setInternalFieldCount(1);
const instance_template = v8.v8__FunctionTemplate__InstanceTemplate(template);
v8.v8__ObjectTemplate__SetInternalFieldCount(instance_template, 1);
}
const class_name = v8.String.initUtf8(isolate, if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi));
template.setClassName(class_name);
const name_str = if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi);
const class_name = v8.v8__String__NewFromUtf8(isolate, name_str.ptr, v8.kNormal, @intCast(name_str.len));
v8.v8__FunctionTemplate__SetClassName(template, class_name);
return template;
}
// Attaches JsApi members to the prototype template (normal case)
fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.FunctionTemplate) void {
const target = template.getPrototypeTemplate();
fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.FunctionTemplate) void {
const target = v8.v8__FunctionTemplate__PrototypeTemplate(template);
const instance = v8.v8__FunctionTemplate__InstanceTemplate(template);
const declarations = @typeInfo(JsApi).@"struct".decls;
inline for (declarations) |d| {
const name: [:0]const u8 = d.name;
const value = @field(JsApi, name);
@@ -395,60 +417,79 @@ fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.FunctionT
switch (definition) {
bridge.Accessor => {
const js_name = v8.String.initUtf8(isolate, name).toName();
const getter_callback = v8.FunctionTemplate.initCallback(isolate, value.getter);
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
const getter_callback = @constCast(v8.v8__FunctionTemplate__New__DEFAULT2(isolate, value.getter).?);
if (value.setter == null) {
if (value.static) {
template.setAccessorGetter(js_name, getter_callback);
v8.v8__Template__SetAccessorProperty__DEFAULT(@ptrCast(template), js_name, getter_callback);
} else {
target.setAccessorGetter(js_name, getter_callback);
v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT(target, js_name, getter_callback);
}
} else {
std.debug.assert(value.static == false);
const setter_callback = v8.FunctionTemplate.initCallback(isolate, value.setter);
target.setAccessorGetterAndSetter(js_name, getter_callback, setter_callback);
if (comptime IS_DEBUG) {
std.debug.assert(value.static == false);
}
const setter_callback = @constCast(v8.v8__FunctionTemplate__New__DEFAULT2(isolate, value.setter.?).?);
v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT2(target, js_name, getter_callback, setter_callback);
}
},
bridge.Function => {
const function_template = v8.FunctionTemplate.initCallback(isolate, value.func);
const js_name = v8.String.initUtf8(isolate, name).toName();
const function_template = @constCast(v8.v8__FunctionTemplate__New__DEFAULT2(isolate, value.func).?);
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
if (value.static) {
template.set(js_name, function_template, v8.PropertyAttribute.None);
v8.v8__Template__Set(@ptrCast(template), js_name, @ptrCast(function_template), v8.None);
} else {
target.set(js_name, function_template, v8.PropertyAttribute.None);
v8.v8__Template__Set(@ptrCast(target), js_name, @ptrCast(function_template), v8.None);
}
},
bridge.Indexed => {
const configuration = v8.IndexedPropertyHandlerConfiguration{
var configuration: v8.IndexedPropertyHandlerConfiguration = .{
.getter = value.getter,
.setter = null,
.query = null,
.deleter = null,
.enumerator = null,
.definer = null,
.descriptor = null,
.data = null,
.flags = 0,
};
target.setIndexedProperty(configuration, null);
v8.v8__ObjectTemplate__SetIndexedHandler(instance, &configuration);
},
bridge.NamedIndexed => {
var configuration: v8.NamedPropertyHandlerConfiguration = .{
.getter = value.getter,
.setter = value.setter,
.query = null,
.deleter = value.deleter,
.enumerator = null,
.definer = null,
.descriptor = null,
.data = null,
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
};
v8.v8__ObjectTemplate__SetNamedHandler(instance, &configuration);
},
bridge.NamedIndexed => template.getInstanceTemplate().setNamedProperty(.{
.getter = value.getter,
.setter = value.setter,
.deleter = value.deleter,
.flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking,
}, null),
bridge.Iterator => {
const function_template = v8.FunctionTemplate.initCallback(isolate, value.func);
const function_template = @constCast(v8.v8__FunctionTemplate__New__DEFAULT2(isolate, value.func).?);
const js_name = if (value.async)
v8.Symbol.getAsyncIterator(isolate).toName()
v8.v8__Symbol__GetAsyncIterator(isolate)
else
v8.Symbol.getIterator(isolate).toName();
target.set(js_name, function_template, v8.PropertyAttribute.None);
v8.v8__Symbol__GetIterator(isolate);
v8.v8__Template__Set(@ptrCast(target), js_name, @ptrCast(function_template), v8.None);
},
bridge.Property => {
// simpleZigValueToJs now returns raw handle directly
const js_value = switch (value) {
.int => |v| js.simpleZigValueToJs(isolate, v, true, false),
.int => |v| js.simpleZigValueToJs(.{ .handle = isolate }, v, true, false),
};
const js_name = v8.String.initUtf8(isolate, name).toName();
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
// apply it both to the type itself
template.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
v8.v8__Template__Set(@ptrCast(template), js_name, js_value, v8.ReadOnly + v8.DontDelete);
// and to instances of the type
target.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
v8.v8__Template__Set(@ptrCast(target), js_name, js_value, v8.ReadOnly + v8.DontDelete);
},
bridge.Constructor => {}, // already handled in generateConstructor
else => {},
@@ -456,15 +497,14 @@ fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.FunctionT
}
if (@hasDecl(JsApi.Meta, "htmldda")) {
const instance_template = template.getInstanceTemplate();
instance_template.markAsUndetectable();
instance_template.setCallAsFunctionHandler(JsApi.Meta.callable.func);
v8.v8__ObjectTemplate__MarkAsUndetectable(instance);
v8.v8__ObjectTemplate__SetCallAsFunctionHandler(instance, JsApi.Meta.callable.func);
}
if (@hasDecl(JsApi.Meta, "name")) {
const js_name = v8.Symbol.getToStringTag(isolate).toName();
const instance_template = template.getInstanceTemplate();
instance_template.set(js_name, v8.String.initUtf8(isolate, JsApi.Meta.name), v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
const js_name = v8.v8__Symbol__GetToStringTag(isolate);
const js_value = v8.v8__String__NewFromUtf8(isolate, JsApi.Meta.name.ptr, v8.kNormal, @intCast(JsApi.Meta.name.len));
v8.v8__Template__Set(@ptrCast(instance), js_name, js_value, v8.ReadOnly + v8.DontDelete);
}
}
@@ -482,10 +522,15 @@ fn protoIndexLookup(comptime JsApi: type) ?bridge.JsApiLookup.BackingInt {
}
// Shared illegal constructor callback for types without explicit constructors
fn illegalConstructorCallback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
const iso = info.getIsolate();
fn illegalConstructorCallback(raw_info: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const isolate = v8.v8__FunctionCallbackInfo__GetIsolate(raw_info);
log.warn(.js, "Illegal constructor call", .{});
const js_exception = iso.throwException(js._createException(iso, "Illegal Constructor"));
info.getReturnValue().set(js_exception);
const message = v8.v8__String__NewFromUtf8(isolate, "Illegal Constructor", v8.kNormal, 19);
const js_exception = v8.v8__Exception__TypeError(message);
_ = v8.v8__Isolate__ThrowException(isolate, js_exception);
var return_value: v8.ReturnValue = undefined;
v8.v8__FunctionCallbackInfo__GetReturnValue(raw_info, &return_value);
v8.v8__ReturnValue__Set(return_value, js_exception);
}

53
src/browser/js/String.zig Normal file
View File

@@ -0,0 +1,53 @@
// 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 js = @import("js.zig");
const Allocator = std.mem.Allocator;
const v8 = js.v8;
const String = @This();
ctx: *js.Context,
handle: *const v8.String,
pub const ToZigOpts = struct {
allocator: ?Allocator = null,
};
pub fn toZig(self: String, opts: ToZigOpts) ![]u8 {
return self._toZig(false, opts);
}
pub fn toZigZ(self: String, opts: ToZigOpts) ![:0]u8 {
return self._toZig(true, opts);
}
fn _toZig(self: String, comptime null_terminate: bool, opts: ToZigOpts) !(if (null_terminate) [:0]u8 else []u8) {
const isolate = self.ctx.isolate.handle;
const allocator = opts.allocator orelse self.ctx.call_arena;
const len: u32 = @intCast(v8.v8__String__Utf8Length(self.handle, isolate));
const buf = if (null_terminate) try allocator.allocSentinel(u8, len, 0) else try allocator.alloc(u8, len);
const options = v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8;
const n = v8.v8__String__WriteUtf8(self.handle, isolate, buf.ptr, buf.len, options);
std.debug.assert(n == len);
return buf;
}

View File

@@ -1,4 +1,4 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
@@ -24,59 +24,84 @@ const Allocator = std.mem.Allocator;
const TryCatch = @This();
inner: v8.TryCatch,
context: *const js.Context,
ctx: *js.Context,
handle: v8.TryCatch,
pub fn init(self: *TryCatch, context: *const js.Context) void {
self.context = context;
self.inner.init(context.isolate);
pub fn init(self: *TryCatch, ctx: *js.Context) void {
self.ctx = ctx;
v8.v8__TryCatch__CONSTRUCT(&self.handle, ctx.isolate.handle);
}
pub fn hasCaught(self: TryCatch) bool {
return self.inner.hasCaught();
return v8.v8__TryCatch__HasCaught(&self.handle);
}
// the caller needs to deinit the string returned
pub fn exception(self: TryCatch, allocator: Allocator) !?[]const u8 {
const msg = self.inner.getException() orelse return null;
return try self.context.valueToString(msg, .{ .allocator = allocator });
}
// the caller needs to deinit the string returned
pub fn stack(self: TryCatch, allocator: Allocator) !?[]const u8 {
const context = self.context;
const s = self.inner.getStackTrace(context.v8_context) orelse return null;
return try context.valueToString(s, .{ .allocator = allocator });
}
// the caller needs to deinit the string returned
pub fn sourceLine(self: TryCatch, allocator: Allocator) !?[]const u8 {
const context = self.context;
const msg = self.inner.getMessage() orelse return null;
const sl = msg.getSourceLine(context.v8_context) orelse return null;
return try context.jsStringToZig(sl, .{ .allocator = allocator });
}
pub fn sourceLineNumber(self: TryCatch) ?u32 {
const context = self.context;
const msg = self.inner.getMessage() orelse return null;
return msg.getLineNumber(context.v8_context);
}
// a shorthand method to return either the entire stack message
// or just the exception message
// - in Debug mode return the stack if available
// - otherwise return the exception if available
// the caller needs to deinit the string returned
pub fn err(self: TryCatch, allocator: Allocator) !?[]const u8 {
if (comptime @import("builtin").mode == .Debug) {
if (try self.stack(allocator)) |msg| {
return msg;
}
pub fn caught(self: TryCatch, allocator: Allocator) ?Caught {
if (!self.hasCaught()) {
return null;
}
return try self.exception(allocator);
const ctx = self.ctx;
var hs: js.HandleScope = undefined;
hs.init(ctx.isolate);
defer hs.deinit();
const line: ?u32 = blk: {
const handle = v8.v8__TryCatch__Message(&self.handle) orelse return null;
const l = v8.v8__Message__GetLineNumber(handle, ctx.handle);
break :blk if (l < 0) null else @intCast(l);
};
const exception: ?[]const u8 = blk: {
const handle = v8.v8__TryCatch__Exception(&self.handle) orelse break :blk null;
break :blk ctx.valueToString(.{ .ctx = ctx, .handle = handle }, .{ .allocator = allocator }) catch |err| @errorName(err);
};
const stack: ?[]const u8 = blk: {
const handle = v8.v8__TryCatch__StackTrace(&self.handle, ctx.handle) orelse break :blk null;
break :blk ctx.valueToString(.{ .ctx = ctx, .handle = handle }, .{ .allocator = allocator }) catch |err| @errorName(err);
};
return .{
.line = line,
.stack = stack,
.caught = true,
.exception = exception,
};
}
pub fn caughtOrError(self: TryCatch, allocator: Allocator, err: anyerror) Caught {
return self.caught(allocator) orelse .{
.caught = false,
.line = null,
.stack = null,
.exception = @errorName(err),
};
}
pub fn deinit(self: *TryCatch) void {
self.inner.deinit();
v8.v8__TryCatch__DESTRUCT(&self.handle);
}
pub const Caught = struct {
line: ?u32,
caught: bool,
stack: ?[]const u8,
exception: ?[]const u8,
pub fn format(self: Caught, writer: *std.Io.Writer) !void {
const separator = @import("../../log.zig").separator();
try writer.print("{s}exception: {?s}", .{ separator, self.exception });
try writer.print("{s}stack: {?s}", .{ separator, self.stack });
try writer.print("{s}line: {?d}", .{ separator, self.line });
try writer.print("{s}caught: {any}", .{ separator, self.caught });
}
pub fn logFmt(self: Caught, comptime prefix: []const u8, writer: anytype) !void {
try writer.write(prefix ++ ".exception", self.exception orelse "???");
try writer.write(prefix ++ ".stack", self.stack orelse "na");
try writer.write(prefix ++ ".line", self.line);
try writer.write(prefix ++ ".caught", self.caught);
}
};

View File

@@ -21,84 +21,302 @@ const js = @import("js.zig");
const v8 = js.v8;
const IS_DEBUG = @import("builtin").mode == .Debug;
const Allocator = std.mem.Allocator;
const PersistentValue = v8.Persistent(v8.Value);
const Value = @This();
js_val: v8.Value,
context: *js.Context,
ctx: *js.Context,
handle: *const v8.Value,
pub fn isObject(self: Value) bool {
return self.js_val.isObject();
return v8.v8__Value__IsObject(self.handle);
}
pub fn isString(self: Value) bool {
return self.js_val.isString();
return v8.v8__Value__IsString(self.handle);
}
pub fn isArray(self: Value) bool {
return self.js_val.isArray();
return v8.v8__Value__IsArray(self.handle);
}
pub fn isSymbol(self: Value) bool {
return v8.v8__Value__IsSymbol(self.handle);
}
pub fn isFunction(self: Value) bool {
return v8.v8__Value__IsFunction(self.handle);
}
pub fn isNull(self: Value) bool {
return self.js_val.isNull();
return v8.v8__Value__IsNull(self.handle);
}
pub fn isUndefined(self: Value) bool {
return self.js_val.isUndefined();
return v8.v8__Value__IsUndefined(self.handle);
}
pub fn toString(self: Value, allocator: Allocator) ![]const u8 {
return self.context.valueToString(self.js_val, .{ .allocator = allocator });
pub fn isNullOrUndefined(self: Value) bool {
return v8.v8__Value__IsNullOrUndefined(self.handle);
}
pub fn isNumber(self: Value) bool {
return v8.v8__Value__IsNumber(self.handle);
}
pub fn isNumberObject(self: Value) bool {
return v8.v8__Value__IsNumberObject(self.handle);
}
pub fn isInt32(self: Value) bool {
return v8.v8__Value__IsInt32(self.handle);
}
pub fn isUint32(self: Value) bool {
return v8.v8__Value__IsUint32(self.handle);
}
pub fn isBigInt(self: Value) bool {
return v8.v8__Value__IsBigInt(self.handle);
}
pub fn isBigIntObject(self: Value) bool {
return v8.v8__Value__IsBigIntObject(self.handle);
}
pub fn isBoolean(self: Value) bool {
return v8.v8__Value__IsBoolean(self.handle);
}
pub fn isBooleanObject(self: Value) bool {
return v8.v8__Value__IsBooleanObject(self.handle);
}
pub fn isTrue(self: Value) bool {
return v8.v8__Value__IsTrue(self.handle);
}
pub fn isFalse(self: Value) bool {
return v8.v8__Value__IsFalse(self.handle);
}
pub fn isTypedArray(self: Value) bool {
return v8.v8__Value__IsTypedArray(self.handle);
}
pub fn isArrayBufferView(self: Value) bool {
return v8.v8__Value__IsArrayBufferView(self.handle);
}
pub fn isArrayBuffer(self: Value) bool {
return v8.v8__Value__IsArrayBuffer(self.handle);
}
pub fn isUint8Array(self: Value) bool {
return v8.v8__Value__IsUint8Array(self.handle);
}
pub fn isUint8ClampedArray(self: Value) bool {
return v8.v8__Value__IsUint8ClampedArray(self.handle);
}
pub fn isInt8Array(self: Value) bool {
return v8.v8__Value__IsInt8Array(self.handle);
}
pub fn isUint16Array(self: Value) bool {
return v8.v8__Value__IsUint16Array(self.handle);
}
pub fn isInt16Array(self: Value) bool {
return v8.v8__Value__IsInt16Array(self.handle);
}
pub fn isUint32Array(self: Value) bool {
return v8.v8__Value__IsUint32Array(self.handle);
}
pub fn isInt32Array(self: Value) bool {
return v8.v8__Value__IsInt32Array(self.handle);
}
pub fn isBigUint64Array(self: Value) bool {
return v8.v8__Value__IsBigUint64Array(self.handle);
}
pub fn isBigInt64Array(self: Value) bool {
return v8.v8__Value__IsBigInt64Array(self.handle);
}
pub fn isPromise(self: Value) bool {
return v8.v8__Value__IsPromise(self.handle);
}
pub fn toBool(self: Value) bool {
return v8.v8__Value__BooleanValue(self.handle, self.ctx.isolate.handle);
}
pub fn typeOf(self: Value) js.String {
const str_handle = v8.v8__Value__TypeOf(self.handle, self.ctx.isolate.handle).?;
return js.String{ .ctx = self.ctx, .handle = str_handle };
}
pub fn toF32(self: Value) !f32 {
return @floatCast(try self.toF64());
}
pub fn toF64(self: Value) !f64 {
var maybe: v8.MaybeF64 = undefined;
v8.v8__Value__NumberValue(self.handle, self.ctx.handle, &maybe);
if (!maybe.has_value) {
return error.JsException;
}
return maybe.value;
}
pub fn toI32(self: Value) !i32 {
var maybe: v8.MaybeI32 = undefined;
v8.v8__Value__Int32Value(self.handle, self.ctx.handle, &maybe);
if (!maybe.has_value) {
return error.JsException;
}
return maybe.value;
}
pub fn toU32(self: Value) !u32 {
var maybe: v8.MaybeU32 = undefined;
v8.v8__Value__Uint32Value(self.handle, self.ctx.handle, &maybe);
if (!maybe.has_value) {
return error.JsException;
}
return maybe.value;
}
pub fn toPromise(self: Value) js.Promise {
if (comptime IS_DEBUG) {
std.debug.assert(self.isPromise());
}
return .{
.ctx = self.ctx,
.handle = @ptrCast(self.handle),
};
}
pub fn toString(self: Value, opts: js.String.ToZigOpts) ![]u8 {
return self._toString(false, opts);
}
pub fn toStringZ(self: Value, opts: js.String.ToZigOpts) ![:0]u8 {
return self._toString(true, opts);
}
pub fn toJson(self: Value, allocator: Allocator) ![]u8 {
const json_str_handle = v8.v8__JSON__Stringify(self.ctx.handle, self.handle, null) orelse return error.JsException;
return self.ctx.jsStringToZig(json_str_handle, .{ .allocator = allocator });
}
fn _toString(self: Value, comptime null_terminate: bool, opts: js.String.ToZigOpts) !(if (null_terminate) [:0]u8 else []u8) {
const ctx = self.ctx;
if (self.isSymbol()) {
const sym_handle = v8.v8__Symbol__Description(@ptrCast(self.handle), ctx.isolate.handle).?;
return _toString(.{ .handle = @ptrCast(sym_handle), .ctx = ctx }, null_terminate, opts);
}
const str_handle = v8.v8__Value__ToString(self.handle, ctx.handle) orelse {
return error.JsException;
};
const str = js.String{ .ctx = ctx, .handle = str_handle };
if (comptime null_terminate) {
return js.String.toZigZ(str, opts);
}
return js.String.toZig(str, opts);
}
pub fn fromJson(ctx: *js.Context, json: []const u8) !Value {
const json_string = v8.String.initUtf8(ctx.isolate, json);
const value = try v8.Json.parse(ctx.v8_context, json_string);
return Value{ .context = ctx, .js_val = value };
const v8_isolate = v8.Isolate{ .handle = ctx.isolate.handle };
const json_string = v8.String.initUtf8(v8_isolate, json);
const v8_context = v8.Context{ .handle = ctx.handle };
const value = try v8.Json.parse(v8_context, json_string);
return .{ .ctx = ctx, .handle = value.handle };
}
pub fn persist(self: Value) !Value {
const js_val = self.js_val;
var context = self.context;
pub fn persist(self: Value) !Global {
var ctx = self.ctx;
const persisted = PersistentValue.init(context.isolate, js_val);
try context.js_value_list.append(context.arena, persisted);
var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
return Value{ .context = context, .js_val = persisted.toValue() };
try ctx.global_values.append(ctx.arena, global);
return .{
.handle = global,
.ctx = ctx,
};
}
pub fn toZig(self: Value, comptime T: type) !T {
return self.context.jsValueToZig(T, self.js_val);
return self.ctx.jsValueToZig(T, self);
}
pub fn toObject(self: Value) js.Object {
if (comptime IS_DEBUG) {
std.debug.assert(self.isObject());
}
return .{
.context = self.context,
.js_obj = self.js_val.castTo(v8.Object),
.ctx = self.ctx,
.handle = @ptrCast(self.handle),
};
}
pub fn toArray(self: Value) js.Array {
if (comptime IS_DEBUG) {
std.debug.assert(self.isArray());
}
return .{
.context = self.context,
.js_arr = self.js_val.castTo(v8.Array),
.ctx = self.ctx,
.handle = @ptrCast(self.handle),
};
}
// pub const Value = struct {
// value: v8.Value,
// context: *const Context,
pub fn toBigInt(self: Value) js.BigInt {
if (comptime IS_DEBUG) {
std.debug.assert(self.isBigInt());
}
// // the caller needs to deinit the string returned
// pub fn toString(self: Value, allocator: Allocator) ![]const u8 {
// return self.context.valueToString(self.value, .{ .allocator = allocator });
// }
return .{
.handle = @ptrCast(self.handle),
};
}
// pub fn fromJson(ctx: *Context, json: []const u8) !Value {
// const json_string = v8.String.initUtf8(ctx.isolate, json);
// const value = try v8.Json.parse(ctx.v8_context, json_string);
// return Value{ .context = ctx, .value = value };
// }
// };
pub fn format(self: Value, writer: *std.Io.Writer) !void {
if (comptime IS_DEBUG) {
return self.ctx.debugValue(self, writer);
}
const str = self.toString(.{}) catch return error.WriteFailed;
return writer.writeAll(str);
}
pub const Global = struct {
handle: v8.Global,
ctx: *js.Context,
pub fn deinit(self: *Global) void {
v8.v8__Global__Reset(&self.handle);
}
pub fn local(self: *const Global) Value {
return .{
.ctx = self.ctx,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)),
};
}
pub fn isEqual(self: *const Global, other: Value) bool {
return v8.v8__Global__IsEqual(&self.handle, other.handle);
}
};

View File

@@ -18,11 +18,556 @@
const std = @import("std");
const js = @import("js.zig");
const lp = @import("lightpanda");
const log = @import("../../log.zig");
const v8 = js.v8;
const Caller = @import("Caller.zig");
const Context = @import("Context.zig");
const Page = @import("../Page.zig");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const CALL_ARENA_RETAIN = 1024 * 16;
const IS_DEBUG = @import("builtin").mode == .Debug;
// ============================================================================
// Internal Callback Info Wrappers
// ============================================================================
// These wrap the raw v8 C API to provide a cleaner interface.
// They are not exported - internal to this module only.
const Value = struct {
handle: *const v8.Value,
fn isArray(self: Value) bool {
return v8.v8__Value__IsArray(self.handle);
}
fn isTypedArray(self: Value) bool {
return v8.v8__Value__IsTypedArray(self.handle);
}
fn isFunction(self: Value) bool {
return v8.v8__Value__IsFunction(self.handle);
}
};
const Name = struct {
handle: *const v8.Name,
};
const FunctionCallbackInfo = struct {
handle: *const v8.FunctionCallbackInfo,
fn length(self: FunctionCallbackInfo) u32 {
return @intCast(v8.v8__FunctionCallbackInfo__Length(self.handle));
}
fn getArg(self: FunctionCallbackInfo, index: u32) Value {
return .{ .handle = v8.v8__FunctionCallbackInfo__INDEX(self.handle, @intCast(index)).? };
}
fn getThis(self: FunctionCallbackInfo) *const v8.Object {
return v8.v8__FunctionCallbackInfo__This(self.handle).?;
}
fn getReturnValue(self: FunctionCallbackInfo) ReturnValue {
var rv: v8.ReturnValue = undefined;
v8.v8__FunctionCallbackInfo__GetReturnValue(self.handle, &rv);
return .{ .handle = rv };
}
fn isConstructCall(self: FunctionCallbackInfo) bool {
return v8.v8__FunctionCallbackInfo__IsConstructCall(self.handle);
}
};
const PropertyCallbackInfo = struct {
handle: *const v8.PropertyCallbackInfo,
fn getThis(self: PropertyCallbackInfo) *const v8.Object {
return v8.v8__PropertyCallbackInfo__This(self.handle).?;
}
fn getReturnValue(self: PropertyCallbackInfo) ReturnValue {
var rv: v8.ReturnValue = undefined;
v8.v8__PropertyCallbackInfo__GetReturnValue(self.handle, &rv);
return .{ .handle = rv };
}
};
const ReturnValue = struct {
handle: v8.ReturnValue,
fn set(self: ReturnValue, value: anytype) void {
const T = @TypeOf(value);
if (T == Value) {
self.setValueHandle(value.handle);
} else if (T == *const v8.Object) {
self.setValueHandle(@ptrCast(value));
} else if (T == *const v8.Value) {
self.setValueHandle(value);
} else if (T == js.Value) {
self.setValueHandle(value.handle);
} else {
@compileError("Unsupported type for ReturnValue.set: " ++ @typeName(T));
}
}
fn setValueHandle(self: ReturnValue, handle: *const v8.Value) void {
v8.v8__ReturnValue__Set(self.handle, handle);
}
};
// ============================================================================
// Caller - Responsible for calling Zig functions from JS invocations
// ============================================================================
pub const Caller = struct {
context: *Context,
isolate: js.Isolate,
call_arena: Allocator,
// Takes the raw v8 isolate and extracts the context from it.
pub fn init(v8_isolate: *v8.Isolate) Caller {
const isolate = js.Isolate{ .handle = v8_isolate };
const v8_context_handle = v8.v8__Isolate__GetCurrentContext(v8_isolate);
const embedder_data = v8.v8__Context__GetEmbedderData(v8_context_handle, 1);
var lossless: bool = undefined;
const context: *Context = @ptrFromInt(v8.v8__BigInt__Uint64Value(embedder_data, &lossless));
context.call_depth += 1;
return .{
.context = context,
.isolate = isolate,
.call_arena = context.call_arena,
};
}
pub fn deinit(self: *Caller) void {
const context = self.context;
const call_depth = context.call_depth - 1;
// Because of callbacks, calls can be nested. Because of this, we
// can't clear the call_arena after _every_ call. Imagine we have
// arr.forEach((i) => { console.log(i); }
//
// First we call forEach. Inside of our forEach call,
// we call console.log. If we reset the call_arena after this call,
// it'll reset it for the `forEach` call after, which might still
// need the data.
//
// Therefore, we keep a call_depth, and only reset the call_arena
// when a top-level (call_depth == 0) function ends.
if (call_depth == 0) {
const arena: *ArenaAllocator = @ptrCast(@alignCast(context.call_arena.ptr));
_ = arena.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN });
}
context.call_depth = call_depth;
}
pub const CallOpts = struct {
dom_exception: bool = false,
null_as_undefined: bool = false,
as_typed_array: bool = false,
};
pub fn constructor(self: *Caller, comptime T: type, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) void {
if (!info.isConstructCall()) {
self.handleError(T, @TypeOf(func), error.InvalidArgument, info, opts);
return;
}
self._constructor(func, info) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
};
}
fn _constructor(self: *Caller, func: anytype, info: FunctionCallbackInfo) !void {
const F = @TypeOf(func);
const args = try self.getArgs(F, 0, info);
const res = @call(.auto, func, args);
const ReturnType = @typeInfo(F).@"fn".return_type orelse {
@compileError(@typeName(F) ++ " has a constructor without a return type");
};
const new_this_handle = info.getThis();
var this = js.Object{ .ctx = self.context, .handle = new_this_handle };
if (@typeInfo(ReturnType) == .error_union) {
const non_error_res = res catch |err| return err;
this = try self.context.mapZigInstanceToJs(new_this_handle, non_error_res);
} else {
this = try self.context.mapZigInstanceToJs(new_this_handle, res);
}
// If we got back a different object (existing wrapper), copy the prototype
// from new object. (this happens when we're upgrading an CustomElement)
if (this.handle != new_this_handle) {
const prototype_handle = v8.v8__Object__GetPrototype(new_this_handle).?;
var out: v8.MaybeBool = undefined;
v8.v8__Object__SetPrototype(this.handle, self.context.handle, prototype_handle, &out);
if (comptime IS_DEBUG) {
std.debug.assert(out.has_value and out.value);
}
}
info.getReturnValue().set(this.handle);
}
pub fn method(self: *Caller, comptime T: type, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) void {
self._method(T, func, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
};
}
fn _method(self: *Caller, comptime T: type, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) !void {
const F = @TypeOf(func);
var handle_scope: js.HandleScope = undefined;
handle_scope.init(self.isolate);
defer handle_scope.deinit();
var args = try self.getArgs(F, 1, info);
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
const res = @call(.auto, func, args);
info.getReturnValue().set(try self.context.zigValueToJs(res, opts));
}
pub fn function(self: *Caller, comptime T: type, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) void {
self._function(func, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
};
}
fn _function(self: *Caller, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) !void {
const F = @TypeOf(func);
const context = self.context;
const args = try self.getArgs(F, 0, info);
const res = @call(.auto, func, args);
info.getReturnValue().set(try context.zigValueToJs(res, opts));
}
pub fn getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, info: PropertyCallbackInfo, comptime opts: CallOpts) u8 {
return self._getIndex(T, func, idx, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
// not intercepted
return 0;
};
}
fn _getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args = try self.getArgs(F, 2, info);
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
@field(args, "1") = idx;
const ret = @call(.auto, func, args);
return self.handleIndexedReturn(T, F, true, ret, info, opts);
}
pub fn getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: Name, info: PropertyCallbackInfo, comptime opts: CallOpts) u8 {
return self._getNamedIndex(T, func, name, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
// not intercepted
return 0;
};
}
fn _getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args = try self.getArgs(F, 2, info);
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
@field(args, "1") = try self.nameToString(name);
const ret = @call(.auto, func, args);
return self.handleIndexedReturn(T, F, true, ret, info, opts);
}
pub fn setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: Name, js_value: Value, info: PropertyCallbackInfo, comptime opts: CallOpts) u8 {
return self._setNamedIndex(T, func, name, js_value, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
// not intercepted
return 0;
};
}
fn _setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: Name, js_value: Value, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args: ParameterTypes(F) = undefined;
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
@field(args, "1") = try self.nameToString(name);
@field(args, "2") = try self.context.jsValueToZig(@TypeOf(@field(args, "2")), js.Value{ .ctx = self.context, .handle = js_value.handle });
if (@typeInfo(F).@"fn".params.len == 4) {
@field(args, "3") = self.context.page;
}
const ret = @call(.auto, func, args);
return self.handleIndexedReturn(T, F, false, ret, info, opts);
}
pub fn deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: Name, info: PropertyCallbackInfo, comptime opts: CallOpts) u8 {
return self._deleteNamedIndex(T, func, name, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
return 0;
};
}
fn _deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args: ParameterTypes(F) = undefined;
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
@field(args, "1") = try self.nameToString(name);
if (@typeInfo(F).@"fn".params.len == 3) {
@field(args, "2") = self.context.page;
}
const ret = @call(.auto, func, args);
return self.handleIndexedReturn(T, F, false, ret, info, opts);
}
fn handleIndexedReturn(self: *Caller, comptime T: type, comptime F: type, comptime getter: bool, ret: anytype, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
// need to unwrap this error immediately for when opts.null_as_undefined == true
// and we need to compare it to null;
const non_error_ret = switch (@typeInfo(@TypeOf(ret))) {
.error_union => |eu| blk: {
break :blk ret catch |err| {
// We can't compare err == error.NotHandled if error.NotHandled
// isn't part of the possible error set. So we first need to check
// if error.NotHandled is part of the error set.
if (isInErrorSet(error.NotHandled, eu.error_set)) {
if (err == error.NotHandled) {
// not intercepted
return 0;
}
}
self.handleError(T, F, err, info, opts);
// not intercepted
return 0;
};
},
else => ret,
};
if (comptime getter) {
info.getReturnValue().set(try self.context.zigValueToJs(non_error_ret, opts));
}
// intercepted
return 1;
}
fn isInErrorSet(err: anyerror, comptime T: type) bool {
inline for (@typeInfo(T).error_set.?) |e| {
if (err == @field(anyerror, e.name)) return true;
}
return false;
}
fn nameToString(self: *Caller, name: Name) ![]const u8 {
return self.context.valueToString(js.Value{ .ctx = self.context, .handle = @ptrCast(name.handle) }, .{});
}
fn handleError(self: *Caller, comptime T: type, comptime F: type, err: anyerror, info: anytype, comptime opts: CallOpts) void {
const isolate = self.isolate;
if (comptime @import("builtin").mode == .Debug and @TypeOf(info) == FunctionCallbackInfo) {
if (log.enabled(.js, .warn)) {
self.logFunctionCallError(@typeName(T), @typeName(F), err, info);
}
}
const js_err: *const v8.Value = switch (err) {
error.InvalidArgument => isolate.createTypeError("invalid argument"),
error.OutOfMemory => isolate.createError("out of memory"),
error.IllegalConstructor => isolate.createError("Illegal Contructor"),
else => blk: {
if (comptime opts.dom_exception) {
const DOMException = @import("../webapi/DOMException.zig");
if (DOMException.fromError(err)) |ex| {
const value = self.context.zigValueToJs(ex, .{}) catch break :blk isolate.createError("internal error");
break :blk value.handle;
}
}
break :blk isolate.createError(@errorName(err));
},
};
const js_exception = isolate.throwException(js_err);
info.getReturnValue().setValueHandle(js_exception);
}
// If we call a method in javascript: cat.lives('nine');
//
// Then we'd expect a Zig function with 2 parameters: a self and the string.
// In this case, offset == 1. Offset is always 1 for setters or methods.
//
// Offset is always 0 for constructors.
//
// For constructors, setters and methods, we can further increase offset + 1
// if the first parameter is an instance of Page.
//
// Finally, if the JS function is called with _more_ parameters and
// the last parameter in Zig is an array, we'll try to slurp the additional
// parameters into the array.
fn getArgs(self: *const Caller, comptime F: type, comptime offset: usize, info: anytype) !ParameterTypes(F) {
const context = self.context;
var args: ParameterTypes(F) = undefined;
const params = @typeInfo(F).@"fn".params[offset..];
// Except for the constructor, the first parameter is always `self`
// This isn't something we'll bind from JS, so skip it.
const params_to_map = blk: {
if (params.len == 0) {
return args;
}
// If the last parameter is the Page, set it, and exclude it
// from our params slice, because we don't want to bind it to
// a JS argument
if (comptime isPage(params[params.len - 1].type.?)) {
@field(args, tupleFieldName(params.len - 1 + offset)) = self.context.page;
break :blk params[0 .. params.len - 1];
}
// we have neither a Page nor a JsObject. All params must be
// bound to a JavaScript value.
break :blk params;
};
if (params_to_map.len == 0) {
return args;
}
const js_parameter_count = info.length();
const last_js_parameter = params_to_map.len - 1;
var is_variadic = false;
{
// This is going to get complicated. If the last Zig parameter
// is a slice AND the corresponding javascript parameter is
// NOT an an array, then we'll treat it as a variadic.
const last_parameter_type = params_to_map[params_to_map.len - 1].type.?;
const last_parameter_type_info = @typeInfo(last_parameter_type);
if (last_parameter_type_info == .pointer and last_parameter_type_info.pointer.size == .slice) {
const slice_type = last_parameter_type_info.pointer.child;
const corresponding_js_value = info.getArg(@as(u32, @intCast(last_js_parameter)));
if (corresponding_js_value.isArray() == false and corresponding_js_value.isTypedArray() == false and slice_type != u8) {
is_variadic = true;
if (js_parameter_count == 0) {
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
} else if (js_parameter_count >= params_to_map.len) {
const arr = try self.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);
for (arr, last_js_parameter..) |*a, i| {
const js_value = info.getArg(@as(u32, @intCast(i)));
a.* = try context.jsValueToZig(slice_type, js.Value{ .ctx = context, .handle = js_value.handle });
}
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = arr;
} else {
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
}
}
}
}
inline for (params_to_map, 0..) |param, i| {
const field_index = comptime i + offset;
if (comptime i == params_to_map.len - 1) {
if (is_variadic) {
break;
}
}
if (comptime isPage(param.type.?)) {
@compileError("Page must be the last parameter (or 2nd last if there's a JsThis): " ++ @typeName(F));
} else if (i >= js_parameter_count) {
if (@typeInfo(param.type.?) != .optional) {
return error.InvalidArgument;
}
@field(args, tupleFieldName(field_index)) = null;
} else {
const js_value = info.getArg(@as(u32, @intCast(i)));
@field(args, tupleFieldName(field_index)) = context.jsValueToZig(param.type.?, js.Value{ .ctx = context, .handle = js_value.handle }) catch {
return error.InvalidArgument;
};
}
}
return args;
}
// This is extracted to speed up compilation. When left inlined in handleError,
// this can add as much as 10 seconds of compilation time.
fn logFunctionCallError(self: *Caller, type_name: []const u8, func: []const u8, err: anyerror, info: FunctionCallbackInfo) void {
const args_dump = self.serializeFunctionArgs(info) catch "failed to serialize args";
log.info(.js, "function call error", .{
.type = type_name,
.func = func,
.err = err,
.args = args_dump,
.stack = self.context.stackTrace() catch |err1| @errorName(err1),
});
}
fn serializeFunctionArgs(self: *Caller, info: FunctionCallbackInfo) ![]const u8 {
const context = self.context;
var buf = std.Io.Writer.Allocating.init(context.call_arena);
const separator = log.separator();
for (0..info.length()) |i| {
try buf.writer.print("{s}{d} - ", .{ separator, i + 1 });
const val = info.getArg(@intCast(i));
try context.debugValue(js.Value{ .ctx = context, .handle = val.handle }, &buf.writer);
}
return buf.written();
}
// Takes a function, and returns a tuple for its argument. Used when we
// @call a function
fn ParameterTypes(comptime F: type) type {
const params = @typeInfo(F).@"fn".params;
var fields: [params.len]std.builtin.Type.StructField = undefined;
inline for (params, 0..) |param, i| {
fields[i] = .{
.name = tupleFieldName(i),
.type = param.type.?,
.default_value_ptr = null,
.is_comptime = false,
.alignment = @alignOf(param.type.?),
};
}
return @Type(.{ .@"struct" = .{
.layout = .auto,
.decls = &.{},
.fields = &fields,
.is_tuple = true,
} });
}
fn tupleFieldName(comptime i: usize) [:0]const u8 {
return switch (i) {
0 => "0",
1 => "1",
2 => "2",
3 => "3",
4 => "4",
5 => "5",
6 => "6",
7 => "7",
8 => "8",
9 => "9",
else => std.fmt.comptimePrint("{d}", .{i}),
};
}
fn isPage(comptime T: type) bool {
return T == *Page or T == *const Page;
}
};
// ============================================================================
// Bridge Builder Functions
// ============================================================================
pub fn Builder(comptime T: type) type {
return struct {
@@ -89,7 +634,7 @@ pub fn Builder(comptime T: type) type {
}
pub const Constructor = struct {
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
const Opts = struct {
dom_exception: bool = false,
@@ -97,11 +642,12 @@ pub const Constructor = struct {
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Constructor {
return .{ .func = struct {
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
caller.constructor(T, func, info, .{
.dom_exception = opts.dom_exception,
});
@@ -112,7 +658,7 @@ pub const Constructor = struct {
pub const Function = struct {
static: bool,
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
const Opts = struct {
static: bool = false,
@@ -125,11 +671,12 @@ pub const Function = struct {
return .{
.static = opts.static,
.func = struct {
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
if (comptime opts.static) {
caller.function(T, func, info, .{
.dom_exception = opts.dom_exception,
@@ -151,8 +698,8 @@ pub const Function = struct {
pub const Accessor = struct {
static: bool = false,
getter: ?*const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void = null,
setter: ?*const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void = null,
getter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,
setter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,
const Opts = struct {
static: bool = false,
@@ -168,28 +715,39 @@ pub const Accessor = struct {
if (@typeInfo(@TypeOf(getter)) != .null) {
accessor.getter = struct {
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
defer caller.deinit();
caller.method(T, getter, info, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
const info = FunctionCallbackInfo{ .handle = handle.? };
if (comptime opts.static) {
caller.function(T, getter, info, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
} else {
caller.method(T, getter, info, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
}
}
}.wrap;
}
if (@typeInfo(@TypeOf(setter)) != .null) {
accessor.setter = struct {
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
std.debug.assert(info.length() == 1);
var caller = Caller.init(info);
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
if (comptime IS_DEBUG) {
lp.assert(info.length() == 1, "bridge.setter", .{ .len = info.length() });
}
caller.method(T, setter, info, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
@@ -203,7 +761,7 @@ pub const Accessor = struct {
};
pub const Indexed = struct {
getter: *const fn (idx: u32, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8,
getter: *const fn (idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8,
const Opts = struct {
as_typed_array: bool = false,
@@ -212,10 +770,12 @@ pub const Indexed = struct {
fn init(comptime T: type, comptime getter: anytype, comptime opts: Opts) Indexed {
return .{ .getter = struct {
fn wrap(idx: u32, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
fn wrap(idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
defer caller.deinit();
const info = PropertyCallbackInfo{ .handle = handle.? };
return caller.getIndex(T, getter, idx, info, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
@@ -226,9 +786,9 @@ pub const Indexed = struct {
};
pub const NamedIndexed = struct {
getter: *const fn (c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8,
setter: ?*const fn (c_name: ?*const v8.C_Name, c_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 = null,
deleter: ?*const fn (c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 = null,
getter: *const fn (c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8,
setter: ?*const fn (c_name: ?*const v8.Name, c_value: ?*const v8.Value, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 = null,
deleter: ?*const fn (c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 = null,
const Opts = struct {
as_typed_array: bool = false,
@@ -237,10 +797,12 @@ pub const NamedIndexed = struct {
fn init(comptime T: type, comptime getter: anytype, setter: anytype, deleter: anytype, comptime opts: Opts) NamedIndexed {
const getter_fn = struct {
fn wrap(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
defer caller.deinit();
const info = PropertyCallbackInfo{ .handle = handle.? };
return caller.getNamedIndex(T, getter, .{ .handle = c_name.? }, info, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
@@ -249,11 +811,12 @@ pub const NamedIndexed = struct {
}.wrap;
const setter_fn = if (@typeInfo(@TypeOf(setter)) == .null) null else struct {
fn wrap(c_name: ?*const v8.C_Name, c_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
fn wrap(c_name: ?*const v8.Name, c_value: ?*const v8.Value, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
defer caller.deinit();
const info = PropertyCallbackInfo{ .handle = handle.? };
return caller.setNamedIndex(T, setter, .{ .handle = c_name.? }, .{ .handle = c_value.? }, info, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
@@ -262,11 +825,12 @@ pub const NamedIndexed = struct {
}.wrap;
const deleter_fn = if (@typeInfo(@TypeOf(deleter)) == .null) null else struct {
fn wrap(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
defer caller.deinit();
const info = PropertyCallbackInfo{ .handle = handle.? };
return caller.deleteNamedIndex(T, deleter, .{ .handle = c_name.? }, info, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
@@ -283,7 +847,7 @@ pub const NamedIndexed = struct {
};
pub const Iterator = struct {
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
async: bool,
const Opts = struct {
@@ -296,8 +860,8 @@ pub const Iterator = struct {
return .{
.async = opts.async,
.func = struct {
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const info = FunctionCallbackInfo{ .handle = handle.? };
info.getReturnValue().set(info.getThis());
}
}.wrap,
@@ -307,10 +871,12 @@ pub const Iterator = struct {
return .{
.async = opts.async,
.func = struct {
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
caller.method(T, struct_or_func, info, .{});
}
}.wrap,
@@ -319,7 +885,7 @@ pub const Iterator = struct {
};
pub const Callable = struct {
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
const Opts = struct {
null_as_undefined: bool = false,
@@ -327,10 +893,12 @@ pub const Callable = struct {
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Callable {
return .{ .func = struct {
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
caller.method(T, func, info, .{
.null_as_undefined = opts.null_as_undefined,
});
@@ -343,6 +911,62 @@ pub const Property = union(enum) {
int: i64,
};
pub fn unknownPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const isolate_handle = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
const context = Context.fromIsolate(.{ .handle = isolate_handle });
const property: []const u8 = context.valueToString(.{ .ctx = context, .handle = c_name.? }, .{}) catch {
return 0;
};
const ignored = std.StaticStringMap(void).initComptime(.{
.{ "process", {} },
.{ "ShadyDOM", {} },
.{ "ShadyCSS", {} },
.{ "litNonce", {} },
.{ "litHtmlVersions", {} },
.{ "litElementVersions", {} },
.{ "litHtmlPolyfillSupport", {} },
.{ "litElementHydrateSupport", {} },
.{ "litElementPolyfillSupport", {} },
.{ "reactiveElementVersions", {} },
.{ "recaptcha", {} },
.{ "grecaptcha", {} },
.{ "___grecaptcha_cfg", {} },
.{ "__recaptcha_api", {} },
.{ "__google_recaptcha_client", {} },
.{ "CLOSURE_FLAGS", {} },
});
if (!ignored.has(property)) {
const page = context.page;
const document = page.document;
if (document.getElementById(property, page)) |el| {
const js_value = context.zigValueToJs(el, .{}) catch {
return 0;
};
var pc = PropertyCallbackInfo{ .handle = handle.? };
pc.getReturnValue().set(js_value);
return 1;
}
if (comptime IS_DEBUG) {
log.debug(.unknown_prop, "unknown global property", .{
.info = "but the property can exist in pure JS",
.stack = context.stackTrace() catch "???",
.property = property,
});
}
}
// not intercepted
return 0;
}
// Given a Type, returns the length of the prototype chain, including self
fn prototypeChainLength(comptime T: type) usize {
var l: usize = 1;
@@ -529,16 +1153,22 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/element/Html.zig"),
@import("../webapi/element/html/IFrame.zig"),
@import("../webapi/element/html/Anchor.zig"),
@import("../webapi/element/html/Area.zig"),
@import("../webapi/element/html/Audio.zig"),
@import("../webapi/element/html/Base.zig"),
@import("../webapi/element/html/Body.zig"),
@import("../webapi/element/html/BR.zig"),
@import("../webapi/element/html/Button.zig"),
@import("../webapi/element/html/Canvas.zig"),
@import("../webapi/element/html/Custom.zig"),
@import("../webapi/element/html/Data.zig"),
@import("../webapi/element/html/DataList.zig"),
@import("../webapi/element/html/Dialog.zig"),
@import("../webapi/element/html/Directory.zig"),
@import("../webapi/element/html/Div.zig"),
@import("../webapi/element/html/Embed.zig"),
@import("../webapi/element/html/FieldSet.zig"),
@import("../webapi/element/html/Font.zig"),
@import("../webapi/element/html/Form.zig"),
@import("../webapi/element/html/Generic.zig"),
@import("../webapi/element/html/Head.zig"),
@@ -547,20 +1177,42 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/element/html/Html.zig"),
@import("../webapi/element/html/Image.zig"),
@import("../webapi/element/html/Input.zig"),
@import("../webapi/element/html/Label.zig"),
@import("../webapi/element/html/Legend.zig"),
@import("../webapi/element/html/LI.zig"),
@import("../webapi/element/html/Link.zig"),
@import("../webapi/element/html/Map.zig"),
@import("../webapi/element/html/Media.zig"),
@import("../webapi/element/html/Meta.zig"),
@import("../webapi/element/html/Meter.zig"),
@import("../webapi/element/html/Mod.zig"),
@import("../webapi/element/html/Object.zig"),
@import("../webapi/element/html/OL.zig"),
@import("../webapi/element/html/OptGroup.zig"),
@import("../webapi/element/html/Option.zig"),
@import("../webapi/element/html/Output.zig"),
@import("../webapi/element/html/Paragraph.zig"),
@import("../webapi/element/html/Param.zig"),
@import("../webapi/element/html/Pre.zig"),
@import("../webapi/element/html/Progress.zig"),
@import("../webapi/element/html/Quote.zig"),
@import("../webapi/element/html/Script.zig"),
@import("../webapi/element/html/Select.zig"),
@import("../webapi/element/html/Slot.zig"),
@import("../webapi/element/html/Source.zig"),
@import("../webapi/element/html/Span.zig"),
@import("../webapi/element/html/Style.zig"),
@import("../webapi/element/html/Table.zig"),
@import("../webapi/element/html/TableCaption.zig"),
@import("../webapi/element/html/TableCell.zig"),
@import("../webapi/element/html/TableCol.zig"),
@import("../webapi/element/html/TableRow.zig"),
@import("../webapi/element/html/TableSection.zig"),
@import("../webapi/element/html/Template.zig"),
@import("../webapi/element/html/TextArea.zig"),
@import("../webapi/element/html/Time.zig"),
@import("../webapi/element/html/Title.zig"),
@import("../webapi/element/html/Track.zig"),
@import("../webapi/element/html/Video.zig"),
@import("../webapi/element/html/UL.zig"),
@import("../webapi/element/html/Unknown.zig"),
@@ -617,4 +1269,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/navigation/NavigationEventTarget.zig"),
@import("../webapi/navigation/NavigationHistoryEntry.zig"),
@import("../webapi/navigation/NavigationActivation.zig"),
@import("../webapi/canvas/CanvasRenderingContext2D.zig"),
@import("../webapi/canvas/WebGLRenderingContext.zig"),
@import("../webapi/SubtleCrypto.zig"),
});

48
src/browser/js/global.zig Normal file
View File

@@ -0,0 +1,48 @@
// 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 js = @import("js.zig");
const v8 = js.v8;
pub fn Global(comptime T: type) type {
const H = @FieldType(T, "handle");
return struct {
global: v8.Global,
const Self = @This();
pub fn init(isolate: *v8.Isolate, handle: H) Self {
var global: v8.Global = undefined;
v8.v8__Global__New(isolate, handle, &global);
return .{
.global = global,
};
}
pub fn deinit(self: *Self) void {
v8.v8__Global__Reset(&self.global);
}
pub fn local(self: *const Self) H {
return @ptrCast(@alignCast(@as(*const anyopaque, @ptrFromInt(self.global.data_ptr))));
}
};
}

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
pub const v8 = @import("v8");
pub const v8 = @import("v8").c;
const log = @import("../../log.zig");
@@ -28,14 +28,23 @@ pub const Context = @import("Context.zig");
pub const Inspector = @import("Inspector.zig");
pub const Snapshot = @import("Snapshot.zig");
pub const Platform = @import("Platform.zig");
pub const Isolate = @import("Isolate.zig");
pub const HandleScope = @import("HandleScope.zig");
// TODO: Is "This" really necessary?
pub const This = @import("This.zig");
pub const Name = @import("Name.zig");
pub const Value = @import("Value.zig");
pub const Array = @import("Array.zig");
pub const String = @import("String.zig");
pub const Object = @import("Object.zig");
pub const TryCatch = @import("TryCatch.zig");
pub const Function = @import("Function.zig");
pub const Promise = @import("Promise.zig");
pub const Module = @import("Module.zig");
pub const BigInt = @import("BigInt.zig");
pub const Number = @import("Number.zig");
pub const Integer = @import("Integer.zig");
pub const Global = @import("global.zig").Global;
pub const PromiseResolver = @import("PromiseResolver.zig");
const Allocator = std.mem.Allocator;
@@ -68,246 +77,47 @@ pub const ArrayBuffer = struct {
}
};
pub const PromiseResolver = struct {
context: *Context,
resolver: v8.PromiseResolver,
pub fn promise(self: PromiseResolver) Promise {
return self.resolver.getPromise();
}
pub fn resolve(self: PromiseResolver, comptime source: []const u8, value: anytype) void {
self._resolve(value) catch |err| {
log.err(.bug, "resolve", .{ .source = source, .err = err, .persistent = false });
};
}
fn _resolve(self: PromiseResolver, value: anytype) !void {
const context = self.context;
const js_value = try context.zigValueToJs(value, .{});
if (self.resolver.resolve(context.v8_context, js_value) == null) {
return error.FailedToResolvePromise;
}
self.context.runMicrotasks();
}
pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype) void {
self._reject(value) catch |err| {
log.err(.bug, "reject", .{ .source = source, .err = err, .persistent = false });
};
}
fn _reject(self: PromiseResolver, value: anytype) !void {
const context = self.context;
const js_value = try context.zigValueToJs(value);
if (self.resolver.reject(context.v8_context, js_value) == null) {
return error.FailedToRejectPromise;
}
self.context.runMicrotasks();
}
};
pub const PersistentPromiseResolver = struct {
context: *Context,
resolver: v8.Persistent(v8.PromiseResolver),
pub fn deinit(self: *PersistentPromiseResolver) void {
self.resolver.deinit();
}
pub fn promise(self: PersistentPromiseResolver) Promise {
return self.resolver.castToPromiseResolver().getPromise();
}
pub fn resolve(self: PersistentPromiseResolver, comptime source: []const u8, value: anytype) void {
self._resolve(value) catch |err| {
log.err(.bug, "resolve", .{ .source = source, .err = err, .persistent = true });
};
}
fn _resolve(self: PersistentPromiseResolver, value: anytype) !void {
const context = self.context;
const js_value = try context.zigValueToJs(value, .{});
defer context.runMicrotasks();
if (self.resolver.castToPromiseResolver().resolve(context.v8_context, js_value) == null) {
return error.FailedToResolvePromise;
}
}
pub fn reject(self: PersistentPromiseResolver, comptime source: []const u8, value: anytype) void {
self._reject(value) catch |err| {
log.err(.bug, "reject", .{ .source = source, .err = err, .persistent = true });
};
}
fn _reject(self: PersistentPromiseResolver, value: anytype) !void {
const context = self.context;
const js_value = try context.zigValueToJs(value, .{});
defer context.runMicrotasks();
// resolver.reject will return null if the promise isn't pending
if (self.resolver.castToPromiseResolver().reject(context.v8_context, js_value) == null) {
return error.FailedToRejectPromise;
}
}
};
pub const Promise = v8.Promise;
// When doing jsValueToZig, string ([]const u8) are managed by the
// call_arena. That means that if the API wants to persist the string
// (which is relatively common), it needs to dupe it again.
// If the parameter is an Env.String rather than a []const u8, then
// the page's arena will be used (rather than the call arena).
pub const String = struct {
string: []const u8,
};
pub const Exception = struct {
inner: v8.Value,
context: *const Context,
ctx: *const Context,
handle: *const v8.Value,
// the caller needs to deinit the string returned
pub fn exception(self: Exception, allocator: Allocator) ![]const u8 {
return self.context.valueToString(self.inner, .{ .allocator = allocator });
}
};
pub fn UndefinedOr(comptime T: type) type {
return union(enum) {
undefined: void,
value: T,
};
}
// An interface for types that want to have their jsScopeEnd function be
// called when the call context ends
const CallScopeEndCallback = struct {
ptr: *anyopaque,
callScopeEndFn: *const fn (ptr: *anyopaque) void,
fn init(ptr: anytype) CallScopeEndCallback {
const T = @TypeOf(ptr);
const ptr_info = @typeInfo(T);
const gen = struct {
pub fn callScopeEnd(pointer: *anyopaque) void {
const self: T = @ptrCast(@alignCast(pointer));
return ptr_info.pointer.child.jsCallScopeEnd(self);
}
};
return .{
.ptr = ptr,
.callScopeEndFn = gen.callScopeEnd,
};
}
pub fn callScopeEnd(self: CallScopeEndCallback) void {
self.callScopeEndFn(self.ptr);
}
};
// Callback called on global's property missing.
// Return true to intercept the execution or false to let the call
// continue the chain.
pub const GlobalMissingCallback = struct {
ptr: *anyopaque,
missingFn: *const fn (ptr: *anyopaque, name: []const u8, ctx: *Context) bool,
pub fn init(ptr: anytype) GlobalMissingCallback {
const T = @TypeOf(ptr);
const ptr_info = @typeInfo(T);
const gen = struct {
pub fn missing(pointer: *anyopaque, name: []const u8, ctx: *Context) bool {
const self: T = @ptrCast(@alignCast(pointer));
return ptr_info.pointer.child.missing(self, name, ctx);
}
};
return .{
.ptr = ptr,
.missingFn = gen.missing,
};
}
pub fn missing(self: GlobalMissingCallback, name: []const u8, ctx: *Context) bool {
return self.missingFn(self.ptr, name, ctx);
}
};
// Attributes that return a primitive type are setup directly on the
// FunctionTemplate when the Env is setup. More complex types need a v8.Context
// and cannot be set directly on the FunctionTemplate.
// We default to saying types are primitives because that's mostly what
// we have. If we add a new complex type that isn't explictly handled here,
// we'll get a compiler error in simpleZigValueToJs, and can then explicitly
// add the type here.
pub fn isComplexAttributeType(ti: std.builtin.Type) bool {
return switch (ti) {
.array => true,
else => false,
};
}
// These are simple types that we can convert to JS with only an isolate. This
// is separated from the Caller's zigValueToJs to make it available when we
// don't have a caller (i.e., when setting static attributes on types)
pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bool, comptime null_as_undefined: bool) if (fail) v8.Value else ?v8.Value {
pub fn simpleZigValueToJs(isolate: Isolate, value: anytype, comptime fail: bool, comptime null_as_undefined: bool) if (fail) *const v8.Value else ?*const v8.Value {
switch (@typeInfo(@TypeOf(value))) {
.void => return v8.initUndefined(isolate).toValue(),
.null => if (comptime null_as_undefined) return v8.initUndefined(isolate).toValue() else return v8.initNull(isolate).toValue(),
.bool => return v8.getValue(if (value) v8.initTrue(isolate) else v8.initFalse(isolate)),
.int => |n| switch (n.signedness) {
.signed => {
if (value > 0 and value <= 4_294_967_295) {
return v8.Integer.initU32(isolate, @intCast(value)).toValue();
}
if (value >= -2_147_483_648 and value <= 2_147_483_647) {
return v8.Integer.initI32(isolate, @intCast(value)).toValue();
}
if (comptime n.bits <= 64) {
return v8.getValue(v8.BigInt.initI64(isolate, @intCast(value)));
}
@compileError(@typeName(value) ++ " is not supported");
},
.unsigned => {
if (value <= 4_294_967_295) {
return v8.Integer.initU32(isolate, @intCast(value)).toValue();
}
if (comptime n.bits <= 64) {
return v8.getValue(v8.BigInt.initU64(isolate, @intCast(value)));
}
@compileError(@typeName(value) ++ " is not supported");
},
.void => return isolate.initUndefined(),
.null => if (comptime null_as_undefined) return isolate.initUndefined() else return isolate.initNull(),
.bool => return if (value) isolate.initTrue() else isolate.initFalse(),
.int => |n| {
if (comptime n.bits <= 32) {
return @ptrCast(isolate.initInteger(value).handle);
}
if (value >= 0 and value <= 4_294_967_295) {
return @ptrCast(isolate.initInteger(@as(u32, @intCast(value))).handle);
}
return @ptrCast(isolate.initBigInt(value).handle);
},
.comptime_int => {
if (value >= 0) {
if (value <= 4_294_967_295) {
return v8.Integer.initU32(isolate, @intCast(value)).toValue();
}
return v8.BigInt.initU64(isolate, @intCast(value)).toValue();
if (value > -2_147_483_648 and value <= 4_294_967_295) {
return @ptrCast(isolate.initInteger(value).handle);
}
if (value >= -2_147_483_648) {
return v8.Integer.initI32(isolate, @intCast(value)).toValue();
}
return v8.BigInt.initI64(isolate, @intCast(value)).toValue();
},
.comptime_float => return v8.Number.init(isolate, value).toValue(),
.float => |f| switch (f.bits) {
64 => return v8.Number.init(isolate, value).toValue(),
32 => return v8.Number.init(isolate, @floatCast(value)).toValue(),
else => @compileError(@typeName(value) ++ " is not supported"),
return @ptrCast(isolate.initBigInt(value).handle);
},
.float, .comptime_float => return @ptrCast(isolate.initNumber(value).handle),
.pointer => |ptr| {
if (ptr.size == .slice and ptr.child == u8) {
return v8.String.initUtf8(isolate, value).toValue();
return @ptrCast(isolate.initStringHandle(value));
}
if (ptr.size == .one) {
const one_info = @typeInfo(ptr.child);
if (one_info == .array and one_info.array.child == u8) {
return v8.String.initUtf8(isolate, value).toValue();
return @ptrCast(isolate.initStringHandle(value));
}
}
},
@@ -317,22 +127,20 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo
return simpleZigValueToJs(isolate, v, fail, null_as_undefined);
}
if (comptime null_as_undefined) {
return v8.initUndefined(isolate).toValue();
return isolate.initUndefined();
}
return v8.initNull(isolate).toValue();
return isolate.initNull();
},
.@"struct" => {
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()));
const backing_store = v8.v8__ArrayBuffer__NewBackingStore(isolate.handle, len);
const data: [*]u8 = @ptrCast(@alignCast(v8.v8__BackingStore__Data(backing_store)));
@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 };
const backing_store_ptr = v8.v8__BackingStore__TO_SHARED_PTR(backing_store);
return @ptrCast(v8.v8__ArrayBuffer__New2(isolate.handle, &backing_store_ptr).?);
},
// zig fmt: off
TypedArray(u8), TypedArray(u16), TypedArray(u32), TypedArray(u64),
@@ -349,37 +157,38 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo
else => @compileError("Invalid TypeArray type: " ++ @typeName(value_type)),
};
var array_buffer: v8.ArrayBuffer = undefined;
var array_buffer: *const v8.ArrayBuffer = undefined;
if (len == 0) {
array_buffer = v8.ArrayBuffer.init(isolate, 0);
array_buffer = v8.v8__ArrayBuffer__New(isolate.handle, 0).?;
} else {
const buffer_len = len * bits / 8;
const backing_store = v8.BackingStore.init(isolate, buffer_len);
const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData()));
const backing_store = v8.v8__ArrayBuffer__NewBackingStore(isolate.handle, buffer_len).?;
const data: [*]u8 = @ptrCast(@alignCast(v8.v8__BackingStore__Data(backing_store)));
@memcpy(data[0..buffer_len], @as([]const u8, @ptrCast(values))[0..buffer_len]);
array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr());
const backing_store_ptr = v8.v8__BackingStore__TO_SHARED_PTR(backing_store);
array_buffer = v8.v8__ArrayBuffer__New2(isolate.handle, &backing_store_ptr).?;
}
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(),
8 => return @ptrCast(v8.v8__Uint8Array__New(array_buffer, 0, len).?),
16 => return @ptrCast(v8.v8__Uint16Array__New(array_buffer, 0, len).?),
32 => return @ptrCast(v8.v8__Uint32Array__New(array_buffer, 0, len).?),
64 => return @ptrCast(v8.v8__BigUint64Array__New(array_buffer, 0, len).?),
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(),
8 => return @ptrCast(v8.v8__Int8Array__New(array_buffer, 0, len).?),
16 => return @ptrCast(v8.v8__Int16Array__New(array_buffer, 0, len).?),
32 => return @ptrCast(v8.v8__Int32Array__New(array_buffer, 0, len).?),
64 => return @ptrCast(v8.v8__BigInt64Array__New(array_buffer, 0, len).?),
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(),
32 => return @ptrCast(v8.v8__Float32Array__New(array_buffer, 0, len).?),
64 => return @ptrCast(v8.v8__Float64Array__New(array_buffer, 0, len).?),
else => {},
},
else => {},
@@ -388,6 +197,7 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo
// but this can never be valid.
@compileError("Invalid TypeArray type: " ++ @typeName(value_type));
},
inline String, BigInt, Integer, Number, Value, Object => return value.handle,
else => {},
}
},
@@ -405,21 +215,6 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo
}
return null;
}
pub fn _createException(isolate: v8.Isolate, msg: []const u8) v8.Value {
return v8.Exception.initError(v8.String.initUtf8(isolate, msg));
}
pub fn classNameForStruct(comptime Struct: type) []const u8 {
if (@hasDecl(Struct, "js_name")) {
return Struct.js_name;
}
@setEvalBranchQuota(10_000);
const full_name = @typeName(Struct);
const last = std.mem.lastIndexOfScalar(u8, full_name, '.') orelse return full_name;
return full_name[last + 1 ..];
}
// When we return a Zig object to V8, we put it on the heap and pass it into
// v8 as an *anyopaque (i.e. void *). When V8 gives us back the value, say, as a
// function parameter, we know what type it _should_ be.
@@ -465,8 +260,8 @@ pub const TaggedAnyOpaque = struct {
// When we're asked to describe an object via the Inspector, we _must_ include
// the proper subtype (and description) fields in the returned JSON.
// V8 will give us a Value and ask us for the subtype. From the v8.Value we
// can get a v8.Object, and from the v8.Object, we can get out TaggedAnyOpaque
// V8 will give us a Value and ask us for the subtype. From the js.Value we
// can get a js.Object, and from the js.Object, we can get out TaggedAnyOpaque
// which is where we store the subtype.
subtype: ?bridge.SubType,
};
@@ -483,10 +278,10 @@ pub const PrototypeChainEntry = struct {
// it'll call this function to gets its [optional] subtype - which, from V8's
// point of view, is an arbitrary string.
pub export fn v8_inspector__Client__IMPL__valueSubtype(
_: *v8.c.InspectorClientImpl,
c_value: *const v8.C_Value,
_: *v8.InspectorClientImpl,
c_value: *const v8.Value,
) callconv(.c) [*c]const u8 {
const external_entry = Inspector.getTaggedAnyOpaque(.{ .handle = c_value }) orelse return null;
const external_entry = Inspector.getTaggedAnyOpaque(c_value) orelse return null;
return if (external_entry.subtype) |st| @tagName(st) else null;
}
@@ -495,15 +290,15 @@ pub export fn v8_inspector__Client__IMPL__valueSubtype(
// present, even if it's empty. So if we have a subType for the value, we'll
// put an empty description.
pub export fn v8_inspector__Client__IMPL__descriptionForValueSubtype(
_: *v8.c.InspectorClientImpl,
v8_context: *const v8.C_Context,
c_value: *const v8.C_Value,
_: *v8.InspectorClientImpl,
v8_context: *const v8.Context,
c_value: *const v8.Value,
) callconv(.c) [*c]const u8 {
_ = v8_context;
// We _must_ include a non-null description in order for the subtype value
// to be included. Besides that, I don't know if the value has any meaning
const external_entry = Inspector.getTaggedAnyOpaque(.{ .handle = c_value }) orelse return null;
const external_entry = Inspector.getTaggedAnyOpaque(c_value) orelse return null;
return if (external_entry.subtype == null) null else "";
}

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const h5e = @import("html5ever.zig");
const Page = @import("../Page.zig");
@@ -104,7 +105,7 @@ pub fn parseXML(self: *Parser, xml: []const u8) void {
xml.len,
&self.container,
self,
createElementCallback,
createXMLElementCallback,
getDataCallback,
appendCallback,
parseErrorCallback,
@@ -162,7 +163,7 @@ pub const Streaming = struct {
}
pub fn start(self: *Streaming) !void {
std.debug.assert(self.handle == null);
lp.assert(self.handle == null, "Parser.start non-null handle", .{});
self.handle = h5e.html5ever_streaming_parser_create(
&self.parser.container,
@@ -225,17 +226,26 @@ fn _popCallback(self: *Parser, node: *Node) !void {
}
fn createElementCallback(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator) callconv(.c) ?*anyopaque {
return _createElementCallbackWithDefaultnamespace(ctx, data, qname, attributes, .unknown);
}
fn createXMLElementCallback(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator) callconv(.c) ?*anyopaque {
return _createElementCallbackWithDefaultnamespace(ctx, data, qname, attributes, .xml);
}
fn _createElementCallbackWithDefaultnamespace(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator, default_namespace: Element.Namespace) ?*anyopaque {
const self: *Parser = @ptrCast(@alignCast(ctx));
return self._createElementCallback(data, qname, attributes) catch |err| {
return self._createElementCallback(data, qname, attributes, default_namespace) catch |err| {
self.err = .{ .err = err, .source = .create_element };
return null;
};
}
fn _createElementCallback(self: *Parser, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator) !*anyopaque {
fn _createElementCallback(self: *Parser, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator, default_namespace: Element.Namespace) !*anyopaque {
const page = self.page;
const name = qname.local.slice();
const namespace = qname.ns.slice();
const node = try page.createElement(namespace, name, attributes);
const namespace_string = qname.ns.slice();
const namespace = if (namespace_string.len == 0) default_namespace else Element.Namespace.parse(namespace_string);
const node = try page.createElementNS(namespace, name, attributes);
const pn = try self.arena.create(ParsedNode);
pn.* = .{
@@ -348,7 +358,7 @@ fn getDataCallback(ctx: *anyopaque) callconv(.c) *anyopaque {
const pn: *ParsedNode = @ptrCast(@alignCast(ctx));
// For non-elements, data is null. But, we expect this to only ever
// be called for elements.
std.debug.assert(pn.data != null);
lp.assert(pn.data != null, "Parser.getDataCallback null data", .{});
return pn.data.?;
}

View File

@@ -0,0 +1,35 @@
<!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>

View File

@@ -0,0 +1,87 @@
<!DOCTYPE html>
<script src="../testing.js"></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);
const { UNMASKED_VENDOR_WEBGL, UNMASKED_RENDERER_WEBGL } = rendererInfo;
testing.expectEqual(UNMASKED_VENDOR_WEBGL, 0x9245);
testing.expectEqual(UNMASKED_RENDERER_WEBGL, 0x9246);
testing.expectEqual("", ctx.getParameter(UNMASKED_VENDOR_WEBGL));
testing.expectEqual("", ctx.getParameter(UNMASKED_RENDERER_WEBGL));
}
// 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

@@ -201,8 +201,8 @@ cdataClassName<!DOCTYPE html>
root.appendChild(cdata);
root.appendChild(elem2);
testing.expectEqual('LAST', cdata.nextElementSibling.tagName);
testing.expectEqual('FIRST', cdata.previousElementSibling.tagName);
testing.expectEqual('last', cdata.nextElementSibling.tagName);
testing.expectEqual('first', cdata.previousElementSibling.tagName);
}
</script>

View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<title>Test Page</title>
</head>
<body>
<h1>Test Page</h1>
<nav>
<a href="/page1" id="link1">First Link</a>
<a href="/page2" id="link2">Second Link</a>
</nav>
<form id="testForm" action="/submit" method="post">
<label for="username">Username:</label>
<input type="text" id="username" name="username" placeholder="Enter username">
<label for="email">Email:</label>
<input type="email" id="email" name="email" placeholder="Enter email">
<label for="password">Password:</label>
<input type="password" id="password" name="password">
<button type="submit">Submit</button>
</form>
</body>
</html>

View File

@@ -54,3 +54,68 @@
const regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
testing.expectEqual(true, regex.test(uuid));
</script> -->
<script id=SubtleCrypto>
testing.expectEqual(true, crypto.subtle instanceof SubtleCrypto);
</script>
<script id=sign-and-verify-hmac>
testing.async(async () => {
let key = await crypto.subtle.generateKey(
{
name: "HMAC",
hash: { name: "SHA-512" },
},
true,
["sign", "verify"],
);
testing.expectEqual(true, key instanceof CryptoKey);
const raw = await crypto.subtle.exportKey("raw", key);
testing.expectEqual(128, raw.byteLength);
const encoder = new TextEncoder();
const signature = await crypto.subtle.sign(
"HMAC",
key,
encoder.encode("Hello, world!")
);
testing.expectEqual(true, signature instanceof ArrayBuffer);
const result = await window.crypto.subtle.verify(
{ name: "HMAC" },
key,
signature,
encoder.encode("Hello, world!")
);
testing.expectEqual(true, result);
});
</script>
<script id=derive-shared-key-x25519>
testing.async(async () => {
const { privateKey, publicKey } = await crypto.subtle.generateKey(
{ name: "X25519" },
true,
["deriveBits"],
);
testing.expectEqual(true, privateKey instanceof CryptoKey);
testing.expectEqual(true, publicKey instanceof CryptoKey);
const sharedKey = await crypto.subtle.deriveBits(
{
name: "X25519",
public: publicKey,
},
privateKey,
128,
);
testing.expectEqual(16, sharedKey.byteLength);
});
</script>

View File

@@ -27,329 +27,329 @@
customElements.define('my-early', MyEarly);
testing.expectEqual(true, early.upgraded);
testing.expectEqual(1, constructorCalled);
testing.expectEqual(1, connectedCalled);
// testing.expectEqual(1, connectedCalled);
}
{
let order = [];
// {
// let order = [];
class UpgradeParent extends HTMLElement {
constructor() {
super();
order.push('parent-constructor');
}
connectedCallback() {
order.push('parent-connected');
}
}
class UpgradeChild extends HTMLElement {
constructor() {
super();
order.push('child-constructor');
}
connectedCallback() {
order.push('child-connected');
}
}
// class UpgradeParent extends HTMLElement {
// constructor() {
// super();
// order.push('parent-constructor');
// }
// connectedCallback() {
// order.push('parent-connected');
// }
// }
// class UpgradeChild extends HTMLElement {
// constructor() {
// super();
// order.push('child-constructor');
// }
// connectedCallback() {
// order.push('child-connected');
// }
// }
const container = document.createElement('div');
container.innerHTML = '<upgrade-parent><upgrade-child></upgrade-child></upgrade-parent>';
document.body.appendChild(container);
testing.expectEqual(0, order.length);
// const container = document.createElement('div');
// container.innerHTML = '<upgrade-parent><upgrade-child></upgrade-child></upgrade-parent>';
// document.body.appendChild(container);
// testing.expectEqual(0, order.length);
customElements.define('upgrade-parent', UpgradeParent);
testing.expectEqual(2, order.length);
testing.expectEqual('parent-constructor', order[0]);
testing.expectEqual('parent-connected', order[1]);
customElements.define('upgrade-child', UpgradeChild);
testing.expectEqual(4, order.length);
testing.expectEqual('child-constructor', order[2]);
testing.expectEqual('child-connected', order[3]);
}
// customElements.define('upgrade-parent', UpgradeParent);
// testing.expectEqual(2, order.length);
// testing.expectEqual('parent-constructor', order[0]);
// testing.expectEqual('parent-connected', order[1]);
// customElements.define('upgrade-child', UpgradeChild);
// testing.expectEqual(4, order.length);
// testing.expectEqual('child-constructor', order[2]);
// testing.expectEqual('child-connected', order[3]);
// }
{
let connectedCalled = 0;
// {
// let connectedCalled = 0;
class DetachedUpgrade extends HTMLElement {
connectedCallback() {
connectedCalled++;
}
}
const container = document.createElement('div');
container.innerHTML = '<detached-upgrade></detached-upgrade>';
testing.expectEqual(0, connectedCalled);
customElements.define('detached-upgrade', DetachedUpgrade);
testing.expectEqual(0, connectedCalled);
document.body.appendChild(container);
testing.expectEqual(1, connectedCalled);
}
{
let constructorCalled = 0;
let connectedCalled = 0;
class ManualUpgrade extends HTMLElement {
constructor() {
super();
constructorCalled++;
this.manuallyUpgraded = true;
}
connectedCallback() {
connectedCalled++;
}
}
// class DetachedUpgrade extends HTMLElement {
// connectedCallback() {
// connectedCalled++;
// }
// }
// const container = document.createElement('div');
// container.innerHTML = '<detached-upgrade></detached-upgrade>';
// testing.expectEqual(0, connectedCalled);
// customElements.define('detached-upgrade', DetachedUpgrade);
// testing.expectEqual(0, connectedCalled);
// document.body.appendChild(container);
// testing.expectEqual(1, connectedCalled);
// }
// {
// let constructorCalled = 0;
// let connectedCalled = 0;
// class ManualUpgrade extends HTMLElement {
// constructor() {
// super();
// constructorCalled++;
// this.manuallyUpgraded = true;
// }
// connectedCallback() {
// connectedCalled++;
// }
// }
customElements.define('manual-upgrade', ManualUpgrade);
// customElements.define('manual-upgrade', ManualUpgrade);
const container = document.createElement('div');
container.innerHTML = '<manual-upgrade id="m1"><manual-upgrade id="m2"></manual-upgrade></manual-upgrade>';
// const container = document.createElement('div');
// container.innerHTML = '<manual-upgrade id="m1"><manual-upgrade id="m2"></manual-upgrade></manual-upgrade>';
testing.expectEqual(2, constructorCalled);
testing.expectEqual(0, connectedCalled);
// testing.expectEqual(2, constructorCalled);
// testing.expectEqual(0, connectedCalled);
customElements.upgrade(container);
// customElements.upgrade(container);
testing.expectEqual(2, constructorCalled);
testing.expectEqual(0, connectedCalled);
const m1 = container.querySelector('#m1');
const m2 = container.querySelector('#m2');
testing.expectEqual(true, m1.manuallyUpgraded);
testing.expectEqual(true, m2.manuallyUpgraded);
document.body.appendChild(container);
testing.expectEqual(2, connectedCalled);
}
{
let alreadyUpgradedCalled = 0;
class AlreadyUpgraded extends HTMLElement {
constructor() {
super();
alreadyUpgradedCalled++;
}
}
// testing.expectEqual(2, constructorCalled);
// testing.expectEqual(0, connectedCalled);
// const m1 = container.querySelector('#m1');
// const m2 = container.querySelector('#m2');
// testing.expectEqual(true, m1.manuallyUpgraded);
// testing.expectEqual(true, m2.manuallyUpgraded);
// document.body.appendChild(container);
// testing.expectEqual(2, connectedCalled);
// }
// {
// let alreadyUpgradedCalled = 0;
// class AlreadyUpgraded extends HTMLElement {
// constructor() {
// super();
// alreadyUpgradedCalled++;
// }
// }
const elem = document.createElement('div');
elem.innerHTML = '<already-upgraded></already-upgraded>';
document.body.appendChild(elem);
// const elem = document.createElement('div');
// elem.innerHTML = '<already-upgraded></already-upgraded>';
// document.body.appendChild(elem);
customElements.define('already-upgraded', AlreadyUpgraded);
testing.expectEqual(1, alreadyUpgradedCalled);
// customElements.define('already-upgraded', AlreadyUpgraded);
// testing.expectEqual(1, alreadyUpgradedCalled);
customElements.upgrade(elem);
testing.expectEqual(1, alreadyUpgradedCalled);
}
// customElements.upgrade(elem);
// testing.expectEqual(1, alreadyUpgradedCalled);
// }
{
let attributeChangedCalls = [];
// {
// let attributeChangedCalls = [];
class UpgradeWithAttrs extends HTMLElement {
static get observedAttributes() {
return ['data-foo', 'data-bar'];
}
// class UpgradeWithAttrs extends HTMLElement {
// static get observedAttributes() {
// return ['data-foo', 'data-bar'];
// }
attributeChangedCallback(name, oldValue, newValue) {
attributeChangedCalls.push({ name, oldValue, newValue });
}
}
// attributeChangedCallback(name, oldValue, newValue) {
// attributeChangedCalls.push({ name, oldValue, newValue });
// }
// }
const container = document.createElement('div');
container.innerHTML = '<upgrade-with-attrs data-foo="hello" data-bar="world"></upgrade-with-attrs>';
document.body.appendChild(container);
// const container = document.createElement('div');
// container.innerHTML = '<upgrade-with-attrs data-foo="hello" data-bar="world"></upgrade-with-attrs>';
// document.body.appendChild(container);
testing.expectEqual(0, attributeChangedCalls.length);
// testing.expectEqual(0, attributeChangedCalls.length);
customElements.define('upgrade-with-attrs', UpgradeWithAttrs);
// customElements.define('upgrade-with-attrs', UpgradeWithAttrs);
testing.expectEqual(2, attributeChangedCalls.length);
testing.expectEqual('data-foo', attributeChangedCalls[0].name);
testing.expectEqual(null, attributeChangedCalls[0].oldValue);
testing.expectEqual('hello', attributeChangedCalls[0].newValue);
testing.expectEqual('data-bar', attributeChangedCalls[1].name);
testing.expectEqual(null, attributeChangedCalls[1].oldValue);
testing.expectEqual('world', attributeChangedCalls[1].newValue);
}
// testing.expectEqual(2, attributeChangedCalls.length);
// testing.expectEqual('data-foo', attributeChangedCalls[0].name);
// testing.expectEqual(null, attributeChangedCalls[0].oldValue);
// testing.expectEqual('hello', attributeChangedCalls[0].newValue);
// testing.expectEqual('data-bar', attributeChangedCalls[1].name);
// testing.expectEqual(null, attributeChangedCalls[1].oldValue);
// testing.expectEqual('world', attributeChangedCalls[1].newValue);
// }
{
let attributeChangedCalls = [];
let connectedCalls = 0;
// {
// let attributeChangedCalls = [];
// let connectedCalls = 0;
class DetachedWithAttrs extends HTMLElement {
static get observedAttributes() {
return ['foo'];
}
// class DetachedWithAttrs extends HTMLElement {
// static get observedAttributes() {
// return ['foo'];
// }
attributeChangedCallback(name, oldValue, newValue) {
attributeChangedCalls.push({ name, oldValue, newValue });
}
// attributeChangedCallback(name, oldValue, newValue) {
// attributeChangedCalls.push({ name, oldValue, newValue });
// }
connectedCallback() {
connectedCalls++;
}
}
// connectedCallback() {
// connectedCalls++;
// }
// }
const container = document.createElement('div');
container.innerHTML = '<detached-with-attrs foo="bar"></detached-with-attrs>';
// const container = document.createElement('div');
// container.innerHTML = '<detached-with-attrs foo="bar"></detached-with-attrs>';
testing.expectEqual(0, attributeChangedCalls.length);
// testing.expectEqual(0, attributeChangedCalls.length);
customElements.define('detached-with-attrs', DetachedWithAttrs);
// customElements.define('detached-with-attrs', DetachedWithAttrs);
testing.expectEqual(0, attributeChangedCalls.length);
testing.expectEqual(0, connectedCalls);
// testing.expectEqual(0, attributeChangedCalls.length);
// testing.expectEqual(0, connectedCalls);
document.body.appendChild(container);
// document.body.appendChild(container);
testing.expectEqual(1, attributeChangedCalls.length);
testing.expectEqual('foo', attributeChangedCalls[0].name);
testing.expectEqual(null, attributeChangedCalls[0].oldValue);
testing.expectEqual('bar', attributeChangedCalls[0].newValue);
testing.expectEqual(1, connectedCalls);
}
// testing.expectEqual(1, attributeChangedCalls.length);
// testing.expectEqual('foo', attributeChangedCalls[0].name);
// testing.expectEqual(null, attributeChangedCalls[0].oldValue);
// testing.expectEqual('bar', attributeChangedCalls[0].newValue);
// testing.expectEqual(1, connectedCalls);
// }
{
let attributeChangedCalls = [];
let constructorCalled = 0;
// {
// let attributeChangedCalls = [];
// let constructorCalled = 0;
class ManualUpgradeWithAttrs extends HTMLElement {
static get observedAttributes() {
return ['x', 'y'];
}
// class ManualUpgradeWithAttrs extends HTMLElement {
// static get observedAttributes() {
// return ['x', 'y'];
// }
constructor() {
super();
constructorCalled++;
}
// constructor() {
// super();
// constructorCalled++;
// }
attributeChangedCallback(name, oldValue, newValue) {
attributeChangedCalls.push({ name, oldValue, newValue });
}
}
// attributeChangedCallback(name, oldValue, newValue) {
// attributeChangedCalls.push({ name, oldValue, newValue });
// }
// }
customElements.define('manual-upgrade-with-attrs', ManualUpgradeWithAttrs);
// customElements.define('manual-upgrade-with-attrs', ManualUpgradeWithAttrs);
const container = document.createElement('div');
container.innerHTML = '<manual-upgrade-with-attrs x="1" y="2"></manual-upgrade-with-attrs>';
// const container = document.createElement('div');
// container.innerHTML = '<manual-upgrade-with-attrs x="1" y="2"></manual-upgrade-with-attrs>';
testing.expectEqual(1, constructorCalled);
testing.expectEqual(2, attributeChangedCalls.length);
// testing.expectEqual(1, constructorCalled);
// testing.expectEqual(2, attributeChangedCalls.length);
const elem = container.querySelector('manual-upgrade-with-attrs');
elem.setAttribute('z', '3');
// const elem = container.querySelector('manual-upgrade-with-attrs');
// elem.setAttribute('z', '3');
customElements.upgrade(container);
// customElements.upgrade(container);
testing.expectEqual(1, constructorCalled);
testing.expectEqual(2, attributeChangedCalls.length);
}
// testing.expectEqual(1, constructorCalled);
// testing.expectEqual(2, attributeChangedCalls.length);
// }
{
let attributeChangedCalls = [];
// {
// let attributeChangedCalls = [];
class MixedAttrs extends HTMLElement {
static get observedAttributes() {
return ['watched'];
}
// class MixedAttrs extends HTMLElement {
// static get observedAttributes() {
// return ['watched'];
// }
attributeChangedCallback(name, oldValue, newValue) {
attributeChangedCalls.push({ name, oldValue, newValue });
}
}
// attributeChangedCallback(name, oldValue, newValue) {
// attributeChangedCalls.push({ name, oldValue, newValue });
// }
// }
const container = document.createElement('div');
container.innerHTML = '<mixed-attrs watched="yes" ignored="no" also-ignored="maybe"></mixed-attrs>';
document.body.appendChild(container);
// const container = document.createElement('div');
// container.innerHTML = '<mixed-attrs watched="yes" ignored="no" also-ignored="maybe"></mixed-attrs>';
// document.body.appendChild(container);
testing.expectEqual(0, attributeChangedCalls.length);
// testing.expectEqual(0, attributeChangedCalls.length);
customElements.define('mixed-attrs', MixedAttrs);
// customElements.define('mixed-attrs', MixedAttrs);
testing.expectEqual(1, attributeChangedCalls.length);
testing.expectEqual('watched', attributeChangedCalls[0].name);
testing.expectEqual('yes', attributeChangedCalls[0].newValue);
}
// testing.expectEqual(1, attributeChangedCalls.length);
// testing.expectEqual('watched', attributeChangedCalls[0].name);
// testing.expectEqual('yes', attributeChangedCalls[0].newValue);
// }
{
let attributeChangedCalls = [];
// {
// let attributeChangedCalls = [];
class EmptyAttr extends HTMLElement {
static get observedAttributes() {
return ['empty', 'non-empty'];
}
// class EmptyAttr extends HTMLElement {
// static get observedAttributes() {
// return ['empty', 'non-empty'];
// }
attributeChangedCallback(name, oldValue, newValue) {
attributeChangedCalls.push({ name, oldValue, newValue });
}
}
// attributeChangedCallback(name, oldValue, newValue) {
// attributeChangedCalls.push({ name, oldValue, newValue });
// }
// }
const container = document.createElement('div');
container.innerHTML = '<empty-attr empty="" non-empty="value"></empty-attr>';
document.body.appendChild(container);
// const container = document.createElement('div');
// container.innerHTML = '<empty-attr empty="" non-empty="value"></empty-attr>';
// document.body.appendChild(container);
customElements.define('empty-attr', EmptyAttr);
// customElements.define('empty-attr', EmptyAttr);
testing.expectEqual(2, attributeChangedCalls.length);
testing.expectEqual('empty', attributeChangedCalls[0].name);
testing.expectEqual('', attributeChangedCalls[0].newValue);
testing.expectEqual('non-empty', attributeChangedCalls[1].name);
testing.expectEqual('value', attributeChangedCalls[1].newValue);
}
// testing.expectEqual(2, attributeChangedCalls.length);
// testing.expectEqual('empty', attributeChangedCalls[0].name);
// testing.expectEqual('', attributeChangedCalls[0].newValue);
// testing.expectEqual('non-empty', attributeChangedCalls[1].name);
// testing.expectEqual('value', attributeChangedCalls[1].newValue);
// }
{
let parentCalls = [];
let childCalls = [];
// {
// let parentCalls = [];
// let childCalls = [];
class NestedParent extends HTMLElement {
static get observedAttributes() {
return ['parent-attr'];
}
// class NestedParent extends HTMLElement {
// static get observedAttributes() {
// return ['parent-attr'];
// }
attributeChangedCallback(name, oldValue, newValue) {
parentCalls.push({ name, oldValue, newValue });
}
}
// attributeChangedCallback(name, oldValue, newValue) {
// parentCalls.push({ name, oldValue, newValue });
// }
// }
class NestedChild extends HTMLElement {
static get observedAttributes() {
return ['child-attr'];
}
// class NestedChild extends HTMLElement {
// static get observedAttributes() {
// return ['child-attr'];
// }
attributeChangedCallback(name, oldValue, newValue) {
childCalls.push({ name, oldValue, newValue });
}
}
// attributeChangedCallback(name, oldValue, newValue) {
// childCalls.push({ name, oldValue, newValue });
// }
// }
const container = document.createElement('div');
container.innerHTML = '<nested-parent parent-attr="p"><nested-child child-attr="c"></nested-child></nested-parent>';
document.body.appendChild(container);
// const container = document.createElement('div');
// container.innerHTML = '<nested-parent parent-attr="p"><nested-child child-attr="c"></nested-child></nested-parent>';
// document.body.appendChild(container);
testing.expectEqual(0, parentCalls.length);
testing.expectEqual(0, childCalls.length);
// testing.expectEqual(0, parentCalls.length);
// testing.expectEqual(0, childCalls.length);
customElements.define('nested-parent', NestedParent);
// customElements.define('nested-parent', NestedParent);
testing.expectEqual(1, parentCalls.length);
testing.expectEqual('parent-attr', parentCalls[0].name);
testing.expectEqual('p', parentCalls[0].newValue);
testing.expectEqual(0, childCalls.length);
// testing.expectEqual(1, parentCalls.length);
// testing.expectEqual('parent-attr', parentCalls[0].name);
// testing.expectEqual('p', parentCalls[0].newValue);
// testing.expectEqual(0, childCalls.length);
customElements.define('nested-child', NestedChild);
// customElements.define('nested-child', NestedChild);
testing.expectEqual(1, parentCalls.length);
testing.expectEqual(1, childCalls.length);
testing.expectEqual('child-attr', childCalls[0].name);
testing.expectEqual('c', childCalls[0].newValue);
}
// testing.expectEqual(1, parentCalls.length);
// testing.expectEqual(1, childCalls.length);
// testing.expectEqual('child-attr', childCalls[0].name);
// testing.expectEqual('c', childCalls[0].newValue);
// }
</script>

View File

@@ -19,12 +19,13 @@
testing.expectEqual('http://www.w3.org/XML/1998/namespace', xmlElement.namespaceURI);
const nullNsElement = document.createElementNS(null, 'span');
testing.expectEqual('SPAN', nullNsElement.tagName);
testing.expectEqual('http://www.w3.org/1999/xhtml', nullNsElement.namespaceURI);
testing.expectEqual('span', nullNsElement.tagName);
testing.expectEqual(null, nullNsElement.namespaceURI);
const unknownNsElement = document.createElementNS('http://example.com/unknown', 'custom');
testing.expectEqual('CUSTOM', unknownNsElement.tagName);
testing.expectEqual('http://www.w3.org/1999/xhtml', unknownNsElement.namespaceURI);
testing.expectEqual('custom', unknownNsElement.tagName);
// Should be http://example.com/unknown
testing.expectEqual('http://lightpanda.io/unsupported/namespace', unknownNsElement.namespaceURI);
const regularDiv = document.createElement('div');
testing.expectEqual('DIV', regularDiv.tagName);
@@ -36,5 +37,5 @@
testing.expectEqual('te:ST', custom.tagName);
testing.expectEqual('te', custom.prefix);
testing.expectEqual('ST', custom.localName);
testing.expectEqual('http://www.w3.org/1999/xhtml', custom.namespaceURI); // Should be test
testing.expectEqual('http://lightpanda.io/unsupported/namespace', custom.namespaceURI); // Should be test
</script>

View File

@@ -0,0 +1,344 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<head>
<title>document.replaceChildren Tests</title>
</head>
<body>
<div id="test">Original content</div>
</body>
<script id=error_multiple_elements>
{
// Test that we cannot have more than one Element child
const doc = new Document();
const div1 = doc.createElement('div');
const div2 = doc.createElement('div');
testing.expectError('HierarchyRequest', () => {
doc.replaceChildren(div1, div2);
});
}
</script>
<script id=error_multiple_elements_via_fragment>
{
// Test that we cannot have more than one Element child via DocumentFragment
const doc = new Document();
const fragment = doc.createDocumentFragment();
fragment.appendChild(doc.createElement('div'));
fragment.appendChild(doc.createElement('span'));
testing.expectError('HierarchyRequest', () => {
doc.replaceChildren(fragment);
});
}
</script>
<script id=error_multiple_doctypes>
{
// Test that we cannot have more than one DocumentType child
const doc = new Document();
const doctype1 = doc.implementation.createDocumentType('html', '', '');
const doctype2 = doc.implementation.createDocumentType('html', '', '');
testing.expectError('HierarchyRequest', () => {
doc.replaceChildren(doctype1, doctype2);
});
}
</script>
<script id=error_text_node>
{
// Test that we cannot insert Text nodes directly into Document
const doc = new Document();
testing.expectError('HierarchyRequest', () => {
doc.replaceChildren('Just text');
});
}
</script>
<script id=error_text_with_element>
{
// Test that we cannot insert Text nodes even with valid Element
const doc = new Document();
const html = doc.createElement('html');
testing.expectError('HierarchyRequest', () => {
doc.replaceChildren('Text 1', html, 'Text 2');
});
}
</script>
<script id=error_append_multiple_elements>
{
// Test that append also validates
const doc = new Document();
doc.append(doc.createElement('html'));
const div = doc.createElement('div');
testing.expectError('HierarchyRequest', () => {
doc.append(div);
});
}
</script>
<script id=error_prepend_multiple_elements>
{
// Test that prepend also validates
const doc = new Document();
doc.prepend(doc.createElement('html'));
const div = doc.createElement('div');
testing.expectError('HierarchyRequest', () => {
doc.prepend(div);
});
}
</script>
<script id=error_append_text>
{
// Test that append rejects text nodes
const doc = new Document();
testing.expectError('HierarchyRequest', () => {
doc.append('text');
});
}
</script>
<script id=error_prepend_text>
{
// Test that prepend rejects text nodes
const doc = new Document();
testing.expectError('HierarchyRequest', () => {
doc.prepend('text');
});
}
</script>
<script id=replace_with_single_element>
{
const doc = new Document();
const html = doc.createElement('html');
html.id = 'replaced';
html.textContent = 'New content';
doc.replaceChildren(html);
testing.expectEqual(1, doc.childNodes.length);
testing.expectEqual(html, doc.firstChild);
testing.expectEqual('replaced', doc.firstChild.id);
}
</script>
<script id=replace_with_comments>
{
const doc = new Document();
const comment1 = doc.createComment('Comment 1');
const html = doc.createElement('html');
const comment2 = doc.createComment('Comment 2');
doc.replaceChildren(comment1, html, comment2);
testing.expectEqual(3, doc.childNodes.length);
testing.expectEqual('#comment', doc.firstChild.nodeName);
testing.expectEqual('Comment 1', doc.firstChild.textContent);
testing.expectEqual('html', doc.childNodes[1].nodeName);
testing.expectEqual('#comment', doc.lastChild.nodeName);
testing.expectEqual('Comment 2', doc.lastChild.textContent);
}
</script>
<script id=replace_with_empty>
{
const doc = new Document();
// First add some content
const div = doc.createElement('div');
doc.replaceChildren(div);
testing.expectEqual(1, doc.childNodes.length);
// Now replace with nothing
doc.replaceChildren();
testing.expectEqual(0, doc.childNodes.length);
testing.expectEqual(null, doc.firstChild);
testing.expectEqual(null, doc.lastChild);
}
</script>
<script id=replace_removes_old_children>
{
const doc = new Document();
const comment1 = doc.createComment('old');
doc.replaceChildren(comment1);
testing.expectEqual(1, doc.childNodes.length);
testing.expectEqual(doc, comment1.parentNode);
const html = doc.createElement('html');
html.id = 'new';
doc.replaceChildren(html);
// Old child should be removed
testing.expectEqual(null, comment1.parentNode);
testing.expectEqual(1, doc.childNodes.length);
testing.expectEqual('new', doc.firstChild.id);
}
</script>
<script id=replace_with_document_fragment_valid>
{
const doc = new Document();
const fragment = doc.createDocumentFragment();
const html = doc.createElement('html');
const comment = doc.createComment('comment');
fragment.appendChild(comment);
fragment.appendChild(html);
doc.replaceChildren(fragment);
// Fragment contents should be moved
testing.expectEqual(2, doc.childNodes.length);
testing.expectEqual('#comment', doc.firstChild.nodeName);
testing.expectEqual('html', doc.lastChild.nodeName);
// Fragment should be empty now
testing.expectEqual(0, fragment.childNodes.length);
}
</script>
<script id=replace_maintains_child_order>
{
const doc = new Document();
const nodes = [];
// Document can have: comment, processing instruction, doctype, element
nodes.push(doc.createComment('comment'));
nodes.push(doc.createElement('html'));
doc.replaceChildren(...nodes);
testing.expectEqual(2, doc.childNodes.length);
testing.expectEqual('#comment', doc.childNodes[0].nodeName);
testing.expectEqual('html', doc.childNodes[1].nodeName);
}
</script>
<script id=replace_with_nested_structure>
{
const doc = new Document();
const outer = doc.createElement('html');
outer.id = 'outer';
const middle = doc.createElement('body');
middle.id = 'middle';
const inner = doc.createElement('span');
inner.id = 'inner';
inner.textContent = 'Nested';
middle.appendChild(inner);
outer.appendChild(middle);
doc.replaceChildren(outer);
testing.expectEqual(1, doc.childNodes.length);
testing.expectEqual('outer', doc.firstChild.id);
const foundInner = doc.getElementById('inner');
testing.expectEqual(inner, foundInner);
testing.expectEqual('Nested', foundInner.textContent);
}
</script>
<script id=consecutive_replaces>
{
const doc = new Document();
const html1 = doc.createElement('html');
html1.id = 'first-replace';
doc.replaceChildren(html1);
testing.expectEqual('first-replace', doc.firstChild.id);
// Replace element with comments
const comment = doc.createComment('in between');
doc.replaceChildren(comment);
testing.expectEqual(1, doc.childNodes.length);
testing.expectEqual('#comment', doc.firstChild.nodeName);
// Replace comments with new element
const html2 = doc.createElement('html');
html2.id = 'second-replace';
doc.replaceChildren(html2);
testing.expectEqual('second-replace', doc.firstChild.id);
testing.expectEqual(1, doc.childNodes.length);
// First element should no longer be in document
testing.expectEqual(null, html1.parentNode);
testing.expectEqual(null, comment.parentNode);
}
</script>
<script id=replace_with_comments_only>
{
const doc = new Document();
const comment1 = doc.createComment('First');
const comment2 = doc.createComment('Second');
doc.replaceChildren(comment1, comment2);
testing.expectEqual(2, doc.childNodes.length);
testing.expectEqual('#comment', doc.firstChild.nodeName);
testing.expectEqual('First', doc.firstChild.textContent);
testing.expectEqual('#comment', doc.lastChild.nodeName);
testing.expectEqual('Second', doc.lastChild.textContent);
}
</script>
<script id=error_fragment_with_text>
{
// DocumentFragment with text should fail when inserted into Document
const doc = new Document();
const fragment = doc.createDocumentFragment();
fragment.appendChild(doc.createTextNode('text'));
fragment.appendChild(doc.createElement('html'));
testing.expectError('HierarchyRequest', () => {
doc.replaceChildren(fragment);
});
}
</script>
<script id=append_valid_nodes>
{
const doc = new Document();
const comment = doc.createComment('test');
const html = doc.createElement('html');
doc.append(comment);
testing.expectEqual(1, doc.childNodes.length);
doc.append(html);
testing.expectEqual(2, doc.childNodes.length);
testing.expectEqual('#comment', doc.firstChild.nodeName);
testing.expectEqual('html', doc.lastChild.nodeName);
}
</script>
<script id=prepend_valid_nodes>
{
const doc = new Document();
const html = doc.createElement('html');
const comment = doc.createComment('test');
doc.prepend(html);
testing.expectEqual(1, doc.childNodes.length);
doc.prepend(comment);
testing.expectEqual(2, doc.childNodes.length);
testing.expectEqual('#comment', doc.firstChild.nodeName);
testing.expectEqual('html', doc.lastChild.nodeName);
}
</script>

View File

@@ -168,7 +168,7 @@
const root = doc.documentElement;
testing.expectEqual(true, root !== null);
// TODO: XML documents should preserve case, but we currently uppercase
testing.expectEqual('ROOT', root.tagName);
testing.expectEqual('root', root.tagName);
}
</script>
@@ -206,10 +206,9 @@
const doc = impl.createDocument('http://example.com', 'prefix:localName', null);
const root = doc.documentElement;
// TODO: XML documents should preserve case, but we currently uppercase
testing.expectEqual('prefix:LOCALNAME', root.tagName);
// TODO: Custom namespaces are being overridden to XHTML namespace
testing.expectEqual('http://www.w3.org/1999/xhtml', root.namespaceURI);
testing.expectEqual('prefix:localName', root.tagName);
// TODO: Custom namespaces are being replaced with an empty value
testing.expectEqual('http://lightpanda.io/unsupported/namespace', root.namespaceURI);
}
</script>
@@ -224,8 +223,7 @@
doc.documentElement.appendChild(child);
testing.expectEqual(1, doc.documentElement.childNodes.length);
// TODO: XML documents should preserve case, but we currently uppercase
testing.expectEqual('CHILD', doc.documentElement.firstChild.tagName);
testing.expectEqual('child', doc.documentElement.firstChild.tagName);
testing.expectEqual('Test', doc.documentElement.firstChild.textContent);
}
</script>

View File

@@ -364,14 +364,14 @@
];
for (const mime of mimes) {
const doc = parser.parseFromString(sampleXML, "text/xml");
const doc = parser.parseFromString(sampleXML, mime);
const { firstChild: { childNodes, children: collection, tagName }, children } = doc;
// doc.
testing.expectEqual(true, doc instanceof XMLDocument);
testing.expectEqual(1, children.length);
// firstChild.
// TODO: Modern browsers expect this in lowercase.
testing.expectEqual("CATALOG", tagName);
testing.expectEqual("catalog", tagName);
testing.expectEqual(25, childNodes.length);
testing.expectEqual(12, collection.length);
// Check children of first child.
@@ -379,12 +379,12 @@
const {children: elements, id} = collection.item(i);
testing.expectEqual("bk" + (100 + i + 1), id);
// TODO: Modern browsers expect these in lowercase.
testing.expectEqual("AUTHOR", elements.item(0).tagName);
testing.expectEqual("TITLE", elements.item(1).tagName);
testing.expectEqual("GENRE", elements.item(2).tagName);
testing.expectEqual("PRICE", elements.item(3).tagName);
testing.expectEqual("PUBLISH_DATE", elements.item(4).tagName);
testing.expectEqual("DESCRIPTION", elements.item(5).tagName);
testing.expectEqual("author", elements.item(0).tagName);
testing.expectEqual("title", elements.item(1).tagName);
testing.expectEqual("genre", elements.item(2).tagName);
testing.expectEqual("price", elements.item(3).tagName);
testing.expectEqual("publish_date", elements.item(4).tagName);
testing.expectEqual("description", elements.item(5).tagName);
}
}
}

View File

@@ -238,6 +238,15 @@
testing.expectEqual('[object HTMLAudioElement]', audio.toString());
testing.expectEqual(true, audio.paused);
}
// Create with `Audio` constructor.
{
const audio = new Audio();
testing.expectEqual(true, audio instanceof HTMLAudioElement);
testing.expectEqual("[object HTMLAudioElement]", audio.toString());
testing.expectEqual(true, audio.paused);
testing.expectEqual("auto", audio.getAttribute("preload"));
}
</script>
<script id="create_video_element">

View File

@@ -42,6 +42,34 @@
testing.expectEqual('initial text', $('#textarea1').defaultValue)
</script>
<script id="defaultValue_set">
{
const textarea = document.createElement('textarea')
testing.expectEqual('', textarea.defaultValue)
testing.expectEqual('', textarea.value)
// Setting defaultValue should update the text content
textarea.defaultValue = 'new default'
testing.expectEqual('new default', textarea.defaultValue)
testing.expectEqual('new default', textarea.value)
testing.expectEqual('new default', textarea.textContent)
// Setting value should not affect defaultValue
textarea.value = 'user input'
testing.expectEqual('new default', textarea.defaultValue)
testing.expectEqual('user input', textarea.value)
// Test setting defaultValue on element that already has content
const textarea2 = document.createElement('textarea')
textarea2.textContent = 'initial content'
testing.expectEqual('initial content', textarea2.defaultValue)
textarea2.defaultValue = 'modified default'
testing.expectEqual('modified default', textarea2.defaultValue)
testing.expectEqual('modified default', textarea2.textContent)
}
</script>
<script id="disabled_initial">
testing.expectEqual(false, $('#textarea1').disabled)
testing.expectEqual(true, $('#textarea3').disabled)
@@ -149,3 +177,55 @@
testing.expectFalse(textarea.outerHTML.includes('required'))
}
</script>
<script id="clone_basic">
{
const original = document.createElement('textarea')
original.defaultValue = 'default text'
testing.expectEqual('default text', original.value)
// Change the value
original.value = 'user modified'
testing.expectEqual('user modified', original.value)
testing.expectEqual('default text', original.defaultValue)
// Clone the textarea
const clone = original.cloneNode(true)
// Clone should have the runtime value copied
testing.expectEqual('user modified', clone.value)
testing.expectEqual('default text', clone.defaultValue)
}
</script>
<script id="clone_preserves_user_changes">
{
// Create a fresh element to avoid interfering with other tests
const original = document.createElement('textarea')
original.textContent = 'initial text'
testing.expectEqual('initial text', original.defaultValue)
testing.expectEqual('initial text', original.value)
// User modifies the value
original.value = 'user typed this'
testing.expectEqual('user typed this', original.value)
testing.expectEqual('initial text', original.defaultValue)
// Clone should preserve the user's changes
const clone = original.cloneNode(true)
testing.expectEqual('user typed this', clone.value)
testing.expectEqual('initial text', clone.defaultValue)
}
</script>
<script id="clone_empty_textarea">
{
const original = document.createElement('textarea')
testing.expectEqual('', original.value)
original.value = 'some content'
const clone = original.cloneNode(true)
testing.expectEqual('some content', clone.value)
}
</script>

View File

@@ -630,3 +630,8 @@
let bubbledEvent = new Event('bubble', {bubbles: true});
testing.expectEqual(false, bubbledEvent.isTrusted);
</script>
<script id=emptyMessageEvent>
// https://github.com/lightpanda-io/browser/pull/1316
testing.expectError('TypeError', () => MessageEvent(''));
</script>

View File

@@ -35,3 +35,4 @@
history.back();
</script>

View File

@@ -112,3 +112,28 @@
});
});
</script>
<script id="microtask_access_to_records">
testing.async(async () => {
let savedRecords;
const promise = new Promise((resolve) => {
const element = document.createElement('div');
const observer = new MutationObserver((records) => {
// Save the records array itself
savedRecords = records;
resolve();
observer.disconnect();
});
observer.observe(element, { attributes: true });
element.setAttribute('test', 'value');
});
await promise;
// Force arena reset by making a Zig call
document.getElementsByTagName('*');
testing.expectEqual(1, savedRecords.length);
testing.expectEqual('attributes', savedRecords[0].type);
testing.expectEqual('test', savedRecords[0].attributeName);
});
</script>

View File

@@ -2,12 +2,12 @@
<script src="../testing.js"></script>
<script id=response>
let response = new Response("Hello, World!");
testing.expectEqual(200, response.status);
testing.expectEqual("", response.statusText);
testing.expectEqual(true, response.ok);
testing.expectEqual("", response.url);
testing.expectEqual(false, response.redirected);
// let response = new Response("Hello, World!");
// testing.expectEqual(200, response.status);
// testing.expectEqual("", response.statusText);
// testing.expectEqual(true, response.ok);
// testing.expectEqual("", response.url);
// testing.expectEqual(false, response.redirected);
let response2 = new Response("Error occurred", {
status: 404,
@@ -18,28 +18,29 @@
"Cache-Control": "no-cache"
}
});
testing.expectEqual(404, response2.status);
testing.expectEqual("Not Found", response2.statusText);
testing.expectEqual(false, response2.ok);
testing.expectEqual("text/plain", response2.headers.get("Content-Type"));
testing.expectEqual("test-value", response2.headers.get("X-Custom"));
testing.expectEqual(true, true);
// testing.expectEqual(404, response2.status);
// testing.expectEqual("Not Found", response2.statusText);
// testing.expectEqual(false, response2.ok);
// testing.expectEqual("text/plain", response2.headers);
// testing.expectEqual("test-value", response2.headers.get("X-Custom"));
testing.expectEqual("no-cache", response2.headers.get("cache-control"));
let response3 = new Response("Created", { status: 201, statusText: "Created" });
testing.expectEqual("basic", response3.type);
testing.expectEqual(201, response3.status);
testing.expectEqual("Created", response3.statusText);
testing.expectEqual(true, response3.ok);
// let response3 = new Response("Created", { status: 201, statusText: "Created" });
// testing.expectEqual("basic", response3.type);
// testing.expectEqual(201, response3.status);
// testing.expectEqual("Created", response3.statusText);
// testing.expectEqual(true, response3.ok);
let nullResponse = new Response(null);
testing.expectEqual(200, nullResponse.status);
testing.expectEqual("", nullResponse.statusText);
// let nullResponse = new Response(null);
// testing.expectEqual(200, nullResponse.status);
// testing.expectEqual("", nullResponse.statusText);
let emptyResponse = new Response("");
testing.expectEqual(200, emptyResponse.status);
// let emptyResponse = new Response("");
// testing.expectEqual(200, emptyResponse.status);
</script>
<script id=json>
<!-- <script id=json>
testing.async(async () => {
const json = await new Promise((resolve) => {
let response = new Response('[]');
@@ -48,3 +49,4 @@
testing.expectEqual([], json);
});
</script>
-->

View File

@@ -36,3 +36,36 @@
performance.mark("operationEnd", { startTime: 34.0 });
}
</script>
<<<<<<< HEAD
<script id="microtask_access_to_list">
{
let savedList;
const promise = new Promise((resolve) => {
const observer = new PerformanceObserver((list, observer) => {
savedList = list;
resolve();
observer.disconnect();
});
observer.observe({ type: "mark" });
performance.mark("testMark");
});
testing.async(async () => {
await promise;
// force a call_depth reset, which will clear the call_arena
document.getElementsByTagName('*');
const entries = savedList.getEntries();
testing.expectEqual(true, entries instanceof Array, {script_id: 'microtask_access_to_list'});
testing.expectEqual(1, entries.length);
testing.expectEqual("testMark", entries[0].name);
testing.expectEqual("mark", entries[0].entryType);
});
}
</script>
<script>
testing.expectEqual(['mark', 'measure'], PerformanceObserver.supportedEntryTypes);
</script>

View File

@@ -820,3 +820,137 @@
});
}
</script>
<script id=deleteContents_crossNode>
{
// Test deleteContents across multiple sibling text nodes
const p = document.createElement('p');
p.appendChild(document.createTextNode('AAAA'));
p.appendChild(document.createTextNode('BBBB'));
p.appendChild(document.createTextNode('CCCC'));
testing.expectEqual(3, p.childNodes.length);
testing.expectEqual('AAAABBBBCCCC', p.textContent);
const range = document.createRange();
// Start at position 2 in first text node ("AA|AA")
range.setStart(p.childNodes[0], 2);
// End at position 2 in third text node ("CC|CC")
range.setEnd(p.childNodes[2], 2);
range.deleteContents();
// Should have truncated first node to "AA" and third node to "CC"
// Middle node should be removed
testing.expectEqual(2, p.childNodes.length);
testing.expectEqual('AA', p.childNodes[0].textContent);
testing.expectEqual('CC', p.childNodes[1].textContent);
testing.expectEqual('AACC', p.textContent);
}
</script>
<script id=deleteContents_crossNode_partial>
{
// Test deleteContents where start node is completely preserved
const p = document.createElement('p');
p.appendChild(document.createTextNode('KEEP'));
p.appendChild(document.createTextNode('DELETE'));
p.appendChild(document.createTextNode('PARTIAL'));
const range = document.createRange();
// Start at end of first text node
range.setStart(p.childNodes[0], 4);
// End in middle of third text node
range.setEnd(p.childNodes[2], 4);
range.deleteContents();
testing.expectEqual(2, p.childNodes.length);
testing.expectEqual('KEEP', p.childNodes[0].textContent);
testing.expectEqual('IAL', p.childNodes[1].textContent);
testing.expectEqual('KEEPIAL', p.textContent);
}
</script>
<script id=extractContents_crossNode>
{
// Test extractContents across multiple sibling text nodes
const p = document.createElement('p');
p.appendChild(document.createTextNode('AAAA'));
p.appendChild(document.createTextNode('BBBB'));
p.appendChild(document.createTextNode('CCCC'));
const range = document.createRange();
range.setStart(p.childNodes[0], 2);
range.setEnd(p.childNodes[2], 2);
const fragment = range.extractContents();
// Original should be truncated
testing.expectEqual(2, p.childNodes.length);
testing.expectEqual('AA', p.childNodes[0].textContent);
testing.expectEqual('CC', p.childNodes[1].textContent);
// Fragment should contain extracted content
testing.expectEqual(3, fragment.childNodes.length);
testing.expectEqual('AA', fragment.childNodes[0].textContent);
testing.expectEqual('BBBB', fragment.childNodes[1].textContent);
testing.expectEqual('CC', fragment.childNodes[2].textContent);
}
</script>
<script id=cloneContents_crossNode>
{
// Test cloneContents across multiple sibling text nodes
const p = document.createElement('p');
p.appendChild(document.createTextNode('AAAA'));
p.appendChild(document.createTextNode('BBBB'));
p.appendChild(document.createTextNode('CCCC'));
const range = document.createRange();
range.setStart(p.childNodes[0], 2);
range.setEnd(p.childNodes[2], 2);
const fragment = range.cloneContents();
// Original should be unchanged
testing.expectEqual(3, p.childNodes.length);
testing.expectEqual('AAAA', p.childNodes[0].textContent);
testing.expectEqual('BBBB', p.childNodes[1].textContent);
testing.expectEqual('CCCC', p.childNodes[2].textContent);
// Fragment should contain cloned content
testing.expectEqual(3, fragment.childNodes.length);
testing.expectEqual('AA', fragment.childNodes[0].textContent);
testing.expectEqual('BBBB', fragment.childNodes[1].textContent);
testing.expectEqual('CC', fragment.childNodes[2].textContent);
}
</script>
<script id=deleteContents_crossNode_withElements>
{
// Test deleteContents with mixed text and element nodes
const div = document.createElement('div');
div.appendChild(document.createTextNode('Start'));
const span = document.createElement('span');
span.textContent = 'Middle';
div.appendChild(span);
div.appendChild(document.createTextNode('End'));
testing.expectEqual(3, div.childNodes.length);
const range = document.createRange();
// Start in middle of first text node
range.setStart(div.childNodes[0], 2);
// End in middle of last text node
range.setEnd(div.childNodes[2], 1);
range.deleteContents();
// Should keep "St" from start, remove span, keep "nd" from end
testing.expectEqual(2, div.childNodes.length);
testing.expectEqual('St', div.childNodes[0].textContent);
testing.expectEqual('nd', div.childNodes[1].textContent);
testing.expectEqual('Stnd', div.textContent);
}
</script>

View File

@@ -36,7 +36,7 @@
function expectError(expected, fn) {
withError((err) => {
expectEqual(expected, err.toString());
expectEqual(true, err.toString().includes(expected));
}, fn);
}

View File

@@ -18,7 +18,7 @@
testing.expectEqual(1, navigator.languages.length);
testing.expectEqual('en-US', navigator.languages[0]);
testing.expectEqual(true, navigator.onLine);
testing.expectEqual(false, navigator.cookieEnabled);
testing.expectEqual(true, navigator.cookieEnabled);
testing.expectEqual(true, navigator.hardwareConcurrency > 0);
testing.expectEqual(4, navigator.hardwareConcurrency);
testing.expectEqual(0, navigator.maxTouchPoints);

View File

@@ -37,8 +37,8 @@ pub fn getSignal(self: *const AbortController) *AbortSignal {
return self._signal;
}
pub fn abort(self: *AbortController, reason_: ?js.Object, page: *Page) !void {
try self._signal.abort(if (reason_) |r| .{ .js_obj = r } else null, page);
pub fn abort(self: *AbortController, reason_: ?js.Value.Global, page: *Page) !void {
try self._signal.abort(if (reason_) |r| .{ .js_val = r } else null, page);
}
pub const JsApi = struct {

View File

@@ -29,7 +29,7 @@ const AbortSignal = @This();
_proto: *EventTarget,
_aborted: bool = false,
_reason: Reason = .undefined,
_on_abort: ?js.Function = null,
_on_abort: ?js.Function.Global = null,
pub fn init(page: *Page) !*AbortSignal {
return page._factory.eventTarget(AbortSignal{
@@ -45,16 +45,12 @@ pub fn getReason(self: *const AbortSignal) Reason {
return self._reason;
}
pub fn getOnAbort(self: *const AbortSignal) ?js.Function {
pub fn getOnAbort(self: *const AbortSignal) ?js.Function.Global {
return self._on_abort;
}
pub fn setOnAbort(self: *AbortSignal, cb_: ?js.Function) !void {
if (cb_) |cb| {
self._on_abort = try cb.withThis(self);
} else {
self._on_abort = null;
}
pub fn setOnAbort(self: *AbortSignal, cb: ?js.Function.Global) !void {
self._on_abort = cb;
}
pub fn asEventTarget(self: *AbortSignal) *EventTarget {
@@ -71,7 +67,7 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, page: *Page) !void {
// Store the abort reason (default to a simple string if none provided)
if (reason_) |reason| {
switch (reason) {
.js_obj => |js_obj| self._reason = .{ .js_obj = try js_obj.persist() },
.js_val => |js_val| self._reason = .{ .js_val = js_val },
.string => |str| self._reason = .{ .string = try page.dupeString(str) },
.undefined => self._reason = reason,
}
@@ -81,18 +77,19 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, page: *Page) !void {
// Dispatch abort event
const event = try Event.initTrusted("abort", .{}, page);
const func = if (self._on_abort) |*g| g.local() else null;
try page._event_manager.dispatchWithFunction(
self.asEventTarget(),
event,
self._on_abort,
func,
.{ .context = "abort signal" },
);
}
// Static method to create an already-aborted signal
pub fn createAborted(reason_: ?js.Object, page: *Page) !*AbortSignal {
pub fn createAborted(reason_: ?js.Value.Global, page: *Page) !*AbortSignal {
const signal = try init(page);
try signal.abort(if (reason_) |r| .{ .js_obj = r } else null, page);
try signal.abort(if (reason_) |r| .{ .js_val = r } else null, page);
return signal;
}
@@ -118,7 +115,7 @@ pub fn throwIfAborted(self: *const AbortSignal, page: *Page) !ThrowIfAborted {
if (self._aborted) {
const exception = switch (self._reason) {
.string => |str| page.js.throw(str),
.js_obj => |js_obj| page.js.throw(try js_obj.toString()),
.js_val => |js_val| page.js.throw(try js_val.local().toString(.{ .allocator = page.call_arena })),
.undefined => page.js.throw("AbortError"),
};
return .{ .exception = exception };
@@ -127,7 +124,7 @@ pub fn throwIfAborted(self: *const AbortSignal, page: *Page) !ThrowIfAborted {
}
const Reason = union(enum) {
js_obj: js.Object,
js_val: js.Value.Global,
string: []const u8,
undefined: void,
};

View File

@@ -55,15 +55,6 @@ pub fn is(self: *CData, comptime T: type) ?*T {
return null;
}
pub fn className(self: *const CData) []const u8 {
return switch (self._type) {
.text => "[object Text]",
.comment => "[object Comment]",
.cdata_section => "[object CDATASection]",
.processing_instruction => "[object ProcessingInstruction]",
};
}
pub fn getData(self: *const CData) []const u8 {
return self._data;
}

View File

@@ -23,25 +23,114 @@ const Page = @import("../Page.zig");
const logger = @import("../../log.zig");
const Console = @This();
_pad: bool = false,
_timers: std.StringHashMapUnmanaged(u64) = .{},
_counts: std.StringHashMapUnmanaged(u64) = .{},
pub const init: Console = .{};
pub fn log(_: *const Console, values: []js.Object, page: *Page) void {
pub fn trace(_: *const Console, values: []js.Value, page: *Page) !void {
logger.debug(.js, "console.trace", .{
.stack = page.js.stackTrace() catch "???",
.args = ValueWriter{ .page = page, .values = values },
});
}
pub fn debug(_: *const Console, values: []js.Value, page: *Page) void {
logger.debug(.js, "console.debug", .{ValueWriter{ .page = page, .values = values }});
}
pub fn info(_: *const Console, values: []js.Value, page: *Page) void {
logger.info(.js, "console.info", .{ValueWriter{ .page = page, .values = values }});
}
pub fn log(_: *const Console, values: []js.Value, page: *Page) void {
logger.info(.js, "console.log", .{ValueWriter{ .page = page, .values = values }});
}
pub fn warn(_: *const Console, values: []js.Object, page: *Page) void {
pub fn warn(_: *const Console, values: []js.Value, page: *Page) void {
logger.warn(.js, "console.warn", .{ValueWriter{ .page = page, .values = values }});
}
pub fn @"error"(_: *const Console, values: []js.Object, page: *Page) void {
pub fn clear(_: *const Console) void {}
pub fn assert(_: *const Console, assertion: js.Value, values: []js.Value, page: *Page) void {
if (assertion.toBool()) {
return;
}
logger.warn(.js, "console.assert", .{ValueWriter{ .page = page, .values = values }});
}
pub fn @"error"(_: *const Console, values: []js.Value, page: *Page) void {
logger.warn(.js, "console.error", .{ValueWriter{ .page = page, .values = values, .include_stack = true }});
}
pub fn count(self: *Console, label_: ?[]const u8, page: *Page) !void {
const label = label_ orelse "default";
const gop = try self._counts.getOrPut(page.arena, label);
var current: u64 = 0;
if (gop.found_existing) {
current = gop.value_ptr.*;
} else {
gop.key_ptr.* = try page.dupeString(label);
}
const c = current + 1;
gop.value_ptr.* = c;
logger.info(.js, "console.count", .{ .label = label, .count = c });
}
pub fn countReset(self: *Console, label_: ?[]const u8) !void {
const label = label_ orelse "default";
const kv = self._counts.fetchRemove(label) orelse {
logger.info(.js, "console.countReset", .{ .label = label, .err = "invalid label" });
return;
};
logger.info(.js, "console.countReset", .{ .label = label, .count = kv.value });
}
pub fn time(self: *Console, label_: ?[]const u8, page: *Page) !void {
const label = label_ orelse "default";
const gop = try self._timers.getOrPut(page.arena, label);
if (gop.found_existing) {
logger.info(.js, "console.time", .{ .label = label, .err = "duplicate timer" });
return;
}
gop.key_ptr.* = try page.dupeString(label);
gop.value_ptr.* = timestamp();
}
pub fn timeLog(self: *Console, label_: ?[]const u8) void {
const elapsed = timestamp();
const label = label_ orelse "default";
const start = self._timers.get(label) orelse {
logger.info(.js, "console.timeLog", .{ .label = label, .err = "invalid timer" });
return;
};
logger.info(.js, "console.timeLog", .{ .label = label, .elapsed = elapsed - start });
}
pub fn timeEnd(self: *Console, label_: ?[]const u8) void {
const elapsed = timestamp();
const label = label_ orelse "default";
const kv = self._timers.fetchRemove(label) orelse {
logger.info(.js, "console.timeEnd", .{ .label = label, .err = "invalid timer" });
return;
};
logger.info(.js, "console.timeEnd", .{ .label = label, .elapsed = elapsed - kv.value });
}
fn timestamp() u64 {
return @import("../../datetime.zig").timestamp(.monotonic);
}
const ValueWriter = struct {
page: *Page,
values: []js.Object,
values: []js.Value,
include_stack: bool = false,
pub fn format(self: ValueWriter, writer: *std.io.Writer) !void {
@@ -57,7 +146,7 @@ const ValueWriter = struct {
var buf: [32]u8 = undefined;
for (self.values, 0..) |value, i| {
const name = try std.fmt.bufPrint(&buf, "param.{d}", .{i});
try writer.write(name, try value.toString());
try writer.write(name, try value.toString(.{}));
}
}
@@ -81,7 +170,18 @@ pub const JsApi = struct {
pub const empty_with_no_proto = true;
};
pub const trace = bridge.function(Console.trace, .{});
pub const debug = bridge.function(Console.debug, .{});
pub const info = bridge.function(Console.info, .{});
pub const log = bridge.function(Console.log, .{});
pub const warn = bridge.function(Console.warn, .{});
pub const clear = bridge.function(Console.clear, .{});
pub const assert = bridge.function(Console.assert, .{});
pub const @"error" = bridge.function(Console.@"error", .{});
pub const exception = bridge.function(Console.@"error", .{});
pub const count = bridge.function(Console.count, .{});
pub const countReset = bridge.function(Console.countReset, .{});
pub const time = bridge.function(Console.time, .{});
pub const timeLog = bridge.function(Console.timeLog, .{});
pub const timeEnd = bridge.function(Console.timeEnd, .{});
};

View File

@@ -19,8 +19,12 @@
const std = @import("std");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const SubtleCrypto = @import("SubtleCrypto.zig");
const Crypto = @This();
_pad: bool = false,
_subtle: SubtleCrypto = .{},
pub const init: Crypto = .{};
@@ -42,6 +46,10 @@ pub fn randomUUID(_: *const Crypto) ![36]u8 {
return hex;
}
pub fn getSubtle(self: *Crypto) *SubtleCrypto {
return &self._subtle;
}
const RandomValues = union(enum) {
int8: []i8,
uint8: []u8,
@@ -78,6 +86,7 @@ pub const JsApi = struct {
pub const getRandomValues = bridge.function(Crypto.getRandomValues, .{});
pub const randomUUID = bridge.function(Crypto.randomUUID, .{});
pub const subtle = bridge.accessor(Crypto.getSubtle, null, .{});
};
const testing = @import("../../testing.zig");

View File

@@ -24,7 +24,7 @@ const Element = @import("Element.zig");
const CustomElementDefinition = @This();
name: []const u8,
constructor: js.Function,
constructor: js.Function.Global,
observed_attributes: std.StringHashMapUnmanaged(void) = .{},
// For customized built-in elements, this is the element tag they extend (e.g., .button)
// For autonomous custom elements, this is null

View File

@@ -30,7 +30,7 @@ const CustomElementDefinition = @import("CustomElementDefinition.zig");
const CustomElementRegistry = @This();
_definitions: std.StringHashMapUnmanaged(*CustomElementDefinition) = .{},
_when_defined: std.StringHashMapUnmanaged(js.PersistentPromiseResolver) = .{},
_when_defined: std.StringHashMapUnmanaged(js.PromiseResolver.Global) = .{},
const DefineOptions = struct {
extends: ?[]const u8 = null,
@@ -63,7 +63,7 @@ pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Fu
const definition = try page._factory.create(CustomElementDefinition{
.name = owned_name,
.constructor = constructor,
.constructor = try constructor.persist(),
.extends = extends_tag,
});
@@ -72,8 +72,8 @@ pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Fu
if (observed_attrs.isArray()) {
var js_arr = observed_attrs.toArray();
for (0..js_arr.len()) |i| {
const attr_val = js_arr.get(i) catch continue;
const attr_name = attr_val.toString(page.arena) catch continue;
const attr_val = js_arr.get(@intCast(i)) catch continue;
const attr_name = attr_val.toString(.{ .allocator = page.arena }) catch continue;
const owned_attr = page.dupeString(attr_name) catch continue;
definition.observed_attributes.put(page.arena, owned_attr, {}) catch continue;
}
@@ -106,11 +106,11 @@ pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Fu
}
if (self._when_defined.fetchRemove(name)) |entry| {
entry.value.resolve("whenDefined", constructor);
entry.value.local().resolve("whenDefined", constructor);
}
}
pub fn get(self: *CustomElementRegistry, name: []const u8) ?js.Function {
pub fn get(self: *CustomElementRegistry, name: []const u8) ?js.Function.Global {
const definition = self._definitions.get(name) orelse return null;
return definition.constructor;
}
@@ -126,16 +126,16 @@ pub fn whenDefined(self: *CustomElementRegistry, name: []const u8, page: *Page)
const gop = try self._when_defined.getOrPut(page.arena, name);
if (gop.found_existing) {
return gop.value_ptr.promise();
return gop.value_ptr.local().promise();
}
errdefer _ = self._when_defined.remove(name);
const owned_name = try page.dupeString(name);
const resolver = try page.js.createPromiseResolver(.page);
const resolver = try page.js.createPromiseResolver().persist();
gop.key_ptr.* = owned_name;
gop.value_ptr.* = resolver;
return resolver.promise();
return resolver.local().promise();
}
fn upgradeNode(self: *CustomElementRegistry, node: *Node, page: *Page) !void {
@@ -174,9 +174,9 @@ pub fn upgradeCustomElement(custom: *Custom, definition: *CustomElementDefinitio
page._upgrading_element = node;
defer page._upgrading_element = prev_upgrading;
var result: js.Function.Result = undefined;
_ = definition.constructor.newInstance(&result) catch |err| {
log.warn(.js, "custom element upgrade", .{ .name = definition.name, .err = err });
var caught: js.TryCatch.Caught = undefined;
_ = definition.constructor.local().newInstance(&caught) catch |err| {
log.warn(.js, "custom element upgrade", .{ .name = definition.name, .err = err, .caught = caught });
return error.CustomElementUpgradeFailed;
};

View File

@@ -127,10 +127,6 @@ pub fn toString(self: *const DOMException, page: *Page) ![]const u8 {
return std.fmt.bufPrint(&page.buf, "{s}: {s}", .{ self.getName(), msg }) catch return msg;
}
pub fn className(_: *const DOMException) []const u8 {
return "[object DOMException]";
}
const Code = enum(u8) {
none = 0,
index_size_error = 1,

View File

@@ -28,19 +28,7 @@ const DOMImplementation = @This();
_pad: bool = false,
pub fn createDocumentType(_: *const DOMImplementation, qualified_name: []const u8, public_id: ?[]const u8, system_id: ?[]const u8, page: *Page) !*DocumentType {
const name = try page.dupeString(qualified_name);
// Firefox converts null to the string "null", not empty string
const pub_id = if (public_id) |p| try page.dupeString(p) else "null";
const sys_id = if (system_id) |s| try page.dupeString(s) else "null";
const doctype = try page._factory.node(DocumentType{
._proto = undefined,
._name = name,
._public_id = pub_id,
._system_id = sys_id,
});
return doctype;
return DocumentType.init(qualified_name, public_id, system_id, page);
}
pub fn createHTMLDocument(_: *const DOMImplementation, title: ?[]const u8, page: *Page) !*Document {
@@ -57,26 +45,26 @@ pub fn createHTMLDocument(_: *const DOMImplementation, title: ?[]const u8, page:
_ = try document.asNode().appendChild(doctype.asNode(), page);
}
const html_node = try page.createElement(null, "html", null);
const html_node = try page.createElementNS(.html, "html", null);
_ = try document.asNode().appendChild(html_node, page);
const head_node = try page.createElement(null, "head", null);
const head_node = try page.createElementNS(.html, "head", null);
_ = try html_node.appendChild(head_node, page);
if (title) |t| {
const title_node = try page.createElement(null, "title", null);
const title_node = try page.createElementNS(.html, "title", null);
_ = try head_node.appendChild(title_node, page);
const text_node = try page.createTextNode(t);
_ = try title_node.appendChild(text_node, page);
}
const body_node = try page.createElement(null, "body", null);
const body_node = try page.createElementNS(.html, "body", null);
_ = try html_node.appendChild(body_node, page);
return document;
}
pub fn createDocument(_: *const DOMImplementation, namespace: ?[]const u8, qualified_name: ?[]const u8, doctype: ?*DocumentType, page: *Page) !*Document {
pub fn createDocument(_: *const DOMImplementation, namespace_: ?[]const u8, qualified_name: ?[]const u8, doctype: ?*DocumentType, page: *Page) !*Document {
// Create XML Document
const document = (try page._factory.document(Node.Document.XMLDocument{ ._proto = undefined })).asDocument();
@@ -88,7 +76,8 @@ pub fn createDocument(_: *const DOMImplementation, namespace: ?[]const u8, quali
// Create and append root element if qualified_name provided
if (qualified_name) |qname| {
if (qname.len > 0) {
const root = try page.createElement(namespace, qname, null);
const namespace = if (namespace_) |ns| Node.Element.Namespace.parse(ns) else .xml;
const root = try page.createElementNS(namespace, qname, null);
_ = try document.asNode().appendChild(root, page);
}
}
@@ -102,10 +91,6 @@ pub fn hasFeature(_: *const DOMImplementation, _: ?[]const u8, _: ?[]const u8) b
return true;
}
pub fn className(_: *const DOMImplementation) []const u8 {
return "[object DOMImplementation]";
}
pub const JsApi = struct {
pub const bridge = js.Bridge(DOMImplementation);

View File

@@ -25,6 +25,7 @@ const Parser = @import("../parser/Parser.zig");
const HTMLDocument = @import("HTMLDocument.zig");
const XMLDocument = @import("XMLDocument.zig");
const Document = @import("Document.zig");
const ProcessingInstruction = @import("../webapi/cdata/ProcessingInstruction.zig");
const DOMParser = @This();
@@ -33,26 +34,21 @@ pub fn init() DOMParser {
return .{};
}
pub const HTMLDocumentOrXMLDocument = union(enum) {
html_document: *HTMLDocument,
xml_document: *XMLDocument,
};
pub fn parseFromString(
_: *const DOMParser,
html: []const u8,
mime_type: []const u8,
page: *Page,
) !HTMLDocumentOrXMLDocument {
const maybe_target_mime = std.meta.stringToEnum(enum {
) !*Document {
const target_mime = std.meta.stringToEnum(enum {
@"text/html",
@"text/xml",
@"application/xml",
@"application/xhtml+xml",
@"image/svg+xml",
}, mime_type);
}, mime_type) orelse return error.NotSupported;
if (maybe_target_mime) |target_mime| switch (target_mime) {
return switch (target_mime) {
.@"text/html" => {
// Create a new HTMLDocument
const doc = try page._factory.document(HTMLDocument{
@@ -72,7 +68,7 @@ pub fn parseFromString(
return pe.err;
}
return .{ .html_document = doc };
return doc.asDocument();
},
else => {
// Create a new XMLDocument.
@@ -100,11 +96,9 @@ pub fn parseFromString(
_ = doc_node.removeChild(first_child, page) catch unreachable;
}
return .{ .xml_document = doc };
return doc.asDocument();
},
};
return error.NotSupported;
}
pub const JsApi = struct {

View File

@@ -54,7 +54,7 @@ _active_element: ?*Element = null,
_style_sheets: ?*StyleSheetList = null,
_write_insertion_point: ?*Node = null,
_script_created_parser: ?Parser.Streaming = null,
_adopted_style_sheets: ?js.Object = null,
_adopted_style_sheets: ?js.Object.Global = null,
pub const Type = union(enum) {
generic,
@@ -124,7 +124,15 @@ const CreateElementOptions = struct {
};
pub fn createElement(self: *Document, name: []const u8, options_: ?CreateElementOptions, page: *Page) !*Element {
const node = try page.createElement(null, name, null);
try validateElementName(name);
const namespace: Element.Namespace = blk: {
if (self._type == .html) {
break :blk .html;
}
// Generic and XML documents create XML elements
break :blk .xml;
};
const node = try page.createElementNS(namespace, name, null);
const element = node.as(Element);
// Track owner document if it's not the main document
@@ -142,7 +150,8 @@ pub fn createElement(self: *Document, name: []const u8, options_: ?CreateElement
}
pub fn createElementNS(self: *Document, namespace: ?[]const u8, name: []const u8, page: *Page) !*Element {
const node = try page.createElement(namespace, name, null);
try validateElementName(name);
const node = try page.createElementNS(Element.Namespace.parse(namespace), name, null);
// Track owner document if it's not the main document
if (self != page.document) {
@@ -275,14 +284,6 @@ pub fn querySelectorAll(self: *Document, input: []const u8, page: *Page) !*Selec
return Selector.querySelectorAll(self.asNode(), input, page);
}
pub fn className(self: *const Document) []const u8 {
return switch (self._type) {
.generic => "[object Document]",
.html => "[object HTMLDocument]",
.xml => "[object XMLDocument]",
};
}
pub fn getImplementation(_: *const Document) DOMImplementation {
return .{};
}
@@ -433,20 +434,103 @@ pub fn importNode(_: *const Document, node: *Node, deep_: ?bool, page: *Page) !*
}
pub fn append(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !void {
try validateDocumentNodes(self, nodes, false);
page.domChanged();
const parent = self.asNode();
const parent_is_connected = parent.isConnected();
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(page);
_ = try parent.appendChild(child, page);
// DocumentFragments are special - append all their children
if (child.is(Node.DocumentFragment)) |_| {
try page.appendAllChildren(child, parent);
continue;
}
var child_connected = false;
if (child._parent) |previous_parent| {
child_connected = child.isConnected();
page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent_is_connected });
}
try page.appendNode(parent, child, .{ .child_already_connected = child_connected });
}
}
pub fn prepend(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !void {
try validateDocumentNodes(self, nodes, false);
page.domChanged();
const parent = self.asNode();
const parent_is_connected = parent.isConnected();
var i = nodes.len;
while (i > 0) {
i -= 1;
const child = try nodes[i].toNode(page);
_ = try parent.insertBefore(child, parent.firstChild(), page);
// DocumentFragments are special - need to insert all their children
if (child.is(Node.DocumentFragment)) |frag| {
const first_child = parent.firstChild();
var frag_child = frag.asNode().lastChild();
while (frag_child) |fc| {
const prev = fc.previousSibling();
page.removeNode(frag.asNode(), fc, .{ .will_be_reconnected = parent_is_connected });
if (first_child) |before| {
try page.insertNodeRelative(parent, fc, .{ .before = before }, .{});
} else {
try page.appendNode(parent, fc, .{});
}
frag_child = prev;
}
continue;
}
var child_connected = false;
if (child._parent) |previous_parent| {
child_connected = child.isConnected();
page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent_is_connected });
}
const first_child = parent.firstChild();
if (first_child) |before| {
try page.insertNodeRelative(parent, child, .{ .before = before }, .{ .child_already_connected = child_connected });
} else {
try page.appendNode(parent, child, .{ .child_already_connected = child_connected });
}
}
}
pub fn replaceChildren(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !void {
try validateDocumentNodes(self, nodes, true);
page.domChanged();
const parent = self.asNode();
// Remove all existing children
var it = parent.childrenIterator();
while (it.next()) |child| {
page.removeNode(parent, child, .{ .will_be_reconnected = false });
}
// Append new children
const parent_is_connected = parent.isConnected();
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(page);
// DocumentFragments are special - append all their children
if (child.is(Node.DocumentFragment)) |_| {
try page.appendAllChildren(child, parent);
continue;
}
var child_connected = false;
if (child._parent) |previous_parent| {
child_connected = child.isConnected();
page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent_is_connected });
}
try page.appendNode(parent, child, .{ .child_already_connected = child_connected });
}
}
@@ -492,7 +576,13 @@ pub fn elementsFromPoint(self: *Document, x: f64, y: f64, page: *Page) ![]const
return result.items;
}
pub fn getDocType(_: *const Document) ?*DocumentType {
pub fn getDocType(self: *Document) ?*Node {
var tw = @import("TreeWalker.zig").Full.init(self.asNode(), .{});
while (tw.next()) |node| {
if (node._type == .document_type) {
return node;
}
}
return null;
}
@@ -676,13 +766,14 @@ pub fn getChildElementCount(self: *Document) u32 {
return i;
}
pub fn getAdoptedStyleSheets(self: *Document, page: *Page) !js.Object {
pub fn getAdoptedStyleSheets(self: *Document, page: *Page) !js.Object.Global {
if (self._adopted_style_sheets) |ass| {
return ass;
}
const obj = try page.js.createArray(0).persist();
self._adopted_style_sheets = obj;
return obj;
const js_arr = page.js.newArray(0);
const js_obj = js_arr.toObject();
self._adopted_style_sheets = try js_obj.persist();
return self._adopted_style_sheets.?;
}
pub fn hasFocus(_: *Document) bool {
@@ -694,6 +785,118 @@ pub fn setAdoptedStyleSheets(self: *Document, sheets: js.Object) !void {
self._adopted_style_sheets = try sheets.persist();
}
// Validates that nodes can be inserted into a Document, respecting Document constraints:
// - At most one Element child
// - At most one DocumentType child
// - No Document, Attribute, or Text nodes
// - Only Element, DocumentType, Comment, and ProcessingInstruction are allowed
// When replacing=true, existing children are not counted (for replaceChildren)
fn validateDocumentNodes(self: *Document, nodes: []const Node.NodeOrText, comptime replacing: bool) !void {
const parent = self.asNode();
// Check existing elements and doctypes (unless we're replacing all children)
var has_element = false;
var has_doctype = false;
if (!replacing) {
var it = parent.childrenIterator();
while (it.next()) |child| {
if (child._type == .element) {
has_element = true;
} else if (child._type == .document_type) {
has_doctype = true;
}
}
}
// Validate new nodes
for (nodes) |node_or_text| {
switch (node_or_text) {
.text => {
// Text nodes are not allowed as direct children of Document
return error.HierarchyError;
},
.node => |child| {
// Check if it's a DocumentFragment - need to validate its children
if (child.is(Node.DocumentFragment)) |frag| {
var frag_it = frag.asNode().childrenIterator();
while (frag_it.next()) |frag_child| {
// Document can only contain: Element, DocumentType, Comment, ProcessingInstruction
switch (frag_child._type) {
.element => {
if (has_element) {
return error.HierarchyError;
}
has_element = true;
},
.document_type => {
if (has_doctype) {
return error.HierarchyError;
}
has_doctype = true;
},
.cdata => |cd| switch (cd._type) {
.comment, .processing_instruction => {}, // Allowed
.text, .cdata_section => return error.HierarchyError, // Not allowed in Document
},
.document, .attribute, .document_fragment => return error.HierarchyError,
}
}
} else {
// Validate node type for direct insertion
switch (child._type) {
.element => {
if (has_element) {
return error.HierarchyError;
}
has_element = true;
},
.document_type => {
if (has_doctype) {
return error.HierarchyError;
}
has_doctype = true;
},
.cdata => |cd| switch (cd._type) {
.comment, .processing_instruction => {}, // Allowed
.text, .cdata_section => return error.HierarchyError, // Not allowed in Document
},
.document, .attribute, .document_fragment => return error.HierarchyError,
}
}
// Check for cycles
if (child.contains(parent)) {
return error.HierarchyError;
}
},
}
}
}
fn validateElementName(name: []const u8) !void {
if (name.len == 0) {
return error.InvalidCharacterError;
}
const first = name[0];
// Element names cannot start with: digits, period, hyphen
if ((first >= '0' and first <= '9') or first == '.' or first == '-') {
return error.InvalidCharacterError;
}
for (name[1..]) |c| {
const is_valid = (c >= 'a' and c <= 'z') or
(c >= 'A' and c <= 'Z') or
(c >= '0' and c <= '9') or
c == '_' or c == '-' or c == '.' or c == ':';
if (!is_valid) {
return error.InvalidCharacterError;
}
}
}
const ReadyState = enum {
loading,
interactive,
@@ -732,8 +935,8 @@ pub const JsApi = struct {
pub const compatMode = bridge.accessor(Document.getCompatMode, null, .{});
pub const referrer = bridge.accessor(Document.getReferrer, null, .{});
pub const domain = bridge.accessor(Document.getDomain, null, .{});
pub const createElement = bridge.function(Document.createElement, .{});
pub const createElementNS = bridge.function(Document.createElementNS, .{});
pub const createElement = bridge.function(Document.createElement, .{ .dom_exception = true });
pub const createElementNS = bridge.function(Document.createElementNS, .{ .dom_exception = true });
pub const createDocumentFragment = bridge.function(Document.createDocumentFragment, .{});
pub const createComment = bridge.function(Document.createComment, .{});
pub const createTextNode = bridge.function(Document.createTextNode, .{});
@@ -763,8 +966,9 @@ pub const JsApi = struct {
pub const getElementsByName = bridge.function(Document.getElementsByName, .{});
pub const adoptNode = bridge.function(Document.adoptNode, .{ .dom_exception = true });
pub const importNode = bridge.function(Document.importNode, .{ .dom_exception = true });
pub const append = bridge.function(Document.append, .{});
pub const prepend = bridge.function(Document.prepend, .{});
pub const append = bridge.function(Document.append, .{ .dom_exception = true });
pub const prepend = bridge.function(Document.prepend, .{ .dom_exception = true });
pub const replaceChildren = bridge.function(Document.replaceChildren, .{ .dom_exception = true });
pub const elementFromPoint = bridge.function(Document.elementFromPoint, .{});
pub const elementsFromPoint = bridge.function(Document.elementsFromPoint, .{});
pub const write = bridge.function(Document.write, .{ .dom_exception = true });

View File

@@ -67,10 +67,6 @@ pub fn asEventTarget(self: *DocumentFragment) *@import("EventTarget.zig") {
return self._proto.asEventTarget();
}
pub fn className(_: *const DocumentFragment) []const u8 {
return "[object DocumentFragment]";
}
pub fn getElementById(self: *DocumentFragment, id: []const u8) ?*Element {
if (id.len == 0) {
return null;

View File

@@ -19,6 +19,8 @@
const std = @import("std");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Node = @import("Node.zig");
const DocumentType = @This();
@@ -28,6 +30,20 @@ _name: []const u8,
_public_id: []const u8,
_system_id: []const u8,
pub fn init(qualified_name: []const u8, public_id: ?[]const u8, system_id: ?[]const u8, page: *Page) !*DocumentType {
const name = try page.dupeString(qualified_name);
// Firefox converts null to the string "null", not empty string
const pub_id = if (public_id) |p| try page.dupeString(p) else "null";
const sys_id = if (system_id) |s| try page.dupeString(s) else "null";
return page._factory.node(DocumentType{
._proto = undefined,
._name = name,
._public_id = pub_id,
._system_id = sys_id,
});
}
pub fn asNode(self: *DocumentType) *Node {
return self._proto;
}
@@ -48,16 +64,16 @@ pub fn getSystemId(self: *const DocumentType) []const u8 {
return self._system_id;
}
pub fn className(_: *const DocumentType) []const u8 {
return "[object DocumentType]";
}
pub fn isEqualNode(self: *const DocumentType, other: *const DocumentType) bool {
return std.mem.eql(u8, self._name, other._name) and
std.mem.eql(u8, self._public_id, other._public_id) and
std.mem.eql(u8, self._system_id, other._system_id);
}
pub fn clone(self: *const DocumentType, page: *Page) !*DocumentType {
return .init(self._name, self._public_id, self._system_id, page);
}
pub const JsApi = struct {
pub const bridge = js.Bridge(DocumentType);

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const log = @import("../../log.zig");
const String = @import("../../string.zig").String;
@@ -53,15 +54,41 @@ pub const Namespace = enum(u8) {
svg,
mathml,
xml,
// We should keep the original value, but don't. If this becomes important
// consider storing it in a page lookup, like `_element_class_lists`, rather
// that adding a slice directly here (directly in every element).
unknown,
null,
pub fn toUri(self: Namespace) []const u8 {
pub fn toUri(self: Namespace) ?[]const u8 {
return switch (self) {
.html => "http://www.w3.org/1999/xhtml",
.svg => "http://www.w3.org/2000/svg",
.mathml => "http://www.w3.org/1998/Math/MathML",
.xml => "http://www.w3.org/XML/1998/namespace",
.unknown => "http://lightpanda.io/unsupported/namespace",
.null => null,
};
}
pub fn parse(namespace_: ?[]const u8) Namespace {
const namespace = namespace_ orelse return .null;
if (namespace.len == "http://www.w3.org/1999/xhtml".len) {
// Common case, avoid the string comparion. Recklessly
@branchHint(.likely);
return .html;
}
if (std.mem.eql(u8, namespace, "http://www.w3.org/XML/1998/namespace")) {
return .xml;
}
if (std.mem.eql(u8, namespace, "http://www.w3.org/2000/svg")) {
return .svg;
}
if (std.mem.eql(u8, namespace, "http://www.w3.org/1998/Math/MathML")) {
return .mathml;
}
return .unknown;
}
};
_type: Type,
@@ -113,12 +140,6 @@ pub fn asConstNode(self: *const Element) *const Node {
return self._proto;
}
pub fn className(self: *const Element) []const u8 {
return switch (self._type) {
inline else => |c| return c.className(),
};
}
pub fn attributesEql(self: *const Element, other: *Element) bool {
if (self._attributes) |attr_list| {
const other_list = other._attributes orelse return false;
@@ -171,15 +192,21 @@ pub fn getTagNameLower(self: *const Element) []const u8 {
},
else => return switch (he._type) {
.anchor => "a",
.area => "area",
.base => "base",
.body => "body",
.br => "br",
.button => "button",
.canvas => "canvas",
.custom => |e| e._tag_name.str(),
.data => "data",
.datalist => "datalist",
.dialog => "dialog",
.directory => "dir",
.div => "div",
.embed => "embed",
.fieldset => "fieldset",
.font => "font",
.form => "form",
.generic => |e| e._tag_name.str(),
.heading => |e| e._tag_name.str(),
@@ -189,24 +216,46 @@ pub fn getTagNameLower(self: *const Element) []const u8 {
.iframe => "iframe",
.img => "img",
.input => "input",
.label => "label",
.legend => "legend",
.li => "li",
.link => "link",
.map => "map",
.media => |m| switch (m._type) {
.audio => "audio",
.video => "video",
.generic => "media",
},
.meta => "meta",
.meter => "meter",
.mod => |e| e._tag_name.str(),
.object => "object",
.ol => "ol",
.optgroup => "optgroup",
.option => "option",
.output => "output",
.p => "p",
.param => "param",
.pre => "pre",
.progress => "progress",
.quote => |e| e._tag_name.str(),
.script => "script",
.select => "select",
.slot => "slot",
.source => "source",
.span => "span",
.style => "style",
.table => "table",
.table_caption => "caption",
.table_cell => |e| e._tag_name.str(),
.table_col => |e| e._tag_name.str(),
.table_row => "tr",
.table_section => |e| e._tag_name.str(),
.template => "template",
.textarea => "textarea",
.time => "time",
.title => "title",
.track => "track",
.ul => "ul",
.unknown => |e| e._tag_name.str(),
},
@@ -216,59 +265,81 @@ pub fn getTagNameLower(self: *const Element) []const u8 {
}
pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 {
switch (self._type) {
return switch (self._type) {
.html => |he| switch (he._type) {
.custom => |e| {
@branchHint(.unlikely);
return upperTagName(&e._tag_name, buf);
.anchor => "A",
.area => "AREA",
.base => "BASE",
.body => "BODY",
.br => "BR",
.button => "BUTTON",
.canvas => "CANVAS",
.custom => |e| upperTagName(&e._tag_name, buf),
.data => "DATA",
.datalist => "DATALIST",
.dialog => "DIALOG",
.directory => "DIR",
.div => "DIV",
.embed => "EMBED",
.fieldset => "FIELDSET",
.font => "FONT",
.form => "FORM",
.generic => |e| upperTagName(&e._tag_name, buf),
.heading => |e| upperTagName(&e._tag_name, buf),
.head => "HEAD",
.html => "HTML",
.hr => "HR",
.iframe => "IFRAME",
.img => "IMG",
.input => "INPUT",
.label => "LABEL",
.legend => "LEGEND",
.li => "LI",
.link => "LINK",
.map => "MAP",
.meta => "META",
.media => |m| switch (m._type) {
.audio => "AUDIO",
.video => "VIDEO",
.generic => "MEDIA",
},
else => return switch (he._type) {
.anchor => "A",
.body => "BODY",
.br => "BR",
.button => "BUTTON",
.canvas => "CANVAS",
.custom => |e| upperTagName(&e._tag_name, buf),
.data => "DATA",
.dialog => "DIALOG",
.div => "DIV",
.embed => "EMBED",
.form => "FORM",
.generic => |e| upperTagName(&e._tag_name, buf),
.heading => |e| upperTagName(&e._tag_name, buf),
.head => "HEAD",
.html => "HTML",
.hr => "HR",
.iframe => "IFRAME",
.img => "IMG",
.input => "INPUT",
.li => "LI",
.link => "LINK",
.meta => "META",
.media => |m| switch (m._type) {
.audio => "AUDIO",
.video => "VIDEO",
.generic => "MEDIA",
},
.ol => "OL",
.option => "OPTION",
.p => "P",
.script => "SCRIPT",
.select => "SELECT",
.slot => "SLOT",
.style => "STYLE",
.template => "TEMPLATE",
.textarea => "TEXTAREA",
.title => "TITLE",
.ul => "UL",
.unknown => |e| switch (self._namespace) {
.html => upperTagName(&e._tag_name, buf),
.svg, .xml, .mathml => return e._tag_name.str(),
},
.meter => "METER",
.mod => |e| upperTagName(&e._tag_name, buf),
.object => "OBJECT",
.ol => "OL",
.optgroup => "OPTGROUP",
.option => "OPTION",
.output => "OUTPUT",
.p => "P",
.param => "PARAM",
.pre => "PRE",
.progress => "PROGRESS",
.quote => |e| upperTagName(&e._tag_name, buf),
.script => "SCRIPT",
.select => "SELECT",
.slot => "SLOT",
.source => "SOURCE",
.span => "SPAN",
.style => "STYLE",
.table => "TABLE",
.table_caption => "CAPTION",
.table_cell => |e| upperTagName(&e._tag_name, buf),
.table_col => |e| upperTagName(&e._tag_name, buf),
.table_row => "TR",
.table_section => |e| upperTagName(&e._tag_name, buf),
.template => "TEMPLATE",
.textarea => "TEXTAREA",
.time => "TIME",
.title => "TITLE",
.track => "TRACK",
.ul => "UL",
.unknown => |e| switch (self._namespace) {
.html => upperTagName(&e._tag_name, buf),
.svg, .xml, .mathml, .unknown, .null => e._tag_name.str(),
},
},
.svg => |svg| return svg._tag_name.str(),
}
.svg => |svg| svg._tag_name.str(),
};
}
pub fn getTagNameDump(self: *const Element) []const u8 {
@@ -278,7 +349,7 @@ pub fn getTagNameDump(self: *const Element) []const u8 {
}
}
pub fn getNamespaceURI(self: *const Element) []const u8 {
pub fn getNamespaceURI(self: *const Element) ?[]const u8 {
return self._namespace.toUri();
}
@@ -397,6 +468,22 @@ pub fn getAttribute(self: *const Element, name: []const u8, page: *Page) !?[]con
return attributes.get(name, page);
}
/// For simplicity, the namespace is currently ignored and only the local name is used.
pub fn getAttributeNS(
self: *const Element,
maybe_namespace: ?[]const u8,
local_name: []const u8,
page: *Page,
) !?[]const u8 {
if (maybe_namespace) |namespace| {
if (!std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml")) {
log.warn(.not_implemented, "Element.getAttributeNS", .{ .namespace = namespace });
}
}
return self.getAttribute(local_name, page);
}
pub fn getAttributeSafe(self: *const Element, name: []const u8) ?[]const u8 {
const attributes = self._attributes orelse return null;
return attributes.getSafe(name);
@@ -429,6 +516,26 @@ pub fn setAttribute(self: *Element, name: []const u8, value: []const u8, page: *
_ = try attributes.put(name, value, self, page);
}
pub fn setAttributeNS(
self: *Element,
maybe_namespace: ?[]const u8,
qualified_name: []const u8,
value: []const u8,
page: *Page,
) !void {
if (maybe_namespace) |namespace| {
if (!std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml")) {
log.warn(.not_implemented, "Element.setAttributeNS", .{ .namespace = namespace });
}
}
const local_name = if (std.mem.indexOfScalarPos(u8, qualified_name, 0, ':')) |idx|
qualified_name[idx + 1 ..]
else
qualified_name;
return self.setAttribute(local_name, value, page);
}
pub fn setAttributeSafe(self: *Element, name: []const u8, value: []const u8, page: *Page) !void {
const attributes = try self.getOrCreateAttributeList(page);
_ = try attributes.putSafe(name, value, self, page);
@@ -439,7 +546,7 @@ pub fn getOrCreateAttributeList(self: *Element, page: *Page) !*Attribute.List {
}
pub fn createAttributeList(self: *Element, page: *Page) !*Attribute.List {
std.debug.assert(self._attributes == null);
lp.assert(self._attributes == null, "Element.createAttributeList non-null _attributes", .{});
const a = try page.arena.create(Attribute.List);
a.* = .{ .normalize = self._namespace == .html };
self._attributes = a;
@@ -750,7 +857,7 @@ pub fn getAnimations(_: *const Element) []*Animation {
return &.{};
}
pub fn animate(_: *Element, _: js.Object, _: js.Object, page: *Page) !*Animation {
pub fn animate(_: *Element, _: ?js.Object, _: ?js.Object, page: *Page) !*Animation {
return Animation.init(page);
}
@@ -998,11 +1105,9 @@ pub fn getElementsByClassName(self: *Element, class_name: []const u8, page: *Pag
return collections.NodeLive(.class_name).init(self.asNode(), class_names.items, page);
}
pub fn cloneElement(self: *Element, deep: bool, page: *Page) !*Node {
pub fn clone(self: *Element, deep: bool, page: *Page) !*Node {
const tag_name = self.getTagNameDump();
const namespace_uri = self.getNamespaceURI();
const node = try page.createElement(namespace_uri, tag_name, self._attributes);
const node = try page.createElementNS(self._namespace, tag_name, self._attributes);
// Allow element-specific types to copy their runtime state
_ = Element.Build.call(node.as(Element), "cloned", .{ self, node.as(Element), page }) catch |err| {
@@ -1060,34 +1165,62 @@ pub fn getTag(self: *const Element) Tag {
return switch (self._type) {
.html => |he| switch (he._type) {
.anchor => .anchor,
.area => .area,
.base => .base,
.div => .div,
.embed => .embed,
.form => .form,
.p => .p,
.custom => .custom,
.data => .data,
.datalist => .datalist,
.dialog => .dialog,
.directory => .unknown,
.iframe => .iframe,
.img => .img,
.br => .br,
.button => .button,
.canvas => .canvas,
.fieldset => .fieldset,
.font => .unknown,
.heading => |h| h._tag,
.label => .unknown,
.legend => .unknown,
.li => .li,
.map => .unknown,
.ul => .ul,
.ol => .ol,
.object => .unknown,
.optgroup => .optgroup,
.output => .unknown,
.param => .unknown,
.pre => .unknown,
.generic => |g| g._tag,
.media => |m| switch (m._type) {
.audio => .audio,
.video => .video,
.generic => .media,
},
.meter => .meter,
.mod => |m| m._tag,
.progress => .progress,
.quote => |q| q._tag,
.script => .script,
.select => .select,
.slot => .slot,
.source => .unknown,
.span => .span,
.option => .option,
.table => .table,
.table_caption => .caption,
.table_cell => |tc| tc._tag,
.table_col => |tc| tc._tag,
.table_row => .tr,
.table_section => |ts| ts._tag,
.template => .template,
.textarea => .textarea,
.time => .time,
.track => .unknown,
.input => .input,
.link => .link,
.meta => .meta,
@@ -1123,6 +1256,8 @@ pub const Tag = enum {
caption,
circle,
code,
col,
colgroup,
custom,
data,
datalist,
@@ -1169,20 +1304,26 @@ pub const Tag = enum {
meta,
meter,
nav,
noscript,
object,
ol,
optgroup,
option,
output,
p,
path,
param,
polygon,
polyline,
progress,
quote,
rect,
s,
script,
section,
select,
slot,
source,
span,
strong,
style,
@@ -1202,6 +1343,7 @@ pub const Tag = enum {
thead,
title,
tr,
track,
ul,
video,
unknown,
@@ -1275,8 +1417,10 @@ pub const JsApi = struct {
pub const hasAttribute = bridge.function(Element.hasAttribute, .{});
pub const hasAttributes = bridge.function(Element.hasAttributes, .{});
pub const getAttribute = bridge.function(Element.getAttribute, .{});
pub const getAttributeNS = bridge.function(Element.getAttributeNS, .{});
pub const getAttributeNode = bridge.function(Element.getAttributeNode, .{});
pub const setAttribute = bridge.function(Element.setAttribute, .{ .dom_exception = true });
pub const setAttributeNS = bridge.function(Element.setAttributeNS, .{ .dom_exception = true });
pub const setAttributeNode = bridge.function(Element.setAttributeNode, .{});
pub const removeAttribute = bridge.function(Element.removeAttribute, .{});
pub const toggleAttribute = bridge.function(Element.toggleAttribute, .{ .dom_exception = true });

View File

@@ -51,6 +51,10 @@ pub fn init(page: *Page) !*EventTarget {
}
pub fn dispatchEvent(self: *EventTarget, event: *Event, page: *Page) !bool {
if (event._event_phase != .none) {
return error.InvalidStateError;
}
event._isTrusted = false;
try page._event_manager.dispatch(self, event);
return !event._cancelable or !event._prevent_default;
}
@@ -67,15 +71,9 @@ pub const EventListenerCallback = union(enum) {
pub fn addEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventListenerCallback, opts_: ?AddEventListenerOptions, page: *Page) !void {
const callback = callback_ orelse return;
if (callback == .object) {
if (try callback.object.getFunction("handleEvent") == null) {
return;
}
}
const em_callback = switch (callback) {
.object => |obj| EventManager.Callback{ .object = obj },
.function => |func| EventManager.Callback{ .function = func },
.object => |obj| EventManager.Callback{ .object = try obj.persist() },
};
const options = blk: {
@@ -108,7 +106,7 @@ pub fn removeEventListener(self: *EventTarget, typ: []const u8, callback_: ?Even
const em_callback = switch (callback) {
.function => |func| EventManager.Callback{ .function = func },
.object => |obj| EventManager.Callback{ .object = try obj.persist() },
.object => |obj| EventManager.Callback{ .object = obj },
};
const use_capture = blk: {
@@ -164,7 +162,7 @@ pub const JsApi = struct {
};
pub const constructor = bridge.constructor(EventTarget.init, .{});
pub const dispatchEvent = bridge.function(EventTarget.dispatchEvent, .{});
pub const dispatchEvent = bridge.function(EventTarget.dispatchEvent, .{ .dom_exception = true });
pub const addEventListener = bridge.function(EventTarget.addEventListener, .{});
pub const removeEventListener = bridge.function(EventTarget.removeEventListener, .{});
};

View File

@@ -44,10 +44,6 @@ pub fn asEventTarget(self: *HTMLDocument) *@import("EventTarget.zig") {
return self._proto.asEventTarget();
}
pub fn className(_: *const HTMLDocument) []const u8 {
return "[object HTMLDocument]";
}
// HTML-specific accessors
pub fn getHead(self: *HTMLDocument) ?*Element.Html.Head {
const doc_el = self._proto.getDocumentElement() orelse return null;
@@ -136,7 +132,7 @@ pub fn setTitle(self: *HTMLDocument, title: []const u8, page: *Page) !void {
}
// No title element found, create one
const title_node = try page.createElement(null, "title", null);
const title_node = try page.createElementNS(.html, "title", null);
const title_element = title_node.as(Element);
// Only add text if non-empty
@@ -219,6 +215,15 @@ pub fn getDocType(self: *HTMLDocument, page: *Page) !*DocumentType {
if (self._document_type) |dt| {
return dt;
}
var tw = @import("TreeWalker.zig").Full.init(self.asNode(), .{});
while (tw.next()) |node| {
if (node._type == .document_type) {
self._document_type = node.as(DocumentType);
return self._document_type.?;
}
}
self._document_type = try page._factory.node(DocumentType{
._proto = undefined,
._name = "html",

View File

@@ -34,7 +34,7 @@ pub fn getLength(_: *const History, page: *Page) u32 {
pub fn getState(_: *const History, page: *Page) !?js.Value {
if (page._session.navigation.getCurrentEntry()._state.value) |state| {
const value = try js.Value.fromJson(page.js, state);
const value = try page.js.parseJSON(state);
return value;
} else return null;
}
@@ -49,7 +49,7 @@ pub fn setScrollRestoration(self: *History, str: []const u8) void {
}
}
pub fn pushState(_: *History, state: js.Object, _: []const u8, _url: ?[]const u8, page: *Page) !void {
pub fn pushState(_: *History, state: js.Value, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
const arena = page._session.arena;
const url = if (_url) |u| try arena.dupeZ(u8, u) else try arena.dupeZ(u8, page.url);
@@ -57,7 +57,7 @@ pub fn pushState(_: *History, state: js.Object, _: []const u8, _url: ?[]const u8
_ = try page._session.navigation.pushEntry(url, .{ .source = .history, .value = json }, page, true);
}
pub fn replaceState(_: *History, state: js.Object, _: []const u8, _url: ?[]const u8, page: *Page) !void {
pub fn replaceState(_: *History, state: js.Value, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
const arena = page._session.arena;
const url = if (_url) |u| try arena.dupeZ(u8, u) else try arena.dupeZ(u8, page.url);
@@ -81,10 +81,11 @@ fn goInner(delta: i32, page: *Page) !void {
if (try page.isSameOrigin(url)) {
const event = try PopStateEvent.initTrusted("popstate", .{ .state = entry._state.value }, page);
const func = if (page.window._on_popstate) |*g| g.local() else null;
try page._event_manager.dispatchWithFunction(
page.window.asEventTarget(),
event.asEvent(),
page.window._on_popstate,
func,
.{ .context = "Pop State" },
);
}

View File

@@ -17,6 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig");
const Page = @import("../Page.zig");
const Element = @import("Element.zig");
const DOMRect = @import("DOMRect.zig");
@@ -30,7 +32,7 @@ pub fn registerTypes() []const type {
const IntersectionObserver = @This();
_callback: js.Function,
_callback: js.Function.Global,
_observing: std.ArrayList(*Element) = .{},
_root: ?*Element = null,
_root_margin: []const u8 = "0px",
@@ -57,7 +59,7 @@ pub const ObserverInit = struct {
};
};
pub fn init(callback: js.Function, options: ?ObserverInit, page: *Page) !*IntersectionObserver {
pub fn init(callback: js.Function.Global, options: ?ObserverInit, page: *Page) !*IntersectionObserver {
const opts = options orelse ObserverInit{};
const root_margin = if (opts.rootMargin) |rm| try page.arena.dupe(u8, rm) else "0px";
@@ -70,7 +72,12 @@ pub fn init(callback: js.Function, options: ?ObserverInit, page: *Page) !*Inters
.array => |arr| try page.arena.dupe(f64, arr),
};
return page._factory.create(IntersectionObserver{ ._callback = callback, ._root = opts.root, ._root_margin = root_margin, ._threshold = threshold });
return page._factory.create(IntersectionObserver{
._callback = callback,
._root = opts.root,
._root_margin = root_margin,
._threshold = threshold,
});
}
pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void {
@@ -238,7 +245,11 @@ pub fn deliverEntries(self: *IntersectionObserver, page: *Page) !void {
}
const entries = try self.takeRecords(page);
try self._callback.call(void, .{ entries, self });
var caught: js.TryCatch.Caught = undefined;
self._callback.local().tryCall(void, .{ entries, self }, &caught) catch |err| {
log.err(.page, "IntsctObserver.deliverEntries", .{ .err = err, .caught = caught });
return err;
};
}
pub const IntersectionObserverEntry = struct {

View File

@@ -68,7 +68,7 @@ pub fn fromJsObject(arena: Allocator, js_obj: js.Object, comptime normalizer: ?N
while (try it.next()) |name| {
const js_value = try js_obj.get(name);
const value = try js_value.toString(arena);
const value = try js_value.toString(.{});
const normalized = if (comptime normalizer) |n| n(name, page) else name;
list._entries.appendAssumeCapacity(.{

View File

@@ -29,8 +29,8 @@ const MessagePort = @This();
_proto: *EventTarget,
_enabled: bool = false,
_closed: bool = false,
_on_message: ?js.Function = null,
_on_message_error: ?js.Function = null,
_on_message: ?js.Function.Global = null,
_on_message_error: ?js.Function.Global = null,
_entangled_port: ?*MessagePort = null,
pub fn init(page: *Page) !*MessagePort {
@@ -48,7 +48,7 @@ pub fn entangle(port1: *MessagePort, port2: *MessagePort) void {
port2._entangled_port = port1;
}
pub fn postMessage(self: *MessagePort, message: js.Object, page: *Page) !void {
pub fn postMessage(self: *MessagePort, message: js.Value.Global, page: *Page) !void {
if (self._closed) {
return;
}
@@ -62,7 +62,7 @@ pub fn postMessage(self: *MessagePort, message: js.Object, page: *Page) !void {
const callback = try page._factory.create(PostMessageCallback{
.page = page,
.port = other,
.message = try message.persist(),
.message = message,
});
try page.scheduler.add(callback, PostMessageCallback.run, 0, .{
@@ -88,33 +88,25 @@ pub fn close(self: *MessagePort) void {
self._entangled_port = null;
}
pub fn getOnMessage(self: *const MessagePort) ?js.Function {
pub fn getOnMessage(self: *const MessagePort) ?js.Function.Global {
return self._on_message;
}
pub fn setOnMessage(self: *MessagePort, cb_: ?js.Function) !void {
if (cb_) |cb| {
self._on_message = cb;
} else {
self._on_message = null;
}
pub fn setOnMessage(self: *MessagePort, cb: ?js.Function.Global) !void {
self._on_message = cb;
}
pub fn getOnMessageError(self: *const MessagePort) ?js.Function {
pub fn getOnMessageError(self: *const MessagePort) ?js.Function.Global {
return self._on_message_error;
}
pub fn setOnMessageError(self: *MessagePort, cb_: ?js.Function) !void {
if (cb_) |cb| {
self._on_message_error = cb;
} else {
self._on_message_error = null;
}
pub fn setOnMessageError(self: *MessagePort, cb: ?js.Function.Global) !void {
self._on_message_error = cb;
}
const PostMessageCallback = struct {
port: *MessagePort,
message: js.Object,
message: js.Value.Global,
page: *Page,
fn deinit(self: *PostMessageCallback) void {
@@ -138,10 +130,11 @@ const PostMessageCallback = struct {
return null;
};
const func = if (self.port._on_message) |*g| g.local() else null;
self.page._event_manager.dispatchWithFunction(
self.port.asEventTarget(),
event.asEvent(),
self.port._on_message,
func,
.{ .context = "MessagePort message" },
) catch |err| {
log.err(.dom, "MessagePort.postMessage", .{ .err = err });

View File

@@ -21,6 +21,7 @@ const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Node = @import("Node.zig");
const Element = @import("Element.zig");
const log = @import("../../log.zig");
pub fn registerTypes() []const type {
return &.{
@@ -31,7 +32,7 @@ pub fn registerTypes() []const type {
const MutationObserver = @This();
_callback: js.Function,
_callback: js.Function.Global,
_observing: std.ArrayList(Observing) = .{},
_pending_records: std.ArrayList(*MutationRecord) = .{},
/// Intrusively linked to next element (see Page.zig).
@@ -52,7 +53,7 @@ pub const ObserveOptions = struct {
attributeFilter: ?[]const []const u8 = null,
};
pub fn init(callback: js.Function, page: *Page) !*MutationObserver {
pub fn init(callback: js.Function.Global, page: *Page) !*MutationObserver {
return page._factory.create(MutationObserver{
._callback = callback,
});
@@ -69,6 +70,10 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
copied_options.attributeFilter = filter_copy;
}
if (options.characterDataOldValue) {
copied_options.characterData = true;
}
// Check if already observing this target
for (self._observing.items) |*obs| {
if (obs.target == target) {
@@ -243,7 +248,11 @@ pub fn deliverRecords(self: *MutationObserver, page: *Page) !void {
// Take a copy of the records and clear the list before calling callback
// This ensures mutations triggered during the callback go into a fresh list
const records = try self.takeRecords(page);
try self._callback.call(void, .{ records, self });
var caught: js.TryCatch.Caught = undefined;
self._callback.local().tryCall(void, .{ records, self }, &caught) catch |err| {
log.err(.page, "MutObserver.deliverRecords", .{ .err = err, .caught = caught });
return err;
};
}
pub const MutationRecord = struct {
@@ -274,6 +283,13 @@ pub const MutationRecord = struct {
return self._target;
}
pub fn getAttributeNamespace(self: *const MutationRecord) ?[]const u8 {
if (self._attribute_name != null) {
return "http://www.w3.org/1999/xhtml";
}
return null;
}
pub fn getAttributeName(self: *const MutationRecord) ?[]const u8 {
return self._attribute_name;
}
@@ -310,6 +326,7 @@ pub const MutationRecord = struct {
pub const @"type" = bridge.accessor(MutationRecord.getType, null, .{});
pub const target = bridge.accessor(MutationRecord.getTarget, null, .{});
pub const attributeName = bridge.accessor(MutationRecord.getAttributeName, null, .{});
pub const attributeNamespace = bridge.accessor(MutationRecord.getAttributeNamespace, null, .{});
pub const oldValue = bridge.accessor(MutationRecord.getOldValue, null, .{});
pub const addedNodes = bridge.accessor(MutationRecord.getAddedNodes, null, .{});
pub const removedNodes = bridge.accessor(MutationRecord.getRemovedNodes, null, .{});

View File

@@ -16,16 +16,18 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const builtin = @import("builtin");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Navigator = @This();
_pad: bool = false,
pub const init: Navigator = .{};
pub fn getUserAgent(_: *const Navigator) []const u8 {
return "Lightpanda/1.0";
pub fn getUserAgent(_: *const Navigator, page: *Page) []const u8 {
return page._session.browser.app.config.user_agent;
}
pub fn getAppName(_: *const Navigator) []const u8 {
@@ -63,8 +65,7 @@ pub fn getOnLine(_: *const Navigator) bool {
}
pub fn getCookieEnabled(_: *const Navigator) bool {
// TODO: Implement cookie support
return false;
return true;
}
pub fn getHardwareConcurrency(_: *const Navigator) u32 {
@@ -95,6 +96,74 @@ pub fn getWebdriver(_: *const Navigator) bool {
return false;
}
pub fn registerProtocolHandler(_: *const Navigator, scheme: []const u8, url: [:0]const u8, page: *const Page) !void {
try validateProtocolHandlerScheme(scheme);
try validateProtocolHandlerURL(url, page);
}
pub fn unregisterProtocolHandler(_: *const Navigator, scheme: []const u8, url: [:0]const u8, page: *const Page) !void {
try validateProtocolHandlerScheme(scheme);
try validateProtocolHandlerURL(url, page);
}
fn validateProtocolHandlerScheme(scheme: []const u8) !void {
const allowed = std.StaticStringMap(void).initComptime(.{
.{ "bitcoin", {} },
.{ "cabal", {} },
.{ "dat", {} },
.{ "did", {} },
.{ "dweb", {} },
.{ "ethereum", .{} },
.{ "ftp", {} },
.{ "ftps", {} },
.{ "geo", {} },
.{ "im", {} },
.{ "ipfs", {} },
.{ "ipns", .{} },
.{ "irc", {} },
.{ "ircs", {} },
.{ "hyper", {} },
.{ "magnet", {} },
.{ "mailto", {} },
.{ "matrix", {} },
.{ "mms", {} },
.{ "news", {} },
.{ "nntp", {} },
.{ "openpgp4fpr", {} },
.{ "sftp", {} },
.{ "sip", {} },
.{ "sms", {} },
.{ "smsto", {} },
.{ "ssb", {} },
.{ "ssh", {} },
.{ "tel", {} },
.{ "urn", {} },
.{ "webcal", {} },
.{ "wtai", {} },
.{ "xmpp", {} },
});
if (allowed.has(scheme)) {
return;
}
if (scheme.len < 5 or !std.mem.startsWith(u8, scheme, "web+")) {
return error.SecurityError;
}
for (scheme[4..]) |b| {
if (std.ascii.isLower(b) == false) {
return error.SecurityError;
}
}
}
fn validateProtocolHandlerURL(url: [:0]const u8, page: *const Page) !void {
if (std.mem.indexOf(u8, url, "%s") == null) {
return error.SyntaxError;
}
if (try page.isSameOrigin(url) == false) {
return error.SyntaxError;
}
}
pub const JsApi = struct {
pub const bridge = js.Bridge(Navigator);
@@ -120,6 +189,8 @@ pub const JsApi = struct {
pub const vendor = bridge.accessor(Navigator.getVendor, null, .{});
pub const product = bridge.accessor(Navigator.getProduct, null, .{});
pub const webdriver = bridge.accessor(Navigator.getWebdriver, null, .{});
pub const registerProtocolHandler = bridge.function(Navigator.registerProtocolHandler, .{ .dom_exception = true });
pub const unregisterProtocolHandler = bridge.function(Navigator.unregisterProtocolHandler, .{ .dom_exception = true });
// Methods
pub const javaEnabled = bridge.function(Navigator.javaEnabled, .{});

View File

@@ -378,8 +378,14 @@ pub fn isConnected(self: *const Node) bool {
root = parent;
}
// A node is connected if its root is a document
return root._type == .document;
switch (root._type) {
.document => return true,
.document_fragment => |df| {
const sr = df.is(ShadowRoot) orelse return false;
return sr._host.asNode().isConnected();
},
else => return false,
}
}
const GetRootNodeOpts = struct {
@@ -652,25 +658,19 @@ pub fn getData(self: *const Node) []const u8 {
};
}
pub fn setData(self: *Node, data: []const u8) void {
pub fn setData(self: *Node, data: []const u8, page: *Page) !void {
switch (self._type) {
.cdata => |c| c._data = data,
.cdata => |c| try c.setData(data, page),
else => {},
}
}
pub fn className(self: *const Node) []const u8 {
switch (self._type) {
inline else => |c| return c.className(),
}
}
pub fn normalize(self: *Node, page: *Page) !void {
var buffer: std.ArrayListUnmanaged(u8) = .empty;
return self._normalize(page.call_arena, &buffer, page);
}
pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) error{ OutOfMemory, StringTooLarge, NotSupported, NotImplemented, InvalidCharacterError }!*Node {
pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) error{ OutOfMemory, StringTooLarge, NotSupported, NotImplemented, InvalidCharacterError, CloneError }!*Node {
const deep = deep_ orelse false;
switch (self._type) {
.cdata => |cd| {
@@ -682,11 +682,17 @@ pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) error{ OutOfMemory, Str
.processing_instruction => |pi| page.createProcessingInstruction(pi._target, data),
};
},
.element => |el| return el.cloneElement(deep, page),
.element => |el| return el.clone(deep, page),
.document => return error.NotSupported,
.document_type => return error.NotSupported,
.document_type => |dt| {
const cloned = dt.clone(page) catch return error.CloneError;
return cloned.asNode();
},
.document_fragment => |frag| return frag.cloneFragment(deep, page),
.attribute => return error.NotSupported,
.attribute => |attr| {
const cloned = attr.clone(page) catch return error.CloneError;
return cloned._proto;
},
}
}

View File

@@ -22,14 +22,14 @@ const Node = @import("Node.zig");
const NodeFilter = @This();
_func: ?js.Function,
_func: ?js.Function.Global,
_original_filter: ?FilterOpts,
pub const FilterOpts = union(enum) {
function: js.Function,
function: js.Function.Global,
object: struct {
pub const js_as_object = true;
acceptNode: js.Function,
acceptNode: js.Function.Global,
},
};
@@ -39,7 +39,10 @@ pub fn init(opts_: ?FilterOpts) !NodeFilter {
.function => |func| func,
.object => |obj| obj.acceptNode,
};
return .{ ._func = func, ._original_filter = opts_ };
return .{
._func = func,
._original_filter = opts_,
};
}
// Constants
@@ -64,7 +67,7 @@ pub const SHOW_NOTATION: u32 = 0x800;
pub fn acceptNode(self: *const NodeFilter, node: *Node) !i32 {
const func = self._func orelse return FILTER_ACCEPT;
return func.call(i32, .{node});
return func.local().call(i32, .{node});
}
pub fn shouldShow(node: *const Node, what_to_show: u32) bool {

View File

@@ -314,10 +314,10 @@ pub const Entry = struct {
pub const Mark = struct {
_proto: *Entry,
_detail: ?js.Object,
_detail: ?js.Value.Global,
const Options = struct {
detail: ?js.Object = null,
detail: ?js.Value = null,
startTime: ?f64 = null,
};
@@ -344,7 +344,7 @@ pub const Mark = struct {
return m;
}
pub fn getDetail(self: *const Mark) ?js.Object {
pub fn getDetail(self: *const Mark) ?js.Value.Global {
return self._detail;
}
@@ -362,7 +362,7 @@ pub const Mark = struct {
pub const Measure = struct {
_proto: *Entry,
_detail: ?js.Object,
_detail: ?js.Object.Global,
const Options = struct {
detail: ?js.Object = null,
@@ -405,7 +405,7 @@ pub const Measure = struct {
return m;
}
pub fn getDetail(self: *const Measure) ?js.Object {
pub fn getDetail(self: *const Measure) ?js.Object.Global {
return self._detail;
}

View File

@@ -19,6 +19,8 @@
const std = @import("std");
const js = @import("../js/js.zig");
const log = @import("../../log.zig");
const Page = @import("../Page.zig");
const Performance = @import("Performance.zig");
@@ -30,7 +32,7 @@ pub fn registerTypes() []const type {
const PerformanceObserver = @This();
/// Emitted when there are events with same interests.
_callback: js.Function,
_callback: js.Function.Global,
/// The threshold to deliver `PerformanceEventTiming` entries.
_duration_threshold: f64,
/// Entry types we're looking for are encoded as bit flags.
@@ -42,7 +44,7 @@ _entries: std.ArrayList(*Performance.Entry),
const DefaultDurationThreshold: f64 = 104;
/// Creates a new PerformanceObserver object with the given observer callback.
pub fn init(callback: js.Function, page: *Page) !*PerformanceObserver {
pub fn init(callback: js.Function.Global, page: *Page) !*PerformanceObserver {
return page._factory.create(PerformanceObserver{
._callback = callback,
._duration_threshold = DefaultDurationThreshold,
@@ -124,12 +126,14 @@ pub fn disconnect(self: *PerformanceObserver, page: *Page) void {
/// Returns the current list of PerformanceEntry objects
/// stored in the performance observer, emptying it out.
pub fn takeRecords(self: *PerformanceObserver, page: *Page) ![]*Performance.Entry {
const records = try page.call_arena.dupe(*Performance.Entry, self._entries.items);
// Use page.arena instead of call_arena because this slice is wrapped in EntryList
// and may be accessed later.
const records = try page.arena.dupe(*Performance.Entry, self._entries.items);
self._entries.clearRetainingCapacity();
return records;
}
pub fn getSupportedEntryTypes(_: *const PerformanceObserver) []const []const u8 {
pub fn getSupportedEntryTypes() []const []const u8 {
return &.{ "mark", "measure" };
}
@@ -149,7 +153,11 @@ pub inline fn hasRecords(self: *const PerformanceObserver) bool {
/// Runs the PerformanceObserver's callback with records; emptying it out.
pub fn dispatch(self: *PerformanceObserver, page: *Page) !void {
const records = try self.takeRecords(page);
_ = try self._callback.call(void, .{ EntryList{ ._entries = records }, self });
var caught: js.TryCatch.Caught = undefined;
self._callback.local().tryCall(void, .{ EntryList{ ._entries = records }, self }, &caught) catch |err| {
log.err(.page, "PerfObserver.dispatch", .{ .err = err, .caught = caught });
return err;
};
}
pub const JsApi = struct {

View File

@@ -357,7 +357,7 @@ pub fn deleteContents(self: *Range, page: *Page) !void {
u8,
&.{ text_data[0..self._proto._start_offset], text_data[self._proto._end_offset..] },
);
self._proto._start_container.setData(new_text);
try self._proto._start_container.setData(new_text, page);
} else {
// Delete child nodes in range
var offset = self._proto._start_offset;
@@ -371,8 +371,43 @@ pub fn deleteContents(self: *Range, page: *Page) !void {
return;
}
// Complex case: different containers - simplified implementation
// Just collapse the range for now
// Complex case: different containers
// Handle start container - if it's a text node, truncate it
if (self._proto._start_container.is(Node.CData)) |_| {
const text_data = self._proto._start_container.getData();
if (self._proto._start_offset < text_data.len) {
// Keep only the part before start_offset
const new_text = text_data[0..self._proto._start_offset];
try self._proto._start_container.setData(new_text, page);
}
}
// Handle end container - if it's a text node, truncate it
if (self._proto._end_container.is(Node.CData)) |_| {
const text_data = self._proto._end_container.getData();
if (self._proto._end_offset < text_data.len) {
// Keep only the part from end_offset onwards
const new_text = text_data[self._proto._end_offset..];
try self._proto._end_container.setData(new_text, page);
} else if (self._proto._end_offset == text_data.len) {
// If we're at the end, set to empty (will be removed if needed)
try self._proto._end_container.setData("", page);
}
}
// Remove nodes between start and end containers
// For now, handle the common case where they're siblings
if (self._proto._start_container.parentNode() == self._proto._end_container.parentNode()) {
var current = self._proto._start_container.nextSibling();
while (current != null and current != self._proto._end_container) {
const next = current.?.nextSibling();
if (current.?.parentNode()) |parent| {
_ = try parent.removeChild(current.?, page);
}
current = next;
}
}
self.collapse(true);
}
@@ -401,6 +436,39 @@ pub fn cloneContents(self: *const Range, page: *Page) !*DocumentFragment {
}
}
}
} else {
// Complex case: different containers
// Clone partial start container
if (self._proto._start_container.is(Node.CData)) |_| {
const text_data = self._proto._start_container.getData();
if (self._proto._start_offset < text_data.len) {
// Clone from start_offset to end of text
const cloned_text = text_data[self._proto._start_offset..];
const text_node = try page.createTextNode(cloned_text);
_ = try fragment.asNode().appendChild(text_node, page);
}
}
// Clone nodes between start and end containers (siblings case)
if (self._proto._start_container.parentNode() == self._proto._end_container.parentNode()) {
var current = self._proto._start_container.nextSibling();
while (current != null and current != self._proto._end_container) {
const cloned = try current.?.cloneNode(true, page);
_ = try fragment.asNode().appendChild(cloned, page);
current = current.?.nextSibling();
}
}
// Clone partial end container
if (self._proto._end_container.is(Node.CData)) |_| {
const text_data = self._proto._end_container.getData();
if (self._proto._end_offset > 0 and self._proto._end_offset <= text_data.len) {
// Clone from start to end_offset
const cloned_text = text_data[0..self._proto._end_offset];
const text_node = try page.createTextNode(cloned_text);
_ = try fragment.asNode().appendChild(text_node, page);
}
}
}
return fragment;
@@ -443,9 +511,9 @@ pub fn createContextualFragment(self: *const Range, html: []const u8, page: *Pag
// Create a temporary element of the same type as the context for parsing
// This preserves the parsing context without modifying the original node
const temp_node = if (context_node.is(Node.Element)) |el|
try page.createElement(el._namespace.toUri(), el.getTagNameLower(), null)
try page.createElementNS(el._namespace, el.getTagNameLower(), null)
else
try page.createElement(null, "div", null);
try page.createElementNS(.html, "div", null);
try page.parseHtmlAsChildren(temp_node, html);

View File

@@ -61,10 +61,6 @@ pub fn asEventTarget(self: *ShadowRoot) *@import("EventTarget.zig") {
return self.asNode().asEventTarget();
}
pub fn className(_: *const ShadowRoot) []const u8 {
return "[object ShadowRoot]";
}
pub fn getMode(self: *const ShadowRoot) []const u8 {
return @tagName(self._mode);
}

View File

@@ -0,0 +1,638 @@
// 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 lp = @import("lightpanda");
const log = @import("../../log.zig");
const crypto = @import("../../crypto.zig");
const Page = @import("../Page.zig");
const js = @import("../js/js.zig");
pub fn registerTypes() []const type {
return &.{ SubtleCrypto, CryptoKey };
}
/// The SubtleCrypto interface of the Web Crypto API provides a number of low-level
/// cryptographic functions.
/// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto
/// https://w3c.github.io/webcrypto/#subtlecrypto-interface
const SubtleCrypto = @This();
/// Don't optimize away the type.
_pad: bool = false,
const Algorithm = union(enum) {
/// For RSASSA-PKCS1-v1_5, RSA-PSS, or RSA-OAEP: pass an RsaHashedKeyGenParams object.
rsa_hashed_key_gen: RsaHashedKeyGen,
/// For HMAC: pass an HmacKeyGenParams object.
hmac_key_gen: HmacKeyGen,
/// Can be Ed25519 or X25519.
name: []const u8,
/// Can be Ed25519 or X25519.
object: struct { name: []const u8 },
/// https://developer.mozilla.org/en-US/docs/Web/API/RsaHashedKeyGenParams
const RsaHashedKeyGen = struct {
name: []const u8,
/// This should be at least 2048.
/// Some organizations are now recommending that it should be 4096.
modulusLength: u32,
publicExponent: js.TypedArray(u8),
hash: union(enum) {
string: []const u8,
object: struct { name: []const u8 },
},
};
/// https://developer.mozilla.org/en-US/docs/Web/API/HmacKeyGenParams
const HmacKeyGen = struct {
/// Always HMAC.
name: []const u8,
/// Its also possible to pass this in an object.
hash: union(enum) {
string: []const u8,
object: struct { name: []const u8 },
},
/// If omitted, default is the block size of the chosen hash function.
length: ?usize,
};
/// Alias.
const HmacImport = HmacKeyGen;
const EcdhKeyDeriveParams = struct {
/// Can be Ed25519 or X25519.
name: []const u8,
public: *const CryptoKey,
};
/// Algorithm for deriveBits() and deriveKey().
const DeriveBits = union(enum) {
ecdh_or_x25519: EcdhKeyDeriveParams,
};
};
/// Generate a new key (for symmetric algorithms) or key pair (for public-key algorithms).
pub fn generateKey(
_: *const SubtleCrypto,
algorithm: Algorithm,
extractable: bool,
key_usages: []const []const u8,
page: *Page,
) !js.Promise {
const key_or_pair = CryptoKey.init(algorithm, extractable, key_usages, page) catch |err| {
return page.js.rejectPromise(@errorName(err));
};
return page.js.resolvePromise(key_or_pair);
}
/// Exports a key: that is, it takes as input a CryptoKey object and gives you
/// the key in an external, portable format.
pub fn exportKey(
_: *const SubtleCrypto,
format: []const u8,
key: *CryptoKey,
page: *Page,
) !js.Promise {
if (!key.canExportKey()) {
return error.InvalidAccessError;
}
if (std.mem.eql(u8, format, "raw")) {
return page.js.resolvePromise(js.ArrayBuffer{ .values = key._key });
}
const is_unsupported = std.mem.eql(u8, format, "pkcs8") or
std.mem.eql(u8, format, "spki") or std.mem.eql(u8, format, "jwk");
if (is_unsupported) {
log.warn(.not_implemented, "SubtleCrypto.exportKey", .{ .format = format });
}
return page.js.rejectPromise(@errorName(error.NotSupported));
}
/// Derive a secret key from a master key.
pub fn deriveBits(
_: *const SubtleCrypto,
algorithm: Algorithm.DeriveBits,
base_key: *const CryptoKey, // Private key.
length: usize,
page: *Page,
) !js.Promise {
return switch (algorithm) {
.ecdh_or_x25519 => |p| {
const name = p.name;
if (std.mem.eql(u8, name, "X25519")) {
return page.js.resolvePromise(base_key.deriveBitsX25519(p.public, length, page));
}
if (std.mem.eql(u8, name, "ECDH")) {
log.warn(.not_implemented, "SubtleCrypto.deriveBits", .{ .name = name });
}
return page.js.rejectPromise(@errorName(error.NotSupported));
},
};
}
const SignatureAlgorithm = union(enum) {
string: []const u8,
object: struct { name: []const u8 },
pub fn isHMAC(self: SignatureAlgorithm) bool {
const name = switch (self) {
.string => |string| string,
.object => |object| object.name,
};
if (name.len < 4) return false;
const hmac: u32 = @bitCast([4]u8{ 'H', 'M', 'A', 'C' });
return @as(u32, @bitCast(name[0..4].*)) == hmac;
}
};
/// Generate a digital signature.
pub fn sign(
_: *const SubtleCrypto,
/// This can either be provided as string or object.
/// We can't use the `Algorithm` type defined before though since there
/// are couple of changes between the two.
/// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/sign#algorithm
algorithm: SignatureAlgorithm,
key: *CryptoKey,
data: []const u8, // ArrayBuffer.
page: *Page,
) !js.Promise {
return switch (key._type) {
.hmac => {
// Verify algorithm.
if (!algorithm.isHMAC()) {
return page.js.rejectPromise(@errorName(error.InvalidAccessError));
}
// Call sign for HMAC.
const result = key.signHMAC(data, page) catch |err| {
return page.js.rejectPromise(@errorName(err));
};
return page.js.resolvePromise(result);
},
else => {
log.warn(.not_implemented, "SubtleCrypto.sign", .{ .key_type = key._type });
return page.js.rejectPromise(@errorName(error.InvalidAccessError));
},
};
}
/// Verify a digital signature.
pub fn verify(
_: *const SubtleCrypto,
algorithm: SignatureAlgorithm,
key: *const CryptoKey,
signature: []const u8, // ArrayBuffer.
data: []const u8, // ArrayBuffer.
page: *Page,
) !js.Promise {
if (!algorithm.isHMAC()) return error.InvalidAccessError;
return switch (key._type) {
.hmac => key.verifyHMAC(signature, data, page),
else => return error.InvalidAccessError,
};
}
/// Returns the desired digest by its name.
fn findDigest(name: []const u8) error{Invalid}!*const crypto.EVP_MD {
if (std.mem.eql(u8, "SHA-256", name)) {
return crypto.EVP_sha256();
}
if (std.mem.eql(u8, "SHA-384", name)) {
return crypto.EVP_sha384();
}
if (std.mem.eql(u8, "SHA-512", name)) {
return crypto.EVP_sha512();
}
if (std.mem.eql(u8, "SHA-1", name)) {
return crypto.EVP_sha1();
}
return error.Invalid;
}
const KeyOrPair = union(enum) { key: *CryptoKey, pair: CryptoKeyPair };
/// https://developer.mozilla.org/en-US/docs/Web/API/CryptoKeyPair
const CryptoKeyPair = struct {
privateKey: *CryptoKey,
publicKey: *CryptoKey,
};
/// Represents a cryptographic key obtained from one of the SubtleCrypto methods
/// generateKey(), deriveKey(), importKey(), or unwrapKey().
pub const CryptoKey = struct {
/// Algorithm being used.
_type: Type,
/// Whether the key is extractable.
_extractable: bool,
/// Bit flags of `usages`; see `Usages` type.
_usages: u8,
/// Raw bytes of key.
_key: []const u8,
/// Different algorithms may use different data structures;
/// this union can be used for such situations. Active field is understood
/// from `_type`.
_vary: extern union {
/// Used by HMAC.
digest: *const crypto.EVP_MD,
/// Used by asymmetric algorithms (X25519, Ed25519).
pkey: *crypto.EVP_PKEY,
},
pub const Type = enum(u8) { hmac, rsa, x25519 };
/// Changing the names of fields would affect bitmask creation.
pub const Usages = struct {
// zig fmt: off
pub const encrypt = 0x001;
pub const decrypt = 0x002;
pub const sign = 0x004;
pub const verify = 0x008;
pub const deriveKey = 0x010;
pub const deriveBits = 0x020;
pub const wrapKey = 0x040;
pub const unwrapKey = 0x080;
// zig fmt: on
};
pub fn init(
algorithm: Algorithm,
extractable: bool,
key_usages: []const []const u8,
page: *Page,
) !KeyOrPair {
return switch (algorithm) {
.hmac_key_gen => |hmac| initHMAC(hmac, extractable, key_usages, page),
.name => |name| {
if (std.mem.eql(u8, "X25519", name)) {
return initX25519(extractable, key_usages, page);
}
log.warn(.not_implemented, "CryptoKey.init", .{ .name = name });
return error.NotSupported;
},
.object => |object| {
// Ditto.
const name = object.name;
if (std.mem.eql(u8, "X25519", name)) {
return initX25519(extractable, key_usages, page);
}
log.warn(.not_implemented, "CryptoKey.init", .{ .name = name });
return error.NotSupported;
},
else => {
log.warn(.not_implemented, "CryptoKey.init", .{ .algorithm = algorithm });
return error.NotSupported;
},
};
}
inline fn canSign(self: *const CryptoKey) bool {
return self._usages & Usages.sign != 0;
}
inline fn canVerify(self: *const CryptoKey) bool {
return self._usages & Usages.verify != 0;
}
inline fn canDeriveBits(self: *const CryptoKey) bool {
return self._usages & Usages.deriveBits != 0;
}
inline fn canExportKey(self: *const CryptoKey) bool {
return self._extractable;
}
/// Only valid for HMAC.
inline fn getDigest(self: *const CryptoKey) *const crypto.EVP_MD {
return self._vary.digest;
}
/// Only valid for asymmetric algorithms (X25519, Ed25519).
inline fn getKeyObject(self: *const CryptoKey) *crypto.EVP_PKEY {
return self._vary.pkey;
}
// HMAC.
fn initHMAC(
algorithm: Algorithm.HmacKeyGen,
extractable: bool,
key_usages: []const []const u8,
page: *Page,
) !KeyOrPair {
const hash = switch (algorithm.hash) {
.string => |str| str,
.object => |obj| obj.name,
};
// Find digest.
const digest = try findDigest(hash);
// We need at least a single usage.
if (key_usages.len == 0) {
return error.SyntaxError;
}
// Calculate usages mask.
const decls = @typeInfo(Usages).@"struct".decls;
var usages_mask: u8 = 0;
iter_usages: for (key_usages) |usage| {
inline for (decls) |decl| {
if (std.mem.eql(u8, decl.name, usage)) {
usages_mask |= @field(Usages, decl.name);
continue :iter_usages;
}
}
// Unknown usage if got here.
return error.SyntaxError;
}
const block_size: usize = blk: {
// Caller provides this in bits, not bytes.
if (algorithm.length) |length| {
break :blk length / 8;
}
// Prefer block size of the hash function instead.
break :blk crypto.EVP_MD_block_size(digest);
};
const key = try page.arena.alloc(u8, block_size);
errdefer page.arena.free(key);
// HMAC is simply CSPRNG.
const res = crypto.RAND_bytes(key.ptr, key.len);
lp.assert(res == 1, "SubtleCrypto.initHMAC", .{ .res = res });
const crypto_key = try page._factory.create(CryptoKey{
._type = .hmac,
._extractable = extractable,
._usages = usages_mask,
._key = key,
._vary = .{ .digest = digest },
});
return .{ .key = crypto_key };
}
fn signHMAC(self: *const CryptoKey, data: []const u8, page: *Page) !js.ArrayBuffer {
if (!self.canSign()) {
return error.InvalidAccessError;
}
const buffer = try page.call_arena.alloc(u8, crypto.EVP_MD_size(self.getDigest()));
errdefer page.call_arena.free(buffer);
var out_len: u32 = 0;
// Try to sign.
const signed = crypto.HMAC(
self.getDigest(),
@ptrCast(self._key.ptr),
self._key.len,
data.ptr,
data.len,
buffer.ptr,
&out_len,
);
if (signed != null) {
return js.ArrayBuffer{ .values = buffer[0..out_len] };
}
// Not DOM exception, failed on our side.
return error.Invalid;
}
fn verifyHMAC(
self: *const CryptoKey,
signature: []const u8,
data: []const u8,
page: *Page,
) !js.Promise {
if (!self.canVerify()) {
return error.InvalidAccessError;
}
var buffer: [crypto.EVP_MAX_MD_BLOCK_SIZE]u8 = undefined;
var out_len: u32 = 0;
// Try to sign.
const signed = crypto.HMAC(
self.getDigest(),
@ptrCast(self._key.ptr),
self._key.len,
data.ptr,
data.len,
&buffer,
&out_len,
);
if (signed != null) {
// CRYPTO_memcmp compare in constant time so prohibits time-based attacks.
const res = crypto.CRYPTO_memcmp(signed, @ptrCast(signature.ptr), signature.len);
return page.js.resolvePromise(res == 0);
}
return page.js.resolvePromise(false);
}
// X25519.
/// Create a pair of X25519.
fn initX25519(
extractable: bool,
key_usages: []const []const u8,
page: *Page,
) !KeyOrPair {
// This code has too many allocations here and there, might be nice to
// gather them together with a single alloc call. Not sure if factory
// pattern is suitable for it though.
// Calculate usages; only matters for private key.
// Only deriveKey() and deriveBits() be used for X25519.
if (key_usages.len == 0) {
return error.SyntaxError;
}
var mask: u8 = 0;
iter_usages: for (key_usages) |usage| {
inline for ([_][]const u8{ "deriveKey", "deriveBits" }) |name| {
if (std.mem.eql(u8, name, usage)) {
mask |= @field(Usages, name);
continue :iter_usages;
}
}
// Unknown usage if got here.
return error.SyntaxError;
}
const public_value = try page.arena.alloc(u8, crypto.X25519_PUBLIC_VALUE_LEN);
errdefer page.arena.free(public_value);
const private_key = try page.arena.alloc(u8, crypto.X25519_PRIVATE_KEY_LEN);
errdefer page.arena.free(private_key);
// There's no info about whether this can fail; so I assume it cannot.
crypto.X25519_keypair(@ptrCast(public_value), @ptrCast(private_key));
// Create EVP_PKEY for public key.
// Seems we can use `EVP_PKEY_from_raw_private_key` for this, Chrome
// prefer not to, yet BoringSSL added it and recommends instead of what
// we're doing currently.
const public_pkey = crypto.EVP_PKEY_new_raw_public_key(
crypto.EVP_PKEY_X25519,
null,
public_value.ptr,
public_value.len,
);
if (public_pkey == null) {
return error.OutOfMemory;
}
// Create EVP_PKEY for private key.
// Seems we can use `EVP_PKEY_from_raw_private_key` for this, Chrome
// prefer not to, yet BoringSSL added it and recommends instead of what
// we're doing currently.
const private_pkey = crypto.EVP_PKEY_new_raw_private_key(
crypto.EVP_PKEY_X25519,
null,
private_key.ptr,
private_key.len,
);
if (private_pkey == null) {
return error.OutOfMemory;
}
const private = try page._factory.create(CryptoKey{
._type = .x25519,
._extractable = extractable,
._usages = mask,
._key = private_key,
._vary = .{ .pkey = private_pkey.? },
});
errdefer page._factory.destroy(private);
const public = try page._factory.create(CryptoKey{
._type = .x25519,
// Public keys are always extractable.
._extractable = true,
// Always empty for public key.
._usages = 0,
._key = public_value,
._vary = .{ .pkey = public_pkey.? },
});
errdefer page._factory.destroy(public);
return .{ .pair = .{ .privateKey = private, .publicKey = public } };
}
fn deriveBitsX25519(
private: *const CryptoKey,
public: *const CryptoKey,
length_in_bits: usize,
page: *Page,
) !js.ArrayBuffer {
if (!private.canDeriveBits()) {
return error.InvalidAccessError;
}
const maybe_ctx = crypto.EVP_PKEY_CTX_new(private.getKeyObject(), null);
if (maybe_ctx) |ctx| {
// Context is valid, free it on failure.
errdefer crypto.EVP_PKEY_CTX_free(ctx);
// Init derive operation and set public key as peer.
if (crypto.EVP_PKEY_derive_init(ctx) != 1 or
crypto.EVP_PKEY_derive_set_peer(ctx, public.getKeyObject()) != 1)
{
// Failed on our end.
return error.Internal;
}
const derived_key = try page.call_arena.alloc(u8, 32);
errdefer page.call_arena.free(derived_key);
var out_key_len: usize = derived_key.len;
const result = crypto.EVP_PKEY_derive(ctx, derived_key.ptr, &out_key_len);
if (result != 1) {
// Failed on our end.
return error.Internal;
}
// Sanity check.
lp.assert(derived_key.len == out_key_len, "SubtleCrypto.deriveBitsX25519", .{});
// Length is in bits, convert to byte length.
const length = (length_in_bits / 8) + (7 + (length_in_bits % 8)) / 8;
// Truncate the slice to specified length.
// Same as `derived_key`.
const tailored = blk: {
if (length > derived_key.len) {
return error.LengthTooLong;
}
break :blk derived_key[0..length];
};
// Zero any "unused bits" in the final byte.
const remainder_bits: u3 = @intCast(length_in_bits % 8);
if (remainder_bits != 0) {
tailored[tailored.len - 1] &= ~(@as(u8, 0xFF) >> remainder_bits);
}
return js.ArrayBuffer{ .values = tailored };
}
// Failed on our end.
return error.Internal;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(CryptoKey);
pub const Meta = struct {
pub const name = "CryptoKey";
pub var class_id: bridge.ClassId = undefined;
pub const prototype_chain = bridge.prototypeChain();
};
};
};
pub const JsApi = struct {
pub const bridge = js.Bridge(SubtleCrypto);
pub const Meta = struct {
pub const name = "SubtleCrypto";
pub var class_id: bridge.ClassId = undefined;
pub const prototype_chain = bridge.prototypeChain();
};
pub const generateKey = bridge.function(SubtleCrypto.generateKey, .{ .dom_exception = true });
pub const exportKey = bridge.function(SubtleCrypto.exportKey, .{ .dom_exception = true });
pub const sign = bridge.function(SubtleCrypto.sign, .{ .dom_exception = true });
pub const verify = bridge.function(SubtleCrypto.verify, .{ .dom_exception = true });
pub const deriveBits = bridge.function(SubtleCrypto.deriveBits, .{ .dom_exception = true });
};

View File

@@ -54,11 +54,11 @@ _navigator: Navigator = .init,
_screen: *Screen,
_performance: Performance,
_storage_bucket: *storage.Bucket,
_on_load: ?js.Function = null,
_on_pageshow: ?js.Function = null,
_on_popstate: ?js.Function = null,
_on_error: ?js.Function = null, // TODO: invoke on error?
_on_unhandled_rejection: ?js.Function = null, // TODO: invoke on error
_on_load: ?js.Function.Global = null,
_on_pageshow: ?js.Function.Global = null,
_on_popstate: ?js.Function.Global = null,
_on_error: ?js.Function.Global = null, // TODO: invoke on error?
_on_unhandled_rejection: ?js.Function.Global = null, // TODO: invoke on error
_location: *Location,
_timer_id: u30 = 0,
_timers: std.AutoHashMapUnmanaged(u32, *ScheduleCallback) = .{},
@@ -145,43 +145,43 @@ pub fn getCustomElements(self: *Window) *CustomElementRegistry {
return &self._custom_elements;
}
pub fn getOnLoad(self: *const Window) ?js.Function {
pub fn getOnLoad(self: *const Window) ?js.Function.Global {
return self._on_load;
}
pub fn setOnLoad(self: *Window, setter: ?FunctionSetter) !void {
pub fn setOnLoad(self: *Window, setter: ?FunctionSetter) void {
self._on_load = getFunctionFromSetter(setter);
}
pub fn getOnPageShow(self: *const Window) ?js.Function {
pub fn getOnPageShow(self: *const Window) ?js.Function.Global {
return self._on_pageshow;
}
pub fn setOnPageShow(self: *Window, setter: ?FunctionSetter) !void {
pub fn setOnPageShow(self: *Window, setter: ?FunctionSetter) void {
self._on_pageshow = getFunctionFromSetter(setter);
}
pub fn getOnPopState(self: *const Window) ?js.Function {
pub fn getOnPopState(self: *const Window) ?js.Function.Global {
return self._on_popstate;
}
pub fn setOnPopState(self: *Window, setter: ?FunctionSetter) !void {
pub fn setOnPopState(self: *Window, setter: ?FunctionSetter) void {
self._on_popstate = getFunctionFromSetter(setter);
}
pub fn getOnError(self: *const Window) ?js.Function {
pub fn getOnError(self: *const Window) ?js.Function.Global {
return self._on_error;
}
pub fn setOnError(self: *Window, setter: ?FunctionSetter) !void {
pub fn setOnError(self: *Window, setter: ?FunctionSetter) void {
self._on_error = getFunctionFromSetter(setter);
}
pub fn getOnUnhandledRejection(self: *const Window) ?js.Function {
pub fn getOnUnhandledRejection(self: *const Window) ?js.Function.Global {
return self._on_unhandled_rejection;
}
pub fn setOnUnhandledRejection(self: *Window, setter: ?FunctionSetter) !void {
pub fn setOnUnhandledRejection(self: *Window, setter: ?FunctionSetter) void {
self._on_unhandled_rejection = getFunctionFromSetter(setter);
}
@@ -189,7 +189,7 @@ pub fn fetch(_: *const Window, input: Fetch.Input, options: ?Fetch.InitOpts, pag
return Fetch.init(input, options, page);
}
pub fn setTimeout(self: *Window, cb: js.Function, delay_ms: ?u32, params: []js.Object, page: *Page) !u32 {
pub fn setTimeout(self: *Window, cb: js.Function.Global, delay_ms: ?u32, params: []js.Value.Global, page: *Page) !u32 {
return self.scheduleCallback(cb, delay_ms orelse 0, .{
.repeat = false,
.params = params,
@@ -198,7 +198,7 @@ pub fn setTimeout(self: *Window, cb: js.Function, delay_ms: ?u32, params: []js.O
}, page);
}
pub fn setInterval(self: *Window, cb: js.Function, delay_ms: ?u32, params: []js.Object, page: *Page) !u32 {
pub fn setInterval(self: *Window, cb: js.Function.Global, delay_ms: ?u32, params: []js.Value.Global, page: *Page) !u32 {
return self.scheduleCallback(cb, delay_ms orelse 0, .{
.repeat = true,
.params = params,
@@ -207,7 +207,7 @@ pub fn setInterval(self: *Window, cb: js.Function, delay_ms: ?u32, params: []js.
}, page);
}
pub fn setImmediate(self: *Window, cb: js.Function, params: []js.Object, page: *Page) !u32 {
pub fn setImmediate(self: *Window, cb: js.Function.Global, params: []js.Value.Global, page: *Page) !u32 {
return self.scheduleCallback(cb, 0, .{
.repeat = false,
.params = params,
@@ -216,7 +216,7 @@ pub fn setImmediate(self: *Window, cb: js.Function, params: []js.Object, page: *
}, page);
}
pub fn requestAnimationFrame(self: *Window, cb: js.Function, page: *Page) !u32 {
pub fn requestAnimationFrame(self: *Window, cb: js.Function.Global, page: *Page) !u32 {
return self.scheduleCallback(cb, 5, .{
.repeat = false,
.params = &.{},
@@ -253,7 +253,7 @@ pub fn cancelAnimationFrame(self: *Window, id: u32) void {
const RequestIdleCallbackOpts = struct {
timeout: ?u32 = null,
};
pub fn requestIdleCallback(self: *Window, cb: js.Function, opts_: ?RequestIdleCallbackOpts, page: *Page) !u32 {
pub fn requestIdleCallback(self: *Window, cb: js.Function.Global, opts_: ?RequestIdleCallbackOpts, page: *Page) !u32 {
const opts = opts_ orelse RequestIdleCallbackOpts{};
return self.scheduleCallback(cb, opts.timeout orelse 50, .{
.mode = .idle,
@@ -269,10 +269,10 @@ pub fn cancelIdleCallback(self: *Window, id: u32) void {
sc.removed = true;
}
pub fn reportError(self: *Window, err: js.Object, page: *Page) !void {
pub fn reportError(self: *Window, err: js.Value.Global, page: *Page) !void {
const error_event = try ErrorEvent.initTrusted("error", .{
.@"error" = err,
.message = err.toString() catch "Unknown error",
.message = err.local().toString(.{}) catch "Unknown error",
.bubbles = false,
.cancelable = true,
}, page);
@@ -316,7 +316,7 @@ pub fn getIsSecureContext(_: *const Window) bool {
return false;
}
pub fn postMessage(self: *Window, message: js.Object, target_origin: ?[]const u8, page: *Page) !void {
pub fn postMessage(self: *Window, message: js.Value.Global, target_origin: ?[]const u8, page: *Page) !void {
// For now, we ignore targetOrigin checking and just dispatch the message
// In a full implementation, we would validate the origin
_ = target_origin;
@@ -325,7 +325,7 @@ pub fn postMessage(self: *Window, message: js.Object, target_origin: ?[]const u8
const origin = try self._location.getOrigin(page);
const callback = try page._factory.create(PostMessageCallback{
.window = self,
.message = try message.persist(),
.message = message,
.origin = try page.arena.dupe(u8, origin),
.page = page,
});
@@ -465,13 +465,13 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
const ScheduleOpts = struct {
repeat: bool,
params: []js.Object,
params: []js.Value.Global,
name: []const u8,
low_priority: bool = false,
animation_frame: bool = false,
mode: ScheduleCallback.Mode = .normal,
};
fn scheduleCallback(self: *Window, cb: js.Function, delay_ms: u32, opts: ScheduleOpts, page: *Page) !u32 {
fn scheduleCallback(self: *Window, cb: js.Function.Global, delay_ms: u32, opts: ScheduleOpts, page: *Page) !u32 {
if (self._timers.count() > 512) {
// these are active
return error.TooManyTimeout;
@@ -481,12 +481,9 @@ fn scheduleCallback(self: *Window, cb: js.Function, delay_ms: u32, opts: Schedul
self._timer_id = timer_id;
const params = opts.params;
var persisted_params: []js.Object = &.{};
var persisted_params: []js.Value.Global = &.{};
if (params.len > 0) {
persisted_params = try page.arena.alloc(js.Object, params.len);
for (params, persisted_params) |a, *ca| {
ca.* = try a.persist();
}
persisted_params = try page.arena.dupe(js.Value.Global, params);
}
const gop = try self._timers.getOrPut(page.arena, timer_id);
@@ -526,11 +523,11 @@ const ScheduleCallback = struct {
// delay, in ms, to repeat. When null, will be removed after the first time
repeat_ms: ?u32,
cb: js.Function,
cb: js.Function.Global,
page: *Page,
params: []const js.Object,
params: []const js.Value.Global,
removed: bool = false,
@@ -558,17 +555,17 @@ const ScheduleCallback = struct {
switch (self.mode) {
.idle => {
const IdleDeadline = @import("IdleDeadline.zig");
self.cb.call(void, .{IdleDeadline{}}) catch |err| {
self.cb.local().call(void, .{IdleDeadline{}}) catch |err| {
log.warn(.js, "window.idleCallback", .{ .name = self.name, .err = err });
};
},
.animation_frame => {
self.cb.call(void, .{page.window._performance.now()}) catch |err| {
self.cb.local().call(void, .{page.window._performance.now()}) catch |err| {
log.warn(.js, "window.RAF", .{ .name = self.name, .err = err });
};
},
.normal => {
self.cb.call(void, self.params) catch |err| {
self.cb.local().call(void, self.params) catch |err| {
log.warn(.js, "window.timer", .{ .name = self.name, .err = err });
};
},
@@ -587,7 +584,7 @@ const ScheduleCallback = struct {
const PostMessageCallback = struct {
window: *Window,
message: js.Object,
message: js.Value.Global,
origin: []const u8,
page: *Page,
@@ -615,17 +612,17 @@ const PostMessageCallback = struct {
};
const FunctionSetter = union(enum) {
func: js.Function,
func: js.Function.Global,
anything: js.Value,
};
// window.onload = {}; doesn't fail, but it doesn't do anything.
// seems like setting to null is ok (though, at least on Firefix, it preserves
// the original value, which we could do, but why?)
fn getFunctionFromSetter(setter_: ?FunctionSetter) ?js.Function {
fn getFunctionFromSetter(setter_: ?FunctionSetter) ?js.Function.Global {
const setter = setter_ orelse return null;
return switch (setter) {
.func => |func| func,
.func => |func| func, // Already a Global from bridge auto-conversion
.anything => null,
};
}

View File

@@ -37,10 +37,6 @@ pub fn asEventTarget(self: *XMLDocument) *@import("EventTarget.zig") {
return self._proto.asEventTarget();
}
pub fn className(_: *const XMLDocument) []const u8 {
return "[object XMLDocument]";
}
pub const JsApi = struct {
pub const bridge = js.Bridge(XMLDocument);

View File

@@ -21,10 +21,10 @@ const Page = @import("../../Page.zig");
const Animation = @This();
_effect: ?js.Object = null,
_timeline: ?js.Object = null,
_ready_resolver: ?js.PromiseResolver = null,
_finished_resolver: ?js.PromiseResolver = null,
_effect: ?js.Object.Global = null,
_timeline: ?js.Object.Global = null,
_ready_resolver: ?js.PromiseResolver.Global = null,
_finished_resolver: ?js.PromiseResolver.Global = null,
pub fn init(page: *Page) !*Animation {
return page._factory.create(Animation{});
@@ -46,36 +46,36 @@ pub fn getPending(_: *const Animation) bool {
pub fn getFinished(self: *Animation, page: *Page) !js.Promise {
if (self._finished_resolver == null) {
const resolver = page.js.createPromiseResolver(.none);
resolver.resolve("Animation.getFinished", self);
const resolver = try page.js.createPromiseResolver().persist();
resolver.local().resolve("Animation.getFinished", self);
self._finished_resolver = resolver;
}
return self._finished_resolver.?.promise();
return self._finished_resolver.?.local().promise();
}
pub fn getReady(self: *Animation, page: *Page) !js.Promise {
// never resolved, because we're always "finished"
if (self._ready_resolver == null) {
const resolver = page.js.createPromiseResolver(.none);
const resolver = try page.js.createPromiseResolver().persist();
self._ready_resolver = resolver;
}
return self._ready_resolver.?.promise();
return self._ready_resolver.?.local().promise();
}
pub fn getEffect(self: *const Animation) ?js.Object {
pub fn getEffect(self: *const Animation) ?js.Object.Global {
return self._effect;
}
pub fn setEffect(self: *Animation, effect: js.Object) !void {
self._effect = try effect.persist();
pub fn setEffect(self: *Animation, effect: ?js.Object.Global) !void {
self._effect = effect;
}
pub fn getTimeline(self: *const Animation) ?js.Object {
pub fn getTimeline(self: *const Animation) ?js.Object.Global {
return self._timeline;
}
pub fn setTimeline(self: *Animation, timeline: js.Object) !void {
self._timeline = try timeline.persist();
pub fn setTimeline(self: *Animation, timeline: ?js.Object.Global) !void {
self._timeline = timeline;
}
pub const JsApi = struct {
@@ -97,7 +97,7 @@ pub const JsApi = struct {
pub const finished = bridge.accessor(Animation.getFinished, null, .{});
pub const ready = bridge.accessor(Animation.getReady, null, .{});
pub const effect = bridge.accessor(Animation.getEffect, Animation.setEffect, .{});
pub const timeline = bridge.accessor(Animation.getTimeline, Animation.getTimeline, .{});
pub const timeline = bridge.accessor(Animation.getTimeline, Animation.setTimeline, .{});
};
const testing = @import("../../../testing.zig");

View File

@@ -0,0 +1,183 @@
// 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 js = @import("../../js/js.zig");
const color = @import("../../color.zig");
const Page = @import("../../Page.zig");
/// 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 getFillStyle(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 setFillStyle(
self: *CanvasRenderingContext2D,
value: []const u8,
) !void {
// Prefer the same fill_style if fails.
self._fill_style = color.RGBA.parse(value) catch self._fill_style;
}
pub fn getGlobalAlpha(_: *const CanvasRenderingContext2D) f64 {
return 1.0;
}
pub fn getGlobalCompositeOperation(_: *const CanvasRenderingContext2D) []const u8 {
return "source-over";
}
pub fn getStrokeStyle(_: *const CanvasRenderingContext2D) []const u8 {
return "#000000";
}
pub fn getLineWidth(_: *const CanvasRenderingContext2D) f64 {
return 1.0;
}
pub fn getLineCap(_: *const CanvasRenderingContext2D) []const u8 {
return "butt";
}
pub fn getLineJoin(_: *const CanvasRenderingContext2D) []const u8 {
return "miter";
}
pub fn getMiterLimit(_: *const CanvasRenderingContext2D) f64 {
return 10.0;
}
pub fn getFont(_: *const CanvasRenderingContext2D) []const u8 {
return "10px sans-serif";
}
pub fn getTextAlign(_: *const CanvasRenderingContext2D) []const u8 {
return "start";
}
pub fn getTextBaseline(_: *const CanvasRenderingContext2D) []const u8 {
return "alphabetic";
}
pub fn save(_: *CanvasRenderingContext2D) void {}
pub fn restore(_: *CanvasRenderingContext2D) void {}
pub fn scale(_: *CanvasRenderingContext2D, _: f64, _: f64) void {}
pub fn rotate(_: *CanvasRenderingContext2D, _: f64) void {}
pub fn translate(_: *CanvasRenderingContext2D, _: f64, _: f64) void {}
pub fn transform(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64) void {}
pub fn setTransform(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64) void {}
pub fn resetTransform(_: *CanvasRenderingContext2D) void {}
pub fn setGlobalAlpha(_: *CanvasRenderingContext2D, _: f64) void {}
pub fn setGlobalCompositeOperation(_: *CanvasRenderingContext2D, _: []const u8) void {}
pub fn setStrokeStyle(_: *CanvasRenderingContext2D, _: []const u8) void {}
pub fn setLineWidth(_: *CanvasRenderingContext2D, _: f64) void {}
pub fn setLineCap(_: *CanvasRenderingContext2D, _: []const u8) void {}
pub fn setLineJoin(_: *CanvasRenderingContext2D, _: []const u8) void {}
pub fn setMiterLimit(_: *CanvasRenderingContext2D, _: f64) void {}
pub fn clearRect(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
pub fn fillRect(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
pub fn strokeRect(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
pub fn beginPath(_: *CanvasRenderingContext2D) void {}
pub fn closePath(_: *CanvasRenderingContext2D) void {}
pub fn moveTo(_: *CanvasRenderingContext2D, _: f64, _: f64) void {}
pub fn lineTo(_: *CanvasRenderingContext2D, _: f64, _: f64) void {}
pub fn quadraticCurveTo(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
pub fn bezierCurveTo(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: f64) void {}
pub fn arc(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64, _: ?bool) void {}
pub fn arcTo(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64, _: f64) void {}
pub fn rect(_: *CanvasRenderingContext2D, _: f64, _: f64, _: f64, _: f64) void {}
pub fn fill(_: *CanvasRenderingContext2D) void {}
pub fn stroke(_: *CanvasRenderingContext2D) void {}
pub fn clip(_: *CanvasRenderingContext2D) void {}
pub fn setFont(_: *CanvasRenderingContext2D, _: []const u8) void {}
pub fn setTextAlign(_: *CanvasRenderingContext2D, _: []const u8) void {}
pub fn setTextBaseline(_: *CanvasRenderingContext2D, _: []const u8) void {}
pub fn fillText(_: *CanvasRenderingContext2D, _: []const u8, _: f64, _: f64, _: ?f64) void {}
pub fn strokeText(_: *CanvasRenderingContext2D, _: []const u8, _: f64, _: f64, _: ?f64) void {}
pub const JsApi = struct {
pub const bridge = js.Bridge(CanvasRenderingContext2D);
pub const Meta = struct {
pub const name = "CanvasRenderingContext2D";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const save = bridge.function(CanvasRenderingContext2D.save, .{});
pub const restore = bridge.function(CanvasRenderingContext2D.restore, .{});
pub const scale = bridge.function(CanvasRenderingContext2D.scale, .{});
pub const rotate = bridge.function(CanvasRenderingContext2D.rotate, .{});
pub const translate = bridge.function(CanvasRenderingContext2D.translate, .{});
pub const transform = bridge.function(CanvasRenderingContext2D.transform, .{});
pub const setTransform = bridge.function(CanvasRenderingContext2D.setTransform, .{});
pub const resetTransform = bridge.function(CanvasRenderingContext2D.resetTransform, .{});
pub const globalAlpha = bridge.accessor(CanvasRenderingContext2D.getGlobalAlpha, CanvasRenderingContext2D.setGlobalAlpha, .{});
pub const globalCompositeOperation = bridge.accessor(CanvasRenderingContext2D.getGlobalCompositeOperation, CanvasRenderingContext2D.setGlobalCompositeOperation, .{});
pub const fillStyle = bridge.accessor(CanvasRenderingContext2D.getFillStyle, CanvasRenderingContext2D.setFillStyle, .{});
pub const strokeStyle = bridge.accessor(CanvasRenderingContext2D.getStrokeStyle, CanvasRenderingContext2D.setStrokeStyle, .{});
pub const lineWidth = bridge.accessor(CanvasRenderingContext2D.getLineWidth, CanvasRenderingContext2D.setLineWidth, .{});
pub const lineCap = bridge.accessor(CanvasRenderingContext2D.getLineCap, CanvasRenderingContext2D.setLineCap, .{});
pub const lineJoin = bridge.accessor(CanvasRenderingContext2D.getLineJoin, CanvasRenderingContext2D.setLineJoin, .{});
pub const miterLimit = bridge.accessor(CanvasRenderingContext2D.getMiterLimit, CanvasRenderingContext2D.setMiterLimit, .{});
pub const clearRect = bridge.function(CanvasRenderingContext2D.clearRect, .{});
pub const fillRect = bridge.function(CanvasRenderingContext2D.fillRect, .{});
pub const strokeRect = bridge.function(CanvasRenderingContext2D.strokeRect, .{});
pub const beginPath = bridge.function(CanvasRenderingContext2D.beginPath, .{});
pub const closePath = bridge.function(CanvasRenderingContext2D.closePath, .{});
pub const moveTo = bridge.function(CanvasRenderingContext2D.moveTo, .{});
pub const lineTo = bridge.function(CanvasRenderingContext2D.lineTo, .{});
pub const quadraticCurveTo = bridge.function(CanvasRenderingContext2D.quadraticCurveTo, .{});
pub const bezierCurveTo = bridge.function(CanvasRenderingContext2D.bezierCurveTo, .{});
pub const arc = bridge.function(CanvasRenderingContext2D.arc, .{});
pub const arcTo = bridge.function(CanvasRenderingContext2D.arcTo, .{});
pub const rect = bridge.function(CanvasRenderingContext2D.rect, .{});
pub const fill = bridge.function(CanvasRenderingContext2D.fill, .{});
pub const stroke = bridge.function(CanvasRenderingContext2D.stroke, .{});
pub const clip = bridge.function(CanvasRenderingContext2D.clip, .{});
pub const font = bridge.accessor(CanvasRenderingContext2D.getFont, CanvasRenderingContext2D.setFont, .{});
pub const textAlign = bridge.accessor(CanvasRenderingContext2D.getTextAlign, CanvasRenderingContext2D.setTextAlign, .{});
pub const textBaseline = bridge.accessor(CanvasRenderingContext2D.getTextBaseline, CanvasRenderingContext2D.setTextBaseline, .{});
pub const fillText = bridge.function(CanvasRenderingContext2D.fillText, .{});
pub const strokeText = bridge.function(CanvasRenderingContext2D.strokeText, .{});
};
const testing = @import("../../../testing.zig");
test "WebApi: CanvasRenderingContext2D" {
try testing.htmlRunner("canvas/canvas_rendering_context_2d.html", .{});
}

View File

@@ -0,0 +1,218 @@
// 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 js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
pub fn registerTypes() []const type {
return &.{
WebGLRenderingContext,
// Extension types should be runtime generated. We might want
// to revisit this.
Extension.Type.WEBGL_debug_renderer_info,
Extension.Type.WEBGL_lose_context,
};
}
const WebGLRenderingContext = @This();
/// 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 {
_: u8 = 0,
pub const UNMASKED_VENDOR_WEBGL: u64 = 0x9245;
pub const UNMASKED_RENDERER_WEBGL: u64 = 0x9246;
pub fn getUnmaskedVendorWebGL(_: *const WEBGL_debug_renderer_info) u64 {
return UNMASKED_VENDOR_WEBGL;
}
pub fn getUnmaskedRendererWebGL(_: *const WEBGL_debug_renderer_info) u64 {
return UNMASKED_RENDERER_WEBGL;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(WEBGL_debug_renderer_info);
pub const Meta = struct {
pub const name = "WEBGL_debug_renderer_info";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const UNMASKED_VENDOR_WEBGL = bridge.accessor(WEBGL_debug_renderer_info.getUnmaskedVendorWebGL, null, .{});
pub const UNMASKED_RENDERER_WEBGL = bridge.accessor(WEBGL_debug_renderer_info.getUnmaskedRendererWebGL, null, .{});
};
};
pub const WEBGL_lose_context = struct {
_: u8 = 0,
pub fn loseContext(_: *const WEBGL_lose_context) void {}
pub fn restoreContext(_: *const WEBGL_lose_context) void {}
pub const JsApi = struct {
pub const bridge = js.Bridge(WEBGL_lose_context);
pub const Meta = struct {
pub const name = "WEBGL_lose_context";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const loseContext = bridge.function(WEBGL_lose_context.loseContext, .{});
pub const restoreContext = bridge.function(WEBGL_lose_context.restoreContext, .{});
};
};
};
};
/// This actually takes "GLenum" which, in fact, is a fancy way to say number.
/// Return value also depends on what's being passed as `pname`; we don't really
/// support any though.
pub fn getParameter(_: *const WebGLRenderingContext, pname: u32) []const u8 {
_ = pname;
return "";
}
/// Enables a WebGL extension.
pub fn getExtension(_: *const WebGLRenderingContext, name: []const u8, page: *Page) !?Extension {
const tag = Extension.find(name) orelse return null;
return switch (tag) {
.WEBGL_debug_renderer_info => {
const info = try page._factory.create(Extension.Type.WEBGL_debug_renderer_info{});
return .{ .WEBGL_debug_renderer_info = info };
},
.WEBGL_lose_context => {
const ctx = try page._factory.create(Extension.Type.WEBGL_lose_context{});
return .{ .WEBGL_lose_context = ctx };
},
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);
}
pub const JsApi = struct {
pub const bridge = js.Bridge(WebGLRenderingContext);
pub const Meta = struct {
pub const name = "WebGLRenderingContext";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const getParameter = bridge.function(WebGLRenderingContext.getParameter, .{});
pub const getExtension = bridge.function(WebGLRenderingContext.getExtension, .{});
pub const getSupportedExtensions = bridge.function(WebGLRenderingContext.getSupportedExtensions, .{});
};
const testing = @import("../../../testing.zig");
test "WebApi: WebGLRenderingContext" {
try testing.htmlRunner("canvas/webgl_rendering_context.html", .{});
}

View File

@@ -189,9 +189,9 @@ pub fn forEach(self: *DOMTokenList, cb_: js.Function, js_this_: ?js.Object, page
if (gop.found_existing) {
continue;
}
var result: js.Function.Result = undefined;
cb.tryCall(void, .{ token, i, self }, &result) catch {
log.debug(.js, "forEach callback", .{ .err = result.exception, .stack = result.stack, .source = "DOMTokenList" });
var caught: js.TryCatch.Caught = undefined;
cb.tryCall(void, .{ token, i, self }, &caught) catch {
log.debug(.js, "forEach callback", .{ .caught = caught, .source = "DOMTokenList" });
return;
};
i += 1;

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