Merge branch 'main' into agent

This commit is contained in:
Adrià Arrufat
2026-05-26 19:48:18 +02:00
90 changed files with 1809 additions and 653 deletions

View File

@@ -18,18 +18,17 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const App = @import("../App.zig");
const CDP = @import("../cdp/CDP.zig");
const Notification = @import("../Notification.zig");
const js = @import("js/js.zig");
const App = @import("../App.zig");
const Page = @import("Page.zig");
const Session = @import("Session.zig");
const HttpClient = @import("HttpClient.zig");
const CDP = @import("../cdp/CDP.zig");
const ArenaPool = App.ArenaPool;
const Session = @import("Session.zig");
const Page = @import("Page.zig");
const Notification = @import("../Notification.zig");
const Allocator = std.mem.Allocator;
// Browser is an instance of the browser.
// You can create multiple browser instances.
@@ -46,6 +45,14 @@ http_client: HttpClient,
// used by sessions to allocate pages.
page_pool: std.heap.MemoryPool(Page),
// Pool for FinalizerCallback.Identity structs — the records V8 weak-callback
// parameters point at. Scoped to the Browser (i.e. the V8 Isolate's lifetime)
// rather than the Session: V8 can run a weak finalizer arbitrarily late, any
// time up until the Isolate is torn down, so these must outlive every Session.
// Freed in deinit *after* env.deinit() tears down the Isolate — the point past
// which no finalizer can fire.
fc_identity_pool: std.heap.MemoryPool(js.FinalizerCallback.Identity),
// Monotonic frame-ID generator scoped to this Browser (one per CDP
// connection). Lives here, not on Session, because CDP target IDs
// (encoded as `FID-{d:0>10}`) must be unique for the lifetime of the
@@ -84,6 +91,7 @@ pub fn init(self: *Browser, app: *App, opts: InitOpts, cdp: ?*CDP) !void {
.arena_pool = &app.arena_pool,
.http_client = undefined,
.page_pool = std.heap.MemoryPool(Page).init(allocator),
.fc_identity_pool = .init(allocator),
};
try self.http_client.init(allocator, &app.network, cdp);
}
@@ -91,6 +99,9 @@ pub fn init(self: *Browser, app: *App, opts: InitOpts, cdp: ?*CDP) !void {
pub fn deinit(self: *Browser) void {
self.closeSession();
self.env.deinit();
// After env.deinit() the Isolate is gone, so no further weak finalizer can
// fire — only now is it safe to free the pool backing their parameters.
self.fc_identity_pool.deinit();
self.page_pool.deinit();
self.http_client.deinit();
}

View File

@@ -0,0 +1,162 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// Implements the spec's "custom element reactions" mechanism: callbacks
// (connectedCallback, disconnectedCallback, adoptedCallback,
// attributeChangedCallback) are enqueued during DOM mutation and invoked at
// the outer algorithm boundary, not synchronously mid-mutation.
//
// The "stack of element queues" is collapsed to a single flat ArrayList plus
// per-scope checkpoint indices: push() captures items.len, popAndInvoke()
// drains items[checkpoint..] and truncates. Nested scopes work naturally —
// inside a callback, a new scope captures its own checkpoint past the current
// length, drains its own range, and the outer iteration continues from where
// it left off.
//
// When a reaction is enqueued without an active scope (e.g. a Web API path
// that wasn't tagged `.ce_reactions = true`, or a non-WebIDL entry point),
// it goes on the backup queue instead and a microtask is scheduled to drain
// it. This matches the spec's "backup element queue" so missing bridge tags
// degrade to delayed reactions rather than crashes.
const std = @import("std");
const lp = @import("lightpanda");
const Frame = @import("Frame.zig");
const Element = @import("webapi/Element.zig");
const Document = @import("webapi/Document.zig");
const Custom = @import("webapi/element/html/Custom.zig");
const String = lp.String;
const Allocator = std.mem.Allocator;
const IS_DEBUG = @import("builtin").mode == .Debug;
const Self = @This();
allocator: Allocator,
queue: std.ArrayList(Reaction) = .empty,
backup_scheduled: bool = false,
backup_queue: std.ArrayList(Reaction) = .empty,
// Number of currently-open scopes (push() that hasn't been pop'd). When 0,
// enqueues route to the backup queue and rely on a microtask to drain.
active_scopes: u32 = 0,
/// Open a new reactions scope. Returns a checkpoint to be passed to popAndInvoke.
pub fn push(self: *Self) usize {
self.active_scopes += 1;
return self.queue.items.len;
}
/// Drain reactions queued at indices >= checkpoint, then truncate. Reactions
/// enqueued within a nested scope drain at that scope's pop, before this loop
/// sees them.
pub fn popAndInvoke(self: *Self, checkpoint: usize, frame: *Frame) void {
// Index, not slice: firing a reaction can recursively enqueue (via JS
// callbacks doing DOM mutations), which may realloc queue.items and
// invalidate any captured slice.
var i = checkpoint;
while (i < self.queue.items.len) : (i += 1) {
Custom.fireReaction(self.queue.items[i], frame);
}
self.queue.items.len = checkpoint;
self.active_scopes -= 1;
}
/// Drain the backup queue. Called from the scheduled microtask. `backup_scheduled`
/// stays true while draining so new enqueues append to backup_queue and get picked
/// up by the same loop instead of scheduling a redundant microtask.
pub fn drainBackup(self: *Self, frame: *Frame) void {
var i: usize = 0;
while (i < self.backup_queue.items.len) : (i += 1) {
Custom.fireReaction(self.backup_queue.items[i], frame);
}
self.backup_queue.clearRetainingCapacity();
self.backup_scheduled = false;
}
fn route(self: *Self, frame: *Frame, reaction: Reaction) !void {
if (self.active_scopes > 0) {
try self.queue.append(self.allocator, reaction);
return;
}
if (comptime IS_DEBUG) {
lp.log.err(.bug, "custom element scope", .{ .note = "Missing explicit reaction scope, using fallback. This log is only generated in debug builds." });
}
try self.backup_queue.append(self.allocator, reaction);
if (!self.backup_scheduled) {
try frame.scheduleCustomElementBackupDrain();
self.backup_scheduled = true;
}
}
pub fn enqueueConnected(self: *Self, frame: *Frame, element: *Element) !void {
try self.route(frame, .{ .connected = element });
}
pub fn enqueueDisconnected(self: *Self, frame: *Frame, element: *Element) !void {
try self.route(frame, .{ .disconnected = element });
}
pub fn enqueueAdopted(self: *Self, frame: *Frame, element: *Element, old_document: *Document, new_document: *Document) !void {
try self.route(frame, .{ .adopted = .{
.element = element,
.old_document = old_document,
.new_document = new_document,
} });
}
pub fn enqueueAttributeChanged(
self: *Self,
frame: *Frame,
element: *Element,
name: String,
old_value: ?String,
new_value: ?String,
namespace: ?String,
) !void {
try self.route(frame, .{ .attribute_changed = .{
.name = name,
.element = element,
.old_value = old_value,
.new_value = new_value,
.namespace = namespace,
} });
}
pub const Reaction = union(enum) {
connected: *Element,
disconnected: *Element,
adopted: Adopted,
attribute_changed: AttributeChanged,
pub const Adopted = struct {
element: *Element,
old_document: *Document,
new_document: *Document,
};
pub const AttributeChanged = struct {
element: *Element,
name: String,
old_value: ?String,
new_value: ?String,
namespace: ?String,
};
};

View File

@@ -32,6 +32,8 @@ const StyleManager = @import("StyleManager.zig");
const Parser = @import("parser/Parser.zig");
const h5e = @import("parser/html5ever.zig");
const CustomElementReactions = @import("CustomElementReactions.zig");
const URL = @import("URL.zig");
const Blob = @import("webapi/Blob.zig");
const Node = @import("webapi/Node.zig");
@@ -185,6 +187,12 @@ _upgrading_element: ?*Node = null,
// List of custom elements that were created before their definition was registered
_undefined_custom_elements: std.ArrayList(*Element.Html.Custom) = .{},
// Pending custom-element reactions (connected/disconnected/adopted/attribute
// changed). Reactions are enqueued during DOM mutation and drained at the
// outer algorithm boundary — set up by the JS bridge for [CEReactions]
// methods and by the parser pump on each yield.
_ce_reactions: CustomElementReactions,
// for heap allocations and managing WebAPI objects
_factory: *Factory,
@@ -295,6 +303,7 @@ pub fn init(self: *Frame, frame_id: u32, page: *Page, parent: ?*Frame) !void {
._type = if (parent == null) .root else .frame,
._style_manager = undefined,
._script_manager = undefined,
._ce_reactions = .{ .allocator = arena },
._event_manager = EventManager.init(arena, self),
};
self._to_load = &self._to_load_1;
@@ -1822,6 +1831,10 @@ pub fn scheduleSlotchangeDelivery(self: *Frame) !void {
try self.js.queueSlotchangeDelivery();
}
pub fn scheduleCustomElementBackupDrain(self: *Frame) !void {
try self.js.queueCustomElementBackupDrain();
}
pub fn performScheduledIntersectionChecks(self: *Frame) void {
if (!self._intersection_check_scheduled) {
return;
@@ -1971,7 +1984,7 @@ pub fn adoptNodeTree(self: *Frame, node: *Node, old_owner: *Document, new_owner:
// Per spec, adopted steps run on each element after its document is set.
if (node.is(Element)) |el| {
Element.Html.Custom.invokeAdoptedCallbackOnElement(el, old_owner, new_owner, self);
Element.Html.Custom.enqueueAdoptedCallbackOnElement(el, old_owner, new_owner, self);
}
var it = node.childrenIterator();
@@ -2707,7 +2720,7 @@ pub fn createElementNS(self: *Frame, namespace: Element.Namespace, name: []const
if (element._attributes) |attributes| {
var it = attributes.iterator();
while (it.next()) |attr| {
Element.Html.Custom.invokeAttributeChangedCallbackOnElement(
Element.Html.Custom.enqueueAttributeChangedCallbackOnElement(
element,
attr._name,
null, // old_value is null for initial attributes
@@ -3100,7 +3113,7 @@ pub fn removeNode(self: *Frame, parent: *Node, child: *Node, opts: RemoveNodeOpt
self.removeElementIdWithMaps(id_maps.?, id);
}
Element.Html.Custom.invokeDisconnectedCallbackOnElement(el, self);
Element.Html.Custom.enqueueDisconnectedCallbackOnElement(el, self);
// If a <style> element is being removed, remove its sheet from the list
if (el.is(Element.Html.Style)) |style| {
@@ -3137,11 +3150,8 @@ pub fn appendAllChildren(self: *Frame, parent: *Node, target: *Node) !void {
self.domChanged();
const dest_connected = target.isConnected();
// Use firstChild() instead of iterator to handle cases where callbacks
// (like custom element connectedCallback) modify the parent during iteration.
// The iterator captures "next" pointers that can become stale.
while (parent.firstChild()) |child| {
// Check if child was connected BEFORE removing it from parent
var it = parent.childrenIterator();
while (it.next()) |child| {
const child_was_connected = child.isConnected();
self.removeNode(parent, child, .{ .will_be_reconnected = dest_connected });
try self.appendNode(target, child, .{ .child_already_connected = child_was_connected });
@@ -3152,31 +3162,16 @@ pub fn insertAllChildrenBefore(self: *Frame, fragment: *Node, parent: *Node, ref
self.domChanged();
const dest_connected = parent.isConnected();
// Use firstChild() instead of iterator to handle cases where callbacks
// (like custom element connectedCallback) modify the fragment during iteration.
// The iterator captures "next" pointers that can become stale.
while (fragment.firstChild()) |child| {
// Check if child was connected BEFORE removing it from fragment
var it = fragment.childrenIterator();
while (it.next()) |child| {
const child_was_connected = child.isConnected();
self.removeNode(fragment, child, .{ .will_be_reconnected = dest_connected });
// A callback fired by a previous iteration's insert (e.g. a custom
// element's connectedCallback) may have detached ref_node from
// parent. In that case, fall back to append so the remaining
// children still land in `parent` in source order.
if (ref_node._parent == parent) {
try self.insertNodeRelative(
parent,
child,
.{ .before = ref_node },
.{ .child_already_connected = child_was_connected },
);
} else {
try self.appendNode(
parent,
child,
.{ .child_already_connected = child_was_connected },
);
}
try self.insertNodeRelative(
parent,
child,
.{ .before = ref_node },
.{ .child_already_connected = child_was_connected },
);
}
}
@@ -3300,7 +3295,7 @@ pub fn _insertNodeRelative(self: *Frame, comptime from_parser: bool, parent: *No
if (el.getAttributeSafe(comptime .wrap("id"))) |id| {
try self.addElementId(parent, el, id);
}
try Element.Html.Custom.invokeConnectedCallbackOnElement(true, el, self);
try Element.Html.Custom.enqueueConnectedCallbackOnElement(true, el, self);
}
}
return;
@@ -3346,7 +3341,7 @@ pub fn _insertNodeRelative(self: *Frame, comptime from_parser: bool, parent: *No
}
if (should_invoke_connected) {
try Element.Html.Custom.invokeConnectedCallbackOnElement(false, el, self);
try Element.Html.Custom.enqueueConnectedCallbackOnElement(false, el, self);
}
}
}
@@ -3356,7 +3351,7 @@ pub fn attributeChange(self: *Frame, element: *Element, name: String, value: Str
log.err(.bug, "build.attributeChange", .{ .tag = element.getTag(), .name = name, .value = value, .err = err, .type = self._type, .url = self.url });
};
Element.Html.Custom.invokeAttributeChangedCallbackOnElement(element, name, old_value, value, null, self);
Element.Html.Custom.enqueueAttributeChangedCallbackOnElement(element, name, old_value, value, null, self);
var it: ?*std.DoublyLinkedList.Node = self._mutation_observers.first;
while (it) |node| : (it = node.next) {
@@ -3382,7 +3377,7 @@ pub fn attributeRemove(self: *Frame, element: *Element, name: String, old_value:
log.err(.bug, "build.attributeRemove", .{ .tag = element.getTag(), .name = name, .err = err, .type = self._type, .url = self.url });
};
Element.Html.Custom.invokeAttributeChangedCallbackOnElement(element, name, old_value, null, null, self);
Element.Html.Custom.enqueueAttributeChangedCallbackOnElement(element, name, old_value, null, null, self);
var it: ?*std.DoublyLinkedList.Node = self._mutation_observers.first;
while (it) |node| : (it = node.next) {
@@ -4052,6 +4047,11 @@ const SubmitFormOpts = struct {
pub fn submitForm(self: *Frame, submitter_: ?*Element, form_: ?*Element.Html.Form, submit_opts: SubmitFormOpts) !void {
const form = form_ orelse return;
// see the `_constructing_entry_list` field documentation
if (form._constructing_entry_list) {
return;
}
if (submitter_) |submitter| {
if (submitter.getAttributeSafe(comptime .wrap("disabled")) != null) {
return;
@@ -4089,6 +4089,14 @@ pub fn submitForm(self: *Frame, submitter_: ?*Element, form_: ?*Element.Html.For
};
if (submit_opts.fire_event) {
// Prevent a submit on the form from firing while we're submit the form.
// This is both spec-correct AND prevents infinite recursion.
if (form._firing_submission_events) {
return;
}
form._firing_submission_events = true;
defer form._firing_submission_events = false;
// Per HTML spec "submit a form element" algorithm: SubmitEvent.submitter
// must be null when the submitter is the form itself, which is what
// Form.requestSubmit() passes when called with no submitter argument.

View File

@@ -95,6 +95,10 @@ transfers: std.AutoHashMapUnmanaged(u32, *Transfer) = .empty,
// When handles has no more available easys, requests get queued.
queue: std.DoublyLinkedList = .{},
// A queue for things that MUST happen on the next tick.
next_tick_queue: std.DoublyLinkedList = .{},
next_tick_count: usize = 0,
// Queue is for Transfers that have no connection. ready_queue is for connections
// that were initiated when performing == true and thus need to wait until
// performing == false before being added. I'm hoping this is temporary and that
@@ -179,6 +183,17 @@ fn layerWith(self: anytype, next: Layer) Layer {
return self.layer();
}
pub const NextTickNode = struct {
pub const Run =
*const fn (*Transfer, *anyopaque) void;
pub const Abort = *const fn (*anyopaque) void;
node: std.DoublyLinkedList.Node = .{},
ctx: *anyopaque,
run: Run,
abort: ?Abort = null,
};
pub fn init(self: *Client, allocator: Allocator, network: *Network, cdp: ?*CDP) !void {
var handles = try http.Handles.init(network.config);
errdefer handles.deinit();
@@ -229,6 +244,15 @@ pub fn init(self: *Client, allocator: Allocator, network: *Network, cdp: ?*CDP)
pub fn deinit(self: *Client) void {
self.abort();
if (comptime IS_DEBUG) {
lp.assert(
self.next_tick_count == 0,
"next_tick_count must be 0",
.{ .value = self.next_tick_count },
);
}
self.handles.deinit();
self.clearUserAgentOverride();
@@ -402,6 +426,7 @@ pub fn tick(self: *Client, timeout_ms: u32, mode: DrainMode) !void {
return error.ClientDisconnected;
}
try self.drainNextTickQueue();
try self.drainQueue();
try self.perform(@intCast(timeout_ms));
// perform/processMessages just released a batch of connections back to
@@ -416,6 +441,47 @@ pub fn tick(self: *Client, timeout_ms: u32, mode: DrainMode) !void {
try self.drainInbox(mode);
}
pub fn runNextTick(
self: *Client,
transfer: *Transfer,
ctx: *anyopaque,
params: struct { run: NextTickNode.Run, abort: ?NextTickNode.Abort = null },
) !void {
transfer._next_tick_node = .{ .ctx = ctx, .run = params.run, .abort = params.abort };
self.next_tick_count += 1;
self.next_tick_queue.append(&transfer._next_tick_node.?.node);
}
fn cancelNextTick(self: *Client, transfer: *Transfer) void {
if (transfer._next_tick_node) |*ntn| {
self.next_tick_queue.remove(&ntn.node);
self.next_tick_count -= 1;
if (ntn.abort) |abort_cb| {
abort_cb(ntn.ctx);
}
}
}
fn drainNextTickQueue(self: *Client) !void {
var remaining = self.next_tick_count;
while (remaining > 0) : (remaining -= 1) {
const node = self.next_tick_queue.popFirst() orelse break;
defer self.next_tick_count -= 1;
const n: *NextTickNode = @fieldParentPtr("node", node);
const transfer: *Transfer = @fieldParentPtr(
"_next_tick_node",
@as(*?NextTickNode, @ptrCast(n)),
);
const ntn = n.*;
transfer._next_tick_node = null;
ntn.run(transfer, ntn.ctx);
}
}
fn drainQueue(self: *Client) !void {
while (self.queue.popFirst()) |queue_node| {
const transfer: *Transfer = @fieldParentPtr("_node", queue_node);
@@ -1254,6 +1320,9 @@ pub const Transfer = struct {
// for when a Transfer is queued in the client.queue
_node: std.DoublyLinkedList.Node = .{},
// for when a Transfer is queued for the next tick.
_next_tick_node: ?NextTickNode = null,
pub const State = union(enum) {
// Pre-commit. Only valid inside the request flow (Client.request
// or a re-entry like continueTransfer / unpark) before any commit
@@ -1334,6 +1403,8 @@ pub const Transfer = struct {
// Any concurrent CDP lookup by id will now see this transfer as gone.
_ = self.client.transfers.remove(self.id);
self.client.cancelNextTick(self);
self.req.deinit();
if (self.owner) |o| {
o.removeTransfer(self);

View File

@@ -75,9 +75,9 @@ identity: js.Identity = .{},
// Finalizer callbacks for Zig instances exposed to v8 in this Page. Keyed by
// Zig instance ptr. The backing FinalizerCallback.Identity structs come from
// Session.fc_identity_pool so they outlive the Page for v8 weak-callback
// safety.
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *Session.FinalizerCallback) = .empty,
// Browser.fc_identity_pool so they outlive the Page (and the Session) for v8
// weak-callback safety.
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *js.FinalizerCallback) = .empty,
// Tracked global v8 objects that need to be released when the Page tears down.
globals: std.ArrayList(v8.Global) = .empty,

View File

@@ -162,7 +162,7 @@ fn _tick(self: *Runner, comptime is_cdp: bool, opts: TickOpts) !TickResult {
// There's no JS to run, and no reason to run the scheduler
// — unless we're the CDP worker, in which case we want
// http_client.tick to drain the inbox.
if (http_client.http_active == 0 and (comptime is_cdp) == false) {
if (http_client.http_active == 0 and http_client.next_tick_count == 0 and (comptime is_cdp) == false) {
// haven't started navigating, I guess.
return .done;
}
@@ -189,7 +189,8 @@ fn _tick(self: *Runner, comptime is_cdp: bool, opts: TickOpts) !TickResult {
try browser.runMacrotasks();
const http_active = http_client.http_active;
const total_network_activity = http_active + http_client.interception_layer.intercepted;
const http_next_tick = http_client.next_tick_count;
const total_network_activity = http_active + http_next_tick + http_client.interception_layer.intercepted;
if (frame._notified_network_almost_idle.check(total_network_activity <= 2)) {
frame.notifyNetworkAlmostIdle();
}
@@ -210,7 +211,7 @@ fn _tick(self: *Runner, comptime is_cdp: bool, opts: TickOpts) !TickResult {
},
}
if (http_active == 0 and http_client.ws_active == 0 and http_client.queue.first == null and http_client.ready_queue.first == null and (comptime is_cdp) == false) {
if (http_active == 0 and http_next_tick == 0 and http_client.ws_active == 0 and http_client.queue.first == null and http_client.ready_queue.first == null and (comptime is_cdp) == false) {
// ready_queue is also part of the check: makeRequest now
// wraps its handles.perform() in a performing=true window,
// and any synchronous libcurl callback that ends up

View File

@@ -210,7 +210,13 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
};
const is_blocking = mode == .normal;
if (is_blocking == false) {
// Once parsing is done, the deferred-script batch has already drained and
// won't run again, so a non-blocking script inserted afterwards would go
// unprocessed. Run it immediately instead. Remote scripts still need to
// be queued so they execute when their fetch completes.
const run_immediately = is_blocking or (self.base.static_scripts_done and remote_url == null);
if (run_immediately == false) {
self.base.scriptList(script).append(&script.node);
}
@@ -278,7 +284,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
handover = true;
}
if (is_blocking == false) {
if (run_immediately == false) {
return;
}

View File

@@ -398,6 +398,16 @@ pub fn staticScriptsDone(self: *ScriptManagerBase) void {
self.evaluate();
}
// A script-created parser (document.open/write/close) finished. Run any
// deferred scripts it produced. Unlike staticScriptsDone, this can run after
// the initial parse already completed (so it must not re-assert the flag): a
// frame that was loaded (or document.write'd into multiple times) keeps
// static_scripts_done set, and evaluate() only drains defer_scripts when it is.
pub fn scriptCreatedParseDone(self: *ScriptManagerBase) void {
self.static_scripts_done = true;
self.evaluate();
}
pub fn evaluate(self: *ScriptManagerBase) void {
if (self.is_evaluating) {
// It's possible for a script.eval to cause evaluate to be called again.
@@ -749,6 +759,13 @@ pub const Script = struct {
try_catch.init(local);
defer try_catch.deinit();
// Custom-element reactions: the script body is a JS-execution
// boundary. Open a scope so any reactions it queues (or that were
// queued by the parser since the previous boundary) drain at the
// end of the script, before the parser resumes.
const ce_checkpoint = frame._ce_reactions.push();
defer frame._ce_reactions.popAndInvoke(ce_checkpoint, frame);
const success = blk: {
const content = self.source.content();
switch (fe.kind) {

View File

@@ -63,10 +63,6 @@ inject_scripts: []const []const u8 = &.{},
// Shared allocator. Used by Session itself and borrowed by Pages.
arena_pool: *ArenaPool,
// Pool for FinalizerCallback.Identity structs. These must survive Page
// teardowns so V8 weak callbacks can validate the FC before dereferencing it.
fc_identity_pool: std.heap.MemoryPool(FinalizerCallback.Identity),
// The currently-active Page
// flips this pointer.
_active: ?*Page = null,
@@ -140,7 +136,6 @@ pub fn init(self: *Session, browser: *Browser, notification: *Notification) !voi
.storage_shed = .{},
.browser = browser,
.notification = notification,
.fc_identity_pool = .init(allocator),
.cookie_jar = storage.Cookie.Jar.init(allocator),
// CLI defaults; LP.configureLoading can flip these per-session.
.subframe_loading_enabled = !browser.app.config.disableSubframes(),
@@ -166,13 +161,6 @@ pub fn deinit(self: *Session) void {
self.cookie_jar.deinit();
// Force V8 to flush any remaining weak callbacks while
// fc_identity_pool is still alive. Identity structs allocated from
// this pool back V8 weak-callback parameters; freeing the pool first
// would leave dangling pointers that segfault on the next GC.
self.browser.env.memoryPressureNotification(.critical);
self.fc_identity_pool.deinit();
self.storage_shed.deinit(self.browser.app.allocator);
self._console_messages.deinit();
self.arena_pool.release(self.arena);
@@ -728,49 +716,3 @@ pub fn nextLoaderId(self: *Session) u32 {
self.loader_id_gen = id;
return id;
}
// Every finalizable instance of Zig gets 1 FinalizerCallback registered in the
// Page. This is to ensure that, if v8 doesn't finalize the value, we can
// release on Page teardown.
pub const FinalizerCallback = struct {
page: *Page,
arena: Allocator,
resolved_ptr_id: usize,
finalizer_ptr_id: usize,
release_ref: *const fn (ptr_id: usize, page: *Page) void,
// Linked list of Identities referencing this FC.
identities: ?*Identity = null,
// Count of active identities (for knowing when to clean up FC).
identity_count: u8 = 0,
// For every FinalizerCallback we'll have 1+ FinalizerCallback.Identity: one
// for every identity that gets the instance. In most cases, that'll be 1.
// Allocated from Session.fc_identity_pool so it survives Page teardowns and
// allows the weak callback to safely check the done flag.
pub const Identity = struct {
session: *Session,
// The Page that owns the FinalizerCallback this Identity references.
// Only safe to dereference when `done == false`. When done is true,
// the Page may have been torn down and this pointer is stale.
page: *Page,
identity: *js.Identity,
finalizer_ptr_id: usize,
resolved_ptr_id: usize,
next: ?*Identity = null,
done: bool = false,
};
// Called during Page teardown to force cleanup regardless of identities.
pub fn deinit(self: *FinalizerCallback, page: *Page) void {
// Mark all identities as done so stale V8 weak callbacks
// won't find the wrong FC if resolved_ptr_id is reused.
var id = self.identities;
while (id) |identity| {
identity.done = true;
id = identity.next;
}
self.release_ref(self.finalizer_ptr_id, page);
page.releaseArena(self.arena);
}
};

View File

@@ -570,6 +570,7 @@ pub const Function = struct {
cache: ?Caching = null,
embedded_receiver: bool = false,
exposed: Exposed = .both,
ce_reactions: bool = false,
pub const Exposed = enum { both, window, worker };
@@ -624,6 +625,26 @@ pub const Function = struct {
caller.initWithContext(ctx, v8_context);
defer caller.deinit();
// [CEReactions] entry: open a reactions scope so any custom-element
// callbacks queued by DOM mutation inside `func` fire after it
// returns, never mid-algorithm.
var ce_checkpoint: usize = undefined;
const ce_frame: ?*Frame = if (comptime opts.ce_reactions) switch (ctx.global) {
.frame => |frame| frame,
.worker => null,
} else null;
if (comptime opts.ce_reactions) {
if (ce_frame) |frame| {
ce_checkpoint = frame._ce_reactions.push();
}
}
defer if (comptime opts.ce_reactions) {
if (ce_frame) |frame| {
frame._ce_reactions.popAndInvoke(ce_checkpoint, frame);
}
};
const js_value = _call(T, &caller.local, info, func, opts) catch |err| {
handleError(T, @TypeOf(func), &caller.local, err, info, .{
.dom_exception = opts.dom_exception,

View File

@@ -610,7 +610,6 @@ pub fn dynamicModuleCallback(
}
pub fn metaObjectCallback(c_context: ?*v8.Context, c_module: ?*v8.Module, c_meta: ?*v8.Value) callconv(.c) void {
// @HandleScope implement this without a fat context/local..
const self = fromC(c_context.?).?;
var local = js.Local{
.ctx = self,
@@ -636,6 +635,66 @@ pub fn metaObjectCallback(c_context: ?*v8.Context, c_module: ?*v8.Module, c_meta
if (!res) {
log.err(.js, "import meta", .{ .err = error.FailedToSet });
}
// import.meta.resolve(specifier) resolves against this module's URL,
// applying the document's importmap. The base is bound per-module so the
// function keeps working even when detached from import.meta.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta/resolve
const resolve_data = self.arena.create(ImportMetaResolveData) catch {
log.err(.js, "import meta", .{ .err = error.OutOfMemory });
return;
};
resolve_data.* = .{ .context = self, .base = url };
const resolve_fn = newFunctionWithData(&local, importMetaResolveCallback, @ptrCast(resolve_data));
const resolve_value = js.Value{ .local = &local, .handle = @ptrCast(resolve_fn.handle) };
const resolve_res = meta.defineOwnProperty("resolve", resolve_value, 0) orelse false;
if (!resolve_res) {
log.err(.js, "import meta", .{ .err = error.FailedToSet });
}
}
const ImportMetaResolveData = struct {
context: *Context,
base: [:0]const u8,
};
// Implements import.meta.resolve(specifier): resolves the specifier against the
// module's base URL (applying the document's importmap) and returns the
// absolute URL.
fn importMetaResolveCallback(callback_handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
var c: Caller = undefined;
if (!c.initFromHandle(callback_handle)) {
return;
}
defer c.deinit();
const l = &c.local;
const info = Caller.FunctionCallbackInfo{ .handle = callback_handle.? };
const data: *ImportMetaResolveData = @ptrCast(@alignCast(info.getData() orelse return));
const ctx = data.context;
const isolate = ctx.isolate;
if (info.length() == 0) {
_ = isolate.throwException(isolate.createTypeError("import.meta.resolve requires a specifier"));
return;
}
const specifier = info.getArg(0, l).toStringSliceZ() catch {
_ = isolate.throwException(isolate.createTypeError("invalid specifier"));
return;
};
const resolved = ctx.script_manager.resolveSpecifier(ctx.call_arena, data.base, specifier) catch {
_ = isolate.throwException(isolate.createTypeError("failed to resolve module specifier"));
return;
};
const result = l.zigValueToJs(resolved, .{}) catch {
_ = isolate.throwException(isolate.createTypeError("failed to resolve module specifier"));
return;
};
info.getReturnValue().set(result);
}
fn _resolveModuleCallback(self: *Context, referrer: js.Module, specifier: [:0]const u8, local: *const js.Local) !?*const v8.Module {
@@ -989,6 +1048,17 @@ pub fn queueSlotchangeDelivery(self: *Context) !void {
}.run);
}
pub fn queueCustomElementBackupDrain(self: *Context) !void {
self.enqueueMicrotask(struct {
fn run(ctx: *Context) void {
switch (ctx.global) {
.frame => |frame| frame._ce_reactions.drainBackup(frame),
.worker => unreachable,
}
}
}.run);
}
// Helper for executing a Microtask on this Context. In V8, microtasks aren't
// associated to a Context - they are just functions to execute in an Isolate.
// But for these Context microtasks, we want to (a) make sure the context isn't

View File

@@ -18,10 +18,10 @@
const std = @import("std");
const lp = @import("lightpanda");
const string = @import("../../string.zig");
const Page = @import("../Page.zig");
const FinalizerCallback = @import("../Session.zig").FinalizerCallback;
const js = @import("js.zig");
const bridge = @import("bridge.zig");
@@ -33,6 +33,7 @@ const TaggedOpaque = @import("TaggedOpaque.zig");
const v8 = js.v8;
const log = lp.log;
const CallOpts = Caller.CallOpts;
const FinalizerCallback = js.FinalizerCallback;
// Where js.Context has a lifetime tied to the frame, and holds the
// v8::Global<v8::Context>, this has a much shorter lifetime and holds a
@@ -293,10 +294,11 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
finalizer_gop.value_ptr.* = try self.createFinalizerCallback(resolved_ptr_id, finalizer_ptr_id, finalizer.release_ref_from_zig);
}
const fc = finalizer_gop.value_ptr.*;
const identity_finalizer = try session.fc_identity_pool.create();
const browser = session.browser;
const identity_finalizer = try browser.fc_identity_pool.create();
identity_finalizer.* = .{
.browser = browser,
.page = page,
.session = session,
.identity = ctx.identity,
.finalizer_ptr_id = finalizer_ptr_id,
.resolved_ptr_id = resolved_ptr_id,
@@ -1248,23 +1250,30 @@ fn resolveT(comptime T: type, value: *T) Resolved {
const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?;
const identity_finalizer: *FinalizerCallback.Identity = @ptrCast(@alignCast(ptr));
// Identity is allocated from pool, so it's valid even after frame reset.
// The Identity lives in browser.fc_identity_pool, which outlives
// the page and the session, so freeing ourselves is always safe.
// `browser` is the only field we may touch unconditionally.
defer identity_finalizer.browser.fc_identity_pool.destroy(identity_finalizer);
// If done, the owning scope (page / isolated world) was already
// torn down: its identity.deinit() reset our Global and the FC
// was already released. The page, identity_map and finalizer_ptr_id
// are all stale (the Page may even be reused by a later Session),
// so we must not dereference them — just free ourselves (defer).
if (identity_finalizer.done) {
return;
}
const page = identity_finalizer.page;
const resolved_ptr_id = identity_finalizer.resolved_ptr_id;
defer page.session.fc_identity_pool.destroy(identity_finalizer);
// Always clean up the identity map entry
// Clean up the identity map entry: the object is being collected,
// so our Global to it is dead.
if (identity_finalizer.identity.identity_map.fetchRemove(resolved_ptr_id)) |kv| {
var global = kv.value;
v8.v8__Global__Reset(&global);
}
// If done, FC was already cleaned up during Page teardown.
// The finalizer_ptr_id may have been reused for a new object,
// so we must not look it up in the map. It's also unsafe to
// dereference identity_finalizer.page after done is true.
if (identity_finalizer.done) return;
const finalizer_ptr_id = identity_finalizer.finalizer_ptr_id;
const fc = page.finalizer_callbacks.get(finalizer_ptr_id) orelse return;

View File

@@ -19,14 +19,14 @@
const std = @import("std");
const lp = @import("lightpanda");
const js = @import("js.zig");
const Frame = @import("../Frame.zig");
const v8 = js.v8;
const js = @import("js.zig");
const Caller = @import("Caller.zig");
const Context = @import("Context.zig");
const v8 = js.v8;
const Allocator = std.mem.Allocator;
const IS_DEBUG = @import("builtin").mode == .Debug;
pub fn Builder(comptime T: type) type {
@@ -130,6 +130,18 @@ pub const Constructor = struct {
}
defer caller.deinit();
// Constructors are a JS-execution boundary, just like
// [CEReactions] methods. Open a reactions scope so any
// callbacks queued by the user's constructor body (or
// by attribute_changed reactions queued before invocation)
// drain at the constructor's exit, not later.
const ce_frame: ?*Frame = switch (caller.local.ctx.global) {
.frame => |frame| frame,
.worker => null,
};
const ce_checkpoint: usize = if (ce_frame) |frame| frame._ce_reactions.push() else 0;
defer if (ce_frame) |frame| frame._ce_reactions.popAndInvoke(ce_checkpoint, frame);
caller.constructor(T, func, handle.?, .{
.dom_exception = opts.dom_exception,
.new_target = opts.new_target,
@@ -222,9 +234,15 @@ pub const Accessor = struct {
};
if (@typeInfo(@TypeOf(getter)) != .null) {
const getter_opts = if (opts.ce_reactions == false) opts else blk: {
var o = opts;
o.ce_reactions = false;
break :blk o;
};
accessor.getter = struct {
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
Caller.Function.call(T, handle.?, getter, opts);
Caller.Function.call(T, handle.?, getter, getter_opts);
}
}.wrap;
}
@@ -296,6 +314,10 @@ pub const NamedIndexed = struct {
const Opts = struct {
as_typed_array: bool = false,
null_as_undefined: bool = false,
// Mirrors [CEReactions] on a named-property setter/deleter (e.g.,
// HTMLElement.dataset, which proxies setAttribute/removeAttribute).
// Only applies to setter and deleter; getters don't mutate.
ce_reactions: bool = false,
};
fn init(comptime T: type, comptime getter: anytype, setter: anytype, deleter: anytype, comptime opts: Opts) NamedIndexed {
@@ -324,6 +346,18 @@ pub const NamedIndexed = struct {
}
defer caller.deinit();
const ce_frame: ?*Frame = if (comptime opts.ce_reactions) switch (caller.local.ctx.global) {
.frame => |frame| frame,
.worker => null,
} else null;
var ce_checkpoint: usize = undefined;
if (comptime opts.ce_reactions) {
if (ce_frame) |frame| ce_checkpoint = frame._ce_reactions.push();
}
defer if (comptime opts.ce_reactions) {
if (ce_frame) |frame| frame._ce_reactions.popAndInvoke(ce_checkpoint, frame);
};
return caller.setNamedIndex(T, setter, c_name.?, c_value.?, handle.?, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
@@ -340,6 +374,18 @@ pub const NamedIndexed = struct {
}
defer caller.deinit();
const ce_frame: ?*Frame = if (comptime opts.ce_reactions) switch (caller.local.ctx.global) {
.frame => |frame| frame,
.worker => null,
} else null;
var ce_checkpoint: usize = undefined;
if (comptime opts.ce_reactions) {
if (ce_frame) |frame| ce_checkpoint = frame._ce_reactions.push();
}
defer if (comptime opts.ce_reactions) {
if (ce_frame) |frame| frame._ce_reactions.popAndInvoke(ce_checkpoint, frame);
};
return caller.deleteNamedIndex(T, deleter, c_name.?, handle.?, .{
.as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined,
@@ -986,13 +1032,11 @@ pub const WorkerJsApis = flattenTypes(&.{
@import("../webapi/canvas/OffscreenCanvasRenderingContext2D.zig"),
@import("../webapi/net/XMLHttpRequest.zig"),
@import("../webapi/net/XMLHttpRequestEventTarget.zig"),
@import("../webapi/net/WebSocket.zig"),
@import("../webapi/FileReader.zig"),
@import("../webapi/ImageData.zig"),
@import("../webapi/Performance.zig"),
@import("../webapi/PerformanceObserver.zig"),
// EventCounts is reachable only via Performance.eventCounts, which is
// [Exposed=Window] (pruned from Worker by Snapshot.pruneExposed). The
// type itself is in PageJsApis via Performance.registerTypes().
});
// Master list of ALL JS APIs across all contexts.

View File

@@ -50,6 +50,7 @@ pub const Integer = @import("Integer.zig");
pub const PromiseResolver = @import("PromiseResolver.zig");
pub const PromiseRejection = @import("PromiseRejection.zig");
const js = @This();
const Allocator = std.mem.Allocator;
pub fn Bridge(comptime T: type) type {
@@ -359,3 +360,60 @@ test "TaggedAnyOpaque" {
// If we grow this, fine, but it should be a conscious decision
try std.testing.expectEqual(24, @sizeOf(@import("TaggedOpaque.zig")));
}
// Every finalizable instance of Zig gets 1 FinalizerCallback registered in the
// Page. This is to ensure that, if v8 doesn't finalize the value, we can
// release on Page teardown.
pub const FinalizerCallback = struct {
page: *Page,
arena: Allocator,
resolved_ptr_id: usize,
finalizer_ptr_id: usize,
release_ref: *const fn (ptr_id: usize, page: *Page) void,
// Linked list of Identities referencing this FC.
identities: ?*FinalizerCallback.Identity = null,
// Count of active identities (for knowing when to clean up FC).
identity_count: u8 = 0,
const Page = @import("../Page.zig");
const Browser = @import("../Browser.zig");
// For every FinalizerCallback we'll have 1+ FinalizerCallback.Identity: one
// for every identity that gets the instance. In most cases, that'll be 1.
// Allocated from Browser.fc_identity_pool so it survives Page *and* Session
// teardowns — V8 may fire the weak callback any time before the Isolate is
// torn down — and lets the callback safely check the done flag.
pub const Identity = struct {
// The Page that owns the FinalizerCallback this Identity references.
// Only safe to dereference when `done == false`. When done is true,
// the Page may have been torn down and this pointer is stale.
page: *Page,
// Stable handle to the pool this struct came from. The weak callback
// reaches the pool through here (not via page/session) so it stays
// valid to self-destruct even when `done` and the page/session are gone.
browser: *Browser,
// The world's identity map. Only safe to dereference when `done == false`
// (see `browser` above) — its teardown already reset every Global.
identity: *js.Identity,
finalizer_ptr_id: usize,
resolved_ptr_id: usize,
next: ?*FinalizerCallback.Identity = null,
done: bool = false,
};
// Called during Page teardown to force cleanup regardless of identities.
pub fn deinit(self: *FinalizerCallback, page: *Page) void {
// Mark all identities as done so stale V8 weak callbacks
// won't find the wrong FC if resolved_ptr_id is reused.
var id = self.identities;
while (id) |identity| {
identity.done = true;
id = identity.next;
}
self.release_ref(self.finalizer_ptr_id, page);
page.releaseArena(self.arena);
}
};

View File

@@ -346,6 +346,8 @@ fn parseErrorCallback(ctx: *anyopaque, err: h5e.StringSlice) callconv(.c) void {
fn popCallback(ctx: *anyopaque, node_ref: *anyopaque) callconv(.c) void {
const self: *Parser = @ptrCast(@alignCast(ctx));
const cp = self.frame._ce_reactions.push();
defer self.frame._ce_reactions.popAndInvoke(cp, self.frame);
self._popCallback(getNode(node_ref)) catch |err| {
self.err = .{ .err = err, .source = .pop };
};
@@ -368,6 +370,8 @@ fn createXMLElementCallback(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualNa
fn _createElementCallbackWithDefaultnamespace(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator, default_namespace: Element.Namespace) ?*anyopaque {
const self: *Parser = @ptrCast(@alignCast(ctx));
const cp = self.frame._ce_reactions.push();
defer self.frame._ce_reactions.popAndInvoke(cp, self.frame);
return self._createElementCallback(data, qname, attributes, default_namespace) catch |err| {
self.err = .{ .err = err, .source = .create_element };
return null;
@@ -390,6 +394,8 @@ fn _createElementCallback(self: *Parser, data: *anyopaque, qname: h5e.QualName,
fn createCommentCallback(ctx: *anyopaque, str: h5e.StringSlice) callconv(.c) ?*anyopaque {
const self: *Parser = @ptrCast(@alignCast(ctx));
const cp = self.frame._ce_reactions.push();
defer self.frame._ce_reactions.popAndInvoke(cp, self.frame);
return self._createCommentCallback(str.slice()) catch |err| {
self.err = .{ .err = err, .source = .create_comment };
return null;
@@ -408,6 +414,8 @@ fn _createCommentCallback(self: *Parser, str: []const u8) !*anyopaque {
fn createProcessingInstruction(ctx: *anyopaque, target: h5e.StringSlice, data: h5e.StringSlice) callconv(.c) ?*anyopaque {
const self: *Parser = @ptrCast(@alignCast(ctx));
const cp = self.frame._ce_reactions.push();
defer self.frame._ce_reactions.popAndInvoke(cp, self.frame);
return self._createProcessingInstruction(target.slice(), data.slice()) catch |err| {
self.err = .{ .err = err, .source = .create_processing_instruction };
return null;
@@ -426,6 +434,8 @@ fn _createProcessingInstruction(self: *Parser, target: []const u8, data: []const
fn appendDoctypeToDocument(ctx: *anyopaque, name: h5e.StringSlice, public_id: h5e.StringSlice, system_id: h5e.StringSlice) callconv(.c) void {
const self: *Parser = @ptrCast(@alignCast(ctx));
const cp = self.frame._ce_reactions.push();
defer self.frame._ce_reactions.popAndInvoke(cp, self.frame);
self._appendDoctypeToDocument(name.slice(), public_id.slice(), system_id.slice()) catch |err| {
self.err = .{ .err = err, .source = .append_doctype_to_document };
};
@@ -448,6 +458,8 @@ fn _appendDoctypeToDocument(self: *Parser, name: []const u8, public_id: []const
fn addAttrsIfMissingCallback(ctx: *anyopaque, target_ref: *anyopaque, attributes: h5e.AttributeIterator) callconv(.c) void {
const self: *Parser = @ptrCast(@alignCast(ctx));
const cp = self.frame._ce_reactions.push();
defer self.frame._ce_reactions.popAndInvoke(cp, self.frame);
self._addAttrsIfMissingCallback(getNode(target_ref), attributes) catch |err| {
self.err = .{ .err = err, .source = .add_attrs_if_missing };
};
@@ -497,6 +509,8 @@ fn getDataCallback(ctx: *anyopaque) callconv(.c) *anyopaque {
fn appendCallback(ctx: *anyopaque, parent_ref: *anyopaque, node_or_text: h5e.NodeOrText) callconv(.c) void {
const self: *Parser = @ptrCast(@alignCast(ctx));
const cp = self.frame._ce_reactions.push();
defer self.frame._ce_reactions.popAndInvoke(cp, self.frame);
self._appendCallback(getNode(parent_ref), node_or_text) catch |err| {
self.err = .{ .err = err, .source = .append };
};
@@ -529,6 +543,8 @@ fn _appendCallback(self: *Parser, parent: *Node, node_or_text: h5e.NodeOrText) !
fn removeFromParentCallback(ctx: *anyopaque, target_ref: *anyopaque) callconv(.c) void {
const self: *Parser = @ptrCast(@alignCast(ctx));
const cp = self.frame._ce_reactions.push();
defer self.frame._ce_reactions.popAndInvoke(cp, self.frame);
self._removeFromParentCallback(getNode(target_ref)) catch |err| {
self.err = .{ .err = err, .source = .remove_from_parent };
};
@@ -545,6 +561,8 @@ fn _removeFromParentCallback(self: *Parser, node: *Node) !void {
fn reparentChildrenCallback(ctx: *anyopaque, node_ref: *anyopaque, new_parent_ref: *anyopaque) callconv(.c) void {
const self: *Parser = @ptrCast(@alignCast(ctx));
const cp = self.frame._ce_reactions.push();
defer self.frame._ce_reactions.popAndInvoke(cp, self.frame);
self._reparentChildrenCallback(getNode(node_ref), getNode(new_parent_ref)) catch |err| {
self.err = .{ .err = err, .source = .reparent_children };
};
@@ -559,6 +577,8 @@ fn _reparentChildrenCallback(self: *Parser, node: *Node, new_parent: *Node) !voi
fn appendBeforeSiblingCallback(ctx: *anyopaque, sibling_ref: *anyopaque, node_or_text: h5e.NodeOrText) callconv(.c) void {
const self: *Parser = @ptrCast(@alignCast(ctx));
const cp = self.frame._ce_reactions.push();
defer self.frame._ce_reactions.popAndInvoke(cp, self.frame);
self._appendBeforeSiblingCallback(getNode(sibling_ref), node_or_text) catch |err| {
self.err = .{ .err = err, .source = .append_before_sibling };
};
@@ -587,6 +607,8 @@ fn _appendBeforeSiblingCallback(self: *Parser, sibling: *Node, node_or_text: h5e
fn appendBasedOnParentNodeCallback(ctx: *anyopaque, element_ref: *anyopaque, prev_element_ref: *anyopaque, node_or_text: h5e.NodeOrText) callconv(.c) void {
const self: *Parser = @ptrCast(@alignCast(ctx));
const cp = self.frame._ce_reactions.push();
defer self.frame._ce_reactions.popAndInvoke(cp, self.frame);
self._appendBasedOnParentNodeCallback(getNode(element_ref), getNode(prev_element_ref), node_or_text) catch |err| {
self.err = .{ .err = err, .source = .append_based_on_parent_node };
};

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head><title>AccName Fixture</title></head>
<body>
<!-- div/span don't take their name from contents by default; an explicit
name-from-content role opts them back in (AccName #comp_name_from_content). -->
<div id="heading" role="heading">Hello World</div>
<span id="button" role="button">Click me</span>
<div id="nested" role="link">Read <span>more</span></div>
<!-- No name-from-content: a bare div, and a role that isn't name-from-content. -->
<div id="plain">ignored text</div>
<div id="pres" role="presentation">ignored</div>
</body>
</html>

View File

@@ -2,19 +2,19 @@
<body>
<script src="../testing.js"></script>
<!-- Regression: Frame.insertAllChildrenBefore (used by Element.outerHTML
setter) loops over fragment children and inserts each one .before
ref_node. _insertNodeRelative invokes connectedCallback for custom
elements, and that callback can detach ref_node from parent. The
next iteration must not panic on `ref_node._parent.? == parent`. -->
<script id="outerHTML_callback_detaches_ref_node">
<!-- Custom-element reactions queue during outerHTML's algorithm and fire
at the boundary, not mid-loop. By the time connectedCallback runs,
insertAllChildrenBefore has already inserted every fragment child
and setOuterHTML has detached the original `target`, so the callback
sees the post-state — its check observes target already detached and
leaves the DOM untouched. -->
<script id="outerHTML_callback_observes_post_state">
{
let targetParentAtCallback = undefined;
class DetachTarget extends HTMLElement {
connectedCallback() {
const target = document.getElementById('outer_target');
if (target && target.parentNode) {
target.parentNode.removeChild(target);
}
targetParentAtCallback = target && target.parentNode;
}
}
customElements.define('detach-target-1', DetachTarget);
@@ -26,60 +26,48 @@
target.id = 'outer_target';
wrap.appendChild(target);
// Two siblings: the first one's connectedCallback removes `target`
// (which is the ref_node passed to insertAllChildrenBefore). The
// second one then tries to insert before a parentless ref_node.
target.outerHTML = '<detach-target-1></detach-target-1><b id="second">x</b>';
// We don't strictly care about the final shape, just that we got
// here without crashing. Sanity-check the first child landed.
// Both replacement children landed; the original target was removed
// before the queued connectedCallback ran.
testing.expectTrue(wrap.querySelector('detach-target-1') !== null);
testing.expectTrue(wrap.querySelector('#second') !== null);
testing.expectEqual(null, targetParentAtCallback);
}
</script>
<!-- Regression: Element.setInnerHTML removes existing children using
`childrenIterator()`. removeNode fires disconnectedCallback for
custom elements, and that callback can mutate the host's child
list, leaving the iterator advancing through a node that no longer
belongs to the parent. -->
<script id="innerHTML_disconnected_mutates_siblings">
<!-- innerHTML removes existing children, then parses and inserts the
new content. With queued reactions, the disconnectedCallbacks for
the removed elements fire after the whole algorithm completes — so
they observe `host` already populated with the new content. -->
<script id="innerHTML_disconnected_observes_post_state">
{
let host;
let removeNextOnDisconnect = false;
class SiblingZapper extends HTMLElement {
let lastSeenChildCount = -1;
let lastSeenFirstTag = null;
class SiblingObserver extends HTMLElement {
disconnectedCallback() {
if (!removeNextOnDisconnect) return;
removeNextOnDisconnect = false;
// Reach into the parent (captured via closure) to remove a
// sibling that the outer setInnerHTML loop is about to visit.
// The element being removed here is what the iterator's
// current node still points its `next` pointer at.
if (host.children.length > 0) {
host.removeChild(host.children[0]);
}
lastSeenChildCount = host.children.length;
lastSeenFirstTag = host.children.length > 0 ? host.children[0].tagName : null;
}
}
customElements.define('sibling-zapper-2', SiblingZapper);
customElements.define('sibling-zapper-2', SiblingObserver);
host = document.createElement('div');
document.body.appendChild(host);
const a = document.createElement('sibling-zapper-2');
const b = document.createElement('sibling-zapper-2');
const c = document.createElement('span');
host.appendChild(a);
host.appendChild(b);
host.appendChild(c);
host.appendChild(document.createElement('sibling-zapper-2'));
host.appendChild(document.createElement('sibling-zapper-2'));
host.appendChild(document.createElement('span'));
removeNextOnDisconnect = true;
// Triggers the removal loop in setInnerHTML. Removing `a` fires its
// disconnectedCallback, which removes `b` (the iterator's next
// target). The loop must not visit a node whose `_parent` is null.
host.innerHTML = '<i>after</i>';
// The disconnectedCallback fires after innerHTML completes; by then
// the new <i> is already in place.
testing.expectEqual(1, host.children.length);
testing.expectEqual('I', host.children[0].tagName);
testing.expectEqual(1, lastSeenChildCount);
testing.expectEqual('I', lastSeenFirstTag);
}
</script>
</body>

View File

@@ -561,3 +561,65 @@
testing.expectEqual('big5', form.getAttribute('accept-charset'));
}
</script>
<!-- Test: re-entrant submission from a `submit` handler is a no-op. Without the
"firing submission events" guard this recurses unbounded and fatally aborts
the V8 isolate. -->
<form id="test_form_reentrant_submit" action="/should-not-navigate8" method="get">
<input type="text" name="q" value="test">
</form>
<script id="submit_reentrant_from_onsubmit_is_noop">
{
const form = $('#test_form_reentrant_submit');
let count = 0;
form.addEventListener('submit', (e) => {
e.preventDefault();
count++;
// Re-entrant submit while submission events are firing must be ignored.
form.requestSubmit();
});
form.requestSubmit();
testing.expectEqual(1, count);
}
</script>
<!-- Test: re-entrant submission from a `formdata` handler is a no-op. Without
the "constructing entry list" guard this recurses unbounded. -->
<script id="submit_reentrant_from_formdata_is_noop">
{
const form = document.createElement('form');
let count = 0;
form.addEventListener('formdata', () => {
count++;
// form.submit() while the entry list is being constructed must be ignored.
form.submit();
});
new FormData(form);
testing.expectEqual(1, count);
}
</script>
<!-- Test: building another FormData from a `formdata` handler throws
InvalidStateError (and is guarded against unbounded recursion). -->
<script id="formdata_reentrant_construction_throws">
{
const form = document.createElement('form');
let caught = null;
form.addEventListener('formdata', () => {
try {
new FormData(form);
} catch (e) {
caught = e.name;
}
});
new FormData(form);
testing.expectEqual('InvalidStateError', caught);
}
</script>

View File

@@ -0,0 +1,37 @@
<!DOCTYPE html>
<head></head>
<script src="../../../testing.js"></script>
<!--
Scripts inserted *after* parsing is done (static_scripts_done == true). At that
point the deferred-script batch has already drained and won't run again, so a
non-blocking script must either run immediately (inline, nothing to fetch) or
still be queued for its fetch (remote). A module script's continuation after
the first `await` runs in this post-parse window, so we use it to do the
insertions.
-->
<script id=postload type=module>
const state = await testing.async();
// Inline (non-blocking, module) script must execute even though the defer
// batch is already done.
window.postload_inline_ran = false;
const inline_mod = document.createElement('script');
inline_mod.type = 'module';
inline_mod.textContent = 'window.postload_inline_ran = true;';
document.head.appendChild(inline_mod);
// Remote script must still be fetched and executed — NOT run synchronously
// with a not-yet-loaded status (which would drop it and free it mid-fetch).
window.loaded1 = 0;
const remote = document.createElement('script');
remote.src = 'dynamic1.js'; // does: loaded1 += 1
remote.onload = () => state.resolve();
remote.onerror = () => state.resolve(); // don't hang if it (wrongly) errors
document.head.appendChild(remote);
await state.done(() => {
testing.expectTrue(window.postload_inline_ran);
testing.expectEqual(1, window.loaded1);
});
</script>

View File

@@ -0,0 +1,50 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<!--
document.write into an iframe must parse and run the written markup in the
IFRAME's own frame, not the calling (main) frame. Previously the scripts ran
in the caller's frame, so their globals / window / parent were the main
page's — which broke cross-frame messaging (parent resolved to self).
-->
<script id=iframe_write_runs_in_iframe>
{
const iframe = document.createElement('iframe');
document.documentElement.appendChild(iframe);
const doc = iframe.contentDocument;
doc.write('<!DOCTYPE html><html><body><script>window.ranHere = true; window.parentIsSelf = (window.parent === window);<\/script></body></html>');
doc.close();
// Read the flags from the IFRAME's window. If the script had (wrongly) run in
// the main frame, they'd be set there and these would read undefined.
testing.expectEqual(true, iframe.contentWindow.ranHere);
// The script's `parent` is the main window, so parent !== self.
testing.expectEqual(false, iframe.contentWindow.parentIsSelf);
}
</script>
<!--
A deferred (module) script written into an iframe must still run. A freshly
loaded iframe never fires its static-parse "done" signal, so document.close()
is what flushes the deferred scripts the write produced.
-->
<script id=iframe_write_flushes_deferred_module type=module>
{
const state = await testing.async();
const iframe = document.createElement('iframe');
document.documentElement.appendChild(iframe);
const doc = iframe.contentDocument;
doc.write('<!DOCTYPE html><html><body><script type="module">window.moduleRan = true;<\/script></body></html>');
doc.close();
// Give the deferred module a tick to run, then confirm it executed in the
// iframe (not the main frame, where contentWindow.moduleRan would be unset).
setTimeout(() => state.resolve(), 5);
await state.done(() => {
testing.expectEqual(true, iframe.contentWindow.moduleRan);
});
}
</script>

View File

@@ -216,3 +216,53 @@
testing.expectEqual('value', req2.headers.get('X-Custom'));
}
</script>
<script id=body_used>
testing.async(async () => {
const req = new Request('https://example.com', { method: 'POST', body: 'hi' });
testing.expectFalse(req.bodyUsed);
await req.text();
testing.expectTrue(req.bodyUsed);
// Re-consuming a used body rejects.
let rejected = false;
try { await req.text(); } catch (e) { rejected = true; }
testing.expectTrue(rejected);
});
testing.async(async () => {
// A request with no body is never "used".
const req = new Request('https://example.com');
testing.expectFalse(req.bodyUsed);
await req.text();
testing.expectFalse(req.bodyUsed);
});
testing.async(async () => {
// text() decodes as UTF-8, stripping a leading BOM.
const req = new Request('https://example.com', { method: 'POST', body: 'hi' });
testing.expectEqual('hi', await req.text());
});
</script>
<script id=content_type_from_body>
{
const req = new Request('https://example.com', { method: 'POST', body: 'x' });
testing.expectEqual('text/plain;charset=UTF-8', req.headers.get('content-type'));
}
{
// Buffer sources carry no default Content-Type.
const req = new Request('https://example.com', { method: 'POST', body: new Uint8Array([1, 2, 3]) });
testing.expectTrue(req.headers.get('content-type') === null);
}
</script>
<script id=priority>
{
new Request('https://example.com', { priority: 'high' });
new Request('https://example.com', { priority: 'low' });
new Request('https://example.com', { priority: 'auto' });
testing.expectTrue(true);
}
testing.expectError('TypeError', () => new Request('https://example.com', { priority: 'bogus' }));
</script>

View File

@@ -30,7 +30,7 @@
{
let response3 = new Response("Created", { status: 201, statusText: "Created" });
testing.expectEqual("basic", response3.type);
testing.expectEqual("default", response3.type);
testing.expectEqual(201, response3.status);
testing.expectEqual("Created", response3.statusText);
testing.expectEqual(true, response3.ok);
@@ -199,3 +199,55 @@
testing.expectEqual(true, body instanceof ReadableStream);
});
</script>
<script id=body_used>
testing.async(async () => {
const response = new Response('hi');
testing.expectFalse(response.bodyUsed);
await response.text();
testing.expectTrue(response.bodyUsed);
// Re-consuming a used body rejects.
let rejected = false;
try { await response.text(); } catch (e) { rejected = true; }
testing.expectTrue(rejected);
});
testing.async(async () => {
// A bodyless response is never "used".
const response = new Response();
testing.expectFalse(response.bodyUsed);
await response.text();
testing.expectFalse(response.bodyUsed);
});
testing.async(async () => {
// text() decodes as UTF-8, stripping a leading BOM.
const response = new Response('hi');
testing.expectEqual('hi', await response.text());
});
</script>
<script id=default_type>
{
testing.expectEqual('default', new Response('x').type);
}
</script>
<script id=content_type_from_body>
{
testing.expectEqual('text/plain;charset=UTF-8', new Response('x').headers.get('content-type'));
}
{
const usp = new URLSearchParams({ a: '1' });
testing.expectEqual('application/x-www-form-urlencoded;charset=UTF-8', new Response(usp).headers.get('content-type'));
}
{
const blob = new Blob(['x'], { type: 'text/CustomCase' });
testing.expectEqual('text/customcase', new Response(blob).headers.get('content-type'));
}
{
// Buffer sources carry no default Content-Type.
testing.expectTrue(new Response(new Uint8Array([1, 2, 3])).headers.get('content-type') === null);
}
</script>

View File

@@ -0,0 +1,46 @@
// Exercises the WebSocket API inside a worker. Posts 'ready' once the message
// handler is wired so the page knows it can send a command without racing
// worker startup. On command, opens a WebSocket to the test echo server,
// sends a message, and reports the echoed reply plus the close code/reason
// back to the page.
self.onmessage = function(e) {
const cmd = e.data;
try {
if (cmd.kind === 'echo') {
const received = [];
const ws = new WebSocket('ws://127.0.0.1:9584/');
ws.addEventListener('open', () => {
ws.send('from-worker');
});
ws.addEventListener('message', (ev) => {
received.push(ev.data);
ws.close(1000, 'bye');
});
ws.addEventListener('close', (ev) => {
postMessage({
ok: true,
received,
url: ws.url,
ready_state: ws.readyState,
code: ev.code,
reason: ev.reason,
was_clean: ev.wasClean,
});
});
ws.addEventListener('error', () => {
postMessage({ ok: false, err: 'websocket error' });
});
return;
}
postMessage({ ok: false, err: 'unknown command' });
} catch (err) {
postMessage({ ok: false, err: String(err), stack: err.stack });
}
};
postMessage({ ready: true });

View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script id=websocket_in_worker type=module>
{
const state = await testing.async();
const worker = new Worker('./websocket-worker.js');
// The worker posts {ready:true} once its onmessage handler is wired.
// Wait for that before sending the command to avoid racing startup.
worker.onmessage = (e) => {
if (e.data && e.data.ready) {
worker.postMessage({ kind: 'echo' });
return;
}
state.resolve(e.data);
};
await state.done((data) => {
testing.expectTrue(data.ok, 'worker websocket error: ' + data.err);
testing.expectEqual(['echo-from-worker'], data.received);
testing.expectEqual('ws://127.0.0.1:9584/', data.url);
testing.expectEqual(3, data.ready_state); // CLOSED
testing.expectEqual(1000, data.code);
testing.expectEqual('bye', data.reason);
testing.expectEqual(true, data.was_clean);
});
}
</script>

View File

@@ -31,18 +31,19 @@
<div id=d3></div>
<script id=appendChild_fragment_mutation>
// Test that appendChild with DocumentFragment handles synchronous callbacks
// (like custom element connectedCallback) that modify the fragment during iteration.
// This reproduces a bug where the iterator captures "next" node pointers
// before processing, but callbacks can remove those nodes from the fragment.
// Custom-element reactions are queued during the appendChild algorithm
// and fire after it completes. The connectedCallback below sees the
// post-move DOM state — both fragment children are already in d3 — so
// its check `b.parentNode === fragment` is false and it leaves b alone.
const d3 = $('#d3');
const fragment = document.createDocumentFragment();
// Create custom element whose connectedCallback modifies the fragment
let bElement = null;
let parentSeenAtCallback = null;
class ModifyingElement extends HTMLElement {
connectedCallback() {
// When this element is connected, remove 'b' from the fragment
// Callback fires after the move; record what we observe.
parentSeenAtCallback = bElement && bElement.parentNode;
if (bElement && bElement.parentNode === fragment) {
fragment.removeChild(bElement);
}
@@ -58,39 +59,37 @@
fragment.appendChild(a);
fragment.appendChild(b);
// This should not crash - appendChild should handle the modification gracefully
d3.appendChild(fragment);
// 'a' should be in d3, 'b' was removed by connectedCallback and is now detached
assertChildren(['a'], d3);
testing.expectEqual(null, b.parentNode);
// Both moved atomically. Callback observed b already in d3.
assertChildren(['a', 'b'], d3);
testing.expectEqual(d3, parentSeenAtCallback);
</script>
<div id=d4></div>
<div id=d4_stash></div>
<script id=appendChild_disconnect_callback_reparents>
// Moving a connected child into a disconnected target makes
// will_be_reconnected=false, so disconnectedCallback fires synchronously
// inside removeNode. The callback re-parents the child; appendChild
// respects that placement instead of overriding it.
<script id=appendChild_disconnect_callback_observes_post_move>
// Custom-element reactions queue and drain at the algorithm boundary, so
// disconnectedCallback fires after the whole move completes. By then the
// element is already in its new (disconnected) parent the callback
// observes the post-move state, never a parentless intermediate.
const d4 = $('#d4');
const stash = $('#d4_stash');
class ReparentOnDisconnect extends HTMLElement {
let parentSeenAtDisconnect = undefined;
class ObserveOnDisconnect extends HTMLElement {
disconnectedCallback() {
if (this.parentNode === null) {
stash.appendChild(this);
}
parentSeenAtDisconnect = this.parentNode;
}
}
customElements.define('reparent-on-disconnect', ReparentOnDisconnect);
customElements.define('observe-on-disconnect', ObserveOnDisconnect);
const rpd = document.createElement('reparent-on-disconnect');
rpd.id = 'rpd';
d4.appendChild(rpd);
const ood = document.createElement('observe-on-disconnect');
ood.id = 'ood';
d4.appendChild(ood);
const detached = document.createElement('div');
detached.appendChild(rpd);
detached.appendChild(ood);
testing.expectEqual(stash, rpd.parentNode);
testing.expectEqual(detached, ood.parentNode);
testing.expectEqual(detached, parentSeenAtDisconnect);
</script>

View File

@@ -42,21 +42,18 @@
<div id=d3></div>
<div id=d3_stash></div>
<script id=insertBefore_disconnect_callback_reparents>
// Same disconnectedCallback re-parenting pattern as in append_child.html,
// exercised through insertBefore. insertBefore respects the callback's
// placement instead of overriding it.
<script id=insertBefore_disconnect_callback_observes_post_move>
// disconnectedCallback fires after insertBefore completes, so the
// callback observes the element already placed in its new parent.
const d3 = $('#d3');
const stash = $('#d3_stash');
class IBReparentOnDisconnect extends HTMLElement {
let parentSeenAtDisconnect = undefined;
class IBObserveOnDisconnect extends HTMLElement {
disconnectedCallback() {
if (this.parentNode === null) {
stash.appendChild(this);
}
parentSeenAtDisconnect = this.parentNode;
}
}
customElements.define('ib-reparent-on-disconnect', IBReparentOnDisconnect);
customElements.define('ib-reparent-on-disconnect', IBObserveOnDisconnect);
const moving = document.createElement('ib-reparent-on-disconnect');
moving.id = 'ib_moving';
@@ -68,5 +65,6 @@
detached.insertBefore(moving, ref);
testing.expectEqual(stash, moving.parentNode);
testing.expectEqual(detached, moving.parentNode);
testing.expectEqual(detached, parentSeenAtDisconnect);
</script>

View File

@@ -5,6 +5,20 @@
testing.expectEqual('/src/browser/tests/page/module.html', new URL(import.meta.url).pathname)
</script>
<script id=meta-resolve type=module>
// Resolves relative to this module's URL.
testing.expectEqual('/src/browser/tests/page/mod1.js', new URL(import.meta.resolve('./mod1.js')).pathname);
testing.expectEqual('/src/browser/tests/page/modules/importer.js', new URL(import.meta.resolve('./modules/importer.js')).pathname);
testing.expectEqual('/src/browser/tests/testing.js', new URL(import.meta.resolve('../testing.js')).pathname);
</script>
<script id=meta-resolve-detached type=module>
// The base is bound to the module, so resolve still works when detached
// from import.meta.
const resolve = import.meta.resolve;
testing.expectEqual('/src/browser/tests/page/mod1.js', new URL(resolve('./mod1.js')).pathname, {script_id: 'meta-resolve-detached'});
</script>
<script id=basic-import type=module>
import { "val1" as val1 } from "./mod1.js";
testing.expectEqual('value-1', val1);

View File

@@ -125,16 +125,16 @@ fn validateMimeType(arena: Allocator, mime_type: []const u8, full_validation: bo
_ = Mime.parse(buf) catch return "";
} else {
// Simple validation per FileAPI spec (for Blob constructor):
// - If any char is outside U+0020-U+007E, return empty string
// - Otherwise lowercase
// any char outside U+0020-U+007E yields the empty string.
for (mime_type) |c| {
if (c < 0x20 or c > 0x7E) {
return "";
}
}
_ = std.ascii.lowerString(buf, buf);
}
// FileAPI: Blob.type is the type converted to ASCII lowercase.
_ = std.ascii.lowerString(buf, buf);
return buf;
}

View File

@@ -423,19 +423,19 @@ pub const JsApi = struct {
pub const enumerable = false;
};
pub const data = bridge.accessor(CData.getData, CData._setData, .{});
pub const data = bridge.accessor(CData.getData, CData._setData, .{ .ce_reactions = true });
pub const length = bridge.accessor(CData.getLength, null, .{});
pub const appendData = bridge.function(CData.appendData, .{});
pub const deleteData = bridge.function(CData.deleteData, .{ .dom_exception = true });
pub const insertData = bridge.function(CData.insertData, .{ .dom_exception = true });
pub const replaceData = bridge.function(CData.replaceData, .{ .dom_exception = true });
pub const appendData = bridge.function(CData.appendData, .{ .ce_reactions = true });
pub const deleteData = bridge.function(CData.deleteData, .{ .dom_exception = true, .ce_reactions = true });
pub const insertData = bridge.function(CData.insertData, .{ .dom_exception = true, .ce_reactions = true });
pub const replaceData = bridge.function(CData.replaceData, .{ .dom_exception = true, .ce_reactions = true });
pub const substringData = bridge.function(CData.substringData, .{ .dom_exception = true });
pub const remove = bridge.function(CData.remove, .{});
pub const before = bridge.function(CData.before, .{});
pub const after = bridge.function(CData.after, .{});
pub const replaceWith = bridge.function(CData.replaceWith, .{});
pub const remove = bridge.function(CData.remove, .{ .ce_reactions = true });
pub const before = bridge.function(CData.before, .{ .ce_reactions = true });
pub const after = bridge.function(CData.after, .{ .ce_reactions = true });
pub const replaceWith = bridge.function(CData.replaceWith, .{ .ce_reactions = true });
pub const nextElementSibling = bridge.accessor(CData.nextElementSibling, null, .{});
pub const previousElementSibling = bridge.accessor(CData.previousElementSibling, null, .{});

View File

@@ -190,17 +190,20 @@ pub fn upgradeCustomElement(custom: *Custom, definition: *CustomElementDefinitio
return error.CustomElementUpgradeFailed;
};
// Invoke attributeChangedCallback for existing observed attributes
var attr_it = custom.asElement().attributeIterator();
// Enqueue attributeChangedCallback for existing observed attributes
const element = custom.asElement();
var attr_it = element.attributeIterator();
while (attr_it.next()) |attr| {
const name = attr._name;
if (definition.isAttributeObserved(name)) {
custom.invokeAttributeChangedCallback(name, null, attr._value, null, frame);
Custom.enqueueAttributeChangedCallbackOnElement(element, name, null, attr._value, null, frame);
}
}
if (node.isConnected()) {
custom.invokeConnectedCallback(frame);
Custom.enqueueConnectedCallbackOnElement(false, element, frame) catch |err| {
log.warn(.bug, "ce_reactions enqueue fail", .{ .err = err });
};
}
}
@@ -257,9 +260,9 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain();
};
pub const define = bridge.function(CustomElementRegistry.define, .{ .dom_exception = true });
pub const define = bridge.function(CustomElementRegistry.define, .{ .dom_exception = true, .ce_reactions = true });
pub const get = bridge.function(CustomElementRegistry.get, .{ .null_as_undefined = true });
pub const upgrade = bridge.function(CustomElementRegistry.upgrade, .{});
pub const upgrade = bridge.function(CustomElementRegistry.upgrade, .{ .ce_reactions = true });
pub const whenDefined = bridge.function(CustomElementRegistry.whenDefined, .{ .dom_exception = true });
};

View File

@@ -129,7 +129,7 @@ pub const JsApi = struct {
};
pub const constructor = bridge.constructor(DOMParser.init, .{});
pub const parseFromString = bridge.function(DOMParser.parseFromString, .{});
pub const parseFromString = bridge.function(DOMParser.parseFromString, .{ .ce_reactions = true });
};
const testing = @import("../../testing.zig");

View File

@@ -718,7 +718,13 @@ pub fn writeln(self: *Document, text: []const []const u8, frame: *Frame) !void {
return self.writeInternal(text, true, frame);
}
fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool, frame: *Frame) !void {
fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool, call_frame: *Frame) !void {
// document.write acts on this document's own frame, which isn't necessarily
// the calling frame — e.g. a parent frame writing into an iframe's document.
// The markup (and any scripts it contains) must be parsed and run in that
// document's context, not the caller's.
const frame = self._frame orelse call_frame;
if (self._type == .xml) {
return error.InvalidStateError;
}
@@ -730,10 +736,13 @@ fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool
const html = blk: {
var joined: std.ArrayList(u8) = .empty;
for (text) |str| {
try joined.appendSlice(frame.call_arena, str);
// Scratch buffer, consumed synchronously below. Keep it on the
// active (calling) frame's call_arena: a script run by the parse
// could reset the document frame's call_arena underfoot.
try joined.appendSlice(call_frame.call_arena, str);
}
if (append_newline) {
try joined.append(frame.call_arena, '\n');
try joined.append(call_frame.call_arena, '\n');
}
break :blk joined.items;
};
@@ -827,7 +836,9 @@ fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool
self._write_insertion_point = children_to_insert.getLast();
}
pub fn open(self: *Document, frame: *Frame) !*Document {
pub fn open(self: *Document, call_frame: *Frame) !*Document {
const frame = self._frame orelse call_frame;
if (self._type == .xml) {
return error.InvalidStateError;
}
@@ -869,7 +880,9 @@ pub fn open(self: *Document, frame: *Frame) !*Document {
return self;
}
pub fn close(self: *Document, frame: *Frame) !void {
pub fn close(self: *Document, call_frame: *Frame) !void {
const frame = self._frame orelse call_frame;
if (self._type == .xml) {
return error.InvalidStateError;
}
@@ -889,6 +902,12 @@ pub fn close(self: *Document, frame: *Frame) !void {
defer self._script_created_parser = null;
try self._script_created_parser.?.done();
// The write'd markup is fully parsed; run any deferred scripts it produced
// (e.g. inline modules) before firing the load event. This frame's initial
// parse may never have set static_scripts_done (e.g. a freshly-loaded
// iframe written into via document.write), so we can't rely on it.
frame._script_manager.base.scriptCreatedParseDone();
frame.documentIsComplete();
}
@@ -1160,17 +1179,17 @@ pub const JsApi = struct {
pub const getSelection = bridge.function(Document.getSelection, .{});
pub const getElementsByClassName = bridge.function(Document.getElementsByClassName, .{});
pub const getElementsByName = bridge.function(Document.getElementsByName, .{});
pub const adoptNode = bridge.function(Document.adoptNode, .{ .dom_exception = true });
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 adoptNode = bridge.function(Document.adoptNode, .{ .dom_exception = true, .ce_reactions = true });
pub const importNode = bridge.function(Document.importNode, .{ .dom_exception = true, .ce_reactions = true });
pub const append = bridge.function(Document.append, .{ .dom_exception = true, .ce_reactions = true });
pub const prepend = bridge.function(Document.prepend, .{ .dom_exception = true, .ce_reactions = true });
pub const replaceChildren = bridge.function(Document.replaceChildren, .{ .dom_exception = true, .ce_reactions = true });
pub const elementFromPoint = bridge.function(Document.elementFromPoint, .{});
pub const elementsFromPoint = bridge.function(Document.elementsFromPoint, .{});
pub const write = bridge.function(Document.write, .{ .dom_exception = true });
pub const writeln = bridge.function(Document.writeln, .{ .dom_exception = true });
pub const open = bridge.function(Document.open, .{ .dom_exception = true });
pub const close = bridge.function(Document.close, .{ .dom_exception = true });
pub const write = bridge.function(Document.write, .{ .dom_exception = true, .ce_reactions = true });
pub const writeln = bridge.function(Document.writeln, .{ .dom_exception = true, .ce_reactions = true });
pub const open = bridge.function(Document.open, .{ .dom_exception = true, .ce_reactions = true });
pub const close = bridge.function(Document.close, .{ .dom_exception = true, .ce_reactions = true });
pub const doctype = bridge.accessor(Document.getDocType, null, .{});
pub const firstElementChild = bridge.accessor(Document.getFirstElementChild, null, .{});
pub const lastElementChild = bridge.accessor(Document.getLastElementChild, null, .{});

View File

@@ -239,10 +239,10 @@ pub const JsApi = struct {
pub const childElementCount = bridge.accessor(DocumentFragment.getChildElementCount, null, .{});
pub const firstElementChild = bridge.accessor(DocumentFragment.firstElementChild, null, .{});
pub const lastElementChild = bridge.accessor(DocumentFragment.lastElementChild, null, .{});
pub const append = bridge.function(DocumentFragment.append, .{ .dom_exception = true });
pub const prepend = bridge.function(DocumentFragment.prepend, .{ .dom_exception = true });
pub const replaceChildren = bridge.function(DocumentFragment.replaceChildren, .{ .dom_exception = true });
pub const innerHTML = bridge.accessor(_innerHTML, DocumentFragment.setInnerHTML, .{});
pub const append = bridge.function(DocumentFragment.append, .{ .dom_exception = true, .ce_reactions = true });
pub const prepend = bridge.function(DocumentFragment.prepend, .{ .dom_exception = true, .ce_reactions = true });
pub const replaceChildren = bridge.function(DocumentFragment.replaceChildren, .{ .dom_exception = true, .ce_reactions = true });
pub const innerHTML = bridge.accessor(_innerHTML, DocumentFragment.setInnerHTML, .{ .ce_reactions = true });
fn _innerHTML(self: *DocumentFragment, frame: *Frame) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(frame.call_arena);

View File

@@ -474,11 +474,7 @@ pub fn setOuterHTML(self: *Element, html: []const u8, frame: *Frame) !void {
try frame.insertAllChildrenBefore(fragment, parent, node);
}
// A custom element callback fired during insertAllChildrenBefore may
// have already detached `node`; only remove it if it's still here.
if (node._parent == parent) {
frame.removeNode(parent, node, .{ .will_be_reconnected = false });
}
frame.removeNode(parent, node, .{ .will_be_reconnected = false });
}
pub fn getInnerHTML(self: *Element, writer: *std.Io.Writer, frame: *Frame) !void {
@@ -489,21 +485,16 @@ pub fn getInnerHTML(self: *Element, writer: *std.Io.Writer, frame: *Frame) !void
pub fn setInnerHTML(self: *Element, html: []const u8, frame: *Frame) !void {
const parent = self.asNode();
// Remove all existing children. Drain via firstChild(): removeNode
// fires disconnectedCallback for custom elements, which can mutate
// the child list and dangle any cached next-pointer the iterator
// would otherwise hold.
frame.domChanged();
while (parent.firstChild()) |child| {
var it = parent.childrenIterator();
while (it.next()) |child| {
frame.removeNode(parent, child, .{ .will_be_reconnected = false });
}
// Fast path: skip parsing if html is empty
if (html.len == 0) {
return;
}
// Parse and add new children
try frame.parseHtmlAsChildren(parent, html);
}
@@ -1743,21 +1734,21 @@ pub const JsApi = struct {
}
pub const namespaceURI = bridge.accessor(Element.getNamespaceURI, null, .{});
pub const innerText = bridge.accessor(_innerText, Element.setInnerText, .{});
pub const innerText = bridge.accessor(_innerText, Element.setInnerText, .{ .ce_reactions = true });
fn _innerText(self: *Element, frame: *const Frame) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
try self.getInnerText(&buf.writer);
return buf.written();
}
pub const outerHTML = bridge.accessor(_outerHTML, Element.setOuterHTML, .{});
pub const outerHTML = bridge.accessor(_outerHTML, Element.setOuterHTML, .{ .ce_reactions = true });
fn _outerHTML(self: *Element, frame: *Frame) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
try self.getOuterHTML(&buf.writer, frame);
return buf.written();
}
pub const innerHTML = bridge.accessor(_innerHTML, Element.setInnerHTML, .{});
pub const innerHTML = bridge.accessor(_innerHTML, Element.setInnerHTML, .{ .ce_reactions = true });
fn _innerHTML(self: *Element, frame: *Frame) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
try self.getInnerHTML(&buf.writer, frame);
@@ -1766,24 +1757,24 @@ pub const JsApi = struct {
pub const prefix = bridge.accessor(Element._prefix, null, .{});
pub const setAttribute = bridge.function(_setAttribute, .{ .dom_exception = true });
pub const setAttribute = bridge.function(_setAttribute, .{ .dom_exception = true, .ce_reactions = true });
fn _setAttribute(self: *Element, name: String, value: js.Value, frame: *Frame) !void {
return self.setAttribute(name, .wrap(try value.toStringSlice()), frame);
}
pub const setAttributeNS = bridge.function(_setAttributeNS, .{ .dom_exception = true });
pub const setAttributeNS = bridge.function(_setAttributeNS, .{ .dom_exception = true, .ce_reactions = true });
fn _setAttributeNS(self: *Element, maybe_ns: ?[]const u8, qn: []const u8, value: js.Value, frame: *Frame) !void {
return self.setAttributeNS(maybe_ns, qn, .wrap(try value.toStringSlice()), frame);
}
pub const localName = bridge.accessor(Element.getLocalName, null, .{});
pub const id = bridge.accessor(Element.getId, Element.setId, .{});
pub const slot = bridge.accessor(Element.getSlot, Element.setSlot, .{});
pub const ariaAtomic = bridge.accessor(Element.getAriaAtomic, Element.setAriaAtomic, .{});
pub const ariaLive = bridge.accessor(Element.getAriaLive, Element.setAriaLive, .{});
pub const dir = bridge.accessor(Element.getDir, Element.setDir, .{});
pub const className = bridge.accessor(Element.getClassName, Element.setClassName, .{});
pub const classList = bridge.accessor(Element.getClassList, Element.setClassList, .{});
pub const id = bridge.accessor(Element.getId, Element.setId, .{ .ce_reactions = true });
pub const slot = bridge.accessor(Element.getSlot, Element.setSlot, .{ .ce_reactions = true });
pub const ariaAtomic = bridge.accessor(Element.getAriaAtomic, Element.setAriaAtomic, .{ .ce_reactions = true });
pub const ariaLive = bridge.accessor(Element.getAriaLive, Element.setAriaLive, .{ .ce_reactions = true });
pub const dir = bridge.accessor(Element.getDir, Element.setDir, .{ .ce_reactions = true });
pub const className = bridge.accessor(Element.getClassName, Element.setClassName, .{ .ce_reactions = true });
pub const classList = bridge.accessor(Element.getClassList, Element.setClassList, .{ .ce_reactions = true });
pub const dataset = bridge.accessor(Element.getDataset, null, .{});
pub const style = bridge.accessor(Element.getOrCreateStyle, null, .{});
pub const attributes = bridge.accessor(Element.getAttributeNamedNodeMap, null, .{});
@@ -1792,17 +1783,17 @@ pub const JsApi = struct {
pub const getAttribute = bridge.function(Element.getAttribute, .{});
pub const getAttributeNS = bridge.function(Element.getAttributeNS, .{});
pub const getAttributeNode = bridge.function(Element.getAttributeNode, .{});
pub const setAttributeNode = bridge.function(Element.setAttributeNode, .{});
pub const removeAttribute = bridge.function(Element.removeAttribute, .{});
pub const toggleAttribute = bridge.function(Element.toggleAttribute, .{ .dom_exception = true });
pub const setAttributeNode = bridge.function(Element.setAttributeNode, .{ .ce_reactions = true });
pub const removeAttribute = bridge.function(Element.removeAttribute, .{ .ce_reactions = true });
pub const toggleAttribute = bridge.function(Element.toggleAttribute, .{ .dom_exception = true, .ce_reactions = true });
pub const getAttributeNames = bridge.function(Element.getAttributeNames, .{});
pub const removeAttributeNode = bridge.function(Element.removeAttributeNode, .{ .dom_exception = true });
pub const removeAttributeNode = bridge.function(Element.removeAttributeNode, .{ .dom_exception = true, .ce_reactions = true });
pub const shadowRoot = bridge.accessor(Element.getShadowRoot, null, .{});
pub const assignedSlot = bridge.accessor(Element.getAssignedSlot, null, .{});
pub const attachShadow = bridge.function(_attachShadow, .{ .dom_exception = true });
pub const insertAdjacentHTML = bridge.function(Element.insertAdjacentHTML, .{ .dom_exception = true });
pub const insertAdjacentElement = bridge.function(Element.insertAdjacentElement, .{ .dom_exception = true });
pub const insertAdjacentText = bridge.function(Element.insertAdjacentText, .{ .dom_exception = true });
pub const insertAdjacentHTML = bridge.function(Element.insertAdjacentHTML, .{ .dom_exception = true, .ce_reactions = true });
pub const insertAdjacentElement = bridge.function(Element.insertAdjacentElement, .{ .dom_exception = true, .ce_reactions = true });
pub const insertAdjacentText = bridge.function(Element.insertAdjacentText, .{ .dom_exception = true, .ce_reactions = true });
const ShadowRootInit = struct {
mode: []const u8,
@@ -1810,13 +1801,13 @@ pub const JsApi = struct {
fn _attachShadow(self: *Element, init: ShadowRootInit, frame: *Frame) !*ShadowRoot {
return self.attachShadow(init.mode, frame);
}
pub const replaceChildren = bridge.function(Element.replaceChildren, .{ .dom_exception = true });
pub const replaceWith = bridge.function(Element.replaceWith, .{ .dom_exception = true });
pub const remove = bridge.function(Element.remove, .{});
pub const append = bridge.function(Element.append, .{ .dom_exception = true });
pub const prepend = bridge.function(Element.prepend, .{ .dom_exception = true });
pub const before = bridge.function(Element.before, .{ .dom_exception = true });
pub const after = bridge.function(Element.after, .{ .dom_exception = true });
pub const replaceChildren = bridge.function(Element.replaceChildren, .{ .dom_exception = true, .ce_reactions = true });
pub const replaceWith = bridge.function(Element.replaceWith, .{ .dom_exception = true, .ce_reactions = true });
pub const remove = bridge.function(Element.remove, .{ .ce_reactions = true });
pub const append = bridge.function(Element.append, .{ .dom_exception = true, .ce_reactions = true });
pub const prepend = bridge.function(Element.prepend, .{ .dom_exception = true, .ce_reactions = true });
pub const before = bridge.function(Element.before, .{ .dom_exception = true, .ce_reactions = true });
pub const after = bridge.function(Element.after, .{ .dom_exception = true, .ce_reactions = true });
pub const firstElementChild = bridge.accessor(Element.firstElementChild, null, .{});
pub const lastElementChild = bridge.accessor(Element.lastElementChild, null, .{});
pub const nextElementSibling = bridge.accessor(Element.nextElementSibling, null, .{});

View File

@@ -288,11 +288,11 @@ pub const JsApi = struct {
});
}
pub const dir = bridge.accessor(HTMLDocument.getDir, HTMLDocument.setDir, .{});
pub const dir = bridge.accessor(HTMLDocument.getDir, HTMLDocument.setDir, .{ .ce_reactions = true });
pub const head = bridge.accessor(HTMLDocument.getHead, null, .{});
pub const body = bridge.accessor(HTMLDocument.getBody, HTMLDocument.setBody, .{ .dom_exception = true });
pub const body = bridge.accessor(HTMLDocument.getBody, HTMLDocument.setBody, .{ .dom_exception = true, .ce_reactions = true });
pub const lang = bridge.accessor(HTMLDocument.getLang, HTMLDocument.setLang, .{});
pub const title = bridge.accessor(HTMLDocument.getTitle, HTMLDocument.setTitle, .{});
pub const title = bridge.accessor(HTMLDocument.getTitle, HTMLDocument.setTitle, .{ .ce_reactions = true });
pub const images = bridge.accessor(HTMLDocument.getImages, null, .{});
pub const scripts = bridge.accessor(HTMLDocument.getScripts, null, .{});
pub const links = bridge.accessor(HTMLDocument.getLinks, null, .{});

View File

@@ -169,7 +169,7 @@ pub const JsApi = struct {
// Read-only properties
pub const userAgent = bridge.accessor(Navigator.getUserAgent, null, .{});
pub const appName = bridge.property("Netscape", .{ .template = false });
pub const appCodeName = bridge.property("Netscape", .{ .template = false });
pub const appCodeName = bridge.property("Mozilla", .{ .template = false });
pub const appVersion = bridge.property("1.0", .{ .template = false });
pub const platform = bridge.accessor(Navigator.getPlatform, null, .{});
pub const language = bridge.property("en-US", .{ .template = false });

View File

@@ -253,11 +253,6 @@ pub fn appendChild(self: *Node, child: *Node, frame: *Frame) !*Node {
try frame.adoptNodeTree(child, child_owner.?, parent_owner);
}
// A custom element callback can re-parent the node. If it does, we're done
if (child._parent != null) {
return child;
}
try frame.appendNode(self, child, .{
.child_already_connected = child_connected,
.adopting_to_new_document = adopting_to_new_document,
@@ -620,22 +615,6 @@ pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, frame: *Fra
try frame.adoptNodeTree(new_node, child_owner.?, parent_owner);
}
// See Node.appendChild: a callback above (disconnectedCallback or
// adoptedCallback) can re-parent new_node. Let that placement stand.
if (new_node._parent != null) {
return new_node;
}
// The same callback could also have detached ref_node from self. Fall
// back to append so new_node still lands in self.
if (ref_node._parent != self) {
try frame.appendNode(self, new_node, .{
.child_already_connected = child_already_connected,
.adopting_to_new_document = adopting_to_new_document,
});
return new_node;
}
try frame.insertNodeRelative(
self,
new_node,
@@ -658,11 +637,8 @@ pub fn replaceChild(self: *Node, new_child: *Node, old_child: *Node, frame: *Fra
_ = try self.insertBefore(new_child, old_child, frame);
// Special case: if we replace a node by itself, we don't remove it.
// insertBefore is an noop in this case.
// Re-check parent after insertBefore since callbacks (e.g. connectedCallback)
// could have already removed old_child from self.
if (new_child != old_child and old_child._parent == self) {
// Special case: if we replace a node by itself, insertBefore was a noop.
if (new_child != old_child) {
frame.removeNode(self, old_child, .{ .will_be_reconnected = false });
}
@@ -1163,7 +1139,7 @@ pub const JsApi = struct {
}.wrap, null, .{});
pub const nodeType = bridge.accessor(Node.getNodeType, null, .{});
pub const textContent = bridge.accessor(_textContext, Node.setTextContent, .{});
pub const textContent = bridge.accessor(_textContext, Node.setTextContent, .{ .ce_reactions = true });
fn _textContext(self: *Node, frame: *const Frame) !?[]const u8 {
// cdata and attributes can return value directly, avoiding the copy
switch (self._type) {
@@ -1185,19 +1161,19 @@ pub const JsApi = struct {
pub const previousSibling = bridge.accessor(Node.previousSibling, null, .{});
pub const parentNode = bridge.accessor(Node.parentNode, null, .{});
pub const parentElement = bridge.accessor(Node.parentElement, null, .{});
pub const appendChild = bridge.function(Node.appendChild, .{ .dom_exception = true });
pub const appendChild = bridge.function(Node.appendChild, .{ .dom_exception = true, .ce_reactions = true });
pub const childNodes = bridge.accessor(Node.childNodes, null, .{ .cache = .{ .private = "child_nodes" } });
pub const isConnected = bridge.accessor(Node.isConnected, null, .{});
pub const ownerDocument = bridge.accessor(Node.ownerDocument, null, .{});
pub const hasChildNodes = bridge.function(Node.hasChildNodes, .{});
pub const isSameNode = bridge.function(Node.isSameNode, .{});
pub const contains = bridge.function(Node.contains, .{});
pub const removeChild = bridge.function(Node.removeChild, .{ .dom_exception = true });
pub const nodeValue = bridge.accessor(Node.getNodeValue, Node.setNodeValue, .{});
pub const insertBefore = bridge.function(Node.insertBefore, .{ .dom_exception = true });
pub const replaceChild = bridge.function(Node.replaceChild, .{ .dom_exception = true });
pub const normalize = bridge.function(Node.normalize, .{});
pub const cloneNode = bridge.function(Node.cloneNode, .{ .dom_exception = true });
pub const removeChild = bridge.function(Node.removeChild, .{ .dom_exception = true, .ce_reactions = true });
pub const nodeValue = bridge.accessor(Node.getNodeValue, Node.setNodeValue, .{ .ce_reactions = true });
pub const insertBefore = bridge.function(Node.insertBefore, .{ .dom_exception = true, .ce_reactions = true });
pub const replaceChild = bridge.function(Node.replaceChild, .{ .dom_exception = true, .ce_reactions = true });
pub const normalize = bridge.function(Node.normalize, .{ .ce_reactions = true });
pub const cloneNode = bridge.function(Node.cloneNode, .{ .dom_exception = true, .ce_reactions = true });
pub const compareDocumentPosition = bridge.function(Node.compareDocumentPosition, .{});
pub const getRootNode = bridge.function(Node.getRootNode, .{});
pub const isEqualNode = bridge.function(Node.isEqualNode, .{});

View File

@@ -718,12 +718,12 @@ pub const JsApi = struct {
pub const isPointInRange = bridge.function(Range.isPointInRange, .{ .dom_exception = true });
pub const intersectsNode = bridge.function(Range.intersectsNode, .{});
pub const cloneRange = bridge.function(Range.cloneRange, .{ .dom_exception = true });
pub const insertNode = bridge.function(Range.insertNode, .{ .dom_exception = true });
pub const deleteContents = bridge.function(Range.deleteContents, .{ .dom_exception = true });
pub const cloneContents = bridge.function(Range.cloneContents, .{ .dom_exception = true });
pub const extractContents = bridge.function(Range.extractContents, .{ .dom_exception = true });
pub const surroundContents = bridge.function(Range.surroundContents, .{ .dom_exception = true });
pub const createContextualFragment = bridge.function(Range.createContextualFragment, .{ .dom_exception = true });
pub const insertNode = bridge.function(Range.insertNode, .{ .dom_exception = true, .ce_reactions = true });
pub const deleteContents = bridge.function(Range.deleteContents, .{ .dom_exception = true, .ce_reactions = true });
pub const cloneContents = bridge.function(Range.cloneContents, .{ .dom_exception = true, .ce_reactions = true });
pub const extractContents = bridge.function(Range.extractContents, .{ .dom_exception = true, .ce_reactions = true });
pub const surroundContents = bridge.function(Range.surroundContents, .{ .dom_exception = true, .ce_reactions = true });
pub const createContextualFragment = bridge.function(Range.createContextualFragment, .{ .dom_exception = true, .ce_reactions = true });
pub const toString = bridge.function(Range.toString, .{ .dom_exception = true });
pub const getBoundingClientRect = bridge.function(Range.getBoundingClientRect, .{});
pub const getClientRects = bridge.function(Range.getClientRects, .{});

View File

@@ -734,7 +734,7 @@ pub const JsApi = struct {
pub const collapseToEnd = bridge.function(Selection.collapseToEnd, .{});
pub const collapseToStart = bridge.function(Selection.collapseToStart, .{ .dom_exception = true });
pub const containsNode = bridge.function(Selection.containsNode, .{});
pub const deleteFromDocument = bridge.function(Selection.deleteFromDocument, .{});
pub const deleteFromDocument = bridge.function(Selection.deleteFromDocument, .{ .ce_reactions = true });
pub const empty = bridge.function(Selection.removeAllRanges, .{});
pub const extend = bridge.function(Selection.extend, .{ .dom_exception = true });
// unimplemented: getComposedRanges

View File

@@ -311,11 +311,11 @@ pub const JsApi = struct {
}
pub const contains = bridge.function(DOMTokenList.contains, .{ .dom_exception = true });
pub const add = bridge.function(DOMTokenList.add, .{ .dom_exception = true });
pub const remove = bridge.function(DOMTokenList.remove, .{ .dom_exception = true });
pub const toggle = bridge.function(DOMTokenList.toggle, .{ .dom_exception = true });
pub const replace = bridge.function(DOMTokenList.replace, .{ .dom_exception = true });
pub const value = bridge.accessor(DOMTokenList.getValue, DOMTokenList.setValue, .{});
pub const add = bridge.function(DOMTokenList.add, .{ .dom_exception = true, .ce_reactions = true });
pub const remove = bridge.function(DOMTokenList.remove, .{ .dom_exception = true, .ce_reactions = true });
pub const toggle = bridge.function(DOMTokenList.toggle, .{ .dom_exception = true, .ce_reactions = true });
pub const replace = bridge.function(DOMTokenList.replace, .{ .dom_exception = true, .ce_reactions = true });
pub const value = bridge.accessor(DOMTokenList.getValue, DOMTokenList.setValue, .{ .ce_reactions = true });
pub const toString = bridge.function(DOMTokenList.getValue, .{});
pub const keys = bridge.function(DOMTokenList.keys, .{});
pub const values = bridge.function(DOMTokenList.values, .{});

View File

@@ -106,6 +106,6 @@ pub const JsApi = struct {
pub const @"[str]" = bridge.namedIndexed(HTMLOptionsCollection.getByName, null, null, .{ .null_as_undefined = true });
pub const selectedIndex = bridge.accessor(HTMLOptionsCollection.getSelectedIndex, HTMLOptionsCollection.setSelectedIndex, .{});
pub const add = bridge.function(HTMLOptionsCollection.add, .{});
pub const remove = bridge.function(HTMLOptionsCollection.remove, .{});
pub const add = bridge.function(HTMLOptionsCollection.add, .{ .ce_reactions = true });
pub const remove = bridge.function(HTMLOptionsCollection.remove, .{ .ce_reactions = true });
};

View File

@@ -104,7 +104,7 @@ pub const JsApi = struct {
pub const name = bridge.accessor(Attribute.getName, null, .{});
pub const localName = bridge.accessor(Attribute.getName, null, .{});
pub const value = bridge.accessor(Attribute.getValue, Attribute.setValue, .{});
pub const value = bridge.accessor(Attribute.getValue, Attribute.setValue, .{ .ce_reactions = true });
pub const namespaceURI = bridge.accessor(Attribute.getNamespaceURI, null, .{});
pub const ownerElement = bridge.accessor(Attribute.getOwnerElement, null, .{});
};
@@ -255,7 +255,11 @@ pub const List = struct {
const existing_attribute = try self.getAttribute(attribute._name, element, frame);
if (existing_attribute) |ea| {
try self.delete(ea._name, element, frame);
// Per DOM "replace an attribute": one handle-attribute-changes call
// with (old, new), not a remove-then-add that would fire two
// attributeChanged reactions. Detach the old wrapper; put() updates
// the entry in place and fires the single reaction.
ea._element = null;
}
const entry = try self.put(attribute._name, attribute._value, element, frame);
@@ -536,8 +540,8 @@ pub const NamedNodeMap = struct {
pub const @"[int]" = bridge.indexed(NamedNodeMap.getAtIndex, null, .{ .null_as_undefined = true });
pub const @"[str]" = bridge.namedIndexed(NamedNodeMap.getByName, null, null, .{ .null_as_undefined = true });
pub const getNamedItem = bridge.function(NamedNodeMap.getByName, .{});
pub const setNamedItem = bridge.function(NamedNodeMap.set, .{});
pub const removeNamedItem = bridge.function(NamedNodeMap.removeByName, .{});
pub const setNamedItem = bridge.function(NamedNodeMap.set, .{ .ce_reactions = true });
pub const removeNamedItem = bridge.function(NamedNodeMap.removeByName, .{ .ce_reactions = true });
pub const item = bridge.function(_item, .{});
fn _item(self: *const NamedNodeMap, index: i32, frame: *Frame) !?*Attribute {
// the bridge.indexed handles this, so if we want

View File

@@ -136,5 +136,5 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const @"[]" = bridge.namedIndexed(getProperty, setProperty, deleteProperty, .{ .null_as_undefined = true });
pub const @"[]" = bridge.namedIndexed(getProperty, setProperty, deleteProperty, .{ .null_as_undefined = true, .ce_reactions = true });
};

View File

@@ -1234,21 +1234,21 @@ pub const JsApi = struct {
pub const constructor = bridge.constructor(HtmlElement.construct, .{ .new_target = true });
pub const innerText = bridge.accessor(_innerText, HtmlElement.setInnerText, .{});
pub const innerText = bridge.accessor(_innerText, HtmlElement.setInnerText, .{ .ce_reactions = true });
fn _innerText(self: *HtmlElement, frame: *const Frame) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
try self.getInnerText(&buf.writer);
return buf.written();
}
pub const insertAdjacentHTML = bridge.function(HtmlElement.insertAdjacentHTML, .{ .dom_exception = true });
pub const insertAdjacentHTML = bridge.function(HtmlElement.insertAdjacentHTML, .{ .dom_exception = true, .ce_reactions = true });
pub const click = bridge.function(HtmlElement.click, .{});
pub const dir = bridge.accessor(HtmlElement.getDir, HtmlElement.setDir, .{});
pub const hidden = bridge.accessor(HtmlElement.getHidden, HtmlElement.setHidden, .{});
pub const dir = bridge.accessor(HtmlElement.getDir, HtmlElement.setDir, .{ .ce_reactions = true });
pub const hidden = bridge.accessor(HtmlElement.getHidden, HtmlElement.setHidden, .{ .ce_reactions = true });
pub const isContentEditable = bridge.accessor(HtmlElement.getIsContentEditable, null, .{});
pub const lang = bridge.accessor(HtmlElement.getLang, HtmlElement.setLang, .{});
pub const tabIndex = bridge.accessor(HtmlElement.getTabIndex, HtmlElement.setTabIndex, .{});
pub const title = bridge.accessor(HtmlElement.getTitle, HtmlElement.setTitle, .{});
pub const lang = bridge.accessor(HtmlElement.getLang, HtmlElement.setLang, .{ .ce_reactions = true });
pub const tabIndex = bridge.accessor(HtmlElement.getTabIndex, HtmlElement.setTabIndex, .{ .ce_reactions = true });
pub const title = bridge.accessor(HtmlElement.getTitle, HtmlElement.setTitle, .{ .ce_reactions = true });
pub const onabort = bridge.accessor(HtmlElement.getOnAbort, HtmlElement.setOnAbort, .{});
pub const onanimationcancel = bridge.accessor(HtmlElement.getOnAnimationCancel, HtmlElement.setOnAnimationCancel, .{});

View File

@@ -214,20 +214,20 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const href = bridge.accessor(Anchor.getHref, Anchor.setHref, .{});
pub const target = bridge.accessor(Anchor.getTarget, Anchor.setTarget, .{});
pub const name = bridge.accessor(Anchor.getName, Anchor.setName, .{});
pub const href = bridge.accessor(Anchor.getHref, Anchor.setHref, .{ .ce_reactions = true });
pub const target = bridge.accessor(Anchor.getTarget, Anchor.setTarget, .{ .ce_reactions = true });
pub const name = bridge.accessor(Anchor.getName, Anchor.setName, .{ .ce_reactions = true });
pub const origin = bridge.accessor(Anchor.getOrigin, null, .{});
pub const protocol = bridge.accessor(Anchor.getProtocol, Anchor.setProtocol, .{});
pub const host = bridge.accessor(Anchor.getHost, Anchor.setHost, .{});
pub const hostname = bridge.accessor(Anchor.getHostname, Anchor.setHostname, .{});
pub const port = bridge.accessor(Anchor.getPort, Anchor.setPort, .{});
pub const pathname = bridge.accessor(Anchor.getPathname, Anchor.setPathname, .{});
pub const search = bridge.accessor(Anchor.getSearch, Anchor.setSearch, .{});
pub const hash = bridge.accessor(Anchor.getHash, Anchor.setHash, .{});
pub const rel = bridge.accessor(Anchor.getRel, Anchor.setRel, .{});
pub const @"type" = bridge.accessor(Anchor.getType, Anchor.setType, .{});
pub const text = bridge.accessor(Anchor.getText, Anchor.setText, .{});
pub const protocol = bridge.accessor(Anchor.getProtocol, Anchor.setProtocol, .{ .ce_reactions = true });
pub const host = bridge.accessor(Anchor.getHost, Anchor.setHost, .{ .ce_reactions = true });
pub const hostname = bridge.accessor(Anchor.getHostname, Anchor.setHostname, .{ .ce_reactions = true });
pub const port = bridge.accessor(Anchor.getPort, Anchor.setPort, .{ .ce_reactions = true });
pub const pathname = bridge.accessor(Anchor.getPathname, Anchor.setPathname, .{ .ce_reactions = true });
pub const search = bridge.accessor(Anchor.getSearch, Anchor.setSearch, .{ .ce_reactions = true });
pub const hash = bridge.accessor(Anchor.getHash, Anchor.setHash, .{ .ce_reactions = true });
pub const rel = bridge.accessor(Anchor.getRel, Anchor.setRel, .{ .ce_reactions = true });
pub const @"type" = bridge.accessor(Anchor.getType, Anchor.setType, .{ .ce_reactions = true });
pub const text = bridge.accessor(Anchor.getText, Anchor.setText, .{ .ce_reactions = true });
pub const relList = bridge.accessor(_getRelList, null, .{ .null_as_undefined = true });
pub const toString = bridge.function(Anchor.getHref, .{});

View File

@@ -237,17 +237,17 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const disabled = bridge.accessor(Button.getDisabled, Button.setDisabled, .{});
pub const name = bridge.accessor(Button.getName, Button.setName, .{});
pub const required = bridge.accessor(Button.getRequired, Button.setRequired, .{});
pub const disabled = bridge.accessor(Button.getDisabled, Button.setDisabled, .{ .ce_reactions = true });
pub const name = bridge.accessor(Button.getName, Button.setName, .{ .ce_reactions = true });
pub const required = bridge.accessor(Button.getRequired, Button.setRequired, .{ .ce_reactions = true });
pub const form = bridge.accessor(Button.getForm, null, .{});
pub const formAction = bridge.accessor(Button.getFormAction, Button.setFormAction, .{});
pub const formEnctype = bridge.accessor(Button.getFormEnctype, Button.setFormEnctype, .{});
pub const formMethod = bridge.accessor(Button.getFormMethod, Button.setFormMethod, .{});
pub const formNoValidate = bridge.accessor(Button.getFormNoValidate, Button.setFormNoValidate, .{});
pub const formTarget = bridge.accessor(Button.getFormTarget, Button.setFormTarget, .{});
pub const value = bridge.accessor(Button.getValue, Button.setValue, .{});
pub const @"type" = bridge.accessor(Button.getType, Button.setType, .{});
pub const formAction = bridge.accessor(Button.getFormAction, Button.setFormAction, .{ .ce_reactions = true });
pub const formEnctype = bridge.accessor(Button.getFormEnctype, Button.setFormEnctype, .{ .ce_reactions = true });
pub const formMethod = bridge.accessor(Button.getFormMethod, Button.setFormMethod, .{ .ce_reactions = true });
pub const formNoValidate = bridge.accessor(Button.getFormNoValidate, Button.setFormNoValidate, .{ .ce_reactions = true });
pub const formTarget = bridge.accessor(Button.getFormTarget, Button.setFormTarget, .{ .ce_reactions = true });
pub const value = bridge.accessor(Button.getValue, Button.setValue, .{ .ce_reactions = true });
pub const @"type" = bridge.accessor(Button.getType, Button.setType, .{ .ce_reactions = true });
pub const labels = bridge.accessor(Button.getLabels, null, .{});
pub const willValidate = bridge.accessor(Button.getWillValidate, null, .{});
pub const validity = bridge.accessor(Button.getValidity, null, .{});

View File

@@ -120,8 +120,8 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const width = bridge.accessor(Canvas.getWidth, Canvas.setWidth, .{});
pub const height = bridge.accessor(Canvas.getHeight, Canvas.setHeight, .{});
pub const width = bridge.accessor(Canvas.getWidth, Canvas.setWidth, .{ .ce_reactions = true });
pub const height = bridge.accessor(Canvas.getHeight, Canvas.setHeight, .{ .ce_reactions = true });
pub const getContext = bridge.function(Canvas.getContext, .{});
pub const transferControlToOffscreen = bridge.function(Canvas.transferControlToOffscreen, .{});
};

View File

@@ -27,6 +27,7 @@ const Element = @import("../../Element.zig");
const Document = @import("../../Document.zig");
const HtmlElement = @import("../Html.zig");
const CustomElementDefinition = @import("../../CustomElementDefinition.zig");
const Reaction = @import("../../../CustomElementReactions.zig").Reaction;
const log = lp.log;
const String = lp.String;
@@ -45,53 +46,18 @@ pub fn asNode(self: *Custom) *Node {
return self.asElement().asNode();
}
pub fn invokeConnectedCallback(self: *Custom, frame: *Frame) void {
// Only invoke if we haven't already called it while connected
if (self._connected_callback_invoked) {
return;
}
// Reactions are queued via enqueue* and fired via fireReaction at the outer
// CEReactions boundary (set up by the JS bridge, the parser pump, etc.).
//
// Dedup happens at enqueue time: the connected/disconnected flags flip when
// we queue a reaction so that a redundant enqueue (already-in-this-state)
// is dropped, and a remove+re-insert in the same scope queues both reactions
// in order. Fire-time is unconditional.
self._connected_callback_invoked = true;
self._disconnected_callback_invoked = false;
self.invokeCallback("connectedCallback", .{}, frame);
}
pub fn invokeDisconnectedCallback(self: *Custom, frame: *Frame) void {
// Only invoke if we haven't already called it while disconnected
if (self._disconnected_callback_invoked) {
return;
}
self._disconnected_callback_invoked = true;
self._connected_callback_invoked = false;
self.invokeCallback("disconnectedCallback", .{}, frame);
}
pub fn invokeAttributeChangedCallback(self: *Custom, name: String, old_value: ?String, new_value: ?String, namespace: ?String, frame: *Frame) void {
const definition = self._definition orelse return;
if (!definition.isAttributeObserved(name)) {
return;
}
self.invokeCallback("attributeChangedCallback", .{ name, old_value, new_value, namespace }, frame);
}
pub fn invokeAdoptedCallback(self: *Custom, old_document: *Document, new_document: *Document, frame: *Frame) void {
self.invokeCallback("adoptedCallback", .{ old_document, new_document }, frame);
}
pub fn invokeAdoptedCallbackOnElement(element: *Element, old_document: *Document, new_document: *Document, frame: *Frame) void {
if (element.is(Custom)) |custom| {
custom.invokeAdoptedCallback(old_document, new_document, frame);
return;
}
const definition = frame.getCustomizedBuiltInDefinition(element) orelse return;
invokeCallbackOnElement(element, definition, "adoptedCallback", .{ old_document, new_document }, frame);
}
pub fn invokeConnectedCallbackOnElement(comptime from_parser: bool, element: *Element, frame: *Frame) !void {
pub fn enqueueConnectedCallbackOnElement(comptime from_parser: bool, element: *Element, frame: *Frame) error{OutOfMemory}!void {
// Autonomous custom element
if (element.is(Custom)) |custom| {
// If the element is undefined, check if a definition now exists and upgrade
// Upgrade if a definition exists but isn't yet attached
if (custom._definition == null) {
const name = custom._tag_name.str();
if (frame.window._custom_elements._definitions.get(name)) |definition| {
@@ -99,30 +65,31 @@ pub fn invokeConnectedCallbackOnElement(comptime from_parser: bool, element: *El
CustomElementRegistry.upgradeCustomElement(custom, definition, frame) catch {};
return;
}
// Element is undefined and no definition exists yet — nothing to queue.
return;
}
if (comptime from_parser) {
// From parser, we know the element is brand new
custom._connected_callback_invoked = true;
custom.invokeCallback("connectedCallback", .{}, frame);
} else {
custom.invokeConnectedCallback(frame);
}
// Dedup: skip if already queued/fired while connected.
if (custom._connected_callback_invoked) return;
custom._connected_callback_invoked = true;
custom._disconnected_callback_invoked = false;
try frame._ce_reactions.enqueueConnected(frame, element);
return;
}
// Customized built-in element - check if it actually has a definition first
const definition = frame.getCustomizedBuiltInDefinition(element) orelse return;
if (frame.getCustomizedBuiltInDefinition(element) == null) {
return;
}
if (comptime from_parser) {
// From parser, we know the element is brand new, skip the tracking check
// From parser, we know the element is brand new; skip the dedup check.
try frame._customized_builtin_connected_callback_invoked.put(
frame.arena,
element,
{},
);
} else {
// Not from parser, check if we've already invoked while connected
const gop = try frame._customized_builtin_connected_callback_invoked.getOrPut(
frame.arena,
element,
@@ -134,43 +101,95 @@ pub fn invokeConnectedCallbackOnElement(comptime from_parser: bool, element: *El
}
_ = frame._customized_builtin_disconnected_callback_invoked.remove(element);
invokeCallbackOnElement(element, definition, "connectedCallback", .{}, frame);
try frame._ce_reactions.enqueueConnected(frame, element);
}
pub fn invokeDisconnectedCallbackOnElement(element: *Element, frame: *Frame) void {
// Autonomous custom element
pub fn enqueueDisconnectedCallbackOnElement(element: *Element, frame: *Frame) void {
if (element.is(Custom)) |custom| {
custom.invokeDisconnectedCallback(frame);
if (custom._definition == null) return;
if (custom._disconnected_callback_invoked) return;
custom._disconnected_callback_invoked = true;
custom._connected_callback_invoked = false;
frame._ce_reactions.enqueueDisconnected(frame, element) catch |err| {
log.warn(.bug, "ce_reactions enqueue fail", .{ .err = err });
};
return;
}
// Customized built-in element - check if it actually has a definition first
const definition = frame.getCustomizedBuiltInDefinition(element) orelse return;
if (frame.getCustomizedBuiltInDefinition(element) == null) {
return;
}
// Check if we've already invoked disconnectedCallback while disconnected
const gop = frame._customized_builtin_disconnected_callback_invoked.getOrPut(
frame.arena,
element,
) catch return;
if (gop.found_existing) return;
gop.value_ptr.* = {};
_ = frame._customized_builtin_connected_callback_invoked.remove(element);
invokeCallbackOnElement(element, definition, "disconnectedCallback", .{}, frame);
frame._ce_reactions.enqueueDisconnected(frame, element) catch |err| {
log.warn(.bug, "ce_reactions enqueue fail", .{ .err = err });
};
}
pub fn invokeAttributeChangedCallbackOnElement(element: *Element, name: String, old_value: ?String, new_value: ?String, namespace: ?String, frame: *Frame) void {
// Autonomous custom element
pub fn enqueueAdoptedCallbackOnElement(element: *Element, old_document: *Document, new_document: *Document, frame: *Frame) void {
if (element.is(Custom)) |custom| {
custom.invokeAttributeChangedCallback(name, old_value, new_value, namespace, frame);
return;
if (custom._definition == null) return;
} else {
if (frame.getCustomizedBuiltInDefinition(element) == null) return;
}
frame._ce_reactions.enqueueAdopted(frame, element, old_document, new_document) catch |err| {
log.warn(.bug, "ce_reactions enqueue fail", .{ .err = err });
};
}
// Customized built-in element - check if attribute is observed
const definition = frame.getCustomizedBuiltInDefinition(element) orelse return;
if (!definition.isAttributeObserved(name)) return;
invokeCallbackOnElement(element, definition, "attributeChangedCallback", .{ name, old_value, new_value, namespace }, frame);
pub fn enqueueAttributeChangedCallbackOnElement(element: *Element, name: String, old_value: ?String, new_value: ?String, namespace: ?String, frame: *Frame) void {
if (element.is(Custom)) |custom| {
const definition = custom._definition orelse return;
if (!definition.isAttributeObserved(name)) return;
} else {
const definition = frame.getCustomizedBuiltInDefinition(element) orelse return;
if (!definition.isAttributeObserved(name)) return;
}
frame._ce_reactions.enqueueAttributeChanged(frame, element, name, old_value, new_value, namespace) catch |err| {
log.warn(.bug, "ce_reactions enqueue fail", .{ .err = err });
};
}
// Called by CustomElementReactions.popAndInvoke for each queued reaction.
// Filtering already happened at enqueue time, so just fire unconditionally.
pub fn fireReaction(reaction: Reaction, frame: *Frame) void {
switch (reaction) {
.connected => |el| {
if (el.is(Custom)) |custom| {
custom.invokeCallback("connectedCallback", .{}, frame);
} else if (frame.getCustomizedBuiltInDefinition(el)) |definition| {
invokeCallbackOnElement(el, definition, "connectedCallback", .{}, frame);
}
},
.disconnected => |el| {
if (el.is(Custom)) |custom| {
custom.invokeCallback("disconnectedCallback", .{}, frame);
} else if (frame.getCustomizedBuiltInDefinition(el)) |definition| {
invokeCallbackOnElement(el, definition, "disconnectedCallback", .{}, frame);
}
},
.adopted => |a| {
if (a.element.is(Custom)) |custom| {
custom.invokeCallback("adoptedCallback", .{ a.old_document, a.new_document }, frame);
} else if (frame.getCustomizedBuiltInDefinition(a.element)) |definition| {
invokeCallbackOnElement(a.element, definition, "adoptedCallback", .{ a.old_document, a.new_document }, frame);
}
},
.attribute_changed => |a| {
if (a.element.is(Custom)) |custom| {
custom.invokeCallback("attributeChangedCallback", .{ a.name, a.old_value, a.new_value, a.namespace }, frame);
} else if (frame.getCustomizedBuiltInDefinition(a.element)) |definition| {
invokeCallbackOnElement(a.element, definition, "attributeChangedCallback", .{ a.name, a.old_value, a.new_value, a.namespace }, frame);
}
},
}
}
fn invokeCallbackOnElement(element: *Element, definition: *CustomElementDefinition, comptime callback_name: [:0]const u8, args: anytype, frame: *Frame) void {

View File

@@ -52,5 +52,5 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const value = bridge.accessor(Data.getValue, Data.setValue, .{});
pub const value = bridge.accessor(Data.getValue, Data.setValue, .{ .ce_reactions = true });
};

View File

@@ -48,8 +48,8 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const open = bridge.accessor(Details.getOpen, Details.setOpen, .{});
pub const name = bridge.accessor(Details.getName, Details.setName, .{});
pub const open = bridge.accessor(Details.getOpen, Details.setOpen, .{ .ce_reactions = true });
pub const name = bridge.accessor(Details.getName, Details.setName, .{ .ce_reactions = true });
};
const testing = @import("../../../../testing.zig");

View File

@@ -80,7 +80,7 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const open = bridge.accessor(Dialog.getOpen, Dialog.setOpen, .{});
pub const open = bridge.accessor(Dialog.getOpen, Dialog.setOpen, .{ .ce_reactions = true });
pub const returnValue = bridge.accessor(Dialog.getReturnValue, Dialog.setReturnValue, .{});
pub const show = bridge.function(Dialog.show, .{});

View File

@@ -44,8 +44,8 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const disabled = bridge.accessor(FieldSet.getDisabled, FieldSet.setDisabled, .{});
pub const name = bridge.accessor(FieldSet.getName, FieldSet.setName, .{});
pub const disabled = bridge.accessor(FieldSet.getDisabled, FieldSet.setDisabled, .{ .ce_reactions = true });
pub const name = bridge.accessor(FieldSet.getName, FieldSet.setName, .{ .ce_reactions = true });
};
const testing = @import("../../../../testing.zig");

View File

@@ -33,6 +33,14 @@ pub const TextArea = @import("TextArea.zig");
const Form = @This();
_proto: *HtmlElement,
// Prevents submission of the form while we're in the process of submitting
// the form. You can imagine an onsubmit = () => form.submit() endless loop.
_firing_submission_events: bool = false,
// Prevents submission of the form while we're building the entry list for the
// form. You can imagine an formdata = () => form.submit() endless loop.
_constructing_entry_list: bool = false,
pub fn asHtmlElement(self: *Form) *HtmlElement {
return self._proto;
}
@@ -227,12 +235,12 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const name = bridge.accessor(Form.getName, Form.setName, .{});
pub const method = bridge.accessor(Form.getMethod, Form.setMethod, .{});
pub const action = bridge.accessor(Form.getAction, Form.setAction, .{});
pub const target = bridge.accessor(Form.getTarget, Form.setTarget, .{});
pub const acceptCharset = bridge.accessor(Form.getAcceptCharset, Form.setAcceptCharset, .{});
pub const enctype = bridge.accessor(Form.getEnctype, Form.setEnctype, .{});
pub const name = bridge.accessor(Form.getName, Form.setName, .{ .ce_reactions = true });
pub const method = bridge.accessor(Form.getMethod, Form.setMethod, .{ .ce_reactions = true });
pub const action = bridge.accessor(Form.getAction, Form.setAction, .{ .ce_reactions = true });
pub const target = bridge.accessor(Form.getTarget, Form.setTarget, .{ .ce_reactions = true });
pub const acceptCharset = bridge.accessor(Form.getAcceptCharset, Form.setAcceptCharset, .{ .ce_reactions = true });
pub const enctype = bridge.accessor(Form.getEnctype, Form.setEnctype, .{ .ce_reactions = true });
pub const elements = bridge.accessor(Form.getElements, null, .{});
pub const length = bridge.accessor(Form.getLength, null, .{});
pub const submit = bridge.function(Form.submit, .{});

View File

@@ -81,8 +81,8 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const src = bridge.accessor(IFrame.getSrc, IFrame.setSrc, .{});
pub const name = bridge.accessor(IFrame.getName, IFrame.setName, .{});
pub const src = bridge.accessor(IFrame.getSrc, IFrame.setSrc, .{ .ce_reactions = true });
pub const name = bridge.accessor(IFrame.getName, IFrame.setName, .{ .ce_reactions = true });
pub const contentWindow = bridge.accessor(IFrame.getContentWindow, null, .{});
pub const contentDocument = bridge.accessor(IFrame.getContentDocument, null, .{});
};

View File

@@ -142,13 +142,13 @@ pub const JsApi = struct {
};
pub const constructor = bridge.constructor(Image.constructor, .{});
pub const src = bridge.accessor(Image.getSrc, Image.setSrc, .{});
pub const src = bridge.accessor(Image.getSrc, Image.setSrc, .{ .ce_reactions = true });
pub const currentSrc = bridge.accessor(Image.getSrc, null, .{});
pub const alt = bridge.accessor(Image.getAlt, Image.setAlt, .{});
pub const width = bridge.accessor(Image.getWidth, Image.setWidth, .{});
pub const height = bridge.accessor(Image.getHeight, Image.setHeight, .{});
pub const crossOrigin = bridge.accessor(Image.getCrossOrigin, Image.setCrossOrigin, .{});
pub const loading = bridge.accessor(Image.getLoading, Image.setLoading, .{});
pub const alt = bridge.accessor(Image.getAlt, Image.setAlt, .{ .ce_reactions = true });
pub const width = bridge.accessor(Image.getWidth, Image.setWidth, .{ .ce_reactions = true });
pub const height = bridge.accessor(Image.getHeight, Image.setHeight, .{ .ce_reactions = true });
pub const crossOrigin = bridge.accessor(Image.getCrossOrigin, Image.setCrossOrigin, .{ .ce_reactions = true });
pub const loading = bridge.accessor(Image.getLoading, Image.setLoading, .{ .ce_reactions = true });
pub const naturalWidth = bridge.accessor(Image.getNaturalWidth, null, .{});
pub const naturalHeight = bridge.accessor(Image.getNaturalHeight, null, .{});
pub const complete = bridge.accessor(Image.getComplete, null, .{});

View File

@@ -1310,21 +1310,21 @@ pub const JsApi = struct {
}
pub const onselectionchange = bridge.accessor(Input.getOnSelectionChange, Input.setOnSelectionChange, .{});
pub const @"type" = bridge.accessor(Input.getType, Input.setType, .{});
pub const @"type" = bridge.accessor(Input.getType, Input.setType, .{ .ce_reactions = true });
pub const value = bridge.accessor(Input.getValue, setValueFromJS, .{ .dom_exception = true });
pub const defaultValue = bridge.accessor(Input.getDefaultValue, Input.setDefaultValue, .{});
pub const defaultValue = bridge.accessor(Input.getDefaultValue, Input.setDefaultValue, .{ .ce_reactions = true });
pub const checked = bridge.accessor(Input.getChecked, Input.setChecked, .{});
pub const defaultChecked = bridge.accessor(Input.getDefaultChecked, Input.setDefaultChecked, .{});
pub const disabled = bridge.accessor(Input.getDisabled, Input.setDisabled, .{});
pub const name = bridge.accessor(Input.getName, Input.setName, .{});
pub const required = bridge.accessor(Input.getRequired, Input.setRequired, .{});
pub const accept = bridge.accessor(Input.getAccept, Input.setAccept, .{});
pub const readOnly = bridge.accessor(Input.getReadonly, Input.setReadonly, .{});
pub const alt = bridge.accessor(Input.getAlt, Input.setAlt, .{});
pub const maxLength = bridge.accessor(Input.getMaxLength, Input.setMaxLength, .{ .dom_exception = true });
pub const minLength = bridge.accessor(Input.getMinLength, Input.setMinLength, .{ .dom_exception = true });
pub const size = bridge.accessor(Input.getSize, Input.setSize, .{});
pub const src = bridge.accessor(Input.getSrc, Input.setSrc, .{});
pub const defaultChecked = bridge.accessor(Input.getDefaultChecked, Input.setDefaultChecked, .{ .ce_reactions = true });
pub const disabled = bridge.accessor(Input.getDisabled, Input.setDisabled, .{ .ce_reactions = true });
pub const name = bridge.accessor(Input.getName, Input.setName, .{ .ce_reactions = true });
pub const required = bridge.accessor(Input.getRequired, Input.setRequired, .{ .ce_reactions = true });
pub const accept = bridge.accessor(Input.getAccept, Input.setAccept, .{ .ce_reactions = true });
pub const readOnly = bridge.accessor(Input.getReadonly, Input.setReadonly, .{ .ce_reactions = true });
pub const alt = bridge.accessor(Input.getAlt, Input.setAlt, .{ .ce_reactions = true });
pub const maxLength = bridge.accessor(Input.getMaxLength, Input.setMaxLength, .{ .dom_exception = true, .ce_reactions = true });
pub const minLength = bridge.accessor(Input.getMinLength, Input.setMinLength, .{ .dom_exception = true, .ce_reactions = true });
pub const size = bridge.accessor(Input.getSize, Input.setSize, .{ .ce_reactions = true });
pub const src = bridge.accessor(Input.getSrc, Input.setSrc, .{ .ce_reactions = true });
pub const form = bridge.accessor(Input.getForm, null, .{});
pub const formAction = bridge.accessor(Input.getFormAction, Input.setFormAction, .{});
pub const formEnctype = bridge.accessor(Input.getFormEnctype, Input.setFormEnctype, .{});
@@ -1333,13 +1333,13 @@ pub const JsApi = struct {
pub const formTarget = bridge.accessor(Input.getFormTarget, Input.setFormTarget, .{});
pub const labels = bridge.accessor(Input.getLabels, null, .{});
pub const indeterminate = bridge.accessor(Input.getIndeterminate, Input.setIndeterminate, .{});
pub const placeholder = bridge.accessor(Input.getPlaceholder, Input.setPlaceholder, .{});
pub const pattern = bridge.accessor(Input.getPattern, Input.setPattern, .{});
pub const min = bridge.accessor(Input.getMin, Input.setMin, .{});
pub const max = bridge.accessor(Input.getMax, Input.setMax, .{});
pub const step = bridge.accessor(Input.getStep, Input.setStep, .{});
pub const multiple = bridge.accessor(Input.getMultiple, Input.setMultiple, .{});
pub const autocomplete = bridge.accessor(Input.getAutocomplete, Input.setAutocomplete, .{});
pub const placeholder = bridge.accessor(Input.getPlaceholder, Input.setPlaceholder, .{ .ce_reactions = true });
pub const pattern = bridge.accessor(Input.getPattern, Input.setPattern, .{ .ce_reactions = true });
pub const min = bridge.accessor(Input.getMin, Input.setMin, .{ .ce_reactions = true });
pub const max = bridge.accessor(Input.getMax, Input.setMax, .{ .ce_reactions = true });
pub const step = bridge.accessor(Input.getStep, Input.setStep, .{ .ce_reactions = true });
pub const multiple = bridge.accessor(Input.getMultiple, Input.setMultiple, .{ .ce_reactions = true });
pub const autocomplete = bridge.accessor(Input.getAutocomplete, Input.setAutocomplete, .{ .ce_reactions = true });
pub const willValidate = bridge.accessor(Input.getWillValidate, null, .{});
pub const validity = bridge.accessor(Input.getValidity, null, .{});
pub const validationMessage = bridge.accessor(Input.getValidationMessage, null, .{});

View File

@@ -52,7 +52,7 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const value = bridge.accessor(LI.getValue, LI.setValue, .{});
pub const value = bridge.accessor(LI.getValue, LI.setValue, .{ .ce_reactions = true });
};
const testing = @import("../../../../testing.zig");

View File

@@ -138,7 +138,7 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const htmlFor = bridge.accessor(Label.getHtmlFor, Label.setHtmlFor, .{});
pub const htmlFor = bridge.accessor(Label.getHtmlFor, Label.setHtmlFor, .{ .ce_reactions = true });
pub const control = bridge.accessor(Label.getControl, null, .{});
};

View File

@@ -140,11 +140,11 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const as = bridge.accessor(Link.getAs, Link.setAs, .{});
pub const rel = bridge.accessor(Link.getRel, Link.setRel, .{});
pub const media = bridge.accessor(Link.getMedia, Link.setMedia, .{});
pub const href = bridge.accessor(Link.getHref, Link.setHref, .{});
pub const crossOrigin = bridge.accessor(Link.getCrossOrigin, Link.setCrossOrigin, .{});
pub const as = bridge.accessor(Link.getAs, Link.setAs, .{ .ce_reactions = true });
pub const rel = bridge.accessor(Link.getRel, Link.setRel, .{ .ce_reactions = true });
pub const media = bridge.accessor(Link.getMedia, Link.setMedia, .{ .ce_reactions = true });
pub const href = bridge.accessor(Link.getHref, Link.setHref, .{ .ce_reactions = true });
pub const crossOrigin = bridge.accessor(Link.getCrossOrigin, Link.setCrossOrigin, .{ .ce_reactions = true });
pub const relList = bridge.accessor(_getRelList, null, .{ .null_as_undefined = true });
fn _getRelList(self: *Link, frame: *Frame) !?*@import("../../collections.zig").DOMTokenList {

View File

@@ -307,13 +307,13 @@ pub const JsApi = struct {
pub const HAVE_FUTURE_DATA = bridge.property(@intFromEnum(ReadyState.HAVE_FUTURE_DATA), .{ .template = true });
pub const HAVE_ENOUGH_DATA = bridge.property(@intFromEnum(ReadyState.HAVE_ENOUGH_DATA), .{ .template = true });
pub const src = bridge.accessor(Media.getSrc, Media.setSrc, .{});
pub const src = bridge.accessor(Media.getSrc, Media.setSrc, .{ .ce_reactions = true });
pub const currentSrc = bridge.accessor(Media.getSrc, null, .{});
pub const autoplay = bridge.accessor(Media.getAutoplay, Media.setAutoplay, .{});
pub const controls = bridge.accessor(Media.getControls, Media.setControls, .{});
pub const loop = bridge.accessor(Media.getLoop, Media.setLoop, .{});
pub const autoplay = bridge.accessor(Media.getAutoplay, Media.setAutoplay, .{ .ce_reactions = true });
pub const controls = bridge.accessor(Media.getControls, Media.setControls, .{ .ce_reactions = true });
pub const loop = bridge.accessor(Media.getLoop, Media.setLoop, .{ .ce_reactions = true });
pub const muted = bridge.accessor(Media.getMuted, Media.setMuted, .{});
pub const preload = bridge.accessor(Media.getPreload, Media.setPreload, .{});
pub const preload = bridge.accessor(Media.getPreload, Media.setPreload, .{ .ce_reactions = true });
pub const volume = bridge.accessor(Media.getVolume, Media.setVolume, .{});
pub const playbackRate = bridge.accessor(Media.getPlaybackRate, Media.setPlaybackRate, .{});
pub const currentTime = bridge.accessor(Media.getCurrentTime, Media.setCurrentTime, .{});

View File

@@ -77,8 +77,8 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const name = bridge.accessor(MetaElement.getName, MetaElement.setName, .{});
pub const httpEquiv = bridge.accessor(MetaElement.getHttpEquiv, MetaElement.setHttpEquiv, .{});
pub const content = bridge.accessor(MetaElement.getContent, MetaElement.setContent, .{});
pub const media = bridge.accessor(MetaElement.getMedia, MetaElement.setMedia, .{});
pub const name = bridge.accessor(MetaElement.getName, MetaElement.setName, .{ .ce_reactions = true });
pub const httpEquiv = bridge.accessor(MetaElement.getHttpEquiv, MetaElement.setHttpEquiv, .{ .ce_reactions = true });
pub const content = bridge.accessor(MetaElement.getContent, MetaElement.setContent, .{ .ce_reactions = true });
pub const media = bridge.accessor(MetaElement.getMedia, MetaElement.setMedia, .{ .ce_reactions = true });
};

View File

@@ -72,9 +72,9 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const start = bridge.accessor(OL.getStart, OL.setStart, .{});
pub const reversed = bridge.accessor(OL.getReversed, OL.setReversed, .{});
pub const @"type" = bridge.accessor(OL.getType, OL.setType, .{});
pub const start = bridge.accessor(OL.getStart, OL.setStart, .{ .ce_reactions = true });
pub const reversed = bridge.accessor(OL.getReversed, OL.setReversed, .{ .ce_reactions = true });
pub const @"type" = bridge.accessor(OL.getType, OL.setType, .{ .ce_reactions = true });
};
const testing = @import("../../../../testing.zig");

View File

@@ -44,8 +44,8 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const disabled = bridge.accessor(OptGroup.getDisabled, OptGroup.setDisabled, .{});
pub const label = bridge.accessor(OptGroup.getLabel, OptGroup.setLabel, .{});
pub const disabled = bridge.accessor(OptGroup.getDisabled, OptGroup.setDisabled, .{ .ce_reactions = true });
pub const label = bridge.accessor(OptGroup.getLabel, OptGroup.setLabel, .{ .ce_reactions = true });
};
const testing = @import("../../../../testing.zig");

View File

@@ -117,12 +117,12 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const value = bridge.accessor(Option.getValue, Option.setValue, .{});
pub const text = bridge.accessor(Option.getText, Option.setText, .{});
pub const value = bridge.accessor(Option.getValue, Option.setValue, .{ .ce_reactions = true });
pub const text = bridge.accessor(Option.getText, Option.setText, .{ .ce_reactions = true });
pub const selected = bridge.accessor(Option.getSelected, Option.setSelected, .{});
pub const defaultSelected = bridge.accessor(Option.getDefaultSelected, null, .{});
pub const disabled = bridge.accessor(Option.getDisabled, Option.setDisabled, .{});
pub const name = bridge.accessor(Option.getName, Option.setName, .{});
pub const disabled = bridge.accessor(Option.getDisabled, Option.setDisabled, .{ .ce_reactions = true });
pub const name = bridge.accessor(Option.getName, Option.setName, .{ .ce_reactions = true });
};
pub const Build = struct {

View File

@@ -41,7 +41,7 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const cite = bridge.accessor(Quote.getCite, Quote.setCite, .{});
pub const cite = bridge.accessor(Quote.getCite, Quote.setCite, .{ .ce_reactions = true });
};
const testing = @import("../../../../testing.zig");

View File

@@ -124,20 +124,20 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const src = bridge.accessor(Script.getSrc, Script.setSrc, .{});
pub const @"defer" = bridge.accessor(Script.getDefer, Script.setDefer, .{});
pub const async = bridge.accessor(Script.getAsync, Script.setAsync, .{});
pub const @"type" = bridge.accessor(Script.getType, Script.setType, .{});
pub const nonce = bridge.accessor(Script.getNonce, Script.setNonce, .{});
pub const charset = bridge.accessor(Script.getCharset, Script.setCharset, .{});
pub const src = bridge.accessor(Script.getSrc, Script.setSrc, .{ .ce_reactions = true });
pub const @"defer" = bridge.accessor(Script.getDefer, Script.setDefer, .{ .ce_reactions = true });
pub const async = bridge.accessor(Script.getAsync, Script.setAsync, .{ .ce_reactions = true });
pub const @"type" = bridge.accessor(Script.getType, Script.setType, .{ .ce_reactions = true });
pub const nonce = bridge.accessor(Script.getNonce, Script.setNonce, .{ .ce_reactions = true });
pub const charset = bridge.accessor(Script.getCharset, Script.setCharset, .{ .ce_reactions = true });
pub const noModule = bridge.accessor(Script.getNoModule, null, .{});
pub const innerText = bridge.accessor(_innerText, Script.setInnerText, .{});
pub const innerText = bridge.accessor(_innerText, Script.setInnerText, .{ .ce_reactions = true });
fn _innerText(self: *Script, frame: *const Frame) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
try self.asNode().getTextContent(&buf.writer);
return buf.written();
}
pub const text = bridge.accessor(_text, Script.setInnerText, .{});
pub const text = bridge.accessor(_text, Script.setInnerText, .{ .ce_reactions = true });
fn _text(self: *Script, frame: *const Frame) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(frame.call_arena);
try self.asNode().getChildTextContent(&buf.writer);

View File

@@ -319,14 +319,14 @@ pub const JsApi = struct {
pub const value = bridge.accessor(Select.getValue, Select.setValue, .{});
pub const selectedIndex = bridge.accessor(Select.getSelectedIndex, Select.setSelectedIndex, .{});
pub const multiple = bridge.accessor(Select.getMultiple, Select.setMultiple, .{});
pub const disabled = bridge.accessor(Select.getDisabled, Select.setDisabled, .{});
pub const name = bridge.accessor(Select.getName, Select.setName, .{});
pub const required = bridge.accessor(Select.getRequired, Select.setRequired, .{});
pub const multiple = bridge.accessor(Select.getMultiple, Select.setMultiple, .{ .ce_reactions = true });
pub const disabled = bridge.accessor(Select.getDisabled, Select.setDisabled, .{ .ce_reactions = true });
pub const name = bridge.accessor(Select.getName, Select.setName, .{ .ce_reactions = true });
pub const required = bridge.accessor(Select.getRequired, Select.setRequired, .{ .ce_reactions = true });
pub const options = bridge.accessor(Select.getOptions, null, .{});
pub const selectedOptions = bridge.accessor(Select.getSelectedOptions, null, .{});
pub const form = bridge.accessor(Select.getForm, null, .{});
pub const size = bridge.accessor(Select.getSize, Select.setSize, .{});
pub const size = bridge.accessor(Select.getSize, Select.setSize, .{ .ce_reactions = true });
pub const length = bridge.accessor(Select.getLength, null, .{});
pub const labels = bridge.accessor(Select.getLabels, null, .{});
pub const willValidate = bridge.accessor(Select.getWillValidate, null, .{});

View File

@@ -155,7 +155,7 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const name = bridge.accessor(Slot.getName, Slot.setName, .{});
pub const name = bridge.accessor(Slot.getName, Slot.setName, .{ .ce_reactions = true });
pub const assignedNodes = bridge.function(Slot.assignedNodes, .{});
pub const assignedElements = bridge.function(Slot.assignedElements, .{});
pub const assign = bridge.function(Slot.assign, .{});

View File

@@ -125,10 +125,10 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const blocking = bridge.accessor(Style.getBlocking, Style.setBlocking, .{});
pub const media = bridge.accessor(Style.getMedia, Style.setMedia, .{});
pub const @"type" = bridge.accessor(Style.getType, Style.setType, .{});
pub const disabled = bridge.accessor(Style.getDisabled, Style.setDisabled, .{});
pub const blocking = bridge.accessor(Style.getBlocking, Style.setBlocking, .{ .ce_reactions = true });
pub const media = bridge.accessor(Style.getMedia, Style.setMedia, .{ .ce_reactions = true });
pub const @"type" = bridge.accessor(Style.getType, Style.setType, .{ .ce_reactions = true });
pub const disabled = bridge.accessor(Style.getDisabled, Style.setDisabled, .{ .ce_reactions = true });
pub const sheet = bridge.accessor(Style.getSheet, null, .{});
};

View File

@@ -54,8 +54,8 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const colSpan = bridge.accessor(TableCell.getColSpan, TableCell.setColSpan, .{});
pub const rowSpan = bridge.accessor(TableCell.getRowSpan, TableCell.setRowSpan, .{});
pub const colSpan = bridge.accessor(TableCell.getColSpan, TableCell.setColSpan, .{ .ce_reactions = true });
pub const rowSpan = bridge.accessor(TableCell.getRowSpan, TableCell.setRowSpan, .{ .ce_reactions = true });
};
const testing = @import("../../../../testing.zig");

View File

@@ -48,7 +48,7 @@ pub const JsApi = struct {
};
pub const content = bridge.accessor(Template.getContent, null, .{});
pub const innerHTML = bridge.accessor(_getInnerHTML, Template.setInnerHTML, .{});
pub const innerHTML = bridge.accessor(_getInnerHTML, Template.setInnerHTML, .{ .ce_reactions = true });
pub const outerHTML = bridge.accessor(_getOuterHTML, null, .{});
fn _getInnerHTML(self: *Template, frame: *Frame) ![]const u8 {

View File

@@ -405,12 +405,12 @@ pub const JsApi = struct {
pub const setCustomValidity = bridge.function(TextArea.setCustomValidity, .{});
pub const onselectionchange = bridge.accessor(TextArea.getOnSelectionChange, TextArea.setOnSelectionChange, .{});
pub const value = bridge.accessor(TextArea.getValue, TextArea.setValue, .{});
pub const defaultValue = bridge.accessor(TextArea.getDefaultValue, TextArea.setDefaultValue, .{});
pub const disabled = bridge.accessor(TextArea.getDisabled, TextArea.setDisabled, .{});
pub const name = bridge.accessor(TextArea.getName, TextArea.setName, .{});
pub const required = bridge.accessor(TextArea.getRequired, TextArea.setRequired, .{});
pub const maxLength = bridge.accessor(TextArea.getMaxLength, TextArea.setMaxLength, .{ .dom_exception = true });
pub const minLength = bridge.accessor(TextArea.getMinLength, TextArea.setMinLength, .{ .dom_exception = true });
pub const defaultValue = bridge.accessor(TextArea.getDefaultValue, TextArea.setDefaultValue, .{ .ce_reactions = true });
pub const disabled = bridge.accessor(TextArea.getDisabled, TextArea.setDisabled, .{ .ce_reactions = true });
pub const name = bridge.accessor(TextArea.getName, TextArea.setName, .{ .ce_reactions = true });
pub const required = bridge.accessor(TextArea.getRequired, TextArea.setRequired, .{ .ce_reactions = true });
pub const maxLength = bridge.accessor(TextArea.getMaxLength, TextArea.setMaxLength, .{ .dom_exception = true, .ce_reactions = true });
pub const minLength = bridge.accessor(TextArea.getMinLength, TextArea.setMinLength, .{ .dom_exception = true, .ce_reactions = true });
pub const form = bridge.accessor(TextArea.getForm, null, .{});
pub const select = bridge.function(TextArea.select, .{});

View File

@@ -32,7 +32,7 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const dateTime = bridge.accessor(Time.getDateTime, Time.setDateTime, .{});
pub const dateTime = bridge.accessor(Time.getDateTime, Time.setDateTime, .{ .ce_reactions = true });
};
const testing = @import("../../../../testing.zig");

View File

@@ -83,7 +83,7 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const kind = bridge.accessor(Track.getKind, Track.setKind, .{});
pub const kind = bridge.accessor(Track.getKind, Track.setKind, .{ .ce_reactions = true });
pub const NONE = bridge.property(@as(u16, @intFromEnum(ReadyState.none)), .{ .template = true });
pub const LOADING = bridge.property(@as(u16, @intFromEnum(ReadyState.loading)), .{ .template = true });

View File

@@ -73,7 +73,7 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const poster = bridge.accessor(Video.getPoster, Video.setPoster, .{});
pub const poster = bridge.accessor(Video.getPoster, Video.setPoster, .{ .ce_reactions = true });
pub const videoWidth = bridge.accessor(Video.getVideoWidth, null, .{});
pub const videoHeight = bridge.accessor(Video.getVideoHeight, null, .{});
};

View File

@@ -19,7 +19,7 @@
const std = @import("std");
const lp = @import("lightpanda");
const Frame = @import("../../Frame.zig");
const Page = @import("../../Page.zig");
const Event = @import("../Event.zig");
const String = lp.String;
@@ -39,23 +39,23 @@ const CloseEventOptions = struct {
const Options = Event.inheritOptions(CloseEvent, CloseEventOptions);
pub fn init(typ: []const u8, _opts: ?Options, frame: *Frame) !*CloseEvent {
const arena = try frame.getArena(.tiny, "CloseEvent");
errdefer frame.releaseArena(arena);
pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*CloseEvent {
const arena = try page.getArena(.tiny, "CloseEvent");
errdefer page.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
return initWithTrusted(arena, type_string, _opts, false, frame);
return initWithTrusted(arena, type_string, _opts, false, page);
}
pub fn initTrusted(typ: String, _opts: ?Options, frame: *Frame) !*CloseEvent {
const arena = try frame.getArena(.tiny, "CloseEvent.trusted");
errdefer frame.releaseArena(arena);
return initWithTrusted(arena, typ, _opts, true, frame);
pub fn initTrusted(typ: String, _opts: ?Options, page: *Page) !*CloseEvent {
const arena = try page.getArena(.tiny, "CloseEvent.trusted");
errdefer page.releaseArena(arena);
return initWithTrusted(arena, typ, _opts, true, page);
}
fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool, frame: *Frame) !*CloseEvent {
fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool, page: *Page) !*CloseEvent {
const opts = _opts orelse Options{};
const event = try frame._factory.event(
const event = try page.factory.event(
arena,
typ,
CloseEvent{

View File

@@ -48,9 +48,15 @@ pub const Input = Request.Input;
pub const InitOpts = Request.InitOpts;
pub fn init(input: Input, options: ?InitOpts, exec: *const Execution) !js.Promise {
const request = try Request.init(input, options, exec);
const resolver = exec.context.local.?.createPromiseResolver();
// A bad RequestInit (e.g. an invalid priority) must reject the promise,
// not throw synchronously.
const request = Request.init(input, options, exec) catch {
resolver.rejectError("fetch init error", .{ .type_error = "Failed to construct Request" });
return resolver.promise();
};
if (request._signal) |signal| {
if (signal._aborted) {
resolver.reject("fetch aborted", DOMException.init("The operation was aborted.", "AbortError"));

View File

@@ -73,6 +73,14 @@ pub fn init(form_: ?*Form, submitter: ?*Element, exec: *const Execution) !*FormD
.worker => lp.assert(false, "FormData worker form", .{}),
};
if (form._constructing_entry_list) {
// see the `_constructing_entry_list` field documentation
return error.InvalidStateError;
}
form._constructing_entry_list = true;
defer form._constructing_entry_list = false;
const form_data = try exec._factory.create(FormData{
._arena = exec.arena,
._entries = try collectForm(frame.arena, form, submitter, frame),
@@ -397,7 +405,7 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const constructor = bridge.constructor(FormData.init, .{});
pub const constructor = bridge.constructor(FormData.init, .{ .dom_exception = true });
pub const has = bridge.function(FormData.has, .{});
pub const get = bridge.function(FormData.get, .{});
pub const set = bridge.function(FormData.set, .{});

View File

@@ -26,7 +26,8 @@ const Blob = @import("../Blob.zig");
const AbortSignal = @import("../AbortSignal.zig");
const Headers = @import("Headers.zig");
const BodyInit = @import("body_init.zig").BodyInit;
const body_init = @import("body_init.zig");
const BodyInit = body_init.BodyInit;
const Execution = js.Execution;
const Allocator = std.mem.Allocator;
@@ -41,6 +42,7 @@ _arena: Allocator,
_cache: Cache,
_credentials: Credentials,
_signal: ?*AbortSignal,
_body_used: bool = false,
pub const Input = union(enum) {
request: *Request,
@@ -54,8 +56,11 @@ pub const InitOpts = struct {
cache: Cache = .default,
credentials: Credentials = .@"same-origin",
signal: ?*AbortSignal = null,
priority: ?[]const u8 = null,
};
const Priority = enum { high, low, auto };
const Credentials = enum {
omit,
include,
@@ -81,6 +86,12 @@ pub fn init(input: Input, opts_: ?InitOpts, exec: *const Execution) !*Request {
};
const opts = opts_ orelse InitOpts{};
if (opts.priority) |p| {
if (std.meta.stringToEnum(Priority, p) == null) {
return error.InvalidArgument;
}
}
const method = if (opts.method) |m|
try parseMethod(m, exec)
else switch (input) {
@@ -182,36 +193,74 @@ pub fn getHeaders(self: *Request, exec: *const Execution) !*Headers {
return headers;
}
pub fn getBodyUsed(self: *const Request) bool {
if (self._body == null) {
return false;
}
return self._body_used;
}
// Marks a present body consumed; returns a rejected promise if it already was.
fn consume(self: *Request, local: *const js.Local) ?js.Promise {
if (self._body == null) {
return null;
}
if (self._body_used) {
return local.rejectPromise(.{ .type_error = "Body has already been read" });
}
self._body_used = true;
return null;
}
pub fn blob(self: *Request, exec: *const Execution) !js.Promise {
const local = exec.context.local.?;
if (self.consume(local)) |rejected| {
return rejected;
}
const body = self._body orelse "";
const headers = try self.getHeaders(exec);
const content_type = try headers.get("content-type", exec) orelse "";
const b = try Blob.initFromBytes(body, content_type, true, exec.context.page);
return exec.context.local.?.resolvePromise(b);
return local.resolvePromise(b);
}
pub fn text(self: *const Request, exec: *const Execution) !js.Promise {
const body = self._body orelse "";
return exec.context.local.?.resolvePromise(body);
}
pub fn json(self: *const Request, exec: *const Execution) !js.Promise {
const body = self._body orelse "";
pub fn text(self: *Request, exec: *const Execution) !js.Promise {
const local = exec.context.local.?;
const value = local.parseJSON(body) catch {
if (self.consume(local)) |rejected| {
return rejected;
}
return local.resolvePromise(body_init.stripUtf8Bom(self._body orelse ""));
}
pub fn json(self: *Request, exec: *const Execution) !js.Promise {
const local = exec.context.local.?;
if (self.consume(local)) |rejected| {
return rejected;
}
const value = local.parseJSON(body_init.stripUtf8Bom(self._body orelse "")) catch {
return local.rejectPromise(.{ .syntax_error = "failed to parse" });
};
return local.resolvePromise(try value.persist());
}
pub fn arrayBuffer(self: *const Request, exec: *const Execution) !js.Promise {
return exec.context.local.?.resolvePromise(js.ArrayBuffer{ .values = self._body orelse "" });
pub fn arrayBuffer(self: *Request, exec: *const Execution) !js.Promise {
const local = exec.context.local.?;
if (self.consume(local)) |rejected| {
return rejected;
}
return local.resolvePromise(js.ArrayBuffer{ .values = self._body orelse "" });
}
pub fn bytes(self: *const Request, exec: *const Execution) !js.Promise {
return exec.context.local.?.resolvePromise(js.TypedArray(u8){ .values = self._body orelse "" });
pub fn bytes(self: *Request, exec: *const Execution) !js.Promise {
const local = exec.context.local.?;
if (self.consume(local)) |rejected| {
return rejected;
}
return local.resolvePromise(js.TypedArray(u8){ .values = self._body orelse "" });
}
pub fn clone(self: *const Request, exec: *const Execution) !*Request {
@@ -243,6 +292,7 @@ pub const JsApi = struct {
pub const cache = bridge.accessor(Request.getCache, null, .{});
pub const credentials = bridge.accessor(Request.getCredentials, null, .{});
pub const signal = bridge.accessor(Request.getSignal, null, .{});
pub const bodyUsed = bridge.accessor(Request.getBodyUsed, null, .{});
pub const blob = bridge.function(Request.blob, .{});
pub const text = bridge.function(Request.text, .{});
pub const json = bridge.function(Request.json, .{});

View File

@@ -27,6 +27,7 @@ const Blob = @import("../Blob.zig");
const ReadableStream = @import("../streams/ReadableStream.zig");
const Headers = @import("Headers.zig");
const body_init = @import("body_init.zig");
const Execution = js.Execution;
const Allocator = std.mem.Allocator;
@@ -36,6 +37,7 @@ const Response = @This();
pub const Type = enum {
basic,
cors,
default,
@"error",
@"opaque",
opaqueredirect,
@@ -51,6 +53,7 @@ _status_text: []const u8,
_url: [:0]const u8,
_is_redirected: bool,
_http_response: ?HttpClient.Response = null,
_body_used: bool = false,
const Body = union(enum) {
empty,
@@ -64,12 +67,7 @@ const InitOpts = struct {
statusText: ?[]const u8 = null,
};
/// Body can be: null, string ([]const u8), ReadableStream, Blob, ArrayBuffer
pub const BodyInit = union(enum) {
stream: *ReadableStream,
bytes: []const u8,
js_val: js.Value,
};
pub const BodyInit = body_init.BodyInit;
pub fn init(body_: ?BodyInit, opts_: ?InitOpts, exec: *const Execution) !*Response {
const session = exec.context.page.session;
@@ -79,22 +77,26 @@ pub fn init(body_: ?BodyInit, opts_: ?InitOpts, exec: *const Execution) !*Respon
const opts = opts_ orelse InitOpts{};
const status_text = if (opts.statusText) |st| try arena.dupe(u8, st) else "";
// Parse body from the union
var content_type: ?[]const u8 = null;
const body: Body = blk: {
const b = body_ orelse break :blk .empty;
switch (b) {
.bytes => |body_bytes| break :blk .{ .bytes = try arena.dupe(u8, body_bytes) },
.stream => |stream| break :blk .{ .stream = stream },
.js_val => |js_val| {
if (js_val.isNullOrUndefined()) {
break :blk .empty;
}
break :blk .{ .bytes = try arena.dupe(u8, try js_val.toStringSmart()) };
else => {
const extracted = try b.extract(arena);
content_type = extracted.content_type;
break :blk .{ .bytes = extracted.bytes };
},
}
break :blk .empty;
};
const headers = try Headers.init(opts.headers, exec);
if (content_type) |ct| {
if (!headers.has("content-type", exec)) {
try headers.append("content-type", ct, exec);
}
}
const self = try arena.create(Response);
self.* = .{
._arena = arena,
@@ -102,9 +104,9 @@ pub fn init(body_: ?BodyInit, opts_: ?InitOpts, exec: *const Execution) !*Respon
._status_text = status_text,
._url = "",
._body = body,
._type = .basic,
._type = .default,
._is_redirected = false,
._headers = try Headers.init(opts.headers, exec),
._headers = headers,
};
return self;
}
@@ -168,10 +170,34 @@ pub fn isOK(self: *const Response) bool {
return self._status >= 200 and self._status <= 299;
}
pub fn getText(self: *const Response, exec: *const Execution) !js.Promise {
pub fn getBodyUsed(self: *const Response) bool {
return switch (self._body) {
.empty => false,
else => self._body_used,
};
}
// Marks a present body consumed; returns a rejected promise if it already was.
fn consume(self: *Response, local: *const js.Local) ?js.Promise {
switch (self._body) {
.empty => return null,
else => {},
}
if (self._body_used) {
return local.rejectPromise(.{ .type_error = "Body has already been read" });
}
self._body_used = true;
return null;
}
pub fn getText(self: *Response, exec: *const Execution) !js.Promise {
const local = exec.context.local.?;
if (self.consume(local)) |rejected| {
return rejected;
}
const body = switch (self._body) {
.bytes => |b| b,
.bytes => |b| body_init.stripUtf8Bom(b),
.empty => "",
.stream => return local.rejectPromise(.{ .type_error = "Cannot read text from stream body" }),
};
@@ -180,8 +206,12 @@ pub fn getText(self: *const Response, exec: *const Execution) !js.Promise {
pub fn getJson(self: *Response, exec: *const Execution) !js.Promise {
const local = exec.context.local.?;
if (self.consume(local)) |rejected| {
return rejected;
}
const body = switch (self._body) {
.bytes => |b| b,
.bytes => |b| body_init.stripUtf8Bom(b),
.empty => "",
.stream => return local.rejectPromise(.{ .type_error = "Cannot read JSON from stream body" }),
};
@@ -193,6 +223,10 @@ pub fn getJson(self: *Response, exec: *const Execution) !js.Promise {
pub fn arrayBuffer(self: *Response, exec: *const Execution) !js.Promise {
const local = exec.context.local.?;
if (self.consume(local)) |rejected| {
return rejected;
}
return switch (self._body) {
.bytes => |body| local.resolvePromise(js.ArrayBuffer{ .values = body }),
.empty => local.resolvePromise(js.ArrayBuffer{ .values = "" }),
@@ -313,8 +347,9 @@ const StreamConsumer = struct {
}
};
pub fn blob(self: *const Response, exec: *const Execution) !js.Promise {
pub fn blob(self: *Response, exec: *const Execution) !js.Promise {
const local = exec.context.local.?;
if (self.consume(local)) |rejected| return rejected;
const body = switch (self._body) {
.bytes => |b| b,
.empty => "",
@@ -325,8 +360,9 @@ pub fn blob(self: *const Response, exec: *const Execution) !js.Promise {
return local.resolvePromise(b);
}
pub fn bytes(self: *const Response, exec: *const Execution) !js.Promise {
pub fn bytes(self: *Response, exec: *const Execution) !js.Promise {
const local = exec.context.local.?;
if (self.consume(local)) |rejected| return rejected;
const body = switch (self._body) {
.bytes => |b| b,
.empty => "",
@@ -386,6 +422,7 @@ pub const JsApi = struct {
pub const json = bridge.function(Response.getJson, .{});
pub const headers = bridge.accessor(Response.getHeaders, null, .{});
pub const body = bridge.accessor(Response.getBody, null, .{});
pub const bodyUsed = bridge.accessor(Response.getBodyUsed, null, .{});
pub const url = bridge.accessor(Response.getURL, null, .{});
pub const redirected = bridge.accessor(Response.isRedirected, null, .{});
pub const arrayBuffer = bridge.function(Response.arrayBuffer, .{});

View File

@@ -26,7 +26,6 @@ const Blob = @import("../Blob.zig");
const URL = @import("../../URL.zig");
const Page = @import("../../Page.zig");
const Frame = @import("../../Frame.zig");
const HttpClient = @import("../../HttpClient.zig");
const Event = @import("../Event.zig");
@@ -35,13 +34,14 @@ const CloseEvent = @import("../event/CloseEvent.zig");
const MessageEvent = @import("../event/MessageEvent.zig");
const log = lp.log;
const Execution = js.Execution;
const Allocator = std.mem.Allocator;
const IS_DEBUG = @import("builtin").mode == .Debug;
const WebSocket = @This();
_rc: lp.RC(u8) = .{},
_frame: *Frame,
_exec: *const Execution,
_proto: *EventTarget,
_arena: Allocator,
@@ -92,12 +92,12 @@ pub const BinaryType = enum {
arraybuffer,
};
pub fn init(url: []const u8, protocols: [][]const u8, frame: *Frame) !*WebSocket {
pub fn init(url: []const u8, protocols: [][]const u8, exec: *const Execution) !*WebSocket {
{
if (url.len < 6) {
return error.SyntaxError;
}
const normalized_start = std.ascii.lowerString(&frame.buf, url[0..6]);
const normalized_start = std.ascii.lowerString(exec.buf, url[0..6]);
if (!std.mem.startsWith(u8, normalized_start, "ws://") and !std.mem.startsWith(u8, normalized_start, "wss://")) {
return error.SyntaxError;
}
@@ -112,12 +112,12 @@ pub fn init(url: []const u8, protocols: [][]const u8, frame: *Frame) !*WebSocket
}
}
const arena = try frame.getArena(.medium, "WebSocket");
errdefer frame.releaseArena(arena);
const arena = try exec.getArena(.medium, "WebSocket");
errdefer exec.releaseArena(arena);
const resolved_url = try URL.resolve(arena, frame.base(), url, .{ .always_dupe = true, .encoding = frame.charset });
const resolved_url = try URL.resolve(arena, exec.base(), url, .{ .always_dupe = true, .encoding = exec.charset.* });
const http_client = &frame._session.browser.http_client;
const http_client = &exec.context.page.session.browser.http_client;
const conn = http_client.network.newConnection() orelse {
return error.NoFreeConnection;
};
@@ -139,8 +139,8 @@ pub fn init(url: []const u8, protocols: [][]const u8, frame: *Frame) !*WebSocket
try conn.setHeaders(&headers);
}
const self = try frame._factory.eventTargetWithAllocator(arena, WebSocket{
._frame = frame,
const self = try exec._factory.eventTargetWithAllocator(arena, WebSocket{
._exec = exec,
._conn = conn,
._arena = arena,
._proto = undefined,
@@ -150,7 +150,7 @@ pub fn init(url: []const u8, protocols: [][]const u8, frame: *Frame) !*WebSocket
});
conn.transport = .{ .websocket = self };
try http_client.trackConn(conn);
frame._http_owner.addWS(self);
exec.httpOwner().addWS(self);
if (comptime IS_DEBUG) {
log.info(.websocket, "connecting", .{ .url = url });
@@ -236,11 +236,11 @@ pub fn disconnected(self: *WebSocket, err_: ?anyerror) void {
fn cleanup(self: *WebSocket) void {
if (self._conn) |conn| {
self._frame._http_owner.removeWS(self);
self._exec.httpOwner().removeWS(self);
self._http_client.removeConn(conn);
self._req_headers.deinit();
self._conn = null;
self.releaseRef(self._frame._page);
self.releaseRef(self._exec.context.page);
self._send_queue.clearRetainingCapacity();
}
}
@@ -308,8 +308,8 @@ pub fn send(self: *WebSocket, data: SendData) !void {
switch (data) {
.blob => |blob| {
const arena = try self._frame.getArena(blob._slice.len, "WebSocket.message");
errdefer self._frame.releaseArena(arena);
const arena = try self._exec.getArena(blob._slice.len, "WebSocket.message");
errdefer self._exec.releaseArena(arena);
try self.queueMessage(.{ .binary = .{
.arena = arena,
.data = try arena.dupe(u8, blob._slice),
@@ -317,8 +317,8 @@ pub fn send(self: *WebSocket, data: SendData) !void {
},
.js_val => |js_val| {
if (js_val.isString()) |str| {
const arena = try self._frame.getArena(str.len(), "WebSocket.message");
errdefer self._frame.releaseArena(arena);
const arena = try self._exec.getArena(str.len(), "WebSocket.message");
errdefer self._exec.releaseArena(arena);
try self.queueMessage(.{ .text = .{
.arena = arena,
.data = try str.toSliceWithAlloc(arena),
@@ -327,8 +327,8 @@ pub fn send(self: *WebSocket, data: SendData) !void {
const binary = try js_val.toZig(BinaryData);
const buffer = binary.asBuffer();
const arena = try self._frame.getArena(buffer.len, "WebSocket.message");
errdefer self._frame.releaseArena(arena);
const arena = try self._exec.getArena(buffer.len, "WebSocket.message");
errdefer self._exec.releaseArena(arena);
try self.queueMessage(.{ .binary = .{
.arena = arena,
.data = try arena.dupe(u8, buffer),
@@ -453,25 +453,25 @@ pub fn setOnClose(self: *WebSocket, cb_: ?js.Function) !void {
}
fn dispatchOpenEvent(self: *WebSocket) !void {
const frame = self._frame;
const exec = self._exec;
const target = self.asEventTarget();
if (frame._event_manager.hasDirectListeners(target, "open", self._on_open)) {
const event = try Event.initTrusted(comptime .wrap("open"), .{}, frame._page);
try frame._event_manager.dispatchDirect(target, event, self._on_open, .{ .context = "WebSocket open" });
if (exec.hasDirectListeners(target, "open", self._on_open)) {
const event = try Event.initTrusted(comptime .wrap("open"), .{}, exec.context.page);
try exec.dispatch(target, event, self._on_open, .{ .context = "WebSocket open" });
}
}
fn dispatchMessageEvent(self: *WebSocket, data: []const u8, frame_type: http.WsFrameType) !void {
const frame = self._frame;
const exec = self._exec;
const target = self.asEventTarget();
if (frame._event_manager.hasDirectListeners(target, "message", self._on_message)) {
if (exec.hasDirectListeners(target, "message", self._on_message)) {
const msg_data: MessageEvent.Data = if (frame_type == .binary)
switch (self._binary_type) {
.arraybuffer => .{ .arraybuffer = .{ .values = data } },
.blob => blk: {
const blob = try Blob.initFromBytes(data, "", false, frame._page);
const blob = try Blob.initFromBytes(data, "", false, exec.context.page);
blob.acquireRef();
break :blk .{ .blob = blob };
},
@@ -482,32 +482,32 @@ fn dispatchMessageEvent(self: *WebSocket, data: []const u8, frame_type: http.WsF
const event = try MessageEvent.initTrusted(comptime .wrap("message"), .{
.data = msg_data,
.origin = "",
}, frame._page);
try frame._event_manager.dispatchDirect(target, event.asEvent(), self._on_message, .{ .context = "WebSocket message" });
}, exec.context.page);
try exec.dispatch(target, event.asEvent(), self._on_message, .{ .context = "WebSocket message" });
}
}
fn dispatchErrorEvent(self: *WebSocket) !void {
const frame = self._frame;
const exec = self._exec;
const target = self.asEventTarget();
if (frame._event_manager.hasDirectListeners(target, "error", self._on_error)) {
const event = try Event.initTrusted(comptime .wrap("error"), .{}, frame._page);
try frame._event_manager.dispatchDirect(target, event, self._on_error, .{ .context = "WebSocket error" });
if (exec.hasDirectListeners(target, "error", self._on_error)) {
const event = try Event.initTrusted(comptime .wrap("error"), .{}, exec.context.page);
try exec.dispatch(target, event, self._on_error, .{ .context = "WebSocket error" });
}
}
fn dispatchCloseEvent(self: *WebSocket, code: u16, reason: []const u8, was_clean: bool) !void {
const frame = self._frame;
const exec = self._exec;
const target = self.asEventTarget();
if (frame._event_manager.hasDirectListeners(target, "close", self._on_close)) {
if (exec.hasDirectListeners(target, "close", self._on_close)) {
const event = try CloseEvent.initTrusted(comptime .wrap("close"), .{
.code = code,
.reason = reason,
.wasClean = was_clean,
}, frame);
try frame._event_manager.dispatchDirect(target, event.asEvent(), self._on_close, .{ .context = "WebSocket close" });
}, exec.context.page);
try exec.dispatch(target, event.asEvent(), self._on_close, .{ .context = "WebSocket close" });
}
}
@@ -580,7 +580,7 @@ fn writeContent(self: *WebSocket, conn: *http.Connection, buf: []u8, byte_msg: M
if (self._send_offset >= byte_msg.data.len) {
const removed = self._send_queue.orderedRemove(0);
removed.deinit(self._frame._page);
removed.deinit(self._exec.context.page);
if (comptime IS_DEBUG) {
log.debug(.websocket, "send complete", .{ .url = self._url, .len = byte_msg.data.len, .queue = self._send_queue.items.len });
}
@@ -768,3 +768,7 @@ const testing = @import("../../../testing.zig");
test "WebApi: WebSocket" {
try testing.htmlRunner("net/websocket.html", .{});
}
test "WebApi: WebSocket in worker" {
try testing.htmlRunner("net/websocket_worker.html", .{});
}

View File

@@ -30,10 +30,13 @@
const std = @import("std");
const js = @import("../../js/js.zig");
const Blob = @import("../Blob.zig");
const FormData = @import("FormData.zig");
const URLSearchParams = @import("URLSearchParams.zig");
const ReadableStream = @import("../streams/ReadableStream.zig");
const Allocator = std.mem.Allocator;
@@ -41,6 +44,8 @@ pub const BodyInit = union(enum) {
blob: *Blob,
form_data: *FormData,
url_search_params: *URLSearchParams,
stream: *ReadableStream,
buffer: js.TypedArray(u8),
bytes: []const u8, // must be last, js.Bridge will map anything to a string
pub fn extract(self: BodyInit, arena: Allocator) !Extracted {
@@ -91,6 +96,22 @@ pub const BodyInit = union(enum) {
.content_type = if (blob._mime.len > 0) try arena.dupe(u8, blob._mime) else null,
};
},
.buffer => |b| {
// Buffer sources carry no default Content-Type (Fetch §6.5).
return .{
.bytes = try arena.dupe(u8, b.values),
.content_type = null,
};
},
.stream => {
// A ReadableStream body cannot be serialized synchronously.
// Callers that support streaming bodies (Response) special-case
// the `.stream` arm before calling extract; the Request/XHR
// paths that reach here have no place to store a stream, so
// they send an empty body. Like other non-string sources, a
// stream carries no default Content-Type.
return .{ .bytes = "", .content_type = null };
},
}
}
};
@@ -104,6 +125,15 @@ pub const Extracted = struct {
content_type: ?[]const u8,
};
// "UTF-8 decode" (Encoding §4.2) strips a leading BOM; consuming a body as
// text/json must use it, while arrayBuffer/blob/bytes keep the raw bytes.
pub fn stripUtf8Bom(bytes: []const u8) []const u8 {
if (std.mem.startsWith(u8, bytes, "\xef\xbb\xbf")) {
return bytes[3..];
}
return bytes;
}
const testing = @import("../../../testing.zig");
test "BodyInit: bytes pass through with text/plain" {
defer testing.reset();
@@ -151,6 +181,20 @@ test "BodyInit: FormData emits multipart with random boundary" {
try testing.expect(std.mem.endsWith(u8, r.bytes, closer));
}
test "BodyInit: buffer source has no default Content-Type" {
defer testing.reset();
const r = try (BodyInit{ .buffer = .{ .values = "hello" } }).extract(testing.arena_allocator);
try testing.expectString("hello", r.bytes);
try testing.expectEqual(true, r.content_type == null);
}
test "stripUtf8Bom" {
try testing.expectString("abc", stripUtf8Bom("\xef\xbb\xbfabc"));
try testing.expectString("abc", stripUtf8Bom("abc"));
try testing.expectString("", stripUtf8Bom("\xef\xbb\xbf"));
try testing.expectString("\xef\xbbno-bom", stripUtf8Bom("\xef\xbbno-bom"));
}
// Blob.extract is exercised end-to-end by the Request/XHR HTML fixture
// tests rather than constructed ad-hoc here — Blob owns `_type`, `_rc`,
// and `_arena` fields that need a Page-backed allocator to initialise

View File

@@ -1041,12 +1041,12 @@ fn writeName(
return .alt;
}
switch (el.getTag()) {
const use_name_for_content: bool = switch (el.getTag()) {
.br => {
try writeString("\n", w);
return .contents;
},
.input => {
.input => blk: {
const input = el.as(DOMNode.Element.Html.Input);
switch (input._input_type) {
.reset, .button, .submit => |t| {
@@ -1063,6 +1063,7 @@ fn writeName(
}
// TODO Check for <label> with matching "for" attribute
// TODO Check if input is wrapped in a <label>
break :blk false;
},
// zig fmt: off
.textarea, .select, .img, .audio, .video, .iframe, .embed,
@@ -1071,16 +1072,17 @@ fn writeName(
.thead, .tbody, .tfoot, .tr, .td, .div, .span, .p, .details, .li,
.style, .script, .html, .body,
// zig fmt: on
=> {},
else => {
// write text content if exists.
var buf: std.Io.Writer.Allocating = .init(scratchAllocator(temp_arena, frame));
try writeAccessibleNameFallback(node, &buf.writer, frame);
if (buf.written().len > 0) {
try writeString(buf.written(), w);
return .contents;
}
},
=> nameFromContentRole(axnode.role_attr),
else => true,
};
if (use_name_for_content) {
var buf: std.Io.Writer.Allocating = .init(scratchAllocator(temp_arena, frame));
try writeAccessibleNameFallback(node, &buf.writer, frame);
if (buf.written().len > 0) {
try writeString(buf.written(), w);
return .contents;
}
}
if (el.getAttributeSafe(comptime .wrap("title"))) |title| {
@@ -1159,6 +1161,36 @@ fn isLabellableTag(tag: DOMNode.Element.Tag) bool {
};
}
// ARIA roles whose accessible name may be computed from descendant content
// (AccName "name from content"). Used so an explicit role can opt a host
// element back into name-from-content when its tag wouldn't otherwise — e.g.
// <div role="heading"> or <span role="button">.
fn nameFromContentRole(role_: ?[]const u8) bool {
const role = role_ orelse return false;
const name_for_content_roles = std.StaticStringMap(void).initComptime(.{
.{ "button", {} },
.{ "cell", {} },
.{ "checkbox", {} },
.{ "columnheader", {} },
.{ "gridcell", {} },
.{ "heading", {} },
.{ "link", {} },
.{ "menuitem", {} },
.{ "menuitemcheckbox", {} },
.{ "menuitemradio", {} },
.{ "option", {} },
.{ "radio", {} },
.{ "row", {} },
.{ "rowheader", {} },
.{ "switch", {} },
.{ "tab", {} },
.{ "tooltip", {} },
.{ "treeitem", {} },
.{ "term", {} },
});
return name_for_content_roles.has(role);
}
// CSS-only toggle switches and custom radios commonly visually-style a
// `<label>` while `display:none`-ing the real `<input>`. Chromium matches
// this by pruning the input from the AX tree, which leaves the label as a
@@ -1862,3 +1894,43 @@ test "AXNode: Writer query no match returns empty array" {
try testing.expectEqual(0, parsed.value.array.items.len);
}
test "AXNode: nameFromContentRole" {
try testing.expect(nameFromContentRole("heading"));
try testing.expect(nameFromContentRole("button"));
try testing.expect(nameFromContentRole("link"));
try testing.expect(nameFromContentRole("presentation") == false);
try testing.expect(nameFromContentRole(null) == false);
}
test "AXNode: getName name-from-content honors explicit role" {
var frame = try testing.pageTest("cdp/accname.html", .{});
defer frame._session.removePage();
var doc = frame.window._document;
const Case = struct { selector: []const u8, expected: ?[]const u8 };
const cases = [_]Case{
// Explicit name-from-content role opts a div/span into name-from-content.
.{ .selector = "#heading", .expected = "Hello World" },
.{ .selector = "#button", .expected = "Click me" },
// Recurses through child elements; the space here is real source whitespace.
.{ .selector = "#nested", .expected = "Read more" },
// No role, or a non-name-from-content role: no name from contents.
.{ .selector = "#plain", .expected = null },
.{ .selector = "#pres", .expected = null },
};
for (cases) |c| {
const el = (try doc.querySelector(.wrap(c.selector), frame)).?;
const axn = AXNode.fromNode(el.asNode());
const name = try axn.getName(frame, testing.allocator);
defer if (name) |n| testing.allocator.free(n);
if (c.expected) |exp| {
try testing.expect(name != null);
try testing.expectEqual(exp, name.?);
} else {
try testing.expect(name == null);
}
}
}

View File

@@ -1026,7 +1026,7 @@ pub const BrowserContext = struct {
// reserve 10 bytes for websocket header
buf.appendSliceAssumeCapacity(&.{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 });
// -1 because we dont' want the closing brace '}'
// -1 because we don't want the closing brace '}'
buf.appendSliceAssumeCapacity(msg[0 .. msg.len - 1]);
buf.appendSliceAssumeCapacity(field);
buf.appendSliceAssumeCapacity(session_id);

View File

@@ -149,7 +149,8 @@ fn setLifecycleEventsEnabled(cmd: *CDP.Command) !void {
const http_client = frame._session.browser.http_client;
const http_active = http_client.http_active;
const total_network_activity = http_active + http_client.interception_layer.intercepted;
const http_next_tick = http_client.next_tick_count;
const total_network_activity = http_active + http_next_tick + http_client.interception_layer.intercepted;
if (frame._notified_network_almost_idle.check(total_network_activity <= 2)) {
try sendPageLifecycle(bc, "networkAlmostIdle", now, frame_id, loader_id);
}

View File

@@ -49,6 +49,12 @@ pub fn put(self: *Cache, metadata: CachedMetadata, body: []const u8) !void {
};
}
pub fn evict(self: *Cache, url: []const u8) void {
return switch (self.kind) {
inline else => |*c| c.evict(url),
};
}
pub fn clear(self: *Cache) !void {
return switch (self.kind) {
inline else => |*c| c.clear(),

View File

@@ -280,6 +280,20 @@ pub fn clear(self: *FsCache) !void {
}
}
pub fn evict(self: *FsCache, url: []const u8) void {
const hashed_key = hashKey(url);
const cache_p = cachePath(&hashed_key);
const lock = self.getLockPtr(&hashed_key);
lock.lock();
defer lock.unlock();
self.dir.deleteFile(&cache_p) catch |e| switch (e) {
error.FileNotFound => {},
else => log.warn(.cache, "evict failed", .{ .url = url, .err = e }),
};
}
const testing = std.testing;
fn setupCache() !struct { tmp: testing.TmpDir, cache: Cache } {
@@ -673,18 +687,46 @@ test "FsCache: clear removes all entries" {
try cache.put(base_meta_b, "body b");
// Sanity check: both are cached
const r1 = cache.get(arena.allocator(), .{ .url = "https://example.com/a", .timestamp = now, .request_headers = &.{} });
const r1 = cache.get(
arena.allocator(),
.{
.url = "https://example.com/a",
.timestamp = now,
.request_headers = &.{},
},
);
try testing.expect(r1 != null);
r1.?.data.file.file.close();
const r2 = cache.get(arena.allocator(), .{ .url = "https://example.com/b", .timestamp = now, .request_headers = &.{} });
const r2 = cache.get(
arena.allocator(),
.{
.url = "https://example.com/b",
.timestamp = now,
.request_headers = &.{},
},
);
try testing.expect(r2 != null);
r2.?.data.file.file.close();
try cache.clear();
try testing.expectEqual(null, cache.get(arena.allocator(), .{ .url = "https://example.com/a", .timestamp = now, .request_headers = &.{} }));
try testing.expectEqual(null, cache.get(arena.allocator(), .{ .url = "https://example.com/b", .timestamp = now, .request_headers = &.{} }));
try testing.expectEqual(null, cache.get(
arena.allocator(),
.{
.url = "https://example.com/a",
.timestamp = now,
.request_headers = &.{},
},
));
try testing.expectEqual(null, cache.get(
arena.allocator(),
.{
.url = "https://example.com/b",
.timestamp = now,
.request_headers = &.{},
},
));
}
test "FsCache: put after clear works" {
@@ -715,11 +757,28 @@ test "FsCache: put after clear works" {
try cache.clear();
// Should be a miss after clear
try testing.expectEqual(null, cache.get(arena.allocator(), .{ .url = "https://example.com", .timestamp = now, .request_headers = &.{} }));
try testing.expectEqual(
null,
cache.get(
arena.allocator(),
.{
.url = "https://example.com",
.timestamp = now,
.request_headers = &.{},
},
),
);
// Put again after clear — should work normally
try cache.put(meta, "after clear");
const result = cache.get(arena.allocator(), .{ .url = "https://example.com", .timestamp = now, .request_headers = &.{} }) orelse return error.CacheMiss;
const result = cache.get(
arena.allocator(),
.{
.url = "https://example.com",
.timestamp = now,
.request_headers = &.{},
},
) orelse return error.CacheMiss;
const f = result.data.file;
defer f.file.close();
@@ -730,3 +789,51 @@ test "FsCache: put after clear works" {
defer testing.allocator.free(read_buf);
try testing.expectEqualStrings("after clear", read_buf);
}
test "FsCache: evict removes entry" {
var setup = try setupCache();
defer {
setup.cache.deinit();
setup.tmp.cleanup();
}
const cache = &setup.cache;
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const now = std.time.timestamp();
const meta = CachedMetadata{
.url = "https://example.com",
.content_type = "text/html",
.status = 200,
.stored_at = now,
.age_at_store = 0,
.cache_control = .{ .max_age = 600 },
.headers = &.{},
.vary_headers = &.{},
};
try cache.put(meta, "hello world");
const result = cache.get(
arena.allocator(),
.{
.url = "https://example.com",
.timestamp = now,
.request_headers = &.{},
},
) orelse return error.CacheMiss;
result.data.file.file.close();
cache.evict("https://example.com");
try testing.expectEqual(null, cache.get(
arena.allocator(),
.{
.url = "https://example.com",
.timestamp = now,
.request_headers = &.{},
},
));
}

View File

@@ -21,6 +21,7 @@ const lp = @import("lightpanda");
const Layer = @import("../../browser/HttpClient.zig").Layer;
const Client = @import("../../browser/HttpClient.zig").Client;
const NextTickNode = @import("../../browser/HttpClient.zig").NextTickNode;
const Request = @import("../../browser/HttpClient.zig").Request;
const Transfer = @import("../../browser/HttpClient.zig").Transfer;
const Response = @import("../../browser/HttpClient.zig").Response;
@@ -72,11 +73,30 @@ fn request(ptr: *anyopaque, transfer: *Transfer) anyerror!void {
&.{ .transfer = transfer },
);
// Cache hit: serve synchronously from the original callbacks, then
// tear down. On error, the transfer is still alive and Client.request's
// errdefer will handle cleanup (state stays .created).
try serveFromCache(req, &cached);
transfer.deinit();
const ctx = try arena.create(CachedResponse);
ctx.* = cached;
try transfer.client.runNextTick(transfer, ctx, .{
.run = struct {
fn run(t: *Transfer, ctx_ptr: *anyopaque) void {
defer t.deinit();
const c: *CachedResponse = @ptrCast(@alignCast(ctx_ptr));
serveFromCache(&t.req, c) catch |err| {
t.req.error_callback(t.req.ctx, err);
};
}
}.run,
.abort = struct {
fn abort(ctx_ptr: *anyopaque) void {
const c: *CachedResponse = @ptrCast(@alignCast(ctx_ptr));
switch (c.data) {
.buffer => |_| {},
.file => |f| f.file.close(),
}
}
}.abort,
});
return;
}

View File

@@ -98,7 +98,7 @@ pub fn release(self: *Pool, conn: Sqlite.Conn) void {
const testing = @import("../../testing.zig");
test "Sqlite: Pool" {
// :memory: _has_ to run with a single connetion in the pool, which isn't
// :memory: _has_ to run with a single connection in the pool, which isn't
// that useful for testing. So we create a temp file.
std.fs.cwd().deleteFile("/tmp/lightpanda_test.sqlite") catch {};

View File

@@ -198,7 +198,7 @@ const Statement = struct {
const data = c.sqlite3_column_text(stmt, @intCast(index));
return @as([*c]const u8, @ptrCast(data))[0..@intCast(len) :0];
},
else => @compileError("unsupport column type: " ++ @typeName(T)),
else => @compileError("unsupported column type: " ++ @typeName(T)),
};
}