Compare commits

...

138 Commits

Author SHA1 Message Date
Karl Seguin
f02fc95958 Merge pull request #1435 from lightpanda-io/handle_invalid_attribute_functions
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
skip invalid attribute functions
2026-01-29 16:50:40 +08:00
Karl Seguin
175edca8c7 Handle invalid attribute functions 2026-01-29 16:26:27 +08:00
Pierre Tachoire
f1f0a66f41 Merge pull request #1434 from lightpanda-io/update-source-deps-v2
update build from source deps
2026-01-29 09:13:08 +01:00
Pierre Tachoire
496c6905af update build from source deps 2026-01-29 08:42:58 +01:00
Halil Durak
232e7a1759 Merge pull request #1430 from lightpanda-io/nikneym/attr-event-listeners
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
e2e-integration-test / zig build release (push) Has been cancelled
e2e-integration-test / demo-integration-scripts (push) Has been cancelled
Support HTML inline event listeners
2026-01-29 02:18:37 +03:00
Karl Seguin
c440d41d57 Merge pull request #1427 from lightpanda-io/arena_pool_double_free_detect
Add double-free detection to ArenaPool (in Debug Mode)
2026-01-29 07:04:33 +08:00
Karl Seguin
dfe5c24404 remove unused import and unused export 2026-01-29 07:04:20 +08:00
Karl Seguin
eba5773d56 Merge pull request #1428 from lightpanda-io/parser_arena_pool
Use ArenaPool when parsing HTML and for TextDecoder (with finalizer)
2026-01-29 06:49:14 +08:00
Karl Seguin
5d56fea2d3 check for leak after context is removed, as that can cause finalizers to run 2026-01-29 06:47:55 +08:00
Karl Seguin
946f02b7a2 Add double-free detection to ArenaPool (in Debug Mode)
Double-freeing should eventually cause a segfault (on ArenaPool.deinit, if not
sooner), but having an explicit check allows us to log the responsible owner.
2026-01-29 06:46:18 +08:00
Pierre Tachoire
d02d974cd0 Merge pull request #1429 from lightpanda-io/update-required-deps
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
update required deps for build from sources
2026-01-28 17:49:59 +01:00
Halil Durak
0a68be695d add tests 2026-01-28 17:46:27 +03:00
Pierre Tachoire
335e781d0c update required deps for build from sources 2026-01-28 15:37:58 +01:00
Halil Durak
9f5c2e4ca7 add getter/setter functions for attribute event listeners
Spec say these belong to `HTMLElement`.
2026-01-28 17:28:16 +03:00
Halil Durak
76a53bedbe split inline event listener logic to Page.zig and Element.zig 2026-01-28 17:26:56 +03:00
Karl Seguin
b0bc84ed21 Merge pull request #1422 from lightpanda-io/log-on-call-err
always log try/catch error on call function
2026-01-28 18:45:02 +08:00
Pierre Tachoire
ae298fc2e6 use caught formatter and init caught into _tryCallWithThis 2026-01-28 11:27:05 +01:00
Pierre Tachoire
3b809b2910 Merge pull request #1421 from lightpanda-io/fix-context-collected
use inspector.resetContextGroup during cdp deinit
2026-01-28 11:22:49 +01:00
Pierre Tachoire
68fbc0bde3 use inspector.resetContextGroup during cdp deinit
Ensure the inspector is correctly reset from context before deinit it.
It fixes the contextCollected crash in a better way.
2026-01-28 11:11:38 +01:00
Pierre Tachoire
9d8e5263a6 Merge pull request #1418 from lightpanda-io/mem-pressure
use less aggressive v8 GC
2026-01-28 11:09:45 +01:00
Pierre Tachoire
7eb026cc0d update zig-v8 deps 2026-01-28 10:38:04 +01:00
Karl Seguin
e51e6aa2b0 Use ArenaPool when parsing HTML and for TextDecoder (with finalizer)
Slowly more page.arena -> ArenaPool wherever possible. In some cases, an arena
from the arenapool will be preferred over the call_arena also.
2026-01-28 14:44:05 +08:00
Karl Seguin
bc700d2044 Merge pull request #1424 from lightpanda-io/parser_append_existing_parent
Add defensiveness around Parser.appendCallback
2026-01-28 09:21:14 +08:00
Karl Seguin
30ed58ff07 fix build 2026-01-28 08:06:22 +08:00
Karl Seguin
066069baad Add defensiveness around Parser.appendCallback
We're seeing an assertion in Page.appendNew fail because the node has a parent.
According to html5ever, this shouldn't be possible (appendNew is only called
from the Parser). BUT, it's possible we're mutating the node in a way that
we shouldn't...maybe there's JavaScript executing as we're parsing which is
mutating the node.

In release, this will be more defensive. In debug, this still crashes. It's
possible this is valid (like I said, maybe there's JS interleaved which is
mutating the node), but if so, I'd like to know the exact scenario that produces
this case.
2026-01-28 07:33:04 +08:00
Karl Seguin
068ec68917 Merge pull request #1420 from lightpanda-io/resolve_fix
Handle URL.resolve with path traversal as part of the filename
2026-01-28 06:46:35 +08:00
Halil Durak
560f028bda remove unused getListenerType 2026-01-28 01:33:17 +03:00
Halil Durak
fd1e77df8f parse event listeners provided as attributes 2026-01-28 01:31:43 +03:00
Karl Seguin
864ac08f16 optimize this more 2026-01-28 06:17:52 +08:00
Halil Durak
6ad1a11593 catch pointer overflows in createLookupKey
Its better to have this; if this is incorrect, its better to get notified.
2026-01-27 23:52:12 +03:00
Halil Durak
89174ba0b6 EventManager: introduce inline_lookup
Idea with this is to have a key-to-function for known event listeners. We pack pointer to event target with listener type to generate key and set function as value. By doing this, we save bytes for optionally and rarely set functions in elements.
2026-01-27 23:37:46 +03:00
Pierre Tachoire
fc5496e570 always log try/catch error on call function
We force log of detailled error caught during function call.
2026-01-27 18:41:12 +01:00
Karl Seguin
fd21d952ac Handle URL.resolve with path traversal as part of the filename 2026-01-27 21:45:16 +08:00
Karl Seguin
073fea2bde Merge pull request #1419 from lightpanda-io/arena_pool_leak_track_use_after_free
Reset _arena_pool_leak_track after the page.arena is reset
2026-01-27 18:04:46 +08:00
Karl Seguin
e548712f5e Reset _arena_pool_leak_track after the page.arena is reset 2026-01-27 17:55:32 +08:00
Pierre Tachoire
c3ba83ff93 use less aggressive v8 GC
Isolate.lowMemoryNotification runs an aggrissive GC.
Using Isolate.memoryPressureNotification allow a more granular control
of GC.
2026-01-27 09:39:08 +01:00
Karl Seguin
451dd0fd64 Merge pull request #1411 from lightpanda-io/more_SSO
more small strings (string.String)
2026-01-27 13:18:56 +08:00
Karl Seguin
aa805c2428 Merge pull request #1415 from lightpanda-io/dynamic_module_import
Improve dynamic module loading
2026-01-27 13:09:13 +08:00
Karl Seguin
58a7590aff Merge pull request #1416 from lightpanda-io/zigfmt
zig fmt
2026-01-27 13:08:54 +08:00
Karl Seguin
563ab30564 Merge pull request #1412 from lightpanda-io/response_arena
Add finalizer to Response and use an pooled arena
2026-01-27 12:55:52 +08:00
Karl Seguin
5050b34361 zig fmt 2026-01-27 12:55:25 +08:00
Karl Seguin
3bb86f196b Improve dynamic module loading
We're seeing cases where known dynamic modules are being requested when the
module isn't compiled yet. It's not clear how this is happening. I believe an
empty cache entry is being created in postCompileModule and then the request
for the dynamic module is happening before the sycnhronous module is loaded.
This seems like the only way to get into this state ,but I can't reproduce it.

Still, we now try to handle this case by simply having the dynamic module
request overwrite the placeholder cache-entry created in postCompileModule.
2026-01-27 12:51:37 +08:00
Karl Seguin
51dca3be11 Merge pull request #1414 from lightpanda-io/cheak_leak_before_free
Check for arena pool leak _before_ resetting page arena
2026-01-27 08:55:31 +08:00
Karl Seguin
adeda6cd75 Merge pull request #1413 from lightpanda-io/always_trycatch_function_call
Always use TryCatch when calling a function
2026-01-27 08:55:20 +08:00
Karl Seguin
09665c3a4a Check for arena pool leak _before_ resetting page arena
Cherry-picked from 31c0ac33d82271ee70b48b209b74a578e5e5c019
2026-01-27 06:43:23 +08:00
Karl Seguin
8f5f6212d2 Always use TryCatch when calling a function
And always ensure caught is initialized.
2026-01-26 20:06:47 +08:00
Karl Seguin
a11ae912b4 Add finalizer to Response and use an pooled arena
Unlike XHR, Response is a bit more complicated as it can exist in Zig code
without ever being given to v8. So we need to track this handoff to know who is
responsible for freeing it (zig code, on error/shutdown) or v8 code after
promise resolution.

This also cleansup a bad merge for the XHR finalizer and adds cleaning up the
`XMLHttpRequestEventTarget` callbacks.
2026-01-26 19:18:32 +08:00
Karl Seguin
3b12240615 remove newString helper in favor of .wrap 2026-01-26 08:00:04 +08:00
Karl Seguin
862520e4b1 micro-optimize String.eql(String) 2026-01-26 07:52:27 +08:00
Karl Seguin
a3d2dd8366 Convert most Attribute related calls from []const u8 -> String 2026-01-26 07:52:27 +08:00
Karl Seguin
16ef487871 Make "Safe" variants of Attribute work on String 2026-01-26 07:52:27 +08:00
Karl Seguin
54c45a0cfd Make js.Bridge aware of string.String for input parameters
Avoids having to allocate small strings when going from v8 -> Zig. Also
added a discriminatory type, string.Global which uses the arena, rather than
the call_arena, if an allocation _is_ necessary. (This is similar to a feature
we had before, but was lost in zigdom). Strings from v8 that need to be
persisted, can be allocated directly v8 -> arena, rather than v8 -> call_arena
-> arena.

I think there are a lot of places where we should use string.String - where
strings are expected to be short (e.g. attribute names). But started with just
document.querySelector and querySelectorAll.
2026-01-26 07:52:27 +08:00
Karl Seguin
1f14eb62d4 Merge pull request #1410 from lightpanda-io/insertAdjacentHtml
Fix insertAdjacentHtml
2026-01-26 07:45:11 +08:00
Karl Seguin
0db86a8b3d Merge pull request #1396 from lightpanda-io/eager_global_reset
Start to eagerly reset globals.
2026-01-26 07:45:00 +08:00
Karl Seguin
c63c85071a Start to eagerly reset globals.
Currently, when you create a Global (Value, Object, Function, ...) it exists
until the context is destroyed.

This PR adds the ability to eagerly free them when they fall out of scope, which
is only possible because of the new finalizer hooks.

Previously, we had js.Value, js.Value.Global; js.Function, js.Function.Global,
etc. This PR introduces a .Temp variant: js.Value.Temp and js.Function.Temp.
This is purely a discriminatory type and it behaves (and IS) a Global. The
difference is that it can be released:

  page.js.release(self._on_ready_state_change.?)

Why a new type? There's no guarantee that a global (the existing .Global or the
new .Temp) will get released before the context ends. For this reason, we always
track them in order to free the on context deninit:

```zig
    for (self.global_functions.items) |*global| {
        v8.v8__Global__Reset(global);
    }
```

If a .Temp is eagerly released, we need to remove it from this list. The simple
solution would be to switch `global_functions` from an ArrayList to a HashMap.
But that adds overhead for values that we know we'll never be able to eagerly
release. For this reason, .Temp are stored in a hashmap (and can be released)
and .Globla are stored in an ArrayList (and cannot be released). It's a micro-
optimization...eagerly releasing doesn't have to O(N) scan the list, and we only
pay the memory overhead of the hashmap for values that have a change to be
eagerly freed.

Eager-freeing is now applied to both the callbacn and the values for window
timers (setTimeout, setInterval, RAF). And to the XHR ready_state_change
callback. (we'll do more as we go).
2026-01-26 07:39:05 +08:00
Karl Seguin
b63d93e325 Add XHR finalizer and ArenaPool
Any object we return from Zig to V8 becomes a v8::Global that we track in our
`ctx.identity_map`. V8 will not free such objects. On the flip side, on its own,
our Zig code never knows if the underlying v8::Object of a global can still be
used from JS. Imagine an XHR request where we fire the last readyStateChange
event..we might think we no longer need that XHR instance, but nothing stops
the JavaScript code from holding a reference to it and calling a property on it,
e.g. `xhr.status`.

What we can do is tell v8 that we're done with the global and register a callback.
We make our reference to the global weak. When v8 determines that this object
cannot be reached from JavaScript, it _may_ call our registered callback. We can
then clean things up on our side and free the global (we actually _have_ to
free the global).

v8 makes no guarantee that our callback will ever be called, so we need to track
these finalizable objects and free them ourselves on context shutdown. Furthermore
there appears to be some possible timing issues, especially during context shutdown,
so we need to be defensive and make sure we don't double-free (we can use the
existing identity_map for this).

An type like XMLHttpRequest can be re-used. After a request succeeds or fails,
it can be re-opened and a new request sent. So we also need a way to revert a
"weak" reference back into a "strong" reference. These are simple v8 calls on
the v8::Global, but it highlights how sensitive all this is. We need to mark
it as weak when we're 100% sure we're done with it, and we need to switch it to
strong under any circumstances where we might need it again on our side.

Finally, none of this makes sense if there isn't something to free. Of course,
the finalizer lets us release the v8::Global, and we can free the memory for the
object itself (i.e. the `*XMLHttpRequest`). This PR also adds an ArenaPool. This
allows the XMLHTTPRequest to be self-contained and not need the `page.arena`.
On init, the `XMLHTTPRequest` acquires an arena from the pool. On finalization
it releases it back to the pool. So we now have:

- page.call_arena: short, guaranteed for 1 v8 -> zig -> v8 flow
- page.arena long: lives for the duration of the entire page
- page.arena_pool: ideally lives for as long as needed by its instance (but no
guarantees from v8 about this, or the script might leak a lot of global, so worst
case, same as page.arena)
2026-01-26 07:38:24 +08:00
Karl Seguin
12c6e50e16 Merge pull request #1383 from lightpanda-io/xhr_finalizer
Add XHR finalizer and ArenaPool
2026-01-26 07:34:23 +08:00
Karl Seguin
53ccc2e04c Merge pull request #1404 from lightpanda-io/crash_handler_stack_trace
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
Capture the stack trace on the crash handler report
2026-01-24 18:29:18 +08:00
Karl Seguin
2d3234b54d Fix insertAdjacentHtml
Handle a wider range of HTML, including invalid html, empty string, head only
comment only, etc..
2026-01-24 10:04:19 +08:00
Karl Seguin
9a57c2a0d4 fix merge 2026-01-24 08:28:26 +08:00
Karl Seguin
fc64abee8f Add finalizer mode
When a type is finalized by V8, it's because it's fallen out of scope. When a
type is finalized by Zig, it's because the Context is being shutdown.

Those are two different environments and might require distinct cleanup logic.
Specifically, a zig-initiated finalization needs to consider that the page and
context are being shutdown. It isn't necessarily safe to execute JavaScript at
this point, and thus, not safe to execute a callback (on_error, on_abort,
ready_state_change, ...).
2026-01-24 07:59:43 +08:00
Karl Seguin
d5f26f6d15 remove temp variable 2026-01-24 07:59:43 +08:00
Karl Seguin
97f9c2991b on XHR shutdown, use terminate to prevent any client callbacks into the XHR 2026-01-24 07:59:43 +08:00
Karl Seguin
81378d4353 Simplify XHR lifetime
Keep this a weak reference (the default now).And rely on transfer abort to
ensure reference isn't needed after finalizer.
2026-01-24 07:59:43 +08:00
Karl Seguin
9f0c902030 more explicit arena pool debug parameter 2026-01-24 07:59:43 +08:00
Karl Seguin
3c0c75be10 Add XHR finalizer and ArenaPool
Any object we return from Zig to V8 becomes a v8::Global that we track in our
`ctx.identity_map`. V8 will not free such objects. On the flip side, on its own,
our Zig code never knows if the underlying v8::Object of a global can still be
used from JS. Imagine an XHR request where we fire the last readyStateChange
event..we might think we no longer need that XHR instance, but nothing stops
the JavaScript code from holding a reference to it and calling a property on it,
e.g. `xhr.status`.

What we can do is tell v8 that we're done with the global and register a callback.
We make our reference to the global weak. When v8 determines that this object
cannot be reached from JavaScript, it _may_ call our registered callback. We can
then clean things up on our side and free the global (we actually _have_ to
free the global).

v8 makes no guarantee that our callback will ever be called, so we need to track
these finalizable objects and free them ourselves on context shutdown. Furthermore
there appears to be some possible timing issues, especially during context shutdown,
so we need to be defensive and make sure we don't double-free (we can use the
existing identity_map for this).

An type like XMLHttpRequest can be re-used. After a request succeeds or fails,
it can be re-opened and a new request sent. So we also need a way to revert a
"weak" reference back into a "strong" reference. These are simple v8 calls on
the v8::Global, but it highlights how sensitive all this is. We need to mark
it as weak when we're 100% sure we're done with it, and we need to switch it to
strong under any circumstances where we might need it again on our side.

Finally, none of this makes sense if there isn't something to free. Of course,
the finalizer lets us release the v8::Global, and we can free the memory for the
object itself (i.e. the `*XMLHttpRequest`). This PR also adds an ArenaPool. This
allows the XMLHTTPRequest to be self-contained and not need the `page.arena`.
On init, the `XMLHTTPRequest` acquires an arena from the pool. On finalization
it releases it back to the pool. So we now have:

- page.call_arena: short, guaranteed for 1 v8 -> zig -> v8 flow
- page.arena long: lives for the duration of the entire page
- page.arena_pool: ideally lives for as long as needed by its instance (but no
guarantees from v8 about this, or the script might leak a lot of global, so worst
case, same as page.arena)
2026-01-24 07:59:41 +08:00
Karl Seguin
90d23abe18 fix null-byte 2026-01-24 07:58:36 +08:00
Karl Seguin
82eccf36d4 Merge pull request #1408 from lightpanda-io/fix-inspector-ctx-collected-crash
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
fix use after free during inspector contextCollected
2026-01-24 07:52:29 +08:00
Karl Seguin
342cb52887 Merge pull request #1409 from lightpanda-io/cleanup
zig fmt, remove unused code
2026-01-24 07:49:25 +08:00
Karl Seguin
cafa4f5173 correct crash report host 2026-01-24 07:46:14 +08:00
Karl Seguin
67cff5af8b zig fmt 2026-01-24 07:46:14 +08:00
Karl Seguin
6d23d91aa5 Capture the stack trace on the crash handler report 2026-01-24 07:46:13 +08:00
Karl Seguin
3a0699fc1d Merge pull request #1405 from lightpanda-io/prevent_fast_double_navigate
Handle fast double navigate
2026-01-24 07:39:39 +08:00
Karl Seguin
027e569087 Merge pull request #1398 from lightpanda-io/handle_non_200_scripts
Handle scripts that don't return a 200 status code
2026-01-24 07:39:19 +08:00
Karl Seguin
830f759f0b zig fmt, remove unused code 2026-01-24 07:37:30 +08:00
Pierre Tachoire
969891c71c fix use after free during inspector contextCollected
This commit fix the use after free crash into inspector contextCollected
run in the pumpMessageLoop.

Removing a context linked to an inspector triggers a contextCollected
task in the message queue.
But if the contextCollected task run after the GC it try to use free
memory. Forcing the message loop to run before the GC fix the issue.
2026-01-23 20:07:49 +01:00
Pierre Tachoire
4eb5c3e907 Merge pull request #1399 from alexisbouchez/window-onerror
Some checks failed
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
e2e-integration-test / zig build release (push) Has been cancelled
e2e-integration-test / demo-integration-scripts (push) Has been cancelled
invoke window.onerror callback in reportError
2026-01-23 12:36:32 +01:00
Karl Seguin
23303a759b Prevents fast double navigate
This puppeteer script [likely will] crash the brower:

page.goto(baseURL + '/campfire-commerce/');
await page.goto(baseURL + '/campfire-commerce/');

The quick double-navigation means parse_state remains .pre and thus the page
isn't reset. We introduce a new load_state, .waiting, which can be used to
detect this state.
2026-01-23 19:09:36 +08:00
Karl Seguin
d1e7f46994 Merge pull request #1402 from lightpanda-io/defensive_local_scopes
Some checks failed
e2e-test / zig build release (push) Waiting to run
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (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
Explicitly creates LocalScope in hard-to-reason callsites
2026-01-23 18:59:45 +08:00
Karl Seguin
65ea70ae90 Merge pull request #1403 from lightpanda-io/module_async_import_self
Support a module dynamically importing itself
2026-01-23 18:59:26 +08:00
Karl Seguin
7522b71c86 Support a module dynamically importing itself 2026-01-23 10:45:41 +08:00
Karl Seguin
70625c86c3 Explicitly creates LocalScope in hard-to-reason callsites
In some cases, it's straightforward to know whether or not there's an implicit
local scope available. But both Navigation and ReadableStream* have more complex
flows. Both could, in theory, be initiated from non-V8 calls, so relying on
the implicit scope isn't safe.

This adds an explicit scope to most callbacks in Navigation and ReadbleStream*.
However, I still don't quite understand how / if these are being initiated from
Zig currently (I could see how they would be, in the future). Therefore, in
debug mode, this will still panic if there's no implicit scope, because I want
to understand what's going on.
2026-01-23 09:58:36 +08:00
Alexis Bouchez
74354d2027 invoke window.onerror callback in reportError 2026-01-22 10:12:06 +01:00
Karl Seguin
f6397e2731 Handle scripts that don't return a 200 status code
This was already being handled for async scripts, but for sync scripts, we'd
log the error then proceed to try and execute the body (which would be some
error message).

This allows the header_callback to return a boolean to indicate whether or not
the http client should continue to process the request or abort it.
2026-01-22 14:15:00 +08:00
Karl Seguin
065ca39d60 Merge pull request #1397 from lightpanda-io/heapprofiler
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
Ability to capture a V8 heap profile and a heap snapshot
2026-01-22 12:17:47 +08:00
Karl Seguin
b4759ae261 Ability to capture a V8 heap profile and a heap snapshot 2026-01-22 10:27:58 +08:00
Karl Seguin
c095950ef9 Merge pull request #1395 from lightpanda-io/microtasks_on_local
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
e2e-integration-test / zig build release (push) Has been cancelled
e2e-integration-test / demo-integration-scripts (push) Has been cancelled
Move runMicrotask from Context to Local
2026-01-22 08:37:56 +08:00
Karl Seguin
24b7035b1b Merge pull request #1394 from lightpanda-io/avoid_double_doctype
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
On dump, use the HTMLDocument's doctype if available
2026-01-21 18:21:43 +08:00
Karl Seguin
7b1f157cf8 Merge pull request #1392 from lightpanda-io/handle_scope_for_message_loop
Create HandleScope for PumpMessageLoop
2026-01-21 15:44:14 +08:00
Karl Seguin
8b8bee4e9c Move runMicrotask from Context to Local
This ensures that there's always a HandleScope avaialble when running microtasks
2026-01-21 15:40:32 +08:00
Pierre Tachoire
c27ab35600 Merge pull request #1393 from lightpanda-io/remove_js_obj_cache
Disable JS object cache
2026-01-21 08:14:24 +01:00
Karl Seguin
446b4dc461 On dump, use the HTMLDocument's doctype if available
We currently force-write a simple HTML doctype for HTMLDocument. But if the
document already has a doctype, that results in us writing the forced one then
writing the correct one. This adds a check and only force-writes a doctype if
the first child of the document isn't a document_type node.
2026-01-21 14:15:11 +08:00
Karl Seguin
ff8ed24622 Merge pull request #1391 from lightpanda-io/lowmem-on-page-reset
call env.lowMemoryNotification() during page reset
2026-01-21 13:23:24 +08:00
Karl Seguin
ae2d6a122b Disable JS object cache
Was added here 43805ad698

But causes segfaults. The issue is hard to understand. At first, it seemed like
the value cached in a v8::Object was persisting through v8::contexts of the
same isolate. Set window.document to the current &document, and in a different
context, it retrieves that cached value (which is now an invalid pointers).

However, upon further investigation, this appears to be limited to a mix of
navigation (which causes a new context to be created, and old values to be
invalidated) + Inspector which continues to send commands to the old context.

Since contextDestroyed is something we're aware of and planning to do shortly,
I think we can disable the cache until that's fixed.
2026-01-21 11:26:59 +08:00
Karl Seguin
3cac375f21 Merge pull request #1386 from lightpanda-io/tweak_global_setup
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
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
Move global setup to the Env (Isolate)
2026-01-21 07:11:47 +08:00
Karl Seguin
7d806dd161 Merge pull request #1369 from lightpanda-io/selection-webapi
`Selection` WebAPI
2026-01-21 07:11:11 +08:00
Karl Seguin
db037c704e Merge pull request #1388 from lightpanda-io/pointer_event
add PointerEvent
2026-01-21 07:10:32 +08:00
Karl Seguin
954184f742 Create HandleScope for PumpMessageLoop 2026-01-21 07:05:59 +08:00
Muki Kiboigo
7650e0b61a fix selection start updating to new len 2026-01-20 11:25:04 -08:00
Muki Kiboigo
4a5c93988f fix selection test expectation 2026-01-20 11:24:50 -08:00
Pierre Tachoire
8ceaf0ac66 call env.lowMemoryNotification() during page reset
calling env.lowMemoryNotification() on page reset encourages v8 to free
memory and keep low usage.
2026-01-20 18:27:25 +01:00
Pierre Tachoire
ca60aa1cc6 Merge pull request #1387 from lightpanda-io/lower_perf_regression
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
Reduce perf regression max
2026-01-20 13:36:16 +01:00
Karl Seguin
596d5906a0 add PointerEvent 2026-01-20 18:38:03 +08:00
Karl Seguin
c02db94522 Reduce perf regression max
Mem 28MB -> 26MB  (currently a 24.1MB)
Time 23 -> 17  (currently at 14)

We've made some memory and performance optimization gains lately. Lowering
these will let us spot incremental changes better.
2026-01-20 18:07:28 +08:00
Karl Seguin
3970803575 Merge pull request #1382 from lightpanda-io/cached_properties
Re-enable cached property support
2026-01-20 18:00:19 +08:00
Karl Seguin
43805ad698 Re-enable cached property support
The idea is that frequently accessed properties, e.g. window.document, can be
cached directly as data properties on the underlying v8::Object, removing the
need for the access call into Zig. This is only used on a handful of properties,
almost all of which are on the Window. It is important that the property be
read-only. For example, window.location cannot be cached this way because
window.location is writable (e.g. window.location.hash = '#blah').

This existed briefly before Zigdom, but was removed as part of the migration.
The implementation has changed. This previously relied on a "postAttach" feature
which no longer exists. It is not integrated in the bridge/callback directly and
lazily applied after the first access.
2026-01-20 17:34:52 +08:00
Karl Seguin
2498e12f19 Move global setup to the Env (Isolate)
Previously, we were doing some global setup in the Snapshot, but this was
always being overwritten when creating a context. This meaningless setup in
the snapshot was removed.

The global setup is now done once per isolate, rather than once per context.
2026-01-20 17:21:45 +08:00
Karl Seguin
6f3cb4b48e Merge pull request #1385 from lightpanda-io/remove_debug_print
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
Remove a debug print
2026-01-20 16:36:26 +08:00
Karl Seguin
fbd047599e Merge pull request #1374 from lightpanda-io/fix_context_lifetime
Fix context lifetime
2026-01-20 16:24:12 +08:00
Karl Seguin
da00117622 Remove a debug print 2026-01-20 16:23:22 +08:00
Karl Seguin
e44c73bdf6 Merge pull request #1384 from lightpanda-io/htmlscript-src-absolute
`HTMLScriptElement` should return an absolute URL in `src`
2026-01-20 12:38:42 +08:00
Karl Seguin
e3cb7bd9f0 add test 2026-01-20 11:14:20 +08:00
Muki Kiboigo
08f5889ee5 getSrc should return an absolute URL 2026-01-19 18:50:24 -08:00
Muki Kiboigo
d5bfe74e1a add selection api to HTMLTextAreaElement 2026-01-19 18:37:52 -08:00
Muki Kiboigo
d7015fa3b6 add selection api to HTMLInputElement 2026-01-19 18:34:02 -08:00
Karl Seguin
9092651b5b Merge branch 'main' into fix_context_lifetime 2026-01-20 08:50:41 +08:00
Karl Seguin
2c53b48e0a add missing handlescope 2026-01-20 08:11:38 +08:00
Muki Kiboigo
319a1c3367 update WPT to include Selection 2026-01-19 07:12:40 -08:00
Muki Kiboigo
80dd590e8f add toString to Selection 2026-01-19 07:12:40 -08:00
Muki Kiboigo
992a8e8774 handle null anchor or focus nodes in Selection 2026-01-19 07:12:40 -08:00
Muki Kiboigo
f56d3bd193 do not modify old range in collapseToX 2026-01-19 07:12:40 -08:00
Muki Kiboigo
4ecc59d0c0 Fix a lot of Selection Issues
This uses the Chrome/Safari approach of only
having a single Range exist in the Selection.
This also better follows the W3C spec of Selection
2026-01-19 07:12:40 -08:00
Muki Kiboigo
5ebf82874b fix selection test inconsistency 2026-01-19 07:12:40 -08:00
Muki Kiboigo
12670a3153 fix extend direction in Selection 2026-01-19 07:12:40 -08:00
Muki Kiboigo
fa3a23134e properly return NotFoundError on removeRange 2026-01-19 07:12:39 -08:00
Muki Kiboigo
8291044abc fix collapseToStart on Selection 2026-01-19 07:12:39 -08:00
Muki Kiboigo
505e0799da add remaining functions to Selection 2026-01-19 07:12:39 -08:00
Muki Kiboigo
be1d463775 add Selection WebAPI test 2026-01-19 07:12:39 -08:00
Muki Kiboigo
a6fc5aa345 add getSelection to Window, Document 2026-01-19 07:12:37 -08:00
Muki Kiboigo
0e6e4db08b add Selection WebAPI 2026-01-19 07:11:45 -08:00
Karl Seguin
0edc1fcec7 fix rebase + migrate SubtleCrypto to new local 2026-01-19 07:36:14 +08:00
Karl Seguin
b46d3b22e2 Remove unnecessary handlescope
There's one _always_ created immediately before it.
2026-01-19 07:28:57 +08:00
Karl Seguin
412c881cd4 fix wpt and legacy_test runners 2026-01-19 07:28:56 +08:00
Karl Seguin
48f07a110f fix bad great rebase 2026-01-19 07:28:35 +08:00
Karl Seguin
5c1b7935e2 remove global handlescope 2026-01-19 07:28:35 +08:00
Karl Seguin
62aa564df1 Remove Global v8::Local<V8::Context>
When we create a js.Context, we create the underlying v8.Context and store it
for the duration of the page lifetime. This works because we have a global
HandleScope - the v8.Context (which is really a v8::Local<v8::Context>) is that
to the global HandleScope, effectively making it a global.

If we want to remove our global HandleScope, then we can no longer pin the
v8.Context in our js.Context. Our js.Context now only holds a v8.Global of the
v8.Context (v8::Global<v8::Context).

This PR introduces a new type, js.Local, which takes over a lot of the
functionality previously found in either js.Caller or js.Context. The simplest
way to think about it is:

1 - For v8 -> zig calls, we create a js.Caller (as always)
2 - For zig -> v8 calls, we go through the js.Context (as always)
3 - The shared functionality, which works on a v8.Context, now belongs to js.Local

For #1 (v8 -> zig), creating a js.Local for a js.Caller is really simple and
centralized. v8 largely gives us everything we need from the
FunctionCallbackInfo or PropertyCallbackInfo.  For #2, it's messier, because we
can only create a local v8::Context if we have a HandleScope, which we may or
may not.

Unfortunately, in many cases, what to do becomes the responsibility of the caller
and much of the code has to become aware of this local-ness. What does it means
for our code? The impact is on WebAPIs that store .Global. Because the global
can't do anything. You always need to convert that .Global to a local
(e.g. js.Function.Global -> js.Function).

If you're 100% sure the WebAPI is only being invoked by a v8 callback, you can
use `page.js.local.?.toLocal(some_global).call(...)` to get the local value.

If you're 100% sure the WebAPI is only being invoked by Zig, you need to create
 `js.Local.Scope` to get access to a local:

```zig
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
ls.toLocal(some_global).call(...)
// can also access `&ls.local` for APIs that require a *const js.Local
```
For functions that can be invoked by either V8 or Zig, you should generally push
the responsibility to the caller by accepting a `local: *const js.Local`. If the
caller is a v8 callback, it can pass `page.js.local.?`. If the caller is a Zig
callback, it can create a `Local.Scope`.

As an alternative, it is possible to simply pass the *Page, and check
`if page.js.local == null` and, if so, create a Local.Scope. But this should only
be done for performance reasons. We currently only do this in 1 place, and it's
because the Zig caller doesn't know whether a Local will actually be needed and
it's potentially called on every element creating from the parser.
2026-01-19 07:28:33 +08:00
Karl Seguin
798ee4a4d5 Make js.Object and js.Value have explicit global
See: bb06900b6f84abaccc7ecfd386af1a9dc0029c50 for details on this change.
2026-01-19 07:27:03 +08:00
Karl Seguin
7d87fb80ec 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:26:33 +08:00
141 changed files with 8392 additions and 3524 deletions

View File

@@ -13,7 +13,7 @@ inputs:
zig-v8:
description: 'zig v8 version to install'
required: false
default: 'v0.2.4'
default: 'v0.2.6'
v8:
description: 'v8 version to install'
required: false
@@ -32,7 +32,7 @@ runs:
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y wget xz-utils python3 ca-certificates git pkg-config libglib2.0-dev gperf libexpat1-dev cmake clang
sudo apt-get install -y wget xz-utils ca-certificates clang make git
# Zig version used from the `minimum_zig_version` field in build.zig.zon
- uses: mlugg/setup-zig@v2

View File

@@ -124,8 +124,8 @@ jobs:
needs: zig-build-release
env:
MAX_MEMORY: 28000
MAX_AVG_DURATION: 23
MAX_MEMORY: 26000
MAX_AVG_DURATION: 17
LIGHTPANDA_DISABLE_TELEMETRY: true
# use a self host runner.

View File

@@ -3,11 +3,12 @@ FROM debian:stable-slim
ARG MINISIG=0.12
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
ARG V8=14.0.365.4
ARG ZIG_V8=v0.2.4
ARG ZIG_V8=v0.2.6
ARG TARGETPLATFORM
RUN apt-get update -yq && \
apt-get install -yq xz-utils ca-certificates \
pkg-config libglib2.0-dev \
clang make curl git
# Get Rust

View File

@@ -178,6 +178,7 @@ For **Debian/Ubuntu based Linux**:
```
sudo apt install xz-utils ca-certificates \
pkg-config libglib2.0-dev \
clang make curl git
```
You also need to [install Rust](https://rust-lang.org/tools/install/).

View File

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

View File

@@ -21,13 +21,14 @@ const std = @import("std");
const Allocator = std.mem.Allocator;
const log = @import("log.zig");
const Http = @import("http/Http.zig");
const Snapshot = @import("browser/js/Snapshot.zig");
const Platform = @import("browser/js/Platform.zig");
const Notification = @import("Notification.zig");
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
pub const Http = @import("http/Http.zig");
pub const ArenaPool = @import("ArenaPool.zig");
pub const Notification = @import("Notification.zig");
// Container for global state / objects that various parts of the system
// might need.
const App = @This();
@@ -38,6 +39,7 @@ platform: Platform,
snapshot: Snapshot,
telemetry: Telemetry,
allocator: Allocator,
arena_pool: ArenaPool,
app_dir_path: ?[]const u8,
notification: *Notification,
shutdown: bool = false,
@@ -96,6 +98,9 @@ pub fn init(allocator: Allocator, config: Config) !*App {
try app.telemetry.register(app.notification);
app.arena_pool = ArenaPool.init(allocator);
errdefer app.arena_pool.deinit();
return app;
}
@@ -114,6 +119,7 @@ pub fn deinit(self: *App) void {
self.http.deinit();
self.snapshot.deinit();
self.platform.deinit();
self.arena_pool.deinit();
allocator.destroy(self);
}

84
src/ArenaPool.zig Normal file
View File

@@ -0,0 +1,84 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const ArenaPool = @This();
allocator: Allocator,
retain_bytes: usize,
free_list_len: u16 = 0,
free_list: ?*Entry = null,
free_list_max: u16,
entry_pool: std.heap.MemoryPool(Entry),
const Entry = struct {
next: ?*Entry,
arena: ArenaAllocator,
};
pub fn init(allocator: Allocator) ArenaPool {
return .{
.allocator = allocator,
.free_list_max = 512, // TODO make configurable
.retain_bytes = 1024 * 16, // TODO make configurable
.entry_pool = std.heap.MemoryPool(Entry).init(allocator),
};
}
pub fn deinit(self: *ArenaPool) void {
var entry = self.free_list;
while (entry) |e| {
entry = e.next;
e.arena.deinit();
}
self.entry_pool.deinit();
}
pub fn acquire(self: *ArenaPool) !Allocator {
if (self.free_list) |entry| {
self.free_list = entry.next;
return entry.arena.allocator();
}
const entry = try self.entry_pool.create();
entry.* = .{
.next = null,
.arena = ArenaAllocator.init(self.allocator),
};
return entry.arena.allocator();
}
pub fn release(self: *ArenaPool, allocator: Allocator) void {
const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(allocator.ptr));
const entry: *Entry = @fieldParentPtr("arena", arena);
if (self.free_list_len == self.free_list_max) {
arena.deinit();
self.entry_pool.destroy(entry);
return;
}
_ = arena.reset(.{ .retain_with_limit = self.retain_bytes });
entry.next = self.free_list;
self.free_list = entry;
}

View File

@@ -24,8 +24,10 @@ const ArenaAllocator = std.heap.ArenaAllocator;
const js = @import("js/js.zig");
const log = @import("../log.zig");
const App = @import("../App.zig");
const HttpClient = @import("../http/Client.zig");
const Notification = @import("../Notification.zig");
const ArenaPool = App.ArenaPool;
const HttpClient = App.Http.Client;
const Notification = App.Notification;
const IS_DEBUG = @import("builtin").mode == .Debug;
@@ -40,6 +42,7 @@ env: js.Env,
app: *App,
session: ?Session,
allocator: Allocator,
arena_pool: *ArenaPool,
http_client: *HttpClient,
call_arena: ArenaAllocator,
page_arena: ArenaAllocator,
@@ -64,6 +67,7 @@ pub fn init(app: *App) !Browser {
.session = null,
.allocator = allocator,
.notification = notification,
.arena_pool = &app.arena_pool,
.http_client = app.http.client,
.call_arena = ArenaAllocator.init(allocator),
.page_arena = ArenaAllocator.init(allocator),
@@ -96,7 +100,7 @@ pub fn closeSession(self: *Browser) void {
session.deinit();
self.session = null;
_ = self.session_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
self.env.lowMemoryNotification();
self.env.memoryPressureNotification(.critical);
}
}

View File

@@ -137,7 +137,10 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) !void
var was_handled = false;
defer if (was_handled) {
self.page.js.runMicrotasks();
var ls: js.Local.Scope = undefined;
self.page.js.localScope(&ls);
defer ls.deinit();
ls.local.runMicrotasks();
};
switch (target._type) {
@@ -180,7 +183,10 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
var was_dispatched = false;
defer if (was_dispatched) {
self.page.js.runMicrotasks();
var ls: js.Local.Scope = undefined;
self.page.js.localScope(&ls);
defer ls.deinit();
ls.local.runMicrotasks();
};
if (function_) |func| {
@@ -367,14 +373,18 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
event._target = getAdjustedTarget(original_target, current_target);
}
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
switch (listener.function) {
.value => |value| try value.local().callWithThis(void, current_target, .{event}),
.value => |value| try ls.toLocal(value).callWithThis(void, current_target, .{event}),
.string => |string| {
const str = try page.call_arena.dupeZ(u8, string.str());
try self.page.js.eval(str, null);
try ls.local.eval(str, null);
},
.object => |*obj_global| {
const obj = obj_global.local();
.object => |obj_global| {
const obj = ls.toLocal(obj_global);
if (try obj.getFunction("handleEvent")) |handleEvent| {
try handleEvent.callWithThis(void, obj, .{event});
}

View File

@@ -1,4 +1,4 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
@@ -29,6 +29,7 @@ const Page = @import("Page.zig");
const Node = @import("webapi/Node.zig");
const Event = @import("webapi/Event.zig");
const UIEvent = @import("webapi/event/UIEvent.zig");
const MouseEvent = @import("webapi/event/MouseEvent.zig");
const Element = @import("webapi/Element.zig");
const Document = @import("webapi/Document.zig");
const EventTarget = @import("webapi/EventTarget.zig");
@@ -36,6 +37,8 @@ const XMLHttpRequestEventTarget = @import("webapi/net/XMLHttpRequestEventTarget.
const Blob = @import("webapi/Blob.zig");
const AbstractRange = @import("webapi/AbstractRange.zig");
const Allocator = std.mem.Allocator;
const IS_DEBUG = builtin.mode == .Debug;
const assert = std.debug.assert;
@@ -213,6 +216,29 @@ pub fn uiEvent(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child)
return chain.get(2);
}
pub fn mouseEvent(self: *Factory, typ: []const u8, mouse: MouseEvent, child: anytype) !*@TypeOf(child) {
const allocator = self._slab.allocator();
const chain = try PrototypeChain(
&.{ Event, UIEvent, MouseEvent, @TypeOf(child) },
).allocate(allocator);
// Special case: Event has a _type_string field, so we need manual setup
const event_ptr = chain.get(0);
event_ptr.* = try eventInit(typ, chain.get(1), self._page);
chain.setMiddle(1, UIEvent.Type);
// Set MouseEvent with all its fields
const mouse_ptr = chain.get(2);
mouse_ptr.* = mouse;
mouse_ptr._proto = chain.get(1);
mouse_ptr._type = unionInit(MouseEvent.Type, chain.get(3));
chain.setLeaf(3, child);
return chain.get(3);
}
pub fn blob(self: *Factory, child: anytype) !*@TypeOf(child) {
const allocator = self._slab.allocator();
@@ -320,9 +346,7 @@ pub fn svgElement(self: *Factory, tag_name: []const u8, child: anytype) !*@TypeO
return chain.get(4);
}
pub fn xhrEventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
const allocator = self._slab.allocator();
pub fn xhrEventTarget(_: *const Factory, allocator: Allocator, child: anytype) !*@TypeOf(child) {
return try AutoPrototypeChain(
&.{ EventTarget, XMLHttpRequestEventTarget, @TypeOf(child) },
).create(allocator, child);
@@ -337,32 +361,6 @@ pub fn textTrackCue(self: *Factory, child: anytype) !*@TypeOf(child) {
).create(allocator, child);
}
fn hasChainRoot(comptime T: type) bool {
// Check if this is a root
if (@hasDecl(T, "_prototype_root")) {
return true;
}
// If no _proto field, we're at the top but not a recognized root
if (!@hasField(T, "_proto")) return false;
// Get the _proto field's type and recurse
const fields = @typeInfo(T).@"struct".fields;
inline for (fields) |field| {
if (std.mem.eql(u8, field.name, "_proto")) {
const ProtoType = reflect.Struct(field.type);
return hasChainRoot(ProtoType);
}
}
return false;
}
fn isChainType(comptime T: type) bool {
if (@hasField(T, "_proto")) return false;
return comptime hasChainRoot(T);
}
pub fn destroy(self: *Factory, value: anytype) void {
const S = reflect.Struct(@TypeOf(value));
@@ -379,7 +377,7 @@ pub fn destroy(self: *Factory, value: anytype) void {
}
}
if (comptime isChainType(S)) {
if (comptime @hasField(S, "_proto")) {
self.destroyChain(value, true, 0, std.mem.Alignment.@"1");
} else {
self.destroyStandalone(value);
@@ -387,20 +385,7 @@ pub fn destroy(self: *Factory, value: anytype) void {
}
pub fn destroyStandalone(self: *Factory, value: anytype) void {
const S = reflect.Struct(@TypeOf(value));
assert(!@hasDecl(S, "_prototype_root"));
const allocator = self._slab.allocator();
if (@hasDecl(S, "deinit")) {
// And it has a deinit, we'll call it
switch (@typeInfo(@TypeOf(S.deinit)).@"fn".params.len) {
1 => value.deinit(),
2 => value.deinit(self._page),
else => @compileLog(@typeName(S) ++ " has an invalid deinit function"),
}
}
allocator.destroy(value);
}
@@ -416,10 +401,8 @@ fn destroyChain(
// aligns the old size to the alignment of this element
const current_size = std.mem.alignForward(usize, old_size, @alignOf(S));
const alignment = std.mem.Alignment.fromByteUnits(@alignOf(S));
const new_align = std.mem.Alignment.max(old_align, alignment);
const new_size = current_size + @sizeOf(S);
const new_align = std.mem.Alignment.max(old_align, std.mem.Alignment.of(S));
// This is initially called from a deinit. We don't want to call that
// same deinit. So when this is the first time destroyChain is called
@@ -438,20 +421,15 @@ fn destroyChain(
if (@hasField(S, "_proto")) {
self.destroyChain(value._proto, false, new_size, new_align);
} else if (@hasDecl(S, "JsApi")) {
// Doesn't have a _proto, but has a JsApi.
if (self._page.js.removeTaggedMapping(@intFromPtr(value))) |tagged| {
allocator.destroy(tagged);
}
} else {
// no proto so this is the head of the chain.
// we use this as the ptr to the start of the chain.
// and we have summed up the length.
assert(@hasDecl(S, "_prototype_root"));
const memory_ptr: [*]const u8 = @ptrCast(value);
const memory_ptr: [*]u8 = @ptrCast(@constCast(value));
const len = std.mem.alignForward(usize, new_size, new_align.toByteUnits());
allocator.free(memory_ptr[0..len]);
allocator.rawFree(memory_ptr[0..len], new_align, @returnAddress());
}
}

View File

@@ -27,7 +27,7 @@ const IS_DEBUG = builtin.mode == .Debug;
const log = @import("../log.zig");
const Http = @import("../http/Http.zig");
const App = @import("../App.zig");
const String = @import("../string.zig").String;
const Mime = @import("Mime.zig");
@@ -59,6 +59,9 @@ const PageTransitionEvent = @import("webapi/event/PageTransitionEvent.zig");
const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind;
const KeyboardEvent = @import("webapi/event/KeyboardEvent.zig");
const Http = App.Http;
const ArenaPool = App.ArenaPool;
const timestamp = @import("../datetime.zig").timestamp;
const milliTimestamp = @import("../datetime.zig").milliTimestamp;
@@ -99,6 +102,20 @@ _element_shadow_roots: Element.ShadowRootLookup = .{},
_node_owner_documents: Node.OwnerDocumentLookup = .{},
_element_assigned_slots: Element.AssignedSlotLookup = .{},
/// Lazily-created inline event listeners (or listeners provided as attributes).
/// Avoids bloating all elements with extra function fields for rare usage.
///
/// Use this when a listener provided like these:
///
/// ```html
/// <img onload="(() => { ... })()" />
/// ```
///
/// ```js
/// img.onload = () => { ... };
/// ```
_element_attr_listeners: Element.AttrListenerLookup = .{},
_script_manager: ScriptManager,
// List of active MutationObservers
@@ -168,6 +185,14 @@ arena: Allocator,
// from JS. Best arena to use, when possible.
call_arena: Allocator,
arena_pool: *ArenaPool,
// In Debug, we use this to see if anything fails to release an arena back to
// the pool.
_arena_pool_leak_track: (if (IS_DEBUG) std.AutoHashMapUnmanaged(usize, struct {
owner: []const u8,
count: usize,
}) else void),
window: *Window,
document: *Document,
@@ -185,10 +210,14 @@ pub fn init(arena: Allocator, call_arena: Allocator, session: *Session) !*Page {
}
const page = try session.browser.allocator.create(Page);
page._session = session;
page.arena = arena;
page.call_arena = call_arena;
page._session = session;
page.arena_pool = session.browser.arena_pool;
if (comptime IS_DEBUG) {
page._arena_pool_leak_track = .empty;
}
try page.reset(true);
return page;
@@ -205,9 +234,14 @@ pub fn deinit(self: *Page) void {
// stats.print(&stream) catch unreachable;
}
// some MicroTasks might be referencing the page, we need to drain it while
// the page still exists
self.js.runMicrotasks();
{
// some MicroTasks might be referencing the page, we need to drain it while
// the page still exists
var ls: JS.Local.Scope = undefined;
self.js.localScope(&ls);
defer ls.deinit();
ls.local.runMicrotasks();
}
const session = self._session;
session.executor.removeContext();
@@ -215,12 +249,38 @@ pub fn deinit(self: *Page) void {
self._script_manager.shutdown = true;
session.browser.http_client.abort();
self._script_manager.deinit();
if (comptime IS_DEBUG) {
var it = self._arena_pool_leak_track.valueIterator();
while (it.next()) |value_ptr| {
if (value_ptr.count > 0) {
log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner });
}
}
}
session.browser.allocator.destroy(self);
}
fn reset(self: *Page, comptime initializing: bool) !void {
if (comptime initializing == false) {
self._session.executor.removeContext();
// removing a context can trigger finalizers, so we can only check for
// a leak after the above.
if (comptime IS_DEBUG) {
var it = self._arena_pool_leak_track.valueIterator();
while (it.next()) |value_ptr| {
log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.* });
}
self._arena_pool_leak_track.clearRetainingCapacity();
}
// We force a garbage collection between page navigations to keep v8
// memory usage as low as possible.
self._session.browser.env.memoryPressureNotification(.moderate);
self._script_manager.shutdown = true;
self._session.browser.http_client.abort();
self._script_manager.deinit();
@@ -250,7 +310,7 @@ fn reset(self: *Page, comptime initializing: bool) !void {
self.window._location = &default_location;
self._parse_state = .pre;
self._load_state = .parsing;
self._load_state = .waiting;
self._queued_navigation = null;
self._parse_mode = .document;
self._attribute_lookup = .empty;
@@ -270,6 +330,9 @@ fn reset(self: *Page, comptime initializing: bool) !void {
self._element_shadow_roots = .{};
self._node_owner_documents = .{};
self._element_assigned_slots = .{};
self._element_attr_listeners = .{};
self._notified_network_idle = .init;
self._notified_network_almost_idle = .init;
@@ -287,6 +350,10 @@ fn reset(self: *Page, comptime initializing: bool) !void {
self._customized_builtin_disconnected_callback_invoked = .{};
self._undefined_custom_elements = .{};
if (comptime IS_DEBUG) {
self._arena_pool_leak_track = .{};
}
try self.registerBackgroundTasks();
}
@@ -322,6 +389,33 @@ pub fn getOrigin(self: *Page, allocator: Allocator) !?[]const u8 {
return try URL.getOrigin(allocator, self.url);
}
const GetArenaOpts = struct {
debug: []const u8,
};
pub fn getArena(self: *Page, comptime opts: GetArenaOpts) !Allocator {
const allocator = try self.arena_pool.acquire();
if (comptime IS_DEBUG) {
const gop = try self._arena_pool_leak_track.getOrPut(self.arena, @intFromPtr(allocator.ptr));
if (gop.found_existing) {
std.debug.assert(gop.value_ptr.count == 0);
}
gop.value_ptr.* = .{ .owner = opts.debug, .count = 1 };
}
return allocator;
}
pub fn releaseArena(self: *Page, allocator: Allocator) void {
if (comptime IS_DEBUG) {
const found = self._arena_pool_leak_track.getPtr(@intFromPtr(allocator.ptr)).?;
if (found.count != 1) {
log.err(.bug, "ArenaPool Double Free", .{ .owner = found.owner, .count = found.count });
return;
}
found.count = 0;
}
return self.arena_pool.release(allocator);
}
pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool {
const current_origin = (try URL.getOrigin(self.call_arena, self.url)) orelse return false;
return std.mem.startsWith(u8, url, current_origin);
@@ -329,11 +423,12 @@ pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool {
pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !void {
const session = self._session;
if (self._parse_state != .pre) {
if (self._parse_state != .pre or self._load_state != .waiting) {
// it's possible for navigate to be called multiple times on the
// same page (via CDP). We want to reset the page between each call.
try self.reset(false);
}
self._load_state = .parsing;
const req_id = self._session.browser.http_client.nextReqId();
log.info(.page, "navigate", .{
@@ -562,26 +657,29 @@ fn _documentIsComplete(self: *Page) !void {
const event = try Event.initTrusted("load", .{}, self);
// this event is weird, it's dispatched directly on the window, but
// with the document as the target
var ls: JS.Local.Scope = undefined;
self.js.localScope(&ls);
defer ls.deinit();
event._target = self.document.asEventTarget();
const on_load = if (self.window._on_load) |*g| g.local() else null;
try self._event_manager.dispatchWithFunction(
self.window.asEventTarget(),
event,
on_load,
ls.toLocal(self.window._on_load),
.{ .inject_target = false, .context = "page load" },
);
const pageshow_event = try PageTransitionEvent.initTrusted("pageshow", .{}, self);
const on_pageshow = if (self.window._on_pageshow) |*g| g.local() else null;
try self._event_manager.dispatchWithFunction(
self.window.asEventTarget(),
pageshow_event.asEvent(),
on_pageshow,
ls.toLocal(self.window._on_pageshow),
.{ .context = "page show" },
);
}
fn pageHeaderDoneCallback(transfer: *Http.Transfer) !void {
fn pageHeaderDoneCallback(transfer: *Http.Transfer) !bool {
var self: *Page = @ptrCast(@alignCast(transfer.ctx));
// would be different than self.url in the case of a redirect
@@ -598,6 +696,8 @@ fn pageHeaderDoneCallback(transfer: *Http.Transfer) !void {
.content_type = header.contentType(),
});
}
return true;
}
fn pageDataCallback(transfer: *Http.Transfer, data: []const u8) !void {
@@ -671,7 +771,10 @@ fn pageDoneCallback(ctx: *anyopaque) !void {
switch (self._parse_state) {
.html => |buf| {
var parser = Parser.init(self.arena, self.document.asNode(), self);
const parse_arena = try self.getArena(.{ .debug = "Page.parse" });
defer self.releaseArena(parse_arena);
var parser = Parser.init(parse_arena, self.document.asNode(), self);
parser.parse(buf.items);
self._script_manager.staticScriptsDone();
if (self._script_manager.isDone()) {
@@ -683,7 +786,11 @@ fn pageDoneCallback(ctx: *anyopaque) !void {
},
.text => |*buf| {
try buf.appendSlice(self.arena, "</pre></body></html>");
var parser = Parser.init(self.arena, self.document.asNode(), self);
const parse_arena = try self.getArena(.{ .debug = "Page.parse" });
defer self.releaseArena(parse_arena);
var parser = Parser.init(parse_arena, self.document.asNode(), self);
parser.parse(buf.items);
self.documentIsComplete();
},
@@ -748,10 +855,6 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult {
var timer = try std.time.Timer.start();
var ms_remaining = wait_ms;
var try_catch: JS.TryCatch = undefined;
try_catch.init(self.js);
defer try_catch.deinit();
var scheduler = &self.scheduler;
var http_client = self._session.browser.http_client;
@@ -808,10 +911,6 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult {
// it AFTER.
const ms_to_next_task = try scheduler.run();
if (try_catch.caught(self.call_arena)) |caught| {
log.info(.js, "page wait", .{ .caught = caught, .src = "scheduler" });
}
const http_active = http_client.active;
const total_network_activity = http_active + http_client.intercepted;
if (self._notified_network_almost_idle.check(total_network_activity <= 2)) {
@@ -962,16 +1061,6 @@ fn printWaitAnalysis(self: *Page) void {
}
}
pub fn tick(self: *Page) void {
if (comptime IS_DEBUG) {
log.debug(.page, "tick", .{});
}
_ = self.scheduler.run() catch |err| {
log.err(.page, "tick", .{ .err = err });
};
self.js.runMicrotasks();
}
pub fn isGoingAway(self: *const Page) bool {
return self._queued_navigation != null;
}
@@ -985,7 +1074,7 @@ pub fn scriptAddedCallback(self: *Page, comptime from_parser: bool, script: *Ele
self._script_manager.addFromElement(from_parser, script, "parsing") catch |err| {
log.err(.page, "page.scriptAddedCallback", .{
.err = err,
.src = script.asElement().getAttributeSafe("src"),
.src = script.asElement().getAttributeSafe(comptime .wrap("src")),
});
};
}
@@ -1070,7 +1159,7 @@ pub fn getElementByIdFromNode(self: *Page, node: *Node, id: []const u8) ?*Elemen
}
var tw = @import("webapi/TreeWalker.zig").Full.Elements.init(node, .{});
while (tw.next()) |el| {
const element_id = el.getAttributeSafe("id") orelse continue;
const element_id = el.getAttributeSafe(comptime .wrap("id")) orelse continue;
if (std.mem.eql(u8, element_id, id)) {
return el;
}
@@ -1078,6 +1167,35 @@ pub fn getElementByIdFromNode(self: *Page, node: *Node, id: []const u8) ?*Elemen
return null;
}
/// Sets an inline event listener (`onload`, `onclick`, `onwheel` etc.);
/// overrides the listener if there's already one.
pub fn setAttrListener(
self: *Page,
element: *Element,
listener_type: Element.KnownListener,
listener_callback: JS.Function.Global,
) !void {
if (comptime IS_DEBUG) {
log.debug(.event, "Page.setAttrListener", .{
.element = element,
.listener_type = listener_type,
});
}
const key = element.calcAttrListenerKey(listener_type);
const gop = try self._element_attr_listeners.getOrPut(self.arena, key);
gop.value_ptr.* = listener_callback;
}
/// Returns the inline event listener by an element and listener type.
pub fn getAttrListener(
self: *const Page,
element: *Element,
listener_type: Element.KnownListener,
) ?JS.Function.Global {
return self._element_attr_listeners.get(element.calcAttrListenerKey(listener_type));
}
pub fn registerPerformanceObserver(self: *Page, observer: *PerformanceObserver) !void {
return self._performance_observers.append(self.arena, observer);
}
@@ -1644,7 +1762,7 @@ pub fn createElementNS(self: *Page, namespace: Element.Namespace, name: []const
// If page's base url is not already set, fill it with the base
// tag.
if (self.base_url == null) {
if (n.as(Element).getAttributeSafe("href")) |href| {
if (n.as(Element).getAttributeSafe(comptime .wrap("href"))) |href| {
self.base_url = try URL.resolve(self.arena, self.url, href, .{});
}
}
@@ -2005,8 +2123,12 @@ pub fn createElementNS(self: *Page, namespace: Element.Namespace, name: []const
self._upgrading_element = node;
defer self._upgrading_element = prev_upgrading;
var ls: JS.Local.Scope = undefined;
self.js.localScope(&ls);
defer ls.deinit();
var caught: JS.TryCatch.Caught = undefined;
_ = def.constructor.local().newInstance(&caught) catch |err| {
_ = ls.toLocal(def.constructor).newInstance(&caught) catch |err| {
log.warn(.js, "custom element constructor", .{ .name = name, .err = err, .caught = caught });
return node;
};
@@ -2018,9 +2140,9 @@ pub fn createElementNS(self: *Page, namespace: Element.Namespace, name: []const
while (it.next()) |attr| {
Element.Html.Custom.invokeAttributeChangedCallbackOnElement(
element,
attr._name.str(),
attr._name,
null, // old_value is null for initial attributes
attr._value.str(),
attr._value,
self,
);
}
@@ -2113,6 +2235,236 @@ fn populateElementAttributes(self: *Page, element: *Element, list: anytype) !voi
}
var attributes = try element.createAttributeList(self);
while (list.next()) |attr| {
// Event handlers can be provided like attributes; here we check if there's such.
const name = attr.name.local;
lp.assert(name.len != 0, "populateElementAttributes: 0-length attr name", .{ .attr = attr });
// Idea here is to make this check as cheap as possible.
const has_on_prefix = @as(u16, @bitCast([2]u8{ name.ptr[0], name.ptr[1 % name.len] })) == asUint("on");
// We may have found an event handler.
if (has_on_prefix) {
// Must be usable as function.
const func = self.js.stringToPersistedFunction(attr.value.slice()) catch continue;
// Longest known listener kind is 32 bytes long.
const remaining: u6 = @truncate(name.len -| 2);
const unsafe = name.ptr + 2;
const Vec16x8 = @Vector(16, u8);
const Vec32x8 = @Vector(32, u8);
switch (remaining) {
3 => if (@as(u24, @bitCast(unsafe[0..3].*)) == asUint("cut")) {
try self.setAttrListener(element, .cut, func);
},
4 => switch (@as(u32, @bitCast(unsafe[0..4].*))) {
asUint("blur") => try self.setAttrListener(element, .blur, func),
asUint("copy") => try self.setAttrListener(element, .copy, func),
asUint("drag") => try self.setAttrListener(element, .drag, func),
asUint("drop") => try self.setAttrListener(element, .drop, func),
asUint("load") => try self.setAttrListener(element, .load, func),
asUint("play") => try self.setAttrListener(element, .play, func),
else => {},
},
5 => switch (@as(u40, @bitCast(unsafe[0..5].*))) {
asUint("abort") => try self.setAttrListener(element, .abort, func),
asUint("click") => try self.setAttrListener(element, .click, func),
asUint("close") => try self.setAttrListener(element, .close, func),
asUint("ended") => try self.setAttrListener(element, .ended, func),
asUint("error") => try self.setAttrListener(element, .@"error", func),
asUint("focus") => try self.setAttrListener(element, .focus, func),
asUint("input") => try self.setAttrListener(element, .input, func),
asUint("keyup") => try self.setAttrListener(element, .keyup, func),
asUint("paste") => try self.setAttrListener(element, .paste, func),
asUint("pause") => try self.setAttrListener(element, .pause, func),
asUint("reset") => try self.setAttrListener(element, .reset, func),
asUint("wheel") => try self.setAttrListener(element, .wheel, func),
else => {},
},
6 => switch (@as(u48, @bitCast(unsafe[0..6].*))) {
asUint("cancel") => try self.setAttrListener(element, .cancel, func),
asUint("change") => try self.setAttrListener(element, .change, func),
asUint("resize") => try self.setAttrListener(element, .resize, func),
asUint("scroll") => try self.setAttrListener(element, .scroll, func),
asUint("seeked") => try self.setAttrListener(element, .seeked, func),
asUint("select") => try self.setAttrListener(element, .select, func),
asUint("submit") => try self.setAttrListener(element, .submit, func),
asUint("toggle") => try self.setAttrListener(element, .toggle, func),
else => {},
},
7 => switch (@as(u56, @bitCast(unsafe[0..7].*))) {
asUint("canplay") => try self.setAttrListener(element, .canplay, func),
asUint("command") => try self.setAttrListener(element, .command, func),
asUint("dragend") => try self.setAttrListener(element, .dragend, func),
asUint("emptied") => try self.setAttrListener(element, .emptied, func),
asUint("invalid") => try self.setAttrListener(element, .invalid, func),
asUint("keydown") => try self.setAttrListener(element, .keydown, func),
asUint("mouseup") => try self.setAttrListener(element, .mouseup, func),
asUint("playing") => try self.setAttrListener(element, .playing, func),
asUint("seeking") => try self.setAttrListener(element, .seeking, func),
asUint("stalled") => try self.setAttrListener(element, .stalled, func),
asUint("suspend") => try self.setAttrListener(element, .@"suspend", func),
asUint("waiting") => try self.setAttrListener(element, .waiting, func),
else => {},
},
8 => switch (@as(u64, @bitCast(unsafe[0..8].*))) {
asUint("auxclick") => try self.setAttrListener(element, .auxclick, func),
asUint("dblclick") => try self.setAttrListener(element, .dblclick, func),
asUint("dragexit") => try self.setAttrListener(element, .dragexit, func),
asUint("dragover") => try self.setAttrListener(element, .dragover, func),
asUint("formdata") => try self.setAttrListener(element, .formdata, func),
asUint("keypress") => try self.setAttrListener(element, .keypress, func),
asUint("mouseout") => try self.setAttrListener(element, .mouseout, func),
asUint("progress") => try self.setAttrListener(element, .progress, func),
else => {},
},
// Won't fit to 64-bit integer; we do 2 checks.
9 => switch (@as(u64, @bitCast(unsafe[0..8].*))) {
asUint("cuechang") => if (unsafe[8] == 'e') try self.setAttrListener(element, .cuechange, func),
asUint("dragente") => if (unsafe[8] == 'r') try self.setAttrListener(element, .dragenter, func),
asUint("dragleav") => if (unsafe[8] == 'e') try self.setAttrListener(element, .dragleave, func),
asUint("dragstar") => if (unsafe[8] == 't') try self.setAttrListener(element, .dragstart, func),
asUint("loadstar") => if (unsafe[8] == 't') try self.setAttrListener(element, .loadstart, func),
asUint("mousedow") => if (unsafe[8] == 'n') try self.setAttrListener(element, .mousedown, func),
asUint("mousemov") => if (unsafe[8] == 'e') try self.setAttrListener(element, .mousemove, func),
asUint("mouseove") => if (unsafe[8] == 'r') try self.setAttrListener(element, .mouseover, func),
asUint("pointeru") => if (unsafe[8] == 'p') try self.setAttrListener(element, .pointerup, func),
asUint("scrollen") => if (unsafe[8] == 'd') try self.setAttrListener(element, .scrollend, func),
else => {},
},
10 => switch (@as(u64, @bitCast(unsafe[0..8].*))) {
asUint("loadedda") => if (asUint("ta") == @as(u16, @bitCast(unsafe[8..10].*)))
try self.setAttrListener(element, .loadeddata, func),
asUint("pointero") => if (asUint("ut") == @as(u16, @bitCast(unsafe[8..10].*)))
try self.setAttrListener(element, .pointerout, func),
asUint("ratechan") => if (asUint("ge") == @as(u16, @bitCast(unsafe[8..10].*)))
try self.setAttrListener(element, .ratechange, func),
asUint("slotchan") => if (asUint("ge") == @as(u16, @bitCast(unsafe[8..10].*)))
try self.setAttrListener(element, .slotchange, func),
asUint("timeupda") => if (asUint("te") == @as(u16, @bitCast(unsafe[8..10].*)))
try self.setAttrListener(element, .timeupdate, func),
else => {},
},
11 => switch (@as(u64, @bitCast(unsafe[0..8].*))) {
asUint("beforein") => if (asUint("put") == @as(u24, @bitCast(unsafe[8..11].*)))
try self.setAttrListener(element, .beforeinput, func),
asUint("beforema") => if (asUint("tch") == @as(u24, @bitCast(unsafe[8..11].*)))
try self.setAttrListener(element, .beforematch, func),
asUint("contextl") => if (asUint("ost") == @as(u24, @bitCast(unsafe[8..11].*)))
try self.setAttrListener(element, .contextlost, func),
asUint("contextm") => if (asUint("enu") == @as(u24, @bitCast(unsafe[8..11].*)))
try self.setAttrListener(element, .contextmenu, func),
asUint("pointerd") => if (asUint("own") == @as(u24, @bitCast(unsafe[8..11].*)))
try self.setAttrListener(element, .pointerdown, func),
asUint("pointerm") => if (asUint("ove") == @as(u24, @bitCast(unsafe[8..11].*)))
try self.setAttrListener(element, .pointermove, func),
asUint("pointero") => if (asUint("ver") == @as(u24, @bitCast(unsafe[8..11].*)))
try self.setAttrListener(element, .pointerover, func),
asUint("selectst") => if (asUint("art") == @as(u24, @bitCast(unsafe[8..11].*)))
try self.setAttrListener(element, .selectstart, func),
else => {},
},
12 => switch (@as(u64, @bitCast(unsafe[0..8].*))) {
asUint("animatio") => if (asUint("nend") == @as(u32, @bitCast(unsafe[8..12].*)))
try self.setAttrListener(element, .animationend, func),
asUint("beforeto") => if (asUint("ggle") == @as(u32, @bitCast(unsafe[8..12].*)))
try self.setAttrListener(element, .beforetoggle, func),
asUint("pointere") => if (asUint("nter") == @as(u32, @bitCast(unsafe[8..12].*)))
try self.setAttrListener(element, .pointerenter, func),
asUint("pointerl") => if (asUint("eave") == @as(u32, @bitCast(unsafe[8..12].*)))
try self.setAttrListener(element, .pointerleave, func),
asUint("volumech") => if (asUint("ange") == @as(u32, @bitCast(unsafe[8..12].*)))
try self.setAttrListener(element, .volumechange, func),
else => {},
},
13 => switch (@as(u64, @bitCast(unsafe[0..8].*))) {
asUint("pointerc") => if (asUint("ancel") == @as(u40, @bitCast(unsafe[8..13].*)))
try self.setAttrListener(element, .pointercancel, func),
asUint("transiti") => switch (@as(u40, @bitCast(unsafe[8..13].*))) {
asUint("onend") => try self.setAttrListener(element, .transitionend, func),
asUint("onrun") => try self.setAttrListener(element, .transitionrun, func),
else => {},
},
else => {},
},
14 => switch (@as(u64, @bitCast(unsafe[0..8].*))) {
asUint("animatio") => if (asUint("nstart") == @as(u48, @bitCast(unsafe[8..14].*)))
try self.setAttrListener(element, .animationstart, func),
asUint("canplayt") => if (asUint("hrough") == @as(u48, @bitCast(unsafe[8..14].*)))
try self.setAttrListener(element, .canplaythrough, func),
asUint("duration") => if (asUint("change") == @as(u48, @bitCast(unsafe[8..14].*)))
try self.setAttrListener(element, .durationchange, func),
asUint("loadedme") => if (asUint("tadata") == @as(u48, @bitCast(unsafe[8..14].*)))
try self.setAttrListener(element, .loadedmetadata, func),
else => {},
},
15 => switch (@as(u64, @bitCast(unsafe[0..8].*))) {
asUint("animatio") => if (asUint("ncancel") == @as(u56, @bitCast(unsafe[8..15].*)))
try self.setAttrListener(element, .animationcancel, func),
asUint("contextr") => if (asUint("estored") == @as(u56, @bitCast(unsafe[8..15].*)))
try self.setAttrListener(element, .contextrestored, func),
asUint("fullscre") => if (asUint("enerror") == @as(u56, @bitCast(unsafe[8..15].*)))
try self.setAttrListener(element, .fullscreenerror, func),
asUint("selectio") => if (asUint("nchange") == @as(u56, @bitCast(unsafe[8..15].*)))
try self.setAttrListener(element, .selectionchange, func),
asUint("transiti") => if (asUint("onstart") == @as(u56, @bitCast(unsafe[8..15].*)))
try self.setAttrListener(element, .transitionstart, func),
else => {},
},
// Can't switch on vector types.
16 => {
const as_vector: Vec16x8 = unsafe[0..16].*;
if (@reduce(.And, as_vector == @as(Vec16x8, "fullscreenchange".*))) {
try self.setAttrListener(element, .fullscreenchange, func);
} else if (@reduce(.And, as_vector == @as(Vec16x8, "pointerrawupdate".*))) {
try self.setAttrListener(element, .pointerrawupdate, func);
} else if (@reduce(.And, as_vector == @as(Vec16x8, "transitioncancel".*))) {
try self.setAttrListener(element, .transitioncancel, func);
}
},
17 => {
const as_vector: Vec16x8 = unsafe[0..16].*;
const dirty = @reduce(.And, as_vector == @as(Vec16x8, "gotpointercaptur".*)) and
unsafe[16] == 'e';
if (dirty) {
try self.setAttrListener(element, .gotpointercapture, func);
}
},
18 => {
const as_vector: Vec16x8 = unsafe[0..16].*;
const is_animationiteration = @reduce(.And, as_vector == @as(Vec16x8, "animationiterati".*)) and
asUint("on") == @as(u16, @bitCast(unsafe[16..18].*));
if (is_animationiteration) {
try self.setAttrListener(element, .animationiteration, func);
} else {
const is_lostpointercapture = @reduce(.And, as_vector == @as(Vec16x8, "lostpointercaptu".*)) and
asUint("re") == @as(u16, @bitCast(unsafe[16..18].*));
if (is_lostpointercapture) {
try self.setAttrListener(element, .lostpointercapture, func);
}
}
},
23 => {
const as_vector: Vec16x8 = unsafe[0..16].*;
const dirty = @reduce(.And, as_vector == @as(Vec16x8, "securitypolicyvi".*)) and
asUint("olation") == @as(u56, @bitCast(unsafe[16..23].*));
if (dirty) {
try self.setAttrListener(element, .securitypolicyviolation, func);
}
},
32 => {
const as_vector: Vec32x8 = unsafe[0..32].*;
if (@reduce(.And, as_vector == @as(Vec32x8, "contentvisibilityautostatechange".*))) {
try self.setAttrListener(element, .contentvisibilityautostatechange, func);
}
},
else => {},
}
}
try attributes.putNew(attr.name.local.slice(), attr.value.slice(), self);
}
}
@@ -2180,7 +2532,7 @@ pub fn createProcessingInstruction(self: *Page, target: []const u8, data: []cons
}
// Validate target follows XML name rules (similar to attribute name validation)
try Element.Attribute.validateAttributeName(target);
try Element.Attribute.validateAttributeName(.wrap(target));
const owned_target = try self.dupeString(target);
const owned_data = try self.dupeString(data);
@@ -2251,7 +2603,7 @@ pub fn removeNode(self: *Page, parent: *Node, child: *Node, opts: RemoveNodeOpts
if (parent.is(Element)) |parent_el| {
if (self._element_shadow_roots.get(parent_el)) |shadow_root| {
// Signal slot changes for any affected slots
const slot_name = el.getAttributeSafe("slot") orelse "";
const slot_name = el.getAttributeSafe(comptime .wrap("slot")) orelse "";
var tw = @import("webapi/TreeWalker.zig").Full.Elements.init(shadow_root.asNode(), .{});
while (tw.next()) |slot_el| {
if (slot_el.is(Element.Html.Slot)) |slot| {
@@ -2290,7 +2642,7 @@ pub fn removeNode(self: *Page, parent: *Node, child: *Node, opts: RemoveNodeOpts
// the ID map and invoking disconnectedCallback for custom elements
var tw = @import("webapi/TreeWalker.zig").Full.Elements.init(child, .{});
while (tw.next()) |el| {
if (el.getAttributeSafe("id")) |id| {
if (el.getAttributeSafe(comptime .wrap("id"))) |id| {
self.removeElementIdWithMaps(id_maps.?, id);
}
@@ -2424,7 +2776,7 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod
// For main document parsing, we know nodes are connected (fast path)
// For fragment parsing (innerHTML), we need to check connectivity
if (child.isConnected() or child.isInShadowTree()) {
if (el.getAttributeSafe("id")) |id| {
if (el.getAttributeSafe(comptime .wrap("id"))) |id| {
try self.addElementId(parent, el, id);
}
try Element.Html.Custom.invokeConnectedCallbackOnElement(true, el, self);
@@ -2463,7 +2815,7 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod
var tw = @import("webapi/TreeWalker.zig").Full.Elements.init(child, .{});
while (tw.next()) |el| {
if (el.getAttributeSafe("id")) |id| {
if (el.getAttributeSafe(comptime .wrap("id"))) |id| {
try self.addElementId(el.asNode()._parent.?, el, id);
}
@@ -2473,7 +2825,7 @@ pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Nod
}
}
pub fn attributeChange(self: *Page, element: *Element, name: []const u8, value: []const u8, old_value: ?[]const u8) void {
pub fn attributeChange(self: *Page, element: *Element, name: String, value: String, old_value: ?String) void {
_ = Element.Build.call(element, "attributeChange", .{ element, name, value, self }) catch |err| {
log.err(.bug, "build.attributeChange", .{ .tag = element.getTag(), .name = name, .value = value, .err = err });
};
@@ -2489,9 +2841,9 @@ pub fn attributeChange(self: *Page, element: *Element, name: []const u8, value:
}
// Handle slot assignment changes
if (std.mem.eql(u8, name, "slot")) {
if (name.eql(comptime .wrap("slot"))) {
self.updateSlotAssignments(element);
} else if (std.mem.eql(u8, name, "name")) {
} else if (name.eql(comptime .wrap("name"))) {
// Check if this is a slot element
if (element.is(Element.Html.Slot)) |slot| {
self.signalSlotChange(slot);
@@ -2499,7 +2851,7 @@ pub fn attributeChange(self: *Page, element: *Element, name: []const u8, value:
}
}
pub fn attributeRemove(self: *Page, element: *Element, name: []const u8, old_value: []const u8) void {
pub fn attributeRemove(self: *Page, element: *Element, name: String, old_value: String) void {
_ = Element.Build.call(element, "attributeRemove", .{ element, name, self }) catch |err| {
log.err(.bug, "build.attributeRemove", .{ .tag = element.getTag(), .name = name, .err = err });
};
@@ -2515,9 +2867,9 @@ pub fn attributeRemove(self: *Page, element: *Element, name: []const u8, old_val
}
// Handle slot assignment changes
if (std.mem.eql(u8, name, "slot")) {
if (name.eql(comptime .wrap("slot"))) {
self.updateSlotAssignments(element);
} else if (std.mem.eql(u8, name, "name")) {
} else if (name.eql(comptime .wrap("name"))) {
// Check if this is a slot element
if (element.is(Element.Html.Slot)) |slot| {
self.signalSlotChange(slot);
@@ -2566,7 +2918,7 @@ fn updateElementAssignedSlot(self: *Page, element: *Element) void {
const parent_el = parent.is(Element) orelse return;
const shadow_root = self._element_shadow_roots.get(parent_el) orelse return;
const slot_name = element.getAttributeSafe("slot") orelse "";
const slot_name = element.getAttributeSafe(comptime .wrap("slot")) orelse "";
// Recursively search through the shadow root for a matching slot
if (findMatchingSlot(shadow_root.asNode(), slot_name)) |slot| {
@@ -2713,6 +3065,9 @@ const ParseState = union(enum) {
};
const LoadState = enum {
// waiting for the main HTML
waiting,
// the main HTML is being parsed (or downloaded)
parsing,
@@ -2860,7 +3215,7 @@ pub fn handleClick(self: *Page, target: *Node) !void {
switch (html_element._type) {
.anchor => |anchor| {
const href = element.getAttributeSafe("href") orelse return;
const href = element.getAttributeSafe(comptime .wrap("href")) orelse return;
if (href.len == 0) {
return;
}
@@ -2876,7 +3231,7 @@ pub fn handleClick(self: *Page, target: *Node) !void {
return;
}
if (try element.hasAttribute("download", self)) {
if (try element.hasAttribute(comptime .wrap("download"), self)) {
log.warn(.browser, "a.download", .{});
return;
}
@@ -2933,16 +3288,7 @@ pub fn handleKeydown(self: *Page, target: *Node, event: *Event) !void {
// Handle printable characters
if (key.isPrintable()) {
// if the input is selected, replace the content.
if (input._selected) {
const new_value = try self.arena.dupe(u8, key.asString());
try input.setValue(new_value, self);
input._selected = false;
return;
}
const current_value = input.getValue();
const new_value = try std.mem.concat(self.arena, u8, &.{ current_value, key.asString() });
try input.setValue(new_value, self);
try input.innerInsert(key.asString(), self);
}
return;
}
@@ -2965,7 +3311,7 @@ pub fn submitForm(self: *Page, submitter_: ?*Element, form_: ?*Element.Html.Form
const form = form_ orelse return;
if (submitter_) |submitter| {
if (submitter.getAttributeSafe("disabled") != null) {
if (submitter.getAttributeSafe(comptime .wrap("disabled")) != null) {
return;
}
}
@@ -2978,13 +3324,13 @@ pub fn submitForm(self: *Page, submitter_: ?*Element, form_: ?*Element.Html.Form
const transfer_arena = self._session.transfer_arena;
const encoding = form_element.getAttributeSafe("enctype");
const encoding = form_element.getAttributeSafe(comptime .wrap("enctype"));
var buf = std.Io.Writer.Allocating.init(transfer_arena);
try form_data.write(encoding, &buf.writer);
const method = form_element.getAttributeSafe("method") orelse "";
var action = form_element.getAttributeSafe("action") orelse self.url;
const method = form_element.getAttributeSafe(comptime .wrap("method")) orelse "";
var action = form_element.getAttributeSafe(comptime .wrap("action")) orelse self.url;
var opts = NavigateOpts{
.reason = .form,
@@ -3011,18 +3357,7 @@ pub fn insertText(self: *Page, v: []const u8) !void {
return;
}
// If the input is selected, replace the existing value
if (input._selected) {
const new_value = try self.arena.dupe(u8, v);
try input.setValue(new_value, self);
input._selected = false;
return;
}
// Or append the value
const current_value = input.getValue();
const new_value = try std.mem.concat(self.arena, u8, &.{ current_value, v });
return input.setValue(new_value, self);
try input.innerInsert(v, self);
}
if (html_element.is(Element.Html.TextArea)) |textarea| {

View File

@@ -19,6 +19,7 @@
const std = @import("std");
const builtin = @import("builtin");
const js = @import("js/js.zig");
const log = @import("../log.zig");
const milliTimestamp = @import("../datetime.zig").milliTimestamp;

View File

@@ -152,14 +152,14 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
script_element._executed = true;
const element = script_element.asElement();
if (element.getAttributeSafe("nomodule") != null) {
if (element.getAttributeSafe(comptime .wrap("nomodule")) != null) {
// these scripts should only be loaded if we don't support modules
// but since we do support modules, we can just skip them.
return;
}
const kind: Script.Kind = blk: {
const script_type = element.getAttributeSafe("type") orelse break :blk .javascript;
const script_type = element.getAttributeSafe(comptime .wrap("type")) orelse break :blk .javascript;
if (script_type.len == 0) {
break :blk .javascript;
}
@@ -186,7 +186,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
var source: Script.Source = undefined;
var remote_url: ?[:0]const u8 = null;
const base_url = page.base();
if (element.getAttributeSafe("src")) |src| {
if (element.getAttributeSafe(comptime .wrap("src"))) |src| {
if (try parseDataURI(page.arena, src)) |data_uri| {
source = .{ .@"inline" = data_uri };
} else {
@@ -217,12 +217,12 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
break :blk if (kind == .module) .@"defer" else .normal;
}
if (element.getAttributeSafe("async") != null) {
if (element.getAttributeSafe(comptime .wrap("async")) != null) {
break :blk .async;
}
// Check for defer or module (before checking dynamic script default)
if (kind == .module or element.getAttributeSafe("defer") != null) {
if (kind == .module or element.getAttributeSafe(comptime .wrap("defer")) != null) {
break :blk .@"defer";
}
@@ -271,11 +271,15 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
});
if (comptime IS_DEBUG) {
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
log.debug(.http, "script queue", .{
.ctx = ctx,
.url = remote_url.?,
.element = element,
.stack = page.js.stackTrace() catch "???",
.stack = ls.local.stackTrace() catch "???",
});
}
}
@@ -357,11 +361,15 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
if (comptime IS_DEBUG) {
var ls: js.Local.Scope = undefined;
self.page.js.localScope(&ls);
defer ls.deinit();
log.debug(.http, "script queue", .{
.url = url,
.ctx = "module",
.referrer = referrer,
.stack = self.page.js.stackTrace() catch "???",
.stack = ls.local.stackTrace() catch "???",
});
}
@@ -448,11 +456,15 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
if (comptime IS_DEBUG) {
var ls: js.Local.Scope = undefined;
self.page.js.localScope(&ls);
defer ls.deinit();
log.debug(.http, "script queue", .{
.url = url,
.ctx = "dynamic module",
.referrer = referrer,
.stack = self.page.js.stackTrace() catch "???",
.stack = ls.local.stackTrace() catch "???",
});
}
@@ -651,7 +663,7 @@ pub const Script = struct {
log.debug(.http, "script fetch start", .{ .req = transfer });
}
fn headerCallback(transfer: *Http.Transfer) !void {
fn headerCallback(transfer: *Http.Transfer) !bool {
const self: *Script = @ptrCast(@alignCast(transfer.ctx));
const header = &transfer.response_header.?;
self.status = header.status;
@@ -661,7 +673,7 @@ pub const Script = struct {
.status = header.status,
.content_type = header.contentType(),
});
return;
return false;
}
if (comptime IS_DEBUG) {
@@ -682,6 +694,7 @@ pub const Script = struct {
try buffer.ensureTotalCapacity(self.manager.allocator, cl);
}
self.source = .{ .remote = buffer };
return true;
}
fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void {
@@ -721,7 +734,7 @@ pub const Script = struct {
log.warn(.http, "script fetch error", .{
.err = err,
.req = self.url,
.mode = self.mode,
.mode = std.meta.activeTag(self.mode),
.kind = self.kind,
.status = self.status,
});
@@ -741,9 +754,13 @@ pub const Script = struct {
return;
}
if (self.mode == .import) {
const entry = self.manager.imported_modules.getPtr(self.url).?;
entry.state = .err;
switch (self.mode) {
.import_async => |ia| ia.callback(ia.data, error.FailedToLoad),
.import => {
const entry = manager.imported_modules.getPtr(self.url).?;
entry.state = .err;
},
else => {},
}
self.deinit(true);
manager.evaluate();
@@ -785,6 +802,12 @@ pub const Script = struct {
.cacheable = cacheable,
});
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
const local = &ls.local;
// Handle importmap special case here: the content is a JSON containing
// imports.
if (self.kind == .importmap) {
@@ -795,25 +818,24 @@ pub const Script = struct {
.kind = self.kind,
.cacheable = cacheable,
});
self.executeCallback("error", script_element._on_error, page);
self.executeCallback("error", local.toLocal(script_element._on_error), page);
return;
};
self.executeCallback("load", script_element._on_load, page);
self.executeCallback("load", local.toLocal(script_element._on_load), page);
return;
}
const js_context = page.js;
var try_catch: js.TryCatch = undefined;
try_catch.init(js_context);
try_catch.init(local);
defer try_catch.deinit();
const success = blk: {
const content = self.source.content();
switch (self.kind) {
.javascript => _ = js_context.eval(content, url) catch break :blk false,
.javascript => _ = local.eval(content, url) catch break :blk false,
.module => {
// We don't care about waiting for the evaluation here.
js_context.module(false, content, url, cacheable) catch break :blk false;
page.js.module(false, local, content, url, cacheable) catch break :blk false;
},
.importmap => unreachable, // handled before the try/catch.
}
@@ -826,14 +848,14 @@ pub const Script = struct {
defer {
// We should run microtasks even if script execution fails.
page.js.runMicrotasks();
local.runMicrotasks();
_ = page.scheduler.run() catch |err| {
log.err(.page, "scheduler", .{ .err = err });
};
}
if (success) {
self.executeCallback("load", script_element._on_load, page);
self.executeCallback("load", local.toLocal(script_element._on_load), page);
return;
}
@@ -844,12 +866,11 @@ pub const Script = struct {
.cacheable = cacheable,
});
self.executeCallback("error", script_element._on_error, page);
self.executeCallback("error", local.toLocal(script_element._on_error), page);
}
fn executeCallback(self: *const Script, comptime typ: []const u8, cb_: ?js.Function.Global, page: *Page) void {
const cb_global = cb_ orelse return;
const cb = cb_global.local();
fn executeCallback(self: *const Script, comptime typ: []const u8, cb_: ?js.Function, page: *Page) void {
const cb = cb_ orelse return;
const Event = @import("webapi/Event.zig");
const event = Event.initTrusted(typ, .{}, page) catch |err| {

View File

@@ -77,8 +77,9 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
}
// trailing space so that we always have space to append the null terminator
var out = try std.mem.join(allocator, "", &.{ normalized_base, "/", path, " " });
const end = out.len - 1;
// and so that we can compare the next two characters without needing to length check
var out = try std.mem.join(allocator, "", &.{ normalized_base, "/", path, " " });
const end = out.len - 2;
const path_marker = path_start + 1;
@@ -88,33 +89,39 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
var in_i: usize = 0;
var out_i: usize = 0;
while (in_i < end) {
if (std.mem.startsWith(u8, out[in_i..], "./")) {
in_i += 2;
continue;
}
if (std.mem.startsWith(u8, out[in_i..], "../")) {
lp.assert(out[out_i - 1] == '/', "URL.resolve", .{ .out = out });
if (out_i > path_marker) {
// go back before the /
out_i -= 2;
while (out_i > 1 and out[out_i - 1] != '/') {
out_i -= 1;
}
} else {
// if out_i == path_marker, than we've reached the start of
// the path. We can't ../ any more. E.g.:
// http://www.example.com/../hello.
// You might think that's an error, but, at least with
// new URL('../hello', 'http://www.example.com/')
// it just ignores the extra ../
if (out[in_i] == '.' and (out_i == 0 or out[out_i - 1] == '/')) {
if (out[in_i + 1] == '/') { // always safe, because we added a whitespace
// /./
in_i += 2;
continue;
}
if (out[in_i + 1] == '.' and out[in_i + 2] == '/') { // always safe, because we added two whitespaces
// /../
if (out_i > path_marker) {
// go back before the /
out_i -= 2;
while (out_i > 1 and out[out_i - 1] != '/') {
out_i -= 1;
}
} else {
// if out_i == path_marker, than we've reached the start of
// the path. We can't ../ any more. E.g.:
// http://www.example.com/../hello.
// You might think that's an error, but, at least with
// new URL('../hello', 'http://www.example.com/')
// it just ignores the extra ../
}
in_i += 3;
continue;
}
if (in_i == end - 1) {
// ignore trailing dot
break;
}
in_i += 3;
continue;
}
out[out_i] = out[in_i];
const c = out[in_i];
out[out_i] = c;
in_i += 1;
out_i += 1;
}
@@ -542,6 +549,21 @@ test "URL: resolve" {
};
const cases = [_]Case{
.{
.base = "https://example/dir",
.path = "abc../test",
.expected = "https://example/abc../test",
},
.{
.base = "https://example/dir",
.path = "abc.",
.expected = "https://example/abc.",
},
.{
.base = "https://example/dir",
.path = "abc/.",
.expected = "https://example/abc/",
},
.{
.base = "https://example/xyz/abc/123",
.path = "something.js",

View File

@@ -51,11 +51,22 @@ pub const Opts = struct {
pub fn root(doc: *Node.Document, opts: RootOpts, writer: *std.Io.Writer, page: *Page) !void {
if (doc.is(Node.Document.HTMLDocument)) |html_doc| {
try writer.writeAll("<!DOCTYPE html>");
blk: {
// Ideally we just render the doctype which is part of the document
if (doc.asNode().firstChild()) |first| {
if (first._type == .document_type) {
break :blk;
}
}
// But if the doc has no child, or the first child isn't a doctype
// well force it.
try writer.writeAll("<!DOCTYPE html>");
}
if (opts.with_base) {
const parent = if (html_doc.getHead()) |head| head.asNode() else doc.asNode();
const base = try doc.createElement("base", null, page);
try base.setAttributeSafe("base", page.base(), page);
try base.setAttributeSafe(comptime .wrap("base"), .wrap(page.base()), page);
_ = try parent.insertBefore(base.asNode(), parent.firstChild(), page);
}
}
@@ -99,7 +110,7 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri
// to render that "active" content, so when we're trying to render
// it, we don't want to skip it.
if ((comptime force_slot == false) and opts.shadow == .rendered) {
if (el.getAttributeSafe("slot")) |_| {
if (el.getAttributeSafe(comptime .wrap("slot"))) |_| {
// Skip - will be rendered by the Slot if it's the active container
return;
}
@@ -242,12 +253,12 @@ fn shouldStripElement(el: *const Node.Element, opts: Opts) bool {
if (std.mem.eql(u8, tag_name, "noscript")) return true;
if (std.mem.eql(u8, tag_name, "link")) {
if (el.getAttributeSafe("as")) |as| {
if (el.getAttributeSafe(comptime .wrap("as"))) |as| {
if (std.mem.eql(u8, as, "script")) return true;
}
if (el.getAttributeSafe("rel")) |rel| {
if (el.getAttributeSafe(comptime .wrap("rel"))) |rel| {
if (std.mem.eql(u8, rel, "modulepreload") or std.mem.eql(u8, rel, "preload")) {
if (el.getAttributeSafe("as")) |as| {
if (el.getAttributeSafe(comptime .wrap("as"))) |as| {
if (std.mem.eql(u8, as, "script")) return true;
}
}
@@ -259,7 +270,7 @@ fn shouldStripElement(el: *const Node.Element, opts: Opts) bool {
if (std.mem.eql(u8, tag_name, "style")) return true;
if (std.mem.eql(u8, tag_name, "link")) {
if (el.getAttributeSafe("rel")) |rel| {
if (el.getAttributeSafe(comptime .wrap("rel"))) |rel| {
if (std.mem.eql(u8, rel, "stylesheet")) return true;
}
}

View File

@@ -22,7 +22,7 @@ const v8 = js.v8;
const Array = @This();
ctx: *js.Context,
local: *const js.Local,
handle: *const v8.Array,
pub fn len(self: Array) usize {
@@ -30,39 +30,37 @@ pub fn len(self: Array) usize {
}
pub fn get(self: Array, index: u32) !js.Value {
const ctx = self.ctx;
const ctx = self.local.ctx;
const idx = js.Integer.init(ctx.isolate.handle, index);
const handle = v8.v8__Object__Get(@ptrCast(self.handle), ctx.handle, idx.handle) orelse {
const handle = v8.v8__Object__Get(@ptrCast(self.handle), self.local.handle, idx.handle) orelse {
return error.JsException;
};
return .{
.ctx = self.ctx,
.local = self.local,
.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);
pub fn set(self: Array, index: u32, value: anytype, comptime opts: js.Caller.CallOpts) !bool {
const js_value = try self.local.zigValueToJs(value, opts);
var out: v8.MaybeBool = undefined;
v8.v8__Object__SetAtIndex(@ptrCast(self.handle), ctx.handle, index, js_value.handle, &out);
v8.v8__Object__SetAtIndex(@ptrCast(self.handle), self.local.handle, index, js_value.handle, &out);
return out.has_value;
}
pub fn toObject(self: Array) js.Object {
return .{
.ctx = self.ctx,
.local = self.local,
.handle = @ptrCast(self.handle),
};
}
pub fn toValue(self: Array) js.Value {
return .{
.ctx = self.ctx,
.local = self.local,
.handle = @ptrCast(self.handle),
};
}

585
src/browser/js/Caller.zig Normal file
View File

@@ -0,0 +1,585 @@
// Copyright (C) 2023-2026 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 string = @import("../../string.zig");
const Page = @import("../Page.zig");
const js = @import("js.zig");
const bridge = @import("bridge.zig");
const Context = @import("Context.zig");
const TaggedOpaque = @import("TaggedOpaque.zig");
const v8 = js.v8;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const CALL_ARENA_RETAIN = 1024 * 16;
const IS_DEBUG = @import("builtin").mode == .Debug;
const Caller = @This();
local: js.Local,
prev_local: ?*const js.Local,
// Takes the raw v8 isolate and extracts the context from it.
pub fn init(self: *Caller, v8_isolate: *v8.Isolate) void {
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 ctx: *Context = @ptrFromInt(v8.v8__BigInt__Uint64Value(embedder_data, &lossless));
ctx.call_depth += 1;
self.* = Caller{
.local = .{
.ctx = ctx,
.handle = v8_context_handle.?,
.call_arena = ctx.call_arena,
.isolate = .{ .handle = v8_isolate },
},
.prev_local = ctx.local,
};
ctx.local = &self.local;
}
pub fn deinit(self: *Caller) void {
const ctx = self.local.ctx;
const call_depth = ctx.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(ctx.call_arena.ptr));
_ = arena.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN });
}
ctx.call_depth = call_depth;
ctx.local = self.prev_local;
}
pub const CallOpts = struct {
cache: ?[]const u8 = null,
dom_exception: bool = false,
null_as_undefined: bool = false,
as_typed_array: bool = false,
};
pub fn constructor(self: *Caller, comptime T: type, func: anytype, handle: *const v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
var hs: js.HandleScope = undefined;
hs.init(self.local.isolate);
defer hs.deinit();
const info = FunctionCallbackInfo{ .handle = handle };
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{ .local = &self.local, .handle = new_this_handle };
if (@typeInfo(ReturnType) == .error_union) {
const non_error_res = res catch |err| return err;
this = try self.local.mapZigInstanceToJs(new_this_handle, non_error_res);
} else {
this = try self.local.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.local.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, handle: *const v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
var hs: js.HandleScope = undefined;
hs.init(self.local.isolate);
defer hs.deinit();
const info = FunctionCallbackInfo{ .handle = handle };
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 args = try self.getArgs(F, 1, info);
const js_this = info.getThis();
@field(args, "0") = try TaggedOpaque.fromJS(*T, js_this);
const res = @call(.auto, func, args);
const mapped = try self.local.zigValueToJs(res, opts);
const return_value = info.getReturnValue();
return_value.set(mapped);
}
pub fn function(self: *Caller, comptime T: type, func: anytype, handle: *const v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
var hs: js.HandleScope = undefined;
hs.init(self.local.isolate);
defer hs.deinit();
const info = FunctionCallbackInfo{ .handle = handle };
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 args = try self.getArgs(F, 0, info);
const res = @call(.auto, func, args);
info.getReturnValue().set(try self.local.zigValueToJs(res, opts));
}
pub fn getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
var hs: js.HandleScope = undefined;
hs.init(self.local.isolate);
defer hs.deinit();
const info = PropertyCallbackInfo{ .handle = handle };
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 TaggedOpaque.fromJS(*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: *const v8.Name, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
var hs: js.HandleScope = undefined;
hs.init(self.local.isolate);
defer hs.deinit();
const info = PropertyCallbackInfo{ .handle = handle };
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: *const v8.Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args = try self.getArgs(F, 2, info);
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
@field(args, "1") = try self.nameToString(@TypeOf(args.@"1"), 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: *const v8.Name, js_value: *const v8.Value, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
var hs: js.HandleScope = undefined;
hs.init(self.local.isolate);
defer hs.deinit();
const info = PropertyCallbackInfo{ .handle = handle };
return self._setNamedIndex(T, func, name, .{ .local = &self.local, .handle = 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: *const v8.Name, js_value: js.Value, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args: ParameterTypes(F) = undefined;
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
@field(args, "1") = try self.nameToString(@TypeOf(args.@"1"), name);
@field(args, "2") = try self.local.jsValueToZig(@TypeOf(@field(args, "2")), js_value);
if (@typeInfo(F).@"fn".params.len == 4) {
@field(args, "3") = self.local.ctx.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: *const v8.Name, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
var hs: js.HandleScope = undefined;
hs.init(self.local.isolate);
defer hs.deinit();
const info = PropertyCallbackInfo{ .handle = handle };
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: *const v8.Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args: ParameterTypes(F) = undefined;
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
@field(args, "1") = try self.nameToString(@TypeOf(args.@"1"), name);
if (@typeInfo(F).@"fn".params.len == 3) {
@field(args, "2") = self.local.ctx.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.local.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: *const Caller, comptime T: type, name: *const v8.Name) !T {
const v8_string = @as(*const v8.String, @ptrCast(name));
if (T == string.String) {
return self.local.jsStringToStringSSO(v8_string, .{});
}
if (T == string.Global) {
return self.local.jsStringToStringSSO(v8_string, .{ .allocator = self.local.ctx.allocator });
}
return try self.local.valueHandleToString(v8_string, .{});
}
fn handleError(self: *Caller, comptime T: type, comptime F: type, err: anyerror, info: anytype, comptime opts: CallOpts) void {
const isolate = self.local.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.local.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 local = &self.local;
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)) = local.ctx.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(@intCast(last_js_parameter), local);
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 local.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);
for (arr, last_js_parameter..) |*a, i| {
a.* = try local.jsValueToZig(slice_type, info.getArg(@intCast(i), local));
}
@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_val = info.getArg(@intCast(i), local);
@field(args, tupleFieldName(field_index)) = local.jsValueToZig(param.type.?, js_val) 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.local.stackTrace() catch |err1| @errorName(err1),
});
}
fn serializeFunctionArgs(self: *Caller, info: FunctionCallbackInfo) ![]const u8 {
const local = &self.local;
var buf = std.Io.Writer.Allocating.init(local.call_arena);
const separator = log.separator();
for (0..info.length()) |i| {
try buf.writer.print("{s}{d} - ", .{ separator, i + 1 });
const js_value = info.getArg(@intCast(i), local);
try local.debugValue(js_value, &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;
}
// These wrap the raw v8 C API to provide a cleaner interface.
pub const FunctionCallbackInfo = struct {
handle: *const v8.FunctionCallbackInfo,
pub fn length(self: FunctionCallbackInfo) u32 {
return @intCast(v8.v8__FunctionCallbackInfo__Length(self.handle));
}
pub fn getArg(self: FunctionCallbackInfo, index: u32, local: *const js.Local) js.Value {
return .{ .local = local, .handle = v8.v8__FunctionCallbackInfo__INDEX(self.handle, @intCast(index)).? };
}
pub fn getThis(self: FunctionCallbackInfo) *const v8.Object {
return v8.v8__FunctionCallbackInfo__This(self.handle).?;
}
pub 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);
}
};
pub const PropertyCallbackInfo = struct {
handle: *const v8.PropertyCallbackInfo,
pub fn getThis(self: PropertyCallbackInfo) *const v8.Object {
return v8.v8__PropertyCallbackInfo__This(self.handle).?;
}
pub 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,
pub fn set(self: ReturnValue, value: anytype) void {
const T = @TypeOf(value);
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));
}
}
pub fn setValueHandle(self: ReturnValue, handle: *const v8.Value) void {
v8.v8__ReturnValue__Set(self.handle, handle);
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -24,11 +24,14 @@ const log = @import("../../log.zig");
const bridge = @import("bridge.zig");
const Context = @import("Context.zig");
const Isolate = @import("Isolate.zig");
const Platform = @import("Platform.zig");
const Snapshot = @import("Snapshot.zig");
const Inspector = @import("Inspector.zig");
const ExecutionWorld = @import("ExecutionWorld.zig");
const Window = @import("../webapi/Window.zig");
const JsApis = bridge.JsApis;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
@@ -59,6 +62,9 @@ eternal_function_templates: []v8.Eternal,
// Dynamic slice to avoid circular dependency on JsApis.len at comptime
templates: []*const v8.FunctionTemplate,
// Global template created once per isolate and reused across all contexts
global_template: v8.Eternal,
pub fn init(allocator: Allocator, platform: *const Platform, snapshot: *Snapshot) !Env {
var params = try allocator.create(v8.CreateParams);
errdefer allocator.destroy(params);
@@ -91,6 +97,7 @@ pub fn init(allocator: Allocator, platform: *const Platform, snapshot: *Snapshot
const templates = try allocator.alloc(*const v8.FunctionTemplate, JsApis.len);
errdefer allocator.free(templates);
var global_eternal: v8.Eternal = undefined;
{
var temp_scope: js.HandleScope = undefined;
temp_scope.init(isolate);
@@ -107,6 +114,29 @@ pub fn init(allocator: Allocator, platform: *const Platform, snapshot: *Snapshot
const eternal_ptr = v8.v8__Eternal__Get(&eternal_function_templates[i], isolate.handle);
templates[i] = @ptrCast(@alignCast(eternal_ptr.?));
}
// Create global template once per isolate
const js_global = v8.v8__FunctionTemplate__New__DEFAULT(isolate.handle);
const window_name = v8.v8__String__NewFromUtf8(isolate.handle, "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]);
const global_template_local = v8.v8__FunctionTemplate__InstanceTemplate(js_global).?;
v8.v8__ObjectTemplate__SetNamedHandler(global_template_local, &.{
.getter = bridge.unknownPropertyCallback,
.setter = null,
.query = null,
.deleter = null,
.enumerator = null,
.definer = null,
.descriptor = null,
.data = null,
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
});
v8.v8__Eternal__New(isolate.handle, @ptrCast(global_template_local), &global_eternal);
}
return .{
@@ -117,6 +147,7 @@ pub fn init(allocator: Allocator, platform: *const Platform, snapshot: *Snapshot
.templates = templates,
.isolate_params = params,
.eternal_function_templates = eternal_function_templates,
.global_template = global_eternal,
};
}
@@ -141,6 +172,10 @@ pub fn runMicrotasks(self: *const Env) void {
}
pub fn pumpMessageLoop(self: *const Env) bool {
var hs: v8.HandleScope = undefined;
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate.handle);
defer v8.v8__HandleScope__DESTRUCT(&hs);
return v8.v8__Platform__PumpMessageLoop(self.platform.handle, self.isolate.handle, false);
}
@@ -159,6 +194,8 @@ pub fn newExecutionWorld(self: *Env) !ExecutionWorld {
// a Context, it's managed by the garbage collector. We use the
// `lowMemoryNotification` call on the isolate to encourage v8 to free
// any contexts which have been freed.
// This GC is very aggressive. Use memoryPressureNotification for less
// aggressive GC passes.
pub fn lowMemoryNotification(self: *Env) void {
var handle_scope: js.HandleScope = undefined;
handle_scope.init(self.isolate);
@@ -166,6 +203,21 @@ pub fn lowMemoryNotification(self: *Env) void {
self.isolate.lowMemoryNotification();
}
// V8 doesn't immediately free memory associated with
// a Context, it's managed by the garbage collector. We use the
// `memoryPressureNotification` call on the isolate to encourage v8 to free
// any contexts which have been freed.
// The level indicates the aggressivity of the GC required:
// moderate speeds up incremental GC
// critical runs one full GC
// For a more aggressive GC, use lowMemoryNotification.
pub fn memoryPressureNotification(self: *Env, level: Isolate.MemoryPressureLevel) void {
var handle_scope: js.HandleScope = undefined;
handle_scope.init(self.isolate);
defer handle_scope.deinit();
self.isolate.memoryPressureNotification(level);
}
pub fn dumpMemoryStats(self: *Env) void {
const stats = self.isolate.getHeapStatistics();
std.debug.print(
@@ -189,19 +241,27 @@ pub fn dumpMemoryStats(self: *Env) void {
fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) void {
const promise_handle = v8.v8__PromiseRejectMessage__GetPromise(&message_handle).?;
const isolate_handle = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?;
const js_isolate = js.Isolate{ .handle = isolate_handle };
const context = Context.fromIsolate(js_isolate);
const v8_isolate = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?;
const js_isolate = js.Isolate{ .handle = v8_isolate };
const ctx = Context.fromIsolate(js_isolate);
const local = js.Local{
.ctx = ctx,
.isolate = js_isolate,
.handle = v8.v8__Isolate__GetCurrentContext(v8_isolate).?,
.call_arena = ctx.call_arena,
};
const value =
if (v8.v8__PromiseRejectMessage__GetValue(&message_handle)) |v8_value|
context.valueToString(.{ .ctx = context, .handle = v8_value }, .{}) catch |err| @errorName(err)
// @HandleScope - no reason to create a js.Context here
local.valueHandleToString(v8_value, .{}) catch |err| @errorName(err)
else
"no value";
log.debug(.js, "unhandled rejection", .{
.value = value,
.stack = context.stackTrace() catch |err| @errorName(err) orelse "???",
.stack = local.stackTrace() catch |err| @errorName(err) orelse "???",
.note = "This should be updated to call window.unhandledrejection",
});
}

View File

@@ -53,7 +53,6 @@ context_arena: ArenaAllocator,
// does all the work, but having all page-specific data structures
// grouped together helps keep things clean.
context: ?Context = null,
persisted_context: ?js.Global(Context) = null,
// no init, must be initialized via env.newExecutionWorld()
@@ -77,43 +76,28 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context
const isolate = env.isolate;
const arena = self.context_arena.allocator();
const persisted_context: js.Global(Context) = blk: {
var temp_scope: js.HandleScope = undefined;
temp_scope.init(isolate);
defer temp_scope.deinit();
var hs: js.HandleScope = undefined;
hs.init(isolate);
defer hs.deinit();
// Getting this into the snapshot is tricky (anything involving the
// global is tricky). Easier to do here
const global_template = @import("Snapshot.zig").createGlobalTemplate(isolate.handle, env.templates);
v8.v8__ObjectTemplate__SetNamedHandler(global_template, &.{
.getter = bridge.unknownPropertyCallback,
.setter = null,
.query = null,
.deleter = null,
.enumerator = null,
.definer = null,
.descriptor = null,
.data = null,
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
});
// Get the global template that was created once per isolate
const global_template: *const v8.ObjectTemplate = @ptrCast(@alignCast(v8.v8__Eternal__Get(&env.global_template, isolate.handle).?));
const v8_context = v8.v8__Context__New(isolate.handle, global_template, null).?;
const context_handle = v8.v8__Context__New(isolate.handle, global_template, null).?;
break :blk js.Global(Context).init(isolate.handle, context_handle);
};
// Create the v8::Context and wrap it in a v8::Global
var context_global: v8.Global = undefined;
v8.v8__Global__New(isolate.handle, v8_context, &context_global);
// our window wrapped in a v8::Global
const global_obj = v8.v8__Context__Global(v8_context).?;
var global_global: v8.Global = undefined;
v8.v8__Global__New(isolate.handle, global_obj, &global_global);
// For a Page we only create one HandleScope, it is stored in the main World (enter==true). A page can have multple contexts, 1 for each World.
// The main Context that enters and holds the HandleScope should therefore always be created first. Following other worlds for this page
// like isolated Worlds, will thereby place their objects on the main page's HandleScope. Note: In the furure the number of context will multiply multiple frames support
const v8_context = persisted_context.local();
var handle_scope: ?js.HandleScope = null;
if (enter) {
handle_scope = @as(js.HandleScope, undefined);
handle_scope.?.init(isolate);
v8.v8__Context__Enter(v8_context);
}
errdefer if (enter) {
v8.v8__Context__Exit(v8_context);
handle_scope.?.deinit();
};
const context_id = env.context_id;
@@ -122,24 +106,24 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context
self.context = Context{
.page = page,
.id = context_id,
.entered = enter,
.isolate = isolate,
.handle = v8_context,
.handle = context_global,
.templates = env.templates,
.handle_scope = handle_scope,
.script_manager = &page._script_manager,
.call_arena = page.call_arena,
.arena = arena,
};
self.persisted_context = persisted_context;
var context = &self.context.?;
try context.identity_map.putNoClobber(arena, @intFromPtr(page.window), global_global);
// Store a pointer to our context inside the v8 context so that, given
// a v8 context, we can get our context out
const data = isolate.initBigInt(@intFromPtr(context));
v8.v8__Context__SetEmbedderData(context.handle, 1, @ptrCast(data.handle));
const data = isolate.initBigInt(@intFromPtr(&self.context.?));
v8.v8__Context__SetEmbedderData(v8_context, 1, @ptrCast(data.handle));
try context.setupGlobal();
return context;
return &self.context.?;
}
pub fn removeContext(self: *ExecutionWorld) void {
@@ -147,17 +131,6 @@ pub fn removeContext(self: *ExecutionWorld) void {
context.deinit();
self.context = null;
self.persisted_context.?.deinit();
self.persisted_context = null;
self.env.isolate.notifyContextDisposed();
_ = self.context_arena.reset(.{ .retain_with_limit = CONTEXT_ARENA_RETAIN });
}
pub fn terminateExecution(self: *const ExecutionWorld) void {
self.env.isolate.terminateExecution();
}
pub fn resumeExecution(self: *const ExecutionWorld) void {
self.env.isolate.cancelTerminateExecution();
}

View File

@@ -20,11 +20,13 @@ const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const log = @import("../../log.zig");
const Allocator = std.mem.Allocator;
const Function = @This();
ctx: *js.Context,
local: *const js.Local,
this: ?*const v8.Object = null,
handle: *const v8.Function,
@@ -34,60 +36,66 @@ pub const Result = struct {
};
pub fn withThis(self: *const Function, value: anytype) !Function {
const local = self.local;
const this_obj = if (@TypeOf(value) == js.Object)
value.handle
else
(try self.ctx.zigValueToJs(value, .{})).handle;
(try local.zigValueToJs(value, .{})).handle;
return .{
.ctx = self.ctx,
.local = local,
.this = this_obj,
.handle = self.handle,
};
}
pub fn newInstance(self: *const Function, caught: *js.TryCatch.Caught) !js.Object {
const ctx = self.ctx;
const local = self.local;
var try_catch: js.TryCatch = undefined;
try_catch.init(ctx);
try_catch.init(local);
defer try_catch.deinit();
// This creates a new instance using this Function as a constructor.
// const c_args = @as(?[*]const ?*c.Value, @ptrCast(&.{}));
const handle = v8.v8__Function__NewInstance(self.handle, ctx.handle, 0, null) orelse {
caught.* = try_catch.caughtOrError(ctx.call_arena, error.Unknown);
const handle = v8.v8__Function__NewInstance(self.handle, local.handle, 0, null) orelse {
caught.* = try_catch.caughtOrError(local.call_arena, error.Unknown);
return error.JsConstructorFailed;
};
return .{
.ctx = ctx,
.local = local,
.handle = handle,
};
}
pub fn call(self: *const Function, comptime T: type, args: anytype) !T {
return self.callWithThis(T, self.getThis(), args);
}
pub fn tryCall(self: *const Function, comptime T: type, args: anytype, caught: *js.TryCatch.Caught) !T {
return self.tryCallWithThis(T, self.getThis(), args, caught);
}
pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught) !T {
var try_catch: js.TryCatch = undefined;
try_catch.init(self.ctx);
defer try_catch.deinit();
return self.callWithThis(T, this, args) catch |err| {
caught.* = try_catch.caughtOrError(self.ctx.call_arena, err);
var caught: js.TryCatch.Caught = undefined;
return self._tryCallWithThis(T, self.getThis(), args, &caught) catch |err| {
log.warn(.js, "call caught", .{ .err = err, .caught = caught });
return err;
};
}
pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T {
const ctx = self.ctx;
var caught: js.TryCatch.Caught = undefined;
return self._tryCallWithThis(T, this, args, &caught) catch |err| {
log.warn(.js, "callWithThis caught", .{ .err = err, .caught = caught });
return err;
};
}
pub fn tryCall(self: *const Function, comptime T: type, args: anytype, caught: *js.TryCatch.Caught) !T {
return self._tryCallWithThis(T, self.getThis(), args, caught);
}
pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught) !T {
return self._tryCallWithThis(T, this, args, caught);
}
pub fn _tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught) !T {
caught.* = .{};
const local = self.local;
// When we're calling a function from within JavaScript itself, this isn't
// necessary. We're within a Caller instantiation, which will already have
@@ -98,6 +106,7 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args
// need to increase the call_depth so that the call_arena remains valid for
// the duration of the function call. If we don't do this, the call_arena
// will be reset after each statement of the function which executes Zig code.
const ctx = local.ctx;
const call_depth = ctx.call_depth;
ctx.call_depth = call_depth + 1;
defer ctx.call_depth = call_depth;
@@ -106,7 +115,7 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args
if (@TypeOf(this) == js.Object) {
break :blk this;
}
break :blk try ctx.zigValueToJs(this, .{});
break :blk try local.zigValueToJs(this, .{});
};
const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args;
@@ -116,15 +125,15 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args
const fields = s.fields;
var js_args: [fields.len]*const v8.Value = undefined;
inline for (fields, 0..) |f, i| {
js_args[i] = (try ctx.zigValueToJs(@field(aargs, f.name), .{})).handle;
js_args[i] = (try local.zigValueToJs(@field(aargs, f.name), .{})).handle;
}
const cargs: [fields.len]*const v8.Value = js_args;
break :blk &cargs;
},
.pointer => blk: {
var values = try ctx.call_arena.alloc(*const v8.Value, args.len);
var values = try local.call_arena.alloc(*const v8.Value, args.len);
for (args, 0..) |a, i| {
values[i] = (try ctx.zigValueToJs(a, .{})).handle;
values[i] = (try local.zigValueToJs(a, .{})).handle;
}
break :blk values;
},
@@ -132,54 +141,71 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args
};
const c_args = @as(?[*]const ?*v8.Value, @ptrCast(js_args.ptr));
const handle = v8.v8__Function__Call(self.handle, ctx.handle, js_this.handle, @as(c_int, @intCast(js_args.len)), c_args) orelse {
// std.debug.print("CB ERR: {s}\n", .{self.src() catch "???"});
var try_catch: js.TryCatch = undefined;
try_catch.init(local);
defer try_catch.deinit();
const handle = v8.v8__Function__Call(self.handle, local.handle, js_this.handle, @as(c_int, @intCast(js_args.len)), c_args) orelse {
caught.* = try_catch.caughtOrError(local.call_arena, error.JSExecCallback);
return error.JSExecCallback;
};
if (@typeInfo(T) == .void) {
return {};
}
return ctx.jsValueToZig(T, .{ .ctx = ctx, .handle = handle });
return local.jsValueToZig(T, .{ .local = local, .handle = handle });
}
fn getThis(self: *const Function) js.Object {
const handle = if (self.this) |t| t else v8.v8__Context__Global(self.ctx.handle).?;
const handle = if (self.this) |t| t else v8.v8__Context__Global(self.local.handle).?;
return .{
.ctx = self.ctx,
.local = self.local,
.handle = handle,
};
}
pub fn src(self: *const Function) ![]const u8 {
return self.context.valueToString(.{ .handle = @ptrCast(self.handle) }, .{});
return self.local.valueToString(.{ .local = self.local, .handle = @ptrCast(self.handle) }, .{});
}
pub fn getPropertyValue(self: *const Function, name: []const u8) !?js.Value {
const ctx = self.ctx;
const key = ctx.isolate.initStringHandle(name);
const handle = v8.v8__Object__Get(self.handle, ctx.handle, key) orelse {
const local = self.local;
const key = local.isolate.initStringHandle(name);
const handle = v8.v8__Object__Get(self.handle, self.local.handle, key) orelse {
return error.JsException;
};
return .{
.ctx = ctx,
.local = local,
.handle = handle,
};
}
pub fn persist(self: *const Function) !Global {
var ctx = self.ctx;
return self._persist(true);
}
pub fn temp(self: *const Function) !Temp {
return self._persist(false);
}
fn _persist(self: *const Function, comptime is_global: bool) !(if (is_global) Global else Temp) {
var ctx = self.local.ctx;
var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
if (comptime is_global) {
try ctx.global_functions.append(ctx.arena, global);
} else {
try ctx.global_functions_temp.put(ctx.arena, global.data_ptr, global);
}
return .{ .handle = global };
}
try ctx.global_functions.append(ctx.arena, global);
return .{
.handle = global,
.ctx = ctx,
};
pub fn tempWithThis(self: *const Function, value: anytype) !Temp {
const with_this = try self.withThis(value);
return with_this.temp();
}
pub fn persistWithThis(self: *const Function, value: anytype) !Global {
@@ -187,22 +213,31 @@ pub fn persistWithThis(self: *const Function, value: anytype) !Global {
return with_this.persist();
}
pub const Global = struct {
handle: v8.Global,
ctx: *js.Context,
pub const Temp = G(0);
pub const Global = G(1);
pub fn deinit(self: *Global) void {
v8.v8__Global__Reset(&self.handle);
}
fn G(comptime discriminator: u8) type {
return struct {
handle: v8.Global,
pub fn local(self: *const Global) Function {
return .{
.ctx = self.ctx,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)),
};
}
// makes the types different (G(0) != G(1)), without taking up space
comptime _: u8 = discriminator,
pub fn isEqual(self: *const Global, other: Function) bool {
return v8.v8__Global__IsEqual(&self.handle, other.handle);
}
};
const Self = @This();
pub fn deinit(self: *Self) void {
v8.v8__Global__Reset(&self.handle);
}
pub fn local(self: *const Self, l: *const js.Local) Function {
return .{
.local = l,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
};
}
pub fn isEqual(self: *const Self, other: Function) bool {
return v8.v8__Global__IsEqual(&self.handle, other.handle);
}
};
}

View File

@@ -20,7 +20,7 @@ const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const Context = @import("Context.zig");
const TaggedOpaque = @import("TaggedOpaque.zig");
const Allocator = std.mem.Allocator;
const RndGen = std.Random.DefaultPrng;
@@ -36,7 +36,7 @@ client: Client,
channel: Channel,
session: Session,
rnd: RndGen = RndGen.init(0),
default_context: ?*const v8.Context = null,
default_context: ?v8.Global,
// We expect allocator to be an arena
// Note: This initializes the pre-allocated inspector in-place
@@ -96,9 +96,9 @@ pub fn init(self: *Inspector, isolate: *v8.Isolate, ctx: anytype) !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);
var hs: v8.HandleScope = undefined;
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate);
defer v8.v8__HandleScope__DESTRUCT(&hs);
self.session.deinit();
self.client.deinit();
@@ -128,7 +128,7 @@ pub fn send(self: *const Inspector, msg: []const u8) void {
// - is_default_context: Whether the execution context is default, should match the auxData
pub fn contextCreated(
self: *Inspector,
context: *const Context,
local: *const js.Local,
name: []const u8,
origin: []const u8,
aux_data: []const u8,
@@ -143,14 +143,26 @@ pub fn contextCreated(
aux_data.ptr,
aux_data.len,
CONTEXT_GROUP_ID,
context.handle,
local.handle,
);
if (is_default_context) {
self.default_context = context.handle;
self.default_context = local.ctx.handle;
}
}
pub fn contextDestroyed(self: *Inspector, local: *const js.Local) void {
v8.v8_inspector__Inspector__ContextDestroyed(self.handle, local.handle);
}
pub fn resetContextGroup(self: *const Inspector) void {
var hs: v8.HandleScope = undefined;
v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate);
defer v8.v8__HandleScope__DESTRUCT(&hs);
v8.v8_inspector__Inspector__ResetContextGroup(self.handle, CONTEXT_GROUP_ID);
}
// Retrieves the RemoteObject for a given value.
// The value is loaded through the ExecutionWorld's mapZigInstanceToJs function,
// just like a method return value. Therefore, if we've mapped this
@@ -158,41 +170,42 @@ pub fn contextCreated(
// we'll create it and track it for cleanup when the context ends.
pub fn getRemoteObject(
self: *const Inspector,
context: *Context,
local: *const js.Local,
group: []const u8,
value: anytype,
) !RemoteObject {
const js_value = try context.zigValueToJs(value, .{});
const js_val = try local.zigValueToJs(value, .{});
// We do not want to expose this as a parameter for now
const generate_preview = false;
return self.session.wrapObject(
context.isolate.handle,
context.handle,
js_value.handle,
local.isolate.handle,
local.handle,
js_val.handle,
group,
generate_preview,
);
}
// Gets a value by object ID regardless of which context it is in.
// Our TaggedAnyOpaque stores the "resolved" ptr value (the most specific _type,
// Our TaggedOpaque stores the "resolved" ptr value (the most specific _type,
// e.g. we store the ptr to the Div not the EventTarget). But, this is asking for
// the pointer to the Node, so we need to use the same resolution mechanism which
// is used when we're calling a function to turn the Div into a Node, which is
// what Context.typeTaggedAnyOpaque does.
pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []const u8) !*anyopaque {
// what TaggedOpaque.fromJS does.
pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []const u8, local: *js.Local) !*anyopaque {
// just to indicate that the caller is responsible for ensure there's a local environment
_ = local;
const unwrapped = try self.session.unwrapObject(allocator, object_id);
// The values context and groupId are not used here
const js_val = unwrapped.value;
if (!v8.v8__Value__IsObject(js_val)) {
return error.ObjectIdIsNotANode;
}
const Node = @import("../webapi/Node.zig");
// Cast to *const v8.Object for typeTaggedAnyOpaque
return Context.typeTaggedAnyOpaque(*Node, @ptrCast(js_val)) catch {
return error.ObjectIdIsNotANode;
};
return TaggedOpaque.fromJS(*Node, @ptrCast(js_val)) catch return error.ObjectIdIsNotANode;
}
pub const RemoteObject = struct {
@@ -399,7 +412,7 @@ fn fromData(data: *anyopaque) *Inspector {
return @ptrCast(@alignCast(data));
}
pub fn getTaggedAnyOpaque(value: *const v8.Value) ?*js.TaggedAnyOpaque {
pub fn getTaggedOpaque(value: *const v8.Value) ?*TaggedOpaque {
if (!v8.v8__Value__IsObject(value)) {
return null;
}
@@ -469,7 +482,8 @@ pub export fn v8_inspector__Client__IMPL__ensureDefaultContextInGroup(
data: *anyopaque,
) callconv(.c) ?*const v8.Context {
const inspector: *Inspector = @ptrCast(@alignCast(data));
return inspector.default_context;
const global_handle = inspector.default_context orelse return null;
return v8.v8__Global__Get(&global_handle, inspector.isolate);
}
pub export fn v8_inspector__Channel__IMPL__sendResponse(

View File

@@ -57,6 +57,16 @@ pub fn lowMemoryNotification(self: Isolate) void {
v8.v8__Isolate__LowMemoryNotification(self.handle);
}
pub const MemoryPressureLevel = enum(u32) {
none = v8.kNone,
moderate = v8.kModerate,
critical = v8.kCritical,
};
pub fn memoryPressureNotification(self: Isolate, level: MemoryPressureLevel) void {
v8.v8__Isolate__MemoryPressureNotification(self.handle, @intFromEnum(level));
}
pub fn notifyContextDisposed(self: Isolate) void {
_ = v8.v8__Isolate__ContextDisposedNotification(self.handle);
}

1391
src/browser/js/Local.zig Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,7 @@ const v8 = js.v8;
const Module = @This();
ctx: *js.Context,
local: *const js.Local,
handle: *const v8.Module,
pub const Status = enum(u32) {
@@ -39,21 +39,21 @@ pub fn getStatus(self: Module) Status {
pub fn getException(self: Module) js.Value {
return .{
.ctx = self.ctx,
.local = self.local,
.handle = v8.v8__Module__GetException(self.handle).?,
};
}
pub fn getModuleRequests(self: Module) Requests {
return .{
.ctx = self.ctx.handle,
.context_handle = self.local.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);
v8.v8__Module__InstantiateModule(self.handle, self.local.handle, cb, &out);
if (out.has_value) {
return out.value;
}
@@ -61,15 +61,14 @@ pub fn instantiate(self: Module, cb: v8.ResolveModuleCallback) !bool {
}
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;
const res = v8.v8__Module__Evaluate(self.handle, self.local.handle) orelse return error.JsException;
if (self.getStatus() == .kErrored) {
return error.JsException;
}
return .{
.ctx = ctx,
.local = self.local,
.handle = res,
};
}
@@ -80,7 +79,7 @@ pub fn getIdentityHash(self: Module) u32 {
pub fn getModuleNamespace(self: Module) js.Value {
return .{
.ctx = self.ctx,
.local = self.local,
.handle = v8.v8__Module__GetModuleNamespace(self.handle).?,
};
}
@@ -90,28 +89,24 @@ pub fn getScriptId(self: Module) u32 {
}
pub fn persist(self: Module) !Global {
var ctx = self.ctx;
var ctx = self.local.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,
};
return .{ .handle = global };
}
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 {
pub fn local(self: *const Global, l: *const js.Local) Module {
return .{
.ctx = self.ctx,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)),
.local = l,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
};
}
@@ -121,15 +116,15 @@ pub const Global = struct {
};
const Requests = struct {
ctx: *const v8.Context,
handle: *const v8.FixedArray,
context_handle: *const v8.Context,
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)).? };
return .{ .handle = v8.v8__FixedArray__Get(self.handle, self.context_handle, @intCast(idx)).? };
}
};

View File

@@ -28,19 +28,15 @@ const Allocator = std.mem.Allocator;
const Object = @This();
ctx: *js.Context,
local: *const js.Local,
handle: *const v8.Object,
pub fn getId(self: Object) u32 {
return @bitCast(v8.v8__Object__GetIdentityHash(self.handle));
}
pub fn has(self: Object, key: anytype) bool {
const ctx = self.ctx;
const ctx = self.local.ctx;
const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key);
var out: v8.MaybeBool = undefined;
v8.v8__Object__Has(self.handle, self.ctx.handle, key_handle, &out);
v8.v8__Object__Has(self.handle, self.local.handle, key_handle, &out);
if (out.has_value) {
return out.value;
}
@@ -48,34 +44,34 @@ pub fn has(self: Object, key: anytype) bool {
}
pub fn get(self: Object, key: anytype) !js.Value {
const ctx = self.ctx;
const ctx = self.local.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;
const js_val_handle = v8.v8__Object__Get(self.handle, self.local.handle, key_handle) orelse return error.JsException;
return .{
.ctx = ctx,
.local = self.local,
.handle = js_val_handle,
};
}
pub fn set(self: Object, key: anytype, value: anytype, comptime opts: js.bridge.Caller.CallOpts) !bool {
const ctx = self.ctx;
pub fn set(self: Object, key: anytype, value: anytype, comptime opts: js.Caller.CallOpts) !bool {
const ctx = self.local.ctx;
const js_value = try ctx.zigValueToJs(value, opts);
const js_value = try self.local.zigValueToJs(value, opts);
const key_handle = if (@TypeOf(key) == *const v8.String) key else ctx.isolate.initStringHandle(key);
var out: v8.MaybeBool = undefined;
v8.v8__Object__Set(self.handle, ctx.handle, key_handle, js_value.handle, &out);
v8.v8__Object__Set(self.handle, self.local.handle, key_handle, js_value.handle, &out);
return out.has_value;
}
pub fn defineOwnProperty(self: Object, name: []const u8, value: js.Value, attr: v8.PropertyAttribute) ?bool {
const ctx = self.ctx;
const ctx = self.local.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);
v8.v8__Object__DefineOwnProperty(self.handle, self.local.handle, @ptrCast(name_handle), value.handle, attr, &out);
if (out.has_value) {
return out.value;
@@ -85,52 +81,49 @@ pub fn defineOwnProperty(self: Object, name: []const u8, value: js.Value, attr:
}
pub fn toString(self: Object) ![]const u8 {
return self.ctx.valueToString(self.toValue(), .{});
return self.local.ctx.valueToString(self.toValue(), .{});
}
pub fn toValue(self: Object) js.Value {
return .{
.ctx = self.ctx,
.local = self.local,
.handle = @ptrCast(self.handle),
};
}
pub fn format(self: Object, writer: *std.Io.Writer) !void {
if (comptime IS_DEBUG) {
return self.ctx.debugValue(self.toValue(), writer);
return self.local.ctx.debugValue(self.toValue(), writer);
}
const str = self.toString() catch return error.WriteFailed;
return writer.writeAll(str);
}
pub fn persist(self: Object) !Global {
var ctx = self.ctx;
var ctx = self.local.ctx;
var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
try ctx.global_objects.append(ctx.arena, global);
return .{
.handle = global,
.ctx = ctx,
};
return .{ .handle = global };
}
pub fn getFunction(self: Object, name: []const u8) !?js.Function {
if (self.isNullOrUndefined()) {
return null;
}
const ctx = self.ctx;
const local = self.local;
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_name = local.isolate.initStringHandle(name);
const js_val_handle = v8.v8__Object__Get(self.handle, local.handle, js_name) orelse return error.JsException;
if (v8.v8__Value__IsFunction(js_val_handle) == false) {
return null;
}
return .{
.ctx = ctx,
.local = local,
.handle = @ptrCast(js_val_handle),
};
}
@@ -145,51 +138,48 @@ pub fn isNullOrUndefined(self: Object) bool {
}
pub fn getOwnPropertyNames(self: Object) js.Array {
const handle = v8.v8__Object__GetOwnPropertyNames(self.handle, self.ctx.handle).?;
const handle = v8.v8__Object__GetOwnPropertyNames(self.handle, self.local.handle).?;
return .{
.ctx = self.ctx,
.local = self.local,
.handle = handle,
};
}
pub fn getPropertyNames(self: Object) js.Array {
const handle = v8.v8__Object__GetPropertyNames(self.handle, self.ctx.handle).?;
const handle = v8.v8__Object__GetPropertyNames(self.handle, self.local.handle).?;
return .{
.ctx = self.ctx,
.local = self.local,
.handle = handle,
};
}
pub fn nameIterator(self: Object) NameIterator {
const ctx = self.ctx;
const handle = v8.v8__Object__GetPropertyNames(self.handle, ctx.handle).?;
const handle = v8.v8__Object__GetPropertyNames(self.handle, self.local.handle).?;
const count = v8.v8__Array__Length(handle);
return .{
.ctx = ctx,
.local = self.local,
.handle = handle,
.count = count,
};
}
pub fn toZig(self: Object, comptime T: type) !T {
const js_value = js.Value{ .ctx = self.ctx, .handle = @ptrCast(self.handle) };
return self.ctx.jsValueToZig(T, js_value);
const js_value = js.Value{ .local = self.local, .handle = @ptrCast(self.handle) };
return self.local.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 {
pub fn local(self: *const Global, l: *const js.Local) Object {
return .{
.ctx = self.ctx,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)),
.local = l,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
};
}
@@ -201,7 +191,7 @@ pub const Global = struct {
pub const NameIterator = struct {
count: u32,
idx: u32 = 0,
ctx: *Context,
local: *const js.Local,
handle: *const v8.Array,
pub fn next(self: *NameIterator) !?[]const u8 {
@@ -211,8 +201,8 @@ pub const NameIterator = struct {
}
self.idx += 1;
const js_val_handle = v8.v8__Object__GetIndex(@ptrCast(self.handle), self.ctx.handle, idx) orelse return error.JsException;
const js_val = js.Value{ .ctx = self.ctx, .handle = js_val_handle };
return try self.ctx.valueToString(js_val, .{});
const js_val_handle = v8.v8__Object__GetIndex(@ptrCast(self.handle), self.local.handle, idx) orelse return error.JsException;
const js_val = js.Value{ .local = self.local, .handle = js_val_handle };
return try self.local.valueToString(js_val, .{});
}
};

View File

@@ -21,63 +21,51 @@ const v8 = js.v8;
const Promise = @This();
ctx: *js.Context,
local: *const js.Local,
handle: *const v8.Promise,
pub fn toObject(self: Promise) js.Object {
return .{
.ctx = self.ctx,
.local = self.local,
.handle = @ptrCast(self.handle),
};
}
pub fn toValue(self: Promise) js.Value {
return .{
.ctx = self.ctx,
.local = self.local,
.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| {
if (v8.v8__Promise__Then2(self.handle, self.local.handle, on_fulfilled.handle, on_rejected.handle)) |handle| {
return .{
.ctx = self.ctx,
.local = self.local,
.handle = handle,
};
}
return error.PromiseChainFailed;
}
pub fn persist(self: Promise) !Global {
var ctx = self.ctx;
var ctx = self.local.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,
};
return .{ .handle = global };
}
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 {
pub fn local(self: *const Global, l: *const js.Local) Promise {
return .{
.ctx = self.ctx,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)),
.local = l,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.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

@@ -22,19 +22,19 @@ const log = @import("../../log.zig");
const PromiseResolver = @This();
ctx: *js.Context,
local: *const js.Local,
handle: *const v8.PromiseResolver,
pub fn init(ctx: *js.Context) PromiseResolver {
pub fn init(local: *const js.Local) PromiseResolver {
return .{
.ctx = ctx,
.handle = v8.v8__Promise__Resolver__New(ctx.handle).?,
.local = local,
.handle = v8.v8__Promise__Resolver__New(local.handle).?,
};
}
pub fn promise(self: PromiseResolver) js.Promise {
return .{
.ctx = self.ctx,
.local = self.local,
.handle = v8.v8__Promise__Resolver__GetPromise(self.handle).?,
};
}
@@ -46,15 +46,15 @@ pub fn resolve(self: PromiseResolver, comptime source: []const u8, value: anytyp
}
fn _resolve(self: PromiseResolver, value: anytype) !void {
const ctx: *js.Context = @constCast(self.ctx);
const js_value = try ctx.zigValueToJs(value, .{});
const local = self.local;
const js_val = try local.zigValueToJs(value, .{});
var out: v8.MaybeBool = undefined;
v8.v8__Promise__Resolver__Resolve(self.handle, self.ctx.handle, js_value.handle, &out);
v8.v8__Promise__Resolver__Resolve(self.handle, self.local.handle, js_val.handle, &out);
if (!out.has_value or !out.value) {
return error.FailedToResolvePromise;
}
ctx.runMicrotasks();
local.runMicrotasks();
}
pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype) void {
@@ -64,44 +64,36 @@ pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype
}
fn _reject(self: PromiseResolver, value: anytype) !void {
const ctx = self.ctx;
const js_value = try ctx.zigValueToJs(value, .{});
const local = self.local;
const js_val = try local.zigValueToJs(value, .{});
var out: v8.MaybeBool = undefined;
v8.v8__Promise__Resolver__Reject(self.handle, ctx.handle, js_value.handle, &out);
v8.v8__Promise__Resolver__Reject(self.handle, local.handle, js_val.handle, &out);
if (!out.has_value or !out.value) {
return error.FailedToRejectPromise;
}
ctx.runMicrotasks();
local.runMicrotasks();
}
pub fn persist(self: PromiseResolver) !Global {
var ctx = self.ctx;
var ctx = self.local.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,
};
return .{ .handle = global };
}
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 {
pub fn local(self: *const Global, l: *const js.Local) PromiseResolver {
return .{
.ctx = self.ctx,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)),
.local = l,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
};
}
pub fn isEqual(self: *const Global, other: PromiseResolver) bool {
return v8.v8__Global__IsEqual(&self.handle, other.handle);
}
};

View File

@@ -22,7 +22,6 @@ const bridge = @import("bridge.zig");
const log = @import("../../log.zig");
const IS_DEBUG = @import("builtin").mode == .Debug;
const Window = @import("../webapi/Window.zig");
const v8 = js.v8;
const JsApis = bridge.JsApis;
@@ -114,20 +113,6 @@ fn isValid(self: Snapshot) bool {
return v8.v8__StartupData__IsValid(self.startup_data);
}
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();
@@ -169,8 +154,7 @@ pub fn create() !Snapshot {
// Set up the global template to inherit from Window's template
// This way the global object gets all Window properties through inheritance
const global_template = createGlobalTemplate(isolate, templates[0..]);
const context = v8.v8__Context__New(isolate, global_template, null);
const context = v8.v8__Context__New(isolate, null, null);
v8.v8__Context__Enter(context);
defer v8.v8__Context__Exit(context);

View File

@@ -25,7 +25,7 @@ const v8 = js.v8;
const String = @This();
ctx: *js.Context,
local: *const js.Local,
handle: *const v8.String,
pub const ToZigOpts = struct {
@@ -41,8 +41,8 @@ pub fn toZigZ(self: String, opts: ToZigOpts) ![:0]u8 {
}
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 isolate = self.local.isolate.handle;
const allocator = opts.allocator orelse self.local.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);

View File

@@ -0,0 +1,156 @@
// Copyright (C) 2023-2026 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 bridge = js.bridge;
// When we return a Zig object to V8, we put it on the heap and pass it into
// v8 as an *anyopaque (i.e. void *). When V8 gives us back the value, say, as a
// function parameter, we know what type it _should_ be.
//
// In a simple/perfect world, we could use this knowledge to cast the *anyopaque
// to the parameter type:
// const arg: @typeInfo(@TypeOf(function)).@"fn".params[0] = @ptrCast(v8_data);
//
// But there are 2 reasons we can't do that.
//
// == Reason 1 ==
// The JS code might pass the wrong type:
//
// var cat = new Cat();
// cat.setOwner(new Cat());
//
// The zig_setOwner method expects the 2nd parameter to be an *Owner, but
// the JS code passed a *Cat.
//
// To solve this issue, we tag every returned value so that we can check what
// type it is. In the above case, we'd expect an *Owner, but the tag would tell
// us that we got a *Cat. We use the type index in our Types lookup as the tag.
//
// == Reason 2 ==
// Because of prototype inheritance, even "correct" code can be a challenge. For
// example, say the above JavaScript is fixed:
//
// var cat = new Cat();
// cat.setOwner(new Owner("Leto"));
//
// The issue is that setOwner might not expect an *Owner, but rather a
// *Person, which is the prototype for Owner. Now our Zig code is expecting
// a *Person, but it was (correctly) given an *Owner.
// For this reason, we also store the prototype chain.
const TaggedOpaque = @This();
prototype_len: u16,
prototype_chain: [*]const PrototypeChainEntry,
// Ptr to the Zig instance. Between the context where it's called (i.e.
// we have the comptime parameter info for all functions), and the index field
// we can figure out what type this is.
value: *anyopaque,
// When we're asked to describe an object via the Inspector, we _must_ include
// the proper subtype (and description) fields in the returned JSON.
// V8 will give us a Value and ask us for the subtype. From the js.Value we
// can get a js.Object, and from the js.Object, we can get out TaggedOpaque
// which is where we store the subtype.
subtype: ?bridge.SubType,
pub const PrototypeChainEntry = struct {
index: bridge.JsApiLookup.BackingInt,
offset: u16, // offset to the _proto field
};
// Reverses the mapZigInstanceToJs, making sure that our TaggedOpaque
// contains a ptr to the correct type.
pub fn fromJS(comptime R: type, js_obj_handle: *const v8.Object) !R {
const ti = @typeInfo(R);
if (ti != .pointer) {
@compileError("non-pointer Zig parameter type: " ++ @typeName(R));
}
const T = ti.pointer.child;
const JsApi = bridge.Struct(T).JsApi;
if (@hasDecl(JsApi.Meta, "empty_with_no_proto")) {
// Empty structs aren't stored as TOAs and there's no data
// stored in the JSObject's IntenrnalField. Why bother when
// we can just return an empty struct here?
return @constCast(@as(*const T, &.{}));
}
const internal_field_count = v8.v8__Object__InternalFieldCount(js_obj_handle);
// Special case for Window: the global object doesn't have internal fields
// Window instance is stored in context.page.window instead
if (internal_field_count == 0) {
// Normally, this would be an error. All JsObject that map to a Zig type
// are either `empty_with_no_proto` (handled above) or have an
// interalFieldCount. The only exception to that is the Window...
const isolate = v8.v8__Object__GetIsolate(js_obj_handle).?;
const context = js.Context.fromIsolate(.{ .handle = isolate });
const Window = @import("../webapi/Window.zig");
if (T == Window) {
return context.page.window;
}
// ... Or the window's prototype.
// We could make this all comptime-fancy, but it's easier to hard-code
// the EventTarget
const EventTarget = @import("../webapi/EventTarget.zig");
if (T == EventTarget) {
return context.page.window._proto;
}
// Type not found in Window's prototype chain
return error.InvalidArgument;
}
// if it isn't an empty struct, then the v8.Object should have an
// InternalFieldCount > 0, since our toa pointer should be embedded
// at index 0 of the internal field count.
if (internal_field_count == 0) {
return error.InvalidArgument;
}
if (!bridge.JsApiLookup.has(JsApi)) {
@compileError("unknown Zig type: " ++ @typeName(R));
}
const internal_field_handle = v8.v8__Object__GetInternalField(js_obj_handle, 0).?;
const tao: *TaggedOpaque = @ptrCast(@alignCast(v8.v8__External__Value(internal_field_handle)));
const expected_type_index = bridge.JsApiLookup.getId(JsApi);
const prototype_chain = tao.prototype_chain[0..tao.prototype_len];
if (prototype_chain[0].index == expected_type_index) {
return @ptrCast(@alignCast(tao.value));
}
// Ok, let's walk up the chain
var ptr = @intFromPtr(tao.value);
for (prototype_chain[1..]) |proto| {
ptr += proto.offset; // the offset to the _proto field
const proto_ptr: **anyopaque = @ptrFromInt(ptr);
if (proto.index == expected_type_index) {
return @ptrCast(@alignCast(proto_ptr.*));
}
ptr = @intFromPtr(proto_ptr.*);
}
return error.InvalidArgument;
}

View File

@@ -24,43 +24,34 @@ const Allocator = std.mem.Allocator;
const TryCatch = @This();
ctx: *js.Context,
handle: v8.TryCatch,
local: *const js.Local,
pub fn init(self: *TryCatch, ctx: *js.Context) void {
self.ctx = ctx;
v8.v8__TryCatch__CONSTRUCT(&self.handle, ctx.isolate.handle);
}
pub fn hasCaught(self: TryCatch) bool {
return v8.v8__TryCatch__HasCaught(&self.handle);
pub fn init(self: *TryCatch, l: *const js.Local) void {
self.local = l;
v8.v8__TryCatch__CONSTRUCT(&self.handle, l.isolate.handle);
}
pub fn caught(self: TryCatch, allocator: Allocator) ?Caught {
if (!self.hasCaught()) {
if (!v8.v8__TryCatch__HasCaught(&self.handle)) {
return null;
}
const ctx = self.ctx;
var hs: js.HandleScope = undefined;
hs.init(ctx.isolate);
defer hs.deinit();
const l = self.local;
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 line = v8.v8__Message__GetLineNumber(handle, l.handle);
break :blk if (line < 0) null else @intCast(line);
};
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);
break :blk l.valueHandleToString(@ptrCast(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);
const handle = v8.v8__TryCatch__StackTrace(&self.handle, l.handle) orelse break :blk null;
break :blk l.valueHandleToString(@ptrCast(handle), .{ .allocator = allocator }) catch |err| @errorName(err);
};
return .{
@@ -85,10 +76,10 @@ pub fn deinit(self: *TryCatch) void {
}
pub const Caught = struct {
line: ?u32,
caught: bool,
stack: ?[]const u8,
exception: ?[]const u8,
line: ?u32 = null,
caught: bool = false,
stack: ?[]const u8 = null,
exception: ?[]const u8 = null,
pub fn format(self: Caught, writer: *std.Io.Writer) !void {
const separator = @import("../../log.zig").separator();

View File

@@ -27,7 +27,7 @@ const Allocator = std.mem.Allocator;
const Value = @This();
ctx: *js.Context,
local: *const js.Local,
handle: *const v8.Value,
pub fn isObject(self: Value) bool {
@@ -155,12 +155,12 @@ pub fn isPromise(self: Value) bool {
}
pub fn toBool(self: Value) bool {
return v8.v8__Value__BooleanValue(self.handle, self.ctx.isolate.handle);
return v8.v8__Value__BooleanValue(self.handle, self.local.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 };
const str_handle = v8.v8__Value__TypeOf(self.handle, self.local.isolate.handle).?;
return js.String{ .local = self.local, .handle = str_handle };
}
pub fn toF32(self: Value) !f32 {
@@ -169,7 +169,7 @@ pub fn toF32(self: Value) !f32 {
pub fn toF64(self: Value) !f64 {
var maybe: v8.MaybeF64 = undefined;
v8.v8__Value__NumberValue(self.handle, self.ctx.handle, &maybe);
v8.v8__Value__NumberValue(self.handle, self.local.handle, &maybe);
if (!maybe.has_value) {
return error.JsException;
}
@@ -178,7 +178,7 @@ pub fn toF64(self: Value) !f64 {
pub fn toI32(self: Value) !i32 {
var maybe: v8.MaybeI32 = undefined;
v8.v8__Value__Int32Value(self.handle, self.ctx.handle, &maybe);
v8.v8__Value__Int32Value(self.handle, self.local.handle, &maybe);
if (!maybe.has_value) {
return error.JsException;
}
@@ -187,7 +187,7 @@ pub fn toI32(self: Value) !i32 {
pub fn toU32(self: Value) !u32 {
var maybe: v8.MaybeU32 = undefined;
v8.v8__Value__Uint32Value(self.handle, self.ctx.handle, &maybe);
v8.v8__Value__Uint32Value(self.handle, self.local.handle, &maybe);
if (!maybe.has_value) {
return error.JsException;
}
@@ -199,7 +199,7 @@ pub fn toPromise(self: Value) js.Promise {
std.debug.assert(self.isPromise());
}
return .{
.ctx = self.ctx,
.local = self.local,
.handle = @ptrCast(self.handle),
};
}
@@ -212,53 +212,52 @@ pub fn toStringZ(self: Value, opts: js.String.ToZigOpts) ![:0]u8 {
}
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 });
const json_str_handle = v8.v8__JSON__Stringify(self.local.handle, self.handle, null) orelse return error.JsException;
return self.local.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;
const l = self.local;
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 sym_handle = v8.v8__Symbol__Description(@ptrCast(self.handle), l.isolate.handle).?;
return _toString(.{ .handle = @ptrCast(sym_handle), .local = l }, null_terminate, opts);
}
const str_handle = v8.v8__Value__ToString(self.handle, ctx.handle) orelse {
const str_handle = v8.v8__Value__ToString(self.handle, l.handle) orelse {
return error.JsException;
};
const str = js.String{ .ctx = ctx, .handle = str_handle };
const str = js.String{ .local = l, .handle = str_handle };
if (comptime null_terminate) {
return js.String.toZigZ(str, opts);
}
return js.String.toZig(str, opts);
}
pub fn fromJson(ctx: *js.Context, json: []const u8) !Value {
const v8_isolate = v8.Isolate{ .handle = ctx.isolate.handle };
const json_string = v8.String.initUtf8(v8_isolate, json);
const v8_context = v8.Context{ .handle = ctx.handle };
const value = try v8.Json.parse(v8_context, json_string);
return .{ .ctx = ctx, .handle = value.handle };
pub fn persist(self: Value) !Global {
return self._persist(true);
}
pub fn persist(self: Value) !Global {
var ctx = self.ctx;
pub fn temp(self: Value) !Temp {
return self._persist(false);
}
fn _persist(self: *const Value, comptime is_global: bool) !(if (is_global) Global else Temp) {
var ctx = self.local.ctx;
var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
try ctx.global_values.append(ctx.arena, global);
return .{
.handle = global,
.ctx = ctx,
};
if (comptime is_global) {
try ctx.global_values.append(ctx.arena, global);
} else {
try ctx.global_values_temp.put(ctx.arena, global.data_ptr, global);
}
return .{ .handle = global };
}
pub fn toZig(self: Value, comptime T: type) !T {
return self.ctx.jsValueToZig(T, self);
return self.local.jsValueToZig(T, self);
}
pub fn toObject(self: Value) js.Object {
@@ -267,7 +266,7 @@ pub fn toObject(self: Value) js.Object {
}
return .{
.ctx = self.ctx,
.local = self.local,
.handle = @ptrCast(self.handle),
};
}
@@ -278,7 +277,7 @@ pub fn toArray(self: Value) js.Array {
}
return .{
.ctx = self.ctx,
.local = self.local,
.handle = @ptrCast(self.handle),
};
}
@@ -295,28 +294,37 @@ pub fn toBigInt(self: Value) js.BigInt {
pub fn format(self: Value, writer: *std.Io.Writer) !void {
if (comptime IS_DEBUG) {
return self.ctx.debugValue(self, writer);
return self.local.debugValue(self, writer);
}
const str = self.toString(.{}) catch return error.WriteFailed;
return writer.writeAll(str);
}
pub const Global = struct {
handle: v8.Global,
ctx: *js.Context,
pub const Temp = G(0);
pub const Global = G(1);
pub fn deinit(self: *Global) void {
v8.v8__Global__Reset(&self.handle);
}
fn G(comptime discriminator: u8) type {
return struct {
handle: v8.Global,
pub fn local(self: *const Global) Value {
return .{
.ctx = self.ctx,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)),
};
}
// makes the types different (G(0) != G(1)), without taking up space
comptime _: u8 = discriminator,
pub fn isEqual(self: *const Global, other: Value) bool {
return v8.v8__Global__IsEqual(&self.handle, other.handle);
}
};
const Self = @This();
pub fn deinit(self: *Self) void {
v8.v8__Global__Reset(&self.handle);
}
pub fn local(self: *const Self, l: *const js.Local) Value {
return .{
.local = l,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
};
}
pub fn isEqual(self: *const Self, other: Value) bool {
return v8.v8__Global__IsEqual(&self.handle, other.handle);
}
};
}

View File

@@ -1,4 +1,4 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
@@ -20,555 +20,15 @@ const std = @import("std");
const js = @import("js.zig");
const lp = @import("lightpanda");
const log = @import("../../log.zig");
const Page = @import("../Page.zig");
const v8 = js.v8;
const Caller = @import("Caller.zig");
const Context = @import("Context.zig");
const Page = @import("../Page.zig");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const CALL_ARENA_RETAIN = 1024 * 16;
const IS_DEBUG = @import("builtin").mode == .Debug;
// ============================================================================
// Internal Callback Info Wrappers
// ============================================================================
// These wrap the raw v8 C API to provide a cleaner interface.
// They are not exported - internal to this module only.
const Value = struct {
handle: *const v8.Value,
fn isArray(self: Value) bool {
return v8.v8__Value__IsArray(self.handle);
}
fn isTypedArray(self: Value) bool {
return v8.v8__Value__IsTypedArray(self.handle);
}
fn isFunction(self: Value) bool {
return v8.v8__Value__IsFunction(self.handle);
}
};
const Name = struct {
handle: *const v8.Name,
};
const FunctionCallbackInfo = struct {
handle: *const v8.FunctionCallbackInfo,
fn length(self: FunctionCallbackInfo) u32 {
return @intCast(v8.v8__FunctionCallbackInfo__Length(self.handle));
}
fn getArg(self: FunctionCallbackInfo, index: u32) Value {
return .{ .handle = v8.v8__FunctionCallbackInfo__INDEX(self.handle, @intCast(index)).? };
}
fn getThis(self: FunctionCallbackInfo) *const v8.Object {
return v8.v8__FunctionCallbackInfo__This(self.handle).?;
}
fn getReturnValue(self: FunctionCallbackInfo) ReturnValue {
var rv: v8.ReturnValue = undefined;
v8.v8__FunctionCallbackInfo__GetReturnValue(self.handle, &rv);
return .{ .handle = rv };
}
fn isConstructCall(self: FunctionCallbackInfo) bool {
return v8.v8__FunctionCallbackInfo__IsConstructCall(self.handle);
}
};
const PropertyCallbackInfo = struct {
handle: *const v8.PropertyCallbackInfo,
fn getThis(self: PropertyCallbackInfo) *const v8.Object {
return v8.v8__PropertyCallbackInfo__This(self.handle).?;
}
fn getReturnValue(self: PropertyCallbackInfo) ReturnValue {
var rv: v8.ReturnValue = undefined;
v8.v8__PropertyCallbackInfo__GetReturnValue(self.handle, &rv);
return .{ .handle = rv };
}
};
const ReturnValue = struct {
handle: v8.ReturnValue,
fn set(self: ReturnValue, value: anytype) void {
const T = @TypeOf(value);
if (T == Value) {
self.setValueHandle(value.handle);
} else if (T == *const v8.Object) {
self.setValueHandle(@ptrCast(value));
} else if (T == *const v8.Value) {
self.setValueHandle(value);
} else if (T == js.Value) {
self.setValueHandle(value.handle);
} else {
@compileError("Unsupported type for ReturnValue.set: " ++ @typeName(T));
}
}
fn setValueHandle(self: ReturnValue, handle: *const v8.Value) void {
v8.v8__ReturnValue__Set(self.handle, handle);
}
};
// ============================================================================
// Caller - Responsible for calling Zig functions from JS invocations
// ============================================================================
pub const Caller = struct {
context: *Context,
isolate: js.Isolate,
call_arena: Allocator,
// Takes the raw v8 isolate and extracts the context from it.
pub fn init(v8_isolate: *v8.Isolate) Caller {
const isolate = js.Isolate{ .handle = v8_isolate };
const v8_context_handle = v8.v8__Isolate__GetCurrentContext(v8_isolate);
const embedder_data = v8.v8__Context__GetEmbedderData(v8_context_handle, 1);
var lossless: bool = undefined;
const context: *Context = @ptrFromInt(v8.v8__BigInt__Uint64Value(embedder_data, &lossless));
context.call_depth += 1;
return .{
.context = context,
.isolate = isolate,
.call_arena = context.call_arena,
};
}
pub fn deinit(self: *Caller) void {
const context = self.context;
const call_depth = context.call_depth - 1;
// Because of callbacks, calls can be nested. Because of this, we
// can't clear the call_arena after _every_ call. Imagine we have
// arr.forEach((i) => { console.log(i); }
//
// First we call forEach. Inside of our forEach call,
// we call console.log. If we reset the call_arena after this call,
// it'll reset it for the `forEach` call after, which might still
// need the data.
//
// Therefore, we keep a call_depth, and only reset the call_arena
// when a top-level (call_depth == 0) function ends.
if (call_depth == 0) {
const arena: *ArenaAllocator = @ptrCast(@alignCast(context.call_arena.ptr));
_ = arena.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN });
}
context.call_depth = call_depth;
}
pub const CallOpts = struct {
dom_exception: bool = false,
null_as_undefined: bool = false,
as_typed_array: bool = false,
};
pub fn constructor(self: *Caller, comptime T: type, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) void {
if (!info.isConstructCall()) {
self.handleError(T, @TypeOf(func), error.InvalidArgument, info, opts);
return;
}
self._constructor(func, info) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
};
}
fn _constructor(self: *Caller, func: anytype, info: FunctionCallbackInfo) !void {
const F = @TypeOf(func);
const args = try self.getArgs(F, 0, info);
const res = @call(.auto, func, args);
const ReturnType = @typeInfo(F).@"fn".return_type orelse {
@compileError(@typeName(F) ++ " has a constructor without a return type");
};
const new_this_handle = info.getThis();
var this = js.Object{ .ctx = self.context, .handle = new_this_handle };
if (@typeInfo(ReturnType) == .error_union) {
const non_error_res = res catch |err| return err;
this = try self.context.mapZigInstanceToJs(new_this_handle, non_error_res);
} else {
this = try self.context.mapZigInstanceToJs(new_this_handle, res);
}
// If we got back a different object (existing wrapper), copy the prototype
// from new object. (this happens when we're upgrading an CustomElement)
if (this.handle != new_this_handle) {
const prototype_handle = v8.v8__Object__GetPrototype(new_this_handle).?;
var out: v8.MaybeBool = undefined;
v8.v8__Object__SetPrototype(this.handle, self.context.handle, prototype_handle, &out);
if (comptime IS_DEBUG) {
std.debug.assert(out.has_value and out.value);
}
}
info.getReturnValue().set(this.handle);
}
pub fn method(self: *Caller, comptime T: type, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) void {
self._method(T, func, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
};
}
fn _method(self: *Caller, comptime T: type, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) !void {
const F = @TypeOf(func);
var handle_scope: js.HandleScope = undefined;
handle_scope.init(self.isolate);
defer handle_scope.deinit();
var args = try self.getArgs(F, 1, info);
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
const res = @call(.auto, func, args);
info.getReturnValue().set(try self.context.zigValueToJs(res, opts));
}
pub fn function(self: *Caller, comptime T: type, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) void {
self._function(func, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
};
}
fn _function(self: *Caller, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) !void {
const F = @TypeOf(func);
const context = self.context;
const args = try self.getArgs(F, 0, info);
const res = @call(.auto, func, args);
info.getReturnValue().set(try context.zigValueToJs(res, opts));
}
pub fn getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, info: PropertyCallbackInfo, comptime opts: CallOpts) u8 {
return self._getIndex(T, func, idx, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
// not intercepted
return 0;
};
}
fn _getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args = try self.getArgs(F, 2, info);
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
@field(args, "1") = idx;
const ret = @call(.auto, func, args);
return self.handleIndexedReturn(T, F, true, ret, info, opts);
}
pub fn getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: Name, info: PropertyCallbackInfo, comptime opts: CallOpts) u8 {
return self._getNamedIndex(T, func, name, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
// not intercepted
return 0;
};
}
fn _getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args = try self.getArgs(F, 2, info);
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
@field(args, "1") = try self.nameToString(name);
const ret = @call(.auto, func, args);
return self.handleIndexedReturn(T, F, true, ret, info, opts);
}
pub fn setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: Name, js_value: Value, info: PropertyCallbackInfo, comptime opts: CallOpts) u8 {
return self._setNamedIndex(T, func, name, js_value, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
// not intercepted
return 0;
};
}
fn _setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: Name, js_value: Value, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args: ParameterTypes(F) = undefined;
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
@field(args, "1") = try self.nameToString(name);
@field(args, "2") = try self.context.jsValueToZig(@TypeOf(@field(args, "2")), js.Value{ .ctx = self.context, .handle = js_value.handle });
if (@typeInfo(F).@"fn".params.len == 4) {
@field(args, "3") = self.context.page;
}
const ret = @call(.auto, func, args);
return self.handleIndexedReturn(T, F, false, ret, info, opts);
}
pub fn deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: Name, info: PropertyCallbackInfo, comptime opts: CallOpts) u8 {
return self._deleteNamedIndex(T, func, name, info, opts) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts);
return 0;
};
}
fn _deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
const F = @TypeOf(func);
var args: ParameterTypes(F) = undefined;
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
@field(args, "1") = try self.nameToString(name);
if (@typeInfo(F).@"fn".params.len == 3) {
@field(args, "2") = self.context.page;
}
const ret = @call(.auto, func, args);
return self.handleIndexedReturn(T, F, false, ret, info, opts);
}
fn handleIndexedReturn(self: *Caller, comptime T: type, comptime F: type, comptime getter: bool, ret: anytype, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
// need to unwrap this error immediately for when opts.null_as_undefined == true
// and we need to compare it to null;
const non_error_ret = switch (@typeInfo(@TypeOf(ret))) {
.error_union => |eu| blk: {
break :blk ret catch |err| {
// We can't compare err == error.NotHandled if error.NotHandled
// isn't part of the possible error set. So we first need to check
// if error.NotHandled is part of the error set.
if (isInErrorSet(error.NotHandled, eu.error_set)) {
if (err == error.NotHandled) {
// not intercepted
return 0;
}
}
self.handleError(T, F, err, info, opts);
// not intercepted
return 0;
};
},
else => ret,
};
if (comptime getter) {
info.getReturnValue().set(try self.context.zigValueToJs(non_error_ret, opts));
}
// intercepted
return 1;
}
fn isInErrorSet(err: anyerror, comptime T: type) bool {
inline for (@typeInfo(T).error_set.?) |e| {
if (err == @field(anyerror, e.name)) return true;
}
return false;
}
fn nameToString(self: *Caller, name: Name) ![]const u8 {
return self.context.valueToString(js.Value{ .ctx = self.context, .handle = @ptrCast(name.handle) }, .{});
}
fn handleError(self: *Caller, comptime T: type, comptime F: type, err: anyerror, info: anytype, comptime opts: CallOpts) void {
const isolate = self.isolate;
if (comptime @import("builtin").mode == .Debug and @TypeOf(info) == FunctionCallbackInfo) {
if (log.enabled(.js, .warn)) {
self.logFunctionCallError(@typeName(T), @typeName(F), err, info);
}
}
const js_err: *const v8.Value = switch (err) {
error.InvalidArgument => isolate.createTypeError("invalid argument"),
error.OutOfMemory => isolate.createError("out of memory"),
error.IllegalConstructor => isolate.createError("Illegal Contructor"),
else => blk: {
if (comptime opts.dom_exception) {
const DOMException = @import("../webapi/DOMException.zig");
if (DOMException.fromError(err)) |ex| {
const value = self.context.zigValueToJs(ex, .{}) catch break :blk isolate.createError("internal error");
break :blk value.handle;
}
}
break :blk isolate.createError(@errorName(err));
},
};
const js_exception = isolate.throwException(js_err);
info.getReturnValue().setValueHandle(js_exception);
}
// If we call a method in javascript: cat.lives('nine');
//
// Then we'd expect a Zig function with 2 parameters: a self and the string.
// In this case, offset == 1. Offset is always 1 for setters or methods.
//
// Offset is always 0 for constructors.
//
// For constructors, setters and methods, we can further increase offset + 1
// if the first parameter is an instance of Page.
//
// Finally, if the JS function is called with _more_ parameters and
// the last parameter in Zig is an array, we'll try to slurp the additional
// parameters into the array.
fn getArgs(self: *const Caller, comptime F: type, comptime offset: usize, info: anytype) !ParameterTypes(F) {
const context = self.context;
var args: ParameterTypes(F) = undefined;
const params = @typeInfo(F).@"fn".params[offset..];
// Except for the constructor, the first parameter is always `self`
// This isn't something we'll bind from JS, so skip it.
const params_to_map = blk: {
if (params.len == 0) {
return args;
}
// If the last parameter is the Page, set it, and exclude it
// from our params slice, because we don't want to bind it to
// a JS argument
if (comptime isPage(params[params.len - 1].type.?)) {
@field(args, tupleFieldName(params.len - 1 + offset)) = self.context.page;
break :blk params[0 .. params.len - 1];
}
// we have neither a Page nor a JsObject. All params must be
// bound to a JavaScript value.
break :blk params;
};
if (params_to_map.len == 0) {
return args;
}
const js_parameter_count = info.length();
const last_js_parameter = params_to_map.len - 1;
var is_variadic = false;
{
// This is going to get complicated. If the last Zig parameter
// is a slice AND the corresponding javascript parameter is
// NOT an an array, then we'll treat it as a variadic.
const last_parameter_type = params_to_map[params_to_map.len - 1].type.?;
const last_parameter_type_info = @typeInfo(last_parameter_type);
if (last_parameter_type_info == .pointer and last_parameter_type_info.pointer.size == .slice) {
const slice_type = last_parameter_type_info.pointer.child;
const corresponding_js_value = info.getArg(@as(u32, @intCast(last_js_parameter)));
if (corresponding_js_value.isArray() == false and corresponding_js_value.isTypedArray() == false and slice_type != u8) {
is_variadic = true;
if (js_parameter_count == 0) {
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
} else if (js_parameter_count >= params_to_map.len) {
const arr = try self.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);
for (arr, last_js_parameter..) |*a, i| {
const js_value = info.getArg(@as(u32, @intCast(i)));
a.* = try context.jsValueToZig(slice_type, js.Value{ .ctx = context, .handle = js_value.handle });
}
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = arr;
} else {
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
}
}
}
}
inline for (params_to_map, 0..) |param, i| {
const field_index = comptime i + offset;
if (comptime i == params_to_map.len - 1) {
if (is_variadic) {
break;
}
}
if (comptime isPage(param.type.?)) {
@compileError("Page must be the last parameter (or 2nd last if there's a JsThis): " ++ @typeName(F));
} else if (i >= js_parameter_count) {
if (@typeInfo(param.type.?) != .optional) {
return error.InvalidArgument;
}
@field(args, tupleFieldName(field_index)) = null;
} else {
const js_value = info.getArg(@as(u32, @intCast(i)));
@field(args, tupleFieldName(field_index)) = context.jsValueToZig(param.type.?, js.Value{ .ctx = context, .handle = js_value.handle }) catch {
return error.InvalidArgument;
};
}
}
return args;
}
// This is extracted to speed up compilation. When left inlined in handleError,
// this can add as much as 10 seconds of compilation time.
fn logFunctionCallError(self: *Caller, type_name: []const u8, func: []const u8, err: anyerror, info: FunctionCallbackInfo) void {
const args_dump = self.serializeFunctionArgs(info) catch "failed to serialize args";
log.info(.js, "function call error", .{
.type = type_name,
.func = func,
.err = err,
.args = args_dump,
.stack = self.context.stackTrace() catch |err1| @errorName(err1),
});
}
fn serializeFunctionArgs(self: *Caller, info: FunctionCallbackInfo) ![]const u8 {
const context = self.context;
var buf = std.Io.Writer.Allocating.init(context.call_arena);
const separator = log.separator();
for (0..info.length()) |i| {
try buf.writer.print("{s}{d} - ", .{ separator, i + 1 });
const val = info.getArg(@intCast(i));
try context.debugValue(js.Value{ .ctx = context, .handle = val.handle }, &buf.writer);
}
return buf.written();
}
// Takes a function, and returns a tuple for its argument. Used when we
// @call a function
fn ParameterTypes(comptime F: type) type {
const params = @typeInfo(F).@"fn".params;
var fields: [params.len]std.builtin.Type.StructField = undefined;
inline for (params, 0..) |param, i| {
fields[i] = .{
.name = tupleFieldName(i),
.type = param.type.?,
.default_value_ptr = null,
.is_comptime = false,
.alignment = @alignOf(param.type.?),
};
}
return @Type(.{ .@"struct" = .{
.layout = .auto,
.decls = &.{},
.fields = &fields,
.is_tuple = true,
} });
}
fn tupleFieldName(comptime i: usize) [:0]const u8 {
return switch (i) {
0 => "0",
1 => "1",
2 => "2",
3 => "3",
4 => "4",
5 => "5",
6 => "6",
7 => "7",
8 => "8",
9 => "9",
else => std.fmt.comptimePrint("{d}", .{i}),
};
}
fn isPage(comptime T: type) bool {
return T == *Page or T == *const Page;
}
};
// ============================================================================
// Bridge Builder Functions
// ============================================================================
pub fn Builder(comptime T: type) type {
return struct {
pub const @"type" = T;
@@ -610,8 +70,9 @@ pub fn Builder(comptime T: type) type {
@compileError("Property for " ++ @typeName(@TypeOf(value)) ++ " hasn't been defined yet");
}
pub fn prototypeChain() [prototypeChainLength(T)]js.PrototypeChainEntry {
var entries: [prototypeChainLength(T)]js.PrototypeChainEntry = undefined;
const PrototypeChainEntry = @import("TaggedOpaque.zig").PrototypeChainEntry;
pub fn prototypeChain() [prototypeChainLength(T)]PrototypeChainEntry {
var entries: [prototypeChainLength(T)]PrototypeChainEntry = undefined;
entries[0] = .{ .offset = 0, .index = JsApiLookup.getId(T.JsApi) };
@@ -630,6 +91,36 @@ pub fn Builder(comptime T: type) type {
}
return entries;
}
pub fn finalizer(comptime func: *const fn (self: *T, shutdown: bool) void) Finalizer {
return .{
.from_zig = struct {
fn wrap(ptr: *anyopaque) void {
func(@ptrCast(@alignCast(ptr)), true);
}
}.wrap,
.from_v8 = struct {
fn wrap(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void {
const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?;
const self: *T = @ptrCast(@alignCast(ptr));
// This is simply a requirement of any type that Finalizes:
// It must have a _page: *Page field. We need it because
// we need to check the item has already been cleared
// (There are all types of weird timing issues that seem
// to be possible between finalization and context shutdown,
// we need to be defensive).
// There _ARE_ alternatives to this. But this is simple.
const ctx = self._page.js;
if (!ctx.identity_map.contains(@intFromPtr(ptr))) {
return;
}
func(self, false);
ctx.release(ptr);
}
}.wrap,
};
}
};
}
@@ -644,11 +135,11 @@ pub const Constructor = struct {
return .{ .func = struct {
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
var caller: Caller = undefined;
caller.init(v8_isolate);
defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
caller.constructor(T, func, info, .{
caller.constructor(T, func, handle.?, .{
.dom_exception = opts.dom_exception,
});
}
@@ -673,18 +164,18 @@ pub const Function = struct {
.func = struct {
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
var caller: Caller = undefined;
caller.init(v8_isolate);
defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
if (comptime opts.static) {
caller.function(T, func, info, .{
caller.function(T, func, handle.?, .{
.dom_exception = opts.dom_exception,
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
} else {
caller.method(T, func, info, .{
caller.method(T, func, handle.?, .{
.dom_exception = opts.dom_exception,
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
@@ -703,7 +194,7 @@ pub const Accessor = struct {
const Opts = struct {
static: bool = false,
cache: ?[]const u8 = null, // @ZIGDOM
cache: ?[]const u8 = null,
as_typed_array: bool = false,
null_as_undefined: bool = false,
};
@@ -717,17 +208,19 @@ pub const Accessor = struct {
accessor.getter = struct {
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
var caller: Caller = undefined;
caller.init(v8_isolate);
defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
if (comptime opts.static) {
caller.function(T, getter, info, .{
caller.function(T, getter, handle.?, .{
.cache = opts.cache,
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
} else {
caller.method(T, getter, info, .{
caller.method(T, getter, handle.?, .{
.cache = opts.cache,
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
@@ -740,15 +233,11 @@ pub const Accessor = struct {
accessor.setter = struct {
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
var caller: Caller = undefined;
caller.init(v8_isolate);
defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
if (comptime IS_DEBUG) {
lp.assert(info.length() == 1, "bridge.setter", .{ .len = info.length() });
}
caller.method(T, setter, info, .{
caller.method(T, setter, handle.?, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
@@ -772,11 +261,11 @@ pub const Indexed = struct {
return .{ .getter = struct {
fn wrap(idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
var caller: Caller = undefined;
caller.init(v8_isolate);
defer caller.deinit();
const info = PropertyCallbackInfo{ .handle = handle.? };
return caller.getIndex(T, getter, idx, info, .{
return caller.getIndex(T, getter, idx, handle.?, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
@@ -799,11 +288,11 @@ pub const NamedIndexed = struct {
const getter_fn = struct {
fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
var caller: Caller = undefined;
caller.init(v8_isolate);
defer caller.deinit();
const info = PropertyCallbackInfo{ .handle = handle.? };
return caller.getNamedIndex(T, getter, .{ .handle = c_name.? }, info, .{
return caller.getNamedIndex(T, getter, c_name.?, handle.?, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
@@ -813,11 +302,11 @@ pub const NamedIndexed = struct {
const setter_fn = if (@typeInfo(@TypeOf(setter)) == .null) null else struct {
fn wrap(c_name: ?*const v8.Name, c_value: ?*const v8.Value, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
var caller: Caller = undefined;
caller.init(v8_isolate);
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, c_name.?, c_value.?, handle.?, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
@@ -827,11 +316,11 @@ pub const NamedIndexed = struct {
const deleter_fn = if (@typeInfo(@TypeOf(deleter)) == .null) null else struct {
fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
var caller: Caller = undefined;
caller.init(v8_isolate);
defer caller.deinit();
const info = PropertyCallbackInfo{ .handle = handle.? };
return caller.deleteNamedIndex(T, deleter, .{ .handle = c_name.? }, info, .{
return caller.deleteNamedIndex(T, deleter, c_name.?, handle.?, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
@@ -861,7 +350,7 @@ pub const Iterator = struct {
.async = opts.async,
.func = struct {
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const info = FunctionCallbackInfo{ .handle = handle.? };
const info = Caller.FunctionCallbackInfo{ .handle = handle.? };
info.getReturnValue().set(info.getThis());
}
}.wrap,
@@ -873,11 +362,10 @@ pub const Iterator = struct {
.func = struct {
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
var caller: Caller = undefined;
caller.init(v8_isolate);
defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
caller.method(T, struct_or_func, info, .{});
caller.method(T, struct_or_func, handle.?, .{});
}
}.wrap,
};
@@ -895,11 +383,11 @@ pub const Callable = struct {
return .{ .func = struct {
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
var caller: Caller = undefined;
caller.init(v8_isolate);
defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
caller.method(T, func, info, .{
caller.method(T, func, handle.?, .{
.null_as_undefined = opts.null_as_undefined,
});
}
@@ -911,53 +399,69 @@ pub const Property = union(enum) {
int: i64,
};
pub fn unknownPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const isolate_handle = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
const context = Context.fromIsolate(.{ .handle = isolate_handle });
const Finalizer = struct {
// The finalizer wrapper when called fro Zig. This is only called on
// Context.deinit
from_zig: *const fn (ctx: *anyopaque) void,
const property: []const u8 = context.valueToString(.{ .ctx = context, .handle = c_name.? }, .{}) catch {
// The finalizer wrapper when called from V8. This may never be called
// (hence why we fallback to calling in Context.denit). If it is called,
// it is only ever called after we SetWeak on the Global.
from_v8: *const fn (?*const v8.WeakCallbackInfo) callconv(.c) void,
};
pub fn unknownPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
var caller: Caller = undefined;
caller.init(v8_isolate);
defer caller.deinit();
const local = &caller.local;
var hs: js.HandleScope = undefined;
hs.init(local.isolate);
defer hs.deinit();
const property: []const u8 = local.valueHandleToString(@ptrCast(c_name.?), .{}) catch {
return 0;
};
const ignored = std.StaticStringMap(void).initComptime(.{
.{ "process", {} },
.{ "ShadyDOM", {} },
.{ "ShadyCSS", {} },
const page = local.ctx.page;
const document = page.document;
.{ "litNonce", {} },
.{ "litHtmlVersions", {} },
.{ "litElementVersions", {} },
.{ "litHtmlPolyfillSupport", {} },
.{ "litElementHydrateSupport", {} },
.{ "litElementPolyfillSupport", {} },
.{ "reactiveElementVersions", {} },
if (document.getElementById(property, page)) |el| {
const js_val = local.zigValueToJs(el, .{}) catch return 0;
var pc = Caller.PropertyCallbackInfo{ .handle = handle.? };
pc.getReturnValue().set(js_val);
return 1;
}
.{ "recaptcha", {} },
.{ "grecaptcha", {} },
.{ "___grecaptcha_cfg", {} },
.{ "__recaptcha_api", {} },
.{ "__google_recaptcha_client", {} },
if (comptime IS_DEBUG) {
const ignored = std.StaticStringMap(void).initComptime(.{
.{ "process", {} },
.{ "ShadyDOM", {} },
.{ "ShadyCSS", {} },
.{ "CLOSURE_FLAGS", {} },
});
.{ "litNonce", {} },
.{ "litHtmlVersions", {} },
.{ "litElementVersions", {} },
.{ "litHtmlPolyfillSupport", {} },
.{ "litElementHydrateSupport", {} },
.{ "litElementPolyfillSupport", {} },
.{ "reactiveElementVersions", {} },
if (!ignored.has(property)) {
const page = context.page;
const document = page.document;
.{ "recaptcha", {} },
.{ "grecaptcha", {} },
.{ "___grecaptcha_cfg", {} },
.{ "__recaptcha_api", {} },
.{ "__google_recaptcha_client", {} },
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) {
.{ "CLOSURE_FLAGS", {} },
});
if (!ignored.has(property)) {
log.debug(.unknown_prop, "unknown global property", .{
.info = "but the property can exist in pure JS",
.stack = context.stackTrace() catch "???",
.stack = local.stackTrace() catch "???",
.property = property,
});
}
@@ -1231,6 +735,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/event/PopStateEvent.zig"),
@import("../webapi/event/UIEvent.zig"),
@import("../webapi/event/MouseEvent.zig"),
@import("../webapi/event/PointerEvent.zig"),
@import("../webapi/event/KeyboardEvent.zig"),
@import("../webapi/MessageChannel.zig"),
@import("../webapi/MessagePort.zig"),
@@ -1272,4 +777,5 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/canvas/CanvasRenderingContext2D.zig"),
@import("../webapi/canvas/WebGLRenderingContext.zig"),
@import("../webapi/SubtleCrypto.zig"),
@import("../webapi/Selection.zig"),
});

View File

@@ -1,48 +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 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

@@ -20,11 +20,14 @@ const std = @import("std");
pub const v8 = @import("v8").c;
const log = @import("../../log.zig");
const string = @import("../../string.zig");
pub const Env = @import("Env.zig");
pub const bridge = @import("bridge.zig");
pub const ExecutionWorld = @import("ExecutionWorld.zig");
pub const Caller = @import("Caller.zig");
pub const Context = @import("Context.zig");
pub const Local = @import("Local.zig");
pub const Inspector = @import("Inspector.zig");
pub const Snapshot = @import("Snapshot.zig");
pub const Platform = @import("Platform.zig");
@@ -43,7 +46,6 @@ 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;
@@ -78,12 +80,8 @@ pub const ArrayBuffer = struct {
};
pub const Exception = struct {
ctx: *const Context,
local: *const Local,
handle: *const v8.Value,
pub fn exception(self: Exception, allocator: Allocator) ![]const u8 {
return self.context.valueToString(self.inner, .{ .allocator = allocator });
}
};
// These are simple types that we can convert to JS with only an isolate. This
@@ -133,6 +131,7 @@ pub fn simpleZigValueToJs(isolate: Isolate, value: anytype, comptime fail: bool,
},
.@"struct" => {
switch (@TypeOf(value)) {
string.String => return isolate.initStringHandle(value.str()),
ArrayBuffer => {
const values = value.values;
const len = values.len;
@@ -215,61 +214,6 @@ pub fn simpleZigValueToJs(isolate: Isolate, value: anytype, comptime fail: bool,
}
return null;
}
// When we return a Zig object to V8, we put it on the heap and pass it into
// v8 as an *anyopaque (i.e. void *). When V8 gives us back the value, say, as a
// function parameter, we know what type it _should_ be.
//
// In a simple/perfect world, we could use this knowledge to cast the *anyopaque
// to the parameter type:
// const arg: @typeInfo(@TypeOf(function)).@"fn".params[0] = @ptrCast(v8_data);
//
// But there are 2 reasons we can't do that.
//
// == Reason 1 ==
// The JS code might pass the wrong type:
//
// var cat = new Cat();
// cat.setOwner(new Cat());
//
// The zig_setOwner method expects the 2nd parameter to be an *Owner, but
// the JS code passed a *Cat.
//
// To solve this issue, we tag every returned value so that we can check what
// type it is. In the above case, we'd expect an *Owner, but the tag would tell
// us that we got a *Cat. We use the type index in our Types lookup as the tag.
//
// == Reason 2 ==
// Because of prototype inheritance, even "correct" code can be a challenge. For
// example, say the above JavaScript is fixed:
//
// var cat = new Cat();
// cat.setOwner(new Owner("Leto"));
//
// The issue is that setOwner might not expect an *Owner, but rather a
// *Person, which is the prototype for Owner. Now our Zig code is expecting
// a *Person, but it was (correctly) given an *Owner.
// For this reason, we also store the prototype chain.
pub const TaggedAnyOpaque = struct {
prototype_len: u16,
prototype_chain: [*]const PrototypeChainEntry,
// Ptr to the Zig instance. Between the context where it's called (i.e.
// we have the comptime parameter info for all functions), and the index field
// we can figure out what type this is.
value: *anyopaque,
// When we're asked to describe an object via the Inspector, we _must_ include
// the proper subtype (and description) fields in the returned JSON.
// V8 will give us a Value and ask us for the subtype. From the js.Value we
// can get a js.Object, and from the js.Object, we can get out TaggedAnyOpaque
// which is where we store the subtype.
subtype: ?bridge.SubType,
};
pub const PrototypeChainEntry = struct {
index: bridge.JsApiLookup.BackingInt,
offset: u16, // offset to the _proto field
};
// These are here, and not in Inspector.zig, because Inspector.zig isn't always
// included (e.g. in the wpt build).
@@ -281,7 +225,7 @@ pub export fn v8_inspector__Client__IMPL__valueSubtype(
_: *v8.InspectorClientImpl,
c_value: *const v8.Value,
) callconv(.c) [*c]const u8 {
const external_entry = Inspector.getTaggedAnyOpaque(c_value) orelse return null;
const external_entry = Inspector.getTaggedOpaque(c_value) orelse return null;
return if (external_entry.subtype) |st| @tagName(st) else null;
}
@@ -298,11 +242,11 @@ pub export fn v8_inspector__Client__IMPL__descriptionForValueSubtype(
// We _must_ include a non-null description in order for the subtype value
// to be included. Besides that, I don't know if the value has any meaning
const external_entry = Inspector.getTaggedAnyOpaque(c_value) orelse return null;
const external_entry = Inspector.getTaggedOpaque(c_value) orelse return null;
return if (external_entry.subtype == null) null else "";
}
test "TaggedAnyOpaque" {
// If we grow this, fine, but it should be a conscious decision
try std.testing.expectEqual(24, @sizeOf(TaggedAnyOpaque));
try std.testing.expectEqual(24, @sizeOf(@import("TaggedOpaque.zig")));
}

View File

@@ -24,6 +24,7 @@ const Page = @import("../Page.zig");
const Node = @import("../webapi/Node.zig");
const Element = @import("../webapi/Element.zig");
const Allocator = std.mem.Allocator;
const IS_DEBUG = @import("builtin").mode == .Debug;
pub const ParsedNode = struct {
node: *Node,
@@ -373,6 +374,17 @@ fn _appendCallback(self: *Parser, parent: *Node, node_or_text: h5e.NodeOrText) !
switch (node_or_text.toUnion()) {
.node => |cpn| {
const child = getNode(cpn);
if (child._parent) |previous_parent| {
// html5ever says this can't happen, but we might be screwing up
// the node on our side. We shouldn't be, but we're seeing this
// in the wild, and I'm not sure why. In debug, let's crash so
// we can try to figure it out. In release, let's disconnect
// the child first.
if (comptime IS_DEBUG) {
unreachable;
}
self.page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent.isConnected() });
}
try self.page.appendNew(parent, .{ .node = child });
},
.text => |txt| try self.page.appendNew(parent, .{ .text = txt }),

View File

@@ -41,4 +41,53 @@
testing.expectEqual("DIV", newElement.tagName);
testing.expectEqual("after begin", newElement.innerText);
testing.expectEqual("afterbegin", newElement.className);
const fuzzWrapper = document.createElement("div");
fuzzWrapper.id = "fuzz-wrapper";
document.body.appendChild(fuzzWrapper);
const fuzzCases = [
// These cases have no <body> element (or empty body), so nothing is inserted
{ name: "empty string", html: "", expectElements: 0 },
{ name: "comment only", html: "<!-- comment -->", expectElements: 0 },
{ name: "doctype only", html: "<!DOCTYPE html>", expectElements: 0 },
{ name: "full empty doc", html: "<!DOCTYPE html><html><head></head><body></body></html>", expectElements: 0 },
{ name: "whitespace only", html: " ", expectElements: 0 },
{ name: "newlines only", html: "\n\n\n", expectElements: 0 },
{ name: "just text", html: "plain text", expectElements: 0 },
// Head-only elements: Extracted from <head> container
{ name: "empty meta", html: "<meta>", expectElements: 1 },
{ name: "empty title", html: "<title></title>", expectElements: 1 },
{ name: "empty head", html: "<head></head>", expectElements: 0 }, // Container with no children
{ name: "empty body", html: "<body></body>", expectElements: 0 }, // Container with no children
{ name: "empty html", html: "<html></html>", expectElements: 0 }, // Container with no children
{ name: "meta only", html: "<meta charset='utf-8'>", expectElements: 1 },
{ name: "title only", html: "<title>Test</title>", expectElements: 1 },
{ name: "link only", html: "<link rel='stylesheet' href='test.css'>", expectElements: 1 },
{ name: "meta and title", html: "<meta charset='utf-8'><title>Test</title>", expectElements: 2 },
{ name: "script only", html: "<script>console.log('hi')<\/script>", expectElements: 1 },
{ name: "style only", html: "<style>body { color: red; }<\/style>", expectElements: 1 },
{ name: "unclosed div", html: "<div>content", expectElements: 1 },
{ name: "unclosed span", html: "<span>text", expectElements: 1 },
{ name: "invalid tag", html: "<notarealtag>content</notarealtag>", expectElements: 1 },
{ name: "malformed", html: "<<div>>test<</div>>", expectElements: 1 }, // Parser handles it
{ name: "just closing tag", html: "</div>", expectElements: 0 },
{ name: "nested empty", html: "<div><div></div></div>", expectElements: 1 },
{ name: "multiple top-level", html: "<span>1</span><span>2</span><span>3</span>", expectElements: 3 },
{ name: "mixed text and elements", html: "Text before<b>bold</b>Text after", expectElements: 1 },
{ name: "deeply nested", html: "<div><div><div><span>deep</span></div></div></div>", expectElements: 1 },
];
fuzzCases.forEach((tc, idx) => {
fuzzWrapper.innerHTML = "";
fuzzWrapper.insertAdjacentHTML("beforeend", tc.html);
if (tc.expectElements !== fuzzWrapper.childElementCount) {
console.warn(`Fuzz idx: ${idx}`);
testing.expectEqual(tc.expectElements, fuzzWrapper.childElementCount);
}
});
// Clean up
document.body.removeChild(fuzzWrapper);
</script>

View File

@@ -0,0 +1,490 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<!-- Test inline event listeners set via HTML attributes -->
<div id="attr-click" onclick="window.x = 1"></div>
<div id="attr-load" onload="window.x = 1"></div>
<div id="attr-error" onerror="window.x = 1"></div>
<div id="attr-focus" onfocus="window.x = 1"></div>
<div id="attr-blur" onblur="window.x = 1"></div>
<div id="attr-keydown" onkeydown="window.x = 1"></div>
<div id="attr-mousedown" onmousedown="window.x = 1"></div>
<div id="attr-submit" onsubmit="window.x = 1"></div>
<div id="attr-wheel" onwheel="window.x = 1"></div>
<div id="attr-scroll" onscroll="window.x = 1"></div>
<div id="attr-contextmenu" oncontextmenu="window.x = 1"></div>
<div id="no-listeners"></div>
<script id="attr_listener_returns_function">
{
// Inline listeners set via HTML attributes should return a function
testing.expectEqual('function', typeof $('#attr-click').onclick);
testing.expectEqual('function', typeof $('#attr-load').onload);
testing.expectEqual('function', typeof $('#attr-error').onerror);
testing.expectEqual('function', typeof $('#attr-focus').onfocus);
testing.expectEqual('function', typeof $('#attr-blur').onblur);
testing.expectEqual('function', typeof $('#attr-keydown').onkeydown);
testing.expectEqual('function', typeof $('#attr-mousedown').onmousedown);
testing.expectEqual('function', typeof $('#attr-submit').onsubmit);
testing.expectEqual('function', typeof $('#attr-wheel').onwheel);
testing.expectEqual('function', typeof $('#attr-scroll').onscroll);
testing.expectEqual('function', typeof $('#attr-contextmenu').oncontextmenu);
}
</script>
<script id="no_attr_listener_returns_null">
{
// Elements without inline listeners should return null
const div = $('#no-listeners');
testing.expectEqual(null, div.onclick);
testing.expectEqual(null, div.onload);
testing.expectEqual(null, div.onerror);
testing.expectEqual(null, div.onfocus);
testing.expectEqual(null, div.onblur);
testing.expectEqual(null, div.onkeydown);
testing.expectEqual(null, div.onmousedown);
testing.expectEqual(null, div.onsubmit);
testing.expectEqual(null, div.onwheel);
testing.expectEqual(null, div.onscroll);
testing.expectEqual(null, div.oncontextmenu);
}
</script>
<script id="js_setter_getter">
{
// Test setting and getting listeners via JavaScript property
const div = document.createElement('div');
// Initially null
testing.expectEqual(null, div.onclick);
testing.expectEqual(null, div.onload);
testing.expectEqual(null, div.onerror);
// Set listeners
const clickHandler = () => {};
const loadHandler = () => {};
const errorHandler = () => {};
div.onclick = clickHandler;
div.onload = loadHandler;
div.onerror = errorHandler;
// Verify they can be retrieved and are functions
testing.expectEqual('function', typeof div.onclick);
testing.expectEqual('function', typeof div.onload);
testing.expectEqual('function', typeof div.onerror);
}
</script>
<script id="js_listener_invoke">
{
// Test that JS-set listeners can be invoked directly
const div = document.createElement('div');
window.jsInvokeResult = 0;
div.onclick = () => { window.jsInvokeResult = 100; };
div.onclick();
testing.expectEqual(100, window.jsInvokeResult);
div.onload = () => { window.jsInvokeResult = 200; };
div.onload();
testing.expectEqual(200, window.jsInvokeResult);
div.onfocus = () => { window.jsInvokeResult = 300; };
div.onfocus();
testing.expectEqual(300, window.jsInvokeResult);
}
</script>
<script id="js_listener_invoke_with_return">
{
// Test that JS-set listeners return values when invoked
const div = document.createElement('div');
div.onclick = () => { return 'click-result'; };
testing.expectEqual('click-result', div.onclick());
div.onload = () => { return 42; };
testing.expectEqual(42, div.onload());
div.onfocus = () => { return { key: 'value' }; };
testing.expectEqual('value', div.onfocus().key);
}
</script>
<script id="js_listener_invoke_with_args">
{
// Test that JS-set listeners can receive arguments when invoked
const div = document.createElement('div');
div.onclick = (a, b) => { return a + b; };
testing.expectEqual(15, div.onclick(10, 5));
div.onload = (msg) => { return 'Hello, ' + msg; };
testing.expectEqual('Hello, World', div.onload('World'));
}
</script>
<script id="js_setter_override">
{
// Test that setting a new listener overrides the old one
const div = document.createElement('div');
const first = () => { return 1; };
const second = () => { return 2; };
div.onclick = first;
testing.expectEqual('function', typeof div.onclick);
testing.expectEqual(1, div.onclick());
div.onclick = second;
testing.expectEqual('function', typeof div.onclick);
testing.expectEqual(2, div.onclick());
}
</script>
<script id="different_event_types_independent">
{
// Test that different event types are stored independently
const div = document.createElement('div');
const clickFn = () => {};
const focusFn = () => {};
const blurFn = () => {};
div.onclick = clickFn;
testing.expectEqual('function', typeof div.onclick);
testing.expectEqual(null, div.onfocus);
testing.expectEqual(null, div.onblur);
div.onfocus = focusFn;
testing.expectEqual('function', typeof div.onclick);
testing.expectEqual('function', typeof div.onfocus);
testing.expectEqual(null, div.onblur);
div.onblur = blurFn;
testing.expectEqual('function', typeof div.onclick);
testing.expectEqual('function', typeof div.onfocus);
testing.expectEqual('function', typeof div.onblur);
}
</script>
<script id="keyboard_event_listeners">
{
// Test keyboard event listener getters/setters
const div = document.createElement('div');
testing.expectEqual(null, div.onkeydown);
testing.expectEqual(null, div.onkeyup);
testing.expectEqual(null, div.onkeypress);
div.onkeydown = () => {};
div.onkeyup = () => {};
div.onkeypress = () => {};
testing.expectEqual('function', typeof div.onkeydown);
testing.expectEqual('function', typeof div.onkeyup);
testing.expectEqual('function', typeof div.onkeypress);
}
</script>
<script id="mouse_event_listeners">
{
// Test mouse event listener getters/setters
const div = document.createElement('div');
testing.expectEqual(null, div.onmousedown);
testing.expectEqual(null, div.onmouseup);
testing.expectEqual(null, div.onmousemove);
testing.expectEqual(null, div.onmouseover);
testing.expectEqual(null, div.onmouseout);
testing.expectEqual(null, div.ondblclick);
div.onmousedown = () => {};
div.onmouseup = () => {};
div.onmousemove = () => {};
div.onmouseover = () => {};
div.onmouseout = () => {};
div.ondblclick = () => {};
testing.expectEqual('function', typeof div.onmousedown);
testing.expectEqual('function', typeof div.onmouseup);
testing.expectEqual('function', typeof div.onmousemove);
testing.expectEqual('function', typeof div.onmouseover);
testing.expectEqual('function', typeof div.onmouseout);
testing.expectEqual('function', typeof div.ondblclick);
}
</script>
<script id="pointer_event_listeners">
{
// Test pointer event listener getters/setters
const div = document.createElement('div');
testing.expectEqual(null, div.onpointerdown);
testing.expectEqual(null, div.onpointerup);
testing.expectEqual(null, div.onpointermove);
testing.expectEqual(null, div.onpointerover);
testing.expectEqual(null, div.onpointerout);
testing.expectEqual(null, div.onpointerenter);
testing.expectEqual(null, div.onpointerleave);
testing.expectEqual(null, div.onpointercancel);
div.onpointerdown = () => {};
div.onpointerup = () => {};
div.onpointermove = () => {};
div.onpointerover = () => {};
div.onpointerout = () => {};
div.onpointerenter = () => {};
div.onpointerleave = () => {};
div.onpointercancel = () => {};
testing.expectEqual('function', typeof div.onpointerdown);
testing.expectEqual('function', typeof div.onpointerup);
testing.expectEqual('function', typeof div.onpointermove);
testing.expectEqual('function', typeof div.onpointerover);
testing.expectEqual('function', typeof div.onpointerout);
testing.expectEqual('function', typeof div.onpointerenter);
testing.expectEqual('function', typeof div.onpointerleave);
testing.expectEqual('function', typeof div.onpointercancel);
}
</script>
<script id="form_event_listeners">
{
// Test form event listener getters/setters
const form = document.createElement('form');
testing.expectEqual(null, form.onsubmit);
testing.expectEqual(null, form.onreset);
testing.expectEqual(null, form.onchange);
testing.expectEqual(null, form.oninput);
testing.expectEqual(null, form.oninvalid);
form.onsubmit = () => {};
form.onreset = () => {};
form.onchange = () => {};
form.oninput = () => {};
form.oninvalid = () => {};
testing.expectEqual('function', typeof form.onsubmit);
testing.expectEqual('function', typeof form.onreset);
testing.expectEqual('function', typeof form.onchange);
testing.expectEqual('function', typeof form.oninput);
testing.expectEqual('function', typeof form.oninvalid);
}
</script>
<script id="drag_event_listeners">
{
// Test drag event listener getters/setters
const div = document.createElement('div');
testing.expectEqual(null, div.ondrag);
testing.expectEqual(null, div.ondragstart);
testing.expectEqual(null, div.ondragend);
testing.expectEqual(null, div.ondragenter);
testing.expectEqual(null, div.ondragleave);
testing.expectEqual(null, div.ondragover);
testing.expectEqual(null, div.ondrop);
div.ondrag = () => {};
div.ondragstart = () => {};
div.ondragend = () => {};
div.ondragenter = () => {};
div.ondragleave = () => {};
div.ondragover = () => {};
div.ondrop = () => {};
testing.expectEqual('function', typeof div.ondrag);
testing.expectEqual('function', typeof div.ondragstart);
testing.expectEqual('function', typeof div.ondragend);
testing.expectEqual('function', typeof div.ondragenter);
testing.expectEqual('function', typeof div.ondragleave);
testing.expectEqual('function', typeof div.ondragover);
testing.expectEqual('function', typeof div.ondrop);
}
</script>
<script id="clipboard_event_listeners">
{
// Test clipboard event listener getters/setters
const div = document.createElement('div');
testing.expectEqual(null, div.oncopy);
testing.expectEqual(null, div.oncut);
testing.expectEqual(null, div.onpaste);
div.oncopy = () => {};
div.oncut = () => {};
div.onpaste = () => {};
testing.expectEqual('function', typeof div.oncopy);
testing.expectEqual('function', typeof div.oncut);
testing.expectEqual('function', typeof div.onpaste);
}
</script>
<script id="scroll_event_listeners">
{
// Test scroll event listener getters/setters
const div = document.createElement('div');
testing.expectEqual(null, div.onscroll);
testing.expectEqual(null, div.onscrollend);
testing.expectEqual(null, div.onresize);
div.onscroll = () => {};
div.onscrollend = () => {};
div.onresize = () => {};
testing.expectEqual('function', typeof div.onscroll);
testing.expectEqual('function', typeof div.onscrollend);
testing.expectEqual('function', typeof div.onresize);
}
</script>
<script id="animation_event_listeners">
{
// Test animation event listener getters/setters
const div = document.createElement('div');
testing.expectEqual(null, div.onanimationstart);
testing.expectEqual(null, div.onanimationend);
testing.expectEqual(null, div.onanimationiteration);
testing.expectEqual(null, div.onanimationcancel);
div.onanimationstart = () => {};
div.onanimationend = () => {};
div.onanimationiteration = () => {};
div.onanimationcancel = () => {};
testing.expectEqual('function', typeof div.onanimationstart);
testing.expectEqual('function', typeof div.onanimationend);
testing.expectEqual('function', typeof div.onanimationiteration);
testing.expectEqual('function', typeof div.onanimationcancel);
}
</script>
<script id="transition_event_listeners">
{
// Test transition event listener getters/setters
const div = document.createElement('div');
testing.expectEqual(null, div.ontransitionstart);
testing.expectEqual(null, div.ontransitionend);
testing.expectEqual(null, div.ontransitionrun);
testing.expectEqual(null, div.ontransitioncancel);
div.ontransitionstart = () => {};
div.ontransitionend = () => {};
div.ontransitionrun = () => {};
div.ontransitioncancel = () => {};
testing.expectEqual('function', typeof div.ontransitionstart);
testing.expectEqual('function', typeof div.ontransitionend);
testing.expectEqual('function', typeof div.ontransitionrun);
testing.expectEqual('function', typeof div.ontransitioncancel);
}
</script>
<script id="misc_event_listeners">
{
// Test miscellaneous event listener getters/setters
const div = document.createElement('div');
testing.expectEqual(null, div.onwheel);
testing.expectEqual(null, div.ontoggle);
testing.expectEqual(null, div.oncontextmenu);
testing.expectEqual(null, div.onselect);
testing.expectEqual(null, div.onabort);
testing.expectEqual(null, div.oncancel);
testing.expectEqual(null, div.onclose);
div.onwheel = () => {};
div.ontoggle = () => {};
div.oncontextmenu = () => {};
div.onselect = () => {};
div.onabort = () => {};
div.oncancel = () => {};
div.onclose = () => {};
testing.expectEqual('function', typeof div.onwheel);
testing.expectEqual('function', typeof div.ontoggle);
testing.expectEqual('function', typeof div.oncontextmenu);
testing.expectEqual('function', typeof div.onselect);
testing.expectEqual('function', typeof div.onabort);
testing.expectEqual('function', typeof div.oncancel);
testing.expectEqual('function', typeof div.onclose);
}
</script>
<script id="media_event_listeners">
{
// Test media event listener getters/setters
const div = document.createElement('div');
testing.expectEqual(null, div.onplay);
testing.expectEqual(null, div.onpause);
testing.expectEqual(null, div.onplaying);
testing.expectEqual(null, div.onended);
testing.expectEqual(null, div.onvolumechange);
testing.expectEqual(null, div.onwaiting);
testing.expectEqual(null, div.onseeking);
testing.expectEqual(null, div.onseeked);
testing.expectEqual(null, div.ontimeupdate);
testing.expectEqual(null, div.onloadstart);
testing.expectEqual(null, div.onprogress);
testing.expectEqual(null, div.onstalled);
testing.expectEqual(null, div.onsuspend);
testing.expectEqual(null, div.oncanplay);
testing.expectEqual(null, div.oncanplaythrough);
testing.expectEqual(null, div.ondurationchange);
testing.expectEqual(null, div.onemptied);
testing.expectEqual(null, div.onloadeddata);
testing.expectEqual(null, div.onloadedmetadata);
testing.expectEqual(null, div.onratechange);
div.onplay = () => {};
div.onpause = () => {};
div.onplaying = () => {};
div.onended = () => {};
div.onvolumechange = () => {};
div.onwaiting = () => {};
div.onseeking = () => {};
div.onseeked = () => {};
div.ontimeupdate = () => {};
div.onloadstart = () => {};
div.onprogress = () => {};
div.onstalled = () => {};
div.onsuspend = () => {};
div.oncanplay = () => {};
div.oncanplaythrough = () => {};
div.ondurationchange = () => {};
div.onemptied = () => {};
div.onloadeddata = () => {};
div.onloadedmetadata = () => {};
div.onratechange = () => {};
testing.expectEqual('function', typeof div.onplay);
testing.expectEqual('function', typeof div.onpause);
testing.expectEqual('function', typeof div.onplaying);
testing.expectEqual('function', typeof div.onended);
testing.expectEqual('function', typeof div.onvolumechange);
testing.expectEqual('function', typeof div.onwaiting);
testing.expectEqual('function', typeof div.onseeking);
testing.expectEqual('function', typeof div.onseeked);
testing.expectEqual('function', typeof div.ontimeupdate);
testing.expectEqual('function', typeof div.onloadstart);
testing.expectEqual('function', typeof div.onprogress);
testing.expectEqual('function', typeof div.onstalled);
testing.expectEqual('function', typeof div.onsuspend);
testing.expectEqual('function', typeof div.oncanplay);
testing.expectEqual('function', typeof div.oncanplaythrough);
testing.expectEqual('function', typeof div.ondurationchange);
testing.expectEqual('function', typeof div.onemptied);
testing.expectEqual('function', typeof div.onloadeddata);
testing.expectEqual('function', typeof div.onloadedmetadata);
testing.expectEqual('function', typeof div.onratechange);
}
</script>

View File

@@ -183,7 +183,7 @@
}
</script>
<script id="defaultChecked">
<!-- <script id="defaultChecked">
testing.expectEqual(true, $('#check1').defaultChecked)
testing.expectEqual(false, $('#check2').defaultChecked)
testing.expectEqual(true, $('#radio1').defaultChecked)
@@ -455,4 +455,4 @@
input_checked.defaultChecked = true;
testing.expectEqual(false, input_checked.checked);
}
</script>
</script> -->

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<script src="../../../testing.js"></script>
<script id="script">
{
let s = document.createElement('script');
testing.expectEqual('', s.src);
s.src = '/over.9000.js';
testing.expectEqual('http://127.0.0.1:9582/over.9000.js', s.src);
}
</script>

View File

@@ -0,0 +1,165 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script id=default>
{
let event = new PointerEvent('pointerdown');
testing.expectEqual('pointerdown', event.type);
testing.expectEqual(true, event instanceof PointerEvent);
testing.expectEqual(true, event instanceof MouseEvent);
testing.expectEqual(true, event instanceof UIEvent);
testing.expectEqual(true, event instanceof Event);
testing.expectEqual(0, event.pointerId);
testing.expectEqual('', event.pointerType);
testing.expectEqual(1.0, event.width);
testing.expectEqual(1.0, event.height);
testing.expectEqual(0.0, event.pressure);
testing.expectEqual(0.0, event.tangentialPressure);
testing.expectEqual(0, event.tiltX);
testing.expectEqual(0, event.tiltY);
testing.expectEqual(0, event.twist);
testing.expectEqual(false, event.isPrimary);
}
</script>
<script id=parameters>
{
let new_event = new PointerEvent('pointerdown', {
pointerId: 42,
pointerType: 'pen',
width: 10.5,
height: 20.5,
pressure: 0.75,
tangentialPressure: -0.25,
tiltX: 30,
tiltY: 45,
twist: 90,
isPrimary: true,
clientX: 100,
clientY: 200,
screenX: 300,
screenY: 400
});
testing.expectEqual(42, new_event.pointerId);
testing.expectEqual('pen', new_event.pointerType);
testing.expectEqual(10.5, new_event.width);
testing.expectEqual(20.5, new_event.height);
testing.expectEqual(0.75, new_event.pressure);
testing.expectEqual(-0.25, new_event.tangentialPressure);
testing.expectEqual(30, new_event.tiltX);
testing.expectEqual(45, new_event.tiltY);
testing.expectEqual(90, new_event.twist);
testing.expectEqual(true, new_event.isPrimary);
testing.expectEqual(100, new_event.clientX);
testing.expectEqual(200, new_event.clientY);
testing.expectEqual(300, new_event.screenX);
testing.expectEqual(400, new_event.screenY);
}
</script>
<script id=mousePointerType>
{
let mouse_event = new PointerEvent('pointerdown', { pointerType: 'mouse' });
testing.expectEqual('mouse', mouse_event.pointerType);
}
</script>
<script id=touchPointerType>
{
let touch_event = new PointerEvent('pointerdown', { pointerType: 'touch', pointerId: 1, pressure: 0.5 });
testing.expectEqual('touch', touch_event.pointerType);
testing.expectEqual(1, touch_event.pointerId);
testing.expectEqual(0.5, touch_event.pressure);
}
</script>
<script id=listener>
{
let pe = new PointerEvent('pointerdown', { pointerId: 123 });
testing.expectEqual(true, pe instanceof PointerEvent);
testing.expectEqual(true, pe instanceof MouseEvent);
testing.expectEqual(true, pe instanceof Event);
var evt = null;
document.addEventListener('pointerdown', function (e) {
evt = e;
});
document.dispatchEvent(pe);
testing.expectEqual('pointerdown', evt.type);
testing.expectEqual(true, evt instanceof PointerEvent);
testing.expectEqual(123, evt.pointerId);
}
</script>
<script id=isTrusted>
{
let pointerEvent = new PointerEvent('pointerup');
testing.expectEqual(false, pointerEvent.isTrusted);
let pointerIsTrusted = null;
document.addEventListener('pointertest', (e) => {
pointerIsTrusted = e.isTrusted;
testing.expectEqual(true, e instanceof PointerEvent);
});
document.dispatchEvent(new PointerEvent('pointertest'));
testing.expectEqual(false, pointerIsTrusted);
}
</script>
<script id=eventTypes>
{
let down = new PointerEvent('pointerdown');
testing.expectEqual('pointerdown', down.type);
let up = new PointerEvent('pointerup');
testing.expectEqual('pointerup', up.type);
let move = new PointerEvent('pointermove');
testing.expectEqual('pointermove', move.type);
let enter = new PointerEvent('pointerenter');
testing.expectEqual('pointerenter', enter.type);
let leave = new PointerEvent('pointerleave');
testing.expectEqual('pointerleave', leave.type);
let over = new PointerEvent('pointerover');
testing.expectEqual('pointerover', over.type);
let out = new PointerEvent('pointerout');
testing.expectEqual('pointerout', out.type);
let cancel = new PointerEvent('pointercancel');
testing.expectEqual('pointercancel', cancel.type);
}
</script>
<script id=inheritedMouseProperties>
{
let pe = new PointerEvent('pointerdown', {
button: 2,
buttons: 4,
altKey: true,
ctrlKey: true,
shiftKey: true,
metaKey: true
});
testing.expectEqual(2, pe.button);
testing.expectEqual(true, pe.altKey);
testing.expectEqual(true, pe.ctrlKey);
testing.expectEqual(true, pe.shiftKey);
testing.expectEqual(true, pe.metaKey);
}
</script>
<script id=inheritedUIEventProperties>
{
let pe = new PointerEvent('pointerdown', {
detail: 5,
bubbles: true,
cancelable: true
});
testing.expectEqual(5, pe.detail);
testing.expectEqual(true, pe.bubbles);
testing.expectEqual(true, pe.cancelable);
}
</script>

View File

@@ -30,7 +30,7 @@
testing.eventually(() => {
testing.expectEqual(true, popstateEventFired);
testing.expectEqual(state, popstateEventState);
testing.expectEqual({testInProgress: true }, popstateEventState);
})
history.back();

View File

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

View File

@@ -58,3 +58,6 @@
testing.expectEqual(true, e.toString().includes("FailedToLoad"), {script_id: 'import-404'});
}
</script>
<!-- this used to crash -->
<script type=module src=modules/self_async.js></script>

View File

@@ -0,0 +1 @@
const c = await import('./self_async.js');

View File

@@ -0,0 +1,543 @@
<!DOCTYPE html>
<meta charset="UTF-8">
<script src="./testing.js"></script>
<div id="test-content">
<p id="p1">The quick brown fox</p>
<p id="p2">jumps over the lazy dog</p>
<div id="nested">
<span id="s1">Hello</span>
<span id="s2">World</span>
</div>
</div>
<script id=basic>
{
const sel = window.getSelection();
sel.removeAllRanges();
testing.expectEqual(0, sel.rangeCount);
testing.expectEqual("None", sel.type);
testing.expectEqual(true, sel.isCollapsed);
testing.expectEqual(null, sel.anchorNode);
testing.expectEqual(null, sel.focusNode);
testing.expectEqual(0, sel.anchorOffset);
testing.expectEqual(0, sel.focusOffset);
testing.expectEqual("none", sel.direction);
}
</script>
<script id=collapse>
{
const sel = window.getSelection();
sel.removeAllRanges();
const p1 = document.getElementById("p1");
const textNode = p1.firstChild;
// Collapse to a position
sel.collapse(textNode, 4);
testing.expectEqual(1, sel.rangeCount);
testing.expectEqual("Caret", sel.type);
testing.expectEqual(true, sel.isCollapsed);
testing.expectEqual(textNode, sel.anchorNode);
testing.expectEqual(textNode, sel.focusNode);
testing.expectEqual(4, sel.anchorOffset);
testing.expectEqual(4, sel.focusOffset);
testing.expectEqual("none", sel.direction);
// Collapse to null removes all ranges
sel.collapse(null);
testing.expectEqual(0, sel.rangeCount);
testing.expectEqual("None", sel.type);
}
</script>
<script id=setPosition>
{
const sel = window.getSelection();
sel.removeAllRanges();
const p2 = document.getElementById("p2");
const textNode = p2.firstChild;
// setPosition is an alias for collapse
sel.setPosition(textNode, 10);
testing.expectEqual(1, sel.rangeCount);
testing.expectEqual("Caret", sel.type);
testing.expectEqual(textNode, sel.anchorNode);
testing.expectEqual(10, sel.anchorOffset);
// Test default offset
sel.setPosition(textNode);
testing.expectEqual(0, sel.anchorOffset);
// Test null
sel.setPosition(null);
testing.expectEqual(0, sel.rangeCount);
}
</script>
<script id=addRange>
{
const sel = window.getSelection();
sel.removeAllRanges();
const range1 = document.createRange();
const p1 = document.getElementById("p1");
range1.selectNodeContents(p1);
sel.addRange(range1);
testing.expectEqual(1, sel.rangeCount);
testing.expectEqual("Range", sel.type);
testing.expectEqual(false, sel.isCollapsed);
// Adding same range again should do nothing
sel.addRange(range1);
testing.expectEqual(1, sel.rangeCount);
// Adding different range
const range2 = document.createRange();
const p2 = document.getElementById("p2");
range2.selectNodeContents(p2);
sel.addRange(range2);
// Firefox does support multiple ranges so it will be 2 here instead of 1.
// Chrome and Safari don't so we don't either.
testing.expectEqual(1, sel.rangeCount);
}
</script>
<script id=getRangeAt>
{
const sel = window.getSelection();
sel.removeAllRanges();
const range = document.createRange();
const p1 = document.getElementById("p1");
range.selectNodeContents(p1);
sel.addRange(range);
const retrieved = sel.getRangeAt(0);
testing.expectEqual(range, retrieved);
}
</script>
<script id=removeRange>
{
const sel = window.getSelection();
sel.removeAllRanges();
const range1 = document.createRange();
const range2 = document.createRange();
const p1 = document.getElementById("p1");
const p2 = document.getElementById("p2");
range1.selectNodeContents(p1);
range2.selectNodeContents(p2);
sel.addRange(range1);
sel.addRange(range2);
// Firefox does support multiple ranges so it will be 2 here instead of 1.
// Chrome and Safari don't so we don't either.
testing.expectEqual(1, sel.rangeCount);
// Chrome doesn't throw an error here even though the spec defines it:
// https://w3c.github.io/selection-api/#dom-selection-removerange
testing.expectError('NotFoundError', () => { sel.removeRange(range2); });
testing.expectEqual(1, sel.rangeCount);
testing.expectEqual(range1, sel.getRangeAt(0));
}
</script>
<script id=removeAllRanges>
{
const sel = window.getSelection();
sel.removeAllRanges();
const range1 = document.createRange();
const range2 = document.createRange();
range1.selectNodeContents(document.getElementById("p1"));
range2.selectNodeContents(document.getElementById("p2"));
sel.addRange(range1);
sel.addRange(range2);
// Firefox does support multiple ranges so it will be 2 here instead of 1.
// Chrome and Safari don't so we don't either.
testing.expectEqual(1, sel.rangeCount);
sel.removeAllRanges();
testing.expectEqual(0, sel.rangeCount);
testing.expectEqual("none", sel.direction);
}
</script>
<script id=empty>
{
const sel = window.getSelection();
sel.removeAllRanges();
const range = document.createRange();
range.selectNodeContents(document.getElementById("p1"));
sel.addRange(range);
testing.expectEqual(1, sel.rangeCount);
// empty() is an alias for removeAllRanges()
sel.empty();
testing.expectEqual(0, sel.rangeCount);
}
</script>
<script id=collapseToStart>
{
const sel = window.getSelection();
const p1 = document.getElementById("p1");
const textNode = p1.firstChild;
const range = document.createRange();
range.setStart(textNode, 4);
range.setEnd(textNode, 15);
sel.removeAllRanges();
sel.addRange(range);
testing.expectEqual(false, sel.isCollapsed);
testing.expectEqual(4, sel.anchorOffset);
testing.expectEqual(15, sel.focusOffset);
sel.collapseToStart();
testing.expectEqual(true, sel.isCollapsed);
testing.expectEqual(1, sel.rangeCount);
testing.expectEqual(textNode, sel.anchorNode);
testing.expectEqual(4, sel.anchorOffset);
testing.expectEqual(4, sel.focusOffset);
testing.expectEqual("none", sel.direction);
}
</script>
<script id=collapseToEnd>
{
const sel = window.getSelection();
const p1 = document.getElementById("p1");
const textNode = p1.firstChild;
const range = document.createRange();
range.setStart(textNode, 4);
range.setEnd(textNode, 15);
sel.removeAllRanges();
sel.addRange(range);
testing.expectEqual(false, sel.isCollapsed);
sel.collapseToEnd();
testing.expectEqual(true, sel.isCollapsed);
testing.expectEqual(1, sel.rangeCount);
testing.expectEqual(textNode, sel.anchorNode);
testing.expectEqual(15, sel.anchorOffset);
testing.expectEqual(15, sel.focusOffset);
testing.expectEqual("none", sel.direction);
}
</script>
<script id=extend>
{
const sel = window.getSelection();
const p1 = document.getElementById("p1");
const textNode = p1.firstChild;
// Start with collapsed selection
sel.collapse(textNode, 10);
testing.expectEqual(true, sel.isCollapsed);
testing.expectEqual(10, sel.anchorOffset);
testing.expectEqual("none", sel.direction);
// Extend forward
sel.extend(textNode, 15);
testing.expectEqual(false, sel.isCollapsed);
testing.expectEqual(10, sel.anchorOffset);
testing.expectEqual(15, sel.focusOffset);
testing.expectEqual("forward", sel.direction);
// Extend backward from anchor
sel.extend(textNode, 5);
testing.expectEqual(false, sel.isCollapsed);
testing.expectEqual(10, sel.anchorOffset);
testing.expectEqual(5, sel.focusOffset);
testing.expectEqual("backward", sel.direction);
// Extend to same position as anchor
sel.extend(textNode, 10);
testing.expectEqual(true, sel.isCollapsed);
testing.expectEqual(10, sel.anchorOffset);
testing.expectEqual(10, sel.focusOffset);
testing.expectEqual("none", sel.direction);
}
</script>
<script id=direction>
{
const sel = window.getSelection();
const p1 = document.getElementById("p1");
const textNode = p1.firstChild;
// Forward selection
sel.collapse(textNode, 5);
sel.extend(textNode, 10);
testing.expectEqual("forward", sel.direction);
testing.expectEqual(5, sel.anchorOffset);
testing.expectEqual(10, sel.focusOffset);
// Backward selection
sel.collapse(textNode, 10);
sel.extend(textNode, 5);
testing.expectEqual("backward", sel.direction);
testing.expectEqual(10, sel.anchorOffset);
testing.expectEqual(5, sel.focusOffset);
// None (collapsed)
sel.collapse(textNode, 7);
testing.expectEqual("none", sel.direction);
}
</script>
<script id=containsNode>
{
const sel = window.getSelection();
const nested = document.getElementById("nested");
const s1 = document.getElementById("s1");
const s2 = document.getElementById("s2");
const range = document.createRange();
range.selectNodeContents(nested);
sel.removeAllRanges();
sel.addRange(range);
// Partial containment
testing.expectEqual(true, sel.containsNode(s1, true));
testing.expectEqual(true, sel.containsNode(s2, true));
testing.expectEqual(true, sel.containsNode(nested, true));
// Node outside selection
const p1 = document.getElementById("p1");
testing.expectEqual(false, sel.containsNode(p1, false));
testing.expectEqual(false, sel.containsNode(p1, true));
}
</script>
<script id=deleteFromDocument>
{
const sel = window.getSelection();
sel.removeAllRanges();
const p1 = document.getElementById("p1");
const textNode = p1.firstChild;
const originalText = textNode.textContent;
const range = document.createRange();
range.setStart(textNode, 4);
range.setEnd(textNode, 15);
sel.removeAllRanges();
sel.addRange(range);
sel.deleteFromDocument();
// Text should be deleted
const expectedText = originalText.slice(0, 4) + originalText.slice(15);
testing.expectEqual(expectedText, textNode.textContent);
// Selection should be collapsed at deletion point
testing.expectEqual(true, sel.isCollapsed);
// Restore original text for other tests
textNode.textContent = originalText;
}
</script>
<script id=typeProperty>
{
const sel = window.getSelection();
const p1 = document.getElementById("p1");
const textNode = p1.firstChild;
// None type
sel.removeAllRanges();
testing.expectEqual("None", sel.type);
// Caret type (collapsed)
sel.collapse(textNode, 5);
testing.expectEqual("Caret", sel.type);
// Range type (not collapsed)
sel.extend(textNode, 10);
testing.expectEqual("Range", sel.type);
}
</script>
<script id=selectAllChildren>
{
const sel = window.getSelection();
sel.removeAllRanges();
const nested = document.getElementById("nested");
const s1 = document.getElementById("s1");
const s2 = document.getElementById("s2");
// Select all children of nested div
sel.selectAllChildren(nested);
testing.expectEqual(1, sel.rangeCount);
testing.expectEqual("Range", sel.type);
testing.expectEqual(false, sel.isCollapsed);
// Anchor and focus should be on the parent node
testing.expectEqual(nested, sel.anchorNode);
testing.expectEqual(nested, sel.focusNode);
// Should start at offset 0 (before first child)
testing.expectEqual(0, sel.anchorOffset);
const childrenCount = nested.childNodes.length;
// Should end at offset equal to number of children (after last child)
testing.expectEqual(childrenCount, sel.focusOffset);
// Direction should be forward
testing.expectEqual("forward", sel.direction);
// Should not fully contain the parent itself
testing.expectEqual(false, sel.containsNode(nested, false));
// But should partially contain the parent
testing.expectEqual(true, sel.containsNode(nested, true));
// Verify the range
const range = sel.getRangeAt(0);
testing.expectEqual(nested, range.startContainer);
testing.expectEqual(nested, range.endContainer);
testing.expectEqual(0, range.startOffset);
testing.expectEqual(childrenCount, range.endOffset);
}
</script>
<script id=selectAllChildrenEmpty>
{
const sel = window.getSelection();
sel.removeAllRanges();
// Create an empty element
const empty = document.createElement("div");
document.body.appendChild(empty);
// Select all children of empty element
sel.selectAllChildren(empty);
testing.expectEqual(1, sel.rangeCount);
testing.expectEqual("Caret", sel.type); // Collapsed because no children
testing.expectEqual(true, sel.isCollapsed);
testing.expectEqual(empty, sel.anchorNode);
testing.expectEqual(0, sel.anchorOffset);
testing.expectEqual(0, sel.focusOffset);
// Clean up
document.body.removeChild(empty);
}
</script>
<script id=selectAllChildrenReplacesSelection>
{
const sel = window.getSelection();
sel.removeAllRanges();
// Start with an existing selection
const p1 = document.getElementById("p1");
sel.selectAllChildren(p1);
testing.expectEqual(1, sel.rangeCount);
testing.expectEqual(p1, sel.anchorNode);
// selectAllChildren should replace the existing selection
const p2 = document.getElementById("p2");
sel.selectAllChildren(p2);
testing.expectEqual(1, sel.rangeCount);
testing.expectEqual(p2, sel.anchorNode);
testing.expectEqual(p2, sel.focusNode);
// Verify old selection is gone
const range = sel.getRangeAt(0);
testing.expectEqual(p2, range.startContainer);
testing.expectEqual(false, p1 == range.startContainer);
}
</script>
<script id=setBaseAndExtent>
{
const sel = window.getSelection();
sel.removeAllRanges();
const p1 = document.getElementById("p1");
const textNode = p1.firstChild;
// Forward selection (anchor before focus)
sel.setBaseAndExtent(textNode, 4, textNode, 15);
testing.expectEqual(1, sel.rangeCount);
testing.expectEqual("Range", sel.type);
testing.expectEqual(false, sel.isCollapsed);
testing.expectEqual(textNode, sel.anchorNode);
testing.expectEqual(4, sel.anchorOffset);
testing.expectEqual(textNode, sel.focusNode);
testing.expectEqual(15, sel.focusOffset);
testing.expectEqual("forward", sel.direction);
// Backward selection (anchor after focus)
sel.setBaseAndExtent(textNode, 15, textNode, 4);
testing.expectEqual(1, sel.rangeCount);
testing.expectEqual("Range", sel.type);
testing.expectEqual(textNode, sel.anchorNode);
testing.expectEqual(15, sel.anchorOffset);
testing.expectEqual(textNode, sel.focusNode);
testing.expectEqual(4, sel.focusOffset);
testing.expectEqual("backward", sel.direction);
// Collapsed selection (anchor equals focus)
sel.setBaseAndExtent(textNode, 10, textNode, 10);
testing.expectEqual(1, sel.rangeCount);
testing.expectEqual("Caret", sel.type);
testing.expectEqual(true, sel.isCollapsed);
testing.expectEqual(10, sel.anchorOffset);
testing.expectEqual(10, sel.focusOffset);
testing.expectEqual("none", sel.direction);
// Across different nodes
const p2 = document.getElementById("p2");
const textNode2 = p2.firstChild;
sel.setBaseAndExtent(textNode, 4, textNode2, 5);
testing.expectEqual(1, sel.rangeCount);
testing.expectEqual(textNode, sel.anchorNode);
testing.expectEqual(4, sel.anchorOffset);
testing.expectEqual(textNode2, sel.focusNode);
testing.expectEqual(5, sel.focusOffset);
testing.expectEqual("forward", sel.direction);
// Should replace existing selection
sel.setBaseAndExtent(textNode, 0, textNode, 3);
testing.expectEqual(1, sel.rangeCount);
testing.expectEqual(0, sel.anchorOffset);
testing.expectEqual(3, sel.focusOffset);
}
</script>

View File

@@ -0,0 +1,211 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script id=onerrorBasicCallback>
{
let callbackCalled = false;
let receivedArgs = null;
window.onerror = function(message, source, lineno, colno, error) {
callbackCalled = true;
receivedArgs = { message, source, lineno, colno, error };
};
const err = new Error('Test error');
window.reportError(err);
testing.expectEqual(true, callbackCalled);
testing.expectEqual(true, receivedArgs.message.includes('Test error'));
testing.expectEqual(err, receivedArgs.error);
window.onerror = null;
}
</script>
<script id=onerrorFiveArguments>
{
let argCount = 0;
window.onerror = function() {
argCount = arguments.length;
};
window.reportError(new Error('Five args test'));
// Per WHATWG spec, onerror receives exactly 5 arguments
testing.expectEqual(5, argCount);
window.onerror = null;
}
</script>
<script id=onerrorReturnTrueCancelsEvent>
{
let listenerCalled = false;
window.onerror = function() {
return true; // Should cancel the event
};
const listener = function() {
listenerCalled = true;
};
window.addEventListener('error', listener);
window.reportError(new Error('Should be cancelled'));
// The event listener should still be called (onerror returning true
// only prevents default, not propagation)
testing.expectEqual(true, listenerCalled);
window.onerror = null;
window.removeEventListener('error', listener);
}
</script>
<script id=onerrorAndEventListenerBothCalled>
{
let onerrorCalled = false;
let listenerCalled = false;
window.onerror = function() {
onerrorCalled = true;
};
const listener = function() {
listenerCalled = true;
};
window.addEventListener('error', listener);
window.reportError(new Error('Both should fire'));
testing.expectEqual(true, onerrorCalled);
testing.expectEqual(true, listenerCalled);
window.onerror = null;
window.removeEventListener('error', listener);
}
</script>
<script id=onerrorCalledBeforeEventListener>
{
let callOrder = [];
window.onerror = function() {
callOrder.push('onerror');
};
const listener = function() {
callOrder.push('listener');
};
window.addEventListener('error', listener);
window.reportError(new Error('Order test'));
// onerror should be called before addEventListener handlers
testing.expectEqual('onerror', callOrder[0]);
testing.expectEqual('listener', callOrder[1]);
window.onerror = null;
window.removeEventListener('error', listener);
}
</script>
<script id=onerrorGetterSetter>
{
const handler = function() {};
testing.expectEqual(null, window.onerror);
window.onerror = handler;
testing.expectEqual(handler, window.onerror);
window.onerror = null;
testing.expectEqual(null, window.onerror);
}
</script>
<script id=onerrorWithNonFunction>
{
// Setting onerror to a non-function should not throw
// but should not be stored as the handler
window.onerror = "not a function";
testing.expectEqual(null, window.onerror);
window.onerror = {};
testing.expectEqual(null, window.onerror);
window.onerror = 123;
testing.expectEqual(null, window.onerror);
}
</script>
<script id=onerrorArgumentTypes>
{
let receivedTypes = null;
window.onerror = function(message, source, lineno, colno, error) {
receivedTypes = {
message: typeof message,
source: typeof source,
lineno: typeof lineno,
colno: typeof colno,
error: typeof error
};
};
window.reportError(new Error('Type check'));
testing.expectEqual('string', receivedTypes.message);
testing.expectEqual('string', receivedTypes.source);
testing.expectEqual('number', receivedTypes.lineno);
testing.expectEqual('number', receivedTypes.colno);
testing.expectEqual('object', receivedTypes.error);
window.onerror = null;
}
</script>
<script id=onerrorReturnFalseDoesNotCancel>
{
let eventDefaultPrevented = false;
window.onerror = function() {
return false; // Should NOT cancel the event
};
const listener = function(e) {
eventDefaultPrevented = e.defaultPrevented;
};
window.addEventListener('error', listener);
window.reportError(new Error('Return false test'));
testing.expectEqual(false, eventDefaultPrevented);
window.onerror = null;
window.removeEventListener('error', listener);
}
</script>
<script id=onerrorReturnTruePreventsDefault>
{
let eventDefaultPrevented = false;
window.onerror = function() {
return true; // Should cancel (prevent default)
};
const listener = function(e) {
eventDefaultPrevented = e.defaultPrevented;
};
window.addEventListener('error', listener);
window.reportError(new Error('Return true test'));
testing.expectEqual(true, eventDefaultPrevented);
window.onerror = null;
window.removeEventListener('error', listener);
}
</script>

View File

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

View File

@@ -57,7 +57,7 @@ pub fn asEventTarget(self: *AbortSignal) *EventTarget {
return self._proto;
}
pub fn abort(self: *AbortSignal, reason_: ?Reason, page: *Page) !void {
pub fn abort(self: *AbortSignal, reason_: ?Reason, local: *const js.Local, page: *Page) !void {
if (self._aborted) {
return;
}
@@ -77,11 +77,10 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, page: *Page) !void {
// Dispatch abort event
const event = try Event.initTrusted("abort", .{}, page);
const func = if (self._on_abort) |*g| g.local() else null;
try page._event_manager.dispatchWithFunction(
self.asEventTarget(),
event,
func,
local.toLocal(self._on_abort),
.{ .context = "abort signal" },
);
}
@@ -89,7 +88,7 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, page: *Page) !void {
// Static method to create an already-aborted signal
pub fn createAborted(reason_: ?js.Value.Global, page: *Page) !*AbortSignal {
const signal = try init(page);
try signal.abort(if (reason_) |r| .{ .js_val = r } else null, page);
try signal.abort(if (reason_) |r| .{ .js_val = r } else null, page.js.local.?, page);
return signal;
}
@@ -112,11 +111,13 @@ const ThrowIfAborted = union(enum) {
undefined: void,
};
pub fn throwIfAborted(self: *const AbortSignal, page: *Page) !ThrowIfAborted {
const local = page.js.local.?;
if (self._aborted) {
const exception = switch (self._reason) {
.string => |str| page.js.throw(str),
.js_val => |js_val| page.js.throw(try js_val.local().toString(.{ .allocator = page.call_arena })),
.undefined => page.js.throw("AbortError"),
.string => |str| local.throw(str),
.js_val => |js_val| local.throw(try local.toLocal(js_val).toString(.{ .allocator = page.call_arena })),
.undefined => local.throw("AbortError"),
};
return .{ .exception = exception };
}
@@ -135,7 +136,11 @@ const TimeoutCallback = struct {
fn run(ctx: *anyopaque) !?u32 {
const self: *TimeoutCallback = @ptrCast(@alignCast(ctx));
self.signal.abort(.{ .string = "TimeoutError" }, self.page) catch |err| {
var ls: js.Local.Scope = undefined;
self.page.js.localScope(&ls);
defer ls.deinit();
self.signal.abort(.{ .string = "TimeoutError" }, &ls.local, self.page) catch |err| {
log.warn(.app, "abort signal timeout", .{ .err = err });
};
return null;

View File

@@ -206,7 +206,7 @@ fn writeBlobParts(
/// Returns a Promise that resolves with the contents of the blob
/// as binary data contained in an ArrayBuffer.
pub fn arrayBuffer(self: *const Blob, page: *Page) !js.Promise {
return page.js.resolvePromise(js.ArrayBuffer{ .values = self._slice });
return page.js.local.?.resolvePromise(js.ArrayBuffer{ .values = self._slice });
}
const ReadableStream = @import("streams/ReadableStream.zig");
@@ -219,7 +219,7 @@ pub fn stream(self: *const Blob, page: *Page) !*ReadableStream {
/// Returns a Promise that resolves with a string containing
/// the contents of the blob, interpreted as UTF-8.
pub fn text(self: *const Blob, page: *Page) !js.Promise {
return page.js.resolvePromise(self._slice);
return page.js.local.?.resolvePromise(self._slice);
}
/// Extension to Blob; works on Firefox and Safari.
@@ -227,7 +227,7 @@ pub fn text(self: *const Blob, page: *Page) !js.Promise {
/// Returns a Promise that resolves with a Uint8Array containing
/// the contents of the blob as an array of bytes.
pub fn bytes(self: *const Blob, page: *Page) !js.Promise {
return page.js.resolvePromise(js.TypedArray(u8){ .values = self._slice });
return page.js.local.?.resolvePromise(js.TypedArray(u8){ .values = self._slice });
}
/// Returns a new Blob object which contains data

View File

@@ -31,7 +31,7 @@ pub const init: Console = .{};
pub fn trace(_: *const Console, values: []js.Value, page: *Page) !void {
logger.debug(.js, "console.trace", .{
.stack = page.js.stackTrace() catch "???",
.stack = page.js.local.?.stackTrace() catch "???",
.args = ValueWriter{ .page = page, .values = values },
});
}
@@ -138,7 +138,7 @@ const ValueWriter = struct {
try writer.print("\n arg({d}): {f}", .{ i, value });
}
if (self.include_stack) {
try writer.print("\n stack: {s}", .{self.page.js.stackTrace() catch |err| @errorName(err) orelse "???"});
try writer.print("\n stack: {s}", .{self.page.js.local.?.stackTrace() catch |err| @errorName(err) orelse "???"});
}
}

View File

@@ -17,6 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const String = @import("../../string.zig").String;
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Element = @import("Element.zig");
@@ -25,13 +27,16 @@ const CustomElementDefinition = @This();
name: []const u8,
constructor: js.Function.Global,
// TODO: Make this a Map<String>
observed_attributes: std.StringHashMapUnmanaged(void) = .{},
// For customized built-in elements, this is the element tag they extend (e.g., .button)
// For autonomous custom elements, this is null
extends: ?Element.Tag = null,
pub fn isAttributeObserved(self: *const CustomElementDefinition, name: []const u8) bool {
return self.observed_attributes.contains(name);
pub fn isAttributeObserved(self: *const CustomElementDefinition, name: String) bool {
return self.observed_attributes.contains(name.str());
}
pub fn isAutonomous(self: *const CustomElementDefinition) bool {

View File

@@ -106,7 +106,7 @@ pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Fu
}
if (self._when_defined.fetchRemove(name)) |entry| {
entry.value.local().resolve("whenDefined", constructor);
page.js.toLocal(entry.value).resolve("whenDefined", constructor);
}
}
@@ -120,22 +120,23 @@ pub fn upgrade(self: *CustomElementRegistry, root: *Node, page: *Page) !void {
}
pub fn whenDefined(self: *CustomElementRegistry, name: []const u8, page: *Page) !js.Promise {
const local = page.js.local.?;
if (self._definitions.get(name)) |definition| {
return page.js.resolvePromise(definition.constructor);
return local.resolvePromise(definition.constructor);
}
const gop = try self._when_defined.getOrPut(page.arena, name);
if (gop.found_existing) {
return gop.value_ptr.local().promise();
return local.toLocal(gop.value_ptr.*).promise();
}
errdefer _ = self._when_defined.remove(name);
const owned_name = try page.dupeString(name);
const resolver = try page.js.createPromiseResolver().persist();
const resolver = local.createPromiseResolver();
gop.key_ptr.* = owned_name;
gop.value_ptr.* = resolver;
gop.value_ptr.* = try resolver.persist();
return resolver.local().promise();
return resolver.promise();
}
fn upgradeNode(self: *CustomElementRegistry, node: *Node, page: *Page) !void {
@@ -174,8 +175,12 @@ pub fn upgradeCustomElement(custom: *Custom, definition: *CustomElementDefinitio
page._upgrading_element = node;
defer page._upgrading_element = prev_upgrading;
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
var caught: js.TryCatch.Caught = undefined;
_ = definition.constructor.local().newInstance(&caught) catch |err| {
_ = ls.toLocal(definition.constructor).newInstance(&caught) catch |err| {
log.warn(.js, "custom element upgrade", .{ .name = definition.name, .err = err, .caught = caught });
return error.CustomElementUpgradeFailed;
};
@@ -183,9 +188,9 @@ pub fn upgradeCustomElement(custom: *Custom, definition: *CustomElementDefinitio
// Invoke attributeChangedCallback for existing observed attributes
var attr_it = custom.asElement().attributeIterator();
while (attr_it.next()) |attr| {
const name = attr._name.str();
const name = attr._name;
if (definition.isAttributeObserved(name)) {
custom.invokeAttributeChangedCallback(name, null, attr._value.str(), page);
custom.invokeAttributeChangedCallback(name, null, attr._value, page);
}
}

View File

@@ -63,14 +63,14 @@ pub fn getFilter(self: *const DOMNodeIterator) ?FilterOpts {
return self._filter._original_filter;
}
pub fn nextNode(self: *DOMNodeIterator) !?*Node {
pub fn nextNode(self: *DOMNodeIterator, page: *Page) !?*Node {
var node = self._reference_node;
var before_node = self._pointer_before_reference_node;
while (true) {
if (before_node) {
before_node = false;
const result = try self.filterNode(node);
const result = try self.filterNode(node, page);
if (result == NodeFilter.FILTER_ACCEPT) {
self._reference_node = node;
self._pointer_before_reference_node = false;
@@ -84,7 +84,7 @@ pub fn nextNode(self: *DOMNodeIterator) !?*Node {
}
node = next.?;
const result = try self.filterNode(node);
const result = try self.filterNode(node, page);
if (result == NodeFilter.FILTER_ACCEPT) {
self._reference_node = node;
self._pointer_before_reference_node = false;
@@ -94,13 +94,13 @@ pub fn nextNode(self: *DOMNodeIterator) !?*Node {
}
}
pub fn previousNode(self: *DOMNodeIterator) !?*Node {
pub fn previousNode(self: *DOMNodeIterator, page: *Page) !?*Node {
var node = self._reference_node;
var before_node = self._pointer_before_reference_node;
while (true) {
if (!before_node) {
const result = try self.filterNode(node);
const result = try self.filterNode(node, page);
if (result == NodeFilter.FILTER_ACCEPT) {
self._reference_node = node;
self._pointer_before_reference_node = true;
@@ -119,7 +119,7 @@ pub fn previousNode(self: *DOMNodeIterator) !?*Node {
}
}
fn filterNode(self: *const DOMNodeIterator, node: *Node) !i32 {
fn filterNode(self: *const DOMNodeIterator, node: *Node, page: *Page) !i32 {
// First check whatToShow
if (!NodeFilter.shouldShow(node, self._what_to_show)) {
return NodeFilter.FILTER_SKIP;
@@ -128,7 +128,7 @@ fn filterNode(self: *const DOMNodeIterator, node: *Node) !i32 {
// Then check the filter callback
// For NodeIterator, REJECT and SKIP are equivalent - both skip the node
// but continue with its descendants
const result = try self._filter.acceptNode(node);
const result = try self._filter.acceptNode(node, page.js.local.?);
return result;
}

View File

@@ -48,6 +48,9 @@ pub fn parseFromString(
@"image/svg+xml",
}, mime_type) orelse return error.NotSupported;
const arena = try page.getArena(.{ .debug = "DOMParser.parseFromString" });
defer page.releaseArena(arena);
return switch (target_mime) {
.@"text/html" => {
// Create a new HTMLDocument
@@ -61,7 +64,7 @@ pub fn parseFromString(
}
// Parse HTML into the document
var parser = Parser.init(page.arena, doc.asNode(), page);
var parser = Parser.init(arena, doc.asNode(), page);
parser.parse(normalized);
if (parser.err) |pe| {
@@ -78,7 +81,7 @@ pub fn parseFromString(
// Parse XML into XMLDocument.
const doc_node = doc.asNode();
var parser = Parser.init(page.arena, doc_node, page);
var parser = Parser.init(arena, doc_node, page);
parser.parseXML(html);
if (parser.err) |pe| {

View File

@@ -62,13 +62,13 @@ pub fn setCurrentNode(self: *DOMTreeWalker, node: *Node) void {
}
// Navigation methods
pub fn parentNode(self: *DOMTreeWalker) !?*Node {
pub fn parentNode(self: *DOMTreeWalker, page: *Page) !?*Node {
var node = self._current._parent;
while (node) |n| {
if (n == self._root._parent) {
return null;
}
if (try self.acceptNode(n) == NodeFilter.FILTER_ACCEPT) {
if (try self.acceptNode(n, page) == NodeFilter.FILTER_ACCEPT) {
self._current = n;
return n;
}
@@ -77,11 +77,11 @@ pub fn parentNode(self: *DOMTreeWalker) !?*Node {
return null;
}
pub fn firstChild(self: *DOMTreeWalker) !?*Node {
pub fn firstChild(self: *DOMTreeWalker, page: *Page) !?*Node {
var node = self._current.firstChild();
while (node) |n| {
const filter_result = try self.acceptNode(n);
const filter_result = try self.acceptNode(n, page);
if (filter_result == NodeFilter.FILTER_ACCEPT) {
self._current = n;
@@ -117,11 +117,11 @@ pub fn firstChild(self: *DOMTreeWalker) !?*Node {
return null;
}
pub fn lastChild(self: *DOMTreeWalker) !?*Node {
pub fn lastChild(self: *DOMTreeWalker, page: *Page) !?*Node {
var node = self._current.lastChild();
while (node) |n| {
const filter_result = try self.acceptNode(n);
const filter_result = try self.acceptNode(n, page);
if (filter_result == NodeFilter.FILTER_ACCEPT) {
self._current = n;
@@ -157,10 +157,10 @@ pub fn lastChild(self: *DOMTreeWalker) !?*Node {
return null;
}
pub fn previousSibling(self: *DOMTreeWalker) !?*Node {
pub fn previousSibling(self: *DOMTreeWalker, page: *Page) !?*Node {
var node = self.previousSiblingOrNull(self._current);
while (node) |n| {
if (try self.acceptNode(n) == NodeFilter.FILTER_ACCEPT) {
if (try self.acceptNode(n, page) == NodeFilter.FILTER_ACCEPT) {
self._current = n;
return n;
}
@@ -169,10 +169,10 @@ pub fn previousSibling(self: *DOMTreeWalker) !?*Node {
return null;
}
pub fn nextSibling(self: *DOMTreeWalker) !?*Node {
pub fn nextSibling(self: *DOMTreeWalker, page: *Page) !?*Node {
var node = self.nextSiblingOrNull(self._current);
while (node) |n| {
if (try self.acceptNode(n) == NodeFilter.FILTER_ACCEPT) {
if (try self.acceptNode(n, page) == NodeFilter.FILTER_ACCEPT) {
self._current = n;
return n;
}
@@ -181,7 +181,7 @@ pub fn nextSibling(self: *DOMTreeWalker) !?*Node {
return null;
}
pub fn previousNode(self: *DOMTreeWalker) !?*Node {
pub fn previousNode(self: *DOMTreeWalker, page: *Page) !?*Node {
var node = self._current;
while (node != self._root) {
var sibling = self.previousSiblingOrNull(node);
@@ -189,7 +189,7 @@ pub fn previousNode(self: *DOMTreeWalker) !?*Node {
node = sib;
// Check if this sibling is rejected before descending into it
const sib_result = try self.acceptNode(node);
const sib_result = try self.acceptNode(node, page);
if (sib_result == NodeFilter.FILTER_REJECT) {
// Skip this sibling and its descendants entirely
sibling = self.previousSiblingOrNull(node);
@@ -204,7 +204,7 @@ pub fn previousNode(self: *DOMTreeWalker) !?*Node {
while (child) |c| {
if (!self.isInSubtree(c)) break;
const filter_result = try self.acceptNode(c);
const filter_result = try self.acceptNode(c, page);
if (filter_result == NodeFilter.FILTER_REJECT) {
// Skip this child and try its previous sibling
child = self.previousSiblingOrNull(c);
@@ -220,7 +220,7 @@ pub fn previousNode(self: *DOMTreeWalker) !?*Node {
node = child.?;
}
if (try self.acceptNode(node) == NodeFilter.FILTER_ACCEPT) {
if (try self.acceptNode(node, page) == NodeFilter.FILTER_ACCEPT) {
self._current = node;
return node;
}
@@ -232,7 +232,7 @@ pub fn previousNode(self: *DOMTreeWalker) !?*Node {
}
const parent = node._parent orelse return null;
if (try self.acceptNode(parent) == NodeFilter.FILTER_ACCEPT) {
if (try self.acceptNode(parent, page) == NodeFilter.FILTER_ACCEPT) {
self._current = parent;
return parent;
}
@@ -241,14 +241,14 @@ pub fn previousNode(self: *DOMTreeWalker) !?*Node {
return null;
}
pub fn nextNode(self: *DOMTreeWalker) !?*Node {
pub fn nextNode(self: *DOMTreeWalker, page: *Page) !?*Node {
var node = self._current;
while (true) {
// Try children first (depth-first)
if (node.firstChild()) |child| {
node = child;
const filter_result = try self.acceptNode(node);
const filter_result = try self.acceptNode(node, page);
if (filter_result == NodeFilter.FILTER_ACCEPT) {
self._current = node;
return node;
@@ -271,7 +271,7 @@ pub fn nextNode(self: *DOMTreeWalker) !?*Node {
if (node.nextSibling()) |sibling| {
node = sibling;
const filter_result = try self.acceptNode(node);
const filter_result = try self.acceptNode(node, page);
if (filter_result == NodeFilter.FILTER_ACCEPT) {
self._current = node;
return node;
@@ -293,7 +293,7 @@ pub fn nextNode(self: *DOMTreeWalker) !?*Node {
}
// Helper methods
fn acceptNode(self: *const DOMTreeWalker, node: *Node) !i32 {
fn acceptNode(self: *const DOMTreeWalker, node: *Node, page: *Page) !i32 {
// First check whatToShow
if (!NodeFilter.shouldShow(node, self._what_to_show)) {
return NodeFilter.FILTER_SKIP;
@@ -303,7 +303,7 @@ fn acceptNode(self: *const DOMTreeWalker, node: *Node) !i32 {
// For TreeWalker, REJECT means reject node and its descendants
// SKIP means skip node but check its descendants
// ACCEPT means accept the node
return try self._filter.acceptNode(node);
return try self._filter.acceptNode(node, page.js.local.?);
}
fn isInSubtree(self: *const DOMTreeWalker, node: *Node) bool {

View File

@@ -36,6 +36,7 @@ const DOMTreeWalker = @import("DOMTreeWalker.zig");
const DOMNodeIterator = @import("DOMNodeIterator.zig");
const DOMImplementation = @import("DOMImplementation.zig");
const StyleSheetList = @import("css/StyleSheetList.zig");
const Selection = @import("Selection.zig");
pub const XMLDocument = @import("XMLDocument.zig");
pub const HTMLDocument = @import("HTMLDocument.zig");
@@ -55,6 +56,7 @@ _style_sheets: ?*StyleSheetList = null,
_write_insertion_point: ?*Node = null,
_script_created_parser: ?Parser.Streaming = null,
_adopted_style_sheets: ?js.Object.Global = null,
_selection: Selection = .init,
pub const Type = union(enum) {
generic,
@@ -142,7 +144,7 @@ pub fn createElement(self: *Document, name: []const u8, options_: ?CreateElement
const options = options_ orelse return element;
if (options.is) |is_value| {
try element.setAttribute("is", is_value, page);
try element.setAttribute(comptime .wrap("is"), .wrap(is_value), page);
try Element.Html.Custom.checkAndAttachBuiltIn(element, page);
}
@@ -160,26 +162,26 @@ pub fn createElementNS(self: *Document, namespace: ?[]const u8, name: []const u8
return node.as(Element);
}
pub fn createAttribute(_: *const Document, name: []const u8, page: *Page) !?*Element.Attribute {
try Element.Attribute.validateAttributeName(name);
pub fn createAttribute(_: *const Document, name: String.Global, page: *Page) !?*Element.Attribute {
try Element.Attribute.validateAttributeName(name.str);
return page._factory.node(Element.Attribute{
._proto = undefined,
._name = try page.dupeString(name),
._value = "",
._name = name.str,
._value = String.empty,
._element = null,
});
}
pub fn createAttributeNS(_: *const Document, namespace: []const u8, name: []const u8, page: *Page) !?*Element.Attribute {
pub fn createAttributeNS(_: *const Document, namespace: []const u8, name: String.Global, page: *Page) !?*Element.Attribute {
if (std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml") == false) {
log.warn(.not_implemented, "document.createAttributeNS", .{ .namespace = namespace });
}
try Element.Attribute.validateAttributeName(name);
try Element.Attribute.validateAttributeName(name.str);
return page._factory.node(Element.Attribute{
._proto = undefined,
._name = try page.dupeString(name),
._value = "",
._name = name.str,
._value = String.empty,
._element = null,
});
}
@@ -197,7 +199,7 @@ pub fn getElementById(self: *Document, id: []const u8, page: *Page) ?*Element {
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;
const element_id = el.getAttributeSafe(comptime .wrap("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
@@ -276,12 +278,16 @@ pub fn getDocumentElement(self: *Document) ?*Element {
return null;
}
pub fn querySelector(self: *Document, input: []const u8, page: *Page) !?*Element {
return Selector.querySelector(self.asNode(), input, page);
pub fn getSelection(self: *Document) *Selection {
return &self._selection;
}
pub fn querySelectorAll(self: *Document, input: []const u8, page: *Page) !*Selector.List {
return Selector.querySelectorAll(self.asNode(), input, page);
pub fn querySelector(self: *Document, input: String, page: *Page) !?*Element {
return Selector.querySelector(self.asNode(), input.str(), page);
}
pub fn querySelectorAll(self: *Document, input: String, page: *Page) !*Selector.List {
return Selector.querySelectorAll(self.asNode(), input.str(), page);
}
pub fn getImplementation(_: *const Document) DOMImplementation {
@@ -642,7 +648,10 @@ pub fn write(self: *Document, text: []const []const u8, page: *Page) !void {
page._parse_mode = .document_write;
defer page._parse_mode = previous_parse_mode;
var parser = Parser.init(page.call_arena, fragment_node, page);
const arena = try page.getArena(.{ .debug = "Document.write" });
defer page.releaseArena(arena);
var parser = Parser.init(arena, fragment_node, page);
parser.parseFragment(html);
// Extract children from wrapper HTML element (html5ever wraps fragments)
@@ -655,7 +664,7 @@ pub fn write(self: *Document, text: []const []const u8, page: *Page) !void {
var it = if (first.is(Element.Html.Html) == null) fragment_node.childrenIterator() else first.childrenIterator();
while (it.next()) |child| {
try children_to_insert.append(page.call_arena, child);
try children_to_insert.append(arena, child);
}
if (children_to_insert.items.len == 0) {
@@ -770,7 +779,7 @@ pub fn getAdoptedStyleSheets(self: *Document, page: *Page) !js.Object.Global {
if (self._adopted_style_sheets) |ass| {
return ass;
}
const js_arr = page.js.newArray(0);
const js_arr = page.js.local.?.newArray(0);
const js_obj = js_arr.toObject();
self._adopted_style_sheets = try js_obj.persist();
return self._adopted_style_sheets.?;
@@ -962,6 +971,7 @@ pub const JsApi = struct {
pub const querySelector = bridge.function(Document.querySelector, .{ .dom_exception = true });
pub const querySelectorAll = bridge.function(Document.querySelectorAll, .{ .dom_exception = true });
pub const getElementsByTagName = bridge.function(Document.getElementsByTagName, .{});
pub const getSelection = bridge.function(Document.getSelection, .{});
pub const getElementsByClassName = bridge.function(Document.getElementsByClassName, .{});
pub const getElementsByName = bridge.function(Document.getElementsByName, .{});
pub const adoptNode = bridge.function(Document.adoptNode, .{ .dom_exception = true });

View File

@@ -74,7 +74,7 @@ pub fn getElementById(self: *DocumentFragment, id: []const u8) ?*Element {
var tw = @import("TreeWalker.zig").Full.Elements.init(self.asNode(), .{});
while (tw.next()) |el| {
if (el.getAttributeSafe("id")) |element_id| {
if (el.getAttributeSafe(comptime .wrap("id"))) |element_id| {
if (std.mem.eql(u8, element_id, id)) {
return el;
}

View File

@@ -49,6 +49,129 @@ pub const RelListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTok
pub const ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot);
pub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Element, *Html.Slot);
/// Better to discriminate it since not directly a pointer int.
///
/// See `calcAttrListenerKey` to obtain one.
const AttrListenerKey = u64;
/// Use `getAttrListenerKey` to create a key.
pub const AttrListenerLookup = std.AutoHashMapUnmanaged(AttrListenerKey, js.Function.Global);
/// Enum of known event listeners; increasing the size of it (u7)
/// can cause `AttrListenerKey` to behave incorrectly.
pub const KnownListener = enum(u7) {
abort,
animationcancel,
animationend,
animationiteration,
animationstart,
auxclick,
beforeinput,
beforematch,
beforetoggle,
blur,
cancel,
canplay,
canplaythrough,
change,
click,
close,
command,
contentvisibilityautostatechange,
contextlost,
contextmenu,
contextrestored,
copy,
cuechange,
cut,
dblclick,
drag,
dragend,
dragenter,
dragexit,
dragleave,
dragover,
dragstart,
drop,
durationchange,
emptied,
ended,
@"error",
focus,
formdata,
fullscreenchange,
fullscreenerror,
gotpointercapture,
input,
invalid,
keydown,
keypress,
keyup,
load,
loadeddata,
loadedmetadata,
loadstart,
lostpointercapture,
mousedown,
mousemove,
mouseout,
mouseover,
mouseup,
paste,
pause,
play,
playing,
pointercancel,
pointerdown,
pointerenter,
pointerleave,
pointermove,
pointerout,
pointerover,
pointerrawupdate,
pointerup,
progress,
ratechange,
reset,
resize,
scroll,
scrollend,
securitypolicyviolation,
seeked,
seeking,
select,
selectionchange,
selectstart,
slotchange,
stalled,
submit,
@"suspend",
timeupdate,
toggle,
transitioncancel,
transitionend,
transitionrun,
transitionstart,
volumechange,
waiting,
wheel,
};
/// Calculates a lookup key (`AttrListenerKey`) to use with `AttrListenerLookup` for an element.
/// NEVER use generated key to retrieve a pointer back. Portability is not guaranteed.
pub fn calcAttrListenerKey(self: *Element, event_type: KnownListener) AttrListenerKey {
// We can use `Element` for the key too; `EventTarget` is strict about
// its size and alignment, though.
const target = self.asEventTarget();
// Check if we have 3 bits available from alignment of 8.
lp.assert(@alignOf(@TypeOf(target)) == 8, "createLookupKey: incorrect alignment", .{
.event_target_alignment = @alignOf(@TypeOf(target)),
});
const ptr = @intFromPtr(target) >> 3;
lp.assert(ptr < (1 << 57), "createLookupKey: pointer overflow", .{ .ptr = ptr });
return ptr | (@as(u64, @intFromEnum(event_type)) << 57);
}
pub const Namespace = enum(u8) {
html,
svg,
@@ -427,35 +550,35 @@ pub fn setInnerHTML(self: *Element, html: []const u8, page: *Page) !void {
}
pub fn getId(self: *const Element) []const u8 {
return self.getAttributeSafe("id") orelse "";
return self.getAttributeSafe(comptime .wrap("id")) orelse "";
}
pub fn setId(self: *Element, value: []const u8, page: *Page) !void {
return self.setAttributeSafe("id", value, page);
return self.setAttributeSafe(comptime .wrap("id"), .wrap(value), page);
}
pub fn getSlot(self: *const Element) []const u8 {
return self.getAttributeSafe("slot") orelse "";
return self.getAttributeSafe(comptime .wrap("slot")) orelse "";
}
pub fn setSlot(self: *Element, value: []const u8, page: *Page) !void {
return self.setAttributeSafe("slot", value, page);
return self.setAttributeSafe(comptime .wrap("slot"), .wrap(value), page);
}
pub fn getDir(self: *const Element) []const u8 {
return self.getAttributeSafe("dir") orelse "";
return self.getAttributeSafe(comptime .wrap("dir")) orelse "";
}
pub fn setDir(self: *Element, value: []const u8, page: *Page) !void {
return self.setAttributeSafe("dir", value, page);
return self.setAttributeSafe(comptime .wrap("dir"), .wrap(value), page);
}
pub fn getClassName(self: *const Element) []const u8 {
return self.getAttributeSafe("class") orelse "";
return self.getAttributeSafe(comptime .wrap("class")) orelse "";
}
pub fn setClassName(self: *Element, value: []const u8, page: *Page) !void {
return self.setAttributeSafe("class", value, page);
return self.setAttributeSafe(comptime .wrap("class"), .wrap(value), page);
}
pub fn attributeIterator(self: *Element) Attribute.InnerIterator {
@@ -463,7 +586,7 @@ pub fn attributeIterator(self: *Element) Attribute.InnerIterator {
return attributes.iterator();
}
pub fn getAttribute(self: *const Element, name: []const u8, page: *Page) !?[]const u8 {
pub fn getAttribute(self: *const Element, name: String, page: *Page) !?String {
const attributes = self._attributes orelse return null;
return attributes.get(name, page);
}
@@ -472,9 +595,9 @@ pub fn getAttribute(self: *const Element, name: []const u8, page: *Page) !?[]con
pub fn getAttributeNS(
self: *const Element,
maybe_namespace: ?[]const u8,
local_name: []const u8,
local_name: String,
page: *Page,
) !?[]const u8 {
) !?String {
if (maybe_namespace) |namespace| {
if (!std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml")) {
log.warn(.not_implemented, "Element.getAttributeNS", .{ .namespace = namespace });
@@ -484,18 +607,18 @@ pub fn getAttributeNS(
return self.getAttribute(local_name, page);
}
pub fn getAttributeSafe(self: *const Element, name: []const u8) ?[]const u8 {
pub fn getAttributeSafe(self: *const Element, name: String) ?[]const u8 {
const attributes = self._attributes orelse return null;
return attributes.getSafe(name);
}
pub fn hasAttribute(self: *const Element, name: []const u8, page: *Page) !bool {
pub fn hasAttribute(self: *const Element, name: String, page: *Page) !bool {
const attributes = self._attributes orelse return false;
const value = try attributes.get(name, page);
return value != null;
}
pub fn hasAttributeSafe(self: *const Element, name: []const u8) bool {
pub fn hasAttributeSafe(self: *const Element, name: String) bool {
const attributes = self._attributes orelse return false;
return attributes.hasSafe(name);
}
@@ -505,12 +628,12 @@ pub fn hasAttributes(self: *const Element) bool {
return attributes.isEmpty() == false;
}
pub fn getAttributeNode(self: *Element, name: []const u8, page: *Page) !?*Attribute {
pub fn getAttributeNode(self: *Element, name: String, page: *Page) !?*Attribute {
const attributes = self._attributes orelse return null;
return attributes.getAttribute(name, self, page);
}
pub fn setAttribute(self: *Element, name: []const u8, value: []const u8, page: *Page) !void {
pub fn setAttribute(self: *Element, name: String, value: String, page: *Page) !void {
try Attribute.validateAttributeName(name);
const attributes = try self.getOrCreateAttributeList(page);
_ = try attributes.put(name, value, self, page);
@@ -533,10 +656,10 @@ pub fn setAttributeNS(
qualified_name[idx + 1 ..]
else
qualified_name;
return self.setAttribute(local_name, value, page);
return self.setAttribute(.wrap(local_name), .wrap(value), page);
}
pub fn setAttributeSafe(self: *Element, name: []const u8, value: []const u8, page: *Page) !void {
pub fn setAttributeSafe(self: *Element, name: String, value: String, page: *Page) !void {
const attributes = try self.getOrCreateAttributeList(page);
_ = try attributes.putSafe(name, value, self, page);
}
@@ -607,19 +730,19 @@ pub fn setAttributeNode(self: *Element, attr: *Attribute, page: *Page) !?*Attrib
return attributes.putAttribute(attr, self, page);
}
pub fn removeAttribute(self: *Element, name: []const u8, page: *Page) !void {
pub fn removeAttribute(self: *Element, name: String, page: *Page) !void {
const attributes = self._attributes orelse return;
return attributes.delete(name, self, page);
}
pub fn toggleAttribute(self: *Element, name: []const u8, force: ?bool, page: *Page) !bool {
pub fn toggleAttribute(self: *Element, name: String, force: ?bool, page: *Page) !bool {
try Attribute.validateAttributeName(name);
const has = try self.hasAttribute(name, page);
const should_add = force orelse !has;
if (should_add and !has) {
try self.setAttribute(name, "", page);
try self.setAttribute(name, String.empty, page);
return true;
} else if (!should_add and has) {
try self.removeAttribute(name, page);
@@ -666,7 +789,7 @@ pub fn getClassList(self: *Element, page: *Page) !*collections.DOMTokenList {
if (!gop.found_existing) {
gop.value_ptr.* = try page._factory.create(collections.DOMTokenList{
._element = self,
._attribute_name = "class",
._attribute_name = comptime .wrap("class"),
});
}
return gop.value_ptr.*;
@@ -677,7 +800,7 @@ pub fn getRelList(self: *Element, page: *Page) !*collections.DOMTokenList {
if (!gop.found_existing) {
gop.value_ptr.* = try page._factory.create(collections.DOMTokenList{
._element = self,
._attribute_name = "rel",
._attribute_name = comptime .wrap("rel"),
});
}
return gop.value_ptr.*;
@@ -919,10 +1042,10 @@ fn getElementDimensions(self: *Element, page: *Page) !struct { width: f64, heigh
if (width == 5.0) width = 1920.0;
if (height == 5.0) height = 100_000_000.0;
} else if (tag == .img or tag == .iframe) {
if (self.getAttributeSafe("width")) |w| {
if (self.getAttributeSafe(comptime .wrap("width"))) |w| {
width = std.fmt.parseFloat(f64, w) catch width;
}
if (self.getAttributeSafe("height")) |h| {
if (self.getAttributeSafe(comptime .wrap("height"))) |h| {
height = std.fmt.parseFloat(f64, h) catch height;
}
}

View File

@@ -261,7 +261,7 @@ pub const JsApi = struct {
pub const applets = bridge.accessor(HTMLDocument.getApplets, null, .{});
pub const plugins = bridge.accessor(HTMLDocument.getEmbeds, null, .{});
pub const currentScript = bridge.accessor(HTMLDocument.getCurrentScript, null, .{});
pub const location = bridge.accessor(HTMLDocument.getLocation, HTMLDocument.setLocation, .{ .cache = "location" });
pub const location = bridge.accessor(HTMLDocument.getLocation, HTMLDocument.setLocation, .{});
pub const all = bridge.accessor(HTMLDocument.getAll, null, .{});
pub const cookie = bridge.accessor(HTMLDocument.getCookie, HTMLDocument.setCookie, .{});
pub const doctype = bridge.accessor(HTMLDocument.getDocType, null, .{});

View File

@@ -34,7 +34,7 @@ pub fn getLength(_: *const History, page: *Page) u32 {
pub fn getState(_: *const History, page: *Page) !?js.Value {
if (page._session.navigation.getCurrentEntry()._state.value) |state| {
const value = try page.js.parseJSON(state);
const value = try page.js.local.?.parseJSON(state);
return value;
} else return null;
}
@@ -81,11 +81,10 @@ fn goInner(delta: i32, page: *Page) !void {
if (try page.isSameOrigin(url)) {
const event = try PopStateEvent.initTrusted("popstate", .{ .state = entry._state.value }, page);
const func = if (page.window._on_popstate) |*g| g.local() else null;
try page._event_manager.dispatchWithFunction(
page.window.asEventTarget(),
event.asEvent(),
func,
page.js.toLocal(page.window._on_popstate),
.{ .context = "Pop State" },
);
}

View File

@@ -246,7 +246,12 @@ pub fn deliverEntries(self: *IntersectionObserver, page: *Page) !void {
const entries = try self.takeRecords(page);
var caught: js.TryCatch.Caught = undefined;
self._callback.local().tryCall(void, .{ entries, self }, &caught) catch |err| {
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
ls.toLocal(self._callback).tryCall(void, .{ entries, self }, &caught) catch |err| {
log.err(.page, "IntsctObserver.deliverEntries", .{ .err = err, .caught = caught });
return err;
};

View File

@@ -116,6 +116,7 @@ const PostMessageCallback = struct {
fn run(ctx: *anyopaque) !?u32 {
const self: *PostMessageCallback = @ptrCast(@alignCast(ctx));
defer self.deinit();
const page = self.page;
if (self.port._closed) {
return null;
@@ -125,16 +126,19 @@ const PostMessageCallback = struct {
.data = self.message,
.origin = "",
.source = null,
}, self.page) catch |err| {
}, page) catch |err| {
log.err(.dom, "MessagePort.postMessage", .{ .err = err });
return null;
};
const func = if (self.port._on_message) |*g| g.local() else null;
self.page._event_manager.dispatchWithFunction(
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
page._event_manager.dispatchWithFunction(
self.port.asEventTarget(),
event.asEvent(),
func,
ls.toLocal(self.port._on_message),
.{ .context = "MessagePort message" },
) catch |err| {
log.err(.dom, "MessagePort.postMessage", .{ .err = err });

View File

@@ -17,6 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const String = @import("../../string.zig").String;
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Node = @import("Node.zig");
@@ -109,8 +111,8 @@ pub fn takeRecords(self: *MutationObserver, page: *Page) ![]*MutationRecord {
pub fn notifyAttributeChange(
self: *MutationObserver,
target: *Element,
attribute_name: []const u8,
old_value: ?[]const u8,
attribute_name: String,
old_value: ?String,
page: *Page,
) !void {
const target_node = target.asNode();
@@ -129,7 +131,7 @@ pub fn notifyAttributeChange(
}
if (obs.options.attributeFilter) |filter| {
for (filter) |name| {
if (std.mem.eql(u8, name, attribute_name)) {
if (attribute_name.eqlSlice(name)) {
break;
}
} else {
@@ -140,9 +142,9 @@ pub fn notifyAttributeChange(
const record = try page._factory.create(MutationRecord{
._type = .attributes,
._target = target_node,
._attribute_name = try page.arena.dupe(u8, attribute_name),
._attribute_name = try page.arena.dupe(u8, attribute_name.str()),
._old_value = if (obs.options.attributeOldValue and old_value != null)
try page.arena.dupe(u8, old_value.?)
try page.arena.dupe(u8, old_value.?.str())
else
null,
._added_nodes = &.{},
@@ -248,8 +250,12 @@ pub fn deliverRecords(self: *MutationObserver, page: *Page) !void {
// Take a copy of the records and clear the list before calling callback
// This ensures mutations triggered during the callback go into a fresh list
const records = try self.takeRecords(page);
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
var caught: js.TryCatch.Caught = undefined;
self._callback.local().tryCall(void, .{ records, self }, &caught) catch |err| {
ls.toLocal(self._callback).tryCall(void, .{ records, self }, &caught) catch |err| {
log.err(.page, "MutObserver.deliverRecords", .{ .err = err, .caught = caught });
return err;
};

View File

@@ -17,8 +17,9 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = @import("../../log.zig");
const String = @import("../../string.zig").String;
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const reflect = @import("../reflect.zig");
@@ -268,7 +269,7 @@ pub fn getTextContent(self: *Node, writer: *std.Io.Writer) error{WriteFailed}!vo
.document => {},
.document_type => {},
.document_fragment => {},
.attribute => |attr| try writer.writeAll(attr._value),
.attribute => |attr| try writer.writeAll(attr._value.str()),
}
}
@@ -297,7 +298,7 @@ pub fn setTextContent(self: *Node, data: []const u8, page: *Page) !void {
}
return frag.replaceChildren(&.{.{ .text = data }}, page);
},
.attribute => |attr| return attr.setValue(data, page),
.attribute => |attr| return attr.setValue(.wrap(data), page),
}
}
@@ -313,7 +314,7 @@ pub fn getNodeName(self: *const Node, buf: []u8) []const u8 {
.document => "#document",
.document_type => |dt| dt.getName(),
.document_fragment => "#document-fragment",
.attribute => |attr| attr._name,
.attribute => |attr| attr._name.str(),
};
}
@@ -559,7 +560,7 @@ pub fn replaceChild(self: *Node, new_child: *Node, old_child: *Node, page: *Page
pub fn getNodeValue(self: *const Node) ?[]const u8 {
return switch (self._type) {
.cdata => |c| c.getData(),
.attribute => |attr| attr._value,
.attribute => |attr| attr._value.str(),
.element => null,
.document => null,
.document_type => null,
@@ -567,9 +568,9 @@ pub fn getNodeValue(self: *const Node) ?[]const u8 {
};
}
pub fn setNodeValue(self: *const Node, value: ?[]const u8, page: *Page) !void {
pub fn setNodeValue(self: *const Node, value: ?String, page: *Page) !void {
switch (self._type) {
.cdata => |c| try c.setData(value, page),
.cdata => |c| try c.setData(if (value) |v| v.str() else null, page),
.attribute => |attr| try attr.setValue(value, page),
.element => {},
.document => {},
@@ -910,7 +911,7 @@ pub const JsApi = struct {
return buf.written();
},
.cdata => |cdata| return cdata.getData(),
.attribute => |attr| return attr._value,
.attribute => |attr| return attr._value.str(),
.document => return null,
.document_type => return null,
.document_fragment => return null,

View File

@@ -65,9 +65,9 @@ pub const SHOW_DOCUMENT_TYPE: u32 = 0x200;
pub const SHOW_DOCUMENT_FRAGMENT: u32 = 0x400;
pub const SHOW_NOTATION: u32 = 0x800;
pub fn acceptNode(self: *const NodeFilter, node: *Node) !i32 {
pub fn acceptNode(self: *const NodeFilter, node: *Node, local: *const js.Local) !i32 {
const func = self._func orelse return FILTER_ACCEPT;
return func.local().call(i32, .{node});
return local.toLocal(func).call(i32, .{node});
}
pub fn shouldShow(node: *const Node, what_to_show: u32) bool {

View File

@@ -362,10 +362,10 @@ pub const Mark = struct {
pub const Measure = struct {
_proto: *Entry,
_detail: ?js.Object.Global,
_detail: ?js.Value.Global,
const Options = struct {
detail: ?js.Object = null,
detail: ?js.Value = null,
start: ?TimestampOrMark,
end: ?TimestampOrMark,
duration: ?f64 = null,
@@ -378,7 +378,7 @@ pub const Measure = struct {
pub fn init(
name: []const u8,
maybe_detail: ?js.Object,
maybe_detail: ?js.Value,
start_timestamp: f64,
end_timestamp: f64,
maybe_duration: ?f64,
@@ -405,7 +405,7 @@ pub const Measure = struct {
return m;
}
pub fn getDetail(self: *const Measure) ?js.Object.Global {
pub fn getDetail(self: *const Measure) ?js.Value.Global {
return self._detail;
}

View File

@@ -153,8 +153,13 @@ pub inline fn hasRecords(self: *const PerformanceObserver) bool {
/// Runs the PerformanceObserver's callback with records; emptying it out.
pub fn dispatch(self: *PerformanceObserver, page: *Page) !void {
const records = try self.takeRecords(page);
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
var caught: js.TryCatch.Caught = undefined;
self._callback.local().tryCall(void, .{ EntryList{ ._entries = records }, self }, &caught) catch |err| {
ls.toLocal(self._callback).tryCall(void, .{ EntryList{ ._entries = records }, self }, &caught) catch |err| {
log.err(.page, "PerfObserver.dispatch", .{ .err = err, .caught = caught });
return err;
};

View File

@@ -0,0 +1,426 @@
// 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/js.zig");
const Page = @import("../Page.zig");
const Range = @import("Range.zig");
const AbstractRange = @import("AbstractRange.zig");
const Node = @import("Node.zig");
/// https://w3c.github.io/selection-api/
const Selection = @This();
pub const SelectionDirection = enum { backward, forward, none };
_range: ?*Range = null,
_direction: SelectionDirection = .none,
pub const init: Selection = .{};
fn isInTree(self: *const Selection) bool {
if (self._range == null) return false;
const anchor_node = self.getAnchorNode() orelse return false;
const focus_node = self.getFocusNode() orelse return false;
return anchor_node.isConnected() and focus_node.isConnected();
}
pub fn getAnchorNode(self: *const Selection) ?*Node {
const range = self._range orelse return null;
const node = switch (self._direction) {
.backward => range.asAbstractRange().getEndContainer(),
.forward, .none => range.asAbstractRange().getStartContainer(),
};
return if (node.isConnected()) node else null;
}
pub fn getAnchorOffset(self: *const Selection) u32 {
const range = self._range orelse return 0;
const anchor_node = self.getAnchorNode() orelse return 0;
if (!anchor_node.isConnected()) return 0;
return switch (self._direction) {
.backward => range.asAbstractRange().getEndOffset(),
.forward, .none => range.asAbstractRange().getStartOffset(),
};
}
pub fn getDirection(self: *const Selection) []const u8 {
return @tagName(self._direction);
}
pub fn getFocusNode(self: *const Selection) ?*Node {
const range = self._range orelse return null;
const node = switch (self._direction) {
.backward => range.asAbstractRange().getStartContainer(),
.forward, .none => range.asAbstractRange().getEndContainer(),
};
return if (node.isConnected()) node else null;
}
pub fn getFocusOffset(self: *const Selection) u32 {
const range = self._range orelse return 0;
const focus_node = self.getFocusNode() orelse return 0;
if (!focus_node.isConnected()) return 0;
return switch (self._direction) {
.backward => range.asAbstractRange().getStartOffset(),
.forward, .none => range.asAbstractRange().getEndOffset(),
};
}
pub fn getIsCollapsed(self: *const Selection) bool {
const range = self._range orelse return true;
return range.asAbstractRange().getCollapsed();
}
pub fn getRangeCount(self: *const Selection) u32 {
if (self._range == null) return 0;
if (!self.isInTree()) return 0;
return 1;
}
pub fn getType(self: *const Selection) []const u8 {
if (self._range == null) return "None";
if (!self.isInTree()) return "None";
if (self.getIsCollapsed()) return "Caret";
return "Range";
}
pub fn addRange(self: *Selection, range: *Range) !void {
if (self._range != null) return;
self._range = range;
}
pub fn removeRange(self: *Selection, range: *Range) !void {
if (self._range == range) {
self._range = null;
return;
} else {
return error.NotFound;
}
}
pub fn removeAllRanges(self: *Selection) void {
self._range = null;
self._direction = .none;
}
pub fn collapseToEnd(self: *Selection, page: *Page) !void {
const range = self._range orelse return;
const abstract = range.asAbstractRange();
const last_node = abstract.getEndContainer();
const last_offset = abstract.getEndOffset();
const new_range = try Range.init(page);
try new_range.setStart(last_node, last_offset);
try new_range.setEnd(last_node, last_offset);
self._range = new_range;
self._direction = .none;
}
pub fn collapseToStart(self: *Selection, page: *Page) !void {
const range = self._range orelse return;
const abstract = range.asAbstractRange();
const first_node = abstract.getStartContainer();
const first_offset = abstract.getStartOffset();
const new_range = try Range.init(page);
try new_range.setStart(first_node, first_offset);
try new_range.setEnd(first_node, first_offset);
self._range = new_range;
self._direction = .none;
}
pub fn containsNode(self: *const Selection, node: *Node, partial: bool) !bool {
const range = self._range orelse return false;
if (partial) {
if (range.intersectsNode(node)) {
return true;
}
} else {
const abstract = range.asAbstractRange();
if (abstract.getStartContainer() == node or abstract.getEndContainer() == node) {
return false;
}
const parent = node.parentNode() orelse return false;
const offset = parent.getChildIndex(node) orelse return false;
const start_cmp = range.comparePoint(parent, offset) catch return false;
const end_cmp = range.comparePoint(parent, offset + 1) catch return false;
if (start_cmp <= 0 and end_cmp >= 0) {
return true;
}
}
return false;
}
pub fn deleteFromDocument(self: *Selection, page: *Page) !void {
const range = self._range orelse return;
try range.deleteContents(page);
}
pub fn extend(self: *Selection, node: *Node, _offset: ?u32, page: *Page) !void {
const range = self._range orelse return error.InvalidState;
const offset = _offset orelse 0;
if (offset > node.getLength()) {
return error.IndexSizeError;
}
const old_anchor = switch (self._direction) {
.backward => range.asAbstractRange().getEndContainer(),
.forward, .none => range.asAbstractRange().getStartContainer(),
};
const old_anchor_offset = switch (self._direction) {
.backward => range.asAbstractRange().getEndOffset(),
.forward, .none => range.asAbstractRange().getStartOffset(),
};
const new_range = try Range.init(page);
const cmp = AbstractRange.compareBoundaryPoints(node, offset, old_anchor, old_anchor_offset);
switch (cmp) {
.before => {
try new_range.setStart(node, offset);
try new_range.setEnd(old_anchor, old_anchor_offset);
self._direction = .backward;
},
.after => {
try new_range.setStart(old_anchor, old_anchor_offset);
try new_range.setEnd(node, offset);
self._direction = .forward;
},
.equal => {
try new_range.setStart(old_anchor, old_anchor_offset);
try new_range.setEnd(old_anchor, old_anchor_offset);
self._direction = .none;
},
}
self._range = new_range;
}
pub fn getRangeAt(self: *Selection, index: u32) !*Range {
if (index != 0) return error.IndexSizeError;
if (!self.isInTree()) return error.IndexSizeError;
const range = self._range orelse return error.IndexSizeError;
return range;
}
const ModifyAlter = enum {
move,
extend,
pub fn fromString(str: []const u8) ?ModifyAlter {
return std.meta.stringToEnum(ModifyAlter, str);
}
};
const ModifyDirection = enum {
forward,
backward,
left,
right,
pub fn fromString(str: []const u8) ?ModifyDirection {
return std.meta.stringToEnum(ModifyDirection, str);
}
};
const ModifyGranularity = enum {
character,
word,
line,
paragraph,
lineboundary,
// Firefox doesn't implement:
// - sentence
// - paragraph
// - sentenceboundary
// - paragraphboundary
// - documentboundary
// so we won't either for now.
pub fn fromString(str: []const u8) ?ModifyGranularity {
return std.meta.stringToEnum(ModifyGranularity, str);
}
};
pub fn modify(
self: *Selection,
alter_str: []const u8,
direction_str: []const u8,
granularity_str: []const u8,
) !void {
const alter = ModifyAlter.fromString(alter_str) orelse return error.InvalidParams;
const direction = ModifyDirection.fromString(direction_str) orelse return error.InvalidParams;
const granularity = ModifyGranularity.fromString(granularity_str) orelse return error.InvalidParams;
_ = self._range orelse return;
log.warn(.not_implemented, "Selection.modify", .{
.alter = alter,
.direction = direction,
.granularity = granularity,
});
}
pub fn selectAllChildren(self: *Selection, parent: *Node, page: *Page) !void {
if (parent._type == .document_type) return error.InvalidNodeTypeError;
const range = try Range.init(page);
try range.setStart(parent, 0);
const child_count = parent.getLength();
try range.setEnd(parent, @intCast(child_count));
self._range = range;
self._direction = .forward;
}
pub fn setBaseAndExtent(
self: *Selection,
anchor_node: *Node,
anchor_offset: u32,
focus_node: *Node,
focus_offset: u32,
page: *Page,
) !void {
if (anchor_offset > anchor_node.getLength()) {
return error.IndexSizeError;
}
if (focus_offset > focus_node.getLength()) {
return error.IndexSizeError;
}
const cmp = AbstractRange.compareBoundaryPoints(
anchor_node,
anchor_offset,
focus_node,
focus_offset,
);
const range = try Range.init(page);
switch (cmp) {
.before => {
try range.setStart(anchor_node, anchor_offset);
try range.setEnd(focus_node, focus_offset);
self._direction = .forward;
},
.after => {
try range.setStart(focus_node, focus_offset);
try range.setEnd(anchor_node, anchor_offset);
self._direction = .backward;
},
.equal => {
try range.setStart(anchor_node, anchor_offset);
try range.setEnd(anchor_node, anchor_offset);
self._direction = .none;
},
}
self._range = range;
}
pub fn collapse(self: *Selection, _node: ?*Node, _offset: ?u32, page: *Page) !void {
const node = _node orelse {
self.removeAllRanges();
return;
};
if (node._type == .document_type) return error.InvalidNodeType;
const offset = _offset orelse 0;
if (offset > node.getLength()) {
return error.IndexSizeError;
}
const range = try Range.init(page);
try range.setStart(node, offset);
try range.setEnd(node, offset);
self._range = range;
self._direction = .none;
}
pub fn toString(self: *const Selection, page: *Page) ![]const u8 {
const range = self._range orelse return "";
return try range.toString(page);
}
pub const JsApi = struct {
pub const bridge = js.Bridge(Selection);
pub const Meta = struct {
pub const name = "Selection";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const anchorNode = bridge.accessor(Selection.getAnchorNode, null, .{});
pub const anchorOffset = bridge.accessor(Selection.getAnchorOffset, null, .{});
pub const direction = bridge.accessor(Selection.getDirection, null, .{});
pub const focusNode = bridge.accessor(Selection.getFocusNode, null, .{});
pub const focusOffset = bridge.accessor(Selection.getFocusOffset, null, .{});
pub const isCollapsed = bridge.accessor(Selection.getIsCollapsed, null, .{});
pub const rangeCount = bridge.accessor(Selection.getRangeCount, null, .{});
pub const @"type" = bridge.accessor(Selection.getType, null, .{});
pub const addRange = bridge.function(Selection.addRange, .{});
pub const collapse = bridge.function(Selection.collapse, .{ .dom_exception = true });
pub const collapseToEnd = bridge.function(Selection.collapseToEnd, .{});
pub const collapseToStart = bridge.function(Selection.collapseToStart, .{});
pub const containsNode = bridge.function(Selection.containsNode, .{});
pub const deleteFromDocument = bridge.function(Selection.deleteFromDocument, .{});
pub const empty = bridge.function(Selection.removeAllRanges, .{});
pub const extend = bridge.function(Selection.extend, .{ .dom_exception = true });
// unimplemented: getComposedRanges
pub const getRangeAt = bridge.function(Selection.getRangeAt, .{ .dom_exception = true });
pub const modify = bridge.function(Selection.modify, .{});
pub const removeAllRanges = bridge.function(Selection.removeAllRanges, .{});
pub const removeRange = bridge.function(Selection.removeRange, .{ .dom_exception = true });
pub const selectAllChildren = bridge.function(Selection.selectAllChildren, .{});
pub const setBaseAndExtent = bridge.function(Selection.setBaseAndExtent, .{ .dom_exception = true });
pub const setPosition = bridge.function(Selection.collapse, .{});
pub const toString = bridge.function(Selection.toString, .{});
};
const testing = @import("../../testing.zig");
test "WebApi: Selection" {
try testing.htmlRunner("selection.html", .{});
}

View File

@@ -84,7 +84,7 @@ pub fn getElementById(self: *ShadowRoot, id: []const u8, page: *Page) ?*Element
// Do a tree walk to find another element with this ID
var tw = @import("TreeWalker.zig").Full.Elements.init(self.asNode(), .{});
while (tw.next()) |el| {
const element_id = el.getAttributeSafe("id") orelse continue;
const element_id = el.getAttributeSafe(comptime .wrap("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

View File

@@ -96,10 +96,10 @@ pub fn generateKey(
page: *Page,
) !js.Promise {
const key_or_pair = CryptoKey.init(algorithm, extractable, key_usages, page) catch |err| {
return page.js.rejectPromise(@errorName(err));
return page.js.local.?.rejectPromise(@errorName(err));
};
return page.js.resolvePromise(key_or_pair);
return page.js.local.?.resolvePromise(key_or_pair);
}
/// Exports a key: that is, it takes as input a CryptoKey object and gives you
@@ -115,7 +115,7 @@ pub fn exportKey(
}
if (std.mem.eql(u8, format, "raw")) {
return page.js.resolvePromise(js.ArrayBuffer{ .values = key._key });
return page.js.local.?.resolvePromise(js.ArrayBuffer{ .values = key._key });
}
const is_unsupported = std.mem.eql(u8, format, "pkcs8") or
@@ -125,7 +125,7 @@ pub fn exportKey(
log.warn(.not_implemented, "SubtleCrypto.exportKey", .{ .format = format });
}
return page.js.rejectPromise(@errorName(error.NotSupported));
return page.js.local.?.rejectPromise(@errorName(error.NotSupported));
}
/// Derive a secret key from a master key.
@@ -140,14 +140,14 @@ pub fn deriveBits(
.ecdh_or_x25519 => |p| {
const name = p.name;
if (std.mem.eql(u8, name, "X25519")) {
return page.js.resolvePromise(base_key.deriveBitsX25519(p.public, length, page));
return page.js.local.?.resolvePromise(base_key.deriveBitsX25519(p.public, length, page));
}
if (std.mem.eql(u8, name, "ECDH")) {
log.warn(.not_implemented, "SubtleCrypto.deriveBits", .{ .name = name });
}
return page.js.rejectPromise(@errorName(error.NotSupported));
return page.js.local.?.rejectPromise(@errorName(error.NotSupported));
},
};
}
@@ -184,19 +184,19 @@ pub fn sign(
.hmac => {
// Verify algorithm.
if (!algorithm.isHMAC()) {
return page.js.rejectPromise(@errorName(error.InvalidAccessError));
return page.js.local.?.rejectPromise(@errorName(error.InvalidAccessError));
}
// Call sign for HMAC.
const result = key.signHMAC(data, page) catch |err| {
return page.js.rejectPromise(@errorName(err));
return page.js.local.?.rejectPromise(@errorName(err));
};
return page.js.resolvePromise(result);
return page.js.local.?.resolvePromise(result);
},
else => {
log.warn(.not_implemented, "SubtleCrypto.sign", .{ .key_type = key._type });
return page.js.rejectPromise(@errorName(error.InvalidAccessError));
return page.js.local.?.rejectPromise(@errorName(error.InvalidAccessError));
},
};
}
@@ -454,10 +454,10 @@ pub const CryptoKey = struct {
if (signed != null) {
// CRYPTO_memcmp compare in constant time so prohibits time-based attacks.
const res = crypto.CRYPTO_memcmp(signed, @ptrCast(signature.ptr), signature.len);
return page.js.resolvePromise(res == 0);
return page.js.local.?.resolvePromise(res == 0);
}
return page.js.resolvePromise(false);
return page.js.local.?.resolvePromise(false);
}
// X25519.

View File

@@ -42,6 +42,7 @@ const storage = @import("storage/storage.zig");
const Element = @import("Element.zig");
const CSSStyleProperties = @import("css/CSSStyleProperties.zig");
const CustomElementRegistry = @import("CustomElementRegistry.zig");
const Selection = @import("Selection.zig");
const Window = @This();
@@ -57,7 +58,7 @@ _storage_bucket: *storage.Bucket,
_on_load: ?js.Function.Global = null,
_on_pageshow: ?js.Function.Global = null,
_on_popstate: ?js.Function.Global = null,
_on_error: ?js.Function.Global = null, // TODO: invoke on error?
_on_error: ?js.Function.Global = null,
_on_unhandled_rejection: ?js.Function.Global = null, // TODO: invoke on error
_location: *Location,
_timer_id: u30 = 0,
@@ -129,6 +130,10 @@ pub fn getLocation(self: *const Window) *Location {
return self._location;
}
pub fn getSelection(self: *const Window) *Selection {
return &self._document._selection;
}
pub fn setLocation(_: *const Window, url: [:0]const u8, page: *Page) !void {
return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .script);
}
@@ -189,7 +194,7 @@ pub fn fetch(_: *const Window, input: Fetch.Input, options: ?Fetch.InitOpts, pag
return Fetch.init(input, options, page);
}
pub fn setTimeout(self: *Window, cb: js.Function.Global, delay_ms: ?u32, params: []js.Value.Global, page: *Page) !u32 {
pub fn setTimeout(self: *Window, cb: js.Function.Temp, delay_ms: ?u32, params: []js.Value.Temp, page: *Page) !u32 {
return self.scheduleCallback(cb, delay_ms orelse 0, .{
.repeat = false,
.params = params,
@@ -198,7 +203,7 @@ pub fn setTimeout(self: *Window, cb: js.Function.Global, delay_ms: ?u32, params:
}, page);
}
pub fn setInterval(self: *Window, cb: js.Function.Global, delay_ms: ?u32, params: []js.Value.Global, page: *Page) !u32 {
pub fn setInterval(self: *Window, cb: js.Function.Temp, delay_ms: ?u32, params: []js.Value.Temp, page: *Page) !u32 {
return self.scheduleCallback(cb, delay_ms orelse 0, .{
.repeat = true,
.params = params,
@@ -207,7 +212,7 @@ pub fn setInterval(self: *Window, cb: js.Function.Global, delay_ms: ?u32, params
}, page);
}
pub fn setImmediate(self: *Window, cb: js.Function.Global, params: []js.Value.Global, page: *Page) !u32 {
pub fn setImmediate(self: *Window, cb: js.Function.Temp, params: []js.Value.Temp, page: *Page) !u32 {
return self.scheduleCallback(cb, 0, .{
.repeat = false,
.params = params,
@@ -216,7 +221,7 @@ pub fn setImmediate(self: *Window, cb: js.Function.Global, params: []js.Value.Gl
}, page);
}
pub fn requestAnimationFrame(self: *Window, cb: js.Function.Global, page: *Page) !u32 {
pub fn requestAnimationFrame(self: *Window, cb: js.Function.Temp, page: *Page) !u32 {
return self.scheduleCallback(cb, 5, .{
.repeat = false,
.params = &.{},
@@ -253,7 +258,7 @@ pub fn cancelAnimationFrame(self: *Window, id: u32) void {
const RequestIdleCallbackOpts = struct {
timeout: ?u32 = null,
};
pub fn requestIdleCallback(self: *Window, cb: js.Function.Global, opts_: ?RequestIdleCallbackOpts, page: *Page) !u32 {
pub fn requestIdleCallback(self: *Window, cb: js.Function.Temp, opts_: ?RequestIdleCallbackOpts, page: *Page) !u32 {
const opts = opts_ orelse RequestIdleCallbackOpts{};
return self.scheduleCallback(cb, opts.timeout orelse 50, .{
.mode = .idle,
@@ -269,15 +274,41 @@ pub fn cancelIdleCallback(self: *Window, id: u32) void {
sc.removed = true;
}
pub fn reportError(self: *Window, err: js.Value.Global, page: *Page) !void {
pub fn reportError(self: *Window, err: js.Value, page: *Page) !void {
const error_event = try ErrorEvent.initTrusted("error", .{
.@"error" = err,
.message = err.local().toString(.{}) catch "Unknown error",
.@"error" = try err.persist(),
.message = err.toString(.{}) catch "Unknown error",
.bubbles = false,
.cancelable = true,
}, page);
const event = error_event.asEvent();
// Invoke window.onerror callback if set (per WHATWG spec, this is called
// with 5 arguments: message, source, lineno, colno, error)
// If it returns true, the event is cancelled.
if (self._on_error) |on_error| {
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
const local_func = ls.toLocal(on_error);
const result = local_func.call(js.Value, .{
error_event._message,
error_event._filename,
error_event._line_number,
error_event._column_number,
err,
}) catch null;
// Per spec: returning true from onerror cancels the event
if (result) |r| {
if (r.isTrue()) {
event._prevent_default = true;
}
}
}
try page._event_manager.dispatch(self.asEventTarget(), event);
if (comptime builtin.is_test == false) {
@@ -465,13 +496,13 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
const ScheduleOpts = struct {
repeat: bool,
params: []js.Value.Global,
params: []js.Value.Temp,
name: []const u8,
low_priority: bool = false,
animation_frame: bool = false,
mode: ScheduleCallback.Mode = .normal,
};
fn scheduleCallback(self: *Window, cb: js.Function.Global, delay_ms: u32, opts: ScheduleOpts, page: *Page) !u32 {
fn scheduleCallback(self: *Window, cb: js.Function.Temp, delay_ms: u32, opts: ScheduleOpts, page: *Page) !u32 {
if (self._timers.count() > 512) {
// these are active
return error.TooManyTimeout;
@@ -481,9 +512,9 @@ fn scheduleCallback(self: *Window, cb: js.Function.Global, delay_ms: u32, opts:
self._timer_id = timer_id;
const params = opts.params;
var persisted_params: []js.Value.Global = &.{};
var persisted_params: []js.Value.Temp = &.{};
if (params.len > 0) {
persisted_params = try page.arena.dupe(js.Value.Global, params);
persisted_params = try page.arena.dupe(js.Value.Temp, params);
}
const gop = try self._timers.getOrPut(page.arena, timer_id);
@@ -523,11 +554,11 @@ const ScheduleCallback = struct {
// delay, in ms, to repeat. When null, will be removed after the first time
repeat_ms: ?u32,
cb: js.Function.Global,
cb: js.Function.Temp,
page: *Page,
params: []const js.Value.Global,
params: []const js.Value.Temp,
removed: bool = false,
@@ -540,6 +571,10 @@ const ScheduleCallback = struct {
};
fn deinit(self: *ScheduleCallback) void {
self.page.js.release(self.cb);
for (self.params) |param| {
self.page.js.release(param);
}
self.page._factory.destroy(self);
}
@@ -552,32 +587,34 @@ const ScheduleCallback = struct {
return null;
}
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
switch (self.mode) {
.idle => {
const IdleDeadline = @import("IdleDeadline.zig");
self.cb.local().call(void, .{IdleDeadline{}}) catch |err| {
ls.toLocal(self.cb).call(void, .{IdleDeadline{}}) catch |err| {
log.warn(.js, "window.idleCallback", .{ .name = self.name, .err = err });
};
},
.animation_frame => {
self.cb.local().call(void, .{page.window._performance.now()}) catch |err| {
ls.toLocal(self.cb).call(void, .{page.window._performance.now()}) catch |err| {
log.warn(.js, "window.RAF", .{ .name = self.name, .err = err });
};
},
.normal => {
self.cb.local().call(void, self.params) catch |err| {
ls.toLocal(self.cb).call(void, self.params) catch |err| {
log.warn(.js, "window.timer", .{ .name = self.name, .err = err });
};
},
}
ls.local.runMicrotasks();
if (self.repeat_ms) |ms| {
return ms;
}
defer self.deinit();
_ = page.window._timers.remove(self.timer_id);
page.js.runMicrotasks();
return null;
}
};
@@ -647,7 +684,7 @@ pub const JsApi = struct {
pub const localStorage = bridge.accessor(Window.getLocalStorage, null, .{ .cache = "localStorage" });
pub const sessionStorage = bridge.accessor(Window.getSessionStorage, null, .{ .cache = "sessionStorage" });
pub const document = bridge.accessor(Window.getDocument, null, .{ .cache = "document" });
pub const location = bridge.accessor(Window.getLocation, Window.setLocation, .{ .cache = "location" });
pub const location = bridge.accessor(Window.getLocation, Window.setLocation, .{});
pub const history = bridge.accessor(Window.getHistory, null, .{});
pub const navigation = bridge.accessor(Window.getNavigation, null, .{});
pub const crypto = bridge.accessor(Window.getCrypto, null, .{ .cache = "crypto" });
@@ -656,7 +693,7 @@ pub const JsApi = struct {
pub const onload = bridge.accessor(Window.getOnLoad, Window.setOnLoad, .{});
pub const onpageshow = bridge.accessor(Window.getOnPageShow, Window.setOnPageShow, .{});
pub const onpopstate = bridge.accessor(Window.getOnPopState, Window.setOnPopState, .{});
pub const onerror = bridge.accessor(Window.getOnError, Window.getOnError, .{});
pub const onerror = bridge.accessor(Window.getOnError, Window.setOnError, .{});
pub const onunhandledrejection = bridge.accessor(Window.getOnUnhandledRejection, Window.setOnUnhandledRejection, .{});
pub const fetch = bridge.function(Window.fetch, .{});
pub const queueMicrotask = bridge.function(Window.queueMicrotask, .{});
@@ -676,6 +713,7 @@ pub const JsApi = struct {
pub const atob = bridge.function(Window.atob, .{});
pub const reportError = bridge.function(Window.reportError, .{});
pub const getComputedStyle = bridge.function(Window.getComputedStyle, .{});
pub const getSelection = bridge.function(Window.getSelection, .{});
pub const isSecureContext = bridge.accessor(Window.getIsSecureContext, null, .{});
pub const frames = bridge.accessor(Window.getWindow, null, .{ .cache = "frames" });
pub const index = bridge.indexed(Window.getFrame, .{ .null_as_undefined = true });

View File

@@ -46,20 +46,22 @@ pub fn getPending(_: *const Animation) bool {
pub fn getFinished(self: *Animation, page: *Page) !js.Promise {
if (self._finished_resolver == null) {
const resolver = try page.js.createPromiseResolver().persist();
resolver.local().resolve("Animation.getFinished", self);
self._finished_resolver = resolver;
const resolver = page.js.local.?.createPromiseResolver();
resolver.resolve("Animation.getFinished", self);
self._finished_resolver = try resolver.persist();
return resolver.promise();
}
return self._finished_resolver.?.local().promise();
return page.js.toLocal(self._finished_resolver).?.promise();
}
pub fn getReady(self: *Animation, page: *Page) !js.Promise {
// never resolved, because we're always "finished"
if (self._ready_resolver == null) {
const resolver = try page.js.createPromiseResolver().persist();
self._ready_resolver = resolver;
const resolver = page.js.local.?.createPromiseResolver();
self._ready_resolver = try resolver.persist();
return resolver.promise();
}
return self._ready_resolver.?.local().promise();
return page.js.toLocal(self._ready_resolver).?.promise();
}
pub fn getEffect(self: *const Animation) ?js.Object.Global {

View File

@@ -18,8 +18,9 @@
const std = @import("std");
const log = @import("../../../log.zig");
const js = @import("../../js/js.zig");
const String = @import("../../../string.zig").String;
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Element = @import("../Element.zig");
const GenericIterator = @import("iterator.zig").Entry;
@@ -31,7 +32,7 @@ pub const DOMTokenList = @This();
// is that lists tend to be very short (often just 1 item).
_element: *Element,
_attribute_name: []const u8,
_attribute_name: String,
pub const KeyIterator = GenericIterator(Iterator, "0");
pub const ValueIterator = GenericIterator(Iterator, "1");
@@ -159,7 +160,7 @@ pub fn getValue(self: *const DOMTokenList) []const u8 {
return self._element.getAttributeSafe(self._attribute_name) orelse "";
}
pub fn setValue(self: *DOMTokenList, value: []const u8, page: *Page) !void {
pub fn setValue(self: *DOMTokenList, value: String, page: *Page) !void {
try self._element.setAttribute(self._attribute_name, value, page);
}
@@ -226,7 +227,7 @@ fn validateToken(token: []const u8) !void {
fn updateAttribute(self: *DOMTokenList, tokens: Lookup, page: *Page) !void {
const joined = try std.mem.join(page.call_arena, " ", tokens.keys());
try self._element.setAttribute(self._attribute_name, joined, page);
try self._element.setAttribute(self._attribute_name, .wrap(joined), page);
}
const Iterator = struct {

View File

@@ -111,7 +111,7 @@ pub fn getByName(self: *HTMLAllCollection, name: []const u8, page: *Page) ?*Elem
while (tw.next()) |node| {
if (node.is(Element)) |el| {
if (el.getAttributeSafe("name")) |attr_name| {
if (el.getAttributeSafe(comptime .wrap("name"))) |attr_name| {
if (std.mem.eql(u8, attr_name, name)) {
return el;
}

View File

@@ -59,12 +59,12 @@ pub fn namedItem(self: *HTMLFormControlsCollection, name: []const u8, page: *Pag
var it = try self.iterator();
while (it.next()) |element| {
const is_match = blk: {
if (element.getAttributeSafe("id")) |id| {
if (element.getAttributeSafe(comptime .wrap("id"))) |id| {
if (std.mem.eql(u8, id, name)) {
break :blk true;
}
}
if (element.getAttributeSafe("name")) |elem_name| {
if (element.getAttributeSafe(comptime .wrap("name"))) |elem_name| {
if (std.mem.eql(u8, elem_name, name)) {
break :blk true;
}

View File

@@ -69,7 +69,7 @@ pub fn getValue(self: *RadioNodeList) ![]const u8 {
if (!input.getChecked()) {
continue;
}
return element.getAttributeSafe("value") orelse "on";
return element.getAttributeSafe(comptime .wrap("value")) orelse "on";
}
return "";
}
@@ -82,7 +82,7 @@ pub fn setValue(self: *RadioNodeList, value: []const u8, page: *Page) !void {
continue;
}
const input_value = element.getAttributeSafe("value");
const input_value = element.getAttributeSafe(comptime .wrap("value"));
const matches_value = blk: {
if (std.mem.eql(u8, value, "on")) {
break :blk input_value == null or (input_value != null and std.mem.eql(u8, input_value.?, "on"));
@@ -99,12 +99,12 @@ pub fn setValue(self: *RadioNodeList, value: []const u8, page: *Page) !void {
}
fn matches(self: *const RadioNodeList, element: *Element) bool {
if (element.getAttributeSafe("id")) |id| {
if (element.getAttributeSafe(comptime .wrap("id"))) |id| {
if (std.mem.eql(u8, id, self._name)) {
return true;
}
}
if (element.getAttributeSafe("name")) |elem_name| {
if (element.getAttributeSafe(comptime .wrap("name"))) |elem_name| {
if (std.mem.eql(u8, elem_name, self._name)) {
return true;
}

View File

@@ -187,7 +187,7 @@ pub fn NodeLive(comptime mode: Mode) type {
// (like length or getAtIndex)
var tw = self._tw.clone();
while (self.nextTw(&tw)) |element| {
const element_name = element.getAttributeSafe("name") orelse continue;
const element_name = element.getAttributeSafe(comptime .wrap("name")) orelse continue;
if (std.mem.eql(u8, element_name, name)) {
return element;
}
@@ -228,7 +228,7 @@ pub fn NodeLive(comptime mode: Mode) type {
}
const el = node.is(Element) orelse return false;
const class_attr = el.getAttributeSafe("class") orelse return false;
const class_attr = el.getAttributeSafe(comptime .wrap("class")) orelse return false;
for (self._filter) |class_name| {
if (!Selector.classAttributeContains(class_attr, class_name)) {
return false;
@@ -238,7 +238,7 @@ pub fn NodeLive(comptime mode: Mode) type {
},
.name => {
const el = node.is(Element) orelse return false;
const name_attr = el.getAttributeSafe("name") orelse return false;
const name_attr = el.getAttributeSafe(comptime .wrap("name")) orelse return false;
return std.mem.eql(u8, name_attr, self._filter);
},
.all_elements => return node._type == .element,
@@ -258,14 +258,14 @@ pub fn NodeLive(comptime mode: Mode) type {
const el = node.is(Element) orelse return false;
const Anchor = Element.Html.Anchor;
if (el.is(Anchor) == null) return false;
return el.hasAttributeSafe("href");
return el.hasAttributeSafe(comptime .wrap("href"));
},
.anchors => {
// Anchors are <a> elements with name attribute
const el = node.is(Element) orelse return false;
const Anchor = Element.Html.Anchor;
if (el.is(Anchor) == null) return false;
return el.hasAttributeSafe("name");
return el.hasAttributeSafe(comptime .wrap("name"));
},
.form => {
const el = node.is(Element) orelse return false;
@@ -273,8 +273,8 @@ pub fn NodeLive(comptime mode: Mode) type {
return false;
}
if (el.getAttributeSafe("form")) |form_attr| {
const form_id = self._filter.asElement().getAttributeSafe("id") orelse return false;
if (el.getAttributeSafe(comptime .wrap("form"))) |form_attr| {
const form_id = self._filter.asElement().getAttributeSafe(comptime .wrap("id")) orelse return false;
return std.mem.eql(u8, form_attr, form_id);
}

View File

@@ -66,7 +66,7 @@ pub fn replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !js.Promise
_ = self;
_ = text;
// TODO: clear self.css_rules
return page.js.resolvePromise({});
return page.js.local.?.resolvePromise({});
}
pub fn replaceSync(self: *CSSStyleSheet, text: []const u8) !void {

View File

@@ -39,33 +39,33 @@ pub fn registerTypes() []const type {
pub const Attribute = @This();
_proto: *Node,
_name: []const u8,
_value: []const u8,
_name: String,
_value: String,
_element: ?*Element,
pub fn format(self: *const Attribute, writer: *std.Io.Writer) !void {
return formatAttribute(self._name, self._value, writer);
}
pub fn getName(self: *const Attribute) []const u8 {
pub fn getName(self: *const Attribute) String {
return self._name;
}
pub fn getValue(self: *const Attribute) []const u8 {
pub fn getValue(self: *const Attribute) String {
return self._value;
}
pub fn setValue(self: *Attribute, data_: ?[]const u8, page: *Page) !void {
const data = data_ orelse "";
pub fn setValue(self: *Attribute, data_: ?String, page: *Page) !void {
const data = data_ orelse String.empty;
const el = self._element orelse {
self._value = try page.dupeString(data);
self._value = try data.dupe(page.arena);
return;
};
// this takes ownership of the data
try el.setAttribute(self._name, data, page);
// not the most efficient, but we don't expect this to be called often
self._value = (try el.getAttribute(self._name, page)) orelse "";
self._value = (try el.getAttribute(self._name, page)) orelse String.empty;
}
pub fn getNamespaceURI(_: *const Attribute) ?[]const u8 {
@@ -77,7 +77,7 @@ pub fn getOwnerElement(self: *const Attribute) ?*Element {
}
pub fn isEqualNode(self: *const Attribute, other: *const Attribute) bool {
return std.mem.eql(u8, self.getName(), other.getName()) and std.mem.eql(u8, self.getValue(), other.getValue());
return self.getName().eql(other.getName()) and self.getValue().eql(other.getValue());
}
pub fn clone(self: *const Attribute, page: *Page) !*Attribute {
@@ -139,9 +139,9 @@ pub const List = struct {
return self._list.first == null;
}
pub fn get(self: *const List, name: []const u8, page: *Page) !?[]const u8 {
pub fn get(self: *const List, name: String, page: *Page) !?String {
const entry = (try self.getEntry(name, page)) orelse return null;
return entry._value.str();
return entry._value;
}
pub inline fn length(self: *const List) usize {
@@ -170,17 +170,17 @@ pub const List = struct {
}
// meant for internal usage, where the name is known to be properly cased
pub fn getSafe(self: *const List, name: []const u8) ?[]const u8 {
pub fn getSafe(self: *const List, name: String) ?[]const u8 {
const entry = self.getEntryWithNormalizedName(name) orelse return null;
return entry._value.str();
}
// meant for internal usage, where the name is known to be properly cased
pub fn hasSafe(self: *const List, name: []const u8) bool {
pub fn hasSafe(self: *const List, name: String) bool {
return self.getEntryWithNormalizedName(name) != null;
}
pub fn getAttribute(self: *const List, name: []const u8, element: ?*Element, page: *Page) !?*Attribute {
pub fn getAttribute(self: *const List, name: String, element: ?*Element, page: *Page) !?*Attribute {
const entry = (try self.getEntry(name, page)) orelse return null;
const gop = try page._attribute_lookup.getOrPut(page.arena, @intFromPtr(entry));
if (gop.found_existing) {
@@ -191,33 +191,33 @@ pub const List = struct {
return attribute;
}
pub fn put(self: *List, name: []const u8, value: []const u8, element: *Element, page: *Page) !*Entry {
pub fn put(self: *List, name: String, value: String, element: *Element, page: *Page) !*Entry {
const result = try self.getEntryAndNormalizedName(name, page);
return self._put(result, value, element, page);
}
pub fn putSafe(self: *List, name: []const u8, value: []const u8, element: *Element, page: *Page) !*Entry {
pub fn putSafe(self: *List, name: String, value: String, element: *Element, page: *Page) !*Entry {
const entry = self.getEntryWithNormalizedName(name);
return self._put(.{ .entry = entry, .normalized = name }, value, element, page);
}
fn _put(self: *List, result: NormalizeAndEntry, value: []const u8, element: *Element, page: *Page) !*Entry {
fn _put(self: *List, result: NormalizeAndEntry, value: String, element: *Element, page: *Page) !*Entry {
const is_id = shouldAddToIdMap(result.normalized, element);
var entry: *Entry = undefined;
var old_value: ?[]const u8 = null;
var old_value: ?String = null;
if (result.entry) |e| {
old_value = try page.call_arena.dupe(u8, e._value.str());
old_value = try e._value.dupe(page.call_arena);
if (is_id) {
page.removeElementId(element, e._value.str());
}
e._value = try String.init(page.arena, value, .{});
e._value = try value.dupe(page.arena);
entry = e;
} else {
entry = try page._factory.create(Entry{
._node = .{},
._name = try String.init(page.arena, result.normalized, .{}),
._value = try String.init(page.arena, value, .{}),
._name = try result.normalized.dupe(page.arena),
._value = try value.dupe(page.arena),
});
self._list.append(&entry._node);
self._len += 1;
@@ -230,7 +230,7 @@ pub const List = struct {
try page.addElementId(parent, element, entry._value.str());
}
page.domChanged();
page.attributeChange(element, result.normalized, entry._value.str(), old_value);
page.attributeChange(element, result.normalized, entry._value, old_value);
return entry;
}
@@ -266,7 +266,7 @@ pub const List = struct {
// called form our parser, names already lower-cased
pub fn putNew(self: *List, name: []const u8, value: []const u8, page: *Page) !void {
if (try self.getEntry(name, page) != null) {
if (try self.getEntry(.wrap(name), page) != null) {
// When parsing, if there are dupicate names, it isn't valid, and
// the first is kept
return;
@@ -281,12 +281,12 @@ pub const List = struct {
self._len += 1;
}
pub fn delete(self: *List, name: []const u8, element: *Element, page: *Page) !void {
pub fn delete(self: *List, name: String, element: *Element, page: *Page) !void {
const result = try self.getEntryAndNormalizedName(name, page);
const entry = result.entry orelse return;
const is_id = shouldAddToIdMap(result.normalized, element);
const old_value = entry._value.str();
const old_value = entry._value;
if (is_id) {
page.removeElementId(element, entry._value.str());
@@ -314,7 +314,7 @@ pub const List = struct {
return .{ ._node = self._list.first };
}
fn getEntry(self: *const List, name: []const u8, page: *Page) !?*Entry {
fn getEntry(self: *const List, name: String, page: *Page) !?*Entry {
const result = try self.getEntryAndNormalizedName(name, page);
return result.entry;
}
@@ -322,10 +322,10 @@ pub const List = struct {
// Dangerous, the returned normalized name is only valid until someone
// else uses pages.buf.
const NormalizeAndEntry = struct {
normalized: []const u8,
entry: ?*Entry,
normalized: String,
};
fn getEntryAndNormalizedName(self: *const List, name: []const u8, page: *Page) !NormalizeAndEntry {
fn getEntryAndNormalizedName(self: *const List, name: String, page: *Page) !NormalizeAndEntry {
const normalized =
if (self.normalize) try normalizeNameForLookup(name, page) else name;
@@ -335,11 +335,11 @@ pub const List = struct {
};
}
fn getEntryWithNormalizedName(self: *const List, name: []const u8) ?*Entry {
fn getEntryWithNormalizedName(self: *const List, name: String) ?*Entry {
var node = self._list.first;
while (node) |n| {
var e = Entry.fromNode(n);
if (e._name.eqlSlice(name)) {
if (e._name.eql(name)) {
return e;
}
node = n.next;
@@ -363,7 +363,7 @@ pub const List = struct {
}
pub fn format(self: *const Entry, writer: *std.Io.Writer) !void {
return formatAttribute(self._name.str(), self._value.str(), writer);
return formatAttribute(self._name, self._value, writer);
}
pub fn toAttribute(self: *const Entry, element: ?*Element, page: *Page) !*Attribute {
@@ -373,15 +373,15 @@ pub const List = struct {
// Cannot directly reference self._name.str() and self._value.str()
// This attribute can outlive the list entry (the node can be
// removed from the element's attribute, but still exist in the DOM)
._name = try page.dupeString(self._name.str()),
._value = try page.dupeString(self._value.str()),
._name = try self._name.dupe(page.arena),
._value = try self._value.dupe(page.arena),
});
}
};
};
fn shouldAddToIdMap(normalized_name: []const u8, element: *Element) bool {
if (!std.mem.eql(u8, normalized_name, "id")) {
fn shouldAddToIdMap(normalized_name: String, element: *Element) bool {
if (!normalized_name.eql(comptime .wrap("id"))) {
return false;
}
@@ -394,17 +394,19 @@ fn shouldAddToIdMap(normalized_name: []const u8, element: *Element) bool {
return node.isConnected();
}
pub fn validateAttributeName(name: []const u8) !void {
if (name.len == 0) {
pub fn validateAttributeName(name: String) !void {
const name_str = name.str();
if (name_str.len == 0) {
return error.InvalidCharacterError;
}
const first = name[0];
const first = name_str[0];
if ((first >= '0' and first <= '9') or first == '-' or first == '.') {
return error.InvalidCharacterError;
}
for (name) |c| {
for (name_str) |c| {
if (c == 0 or c == '/' or c == '=' or c == '>' or std.ascii.isWhitespace(c)) {
return error.InvalidCharacterError;
}
@@ -420,14 +422,16 @@ pub fn validateAttributeName(name: []const u8) !void {
}
}
pub fn normalizeNameForLookup(name: []const u8, page: *Page) ![]const u8 {
if (!needsLowerCasing(name)) {
pub fn normalizeNameForLookup(name: String, page: *Page) !String {
if (!needsLowerCasing(name.str())) {
return name;
}
if (name.len < page.buf.len) {
return std.ascii.lowerString(&page.buf, name);
}
return std.ascii.allocLowerString(page.call_arena, name);
const normalized = if (name.len < page.buf.len)
std.ascii.lowerString(&page.buf, name.str())
else
try std.ascii.allocLowerString(page.call_arena, name.str());
return .wrap(normalized);
}
fn needsLowerCasing(name: []const u8) bool {
@@ -481,7 +485,7 @@ pub const NamedNodeMap = struct {
return null;
}
pub fn getByName(self: *const NamedNodeMap, name: []const u8, page: *Page) !?*Attribute {
pub fn getByName(self: *const NamedNodeMap, name: String, page: *Page) !?*Attribute {
return self._list.getAttribute(name, self._element, page);
}
@@ -490,7 +494,7 @@ pub const NamedNodeMap = struct {
return self._list.putAttribute(attribute, self._element, page);
}
pub fn removeByName(self: *const NamedNodeMap, name: []const u8, page: *Page) !?*Attribute {
pub fn removeByName(self: *const NamedNodeMap, name: String, page: *Page) !?*Attribute {
// this 2-step process (get then delete) isn't efficient. But we don't
// expect this to be called often, and this lets us keep delete straightforward.
const attr = (try self.getByName(name, page)) orelse return null;
@@ -556,11 +560,13 @@ pub const InnerIterator = struct {
}
};
fn formatAttribute(name: []const u8, value: []const u8, writer: *std.Io.Writer) !void {
try writer.writeAll(name);
fn formatAttribute(name: String, value_: String, writer: *std.Io.Writer) !void {
try writer.writeAll(name.str());
// Boolean attributes with empty values are serialized without a value
if (value.len == 0 and boolean_attributes_lookup.has(name)) {
const value = value_.str();
if (value.len == 0 and boolean_attributes_lookup.has(name.str())) {
return;
}

View File

@@ -21,6 +21,7 @@ const js = @import("../../js/js.zig");
const Element = @import("../Element.zig");
const Page = @import("../../Page.zig");
const String = @import("../../../string.zig").String;
const Allocator = std.mem.Allocator;
@@ -28,28 +29,60 @@ const DOMStringMap = @This();
_element: *Element,
fn getProperty(self: *DOMStringMap, name: []const u8, page: *Page) !?[]const u8 {
fn getProperty(self: *DOMStringMap, name: String, page: *Page) !?String {
const attr_name = try camelToKebab(page.call_arena, name);
return try self._element.getAttribute(attr_name, page);
}
fn setProperty(self: *DOMStringMap, name: []const u8, value: []const u8, page: *Page) !void {
fn setProperty(self: *DOMStringMap, name: String, value: String, page: *Page) !void {
const attr_name = try camelToKebab(page.call_arena, name);
return self._element.setAttributeSafe(attr_name, value, page);
}
fn deleteProperty(self: *DOMStringMap, name: []const u8, page: *Page) !void {
fn deleteProperty(self: *DOMStringMap, name: String, page: *Page) !void {
const attr_name = try camelToKebab(page.call_arena, name);
try self._element.removeAttribute(attr_name, page);
}
// fooBar -> foo-bar
fn camelToKebab(arena: Allocator, camel: []const u8) ![]const u8 {
// fooBar -> data-foo-bar (with SSO optimization for short strings)
fn camelToKebab(arena: Allocator, camel: String) !String {
const camel_str = camel.str();
// Calculate output length
var output_len: usize = 5; // "data-"
for (camel_str, 0..) |c, i| {
output_len += 1;
if (std.ascii.isUpper(c) and i > 0) output_len += 1; // extra char for '-'
}
if (output_len <= 12) {
// SSO path - no allocation!
var content: [12]u8 = @splat(0);
@memcpy(content[0..5], "data-");
var idx: usize = 5;
for (camel_str, 0..) |c, i| {
if (std.ascii.isUpper(c)) {
if (i > 0) {
content[idx] = '-';
idx += 1;
}
content[idx] = std.ascii.toLower(c);
} else {
content[idx] = c;
}
idx += 1;
}
return .{ .len = @intCast(output_len), .payload = .{ .content = content } };
}
// Fallback: allocate for longer strings
var result: std.ArrayList(u8) = .empty;
try result.ensureTotalCapacity(arena, 5 + camel.len * 2);
try result.ensureTotalCapacity(arena, output_len);
result.appendSliceAssumeCapacity("data-");
for (camel, 0..) |c, i| {
for (camel_str, 0..) |c, i| {
if (std.ascii.isUpper(c)) {
if (i > 0) {
result.appendAssumeCapacity('-');
@@ -60,7 +93,7 @@ fn camelToKebab(arena: Allocator, camel: []const u8) ![]const u8 {
}
}
return result.items;
return try String.init(arena, result.items, .{});
}
// data-foo-bar -> fooBar

View File

@@ -243,7 +243,7 @@ fn _getInnerText(self: *HtmlElement, writer: *std.Io.Writer, state: *innerTextSt
.document => {},
.document_type => {},
.document_fragment => {},
.attribute => |attr| try writer.writeAll(attr._value),
.attribute => |attr| try writer.writeAll(attr._value.str()),
}
}
}
@@ -281,8 +281,11 @@ pub fn insertAdjacentHTML(
});
const doc_node = doc.asNode();
const arena = try page.getArena(.{ .debug = "HTML.insertAdjacentHTML" });
defer page.releaseArena(arena);
const Parser = @import("../../parser/Parser.zig");
var parser = Parser.init(page.call_arena, doc_node, page);
var parser = Parser.init(arena, doc_node, page);
parser.parse(html);
// Check if there's parsing error.
@@ -290,22 +293,23 @@ pub fn insertAdjacentHTML(
return error.Invalid;
}
// We always get it wrapped like so:
// <html><head></head><body>{ ... }</body></html>
// None of the following can be null.
const maybe_html_node = doc_node.firstChild();
lp.assert(maybe_html_node != null, "Html.insertAdjacentHTML null html", .{});
const html_node = maybe_html_node orelse return;
const maybe_body_node = html_node.lastChild();
lp.assert(maybe_body_node != null, "Html.insertAdjacentHTML null bodys", .{});
const body = maybe_body_node orelse return;
// The parser wraps content in a document structure:
// - Typical: <html><head>...</head><body>...</body></html>
// - Head-only: <html><head><meta></head></html> (no body)
// - Empty/comments: May have no <html> element at all
const html_node = doc_node.firstChild() orelse return;
const target_node, const prev_node = try self.asElement().asNode().findAdjacentNodes(position);
var iter = body.childrenIterator();
while (iter.next()) |child_node| {
_ = try target_node.insertBefore(child_node, prev_node, page);
// Iterate through all children of <html> (typically <head> and/or <body>)
// and insert their children (not the containers themselves) into the target.
// This handles both body content AND head-only elements like <meta>, <title>, etc.
var html_children = html_node.childrenIterator();
while (html_children.next()) |container| {
var iter = container.childrenIterator();
while (iter.next()) |child_node| {
_ = try target_node.insertBefore(child_node, prev_node, page);
}
}
}
@@ -329,6 +333,766 @@ pub fn click(self: *HtmlElement, page: *Page) !void {
try page._event_manager.dispatch(self.asEventTarget(), event.asEvent());
}
pub fn setOnAbort(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .abort, callback);
}
pub fn getOnAbort(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .abort);
}
pub fn setOnAnimationCancel(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .animationcancel, callback);
}
pub fn getOnAnimationCancel(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .animationcancel);
}
pub fn setOnAnimationEnd(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .animationend, callback);
}
pub fn getOnAnimationEnd(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .animationend);
}
pub fn setOnAnimationIteration(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .animationiteration, callback);
}
pub fn getOnAnimationIteration(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .animationiteration);
}
pub fn setOnAnimationStart(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .animationstart, callback);
}
pub fn getOnAnimationStart(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .animationstart);
}
pub fn setOnAuxClick(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .auxclick, callback);
}
pub fn getOnAuxClick(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .auxclick);
}
pub fn setOnBeforeInput(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .beforeinput, callback);
}
pub fn getOnBeforeInput(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .beforeinput);
}
pub fn setOnBeforeMatch(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .beforematch, callback);
}
pub fn getOnBeforeMatch(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .beforematch);
}
pub fn setOnBeforeToggle(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .beforetoggle, callback);
}
pub fn getOnBeforeToggle(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .beforetoggle);
}
pub fn setOnBlur(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .blur, callback);
}
pub fn getOnBlur(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .blur);
}
pub fn setOnCancel(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .cancel, callback);
}
pub fn getOnCancel(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .cancel);
}
pub fn setOnCanPlay(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .canplay, callback);
}
pub fn getOnCanPlay(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .canplay);
}
pub fn setOnCanPlayThrough(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .canplaythrough, callback);
}
pub fn getOnCanPlayThrough(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .canplaythrough);
}
pub fn setOnChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .change, callback);
}
pub fn getOnChange(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .change);
}
pub fn setOnClick(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .click, callback);
}
pub fn getOnClick(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .click);
}
pub fn setOnClose(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .close, callback);
}
pub fn getOnClose(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .close);
}
pub fn setOnCommand(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .command, callback);
}
pub fn getOnCommand(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .command);
}
pub fn setOnContentVisibilityAutoStateChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .contentvisibilityautostatechange, callback);
}
pub fn getOnContentVisibilityAutoStateChange(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .contentvisibilityautostatechange);
}
pub fn setOnContextLost(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .contextlost, callback);
}
pub fn getOnContextLost(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .contextlost);
}
pub fn setOnContextMenu(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .contextmenu, callback);
}
pub fn getOnContextMenu(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .contextmenu);
}
pub fn setOnContextRestored(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .contextrestored, callback);
}
pub fn getOnContextRestored(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .contextrestored);
}
pub fn setOnCopy(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .copy, callback);
}
pub fn getOnCopy(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .copy);
}
pub fn setOnCueChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .cuechange, callback);
}
pub fn getOnCueChange(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .cuechange);
}
pub fn setOnCut(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .cut, callback);
}
pub fn getOnCut(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .cut);
}
pub fn setOnDblClick(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .dblclick, callback);
}
pub fn getOnDblClick(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .dblclick);
}
pub fn setOnDrag(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .drag, callback);
}
pub fn getOnDrag(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .drag);
}
pub fn setOnDragEnd(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .dragend, callback);
}
pub fn getOnDragEnd(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .dragend);
}
pub fn setOnDragEnter(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .dragenter, callback);
}
pub fn getOnDragEnter(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .dragenter);
}
pub fn setOnDragExit(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .dragexit, callback);
}
pub fn getOnDragExit(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .dragexit);
}
pub fn setOnDragLeave(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .dragleave, callback);
}
pub fn getOnDragLeave(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .dragleave);
}
pub fn setOnDragOver(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .dragover, callback);
}
pub fn getOnDragOver(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .dragover);
}
pub fn setOnDragStart(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .dragstart, callback);
}
pub fn getOnDragStart(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .dragstart);
}
pub fn setOnDrop(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .drop, callback);
}
pub fn getOnDrop(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .drop);
}
pub fn setOnDurationChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .durationchange, callback);
}
pub fn getOnDurationChange(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .durationchange);
}
pub fn setOnEmptied(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .emptied, callback);
}
pub fn getOnEmptied(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .emptied);
}
pub fn setOnEnded(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .ended, callback);
}
pub fn getOnEnded(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .ended);
}
pub fn setOnError(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .@"error", callback);
}
pub fn getOnError(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .@"error");
}
pub fn setOnFocus(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .focus, callback);
}
pub fn getOnFocus(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .focus);
}
pub fn setOnFormData(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .formdata, callback);
}
pub fn getOnFormData(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .formdata);
}
pub fn setOnFullscreenChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .fullscreenchange, callback);
}
pub fn getOnFullscreenChange(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .fullscreenchange);
}
pub fn setOnFullscreenError(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .fullscreenerror, callback);
}
pub fn getOnFullscreenError(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .fullscreenerror);
}
pub fn setOnGotPointerCapture(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .gotpointercapture, callback);
}
pub fn getOnGotPointerCapture(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .gotpointercapture);
}
pub fn setOnInput(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .input, callback);
}
pub fn getOnInput(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .input);
}
pub fn setOnInvalid(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .invalid, callback);
}
pub fn getOnInvalid(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .invalid);
}
pub fn setOnKeyDown(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .keydown, callback);
}
pub fn getOnKeyDown(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .keydown);
}
pub fn setOnKeyPress(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .keypress, callback);
}
pub fn getOnKeyPress(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .keypress);
}
pub fn setOnKeyUp(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .keyup, callback);
}
pub fn getOnKeyUp(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .keyup);
}
pub fn setOnLoad(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .load, callback);
}
pub fn getOnLoad(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .load);
}
pub fn setOnLoadedData(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .loadeddata, callback);
}
pub fn getOnLoadedData(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .loadeddata);
}
pub fn setOnLoadedMetadata(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .loadedmetadata, callback);
}
pub fn getOnLoadedMetadata(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .loadedmetadata);
}
pub fn setOnLoadStart(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .loadstart, callback);
}
pub fn getOnLoadStart(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .loadstart);
}
pub fn setOnLostPointerCapture(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .lostpointercapture, callback);
}
pub fn getOnLostPointerCapture(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .lostpointercapture);
}
pub fn setOnMouseDown(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .mousedown, callback);
}
pub fn getOnMouseDown(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .mousedown);
}
pub fn setOnMouseMove(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .mousemove, callback);
}
pub fn getOnMouseMove(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .mousemove);
}
pub fn setOnMouseOut(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .mouseout, callback);
}
pub fn getOnMouseOut(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .mouseout);
}
pub fn setOnMouseOver(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .mouseover, callback);
}
pub fn getOnMouseOver(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .mouseover);
}
pub fn setOnMouseUp(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .mouseup, callback);
}
pub fn getOnMouseUp(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .mouseup);
}
pub fn setOnPaste(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .paste, callback);
}
pub fn getOnPaste(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .paste);
}
pub fn setOnPause(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .pause, callback);
}
pub fn getOnPause(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .pause);
}
pub fn setOnPlay(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .play, callback);
}
pub fn getOnPlay(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .play);
}
pub fn setOnPlaying(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .playing, callback);
}
pub fn getOnPlaying(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .playing);
}
pub fn setOnPointerCancel(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .pointercancel, callback);
}
pub fn getOnPointerCancel(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .pointercancel);
}
pub fn setOnPointerDown(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .pointerdown, callback);
}
pub fn getOnPointerDown(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .pointerdown);
}
pub fn setOnPointerEnter(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .pointerenter, callback);
}
pub fn getOnPointerEnter(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .pointerenter);
}
pub fn setOnPointerLeave(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .pointerleave, callback);
}
pub fn getOnPointerLeave(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .pointerleave);
}
pub fn setOnPointerMove(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .pointermove, callback);
}
pub fn getOnPointerMove(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .pointermove);
}
pub fn setOnPointerOut(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .pointerout, callback);
}
pub fn getOnPointerOut(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .pointerout);
}
pub fn setOnPointerOver(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .pointerover, callback);
}
pub fn getOnPointerOver(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .pointerover);
}
pub fn setOnPointerRawUpdate(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .pointerrawupdate, callback);
}
pub fn getOnPointerRawUpdate(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .pointerrawupdate);
}
pub fn setOnPointerUp(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .pointerup, callback);
}
pub fn getOnPointerUp(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .pointerup);
}
pub fn setOnProgress(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .progress, callback);
}
pub fn getOnProgress(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .progress);
}
pub fn setOnRateChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .ratechange, callback);
}
pub fn getOnRateChange(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .ratechange);
}
pub fn setOnReset(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .reset, callback);
}
pub fn getOnReset(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .reset);
}
pub fn setOnResize(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .resize, callback);
}
pub fn getOnResize(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .resize);
}
pub fn setOnScroll(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .scroll, callback);
}
pub fn getOnScroll(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .scroll);
}
pub fn setOnScrollEnd(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .scrollend, callback);
}
pub fn getOnScrollEnd(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .scrollend);
}
pub fn setOnSecurityPolicyViolation(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .securitypolicyviolation, callback);
}
pub fn getOnSecurityPolicyViolation(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .securitypolicyviolation);
}
pub fn setOnSeeked(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .seeked, callback);
}
pub fn getOnSeeked(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .seeked);
}
pub fn setOnSeeking(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .seeking, callback);
}
pub fn getOnSeeking(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .seeking);
}
pub fn setOnSelect(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .select, callback);
}
pub fn getOnSelect(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .select);
}
pub fn setOnSelectionChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .selectionchange, callback);
}
pub fn getOnSelectionChange(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .selectionchange);
}
pub fn setOnSelectStart(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .selectstart, callback);
}
pub fn getOnSelectStart(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .selectstart);
}
pub fn setOnSlotChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .slotchange, callback);
}
pub fn getOnSlotChange(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .slotchange);
}
pub fn setOnStalled(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .stalled, callback);
}
pub fn getOnStalled(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .stalled);
}
pub fn setOnSubmit(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .submit, callback);
}
pub fn getOnSubmit(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .submit);
}
pub fn setOnSuspend(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .@"suspend", callback);
}
pub fn getOnSuspend(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .@"suspend");
}
pub fn setOnTimeUpdate(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .timeupdate, callback);
}
pub fn getOnTimeUpdate(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .timeupdate);
}
pub fn setOnToggle(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .toggle, callback);
}
pub fn getOnToggle(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .toggle);
}
pub fn setOnTransitionCancel(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .transitioncancel, callback);
}
pub fn getOnTransitionCancel(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .transitioncancel);
}
pub fn setOnTransitionEnd(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .transitionend, callback);
}
pub fn getOnTransitionEnd(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .transitionend);
}
pub fn setOnTransitionRun(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .transitionrun, callback);
}
pub fn getOnTransitionRun(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .transitionrun);
}
pub fn setOnTransitionStart(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .transitionstart, callback);
}
pub fn getOnTransitionStart(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .transitionstart);
}
pub fn setOnVolumeChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .volumechange, callback);
}
pub fn getOnVolumeChange(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .volumechange);
}
pub fn setOnWaiting(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .waiting, callback);
}
pub fn getOnWaiting(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .waiting);
}
pub fn setOnWheel(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void {
return page.setAttrListener(self.asElement(), .wheel, callback);
}
pub fn getOnWheel(self: *HtmlElement, page: *Page) ?js.Function.Global {
return page.getAttrListener(self.asElement(), .wheel);
}
pub const JsApi = struct {
pub const bridge = js.Bridge(HtmlElement);
@@ -348,6 +1112,102 @@ pub const JsApi = struct {
}
pub const insertAdjacentHTML = bridge.function(HtmlElement.insertAdjacentHTML, .{ .dom_exception = true });
pub const click = bridge.function(HtmlElement.click, .{});
pub const onabort = bridge.accessor(HtmlElement.getOnAbort, HtmlElement.setOnAbort, .{});
pub const onanimationcancel = bridge.accessor(HtmlElement.getOnAnimationCancel, HtmlElement.setOnAnimationCancel, .{});
pub const onanimationend = bridge.accessor(HtmlElement.getOnAnimationEnd, HtmlElement.setOnAnimationEnd, .{});
pub const onanimationiteration = bridge.accessor(HtmlElement.getOnAnimationIteration, HtmlElement.setOnAnimationIteration, .{});
pub const onanimationstart = bridge.accessor(HtmlElement.getOnAnimationStart, HtmlElement.setOnAnimationStart, .{});
pub const onauxclick = bridge.accessor(HtmlElement.getOnAuxClick, HtmlElement.setOnAuxClick, .{});
pub const onbeforeinput = bridge.accessor(HtmlElement.getOnBeforeInput, HtmlElement.setOnBeforeInput, .{});
pub const onbeforematch = bridge.accessor(HtmlElement.getOnBeforeMatch, HtmlElement.setOnBeforeMatch, .{});
pub const onbeforetoggle = bridge.accessor(HtmlElement.getOnBeforeToggle, HtmlElement.setOnBeforeToggle, .{});
pub const onblur = bridge.accessor(HtmlElement.getOnBlur, HtmlElement.setOnBlur, .{});
pub const oncancel = bridge.accessor(HtmlElement.getOnCancel, HtmlElement.setOnCancel, .{});
pub const oncanplay = bridge.accessor(HtmlElement.getOnCanPlay, HtmlElement.setOnCanPlay, .{});
pub const oncanplaythrough = bridge.accessor(HtmlElement.getOnCanPlayThrough, HtmlElement.setOnCanPlayThrough, .{});
pub const onchange = bridge.accessor(HtmlElement.getOnChange, HtmlElement.setOnChange, .{});
pub const onclick = bridge.accessor(HtmlElement.getOnClick, HtmlElement.setOnClick, .{});
pub const onclose = bridge.accessor(HtmlElement.getOnClose, HtmlElement.setOnClose, .{});
pub const oncommand = bridge.accessor(HtmlElement.getOnCommand, HtmlElement.setOnCommand, .{});
pub const oncontentvisibilityautostatechange = bridge.accessor(HtmlElement.getOnContentVisibilityAutoStateChange, HtmlElement.setOnContentVisibilityAutoStateChange, .{});
pub const oncontextlost = bridge.accessor(HtmlElement.getOnContextLost, HtmlElement.setOnContextLost, .{});
pub const oncontextmenu = bridge.accessor(HtmlElement.getOnContextMenu, HtmlElement.setOnContextMenu, .{});
pub const oncontextrestored = bridge.accessor(HtmlElement.getOnContextRestored, HtmlElement.setOnContextRestored, .{});
pub const oncopy = bridge.accessor(HtmlElement.getOnCopy, HtmlElement.setOnCopy, .{});
pub const oncuechange = bridge.accessor(HtmlElement.getOnCueChange, HtmlElement.setOnCueChange, .{});
pub const oncut = bridge.accessor(HtmlElement.getOnCut, HtmlElement.setOnCut, .{});
pub const ondblclick = bridge.accessor(HtmlElement.getOnDblClick, HtmlElement.setOnDblClick, .{});
pub const ondrag = bridge.accessor(HtmlElement.getOnDrag, HtmlElement.setOnDrag, .{});
pub const ondragend = bridge.accessor(HtmlElement.getOnDragEnd, HtmlElement.setOnDragEnd, .{});
pub const ondragenter = bridge.accessor(HtmlElement.getOnDragEnter, HtmlElement.setOnDragEnter, .{});
pub const ondragexit = bridge.accessor(HtmlElement.getOnDragExit, HtmlElement.setOnDragExit, .{});
pub const ondragleave = bridge.accessor(HtmlElement.getOnDragLeave, HtmlElement.setOnDragLeave, .{});
pub const ondragover = bridge.accessor(HtmlElement.getOnDragOver, HtmlElement.setOnDragOver, .{});
pub const ondragstart = bridge.accessor(HtmlElement.getOnDragStart, HtmlElement.setOnDragStart, .{});
pub const ondrop = bridge.accessor(HtmlElement.getOnDrop, HtmlElement.setOnDrop, .{});
pub const ondurationchange = bridge.accessor(HtmlElement.getOnDurationChange, HtmlElement.setOnDurationChange, .{});
pub const onemptied = bridge.accessor(HtmlElement.getOnEmptied, HtmlElement.setOnEmptied, .{});
pub const onended = bridge.accessor(HtmlElement.getOnEnded, HtmlElement.setOnEnded, .{});
pub const onerror = bridge.accessor(HtmlElement.getOnError, HtmlElement.setOnError, .{});
pub const onfocus = bridge.accessor(HtmlElement.getOnFocus, HtmlElement.setOnFocus, .{});
pub const onformdata = bridge.accessor(HtmlElement.getOnFormData, HtmlElement.setOnFormData, .{});
pub const onfullscreenchange = bridge.accessor(HtmlElement.getOnFullscreenChange, HtmlElement.setOnFullscreenChange, .{});
pub const onfullscreenerror = bridge.accessor(HtmlElement.getOnFullscreenError, HtmlElement.setOnFullscreenError, .{});
pub const ongotpointercapture = bridge.accessor(HtmlElement.getOnGotPointerCapture, HtmlElement.setOnGotPointerCapture, .{});
pub const oninput = bridge.accessor(HtmlElement.getOnInput, HtmlElement.setOnInput, .{});
pub const oninvalid = bridge.accessor(HtmlElement.getOnInvalid, HtmlElement.setOnInvalid, .{});
pub const onkeydown = bridge.accessor(HtmlElement.getOnKeyDown, HtmlElement.setOnKeyDown, .{});
pub const onkeypress = bridge.accessor(HtmlElement.getOnKeyPress, HtmlElement.setOnKeyPress, .{});
pub const onkeyup = bridge.accessor(HtmlElement.getOnKeyUp, HtmlElement.setOnKeyUp, .{});
pub const onload = bridge.accessor(HtmlElement.getOnLoad, HtmlElement.setOnLoad, .{});
pub const onloadeddata = bridge.accessor(HtmlElement.getOnLoadedData, HtmlElement.setOnLoadedData, .{});
pub const onloadedmetadata = bridge.accessor(HtmlElement.getOnLoadedMetadata, HtmlElement.setOnLoadedMetadata, .{});
pub const onloadstart = bridge.accessor(HtmlElement.getOnLoadStart, HtmlElement.setOnLoadStart, .{});
pub const onlostpointercapture = bridge.accessor(HtmlElement.getOnLostPointerCapture, HtmlElement.setOnLostPointerCapture, .{});
pub const onmousedown = bridge.accessor(HtmlElement.getOnMouseDown, HtmlElement.setOnMouseDown, .{});
pub const onmousemove = bridge.accessor(HtmlElement.getOnMouseMove, HtmlElement.setOnMouseMove, .{});
pub const onmouseout = bridge.accessor(HtmlElement.getOnMouseOut, HtmlElement.setOnMouseOut, .{});
pub const onmouseover = bridge.accessor(HtmlElement.getOnMouseOver, HtmlElement.setOnMouseOver, .{});
pub const onmouseup = bridge.accessor(HtmlElement.getOnMouseUp, HtmlElement.setOnMouseUp, .{});
pub const onpaste = bridge.accessor(HtmlElement.getOnPaste, HtmlElement.setOnPaste, .{});
pub const onpause = bridge.accessor(HtmlElement.getOnPause, HtmlElement.setOnPause, .{});
pub const onplay = bridge.accessor(HtmlElement.getOnPlay, HtmlElement.setOnPlay, .{});
pub const onplaying = bridge.accessor(HtmlElement.getOnPlaying, HtmlElement.setOnPlaying, .{});
pub const onpointercancel = bridge.accessor(HtmlElement.getOnPointerCancel, HtmlElement.setOnPointerCancel, .{});
pub const onpointerdown = bridge.accessor(HtmlElement.getOnPointerDown, HtmlElement.setOnPointerDown, .{});
pub const onpointerenter = bridge.accessor(HtmlElement.getOnPointerEnter, HtmlElement.setOnPointerEnter, .{});
pub const onpointerleave = bridge.accessor(HtmlElement.getOnPointerLeave, HtmlElement.setOnPointerLeave, .{});
pub const onpointermove = bridge.accessor(HtmlElement.getOnPointerMove, HtmlElement.setOnPointerMove, .{});
pub const onpointerout = bridge.accessor(HtmlElement.getOnPointerOut, HtmlElement.setOnPointerOut, .{});
pub const onpointerover = bridge.accessor(HtmlElement.getOnPointerOver, HtmlElement.setOnPointerOver, .{});
pub const onpointerrawupdate = bridge.accessor(HtmlElement.getOnPointerRawUpdate, HtmlElement.setOnPointerRawUpdate, .{});
pub const onpointerup = bridge.accessor(HtmlElement.getOnPointerUp, HtmlElement.setOnPointerUp, .{});
pub const onprogress = bridge.accessor(HtmlElement.getOnProgress, HtmlElement.setOnProgress, .{});
pub const onratechange = bridge.accessor(HtmlElement.getOnRateChange, HtmlElement.setOnRateChange, .{});
pub const onreset = bridge.accessor(HtmlElement.getOnReset, HtmlElement.setOnReset, .{});
pub const onresize = bridge.accessor(HtmlElement.getOnResize, HtmlElement.setOnResize, .{});
pub const onscroll = bridge.accessor(HtmlElement.getOnScroll, HtmlElement.setOnScroll, .{});
pub const onscrollend = bridge.accessor(HtmlElement.getOnScrollEnd, HtmlElement.setOnScrollEnd, .{});
pub const onsecuritypolicyviolation = bridge.accessor(HtmlElement.getOnSecurityPolicyViolation, HtmlElement.setOnSecurityPolicyViolation, .{});
pub const onseeked = bridge.accessor(HtmlElement.getOnSeeked, HtmlElement.setOnSeeked, .{});
pub const onseeking = bridge.accessor(HtmlElement.getOnSeeking, HtmlElement.setOnSeeking, .{});
pub const onselect = bridge.accessor(HtmlElement.getOnSelect, HtmlElement.setOnSelect, .{});
pub const onselectionchange = bridge.accessor(HtmlElement.getOnSelectionChange, HtmlElement.setOnSelectionChange, .{});
pub const onselectstart = bridge.accessor(HtmlElement.getOnSelectStart, HtmlElement.setOnSelectStart, .{});
pub const onslotchange = bridge.accessor(HtmlElement.getOnSlotChange, HtmlElement.setOnSlotChange, .{});
pub const onstalled = bridge.accessor(HtmlElement.getOnStalled, HtmlElement.setOnStalled, .{});
pub const onsubmit = bridge.accessor(HtmlElement.getOnSubmit, HtmlElement.setOnSubmit, .{});
pub const onsuspend = bridge.accessor(HtmlElement.getOnSuspend, HtmlElement.setOnSuspend, .{});
pub const ontimeupdate = bridge.accessor(HtmlElement.getOnTimeUpdate, HtmlElement.setOnTimeUpdate, .{});
pub const ontoggle = bridge.accessor(HtmlElement.getOnToggle, HtmlElement.setOnToggle, .{});
pub const ontransitioncancel = bridge.accessor(HtmlElement.getOnTransitionCancel, HtmlElement.setOnTransitionCancel, .{});
pub const ontransitionend = bridge.accessor(HtmlElement.getOnTransitionEnd, HtmlElement.setOnTransitionEnd, .{});
pub const ontransitionrun = bridge.accessor(HtmlElement.getOnTransitionRun, HtmlElement.setOnTransitionRun, .{});
pub const ontransitionstart = bridge.accessor(HtmlElement.getOnTransitionStart, HtmlElement.setOnTransitionStart, .{});
pub const onvolumechange = bridge.accessor(HtmlElement.getOnVolumeChange, HtmlElement.setOnVolumeChange, .{});
pub const onwaiting = bridge.accessor(HtmlElement.getOnWaiting, HtmlElement.setOnWaiting, .{});
pub const onwheel = bridge.accessor(HtmlElement.getOnWheel, HtmlElement.setOnWheel, .{});
};
pub const Build = struct {
@@ -378,3 +1238,8 @@ pub const Build = struct {
return false;
}
};
const testing = @import("../../../testing.zig");
test "WebApi: HTML.event_listeners" {
try testing.htmlRunner("element/html/event_listeners.html", .{});
}

View File

@@ -40,7 +40,7 @@ pub fn asNode(self: *Anchor) *Node {
pub fn getHref(self: *Anchor, page: *Page) ![]const u8 {
const element = self.asElement();
const href = element.getAttributeSafe("href") orelse return "";
const href = element.getAttributeSafe(comptime .wrap("href")) orelse return "";
if (href.len == 0) {
return "";
}
@@ -48,15 +48,15 @@ pub fn getHref(self: *Anchor, page: *Page) ![]const u8 {
}
pub fn setHref(self: *Anchor, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("href", value, page);
try self.asElement().setAttributeSafe(comptime .wrap("href"), .wrap(value), page);
}
pub fn getTarget(self: *Anchor) []const u8 {
return self.asElement().getAttributeSafe("target") orelse "";
return self.asElement().getAttributeSafe(comptime .wrap("target")) orelse "";
}
pub fn setTarget(self: *Anchor, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("target", value, page);
try self.asElement().setAttributeSafe(comptime .wrap("target"), .wrap(value), page);
}
pub fn getOrigin(self: *Anchor, page: *Page) ![]const u8 {
@@ -167,19 +167,19 @@ pub fn setProtocol(self: *Anchor, value: []const u8, page: *Page) !void {
}
pub fn getType(self: *Anchor) []const u8 {
return self.asElement().getAttributeSafe("type") orelse "";
return self.asElement().getAttributeSafe(comptime .wrap("type")) orelse "";
}
pub fn setType(self: *Anchor, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("type", value, page);
try self.asElement().setAttributeSafe(comptime .wrap("type"), .wrap(value), page);
}
pub fn getName(self: *const Anchor) []const u8 {
return self.asConstElement().getAttributeSafe("name") orelse "";
return self.asConstElement().getAttributeSafe(comptime .wrap("name")) orelse "";
}
pub fn setName(self: *Anchor, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("name", value, page);
try self.asElement().setAttributeSafe(comptime .wrap("name"), .wrap(value), page);
}
pub fn getText(self: *Anchor, page: *Page) ![:0]const u8 {
@@ -191,7 +191,7 @@ pub fn setText(self: *Anchor, value: []const u8, page: *Page) !void {
}
fn getResolvedHref(self: *Anchor, page: *Page) !?[:0]const u8 {
const href = self.asElement().getAttributeSafe("href") orelse return null;
const href = self.asElement().getAttributeSafe(comptime .wrap("href")) orelse return null;
if (href.len == 0) {
return null;
}

View File

@@ -16,6 +16,8 @@
// 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 String = @import("../../../../string.zig").String;
const js = @import("../../../js/js.zig");
const Page = @import("../../../Page.zig");
@@ -27,16 +29,16 @@ const Audio = @This();
_proto: *Media,
pub fn constructor(maybe_url: ?[]const u8, page: *Page) !*Media {
pub fn constructor(maybe_url: ?String, page: *Page) !*Media {
const node = try page.createElementNS(.html, "audio", null);
const el = node.as(Element);
const list = try el.getOrCreateAttributeList(page);
// Always set to "auto" initially.
_ = try list.putSafe("preload", "auto", el, page);
_ = try list.putSafe(comptime .wrap("preload"), comptime .wrap("auto"), el, page);
// Set URL if provided.
if (maybe_url) |url| {
_ = try list.putSafe("src", url, el, page);
_ = try list.putSafe(comptime .wrap("src"), url, el, page);
}
return node.as(Media);

View File

@@ -49,9 +49,9 @@ pub const JsApi = struct {
pub const Build = struct {
pub fn complete(node: *Node, page: *Page) !void {
const el = node.as(Element);
const on_load = el.getAttributeSafe("onload") orelse return;
if (page.js.stringToFunction(on_load)) |func| {
page.window._on_load = try func.persist();
const on_load = el.getAttributeSafe(comptime .wrap("onload")) orelse return;
if (page.js.stringToPersistedFunction(on_load)) |func| {
page.window._on_load = func;
} else |err| {
log.err(.js, "body.onload", .{ .err = err, .str = on_load });
}

View File

@@ -39,50 +39,50 @@ pub fn asNode(self: *Button) *Node {
}
pub fn getDisabled(self: *const Button) bool {
return self.asConstElement().getAttributeSafe("disabled") != null;
return self.asConstElement().getAttributeSafe(comptime .wrap("disabled")) != null;
}
pub fn setDisabled(self: *Button, disabled: bool, page: *Page) !void {
if (disabled) {
try self.asElement().setAttributeSafe("disabled", "", page);
try self.asElement().setAttributeSafe(comptime .wrap("disabled"), .wrap(""), page);
} else {
try self.asElement().removeAttribute("disabled", page);
try self.asElement().removeAttribute(comptime .wrap("disabled"), page);
}
}
pub fn getName(self: *const Button) []const u8 {
return self.asConstElement().getAttributeSafe("name") orelse "";
return self.asConstElement().getAttributeSafe(comptime .wrap("name")) orelse "";
}
pub fn setName(self: *Button, name: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("name", name, page);
try self.asElement().setAttributeSafe(comptime .wrap("name"), .wrap(name), page);
}
pub fn getType(self: *const Button) []const u8 {
return self.asConstElement().getAttributeSafe("type") orelse "submit";
return self.asConstElement().getAttributeSafe(comptime .wrap("type")) orelse "submit";
}
pub fn setType(self: *Button, typ: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("type", typ, page);
try self.asElement().setAttributeSafe(comptime .wrap("type"), .wrap(typ), page);
}
pub fn getValue(self: *const Button) []const u8 {
return self.asConstElement().getAttributeSafe("value") orelse "";
return self.asConstElement().getAttributeSafe(comptime .wrap("value")) orelse "";
}
pub fn setValue(self: *Button, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("value", value, page);
try self.asElement().setAttributeSafe(comptime .wrap("value"), .wrap(value), page);
}
pub fn getRequired(self: *const Button) bool {
return self.asConstElement().getAttributeSafe("required") != null;
return self.asConstElement().getAttributeSafe(comptime .wrap("required")) != null;
}
pub fn setRequired(self: *Button, required: bool, page: *Page) !void {
if (required) {
try self.asElement().setAttributeSafe("required", "", page);
try self.asElement().setAttributeSafe(comptime .wrap("required"), .wrap(""), page);
} else {
try self.asElement().removeAttribute("required", page);
try self.asElement().removeAttribute(comptime .wrap("required"), page);
}
}
@@ -90,7 +90,7 @@ pub fn getForm(self: *Button, page: *Page) ?*Form {
const element = self.asElement();
// If form attribute exists, ONLY use that (even if it references nothing)
if (element.getAttributeSafe("form")) |form_id| {
if (element.getAttributeSafe(comptime .wrap("form"))) |form_id| {
if (page.document.getElementById(form_id, page)) |form_element| {
return form_element.is(Form);
}

View File

@@ -40,23 +40,23 @@ pub fn asNode(self: *Canvas) *Node {
}
pub fn getWidth(self: *const Canvas) u32 {
const attr = self.asConstElement().getAttributeSafe("width") orelse return 300;
const attr = self.asConstElement().getAttributeSafe(comptime .wrap("width")) orelse return 300;
return std.fmt.parseUnsigned(u32, attr, 10) catch 300;
}
pub fn setWidth(self: *Canvas, value: u32, page: *Page) !void {
const str = try std.fmt.allocPrint(page.call_arena, "{d}", .{value});
try self.asElement().setAttributeSafe("width", str, page);
try self.asElement().setAttributeSafe(comptime .wrap("width"), .wrap(str), page);
}
pub fn getHeight(self: *const Canvas) u32 {
const attr = self.asConstElement().getAttributeSafe("height") orelse return 150;
const attr = self.asConstElement().getAttributeSafe(comptime .wrap("height")) orelse return 150;
return std.fmt.parseUnsigned(u32, attr, 10) catch 150;
}
pub fn setHeight(self: *Canvas, value: u32, page: *Page) !void {
const str = try std.fmt.allocPrint(page.call_arena, "{d}", .{value});
try self.asElement().setAttributeSafe("height", str, page);
try self.asElement().setAttributeSafe(comptime .wrap("height"), .wrap(str), page);
}
/// Since there's no base class rendering contextes inherit from,

View File

@@ -64,7 +64,7 @@ pub fn invokeDisconnectedCallback(self: *Custom, page: *Page) void {
self.invokeCallback("disconnectedCallback", .{}, page);
}
pub fn invokeAttributeChangedCallback(self: *Custom, name: []const u8, old_value: ?[]const u8, new_value: ?[]const u8, page: *Page) void {
pub fn invokeAttributeChangedCallback(self: *Custom, name: String, old_value: ?String, new_value: ?String, page: *Page) void {
const definition = self._definition orelse return;
if (!definition.isAttributeObserved(name)) {
return;
@@ -144,7 +144,7 @@ pub fn invokeDisconnectedCallbackOnElement(element: *Element, page: *Page) void
invokeCallbackOnElement(element, definition, "disconnectedCallback", .{}, page);
}
pub fn invokeAttributeChangedCallbackOnElement(element: *Element, name: []const u8, old_value: ?[]const u8, new_value: ?[]const u8, page: *Page) void {
pub fn invokeAttributeChangedCallbackOnElement(element: *Element, name: String, old_value: ?String, new_value: ?String, page: *Page) void {
// Autonomous custom element
if (element.is(Custom)) |custom| {
custom.invokeAttributeChangedCallback(name, old_value, new_value, page);
@@ -160,10 +160,12 @@ pub fn invokeAttributeChangedCallbackOnElement(element: *Element, name: []const
fn invokeCallbackOnElement(element: *Element, definition: *CustomElementDefinition, comptime callback_name: [:0]const u8, args: anytype, page: *Page) void {
_ = definition;
const ctx = page.js;
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
// Get the JS element object
const js_val = ctx.zigValueToJs(element, .{}) catch return;
const js_val = ls.local.zigValueToJs(element, .{}) catch return;
const js_element = js_val.toObject();
// Call the callback method if it exists
@@ -172,7 +174,7 @@ fn invokeCallbackOnElement(element: *Element, definition: *CustomElementDefiniti
// Check if element has "is" attribute and attach customized built-in definition
pub fn checkAndAttachBuiltIn(element: *Element, page: *Page) !void {
const is_value = element.getAttributeSafe("is") orelse return;
const is_value = element.getAttributeSafe(comptime .wrap("is")) orelse return;
const custom_elements = page.window.getCustomElements();
const definition = custom_elements._definitions.get(is_value) orelse return;
@@ -195,8 +197,26 @@ pub fn checkAndAttachBuiltIn(element: *Element, page: *Page) !void {
page._upgrading_element = node;
defer page._upgrading_element = prev_upgrading;
// PERFORMANCE OPTIMIZATION: This pattern is discouraged in general code.
// Used here because: (1) multiple early returns before needing Local,
// (2) called from both V8 callbacks (Local exists) and parser (no Local).
// Prefer either: requiring *const js.Local parameter, OR always creating
// Local.Scope upfront.
var ls: ?js.Local.Scope = null;
var local = blk: {
if (page.js.local) |l| {
break :blk l;
}
ls = undefined;
page.js.localScope(&ls.?);
break :blk &ls.?.local;
};
defer if (ls) |*_ls| {
_ls.deinit();
};
var caught: js.TryCatch.Caught = undefined;
_ = definition.constructor.local().newInstance(&caught) catch |err| {
_ = local.toLocal(definition.constructor).newInstance(&caught) catch |err| {
log.warn(.js, "custom builtin ctor", .{ .name = is_value, .err = err, .caught = caught });
return;
};
@@ -207,9 +227,11 @@ fn invokeCallback(self: *Custom, comptime callback_name: [:0]const u8, args: any
return;
}
const ctx = page.js;
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
const js_val = ctx.zigValueToJs(self, .{}) catch return;
const js_val = ls.local.zigValueToJs(self, .{}) catch return;
const js_element = js_val.toObject();
js_element.callMethod(void, callback_name, args) catch return;

View File

@@ -36,11 +36,11 @@ pub fn asNode(self: *Data) *Node {
}
pub fn getValue(self: *Data) []const u8 {
return self.asElement().getAttributeSafe("value") orelse "";
return self.asElement().getAttributeSafe(comptime .wrap("value")) orelse "";
}
pub fn setValue(self: *Data, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("value", value, page);
try self.asElement().setAttributeSafe(comptime .wrap("value"), .wrap(value), page);
}
pub const JsApi = struct {

View File

@@ -20,23 +20,23 @@ pub fn asNode(self: *Dialog) *Node {
}
pub fn getOpen(self: *const Dialog) bool {
return self.asConstElement().getAttributeSafe("open") != null;
return self.asConstElement().getAttributeSafe(comptime .wrap("open")) != null;
}
pub fn setOpen(self: *Dialog, open: bool, page: *Page) !void {
if (open) {
try self.asElement().setAttributeSafe("open", "", page);
try self.asElement().setAttributeSafe(comptime .wrap("open"), .wrap(""), page);
} else {
try self.asElement().removeAttribute("open", page);
try self.asElement().removeAttribute(comptime .wrap("open"), page);
}
}
pub fn getReturnValue(self: *const Dialog) []const u8 {
return self.asConstElement().getAttributeSafe("returnvalue") orelse "";
return self.asConstElement().getAttributeSafe(comptime .wrap("returnvalue")) orelse "";
}
pub fn setReturnValue(self: *Dialog, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("returnvalue", value, page);
try self.asElement().setAttributeSafe(comptime .wrap("returnvalue"), .wrap(value), page);
}
pub const JsApi = struct {

View File

@@ -43,15 +43,15 @@ pub fn asNode(self: *Form) *Node {
}
pub fn getName(self: *const Form) []const u8 {
return self.asConstElement().getAttributeSafe("name") orelse "";
return self.asConstElement().getAttributeSafe(comptime .wrap("name")) orelse "";
}
pub fn setName(self: *Form, name: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("name", name, page);
try self.asElement().setAttributeSafe(comptime .wrap("name"), .wrap(name), page);
}
pub fn getMethod(self: *const Form) []const u8 {
const method = self.asConstElement().getAttributeSafe("method") orelse return "get";
const method = self.asConstElement().getAttributeSafe(comptime .wrap("method")) orelse return "get";
if (std.ascii.eqlIgnoreCase(method, "post")) {
return "post";
@@ -64,11 +64,11 @@ pub fn getMethod(self: *const Form) []const u8 {
}
pub fn setMethod(self: *Form, method: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("method", method, page);
try self.asElement().setAttributeSafe(comptime .wrap("method"), .wrap(method), page);
}
pub fn getElements(self: *Form, page: *Page) !*collections.HTMLFormControlsCollection {
const form_id = self.asElement().getAttributeSafe("id");
const form_id = self.asElement().getAttributeSafe(comptime .wrap("id"));
const root = if (form_id != null)
self.asNode().getRootNode(null) // Has ID: walk entire document to find form=ID controls
else

View File

@@ -15,11 +15,11 @@ pub fn constructor(w_: ?u32, h_: ?u32, page: *Page) !*Image {
if (w_) |w| blk: {
const w_string = std.fmt.bufPrint(&page.buf, "{d}", .{w}) catch break :blk;
try el.setAttributeSafe("width", w_string, page);
try el.setAttributeSafe(comptime .wrap("width"), .wrap(w_string), page);
}
if (h_) |h| blk: {
const h_string = std.fmt.bufPrint(&page.buf, "{d}", .{h}) catch break :blk;
try el.setAttributeSafe("height", h_string, page);
try el.setAttributeSafe(comptime .wrap("height"), .wrap(h_string), page);
}
return el.as(Image);
}
@@ -36,7 +36,7 @@ pub fn asNode(self: *Image) *Node {
pub fn getSrc(self: *const Image, page: *Page) ![]const u8 {
const element = self.asConstElement();
const src = element.getAttributeSafe("src") orelse return "";
const src = element.getAttributeSafe(comptime .wrap("src")) orelse return "";
if (src.len == 0) {
return "";
}
@@ -46,54 +46,54 @@ pub fn getSrc(self: *const Image, page: *Page) ![]const u8 {
}
pub fn setSrc(self: *Image, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("src", value, page);
try self.asElement().setAttributeSafe(comptime .wrap("src"), .wrap(value), page);
}
pub fn getAlt(self: *const Image) []const u8 {
return self.asConstElement().getAttributeSafe("alt") orelse "";
return self.asConstElement().getAttributeSafe(comptime .wrap("alt")) orelse "";
}
pub fn setAlt(self: *Image, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("alt", value, page);
try self.asElement().setAttributeSafe(comptime .wrap("alt"), .wrap(value), page);
}
pub fn getWidth(self: *const Image) u32 {
const attr = self.asConstElement().getAttributeSafe("width") orelse return 0;
const attr = self.asConstElement().getAttributeSafe(comptime .wrap("width")) orelse return 0;
return std.fmt.parseUnsigned(u32, attr, 10) catch 0;
}
pub fn setWidth(self: *Image, value: u32, page: *Page) !void {
const str = try std.fmt.allocPrint(page.call_arena, "{d}", .{value});
try self.asElement().setAttributeSafe("width", str, page);
try self.asElement().setAttributeSafe(comptime .wrap("width"), .wrap(str), page);
}
pub fn getHeight(self: *const Image) u32 {
const attr = self.asConstElement().getAttributeSafe("height") orelse return 0;
const attr = self.asConstElement().getAttributeSafe(comptime .wrap("height")) orelse return 0;
return std.fmt.parseUnsigned(u32, attr, 10) catch 0;
}
pub fn setHeight(self: *Image, value: u32, page: *Page) !void {
const str = try std.fmt.allocPrint(page.call_arena, "{d}", .{value});
try self.asElement().setAttributeSafe("height", str, page);
try self.asElement().setAttributeSafe(comptime .wrap("height"), .wrap(str), page);
}
pub fn getCrossOrigin(self: *const Image) ?[]const u8 {
return self.asConstElement().getAttributeSafe("crossorigin");
return self.asConstElement().getAttributeSafe(comptime .wrap("crossorigin"));
}
pub fn setCrossOrigin(self: *Image, value: ?[]const u8, page: *Page) !void {
if (value) |v| {
return self.asElement().setAttributeSafe("crossorigin", v, page);
return self.asElement().setAttributeSafe(comptime .wrap("crossorigin"), .wrap(v), page);
}
return self.asElement().removeAttribute("crossorigin", page);
return self.asElement().removeAttribute(comptime .wrap("crossorigin"), page);
}
pub fn getLoading(self: *const Image) []const u8 {
return self.asConstElement().getAttributeSafe("loading") orelse "eager";
return self.asConstElement().getAttributeSafe(comptime .wrap("loading")) orelse "eager";
}
pub fn setLoading(self: *Image, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("loading", value, page);
try self.asElement().setAttributeSafe(comptime .wrap("loading"), .wrap(value), page);
}
pub const JsApi = struct {

View File

@@ -17,6 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const String = @import("../../../../string.zig").String;
const js = @import("../../../js/js.zig");
const Page = @import("../../../Page.zig");
@@ -24,6 +26,7 @@ const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig");
const Form = @import("Form.zig");
const Selection = @import("../../Selection.zig");
const Input = @This();
@@ -74,9 +77,12 @@ _value: ?[]const u8 = null,
_checked: bool = false,
_checked_dirty: bool = false,
_input_type: Type = .text,
_selected: bool = false,
_indeterminate: bool = false,
_selection_start: u32 = 0,
_selection_end: u32 = 0,
_selection_direction: Selection.SelectionDirection = .none,
pub fn asElement(self: *Input) *Element {
return self._proto._proto;
}
@@ -94,7 +100,7 @@ pub fn getType(self: *const Input) []const u8 {
pub fn setType(self: *Input, typ: []const u8, page: *Page) !void {
// Setting the type property should update the attribute, which will trigger attributeChange
const type_enum = Type.fromString(typ);
try self.asElement().setAttributeSafe("type", type_enum.toString(), page);
try self.asElement().setAttributeSafe(comptime .wrap("type"), .wrap(type_enum.toString()), page);
}
pub fn getValue(self: *const Input) []const u8 {
@@ -112,7 +118,7 @@ pub fn getDefaultValue(self: *const Input) []const u8 {
}
pub fn setDefaultValue(self: *Input, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("value", value, page);
try self.asElement().setAttributeSafe(comptime .wrap("value"), .wrap(value), page);
}
pub fn getChecked(self: *const Input) bool {
@@ -143,52 +149,52 @@ pub fn getDefaultChecked(self: *const Input) bool {
pub fn setDefaultChecked(self: *Input, checked: bool, page: *Page) !void {
if (checked) {
try self.asElement().setAttributeSafe("checked", "", page);
try self.asElement().setAttributeSafe(comptime .wrap("checked"), .wrap(""), page);
} else {
try self.asElement().removeAttribute("checked", page);
try self.asElement().removeAttribute(comptime .wrap("checked"), page);
}
}
pub fn getDisabled(self: *const Input) bool {
// TODO: Also check for disabled fieldset ancestors
// (but not if we're inside a <legend> of that fieldset)
return self.asConstElement().getAttributeSafe("disabled") != null;
return self.asConstElement().getAttributeSafe(comptime .wrap("disabled")) != null;
}
pub fn setDisabled(self: *Input, disabled: bool, page: *Page) !void {
if (disabled) {
try self.asElement().setAttributeSafe("disabled", "", page);
try self.asElement().setAttributeSafe(comptime .wrap("disabled"), .wrap(""), page);
} else {
try self.asElement().removeAttribute("disabled", page);
try self.asElement().removeAttribute(comptime .wrap("disabled"), page);
}
}
pub fn getName(self: *const Input) []const u8 {
return self.asConstElement().getAttributeSafe("name") orelse "";
return self.asConstElement().getAttributeSafe(comptime .wrap("name")) orelse "";
}
pub fn setName(self: *Input, name: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("name", name, page);
try self.asElement().setAttributeSafe(comptime .wrap("name"), .wrap(name), page);
}
pub fn getAccept(self: *const Input) []const u8 {
return self.asConstElement().getAttributeSafe("accept") orelse "";
return self.asConstElement().getAttributeSafe(comptime .wrap("accept")) orelse "";
}
pub fn setAccept(self: *Input, accept: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("accept", accept, page);
try self.asElement().setAttributeSafe(comptime .wrap("accept"), .wrap(accept), page);
}
pub fn getAlt(self: *const Input) []const u8 {
return self.asConstElement().getAttributeSafe("alt") orelse "";
return self.asConstElement().getAttributeSafe(comptime .wrap("alt")) orelse "";
}
pub fn setAlt(self: *Input, alt: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("alt", alt, page);
try self.asElement().setAttributeSafe(comptime .wrap("alt"), .wrap(alt), page);
}
pub fn getMaxLength(self: *const Input) i32 {
const attr = self.asConstElement().getAttributeSafe("maxlength") orelse return -1;
const attr = self.asConstElement().getAttributeSafe(comptime .wrap("maxlength")) orelse return -1;
return std.fmt.parseInt(i32, attr, 10) catch -1;
}
@@ -198,11 +204,11 @@ pub fn setMaxLength(self: *Input, max_length: i32, page: *Page) !void {
}
var buf: [32]u8 = undefined;
const value = std.fmt.bufPrint(&buf, "{d}", .{max_length}) catch unreachable;
try self.asElement().setAttributeSafe("maxlength", value, page);
try self.asElement().setAttributeSafe(comptime .wrap("maxlength"), .wrap(value), page);
}
pub fn getSize(self: *const Input) i32 {
const attr = self.asConstElement().getAttributeSafe("size") orelse return 20;
const attr = self.asConstElement().getAttributeSafe(comptime .wrap("size")) orelse return 20;
const parsed = std.fmt.parseInt(i32, attr, 10) catch return 20;
return if (parsed == 0) 20 else parsed;
}
@@ -212,58 +218,170 @@ pub fn setSize(self: *Input, size: i32, page: *Page) !void {
return error.ZeroNotAllowed;
}
if (size < 0) {
return self.asElement().setAttributeSafe("size", "20", page);
return self.asElement().setAttributeSafe(comptime .wrap("size"), .wrap("20"), page);
}
var buf: [32]u8 = undefined;
const value = std.fmt.bufPrint(&buf, "{d}", .{size}) catch unreachable;
try self.asElement().setAttributeSafe("size", value, page);
try self.asElement().setAttributeSafe(comptime .wrap("size"), .wrap(value), page);
}
pub fn getSrc(self: *const Input, page: *Page) ![]const u8 {
const src = self.asConstElement().getAttributeSafe("src") orelse return "";
const src = self.asConstElement().getAttributeSafe(comptime .wrap("src")) orelse return "";
// If attribute is explicitly set (even if empty), resolve it against the base URL
return @import("../../URL.zig").resolve(page.call_arena, page.base(), src, .{});
}
pub fn setSrc(self: *Input, src: []const u8, page: *Page) !void {
const trimmed = std.mem.trim(u8, src, &std.ascii.whitespace);
try self.asElement().setAttributeSafe("src", trimmed, page);
try self.asElement().setAttributeSafe(comptime .wrap("src"), .wrap(trimmed), page);
}
pub fn getReadonly(self: *const Input) bool {
return self.asConstElement().getAttributeSafe("readonly") != null;
return self.asConstElement().getAttributeSafe(comptime .wrap("readonly")) != null;
}
pub fn setReadonly(self: *Input, readonly: bool, page: *Page) !void {
if (readonly) {
try self.asElement().setAttributeSafe("readonly", "", page);
try self.asElement().setAttributeSafe(comptime .wrap("readonly"), .wrap(""), page);
} else {
try self.asElement().removeAttribute("readonly", page);
try self.asElement().removeAttribute(comptime .wrap("readonly"), page);
}
}
pub fn getRequired(self: *const Input) bool {
return self.asConstElement().getAttributeSafe("required") != null;
return self.asConstElement().getAttributeSafe(comptime .wrap("required")) != null;
}
pub fn setRequired(self: *Input, required: bool, page: *Page) !void {
if (required) {
try self.asElement().setAttributeSafe("required", "", page);
try self.asElement().setAttributeSafe(comptime .wrap("required"), .wrap(""), page);
} else {
try self.asElement().removeAttribute("required", page);
try self.asElement().removeAttribute(comptime .wrap("required"), page);
}
}
pub fn select(self: *Input) void {
self._selected = true;
pub fn select(self: *Input) !void {
const len = if (self._value) |v| @as(u32, @intCast(v.len)) else 0;
try self.setSelectionRange(0, len, null);
}
fn selectionAvailable(self: *const Input) bool {
switch (self._input_type) {
.text, .search, .url, .tel, .password => return true,
else => return false,
}
}
const HowSelected = union(enum) { partial: struct { u32, u32 }, full, none };
fn howSelected(self: *const Input) HowSelected {
if (!self.selectionAvailable()) return .none;
const value = self._value orelse return .none;
if (self._selection_start == self._selection_end) return .none;
if (self._selection_start == 0 and self._selection_end == value.len) return .full;
return .{ .partial = .{ self._selection_start, self._selection_end } };
}
pub fn innerInsert(self: *Input, str: []const u8, page: *Page) !void {
const arena = page.arena;
switch (self.howSelected()) {
.full => {
// if the input is fully selected, replace the content.
const new_value = try arena.dupe(u8, str);
try self.setValue(new_value, page);
self._selection_start = @intCast(new_value.len);
self._selection_end = @intCast(new_value.len);
self._selection_direction = .none;
},
.partial => |range| {
// if the input is partially selected, replace the selected content.
const current_value = self.getValue();
const before = current_value[0..range[0]];
const remaining = current_value[range[1]..];
const new_value = try std.mem.concat(
arena,
u8,
&.{ before, str, remaining },
);
try self.setValue(new_value, page);
const new_pos = range[0] + str.len;
self._selection_start = @intCast(new_pos);
self._selection_end = @intCast(new_pos);
self._selection_direction = .none;
},
.none => {
// if the input is not selected, just insert at cursor.
const current_value = self.getValue();
const new_value = try std.mem.concat(arena, u8, &.{ current_value, str });
try self.setValue(new_value, page);
},
}
}
pub fn getSelectionDirection(self: *const Input) []const u8 {
return @tagName(self._selection_direction);
}
pub fn getSelectionStart(self: *const Input) !?u32 {
if (!self.selectionAvailable()) return null;
return self._selection_start;
}
pub fn setSelectionStart(self: *Input, value: u32) !void {
if (!self.selectionAvailable()) return error.InvalidStateError;
self._selection_start = value;
}
pub fn getSelectionEnd(self: *const Input) !?u32 {
if (!self.selectionAvailable()) return null;
return self._selection_end;
}
pub fn setSelectionEnd(self: *Input, value: u32) !void {
if (!self.selectionAvailable()) return error.InvalidStateError;
self._selection_end = value;
}
pub fn setSelectionRange(self: *Input, selection_start: u32, selection_end: u32, selection_dir: ?[]const u8) !void {
if (!self.selectionAvailable()) return error.InvalidStateError;
const direction = blk: {
if (selection_dir) |sd| {
break :blk std.meta.stringToEnum(Selection.SelectionDirection, sd) orelse .none;
} else break :blk .none;
};
const value = self._value orelse {
self._selection_start = 0;
self._selection_end = 0;
self._selection_direction = .none;
return;
};
const len_u32: u32 = @intCast(value.len);
var start: u32 = if (selection_start > len_u32) len_u32 else selection_start;
const end: u32 = if (selection_end > len_u32) len_u32 else selection_end;
// If end is less than start, both are equal to end.
if (end < start) {
start = end;
}
self._selection_direction = direction;
self._selection_start = start;
self._selection_end = end;
}
pub fn getForm(self: *Input, page: *Page) ?*Form {
const element = self.asElement();
// If form attribute exists, ONLY use that (even if it references nothing)
if (element.getAttributeSafe("form")) |form_id| {
if (element.getAttributeSafe(comptime .wrap("form"))) |form_id| {
if (page.document.getElementById(form_id, page)) |form_element| {
return form_element.is(Form);
}
@@ -286,7 +404,7 @@ pub fn getForm(self: *Input, page: *Page) ?*Form {
fn uncheckRadioGroup(self: *Input, page: *Page) !void {
const element = self.asElement();
const name = element.getAttributeSafe("name") orelse return;
const name = element.getAttributeSafe(comptime .wrap("name")) orelse return;
if (name.len == 0) {
return;
}
@@ -304,7 +422,7 @@ fn uncheckRadioGroup(self: *Input, page: *Page) !void {
continue;
}
const other_name = other_element.getAttributeSafe("name") orelse continue;
const other_name = other_element.getAttributeSafe(comptime .wrap("name")) orelse continue;
if (!std.mem.eql(u8, name, other_name)) {
continue;
}
@@ -352,6 +470,11 @@ pub const JsApi = struct {
pub const form = bridge.accessor(Input.getForm, null, .{});
pub const indeterminate = bridge.accessor(Input.getIndeterminate, Input.setIndeterminate, .{});
pub const select = bridge.function(Input.select, .{});
pub const selectionStart = bridge.accessor(Input.getSelectionStart, Input.setSelectionStart, .{});
pub const selectionEnd = bridge.accessor(Input.getSelectionEnd, Input.setSelectionEnd, .{});
pub const selectionDirection = bridge.accessor(Input.getSelectionDirection, null, .{});
pub const setSelectionRange = bridge.function(Input.setSelectionRange, .{ .dom_exception = true });
};
pub const Build = struct {
@@ -360,14 +483,14 @@ pub const Build = struct {
const element = self.asElement();
// Store initial values from attributes
self._default_value = element.getAttributeSafe("value");
self._default_checked = element.getAttributeSafe("checked") != null;
self._default_value = element.getAttributeSafe(comptime .wrap("value"));
self._default_checked = element.getAttributeSafe(comptime .wrap("checked")) != null;
// Current state starts equal to default
self._value = self._default_value;
self._checked = self._default_checked;
self._input_type = if (element.getAttributeSafe("type")) |type_attr|
self._input_type = if (element.getAttributeSafe(comptime .wrap("type"))) |type_attr|
Type.fromString(type_attr)
else
.text;
@@ -378,12 +501,12 @@ pub const Build = struct {
}
}
pub fn attributeChange(element: *Element, name: []const u8, value: []const u8, page: *Page) !void {
const attribute = std.meta.stringToEnum(enum { type, value, checked }, name) orelse return;
pub fn attributeChange(element: *Element, name: String, value: String, page: *Page) !void {
const attribute = std.meta.stringToEnum(enum { type, value, checked }, name.str()) orelse return;
const self = element.as(Input);
switch (attribute) {
.type => self._input_type = Type.fromString(value),
.value => self._default_value = value,
.type => self._input_type = Type.fromString(value.str()),
.value => self._default_value = try page.arena.dupe(u8, value.str()),
.checked => {
self._default_checked = true;
// Only update checked state if it hasn't been manually modified
@@ -398,8 +521,8 @@ pub const Build = struct {
}
}
pub fn attributeRemove(element: *Element, name: []const u8, _: *Page) !void {
const attribute = std.meta.stringToEnum(enum { type, value, checked }, name) orelse return;
pub fn attributeRemove(element: *Element, name: String, _: *Page) !void {
const attribute = std.meta.stringToEnum(enum { type, value, checked }, name.str()) orelse return;
const self = element.as(Input);
switch (attribute) {
.type => self._input_type = .text,
@@ -422,7 +545,9 @@ pub const Build = struct {
clone._value = source._value;
clone._checked = source._checked;
clone._checked_dirty = source._checked_dirty;
clone._selected = source._selected;
clone._selection_direction = source._selection_direction;
clone._selection_start = source._selection_start;
clone._selection_end = source._selection_end;
clone._indeterminate = source._indeterminate;
}
};

View File

@@ -36,7 +36,7 @@ pub fn asNode(self: *Link) *Node {
pub fn getHref(self: *Link, page: *Page) ![]const u8 {
const element = self.asElement();
const href = element.getAttributeSafe("href") orelse return "";
const href = element.getAttributeSafe(comptime .wrap("href")) orelse return "";
if (href.len == 0) {
return "";
}
@@ -46,15 +46,15 @@ pub fn getHref(self: *Link, page: *Page) ![]const u8 {
}
pub fn setHref(self: *Link, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("href", value, page);
try self.asElement().setAttributeSafe(comptime .wrap("href"), .wrap(value), page);
}
pub fn getRel(self: *Link) []const u8 {
return self.asElement().getAttributeSafe("rel") orelse return "";
return self.asElement().getAttributeSafe(comptime .wrap("rel")) orelse return "";
}
pub fn setRel(self: *Link, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("rel", value, page);
try self.asElement().setAttributeSafe(comptime .wrap("rel"), .wrap(value), page);
}
pub const JsApi = struct {

View File

@@ -219,7 +219,7 @@ pub fn setCurrentTime(self: *Media, value: f64) void {
pub fn getSrc(self: *const Media, page: *Page) ![]const u8 {
const element = self.asConstElement();
const src = element.getAttributeSafe("src") orelse return "";
const src = element.getAttributeSafe(comptime .wrap("src")) orelse return "";
if (src.len == 0) {
return "";
}
@@ -228,51 +228,51 @@ pub fn getSrc(self: *const Media, page: *Page) ![]const u8 {
}
pub fn setSrc(self: *Media, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("src", value, page);
try self.asElement().setAttributeSafe(comptime .wrap("src"), .wrap(value), page);
}
pub fn getAutoplay(self: *const Media) bool {
return self.asConstElement().getAttributeSafe("autoplay") != null;
return self.asConstElement().getAttributeSafe(comptime .wrap("autoplay")) != null;
}
pub fn setAutoplay(self: *Media, value: bool, page: *Page) !void {
if (value) {
try self.asElement().setAttributeSafe("autoplay", "", page);
try self.asElement().setAttributeSafe(comptime .wrap("autoplay"), .wrap(""), page);
} else {
try self.asElement().removeAttribute("autoplay", page);
try self.asElement().removeAttribute(comptime .wrap("autoplay"), page);
}
}
pub fn getControls(self: *const Media) bool {
return self.asConstElement().getAttributeSafe("controls") != null;
return self.asConstElement().getAttributeSafe(comptime .wrap("controls")) != null;
}
pub fn setControls(self: *Media, value: bool, page: *Page) !void {
if (value) {
try self.asElement().setAttributeSafe("controls", "", page);
try self.asElement().setAttributeSafe(comptime .wrap("controls"), .wrap(""), page);
} else {
try self.asElement().removeAttribute("controls", page);
try self.asElement().removeAttribute(comptime .wrap("controls"), page);
}
}
pub fn getLoop(self: *const Media) bool {
return self.asConstElement().getAttributeSafe("loop") != null;
return self.asConstElement().getAttributeSafe(comptime .wrap("loop")) != null;
}
pub fn setLoop(self: *Media, value: bool, page: *Page) !void {
if (value) {
try self.asElement().setAttributeSafe("loop", "", page);
try self.asElement().setAttributeSafe(comptime .wrap("loop"), .wrap(""), page);
} else {
try self.asElement().removeAttribute("loop", page);
try self.asElement().removeAttribute(comptime .wrap("loop"), page);
}
}
pub fn getPreload(self: *const Media) []const u8 {
return self.asConstElement().getAttributeSafe("preload") orelse "auto";
return self.asConstElement().getAttributeSafe(comptime .wrap("preload")) orelse "auto";
}
pub fn setPreload(self: *Media, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("preload", value, page);
try self.asElement().setAttributeSafe(comptime .wrap("preload"), .wrap(value), page);
}
pub const JsApi = struct {

View File

@@ -37,35 +37,35 @@ pub fn asNode(self: *Meta) *Node {
}
pub fn getName(self: *Meta) []const u8 {
return self.asElement().getAttributeSafe("name") orelse return "";
return self.asElement().getAttributeSafe(comptime .wrap("name")) orelse return "";
}
pub fn setName(self: *Meta, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("name", value, page);
try self.asElement().setAttributeSafe(comptime .wrap("name"), .wrap(value), page);
}
pub fn getHttpEquiv(self: *Meta) []const u8 {
return self.asElement().getAttributeSafe("http-equiv") orelse return "";
return self.asElement().getAttributeSafe(comptime .wrap("http-equiv")) orelse return "";
}
pub fn setHttpEquiv(self: *Meta, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("http-equiv", value, page);
try self.asElement().setAttributeSafe(comptime .wrap("http-equiv"), .wrap(value), page);
}
pub fn getContent(self: *Meta) []const u8 {
return self.asElement().getAttributeSafe("content") orelse return "";
return self.asElement().getAttributeSafe(comptime .wrap("content")) orelse return "";
}
pub fn setContent(self: *Meta, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("content", value, page);
try self.asElement().setAttributeSafe(comptime .wrap("content"), .wrap(value), page);
}
pub fn getMedia(self: *Meta) []const u8 {
return self.asElement().getAttributeSafe("media") orelse return "";
return self.asElement().getAttributeSafe(comptime .wrap("media")) orelse return "";
}
pub fn setMedia(self: *Meta, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("media", value, page);
try self.asElement().setAttributeSafe(comptime .wrap("media"), .wrap(value), page);
}
pub const JsApi = struct {

View File

@@ -17,6 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const String = @import("../../../../string.zig").String;
const js = @import("../../../js/js.zig");
const Page = @import("../../../Page.zig");
@@ -55,7 +57,7 @@ pub fn getValue(self: *Option, page: *Page) []const u8 {
pub fn setValue(self: *Option, value: []const u8, page: *Page) !void {
const owned = try page.dupeString(value);
try self.asElement().setAttributeSafe("value", owned, page);
try self.asElement().setAttributeSafe(comptime .wrap("value"), .wrap(owned), page);
self._value = owned;
}
@@ -87,18 +89,18 @@ pub fn getDisabled(self: *const Option) bool {
pub fn setDisabled(self: *Option, disabled: bool, page: *Page) !void {
self._disabled = disabled;
if (disabled) {
try self.asElement().setAttributeSafe("disabled", "", page);
try self.asElement().setAttributeSafe(comptime .wrap("disabled"), .wrap(""), page);
} else {
try self.asElement().removeAttribute("disabled", page);
try self.asElement().removeAttribute(comptime .wrap("disabled"), page);
}
}
pub fn getName(self: *const Option) []const u8 {
return self.asConstElement().getAttributeSafe("name") orelse "";
return self.asConstElement().getAttributeSafe(comptime .wrap("name")) orelse "";
}
pub fn setName(self: *Option, name: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("name", name, page);
try self.asElement().setAttributeSafe(comptime .wrap("name"), .wrap(name), page);
}
pub const JsApi = struct {
@@ -124,21 +126,21 @@ pub const Build = struct {
const element = self.asElement();
// Check for value attribute
self._value = element.getAttributeSafe("value");
self._value = element.getAttributeSafe(comptime .wrap("value"));
// Check for selected attribute
self._default_selected = element.getAttributeSafe("selected") != null;
self._default_selected = element.getAttributeSafe(comptime .wrap("selected")) != null;
self._selected = self._default_selected;
// Check for disabled attribute
self._disabled = element.getAttributeSafe("disabled") != null;
self._disabled = element.getAttributeSafe(comptime .wrap("disabled")) != null;
}
pub fn attributeChange(element: *Element, name: []const u8, value: []const u8, _: *Page) !void {
const attribute = std.meta.stringToEnum(enum { value, selected }, name) orelse return;
pub fn attributeChange(element: *Element, name: String, value: String, _: *Page) !void {
const attribute = std.meta.stringToEnum(enum { value, selected }, name.str()) orelse return;
const self = element.as(Option);
switch (attribute) {
.value => self._value = value,
.value => self._value = value.str(),
.selected => {
self._default_selected = true;
self._selected = true;
@@ -146,8 +148,8 @@ pub const Build = struct {
}
}
pub fn attributeRemove(element: *Element, name: []const u8, _: *Page) !void {
const attribute = std.meta.stringToEnum(enum { value, selected }, name) orelse return;
pub fn attributeRemove(element: *Element, name: String, _: *Page) !void {
const attribute = std.meta.stringToEnum(enum { value, selected }, name.str()) orelse return;
const self = element.as(Option);
switch (attribute) {
.value => self._value = null,

View File

@@ -24,6 +24,7 @@ const Page = @import("../../../Page.zig");
const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig");
const URL = @import("../../URL.zig");
const Script = @This();
@@ -45,33 +46,34 @@ pub fn asNode(self: *Script) *Node {
return self.asElement().asNode();
}
pub fn getSrc(self: *const Script) []const u8 {
return self._src;
pub fn getSrc(self: *const Script, page: *Page) ![]const u8 {
if (self._src.len == 0) return "";
return try URL.resolve(page.call_arena, page.base(), self._src, .{});
}
pub fn setSrc(self: *Script, src: []const u8, page: *Page) !void {
const element = self.asElement();
try element.setAttributeSafe("src", src, page);
self._src = element.getAttributeSafe("src") orelse unreachable;
try element.setAttributeSafe(comptime .wrap("src"), .wrap(src), page);
self._src = element.getAttributeSafe(comptime .wrap("src")) orelse unreachable;
if (element.asNode().isConnected()) {
try page.scriptAddedCallback(false, self);
}
}
pub fn getType(self: *const Script) []const u8 {
return self.asConstElement().getAttributeSafe("type") orelse "";
return self.asConstElement().getAttributeSafe(comptime .wrap("type")) orelse "";
}
pub fn setType(self: *Script, value: []const u8, page: *Page) !void {
return self.asElement().setAttributeSafe("type", value, page);
return self.asElement().setAttributeSafe(comptime .wrap("type"), .wrap(value), page);
}
pub fn getNonce(self: *const Script) []const u8 {
return self.asConstElement().getAttributeSafe("nonce") orelse "";
return self.asConstElement().getAttributeSafe(comptime .wrap("nonce")) orelse "";
}
pub fn setNonce(self: *Script, value: []const u8, page: *Page) !void {
return self.asElement().setAttributeSafe("nonce", value, page);
return self.asElement().setAttributeSafe(comptime .wrap("nonce"), .wrap(value), page);
}
pub fn getOnLoad(self: *const Script) ?js.Function.Global {
@@ -91,7 +93,7 @@ pub fn setOnError(self: *Script, cb: ?js.Function.Global) void {
}
pub fn getNoModule(self: *const Script) bool {
return self.asConstElement().getAttributeSafe("nomodule") != null;
return self.asConstElement().getAttributeSafe(comptime .wrap("nomodule")) != null;
}
pub fn setInnerText(self: *Script, text: []const u8, page: *Page) !void {
@@ -125,19 +127,19 @@ pub const Build = struct {
pub fn complete(node: *Node, page: *Page) !void {
const self = node.as(Script);
const element = self.asElement();
self._src = element.getAttributeSafe("src") orelse "";
self._src = element.getAttributeSafe(comptime .wrap("src")) orelse "";
if (element.getAttributeSafe("onload")) |on_load| {
if (page.js.stringToFunction(on_load)) |func| {
self._on_load = try func.persist();
if (element.getAttributeSafe(comptime .wrap("onload"))) |on_load| {
if (page.js.stringToPersistedFunction(on_load)) |func| {
self._on_load = func;
} else |err| {
log.err(.js, "script.onload", .{ .err = err, .str = on_load });
}
}
if (element.getAttributeSafe("onerror")) |on_error| {
if (page.js.stringToFunction(on_error)) |func| {
self._on_error = try func.persist();
if (element.getAttributeSafe(comptime .wrap("onerror"))) |on_error| {
if (page.js.stringToPersistedFunction(on_error)) |func| {
self._on_error = func;
} else |err| {
log.err(.js, "script.onerror", .{ .err = err, .str = on_error });
}

View File

@@ -121,39 +121,39 @@ pub fn setSelectedIndex(self: *Select, index: i32) !void {
}
pub fn getMultiple(self: *const Select) bool {
return self.asConstElement().getAttributeSafe("multiple") != null;
return self.asConstElement().getAttributeSafe(comptime .wrap("multiple")) != null;
}
pub fn setMultiple(self: *Select, multiple: bool, page: *Page) !void {
if (multiple) {
try self.asElement().setAttributeSafe("multiple", "", page);
try self.asElement().setAttributeSafe(comptime .wrap("multiple"), .wrap(""), page);
} else {
try self.asElement().removeAttribute("multiple", page);
try self.asElement().removeAttribute(comptime .wrap("multiple"), page);
}
}
pub fn getDisabled(self: *const Select) bool {
return self.asConstElement().getAttributeSafe("disabled") != null;
return self.asConstElement().getAttributeSafe(comptime .wrap("disabled")) != null;
}
pub fn setDisabled(self: *Select, disabled: bool, page: *Page) !void {
if (disabled) {
try self.asElement().setAttributeSafe("disabled", "", page);
try self.asElement().setAttributeSafe(comptime .wrap("disabled"), .wrap(""), page);
} else {
try self.asElement().removeAttribute("disabled", page);
try self.asElement().removeAttribute(comptime .wrap("disabled"), page);
}
}
pub fn getName(self: *const Select) []const u8 {
return self.asConstElement().getAttributeSafe("name") orelse "";
return self.asConstElement().getAttributeSafe(comptime .wrap("name")) orelse "";
}
pub fn setName(self: *Select, name: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("name", name, page);
try self.asElement().setAttributeSafe(comptime .wrap("name"), .wrap(name), page);
}
pub fn getSize(self: *const Select) u32 {
const s = self.asConstElement().getAttributeSafe("size") orelse return 0;
const s = self.asConstElement().getAttributeSafe(comptime .wrap("size")) orelse return 0;
const trimmed = std.mem.trimLeft(u8, s, &std.ascii.whitespace);
@@ -172,18 +172,18 @@ pub fn getSize(self: *const Select) u32 {
pub fn setSize(self: *Select, size: u32, page: *Page) !void {
const size_string = try std.fmt.allocPrint(page.call_arena, "{d}", .{size});
try self.asElement().setAttributeSafe("size", size_string, page);
try self.asElement().setAttributeSafe(comptime .wrap("size"), .wrap(size_string), page);
}
pub fn getRequired(self: *const Select) bool {
return self.asConstElement().getAttributeSafe("required") != null;
return self.asConstElement().getAttributeSafe(comptime .wrap("required")) != null;
}
pub fn setRequired(self: *Select, required: bool, page: *Page) !void {
if (required) {
try self.asElement().setAttributeSafe("required", "", page);
try self.asElement().setAttributeSafe(comptime .wrap("required"), .wrap(""), page);
} else {
try self.asElement().removeAttribute("required", page);
try self.asElement().removeAttribute(comptime .wrap("required"), page);
}
}
@@ -218,7 +218,7 @@ pub fn getForm(self: *Select, page: *Page) ?*Form {
const element = self.asElement();
// If form attribute exists, ONLY use that (even if it references nothing)
if (element.getAttributeSafe("form")) |form_id| {
if (element.getAttributeSafe(comptime .wrap("form"))) |form_id| {
if (page.document.getElementById(form_id, page)) |form_element| {
return form_element.is(Form);
}

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