mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 01:25:53 -04:00
Merge branch 'main' into agent
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
162
src/browser/CustomElementReactions.zig
Normal file
162
src/browser/CustomElementReactions.zig
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
15
src/browser/tests/cdp/accname.html
Normal file
15
src/browser/tests/cdp/accname.html
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
37
src/browser/tests/element/html/script/postload.html
Normal file
37
src/browser/tests/element/html/script/postload.html
Normal 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>
|
||||
50
src/browser/tests/frames/document_write.html
Normal file
50
src/browser/tests/frames/document_write.html
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
46
src/browser/tests/net/websocket-worker.js
Normal file
46
src/browser/tests/net/websocket-worker.js
Normal 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 });
|
||||
28
src/browser/tests/net/websocket_worker.html
Normal file
28
src/browser/tests/net/websocket_worker.html
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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, .{});
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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, .{});
|
||||
};
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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, .{});
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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, .{});
|
||||
};
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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, .{});
|
||||
};
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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", .{});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
6
src/network/cache/Cache.zig
vendored
6
src/network/cache/Cache.zig
vendored
@@ -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(),
|
||||
|
||||
119
src/network/cache/FsCache.zig
vendored
119
src/network/cache/FsCache.zig
vendored
@@ -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 = &.{},
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
@@ -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)),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user