226 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
Karl Seguin
060afcd459 Merge pull request #1313 from lightpanda-io/nikneym/xml-parsing
Support XML parsing
2026-01-08 08:42:21 +08:00
Karl Seguin
5d1522a61f Don't dispatch to listeners added during dispatching
Use the last current listener as a sentinel, so that any listener added during
dispatching can be skipped.
2026-01-08 08:39:49 +08:00
Karl Seguin
b1b54afc56 Merge pull request #1335 from lightpanda-io/build
Embed v8 snapshot in builds
2026-01-08 06:54:38 +08:00
Pierre Tachoire
2abc490732 ci: support build on tag push 2026-01-07 21:11:20 +01:00
Pierre Tachoire
d4807df2e9 add v8 snapshot instructions into the README 2026-01-07 17:22:50 +01:00
Pierre Tachoire
d5f4ca15cc add v8 snapshot in build processes 2026-01-07 17:22:49 +01:00
Pierre Tachoire
e642c85ebd use ReleaseFast build 2026-01-07 17:22:49 +01:00
Muki Kiboigo
3930524bbf use tokenizeAny instead of tokenizeScalar in Selector 2026-01-07 06:12:49 -08:00
Halil Durak
7ea0cdba36 update DomParser test 2026-01-07 14:37:44 +03:00
Halil Durak
612b3a26b7 allow other XML MIMEs in parseFromString 2026-01-07 14:37:44 +03:00
Halil Durak
56d89895a8 initial XML parsing support in DOMParser 2026-01-07 14:37:43 +03:00
Karl Seguin
21d502b81f Merge pull request #1326 from lightpanda-io/wp/mrdimidim/use-css-tokenizer
Use css tokenizer for parsing style attrs
2026-01-07 18:09:06 +08:00
Karl Seguin
dd3de6efea Merge pull request #1327 from lightpanda-io/zigdom-cdata-length
Fix `dom/nodes/CharacterData-appendData` WPT
2026-01-07 18:04:05 +08:00
Karl Seguin
d934fe6d4e Merge pull request #1330 from lightpanda-io/zigdom-element-get-by-class-name-fix
fix `dom/nodes/Element-getElementsByClassName` wpt
2026-01-07 18:02:50 +08:00
Karl Seguin
dab6345885 dispatch events with proper this 2026-01-07 17:57:34 +08:00
Karl Seguin
39874137d6 Merge pull request #1333 from lightpanda-io/attribute_removeNamedItem
add attribute.removeNamedItem
2026-01-07 17:47:33 +08:00
Karl Seguin
89f215c3ee Merge pull request #1332 from lightpanda-io/getElementById
getElementById duplicate-id handling
2026-01-07 17:47:19 +08:00
Karl Seguin
408d3f0a53 Track owning documents for nodes which aren't the default document
Track this in a lookup on the page, to avoid having to store a pointer for
_every_ node, given that most nodes _are_ owned by the document.

This helps us ensure nodes can be properly adopted.
2026-01-07 17:46:09 +08:00
Karl Seguin
a010684ce9 Add deprecated Node constants
Remove toString where the [new] auto-generated toString symbol works.

Reject node mutation on attributes.
2026-01-07 17:36:26 +08:00
Karl Seguin
a4a98da4a4 add attribute.removeNamedItem 2026-01-07 16:51:12 +08:00
Karl Seguin
6f30d459d5 getElementById duplicate-id handling
If 2 elements have the same id then,
1 - The first in document-order has to be retrieved. We were ordering by
    insertion order.

2 - When the element is removed, then document.getElementById should return the
    next element with that id in document-order. We were returning null
2026-01-07 15:49:17 +08:00
Muki Kiboigo
622ca3121f add case insensitivity support to selector parsing 2026-01-06 23:31:34 -08:00
Muki Kiboigo
71f27a55e1 fix duping of string for getElementsByClassName 2026-01-06 23:07:32 -08:00
Karl Seguin
c92903aae5 Merge pull request #1328 from lightpanda-io/set_outerHTML
add outerHTML setter
2026-01-07 15:04:14 +08:00
Karl Seguin
518e0aa07a add outerHTML setter 2026-01-07 14:49:30 +08:00
Nikolay Govorov
b908b0bf8a Used css tokenizer for parse html attributes 2026-01-07 05:46:18 +00:00
Muki Kiboigo
f9fa5be324 count utf8 codepoints for CData getLength 2026-01-06 21:29:15 -08:00
Karl Seguin
8ec6bb1577 Merge pull request #1322 from lightpanda-io/input_clone
Add 'clone' callback to build, implement for Input
2026-01-07 13:08:20 +08:00
Karl Seguin
70f8c53703 add deprecated properties to Event and improve initEvent 2026-01-07 12:05:11 +08:00
Karl Seguin
6d5a984413 Merge pull request #1323 from lightpanda-io/document_title
Improve document.title getter
2026-01-07 10:42:03 +08:00
Karl Seguin
5fa8fbc6f8 Merge pull request #1324 from lightpanda-io/elements_by_name_nodelist
getElementsByName now returns a NodeList rather than an HTMLCollection
2026-01-07 10:41:54 +08:00
Karl Seguin
7050d5fc68 Merge pull request #1325 from lightpanda-io/tokenlist_treewalker
support element.relList and improve TreeWalker
2026-01-07 10:41:42 +08:00
Karl Seguin
6af9d12f71 support element.relList and improve TreeWalker 2026-01-07 10:34:47 +08:00
Karl Seguin
a54e1db784 getElementsByName now returns a NodeList rather than an HTMLCollection
Auto-implement a toString accessor for any type that has a JsApi.Meta.name
2026-01-07 09:17:51 +08:00
Karl Seguin
2319b0fda5 Improve document.title getter
Collapse whitespace and find the first title, no matter where it is.
2026-01-07 07:52:20 +08:00
Karl Seguin
6864a22721 fix datetime-local input type 2026-01-07 07:39:42 +08:00
Karl Seguin
c9d0e2097d add input indeterminate accessor 2026-01-07 07:35:24 +08:00
Karl Seguin
d8f7eb3f24 Add 'clone' callback to build, implement for Input 2026-01-07 07:29:43 +08:00
Karl Seguin
90ee919f45 Merge pull request #1321 from lightpanda-io/event-init
set _time_stamp in the Event factory
2026-01-07 07:14:34 +08:00
Karl Seguin
ddc6431720 Merge pull request #1316 from lightpanda-io/reject_non_new_constructor
Reject constructors called as function (i.e. without 'new')
2026-01-07 07:14:13 +08:00
Pierre Tachoire
2ea6557fb7 add initEvent into Factory
and remove default value for Event._time_stamp
2026-01-06 15:30:13 +01:00
Karl Seguin
15358c1928 Improve Range, adding missing functions and more validation 2026-01-06 20:27:16 +08:00
Karl Seguin
d65025b3cb Merge pull request #1320 from lightpanda-io/fix-replaceChildren
remove children from previous parent
2026-01-06 19:07:15 +08:00
Pierre Tachoire
54fa3bc054 remove children from previous parent 2026-01-06 11:52:47 +01:00
Pierre Tachoire
68f5fa738c remove dead code Page._appendNode 2026-01-06 11:49:13 +01:00
Karl Seguin
2ea57ba979 update v8 dep 2026-01-06 18:30:25 +08:00
Karl Seguin
1acc0b0dc8 Merge pull request #1310 from lightpanda-io/zigdom-named-access
Named Access on the Window Object
2026-01-06 18:07:43 +08:00
Karl Seguin
645ec79fce access page from context, document call_depth usage 2026-01-06 18:04:17 +08:00
Karl Seguin
97e897e80e Merge pull request #1318 from lightpanda-io/node-self-replace
handle Node self replacement in insertBefore
2026-01-06 17:23:12 +08:00
Karl Seguin
6f72eeae65 Merge pull request #1319 from lightpanda-io/script_list_cleanup
Handle immediate call to Script.errorCallback
2026-01-06 17:22:14 +08:00
Karl Seguin
a845b2e35e Handle immediate call to Script.errorCallback
It's possible for Script.errorCallback to be called as part of the call to
`client.request`. This happens because we eagerly pump the libcurl message loop
to get the request going ASAP. For very obvious failures (e.g. an invalid URL)
this means that the error callback can be called from `client.request`.

Previously, we were only adding the script to its list _after_ the call to
`client.request`, but the error handler tries to remove the script from the list
.

This commit changes the order so that the script is first added to the list
and then the request is made.
2026-01-06 17:03:27 +08:00
Pierre Tachoire
b164ffeb95 handle Node self replacement in insertBefore 2026-01-06 09:49:08 +01:00
Karl Seguin
7ba34af884 Merge pull request #1317 from lightpanda-io/zigValueToJs-opts-pass-down
pass down opts to zigValueToJs
2026-01-06 16:36:44 +08:00
Pierre Tachoire
7f543ac7c8 pass down opts to zigValueToJs 2026-01-06 09:35:38 +01:00
Karl Seguin
a1bf92c07f Reject constructors called as function (i.e. without 'new')
Previously, `MessageEvent('')` would have been allowed, but invalid. This caused
problems as the receiver was the window. All such calls are now rejected.

Depends on https://github.com/lightpanda-io/zig-v8-fork/pull/131
2026-01-06 16:03:37 +08:00
Karl Seguin
0b221615b7 Merge pull request #1315 from lightpanda-io/replaceChild-itself
fix Node.replaceChild when of new child equals old
2026-01-06 08:00:03 +08:00
Pierre Tachoire
f81a9b54a7 fix Node.replaceChild when of new child equals old 2026-01-05 21:48:59 +01:00
Muki Kiboigo
05da040ce1 increment call_depth on callWithThis 2026-01-05 09:27:17 -08:00
Muki Kiboigo
b911051842 add named access shadowing test 2026-01-05 09:08:57 -08:00
Muki Kiboigo
a67f46b550 add named access on the Window object 2026-01-05 08:41:42 -08:00
213 changed files with 12748 additions and 4279 deletions

View File

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

View File

@@ -5,8 +5,12 @@ env:
AWS_SECRET_ACCESS_KEY: ${{ secrets.NIGHTLY_BUILD_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.NIGHTLY_BUILD_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.NIGHTLY_BUILD_AWS_BUCKET }} AWS_BUCKET: ${{ vars.NIGHTLY_BUILD_AWS_BUCKET }}
AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }} AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }}
RELEASE: ${{ github.ref_type == 'tag' && github.ref_name || 'nightly' }}
on: on:
push:
tags:
- '*'
schedule: schedule:
- cron: "2 2 * * *" - cron: "2 2 * * *"
@@ -23,7 +27,7 @@ jobs:
OS: linux OS: linux
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
timeout-minutes: 15 timeout-minutes: 20
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
@@ -38,8 +42,11 @@ jobs:
arch: ${{env.ARCH}} arch: ${{env.ARCH}}
mode: 'release' mode: 'release'
- name: v8 snapshot
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
- name: zig build - name: zig build
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseSafe -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary - name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }} run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -54,7 +61,8 @@ jobs:
with: with:
allowUpdates: true allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }} artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: nightly tag: ${{ env.RELEASE }}
makeLatest: true
build-linux-aarch64: build-linux-aarch64:
env: env:
@@ -62,7 +70,7 @@ jobs:
OS: linux OS: linux
runs-on: ubuntu-22.04-arm runs-on: ubuntu-22.04-arm
timeout-minutes: 15 timeout-minutes: 20
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -77,8 +85,11 @@ jobs:
arch: ${{env.ARCH}} arch: ${{env.ARCH}}
mode: 'release' mode: 'release'
- name: v8 snapshot
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
- name: zig build - name: zig build
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseSafe -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary - name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }} run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -93,7 +104,8 @@ jobs:
with: with:
allowUpdates: true allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }} artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: nightly tag: ${{ env.RELEASE }}
makeLatest: true
build-macos-aarch64: build-macos-aarch64:
env: env:
@@ -103,7 +115,7 @@ jobs:
# macos-14 runs on arm CPU. see # macos-14 runs on arm CPU. see
# https://github.com/actions/runner-images?tab=readme-ov-file # https://github.com/actions/runner-images?tab=readme-ov-file
runs-on: macos-14 runs-on: macos-14
timeout-minutes: 15 timeout-minutes: 20
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -118,8 +130,11 @@ jobs:
arch: ${{env.ARCH}} arch: ${{env.ARCH}}
mode: 'release' mode: 'release'
- name: v8 snapshot
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
- name: zig build - name: zig build
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary - name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }} run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -134,7 +149,8 @@ jobs:
with: with:
allowUpdates: true allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }} artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: nightly tag: ${{ env.RELEASE }}
makeLatest: true
build-macos-x86_64: build-macos-x86_64:
env: env:
@@ -142,7 +158,7 @@ jobs:
OS: macos OS: macos
runs-on: macos-14-large runs-on: macos-14-large
timeout-minutes: 15 timeout-minutes: 20
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -157,8 +173,11 @@ jobs:
arch: ${{env.ARCH}} arch: ${{env.ARCH}}
mode: 'release' mode: 'release'
- name: v8 snapshot
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
- name: zig build - name: zig build
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary - name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }} run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -173,4 +192,5 @@ jobs:
with: with:
allowUpdates: true allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }} artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: nightly tag: ${{ env.RELEASE }}
makeLatest: true

View File

@@ -232,3 +232,19 @@ jobs:
- name: format and send json result - name: format and send json result
run: /perf-fmt hyperfine ${{ github.sha }} hyperfine.json 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 - uses: ./.github/actions/install
- name: json output - name: build wpt
run: zig build wpt -- --json > wpt.json 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 - name: write commit
run: | run: |

View File

@@ -38,52 +38,6 @@ on:
workflow_dispatch: workflow_dispatch:
jobs: 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: zig-test:
name: zig test name: zig test
timeout-minutes: 15 timeout-minutes: 15
@@ -103,7 +57,7 @@ jobs:
- uses: ./.github/actions/install - uses: ./.github/actions/install
- name: zig build test - 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 - name: write commit
run: | run: |

View File

@@ -3,7 +3,7 @@ FROM debian:stable-slim
ARG MINISIG=0.12 ARG MINISIG=0.12
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
ARG V8=14.0.365.4 ARG V8=14.0.365.4
ARG ZIG_V8=v0.1.37 ARG ZIG_V8=v0.2.4
ARG TARGETPLATFORM ARG TARGETPLATFORM
RUN apt-get update -yq && \ RUN apt-get update -yq && \
@@ -48,8 +48,16 @@ RUN case $TARGETPLATFORM in \
mkdir -p v8/ && \ mkdir -p v8/ && \
mv libc_v8.a v8/libc_v8.a mv libc_v8.a v8/libc_v8.a
# build v8 snapshot
RUN zig build -Doptimize=ReleaseFast \
-Dprebuilt_v8_path=v8/libc_v8.a \
snapshot_creator -- src/snapshot.bin
# build release # build release
RUN zig build -Doptimize=ReleaseFast -Dprebuilt_v8_path=v8/libc_v8.a -Dgit_commit=$(git rev-parse --short HEAD) RUN zig build -Doptimize=ReleaseFast \
-Dsnapshot_path=../../snapshot.bin \
-Dprebuilt_v8_path=v8/libc_v8.a \
-Dgit_commit=$(git rev-parse --short HEAD)
FROM debian:stable-slim FROM debian:stable-slim

View File

@@ -47,12 +47,18 @@ help:
# $(ZIG) commands # $(ZIG) commands
# ------------ # ------------
.PHONY: build build-dev run run-release shell test bench wpt data end2end .PHONY: build build-v8-snapshot build-dev run run-release shell test bench wpt data end2end
## Build in release-safe mode ## Build v8 snapshot
build: build-v8-snapshot:
@printf "\033[36mBuilding v8 snapshot (release safe)...\033[0m\n"
@$(ZIG) build -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
@printf "\033[33mBuild OK\033[0m\n"
## Build in release-fast mode
build: build-v8-snapshot
@printf "\033[36mBuilding (release safe)...\033[0m\n" @printf "\033[36mBuilding (release safe)...\033[0m\n"
@$(ZIG) build -Doptimize=ReleaseSafe -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;) @$(ZIG) build -Doptimize=ReleaseFast -Dsnapshot_path=../../snapshot.bin -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
@printf "\033[33mBuild OK\033[0m\n" @printf "\033[33mBuild OK\033[0m\n"
## Build in debug mode ## Build in debug mode

View File

@@ -211,6 +211,23 @@ env.
But you can directly use the zig command: `zig build run`. But you can directly use the zig command: `zig build run`.
#### Embed v8 snapshot
Lighpanda uses v8 snapshot. By default, it is created on startup but you can
embed it by using the following commands:
Generate the snapshot.
```
zig build snapshot_creator -- src/snapshot.bin
```
Build using the snapshot binary.
```
zig build -Dsnapshot_path=../../snapshot.bin
```
See [#1279](https://github.com/lightpanda-io/browser/pull/1279) for more details.
## Test ## Test
### Unit Tests ### Unit Tests

View File

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

View File

@@ -6,10 +6,10 @@
.minimum_zig_version = "0.15.2", .minimum_zig_version = "0.15.2",
.dependencies = .{ .dependencies = .{
.v8 = .{ .v8 = .{
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/0d64a3d5b36ac94067df3e13fddbf715caa6f391.tar.gz", .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/v0.2.4.tar.gz",
.hash = "v8-0.0.0-xddH65sfBAC8o3q41YxhOms5uY2fvMzBrsgN8IeCXZgE", .hash = "v8-0.0.0-xddH66YvBAD0YI9xr6F0Xgnw9wN30FdZ10FLyuoV3e66",
}, },
//.v8 = .{ .path = "../zig-v8-fork" }, // .v8 = .{ .path = "../zig-v8-fork" },
.@"boringssl-zig" = .{ .@"boringssl-zig" = .{
.url = "git+https://github.com/Syndica/boringssl-zig.git#c53df00d06b02b755ad88bbf4d1202ed9687b096", .url = "git+https://github.com/Syndica/boringssl-zig.git#c53df00d06b02b755ad88bbf4d1202ed9687b096",
.hash = "boringssl-0.1.0-VtJeWehMAAA4RNnwRnzEvKcS9rjsR1QVRw1uJrwXxmVK", .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(); app.platform = try Platform.init();
errdefer app.platform.deinit(); errdefer app.platform.deinit();
app.snapshot = try Snapshot.load(allocator); app.snapshot = try Snapshot.load();
errdefer app.snapshot.deinit(allocator); errdefer app.snapshot.deinit();
app.app_dir_path = getAndMakeAppDir(allocator); app.app_dir_path = getAndMakeAppDir(allocator);
@@ -112,7 +112,7 @@ pub fn deinit(self: *App) void {
self.telemetry.deinit(); self.telemetry.deinit();
self.notification.deinit(); self.notification.deinit();
self.http.deinit(); self.http.deinit();
self.snapshot.deinit(allocator); self.snapshot.deinit();
self.platform.deinit(); self.platform.deinit();
allocator.destroy(self); allocator.destroy(self);

View File

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

View File

@@ -40,7 +40,8 @@ arena: Allocator,
listener_pool: std.heap.MemoryPool(Listener), listener_pool: std.heap.MemoryPool(Listener),
list_pool: std.heap.MemoryPool(std.DoublyLinkedList), list_pool: std.heap.MemoryPool(std.DoublyLinkedList),
lookup: std.AutoHashMapUnmanaged(usize, *std.DoublyLinkedList), lookup: std.AutoHashMapUnmanaged(usize, *std.DoublyLinkedList),
dispatch_depth: u32 = 0, dispatch_depth: usize,
deferred_removals: std.ArrayList(struct { list: *std.DoublyLinkedList, listener: *Listener }),
pub fn init(page: *Page) EventManager { pub fn init(page: *Page) EventManager {
return .{ return .{
@@ -50,6 +51,7 @@ pub fn init(page: *Page) EventManager {
.list_pool = std.heap.MemoryPool(std.DoublyLinkedList).init(page.arena), .list_pool = std.heap.MemoryPool(std.DoublyLinkedList).init(page.arena),
.listener_pool = std.heap.MemoryPool(Listener).init(page.arena), .listener_pool = std.heap.MemoryPool(Listener).init(page.arena),
.dispatch_depth = 0, .dispatch_depth = 0,
.deferred_removals = .{},
}; };
} }
@@ -100,8 +102,8 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, call
} }
const func = switch (callback) { const func = switch (callback) {
.function => |f| Function{ .value = f }, .function => |f| Function{ .value = try f.persist() },
.object => |o| Function{ .object = o }, .object => |o| Function{ .object = try o.persist() },
}; };
const listener = try self.listener_pool.create(); const listener = try self.listener_pool.create();
@@ -119,9 +121,6 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, call
} }
pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, use_capture: bool) void { pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, use_capture: bool) void {
if (comptime IS_DEBUG) {
log.debug(.event, "eventManager.remove", .{ .type = typ, .capture = use_capture, .target = target });
}
const list = self.lookup.get(@intFromPtr(target)) orelse return; const list = self.lookup.get(@intFromPtr(target)) orelse return;
if (findListener(list, typ, callback, use_capture)) |listener| { if (findListener(list, typ, callback, use_capture)) |listener| {
self.removeListener(list, listener); self.removeListener(list, listener);
@@ -186,7 +185,7 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
if (function_) |func| { if (function_) |func| {
event._current_target = target; event._current_target = target;
if (func.call(void, .{event})) { if (func.callWithThis(void, target, .{event})) {
was_dispatched = true; was_dispatched = true;
} else |err| { } else |err| {
// a non-JS error // a non-JS error
@@ -299,38 +298,53 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
const page = self.page; const page = self.page;
const typ = event._type_string; const typ = event._type_string;
// Track that we're dispatching to prevent immediate removal // Track dispatch depth for deferred removal
self.dispatch_depth += 1; self.dispatch_depth += 1;
defer { defer {
self.dispatch_depth -= 1; const dispatch_depth = self.dispatch_depth;
// Clean up any marked listeners in this target's list after this phase // Only destroy deferred listeners when we exit the outermost dispatch
// We do this regardless of depth to handle cross-target removals correctly if (dispatch_depth == 1) {
self.cleanupMarkedListeners(list); for (self.deferred_removals.items) |removal| {
removal.list.remove(&removal.listener.node);
self.listener_pool.destroy(removal.listener);
}
self.deferred_removals.clearRetainingCapacity();
} else {
self.dispatch_depth = dispatch_depth - 1;
}
} }
// Use the last listener in the list as sentinel - listeners added during dispatch will be after it
const last_node = list.last orelse return;
const last_listener: *Listener = @alignCast(@fieldParentPtr("node", last_node));
// Iterate through the list, stopping after we've encountered the last_listener
var node = list.first; var node = list.first;
var is_done = false;
while (node) |n| { while (node) |n| {
// do this now, in case we need to remove n (once: true or aborted signal) if (is_done) {
node = n.next; break;
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
// Skip listeners that were marked for removal
if (listener.marked_for_removal) {
continue;
} }
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
is_done = (listener == last_listener);
node = n.next;
// Skip non-matching listeners
if (!listener.typ.eql(typ)) { if (!listener.typ.eql(typ)) {
continue; continue;
} }
// Can be null when dispatching to the target itself
if (comptime capture_only) |capture| { if (comptime capture_only) |capture| {
if (listener.capture != capture) { if (listener.capture != capture) {
continue; continue;
} }
} }
// Skip removed listeners
if (listener.removed) {
continue;
}
// If the listener has an aborted signal, remove it and skip // If the listener has an aborted signal, remove it and skip
if (listener.signal) |signal| { if (listener.signal) |signal| {
if (signal.getAborted()) { if (signal.getAborted()) {
@@ -339,6 +353,11 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
} }
} }
// Remove "once" listeners BEFORE calling them so nested dispatches don't see them
if (listener.once) {
self.removeListener(list, listener);
}
was_handled.* = true; was_handled.* = true;
event._current_target = current_target; event._current_target = current_target;
@@ -349,12 +368,13 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
} }
switch (listener.function) { switch (listener.function) {
.value => |value| try value.call(void, .{event}), .value => |value| try value.local().callWithThis(void, current_target, .{event}),
.string => |string| { .string => |string| {
const str = try page.call_arena.dupeZ(u8, string.str()); const str = try page.call_arena.dupeZ(u8, string.str());
try self.page.js.eval(str, null); try self.page.js.eval(str, null);
}, },
.object => |obj| { .object => |*obj_global| {
const obj = obj_global.local();
if (try obj.getFunction("handleEvent")) |handleEvent| { if (try obj.getFunction("handleEvent")) |handleEvent| {
try handleEvent.callWithThis(void, obj, .{event}); try handleEvent.callWithThis(void, obj, .{event});
} }
@@ -366,10 +386,6 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
event._target = original_target; event._target = original_target;
} }
if (listener.once) {
self.removeListener(list, listener);
}
if (event._stop_immediate_propagation) { if (event._stop_immediate_propagation) {
return; return;
} }
@@ -382,29 +398,17 @@ fn dispatchAll(self: *EventManager, list: *std.DoublyLinkedList, current_target:
} }
fn removeListener(self: *EventManager, list: *std.DoublyLinkedList, listener: *Listener) void { fn removeListener(self: *EventManager, list: *std.DoublyLinkedList, listener: *Listener) void {
// If we're in a dispatch, defer removal to avoid invalidating iteration
if (self.dispatch_depth > 0) { if (self.dispatch_depth > 0) {
// We're in the middle of dispatching, just mark for removal listener.removed = true;
// This prevents invalidating the linked list during iteration self.deferred_removals.append(self.arena, .{ .list = list, .listener = listener }) catch unreachable;
listener.marked_for_removal = true;
} else { } else {
// Safe to remove immediately // Outside dispatch, remove immediately
list.remove(&listener.node); list.remove(&listener.node);
self.listener_pool.destroy(listener); self.listener_pool.destroy(listener);
} }
} }
fn cleanupMarkedListeners(self: *EventManager, list: *std.DoublyLinkedList) void {
var node = list.first;
while (node) |n| {
node = n.next;
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
if (listener.marked_for_removal) {
list.remove(&listener.node);
self.listener_pool.destroy(listener);
}
}
}
fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, callback: Callback, capture: bool) ?*Listener { fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, callback: Callback, capture: bool) ?*Listener {
var node = list.first; var node = list.first;
while (node) |n| { while (node) |n| {
@@ -436,24 +440,24 @@ const Listener = struct {
function: Function, function: Function,
signal: ?*@import("webapi/AbortSignal.zig") = null, signal: ?*@import("webapi/AbortSignal.zig") = null,
node: std.DoublyLinkedList.Node, node: std.DoublyLinkedList.Node,
marked_for_removal: bool = false, removed: bool = false,
}; };
const Function = union(enum) { const Function = union(enum) {
value: js.Function, value: js.Function.Global,
string: String, string: String,
object: js.Object, object: js.Object.Global,
fn eqlFunction(self: Function, func: js.Function) bool { fn eqlFunction(self: Function, func: js.Function) bool {
return switch (self) { return switch (self) {
.value => |v| return v.id == func.id, .value => |v| v.isEqual(func),
else => false, else => false,
}; };
} }
fn eqlObject(self: Function, obj: js.Object) bool { fn eqlObject(self: Function, obj: js.Object) bool {
return switch (self) { return switch (self) {
.object => |o| return o.getId() == obj.getId(), .object => |o| return o.isEqual(obj),
else => false, else => false,
}; };
} }

View File

@@ -17,10 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert;
const builtin = @import("builtin"); const builtin = @import("builtin");
const reflect = @import("reflect.zig"); const reflect = @import("reflect.zig");
const IS_DEBUG = builtin.mode == .Debug;
const log = @import("../log.zig"); const log = @import("../log.zig");
const String = @import("../string.zig").String; const String = @import("../string.zig").String;
@@ -38,6 +36,9 @@ const XMLHttpRequestEventTarget = @import("webapi/net/XMLHttpRequestEventTarget.
const Blob = @import("webapi/Blob.zig"); const Blob = @import("webapi/Blob.zig");
const AbstractRange = @import("webapi/AbstractRange.zig"); const AbstractRange = @import("webapi/AbstractRange.zig");
const IS_DEBUG = builtin.mode == .Debug;
const assert = std.debug.assert;
const Factory = @This(); const Factory = @This();
_page: *Page, _page: *Page,
_slab: SlabAllocator, _slab: SlabAllocator,
@@ -168,6 +169,18 @@ pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
return chain.get(1); return chain.get(1);
} }
fn eventInit(typ: []const u8, value: anytype, page: *Page) !Event {
// Round to 2ms for privacy (browsers do this)
const raw_timestamp = @import("../datetime.zig").milliTimestamp(.monotonic);
const time_stamp = (raw_timestamp / 2) * 2;
return .{
._type = unionInit(Event.Type, value),
._type_string = try String.init(page.arena, typ, .{}),
._time_stamp = time_stamp,
};
}
// this is a root object // this is a root object
pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) { pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) {
const allocator = self._slab.allocator(); const allocator = self._slab.allocator();
@@ -178,10 +191,7 @@ pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) {
// Special case: Event has a _type_string field, so we need manual setup // Special case: Event has a _type_string field, so we need manual setup
const event_ptr = chain.get(0); const event_ptr = chain.get(0);
event_ptr.* = .{ event_ptr.* = try eventInit(typ, chain.get(1), self._page);
._type = unionInit(Event.Type, chain.get(1)),
._type_string = try String.init(self._page.arena, typ, .{}),
};
chain.setLeaf(1, child); chain.setLeaf(1, child);
return chain.get(1); return chain.get(1);
@@ -196,10 +206,7 @@ pub fn uiEvent(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child)
// Special case: Event has a _type_string field, so we need manual setup // Special case: Event has a _type_string field, so we need manual setup
const event_ptr = chain.get(0); const event_ptr = chain.get(0);
event_ptr.* = .{ event_ptr.* = try eventInit(typ, chain.get(1), self._page);
._type = unionInit(Event.Type, chain.get(1)),
._type_string = try String.init(self._page.arena, typ, .{}),
};
chain.setMiddle(1, UIEvent.Type); chain.setMiddle(1, UIEvent.Type);
chain.setLeaf(2, child); chain.setLeaf(2, child);

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| { if (repeat_in_ms) |ms| {
// Task cannot be repeated immediately, and they should know that // 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; task.run_at = now + ms;
try self.low_priority.add(task); try self.low_priority.add(task);
} }

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const builtin = @import("builtin"); const builtin = @import("builtin");
const js = @import("js/js.zig"); const js = @import("js/js.zig");
@@ -239,8 +240,17 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
}; };
const is_blocking = script.mode == .normal; const is_blocking = script.mode == .normal;
if (is_blocking == false) {
self.scriptList(script).append(&script.node);
}
if (remote_url) |url| { if (remote_url) |url| {
errdefer script.deinit(true); errdefer {
if (is_blocking == false) {
self.scriptList(script).remove(&script.node);
}
script.deinit(true);
}
var headers = try self.client.newHeaders(); var headers = try self.client.newHeaders();
try page.requestCookie(.{}).headersForRequest(page.arena, url, &headers); try page.requestCookie(.{}).headersForRequest(page.arena, url, &headers);
@@ -271,8 +281,6 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
} }
if (is_blocking == false) { if (is_blocking == false) {
const list = self.scriptList(script);
list.append(&script.node);
return; return;
} }
@@ -477,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 // 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. // we know this so that we know that we can start evaluating deferred scripts.
pub fn staticScriptsDone(self: *ScriptManager) void { 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.static_scripts_done = true;
self.evaluate(); self.evaluate();
} }
@@ -668,7 +676,7 @@ pub const Script = struct {
// set `CURLOPT_SUPPRESS_CONNECT_HEADERS` and CONNECT to a proxy, this // set `CURLOPT_SUPPRESS_CONNECT_HEADERS` and CONNECT to a proxy, this
// will fail. This assertion exists to catch incorrect assumptions about // will fail. This assertion exists to catch incorrect assumptions about
// how libcurl works, or about how we've configured it. // 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(); var buffer = self.manager.buffer_pool.get();
if (transfer.getContentLength()) |cl| { if (transfer.getContentLength()) |cl| {
try buffer.ensureTotalCapacity(self.manager.allocator, cl); try buffer.ensureTotalCapacity(self.manager.allocator, cl);
@@ -743,10 +751,12 @@ pub const Script = struct {
fn eval(self: *Script, page: *Page) void { fn eval(self: *Script, page: *Page) void {
// never evaluated, source is passed back to v8, via callbacks. // 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. // never evaluated, source is passed back to v8 when asked for it.
std.debug.assert(self.mode != .import); std.debug.assert(self.mode != .import);
}
if (page.isGoingAway()) { if (page.isGoingAway()) {
// don't evaluate scripts for a dying page. // don't evaluate scripts for a dying page.
@@ -827,20 +837,19 @@ pub const Script = struct {
return; 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", .{ log.warn(.js, "eval script", .{
.url = url, .url = url,
.err = msg, .caught = caught,
.stack = try_catch.stack(page.call_arena) catch null,
.line = try_catch.sourceLineNumber() orelse 0,
.cacheable = cacheable, .cacheable = cacheable,
}); });
self.executeCallback("error", script_element._on_error, page); self.executeCallback("error", script_element._on_error, page);
} }
fn executeCallback(self: *const Script, comptime typ: []const u8, cb_: ?js.Function, page: *Page) void { fn executeCallback(self: *const Script, comptime typ: []const u8, cb_: ?js.Function.Global, page: *Page) void {
const cb = cb_ orelse return; const cb_global = cb_ orelse return;
const cb = cb_global.local();
const Event = @import("webapi/Event.zig"); const Event = @import("webapi/Event.zig");
const event = Event.initTrusted(typ, .{}, page) catch |err| { const event = Event.initTrusted(typ, .{}, page) catch |err| {
@@ -852,13 +861,12 @@ pub const Script = struct {
return; return;
}; };
var result: js.Function.Result = undefined; var caught: js.TryCatch.Caught = undefined;
cb.tryCall(void, .{event}, &result) catch { cb.tryCall(void, .{event}, &caught) catch {
log.warn(.js, "script callback", .{ log.warn(.js, "script callback", .{
.url = self.url, .url = self.url,
.type = typ, .type = typ,
.err = result.exception, .caught = caught,
.stack = result.stack,
}); });
}; };
} }

View File

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

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ResolveOpts = struct { 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..], "../")) { 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) { if (out_i > path_marker) {
// go back before the / // 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;
}
};

295
src/browser/css/Parser.zig Normal file
View File

@@ -0,0 +1,295 @@
// 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 Tokenizer = @import("Tokenizer.zig");
pub const Declaration = struct {
name: []const u8,
value: []const u8,
important: bool,
};
const TokenSpan = struct {
token: Tokenizer.Token,
start: usize,
end: usize,
};
const TokenStream = struct {
tokenizer: Tokenizer,
peeked: ?TokenSpan = null,
fn init(input: []const u8) TokenStream {
return .{ .tokenizer = .{ .input = input } };
}
fn nextRaw(self: *TokenStream) ?TokenSpan {
const start = self.tokenizer.position;
const token = self.tokenizer.next() orelse return null;
const end = self.tokenizer.position;
return .{ .token = token, .start = start, .end = end };
}
fn next(self: *TokenStream) ?TokenSpan {
if (self.peeked) |token| {
self.peeked = null;
return token;
}
return self.nextRaw();
}
fn peek(self: *TokenStream) ?TokenSpan {
if (self.peeked == null) {
self.peeked = self.nextRaw();
}
return self.peeked;
}
};
pub fn parseDeclarationsList(input: []const u8) DeclarationsIterator {
return DeclarationsIterator.init(input);
}
pub const DeclarationsIterator = struct {
input: []const u8,
stream: TokenStream,
pub fn init(input: []const u8) DeclarationsIterator {
return .{
.input = input,
.stream = TokenStream.init(input),
};
}
pub fn next(self: *DeclarationsIterator) ?Declaration {
while (true) {
self.skipTriviaAndSemicolons();
const peeked = self.stream.peek() orelse return null;
switch (peeked.token) {
.at_keyword => {
_ = self.stream.next();
self.skipAtRule();
},
.ident => |name| {
_ = self.stream.next();
if (self.consumeDeclaration(name)) |declaration| {
return declaration;
}
},
else => {
_ = self.stream.next();
self.skipInvalidDeclaration();
},
}
}
return null;
}
fn consumeDeclaration(self: *DeclarationsIterator, name: []const u8) ?Declaration {
self.skipTrivia();
const colon = self.stream.next() orelse return null;
if (!isColon(colon.token)) {
self.skipInvalidDeclaration();
return null;
}
const value = self.consumeValue() orelse return null;
return .{
.name = name,
.value = value.value,
.important = value.important,
};
}
const ValueResult = struct {
value: []const u8,
important: bool,
};
fn consumeValue(self: *DeclarationsIterator) ?ValueResult {
self.skipTrivia();
var depth: usize = 0;
var start: ?usize = null;
var last_sig: ?TokenSpan = null;
var prev_sig: ?TokenSpan = null;
while (true) {
const peeked = self.stream.peek() orelse break;
if (isSemicolon(peeked.token) and depth == 0) {
_ = self.stream.next();
break;
}
const span = self.stream.next() orelse break;
if (isWhitespaceOrComment(span.token)) {
continue;
}
if (start == null) start = span.start;
prev_sig = last_sig;
last_sig = span;
updateDepth(span.token, &depth);
}
const value_start = start orelse return null;
const last = last_sig orelse return null;
var important = false;
var end_pos = last.end;
if (isImportantPair(prev_sig, last)) {
important = true;
const bang = prev_sig orelse return null;
if (value_start >= bang.start) return null;
end_pos = bang.start;
}
var value_slice = self.input[value_start..end_pos];
value_slice = std.mem.trim(u8, value_slice, &std.ascii.whitespace);
if (value_slice.len == 0) return null;
return .{ .value = value_slice, .important = important };
}
fn skipTrivia(self: *DeclarationsIterator) void {
while (self.stream.peek()) |peeked| {
if (!isWhitespaceOrComment(peeked.token)) break;
_ = self.stream.next();
}
}
fn skipTriviaAndSemicolons(self: *DeclarationsIterator) void {
while (self.stream.peek()) |peeked| {
if (isWhitespaceOrComment(peeked.token) or isSemicolon(peeked.token)) {
_ = self.stream.next();
} else {
break;
}
}
}
fn skipAtRule(self: *DeclarationsIterator) void {
var depth: usize = 0;
var saw_block = false;
while (true) {
const peeked = self.stream.peek() orelse return;
if (!saw_block and isSemicolon(peeked.token) and depth == 0) {
_ = self.stream.next();
return;
}
const span = self.stream.next() orelse return;
if (isWhitespaceOrComment(span.token)) continue;
if (isBlockStart(span.token)) {
depth += 1;
saw_block = true;
} else if (isBlockEnd(span.token)) {
if (depth > 0) depth -= 1;
if (saw_block and depth == 0) return;
}
}
}
fn skipInvalidDeclaration(self: *DeclarationsIterator) void {
var depth: usize = 0;
while (self.stream.peek()) |peeked| {
if (isSemicolon(peeked.token) and depth == 0) {
_ = self.stream.next();
return;
}
const span = self.stream.next() orelse return;
if (isWhitespaceOrComment(span.token)) continue;
updateDepth(span.token, &depth);
}
}
};
fn isWhitespaceOrComment(token: Tokenizer.Token) bool {
return switch (token) {
.white_space, .comment => true,
else => false,
};
}
fn isSemicolon(token: Tokenizer.Token) bool {
return switch (token) {
.semicolon => true,
else => false,
};
}
fn isColon(token: Tokenizer.Token) bool {
return switch (token) {
.colon => true,
else => false,
};
}
fn isBlockStart(token: Tokenizer.Token) bool {
return switch (token) {
.curly_bracket_block, .square_bracket_block, .parenthesis_block, .function => true,
else => false,
};
}
fn isBlockEnd(token: Tokenizer.Token) bool {
return switch (token) {
.close_curly_bracket, .close_parenthesis, .close_square_bracket => true,
else => false,
};
}
fn updateDepth(token: Tokenizer.Token, depth: *usize) void {
if (isBlockStart(token)) {
depth.* += 1;
return;
}
if (isBlockEnd(token)) {
if (depth.* > 0) depth.* -= 1;
}
}
fn isImportantPair(prev_sig: ?TokenSpan, last_sig: TokenSpan) bool {
if (!isIdentImportant(last_sig.token)) return false;
const prev = prev_sig orelse return false;
return isBang(prev.token);
}
fn isIdentImportant(token: Tokenizer.Token) bool {
return switch (token) {
.ident => |name| std.ascii.eqlIgnoreCase(name, "important"),
else => false,
};
}
fn isBang(token: Tokenizer.Token) bool {
return switch (token) {
.delim => |c| c == '!',
else => false,
};
}

View File

@@ -644,8 +644,10 @@ fn consumeNumeric(self: *Tokenizer) Token {
fn consumeUnquotedUrl(self: *Tokenizer) ?Token { fn consumeUnquotedUrl(self: *Tokenizer) ?Token {
// TODO: true url parser // TODO: true url parser
if (self.nextByte()) |it| { if (self.nextByte()) |it| {
self.consumeString(it == '\''); return self.consumeString(it == '\'');
} }
return null;
} }
fn consumeIdentLike(self: *Tokenizer) Token { fn consumeIdentLike(self: *Tokenizer) Token {

View File

@@ -21,18 +21,48 @@ const js = @import("js.zig");
const v8 = js.v8; const v8 = js.v8;
const Array = @This(); const Array = @This();
js_arr: v8.Array,
context: *js.Context, ctx: *js.Context,
handle: *const v8.Array,
pub fn len(self: Array) usize { 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 { pub fn get(self: Array, index: u32) !js.Value {
const idx_key = v8.Integer.initU32(self.context.isolate, @intCast(index)); const ctx = self.ctx;
const js_obj = self.js_arr.castTo(v8.Object);
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 .{ return .{
.context = self.context, .ctx = self.ctx,
.js_val = try js_obj.getValue(self.context.v8_context, idx_key.toValue()), .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,534 +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 {
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, platform: *const Platform,
// the global isolate // the global isolate
isolate: v8.Isolate, isolate: js.Isolate,
// just kept around because we need to free it on deinit // just kept around because we need to free it on deinit
isolate_params: *v8.CreateParams, isolate_params: *v8.CreateParams,
context_id: usize, 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 // 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 { pub fn init(allocator: Allocator, platform: *const Platform, snapshot: *Snapshot) !Env {
var params = try allocator.create(v8.CreateParams); var params = try allocator.create(v8.CreateParams);
errdefer allocator.destroy(params); 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.snapshot_blob = @ptrCast(&snapshot.startup_data);
params.array_buffer_allocator = v8.createDefaultArrayBufferAllocator(); params.array_buffer_allocator = v8.v8__ArrayBuffer__Allocator__NewDefaultAllocator().?;
errdefer v8.destroyArrayBufferAllocator(params.array_buffer_allocator.?); errdefer v8.v8__ArrayBuffer__Allocator__DELETE(params.array_buffer_allocator.?);
params.external_references = &snapshot.external_references; params.external_references = &snapshot.external_references;
var isolate = v8.Isolate.init(params); var isolate = js.Isolate.init(params);
errdefer isolate.deinit(); errdefer isolate.deinit();
// This is the callback that runs whenever a module is dynamically imported. v8.v8__Isolate__SetHostImportModuleDynamicallyCallback(isolate.handle, Context.dynamicModuleCallback);
isolate.setHostImportModuleDynamicallyCallback(Context.dynamicModuleCallback); v8.v8__Isolate__SetPromiseRejectCallback(isolate.handle, promiseRejectCallback);
isolate.setPromiseRejectCallback(promiseRejectCallback); v8.v8__Isolate__SetMicrotasksPolicy(isolate.handle, v8.kExplicit);
isolate.setMicrotasksPolicy(v8.c.kExplicit); v8.v8__Isolate__SetFatalErrorHandler(isolate.handle, fatalCallback);
v8.v8__Isolate__SetOOMErrorHandler(isolate.handle, oomCallback);
isolate.enter(); isolate.enter();
errdefer isolate.exit(); 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 // Allocate arrays dynamically to avoid comptime dependency on JsApis.len
const templates = try allocator.alloc(v8.FunctionTemplate, 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); errdefer allocator.free(templates);
{ {
var temp_scope: v8.HandleScope = undefined; var temp_scope: js.HandleScope = undefined;
v8.HandleScope.init(&temp_scope, isolate); temp_scope.init(isolate);
defer temp_scope.deinit(); defer temp_scope.deinit();
const context = v8.Context.init(isolate, null, null);
context.enter();
defer context.exit();
inline for (JsApis, 0..) |JsApi, i| { inline for (JsApis, 0..) |JsApi, i| {
JsApi.Meta.class_id = i; JsApi.Meta.class_id = i;
const data = context.getDataFromSnapshotOnce(snapshot.data_start + i); const data = v8.v8__Isolate__GetDataFromSnapshotOnce(isolate.handle, snapshot.data_start + i);
const function = v8.FunctionTemplate{ .handle = @ptrCast(data) }; const function_handle: *const v8.FunctionTemplate = @ptrCast(data);
templates[i] = v8.Persistent(v8.FunctionTemplate).init(isolate, function).castToFunctionTemplate(); // 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, .allocator = allocator,
.templates = templates, .templates = templates,
.isolate_params = params, .isolate_params = params,
.eternal_function_templates = eternal_function_templates,
}; };
} }
pub fn deinit(self: *Env) void { pub fn deinit(self: *Env) void {
self.allocator.free(self.templates);
self.allocator.free(self.eternal_function_templates);
self.isolate.exit(); self.isolate.exit();
self.isolate.deinit(); 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.destroy(self.isolate_params);
self.allocator.free(self.templates);
} }
pub fn newInspector(self: *Env, arena: Allocator, ctx: anytype) !Inspector { pub fn newInspector(self: *Env, arena: Allocator, ctx: anytype) !*Inspector {
return Inspector.init(arena, self.isolate, ctx); const inspector = try arena.create(Inspector);
try Inspector.init(inspector, self.isolate.handle, ctx);
return inspector;
} }
pub fn runMicrotasks(self: *const Env) void { 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 { 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 { 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 { pub fn newExecutionWorld(self: *Env) !ExecutionWorld {
return .{ return .{
@@ -147,8 +160,8 @@ pub fn newExecutionWorld(self: *Env) !ExecutionWorld {
// `lowMemoryNotification` call on the isolate to encourage v8 to free // `lowMemoryNotification` call on the isolate to encourage v8 to free
// any contexts which have been freed. // any contexts which have been freed.
pub fn lowMemoryNotification(self: *Env) void { pub fn lowMemoryNotification(self: *Env) void {
var handle_scope: v8.HandleScope = undefined; var handle_scope: js.HandleScope = undefined;
v8.HandleScope.init(&handle_scope, self.isolate); handle_scope.init(self.isolate);
defer handle_scope.deinit(); defer handle_scope.deinit();
self.isolate.lowMemoryNotification(); 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 }); , .{ 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 { fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) void {
const msg = v8.PromiseRejectMessage.initFromC(v8_msg); const promise_handle = v8.v8__PromiseRejectMessage__GetPromise(&message_handle).?;
const isolate = msg.getPromise().toObject().getIsolate(); const isolate_handle = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?;
const context = Context.fromIsolate(isolate); const js_isolate = js.Isolate{ .handle = isolate_handle };
const context = Context.fromIsolate(js_isolate);
const value = const value =
if (msg.getValue()) |v8_value| if (v8.v8__PromiseRejectMessage__GetValue(&message_handle)) |v8_value|
context.valueToString(v8_value, .{}) catch |err| @errorName(err) context.valueToString(.{ .ctx = context, .handle = v8_value }, .{}) catch |err| @errorName(err)
else else
"no value"; "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", .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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const IS_DEBUG = @import("builtin").mode == .Debug; const lp = @import("lightpanda");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const Page = @import("../Page.zig");
const js = @import("js.zig"); const js = @import("js.zig");
const v8 = js.v8; const v8 = js.v8;
const Env = @import("Env.zig"); const Env = @import("Env.zig");
const bridge = @import("bridge.zig");
const Context = @import("Context.zig"); const Context = @import("Context.zig");
const Page = @import("../Page.zig");
const ArenaAllocator = std.heap.ArenaAllocator; const ArenaAllocator = std.heap.ArenaAllocator;
const IS_DEBUG = @import("builtin").mode == .Debug;
const CONTEXT_ARENA_RETAIN = 1024 * 64; const CONTEXT_ARENA_RETAIN = 1024 * 64;
@@ -52,6 +53,7 @@ context_arena: ArenaAllocator,
// does all the work, but having all page-specific data structures // does all the work, but having all page-specific data structures
// grouped together helps keep things clean. // grouped together helps keep things clean.
context: ?Context = null, context: ?Context = null,
persisted_context: ?js.Global(Context) = null,
// no init, must be initialized via env.newExecutionWorld() // no init, must be initialized via env.newExecutionWorld()
@@ -59,56 +61,58 @@ pub fn deinit(self: *ExecutionWorld) void {
if (self.context != null) { if (self.context != null) {
self.removeContext(); self.removeContext();
} }
self.context_arena.deinit(); self.context_arena.deinit();
} }
// Only the top Context in the Main ExecutionWorld should hold a handle_scope. // 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) // v8 creates will be released (or at least, releasable by the v8 GC)
// when the handle_scope is freed. // when the handle_scope is freed.
// We also maintain our own "context_arena" which allows us to have // We also maintain our own "context_arena" which allows us to have
// all page related memory easily managed. // all page related memory easily managed.
pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context { 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 env = self.env;
const isolate = env.isolate; const isolate = env.isolate;
const arena = self.context_arena.allocator();
var v8_context: v8.Context = blk: { const persisted_context: js.Global(Context) = blk: {
var temp_scope: v8.HandleScope = undefined; var temp_scope: js.HandleScope = undefined;
v8.HandleScope.init(&temp_scope, isolate); temp_scope.init(isolate);
defer temp_scope.deinit(); defer temp_scope.deinit();
if (comptime IS_DEBUG) { // Getting this into the snapshot is tricky (anything involving the
// Getting this into the snapshot is tricky (anything involving the // global is tricky). Easier to do here
// global is tricky). Easier to do here, and in debug mode, we're const global_template = @import("Snapshot.zig").createGlobalTemplate(isolate.handle, env.templates);
// fine with paying the small perf hit. v8.v8__ObjectTemplate__SetNamedHandler(global_template, &.{
const js_global = v8.FunctionTemplate.initDefault(isolate); .getter = bridge.unknownPropertyCallback,
const global_template = js_global.getInstanceTemplate(); .setter = null,
.query = null,
.deleter = null,
.enumerator = null,
.definer = null,
.descriptor = null,
.data = null,
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
});
global_template.setNamedProperty(v8.NamedPropertyHandlerConfiguration{ const context_handle = v8.v8__Context__New(isolate.handle, global_template, null).?;
.getter = unknownPropertyCallback, break :blk js.Global(Context).init(isolate.handle, context_handle);
.flags = v8.PropertyHandlerFlags.NonMasking | v8.PropertyHandlerFlags.OnlyInterceptStrings,
}, null);
}
const context_local = v8.Context.init(isolate, null, null);
const v8_context = v8.Persistent(v8.Context).init(isolate, context_local).castToContext();
break :blk v8_context;
}; };
// 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. // 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 // 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 // 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) { if (enter) {
handle_scope = @as(v8.HandleScope, undefined); handle_scope = @as(js.HandleScope, undefined);
v8.HandleScope.init(&handle_scope.?, isolate); handle_scope.?.init(isolate);
v8_context.enter(); v8.v8__Context__Enter(v8_context);
} }
errdefer if (enter) { errdefer if (enter) {
v8_context.exit(); v8.v8__Context__Exit(v8_context);
handle_scope.?.deinit(); handle_scope.?.deinit();
}; };
@@ -119,33 +123,34 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context
.page = page, .page = page,
.id = context_id, .id = context_id,
.isolate = isolate, .isolate = isolate,
.v8_context = v8_context, .handle = v8_context,
.templates = env.templates, .templates = env.templates,
.handle_scope = handle_scope, .handle_scope = handle_scope,
.script_manager = &page._script_manager, .script_manager = &page._script_manager,
.call_arena = page.call_arena, .call_arena = page.call_arena,
.arena = self.context_arena.allocator(), .arena = arena,
}; };
self.persisted_context = persisted_context;
var context = &self.context.?; var context = &self.context.?;
// Store a pointer to our context inside the v8 context so that, given // Store a pointer to our context inside the v8 context so that, given
// a v8 context, we can get our context out // a v8 context, we can get our context out
const data = isolate.initBigIntU64(@intCast(@intFromPtr(context))); const data = isolate.initBigInt(@intFromPtr(context));
v8_context.setEmbedderData(1, data); v8.v8__Context__SetEmbedderData(context.handle, 1, @ptrCast(data.handle));
try context.setupGlobal(); try context.setupGlobal();
return context; return context;
} }
pub fn removeContext(self: *ExecutionWorld) void { pub fn removeContext(self: *ExecutionWorld) void {
// Force running the micro task to drain the queue before reseting the var context = &(self.context orelse return);
// context arena. context.deinit();
// 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();
self.context = null; 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 }); _ = self.context_arena.reset(.{ .retain_with_limit = CONTEXT_ARENA_RETAIN });
} }
@@ -156,42 +161,3 @@ pub fn terminateExecution(self: *const ExecutionWorld) void {
pub fn resumeExecution(self: *const ExecutionWorld) void { pub fn resumeExecution(self: *const ExecutionWorld) void {
self.env.isolate.cancelTerminateExecution(); 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 property = context.valueToString(.{ .handle = c_name.? }, .{}) catch "???";
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)) {
log.debug(.unknown_prop, "unkown global property", .{
.info = "but the property can exist in pure JS",
.stack = context.stackTrace() catch "???",
.property = property,
});
}
return v8.Intercepted.No;
}

View File

@@ -20,70 +20,49 @@ const std = @import("std");
const js = @import("js.zig"); const js = @import("js.zig");
const v8 = js.v8; const v8 = js.v8;
const PersistentFunction = v8.Persistent(v8.Function);
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Function = @This(); const Function = @This();
id: usize, ctx: *js.Context,
context: *js.Context, this: ?*const v8.Object = null,
this: ?v8.Object = null, handle: *const v8.Function,
func: PersistentFunction,
pub const Result = struct { pub const Result = struct {
stack: ?[]const u8, stack: ?[]const u8,
exception: []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 { pub fn withThis(self: *const Function, value: anytype) !Function {
const this_obj = if (@TypeOf(value) == js.Object) const this_obj = if (@TypeOf(value) == js.Object)
value.js_obj value.handle
else else
(try self.context.zigValueToJs(value, .{})).castTo(v8.Object); (try self.ctx.zigValueToJs(value, .{})).handle;
return .{ return .{
.id = self.id, .ctx = self.ctx,
.this = this_obj, .this = this_obj,
.func = self.func, .handle = self.handle,
.context = self.context,
}; };
} }
pub fn newInstance(self: *const Function, result: *Result) !js.Object { pub fn newInstance(self: *const Function, caught: *js.TryCatch.Caught) !js.Object {
const context = self.context; const ctx = self.ctx;
var try_catch: js.TryCatch = undefined; var try_catch: js.TryCatch = undefined;
try_catch.init(context); try_catch.init(ctx);
defer try_catch.deinit(); defer try_catch.deinit();
// This creates a new instance using this Function as a constructor. // This creates a new instance using this Function as a constructor.
// This returns a generic Object // const c_args = @as(?[*]const ?*c.Value, @ptrCast(&.{}));
const js_obj = self.func.castToFunction().initInstance(context.v8_context, &.{}) orelse { const handle = v8.v8__Function__NewInstance(self.handle, ctx.handle, 0, null) orelse {
if (try_catch.hasCaught()) { caught.* = try_catch.caughtOrError(ctx.call_arena, error.Unknown);
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 = "???";
}
return error.JsConstructorFailed; return error.JsConstructorFailed;
}; };
return .{ return .{
.context = context, .ctx = ctx,
.js_obj = js_obj, .handle = handle,
}; };
} }
@@ -91,77 +70,139 @@ pub fn call(self: *const Function, comptime T: type, args: anytype) !T {
return self.callWithThis(T, self.getThis(), args); return self.callWithThis(T, self.getThis(), args);
} }
pub fn tryCall(self: *const Function, comptime T: type, args: anytype, result: *Result) !T { pub fn tryCall(self: *const Function, comptime T: type, args: anytype, caught: *js.TryCatch.Caught) !T {
return self.tryCallWithThis(T, self.getThis(), args, result); 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; var try_catch: js.TryCatch = undefined;
try_catch.init(self.context);
try_catch.init(self.ctx);
defer try_catch.deinit(); defer try_catch.deinit();
return self.callWithThis(T, this, args) catch |err| { return self.callWithThis(T, this, args) catch |err| {
if (try_catch.hasCaught()) { caught.* = try_catch.caughtOrError(self.ctx.call_arena, err);
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);
}
return err; return err;
}; };
} }
pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T { pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T {
const context = self.context; const ctx = self.ctx;
const js_this = try context.valueToExistingObject(this); // When we're calling a function from within JavaScript itself, this isn't
// necessary. We're within a Caller instantiation, which will already have
// incremented the call_depth and it won't decrement it until the Caller is
// done.
// But some JS functions are initiated from Zig code, and not v8. For
// example, Observers, some event and window callbacks. In those cases, we
// 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 = ctx.call_depth;
ctx.call_depth = call_depth + 1;
defer ctx.call_depth = call_depth;
const js_this = blk: {
if (@TypeOf(this) == js.Object) {
break :blk this;
}
break :blk try ctx.zigValueToJs(this, .{});
};
const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args; 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: { .@"struct" => |s| blk: {
const fields = s.fields; 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| { 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; break :blk &cargs;
}, },
.pointer => blk: { .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| { for (args, 0..) |a, i| {
values[i] = try context.zigValueToJs(a, .{}); values[i] = (try ctx.zigValueToJs(a, .{})).handle;
} }
break :blk values; break :blk values;
}, },
else => @compileError("JS Function called with invalid paremter type"), else => @compileError("JS Function called with invalid paremter type"),
}; };
const result = self.func.castToFunction().call(context.v8_context, js_this, js_args); const c_args = @as(?[*]const ?*v8.Value, @ptrCast(js_args.ptr));
if (result == null) { 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 "???"}); // std.debug.print("CB ERR: {s}\n", .{self.src() catch "???"});
return error.JSExecCallback; return error.JSExecCallback;
} };
if (@typeInfo(T) == .void) return {}; if (@typeInfo(T) == .void) {
return context.jsValueToZig(T, result.?); return {};
}
return ctx.jsValueToZig(T, .{ .ctx = ctx, .handle = handle });
} }
fn getThis(self: *const Function) v8.Object { fn getThis(self: *const Function) js.Object {
return self.this orelse self.context.v8_context.getGlobal(); 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 { pub fn src(self: *const Function) ![]const u8 {
const value = self.func.castToFunction().toValue(); return self.context.valueToString(.{ .handle = @ptrCast(self.handle) }, .{});
return self.context.valueToString(value, .{});
} }
pub fn getPropertyValue(self: *const Function, name: []const u8) !?js.Value { pub fn getPropertyValue(self: *const Function, name: []const u8) !?js.Value {
const func_obj = self.func.castToFunction().toObject(); const ctx = self.ctx;
const key = v8.String.initUtf8(self.context.isolate, name); const key = ctx.isolate.initStringHandle(name);
const value = func_obj.getValue(self.context.v8_context, key) catch return null; const handle = v8.v8__Object__Get(self.handle, ctx.handle, key) orelse {
return self.context.createValue(value); 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 Context = @import("Context.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const RndGen = std.Random.DefaultPrng;
const CONTEXT_GROUP_ID = 1;
const CLIENT_TRUST_LEVEL = 1;
const Inspector = @This(); const Inspector = @This();
pub const RemoteObject = v8.RemoteObject; handle: *v8.Inspector,
isolate: *v8.Isolate,
isolate: v8.Isolate, client: Client,
inner: *v8.Inspector, channel: Channel,
session: v8.InspectorSession, session: Session,
rnd: RndGen = RndGen.init(0),
default_context: ?*const v8.Context = null,
// We expect allocator to be an arena // 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 ContextT = @TypeOf(ctx);
const InspectorContainer = switch (@typeInfo(ContextT)) { const Container = switch (@typeInfo(ContextT)) {
.@"struct" => ContextT, .@"struct" => ContextT,
.pointer => |ptr| ptr.child, .pointer => |ptr| ptr.child,
.void => NoopInspector, .void => NoopInspector,
else => @compileError("invalid context type"), else => @compileError("invalid context type"),
}; };
// If necessary, turn a void context into something we can safely ptrCast // If necessary, turn a void context into something we can safely ptrCast
const safe_context: *anyopaque = if (ContextT == void) @ptrCast(@constCast(&{})) else ctx; const safe_context: *anyopaque = if (ContextT == void) @ptrCast(@constCast(&{})) else ctx;
const channel = v8.InspectorChannel.init( // 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, safe_context,
InspectorContainer.onInspectorResponse, Container.onInspectorResponse,
InspectorContainer.onInspectorEvent, Container.onInspectorEvent,
InspectorContainer.onRunMessageLoopOnPause, Container.onRunMessageLoopOnPause,
InspectorContainer.onQuitMessageLoopOnPause, Container.onQuitMessageLoopOnPause,
isolate, isolate,
); );
self.channel = channel;
channel.setInspector(self);
const client = v8.InspectorClient.init(); // Create the session
const session_handle = v8.v8_inspector__Inspector__Connect(
const inner = try allocator.create(v8.Inspector); handle,
v8.Inspector.init(inner, client, channel, isolate); CONTEXT_GROUP_ID,
return .{ .inner = inner, .isolate = isolate, .session = inner.connect() }; channel.handle,
CLIENT_TRUST_LEVEL,
).?;
self.session = .{ .handle = session_handle };
} }
pub fn deinit(self: *const Inspector) void { 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.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 { 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. // comes and goes, but CDP can keep sending messages.
const isolate = self.isolate; const isolate = self.isolate;
var temp_scope: v8.HandleScope = undefined; var temp_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&temp_scope, isolate); v8.v8__HandleScope__CONSTRUCT(&temp_scope, isolate);
defer temp_scope.deinit(); defer v8.v8__HandleScope__DESTRUCT(&temp_scope);
self.session.dispatchProtocolMessage(isolate, msg); 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} // {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}
// - is_default_context: Whether the execution context is default, should match the auxData // - is_default_context: Whether the execution context is default, should match the auxData
pub fn contextCreated( pub fn contextCreated(
self: *const Inspector, self: *Inspector,
context: *const Context, context: *const Context,
name: []const u8, name: []const u8,
origin: []const u8, origin: []const u8,
aux_data: ?[]const u8, aux_data: []const u8,
is_default_context: bool, is_default_context: bool,
) void { ) 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. // Retrieves the RemoteObject for a given value.
// The value is loaded through the ExecutionWorld's mapZigInstanceToJs function, // The value is loaded through the ExecutionWorld's mapZigInstanceToJs function,
// just like a method return value. Therefore, if we've mapped this // 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. // we'll create it and track it for cleanup when the context ends.
pub fn getRemoteObject( pub fn getRemoteObject(
self: *const Inspector, self: *const Inspector,
@@ -114,9 +167,9 @@ pub fn getRemoteObject(
// We do not want to expose this as a parameter for now // We do not want to expose this as a parameter for now
const generate_preview = false; const generate_preview = false;
return self.session.wrapObject( return self.session.wrapObject(
context.isolate, context.isolate.handle,
context.v8_context, context.handle,
js_value, js_value.handle,
group, group,
generate_preview, 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); const unwrapped = try self.session.unwrapObject(allocator, object_id);
// The values context and groupId are not used here // The values context and groupId are not used here
const js_val = unwrapped.value; const js_val = unwrapped.value;
if (js_val.isObject() == false) { if (!v8.v8__Value__IsObject(js_val)) {
return error.ObjectIdIsNotANode; return error.ObjectIdIsNotANode;
} }
const Node = @import("../webapi/Node.zig"); 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; 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 { const NoopInspector = struct {
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {} pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {} pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}
@@ -148,15 +395,107 @@ const NoopInspector = struct {
pub fn onQuitMessageLoopOnPause(_: *anyopaque) void {} pub fn onQuitMessageLoopOnPause(_: *anyopaque) void {}
}; };
pub fn getTaggedAnyOpaque(value: v8.Value) ?*js.TaggedAnyOpaque { fn fromData(data: *anyopaque) *Inspector {
if (value.isObject() == false) { return @ptrCast(@alignCast(data));
}
pub fn getTaggedAnyOpaque(value: *const v8.Value) ?*js.TaggedAnyOpaque {
if (!v8.v8__Value__IsObject(value)) {
return null; return null;
} }
const obj = value.castTo(v8.Object); const internal_field_count = v8.v8__Object__InternalFieldCount(value);
if (obj.internalFieldCount() == 0) { if (internal_field_count == 0) {
return null; 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)); 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 std = @import("std");
const js = @import("js.zig"); const js = @import("js.zig");
// This only exists so that we know whether a function wants the opaque const v8 = js.v8;
// 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 This = @This(); const Integer = @This();
obj: js.Object,
pub fn setIndex(self: This, index: u32, value: anytype, opts: js.Object.SetOpts) !void { handle: *const v8.Integer,
return self.obj.setIndex(index, value, opts);
} pub fn init(isolate: *v8.Isolate, value: anytype) Integer {
const handle = switch (@TypeOf(value)) {
pub fn set(self: This, key: []const u8, value: anytype, opts: js.Object.SetOpts) !void { i8, i16, i32 => v8.v8__Integer__New(isolate, value).?,
return self.obj.set(key, value, opts); 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 IS_DEBUG = @import("builtin").mode == .Debug;
const Context = @import("Context.zig"); const Context = @import("Context.zig");
const PersistentObject = v8.Persistent(v8.Object);
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Object = @This(); const Object = @This();
js_obj: v8.Object,
context: *js.Context, ctx: *js.Context,
handle: *const v8.Object,
pub fn getId(self: Object) u32 { 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) { pub fn has(self: Object, key: anytype) bool {
READ_ONLY: bool = false, const ctx = self.ctx;
DONT_ENUM: bool = false, const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key);
DONT_DELETE: bool = false,
_: u29 = 0, var out: v8.MaybeBool = undefined;
}; v8.v8__Object__Has(self.handle, self.ctx.handle, key_handle, &out);
pub fn setIndex(self: Object, index: u32, value: anytype, opts: SetOpts) !void { if (out.has_value) {
@setEvalBranchQuota(10000); return out.value;
const key = switch (index) { }
inline 0...20 => |i| std.fmt.comptimePrint("{d}", .{i}), return false;
else => try std.fmt.allocPrint(self.context.arena, "{d}", .{index}), }
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 { pub fn set(self: Object, key: anytype, value: anytype, comptime opts: js.bridge.Caller.CallOpts) !bool {
const context = self.context; const ctx = self.ctx;
const js_key = v8.String.initUtf8(context.isolate, key); const js_value = try ctx.zigValueToJs(value, opts);
const js_value = try context.zigValueToJs(value); 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; var out: v8.MaybeBool = undefined;
if (!res) { v8.v8__Object__Set(self.handle, ctx.handle, key_handle, js_value.handle, &out);
return error.FailedToSet; 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 { pub fn toString(self: Object) ![]const u8 {
const js_value = self.js_obj.toValue(); return self.ctx.valueToString(self.toValue(), .{});
return self.context.valueToString(js_value, .{}); }
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 { pub fn format(self: Object, writer: *std.Io.Writer) !void {
if (comptime IS_DEBUG) { 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; const str = self.toString() catch return error.WriteFailed;
return writer.writeAll(str); return writer.writeAll(str);
} }
pub fn toJson(self: Object, allocator: Allocator) ![]u8 { pub fn persist(self: Object) !Global {
const json_string = try v8.Json.stringify(self.context.v8_context, self.js_obj.toValue(), null); var ctx = self.ctx;
const str = try self.context.jsStringToZig(json_string, .{ .allocator = allocator });
return str;
}
pub fn persist(self: Object) !Object { var global: v8.Global = undefined;
var context = self.context; v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
const js_obj = self.js_obj;
const persisted = PersistentObject.init(context.isolate, js_obj); try ctx.global_objects.append(ctx.arena, global);
try context.js_object_list.append(context.arena, persisted);
return .{ return .{
.context = context, .handle = global,
.js_obj = persisted.castToObject(), .ctx = ctx,
}; };
} }
@@ -110,15 +121,18 @@ pub fn getFunction(self: Object, name: []const u8) !?js.Function {
if (self.isNullOrUndefined()) { if (self.isNullOrUndefined()) {
return null; 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 (v8.v8__Value__IsFunction(js_val_handle) == false) {
if (!js_value.isFunction()) {
return null; 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 { 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); 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 { 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 { pub fn nameIterator(self: Object) NameIterator {
const context = self.context; const ctx = self.ctx;
const js_obj = self.js_obj;
const array = js_obj.getPropertyNames(context.v8_context); const handle = v8.v8__Object__GetPropertyNames(self.handle, ctx.handle).?;
const count = array.length(); const count = v8.v8__Array__Length(handle);
return .{ return .{
.ctx = ctx,
.handle = handle,
.count = count, .count = count,
.context = context,
.js_obj = array.castTo(v8.Object),
}; };
} }
pub fn toZig(self: Object, comptime T: type) !T { 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 { pub const NameIterator = struct {
count: u32, count: u32,
idx: u32 = 0, idx: u32 = 0,
js_obj: v8.Object, ctx: *Context,
context: *const Context, handle: *const v8.Array,
pub fn next(self: *NameIterator) !?[]const u8 { pub fn next(self: *NameIterator) !?[]const u8 {
const idx = self.idx; const idx = self.idx;
@@ -169,8 +211,8 @@ pub const NameIterator = struct {
} }
self.idx += 1; self.idx += 1;
const context = self.context; const js_val_handle = v8.v8__Object__GetIndex(@ptrCast(self.handle), self.ctx.handle, idx) orelse return error.JsException;
const js_val = try self.js_obj.getAtIndex(context.v8_context, idx); const js_val = js.Value{ .ctx = self.ctx, .handle = js_val_handle };
return try context.valueToString(js_val, .{}); return try self.ctx.valueToString(js_val, .{});
} }
}; };

View File

@@ -20,20 +20,22 @@ const js = @import("js.zig");
const v8 = js.v8; const v8 = js.v8;
const Platform = @This(); const Platform = @This();
inner: v8.Platform, handle: *v8.Platform,
pub fn init() !Platform { pub fn init() !Platform {
if (v8.initV8ICU() == false) { if (v8.v8__V8__InitializeICU() == false) {
return error.FailedToInitializeICU; return error.FailedToInitializeICU;
} }
const platform = v8.Platform.initDefault(0, true); // 0 - threadpool size, 0 == let v8 decide
v8.initV8Platform(platform); // 1 - idle_task_support, 1 == enabled
v8.initV8(); const handle = v8.v8__Platform__NewDefaultPlatform(0, 1).?;
return .{ .inner = platform }; v8.v8__V8__InitializePlatform(handle);
v8.v8__V8__Initialize();
return .{ .handle = handle };
} }
pub fn deinit(self: Platform) void { pub fn deinit(self: Platform) void {
_ = v8.deinitV8(); _ = v8.v8__V8__Dispose();
v8.deinitV8Platform(); v8.v8__V8__DisposePlatform();
self.inner.deinit(); 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, external_references: [countExternalReferences()]isize,
// Track whether this snapshot owns its data (was created in-process) // 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, owns_data: bool = false,
pub fn load(allocator: Allocator) !Snapshot { pub fn load() !Snapshot {
if (loadEmbedded()) |snapshot| { if (loadEmbedded()) |snapshot| {
return snapshot; return snapshot;
} }
return create(allocator); return create();
} }
fn loadEmbedded() ?Snapshot { fn loadEmbedded() ?Snapshot {
@@ -75,7 +75,7 @@ fn loadEmbedded() ?Snapshot {
const blob = embedded_snapshot_blob[@sizeOf(usize)..]; const blob = embedded_snapshot_blob[@sizeOf(usize)..];
const startup_data = v8.StartupData{ .data = blob.ptr, .raw_size = @intCast(blob.len) }; 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; 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) // Only free if we own the data (was created in-process)
if (self.owns_data) { 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,39 +106,53 @@ pub fn write(self: Snapshot, writer: *std.Io.Writer) !void {
pub fn fromEmbedded(self: Snapshot) bool { pub fn fromEmbedded(self: Snapshot) bool {
// if the snapshot comes from the embedFile, then it'll be flagged as not // 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; return self.owns_data == false;
} }
fn isValid(self: Snapshot) bool { fn isValid(self: Snapshot) bool {
return v8.SnapshotCreator.startupDataIsValid(self.startup_data); return v8.v8__StartupData__IsValid(self.startup_data);
} }
pub fn create(allocator: Allocator) !Snapshot { 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.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);
v8.v8__FunctionTemplate__Inherit(js_global, templates[window_index]);
return v8.v8__FunctionTemplate__InstanceTemplate(js_global).?;
}
pub fn create() !Snapshot {
var external_references = collectExternalReferences(); var external_references = collectExternalReferences();
var params = v8.initCreateParams(); var params: v8.CreateParams = undefined;
params.array_buffer_allocator = v8.createDefaultArrayBufferAllocator(); v8.v8__Isolate__CreateParams__CONSTRUCT(&params);
defer v8.destroyArrayBufferAllocator(params.array_buffer_allocator.?); 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); params.external_references = @ptrCast(&external_references);
var snapshot_creator: v8.SnapshotCreator = undefined; const snapshot_creator = v8.v8__SnapshotCreator__CREATE(&params);
v8.SnapshotCreator.init(&snapshot_creator, &params); defer v8.v8__SnapshotCreator__DESTRUCT(snapshot_creator);
defer snapshot_creator.deinit();
var data_start: usize = 0; 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 // CreateBlob, which we'll call once everything is setup, MUST NOT
// be called from an active HandleScope. Hence we have this scope to // be called from an active HandleScope. Hence we have this scope to
// clean it up before we call CreateBlob // clean it up before we call CreateBlob
var handle_scope: v8.HandleScope = undefined; var handle_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&handle_scope, isolate); v8.v8__HandleScope__CONSTRUCT(&handle_scope, isolate);
defer handle_scope.deinit(); defer v8.v8__HandleScope__DESTRUCT(&handle_scope);
// Create templates (constructors only) FIRST // 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| { inline for (JsApis, 0..) |JsApi, i| {
@setEvalBranchQuota(10_000); @setEvalBranchQuota(10_000);
templates[i] = generateConstructor(JsApi, isolate); templates[i] = generateConstructor(JsApi, isolate);
@@ -148,30 +163,22 @@ pub fn create(allocator: Allocator) !Snapshot {
// This must come before attachClass so inheritance is set up first // This must come before attachClass so inheritance is set up first
inline for (JsApis, 0..) |JsApi, i| { inline for (JsApis, 0..) |JsApi, i| {
if (comptime protoIndexLookup(JsApi)) |proto_index| { 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 // Set up the global template to inherit from Window's template
// This way the global object gets all Window properties through inheritance // This way the global object gets all Window properties through inheritance
const js_global = v8.FunctionTemplate.initDefault(isolate); const global_template = createGlobalTemplate(isolate, templates[0..]);
js_global.setClassName(v8.String.initUtf8(isolate, "Window")); const context = v8.v8__Context__New(isolate, global_template, null);
v8.v8__Context__Enter(context);
// Find Window in JsApis by name (avoids circular import) defer v8.v8__Context__Exit(context);
const window_index = comptime bridge.JsApiLookup.getId(Window.JsApi);
js_global.inherit(templates[window_index]);
const global_template = js_global.getInstanceTemplate();
const context = v8.Context.init(isolate, global_template, null);
context.enter();
defer context.exit();
// Add templates to context snapshot // Add templates to context snapshot
var last_data_index: usize = 0; var last_data_index: usize = 0;
inline for (JsApis, 0..) |_, i| { inline for (JsApis, 0..) |_, i| {
@setEvalBranchQuota(10_000); @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) { if (i == 0) {
data_start = data_index; data_start = data_index;
last_data_index = data_index; last_data_index = data_index;
@@ -189,16 +196,18 @@ pub fn create(allocator: Allocator) !Snapshot {
} }
// Realize all templates by getting their functions and attaching to global // 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| { 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 // Attach to global if it has a name
if (@hasDecl(JsApi.Meta, "name")) { if (@hasDecl(JsApi.Meta, "name")) {
if (@hasDecl(JsApi.Meta, "constructor_alias")) { if (@hasDecl(JsApi.Meta, "constructor_alias")) {
const v8_class_name = v8.String.initUtf8(isolate, JsApi.Meta.constructor_alias); const alias = JsApi.Meta.constructor_alias;
_ = global_obj.setValue(context, v8_class_name, func); 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 // @TODO: This is wrong. This name should be registered with the
// illegalConstructorCallback. I.e. new Image() is OK, but // illegalConstructorCallback. I.e. new Image() is OK, but
@@ -206,11 +215,15 @@ pub fn create(allocator: Allocator) !Snapshot {
// But we _have_ to register the name, i.e. HTMLImageElement // But we _have_ to register the name, i.e. HTMLImageElement
// has to be registered so, for now, instead of creating another // has to be registered so, for now, instead of creating another
// template, we just hook it into the constructor. // template, we just hook it into the constructor.
const illegal_class_name = v8.String.initUtf8(isolate, JsApi.Meta.name); const name = JsApi.Meta.name;
_ = global_obj.setValue(context, illegal_class_name, func); 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 { } else {
const v8_class_name = v8.String.initUtf8(isolate, JsApi.Meta.name); const name = JsApi.Meta.name;
_ = global_obj.setValue(context, v8_class_name, func); 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);
} }
} }
} }
@@ -218,8 +231,10 @@ pub fn create(allocator: Allocator) !Snapshot {
{ {
// If we want to overwrite the built-in console, we have to // If we want to overwrite the built-in console, we have to
// delete the built-in one. // delete the built-in one.
const console_key = v8.String.initUtf8(isolate, "console"); const console_key = v8.v8__String__NewFromUtf8(isolate, "console", v8.kNormal, 7);
if (global_obj.deleteValue(context, console_key) == false) { 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; return error.ConsoleDeleteError;
} }
} }
@@ -229,30 +244,36 @@ pub fn create(allocator: Allocator) !Snapshot {
// TODO: see if newer V8 engines have a way around this. // TODO: see if newer V8 engines have a way around this.
inline for (JsApis, 0..) |JsApi, i| { inline for (JsApis, 0..) |JsApi, i| {
if (comptime protoIndexLookup(JsApi)) |proto_index| { if (comptime protoIndexLookup(JsApi)) |proto_index| {
const proto_obj = templates[proto_index].getFunction(context).toObject(); const proto_func = v8.v8__FunctionTemplate__GetFunction(templates[proto_index], context);
const self_obj = templates[i].getFunction(context).toObject(); const proto_obj: *const v8.Object = @ptrCast(proto_func);
_ = self_obj.setPrototype(context, proto_obj);
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 // Custom exception
// TODO: this is an horrible hack, I can't figure out how to do this cleanly. // 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"); const code_str = "DOMException.prototype.__proto__ = Error.prototype";
_ = try (try v8.Script.compile(context, code, null)).run(context); 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 blob = v8.v8__SnapshotCreator__createBlob(snapshot_creator, v8.kKeep);
const owned = try allocator.dupe(u8, blob.data[0..@intCast(blob.raw_size)]);
return .{ return .{
.owns_data = true, .owns_data = true,
.data_start = data_start, .data_start = data_start,
.external_references = external_references, .external_references = external_references,
.startup_data = .{ .data = owned.ptr, .raw_size = @intCast(owned.len) }, .startup_data = blob,
}; };
} }
@@ -361,7 +382,7 @@ fn collectExternalReferences() [countExternalReferences()]isize {
// via `new ClassName()` - but they could, for example, be created in // via `new ClassName()` - but they could, for example, be created in
// Zig and returned from a function call, which is why we need the // Zig and returned from a function call, which is why we need the
// FunctionTemplate. // FunctionTemplate.
fn generateConstructor(comptime JsApi: type, isolate: v8.Isolate) v8.FunctionTemplate { fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *v8.FunctionTemplate {
const callback = blk: { const callback = blk: {
if (@hasDecl(JsApi, "constructor")) { if (@hasDecl(JsApi, "constructor")) {
break :blk JsApi.constructor.func; break :blk JsApi.constructor.func;
@@ -371,19 +392,24 @@ fn generateConstructor(comptime JsApi: type, isolate: v8.Isolate) v8.FunctionTem
break :blk illegalConstructorCallback; 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")) { 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)); const name_str = if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi);
template.setClassName(class_name); 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; return template;
} }
// Attaches JsApi members to the prototype template (normal case) // Attaches JsApi members to the prototype template (normal case)
fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.FunctionTemplate) void { fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.FunctionTemplate) void {
const target = template.getPrototypeTemplate(); const target = v8.v8__FunctionTemplate__PrototypeTemplate(template);
const instance = v8.v8__FunctionTemplate__InstanceTemplate(template);
const declarations = @typeInfo(JsApi).@"struct".decls; const declarations = @typeInfo(JsApi).@"struct".decls;
inline for (declarations) |d| { inline for (declarations) |d| {
const name: [:0]const u8 = d.name; const name: [:0]const u8 = d.name;
const value = @field(JsApi, name); const value = @field(JsApi, name);
@@ -391,60 +417,79 @@ fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.FunctionT
switch (definition) { switch (definition) {
bridge.Accessor => { bridge.Accessor => {
const js_name = v8.String.initUtf8(isolate, name).toName(); const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
const getter_callback = v8.FunctionTemplate.initCallback(isolate, value.getter); const getter_callback = @constCast(v8.v8__FunctionTemplate__New__DEFAULT2(isolate, value.getter).?);
if (value.setter == null) { if (value.setter == null) {
if (value.static) { if (value.static) {
template.setAccessorGetter(js_name, getter_callback); v8.v8__Template__SetAccessorProperty__DEFAULT(@ptrCast(template), js_name, getter_callback);
} else { } else {
target.setAccessorGetter(js_name, getter_callback); v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT(target, js_name, getter_callback);
} }
} else { } else {
std.debug.assert(value.static == false); if (comptime IS_DEBUG) {
const setter_callback = v8.FunctionTemplate.initCallback(isolate, value.setter); std.debug.assert(value.static == false);
target.setAccessorGetterAndSetter(js_name, getter_callback, setter_callback); }
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 => { bridge.Function => {
const function_template = v8.FunctionTemplate.initCallback(isolate, value.func); const function_template = @constCast(v8.v8__FunctionTemplate__New__DEFAULT2(isolate, value.func).?);
const js_name: v8.Name = v8.String.initUtf8(isolate, name).toName(); const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
if (value.static) { 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 { } 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 => { bridge.Indexed => {
const configuration = v8.IndexedPropertyHandlerConfiguration{ var configuration: v8.IndexedPropertyHandlerConfiguration = .{
.getter = value.getter, .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 => { 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) const js_name = if (value.async)
v8.Symbol.getAsyncIterator(isolate).toName() v8.v8__Symbol__GetAsyncIterator(isolate)
else else
v8.Symbol.getIterator(isolate).toName(); v8.v8__Symbol__GetIterator(isolate);
target.set(js_name, function_template, v8.PropertyAttribute.None); v8.v8__Template__Set(@ptrCast(target), js_name, @ptrCast(function_template), v8.None);
}, },
bridge.Property => { bridge.Property => {
// simpleZigValueToJs now returns raw handle directly
const js_value = switch (value) { 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 // 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 // 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 bridge.Constructor => {}, // already handled in generateConstructor
else => {}, else => {},
@@ -452,9 +497,14 @@ fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.FunctionT
} }
if (@hasDecl(JsApi.Meta, "htmldda")) { if (@hasDecl(JsApi.Meta, "htmldda")) {
const instance_template = template.getInstanceTemplate(); v8.v8__ObjectTemplate__MarkAsUndetectable(instance);
instance_template.markAsUndetectable(); v8.v8__ObjectTemplate__SetCallAsFunctionHandler(instance, JsApi.Meta.callable.func);
instance_template.setCallAsFunctionHandler(JsApi.Meta.callable.func); }
if (@hasDecl(JsApi.Meta, "name")) {
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);
} }
} }
@@ -472,10 +522,15 @@ fn protoIndexLookup(comptime JsApi: type) ?bridge.JsApiLookup.BackingInt {
} }
// Shared illegal constructor callback for types without explicit constructors // Shared illegal constructor callback for types without explicit constructors
fn illegalConstructorCallback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { fn illegalConstructorCallback(raw_info: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info); const isolate = v8.v8__FunctionCallbackInfo__GetIsolate(raw_info);
const iso = info.getIsolate();
log.warn(.js, "Illegal constructor call", .{}); 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> // Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io> // Pierre Tachoire <pierre@lightpanda.io>
@@ -24,59 +24,84 @@ const Allocator = std.mem.Allocator;
const TryCatch = @This(); const TryCatch = @This();
inner: v8.TryCatch, ctx: *js.Context,
context: *const js.Context, handle: v8.TryCatch,
pub fn init(self: *TryCatch, context: *const js.Context) void { pub fn init(self: *TryCatch, ctx: *js.Context) void {
self.context = context; self.ctx = ctx;
self.inner.init(context.isolate); v8.v8__TryCatch__CONSTRUCT(&self.handle, ctx.isolate.handle);
} }
pub fn hasCaught(self: TryCatch) bool { 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 caught(self: TryCatch, allocator: Allocator) ?Caught {
pub fn exception(self: TryCatch, allocator: Allocator) !?[]const u8 { if (!self.hasCaught()) {
const msg = self.inner.getException() orelse return null; 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;
}
} }
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 { 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,72 +21,302 @@ const js = @import("js.zig");
const v8 = js.v8; const v8 = js.v8;
const IS_DEBUG = @import("builtin").mode == .Debug;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const PersistentValue = v8.Persistent(v8.Value);
const Value = @This(); const Value = @This();
js_val: v8.Value,
context: *js.Context, ctx: *js.Context,
handle: *const v8.Value,
pub fn isObject(self: Value) bool { pub fn isObject(self: Value) bool {
return self.js_val.isObject(); return v8.v8__Value__IsObject(self.handle);
} }
pub fn isString(self: Value) bool { pub fn isString(self: Value) bool {
return self.js_val.isString(); return v8.v8__Value__IsString(self.handle);
} }
pub fn isArray(self: Value) bool { pub fn isArray(self: Value) bool {
return self.js_val.isArray(); return v8.v8__Value__IsArray(self.handle);
} }
pub fn toString(self: Value, allocator: Allocator) ![]const u8 { pub fn isSymbol(self: Value) bool {
return self.context.valueToString(self.js_val, .{ .allocator = allocator }); 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 v8.v8__Value__IsNull(self.handle);
}
pub fn isUndefined(self: Value) bool {
return v8.v8__Value__IsUndefined(self.handle);
}
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 { pub fn fromJson(ctx: *js.Context, json: []const u8) !Value {
const json_string = v8.String.initUtf8(ctx.isolate, json); const v8_isolate = v8.Isolate{ .handle = ctx.isolate.handle };
const value = try v8.Json.parse(ctx.v8_context, json_string); const json_string = v8.String.initUtf8(v8_isolate, json);
return Value{ .context = ctx, .js_val = value }; 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 { pub fn persist(self: Value) !Global {
const js_val = self.js_val; var ctx = self.ctx;
var context = self.context;
const persisted = PersistentValue.init(context.isolate, js_val); var global: v8.Global = undefined;
try context.js_value_list.append(context.arena, persisted); 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.ctx.jsValueToZig(T, self);
} }
pub fn toObject(self: Value) js.Object { pub fn toObject(self: Value) js.Object {
if (comptime IS_DEBUG) {
std.debug.assert(self.isObject());
}
return .{ return .{
.context = self.context, .ctx = self.ctx,
.js_obj = self.js_val.castTo(v8.Object), .handle = @ptrCast(self.handle),
}; };
} }
pub fn toArray(self: Value) js.Array { pub fn toArray(self: Value) js.Array {
if (comptime IS_DEBUG) {
std.debug.assert(self.isArray());
}
return .{ return .{
.context = self.context, .ctx = self.ctx,
.js_arr = self.js_val.castTo(v8.Array), .handle = @ptrCast(self.handle),
}; };
} }
// pub const Value = struct { pub fn toBigInt(self: Value) js.BigInt {
// value: v8.Value, if (comptime IS_DEBUG) {
// context: *const Context, std.debug.assert(self.isBigInt());
}
// // the caller needs to deinit the string returned return .{
// pub fn toString(self: Value, allocator: Allocator) ![]const u8 { .handle = @ptrCast(self.handle),
// return self.context.valueToString(self.value, .{ .allocator = allocator }); };
// } }
// pub fn fromJson(ctx: *Context, json: []const u8) !Value { pub fn format(self: Value, writer: *std.Io.Writer) !void {
// const json_string = v8.String.initUtf8(ctx.isolate, json); if (comptime IS_DEBUG) {
// const value = try v8.Json.parse(ctx.v8_context, json_string); return self.ctx.debugValue(self, writer);
// return Value{ .context = ctx, .value = value }; }
// } 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 std = @import("std");
const js = @import("js.zig"); const js = @import("js.zig");
const lp = @import("lightpanda");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const v8 = js.v8; 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 { pub fn Builder(comptime T: type) type {
return struct { return struct {
@@ -89,7 +634,7 @@ pub fn Builder(comptime T: type) type {
} }
pub const Constructor = struct { 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 { const Opts = struct {
dom_exception: bool = false, dom_exception: bool = false,
@@ -97,11 +642,12 @@ pub const Constructor = struct {
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Constructor { fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Constructor {
return .{ .func = struct { return .{ .func = struct {
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info); const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(info); var caller = Caller.init(v8_isolate);
defer caller.deinit(); defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
caller.constructor(T, func, info, .{ caller.constructor(T, func, info, .{
.dom_exception = opts.dom_exception, .dom_exception = opts.dom_exception,
}); });
@@ -112,7 +658,7 @@ pub const Constructor = struct {
pub const Function = struct { pub const Function = struct {
static: bool, static: bool,
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void, func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
const Opts = struct { const Opts = struct {
static: bool = false, static: bool = false,
@@ -125,11 +671,12 @@ pub const Function = struct {
return .{ return .{
.static = opts.static, .static = opts.static,
.func = struct { .func = struct {
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info); const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(info); var caller = Caller.init(v8_isolate);
defer caller.deinit(); defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
if (comptime opts.static) { if (comptime opts.static) {
caller.function(T, func, info, .{ caller.function(T, func, info, .{
.dom_exception = opts.dom_exception, .dom_exception = opts.dom_exception,
@@ -151,8 +698,8 @@ pub const Function = struct {
pub const Accessor = struct { pub const Accessor = struct {
static: bool = false, static: bool = false,
getter: ?*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.C_FunctionCallbackInfo) callconv(.c) void = null, setter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,
const Opts = struct { const Opts = struct {
static: bool = false, static: bool = false,
@@ -168,28 +715,39 @@ pub const Accessor = struct {
if (@typeInfo(@TypeOf(getter)) != .null) { if (@typeInfo(@TypeOf(getter)) != .null) {
accessor.getter = struct { accessor.getter = struct {
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info); const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(info); var caller = Caller.init(v8_isolate);
defer caller.deinit(); defer caller.deinit();
caller.method(T, getter, info, .{ const info = FunctionCallbackInfo{ .handle = handle.? };
.as_typed_array = opts.as_typed_array, if (comptime opts.static) {
.null_as_undefined = opts.null_as_undefined, 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; }.wrap;
} }
if (@typeInfo(@TypeOf(setter)) != .null) { if (@typeInfo(@TypeOf(setter)) != .null) {
accessor.setter = struct { accessor.setter = struct {
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info); const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
std.debug.assert(info.length() == 1); var caller = Caller.init(v8_isolate);
var caller = Caller.init(info);
defer caller.deinit(); 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, .{ caller.method(T, setter, info, .{
.as_typed_array = opts.as_typed_array, .as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined, .null_as_undefined = opts.null_as_undefined,
@@ -203,7 +761,7 @@ pub const Accessor = struct {
}; };
pub const Indexed = 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 { const Opts = struct {
as_typed_array: bool = false, 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 { fn init(comptime T: type, comptime getter: anytype, comptime opts: Opts) Indexed {
return .{ .getter = struct { return .{ .getter = struct {
fn wrap(idx: u32, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 { fn wrap(idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info); const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(info); var caller = Caller.init(v8_isolate);
defer caller.deinit(); defer caller.deinit();
const info = PropertyCallbackInfo{ .handle = handle.? };
return caller.getIndex(T, getter, idx, info, .{ return caller.getIndex(T, getter, idx, info, .{
.as_typed_array = opts.as_typed_array, .as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined, .null_as_undefined = opts.null_as_undefined,
@@ -226,9 +786,9 @@ pub const Indexed = struct {
}; };
pub const NamedIndexed = struct { pub const NamedIndexed = struct {
getter: *const fn (c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8, getter: *const fn (c_name: ?*const v8.Name, handle: ?*const v8.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, 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.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 = null, deleter: ?*const fn (c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 = null,
const Opts = struct { const Opts = struct {
as_typed_array: bool = false, 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 { fn init(comptime T: type, comptime getter: anytype, setter: anytype, deleter: anytype, comptime opts: Opts) NamedIndexed {
const getter_fn = struct { const getter_fn = struct {
fn wrap(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 { fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info); const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(info); var caller = Caller.init(v8_isolate);
defer caller.deinit(); defer caller.deinit();
const info = PropertyCallbackInfo{ .handle = handle.? };
return caller.getNamedIndex(T, getter, .{ .handle = c_name.? }, info, .{ return caller.getNamedIndex(T, getter, .{ .handle = c_name.? }, info, .{
.as_typed_array = opts.as_typed_array, .as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined, .null_as_undefined = opts.null_as_undefined,
@@ -249,11 +811,12 @@ pub const NamedIndexed = struct {
}.wrap; }.wrap;
const setter_fn = if (@typeInfo(@TypeOf(setter)) == .null) null else struct { 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 { fn wrap(c_name: ?*const v8.Name, c_value: ?*const v8.Value, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info); const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(info); var caller = Caller.init(v8_isolate);
defer caller.deinit(); defer caller.deinit();
const info = PropertyCallbackInfo{ .handle = handle.? };
return caller.setNamedIndex(T, setter, .{ .handle = c_name.? }, .{ .handle = c_value.? }, info, .{ return caller.setNamedIndex(T, setter, .{ .handle = c_name.? }, .{ .handle = c_value.? }, info, .{
.as_typed_array = opts.as_typed_array, .as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined, .null_as_undefined = opts.null_as_undefined,
@@ -262,11 +825,12 @@ pub const NamedIndexed = struct {
}.wrap; }.wrap;
const deleter_fn = if (@typeInfo(@TypeOf(deleter)) == .null) null else struct { 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 { fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info); const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(info); var caller = Caller.init(v8_isolate);
defer caller.deinit(); defer caller.deinit();
const info = PropertyCallbackInfo{ .handle = handle.? };
return caller.deleteNamedIndex(T, deleter, .{ .handle = c_name.? }, info, .{ return caller.deleteNamedIndex(T, deleter, .{ .handle = c_name.? }, info, .{
.as_typed_array = opts.as_typed_array, .as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined, .null_as_undefined = opts.null_as_undefined,
@@ -283,7 +847,7 @@ pub const NamedIndexed = struct {
}; };
pub const Iterator = 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, async: bool,
const Opts = struct { const Opts = struct {
@@ -296,8 +860,8 @@ pub const Iterator = struct {
return .{ return .{
.async = opts.async, .async = opts.async,
.func = struct { .func = struct {
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info); const info = FunctionCallbackInfo{ .handle = handle.? };
info.getReturnValue().set(info.getThis()); info.getReturnValue().set(info.getThis());
} }
}.wrap, }.wrap,
@@ -307,10 +871,12 @@ pub const Iterator = struct {
return .{ return .{
.async = opts.async, .async = opts.async,
.func = struct { .func = struct {
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info); const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(info); var caller = Caller.init(v8_isolate);
defer caller.deinit(); defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
caller.method(T, struct_or_func, info, .{}); caller.method(T, struct_or_func, info, .{});
} }
}.wrap, }.wrap,
@@ -319,7 +885,7 @@ pub const Iterator = struct {
}; };
pub const Callable = 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 { const Opts = struct {
null_as_undefined: bool = false, 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 { fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Callable {
return .{ .func = struct { return .{ .func = struct {
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info); const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(info); var caller = Caller.init(v8_isolate);
defer caller.deinit(); defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
caller.method(T, func, info, .{ caller.method(T, func, info, .{
.null_as_undefined = opts.null_as_undefined, .null_as_undefined = opts.null_as_undefined,
}); });
@@ -343,6 +911,62 @@ pub const Property = union(enum) {
int: i64, 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 // Given a Type, returns the length of the prototype chain, including self
fn prototypeChainLength(comptime T: type) usize { fn prototypeChainLength(comptime T: type) usize {
var l: usize = 1; var l: usize = 1;
@@ -529,16 +1153,22 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/element/Html.zig"), @import("../webapi/element/Html.zig"),
@import("../webapi/element/html/IFrame.zig"), @import("../webapi/element/html/IFrame.zig"),
@import("../webapi/element/html/Anchor.zig"), @import("../webapi/element/html/Anchor.zig"),
@import("../webapi/element/html/Area.zig"),
@import("../webapi/element/html/Audio.zig"), @import("../webapi/element/html/Audio.zig"),
@import("../webapi/element/html/Base.zig"),
@import("../webapi/element/html/Body.zig"), @import("../webapi/element/html/Body.zig"),
@import("../webapi/element/html/BR.zig"), @import("../webapi/element/html/BR.zig"),
@import("../webapi/element/html/Button.zig"), @import("../webapi/element/html/Button.zig"),
@import("../webapi/element/html/Canvas.zig"), @import("../webapi/element/html/Canvas.zig"),
@import("../webapi/element/html/Custom.zig"), @import("../webapi/element/html/Custom.zig"),
@import("../webapi/element/html/Data.zig"), @import("../webapi/element/html/Data.zig"),
@import("../webapi/element/html/DataList.zig"),
@import("../webapi/element/html/Dialog.zig"), @import("../webapi/element/html/Dialog.zig"),
@import("../webapi/element/html/Directory.zig"),
@import("../webapi/element/html/Div.zig"), @import("../webapi/element/html/Div.zig"),
@import("../webapi/element/html/Embed.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/Form.zig"),
@import("../webapi/element/html/Generic.zig"), @import("../webapi/element/html/Generic.zig"),
@import("../webapi/element/html/Head.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/Html.zig"),
@import("../webapi/element/html/Image.zig"), @import("../webapi/element/html/Image.zig"),
@import("../webapi/element/html/Input.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/LI.zig"),
@import("../webapi/element/html/Link.zig"), @import("../webapi/element/html/Link.zig"),
@import("../webapi/element/html/Map.zig"),
@import("../webapi/element/html/Media.zig"), @import("../webapi/element/html/Media.zig"),
@import("../webapi/element/html/Meta.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/OL.zig"),
@import("../webapi/element/html/OptGroup.zig"),
@import("../webapi/element/html/Option.zig"), @import("../webapi/element/html/Option.zig"),
@import("../webapi/element/html/Output.zig"),
@import("../webapi/element/html/Paragraph.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/Script.zig"),
@import("../webapi/element/html/Select.zig"), @import("../webapi/element/html/Select.zig"),
@import("../webapi/element/html/Slot.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/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/Template.zig"),
@import("../webapi/element/html/TextArea.zig"), @import("../webapi/element/html/TextArea.zig"),
@import("../webapi/element/html/Time.zig"),
@import("../webapi/element/html/Title.zig"), @import("../webapi/element/html/Title.zig"),
@import("../webapi/element/html/Track.zig"),
@import("../webapi/element/html/Video.zig"), @import("../webapi/element/html/Video.zig"),
@import("../webapi/element/html/UL.zig"), @import("../webapi/element/html/UL.zig"),
@import("../webapi/element/html/Unknown.zig"), @import("../webapi/element/html/Unknown.zig"),
@@ -617,4 +1269,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/navigation/NavigationEventTarget.zig"), @import("../webapi/navigation/NavigationEventTarget.zig"),
@import("../webapi/navigation/NavigationHistoryEntry.zig"), @import("../webapi/navigation/NavigationHistoryEntry.zig"),
@import("../webapi/navigation/NavigationActivation.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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
pub const v8 = @import("v8"); pub const v8 = @import("v8").c;
const log = @import("../../log.zig"); const log = @import("../../log.zig");
@@ -28,14 +28,23 @@ pub const Context = @import("Context.zig");
pub const Inspector = @import("Inspector.zig"); pub const Inspector = @import("Inspector.zig");
pub const Snapshot = @import("Snapshot.zig"); pub const Snapshot = @import("Snapshot.zig");
pub const Platform = @import("Platform.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 Name = @import("Name.zig");
pub const This = @import("This.zig");
pub const Value = @import("Value.zig"); pub const Value = @import("Value.zig");
pub const Array = @import("Array.zig"); pub const Array = @import("Array.zig");
pub const String = @import("String.zig");
pub const Object = @import("Object.zig"); pub const Object = @import("Object.zig");
pub const TryCatch = @import("TryCatch.zig"); pub const TryCatch = @import("TryCatch.zig");
pub const Function = @import("Function.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; 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 { pub const Exception = struct {
inner: v8.Value, ctx: *const Context,
context: *const Context, handle: *const v8.Value,
// the caller needs to deinit the string returned
pub fn exception(self: Exception, allocator: Allocator) ![]const u8 { pub fn exception(self: Exception, allocator: Allocator) ![]const u8 {
return self.context.valueToString(self.inner, .{ .allocator = allocator }); 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 // 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 // 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) // 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))) { switch (@typeInfo(@TypeOf(value))) {
.void => return v8.initUndefined(isolate).toValue(), .void => return isolate.initUndefined(),
.null => if (comptime null_as_undefined) return v8.initUndefined(isolate).toValue() else return v8.initNull(isolate).toValue(), .null => if (comptime null_as_undefined) return isolate.initUndefined() else return isolate.initNull(),
.bool => return v8.getValue(if (value) v8.initTrue(isolate) else v8.initFalse(isolate)), .bool => return if (value) isolate.initTrue() else isolate.initFalse(),
.int => |n| switch (n.signedness) { .int => |n| {
.signed => { if (comptime n.bits <= 32) {
if (value > 0 and value <= 4_294_967_295) { return @ptrCast(isolate.initInteger(value).handle);
return v8.Integer.initU32(isolate, @intCast(value)).toValue(); }
} if (value >= 0 and value <= 4_294_967_295) {
if (value >= -2_147_483_648 and value <= 2_147_483_647) { return @ptrCast(isolate.initInteger(@as(u32, @intCast(value))).handle);
return v8.Integer.initI32(isolate, @intCast(value)).toValue(); }
} return @ptrCast(isolate.initBigInt(value).handle);
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");
},
}, },
.comptime_int => { .comptime_int => {
if (value >= 0) { if (value > -2_147_483_648 and value <= 4_294_967_295) {
if (value <= 4_294_967_295) { return @ptrCast(isolate.initInteger(value).handle);
return v8.Integer.initU32(isolate, @intCast(value)).toValue();
}
return v8.BigInt.initU64(isolate, @intCast(value)).toValue();
} }
if (value >= -2_147_483_648) { return @ptrCast(isolate.initBigInt(value).handle);
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"),
}, },
.float, .comptime_float => return @ptrCast(isolate.initNumber(value).handle),
.pointer => |ptr| { .pointer => |ptr| {
if (ptr.size == .slice and ptr.child == u8) { if (ptr.size == .slice and ptr.child == u8) {
return v8.String.initUtf8(isolate, value).toValue(); return @ptrCast(isolate.initStringHandle(value));
} }
if (ptr.size == .one) { if (ptr.size == .one) {
const one_info = @typeInfo(ptr.child); const one_info = @typeInfo(ptr.child);
if (one_info == .array and one_info.array.child == u8) { 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); return simpleZigValueToJs(isolate, v, fail, null_as_undefined);
} }
if (comptime 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" => { .@"struct" => {
switch (@TypeOf(value)) { switch (@TypeOf(value)) {
ArrayBuffer => { ArrayBuffer => {
const values = value.values; const values = value.values;
const len = values.len; const len = values.len;
var array_buffer: v8.ArrayBuffer = undefined; const backing_store = v8.v8__ArrayBuffer__NewBackingStore(isolate.handle, len);
const backing_store = v8.BackingStore.init(isolate, len); const data: [*]u8 = @ptrCast(@alignCast(v8.v8__BackingStore__Data(backing_store)));
const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData()));
@memcpy(data[0..len], @as([]const u8, @ptrCast(values))[0..len]); @memcpy(data[0..len], @as([]const u8, @ptrCast(values))[0..len]);
array_buffer = v8.ArrayBuffer.initWithBackingStore(isolate, &backing_store.toSharedPtr()); const backing_store_ptr = v8.v8__BackingStore__TO_SHARED_PTR(backing_store);
return @ptrCast(v8.v8__ArrayBuffer__New2(isolate.handle, &backing_store_ptr).?);
return .{ .handle = array_buffer.handle };
}, },
// zig fmt: off // zig fmt: off
TypedArray(u8), TypedArray(u16), TypedArray(u32), TypedArray(u64), 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)), else => @compileError("Invalid TypeArray type: " ++ @typeName(value_type)),
}; };
var array_buffer: v8.ArrayBuffer = undefined; var array_buffer: *const v8.ArrayBuffer = undefined;
if (len == 0) { if (len == 0) {
array_buffer = v8.ArrayBuffer.init(isolate, 0); array_buffer = v8.v8__ArrayBuffer__New(isolate.handle, 0).?;
} else { } else {
const buffer_len = len * bits / 8; const buffer_len = len * bits / 8;
const backing_store = v8.BackingStore.init(isolate, buffer_len); const backing_store = v8.v8__ArrayBuffer__NewBackingStore(isolate.handle, buffer_len).?;
const data: [*]u8 = @ptrCast(@alignCast(backing_store.getData())); const data: [*]u8 = @ptrCast(@alignCast(v8.v8__BackingStore__Data(backing_store)));
@memcpy(data[0..buffer_len], @as([]const u8, @ptrCast(values))[0..buffer_len]); @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)) { switch (@typeInfo(value_type)) {
.int => |n| switch (n.signedness) { .int => |n| switch (n.signedness) {
.unsigned => switch (n.bits) { .unsigned => switch (n.bits) {
8 => return v8.Uint8Array.init(array_buffer, 0, len).toValue(), 8 => return @ptrCast(v8.v8__Uint8Array__New(array_buffer, 0, len).?),
16 => return v8.Uint16Array.init(array_buffer, 0, len).toValue(), 16 => return @ptrCast(v8.v8__Uint16Array__New(array_buffer, 0, len).?),
32 => return v8.Uint32Array.init(array_buffer, 0, len).toValue(), 32 => return @ptrCast(v8.v8__Uint32Array__New(array_buffer, 0, len).?),
64 => return v8.BigUint64Array.init(array_buffer, 0, len).toValue(), 64 => return @ptrCast(v8.v8__BigUint64Array__New(array_buffer, 0, len).?),
else => {}, else => {},
}, },
.signed => switch (n.bits) { .signed => switch (n.bits) {
8 => return v8.Int8Array.init(array_buffer, 0, len).toValue(), 8 => return @ptrCast(v8.v8__Int8Array__New(array_buffer, 0, len).?),
16 => return v8.Int16Array.init(array_buffer, 0, len).toValue(), 16 => return @ptrCast(v8.v8__Int16Array__New(array_buffer, 0, len).?),
32 => return v8.Int32Array.init(array_buffer, 0, len).toValue(), 32 => return @ptrCast(v8.v8__Int32Array__New(array_buffer, 0, len).?),
64 => return v8.BigInt64Array.init(array_buffer, 0, len).toValue(), 64 => return @ptrCast(v8.v8__BigInt64Array__New(array_buffer, 0, len).?),
else => {}, else => {},
}, },
}, },
.float => |f| switch (f.bits) { .float => |f| switch (f.bits) {
32 => return v8.Float32Array.init(array_buffer, 0, len).toValue(), 32 => return @ptrCast(v8.v8__Float32Array__New(array_buffer, 0, len).?),
64 => return v8.Float64Array.init(array_buffer, 0, len).toValue(), 64 => return @ptrCast(v8.v8__Float64Array__New(array_buffer, 0, len).?),
else => {}, else => {},
}, },
else => {}, else => {},
@@ -388,6 +197,7 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo
// but this can never be valid. // but this can never be valid.
@compileError("Invalid TypeArray type: " ++ @typeName(value_type)); @compileError("Invalid TypeArray type: " ++ @typeName(value_type));
}, },
inline String, BigInt, Integer, Number, Value, Object => return value.handle,
else => {}, else => {},
} }
}, },
@@ -405,21 +215,6 @@ pub fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bo
} }
return null; 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 // 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 // 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. // 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 // When we're asked to describe an object via the Inspector, we _must_ include
// the proper subtype (and description) fields in the returned JSON. // 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 // V8 will give us a Value and ask us for the subtype. From the js.Value we
// can get a v8.Object, and from the v8.Object, we can get out TaggedAnyOpaque // can get a js.Object, and from the js.Object, we can get out TaggedAnyOpaque
// which is where we store the subtype. // which is where we store the subtype.
subtype: ?bridge.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 // it'll call this function to gets its [optional] subtype - which, from V8's
// point of view, is an arbitrary string. // point of view, is an arbitrary string.
pub export fn v8_inspector__Client__IMPL__valueSubtype( pub export fn v8_inspector__Client__IMPL__valueSubtype(
_: *v8.c.InspectorClientImpl, _: *v8.InspectorClientImpl,
c_value: *const v8.C_Value, c_value: *const v8.Value,
) callconv(.c) [*c]const u8 { ) 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; 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 // present, even if it's empty. So if we have a subType for the value, we'll
// put an empty description. // put an empty description.
pub export fn v8_inspector__Client__IMPL__descriptionForValueSubtype( pub export fn v8_inspector__Client__IMPL__descriptionForValueSubtype(
_: *v8.c.InspectorClientImpl, _: *v8.InspectorClientImpl,
v8_context: *const v8.C_Context, v8_context: *const v8.Context,
c_value: *const v8.C_Value, c_value: *const v8.Value,
) callconv(.c) [*c]const u8 { ) callconv(.c) [*c]const u8 {
_ = v8_context; _ = v8_context;
// We _must_ include a non-null description in order for the subtype value // 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 // 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 ""; 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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const h5e = @import("html5ever.zig"); const h5e = @import("html5ever.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
@@ -98,6 +99,29 @@ pub fn parse(self: *Parser, html: []const u8) void {
); );
} }
pub fn parseXML(self: *Parser, xml: []const u8) void {
h5e.xml5ever_parse_document(
xml.ptr,
xml.len,
&self.container,
self,
createXMLElementCallback,
getDataCallback,
appendCallback,
parseErrorCallback,
popCallback,
createCommentCallback,
createProcessingInstruction,
appendDoctypeToDocument,
addAttrsIfMissingCallback,
getTemplateContentsCallback,
removeFromParentCallback,
reparentChildrenCallback,
appendBeforeSiblingCallback,
appendBasedOnParentNodeCallback,
);
}
pub fn parseFragment(self: *Parser, html: []const u8) void { pub fn parseFragment(self: *Parser, html: []const u8) void {
h5e.html5ever_parse_fragment( h5e.html5ever_parse_fragment(
html.ptr, html.ptr,
@@ -139,7 +163,7 @@ pub const Streaming = struct {
} }
pub fn start(self: *Streaming) !void { 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.handle = h5e.html5ever_streaming_parser_create(
&self.parser.container, &self.parser.container,
@@ -202,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 { 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)); 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 }; self.err = .{ .err = err, .source = .create_element };
return null; 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 page = self.page;
const name = qname.local.slice(); const name = qname.local.slice();
const namespace = qname.ns.slice(); const namespace_string = qname.ns.slice();
const node = try page.createElement(namespace, name, attributes); 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); const pn = try self.arena.create(ParsedNode);
pn.* = .{ pn.* = .{
@@ -325,7 +358,7 @@ fn getDataCallback(ctx: *anyopaque) callconv(.c) *anyopaque {
const pn: *ParsedNode = @ptrCast(@alignCast(ctx)); const pn: *ParsedNode = @ptrCast(@alignCast(ctx));
// For non-elements, data is null. But, we expect this to only ever // For non-elements, data is null. But, we expect this to only ever
// be called for elements. // be called for elements.
std.debug.assert(pn.data != null); lp.assert(pn.data != null, "Parser.getDataCallback null data", .{});
return pn.data.?; return pn.data.?;
} }

View File

@@ -171,3 +171,24 @@ pub const NodeOrText = extern struct {
text: []const u8, text: []const u8,
}; };
}; };
pub extern "c" fn xml5ever_parse_document(
html: [*c]const u8,
len: usize,
doc: *anyopaque,
ctx: *anyopaque,
createElementCallback: *const fn (ctx: *anyopaque, data: *anyopaque, QualName, AttributeIterator) callconv(.c) ?*anyopaque,
elemNameCallback: *const fn (node_ref: *anyopaque) callconv(.c) *anyopaque,
appendCallback: *const fn (ctx: *anyopaque, parent_ref: *anyopaque, NodeOrText) callconv(.c) void,
parseErrorCallback: *const fn (ctx: *anyopaque, StringSlice) callconv(.c) void,
popCallback: *const fn (ctx: *anyopaque, node_ref: *anyopaque) callconv(.c) void,
createCommentCallback: *const fn (ctx: *anyopaque, StringSlice) callconv(.c) ?*anyopaque,
createProcessingInstruction: *const fn (ctx: *anyopaque, StringSlice, StringSlice) callconv(.c) ?*anyopaque,
appendDoctypeToDocument: *const fn (ctx: *anyopaque, StringSlice, StringSlice, StringSlice) callconv(.c) void,
addAttrsIfMissingCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque, AttributeIterator) callconv(.c) void,
getTemplateContentsCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque) callconv(.c) ?*anyopaque,
removeFromParentCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque) callconv(.c) void,
reparentChildrenCallback: *const fn (ctx: *anyopaque, node_ref: *anyopaque, new_parent_ref: *anyopaque) callconv(.c) void,
appendBeforeSiblingCallback: *const fn (ctx: *anyopaque, sibling_ref: *anyopaque, NodeOrText) callconv(.c) void,
appendBasedOnParentNodeCallback: *const fn (ctx: *anyopaque, element_ref: *anyopaque, prev_element_ref: *anyopaque, NodeOrText) callconv(.c) void,
) void;

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

View File

@@ -19,12 +19,13 @@
testing.expectEqual('http://www.w3.org/XML/1998/namespace', xmlElement.namespaceURI); testing.expectEqual('http://www.w3.org/XML/1998/namespace', xmlElement.namespaceURI);
const nullNsElement = document.createElementNS(null, 'span'); const nullNsElement = document.createElementNS(null, 'span');
testing.expectEqual('SPAN', nullNsElement.tagName); testing.expectEqual('span', nullNsElement.tagName);
testing.expectEqual('http://www.w3.org/1999/xhtml', nullNsElement.namespaceURI); testing.expectEqual(null, nullNsElement.namespaceURI);
const unknownNsElement = document.createElementNS('http://example.com/unknown', 'custom'); const unknownNsElement = document.createElementNS('http://example.com/unknown', 'custom');
testing.expectEqual('CUSTOM', unknownNsElement.tagName); testing.expectEqual('custom', unknownNsElement.tagName);
testing.expectEqual('http://www.w3.org/1999/xhtml', unknownNsElement.namespaceURI); // Should be http://example.com/unknown
testing.expectEqual('http://lightpanda.io/unsupported/namespace', unknownNsElement.namespaceURI);
const regularDiv = document.createElement('div'); const regularDiv = document.createElement('div');
testing.expectEqual('DIV', regularDiv.tagName); testing.expectEqual('DIV', regularDiv.tagName);
@@ -36,5 +37,5 @@
testing.expectEqual('te:ST', custom.tagName); testing.expectEqual('te:ST', custom.tagName);
testing.expectEqual('te', custom.prefix); testing.expectEqual('te', custom.prefix);
testing.expectEqual('ST', custom.localName); 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> </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; const root = doc.documentElement;
testing.expectEqual(true, root !== null); testing.expectEqual(true, root !== null);
// TODO: XML documents should preserve case, but we currently uppercase // TODO: XML documents should preserve case, but we currently uppercase
testing.expectEqual('ROOT', root.tagName); testing.expectEqual('root', root.tagName);
} }
</script> </script>
@@ -206,10 +206,9 @@
const doc = impl.createDocument('http://example.com', 'prefix:localName', null); const doc = impl.createDocument('http://example.com', 'prefix:localName', null);
const root = doc.documentElement; const root = doc.documentElement;
// TODO: XML documents should preserve case, but we currently uppercase testing.expectEqual('prefix:localName', root.tagName);
testing.expectEqual('prefix:LOCALNAME', root.tagName); // TODO: Custom namespaces are being replaced with an empty value
// TODO: Custom namespaces are being overridden to XHTML namespace testing.expectEqual('http://lightpanda.io/unsupported/namespace', root.namespaceURI);
testing.expectEqual('http://www.w3.org/1999/xhtml', root.namespaceURI);
} }
</script> </script>
@@ -224,8 +223,7 @@
doc.documentElement.appendChild(child); doc.documentElement.appendChild(child);
testing.expectEqual(1, doc.documentElement.childNodes.length); 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); testing.expectEqual('Test', doc.documentElement.firstChild.textContent);
} }
</script> </script>

View File

@@ -107,19 +107,6 @@
} }
</script> </script>
<script id=unsupportedMimeType>
{
const parser = new DOMParser();
// Should throw an error for unsupported MIME types
testing.withError((err) => {
testing.expectEqual('NotSupported', err.message);
}, () => {
parser.parseFromString('<div>test</div>', 'application/xml');
});
}
</script>
<script id=getElementById> <script id=getElementById>
{ {
const doc = new DOMParser().parseFromString('<div id="new-node">new-node</div>', 'text/html'); const doc = new DOMParser().parseFromString('<div id="new-node">new-node</div>', 'text/html');
@@ -244,3 +231,161 @@
testing.expectEqual('<html><head></head><body>spice</body></html>', new DOMParser().parseFromString('spice', "text/html").documentElement.outerHTML); testing.expectEqual('<html><head></head><body>spice</body></html>', new DOMParser().parseFromString('spice', "text/html").documentElement.outerHTML);
testing.expectEqual('<html><head></head><body></body></html>', new DOMParser().parseFromString('<html></html>', "text/html").documentElement.outerHTML); testing.expectEqual('<html><head></head><body></body></html>', new DOMParser().parseFromString('<html></html>', "text/html").documentElement.outerHTML);
</script> </script>
<script id=parse-xml>
{
const sampleXML = `<?xml version="1.0"?>
<catalog>
<book id="bk101">
<author>Gambardella, Matthew</author>
<title>XML Developer's Guide</title>
<genre>Computer</genre>
<price>44.95</price>
<publish_date>2000-10-01</publish_date>
<description>An in-depth look at creating applications
with XML.</description>
</book>
<book id="bk102">
<author>Ralls, Kim</author>
<title>Midnight Rain</title>
<genre>Fantasy</genre>
<price>5.95</price>
<publish_date>2000-12-16</publish_date>
<description>A former architect battles corporate zombies,
an evil sorceress, and her own childhood to become queen
of the world.</description>
</book>
<book id="bk103">
<author>Corets, Eva</author>
<title>Maeve Ascendant</title>
<genre>Fantasy</genre>
<price>5.95</price>
<publish_date>2000-11-17</publish_date>
<description>After the collapse of a nanotechnology
society in England, the young survivors lay the
foundation for a new society.</description>
</book>
<book id="bk104">
<author>Corets, Eva</author>
<title>Oberon's Legacy</title>
<genre>Fantasy</genre>
<price>5.95</price>
<publish_date>2001-03-10</publish_date>
<description>In post-apocalypse England, the mysterious
agent known only as Oberon helps to create a new life
for the inhabitants of London. Sequel to Maeve
Ascendant.</description>
</book>
<book id="bk105">
<author>Corets, Eva</author>
<title>The Sundered Grail</title>
<genre>Fantasy</genre>
<price>5.95</price>
<publish_date>2001-09-10</publish_date>
<description>The two daughters of Maeve, half-sisters,
battle one another for control of England. Sequel to
Oberon's Legacy.</description>
</book>
<book id="bk106">
<author>Randall, Cynthia</author>
<title>Lover Birds</title>
<genre>Romance</genre>
<price>4.95</price>
<publish_date>2000-09-02</publish_date>
<description>When Carla meets Paul at an ornithology
conference, tempers fly as feathers get ruffled.</description>
</book>
<book id="bk107">
<author>Thurman, Paula</author>
<title>Splish Splash</title>
<genre>Romance</genre>
<price>4.95</price>
<publish_date>2000-11-02</publish_date>
<description>A deep sea diver finds true love twenty
thousand leagues beneath the sea.</description>
</book>
<book id="bk108">
<author>Knorr, Stefan</author>
<title>Creepy Crawlies</title>
<genre>Horror</genre>
<price>4.95</price>
<publish_date>2000-12-06</publish_date>
<description>An anthology of horror stories about roaches,
centipedes, scorpions and other insects.</description>
</book>
<book id="bk109">
<author>Kress, Peter</author>
<title>Paradox Lost</title>
<genre>Science Fiction</genre>
<price>6.95</price>
<publish_date>2000-11-02</publish_date>
<description>After an inadvertant trip through a Heisenberg
Uncertainty Device, James Salway discovers the problems
of being quantum.</description>
</book>
<book id="bk110">
<author>O'Brien, Tim</author>
<title>Microsoft .NET: The Programming Bible</title>
<genre>Computer</genre>
<price>36.95</price>
<publish_date>2000-12-09</publish_date>
<description>Microsoft's .NET initiative is explored in
detail in this deep programmer's reference.</description>
</book>
<book id="bk111">
<author>O'Brien, Tim</author>
<title>MSXML3: A Comprehensive Guide</title>
<genre>Computer</genre>
<price>36.95</price>
<publish_date>2000-12-01</publish_date>
<description>The Microsoft MSXML3 parser is covered in
detail, with attention to XML DOM interfaces, XSLT processing,
SAX and more.</description>
</book>
<book id="bk112">
<author>Galos, Mike</author>
<title>Visual Studio 7: A Comprehensive Guide</title>
<genre>Computer</genre>
<price>49.95</price>
<publish_date>2001-04-16</publish_date>
<description>Microsoft Visual Studio 7 is explored in depth,
looking at how Visual Basic, Visual C++, C#, and ASP+ are
integrated into a comprehensive development
environment.</description>
</book>
</catalog>`;
const parser = new DOMParser();
const mimes = [
"text/xml",
"application/xml",
"application/xhtml+xml",
"image/svg+xml",
];
for (const mime of mimes) {
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(25, childNodes.length);
testing.expectEqual(12, collection.length);
// Check children of first child.
for (let i = 0; i < collection.length; i++) {
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);
}
}
}
</script>

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id="test">first</div>
<div id="test">second</div>
<script id=duplicateIds>
const first = document.getElementById('test');
testing.expectEqual('first', first.textContent);
first.remove();
const second = document.getElementById('test');
testing.expectEqual('second', second.textContent);
// second.remove();
// testing.expectEqual(null, document.getElementById('test'));
</script>

View File

@@ -238,6 +238,15 @@
testing.expectEqual('[object HTMLAudioElement]', audio.toString()); testing.expectEqual('[object HTMLAudioElement]', audio.toString());
testing.expectEqual(true, audio.paused); 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>
<script id="create_video_element"> <script id="create_video_element">

View File

@@ -42,6 +42,34 @@
testing.expectEqual('initial text', $('#textarea1').defaultValue) testing.expectEqual('initial text', $('#textarea1').defaultValue)
</script> </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"> <script id="disabled_initial">
testing.expectEqual(false, $('#textarea1').disabled) testing.expectEqual(false, $('#textarea1').disabled)
testing.expectEqual(true, $('#textarea3').disabled) testing.expectEqual(true, $('#textarea3').disabled)
@@ -149,3 +177,55 @@
testing.expectFalse(textarea.outerHTML.includes('required')) testing.expectFalse(textarea.outerHTML.includes('required'))
} }
</script> </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

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id=d1>hello <em>world</em></div>
<script id=outerHTML>
const d1 = $('#d1');
testing.expectEqual('<div id=\"d1\">hello <em>world</em></div>', d1.outerHTML);
d1.outerHTML = '<p id=p1>spice</p>';
// setting outerHTML doesn't update what d1 points to
testing.expectEqual('<div id="d1">hello <em>world</em></div>', d1.outerHTML);
// but it does update the document
testing.expectEqual(null, document.getElementById('d1'));
testing.expectEqual(true, document.getElementById('p1') != null);
testing.expectEqual('<p id="p1">spice</p>', document.getElementById('p1').outerHTML);
// testing.expectEqual(true, document.body.outerHTML.replaceAll(/\n/g, '').startsWith('<body><p id="p1">spice</p><script id="outerHTML">'));
// document.getElementById('p1').outerHTML = '';
// testing.expectEqual(null, document.getElementById('p1'));
// testing.expectEqual(true, document.body.outerHTML.replaceAll(/\n/g, '').startsWith('<body><script id="outerHTML">'));
</script>

View File

@@ -0,0 +1,53 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script id="removeListenerDuringDispatch">
const target = document.createElement("div");
let listener1Called = 0;
let listener2Called = 0;
let listener3Called = 0;
function listener1() {
listener1Called++;
console.warn("listener1 called, removing listener2 and adding listener3");
target.removeEventListener("foo", listener2);
target.addEventListener("foo", listener3);
}
function listener2() {
listener2Called++;
console.warn("listener2 called (SHOULD NOT HAPPEN)");
}
function listener3() {
listener3Called++;
console.warn("listener3 called (SHOULD NOT HAPPEN IN FIRST DISPATCH)");
}
target.addEventListener("foo", listener1);
target.addEventListener("foo", listener2);
console.warn("Dispatching first event");
target.dispatchEvent(new Event("foo"));
console.warn("After first dispatch:");
console.warn(" listener1Called:", listener1Called);
console.warn(" listener2Called:", listener2Called);
console.warn(" listener3Called:", listener3Called);
testing.expectEqual(1, listener1Called);
testing.expectEqual(0, listener2Called);
testing.expectEqual(0, listener3Called);
console.warn("Dispatching second event");
target.dispatchEvent(new Event("foo"));
console.warn("After second dispatch:");
console.warn(" listener1Called:", listener1Called);
console.warn(" listener2Called:", listener2Called);
console.warn(" listener3Called:", listener3Called);
testing.expectEqual(2, listener1Called);
testing.expectEqual(0, listener2Called);
testing.expectEqual(1, listener3Called);
</script>

View File

@@ -630,3 +630,8 @@
let bubbledEvent = new Event('bubble', {bubbles: true}); let bubbledEvent = new Event('bubble', {bubbles: true});
testing.expectEqual(false, bubbledEvent.isTrusted); testing.expectEqual(false, bubbledEvent.isTrusted);
</script> </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(); history.back();
</script> </script>

View File

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

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<body></body>
<script id="adoptNode">
const old = document.implementation.createHTMLDocument("");
const div = old.createElement("div");
div.appendChild(old.createTextNode("text"));
testing.expectEqual(old, div.ownerDocument);
testing.expectEqual(old, div.firstChild.ownerDocument);
document.body.appendChild(div);
testing.expectEqual(document, div.ownerDocument);
testing.expectEqual(document, div.firstChild.ownerDocument);
</script>

View File

@@ -37,4 +37,7 @@
testing.expectEqual(null, c2.parentNode); testing.expectEqual(null, c2.parentNode);
assertChildren([c3, c4], d1) assertChildren([c3, c4], d1)
assertChildren([], d2) assertChildren([], d2)
testing.expectEqual(c3, d1.replaceChild(c3, c3));
assertChildren([c3, c4], d1)
</script> </script>

View File

@@ -36,3 +36,36 @@
performance.mark("operationEnd", { startTime: 34.0 }); performance.mark("operationEnd", { startTime: 34.0 });
} }
</script> </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

@@ -376,3 +376,581 @@
testing.expectEqual('Bold', fragment.childNodes[0].textContent); testing.expectEqual('Bold', fragment.childNodes[0].textContent);
} }
</script> </script>
<script id=offset_validation_setStart>
{
const range = document.createRange();
const p1 = $('#p1');
// Test setStart with offset beyond node length
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
range.setStart(p1, 999);
});
// Test with negative offset (wraps to large u32)
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
range.setStart(p1.firstChild, -1);
});
}
</script>
<script id=offset_validation_setEnd>
{
const range = document.createRange();
const p1 = $('#p1');
// Test setEnd with offset beyond node length
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
range.setEnd(p1, 999);
});
// Test with text node
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
range.setEnd(p1.firstChild, 9999);
});
}
</script>
<script id=comparePoint_basic>
{
// Create fresh elements to avoid DOM pollution from other tests
const div = document.createElement('div');
const p1 = document.createElement('p');
const p2 = document.createElement('p');
p1.textContent = 'First paragraph text';
p2.textContent = 'Second paragraph text';
div.appendChild(p1);
div.appendChild(p2);
const range = document.createRange();
range.setStart(p1.firstChild, 0);
range.setEnd(p2.firstChild, 5);
// Point before range
testing.expectEqual(-1, range.comparePoint(div, 0));
// Point at start boundary
testing.expectEqual(0, range.comparePoint(p1.firstChild, 0));
// Point inside range (in p1)
testing.expectEqual(0, range.comparePoint(p1.firstChild, 3));
// Point inside range (in p2)
testing.expectEqual(0, range.comparePoint(p2.firstChild, 2));
// Point at end boundary
testing.expectEqual(0, range.comparePoint(p2.firstChild, 5));
// Point after range
testing.expectEqual(1, range.comparePoint(p2.firstChild, 10));
}
</script>
<script id=comparePoint_validation>
{
// Create fresh element
const p1 = document.createElement('p');
p1.textContent = 'Test content';
const range = document.createRange();
range.setStart(p1, 0);
range.setEnd(p1, 1);
// Test comparePoint with invalid offset
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
range.comparePoint(p1, 20);
});
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
range.comparePoint(p1.firstChild, -1);
});
}
</script>
<script id=different_document_collapse>
{
// Create fresh element in current document
const p1 = document.createElement('p');
p1.textContent = 'Local content';
const range = document.createRange();
// Create a foreign document
const foreignDoc = document.implementation.createHTMLDocument('');
const foreignP = foreignDoc.createElement('p');
foreignP.textContent = 'Foreign';
foreignDoc.body.appendChild(foreignP);
// Set up range in current document
range.setStart(p1, 0);
range.setEnd(p1, 1);
testing.expectEqual(false, range.collapsed);
// Setting start to foreign document should collapse to that point
range.setStart(foreignP, 0);
testing.expectEqual(true, range.collapsed);
testing.expectEqual(foreignP, range.startContainer);
testing.expectEqual(foreignP, range.endContainer);
}
</script>
<script id=detached_node_collapse>
{
// Create fresh element
const p1 = document.createElement('p');
p1.textContent = 'Attached content';
const range = document.createRange();
// Create a detached element
const detached = document.createElement('div');
detached.textContent = 'Detached';
// Set up range in document
range.setStart(p1, 0);
range.setEnd(p1, 1);
testing.expectEqual(false, range.collapsed);
// Setting end to detached node should collapse
range.setEnd(detached.firstChild, 0);
testing.expectEqual(true, range.collapsed);
testing.expectEqual(detached.firstChild, range.startContainer);
testing.expectEqual(detached.firstChild, range.endContainer);
}
</script>
<script id=isPointInRange_basic>
{
// Create fresh elements
const div = document.createElement('div');
const p1 = document.createElement('p');
const p2 = document.createElement('p');
p1.textContent = 'First paragraph';
p2.textContent = 'Second paragraph';
div.appendChild(p1);
div.appendChild(p2);
const range = document.createRange();
range.setStart(p1.firstChild, 5);
range.setEnd(p2.firstChild, 6);
// Point before range
testing.expectEqual(false, range.isPointInRange(p1.firstChild, 0));
// Point at start boundary
testing.expectEqual(true, range.isPointInRange(p1.firstChild, 5));
// Point inside range
testing.expectEqual(true, range.isPointInRange(p1.firstChild, 7));
testing.expectEqual(true, range.isPointInRange(p2.firstChild, 3));
// Point at end boundary
testing.expectEqual(true, range.isPointInRange(p2.firstChild, 6));
// Point after range
testing.expectEqual(false, range.isPointInRange(p2.firstChild, 10));
}
</script>
<script id=isPointInRange_different_root>
{
// Create element in current document
const p1 = document.createElement('p');
p1.textContent = 'Local content';
const range = document.createRange();
range.setStart(p1, 0);
range.setEnd(p1, 1);
// Create element in different document
const foreignDoc = document.implementation.createHTMLDocument('');
const foreignP = foreignDoc.createElement('p');
foreignP.textContent = 'Foreign';
// Point in different root should return false (not throw)
testing.expectEqual(false, range.isPointInRange(foreignP, 0));
}
</script>
<script id=isPointInRange_validation>
{
const p1 = document.createElement('p');
p1.textContent = 'Test content';
const range = document.createRange();
range.setStart(p1, 0);
range.setEnd(p1, 1);
// Invalid offset should throw IndexSizeError
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
range.isPointInRange(p1, 999);
});
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
range.isPointInRange(p1.firstChild, 9999);
});
}
</script>
<script id=intersectsNode_basic>
{
// Create fresh elements
const div = document.createElement('div');
const p1 = document.createElement('p');
const p2 = document.createElement('p');
const p3 = document.createElement('p');
p1.textContent = 'First';
p2.textContent = 'Second';
p3.textContent = 'Third';
div.appendChild(p1);
div.appendChild(p2);
div.appendChild(p3);
const range = document.createRange();
range.setStart(p1.firstChild, 2);
range.setEnd(p2.firstChild, 3);
// Node that intersects (p1 contains the start)
testing.expectEqual(true, range.intersectsNode(p1));
// Node that intersects (p2 contains the end)
testing.expectEqual(true, range.intersectsNode(p2));
// Node that doesn't intersect (p3 is after the range)
testing.expectEqual(false, range.intersectsNode(p3));
// Container intersects
testing.expectEqual(true, range.intersectsNode(div));
}
</script>
<script id=intersectsNode_detached>
{
const div = document.createElement('div');
const p1 = document.createElement('p');
p1.textContent = 'Content';
div.appendChild(p1);
const range = document.createRange();
range.setStart(p1, 0);
range.setEnd(p1, 1);
// The root node (div) should return true when it has no parent
// (Note: div is detached, so it's in the same tree as the range)
testing.expectEqual(true, range.intersectsNode(div));
}
</script>
<script id=intersectsNode_different_root>
{
const p1 = document.createElement('p');
p1.textContent = 'Local';
const range = document.createRange();
range.setStart(p1, 0);
range.setEnd(p1, 1);
// Node in different document should return false
const foreignDoc = document.implementation.createHTMLDocument('');
const foreignP = foreignDoc.createElement('p');
testing.expectEqual(false, range.intersectsNode(foreignP));
}
</script>
<script id=commonAncestorContainer_same_node>
{
const p = document.createElement('p');
p.textContent = 'Content';
const range = document.createRange();
range.setStart(p.firstChild, 0);
range.setEnd(p.firstChild, 3);
// Both boundaries in same text node, so that's the common ancestor
testing.expectEqual(p.firstChild, range.commonAncestorContainer);
}
</script>
<script id=commonAncestorContainer_siblings>
{
const div = document.createElement('div');
const p1 = document.createElement('p');
const p2 = document.createElement('p');
p1.textContent = 'First';
p2.textContent = 'Second';
div.appendChild(p1);
div.appendChild(p2);
const range = document.createRange();
range.setStart(p1.firstChild, 0);
range.setEnd(p2.firstChild, 3);
// Start and end in different siblings, common ancestor is the parent div
testing.expectEqual(div, range.commonAncestorContainer);
}
</script>
<script id=commonAncestorContainer_nested>
{
const div = document.createElement('div');
const section = document.createElement('section');
const p = document.createElement('p');
const span = document.createElement('span');
p.textContent = 'Text';
span.textContent = 'Span';
div.appendChild(section);
section.appendChild(p);
div.appendChild(span);
const range = document.createRange();
range.setStart(p.firstChild, 0);
range.setEnd(span.firstChild, 2);
// Common ancestor of deeply nested p and sibling span is div
testing.expectEqual(div, range.commonAncestorContainer);
}
</script>
<script id=compareBoundaryPoints_constants>
{
// Test that the constants are defined
testing.expectEqual(0, Range.START_TO_START);
testing.expectEqual(1, Range.START_TO_END);
testing.expectEqual(2, Range.END_TO_END);
testing.expectEqual(3, Range.END_TO_START);
}
</script>
<script id=compareBoundaryPoints_basic>
{
const div = document.createElement('div');
const p1 = document.createElement('p');
const p2 = document.createElement('p');
p1.textContent = 'First paragraph';
p2.textContent = 'Second paragraph';
div.appendChild(p1);
div.appendChild(p2);
const range1 = document.createRange();
range1.setStart(p1.firstChild, 0);
range1.setEnd(p1.firstChild, 5);
const range2 = document.createRange();
range2.setStart(p1.firstChild, 3);
range2.setEnd(p2.firstChild, 5);
// range1 start is before range2 start
testing.expectEqual(-1, range1.compareBoundaryPoints(Range.START_TO_START, range2));
// range1 start is before range2 end
testing.expectEqual(-1, range1.compareBoundaryPoints(Range.START_TO_END, range2));
// range1 end is after range2 start
testing.expectEqual(1, range1.compareBoundaryPoints(Range.END_TO_START, range2));
// range1 end is before range2 end
testing.expectEqual(-1, range1.compareBoundaryPoints(Range.END_TO_END, range2));
}
</script>
<script id=compareBoundaryPoints_same_range>
{
const p = document.createElement('p');
p.textContent = 'Content';
const range = document.createRange();
range.setStart(p.firstChild, 2);
range.setEnd(p.firstChild, 5);
// Comparing a range to itself should return 0
testing.expectEqual(0, range.compareBoundaryPoints(Range.START_TO_START, range));
testing.expectEqual(0, range.compareBoundaryPoints(Range.END_TO_END, range));
// Start is before end
testing.expectEqual(-1, range.compareBoundaryPoints(Range.START_TO_END, range));
// End is after start
testing.expectEqual(1, range.compareBoundaryPoints(Range.END_TO_START, range));
}
</script>
<script id=compareBoundaryPoints_invalid_how>
{
const p = document.createElement('p');
p.textContent = 'Test';
const range1 = document.createRange();
const range2 = document.createRange();
range1.setStart(p, 0);
range2.setStart(p, 0);
// Invalid how parameter should throw NotSupportedError
testing.expectError('NotSupportedError: Not Supported', () => {
range1.compareBoundaryPoints(4, range2);
});
testing.expectError('NotSupportedError: Not Supported', () => {
range1.compareBoundaryPoints(99, range2);
});
}
</script>
<script id=compareBoundaryPoints_different_root>
{
const p1 = document.createElement('p');
p1.textContent = 'Local';
const range1 = document.createRange();
range1.setStart(p1, 0);
range1.setEnd(p1, 1);
// Create range in different document
const foreignDoc = document.implementation.createHTMLDocument('');
const foreignP = foreignDoc.createElement('p');
foreignP.textContent = 'Foreign';
const range2 = foreignDoc.createRange();
range2.setStart(foreignP, 0);
range2.setEnd(foreignP, 1);
// Comparing ranges in different documents should throw WrongDocumentError
testing.expectError('WrongDocumentError: wrong_document_error', () => {
range1.compareBoundaryPoints(Range.START_TO_START, range2);
});
}
</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) { function expectError(expected, fn) {
withError((err) => { withError((err) => {
expectEqual(expected, err.toString()); expectEqual(true, err.toString().includes(expected));
}, fn); }, fn);
} }

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id=i></div>
<div id=testDiv></div>
<span id=mySpan></span>
<p id=paragraph></p>
<script id=named_access_global>
testing.expectEqual('i', i.id);
testing.expectEqual('testDiv',testDiv.id);
testing.expectEqual('mySpan', mySpan.id);
testing.expectEqual('paragraph', paragraph.id);
</script>
<script id=named_access_window>
testing.expectEqual('i', window.i.id);
testing.expectEqual('testDiv', window.testDiv.id);
testing.expectEqual('mySpan', window.mySpan.id);
testing.expectEqual('paragraph', window.paragraph.id);
</script>
<script id=named_access_shadowing>
const i = 100;
testing.expectEqual(100, i);
</script>

View File

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

View File

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

View File

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

View File

@@ -69,6 +69,19 @@ pub fn getCollapsed(self: *const AbstractRange) bool {
self._start_offset == self._end_offset; self._start_offset == self._end_offset;
} }
pub fn getCommonAncestorContainer(self: *const AbstractRange) *Node {
// Let container be start container
var container = self._start_container;
// While container is not an inclusive ancestor of end container
while (!isInclusiveAncestorOf(container, self._end_container)) {
// Let container be container's parent
container = container.parentNode() orelse break;
}
return container;
}
pub fn isStartAfterEnd(self: *const AbstractRange) bool { pub fn isStartAfterEnd(self: *const AbstractRange) bool {
return compareBoundaryPoints( return compareBoundaryPoints(
self._start_container, self._start_container,
@@ -84,7 +97,7 @@ const BoundaryComparison = enum {
after, after,
}; };
fn compareBoundaryPoints( pub fn compareBoundaryPoints(
node_a: *Node, node_a: *Node,
offset_a: u32, offset_a: u32,
node_b: *Node, node_b: *Node,
@@ -195,6 +208,13 @@ fn isAncestorOf(potential_ancestor: *Node, node: *Node) bool {
return false; return false;
} }
fn isInclusiveAncestorOf(potential_ancestor: *Node, node: *Node) bool {
if (potential_ancestor == node) {
return true;
}
return isAncestorOf(potential_ancestor, node);
}
pub const JsApi = struct { pub const JsApi = struct {
pub const bridge = js.Bridge(AbstractRange); pub const bridge = js.Bridge(AbstractRange);
@@ -209,4 +229,5 @@ pub const JsApi = struct {
pub const endContainer = bridge.accessor(AbstractRange.getEndContainer, null, .{}); pub const endContainer = bridge.accessor(AbstractRange.getEndContainer, null, .{});
pub const endOffset = bridge.accessor(AbstractRange.getEndOffset, null, .{}); pub const endOffset = bridge.accessor(AbstractRange.getEndOffset, null, .{});
pub const collapsed = bridge.accessor(AbstractRange.getCollapsed, null, .{}); pub const collapsed = bridge.accessor(AbstractRange.getCollapsed, null, .{});
pub const commonAncestorContainer = bridge.accessor(AbstractRange.getCommonAncestorContainer, null, .{});
}; };

View File

@@ -55,15 +55,6 @@ pub fn is(self: *CData, comptime T: type) ?*T {
return null; 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 { pub fn getData(self: *const CData) []const u8 {
return self._data; return self._data;
} }
@@ -147,7 +138,7 @@ pub fn format(self: *const CData, writer: *std.io.Writer) !void {
} }
pub fn getLength(self: *const CData) usize { pub fn getLength(self: *const CData) usize {
return self._data.len; return std.unicode.utf8CountCodepoints(self._data) catch self._data.len;
} }
pub fn isEqualNode(self: *const CData, other: *const CData) bool { pub fn isEqualNode(self: *const CData, other: *const CData) bool {

View File

@@ -23,25 +23,114 @@ const Page = @import("../Page.zig");
const logger = @import("../../log.zig"); const logger = @import("../../log.zig");
const Console = @This(); const Console = @This();
_pad: bool = false,
_timers: std.StringHashMapUnmanaged(u64) = .{},
_counts: std.StringHashMapUnmanaged(u64) = .{},
pub const init: Console = .{}; 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 }}); 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 }}); 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 }}); 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 { const ValueWriter = struct {
page: *Page, page: *Page,
values: []js.Object, values: []js.Value,
include_stack: bool = false, include_stack: bool = false,
pub fn format(self: ValueWriter, writer: *std.io.Writer) !void { pub fn format(self: ValueWriter, writer: *std.io.Writer) !void {
@@ -57,7 +146,7 @@ const ValueWriter = struct {
var buf: [32]u8 = undefined; var buf: [32]u8 = undefined;
for (self.values, 0..) |value, i| { for (self.values, 0..) |value, i| {
const name = try std.fmt.bufPrint(&buf, "param.{d}", .{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 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 log = bridge.function(Console.log, .{});
pub const warn = bridge.function(Console.warn, .{}); 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 @"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 std = @import("std");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const SubtleCrypto = @import("SubtleCrypto.zig");
const Crypto = @This(); const Crypto = @This();
_pad: bool = false, _subtle: SubtleCrypto = .{},
pub const init: Crypto = .{}; pub const init: Crypto = .{};
@@ -42,6 +46,10 @@ pub fn randomUUID(_: *const Crypto) ![36]u8 {
return hex; return hex;
} }
pub fn getSubtle(self: *Crypto) *SubtleCrypto {
return &self._subtle;
}
const RandomValues = union(enum) { const RandomValues = union(enum) {
int8: []i8, int8: []i8,
uint8: []u8, uint8: []u8,
@@ -78,6 +86,7 @@ pub const JsApi = struct {
pub const getRandomValues = bridge.function(Crypto.getRandomValues, .{}); pub const getRandomValues = bridge.function(Crypto.getRandomValues, .{});
pub const randomUUID = bridge.function(Crypto.randomUUID, .{}); pub const randomUUID = bridge.function(Crypto.randomUUID, .{});
pub const subtle = bridge.accessor(Crypto.getSubtle, null, .{});
}; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");

View File

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

View File

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

View File

@@ -28,19 +28,7 @@ const DOMImplementation = @This();
_pad: bool = false, _pad: bool = false,
pub fn createDocumentType(_: *const DOMImplementation, qualified_name: []const u8, public_id: ?[]const u8, system_id: ?[]const u8, page: *Page) !*DocumentType { 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); return DocumentType.init(qualified_name, public_id, system_id, page);
// 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;
} }
pub fn createHTMLDocument(_: *const DOMImplementation, title: ?[]const u8, page: *Page) !*Document { 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); _ = 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); _ = 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); _ = try html_node.appendChild(head_node, page);
if (title) |t| { 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); _ = try head_node.appendChild(title_node, page);
const text_node = try page.createTextNode(t); const text_node = try page.createTextNode(t);
_ = try title_node.appendChild(text_node, page); _ = 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); _ = try html_node.appendChild(body_node, page);
return document; 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 // Create XML Document
const document = (try page._factory.document(Node.Document.XMLDocument{ ._proto = undefined })).asDocument(); 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 // Create and append root element if qualified_name provided
if (qualified_name) |qname| { if (qualified_name) |qname| {
if (qname.len > 0) { 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); _ = try document.asNode().appendChild(root, page);
} }
} }
@@ -102,10 +91,6 @@ pub fn hasFeature(_: *const DOMImplementation, _: ?[]const u8, _: ?[]const u8) b
return true; return true;
} }
pub fn className(_: *const DOMImplementation) []const u8 {
return "[object DOMImplementation]";
}
pub const JsApi = struct { pub const JsApi = struct {
pub const bridge = js.Bridge(DOMImplementation); pub const bridge = js.Bridge(DOMImplementation);
@@ -120,11 +105,6 @@ pub const JsApi = struct {
pub const createDocument = bridge.function(DOMImplementation.createDocument, .{}); pub const createDocument = bridge.function(DOMImplementation.createDocument, .{});
pub const createHTMLDocument = bridge.function(DOMImplementation.createHTMLDocument, .{}); pub const createHTMLDocument = bridge.function(DOMImplementation.createHTMLDocument, .{});
pub const hasFeature = bridge.function(DOMImplementation.hasFeature, .{}); pub const hasFeature = bridge.function(DOMImplementation.hasFeature, .{});
pub const toString = bridge.function(_toString, .{});
fn _toString(_: *const DOMImplementation) []const u8 {
return "[object DOMImplementation]";
}
}; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");

View File

@@ -19,8 +19,14 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const Parser = @import("../parser/Parser.zig");
const HTMLDocument = @import("HTMLDocument.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(); const DOMParser = @This();
@@ -28,34 +34,71 @@ pub fn init() DOMParser {
return .{}; return .{};
} }
pub fn parseFromString(self: *const DOMParser, html: []const u8, mime_type: []const u8, page: *Page) !*HTMLDocument { pub fn parseFromString(
_ = self; _: *const DOMParser,
html: []const u8,
mime_type: []const u8,
page: *Page,
) !*Document {
const target_mime = std.meta.stringToEnum(enum {
@"text/html",
@"text/xml",
@"application/xml",
@"application/xhtml+xml",
@"image/svg+xml",
}, mime_type) orelse return error.NotSupported;
// For now, only support text/html return switch (target_mime) {
if (!std.mem.eql(u8, mime_type, "text/html")) { .@"text/html" => {
return error.NotSupported; // Create a new HTMLDocument
} const doc = try page._factory.document(HTMLDocument{
._proto = undefined,
});
// Create a new HTMLDocument var normalized = std.mem.trim(u8, html, &std.ascii.whitespace);
const doc = try page._factory.document(HTMLDocument{ if (normalized.len == 0) {
._proto = undefined, normalized = "<html></html>";
}); }
var normalized = std.mem.trim(u8, html, &std.ascii.whitespace); // Parse HTML into the document
if (normalized.len == 0) { var parser = Parser.init(page.arena, doc.asNode(), page);
normalized = "<html></html>"; parser.parse(normalized);
}
// Parse HTML into the document if (parser.err) |pe| {
const Parser = @import("../parser/Parser.zig"); return pe.err;
var parser = Parser.init(page.arena, doc.asNode(), page); }
parser.parse(normalized);
if (parser.err) |pe| { return doc.asDocument();
return pe.err; },
} else => {
// Create a new XMLDocument.
const doc = try page._factory.document(XMLDocument{
._proto = undefined,
});
return doc; // Parse XML into XMLDocument.
const doc_node = doc.asNode();
var parser = Parser.init(page.arena, doc_node, page);
parser.parseXML(html);
if (parser.err) |pe| {
return pe.err;
}
// If first node is a `ProcessingInstruction`, skip it.
const first_child = doc_node.firstChild() orelse {
// Parsing should fail if there aren't any nodes.
unreachable;
};
if (first_child.getNodeType() == 7) {
// We're sure that firstChild exist, this cannot fail.
_ = doc_node.removeChild(first_child, page) catch unreachable;
}
return doc.asDocument();
},
};
} }
pub const JsApi = struct { pub const JsApi = struct {

View File

@@ -79,25 +79,81 @@ pub fn parentNode(self: *DOMTreeWalker) !?*Node {
pub fn firstChild(self: *DOMTreeWalker) !?*Node { pub fn firstChild(self: *DOMTreeWalker) !?*Node {
var node = self._current.firstChild(); var node = self._current.firstChild();
while (node) |n| { while (node) |n| {
if (try self.acceptNode(n) == NodeFilter.FILTER_ACCEPT) { const filter_result = try self.acceptNode(n);
if (filter_result == NodeFilter.FILTER_ACCEPT) {
self._current = n; self._current = n;
return n; return n;
} }
node = self.nextSiblingOrNull(n);
if (filter_result == NodeFilter.FILTER_SKIP) {
// Descend into children of this skipped node
if (n.firstChild()) |child| {
node = child;
continue;
}
}
// REJECT or SKIP with no children - find next sibling, walking up if necessary
var current_node = n;
while (true) {
if (current_node.nextSibling()) |sibling| {
node = sibling;
break;
}
// No sibling, go up to parent
const parent = current_node._parent orelse return null;
if (parent == self._current) {
// We've exhausted all children of self._current
return null;
}
current_node = parent;
}
} }
return null; return null;
} }
pub fn lastChild(self: *DOMTreeWalker) !?*Node { pub fn lastChild(self: *DOMTreeWalker) !?*Node {
var node = self._current.lastChild(); var node = self._current.lastChild();
while (node) |n| { while (node) |n| {
if (try self.acceptNode(n) == NodeFilter.FILTER_ACCEPT) { const filter_result = try self.acceptNode(n);
if (filter_result == NodeFilter.FILTER_ACCEPT) {
self._current = n; self._current = n;
return n; return n;
} }
node = self.previousSiblingOrNull(n);
if (filter_result == NodeFilter.FILTER_SKIP) {
// Descend into children of this skipped node
if (n.lastChild()) |child| {
node = child;
continue;
}
}
// REJECT or SKIP with no children - find previous sibling, walking up if necessary
var current_node = n;
while (true) {
if (current_node.previousSibling()) |sibling| {
node = sibling;
break;
}
// No sibling, go up to parent
const parent = current_node._parent orelse return null;
if (parent == self._current) {
// We've exhausted all children of self._current
return null;
}
current_node = parent;
}
} }
return null; return null;
} }
@@ -131,15 +187,39 @@ pub fn previousNode(self: *DOMTreeWalker) !?*Node {
var sibling = self.previousSiblingOrNull(node); var sibling = self.previousSiblingOrNull(node);
while (sibling) |sib| { while (sibling) |sib| {
node = sib; node = sib;
var child = self.lastChildOrNull(node);
while (child) |c| { // Check if this sibling is rejected before descending into it
if (self.isInSubtree(c)) { const sib_result = try self.acceptNode(node);
node = c; if (sib_result == NodeFilter.FILTER_REJECT) {
child = self.lastChildOrNull(node); // Skip this sibling and its descendants entirely
} else { sibling = self.previousSiblingOrNull(node);
break; continue;
}
} }
// Descend to the deepest last child, but respect FILTER_REJECT
while (true) {
var child = self.lastChildOrNull(node);
// Find the rightmost non-rejected child
while (child) |c| {
if (!self.isInSubtree(c)) break;
const filter_result = try self.acceptNode(c);
if (filter_result == NodeFilter.FILTER_REJECT) {
// Skip this child and try its previous sibling
child = self.previousSiblingOrNull(c);
} else {
// ACCEPT or SKIP - use this child
break;
}
}
if (child == null) break; // No acceptable children
// Descend into this child
node = child.?;
}
if (try self.acceptNode(node) == NodeFilter.FILTER_ACCEPT) { if (try self.acceptNode(node) == NodeFilter.FILTER_ACCEPT) {
self._current = node; self._current = node;
return node; return node;

View File

@@ -48,11 +48,13 @@ _location: ?*Location = null,
_ready_state: ReadyState = .loading, _ready_state: ReadyState = .loading,
_current_script: ?*Element.Html.Script = null, _current_script: ?*Element.Html.Script = null,
_elements_by_id: std.StringHashMapUnmanaged(*Element) = .empty, _elements_by_id: std.StringHashMapUnmanaged(*Element) = .empty,
// Track IDs that were removed from the map - they might have duplicates in the tree
_removed_ids: std.StringHashMapUnmanaged(void) = .empty,
_active_element: ?*Element = null, _active_element: ?*Element = null,
_style_sheets: ?*StyleSheetList = null, _style_sheets: ?*StyleSheetList = null,
_write_insertion_point: ?*Node = null, _write_insertion_point: ?*Node = null,
_script_created_parser: ?Parser.Streaming = 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) { pub const Type = union(enum) {
generic, generic,
@@ -121,10 +123,23 @@ const CreateElementOptions = struct {
is: ?[]const u8 = null, is: ?[]const u8 = null,
}; };
pub fn createElement(_: *const Document, name: []const u8, options_: ?CreateElementOptions, page: *Page) !*Element { 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); const element = node.as(Element);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
}
const options = options_ orelse return element; const options = options_ orelse return element;
if (options.is) |is_value| { if (options.is) |is_value| {
try element.setAttribute("is", is_value, page); try element.setAttribute("is", is_value, page);
@@ -134,8 +149,14 @@ pub fn createElement(_: *const Document, name: []const u8, options_: ?CreateElem
return element; return element;
} }
pub fn createElementNS(_: *const Document, namespace: ?[]const u8, name: []const u8, page: *Page) !*Element { 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) {
try page.setNodeOwnerDocument(node, self);
}
return node.as(Element); return node.as(Element);
} }
@@ -163,9 +184,32 @@ pub fn createAttributeNS(_: *const Document, namespace: []const u8, name: []cons
}); });
} }
pub fn getElementById(self: *const Document, id_: ?[]const u8) ?*Element { pub fn getElementById(self: *Document, id: []const u8, page: *Page) ?*Element {
const id = id_ orelse return null; if (id.len == 0) {
return self._elements_by_id.get(id); return null;
}
if (self._elements_by_id.get(id)) |element| {
return element;
}
//ID was removed but might have duplicates
if (self._removed_ids.remove(id)) {
var tw = @import("TreeWalker.zig").Full.Elements.init(self.asNode(), .{});
while (tw.next()) |el| {
const element_id = el.getAttributeSafe("id") orelse continue;
if (std.mem.eql(u8, element_id, id)) {
// we ignore this error to keep getElementById easy to call
// if it really failed, then we're out of memory and nothing's
// going to work like it should anyways.
const owned_id = page.dupeString(id) catch return null;
self._elements_by_id.put(page.arena, owned_id, el) catch return null;
return el;
}
}
}
return null;
} }
const GetElementsByTagNameResult = union(enum) { const GetElementsByTagNameResult = union(enum) {
@@ -240,40 +284,57 @@ pub fn querySelectorAll(self: *Document, input: []const u8, page: *Page) !*Selec
return Selector.querySelectorAll(self.asNode(), input, page); 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 { pub fn getImplementation(_: *const Document) DOMImplementation {
return .{}; return .{};
} }
pub fn createDocumentFragment(_: *const Document, page: *Page) !*Node.DocumentFragment { pub fn createDocumentFragment(self: *Document, page: *Page) !*Node.DocumentFragment {
return Node.DocumentFragment.init(page); const frag = try Node.DocumentFragment.init(page);
} // Track owner document if it's not the main document
if (self != page.document) {
pub fn createComment(_: *const Document, data: []const u8, page: *Page) !*Node { try page.setNodeOwnerDocument(frag.asNode(), self);
return page.createComment(data);
}
pub fn createTextNode(_: *const Document, data: []const u8, page: *Page) !*Node {
return page.createTextNode(data);
}
pub fn createCDATASection(self: *const Document, data: []const u8, page: *Page) !*Node {
switch (self._type) {
.html => return error.NotSupported, // cannot create a CDataSection in an HTMLDocument
.xml => return page.createCDATASection(data),
.generic => return page.createCDATASection(data),
} }
return frag;
} }
pub fn createProcessingInstruction(_: *const Document, target: []const u8, data: []const u8, page: *Page) !*Node { pub fn createComment(self: *Document, data: []const u8, page: *Page) !*Node {
return page.createProcessingInstruction(target, data); const node = try page.createComment(data);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
}
return node;
}
pub fn createTextNode(self: *Document, data: []const u8, page: *Page) !*Node {
const node = try page.createTextNode(data);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
}
return node;
}
pub fn createCDATASection(self: *Document, data: []const u8, page: *Page) !*Node {
const node = switch (self._type) {
.html => return error.NotSupported, // cannot create a CDataSection in an HTMLDocument
.xml => try page.createCDATASection(data),
.generic => try page.createCDATASection(data),
};
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
}
return node;
}
pub fn createProcessingInstruction(self: *Document, target: []const u8, data: []const u8, page: *Page) !*Node {
const node = try page.createProcessingInstruction(target, data);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
}
return node;
} }
const Range = @import("Range.zig"); const Range = @import("Range.zig");
@@ -301,14 +362,26 @@ pub fn createEvent(_: *const Document, event_type: []const u8, page: *Page) !*@i
return error.NotSupported; return error.NotSupported;
} }
pub fn createTreeWalker(_: *const Document, root: *Node, what_to_show: ?u32, filter: ?DOMTreeWalker.FilterOpts, page: *Page) !*DOMTreeWalker { pub fn createTreeWalker(_: *const Document, root: *Node, what_to_show: ?js.Value, filter: ?DOMTreeWalker.FilterOpts, page: *Page) !*DOMTreeWalker {
const show = what_to_show orelse NodeFilter.SHOW_ALL; return DOMTreeWalker.init(root, try whatToShow(what_to_show), filter, page);
return DOMTreeWalker.init(root, show, filter, page);
} }
pub fn createNodeIterator(_: *const Document, root: *Node, what_to_show: ?u32, filter: ?DOMNodeIterator.FilterOpts, page: *Page) !*DOMNodeIterator { pub fn createNodeIterator(_: *const Document, root: *Node, what_to_show: ?js.Value, filter: ?DOMNodeIterator.FilterOpts, page: *Page) !*DOMNodeIterator {
const show = what_to_show orelse NodeFilter.SHOW_ALL; return DOMNodeIterator.init(root, try whatToShow(what_to_show), filter, page);
return DOMNodeIterator.init(root, show, filter, page); }
fn whatToShow(value_: ?js.Value) !u32 {
const value = value_ orelse return 4294967295; // show all when undefined
if (value.isUndefined()) {
// undefined explicitly passed
return 4294967295;
}
if (value.isNull()) {
return 0;
}
return value.toZig(u32);
} }
pub fn getReadyState(self: *const Document) []const u8 { pub fn getReadyState(self: *const Document) []const u8 {
@@ -361,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 { 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 = self.asNode();
const parent_is_connected = parent.isConnected();
for (nodes) |node_or_text| { for (nodes) |node_or_text| {
const child = try node_or_text.toNode(page); 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 { 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 = self.asNode();
const parent_is_connected = parent.isConnected();
var i = nodes.len; var i = nodes.len;
while (i > 0) { while (i > 0) {
i -= 1; i -= 1;
const child = try nodes[i].toNode(page); 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 });
} }
} }
@@ -420,7 +576,13 @@ pub fn elementsFromPoint(self: *Document, x: f64, y: f64, page: *Page) ![]const
return result.items; 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; return null;
} }
@@ -604,13 +766,14 @@ pub fn getChildElementCount(self: *Document) u32 {
return i; 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| { if (self._adopted_style_sheets) |ass| {
return ass; return ass;
} }
const obj = try page.js.createArray(0).persist(); const js_arr = page.js.newArray(0);
self._adopted_style_sheets = obj; const js_obj = js_arr.toObject();
return obj; self._adopted_style_sheets = try js_obj.persist();
return self._adopted_style_sheets.?;
} }
pub fn hasFocus(_: *Document) bool { pub fn hasFocus(_: *Document) bool {
@@ -622,6 +785,118 @@ pub fn setAdoptedStyleSheets(self: *Document, sheets: js.Object) !void {
self._adopted_style_sheets = try sheets.persist(); 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 { const ReadyState = enum {
loading, loading,
interactive, interactive,
@@ -660,8 +935,8 @@ pub const JsApi = struct {
pub const compatMode = bridge.accessor(Document.getCompatMode, null, .{}); pub const compatMode = bridge.accessor(Document.getCompatMode, null, .{});
pub const referrer = bridge.accessor(Document.getReferrer, null, .{}); pub const referrer = bridge.accessor(Document.getReferrer, null, .{});
pub const domain = bridge.accessor(Document.getDomain, null, .{}); pub const domain = bridge.accessor(Document.getDomain, null, .{});
pub const createElement = bridge.function(Document.createElement, .{}); pub const createElement = bridge.function(Document.createElement, .{ .dom_exception = true });
pub const createElementNS = bridge.function(Document.createElementNS, .{}); pub const createElementNS = bridge.function(Document.createElementNS, .{ .dom_exception = true });
pub const createDocumentFragment = bridge.function(Document.createDocumentFragment, .{}); pub const createDocumentFragment = bridge.function(Document.createDocumentFragment, .{});
pub const createComment = bridge.function(Document.createComment, .{}); pub const createComment = bridge.function(Document.createComment, .{});
pub const createTextNode = bridge.function(Document.createTextNode, .{}); pub const createTextNode = bridge.function(Document.createTextNode, .{});
@@ -673,7 +948,17 @@ pub const JsApi = struct {
pub const createEvent = bridge.function(Document.createEvent, .{ .dom_exception = true }); pub const createEvent = bridge.function(Document.createEvent, .{ .dom_exception = true });
pub const createTreeWalker = bridge.function(Document.createTreeWalker, .{}); pub const createTreeWalker = bridge.function(Document.createTreeWalker, .{});
pub const createNodeIterator = bridge.function(Document.createNodeIterator, .{}); pub const createNodeIterator = bridge.function(Document.createNodeIterator, .{});
pub const getElementById = bridge.function(Document.getElementById, .{}); pub const getElementById = bridge.function(_getElementById, .{});
fn _getElementById(self: *Document, value_: ?js.Value, page: *Page) !?*Element {
const value = value_ orelse return null;
if (value.isNull()) {
return self.getElementById("null", page);
}
if (value.isUndefined()) {
return self.getElementById("undefined", page);
}
return self.getElementById(try value.toZig([]const u8), page);
}
pub const querySelector = bridge.function(Document.querySelector, .{ .dom_exception = true }); pub const querySelector = bridge.function(Document.querySelector, .{ .dom_exception = true });
pub const querySelectorAll = bridge.function(Document.querySelectorAll, .{ .dom_exception = true }); pub const querySelectorAll = bridge.function(Document.querySelectorAll, .{ .dom_exception = true });
pub const getElementsByTagName = bridge.function(Document.getElementsByTagName, .{}); pub const getElementsByTagName = bridge.function(Document.getElementsByTagName, .{});
@@ -681,8 +966,9 @@ pub const JsApi = struct {
pub const getElementsByName = bridge.function(Document.getElementsByName, .{}); pub const getElementsByName = bridge.function(Document.getElementsByName, .{});
pub const adoptNode = bridge.function(Document.adoptNode, .{ .dom_exception = true }); pub const adoptNode = bridge.function(Document.adoptNode, .{ .dom_exception = true });
pub const importNode = bridge.function(Document.importNode, .{ .dom_exception = true }); pub const importNode = bridge.function(Document.importNode, .{ .dom_exception = true });
pub const append = bridge.function(Document.append, .{}); pub const append = bridge.function(Document.append, .{ .dom_exception = true });
pub const prepend = bridge.function(Document.prepend, .{}); 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 elementFromPoint = bridge.function(Document.elementFromPoint, .{});
pub const elementsFromPoint = bridge.function(Document.elementsFromPoint, .{}); pub const elementsFromPoint = bridge.function(Document.elementsFromPoint, .{});
pub const write = bridge.function(Document.write, .{ .dom_exception = true }); pub const write = bridge.function(Document.write, .{ .dom_exception = true });

View File

@@ -67,12 +67,10 @@ pub fn asEventTarget(self: *DocumentFragment) *@import("EventTarget.zig") {
return self._proto.asEventTarget(); return self._proto.asEventTarget();
} }
pub fn className(_: *const DocumentFragment) []const u8 { pub fn getElementById(self: *DocumentFragment, id: []const u8) ?*Element {
return "[object DocumentFragment]"; if (id.len == 0) {
} return null;
}
pub fn getElementById(self: *DocumentFragment, id_: ?[]const u8) ?*Element {
const id = id_ orelse return null;
var tw = @import("TreeWalker.zig").Full.Elements.init(self.asNode(), .{}); var tw = @import("TreeWalker.zig").Full.Elements.init(self.asNode(), .{});
while (tw.next()) |el| { while (tw.next()) |el| {
@@ -156,6 +154,12 @@ pub fn replaceChildren(self: *DocumentFragment, nodes: []const Node.NodeOrText,
const parent_is_connected = parent.isConnected(); const parent_is_connected = parent.isConnected();
for (nodes) |node_or_text| { for (nodes) |node_or_text| {
const child = try node_or_text.toNode(page); const child = try node_or_text.toNode(page);
// If the new children has already a parent, remove from it.
if (child._parent) |p| {
page.removeNode(p, child, .{ .will_be_reconnected = true });
}
try page.appendNode(parent, child, .{ .child_already_connected = parent_is_connected }); try page.appendNode(parent, child, .{ .child_already_connected = parent_is_connected });
} }
} }
@@ -233,7 +237,18 @@ pub const JsApi = struct {
pub const constructor = bridge.constructor(DocumentFragment.init, .{}); pub const constructor = bridge.constructor(DocumentFragment.init, .{});
pub const getElementById = bridge.function(DocumentFragment.getElementById, .{}); pub const getElementById = bridge.function(_getElementById, .{});
fn _getElementById(self: *DocumentFragment, value_: ?js.Value) !?*Element {
const value = value_ orelse return null;
if (value.isNull()) {
return self.getElementById("null");
}
if (value.isUndefined()) {
return self.getElementById("undefined");
}
return self.getElementById(try value.toZig([]const u8));
}
pub const querySelector = bridge.function(DocumentFragment.querySelector, .{ .dom_exception = true }); pub const querySelector = bridge.function(DocumentFragment.querySelector, .{ .dom_exception = true });
pub const querySelectorAll = bridge.function(DocumentFragment.querySelectorAll, .{ .dom_exception = true }); pub const querySelectorAll = bridge.function(DocumentFragment.querySelectorAll, .{ .dom_exception = true });
pub const children = bridge.accessor(DocumentFragment.getChildren, null, .{}); pub const children = bridge.accessor(DocumentFragment.getChildren, null, .{});

View File

@@ -19,6 +19,8 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Node = @import("Node.zig"); const Node = @import("Node.zig");
const DocumentType = @This(); const DocumentType = @This();
@@ -28,6 +30,20 @@ _name: []const u8,
_public_id: []const u8, _public_id: []const u8,
_system_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 { pub fn asNode(self: *DocumentType) *Node {
return self._proto; return self._proto;
} }
@@ -48,16 +64,16 @@ pub fn getSystemId(self: *const DocumentType) []const u8 {
return self._system_id; return self._system_id;
} }
pub fn className(_: *const DocumentType) []const u8 {
return "[object DocumentType]";
}
pub fn isEqualNode(self: *const DocumentType, other: *const DocumentType) bool { pub fn isEqualNode(self: *const DocumentType, other: *const DocumentType) bool {
return std.mem.eql(u8, self._name, other._name) and 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._public_id, other._public_id) and
std.mem.eql(u8, self._system_id, other._system_id); 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 JsApi = struct {
pub const bridge = js.Bridge(DocumentType); pub const bridge = js.Bridge(DocumentType);
@@ -70,9 +86,4 @@ pub const JsApi = struct {
pub const name = bridge.accessor(DocumentType.getName, null, .{}); pub const name = bridge.accessor(DocumentType.getName, null, .{});
pub const publicId = bridge.accessor(DocumentType.getPublicId, null, .{}); pub const publicId = bridge.accessor(DocumentType.getPublicId, null, .{});
pub const systemId = bridge.accessor(DocumentType.getSystemId, null, .{}); pub const systemId = bridge.accessor(DocumentType.getSystemId, null, .{});
pub const toString = bridge.function(_toString, .{});
fn _toString(self: *const DocumentType) []const u8 {
return self.className();
}
}; };

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const String = @import("../../string.zig").String; const String = @import("../../string.zig").String;
@@ -44,6 +45,7 @@ const Element = @This();
pub const DatasetLookup = std.AutoHashMapUnmanaged(*Element, *DOMStringMap); pub const DatasetLookup = std.AutoHashMapUnmanaged(*Element, *DOMStringMap);
pub const StyleLookup = std.AutoHashMapUnmanaged(*Element, *CSSStyleProperties); pub const StyleLookup = std.AutoHashMapUnmanaged(*Element, *CSSStyleProperties);
pub const ClassListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList); pub const ClassListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList);
pub const RelListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList);
pub const ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot); pub const ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot);
pub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Element, *Html.Slot); pub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Element, *Html.Slot);
@@ -52,15 +54,41 @@ pub const Namespace = enum(u8) {
svg, svg,
mathml, mathml,
xml, 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) { return switch (self) {
.html => "http://www.w3.org/1999/xhtml", .html => "http://www.w3.org/1999/xhtml",
.svg => "http://www.w3.org/2000/svg", .svg => "http://www.w3.org/2000/svg",
.mathml => "http://www.w3.org/1998/Math/MathML", .mathml => "http://www.w3.org/1998/Math/MathML",
.xml => "http://www.w3.org/XML/1998/namespace", .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, _type: Type,
@@ -112,12 +140,6 @@ pub fn asConstNode(self: *const Element) *const Node {
return self._proto; 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 { pub fn attributesEql(self: *const Element, other: *Element) bool {
if (self._attributes) |attr_list| { if (self._attributes) |attr_list| {
const other_list = other._attributes orelse return false; const other_list = other._attributes orelse return false;
@@ -170,15 +192,21 @@ pub fn getTagNameLower(self: *const Element) []const u8 {
}, },
else => return switch (he._type) { else => return switch (he._type) {
.anchor => "a", .anchor => "a",
.area => "area",
.base => "base",
.body => "body", .body => "body",
.br => "br", .br => "br",
.button => "button", .button => "button",
.canvas => "canvas", .canvas => "canvas",
.custom => |e| e._tag_name.str(), .custom => |e| e._tag_name.str(),
.data => "data", .data => "data",
.datalist => "datalist",
.dialog => "dialog", .dialog => "dialog",
.directory => "dir",
.div => "div", .div => "div",
.embed => "embed", .embed => "embed",
.fieldset => "fieldset",
.font => "font",
.form => "form", .form => "form",
.generic => |e| e._tag_name.str(), .generic => |e| e._tag_name.str(),
.heading => |e| e._tag_name.str(), .heading => |e| e._tag_name.str(),
@@ -188,24 +216,46 @@ pub fn getTagNameLower(self: *const Element) []const u8 {
.iframe => "iframe", .iframe => "iframe",
.img => "img", .img => "img",
.input => "input", .input => "input",
.label => "label",
.legend => "legend",
.li => "li", .li => "li",
.link => "link", .link => "link",
.map => "map",
.media => |m| switch (m._type) { .media => |m| switch (m._type) {
.audio => "audio", .audio => "audio",
.video => "video", .video => "video",
.generic => "media", .generic => "media",
}, },
.meta => "meta", .meta => "meta",
.meter => "meter",
.mod => |e| e._tag_name.str(),
.object => "object",
.ol => "ol", .ol => "ol",
.optgroup => "optgroup",
.option => "option", .option => "option",
.output => "output",
.p => "p", .p => "p",
.param => "param",
.pre => "pre",
.progress => "progress",
.quote => |e| e._tag_name.str(),
.script => "script", .script => "script",
.select => "select", .select => "select",
.slot => "slot", .slot => "slot",
.source => "source",
.span => "span",
.style => "style", .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", .template => "template",
.textarea => "textarea", .textarea => "textarea",
.time => "time",
.title => "title", .title => "title",
.track => "track",
.ul => "ul", .ul => "ul",
.unknown => |e| e._tag_name.str(), .unknown => |e| e._tag_name.str(),
}, },
@@ -215,59 +265,81 @@ pub fn getTagNameLower(self: *const Element) []const u8 {
} }
pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 { pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 {
switch (self._type) { return switch (self._type) {
.html => |he| switch (he._type) { .html => |he| switch (he._type) {
.custom => |e| { .anchor => "A",
@branchHint(.unlikely); .area => "AREA",
return upperTagName(&e._tag_name, buf); .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) { .meter => "METER",
.anchor => "A", .mod => |e| upperTagName(&e._tag_name, buf),
.body => "BODY", .object => "OBJECT",
.br => "BR", .ol => "OL",
.button => "BUTTON", .optgroup => "OPTGROUP",
.canvas => "CANVAS", .option => "OPTION",
.custom => |e| upperTagName(&e._tag_name, buf), .output => "OUTPUT",
.data => "DATA", .p => "P",
.dialog => "DIALOG", .param => "PARAM",
.div => "DIV", .pre => "PRE",
.embed => "EMBED", .progress => "PROGRESS",
.form => "FORM", .quote => |e| upperTagName(&e._tag_name, buf),
.generic => |e| upperTagName(&e._tag_name, buf), .script => "SCRIPT",
.heading => |e| upperTagName(&e._tag_name, buf), .select => "SELECT",
.head => "HEAD", .slot => "SLOT",
.html => "HTML", .source => "SOURCE",
.hr => "HR", .span => "SPAN",
.iframe => "IFRAME", .style => "STYLE",
.img => "IMG", .table => "TABLE",
.input => "INPUT", .table_caption => "CAPTION",
.li => "LI", .table_cell => |e| upperTagName(&e._tag_name, buf),
.link => "LINK", .table_col => |e| upperTagName(&e._tag_name, buf),
.meta => "META", .table_row => "TR",
.media => |m| switch (m._type) { .table_section => |e| upperTagName(&e._tag_name, buf),
.audio => "AUDIO", .template => "TEMPLATE",
.video => "VIDEO", .textarea => "TEXTAREA",
.generic => "MEDIA", .time => "TIME",
}, .title => "TITLE",
.ol => "OL", .track => "TRACK",
.option => "OPTION", .ul => "UL",
.p => "P", .unknown => |e| switch (self._namespace) {
.script => "SCRIPT", .html => upperTagName(&e._tag_name, buf),
.select => "SELECT", .svg, .xml, .mathml, .unknown, .null => e._tag_name.str(),
.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(),
},
}, },
}, },
.svg => |svg| return svg._tag_name.str(), .svg => |svg| svg._tag_name.str(),
} };
} }
pub fn getTagNameDump(self: *const Element) []const u8 { pub fn getTagNameDump(self: *const Element) []const u8 {
@@ -277,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(); return self._namespace.toUri();
} }
@@ -316,6 +388,20 @@ pub fn getOuterHTML(self: *Element, writer: *std.Io.Writer, page: *Page) !void {
return dump.deep(self.asNode(), .{ .shadow = .skip }, writer, page); return dump.deep(self.asNode(), .{ .shadow = .skip }, writer, page);
} }
pub fn setOuterHTML(self: *Element, html: []const u8, page: *Page) !void {
const node = self.asNode();
const parent = node._parent orelse return;
page.domChanged();
if (html.len > 0) {
const fragment = (try Node.DocumentFragment.init(page)).asNode();
try page.parseHtmlAsChildren(fragment, html);
try page.insertAllChildrenBefore(fragment, parent, node);
}
page.removeNode(parent, node, .{ .will_be_reconnected = false });
}
pub fn getInnerHTML(self: *Element, writer: *std.Io.Writer, page: *Page) !void { pub fn getInnerHTML(self: *Element, writer: *std.Io.Writer, page: *Page) !void {
const dump = @import("../dump.zig"); const dump = @import("../dump.zig");
return dump.children(self.asNode(), .{ .shadow = .skip }, writer, page); return dump.children(self.asNode(), .{ .shadow = .skip }, writer, page);
@@ -382,6 +468,22 @@ pub fn getAttribute(self: *const Element, name: []const u8, page: *Page) !?[]con
return attributes.get(name, page); 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 { pub fn getAttributeSafe(self: *const Element, name: []const u8) ?[]const u8 {
const attributes = self._attributes orelse return null; const attributes = self._attributes orelse return null;
return attributes.getSafe(name); return attributes.getSafe(name);
@@ -414,6 +516,26 @@ pub fn setAttribute(self: *Element, name: []const u8, value: []const u8, page: *
_ = try attributes.put(name, value, self, 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 { pub fn setAttributeSafe(self: *Element, name: []const u8, value: []const u8, page: *Page) !void {
const attributes = try self.getOrCreateAttributeList(page); const attributes = try self.getOrCreateAttributeList(page);
_ = try attributes.putSafe(name, value, self, page); _ = try attributes.putSafe(name, value, self, page);
@@ -424,7 +546,7 @@ pub fn getOrCreateAttributeList(self: *Element, page: *Page) !*Attribute.List {
} }
pub fn createAttributeList(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); const a = try page.arena.create(Attribute.List);
a.* = .{ .normalize = self._namespace == .html }; a.* = .{ .normalize = self._namespace == .html };
self._attributes = a; self._attributes = a;
@@ -550,6 +672,17 @@ pub fn getClassList(self: *Element, page: *Page) !*collections.DOMTokenList {
return gop.value_ptr.*; return gop.value_ptr.*;
} }
pub fn getRelList(self: *Element, page: *Page) !*collections.DOMTokenList {
const gop = try page._element_rel_lists.getOrPut(page.arena, self);
if (!gop.found_existing) {
gop.value_ptr.* = try page._factory.create(collections.DOMTokenList{
._element = self,
._attribute_name = "rel",
});
}
return gop.value_ptr.*;
}
pub fn getDataset(self: *Element, page: *Page) !*DOMStringMap { pub fn getDataset(self: *Element, page: *Page) !*DOMStringMap {
const gop = try page._element_datasets.getOrPut(page.arena, self); const gop = try page._element_datasets.getOrPut(page.arena, self);
if (!gop.found_existing) { if (!gop.found_existing) {
@@ -724,7 +857,7 @@ pub fn getAnimations(_: *const Element) []*Animation {
return &.{}; 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); return Animation.init(page);
} }
@@ -966,17 +1099,20 @@ pub fn getElementsByClassName(self: *Element, class_name: []const u8, page: *Pag
var class_names: std.ArrayList([]const u8) = .empty; var class_names: std.ArrayList([]const u8) = .empty;
var it = std.mem.tokenizeAny(u8, class_name, &std.ascii.whitespace); var it = std.mem.tokenizeAny(u8, class_name, &std.ascii.whitespace);
while (it.next()) |name| { while (it.next()) |name| {
try class_names.append(arena, name); try class_names.append(arena, try page.dupeString(name));
} }
return collections.NodeLive(.class_name).init(self.asNode(), class_names.items, page); 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 tag_name = self.getTagNameDump();
const namespace_uri = self.getNamespaceURI(); const node = try page.createElementNS(self._namespace, tag_name, self._attributes);
const node = try page.createElement(namespace_uri, 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| {
log.err(.dom, "element.clone.failed", .{ .err = err });
};
if (deep) { if (deep) {
var child_it = self.asNode().childrenIterator(); var child_it = self.asNode().childrenIterator();
@@ -1029,34 +1165,62 @@ pub fn getTag(self: *const Element) Tag {
return switch (self._type) { return switch (self._type) {
.html => |he| switch (he._type) { .html => |he| switch (he._type) {
.anchor => .anchor, .anchor => .anchor,
.area => .area,
.base => .base,
.div => .div, .div => .div,
.embed => .embed, .embed => .embed,
.form => .form, .form => .form,
.p => .p, .p => .p,
.custom => .custom, .custom => .custom,
.data => .data, .data => .data,
.datalist => .datalist,
.dialog => .dialog, .dialog => .dialog,
.directory => .unknown,
.iframe => .iframe, .iframe => .iframe,
.img => .img, .img => .img,
.br => .br, .br => .br,
.button => .button, .button => .button,
.canvas => .canvas, .canvas => .canvas,
.fieldset => .fieldset,
.font => .unknown,
.heading => |h| h._tag, .heading => |h| h._tag,
.label => .unknown,
.legend => .unknown,
.li => .li, .li => .li,
.map => .unknown,
.ul => .ul, .ul => .ul,
.ol => .ol, .ol => .ol,
.object => .unknown,
.optgroup => .optgroup,
.output => .unknown,
.param => .unknown,
.pre => .unknown,
.generic => |g| g._tag, .generic => |g| g._tag,
.media => |m| switch (m._type) { .media => |m| switch (m._type) {
.audio => .audio, .audio => .audio,
.video => .video, .video => .video,
.generic => .media, .generic => .media,
}, },
.meter => .meter,
.mod => |m| m._tag,
.progress => .progress,
.quote => |q| q._tag,
.script => .script, .script => .script,
.select => .select, .select => .select,
.slot => .slot, .slot => .slot,
.source => .unknown,
.span => .span,
.option => .option, .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, .template => .template,
.textarea => .textarea, .textarea => .textarea,
.time => .time,
.track => .unknown,
.input => .input, .input => .input,
.link => .link, .link => .link,
.meta => .meta, .meta => .meta,
@@ -1092,6 +1256,8 @@ pub const Tag = enum {
caption, caption,
circle, circle,
code, code,
col,
colgroup,
custom, custom,
data, data,
datalist, datalist,
@@ -1138,20 +1304,26 @@ pub const Tag = enum {
meta, meta,
meter, meter,
nav, nav,
noscript,
object,
ol, ol,
optgroup, optgroup,
option, option,
output,
p, p,
path, path,
param,
polygon, polygon,
polyline, polyline,
progress, progress,
quote,
rect, rect,
s, s,
script, script,
section, section,
select, select,
slot, slot,
source,
span, span,
strong, strong,
style, style,
@@ -1171,6 +1343,7 @@ pub const Tag = enum {
thead, thead,
title, title,
tr, tr,
track,
ul, ul,
video, video,
unknown, unknown,
@@ -1208,7 +1381,7 @@ pub const JsApi = struct {
return buf.written(); return buf.written();
} }
pub const outerHTML = bridge.accessor(_outerHTML, null, .{}); pub const outerHTML = bridge.accessor(_outerHTML, Element.setOuterHTML, .{});
fn _outerHTML(self: *Element, page: *Page) ![]const u8 { fn _outerHTML(self: *Element, page: *Page) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(page.call_arena); var buf = std.Io.Writer.Allocating.init(page.call_arena);
try self.getOuterHTML(&buf.writer, page); try self.getOuterHTML(&buf.writer, page);
@@ -1244,8 +1417,10 @@ pub const JsApi = struct {
pub const hasAttribute = bridge.function(Element.hasAttribute, .{}); pub const hasAttribute = bridge.function(Element.hasAttribute, .{});
pub const hasAttributes = bridge.function(Element.hasAttributes, .{}); pub const hasAttributes = bridge.function(Element.hasAttributes, .{});
pub const getAttribute = bridge.function(Element.getAttribute, .{}); pub const getAttribute = bridge.function(Element.getAttribute, .{});
pub const getAttributeNS = bridge.function(Element.getAttributeNS, .{});
pub const getAttributeNode = bridge.function(Element.getAttributeNode, .{}); pub const getAttributeNode = bridge.function(Element.getAttributeNode, .{});
pub const setAttribute = bridge.function(Element.setAttribute, .{ .dom_exception = true }); 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 setAttributeNode = bridge.function(Element.setAttributeNode, .{});
pub const removeAttribute = bridge.function(Element.removeAttribute, .{}); pub const removeAttribute = bridge.function(Element.removeAttribute, .{});
pub const toggleAttribute = bridge.function(Element.toggleAttribute, .{ .dom_exception = true }); pub const toggleAttribute = bridge.function(Element.toggleAttribute, .{ .dom_exception = true });

View File

@@ -40,7 +40,7 @@ _prevent_default: bool = false,
_stop_propagation: bool = false, _stop_propagation: bool = false,
_stop_immediate_propagation: bool = false, _stop_immediate_propagation: bool = false,
_event_phase: EventPhase = .none, _event_phase: EventPhase = .none,
_time_stamp: u64 = 0, _time_stamp: u64,
_needs_retargeting: bool = false, _needs_retargeting: bool = false,
_isTrusted: bool = false, _isTrusted: bool = false,
@@ -105,9 +105,14 @@ pub fn initEvent(
cancelable: ?bool, cancelable: ?bool,
page: *Page, page: *Page,
) !void { ) !void {
if (self._event_phase != .none) {
return;
}
self._type_string = try String.init(page.arena, event_string, .{}); self._type_string = try String.init(page.arena, event_string, .{});
self._bubbles = bubbles orelse false; self._bubbles = bubbles orelse false;
self._cancelable = cancelable orelse false; self._cancelable = cancelable orelse false;
self._stop_propagation = false;
} }
pub fn as(self: *Event, comptime T: type) *T { pub fn as(self: *Event, comptime T: type) *T {
@@ -176,6 +181,22 @@ pub fn getDefaultPrevented(self: *const Event) bool {
return self._prevent_default; return self._prevent_default;
} }
pub fn getReturnValue(self: *const Event) bool {
return !self._prevent_default;
}
pub fn setReturnValue(self: *Event, v: bool) void {
self._prevent_default = !v;
}
pub fn getCancelBubble(self: *const Event) bool {
return self._stop_propagation;
}
pub fn setCancelBubble(self: *Event) void {
self.stopPropagation();
}
pub fn getEventPhase(self: *const Event) u8 { pub fn getEventPhase(self: *const Event) u8 {
return @intFromEnum(self._event_phase); return @intFromEnum(self._event_phase);
} }
@@ -372,6 +393,7 @@ pub const JsApi = struct {
pub const cancelable = bridge.accessor(Event.getCancelable, null, .{}); pub const cancelable = bridge.accessor(Event.getCancelable, null, .{});
pub const composed = bridge.accessor(Event.getComposed, null, .{}); pub const composed = bridge.accessor(Event.getComposed, null, .{});
pub const target = bridge.accessor(Event.getTarget, null, .{}); pub const target = bridge.accessor(Event.getTarget, null, .{});
pub const srcElement = bridge.accessor(Event.getTarget, null, .{});
pub const currentTarget = bridge.accessor(Event.getCurrentTarget, null, .{}); pub const currentTarget = bridge.accessor(Event.getCurrentTarget, null, .{});
pub const eventPhase = bridge.accessor(Event.getEventPhase, null, .{}); pub const eventPhase = bridge.accessor(Event.getEventPhase, null, .{});
pub const defaultPrevented = bridge.accessor(Event.getDefaultPrevented, null, .{}); pub const defaultPrevented = bridge.accessor(Event.getDefaultPrevented, null, .{});
@@ -382,6 +404,10 @@ pub const JsApi = struct {
pub const stopImmediatePropagation = bridge.function(Event.stopImmediatePropagation, .{}); pub const stopImmediatePropagation = bridge.function(Event.stopImmediatePropagation, .{});
pub const composedPath = bridge.function(Event.composedPath, .{}); pub const composedPath = bridge.function(Event.composedPath, .{});
pub const initEvent = bridge.function(Event.initEvent, .{}); pub const initEvent = bridge.function(Event.initEvent, .{});
// deprecated
pub const returnValue = bridge.accessor(Event.getReturnValue, Event.setReturnValue, .{});
// deprecated
pub const cancelBubble = bridge.accessor(Event.getCancelBubble, Event.setCancelBubble, .{});
// Event phase constants // Event phase constants
pub const NONE = bridge.property(@intFromEnum(EventPhase.none)); pub const NONE = bridge.property(@intFromEnum(EventPhase.none));

View File

@@ -51,6 +51,10 @@ pub fn init(page: *Page) !*EventTarget {
} }
pub fn dispatchEvent(self: *EventTarget, event: *Event, page: *Page) !bool { 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); try page._event_manager.dispatch(self, event);
return !event._cancelable or !event._prevent_default; 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 { pub fn addEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventListenerCallback, opts_: ?AddEventListenerOptions, page: *Page) !void {
const callback = callback_ orelse return; const callback = callback_ orelse return;
if (callback == .object) {
if (try callback.object.getFunction("handleEvent") == null) {
return;
}
}
const em_callback = switch (callback) { const em_callback = switch (callback) {
.object => |obj| EventManager.Callback{ .object = obj },
.function => |func| EventManager.Callback{ .function = func }, .function => |func| EventManager.Callback{ .function = func },
.object => |obj| EventManager.Callback{ .object = try obj.persist() },
}; };
const options = blk: { const options = blk: {
@@ -108,7 +106,7 @@ pub fn removeEventListener(self: *EventTarget, typ: []const u8, callback_: ?Even
const em_callback = switch (callback) { const em_callback = switch (callback) {
.function => |func| EventManager.Callback{ .function = func }, .function => |func| EventManager.Callback{ .function = func },
.object => |obj| EventManager.Callback{ .object = try obj.persist() }, .object => |obj| EventManager.Callback{ .object = obj },
}; };
const use_capture = blk: { const use_capture = blk: {
@@ -164,10 +162,9 @@ pub const JsApi = struct {
}; };
pub const constructor = bridge.constructor(EventTarget.init, .{}); 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 addEventListener = bridge.function(EventTarget.addEventListener, .{});
pub const removeEventListener = bridge.function(EventTarget.removeEventListener, .{}); pub const removeEventListener = bridge.function(EventTarget.removeEventListener, .{});
pub const toString = bridge.function(EventTarget.toString, .{});
}; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");

View File

@@ -44,10 +44,6 @@ pub fn asEventTarget(self: *HTMLDocument) *@import("EventTarget.zig") {
return self._proto.asEventTarget(); return self._proto.asEventTarget();
} }
pub fn className(_: *const HTMLDocument) []const u8 {
return "[object HTMLDocument]";
}
// HTML-specific accessors // HTML-specific accessors
pub fn getHead(self: *HTMLDocument) ?*Element.Html.Head { pub fn getHead(self: *HTMLDocument) ?*Element.Html.Head {
const doc_el = self._proto.getDocumentElement() orelse return null; const doc_el = self._proto.getDocumentElement() orelse return null;
@@ -74,30 +70,76 @@ pub fn getBody(self: *HTMLDocument) ?*Element.Html.Body {
} }
pub fn getTitle(self: *HTMLDocument, page: *Page) ![]const u8 { pub fn getTitle(self: *HTMLDocument, page: *Page) ![]const u8 {
const head = self.getHead() orelse return ""; // Search the entire document for the first <title> element
var it = head.asNode().childrenIterator(); const root = self._proto.getDocumentElement() orelse return "";
while (it.next()) |node| { const title_element = blk: {
if (node.is(Element.Html.Title)) |title| { var walker = @import("TreeWalker.zig").Full.init(root.asNode(), .{});
var buf = std.Io.Writer.Allocating.init(page.call_arena); while (walker.next()) |node| {
try title.asElement().getInnerText(&buf.writer); if (node.is(Element.Html.Title)) |title| {
return buf.written(); break :blk title;
}
}
return "";
};
var buf = std.Io.Writer.Allocating.init(page.call_arena);
try title_element.asNode().getTextContent(&buf.writer);
const text = buf.written();
if (text.len == 0) {
return "";
}
var started = false;
var in_whitespace = false;
var result: std.ArrayList(u8) = .empty;
try result.ensureTotalCapacity(page.call_arena, text.len);
for (text) |c| {
const is_ascii_ws = c == ' ' or c == '\t' or c == '\n' or c == '\r' or c == '\x0C';
if (is_ascii_ws) {
if (started) {
in_whitespace = true;
}
} else {
if (in_whitespace) {
result.appendAssumeCapacity(' ');
in_whitespace = false;
}
result.appendAssumeCapacity(c);
started = true;
} }
} }
return "";
return result.items;
} }
pub fn setTitle(self: *HTMLDocument, title: []const u8, page: *Page) !void { pub fn setTitle(self: *HTMLDocument, title: []const u8, page: *Page) !void {
const head = self.getHead() orelse return; const head = self.getHead() orelse return;
// Find existing title element in head
var it = head.asNode().childrenIterator(); var it = head.asNode().childrenIterator();
while (it.next()) |node| { while (it.next()) |node| {
if (node.is(Element.Html.Title)) |title_element| { if (node.is(Element.Html.Title)) |title_element| {
return title_element.asElement().replaceChildren(&.{.{ .text = title }}, page); // Replace children, but don't create text node for empty string
if (title.len == 0) {
return title_element.asElement().replaceChildren(&.{}, page);
} else {
return title_element.asElement().replaceChildren(&.{.{ .text = title }}, page);
}
} }
} }
const title_node = try page.createElement(null, "title", null); // No title element found, create one
const title_node = try page.createElementNS(.html, "title", null);
const title_element = title_node.as(Element); const title_element = title_node.as(Element);
try title_element.replaceChildren(&.{.{ .text = title }}, page);
// Only add text if non-empty
if (title.len > 0) {
try title_element.replaceChildren(&.{.{ .text = title }}, page);
}
_ = try head.asNode().appendChild(title_node, page); _ = try head.asNode().appendChild(title_node, page);
} }
@@ -173,6 +215,15 @@ pub fn getDocType(self: *HTMLDocument, page: *Page) !*DocumentType {
if (self._document_type) |dt| { if (self._document_type) |dt| {
return 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{ self._document_type = try page._factory.node(DocumentType{
._proto = undefined, ._proto = undefined,
._name = "html", ._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 { pub fn getState(_: *const History, page: *Page) !?js.Value {
if (page._session.navigation.getCurrentEntry()._state.value) |state| { 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; return value;
} else return null; } 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 arena = page._session.arena;
const url = if (_url) |u| try arena.dupeZ(u8, u) else try arena.dupeZ(u8, page.url); 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); _ = 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 arena = page._session.arena;
const url = if (_url) |u| try arena.dupeZ(u8, u) else try arena.dupeZ(u8, page.url); 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)) { if (try page.isSameOrigin(url)) {
const event = try PopStateEvent.initTrusted("popstate", .{ .state = entry._state.value }, page); 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( try page._event_manager.dispatchWithFunction(
page.window.asEventTarget(), page.window.asEventTarget(),
event.asEvent(), event.asEvent(),
page.window._on_popstate, func,
.{ .context = "Pop State" }, .{ .context = "Pop State" },
); );
} }

View File

@@ -17,6 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const log = @import("../../log.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const Element = @import("Element.zig"); const Element = @import("Element.zig");
const DOMRect = @import("DOMRect.zig"); const DOMRect = @import("DOMRect.zig");
@@ -30,7 +32,7 @@ pub fn registerTypes() []const type {
const IntersectionObserver = @This(); const IntersectionObserver = @This();
_callback: js.Function, _callback: js.Function.Global,
_observing: std.ArrayList(*Element) = .{}, _observing: std.ArrayList(*Element) = .{},
_root: ?*Element = null, _root: ?*Element = null,
_root_margin: []const u8 = "0px", _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 opts = options orelse ObserverInit{};
const root_margin = if (opts.rootMargin) |rm| try page.arena.dupe(u8, rm) else "0px"; 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), .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 { 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); 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 { 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| { while (try it.next()) |name| {
const js_value = try js_obj.get(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; const normalized = if (comptime normalizer) |n| n(name, page) else name;
list._entries.appendAssumeCapacity(.{ list._entries.appendAssumeCapacity(.{

View File

@@ -29,8 +29,8 @@ const MessagePort = @This();
_proto: *EventTarget, _proto: *EventTarget,
_enabled: bool = false, _enabled: bool = false,
_closed: bool = false, _closed: bool = false,
_on_message: ?js.Function = null, _on_message: ?js.Function.Global = null,
_on_message_error: ?js.Function = null, _on_message_error: ?js.Function.Global = null,
_entangled_port: ?*MessagePort = null, _entangled_port: ?*MessagePort = null,
pub fn init(page: *Page) !*MessagePort { pub fn init(page: *Page) !*MessagePort {
@@ -48,7 +48,7 @@ pub fn entangle(port1: *MessagePort, port2: *MessagePort) void {
port2._entangled_port = port1; 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) { if (self._closed) {
return; return;
} }
@@ -62,7 +62,7 @@ pub fn postMessage(self: *MessagePort, message: js.Object, page: *Page) !void {
const callback = try page._factory.create(PostMessageCallback{ const callback = try page._factory.create(PostMessageCallback{
.page = page, .page = page,
.port = other, .port = other,
.message = try message.persist(), .message = message,
}); });
try page.scheduler.add(callback, PostMessageCallback.run, 0, .{ try page.scheduler.add(callback, PostMessageCallback.run, 0, .{
@@ -88,33 +88,25 @@ pub fn close(self: *MessagePort) void {
self._entangled_port = null; 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; return self._on_message;
} }
pub fn setOnMessage(self: *MessagePort, cb_: ?js.Function) !void { pub fn setOnMessage(self: *MessagePort, cb: ?js.Function.Global) !void {
if (cb_) |cb| { self._on_message = cb;
self._on_message = cb;
} else {
self._on_message = null;
}
} }
pub fn getOnMessageError(self: *const MessagePort) ?js.Function { pub fn getOnMessageError(self: *const MessagePort) ?js.Function.Global {
return self._on_message_error; return self._on_message_error;
} }
pub fn setOnMessageError(self: *MessagePort, cb_: ?js.Function) !void { pub fn setOnMessageError(self: *MessagePort, cb: ?js.Function.Global) !void {
if (cb_) |cb| { self._on_message_error = cb;
self._on_message_error = cb;
} else {
self._on_message_error = null;
}
} }
const PostMessageCallback = struct { const PostMessageCallback = struct {
port: *MessagePort, port: *MessagePort,
message: js.Object, message: js.Value.Global,
page: *Page, page: *Page,
fn deinit(self: *PostMessageCallback) void { fn deinit(self: *PostMessageCallback) void {
@@ -138,10 +130,11 @@ const PostMessageCallback = struct {
return null; return null;
}; };
const func = if (self.port._on_message) |*g| g.local() else null;
self.page._event_manager.dispatchWithFunction( self.page._event_manager.dispatchWithFunction(
self.port.asEventTarget(), self.port.asEventTarget(),
event.asEvent(), event.asEvent(),
self.port._on_message, func,
.{ .context = "MessagePort message" }, .{ .context = "MessagePort message" },
) catch |err| { ) catch |err| {
log.err(.dom, "MessagePort.postMessage", .{ .err = 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 Page = @import("../Page.zig");
const Node = @import("Node.zig"); const Node = @import("Node.zig");
const Element = @import("Element.zig"); const Element = @import("Element.zig");
const log = @import("../../log.zig");
pub fn registerTypes() []const type { pub fn registerTypes() []const type {
return &.{ return &.{
@@ -31,7 +32,7 @@ pub fn registerTypes() []const type {
const MutationObserver = @This(); const MutationObserver = @This();
_callback: js.Function, _callback: js.Function.Global,
_observing: std.ArrayList(Observing) = .{}, _observing: std.ArrayList(Observing) = .{},
_pending_records: std.ArrayList(*MutationRecord) = .{}, _pending_records: std.ArrayList(*MutationRecord) = .{},
/// Intrusively linked to next element (see Page.zig). /// Intrusively linked to next element (see Page.zig).
@@ -52,7 +53,7 @@ pub const ObserveOptions = struct {
attributeFilter: ?[]const []const u8 = null, 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{ return page._factory.create(MutationObserver{
._callback = callback, ._callback = callback,
}); });
@@ -69,6 +70,10 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
copied_options.attributeFilter = filter_copy; copied_options.attributeFilter = filter_copy;
} }
if (options.characterDataOldValue) {
copied_options.characterData = true;
}
// Check if already observing this target // Check if already observing this target
for (self._observing.items) |*obs| { for (self._observing.items) |*obs| {
if (obs.target == target) { 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 // 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 // This ensures mutations triggered during the callback go into a fresh list
const records = try self.takeRecords(page); 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 { pub const MutationRecord = struct {
@@ -274,6 +283,13 @@ pub const MutationRecord = struct {
return self._target; 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 { pub fn getAttributeName(self: *const MutationRecord) ?[]const u8 {
return self._attribute_name; return self._attribute_name;
} }
@@ -310,6 +326,7 @@ pub const MutationRecord = struct {
pub const @"type" = bridge.accessor(MutationRecord.getType, null, .{}); pub const @"type" = bridge.accessor(MutationRecord.getType, null, .{});
pub const target = bridge.accessor(MutationRecord.getTarget, null, .{}); pub const target = bridge.accessor(MutationRecord.getTarget, null, .{});
pub const attributeName = bridge.accessor(MutationRecord.getAttributeName, 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 oldValue = bridge.accessor(MutationRecord.getOldValue, null, .{});
pub const addedNodes = bridge.accessor(MutationRecord.getAddedNodes, null, .{}); pub const addedNodes = bridge.accessor(MutationRecord.getAddedNodes, null, .{});
pub const removedNodes = bridge.accessor(MutationRecord.getRemovedNodes, null, .{}); pub const removedNodes = bridge.accessor(MutationRecord.getRemovedNodes, null, .{});

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