1 Commits

Author SHA1 Message Date
Muki Kiboigo
b6e8aff2c9 ensure that records persist in arena 2026-01-05 09:15:15 -08:00
213 changed files with 4262 additions and 12731 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -17,20 +17,19 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const IS_DEBUG = @import("builtin").mode == .Debug;
const log = @import("../../log.zig");
const Page = @import("../Page.zig");
const js = @import("js.zig");
const v8 = js.v8;
const Env = @import("Env.zig");
const bridge = @import("bridge.zig");
const Context = @import("Context.zig");
const Page = @import("../Page.zig");
const ArenaAllocator = std.heap.ArenaAllocator;
const IS_DEBUG = @import("builtin").mode == .Debug;
const CONTEXT_ARENA_RETAIN = 1024 * 64;
@@ -53,7 +52,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()
@@ -61,58 +59,56 @@ pub fn deinit(self: *ExecutionWorld) void {
if (self.context != null) {
self.removeContext();
}
self.context_arena.deinit();
}
// Only the top Context in the Main ExecutionWorld should hold a handle_scope.
// A js.HandleScope is like an arena. Once created, any "Local" that
// A v8.HandleScope is like an arena. Once created, any "Local" that
// v8 creates will be released (or at least, releasable by the v8 GC)
// when the handle_scope is freed.
// We also maintain our own "context_arena" which allows us to have
// all page related memory easily managed.
pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context {
lp.assert(self.context == null, "ExecptionWorld.createContext has context", .{});
std.debug.assert(self.context == null);
const env = self.env;
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);
var v8_context: v8.Context = blk: {
var temp_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&temp_scope, isolate);
defer temp_scope.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,
});
if (comptime IS_DEBUG) {
// Getting this into the snapshot is tricky (anything involving the
// global is tricky). Easier to do here, and in debug mode, we're
// fine with paying the small perf hit.
const js_global = v8.FunctionTemplate.initDefault(isolate);
const global_template = js_global.getInstanceTemplate();
const context_handle = v8.v8__Context__New(isolate.handle, global_template, null).?;
break :blk js.Global(Context).init(isolate.handle, context_handle);
global_template.setNamedProperty(v8.NamedPropertyHandlerConfiguration{
.getter = unknownPropertyCallback,
.flags = v8.PropertyHandlerFlags.NonMasking | v8.PropertyHandlerFlags.OnlyInterceptStrings,
}, null);
}
const context_local = v8.Context.init(isolate, null, null);
const v8_context = v8.Persistent(v8.Context).init(isolate, context_local).castToContext();
break :blk v8_context;
};
// For a Page we only create one HandleScope, it is stored in the main World (enter==true). A page can have multple contexts, 1 for each World.
// 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;
var handle_scope: ?v8.HandleScope = null;
if (enter) {
handle_scope = @as(js.HandleScope, undefined);
handle_scope.?.init(isolate);
v8.v8__Context__Enter(v8_context);
handle_scope = @as(v8.HandleScope, undefined);
v8.HandleScope.init(&handle_scope.?, isolate);
v8_context.enter();
}
errdefer if (enter) {
v8.v8__Context__Exit(v8_context);
v8_context.exit();
handle_scope.?.deinit();
};
@@ -123,34 +119,33 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context
.page = page,
.id = context_id,
.isolate = isolate,
.handle = v8_context,
.v8_context = v8_context,
.templates = env.templates,
.handle_scope = handle_scope,
.script_manager = &page._script_manager,
.call_arena = page.call_arena,
.arena = arena,
.arena = self.context_arena.allocator(),
};
self.persisted_context = persisted_context;
var context = &self.context.?;
// Store a pointer to our context inside the v8 context so that, given
// a v8 context, we can get our context out
const data = isolate.initBigInt(@intFromPtr(context));
v8.v8__Context__SetEmbedderData(context.handle, 1, @ptrCast(data.handle));
const data = isolate.initBigIntU64(@intCast(@intFromPtr(context)));
v8_context.setEmbedderData(1, data);
try context.setupGlobal();
return context;
}
pub fn removeContext(self: *ExecutionWorld) void {
var context = &(self.context orelse return);
context.deinit();
// Force running the micro task to drain the queue before reseting the
// context arena.
// Tasks in the queue are relying to the arena memory could be present in
// the queue. Running them later could lead to invalid memory accesses.
self.env.runMicrotasks();
self.context.?.deinit();
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 });
}
@@ -161,3 +156,42 @@ pub fn terminateExecution(self: *const ExecutionWorld) void {
pub fn resumeExecution(self: *const ExecutionWorld) void {
self.env.isolate.cancelTerminateExecution();
}
pub fn unknownPropertyCallback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
const context = Context.fromIsolate(info.getIsolate());
const property = context.valueToString(.{ .handle = c_name.? }, .{}) catch "???";
const ignored = std.StaticStringMap(void).initComptime(.{
.{ "process", {} },
.{ "ShadyDOM", {} },
.{ "ShadyCSS", {} },
.{ "litNonce", {} },
.{ "litHtmlVersions", {} },
.{ "litElementVersions", {} },
.{ "litHtmlPolyfillSupport", {} },
.{ "litElementHydrateSupport", {} },
.{ "litElementPolyfillSupport", {} },
.{ "reactiveElementVersions", {} },
.{ "recaptcha", {} },
.{ "grecaptcha", {} },
.{ "___grecaptcha_cfg", {} },
.{ "__recaptcha_api", {} },
.{ "__google_recaptcha_client", {} },
.{ "CLOSURE_FLAGS", {} },
});
if (!ignored.has(property)) {
log.debug(.unknown_prop, "unkown global property", .{
.info = "but the property can exist in pure JS",
.stack = context.stackTrace() catch "???",
.property = property,
});
}
return v8.Intercepted.No;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,107 +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 js = @import("js.zig");
const v8 = js.v8;
const log = @import("../../log.zig");
const PromiseResolver = @This();
ctx: *js.Context,
handle: *const v8.PromiseResolver,
pub fn init(ctx: *js.Context) PromiseResolver {
return .{
.ctx = ctx,
.handle = v8.v8__Promise__Resolver__New(ctx.handle).?,
};
}
pub fn promise(self: PromiseResolver) js.Promise {
return .{
.ctx = self.ctx,
.handle = v8.v8__Promise__Resolver__GetPromise(self.handle).?,
};
}
pub fn resolve(self: PromiseResolver, comptime source: []const u8, value: anytype) void {
self._resolve(value) catch |err| {
log.err(.bug, "resolve", .{ .source = source, .err = err, .persistent = false });
};
}
fn _resolve(self: PromiseResolver, value: anytype) !void {
const ctx: *js.Context = @constCast(self.ctx);
const js_value = try ctx.zigValueToJs(value, .{});
var out: v8.MaybeBool = undefined;
v8.v8__Promise__Resolver__Resolve(self.handle, self.ctx.handle, js_value.handle, &out);
if (!out.has_value or !out.value) {
return error.FailedToResolvePromise;
}
ctx.runMicrotasks();
}
pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype) void {
self._reject(value) catch |err| {
log.err(.bug, "reject", .{ .source = source, .err = err, .persistent = false });
};
}
fn _reject(self: PromiseResolver, value: anytype) !void {
const ctx = self.ctx;
const js_value = try ctx.zigValueToJs(value, .{});
var out: v8.MaybeBool = undefined;
v8.v8__Promise__Resolver__Reject(self.handle, ctx.handle, js_value.handle, &out);
if (!out.has_value or !out.value) {
return error.FailedToRejectPromise;
}
ctx.runMicrotasks();
}
pub fn persist(self: PromiseResolver) !Global {
var ctx = self.ctx;
var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
try ctx.global_promise_resolvers.append(ctx.arena, global);
return .{
.handle = global,
.ctx = ctx,
};
}
pub const Global = struct {
handle: v8.Global,
ctx: *js.Context,
pub fn deinit(self: *Global) void {
v8.v8__Global__Reset(&self.handle);
}
pub fn local(self: *const Global) PromiseResolver {
return .{
.ctx = self.ctx,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, self.ctx.isolate.handle)),
};
}
pub fn isEqual(self: *const Global, other: PromiseResolver) bool {
return v8.v8__Global__IsEqual(&self.handle, other.handle);
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,556 +18,11 @@
const std = @import("std");
const js = @import("js.zig");
const lp = @import("lightpanda");
const log = @import("../../log.zig");
const v8 = js.v8;
const Context = @import("Context.zig");
const Page = @import("../Page.zig");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const CALL_ARENA_RETAIN = 1024 * 16;
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
// ============================================================================
const Caller = @import("Caller.zig");
pub fn Builder(comptime T: type) type {
return struct {
@@ -634,7 +89,7 @@ pub fn Builder(comptime T: type) type {
}
pub const Constructor = struct {
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
const Opts = struct {
dom_exception: bool = false,
@@ -642,12 +97,11 @@ pub const Constructor = struct {
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Constructor {
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);
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
caller.constructor(T, func, info, .{
.dom_exception = opts.dom_exception,
});
@@ -658,7 +112,7 @@ pub const Constructor = struct {
pub const Function = struct {
static: bool,
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
const Opts = struct {
static: bool = false,
@@ -671,12 +125,11 @@ pub const Function = struct {
return .{
.static = opts.static,
.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);
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
if (comptime opts.static) {
caller.function(T, func, info, .{
.dom_exception = opts.dom_exception,
@@ -698,8 +151,8 @@ pub const Function = struct {
pub const Accessor = struct {
static: bool = false,
getter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,
setter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,
getter: ?*const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void = null,
setter: ?*const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void = null,
const Opts = struct {
static: bool = false,
@@ -715,38 +168,27 @@ pub const Accessor = struct {
if (@typeInfo(@TypeOf(getter)) != .null) {
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);
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
if (comptime opts.static) {
caller.function(T, getter, info, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
} else {
caller.method(T, getter, info, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
}
caller.method(T, getter, info, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
});
}
}.wrap;
}
if (@typeInfo(@TypeOf(setter)) != .null) {
accessor.setter = struct {
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
defer caller.deinit();
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
std.debug.assert(info.length() == 1);
const info = FunctionCallbackInfo{ .handle = handle.? };
if (comptime IS_DEBUG) {
lp.assert(info.length() == 1, "bridge.setter", .{ .len = info.length() });
}
var caller = Caller.init(info);
defer caller.deinit();
caller.method(T, setter, info, .{
.as_typed_array = opts.as_typed_array,
@@ -761,7 +203,7 @@ pub const Accessor = struct {
};
pub const Indexed = struct {
getter: *const fn (idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8,
getter: *const fn (idx: u32, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8,
const Opts = struct {
as_typed_array: bool = false,
@@ -770,12 +212,10 @@ pub const Indexed = struct {
fn init(comptime T: type, comptime getter: anytype, comptime opts: Opts) Indexed {
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);
fn wrap(idx: u32, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const info = PropertyCallbackInfo{ .handle = handle.? };
return caller.getIndex(T, getter, idx, info, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
@@ -786,9 +226,9 @@ pub const Indexed = struct {
};
pub const NamedIndexed = struct {
getter: *const fn (c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8,
setter: ?*const fn (c_name: ?*const v8.Name, c_value: ?*const v8.Value, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 = null,
deleter: ?*const fn (c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 = null,
getter: *const fn (c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8,
setter: ?*const fn (c_name: ?*const v8.C_Name, c_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 = null,
deleter: ?*const fn (c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 = null,
const Opts = struct {
as_typed_array: bool = false,
@@ -797,12 +237,10 @@ pub const NamedIndexed = struct {
fn init(comptime T: type, comptime getter: anytype, setter: anytype, deleter: anytype, comptime opts: Opts) NamedIndexed {
const getter_fn = struct {
fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
var caller = Caller.init(v8_isolate);
fn wrap(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const info = PropertyCallbackInfo{ .handle = handle.? };
return caller.getNamedIndex(T, getter, .{ .handle = c_name.? }, info, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
@@ -811,12 +249,11 @@ pub const NamedIndexed = struct {
}.wrap;
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);
fn wrap(c_name: ?*const v8.C_Name, c_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const info = PropertyCallbackInfo{ .handle = handle.? };
return caller.setNamedIndex(T, setter, .{ .handle = c_name.? }, .{ .handle = c_value.? }, info, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
@@ -825,12 +262,11 @@ pub const NamedIndexed = struct {
}.wrap;
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);
fn wrap(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const info = PropertyCallbackInfo{ .handle = handle.? };
return caller.deleteNamedIndex(T, deleter, .{ .handle = c_name.? }, info, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
@@ -847,7 +283,7 @@ pub const NamedIndexed = struct {
};
pub const Iterator = struct {
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
async: bool,
const Opts = struct {
@@ -860,8 +296,8 @@ pub const Iterator = struct {
return .{
.async = opts.async,
.func = struct {
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
const info = FunctionCallbackInfo{ .handle = handle.? };
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
info.getReturnValue().set(info.getThis());
}
}.wrap,
@@ -871,12 +307,10 @@ pub const Iterator = struct {
return .{
.async = opts.async,
.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);
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
caller.method(T, struct_or_func, info, .{});
}
}.wrap,
@@ -885,7 +319,7 @@ pub const Iterator = struct {
};
pub const Callable = struct {
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
const Opts = struct {
null_as_undefined: bool = false,
@@ -893,12 +327,10 @@ pub const Callable = struct {
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Callable {
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);
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info);
defer caller.deinit();
const info = FunctionCallbackInfo{ .handle = handle.? };
caller.method(T, func, info, .{
.null_as_undefined = opts.null_as_undefined,
});
@@ -911,62 +343,6 @@ pub const Property = union(enum) {
int: i64,
};
pub fn unknownPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const isolate_handle = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
const context = Context.fromIsolate(.{ .handle = isolate_handle });
const property: []const u8 = context.valueToString(.{ .ctx = context, .handle = c_name.? }, .{}) catch {
return 0;
};
const ignored = std.StaticStringMap(void).initComptime(.{
.{ "process", {} },
.{ "ShadyDOM", {} },
.{ "ShadyCSS", {} },
.{ "litNonce", {} },
.{ "litHtmlVersions", {} },
.{ "litElementVersions", {} },
.{ "litHtmlPolyfillSupport", {} },
.{ "litElementHydrateSupport", {} },
.{ "litElementPolyfillSupport", {} },
.{ "reactiveElementVersions", {} },
.{ "recaptcha", {} },
.{ "grecaptcha", {} },
.{ "___grecaptcha_cfg", {} },
.{ "__recaptcha_api", {} },
.{ "__google_recaptcha_client", {} },
.{ "CLOSURE_FLAGS", {} },
});
if (!ignored.has(property)) {
const page = context.page;
const document = page.document;
if (document.getElementById(property, page)) |el| {
const js_value = context.zigValueToJs(el, .{}) catch {
return 0;
};
var pc = PropertyCallbackInfo{ .handle = handle.? };
pc.getReturnValue().set(js_value);
return 1;
}
if (comptime IS_DEBUG) {
log.debug(.unknown_prop, "unknown global property", .{
.info = "but the property can exist in pure JS",
.stack = context.stackTrace() catch "???",
.property = property,
});
}
}
// not intercepted
return 0;
}
// Given a Type, returns the length of the prototype chain, including self
fn prototypeChainLength(comptime T: type) usize {
var l: usize = 1;
@@ -1153,22 +529,16 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/element/Html.zig"),
@import("../webapi/element/html/IFrame.zig"),
@import("../webapi/element/html/Anchor.zig"),
@import("../webapi/element/html/Area.zig"),
@import("../webapi/element/html/Audio.zig"),
@import("../webapi/element/html/Base.zig"),
@import("../webapi/element/html/Body.zig"),
@import("../webapi/element/html/BR.zig"),
@import("../webapi/element/html/Button.zig"),
@import("../webapi/element/html/Canvas.zig"),
@import("../webapi/element/html/Custom.zig"),
@import("../webapi/element/html/Data.zig"),
@import("../webapi/element/html/DataList.zig"),
@import("../webapi/element/html/Dialog.zig"),
@import("../webapi/element/html/Directory.zig"),
@import("../webapi/element/html/Div.zig"),
@import("../webapi/element/html/Embed.zig"),
@import("../webapi/element/html/FieldSet.zig"),
@import("../webapi/element/html/Font.zig"),
@import("../webapi/element/html/Form.zig"),
@import("../webapi/element/html/Generic.zig"),
@import("../webapi/element/html/Head.zig"),
@@ -1177,42 +547,20 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/element/html/Html.zig"),
@import("../webapi/element/html/Image.zig"),
@import("../webapi/element/html/Input.zig"),
@import("../webapi/element/html/Label.zig"),
@import("../webapi/element/html/Legend.zig"),
@import("../webapi/element/html/LI.zig"),
@import("../webapi/element/html/Link.zig"),
@import("../webapi/element/html/Map.zig"),
@import("../webapi/element/html/Media.zig"),
@import("../webapi/element/html/Meta.zig"),
@import("../webapi/element/html/Meter.zig"),
@import("../webapi/element/html/Mod.zig"),
@import("../webapi/element/html/Object.zig"),
@import("../webapi/element/html/OL.zig"),
@import("../webapi/element/html/OptGroup.zig"),
@import("../webapi/element/html/Option.zig"),
@import("../webapi/element/html/Output.zig"),
@import("../webapi/element/html/Paragraph.zig"),
@import("../webapi/element/html/Param.zig"),
@import("../webapi/element/html/Pre.zig"),
@import("../webapi/element/html/Progress.zig"),
@import("../webapi/element/html/Quote.zig"),
@import("../webapi/element/html/Script.zig"),
@import("../webapi/element/html/Select.zig"),
@import("../webapi/element/html/Slot.zig"),
@import("../webapi/element/html/Source.zig"),
@import("../webapi/element/html/Span.zig"),
@import("../webapi/element/html/Style.zig"),
@import("../webapi/element/html/Table.zig"),
@import("../webapi/element/html/TableCaption.zig"),
@import("../webapi/element/html/TableCell.zig"),
@import("../webapi/element/html/TableCol.zig"),
@import("../webapi/element/html/TableRow.zig"),
@import("../webapi/element/html/TableSection.zig"),
@import("../webapi/element/html/Template.zig"),
@import("../webapi/element/html/TextArea.zig"),
@import("../webapi/element/html/Time.zig"),
@import("../webapi/element/html/Title.zig"),
@import("../webapi/element/html/Track.zig"),
@import("../webapi/element/html/Video.zig"),
@import("../webapi/element/html/UL.zig"),
@import("../webapi/element/html/Unknown.zig"),
@@ -1269,7 +617,4 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/navigation/NavigationEventTarget.zig"),
@import("../webapi/navigation/NavigationHistoryEntry.zig"),
@import("../webapi/navigation/NavigationActivation.zig"),
@import("../webapi/canvas/CanvasRenderingContext2D.zig"),
@import("../webapi/canvas/WebGLRenderingContext.zig"),
@import("../webapi/SubtleCrypto.zig"),
});

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

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

View File

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

View File

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

View File

@@ -1,35 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script id=CanvasRenderingContext2D>
{
const element = document.createElement("canvas");
const ctx = element.getContext("2d");
testing.expectEqual(true, ctx instanceof CanvasRenderingContext2D);
// We can't really test this but let's try to call it at least.
ctx.fillRect(0, 0, 0, 0);
}
</script>
<script id=CanvasRenderingContext2D#fillStyle>
{
const element = document.createElement("canvas");
const ctx = element.getContext("2d");
// Black by default.
testing.expectEqual(ctx.fillStyle, "#000000");
ctx.fillStyle = "red";
testing.expectEqual(ctx.fillStyle, "#ff0000");
ctx.fillStyle = "rebeccapurple";
testing.expectEqual(ctx.fillStyle, "#663399");
// No changes made if color is invalid.
ctx.fillStyle = "invalid-color";
testing.expectEqual(ctx.fillStyle, "#663399");
ctx.fillStyle = "#fc0";
testing.expectEqual(ctx.fillStyle, "#ffcc00");
ctx.fillStyle = "#ff0000";
testing.expectEqual(ctx.fillStyle, "#ff0000");
ctx.fillStyle = "#fF00000F";
testing.expectEqual(ctx.fillStyle, "rgba(255, 0, 0, 0.06)");
}
</script>

View File

@@ -1,87 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script id=WebGLRenderingContext#getSupportedExtensions>
{
const element = document.createElement("canvas");
const ctx = element.getContext("webgl");
testing.expectEqual(true, ctx instanceof WebGLRenderingContext);
const supportedExtensions = ctx.getSupportedExtensions();
// The order Chrome prefer.
const expectedExtensions = [
"ANGLE_instanced_arrays",
"EXT_blend_minmax",
"EXT_clip_control",
"EXT_color_buffer_half_float",
"EXT_depth_clamp",
"EXT_disjoint_timer_query",
"EXT_float_blend",
"EXT_frag_depth",
"EXT_polygon_offset_clamp",
"EXT_shader_texture_lod",
"EXT_texture_compression_bptc",
"EXT_texture_compression_rgtc",
"EXT_texture_filter_anisotropic",
"EXT_texture_mirror_clamp_to_edge",
"EXT_sRGB",
"KHR_parallel_shader_compile",
"OES_element_index_uint",
"OES_fbo_render_mipmap",
"OES_standard_derivatives",
"OES_texture_float",
"OES_texture_float_linear",
"OES_texture_half_float",
"OES_texture_half_float_linear",
"OES_vertex_array_object",
"WEBGL_blend_func_extended",
"WEBGL_color_buffer_float",
"WEBGL_compressed_texture_astc",
"WEBGL_compressed_texture_etc",
"WEBGL_compressed_texture_etc1",
"WEBGL_compressed_texture_pvrtc",
"WEBGL_compressed_texture_s3tc",
"WEBGL_compressed_texture_s3tc_srgb",
"WEBGL_debug_renderer_info",
"WEBGL_debug_shaders",
"WEBGL_depth_texture",
"WEBGL_draw_buffers",
"WEBGL_lose_context",
"WEBGL_multi_draw",
"WEBGL_polygon_mode"
];
testing.expectEqual(expectedExtensions.length, supportedExtensions.length);
for (let i = 0; i < expectedExtensions.length; i++) {
testing.expectEqual(expectedExtensions[i], supportedExtensions[i]);
}
}
</script>
<script id=WebGLRenderingCanvas#getExtension>
// WEBGL_debug_renderer_info
{
const element = document.createElement("canvas");
const ctx = element.getContext("webgl");
const rendererInfo = ctx.getExtension("WEBGL_debug_renderer_info");
testing.expectEqual(true, rendererInfo instanceof WEBGL_debug_renderer_info);
const { UNMASKED_VENDOR_WEBGL, UNMASKED_RENDERER_WEBGL } = rendererInfo;
testing.expectEqual(UNMASKED_VENDOR_WEBGL, 0x9245);
testing.expectEqual(UNMASKED_RENDERER_WEBGL, 0x9246);
testing.expectEqual("", ctx.getParameter(UNMASKED_VENDOR_WEBGL));
testing.expectEqual("", ctx.getParameter(UNMASKED_RENDERER_WEBGL));
}
// WEBGL_lose_context
{
const element = document.createElement("canvas");
const ctx = element.getContext("webgl");
const loseContext = ctx.getExtension("WEBGL_lose_context");
testing.expectEqual(true, loseContext instanceof WEBGL_lose_context);
loseContext.loseContext();
loseContext.restoreContext();
}
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -376,581 +376,3 @@
testing.expectEqual('Bold', fragment.childNodes[0].textContent);
}
</script>
<script id=offset_validation_setStart>
{
const range = document.createRange();
const p1 = $('#p1');
// Test setStart with offset beyond node length
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
range.setStart(p1, 999);
});
// Test with negative offset (wraps to large u32)
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
range.setStart(p1.firstChild, -1);
});
}
</script>
<script id=offset_validation_setEnd>
{
const range = document.createRange();
const p1 = $('#p1');
// Test setEnd with offset beyond node length
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
range.setEnd(p1, 999);
});
// Test with text node
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
range.setEnd(p1.firstChild, 9999);
});
}
</script>
<script id=comparePoint_basic>
{
// Create fresh elements to avoid DOM pollution from other tests
const div = document.createElement('div');
const p1 = document.createElement('p');
const p2 = document.createElement('p');
p1.textContent = 'First paragraph text';
p2.textContent = 'Second paragraph text';
div.appendChild(p1);
div.appendChild(p2);
const range = document.createRange();
range.setStart(p1.firstChild, 0);
range.setEnd(p2.firstChild, 5);
// Point before range
testing.expectEqual(-1, range.comparePoint(div, 0));
// Point at start boundary
testing.expectEqual(0, range.comparePoint(p1.firstChild, 0));
// Point inside range (in p1)
testing.expectEqual(0, range.comparePoint(p1.firstChild, 3));
// Point inside range (in p2)
testing.expectEqual(0, range.comparePoint(p2.firstChild, 2));
// Point at end boundary
testing.expectEqual(0, range.comparePoint(p2.firstChild, 5));
// Point after range
testing.expectEqual(1, range.comparePoint(p2.firstChild, 10));
}
</script>
<script id=comparePoint_validation>
{
// Create fresh element
const p1 = document.createElement('p');
p1.textContent = 'Test content';
const range = document.createRange();
range.setStart(p1, 0);
range.setEnd(p1, 1);
// Test comparePoint with invalid offset
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
range.comparePoint(p1, 20);
});
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
range.comparePoint(p1.firstChild, -1);
});
}
</script>
<script id=different_document_collapse>
{
// Create fresh element in current document
const p1 = document.createElement('p');
p1.textContent = 'Local content';
const range = document.createRange();
// Create a foreign document
const foreignDoc = document.implementation.createHTMLDocument('');
const foreignP = foreignDoc.createElement('p');
foreignP.textContent = 'Foreign';
foreignDoc.body.appendChild(foreignP);
// Set up range in current document
range.setStart(p1, 0);
range.setEnd(p1, 1);
testing.expectEqual(false, range.collapsed);
// Setting start to foreign document should collapse to that point
range.setStart(foreignP, 0);
testing.expectEqual(true, range.collapsed);
testing.expectEqual(foreignP, range.startContainer);
testing.expectEqual(foreignP, range.endContainer);
}
</script>
<script id=detached_node_collapse>
{
// Create fresh element
const p1 = document.createElement('p');
p1.textContent = 'Attached content';
const range = document.createRange();
// Create a detached element
const detached = document.createElement('div');
detached.textContent = 'Detached';
// Set up range in document
range.setStart(p1, 0);
range.setEnd(p1, 1);
testing.expectEqual(false, range.collapsed);
// Setting end to detached node should collapse
range.setEnd(detached.firstChild, 0);
testing.expectEqual(true, range.collapsed);
testing.expectEqual(detached.firstChild, range.startContainer);
testing.expectEqual(detached.firstChild, range.endContainer);
}
</script>
<script id=isPointInRange_basic>
{
// Create fresh elements
const div = document.createElement('div');
const p1 = document.createElement('p');
const p2 = document.createElement('p');
p1.textContent = 'First paragraph';
p2.textContent = 'Second paragraph';
div.appendChild(p1);
div.appendChild(p2);
const range = document.createRange();
range.setStart(p1.firstChild, 5);
range.setEnd(p2.firstChild, 6);
// Point before range
testing.expectEqual(false, range.isPointInRange(p1.firstChild, 0));
// Point at start boundary
testing.expectEqual(true, range.isPointInRange(p1.firstChild, 5));
// Point inside range
testing.expectEqual(true, range.isPointInRange(p1.firstChild, 7));
testing.expectEqual(true, range.isPointInRange(p2.firstChild, 3));
// Point at end boundary
testing.expectEqual(true, range.isPointInRange(p2.firstChild, 6));
// Point after range
testing.expectEqual(false, range.isPointInRange(p2.firstChild, 10));
}
</script>
<script id=isPointInRange_different_root>
{
// Create element in current document
const p1 = document.createElement('p');
p1.textContent = 'Local content';
const range = document.createRange();
range.setStart(p1, 0);
range.setEnd(p1, 1);
// Create element in different document
const foreignDoc = document.implementation.createHTMLDocument('');
const foreignP = foreignDoc.createElement('p');
foreignP.textContent = 'Foreign';
// Point in different root should return false (not throw)
testing.expectEqual(false, range.isPointInRange(foreignP, 0));
}
</script>
<script id=isPointInRange_validation>
{
const p1 = document.createElement('p');
p1.textContent = 'Test content';
const range = document.createRange();
range.setStart(p1, 0);
range.setEnd(p1, 1);
// Invalid offset should throw IndexSizeError
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
range.isPointInRange(p1, 999);
});
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
range.isPointInRange(p1.firstChild, 9999);
});
}
</script>
<script id=intersectsNode_basic>
{
// Create fresh elements
const div = document.createElement('div');
const p1 = document.createElement('p');
const p2 = document.createElement('p');
const p3 = document.createElement('p');
p1.textContent = 'First';
p2.textContent = 'Second';
p3.textContent = 'Third';
div.appendChild(p1);
div.appendChild(p2);
div.appendChild(p3);
const range = document.createRange();
range.setStart(p1.firstChild, 2);
range.setEnd(p2.firstChild, 3);
// Node that intersects (p1 contains the start)
testing.expectEqual(true, range.intersectsNode(p1));
// Node that intersects (p2 contains the end)
testing.expectEqual(true, range.intersectsNode(p2));
// Node that doesn't intersect (p3 is after the range)
testing.expectEqual(false, range.intersectsNode(p3));
// Container intersects
testing.expectEqual(true, range.intersectsNode(div));
}
</script>
<script id=intersectsNode_detached>
{
const div = document.createElement('div');
const p1 = document.createElement('p');
p1.textContent = 'Content';
div.appendChild(p1);
const range = document.createRange();
range.setStart(p1, 0);
range.setEnd(p1, 1);
// The root node (div) should return true when it has no parent
// (Note: div is detached, so it's in the same tree as the range)
testing.expectEqual(true, range.intersectsNode(div));
}
</script>
<script id=intersectsNode_different_root>
{
const p1 = document.createElement('p');
p1.textContent = 'Local';
const range = document.createRange();
range.setStart(p1, 0);
range.setEnd(p1, 1);
// Node in different document should return false
const foreignDoc = document.implementation.createHTMLDocument('');
const foreignP = foreignDoc.createElement('p');
testing.expectEqual(false, range.intersectsNode(foreignP));
}
</script>
<script id=commonAncestorContainer_same_node>
{
const p = document.createElement('p');
p.textContent = 'Content';
const range = document.createRange();
range.setStart(p.firstChild, 0);
range.setEnd(p.firstChild, 3);
// Both boundaries in same text node, so that's the common ancestor
testing.expectEqual(p.firstChild, range.commonAncestorContainer);
}
</script>
<script id=commonAncestorContainer_siblings>
{
const div = document.createElement('div');
const p1 = document.createElement('p');
const p2 = document.createElement('p');
p1.textContent = 'First';
p2.textContent = 'Second';
div.appendChild(p1);
div.appendChild(p2);
const range = document.createRange();
range.setStart(p1.firstChild, 0);
range.setEnd(p2.firstChild, 3);
// Start and end in different siblings, common ancestor is the parent div
testing.expectEqual(div, range.commonAncestorContainer);
}
</script>
<script id=commonAncestorContainer_nested>
{
const div = document.createElement('div');
const section = document.createElement('section');
const p = document.createElement('p');
const span = document.createElement('span');
p.textContent = 'Text';
span.textContent = 'Span';
div.appendChild(section);
section.appendChild(p);
div.appendChild(span);
const range = document.createRange();
range.setStart(p.firstChild, 0);
range.setEnd(span.firstChild, 2);
// Common ancestor of deeply nested p and sibling span is div
testing.expectEqual(div, range.commonAncestorContainer);
}
</script>
<script id=compareBoundaryPoints_constants>
{
// Test that the constants are defined
testing.expectEqual(0, Range.START_TO_START);
testing.expectEqual(1, Range.START_TO_END);
testing.expectEqual(2, Range.END_TO_END);
testing.expectEqual(3, Range.END_TO_START);
}
</script>
<script id=compareBoundaryPoints_basic>
{
const div = document.createElement('div');
const p1 = document.createElement('p');
const p2 = document.createElement('p');
p1.textContent = 'First paragraph';
p2.textContent = 'Second paragraph';
div.appendChild(p1);
div.appendChild(p2);
const range1 = document.createRange();
range1.setStart(p1.firstChild, 0);
range1.setEnd(p1.firstChild, 5);
const range2 = document.createRange();
range2.setStart(p1.firstChild, 3);
range2.setEnd(p2.firstChild, 5);
// range1 start is before range2 start
testing.expectEqual(-1, range1.compareBoundaryPoints(Range.START_TO_START, range2));
// range1 start is before range2 end
testing.expectEqual(-1, range1.compareBoundaryPoints(Range.START_TO_END, range2));
// range1 end is after range2 start
testing.expectEqual(1, range1.compareBoundaryPoints(Range.END_TO_START, range2));
// range1 end is before range2 end
testing.expectEqual(-1, range1.compareBoundaryPoints(Range.END_TO_END, range2));
}
</script>
<script id=compareBoundaryPoints_same_range>
{
const p = document.createElement('p');
p.textContent = 'Content';
const range = document.createRange();
range.setStart(p.firstChild, 2);
range.setEnd(p.firstChild, 5);
// Comparing a range to itself should return 0
testing.expectEqual(0, range.compareBoundaryPoints(Range.START_TO_START, range));
testing.expectEqual(0, range.compareBoundaryPoints(Range.END_TO_END, range));
// Start is before end
testing.expectEqual(-1, range.compareBoundaryPoints(Range.START_TO_END, range));
// End is after start
testing.expectEqual(1, range.compareBoundaryPoints(Range.END_TO_START, range));
}
</script>
<script id=compareBoundaryPoints_invalid_how>
{
const p = document.createElement('p');
p.textContent = 'Test';
const range1 = document.createRange();
const range2 = document.createRange();
range1.setStart(p, 0);
range2.setStart(p, 0);
// Invalid how parameter should throw NotSupportedError
testing.expectError('NotSupportedError: Not Supported', () => {
range1.compareBoundaryPoints(4, range2);
});
testing.expectError('NotSupportedError: Not Supported', () => {
range1.compareBoundaryPoints(99, range2);
});
}
</script>
<script id=compareBoundaryPoints_different_root>
{
const p1 = document.createElement('p');
p1.textContent = 'Local';
const range1 = document.createRange();
range1.setStart(p1, 0);
range1.setEnd(p1, 1);
// Create range in different document
const foreignDoc = document.implementation.createHTMLDocument('');
const foreignP = foreignDoc.createElement('p');
foreignP.textContent = 'Foreign';
const range2 = foreignDoc.createRange();
range2.setStart(foreignP, 0);
range2.setEnd(foreignP, 1);
// Comparing ranges in different documents should throw WrongDocumentError
testing.expectError('WrongDocumentError: wrong_document_error', () => {
range1.compareBoundaryPoints(Range.START_TO_START, range2);
});
}
</script>
<script id=deleteContents_crossNode>
{
// Test deleteContents across multiple sibling text nodes
const p = document.createElement('p');
p.appendChild(document.createTextNode('AAAA'));
p.appendChild(document.createTextNode('BBBB'));
p.appendChild(document.createTextNode('CCCC'));
testing.expectEqual(3, p.childNodes.length);
testing.expectEqual('AAAABBBBCCCC', p.textContent);
const range = document.createRange();
// Start at position 2 in first text node ("AA|AA")
range.setStart(p.childNodes[0], 2);
// End at position 2 in third text node ("CC|CC")
range.setEnd(p.childNodes[2], 2);
range.deleteContents();
// Should have truncated first node to "AA" and third node to "CC"
// Middle node should be removed
testing.expectEqual(2, p.childNodes.length);
testing.expectEqual('AA', p.childNodes[0].textContent);
testing.expectEqual('CC', p.childNodes[1].textContent);
testing.expectEqual('AACC', p.textContent);
}
</script>
<script id=deleteContents_crossNode_partial>
{
// Test deleteContents where start node is completely preserved
const p = document.createElement('p');
p.appendChild(document.createTextNode('KEEP'));
p.appendChild(document.createTextNode('DELETE'));
p.appendChild(document.createTextNode('PARTIAL'));
const range = document.createRange();
// Start at end of first text node
range.setStart(p.childNodes[0], 4);
// End in middle of third text node
range.setEnd(p.childNodes[2], 4);
range.deleteContents();
testing.expectEqual(2, p.childNodes.length);
testing.expectEqual('KEEP', p.childNodes[0].textContent);
testing.expectEqual('IAL', p.childNodes[1].textContent);
testing.expectEqual('KEEPIAL', p.textContent);
}
</script>
<script id=extractContents_crossNode>
{
// Test extractContents across multiple sibling text nodes
const p = document.createElement('p');
p.appendChild(document.createTextNode('AAAA'));
p.appendChild(document.createTextNode('BBBB'));
p.appendChild(document.createTextNode('CCCC'));
const range = document.createRange();
range.setStart(p.childNodes[0], 2);
range.setEnd(p.childNodes[2], 2);
const fragment = range.extractContents();
// Original should be truncated
testing.expectEqual(2, p.childNodes.length);
testing.expectEqual('AA', p.childNodes[0].textContent);
testing.expectEqual('CC', p.childNodes[1].textContent);
// Fragment should contain extracted content
testing.expectEqual(3, fragment.childNodes.length);
testing.expectEqual('AA', fragment.childNodes[0].textContent);
testing.expectEqual('BBBB', fragment.childNodes[1].textContent);
testing.expectEqual('CC', fragment.childNodes[2].textContent);
}
</script>
<script id=cloneContents_crossNode>
{
// Test cloneContents across multiple sibling text nodes
const p = document.createElement('p');
p.appendChild(document.createTextNode('AAAA'));
p.appendChild(document.createTextNode('BBBB'));
p.appendChild(document.createTextNode('CCCC'));
const range = document.createRange();
range.setStart(p.childNodes[0], 2);
range.setEnd(p.childNodes[2], 2);
const fragment = range.cloneContents();
// Original should be unchanged
testing.expectEqual(3, p.childNodes.length);
testing.expectEqual('AAAA', p.childNodes[0].textContent);
testing.expectEqual('BBBB', p.childNodes[1].textContent);
testing.expectEqual('CCCC', p.childNodes[2].textContent);
// Fragment should contain cloned content
testing.expectEqual(3, fragment.childNodes.length);
testing.expectEqual('AA', fragment.childNodes[0].textContent);
testing.expectEqual('BBBB', fragment.childNodes[1].textContent);
testing.expectEqual('CC', fragment.childNodes[2].textContent);
}
</script>
<script id=deleteContents_crossNode_withElements>
{
// Test deleteContents with mixed text and element nodes
const div = document.createElement('div');
div.appendChild(document.createTextNode('Start'));
const span = document.createElement('span');
span.textContent = 'Middle';
div.appendChild(span);
div.appendChild(document.createTextNode('End'));
testing.expectEqual(3, div.childNodes.length);
const range = document.createRange();
// Start in middle of first text node
range.setStart(div.childNodes[0], 2);
// End in middle of last text node
range.setEnd(div.childNodes[2], 1);
range.deleteContents();
// Should keep "St" from start, remove span, keep "nd" from end
testing.expectEqual(2, div.childNodes.length);
testing.expectEqual('St', div.childNodes[0].textContent);
testing.expectEqual('nd', div.childNodes[1].textContent);
testing.expectEqual('Stnd', div.textContent);
}
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -55,6 +55,15 @@ pub fn is(self: *CData, comptime T: type) ?*T {
return null;
}
pub fn className(self: *const CData) []const u8 {
return switch (self._type) {
.text => "[object Text]",
.comment => "[object Comment]",
.cdata_section => "[object CDATASection]",
.processing_instruction => "[object ProcessingInstruction]",
};
}
pub fn getData(self: *const CData) []const u8 {
return self._data;
}
@@ -138,7 +147,7 @@ pub fn format(self: *const CData, writer: *std.io.Writer) !void {
}
pub fn getLength(self: *const CData) usize {
return std.unicode.utf8CountCodepoints(self._data) catch self._data.len;
return self._data.len;
}
pub fn isEqualNode(self: *const CData, other: *const CData) bool {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -48,13 +48,11 @@ _location: ?*Location = null,
_ready_state: ReadyState = .loading,
_current_script: ?*Element.Html.Script = null,
_elements_by_id: std.StringHashMapUnmanaged(*Element) = .empty,
// Track IDs that were removed from the map - they might have duplicates in the tree
_removed_ids: std.StringHashMapUnmanaged(void) = .empty,
_active_element: ?*Element = null,
_style_sheets: ?*StyleSheetList = null,
_write_insertion_point: ?*Node = null,
_script_created_parser: ?Parser.Streaming = null,
_adopted_style_sheets: ?js.Object.Global = null,
_adopted_style_sheets: ?js.Object = null,
pub const Type = union(enum) {
generic,
@@ -123,23 +121,10 @@ const CreateElementOptions = struct {
is: ?[]const u8 = null,
};
pub fn createElement(self: *Document, name: []const u8, options_: ?CreateElementOptions, page: *Page) !*Element {
try validateElementName(name);
const namespace: Element.Namespace = blk: {
if (self._type == .html) {
break :blk .html;
}
// Generic and XML documents create XML elements
break :blk .xml;
};
const node = try page.createElementNS(namespace, name, null);
pub fn createElement(_: *const Document, name: []const u8, options_: ?CreateElementOptions, page: *Page) !*Element {
const node = try page.createElement(null, name, null);
const element = node.as(Element);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
}
const options = options_ orelse return element;
if (options.is) |is_value| {
try element.setAttribute("is", is_value, page);
@@ -149,14 +134,8 @@ pub fn createElement(self: *Document, name: []const u8, options_: ?CreateElement
return element;
}
pub fn createElementNS(self: *Document, namespace: ?[]const u8, name: []const u8, page: *Page) !*Element {
try validateElementName(name);
const node = try page.createElementNS(Element.Namespace.parse(namespace), name, null);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
}
pub fn createElementNS(_: *const Document, namespace: ?[]const u8, name: []const u8, page: *Page) !*Element {
const node = try page.createElement(namespace, name, null);
return node.as(Element);
}
@@ -184,32 +163,9 @@ pub fn createAttributeNS(_: *const Document, namespace: []const u8, name: []cons
});
}
pub fn getElementById(self: *Document, id: []const u8, page: *Page) ?*Element {
if (id.len == 0) {
return null;
}
if (self._elements_by_id.get(id)) |element| {
return element;
}
//ID was removed but might have duplicates
if (self._removed_ids.remove(id)) {
var tw = @import("TreeWalker.zig").Full.Elements.init(self.asNode(), .{});
while (tw.next()) |el| {
const element_id = el.getAttributeSafe("id") orelse continue;
if (std.mem.eql(u8, element_id, id)) {
// we ignore this error to keep getElementById easy to call
// if it really failed, then we're out of memory and nothing's
// going to work like it should anyways.
const owned_id = page.dupeString(id) catch return null;
self._elements_by_id.put(page.arena, owned_id, el) catch return null;
return el;
}
}
}
return null;
pub fn getElementById(self: *const Document, id_: ?[]const u8) ?*Element {
const id = id_ orelse return null;
return self._elements_by_id.get(id);
}
const GetElementsByTagNameResult = union(enum) {
@@ -284,57 +240,40 @@ pub fn querySelectorAll(self: *Document, input: []const u8, page: *Page) !*Selec
return Selector.querySelectorAll(self.asNode(), input, page);
}
pub fn className(self: *const Document) []const u8 {
return switch (self._type) {
.generic => "[object Document]",
.html => "[object HTMLDocument]",
.xml => "[object XMLDocument]",
};
}
pub fn getImplementation(_: *const Document) DOMImplementation {
return .{};
}
pub fn createDocumentFragment(self: *Document, page: *Page) !*Node.DocumentFragment {
const frag = try Node.DocumentFragment.init(page);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(frag.asNode(), self);
}
return frag;
pub fn createDocumentFragment(_: *const Document, page: *Page) !*Node.DocumentFragment {
return Node.DocumentFragment.init(page);
}
pub fn createComment(self: *Document, data: []const u8, page: *Page) !*Node {
const node = try page.createComment(data);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
}
return node;
pub fn createComment(_: *const Document, data: []const u8, page: *Page) !*Node {
return page.createComment(data);
}
pub fn createTextNode(self: *Document, data: []const u8, page: *Page) !*Node {
const node = try page.createTextNode(data);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
}
return node;
pub fn createTextNode(_: *const Document, data: []const u8, page: *Page) !*Node {
return page.createTextNode(data);
}
pub fn createCDATASection(self: *Document, data: []const u8, page: *Page) !*Node {
const node = switch (self._type) {
pub fn createCDATASection(self: *const Document, data: []const u8, page: *Page) !*Node {
switch (self._type) {
.html => return error.NotSupported, // cannot create a CDataSection in an HTMLDocument
.xml => try page.createCDATASection(data),
.generic => try page.createCDATASection(data),
};
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
.xml => return page.createCDATASection(data),
.generic => return page.createCDATASection(data),
}
return node;
}
pub fn createProcessingInstruction(self: *Document, target: []const u8, data: []const u8, page: *Page) !*Node {
const node = try page.createProcessingInstruction(target, data);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
}
return node;
pub fn createProcessingInstruction(_: *const Document, target: []const u8, data: []const u8, page: *Page) !*Node {
return page.createProcessingInstruction(target, data);
}
const Range = @import("Range.zig");
@@ -362,26 +301,14 @@ pub fn createEvent(_: *const Document, event_type: []const u8, page: *Page) !*@i
return error.NotSupported;
}
pub fn createTreeWalker(_: *const Document, root: *Node, what_to_show: ?js.Value, filter: ?DOMTreeWalker.FilterOpts, page: *Page) !*DOMTreeWalker {
return DOMTreeWalker.init(root, try whatToShow(what_to_show), filter, page);
pub fn createTreeWalker(_: *const Document, root: *Node, what_to_show: ?u32, filter: ?DOMTreeWalker.FilterOpts, page: *Page) !*DOMTreeWalker {
const show = what_to_show orelse NodeFilter.SHOW_ALL;
return DOMTreeWalker.init(root, show, filter, page);
}
pub fn createNodeIterator(_: *const Document, root: *Node, what_to_show: ?js.Value, filter: ?DOMNodeIterator.FilterOpts, page: *Page) !*DOMNodeIterator {
return DOMNodeIterator.init(root, try whatToShow(what_to_show), filter, page);
}
fn whatToShow(value_: ?js.Value) !u32 {
const value = value_ orelse return 4294967295; // show all when undefined
if (value.isUndefined()) {
// undefined explicitly passed
return 4294967295;
}
if (value.isNull()) {
return 0;
}
return value.toZig(u32);
pub fn createNodeIterator(_: *const Document, root: *Node, what_to_show: ?u32, filter: ?DOMNodeIterator.FilterOpts, page: *Page) !*DOMNodeIterator {
const show = what_to_show orelse NodeFilter.SHOW_ALL;
return DOMNodeIterator.init(root, show, filter, page);
}
pub fn getReadyState(self: *const Document) []const u8 {
@@ -434,103 +361,20 @@ pub fn importNode(_: *const Document, node: *Node, deep_: ?bool, page: *Page) !*
}
pub fn append(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !void {
try validateDocumentNodes(self, nodes, false);
page.domChanged();
const parent = self.asNode();
const parent_is_connected = parent.isConnected();
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(page);
// DocumentFragments are special - append all their children
if (child.is(Node.DocumentFragment)) |_| {
try page.appendAllChildren(child, parent);
continue;
}
var child_connected = false;
if (child._parent) |previous_parent| {
child_connected = child.isConnected();
page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent_is_connected });
}
try page.appendNode(parent, child, .{ .child_already_connected = child_connected });
_ = try parent.appendChild(child, page);
}
}
pub fn prepend(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !void {
try validateDocumentNodes(self, nodes, false);
page.domChanged();
const parent = self.asNode();
const parent_is_connected = parent.isConnected();
var i = nodes.len;
while (i > 0) {
i -= 1;
const child = try nodes[i].toNode(page);
// DocumentFragments are special - need to insert all their children
if (child.is(Node.DocumentFragment)) |frag| {
const first_child = parent.firstChild();
var frag_child = frag.asNode().lastChild();
while (frag_child) |fc| {
const prev = fc.previousSibling();
page.removeNode(frag.asNode(), fc, .{ .will_be_reconnected = parent_is_connected });
if (first_child) |before| {
try page.insertNodeRelative(parent, fc, .{ .before = before }, .{});
} else {
try page.appendNode(parent, fc, .{});
}
frag_child = prev;
}
continue;
}
var child_connected = false;
if (child._parent) |previous_parent| {
child_connected = child.isConnected();
page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent_is_connected });
}
const first_child = parent.firstChild();
if (first_child) |before| {
try page.insertNodeRelative(parent, child, .{ .before = before }, .{ .child_already_connected = child_connected });
} else {
try page.appendNode(parent, child, .{ .child_already_connected = child_connected });
}
}
}
pub fn replaceChildren(self: *Document, nodes: []const Node.NodeOrText, page: *Page) !void {
try validateDocumentNodes(self, nodes, true);
page.domChanged();
const parent = self.asNode();
// Remove all existing children
var it = parent.childrenIterator();
while (it.next()) |child| {
page.removeNode(parent, child, .{ .will_be_reconnected = false });
}
// Append new children
const parent_is_connected = parent.isConnected();
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(page);
// DocumentFragments are special - append all their children
if (child.is(Node.DocumentFragment)) |_| {
try page.appendAllChildren(child, parent);
continue;
}
var child_connected = false;
if (child._parent) |previous_parent| {
child_connected = child.isConnected();
page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent_is_connected });
}
try page.appendNode(parent, child, .{ .child_already_connected = child_connected });
_ = try parent.insertBefore(child, parent.firstChild(), page);
}
}
@@ -576,13 +420,7 @@ pub fn elementsFromPoint(self: *Document, x: f64, y: f64, page: *Page) ![]const
return result.items;
}
pub fn getDocType(self: *Document) ?*Node {
var tw = @import("TreeWalker.zig").Full.init(self.asNode(), .{});
while (tw.next()) |node| {
if (node._type == .document_type) {
return node;
}
}
pub fn getDocType(_: *const Document) ?*DocumentType {
return null;
}
@@ -766,14 +604,13 @@ pub fn getChildElementCount(self: *Document) u32 {
return i;
}
pub fn getAdoptedStyleSheets(self: *Document, page: *Page) !js.Object.Global {
pub fn getAdoptedStyleSheets(self: *Document, page: *Page) !js.Object {
if (self._adopted_style_sheets) |ass| {
return ass;
}
const js_arr = page.js.newArray(0);
const js_obj = js_arr.toObject();
self._adopted_style_sheets = try js_obj.persist();
return self._adopted_style_sheets.?;
const obj = try page.js.createArray(0).persist();
self._adopted_style_sheets = obj;
return obj;
}
pub fn hasFocus(_: *Document) bool {
@@ -785,118 +622,6 @@ pub fn setAdoptedStyleSheets(self: *Document, sheets: js.Object) !void {
self._adopted_style_sheets = try sheets.persist();
}
// Validates that nodes can be inserted into a Document, respecting Document constraints:
// - At most one Element child
// - At most one DocumentType child
// - No Document, Attribute, or Text nodes
// - Only Element, DocumentType, Comment, and ProcessingInstruction are allowed
// When replacing=true, existing children are not counted (for replaceChildren)
fn validateDocumentNodes(self: *Document, nodes: []const Node.NodeOrText, comptime replacing: bool) !void {
const parent = self.asNode();
// Check existing elements and doctypes (unless we're replacing all children)
var has_element = false;
var has_doctype = false;
if (!replacing) {
var it = parent.childrenIterator();
while (it.next()) |child| {
if (child._type == .element) {
has_element = true;
} else if (child._type == .document_type) {
has_doctype = true;
}
}
}
// Validate new nodes
for (nodes) |node_or_text| {
switch (node_or_text) {
.text => {
// Text nodes are not allowed as direct children of Document
return error.HierarchyError;
},
.node => |child| {
// Check if it's a DocumentFragment - need to validate its children
if (child.is(Node.DocumentFragment)) |frag| {
var frag_it = frag.asNode().childrenIterator();
while (frag_it.next()) |frag_child| {
// Document can only contain: Element, DocumentType, Comment, ProcessingInstruction
switch (frag_child._type) {
.element => {
if (has_element) {
return error.HierarchyError;
}
has_element = true;
},
.document_type => {
if (has_doctype) {
return error.HierarchyError;
}
has_doctype = true;
},
.cdata => |cd| switch (cd._type) {
.comment, .processing_instruction => {}, // Allowed
.text, .cdata_section => return error.HierarchyError, // Not allowed in Document
},
.document, .attribute, .document_fragment => return error.HierarchyError,
}
}
} else {
// Validate node type for direct insertion
switch (child._type) {
.element => {
if (has_element) {
return error.HierarchyError;
}
has_element = true;
},
.document_type => {
if (has_doctype) {
return error.HierarchyError;
}
has_doctype = true;
},
.cdata => |cd| switch (cd._type) {
.comment, .processing_instruction => {}, // Allowed
.text, .cdata_section => return error.HierarchyError, // Not allowed in Document
},
.document, .attribute, .document_fragment => return error.HierarchyError,
}
}
// Check for cycles
if (child.contains(parent)) {
return error.HierarchyError;
}
},
}
}
}
fn validateElementName(name: []const u8) !void {
if (name.len == 0) {
return error.InvalidCharacterError;
}
const first = name[0];
// Element names cannot start with: digits, period, hyphen
if ((first >= '0' and first <= '9') or first == '.' or first == '-') {
return error.InvalidCharacterError;
}
for (name[1..]) |c| {
const is_valid = (c >= 'a' and c <= 'z') or
(c >= 'A' and c <= 'Z') or
(c >= '0' and c <= '9') or
c == '_' or c == '-' or c == '.' or c == ':';
if (!is_valid) {
return error.InvalidCharacterError;
}
}
}
const ReadyState = enum {
loading,
interactive,
@@ -935,8 +660,8 @@ pub const JsApi = struct {
pub const compatMode = bridge.accessor(Document.getCompatMode, null, .{});
pub const referrer = bridge.accessor(Document.getReferrer, null, .{});
pub const domain = bridge.accessor(Document.getDomain, null, .{});
pub const createElement = bridge.function(Document.createElement, .{ .dom_exception = true });
pub const createElementNS = bridge.function(Document.createElementNS, .{ .dom_exception = true });
pub const createElement = bridge.function(Document.createElement, .{});
pub const createElementNS = bridge.function(Document.createElementNS, .{});
pub const createDocumentFragment = bridge.function(Document.createDocumentFragment, .{});
pub const createComment = bridge.function(Document.createComment, .{});
pub const createTextNode = bridge.function(Document.createTextNode, .{});
@@ -948,17 +673,7 @@ pub const JsApi = struct {
pub const createEvent = bridge.function(Document.createEvent, .{ .dom_exception = true });
pub const createTreeWalker = bridge.function(Document.createTreeWalker, .{});
pub const createNodeIterator = bridge.function(Document.createNodeIterator, .{});
pub const getElementById = bridge.function(_getElementById, .{});
fn _getElementById(self: *Document, value_: ?js.Value, page: *Page) !?*Element {
const value = value_ orelse return null;
if (value.isNull()) {
return self.getElementById("null", page);
}
if (value.isUndefined()) {
return self.getElementById("undefined", page);
}
return self.getElementById(try value.toZig([]const u8), page);
}
pub const getElementById = bridge.function(Document.getElementById, .{});
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, .{});
@@ -966,9 +681,8 @@ pub const JsApi = struct {
pub const getElementsByName = bridge.function(Document.getElementsByName, .{});
pub const adoptNode = bridge.function(Document.adoptNode, .{ .dom_exception = true });
pub const importNode = bridge.function(Document.importNode, .{ .dom_exception = true });
pub const append = bridge.function(Document.append, .{ .dom_exception = true });
pub const prepend = bridge.function(Document.prepend, .{ .dom_exception = true });
pub const replaceChildren = bridge.function(Document.replaceChildren, .{ .dom_exception = true });
pub const append = bridge.function(Document.append, .{});
pub const prepend = bridge.function(Document.prepend, .{});
pub const elementFromPoint = bridge.function(Document.elementFromPoint, .{});
pub const elementsFromPoint = bridge.function(Document.elementsFromPoint, .{});
pub const write = bridge.function(Document.write, .{ .dom_exception = true });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -34,7 +34,7 @@ pub fn getLength(_: *const History, page: *Page) u32 {
pub fn getState(_: *const History, page: *Page) !?js.Value {
if (page._session.navigation.getCurrentEntry()._state.value) |state| {
const value = try page.js.parseJSON(state);
const value = try js.Value.fromJson(page.js, state);
return value;
} else return null;
}
@@ -49,7 +49,7 @@ pub fn setScrollRestoration(self: *History, str: []const u8) void {
}
}
pub fn pushState(_: *History, state: js.Value, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
pub fn pushState(_: *History, state: js.Object, _: []const u8, _url: ?[]const u8, page: *Page) !void {
const arena = page._session.arena;
const url = if (_url) |u| try arena.dupeZ(u8, u) else try arena.dupeZ(u8, page.url);
@@ -57,7 +57,7 @@ pub fn pushState(_: *History, state: js.Value, _: ?[]const u8, _url: ?[]const u8
_ = try page._session.navigation.pushEntry(url, .{ .source = .history, .value = json }, page, true);
}
pub fn replaceState(_: *History, state: js.Value, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
pub fn replaceState(_: *History, state: js.Object, _: []const u8, _url: ?[]const u8, page: *Page) !void {
const arena = page._session.arena;
const url = if (_url) |u| try arena.dupeZ(u8, u) else try arena.dupeZ(u8, page.url);
@@ -81,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.window._on_popstate,
.{ .context = "Pop State" },
);
}

View File

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

View File

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

View File

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

View File

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

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