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:
113
.github/workflows/release-agent.yml
vendored
Normal file
113
.github/workflows/release-agent.yml
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
name: agent nightly pre release build
|
||||
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ vars.NIGHTLY_BUILD_AWS_ACCESS_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NIGHTLY_BUILD_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_BUCKET: ${{ vars.NIGHTLY_BUILD_AWS_BUCKET }}
|
||||
AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }}
|
||||
AWS_CLOUDFRONT_DISTRIBUTION_ID: 'E2LP2HUMLR5GQD'
|
||||
|
||||
VERSION_FLAG: '-Dversion=agent-nightly'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "2 2 * * *"
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x86_64
|
||||
runner: ubuntu-22.04
|
||||
cpu_flag: -Dcpu=x86_64
|
||||
- arch: aarch64
|
||||
runner: ubuntu-22.04-arm
|
||||
cpu_flag: -Dcpu=generic
|
||||
|
||||
env:
|
||||
ARCH: ${{ matrix.arch }}
|
||||
OS: linux
|
||||
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: agent
|
||||
|
||||
- uses: ./.github/actions/install
|
||||
with:
|
||||
os: ${{env.OS}}
|
||||
arch: ${{env.ARCH}}
|
||||
|
||||
- name: v8 snapshot
|
||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||
|
||||
- name: zig build
|
||||
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast ${{ matrix.cpu_flag }} ${{ env.VERSION_FLAG }}
|
||||
|
||||
- name: Rename binary
|
||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
|
||||
- name: upload on s3
|
||||
run: |
|
||||
aws s3 cp --storage-class=GLACIER_IR lightpanda-${{ env.ARCH }}-${{ env.OS }} s3://lpd-nightly-build/agent-nightly/lightpanda-agent-${{ env.ARCH }}-${{ env.OS }}
|
||||
|
||||
build-macos:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# macos-14 runs on arm CPU. see
|
||||
# https://github.com/actions/runner-images?tab=readme-ov-file
|
||||
- arch: aarch64
|
||||
runner: macos-14
|
||||
- arch: x86_64
|
||||
runner: macos-14-large
|
||||
|
||||
env:
|
||||
ARCH: ${{ matrix.arch }}
|
||||
OS: macos
|
||||
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: agent
|
||||
|
||||
- uses: ./.github/actions/install
|
||||
with:
|
||||
os: ${{env.OS}}
|
||||
arch: ${{env.ARCH}}
|
||||
|
||||
- name: v8 snapshot
|
||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||
|
||||
- name: zig build
|
||||
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast ${{ env.VERSION_FLAG }}
|
||||
|
||||
- name: Rename binary
|
||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
|
||||
- name: upload on s3
|
||||
run: |
|
||||
aws s3 cp --storage-class=GLACIER_IR lightpanda-${{ env.ARCH }}-${{ env.OS }} s3://lpd-nightly-build/agent-nightly/lightpanda-agent-${{ env.ARCH }}-${{ env.OS }}
|
||||
|
||||
invalidate-cloudfront:
|
||||
needs: [build-linux, build-macos]
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: cloudfront cache invalidation
|
||||
run: |
|
||||
aws cloudfront create-invalidation --distribution-id ${{ env.AWS_CLOUDFRONT_DISTRIBUTION_ID }} --paths "/agent-nightly/*"
|
||||
@@ -99,6 +99,7 @@ const CommonOptions = .{
|
||||
.{ .name = "cookie_jar", .type = ?[]const u8 },
|
||||
.{ .name = "storage_engine", .type = ?Storage.EngineType },
|
||||
.{ .name = "storage_sqlite_path", .type = ?[:0]const u8 },
|
||||
.{ .name = "disable_subframes", .type = bool },
|
||||
};
|
||||
|
||||
fn dumpValidator(_: Allocator, args: *std.process.ArgIterator) !?DumpFormat {
|
||||
@@ -233,7 +234,7 @@ const Commands = cli.Builder(.{
|
||||
.shared_options = CommonOptions,
|
||||
},
|
||||
.{ .name = "version", .options = .{} },
|
||||
.{ .name = "help", .options = .{} },
|
||||
.{ .name = "help", .positional = .{ .name = "subcommand", .type = ?[]const u8 }, .options = .{} },
|
||||
});
|
||||
|
||||
pub const RunMode = Commands.Enum;
|
||||
@@ -280,6 +281,13 @@ pub fn obeyRobots(self: *const Config) bool {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn disableSubframes(self: *const Config) bool {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch, .mcp => |opts| opts.disable_subframes,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn httpProxy(self: *const Config) ?[:0]const u8 {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch, .mcp, .agent => |opts| opts.http_proxy,
|
||||
@@ -573,6 +581,17 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
|
||||
\\ we make requests towards.
|
||||
\\ Defaults to false.
|
||||
\\
|
||||
\\--disable-subframes
|
||||
\\ Skip loading <iframe> elements. The HTML parser registers them
|
||||
\\ in the DOM but no child frame, document fetch, or
|
||||
\\ Page.frameAttached / Runtime.executionContextCreated events are
|
||||
\\ produced. Useful for pages that load many analytics / pixel
|
||||
\\ iframes where each subframe navigation invalidates driver-side
|
||||
\\ executionContextIds (lightpanda-io/browser#2400). On the CDP
|
||||
\\ serve path, drivers can also toggle this per-session via the
|
||||
\\ LP.configureLoading method.
|
||||
\\ Defaults to false.
|
||||
\\
|
||||
\\--block-private-networks
|
||||
\\ Blocks HTTP requests to private/internal IP addresses
|
||||
\\ after DNS resolution. Useful for sandboxing, multi-tenant
|
||||
@@ -670,11 +689,7 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
|
||||
;
|
||||
|
||||
// MAX_HELP_LEN|
|
||||
const usage =
|
||||
\\usage: {0s} command [options] [URL]
|
||||
\\
|
||||
\\Command can be either 'fetch', 'serve', 'mcp', 'agent' or 'help'
|
||||
\\
|
||||
const fetch_options =
|
||||
\\fetch command
|
||||
\\Fetches the specified URL
|
||||
\\Example: {0s} fetch --dump html https://lightpanda.io/
|
||||
@@ -733,11 +748,12 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
|
||||
\\ Defaults to no cookie loading.
|
||||
\\
|
||||
\\--cookie-jar Path to a JSON file to save cookies to on exit (write-only).
|
||||
\\ Available for fetch and mcp commands.
|
||||
\\ Defaults to no cookie saving.
|
||||
\\
|
||||
++ common_options ++
|
||||
\\
|
||||
++ common_options;
|
||||
|
||||
// MAX_HELP_LEN|
|
||||
const serve_options =
|
||||
\\serve command
|
||||
\\Starts a websocket CDP server
|
||||
\\Example: {0s} serve --host 127.0.0.1 --port 9222
|
||||
@@ -765,21 +781,25 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
|
||||
\\--cookie Path to a JSON file to load cookies from (read-only).
|
||||
\\ Defaults to no cookie loading.
|
||||
\\
|
||||
++ common_options ++
|
||||
\\
|
||||
++ common_options;
|
||||
|
||||
// MAX_HELP_LEN|
|
||||
const mcp_options =
|
||||
\\mcp command
|
||||
\\Starts an MCP (Model Context Protocol) server over stdio
|
||||
\\Example: {0s} mcp
|
||||
\\
|
||||
\\Options:
|
||||
\\--cookie Path to a JSON file to load cookies from (read-only).
|
||||
\\ Defaults to no cookie loading.
|
||||
\\
|
||||
\\--cookie-jar Path to a JSON file to save cookies to on exit (write-only).
|
||||
\\ Available for fetch and mcp commands.
|
||||
\\ Defaults to no cookie saving.
|
||||
\\
|
||||
++ common_options ++
|
||||
\\
|
||||
++ common_options;
|
||||
|
||||
// MAX_HELP_LEN|
|
||||
const agent_options =
|
||||
\\agent command
|
||||
\\Starts an interactive AI agent that can browse the web
|
||||
\\Example: {0s} agent (auto-detects API key from env)
|
||||
@@ -861,7 +881,24 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
|
||||
\\ANTHROPIC_API_KEY, OPENAI_API_KEY, or GOOGLE_API_KEY/GEMINI_API_KEY.
|
||||
\\Ollama does not require an API key.
|
||||
\\
|
||||
++ common_options ++
|
||||
++ common_options;
|
||||
|
||||
// MAX_HELP_LEN|
|
||||
const usage =
|
||||
\\usage: {0s} command [options] [URL]
|
||||
\\
|
||||
\\Command can be either 'fetch', 'serve', 'mcp', 'agent' or 'help'
|
||||
\\
|
||||
++ fetch_options ++
|
||||
\\
|
||||
\\
|
||||
++ serve_options ++
|
||||
\\
|
||||
\\
|
||||
++ mcp_options ++
|
||||
\\
|
||||
\\
|
||||
++ agent_options ++
|
||||
\\
|
||||
\\version command
|
||||
\\Displays the version of {0s}
|
||||
@@ -870,6 +907,28 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
|
||||
\\Displays this message
|
||||
\\
|
||||
;
|
||||
|
||||
// When called with a subcommand argument,
|
||||
// print only the relevant subcommand section instead of the full help.
|
||||
switch (self.mode) {
|
||||
.help => |h| if (h.subcommand) |sub| {
|
||||
if (std.mem.eql(u8, sub, "fetch")) {
|
||||
std.debug.print(fetch_options ++ "\n", .{self.exec_name});
|
||||
} else if (std.mem.eql(u8, sub, "serve")) {
|
||||
std.debug.print(serve_options ++ "\n", .{self.exec_name});
|
||||
} else if (std.mem.eql(u8, sub, "mcp")) {
|
||||
std.debug.print(mcp_options ++ "\n", .{self.exec_name});
|
||||
} else if (std.mem.eql(u8, sub, "agent")) {
|
||||
std.debug.print(agent_options ++ "\n", .{self.exec_name});
|
||||
} else {
|
||||
std.debug.print(usage, .{self.exec_name});
|
||||
}
|
||||
if (success) return std.process.cleanExit();
|
||||
std.process.exit(1);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
std.debug.print(usage, .{self.exec_name});
|
||||
if (success) {
|
||||
return std.process.cleanExit();
|
||||
|
||||
129
src/browser/CustomElementReactions.zig
Normal file
129
src/browser/CustomElementReactions.zig
Normal file
@@ -0,0 +1,129 @@
|
||||
// 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.
|
||||
|
||||
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 Self = @This();
|
||||
|
||||
allocator: Allocator,
|
||||
queue: std.ArrayList(Reaction) = .empty,
|
||||
// Number of currently-open scopes (push() that hasn't been pop'd). Every
|
||||
// enqueue must happen inside a scope — that's the leak-detection invariant.
|
||||
// Checked in debug at enqueue time so leaks surface where the bug is, not
|
||||
// later at some unrelated boundary.
|
||||
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 {
|
||||
for (self.queue.items[checkpoint..]) |reaction| {
|
||||
Custom.fireReaction(reaction, frame);
|
||||
}
|
||||
self.queue.items.len = checkpoint;
|
||||
self.active_scopes -= 1;
|
||||
}
|
||||
|
||||
inline fn assertScopeActive(self: *const Self) void {
|
||||
lp.assert(self.active_scopes > 0, "ce_reactions enqueue without active scope", .{});
|
||||
}
|
||||
|
||||
pub fn enqueueConnected(self: *Self, element: *Element) !void {
|
||||
self.assertScopeActive();
|
||||
try self.queue.append(self.allocator, .{ .connected = element });
|
||||
}
|
||||
|
||||
pub fn enqueueDisconnected(self: *Self, element: *Element) !void {
|
||||
self.assertScopeActive();
|
||||
try self.queue.append(self.allocator, .{ .disconnected = element });
|
||||
}
|
||||
|
||||
pub fn enqueueAdopted(self: *Self, element: *Element, old_document: *Document, new_document: *Document) !void {
|
||||
self.assertScopeActive();
|
||||
try self.queue.append(self.allocator, .{ .adopted = .{
|
||||
.element = element,
|
||||
.old_document = old_document,
|
||||
.new_document = new_document,
|
||||
} });
|
||||
}
|
||||
|
||||
pub fn enqueueAttributeChanged(
|
||||
self: *Self,
|
||||
element: *Element,
|
||||
name: String,
|
||||
old_value: ?String,
|
||||
new_value: ?String,
|
||||
namespace: ?String,
|
||||
) !void {
|
||||
self.assertScopeActive();
|
||||
try self.queue.append(self.allocator, .{ .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,
|
||||
};
|
||||
};
|
||||
@@ -61,10 +61,7 @@ pub fn init(arena: Allocator, frame: *Frame) EventManager {
|
||||
}
|
||||
|
||||
pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, opts: RegisterOptions) !void {
|
||||
const listener = self.base.register(target, typ, callback, opts) catch |err| switch (err) {
|
||||
error.SignalAborted, error.DuplicateListener => return,
|
||||
else => return err,
|
||||
};
|
||||
const listener = (try self.base.register(target, typ, callback, opts)) orelse return;
|
||||
|
||||
if (listener.typ.eql(comptime .wrap("load"))) {
|
||||
if (target._type == .node) {
|
||||
@@ -321,16 +318,14 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
||||
// Track dispatch depth for deferred removal
|
||||
base.dispatch_depth += 1;
|
||||
defer {
|
||||
const dispatch_depth = base.dispatch_depth;
|
||||
base.dispatch_depth -= 1;
|
||||
// Only destroy deferred listeners when we exit the outermost dispatch
|
||||
if (dispatch_depth == 1) {
|
||||
if (base.dispatch_depth == 0) {
|
||||
for (base.deferred_removals.items) |removal| {
|
||||
removal.list.remove(&removal.listener.node);
|
||||
base.listener_pool.destroy(removal.listener);
|
||||
}
|
||||
base.deferred_removals.clearRetainingCapacity();
|
||||
} else {
|
||||
base.dispatch_depth = dispatch_depth - 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,6 +380,7 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
||||
|
||||
was_handled.* = true;
|
||||
event._current_target = current_target;
|
||||
event._in_passive_listener = listener.passive;
|
||||
|
||||
// Compute adjusted target for shadow DOM retargeting (only if needed)
|
||||
const original_target = event._target;
|
||||
@@ -394,6 +390,8 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
||||
|
||||
try listener.run(frame.call_arena, local, event, "listener");
|
||||
|
||||
event._in_passive_listener = false;
|
||||
|
||||
// Restore original target (only if we changed it)
|
||||
if (event._needs_retargeting) {
|
||||
event._target = original_target;
|
||||
|
||||
@@ -89,7 +89,13 @@ pub const Callback = union(enum) {
|
||||
object: js.Object,
|
||||
};
|
||||
|
||||
pub fn register(self: *EventManagerBase, target: *EventTarget, typ: []const u8, callback: Callback, opts: RegisterOptions) !*Listener {
|
||||
// Returns null when the listener is a no-op per spec: signal already
|
||||
// aborted, or a duplicate (same type + callback + capture) of an
|
||||
// already-registered listener. Real errors (OOM, StringTooLarge) still
|
||||
// propagate. Callers don't have to distinguish "skipped" from "registered"
|
||||
// unless they need the resulting *Listener (e.g. Frame's load-listener
|
||||
// tracking).
|
||||
pub fn register(self: *EventManagerBase, target: *EventTarget, typ: []const u8, callback: Callback, opts: RegisterOptions) !?*Listener {
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.event, "EventManager.register", .{
|
||||
.type = typ,
|
||||
@@ -102,7 +108,7 @@ pub fn register(self: *EventManagerBase, target: *EventTarget, typ: []const u8,
|
||||
// If a signal is provided and already aborted, don't register the listener
|
||||
if (opts.signal) |signal| {
|
||||
if (signal.getAborted()) {
|
||||
return error.SignalAborted;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,18 +120,22 @@ pub fn register(self: *EventManagerBase, target: *EventTarget, typ: []const u8,
|
||||
.event_target = @intFromPtr(target),
|
||||
});
|
||||
if (gop.found_existing) {
|
||||
// check for duplicate callbacks already registered
|
||||
// check for duplicate callbacks already registered. Listeners that
|
||||
// have been removed (e.g. a `once` listener that fired mid-dispatch
|
||||
// and is awaiting destruction in deferred_removals) are not "in"
|
||||
// the listener list per spec — skip them.
|
||||
var node = gop.value_ptr.*.first;
|
||||
while (node) |n| {
|
||||
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
||||
node = n.next;
|
||||
if (listener.removed) continue;
|
||||
const is_duplicate = switch (callback) {
|
||||
.object => |obj| listener.function.eqlObject(obj),
|
||||
.function => |func| listener.function.eqlFunction(func),
|
||||
};
|
||||
if (is_duplicate and listener.capture == opts.capture) {
|
||||
return error.DuplicateListener;
|
||||
return null;
|
||||
}
|
||||
node = n.next;
|
||||
}
|
||||
} else {
|
||||
gop.value_ptr.* = try self.list_pool.create();
|
||||
@@ -164,6 +174,11 @@ pub fn remove(self: *EventManagerBase, target: *EventTarget, typ: []const u8, ca
|
||||
}
|
||||
|
||||
pub fn removeListener(self: *EventManagerBase, list: *std.DoublyLinkedList, listener: *Listener) void {
|
||||
// Already removed (or queued for removal). Avoids double-pushing the
|
||||
// same listener into deferred_removals — which would double-free at
|
||||
// the outer-dispatch cleanup — if e.g. a `once` listener also calls
|
||||
// removeEventListener on itself.
|
||||
if (listener.removed) return;
|
||||
// If we're in a dispatch, defer removal to avoid invalidating iteration
|
||||
if (self.dispatch_depth > 0) {
|
||||
listener.removed = true;
|
||||
@@ -256,16 +271,14 @@ pub fn dispatchDirect(
|
||||
// Track dispatch depth for deferred removal
|
||||
self.dispatch_depth += 1;
|
||||
defer {
|
||||
const dispatch_depth = self.dispatch_depth;
|
||||
self.dispatch_depth -= 1;
|
||||
// Only destroy deferred listeners when we exit the outermost dispatch
|
||||
if (dispatch_depth == 1) {
|
||||
if (self.dispatch_depth == 0) {
|
||||
for (self.deferred_removals.items) |removal| {
|
||||
removal.list.remove(&removal.listener.node);
|
||||
self.listener_pool.destroy(removal.listener);
|
||||
}
|
||||
self.deferred_removals.clearRetainingCapacity();
|
||||
} else {
|
||||
self.dispatch_depth = dispatch_depth - 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,9 +317,12 @@ pub fn dispatchDirect(
|
||||
}
|
||||
|
||||
event._current_target = target;
|
||||
event._in_passive_listener = listener.passive;
|
||||
|
||||
try listener.run(arena, &ls.local, event, opts.context);
|
||||
|
||||
event._in_passive_listener = false;
|
||||
|
||||
if (event._stop_immediate_propagation) {
|
||||
return;
|
||||
}
|
||||
@@ -356,6 +372,10 @@ fn findListener(list: *const std.DoublyLinkedList, callback: Callback, capture:
|
||||
while (node) |n| {
|
||||
node = n.next;
|
||||
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
||||
// Per spec, a removed listener isn't "in" the list anymore; skip
|
||||
// entries still present only because their deferred removal hasn't
|
||||
// been flushed yet.
|
||||
if (listener.removed) continue;
|
||||
const matches = switch (callback) {
|
||||
.object => |obj| listener.function.eqlObject(obj),
|
||||
.function => |func| listener.function.eqlFunction(func),
|
||||
|
||||
@@ -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");
|
||||
@@ -188,6 +190,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,
|
||||
|
||||
@@ -288,6 +296,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),
|
||||
._console_messages = .init(arena),
|
||||
};
|
||||
@@ -1296,6 +1305,11 @@ pub fn iframeAddedCallback(self: *Frame, iframe: *IFrame) !void {
|
||||
if (iframe._executed) {
|
||||
return;
|
||||
}
|
||||
if (!self._session.subframe_loading_enabled) {
|
||||
// configured not to load frames
|
||||
iframe._executed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var src = iframe.asElement().getAttributeSafe(comptime .wrap("src")) orelse "";
|
||||
if (src.len == 0) {
|
||||
@@ -1854,7 +1868,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();
|
||||
@@ -2590,7 +2604,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
|
||||
@@ -3012,7 +3026,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| {
|
||||
@@ -3035,11 +3049,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 });
|
||||
@@ -3050,31 +3061,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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3198,7 +3194,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;
|
||||
@@ -3244,7 +3240,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3254,7 +3250,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) {
|
||||
@@ -3280,7 +3276,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) {
|
||||
|
||||
@@ -749,6 +749,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) {
|
||||
|
||||
@@ -78,6 +78,9 @@ _pending: ?*Page = null,
|
||||
frame_id_gen: u32 = 0,
|
||||
loader_id_gen: u32 = 0,
|
||||
|
||||
// configuration (or CDP command) to disable iframe loading
|
||||
subframe_loading_enabled: bool = true,
|
||||
|
||||
pub fn init(self: *Session, browser: *Browser, notification: *Notification) !void {
|
||||
const allocator = browser.app.allocator;
|
||||
const arena_pool = browser.arena_pool;
|
||||
@@ -96,6 +99,8 @@ pub fn init(self: *Session, browser: *Browser, notification: *Notification) !voi
|
||||
.notification = notification,
|
||||
.fc_identity_pool = .init(allocator),
|
||||
.cookie_jar = storage.Cookie.Jar.init(allocator),
|
||||
// CLI default; LP.configureLoading can flip this per-session.
|
||||
.subframe_loading_enabled = !browser.app.config.disableSubframes(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -569,6 +569,7 @@ pub const Function = struct {
|
||||
null_as_undefined: bool = false,
|
||||
cache: ?Caching = null,
|
||||
embedded_receiver: bool = false,
|
||||
ce_reactions: bool = false,
|
||||
|
||||
// We support two ways to cache a value directly into a v8::Object. The
|
||||
// difference between the two is like the difference between a Map
|
||||
@@ -621,6 +622,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,
|
||||
|
||||
@@ -1488,11 +1488,14 @@ pub fn parseJSON(self: *const Local, json: []const u8) !js.Value {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn throw(self: *const Local, err: []const u8) js.Exception {
|
||||
const handle = self.isolate.createError(err);
|
||||
pub fn newException(self: *const Local, ex: anytype) js.Exception {
|
||||
const js_val = self.zigValueToJs(ex, .{}) catch {
|
||||
return .{ .local = self, .handle = self.isolate.createError("internal error") };
|
||||
};
|
||||
|
||||
return .{
|
||||
.local = self,
|
||||
.handle = handle,
|
||||
.handle = js_val.handle,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -947,6 +959,11 @@ pub const WorkerJsApis = flattenTypes(&.{
|
||||
@import("../webapi/WorkerGlobalScope.zig"),
|
||||
@import("../webapi/WorkerLocation.zig"),
|
||||
@import("../webapi/EventTarget.zig"),
|
||||
@import("../webapi/Event.zig"),
|
||||
@import("../webapi/event/MessageEvent.zig"),
|
||||
@import("../webapi/event/ErrorEvent.zig"),
|
||||
@import("../webapi/event/PromiseRejectionEvent.zig"),
|
||||
@import("../webapi/event/CloseEvent.zig"),
|
||||
@import("../webapi/DOMException.zig"),
|
||||
@import("../webapi/net/URLSearchParams.zig"),
|
||||
@import("../webapi/encoding/TextEncoder.zig"),
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -23,51 +23,63 @@
|
||||
<main>Main</main>
|
||||
|
||||
<script id=basic>
|
||||
testing.expectEqual(24, document.getElementsByTagName('*').length);
|
||||
testing.expectEqual(0, document.getElementsByTagName('a').length);
|
||||
testing.expectEqual(0, document.getElementsByTagName('unknown').length);
|
||||
{
|
||||
const all = document.getElementsByTagName('*');
|
||||
testing.expectEqual(24, all.length);
|
||||
testing.expectEqual(false, "values" in all);
|
||||
testing.expectEqual(false, "entries" in all);
|
||||
testing.expectEqual(false, "forEach" in all);
|
||||
testing.expectEqual(0, document.getElementsByTagName('a').length);
|
||||
testing.expectEqual(0, document.getElementsByTagName('unknown').length);
|
||||
|
||||
const divs = document.getElementsByTagName('div');
|
||||
testing.expectEqual(true, divs instanceof HTMLCollection);
|
||||
testing.expectEqual(3, divs.length);
|
||||
testing.expectEqual(3, divs.length);
|
||||
testing.expectEqual('0', divs[0].textContent);
|
||||
testing.expectEqual('0', divs[0].textContent);
|
||||
testing.expectEqual('0', divs[0].textContent);
|
||||
testing.expectEqual('1', divs[1].textContent);
|
||||
testing.expectEqual('2', divs[2].textContent);
|
||||
testing.expectEqual('2', divs[2].textContent);
|
||||
testing.expectEqual('1', divs[1].textContent);
|
||||
testing.expectEqual('1', divs[1].textContent);
|
||||
testing.expectEqual('2', divs[2].textContent);
|
||||
testing.expectEqual('0', divs[0].textContent);
|
||||
testing.expectEqual(undefined, divs[-1]);
|
||||
testing.expectEqual(undefined, divs[4]);
|
||||
const divs = document.getElementsByTagName('div');
|
||||
testing.expectEqual(true, divs instanceof HTMLCollection);
|
||||
testing.expectEqual(3, divs.length);
|
||||
testing.expectEqual(3, divs.length);
|
||||
testing.expectEqual('0', divs[0].textContent);
|
||||
testing.expectEqual('0', divs[0].textContent);
|
||||
testing.expectEqual('0', divs[0].textContent);
|
||||
testing.expectEqual('1', divs[1].textContent);
|
||||
testing.expectEqual('2', divs[2].textContent);
|
||||
testing.expectEqual('2', divs[2].textContent);
|
||||
testing.expectEqual('1', divs[1].textContent);
|
||||
testing.expectEqual('1', divs[1].textContent);
|
||||
testing.expectEqual('2', divs[2].textContent);
|
||||
testing.expectEqual('0', divs[0].textContent);
|
||||
testing.expectEqual(undefined, divs[-1]);
|
||||
testing.expectEqual(undefined, divs[4]);
|
||||
|
||||
testing.expectEqual('2', divs.item(2).textContent);
|
||||
testing.expectEqual(null, divs.item(-3));
|
||||
testing.expectEqual('0', divs.item(0).textContent);
|
||||
testing.expectEqual(null, divs.item(-100));
|
||||
testing.expectEqual('2', divs.item(2).textContent);
|
||||
testing.expectEqual(null, divs.item(-3));
|
||||
testing.expectEqual('0', divs.item(0).textContent);
|
||||
testing.expectEqual(null, divs.item(-100));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=namedItem>
|
||||
testing.expectEqual(null, divs.namedItem('d1'));
|
||||
{
|
||||
const divs = document.getElementsByTagName('div');
|
||||
testing.expectEqual(null, divs.namedItem('d1'));
|
||||
|
||||
const ps = document.getElementsByTagName('p');
|
||||
testing.expectEqual('p1', ps.namedItem('p1').id);
|
||||
testing.expectEqual('p2', ps.namedItem('p2').id);
|
||||
testing.expectEqual('p3', ps.namedItem('p3').id);
|
||||
testing.expectEqual(null, ps.namedItem('p4'));
|
||||
const ps = document.getElementsByTagName('p');
|
||||
testing.expectEqual('p1', ps.namedItem('p1').id);
|
||||
testing.expectEqual('p2', ps.namedItem('p2').id);
|
||||
testing.expectEqual('p3', ps.namedItem('p3').id);
|
||||
testing.expectEqual(null, ps.namedItem('p4'));
|
||||
|
||||
testing.expectEqual('p1', ps['p1'].id);
|
||||
testing.expectEqual('p1', ps['p1'].id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=iterator>
|
||||
let acc = [];
|
||||
for (let x of ps) {
|
||||
acc.push(x.id);
|
||||
{
|
||||
const ps = document.getElementsByTagName('p');
|
||||
let acc = [];
|
||||
for (let x of ps) {
|
||||
acc.push(x.id);
|
||||
}
|
||||
testing.expectEqual(['p1', 'p2', 'p3'], acc);
|
||||
}
|
||||
testing.expectEqual(['p1', 'p2', 'p3'], acc);
|
||||
</script>
|
||||
|
||||
<script id=extendedTags>
|
||||
|
||||
@@ -123,7 +123,8 @@
|
||||
const signal = AbortSignal.abort();
|
||||
|
||||
testing.expectEqual(true, signal.aborted);
|
||||
testing.expectEqual("AbortError", signal.reason);
|
||||
testing.expectEqual(true, signal.reason instanceof DOMException);
|
||||
testing.expectEqual("AbortError", signal.reason.name);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -229,7 +230,8 @@
|
||||
a1.abort();
|
||||
testing.expectEqual(true, s1.aborted)
|
||||
testing.expectEqual(s1, target)
|
||||
testing.expectEqual('AbortError', s1.reason)
|
||||
testing.expectEqual(true, s1.reason instanceof DOMException)
|
||||
testing.expectEqual('AbortError', s1.reason.name)
|
||||
testing.expectEqual(1, called)
|
||||
</script>
|
||||
|
||||
@@ -237,7 +239,80 @@
|
||||
var s2 = AbortSignal.abort('over 9000');
|
||||
testing.expectEqual(true, s2.aborted);
|
||||
testing.expectEqual('over 9000', s2.reason);
|
||||
testing.expectEqual('AbortError', AbortSignal.abort().reason);
|
||||
testing.expectEqual('AbortError', AbortSignal.abort().reason.name);
|
||||
</script>
|
||||
|
||||
<script id=abortSignalAnyEmpty>
|
||||
{
|
||||
const signal = AbortSignal.any([]);
|
||||
testing.expectEqual(false, signal.aborted);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=abortSignalAnyAlreadyAborted>
|
||||
{
|
||||
const aborted = AbortSignal.abort("already gone");
|
||||
const c = new AbortController();
|
||||
const signal = AbortSignal.any([c.signal, aborted]);
|
||||
|
||||
testing.expectEqual(true, signal.aborted);
|
||||
testing.expectEqual("already gone", signal.reason);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=abortSignalAnyPropagates>
|
||||
{
|
||||
const c1 = new AbortController();
|
||||
const c2 = new AbortController();
|
||||
const signal = AbortSignal.any([c1.signal, c2.signal]);
|
||||
|
||||
let eventFired = false;
|
||||
signal.addEventListener('abort', () => {
|
||||
eventFired = true;
|
||||
});
|
||||
|
||||
testing.expectEqual(false, signal.aborted);
|
||||
|
||||
c2.abort("second");
|
||||
|
||||
testing.expectEqual(true, signal.aborted);
|
||||
testing.expectEqual("second", signal.reason);
|
||||
testing.expectEqual(true, eventFired);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=abortSignalAnyFirstSourceWins>
|
||||
{
|
||||
const c1 = new AbortController();
|
||||
const c2 = new AbortController();
|
||||
const signal = AbortSignal.any([c1.signal, c2.signal]);
|
||||
|
||||
c1.abort("first");
|
||||
c2.abort("second");
|
||||
|
||||
testing.expectEqual(true, signal.aborted);
|
||||
testing.expectEqual("first", signal.reason);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=abortSignalAnyTransitive>
|
||||
{
|
||||
const c = new AbortController();
|
||||
const mid = AbortSignal.any([c.signal]);
|
||||
const leaf = AbortSignal.any([mid]);
|
||||
|
||||
let leafFired = false;
|
||||
leaf.addEventListener('abort', () => {
|
||||
leafFired = true;
|
||||
});
|
||||
|
||||
c.abort("root");
|
||||
|
||||
testing.expectEqual(true, mid.aborted);
|
||||
testing.expectEqual(true, leaf.aborted);
|
||||
testing.expectEqual("root", leaf.reason);
|
||||
testing.expectEqual(true, leafFired);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=abortsignal_timeout type=module>
|
||||
@@ -250,8 +325,9 @@
|
||||
|
||||
await state.done(() => {
|
||||
testing.expectEqual(true, s3.aborted);
|
||||
testing.expectEqual('TimeoutError', s3.reason);
|
||||
testing.expectError('Error: TimeoutError', () => {
|
||||
testing.expectEqual(true, s3.reason instanceof DOMException);
|
||||
testing.expectEqual('TimeoutError', s3.reason.name);
|
||||
testing.expectError('TimeoutError: The operation timed out', () => {
|
||||
s3.throwIfAborted()
|
||||
});
|
||||
});
|
||||
|
||||
@@ -613,6 +613,52 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id=once_readd_test></div>
|
||||
<script id=onceListenerReaddedDuringNestedDispatch>
|
||||
// Regression: a `once: true` listener that re-adds itself inside its
|
||||
// callback and triggers a nested dispatch must see the *new* registration
|
||||
// fire. The original `once` instance is `removed=true` but still parked
|
||||
// in the listener list until the outer dispatch flushes deferred
|
||||
// removals, so `register`'s duplicate check has to skip removed entries
|
||||
// — otherwise the re-add is silently swallowed as a "duplicate".
|
||||
{
|
||||
const el = $('#once_readd_test');
|
||||
let invoked = 0;
|
||||
function h() {
|
||||
invoked++;
|
||||
if (invoked == 1) el.addEventListener('reonce', h, {once: true});
|
||||
if (invoked <= 2) el.dispatchEvent(new Event('reonce'));
|
||||
}
|
||||
el.addEventListener('reonce', h, {once: true});
|
||||
el.dispatchEvent(new Event('reonce'));
|
||||
testing.expectEqual(2, invoked);
|
||||
|
||||
// Third dispatch shouldn't fire anything: every prior registration was
|
||||
// `once` and has been consumed.
|
||||
el.dispatchEvent(new Event('reonce'));
|
||||
testing.expectEqual(2, invoked);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id=once_self_remove_test></div>
|
||||
<script id=onceListenerExplicitlyRemovesItself>
|
||||
// Regression: a `once: true` listener that also calls removeEventListener
|
||||
// on itself must not double-defer the same node (which would double-free
|
||||
// on outer-dispatch cleanup).
|
||||
{
|
||||
const el = $('#once_self_remove_test');
|
||||
let calls = 0;
|
||||
const fn = () => {
|
||||
calls++;
|
||||
el.removeEventListener('osr', fn);
|
||||
};
|
||||
el.addEventListener('osr', fn, {once: true});
|
||||
el.dispatchEvent(new Event('osr'));
|
||||
el.dispatchEvent(new Event('osr'));
|
||||
testing.expectEqual(1, calls);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=isTrusted>
|
||||
// Test isTrusted property on generic Event
|
||||
let untrustedEvent = new Event('test');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -23,6 +23,7 @@ const js = @import("../js/js.zig");
|
||||
|
||||
const Event = @import("Event.zig");
|
||||
const EventTarget = @import("EventTarget.zig");
|
||||
const DOMException = @import("DOMException.zig");
|
||||
|
||||
const log = lp.log;
|
||||
const Execution = js.Execution;
|
||||
@@ -31,8 +32,11 @@ const AbortSignal = @This();
|
||||
|
||||
_proto: *EventTarget,
|
||||
_aborted: bool = false,
|
||||
_is_dependent: bool = false,
|
||||
_reason: Reason = .undefined,
|
||||
_on_abort: ?js.Function.Global = null,
|
||||
_dependents: std.ArrayList(*AbortSignal) = .{},
|
||||
_source_signals: std.ArrayList(*AbortSignal) = .{},
|
||||
|
||||
pub fn init(exec: *const Execution) !*AbortSignal {
|
||||
return exec._factory.eventTarget(AbortSignal{
|
||||
@@ -65,19 +69,41 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, exec: *const Execution) !void
|
||||
return;
|
||||
}
|
||||
|
||||
self._aborted = true;
|
||||
try self.markAborted(reason_, exec);
|
||||
|
||||
// Store the abort reason (default to a simple string if none provided)
|
||||
// Per spec: mark all direct dependents aborted (with this signal's reason)
|
||||
// BEFORE firing any abort events. The graph is flattened at any() creation,
|
||||
// so we never need to recurse here.
|
||||
var to_dispatch: std.ArrayList(*AbortSignal) = .{};
|
||||
for (self._dependents.items) |dep| {
|
||||
if (dep._aborted) continue;
|
||||
try dep.markAborted(self._reason, exec);
|
||||
try to_dispatch.append(exec.arena, dep);
|
||||
}
|
||||
|
||||
try self.dispatchAbortEvent(exec);
|
||||
for (to_dispatch.items) |dep| {
|
||||
dep.dispatchAbortEvent(exec) catch |err| {
|
||||
log.warn(.app, "abort dependent dispatch", .{ .err = err });
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn markAborted(self: *AbortSignal, reason_: ?Reason, exec: *const Execution) !void {
|
||||
self._aborted = true;
|
||||
if (reason_) |reason| {
|
||||
switch (reason) {
|
||||
.dom => |dom| self._reason = .{ .dom = dom },
|
||||
.js_val => |js_val| self._reason = .{ .js_val = js_val },
|
||||
.string => |str| self._reason = .{ .string = try exec.dupeString(str) },
|
||||
.undefined => self._reason = reason,
|
||||
}
|
||||
} else {
|
||||
self._reason = .{ .string = "AbortError" };
|
||||
self._reason = .{ .dom = DOMException.fromError(error.AbortError).? };
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatchAbortEvent(self: *AbortSignal, exec: *const Execution) !void {
|
||||
const target = self.asEventTarget();
|
||||
const on_abort = self._on_abort;
|
||||
switch (exec.context.global) {
|
||||
@@ -97,6 +123,31 @@ pub fn createAborted(reason_: ?js.Value.Global, exec: *const Execution) !*AbortS
|
||||
return signal;
|
||||
}
|
||||
|
||||
pub fn createAny(signals: []const *AbortSignal, exec: *const Execution) !*AbortSignal {
|
||||
const result = try init(exec);
|
||||
for (signals) |source| {
|
||||
if (source._aborted) {
|
||||
try result.abort(source._reason, exec);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
result._is_dependent = true;
|
||||
|
||||
for (signals) |source| {
|
||||
if (!source._is_dependent) {
|
||||
try source._dependents.append(exec.arena, result);
|
||||
try result._source_signals.append(exec.arena, source);
|
||||
} else {
|
||||
for (source._source_signals.items) |s| {
|
||||
try s._dependents.append(exec.arena, result);
|
||||
try result._source_signals.append(exec.arena, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn createTimeout(delay: u32, exec: *const Execution) !*AbortSignal {
|
||||
const callback = try exec.arena.create(TimeoutCallback);
|
||||
callback.* = .{
|
||||
@@ -120,9 +171,10 @@ pub fn throwIfAborted(self: *const AbortSignal, exec: *const Execution) !ThrowIf
|
||||
|
||||
if (self._aborted) {
|
||||
const exception = switch (self._reason) {
|
||||
.string => |str| local.throw(str),
|
||||
.js_val => |js_val| local.throw(try local.toLocal(js_val).toStringSlice()),
|
||||
.undefined => local.throw("AbortError"),
|
||||
.dom => |err| local.newException(err),
|
||||
.string => |str| local.newException(str),
|
||||
.js_val => |js_val| local.newException(js_val),
|
||||
.undefined => local.newException(DOMException.fromError(error.AbortError).?),
|
||||
};
|
||||
return .{ .exception = exception };
|
||||
}
|
||||
@@ -131,6 +183,7 @@ pub fn throwIfAborted(self: *const AbortSignal, exec: *const Execution) !ThrowIf
|
||||
|
||||
const Reason = union(enum) {
|
||||
js_val: js.Value.Global,
|
||||
dom: DOMException,
|
||||
string: []const u8,
|
||||
undefined: void,
|
||||
};
|
||||
@@ -141,7 +194,7 @@ const TimeoutCallback = struct {
|
||||
|
||||
fn run(ctx: *anyopaque) !?u32 {
|
||||
const self: *TimeoutCallback = @ptrCast(@alignCast(ctx));
|
||||
self.signal.abort(.{ .string = "TimeoutError" }, self.exec) catch |err| {
|
||||
self.signal.abort(.{ .dom = DOMException.fromError(error.TimeoutError).? }, self.exec) catch |err| {
|
||||
log.warn(.app, "abort signal timeout", .{ .err = err });
|
||||
};
|
||||
return null;
|
||||
@@ -169,5 +222,6 @@ pub const JsApi = struct {
|
||||
|
||||
// Static method
|
||||
pub const abort = bridge.function(AbortSignal.createAborted, .{ .static = true });
|
||||
pub const any = bridge.function(AbortSignal.createAny, .{ .static = true });
|
||||
pub const timeout = bridge.function(AbortSignal.createTimeout, .{ .static = true });
|
||||
};
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -118,7 +118,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");
|
||||
|
||||
@@ -617,20 +617,44 @@ pub fn replaceChildren(self: *Document, nodes: []const Node.NodeOrText, frame: *
|
||||
}
|
||||
|
||||
pub fn elementFromPoint(self: *Document, x: f64, y: f64, frame: *Frame) !?*Element {
|
||||
// Traverse document in depth-first order to find the topmost (last in document order)
|
||||
// element that contains the point (x, y)
|
||||
// DFS in document order; topmost = last visited element whose rect contains (x, y).
|
||||
//
|
||||
// Faux-layout shortcut: rect.top is calculateDocumentPosition × 5, which is
|
||||
// monotonically increasing in document order. So we maintain a running
|
||||
// preorder counter instead of calling calculateDocumentPosition per node
|
||||
// (which itself is O(N)). Once the counter's y passes the query y, no
|
||||
// later element can contain the point, and we can return.
|
||||
//
|
||||
// We also share a single VisibilityCache across all elements so the
|
||||
// ancestor-walk inside isHidden gets amortized.
|
||||
var topmost: ?*Element = null;
|
||||
|
||||
const root = self.asNode();
|
||||
var stack: std.ArrayList(*Node) = .empty;
|
||||
try stack.append(frame.call_arena, root);
|
||||
|
||||
var visibility_cache: Element.VisibilityCache = .{};
|
||||
var preorder_index: f64 = 0;
|
||||
|
||||
while (stack.items.len > 0) {
|
||||
const node = stack.pop() orelse break;
|
||||
const pos = preorder_index * 5.0;
|
||||
|
||||
if (pos > y) {
|
||||
// Monotonic: no later element has top <= y, so none can contain (x, y).
|
||||
return topmost;
|
||||
}
|
||||
|
||||
preorder_index += 1;
|
||||
if (node.is(Element)) |element| {
|
||||
if (element.checkVisibilityCached(null, frame)) {
|
||||
const rect = element.getBoundingClientRectForVisible(frame);
|
||||
if (x >= rect.getLeft() and x <= rect.getRight() and y >= rect.getTop() and y <= rect.getBottom()) {
|
||||
if (element.checkVisibilityCached(&visibility_cache, frame)) {
|
||||
const dims = element.getElementDimensions(frame);
|
||||
// x and y both come from preorder position in our faux layout.
|
||||
const left = pos;
|
||||
const top = pos;
|
||||
const right = pos + dims.width;
|
||||
const bottom = pos + dims.height;
|
||||
if (x >= left and x <= right and y >= top and y <= bottom) {
|
||||
topmost = element;
|
||||
}
|
||||
}
|
||||
@@ -1136,15 +1160,15 @@ 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 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 });
|
||||
pub const close = bridge.function(Document.close, .{ .dom_exception = true });
|
||||
pub const doctype = bridge.accessor(Document.getDocType, 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);
|
||||
}
|
||||
|
||||
@@ -1144,7 +1135,7 @@ pub fn checkVisibility(self: *Element, opts_: ?CheckVisibilityOpts, frame: *Fram
|
||||
});
|
||||
}
|
||||
|
||||
fn getElementDimensions(self: *Element, frame: *Frame) struct { width: f64, height: f64 } {
|
||||
pub fn getElementDimensions(self: *Element, frame: *Frame) struct { width: f64, height: f64 } {
|
||||
var width: f64 = 5.0;
|
||||
var height: f64 = 5.0;
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -48,6 +48,7 @@ _event_phase: EventPhase = .none,
|
||||
_time_stamp: u64,
|
||||
_needs_retargeting: bool = false,
|
||||
_is_trusted: bool = false,
|
||||
_in_passive_listener: bool = false,
|
||||
|
||||
// There's a period of time between creating an event and handing it off to v8
|
||||
// where things can fail. If it does fail, we need to deinit the event. The timing
|
||||
@@ -204,7 +205,7 @@ pub fn getCurrentTarget(self: *const Event) ?*EventTarget {
|
||||
}
|
||||
|
||||
pub fn preventDefault(self: *Event) void {
|
||||
if (self._cancelable) {
|
||||
if (self._cancelable and !self._in_passive_listener) {
|
||||
self._prevent_default = true;
|
||||
}
|
||||
}
|
||||
@@ -229,7 +230,7 @@ pub fn getReturnValue(self: *const Event) bool {
|
||||
pub fn setReturnValue(self: *Event, v: bool) void {
|
||||
if (!v) {
|
||||
// Setting returnValue=false is equivalent to preventDefault()
|
||||
if (self._cancelable) {
|
||||
if (self._cancelable and !self._in_passive_listener) {
|
||||
self._prevent_default = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -156,7 +156,7 @@ pub const JsApi = struct {
|
||||
return error.NotHandled;
|
||||
}
|
||||
|
||||
return self.getByName(name, frame);
|
||||
return self.getByName(name, frame) orelse error.NotHandled;
|
||||
}
|
||||
}.wrap, null, null, .{ .null_as_undefined = 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, .{});
|
||||
};
|
||||
@@ -536,8 +536,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
|
||||
|
||||
@@ -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(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(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(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(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(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(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 {
|
||||
|
||||
@@ -42,6 +42,7 @@ pub fn processMessage(cmd: *CDP.Command) !void {
|
||||
scrollNode,
|
||||
waitForSelector,
|
||||
handleJavaScriptDialog,
|
||||
configureLoading,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
@@ -56,9 +57,20 @@ pub fn processMessage(cmd: *CDP.Command) !void {
|
||||
.scrollNode => return scrollNode(cmd),
|
||||
.waitForSelector => return waitForSelector(cmd),
|
||||
.handleJavaScriptDialog => return handleJavaScriptDialog(cmd),
|
||||
.configureLoading => return configureLoading(cmd),
|
||||
}
|
||||
}
|
||||
|
||||
fn configureLoading(cmd: *CDP.Command) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
subFrame: bool = true,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.NoBrowserContext;
|
||||
bc.session.subframe_loading_enabled = params.subFrame;
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
fn getSemanticTree(cmd: anytype) !void {
|
||||
const Params = struct {
|
||||
format: ?enum { text } = null,
|
||||
|
||||
29
src/cli.zig
29
src/cli.zig
@@ -268,7 +268,17 @@ pub fn Builder(comptime commands: anytype) type {
|
||||
inline for (commands) |command| {
|
||||
// Match a command.
|
||||
if (std.mem.eql(u8, cmd_str, command.name)) {
|
||||
return .{ exec_name, try parseCommand(allocator, command, &args) };
|
||||
const cmd_parsed = parseCommand(allocator, command, &args) catch |err| {
|
||||
if (err == error.HelpRequested) {
|
||||
// <subcommand> help requested, return help <subcommand>
|
||||
var h = @FieldType(Union, "help"){};
|
||||
if (@hasField(@FieldType(Union, "help"), "subcommand")) {
|
||||
h.subcommand = command.name;
|
||||
}
|
||||
return .{ exec_name, @unionInit(Union, "help", h) };
|
||||
} else return err;
|
||||
};
|
||||
return .{ exec_name, cmd_parsed };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,7 +301,17 @@ pub fn Builder(comptime commands: anytype) type {
|
||||
|
||||
inline for (commands) |command| {
|
||||
if (std.mem.eql(u8, @tagName(command_enum), command.name)) {
|
||||
return .{ exec_name, try parseCommand(allocator, command, &args) };
|
||||
const cmd_parsed = parseCommand(allocator, command, &args) catch |err| {
|
||||
if (err == error.HelpRequested) {
|
||||
// <subcommand> help requested, return help <subcommand>
|
||||
var h = @FieldType(Union, "help"){};
|
||||
if (@hasField(@FieldType(Union, "help"), "subcommand")) {
|
||||
h.subcommand = command.name;
|
||||
}
|
||||
return .{ exec_name, @unionInit(Union, "help", h) };
|
||||
} else return err;
|
||||
};
|
||||
return .{ exec_name, cmd_parsed };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -589,6 +609,11 @@ pub fn Builder(comptime commands: anytype) type {
|
||||
}
|
||||
}
|
||||
|
||||
// Subcommand help: `lightpanda fetch help` or `lightpanda fetch --help`
|
||||
if (std.mem.eql(u8, option_name, "help") or std.mem.eql(u8, option_name, "--help")) {
|
||||
return error.HelpRequested;
|
||||
}
|
||||
|
||||
// Encountered an option we don't know of.
|
||||
if (std.mem.startsWith(u8, option_name, "--")) {
|
||||
log.fatal(.app, "unknown argument", .{ .mode = command.name, .arg = option_name });
|
||||
|
||||
Reference in New Issue
Block a user