mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-09-15 07:19:20 -04:00
Merge branch 'main' into c-api
This commit is contained in:
commit
f3157935c9
209 files changed
+5057
-1556
No files matched your search
@@ -164,7 +164,7 @@ The output of an agent session is a
|
||||
[PandaScript](https://lightpanda.io/docs/usage/pandascript): vanilla JavaScript
|
||||
with a small set of native browser primitives built directly into Lightpanda.
|
||||
Run `/save` to export one from your current session, then replay it with
|
||||
`lightpanda agent <script>.js`. Scripts are deterministic and token-free, so
|
||||
`lightpanda run <script>.js`. Scripts are deterministic and token-free, so
|
||||
you can prototype with the LLM and ship the output to production without a
|
||||
model at runtime.
|
||||
|
||||
@@ -178,7 +178,7 @@ reference.
|
||||
./lightpanda agent # auto-detects API key from env
|
||||
./lightpanda agent --task "top story on news.ycombinator.com?"
|
||||
./lightpanda agent --no-llm # basic REPL, no LLM
|
||||
./lightpanda agent session.js # run a recorded script
|
||||
./lightpanda run session.js # run a recorded script
|
||||
./lightpanda agent --provider gemini --task "..." # force a specific provider
|
||||
VERTEX_API_KEY=... ./lightpanda agent --provider vertex # Vertex AI, express mode
|
||||
GOOGLE_CLOUD_PROJECT=my-proj ./lightpanda agent --provider vertex # Vertex AI, token via gcloud auth
|
||||
|
||||
@@ -42,12 +42,39 @@ const Build = blk: {
|
||||
};
|
||||
|
||||
pub fn build(b: *Build) !void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
// The three settings below only work as a set, so they get one knob rather
|
||||
// than three defaults: the self-hosted backend needs the shared V8 (it
|
||||
// cannot apply the CREL relocations in the archive) and needs zig's own CRT
|
||||
// (a system crt1.o with SFrame unwind data has relocations its linker does
|
||||
// not handle). Debug-only, opt-in, and a good deal faster to rebuild.
|
||||
const dev_fast = b.option(bool, "dev_fast", "Linux debug builds: shared V8 + self-hosted backend. Implies -Dshared_v8, -Duse_llvm=false and a bundled-CRT target") orelse false;
|
||||
|
||||
const target = if (dev_fast) b.resolveTargetQuery(.{
|
||||
.cpu_arch = .x86_64,
|
||||
.os_tag = .linux,
|
||||
.abi = .gnu,
|
||||
// https://codeberg.org/ziglang/zig/issues/31272
|
||||
.glibc_version = .{ .major = 2, .minor = 43, .patch = 0 },
|
||||
}) else b.standardTargetOptions(.{});
|
||||
|
||||
if (dev_fast) {
|
||||
if (builtin.os.tag != .linux) {
|
||||
std.debug.print("-Ddev_fast is Linux-only (host is {s})\n", .{@tagName(builtin.os.tag)});
|
||||
return error.DevFastUnsupportedHost;
|
||||
}
|
||||
if (optimize != .Debug) {
|
||||
std.debug.print("-Ddev_fast is Debug-only (optimize is {s})\n", .{@tagName(optimize)});
|
||||
return error.DevFastRequiresDebug;
|
||||
}
|
||||
}
|
||||
|
||||
const prebuilt_v8_path = b.option([]const u8, "prebuilt_v8_path", "Path to prebuilt libc_v8.a");
|
||||
const snapshot_path = b.option([]const u8, "snapshot_path", "Path to v8 snapshot");
|
||||
const wpt_extensions = b.option(bool, "wpt_extensions", "Extend WebAPI with WPT driver behavior") orelse false;
|
||||
const shared_v8 = b.option(bool, "shared_v8", "Link V8 as a shared library") orelse dev_fast;
|
||||
const use_llvm = b.option(bool, "use_llvm", "Use the LLVM backend") orelse !dev_fast;
|
||||
|
||||
const version = resolveVersion(b);
|
||||
std.debug.print("Lightpanda {f}\n", .{version});
|
||||
@@ -88,7 +115,7 @@ pub fn build(b: *Build) !void {
|
||||
// Set default behavior
|
||||
b.default_step.dependOn(fmt_step);
|
||||
|
||||
try linkV8(b, mod, enable_asan, enable_tsan, prebuilt_v8_path);
|
||||
try linkV8(b, mod, enable_asan, enable_tsan, prebuilt_v8_path, shared_v8);
|
||||
try linkCurl(b, mod, enable_tsan);
|
||||
try linkHtml5Ever(b, mod);
|
||||
linkZenai(b, mod);
|
||||
@@ -117,7 +144,7 @@ pub fn build(b: *Build) !void {
|
||||
// browser
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "lightpanda",
|
||||
.use_llvm = true,
|
||||
.use_llvm = use_llvm,
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
@@ -154,7 +181,7 @@ pub fn build(b: *Build) !void {
|
||||
// snapshot creator
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "lightpanda-snapshot-creator",
|
||||
.use_llvm = true,
|
||||
.use_llvm = use_llvm,
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main_snapshot_creator.zig"),
|
||||
.target = target,
|
||||
@@ -184,7 +211,7 @@ pub fn build(b: *Build) !void {
|
||||
// skills generator
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "lightpanda-skills",
|
||||
.use_llvm = true,
|
||||
.use_llvm = use_llvm,
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main_skills.zig"),
|
||||
.target = target,
|
||||
@@ -216,7 +243,7 @@ pub fn build(b: *Build) !void {
|
||||
// test
|
||||
const tests = b.addTest(.{
|
||||
.root_module = lightpanda_module,
|
||||
.use_llvm = true,
|
||||
.use_llvm = use_llvm,
|
||||
.test_runner = .{ .path = b.path("src/test_runner.zig"), .mode = .simple },
|
||||
});
|
||||
const run_tests = b.addRunArtifact(tests);
|
||||
@@ -352,6 +379,7 @@ fn linkV8(
|
||||
is_asan: bool,
|
||||
is_tsan: bool,
|
||||
prebuilt_v8_path: ?[]const u8,
|
||||
shared_v8: bool,
|
||||
) !void {
|
||||
const target = mod.resolved_target.?;
|
||||
|
||||
@@ -364,6 +392,7 @@ fn linkV8(
|
||||
.v8_enable_sandbox = is_tsan,
|
||||
.cache_root = b.pathFromRoot(".lp-cache"),
|
||||
.prebuilt_v8_path = prebuilt_v8_path,
|
||||
.shared_v8 = shared_v8,
|
||||
});
|
||||
mod.addImport("v8", dep.module("v8"));
|
||||
}
|
||||
|
||||
+4
-4
@@ -5,8 +5,8 @@
|
||||
.minimum_zig_version = "0.16.0",
|
||||
.dependencies = .{
|
||||
.v8 = .{
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/508ae5ef169b7202886decd751ccbde1f872e0cb.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH62D1AgDOizgZ_Il7HVloE_bAK3H4oQnxLcgeKGyW",
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/db264d2c4d70c09e102167799e99cebb8a74713f.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH6wsDAwA_VE-G-JbVC9XnMTGzeuYmzGPmxHpj0KIF",
|
||||
},
|
||||
// .v8 = .{ .path = "../zig-v8-fork" },
|
||||
.brotli = .{
|
||||
@@ -36,8 +36,8 @@
|
||||
.hash = "sqlite3-3.53.2-DMxLWuAOAAA_Px0arJOIOaP4AKEu5prbsQgPMA35W1zz",
|
||||
},
|
||||
.zenai = .{
|
||||
.url = "git+https://github.com/lightpanda-io/zenai.git#dcf17cf1c944c8b16e5ecb7419374b134cf47df3",
|
||||
.hash = "zenai-0.0.0-iOY_VH4NBgA-zH-jOigwD0H4QTUSq1Tvm5qH99XNqd7a",
|
||||
.url = "git+https://github.com/lightpanda-io/zenai.git#f721fd4171afb95c65182923554d0f1de10d99bf",
|
||||
.hash = "zenai-0.0.0-iOY_VNgmBgBok23JrHb7N0U67VKSSqRb0BJWIpMMcnoy",
|
||||
},
|
||||
.isocline = .{
|
||||
.url = "git+https://github.com/arrufat/isocline?ref=lightpanda#832a9fe25f5f4458fcc47b5acc7c21db669c2f47",
|
||||
|
||||
@@ -191,6 +191,7 @@ const CommonOptions = .{
|
||||
.{ .name = "log_filter_scopes", .type = log.FilterRule, .multiple = true, .validator = logFilterScopesValidator },
|
||||
.{ .name = "user_agent_suffix", .type = ?[]const u8 },
|
||||
.{ .name = "http_cache_dir", .type = ?[]const u8 },
|
||||
.{ .name = "http_cache_entry_limit", .type = ?u32, .default = 1000 },
|
||||
.{ .name = "web_bot_auth_key_file", .type = ?[]const u8 },
|
||||
.{ .name = "web_bot_auth_keyid", .type = ?[]const u8 },
|
||||
.{ .name = "web_bot_auth_domain", .type = ?[]const u8 },
|
||||
@@ -639,6 +640,13 @@ pub fn httpCacheDir(self: *const Config) ?[]const u8 {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn httpCacheEntryLimit(self: *const Config) u32 {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch, .mcp, .agent => |opts| opts.http_cache_entry_limit.?,
|
||||
else => 1000,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cookieFile(self: *const Config) ?[]const u8 {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch, .mcp, .agent, .embed => |opts| opts.cookie,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const posix = std.posix;
|
||||
const sys_net = @import("sys/net.zig");
|
||||
const URL = @import("browser/URL.zig");
|
||||
|
||||
@@ -76,6 +77,10 @@ pub fn run(self: *TestHTTPServer, wg: *lp.WaitGroup) !void {
|
||||
fn handleConnection(self: *TestHTTPServer, conn: std.Io.net.Stream) !void {
|
||||
defer conn.close(lp.io);
|
||||
|
||||
if (@hasDecl(posix.TCP, "NODELAY")) {
|
||||
posix.setsockopt(conn.socket.handle, posix.IPPROTO.TCP, posix.TCP.NODELAY, &std.mem.toBytes(@as(c_int, 1))) catch {};
|
||||
}
|
||||
|
||||
var req_buf: [2048]u8 = undefined;
|
||||
var conn_reader = conn.reader(lp.io, &req_buf);
|
||||
var conn_writer = conn.writer(lp.io, &req_buf);
|
||||
|
||||
@@ -87,6 +87,10 @@ fn runImpl(self: *TestWSServer, wg: *lp.WaitGroup) !void {
|
||||
fn handleClient(client: posix.socket_t) void {
|
||||
defer _ = std.c.close(client);
|
||||
|
||||
if (@hasDecl(posix.TCP, "NODELAY")) {
|
||||
posix.setsockopt(client, posix.IPPROTO.TCP, posix.TCP.NODELAY, &std.mem.toBytes(@as(c_int, 1))) catch {};
|
||||
}
|
||||
|
||||
var buf: [4096]u8 = undefined;
|
||||
const n = posix.read(client, &buf) catch return;
|
||||
|
||||
|
||||
+2
-2
@@ -1494,7 +1494,7 @@ fn runCommand(self: *Agent, arena: std.mem.Allocator, cmd: Command) browser_tool
|
||||
.text = switch (err) {
|
||||
error.OutOfMemory => "out of memory",
|
||||
error.FrameNotLoaded => "no page loaded — run /goto <url> first",
|
||||
else => std.fmt.allocPrint(arena, "{s} failed: {s}", .{ tc.name(), @errorName(err) }) catch "tool failed",
|
||||
else => std.fmt.allocPrint(arena, "{s} failed: {s}", .{ tc.name(), browser_tools.errorMessage(err) }) catch "tool failed",
|
||||
},
|
||||
.is_error = true,
|
||||
};
|
||||
@@ -1927,7 +1927,7 @@ fn handleToolCall(ctx: *anyopaque, allocator: std.mem.Allocator, tool_name: []co
|
||||
const outcome: zenai.provider.Client.ToolHandler.Result = if (browser_tools.call(allocator, self.session, &self.node_registry, tool_name, arguments)) |result|
|
||||
.{ .content = capToolOutput(allocator, tool_name, result.text), .is_error = result.is_error }
|
||||
else |err|
|
||||
.{ .content = std.fmt.allocPrint(allocator, "Error: {s}", .{@errorName(err)}) catch "Error: tool execution failed", .is_error = true };
|
||||
.{ .content = std.fmt.allocPrint(allocator, "Error: {s}", .{browser_tools.errorMessage(err)}) catch "Error: tool execution failed", .is_error = true };
|
||||
|
||||
self.terminal.agentToolDone(tool_name, args_str, !outcome.is_error);
|
||||
if (self.terminal.verbosity == .high) self.terminal.printToolOutcome(tool_name, outcome.content, outcome.is_error);
|
||||
|
||||
+152
-67
@@ -134,18 +134,24 @@ pub fn hasDirectListeners(self: *EventManager, target: *EventTarget, typ: []cons
|
||||
}
|
||||
|
||||
fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void {
|
||||
{
|
||||
const et = target.asEventTarget();
|
||||
event._target = et;
|
||||
event._dispatch_target = et; // Store original target for composedPath()
|
||||
const target_et = target.asEventTarget();
|
||||
event._target = target_et;
|
||||
event._dispatch_target = target_et; // Store original target for composedPath()
|
||||
|
||||
// Retarget the relatedTarget against the dispatch target up front
|
||||
// (DOM dispatch step 4); listeners observe the retargeted value and
|
||||
// it survives the dispatch.
|
||||
if (event.relatedTargetPtr()) |related_ptr| {
|
||||
if (related_ptr.*) |related| {
|
||||
related_ptr.* = getAdjustedTarget(related, et);
|
||||
}
|
||||
// The relatedTarget as authored. Every invocation sees it retargeted
|
||||
// against its own currentTarget (DOM dispatch step 5.7), so the event
|
||||
// keeps the unadjusted value between invocations.
|
||||
const original_related: ?*EventTarget = if (event.relatedTargetPtr()) |p| p.* else null;
|
||||
event._dispatch_related_target = original_related;
|
||||
if (original_related) |related| {
|
||||
if (rootIsShadowRoot(related)) {
|
||||
event._needs_retargeting = true;
|
||||
}
|
||||
// DOM dispatch step 5: an event whose relatedTarget retargets onto the
|
||||
// target itself isn't dispatched at all.
|
||||
const adjusted = getAdjustedTarget(related, target_et);
|
||||
if (adjusted == target_et and related != target_et) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,8 +199,12 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void {
|
||||
related_ptr.* = null;
|
||||
}
|
||||
} else if (event._needs_retargeting and node_path_len > 0) {
|
||||
const adjusted = getAdjustedTarget(event._dispatch_target, path_buffer[node_path_len - 1]);
|
||||
const last = path_buffer[node_path_len - 1];
|
||||
const adjusted = getAdjustedTarget(event._dispatch_target, last);
|
||||
event._target = if (rootIsShadowRoot(adjusted)) null else adjusted;
|
||||
if (event.relatedTargetPtr()) |related_ptr| {
|
||||
related_ptr.* = getAdjustedTarget(original_related, last);
|
||||
}
|
||||
}
|
||||
// Handle checkbox/radio activation rollback or commit
|
||||
if (activation_state) |state| {
|
||||
@@ -222,38 +232,12 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void {
|
||||
}
|
||||
}
|
||||
|
||||
const target_root = target.getRootNode(.{});
|
||||
var node: ?*Node = target;
|
||||
while (node) |n| {
|
||||
if (path_len >= path_buffer.len) break;
|
||||
path_buffer[path_len] = n.asEventTarget();
|
||||
path_len += 1;
|
||||
|
||||
// Check if this node is a shadow root
|
||||
if (n.is(ShadowRoot)) |shadow| {
|
||||
event._needs_retargeting = true;
|
||||
|
||||
// A non-composed event stops at its own tree's root.
|
||||
if (!event._composed and n == target_root) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Otherwise, jump to the shadow host and continue
|
||||
node = shadow._host.asNode();
|
||||
continue;
|
||||
}
|
||||
|
||||
// an assigned slottable's event-path parent is its assigned slot,
|
||||
// routing the event into the slot's shadow tree
|
||||
if (frame._assigned_slots.get(n)) |slot| {
|
||||
node = slot.asNode();
|
||||
continue;
|
||||
}
|
||||
|
||||
node = n._parent;
|
||||
}
|
||||
|
||||
const built = buildEventPath(target, event, frame, &path_buffer);
|
||||
path_len = built.len;
|
||||
node_path_len = path_len;
|
||||
if (built.crosses_shadow_root) {
|
||||
event._needs_retargeting = true;
|
||||
}
|
||||
|
||||
// Even though the window isn't part of the DOM, most events propagate
|
||||
// through it in the capture phase. It only participates when the tree's
|
||||
@@ -308,7 +292,6 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void {
|
||||
// Phase 2: At target
|
||||
if (event._stop_propagation) return;
|
||||
event._event_phase = .at_target;
|
||||
const target_et = target.asEventTarget();
|
||||
|
||||
blk: {
|
||||
// Get inline handler (e.g., onclick property) for this target
|
||||
@@ -320,9 +303,11 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void {
|
||||
window._current_event = currentEventForTarget(target_et, event);
|
||||
defer window._current_event = prev_current_event;
|
||||
|
||||
const adjusted: ?AdjustedTargets = if (event._needs_retargeting) .apply(event, target_et) else null;
|
||||
|
||||
// Inline handlers (e.g. onclick property) follow the same "report,
|
||||
// don't propagate" rule as addEventListener listeners — see Listener.run.
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
var caught: js.TryCatch.Caught = .{};
|
||||
const handler_return: ?js.Value = ls.toLocal(inline_handler).tryCallWithThis(js.Value, target_et, .{event}, &caught) catch |err| ret: {
|
||||
if (err == error.ExecutionTerminated) {
|
||||
return error.ExecutionTerminated;
|
||||
@@ -333,6 +318,10 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void {
|
||||
};
|
||||
processHandlerReturnValue(event, handler_return);
|
||||
|
||||
if (adjusted) |a| {
|
||||
a.restore(event);
|
||||
}
|
||||
|
||||
if (event._stop_propagation) {
|
||||
return;
|
||||
}
|
||||
@@ -377,12 +366,9 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void {
|
||||
window._current_event = currentEventForTarget(current_target, event);
|
||||
defer window._current_event = prev_current_event;
|
||||
|
||||
const original_target = event._target;
|
||||
if (event._needs_retargeting) {
|
||||
event._target = getAdjustedTarget(original_target, current_target);
|
||||
}
|
||||
const adjusted: ?AdjustedTargets = if (event._needs_retargeting) .apply(event, current_target) else null;
|
||||
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
var caught: js.TryCatch.Caught = .{};
|
||||
const handler_return: ?js.Value = ls.toLocal(inline_handler).tryCallWithThis(js.Value, current_target, .{event}, &caught) catch |err| ret: {
|
||||
if (err == error.ExecutionTerminated) {
|
||||
return error.ExecutionTerminated;
|
||||
@@ -393,8 +379,8 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void {
|
||||
};
|
||||
processHandlerReturnValue(event, handler_return);
|
||||
|
||||
if (event._needs_retargeting) {
|
||||
event._target = original_target;
|
||||
if (adjusted) |a| {
|
||||
a.restore(event);
|
||||
}
|
||||
|
||||
if (event._stop_propagation) {
|
||||
@@ -493,19 +479,15 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
||||
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;
|
||||
if (event._needs_retargeting) {
|
||||
event._target = getAdjustedTarget(original_target, current_target);
|
||||
}
|
||||
// Compute adjusted targets for shadow DOM retargeting (only if needed)
|
||||
const adjusted: ?AdjustedTargets = if (event._needs_retargeting) .apply(event, current_target) else null;
|
||||
|
||||
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;
|
||||
if (adjusted) |a| {
|
||||
a.restore(event);
|
||||
}
|
||||
|
||||
if (event._stop_immediate_propagation) {
|
||||
@@ -547,6 +529,113 @@ fn getInlineHandler(self: *EventManager, target: *EventTarget, event: *Event) ?j
|
||||
};
|
||||
}
|
||||
|
||||
// An invocation sees the target and the relatedTarget retargeted against its
|
||||
// own currentTarget (DOM dispatch step 5.7). The event carries the unadjusted
|
||||
// values in between, so each invocation adjusts and then restores them.
|
||||
const AdjustedTargets = struct {
|
||||
target: ?*EventTarget,
|
||||
related: ?*EventTarget,
|
||||
related_ptr: ?*?*EventTarget,
|
||||
|
||||
fn apply(event: *Event, current_target: *EventTarget) AdjustedTargets {
|
||||
const related_ptr = event.relatedTargetPtr();
|
||||
const original: AdjustedTargets = .{
|
||||
.target = event._target,
|
||||
.related = if (related_ptr) |p| p.* else null,
|
||||
.related_ptr = related_ptr,
|
||||
};
|
||||
|
||||
event._target = getAdjustedTarget(original.target, current_target);
|
||||
if (related_ptr) |p| {
|
||||
p.* = getAdjustedTarget(original.related, current_target);
|
||||
}
|
||||
return original;
|
||||
}
|
||||
|
||||
fn restore(self: AdjustedTargets, event: *Event) void {
|
||||
event._target = self.target;
|
||||
if (self.related_ptr) |p| {
|
||||
p.* = self.related;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub const EventPath = struct {
|
||||
len: usize,
|
||||
// Whether a shadow root sits on the path, i.e. whether an invocation can
|
||||
// see a target other than the one the event was dispatched at.
|
||||
crosses_shadow_root: bool,
|
||||
};
|
||||
|
||||
// Builds the node portion of an event's propagation path (DOM dispatch step
|
||||
// 5.7) into `buffer`. Window, which follows the document at the end of the
|
||||
// path, is left to the caller: the rules for including it differ between
|
||||
// dispatch and composedPath().
|
||||
pub fn buildEventPath(target: *Node, event: *Event, frame: ?*Frame, buffer: []*EventTarget) EventPath {
|
||||
if (buffer.len == 0) {
|
||||
return .{ .len = 0, .crosses_shadow_root = false };
|
||||
}
|
||||
|
||||
const target_root = target.getRootNode(.{});
|
||||
const related = event._dispatch_related_target;
|
||||
|
||||
// The root of the spec's `target` variable, which moves to each host we
|
||||
// cross on the way out. A node it still contains is inside the current
|
||||
// target's tree, where the event always propagates; the first node beyond
|
||||
// it is where the relatedTarget can cut the path short.
|
||||
var scope_root = target_root;
|
||||
|
||||
buffer[0] = target.asEventTarget();
|
||||
var path: EventPath = .{ .len = 1, .crosses_shadow_root = target.is(ShadowRoot) != null };
|
||||
|
||||
var node = eventPathParent(target, event, target_root, frame);
|
||||
while (node) |n| {
|
||||
if (path.len == buffer.len) {
|
||||
break;
|
||||
}
|
||||
|
||||
const et = n.asEventTarget();
|
||||
if (!isShadowIncludingInclusiveAncestor(scope_root, n)) {
|
||||
// DOM dispatch step 5.7: the path stops at the relatedTarget.
|
||||
if (related != null and getAdjustedTarget(related, et) == et) {
|
||||
break;
|
||||
}
|
||||
scope_root = n.getRootNode(.{});
|
||||
}
|
||||
|
||||
if (n.is(ShadowRoot) != null) {
|
||||
path.crosses_shadow_root = true;
|
||||
}
|
||||
buffer[path.len] = et;
|
||||
path.len += 1;
|
||||
|
||||
node = eventPathParent(n, event, target_root, frame);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
// DOM spec "get the parent" for a node on the event path: an assigned
|
||||
// slottable's parent is its slot, routing the event into the slot's shadow
|
||||
// tree, and a shadow root's is its host — except for a non-composed event,
|
||||
// which stops at the root of the tree it was dispatched in.
|
||||
fn eventPathParent(node: *Node, event: *Event, target_root: *Node, frame: ?*Frame) ?*Node {
|
||||
if (node.is(ShadowRoot)) |shadow| {
|
||||
if (!event._composed and node == target_root) {
|
||||
return null;
|
||||
}
|
||||
return shadow._host.asNode();
|
||||
}
|
||||
|
||||
if (frame) |f| {
|
||||
if (f._assigned_slots.get(node)) |slot| {
|
||||
return slot.asNode();
|
||||
}
|
||||
}
|
||||
|
||||
return node._parent;
|
||||
}
|
||||
|
||||
// DOM spec "retarget": walk original_target out of shadow trees until the
|
||||
// node is visible from current_target's tree.
|
||||
fn getAdjustedTarget(original_target: ?*EventTarget, current_target: *EventTarget) ?*EventTarget {
|
||||
@@ -589,14 +678,10 @@ fn isShadowIncludingInclusiveAncestor(ancestor: *Node, node: *Node) bool {
|
||||
// shadow root. Used for the spec's post-dispatch "clear targets" step.
|
||||
fn rootIsShadowRoot(target_: ?*EventTarget) bool {
|
||||
const target = target_ orelse return false;
|
||||
var current: *Node = switch (target._type) {
|
||||
.node => |n| n,
|
||||
else => return false,
|
||||
return switch (target._type) {
|
||||
.node => |n| n.containingShadowRoot() != null,
|
||||
else => false,
|
||||
};
|
||||
while (current._parent) |p| {
|
||||
current = p;
|
||||
}
|
||||
return current.is(ShadowRoot) != null;
|
||||
}
|
||||
|
||||
// Check if ancestor is an ancestor of (or the same as) node
|
||||
|
||||
@@ -219,6 +219,7 @@ pub const DispatchError = error{
|
||||
pub const DispatchDirectOptions = struct {
|
||||
context: []const u8 = "dispatchDirect",
|
||||
inject_target: bool = true,
|
||||
run_microtasks: bool = true,
|
||||
};
|
||||
|
||||
/// Direct dispatch for non-DOM targets. No propagation - just calls the property
|
||||
@@ -249,7 +250,9 @@ pub fn dispatchDirect(
|
||||
var ls: js.Local.Scope = undefined;
|
||||
ctx.localScope(&ls);
|
||||
defer {
|
||||
ls.local.runMicrotasks();
|
||||
if (comptime opts.run_microtasks) {
|
||||
ls.local.runMicrotasks();
|
||||
}
|
||||
ls.deinit();
|
||||
}
|
||||
|
||||
@@ -287,7 +290,7 @@ pub fn dispatchDirect(
|
||||
// Call the property handler (e.g., onmessage) if present
|
||||
if (getFunction(handler, &ls.local)) |func| {
|
||||
event._current_target = target;
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
var caught: js.TryCatch.Caught = .{};
|
||||
_ = func.tryCallWithThis(void, target, .{event}, &caught) catch |err| {
|
||||
if (err == error.ExecutionTerminated) {
|
||||
return error.ExecutionTerminated;
|
||||
|
||||
@@ -323,7 +323,9 @@ pub fn cdataNode(self: *Factory, cd: Node.CData, leaf: anytype) !*Node.CData {
|
||||
|
||||
// only the CDATASection chain has a middle here (its Text)
|
||||
inline for (3..types.len - 1) |i| {
|
||||
chain.set(i, .{ ._proto = chain.get(i - 1) });
|
||||
const ptr = chain.get(i);
|
||||
ptr.* = .{};
|
||||
setProto(ptr, chain.get(i - 1));
|
||||
}
|
||||
chain.setLeaf(types.len - 1, leaf);
|
||||
return cd_ptr;
|
||||
@@ -474,7 +476,7 @@ pub fn svgElement(self: *Factory, tag_name: []const u8, child: anytype) !*@TypeO
|
||||
const svg_ptr = chain.get(i);
|
||||
svg_ptr.* = .{
|
||||
._tag_name = try String.init(self._arena, tag_name, .{}),
|
||||
._type = unionInit(Element.Svg.Type, chain.get(i + 1)),
|
||||
._type = typeInit(Element.Svg, chain.get(i + 1)),
|
||||
};
|
||||
setProto(svg_ptr, chain.get(i - 1));
|
||||
} else {
|
||||
|
||||
+142
-40
@@ -34,6 +34,7 @@ const h5e = @import("parser/html5ever.zig");
|
||||
const CustomElementReactions = @import("CustomElementReactions.zig");
|
||||
|
||||
const URL = @import("URL.zig");
|
||||
const referrer = @import("referrer.zig");
|
||||
const Blob = @import("webapi/Blob.zig");
|
||||
const FileList = @import("webapi/FileList.zig");
|
||||
const Node = @import("webapi/Node.zig");
|
||||
@@ -135,9 +136,10 @@ _attribute_named_node_map_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attri
|
||||
// that actually access these features via JavaScript, saving 24 bytes per element.
|
||||
_element_styles: Element.StyleLookup = .empty,
|
||||
// Computed-style views handed out by window.getComputedStyle. The computed
|
||||
// variant is a stateless lazy view, so one per element suffices — and Chrome
|
||||
// returns the same object for repeated calls, so identity is also conformance.
|
||||
_element_computed_styles: Element.StyleLookup = .empty,
|
||||
// variant is a stateless lazy view, so one per (element, pseudo-element)
|
||||
// suffices — and Chrome returns the same object for repeated calls, so
|
||||
// identity is also conformance.
|
||||
_element_computed_styles: Element.ComputedStyleLookup = .empty,
|
||||
_element_datasets: Element.DatasetLookup = .empty,
|
||||
_element_class_lists: Element.ClassListLookup = .empty,
|
||||
_element_rel_lists: Element.RelListLookup = .empty,
|
||||
@@ -230,6 +232,10 @@ _customized_builtin_disconnected_callback_invoked: std.AutoHashMapUnmanaged(*Ele
|
||||
// The constructor can access this to get the element being upgraded.
|
||||
_upgrading_element: ?*Node = null,
|
||||
|
||||
// _upgrading_element can be consumed once. A second HTMLElement construction
|
||||
// during upgrade is a TypeError.
|
||||
_upgrading_consumed: bool = false,
|
||||
|
||||
// Set when materializing the fragment parser's context element. The element
|
||||
// is never inserted into the tree so if its a custom element ,we must not run
|
||||
// its constructor (else we'll end up in an endless loop if the constructor
|
||||
@@ -331,6 +337,9 @@ _navigated_options: ?NavigatedOpts = null,
|
||||
_http_status: ?u16 = null,
|
||||
_http_headers: std.ArrayList(HttpHeader) = .empty,
|
||||
|
||||
_referrer: ?[]const u8 = null,
|
||||
referrer_policy: referrer.Policy = .default,
|
||||
|
||||
pub const HttpHeader = struct {
|
||||
name: []const u8,
|
||||
value: []const u8,
|
||||
@@ -569,6 +578,16 @@ pub fn base(self: *const Frame) [:0]const u8 {
|
||||
return self.base_url orelse self.url;
|
||||
}
|
||||
|
||||
fn referrerSource(self: *const Frame) [:0]const u8 {
|
||||
var frame = self;
|
||||
while (std.mem.startsWith(u8, frame.url, "about:")) {
|
||||
// about:blank and about:srcdoc documents aren't valid referrer sources,
|
||||
// use the parents
|
||||
frame = frame.parent orelse return frame.url;
|
||||
}
|
||||
return frame.url;
|
||||
}
|
||||
|
||||
pub fn getTitle(self: *Frame) !?[]const u8 {
|
||||
if (self.window._document.is(Document.HTMLDocument)) |html_doc| {
|
||||
return try html_doc.getTitle(self);
|
||||
@@ -593,8 +612,10 @@ pub fn httpMetadata(self: *const Frame) HttpMetadata {
|
||||
// Add common headers for a request:
|
||||
// * referer
|
||||
pub fn headersForRequest(self: *Frame, transfer: *HttpClient.Transfer) !void {
|
||||
if (std.mem.startsWith(u8, self.url, "http")) {
|
||||
try transfer.addHeader("Referer", self.url, .{});
|
||||
const arena = transfer.arena.allocator();
|
||||
if (try referrer.compute(arena, self.referrer_policy, self.referrerSource(), transfer.req.url)) |ref| {
|
||||
try transfer.addHeader("Referer", ref, .{});
|
||||
transfer.req.referrer_policy = self.referrer_policy;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -635,11 +656,12 @@ pub fn navigate(self: *Frame, request_url: [:0]const u8, opts: NavigateOpts) !vo
|
||||
|
||||
const http_client = &session.browser.http_client;
|
||||
|
||||
// Handle synthetic navigations: about:blank and blob: URLs
|
||||
// Handle synthetic navigations: about:blank, about:srcdoc and blob: URLs
|
||||
const is_about_blank = std.mem.eql(u8, "about:blank", request_url);
|
||||
const is_blob = !is_about_blank and std.mem.startsWith(u8, request_url, "blob:");
|
||||
const is_srcdoc = !is_about_blank and std.mem.eql(u8, "about:srcdoc", request_url);
|
||||
const is_blob = !is_about_blank and !is_srcdoc and std.mem.startsWith(u8, request_url, "blob:");
|
||||
|
||||
if (is_about_blank or is_blob) {
|
||||
if (is_about_blank or is_srcdoc or is_blob) {
|
||||
if (is_blob) {
|
||||
if (!Blob.urlBelongsToOrigin(request_url, opts.initiator_origin)) {
|
||||
log.warn(.js, "invalid blob", .{ .url = request_url });
|
||||
@@ -647,7 +669,12 @@ pub fn navigate(self: *Frame, request_url: [:0]const u8, opts: NavigateOpts) !vo
|
||||
}
|
||||
}
|
||||
|
||||
self.url = if (is_about_blank) "about:blank" else try self.arena.dupeZ(u8, request_url);
|
||||
self.url = if (is_about_blank)
|
||||
"about:blank"
|
||||
else if (is_srcdoc)
|
||||
"about:srcdoc"
|
||||
else
|
||||
try self.arena.dupeZ(u8, request_url);
|
||||
|
||||
// even though about:blank navigations may share the same _data_, we
|
||||
// have to do this to make sure window.location is at a unique _address_.
|
||||
@@ -664,13 +691,17 @@ pub fn navigate(self: *Frame, request_url: [:0]const u8, opts: NavigateOpts) !vo
|
||||
self.origin = try URL.getOrigin(self.arena, request_url[5.. :0]);
|
||||
} else if (self.parent) |parent| {
|
||||
self.origin = parent.origin;
|
||||
if (is_about_blank) {
|
||||
if (is_about_blank or is_srcdoc) {
|
||||
self.base_url = parent.base();
|
||||
// about:blank and about:srcdoc documents inherit their
|
||||
// creator's policy container, including the referrer policy
|
||||
self.referrer_policy = parent.referrer_policy;
|
||||
}
|
||||
} else if (self.window._opener) |opener| {
|
||||
self.origin = opener._frame.origin;
|
||||
if (is_about_blank) {
|
||||
self.base_url = opener._frame.base();
|
||||
self.referrer_policy = opener._frame.referrer_policy;
|
||||
}
|
||||
} else {
|
||||
self.origin = null;
|
||||
@@ -695,6 +726,30 @@ pub fn navigate(self: *Frame, request_url: [:0]const u8, opts: NavigateOpts) !vo
|
||||
const html = try parse_arena.dupe(u8, blob._slice);
|
||||
var parser = Parser.init(parse_arena.allocator(), self.document.asNode(), self, .{ .allow_declarative_shadow = true });
|
||||
parser.parse(html);
|
||||
} else if (is_srcdoc) {
|
||||
// The "response body" is the iframe's srcdoc attribute. Only an
|
||||
// iframe can navigate here (e.g. location = 'about:srcdoc' on a
|
||||
// root frame ends up with an empty document, like Chrome).
|
||||
const content = blk: {
|
||||
const iframe = self.iframe orelse break :blk "";
|
||||
break :blk iframe.asElement().getAttributeSafe(comptime .wrap("srcdoc")) orelse "";
|
||||
};
|
||||
if (content.len == 0) {
|
||||
// the parser emits nothing for an empty input; commit the
|
||||
// same html/head/body scaffolding an empty srcdoc implies
|
||||
self.document.injectBlank(self) catch |err| {
|
||||
log.err(.browser, "inject blank", .{ .err = err });
|
||||
return error.InjectBlankFailed;
|
||||
};
|
||||
} else {
|
||||
const parse_arena = try self.getArena(content.len, "Frame.parseSrcdoc");
|
||||
defer parse_arena.release();
|
||||
// A script executed mid-parse can rewrite the srcdoc attribute,
|
||||
// freeing the value under the parser; parse a copy.
|
||||
const html = try parse_arena.dupe(u8, content);
|
||||
var parser = Parser.init(parse_arena.allocator(), self.document.asNode(), self, .{ .allow_declarative_shadow = true });
|
||||
parser.parse(html);
|
||||
}
|
||||
} else {
|
||||
self.document.injectBlank(self) catch |err| {
|
||||
log.err(.browser, "inject blank", .{ .err = err });
|
||||
@@ -742,6 +797,9 @@ pub fn navigate(self: *Frame, request_url: [:0]const u8, opts: NavigateOpts) !vo
|
||||
self._http_status = null;
|
||||
self._http_headers = .empty;
|
||||
|
||||
self._referrer = null;
|
||||
self.referrer_policy = .default;
|
||||
|
||||
self.url = blk: {
|
||||
if (URL.isCompleteHTTPUrl(request_url)) {
|
||||
break :blk try self.arena.dupeZ(u8, request_url);
|
||||
@@ -793,6 +851,8 @@ pub fn navigate(self: *Frame, request_url: [:0]const u8, opts: NavigateOpts) !vo
|
||||
}
|
||||
if (opts.referer) |ref| {
|
||||
try transfer.addHeader("Referer", ref, .{});
|
||||
self._referrer = try self.arena.dupe(u8, ref);
|
||||
transfer.req.referrer_policy = opts.referrer_policy;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -859,7 +919,7 @@ pub fn scheduleNavigation(self: *Frame, request_url: []const u8, opts: NavigateO
|
||||
// might change inside the function. So the code should be explicit about the
|
||||
// frame that it's acting on.
|
||||
fn scheduleNavigationWithArena(originator: *Frame, arena: *lp.Arena, request_url: []const u8, opts: NavigateOpts, nt: Navigation) !void {
|
||||
const resolved_url, const is_about_blank = blk: {
|
||||
const resolved_url, const is_about_something = blk: {
|
||||
if (URL.isCompleteHTTPUrl(request_url)) {
|
||||
break :blk .{ try arena.dupeZ(u8, request_url), false };
|
||||
}
|
||||
@@ -869,6 +929,11 @@ fn scheduleNavigationWithArena(originator: *Frame, arena: *lp.Arena, request_url
|
||||
break :blk .{ "about:blank", true };
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, request_url, "about:srcdoc")) {
|
||||
// like about:blank, a synchronous navigation handled by navigate
|
||||
break :blk .{ "about:srcdoc", true };
|
||||
}
|
||||
|
||||
// request_url isn't a "complete" URL, so it has to be resolved with the
|
||||
// originator's base. Unless, originator's base is "about:blank", in which
|
||||
// case we have to walk up the parents and find a real base.
|
||||
@@ -951,20 +1016,17 @@ fn scheduleNavigationWithArena(originator: *Frame, arena: *lp.Arena, request_url
|
||||
|
||||
// Capture the originating frame's URL as the Referer for this
|
||||
// navigation. The originator's frame may be torn down before navigate()
|
||||
// runs (processRootQueuedNavigation rebuilds the Page in-place), so dup
|
||||
// into the QueuedNavigation arena which outlives that tear-down.
|
||||
// runs (processRootQueuedNavigation rebuilds the Page in-place), so
|
||||
// allocate from the QueuedNavigation arena which outlives that tear-down.
|
||||
var nav_opts = opts;
|
||||
if (std.mem.startsWith(u8, originator.url, "http")) {
|
||||
// The same dup feeds two purposes: Referer header (subject to
|
||||
// Referrer-Policy in the future) and SameSite computation (which
|
||||
// must use the real initiator regardless of policy). We share the
|
||||
// same allocation for both.
|
||||
const dup = try arena.dupeZ(u8, originator.url);
|
||||
const referrer_source = originator.referrerSource();
|
||||
if (std.mem.startsWith(u8, referrer_source, "http")) {
|
||||
if (nav_opts.referer == null) {
|
||||
nav_opts.referer = dup;
|
||||
nav_opts.referer = try referrer.compute(arena.allocator(), originator.referrer_policy, referrer_source, resolved_url);
|
||||
nav_opts.referrer_policy = originator.referrer_policy;
|
||||
}
|
||||
if (nav_opts.initiator_url == null) {
|
||||
nav_opts.initiator_url = dup;
|
||||
nav_opts.initiator_url = try arena.dupeZ(u8, referrer_source);
|
||||
}
|
||||
}
|
||||
if (nav_opts.initiator_origin == null) {
|
||||
@@ -978,7 +1040,7 @@ fn scheduleNavigationWithArena(originator: *Frame, arena: *lp.Arena, request_url
|
||||
.opts = nav_opts,
|
||||
.arena = arena,
|
||||
.url = resolved_url,
|
||||
.is_about_blank = is_about_blank,
|
||||
.is_about_something = is_about_something,
|
||||
.navigation_type = std.meta.activeTag(nt),
|
||||
};
|
||||
|
||||
@@ -1105,8 +1167,8 @@ pub fn iframeCompletedLoading(self: *Frame, iframe: *IFrame, delays_load: bool)
|
||||
.html => true,
|
||||
else => false,
|
||||
};
|
||||
if (parsing_html and iframe._src.len > 0) {
|
||||
self.queueElementEvent(iframe._proto, .load) catch |err| {
|
||||
if (parsing_html and (iframe._src.len > 0 or iframe.hasSrcdoc())) {
|
||||
self.queueElementEvent(Factory.protoOf(iframe), .load) catch |err| {
|
||||
log.err(.frame, "iframe queue load", .{ .err = err, .url = iframe._src });
|
||||
};
|
||||
if (delays_load) {
|
||||
@@ -1250,6 +1312,13 @@ fn frameHeaderDoneCallback(transfer: *HttpClient.Transfer) !HttpClient.Transfer.
|
||||
no.body = null;
|
||||
no.header = null;
|
||||
}
|
||||
|
||||
// The Referer may have been recomputed at each hop; document.referrer
|
||||
// reports what the final request actually sent.
|
||||
self._referrer = if (transfer.findRequestHeader("referer")) |ref|
|
||||
try self.arena.dupe(u8, ref)
|
||||
else
|
||||
null;
|
||||
}
|
||||
|
||||
// Init new location.
|
||||
@@ -1274,6 +1343,11 @@ fn frameHeaderDoneCallback(transfer: *HttpClient.Transfer) !HttpClient.Transfer.
|
||||
.name = try self.arena.dupe(u8, hdr.name),
|
||||
.value = try self.arena.dupe(u8, hdr.value),
|
||||
});
|
||||
if (std.ascii.eqlIgnoreCase(hdr.name, "referrer-policy")) {
|
||||
if (referrer.parseHeader(hdr.value)) |rp| {
|
||||
self.referrer_policy = rp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (self._navigated_options) |no| {
|
||||
@@ -1773,15 +1847,23 @@ pub fn iframeAddedCallback(self: *Frame, iframe: *IFrame) !void {
|
||||
return;
|
||||
}
|
||||
|
||||
var src = iframe.asElement().getAttributeSafe(comptime .wrap("src")) orelse "";
|
||||
if (src.len == 0) {
|
||||
src = "about:blank";
|
||||
}
|
||||
const src = blk: {
|
||||
if (iframe.hasSrcdoc()) {
|
||||
// srcdoc takes precedence over src, even when empty
|
||||
break :blk "about:srcdoc";
|
||||
}
|
||||
|
||||
if (URL.isCompleteHTTPUrl(src) and !URL.canParse(src, null)) {
|
||||
// per spec, if we can't parse the URL, we should load about:blank
|
||||
src = "about:blank";
|
||||
}
|
||||
var src = iframe.asElement().getAttributeSafe(comptime .wrap("src")) orelse "";
|
||||
if (src.len == 0) {
|
||||
src = "about:blank";
|
||||
}
|
||||
|
||||
if (URL.isCompleteHTTPUrl(src) and !URL.canParse(src, null)) {
|
||||
// per spec, if we can't parse the URL, we should load about:blank
|
||||
src = "about:blank";
|
||||
}
|
||||
break :blk src;
|
||||
};
|
||||
|
||||
if (iframe._window != null) {
|
||||
// This frame is being re-navigated. We need to do this through a
|
||||
@@ -1825,6 +1907,9 @@ pub fn iframeAddedCallback(self: *Frame, iframe: *IFrame) !void {
|
||||
if (std.mem.eql(u8, src, "about:blank")) {
|
||||
break :blk "about:blank"; // navigate will handle this special case
|
||||
}
|
||||
if (std.mem.eql(u8, src, "about:srcdoc")) {
|
||||
break :blk "about:srcdoc"; // navigate will handle this special case
|
||||
}
|
||||
break :blk try URL.resolve(
|
||||
self.call_arena, // ok to use, frame.navigate dupes this
|
||||
self.base(),
|
||||
@@ -1843,13 +1928,17 @@ pub fn iframeAddedCallback(self: *Frame, iframe: *IFrame) !void {
|
||||
const was_sorted = self.child_frames_sorted;
|
||||
self.child_frames_sorted = false;
|
||||
|
||||
// Iframe's initial src request carries the parent's URL as Referer and
|
||||
// as the SameSite initiator. Parent frame outlives this navigate()
|
||||
// call, so the slice is safe.
|
||||
const parent_url: ?[:0]const u8 = if (std.mem.startsWith(u8, self.url, "http")) self.url else null;
|
||||
// Iframe's initial src request carries the parent's URL as Referer
|
||||
// (subject to the parent's Referrer-Policy) and as the SameSite
|
||||
// initiator. When this frame is itself an about: document, the nearest
|
||||
// ancestor's URL is the referrer source. Parent frame outlives this
|
||||
// navigate() call, so the slice is safe; navigate dupes what it keeps.
|
||||
const referrer_source = self.referrerSource();
|
||||
const parent_url: ?[:0]const u8 = if (std.mem.startsWith(u8, referrer_source, "http")) referrer_source else null;
|
||||
new_frame.navigate(url, .{
|
||||
.reason = .initialFrameNavigation,
|
||||
.referer = parent_url,
|
||||
.referer = try referrer.compute(self.call_arena, self.referrer_policy, referrer_source, url),
|
||||
.referrer_policy = self.referrer_policy,
|
||||
.initiator_url = parent_url,
|
||||
.initiator_origin = self.origin,
|
||||
}) catch |err| {
|
||||
@@ -2157,7 +2246,7 @@ pub fn loadExternalStylesheet(self: *Frame, link: *Element.Html.Link, href: []co
|
||||
// this feature is disabled by default, and can be turned on via a command
|
||||
// line flag or via an CDP command
|
||||
if (session.load_external_stylesheets == false) {
|
||||
return self.queueLoad(link._proto);
|
||||
return self.queueLoad(Factory.protoOf(link));
|
||||
}
|
||||
|
||||
// Fragment-parsed links (innerHTML, DOMParser, ...) may not be attached.
|
||||
@@ -2489,6 +2578,15 @@ pub fn removeNode(self: *Frame, parent: *Node, child: *Node, opts: RemoveNodeOpt
|
||||
return;
|
||||
}
|
||||
|
||||
// Focus goes with the removed subtree. The focused element can be inside a
|
||||
// shadow tree hanging off it, which the walk below doesn't descend into,
|
||||
// so ask it directly whether it's still in the document.
|
||||
if (self.document._active_element) |active| {
|
||||
if (active.asNode().isConnected() == false) {
|
||||
self.document._active_element = null;
|
||||
}
|
||||
}
|
||||
|
||||
// The child was connected and now it no longer is. We need to "disconnect"
|
||||
// it and all of its descendants. For now "disconnect" just means updating
|
||||
// the ID map and invoking disconnectedCallback for custom elements
|
||||
@@ -2720,7 +2818,7 @@ pub fn _insertNodeRelative(self: *Frame, comptime from_parser: bool, parent: *No
|
||||
return;
|
||||
}
|
||||
|
||||
const parent_in_shadow = parent.is(ShadowRoot) != null or parent.isInShadowTree();
|
||||
const parent_in_shadow = parent.containingShadowRoot() != null;
|
||||
|
||||
if (!parent_in_shadow and !parent_is_connected) {
|
||||
return;
|
||||
@@ -3106,6 +3204,10 @@ pub const NavigateOpts = struct {
|
||||
// anchor click / form submit / location.href navigations carry a Referer.
|
||||
// null on CDP Page.navigate (address-bar) and Page.reload — matches Chrome.
|
||||
referer: ?[]const u8 = null,
|
||||
// The originating frame's policy, paired with `referer` so redirect hops
|
||||
// can recompute the header. null (e.g. a CDP-supplied referrer) leaves
|
||||
// the Referer untouched across redirects.
|
||||
referrer_policy: ?referrer.Policy = null,
|
||||
// The URL of the document that initiated this navigation, used as the
|
||||
// "site for cookies" when computing SameSite. Distinct from `referer`
|
||||
// because a Referrer-Policy can suppress the Referer header without
|
||||
@@ -3144,7 +3246,7 @@ pub const QueuedNavigation = struct {
|
||||
arena: *lp.Arena,
|
||||
url: [:0]const u8,
|
||||
opts: NavigateOpts,
|
||||
is_about_blank: bool,
|
||||
is_about_something: bool, // about:blank or about:srcdoc
|
||||
navigation_type: NavigationType,
|
||||
};
|
||||
|
||||
|
||||
@@ -550,7 +550,7 @@ pub fn serialize(arena: Allocator, input: []const u8) ![]const u8 {
|
||||
|
||||
// The serialized output is the input length plus quoting overhead; reserve
|
||||
// the input length so the common (no-escape) case appends without growing.
|
||||
try out.ensureTotalCapacity(arena, trimmed.len);
|
||||
try out.ensureTotalCapacityPrecise(arena, trimmed.len);
|
||||
|
||||
// Lowercased names already emitted, for first-wins dedupe.
|
||||
var seen: std.ArrayList([]const u8) = .empty;
|
||||
|
||||
+44
-5
@@ -118,10 +118,9 @@ fn _wait(self: *Runner, comptime is_cdp: bool, timeout_ms: u32, conditions: []Wa
|
||||
const timer: std.Io.Timestamp = .now(io, .boot);
|
||||
|
||||
// Periodic V8 GC hint during long waits. V8 is otherwise only nudged on
|
||||
// session/page teardown (Browser.zig, Page.zig), so a page that stays
|
||||
// session/page teardown (Session.zig, Page.zig), so a page that stays
|
||||
// alive for seconds while running heavy JS accumulates wrappers and
|
||||
// external-ref'd Zig allocations V8 has no reason to drop. `.moderate`
|
||||
// speeds up incremental GC without stalling the tick.
|
||||
// external-ref'd Zig allocations V8 has no reason to drop.
|
||||
const gc_hint_period_ns: u64 = std.time.ns_per_s * 5;
|
||||
var gc_hint_timer: std.Io.Timestamp = .now(io, .boot);
|
||||
|
||||
@@ -231,6 +230,18 @@ fn _tick(self: *Runner, comptime is_cdp: bool, timeout_ms: u32, conditions: []Wa
|
||||
const network_idle = activity.idle();
|
||||
const is_done = browser.hasMacrotasks() == false and network_idle;
|
||||
|
||||
// Outside the condition loop: it skips resolved conditions, but an idle
|
||||
// notification needs a check 500ms+ after the hold starts, and on a quiet
|
||||
// page one tick both starts the hold and resolves the condition. Before
|
||||
// it, so `.networkidle` conditions read fresh state.
|
||||
var page_index: usize = 0;
|
||||
while (page_index < session.pages.items.len) : (page_index += 1) {
|
||||
// Indexed: notifyNetworkIdle dispatches to listeners.
|
||||
const page = session.pages.items[page_index];
|
||||
if (page.replacement != null) continue; // frozen; the replacement is live
|
||||
page.frame.checkIdleNotifications(total_http_activity);
|
||||
}
|
||||
|
||||
// _we_ have nothing to run, but v8 is working on background tasks. We'll
|
||||
// wait for them. Don't do this for CDP, since new CDP messages can always
|
||||
// come in at any time.
|
||||
@@ -273,8 +284,6 @@ fn _tick(self: *Runner, comptime is_cdp: bool, timeout_ms: u32, conditions: []Wa
|
||||
}
|
||||
},
|
||||
.html, .complete => {
|
||||
frame.checkIdleNotifications(total_http_activity);
|
||||
|
||||
const met = switch (condition.until) {
|
||||
.done => is_done,
|
||||
.domcontentloaded => frame._load_state == .load or frame._load_state == .complete,
|
||||
@@ -297,6 +306,8 @@ fn _tick(self: *Runner, comptime is_cdp: bool, timeout_ms: u32, conditions: []Wa
|
||||
}
|
||||
}
|
||||
|
||||
// Always taken for is_cdp and every exit returns .ok, so _tick never yields
|
||||
// .done to the CDP pump: _wait's .done/is_cdp arm is dormant.
|
||||
if ((comptime is_cdp) or want_http_tick) {
|
||||
const ms_to_next_task = blk: {
|
||||
if (has_runnable_page == false) {
|
||||
@@ -544,3 +555,31 @@ test "Runner: lazy iframe does not delay the load event" {
|
||||
try testing.expectEqual(true, lazy_child._load_state == .complete);
|
||||
try testing.expectEqual(true, lazy_child._parent_notified);
|
||||
}
|
||||
|
||||
test "Runner: idle notifications advance past a resolved condition" {
|
||||
const page = try testing.pageTest("runner/runner1.html", .{});
|
||||
defer page.close();
|
||||
|
||||
const frame = page.frame().?;
|
||||
|
||||
// What a quiet page looks like one tick in: the hold has started, and the
|
||||
// same tick resolved the wait condition. Seeded past the 500ms hold so the
|
||||
// test doesn't spend it.
|
||||
const held_since = lp.datetime.milliTimestamp(.boot) -| 600;
|
||||
frame._notified_network_idle = .{ .triggered = held_since };
|
||||
frame._notified_network_almost_idle = .{ .triggered = held_since };
|
||||
|
||||
var conditions = [_]WaitCondition{.{
|
||||
.frame_id = page.frame_id,
|
||||
.until = .done,
|
||||
.status = .complete,
|
||||
}};
|
||||
|
||||
// is_cdp mirrors CDP.pageWait, which keeps ticking after the condition
|
||||
// resolves.
|
||||
var runner = page.session.runner(.{});
|
||||
_ = try runner._wait(true, 50, &conditions);
|
||||
|
||||
try testing.expectEqual(true, frame._notified_network_idle == .done);
|
||||
try testing.expectEqual(true, frame._notified_network_almost_idle == .done);
|
||||
}
|
||||
@@ -760,7 +760,7 @@ pub const Script = struct {
|
||||
|
||||
var buffer: std.ArrayList(u8) = .empty;
|
||||
if (content_length) |cl| {
|
||||
try buffer.ensureTotalCapacity(self.sourceAllocator(), cl);
|
||||
try buffer.ensureTotalCapacityPrecise(self.sourceAllocator(), cl);
|
||||
}
|
||||
self.source = .{ .remote = buffer };
|
||||
return .proceed;
|
||||
@@ -962,7 +962,7 @@ pub const Script = struct {
|
||||
log.debug(.browser, "executed script", .{ .src = url, .success = success });
|
||||
}
|
||||
|
||||
if (!success and frame.js.env.isExecutionTerminating()) {
|
||||
if (!success and frame.js.env.terminatePending()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -598,8 +598,8 @@ fn processPageQueuedNavigation(self: *Session, page: *Page) !void {
|
||||
continue;
|
||||
};
|
||||
|
||||
if (qn.is_about_blank) {
|
||||
// Defer about:blank to second pass
|
||||
if (qn.is_about_something) {
|
||||
// Defer about:blank or about:srcdoc to second pass
|
||||
try about_blank_queue.append(self.arena.allocator(), frame);
|
||||
continue;
|
||||
}
|
||||
@@ -637,7 +637,7 @@ fn processPageQueuedNavigation(self: *Session, page: *Page) !void {
|
||||
while (i < new_navigations.items.len) {
|
||||
const frame = new_navigations.items[i];
|
||||
if (frame._queued_navigation) |qn| {
|
||||
if (qn.is_about_blank) {
|
||||
if (qn.is_about_something) {
|
||||
log.warn(.frame, "recursive about blank", .{});
|
||||
_ = page.queued_navigation.swapRemove(i);
|
||||
continue;
|
||||
@@ -783,7 +783,7 @@ fn processRootQueuedNavigation(self: *Session, page: *Page) !void {
|
||||
// Synthetic navigations (about:blank, blob:) commit instantly — no HTTP,
|
||||
// so there is no in-flight window to worry about. Use the optimized
|
||||
// immediate-swap path for them.
|
||||
const is_synthetic = qn.is_about_blank or std.mem.startsWith(u8, qn.url, "blob:");
|
||||
const is_synthetic = qn.is_about_something or std.mem.startsWith(u8, qn.url, "blob:");
|
||||
|
||||
// The qn arena is consumed here regardless of success — frame.navigate
|
||||
// dupes the URL into the page's own arena, so we can release the qn
|
||||
|
||||
+1
-1
@@ -579,7 +579,7 @@ pub fn concatQueryString(arena: Allocator, url: []const u8, query_string: []cons
|
||||
var buf: std.ArrayList(u8) = .empty;
|
||||
|
||||
// the most space well need is the url + ('?' or '&') + the query_string + null terminator
|
||||
try buf.ensureTotalCapacity(arena, url.len + 2 + query_string.len);
|
||||
try buf.ensureTotalCapacityPrecise(arena, url.len + 2 + query_string.len);
|
||||
buf.appendSliceAssumeCapacity(url);
|
||||
|
||||
if (std.mem.indexOfScalar(u8, url, '?')) |index| {
|
||||
|
||||
@@ -276,7 +276,10 @@ fn remainingMs(timeout_ms: u32, timer: std.Io.Timestamp) u32 {
|
||||
pub fn waitForSelector(selector: [:0]const u8, timeout_ms: u32, frame_id: u32, session: *Session) !*DOMNode {
|
||||
const timer: std.Io.Timestamp = .now(lp.io, .boot);
|
||||
var runner = session.runner(.{});
|
||||
try runner.waitForFrame(frame_id, timeout_ms, .{ .until = .load });
|
||||
// Polling needs a parsed document, nothing more. Gating on `.load` would
|
||||
// re-wait the late-script tail a `waitUntil: domcontentloaded` navigation
|
||||
// deliberately skipped.
|
||||
try runner.waitForFrame(frame_id, timeout_ms, .{ .until = .domcontentloaded });
|
||||
|
||||
const el = try runner.waitForSelector(frame_id, selector, remainingMs(timeout_ms, timer));
|
||||
return el.asNode();
|
||||
@@ -285,7 +288,7 @@ pub fn waitForSelector(selector: [:0]const u8, timeout_ms: u32, frame_id: u32, s
|
||||
pub fn waitForScript(script: [:0]const u8, timeout_ms: u32, frame_id: u32, session: *Session) !void {
|
||||
const timer: std.Io.Timestamp = .now(lp.io, .boot);
|
||||
var runner = session.runner(.{});
|
||||
try runner.waitForFrame(frame_id, timeout_ms, .{ .until = .load });
|
||||
try runner.waitForFrame(frame_id, timeout_ms, .{ .until = .domcontentloaded });
|
||||
|
||||
return runner.waitForScript(frame_id, script, remainingMs(timeout_ms, timer));
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@ fn dumpSlotContent(slot: *Slot, opts: Opts, writer: *std.Io.Writer, frame: *Fram
|
||||
|
||||
fn isVoidElement(el: *const Node.Element) bool {
|
||||
return switch (el._type) {
|
||||
.html => |html| switch (html._type) {
|
||||
.html => switch (el.subtype(Node.Element.Html)._type) {
|
||||
.br, .hr, .img, .input, .link, .meta => true,
|
||||
else => false,
|
||||
},
|
||||
|
||||
+246
-178
File diff suppressed because it is too large.
Load diff
@@ -204,15 +204,27 @@ fn deltaToScroll(d: f64) i32 {
|
||||
// implements.
|
||||
fn hasClickActivationBehavior(node: *Node) bool {
|
||||
const element = node.is(Element) orelse return false;
|
||||
const html_element = element.is(Element.Html) orelse return false;
|
||||
|
||||
const html_element = element.is(Element.Html) orelse {
|
||||
if (element.is(Element.Svg.Graphics.A) != null) {
|
||||
return svgAnchorHref(element) != null;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return switch (html_element._type) {
|
||||
.anchor => element.getAttributeSafe(comptime .wrap("href")) != null,
|
||||
.input, .button, .select, .textarea, .label => true,
|
||||
.generic => |generic| generic._tag == .summary,
|
||||
.generic => html_element.subtype(Element.Html.Generic)._tag == .summary,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
// SVG 2 <a> links via `href`; xlink:href is the deprecated SVG 1.1 spelling.
|
||||
fn svgAnchorHref(element: *Element) ?[]const u8 {
|
||||
return element.getAttributeSafe(comptime .wrap("href")) orelse element.getAttributeSafe(comptime .wrap("xlink:href"));
|
||||
}
|
||||
|
||||
// Clicks on editable content are for editing: they don't activate the
|
||||
// element or any enclosing link.
|
||||
// "contenteditable" is 15 bytes — past the comptime SSO limit — so the
|
||||
@@ -325,45 +337,23 @@ const JavascriptUrlTask = struct {
|
||||
pub fn handleClick(frame: *Frame, target: *Node) !void {
|
||||
// TODO: Also support <area> elements when implement
|
||||
const element = target.is(Element) orelse return;
|
||||
|
||||
if (element.is(Element.Svg.Graphics.A) != null) {
|
||||
const href = svgAnchorHref(element) orelse return;
|
||||
const target_name = element.getAttributeSafe(comptime .wrap("target")) orelse "";
|
||||
return followLink(frame, target, element, href, target_name);
|
||||
}
|
||||
|
||||
const html_element = element.is(Element.Html) orelse return;
|
||||
|
||||
switch (html_element._type) {
|
||||
.anchor => |anchor| {
|
||||
.anchor => {
|
||||
const anchor = html_element.subtype(Element.Html.Anchor);
|
||||
const href = element.getAttributeSafe(comptime .wrap("href")) orelse return;
|
||||
if (href.len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (std.mem.startsWith(u8, href, "javascript:")) {
|
||||
// Navigating to a javascript: URL evaluates the script in the
|
||||
// node's frame as a queued task. (A string completion value
|
||||
// would replace the document; we ignore results.)
|
||||
return runJavascriptUrl(target.ownerFrame(frame), href["javascript:".len..]);
|
||||
}
|
||||
|
||||
if (try element.hasAttribute(comptime .wrap("download"), frame)) {
|
||||
log.warn(.browser, "a.download", .{ .type = frame._type, .url = frame.url });
|
||||
return;
|
||||
}
|
||||
|
||||
const target_frame = blk: {
|
||||
const target_name = anchor.getTarget();
|
||||
if (target_name.len == 0) {
|
||||
break :blk target.ownerFrame(frame);
|
||||
}
|
||||
break :blk frame.resolveTargetFrame(target_name) orelse {
|
||||
log.warn(.not_implemented, "target", .{ .type = frame._type, .url = frame.url, .target = target_name });
|
||||
return;
|
||||
};
|
||||
};
|
||||
|
||||
try element.focus(frame);
|
||||
try frame.scheduleNavigation(href, .{
|
||||
.reason = .script,
|
||||
.kind = .{ .push = null },
|
||||
}, .{ .anchor = target_frame });
|
||||
return followLink(frame, target, element, href, anchor.getTarget());
|
||||
},
|
||||
.input => |input| {
|
||||
.input => {
|
||||
const input = html_element.subtype(Element.Html.Input);
|
||||
try element.focus(frame);
|
||||
// Per HTML §4.10.18.6.4 "Image Button state (type=image)", clicking an
|
||||
// image button submits its form. The form-data set already gets the
|
||||
@@ -373,14 +363,16 @@ pub fn handleClick(frame: *Frame, target: *Node) !void {
|
||||
return frame.submitForm(element, input.getForm(frame), .{});
|
||||
}
|
||||
},
|
||||
.button => |button| {
|
||||
.button => {
|
||||
const button = html_element.subtype(Element.Html.Button);
|
||||
try element.focus(frame);
|
||||
if (std.mem.eql(u8, button.getType(), "submit")) {
|
||||
return frame.submitForm(element, button.getForm(frame), .{});
|
||||
}
|
||||
},
|
||||
.select, .textarea => try element.focus(frame),
|
||||
.label => |label| {
|
||||
.label => {
|
||||
const label = html_element.subtype(Element.Html.Label);
|
||||
// Per HTML §4.10.4 "The label element", a label's activation
|
||||
// behavior is to run the synthetic click activation steps on the
|
||||
// labeled control. Mirrors Chrome's HTMLLabelElement::DefaultEventHandler.
|
||||
@@ -388,8 +380,8 @@ pub fn handleClick(frame: *Frame, target: *Node) !void {
|
||||
const control_html = control.is(Element.Html) orelse return;
|
||||
try control_html.click(frame);
|
||||
},
|
||||
.generic => |generic| {
|
||||
switch (generic._tag) {
|
||||
.generic => {
|
||||
switch (html_element.subtype(Element.Html.Generic)._tag) {
|
||||
.summary => {
|
||||
const parent_el = target.parentElement() orelse return;
|
||||
const details = parent_el.is(Element.Html.Details) orelse return;
|
||||
@@ -410,6 +402,41 @@ pub fn handleClick(frame: *Frame, target: *Node) !void {
|
||||
}
|
||||
}
|
||||
|
||||
// Follow a link on activation. Shared by HTML <a> and SVG <a>.
|
||||
fn followLink(frame: *Frame, target: *Node, element: *Element, href: []const u8, target_name: []const u8) !void {
|
||||
if (href.len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (std.mem.startsWith(u8, href, "javascript:")) {
|
||||
// Navigating to a javascript: URL evaluates the script in the
|
||||
// node's frame as a queued task. (A string completion value
|
||||
// would replace the document; we ignore results.)
|
||||
return runJavascriptUrl(target.ownerFrame(frame), href["javascript:".len..]);
|
||||
}
|
||||
|
||||
if (try element.hasAttribute(comptime .wrap("download"), frame)) {
|
||||
log.warn(.browser, "a.download", .{ .type = frame._type, .url = frame.url });
|
||||
return;
|
||||
}
|
||||
|
||||
const target_frame = blk: {
|
||||
if (target_name.len == 0) {
|
||||
break :blk target.ownerFrame(frame);
|
||||
}
|
||||
break :blk frame.resolveTargetFrame(target_name) orelse {
|
||||
log.warn(.not_implemented, "target", .{ .type = frame._type, .url = frame.url, .target = target_name });
|
||||
return;
|
||||
};
|
||||
};
|
||||
|
||||
try element.focus(frame);
|
||||
try frame.scheduleNavigation(href, .{
|
||||
.reason = .script,
|
||||
.kind = .{ .push = null },
|
||||
}, .{ .anchor = target_frame });
|
||||
}
|
||||
|
||||
pub fn triggerKeyboard(frame: *Frame, keyboard_event: *KeyboardEvent) !void {
|
||||
const event = keyboard_event.asEvent();
|
||||
// Dispatch to the effective active element. When nothing is explicitly
|
||||
|
||||
+16
-17
@@ -41,6 +41,8 @@ const Allocator = std.mem.Allocator;
|
||||
|
||||
const MAX_CONTEXTS = if (lp.build_config.wpt_extensions) 8192 else 128;
|
||||
|
||||
const GC_HINT_FLOOR = 1 * 1024 * 1024;
|
||||
|
||||
fn initClassIds() void {
|
||||
inline for (JsApis, 0..) |JsApi, i| {
|
||||
JsApi.Meta.class_id = i;
|
||||
@@ -412,10 +414,7 @@ pub fn runMicrotasks(self: *Env) void {
|
||||
|
||||
const v8_isolate = self.isolate.handle;
|
||||
|
||||
// terminatePending: once a forcible terminate is requested (and not
|
||||
// canceled), refuse to start new work — IsExecutionTerminating alone
|
||||
// goes false again as soon as the killed script finishes unwinding.
|
||||
if (v8.v8__Isolate__IsExecutionTerminating(v8_isolate) or self.terminatePending()) {
|
||||
if (self.terminatePending()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -433,7 +432,7 @@ pub fn runMicrotasks(self: *Env) void {
|
||||
}
|
||||
|
||||
pub fn runMacrotasks(self: *Env) !void {
|
||||
if (v8.v8__Isolate__IsExecutionTerminating(self.isolate.handle) or self.terminatePending()) {
|
||||
if (self.terminatePending()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -512,10 +511,12 @@ pub fn runIdleTasks(self: *const Env) void {
|
||||
// a Context, it's managed by the garbage collector. We use the
|
||||
// `memoryPressureNotification` call on the isolate to encourage v8 to free
|
||||
// any contexts which have been freed.
|
||||
// The level indicates the aggressivity of the GC required:
|
||||
// moderate speeds up incremental GC
|
||||
// critical runs one full GC
|
||||
// Skips if there's little to reclaim
|
||||
pub fn memoryPressureNotification(self: *Env, level: Isolate.MemoryPressureLevel) void {
|
||||
const stats = self.isolate.getHeapStatistics();
|
||||
if (stats.used_heap_size + stats.external_memory < GC_HINT_FLOOR) {
|
||||
return;
|
||||
}
|
||||
var handle_scope: js.HandleScope = undefined;
|
||||
handle_scope.init(self.isolate);
|
||||
defer handle_scope.deinit();
|
||||
@@ -543,14 +544,11 @@ pub fn dumpMemoryStats(self: *Env) void {
|
||||
, .{ stats.total_heap_size, stats.total_heap_size_executable, stats.total_physical_size, stats.total_available_size, stats.used_heap_size, stats.heap_size_limit, stats.malloced_memory, stats.external_memory, stats.peak_malloced_memory, stats.number_of_native_contexts, stats.number_of_detached_contexts, stats.total_global_handles_size, stats.used_global_handles_size, stats.does_zap_garbage });
|
||||
}
|
||||
|
||||
pub fn isExecutionTerminating(self: *const Env) bool {
|
||||
return v8.v8__Isolate__IsExecutionTerminating(self.isolate.handle);
|
||||
}
|
||||
|
||||
// Whether a forcible terminate has been requested (and not yet cleared by
|
||||
// cancelTerminate). Unlike isExecutionTerminating, this is our own sticky
|
||||
// flag, so it stays true after V8 consumes the terminate on the JSEntry
|
||||
// unwind. Callers about to enter a fresh eval use it to refuse to run.
|
||||
// The single "must not run JS" predicate. We're the only ones who ever call
|
||||
// TerminateExecution (here and in terminateInterrupt) and both set this flag,
|
||||
// so it's always at least as true as v8__Isolate__IsExecutionTerminating —
|
||||
// and it stays true after V8 consumes the terminate on the JSEntry unwind,
|
||||
// which is what stops a fresh eval from re-entering mid-teardown.
|
||||
pub fn terminatePending(self: *const Env) bool {
|
||||
return self.terminate_requested.load(.acquire);
|
||||
}
|
||||
@@ -558,6 +556,7 @@ pub fn terminatePending(self: *const Env) bool {
|
||||
pub fn terminate(self: *Env) void {
|
||||
self.terminate_mutex.lockUncancelable(lp.io);
|
||||
defer self.terminate_mutex.unlock(lp.io);
|
||||
self.terminate_requested.store(true, .release);
|
||||
v8.v8__Isolate__TerminateExecution(self.isolate.handle);
|
||||
}
|
||||
|
||||
@@ -632,7 +631,7 @@ pub fn cancelTerminate(self: *Env) void {
|
||||
pub fn performIsolateMicrotasks(self: *Env) void {
|
||||
self.terminate_mutex.lockUncancelable(lp.io);
|
||||
defer self.terminate_mutex.unlock(lp.io);
|
||||
if (v8.v8__Isolate__IsExecutionTerminating(self.isolate.handle)) return;
|
||||
if (self.terminatePending()) return;
|
||||
v8.v8__Isolate__PerformMicrotaskCheckpoint(self.isolate.handle);
|
||||
}
|
||||
|
||||
|
||||
+36
-19
@@ -49,6 +49,21 @@ pub fn withThis(self: *const Function, value: anytype) !Function {
|
||||
}
|
||||
|
||||
pub fn newInstance(self: *const Function, caught: *js.TryCatch.Caught) !js.Object {
|
||||
var try_catch: js.TryCatch = undefined;
|
||||
try_catch.init(self.local);
|
||||
defer try_catch.deinit();
|
||||
|
||||
return self.newInstanceThrow() catch |err| {
|
||||
if (err == error.JsConstructorFailed) {
|
||||
caught.* = try_catch.caughtOrError(self.local.call_arena, error.Unknown);
|
||||
}
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
// Like newInstance, but with no TryCatch of our own. Gives more flexibility to
|
||||
// the caller on how to handle the error (e.g. window.reportError)
|
||||
pub fn newInstanceThrow(self: *const Function) !js.Object {
|
||||
const local = self.local;
|
||||
|
||||
if (comptime lp.IS_DEBUG == false) {
|
||||
@@ -66,21 +81,16 @@ pub fn newInstance(self: *const Function, caught: *js.TryCatch.Caught) !js.Objec
|
||||
}
|
||||
|
||||
// See _tryCallWithThis for why a pending termination blocks V8 entry.
|
||||
if (v8.v8__Isolate__IsExecutionTerminating(local.isolate.handle)) {
|
||||
if (local.ctx.env.terminatePending()) {
|
||||
return error.ExecutionTerminated;
|
||||
}
|
||||
|
||||
var try_catch: js.TryCatch = undefined;
|
||||
try_catch.init(local);
|
||||
defer try_catch.deinit();
|
||||
|
||||
// This creates a new instance using this Function as a constructor.
|
||||
// const c_args = @as(?[*]const ?*c.Value, @ptrCast(&.{}));
|
||||
const handle = v8.v8__Function__NewInstance(self.handle, local.handle, 0, null) orelse {
|
||||
if (v8.v8__Isolate__IsExecutionTerminating(local.isolate.handle)) {
|
||||
if (local.ctx.env.terminatePending()) {
|
||||
return error.ExecutionTerminated;
|
||||
}
|
||||
caught.* = try_catch.caughtOrError(local.call_arena, error.Unknown);
|
||||
return error.JsConstructorFailed;
|
||||
};
|
||||
|
||||
@@ -91,7 +101,7 @@ pub fn newInstance(self: *const Function, caught: *js.TryCatch.Caught) !js.Objec
|
||||
}
|
||||
|
||||
pub fn call(self: *const Function, comptime T: type, args: anytype) !T {
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
var caught: js.TryCatch.Caught = .{};
|
||||
return self._tryCallWithThis(T, self.getThis(), args, &caught, .{}) catch |err| {
|
||||
log.warn(.js, "call caught", .{ .err = err, .caught = caught });
|
||||
return err;
|
||||
@@ -99,7 +109,7 @@ pub fn call(self: *const Function, comptime T: type, args: anytype) !T {
|
||||
}
|
||||
|
||||
pub fn callRethrow(self: *const Function, comptime T: type, args: anytype) !T {
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
var caught: js.TryCatch.Caught = .{};
|
||||
return self._tryCallWithThis(T, self.getThis(), args, &caught, .{ .rethrow = true }) catch |err| {
|
||||
if (err != error.TryCatchRethrow) {
|
||||
// error.TryCatchRethrow is a control flow (sorry!), not an actual
|
||||
@@ -111,7 +121,7 @@ pub fn callRethrow(self: *const Function, comptime T: type, args: anytype) !T {
|
||||
}
|
||||
|
||||
pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T {
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
var caught: js.TryCatch.Caught = .{};
|
||||
return self._tryCallWithThis(T, this, args, &caught, .{}) catch |err| {
|
||||
log.warn(.js, "callWithThis caught", .{ .err = err, .caught = caught });
|
||||
return err;
|
||||
@@ -122,7 +132,7 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args
|
||||
// TryCatch, so an enclosing TryCatch of the caller can observe the exception
|
||||
// value itself (e.g. to report it to window.onerror).
|
||||
pub fn callWithThisRethrow(self: *const Function, comptime T: type, this: anytype, args: anytype) !T {
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
var caught: js.TryCatch.Caught = .{};
|
||||
return self._tryCallWithThis(T, this, args, &caught, .{ .rethrow = true });
|
||||
}
|
||||
|
||||
@@ -138,7 +148,6 @@ const CallOpts = struct {
|
||||
rethrow: bool = false,
|
||||
};
|
||||
fn _tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught, comptime opts: CallOpts) !T {
|
||||
caught.* = .{};
|
||||
const local = self.local;
|
||||
|
||||
if (comptime lp.IS_DEBUG == false) {
|
||||
@@ -158,7 +167,7 @@ fn _tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args
|
||||
// A pending termination (watchdog / CDP-disconnect kill) must not be
|
||||
// followed by another V8 entry. Callers must treat ExecutionTerminated as
|
||||
// stop running JS and unwind".
|
||||
if (v8.v8__Isolate__IsExecutionTerminating(local.isolate.handle)) {
|
||||
if (local.ctx.env.terminatePending()) {
|
||||
return error.ExecutionTerminated;
|
||||
}
|
||||
|
||||
@@ -212,7 +221,7 @@ fn _tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args
|
||||
defer try_catch.deinit();
|
||||
|
||||
const handle = v8.v8__Function__Call(self.handle, local.handle, js_this.handle, @as(c_int, @intCast(js_args.len)), c_args) orelse {
|
||||
if (v8.v8__Isolate__IsExecutionTerminating(local.isolate.handle)) {
|
||||
if (local.ctx.env.terminatePending()) {
|
||||
// Terminated mid-call, not a JS throw: no rethrow, no reporting.
|
||||
return error.ExecutionTerminated;
|
||||
}
|
||||
@@ -265,7 +274,7 @@ pub fn persistWithThis(self: *const Function, value: anytype) !Global {
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Function: termination is classified and blocks re-entry" {
|
||||
test "Function: requested termination is classified and blocks re-entry" {
|
||||
const frame = try testing.createFrame();
|
||||
defer testing.test_session.closeAllPages();
|
||||
|
||||
@@ -284,9 +293,10 @@ test "Function: termination is classified and blocks re-entry" {
|
||||
probe_ran: bool = false,
|
||||
kill_err: ?anyerror = null,
|
||||
probe_err: ?anyerror = null,
|
||||
nested_probe_err: ?anyerror = null,
|
||||
|
||||
fn kill(self: *@This()) void {
|
||||
self.env.terminate();
|
||||
self.env.requestTerminate();
|
||||
}
|
||||
|
||||
fn probed(self: *@This()) void {
|
||||
@@ -297,12 +307,12 @@ test "Function: termination is classified and blocks re-entry" {
|
||||
// termination pending, and the follow-up call must refuse to enter V8
|
||||
// (running it would silently clear the pending termination).
|
||||
fn nested(self: *@This()) void {
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
var caught: js.TryCatch.Caught = .{};
|
||||
_ = self.f_kill.?.tryCall(void, .{}, &caught) catch |err| {
|
||||
self.kill_err = err;
|
||||
};
|
||||
_ = self.f_probe.?.tryCall(void, .{}, &caught) catch |err| {
|
||||
self.probe_err = err;
|
||||
self.nested_probe_err = err;
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -319,9 +329,16 @@ test "Function: termination is classified and blocks re-entry" {
|
||||
const driver = try local.exec("(function(n){ n(); })", null);
|
||||
const driver_fn = Function{ .local = local, .handle = @ptrCast(driver.handle) };
|
||||
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
var caught: js.TryCatch.Caught = .{};
|
||||
try testing.expectError(error.ExecutionTerminated, driver_fn.tryCall(void, .{nested_cb}, &caught));
|
||||
try testing.expectEqual(error.ExecutionTerminated, state.kill_err.?);
|
||||
try testing.expectEqual(error.ExecutionTerminated, state.nested_probe_err.?);
|
||||
try testing.expectEqual(true, env.terminatePending());
|
||||
try testing.expectEqual(false, v8.v8__Isolate__IsExecutionTerminating(env.isolate.handle));
|
||||
|
||||
_ = state.f_probe.?.tryCall(void, .{}, &caught) catch |err| {
|
||||
state.probe_err = err;
|
||||
};
|
||||
try testing.expectEqual(error.ExecutionTerminated, state.probe_err.?);
|
||||
try testing.expectEqual(false, state.probe_ran);
|
||||
|
||||
|
||||
@@ -1267,7 +1267,15 @@ pub fn resolveValue(value: anytype) Resolved {
|
||||
// (e.g. CData); the type maps the tag to the member's type.
|
||||
if (comptime @typeInfo(@TypeOf(value._type)) == .@"enum" and @hasDecl(T, "Subtype")) {
|
||||
switch (value._type) {
|
||||
inline else => |tag| return resolveValue(value.subtype(T.Subtype(tag))),
|
||||
inline else => |tag| {
|
||||
const S = T.Subtype(tag);
|
||||
if (S == T) {
|
||||
// A tag can map to the type itself (e.g. Media.generic);
|
||||
// the value is already the most specific type.
|
||||
return resolveT(T, value);
|
||||
}
|
||||
return resolveValue(value.subtype(S));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,12 +32,12 @@ handle: *const v8.Script,
|
||||
pub fn run(self: Script) !js.Value {
|
||||
// See Function._tryCallWithThis for why a pending termination blocks V8
|
||||
// entry and is distinct from a JS throw.
|
||||
const isolate_handle = self.local.isolate.handle;
|
||||
if (v8.v8__Isolate__IsExecutionTerminating(isolate_handle)) {
|
||||
const env = self.local.ctx.env;
|
||||
if (env.terminatePending()) {
|
||||
return error.ExecutionTerminated;
|
||||
}
|
||||
const result = v8.v8__Script__Run(self.handle, self.local.handle) orelse {
|
||||
if (v8.v8__Isolate__IsExecutionTerminating(isolate_handle)) {
|
||||
if (env.terminatePending()) {
|
||||
return error.ExecutionTerminated;
|
||||
}
|
||||
return error.JsException;
|
||||
|
||||
@@ -133,7 +133,7 @@ fn appendTextChunk(self: *Parser, parent: *Node, txt: []const u8) !void {
|
||||
// Existing text sibling without a matching pending_text. Seed the
|
||||
// buf from its _data and register pending so subsequent chunks
|
||||
// accumulate cheaply.
|
||||
const cdata = tn._proto;
|
||||
const cdata = tn.asCData();
|
||||
const existing = cdata.getData().str();
|
||||
try self.buf.ensureTotalCapacity(self.arena, existing.len + txt.len);
|
||||
self.buf.appendSliceAssumeCapacity(existing);
|
||||
@@ -149,7 +149,7 @@ fn appendTextChunk(self: *Parser, parent: *Node, txt: []const u8) !void {
|
||||
try self.frame.appendNew(parent, new_text);
|
||||
self.pending_text = .{
|
||||
.parent = parent,
|
||||
.text_node = new_text.is(CData.Text).?._proto,
|
||||
.text_node = new_text.is(CData.Text).?.asCData(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -600,7 +600,7 @@ fn getTemplateContentsCallback(ctx: *anyopaque, target_ref: *anyopaque) callconv
|
||||
|
||||
fn _getTemplateContentsCallback(self: *Parser, node: *Node) !*anyopaque {
|
||||
const element = node.as(Element);
|
||||
const template = element._type.html.is(Element.Html.Template) orelse unreachable;
|
||||
const template = element.subtype(Element.Html).is(Element.Html.Template) orelse unreachable;
|
||||
const content_node = template.getContent().asNode();
|
||||
|
||||
// Create a ParsedNode wrapper for the content DocumentFragment
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
// 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/>.
|
||||
|
||||
// Referrer Policy: https://www.w3.org/TR/referrer-policy/
|
||||
const std = @import("std");
|
||||
const URL = @import("URL.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
pub const Policy = enum {
|
||||
no_referrer,
|
||||
no_referrer_when_downgrade,
|
||||
origin,
|
||||
origin_when_cross_origin,
|
||||
same_origin,
|
||||
strict_origin,
|
||||
strict_origin_when_cross_origin,
|
||||
unsafe_url,
|
||||
|
||||
pub const default: Policy = .strict_origin_when_cross_origin;
|
||||
};
|
||||
|
||||
pub fn parse(value: []const u8) ?Policy {
|
||||
const map = std.StaticStringMapWithEql(Policy, staticStringMapEqlAsciiIgnoreCase).initComptime(.{
|
||||
.{ "no-referrer", Policy.no_referrer },
|
||||
.{ "no-referrer-when-downgrade", Policy.no_referrer_when_downgrade },
|
||||
.{ "origin", Policy.origin },
|
||||
.{ "origin-when-cross-origin", Policy.origin_when_cross_origin },
|
||||
.{ "same-origin", Policy.same_origin },
|
||||
.{ "strict-origin", Policy.strict_origin },
|
||||
.{ "strict-origin-when-cross-origin", Policy.strict_origin_when_cross_origin },
|
||||
.{ "unsafe-url", Policy.unsafe_url },
|
||||
});
|
||||
return map.get(value);
|
||||
}
|
||||
|
||||
pub fn parseHeader(value: []const u8) ?Policy {
|
||||
var policy: ?Policy = null;
|
||||
var it = std.mem.splitScalar(u8, value, ',');
|
||||
while (it.next()) |token| {
|
||||
if (parse(std.mem.trim(u8, token, " \t"))) |p| {
|
||||
policy = p;
|
||||
}
|
||||
}
|
||||
return policy;
|
||||
}
|
||||
|
||||
pub fn parseMeta(value: []const u8) ?Policy {
|
||||
if (parse(value)) |p| {
|
||||
return p;
|
||||
}
|
||||
// legacy values
|
||||
const map = std.StaticStringMapWithEql(Policy, staticStringMapEqlAsciiIgnoreCase).initComptime(.{
|
||||
.{ "never", Policy.no_referrer },
|
||||
.{ "always", Policy.unsafe_url },
|
||||
.{ "origin-when-crossorigin", Policy.origin_when_cross_origin },
|
||||
.{ "default", Policy.default },
|
||||
});
|
||||
return map.get(value);
|
||||
}
|
||||
|
||||
// returns the value to send as the Referrer header based on the target_url
|
||||
// and the given policy
|
||||
pub fn compute(arena: Allocator, policy: Policy, referrer_url: [:0]const u8, target_url: [:0]const u8) !?[]const u8 {
|
||||
if (policy == .no_referrer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const referrer_origin = (try URL.getOrigin(arena, referrer_url)) orelse {
|
||||
// blob:, data: ... don't set get a referrer
|
||||
return null;
|
||||
};
|
||||
|
||||
const same_origin = blk: {
|
||||
const target_origin = (try URL.getOrigin(arena, target_url)) orelse break :blk false;
|
||||
break :blk std.ascii.eqlIgnoreCase(referrer_origin, target_origin);
|
||||
};
|
||||
const downgrade = URL.isSecure(referrer_url) and !URL.isSecure(target_url);
|
||||
|
||||
const full = switch (policy) {
|
||||
.no_referrer => unreachable,
|
||||
.unsafe_url => true,
|
||||
.origin => false,
|
||||
.no_referrer_when_downgrade => if (downgrade) return null else true,
|
||||
.same_origin => if (same_origin) true else return null,
|
||||
.origin_when_cross_origin => same_origin,
|
||||
.strict_origin => if (downgrade) return null else false,
|
||||
.strict_origin_when_cross_origin => if (same_origin) true else if (downgrade) return null else false,
|
||||
};
|
||||
|
||||
if (full) {
|
||||
// Serializing through origin + path + query strips credentials and
|
||||
// the fragment, and normalizes away default ports.
|
||||
const value = try std.fmt.allocPrint(arena, "{s}{s}{s}", .{
|
||||
referrer_origin,
|
||||
URL.getPathname(referrer_url),
|
||||
URL.getSearch(referrer_url),
|
||||
});
|
||||
|
||||
if (value.len <= 4096) {
|
||||
// spec limit, if it's more than this, we falllback to the origin
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return try std.fmt.allocPrint(arena, "{s}/", .{referrer_origin});
|
||||
}
|
||||
|
||||
fn staticStringMapEqlAsciiIgnoreCase(a: []const u8, b: []const u8) bool {
|
||||
for (a, b) |a_c, b_c| {
|
||||
if (std.ascii.toLower(a_c) != std.ascii.toLower(b_c)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
test "referrer: parse" {
|
||||
try testing.expectEqual(Policy.no_referrer, parse("no-referrer"));
|
||||
try testing.expectEqual(Policy.unsafe_url, parse("Unsafe-URL"));
|
||||
try testing.expectEqual(null, parse(""));
|
||||
try testing.expectEqual(null, parse("never"));
|
||||
try testing.expectEqual(null, parse("no-referrer "));
|
||||
|
||||
try testing.expectEqual(null, parseHeader(""));
|
||||
try testing.expectEqual(null, parseHeader("nope"));
|
||||
try testing.expectEqual(Policy.origin, parseHeader("origin"));
|
||||
try testing.expectEqual(Policy.same_origin, parseHeader("origin, same-origin"));
|
||||
try testing.expectEqual(Policy.origin, parseHeader("origin, garbage"));
|
||||
try testing.expectEqual(Policy.same_origin, parseHeader(" origin ,\tsame-origin "));
|
||||
|
||||
try testing.expectEqual(Policy.no_referrer, parseMeta("never"));
|
||||
try testing.expectEqual(Policy.unsafe_url, parseMeta("always"));
|
||||
try testing.expectEqual(Policy.origin_when_cross_origin, parseMeta("origin-when-crossorigin"));
|
||||
try testing.expectEqual(Policy.strict_origin_when_cross_origin, parseMeta("default"));
|
||||
try testing.expectEqual(Policy.origin, parseMeta("origin"));
|
||||
try testing.expectEqual(null, parseMeta("garbage"));
|
||||
}
|
||||
|
||||
test "referrer: compute" {
|
||||
const Case = struct {
|
||||
policy: Policy,
|
||||
referrer: [:0]const u8,
|
||||
target: [:0]const u8,
|
||||
expected: ?[]const u8,
|
||||
};
|
||||
|
||||
const cases = [_]Case{
|
||||
.{ .policy = .no_referrer, .referrer = "http://a.com/p", .target = "http://a.com/x", .expected = null },
|
||||
|
||||
.{ .policy = .unsafe_url, .referrer = "http://a.com/p?q=1#frag", .target = "https://b.com/", .expected = "http://a.com/p?q=1" },
|
||||
.{ .policy = .unsafe_url, .referrer = "https://a.com/p", .target = "http://b.com/", .expected = "https://a.com/p" },
|
||||
.{ .policy = .unsafe_url, .referrer = "https://user:pass@a.com/p", .target = "http://b.com/", .expected = "https://a.com/p" },
|
||||
.{ .policy = .unsafe_url, .referrer = "https://a.com:443/p", .target = "http://b.com/", .expected = "https://a.com/p" },
|
||||
.{ .policy = .unsafe_url, .referrer = "http://a.com", .target = "http://b.com/", .expected = "http://a.com/" },
|
||||
|
||||
.{ .policy = .origin, .referrer = "http://a.com:8000/p?q=1", .target = "http://a.com:8000/x", .expected = "http://a.com:8000/" },
|
||||
|
||||
.{ .policy = .same_origin, .referrer = "http://a.com/p", .target = "http://a.com/x", .expected = "http://a.com/p" },
|
||||
.{ .policy = .same_origin, .referrer = "http://a.com/p", .target = "http://b.com/x", .expected = null },
|
||||
.{ .policy = .same_origin, .referrer = "http://a.com/p", .target = "https://a.com/x", .expected = null },
|
||||
|
||||
.{ .policy = .origin_when_cross_origin, .referrer = "http://a.com/p", .target = "http://a.com/x", .expected = "http://a.com/p" },
|
||||
.{ .policy = .origin_when_cross_origin, .referrer = "http://a.com/p", .target = "http://b.com/x", .expected = "http://a.com/" },
|
||||
|
||||
.{ .policy = .strict_origin, .referrer = "https://a.com/p", .target = "http://a.com/x", .expected = null },
|
||||
.{ .policy = .strict_origin, .referrer = "https://a.com/p", .target = "https://b.com/x", .expected = "https://a.com/" },
|
||||
.{ .policy = .strict_origin, .referrer = "http://a.com/p", .target = "http://b.com/x", .expected = "http://a.com/" },
|
||||
|
||||
.{ .policy = .no_referrer_when_downgrade, .referrer = "https://a.com/p", .target = "http://b.com/x", .expected = null },
|
||||
.{ .policy = .no_referrer_when_downgrade, .referrer = "https://a.com/p", .target = "https://b.com/x", .expected = "https://a.com/p" },
|
||||
.{ .policy = .no_referrer_when_downgrade, .referrer = "http://a.com/p", .target = "http://b.com/x", .expected = "http://a.com/p" },
|
||||
|
||||
.{ .policy = .strict_origin_when_cross_origin, .referrer = "http://a.com/p?q=1", .target = "http://a.com/x", .expected = "http://a.com/p?q=1" },
|
||||
.{ .policy = .strict_origin_when_cross_origin, .referrer = "http://a.com/p", .target = "http://b.com/x", .expected = "http://a.com/" },
|
||||
.{ .policy = .strict_origin_when_cross_origin, .referrer = "https://a.com/p", .target = "http://b.com/x", .expected = null },
|
||||
.{ .policy = .strict_origin_when_cross_origin, .referrer = "https://a.com/p", .target = "http://a.com/x", .expected = null },
|
||||
|
||||
// no referrer from non-http(s) documents
|
||||
.{ .policy = .unsafe_url, .referrer = "about:blank", .target = "http://b.com/x", .expected = null },
|
||||
.{ .policy = .unsafe_url, .referrer = "data:text/html,x", .target = "http://b.com/x", .expected = null },
|
||||
};
|
||||
|
||||
for (cases) |case| {
|
||||
const actual = try compute(testing.arena_allocator, case.policy, case.referrer, case.target);
|
||||
if (case.expected) |expected| {
|
||||
try testing.expectEqual(expected, actual orelse return error.UnexpectedNull);
|
||||
} else {
|
||||
try testing.expectEqual(null, actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "referrer: compute caps at 4096 bytes" {
|
||||
const path = "/" ++ ("a" ** 4096);
|
||||
const url = "http://a.com" ++ path;
|
||||
// over the cap: falls back to the origin form
|
||||
try testing.expectEqual("http://a.com/", (try compute(testing.arena_allocator, .unsafe_url, url, "http://b.com/x")).?);
|
||||
try testing.expectEqual("http://a.com/", (try compute(testing.arena_allocator, .no_referrer_when_downgrade, url, "http://a.com/x")).?);
|
||||
|
||||
// exactly at the cap: sent in full
|
||||
const at_cap = "http://a.com/" ++ ("a" ** (4096 - "http://a.com/".len));
|
||||
try testing.expectEqual(at_cap, (try compute(testing.arena_allocator, .unsafe_url, at_cap, "http://b.com/x")).?);
|
||||
|
||||
// origin-only policies are unaffected by the referrer's length
|
||||
try testing.expectEqual("http://a.com/", (try compute(testing.arena_allocator, .origin, url, "http://b.com/x")).?);
|
||||
}
|
||||
@@ -20,9 +20,13 @@
|
||||
}
|
||||
customElements.define('ce-ctor-innerhtml', CeCtorInnerHtml);
|
||||
|
||||
// Adding children during construction violates a post-condition of
|
||||
// "create an element", so the result is a fallback HTMLUnknownElement
|
||||
// rather than the constructed instance. What matters here is that the
|
||||
// constructor ran exactly once instead of recursing.
|
||||
const el = document.createElement('ce-ctor-innerhtml');
|
||||
testing.expectEqual(1, calls);
|
||||
testing.expectEqual('<span>hi</span>', el.innerHTML);
|
||||
testing.expectEqual(true, el instanceof HTMLUnknownElement);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<script id="uses_constructor_return_value">
|
||||
{
|
||||
// https://dom.spec.whatwg.org/#concept-create-element -- creation does
|
||||
// not push onto the construction stack, so super() mints its own
|
||||
// element and the element we keep is whatever the constructor returns.
|
||||
let returned = null;
|
||||
class ReturnsAnother extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
if (returned === null) {
|
||||
returned = document.createElement('div');
|
||||
// A div is not a valid result for this token, so keep it
|
||||
// simple: return a second instance of ourselves instead.
|
||||
returned = Reflect.construct(HTMLElement, [], ReturnsAnother);
|
||||
return returned;
|
||||
}
|
||||
}
|
||||
}
|
||||
customElements.define('ce-returns-another', ReturnsAnother);
|
||||
|
||||
const el = document.createElement('ce-returns-another');
|
||||
testing.expectEqual(true, el === returned);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="rejects_element_with_children">
|
||||
{
|
||||
class AddsChild extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.appendChild(document.createElement('span'));
|
||||
}
|
||||
}
|
||||
customElements.define('ce-adds-child', AddsChild);
|
||||
|
||||
const el = document.createElement('ce-adds-child');
|
||||
testing.expectEqual(true, el instanceof HTMLUnknownElement);
|
||||
testing.expectEqual('ce-adds-child', el.localName);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="allows_child_added_then_removed">
|
||||
{
|
||||
// The post-condition is on the final state, not on what the
|
||||
// constructor touched along the way.
|
||||
class AddsAndRemoves extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.appendChild(document.createElement('span'));
|
||||
this.removeChild(this.firstChild);
|
||||
}
|
||||
}
|
||||
customElements.define('ce-adds-and-removes', AddsAndRemoves);
|
||||
|
||||
const el = document.createElement('ce-adds-and-removes');
|
||||
testing.expectEqual(true, el instanceof AddsAndRemoves);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="rejects_element_with_attribute">
|
||||
{
|
||||
class AddsAttribute extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.setAttribute('id', 'nope');
|
||||
}
|
||||
}
|
||||
customElements.define('ce-adds-attribute', AddsAttribute);
|
||||
|
||||
testing.expectEqual(true, document.createElement('ce-adds-attribute') instanceof HTMLUnknownElement);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="rejects_parented_element">
|
||||
{
|
||||
// The case that crashed the parser: a constructor that attaches its own
|
||||
// element leaves a node with a parent, which the tree builder would
|
||||
// then try to append a second time.
|
||||
const host = document.createElement('div');
|
||||
class AttachesSelf extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
host.appendChild(this);
|
||||
}
|
||||
}
|
||||
customElements.define('ce-attaches-self', AttachesSelf);
|
||||
|
||||
const el = document.createElement('ce-attaches-self');
|
||||
testing.expectEqual(true, el instanceof HTMLUnknownElement);
|
||||
testing.expectEqual(null, el.parentNode);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="parser_falls_back_without_crashing">
|
||||
{
|
||||
// Same thing, but driven through the fragment parser rather than
|
||||
// createElement: the fallback element is what gets inserted.
|
||||
const host = document.createElement('div');
|
||||
class ParserAttachesSelf extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
host.appendChild(this);
|
||||
}
|
||||
}
|
||||
customElements.define('ce-parser-attaches-self', ParserAttachesSelf);
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<ce-parser-attaches-self></ce-parser-attaches-self>';
|
||||
testing.expectEqual(1, container.children.length);
|
||||
testing.expectEqual(true, container.firstElementChild instanceof HTMLUnknownElement);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="attributes_applied_after_constructor">
|
||||
{
|
||||
// The constructor must not observe the token's attributes; they are
|
||||
// applied afterwards, which is also what enqueues their reactions.
|
||||
let seen = 'unset';
|
||||
class ObservesAttribute extends HTMLElement {
|
||||
static get observedAttributes() { return ['title']; }
|
||||
constructor() {
|
||||
super();
|
||||
seen = this.getAttribute('title');
|
||||
}
|
||||
}
|
||||
customElements.define('ce-observes-attribute', ObservesAttribute);
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<ce-observes-attribute title="after"></ce-observes-attribute>';
|
||||
testing.expectEqual(null, seen);
|
||||
testing.expectEqual('after', container.firstElementChild.getAttribute('title'));
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,286 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<script id="create_element_reports_thrown_exception">
|
||||
{
|
||||
// https://dom.spec.whatwg.org/#concept-create-element -- a failed
|
||||
// synchronous construction is reported to the global, and the very
|
||||
// exception object must reach window.onerror.
|
||||
const thrown = new Error('from constructor');
|
||||
class ThrowsInConstructor extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
throw thrown;
|
||||
}
|
||||
}
|
||||
customElements.define('ce-report-throw', ThrowsInConstructor);
|
||||
|
||||
let reported = null;
|
||||
window.onerror = (m, u, l, c, error) => { reported = error; return true; };
|
||||
const el = document.createElement('ce-report-throw');
|
||||
window.onerror = null;
|
||||
|
||||
testing.expectEqual(true, reported === thrown);
|
||||
testing.expectEqual(true, el instanceof HTMLUnknownElement);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="create_element_reports_type_error_for_bad_return">
|
||||
{
|
||||
class ReturnsObject extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
return {};
|
||||
}
|
||||
}
|
||||
customElements.define('ce-report-bad-return', ReturnsObject);
|
||||
|
||||
let reported = null;
|
||||
window.onerror = (m, u, l, c, error) => { reported = error; return true; };
|
||||
document.createElement('ce-report-bad-return');
|
||||
window.onerror = null;
|
||||
|
||||
testing.expectEqual('TypeError', reported.name);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="create_element_reports_not_supported_for_state_violation">
|
||||
{
|
||||
class AddsAttribute extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.setAttribute('id', 'nope');
|
||||
}
|
||||
}
|
||||
customElements.define('ce-report-attribute', AddsAttribute);
|
||||
|
||||
let reported = null;
|
||||
window.onerror = (m, u, l, c, error) => { reported = error; return true; };
|
||||
document.createElement('ce-report-attribute');
|
||||
window.onerror = null;
|
||||
|
||||
testing.expectEqual('NotSupportedError', reported.name);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="create_element_rejects_adoption_into_another_document">
|
||||
{
|
||||
const other = document.implementation.createHTMLDocument();
|
||||
class AdoptsSelf extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
other.adoptNode(this);
|
||||
}
|
||||
}
|
||||
customElements.define('ce-report-adopts-self', AdoptsSelf);
|
||||
|
||||
let reported = null;
|
||||
window.onerror = (m, u, l, c, error) => { reported = error; return true; };
|
||||
const el = document.createElement('ce-report-adopts-self');
|
||||
window.onerror = null;
|
||||
|
||||
testing.expectEqual('NotSupportedError', reported.name);
|
||||
testing.expectEqual(true, el instanceof HTMLUnknownElement);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="upgrade_rejects_constructor_returning_another_element">
|
||||
{
|
||||
// https://html.spec.whatwg.org/#upgrades -- the construction result
|
||||
// must be SameValue with the element being upgraded.
|
||||
const a = document.createElement('ce-report-same-value');
|
||||
const b = document.createElement('ce-report-same-value');
|
||||
document.documentElement.appendChild(a);
|
||||
document.documentElement.appendChild(b);
|
||||
|
||||
class ReturnsOther extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
if (this === a) {
|
||||
return b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let reported = null;
|
||||
window.onerror = (m, u, l, c, error) => { reported = error; return true; };
|
||||
customElements.define('ce-report-same-value', ReturnsOther);
|
||||
window.onerror = null;
|
||||
|
||||
testing.expectEqual('TypeError', reported.name);
|
||||
// a's failed upgrade must not prevent b's.
|
||||
testing.expectEqual(true, b instanceof ReturnsOther);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="upgrade_rejects_self_instantiation">
|
||||
{
|
||||
// The construction stack's "already constructed" marker: a constructor
|
||||
// instantiating itself during its own upgrade is a TypeError.
|
||||
class InstantiatesItself extends HTMLElement {
|
||||
constructor(stop) {
|
||||
super();
|
||||
if (!stop) {
|
||||
new InstantiatesItself(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
const el = document.createElement('ce-instantiates-itself');
|
||||
document.documentElement.appendChild(el);
|
||||
|
||||
let reported = null;
|
||||
window.onerror = (m, u, l, c, error) => { reported = error; return true; };
|
||||
customElements.define('ce-instantiates-itself', InstantiatesItself);
|
||||
window.onerror = null;
|
||||
|
||||
testing.expectEqual('TypeError', reported.name);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="failed_upgrade_is_not_retried">
|
||||
{
|
||||
let attempts = 0;
|
||||
class FailsToUpgrade extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
attempts += 1;
|
||||
throw new Error('nope');
|
||||
}
|
||||
}
|
||||
const el = document.createElement('ce-fails-to-upgrade');
|
||||
document.documentElement.appendChild(el);
|
||||
|
||||
window.onerror = () => true;
|
||||
customElements.define('ce-fails-to-upgrade', FailsToUpgrade);
|
||||
|
||||
// Reconnecting a "failed" element must not attempt another upgrade.
|
||||
el.remove();
|
||||
document.documentElement.appendChild(el);
|
||||
window.onerror = null;
|
||||
|
||||
testing.expectEqual(1, attempts);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="parser_reports_constructor_exception">
|
||||
{
|
||||
// The parser's create-element path must report too, and still insert
|
||||
// the HTMLUnknownElement fallback.
|
||||
const thrown = new Error('from parser constructor');
|
||||
class ParserThrows extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
throw thrown;
|
||||
}
|
||||
}
|
||||
customElements.define('ce-parser-throws', ParserThrows);
|
||||
|
||||
let reported = null;
|
||||
window.onerror = (m, u, l, c, error) => { reported = error; return true; };
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<ce-parser-throws></ce-parser-throws>';
|
||||
window.onerror = null;
|
||||
|
||||
testing.expectEqual(true, reported === thrown);
|
||||
testing.expectEqual(1, container.children.length);
|
||||
testing.expectEqual(true, container.firstElementChild instanceof HTMLUnknownElement);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="upgrade_rejects_self_instantiation_before_super">
|
||||
{
|
||||
// The before-super variant: the nested construction consumes the
|
||||
// upgrading element, so the outer super() finds the marker set.
|
||||
class InstantiatesBeforeSuper extends HTMLElement {
|
||||
constructor(stop) {
|
||||
if (!stop) {
|
||||
new InstantiatesBeforeSuper(true);
|
||||
}
|
||||
super();
|
||||
}
|
||||
}
|
||||
const el = document.createElement('ce-before-super');
|
||||
document.documentElement.appendChild(el);
|
||||
|
||||
let reported = null;
|
||||
window.onerror = (m, u, l, c, error) => { reported = error; return true; };
|
||||
customElements.define('ce-before-super', InstantiatesBeforeSuper);
|
||||
window.onerror = null;
|
||||
|
||||
testing.expectEqual('TypeError', reported.name);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="construction_during_upgrade_is_isolated">
|
||||
{
|
||||
// createElement of another custom element inside an upgrading
|
||||
// constructor must not inherit the outer construction's
|
||||
// already-constructed marker.
|
||||
class Inner extends HTMLElement {}
|
||||
customElements.define('ce-marker-inner', Inner);
|
||||
|
||||
let inner = null;
|
||||
class Outer extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
inner = document.createElement('ce-marker-inner');
|
||||
}
|
||||
}
|
||||
const el = document.createElement('ce-marker-outer');
|
||||
document.documentElement.appendChild(el);
|
||||
|
||||
let reported = null;
|
||||
window.onerror = (m, u, l, c, error) => { reported = error; return true; };
|
||||
customElements.define('ce-marker-outer', Outer);
|
||||
window.onerror = null;
|
||||
|
||||
testing.expectEqual(null, reported);
|
||||
testing.expectEqual(true, inner instanceof Inner);
|
||||
testing.expectEqual(true, el instanceof Outer);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="report_during_report_dispatch_is_not_swallowed">
|
||||
{
|
||||
// Regression test: reportError's event dispatch must not pump the
|
||||
// microtask queue. It used to, and a report fired by a pumped
|
||||
// continuation was silently dropped by the error-reporting-mode
|
||||
// guard.
|
||||
class ThrowsSync extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
throw new Error('sync');
|
||||
}
|
||||
}
|
||||
class ThrowsNested extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
throw new Error('nested');
|
||||
}
|
||||
}
|
||||
customElements.define('ce-swallow-sync', ThrowsSync);
|
||||
customElements.define('ce-swallow-nested', ThrowsNested);
|
||||
|
||||
window.__nested_report = null;
|
||||
Promise.resolve().then(() => {
|
||||
window.onerror = (m, u, l, c, error) => { window.__nested_report = error; return true; };
|
||||
document.createElement('ce-swallow-nested');
|
||||
window.onerror = null;
|
||||
});
|
||||
|
||||
let reported = null;
|
||||
window.onerror = (m, u, l, c, error) => { reported = error; return true; };
|
||||
document.createElement('ce-swallow-sync');
|
||||
window.onerror = null;
|
||||
testing.expectEqual('sync', reported.message);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="report_during_report_dispatch_is_not_swallowed_check">
|
||||
{
|
||||
// The continuation queued by the previous block has run by now (the
|
||||
// microtask checkpoint sits between the two script evaluations) and
|
||||
// its report must have been delivered.
|
||||
testing.expectEqual('nested', window.__nested_report.message);
|
||||
}
|
||||
</script>
|
||||
@@ -62,6 +62,26 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=webkitMatchesSelector>
|
||||
{
|
||||
// Legacy alias of matches(): same behaviour, including error handling.
|
||||
const container = $('#test-container');
|
||||
const span = $('#special');
|
||||
|
||||
testing.expectEqual(true, container.webkitMatchesSelector('#test-container'));
|
||||
testing.expectEqual(true, container.webkitMatchesSelector('.container.main'));
|
||||
testing.expectEqual(true, container.webkitMatchesSelector('*'));
|
||||
testing.expectEqual(false, container.webkitMatchesSelector('p'));
|
||||
|
||||
testing.expectEqual(true, span.webkitMatchesSelector('span#special.wrapper'));
|
||||
testing.expectEqual(false, span.webkitMatchesSelector('div'));
|
||||
|
||||
testing.expectError("SyntaxError", () => container.webkitMatchesSelector(''));
|
||||
|
||||
testing.expectEqual(1, Element.prototype.webkitMatchesSelector.length);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=errorHandling>
|
||||
{
|
||||
const container = $('#test-container');
|
||||
|
||||
@@ -53,7 +53,8 @@
|
||||
|
||||
box.appendChild(document.createElement('span'));
|
||||
testing.expectTrue(box.scrollWidth > oneChild);
|
||||
testing.expectTrue(box.scrollWidth > box.clientWidth);
|
||||
// An unsized box shrink-wraps: clientWidth tracks the same content sum.
|
||||
testing.expectEqual(box.scrollWidth, box.clientWidth);
|
||||
|
||||
// Text contributes nothing, however long. Estimating a run from its length
|
||||
// would need a per-character advance that tracks font-size, and would report
|
||||
@@ -133,6 +134,72 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="clientWidthMarqueeLoop">
|
||||
{
|
||||
// The clientWidth variant of the marquee idiom (amivoice.com): clone items
|
||||
// into an unsized wrap until it is twice the width of its container.
|
||||
// clientWidth of the wrap has to respond to the insertion, while the
|
||||
// container — whose direct child is the wrap, not the items — must not
|
||||
// grow in lockstep or the loop still never terminates.
|
||||
const container = document.createElement('div');
|
||||
const wrap = document.createElement('div');
|
||||
const item = document.createElement('span');
|
||||
wrap.appendChild(item);
|
||||
container.appendChild(wrap);
|
||||
document.body.appendChild(container);
|
||||
|
||||
const containerBefore = container.clientWidth;
|
||||
let guard = 0;
|
||||
while (wrap.clientWidth < container.clientWidth * 2 && guard < 50) {
|
||||
wrap.appendChild(item.cloneNode(true));
|
||||
guard++;
|
||||
}
|
||||
testing.expectTrue(guard < 50);
|
||||
testing.expectEqual(containerBefore, container.clientWidth);
|
||||
|
||||
// An explicit inline size pins the box regardless of content.
|
||||
const pinned = document.createElement('div');
|
||||
pinned.style.width = '40px';
|
||||
pinned.appendChild(document.createElement('span'));
|
||||
pinned.appendChild(document.createElement('span'));
|
||||
pinned.appendChild(document.createElement('span'));
|
||||
document.body.appendChild(pinned);
|
||||
testing.expectEqual(40, pinned.clientWidth);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="computedWidthMarqueeLoop">
|
||||
{
|
||||
// The getComputedStyle variant of the marquee idiom (inchurch.com.br's
|
||||
// dynamic-marquee-widget via jQuery .width()): the computed value must
|
||||
// carry the same content fallback as clientWidth or the loop never
|
||||
// terminates.
|
||||
const container = document.createElement('div');
|
||||
const wrap = document.createElement('div');
|
||||
const item = document.createElement('span');
|
||||
wrap.appendChild(item);
|
||||
container.appendChild(wrap);
|
||||
document.body.appendChild(container);
|
||||
|
||||
const width = (el) => parseFloat(getComputedStyle(el).width);
|
||||
const containerWidth = width(container);
|
||||
let guard = 0;
|
||||
while (width(wrap) < containerWidth * 2 && guard < 50) {
|
||||
wrap.appendChild(item.cloneNode(true));
|
||||
guard++;
|
||||
}
|
||||
testing.expectTrue(guard < 50);
|
||||
|
||||
// An explicit inline size still wins over the content sum.
|
||||
const pinned = document.createElement('div');
|
||||
pinned.style.width = '40px';
|
||||
pinned.appendChild(document.createElement('span'));
|
||||
pinned.appendChild(document.createElement('span'));
|
||||
document.body.appendChild(pinned);
|
||||
testing.expectEqual('40px', getComputedStyle(pinned).width);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="scrollHeightFromContent">
|
||||
{
|
||||
// An empty element has no content to overflow its box.
|
||||
@@ -149,7 +216,8 @@
|
||||
|
||||
box.appendChild(document.createElement('div'));
|
||||
testing.expectTrue(box.scrollHeight > oneChild);
|
||||
testing.expectTrue(box.scrollHeight > box.clientHeight);
|
||||
// An unsized box shrink-wraps: clientHeight tracks the same content sum.
|
||||
testing.expectEqual(box.scrollHeight, box.clientHeight);
|
||||
|
||||
// Text contributes no height, as it contributes no width: a text child's
|
||||
// line box is what the element's own size already stands in for.
|
||||
|
||||
@@ -224,6 +224,17 @@
|
||||
div.style.setProperty('text-transform', 'lowercase');
|
||||
testing.expectEqual('lowercase', window.getComputedStyle(div).getPropertyValue('text-transform'));
|
||||
|
||||
// Pseudo-elements get their own cache entry, keyed per (element, pseudo);
|
||||
// the legacy single-colon form maps to the same entry as the double-colon
|
||||
// one, and a pseudoElt without a leading colon is ignored (CSSOM).
|
||||
const before = window.getComputedStyle(div, ':before');
|
||||
testing.expectTrue(before === window.getComputedStyle(div, '::before'));
|
||||
testing.expectTrue(before === window.getComputedStyle(div, ':BEFORE'));
|
||||
testing.expectFalse(before === cs);
|
||||
testing.expectFalse(before === window.getComputedStyle(div, '::after'));
|
||||
testing.expectTrue(cs === window.getComputedStyle(div, 'before'));
|
||||
testing.expectTrue(cs === window.getComputedStyle(div, ''));
|
||||
|
||||
// A normal declaration must not override an earlier !important one, and the
|
||||
// computed and inline (el.style) paths must agree on the resolved value.
|
||||
const impDiv = document.createElement('div');
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<!-- parser-inserted srcdoc iframe; its load event is queued while the parent
|
||||
is still parsing, so a later script's onload check observes it -->
|
||||
<iframe id=static_srcdoc srcdoc="<p>static</p>"></iframe>
|
||||
|
||||
<script id=static_parse>
|
||||
testing.onload(() => {
|
||||
const doc = document.getElementById('static_srcdoc').contentDocument;
|
||||
testing.expectEqual('static', doc.querySelector('p').textContent);
|
||||
});
|
||||
</script>
|
||||
|
||||
<script id=basic type=module>
|
||||
{
|
||||
const state = await testing.async();
|
||||
|
||||
const f = document.createElement('iframe');
|
||||
f.srcdoc = '<p>hello</p>';
|
||||
f.onload = () => state.resolve(f);
|
||||
document.body.appendChild(f);
|
||||
|
||||
await state.done((f) => {
|
||||
// same-origin as the parent: contentDocument is readable
|
||||
testing.expectEqual('hello', f.contentDocument.querySelector('p').textContent);
|
||||
testing.expectEqual('about:srcdoc', f.contentWindow.location.href);
|
||||
testing.expectEqual('about:srcdoc', f.contentDocument.URL);
|
||||
// base URL is inherited from the parent
|
||||
testing.expectEqual(document.baseURI, f.contentDocument.baseURI);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=srcdoc_precedence_over_src type=module>
|
||||
{
|
||||
const state = await testing.async();
|
||||
|
||||
const f = document.createElement('iframe');
|
||||
f.setAttribute('src', '/xhr');
|
||||
f.setAttribute('srcdoc', '<p>doc wins</p>');
|
||||
f.onload = () => state.resolve(f);
|
||||
document.body.appendChild(f);
|
||||
|
||||
await state.done((f) => {
|
||||
testing.expectEqual('doc wins', f.contentDocument.querySelector('p').textContent);
|
||||
testing.expectEqual('about:srcdoc', f.contentWindow.location.href);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=script_runs_and_posts type=module>
|
||||
{
|
||||
const state = await testing.async();
|
||||
const listener = (e) => {
|
||||
if (e.data && e.data.test === 'script_runs') {
|
||||
window.removeEventListener('message', listener);
|
||||
state.resolve(e.data);
|
||||
}
|
||||
};
|
||||
window.addEventListener('message', listener);
|
||||
|
||||
const f = document.createElement('iframe');
|
||||
f.srcdoc = "<script>parent.postMessage({test: 'script_runs', base: document.baseURI}, '*')<\/script>";
|
||||
document.body.appendChild(f);
|
||||
|
||||
await state.done((data) => {
|
||||
testing.expectEqual(document.baseURI, data.base);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=reassign_reloads type=module>
|
||||
{
|
||||
const state = await testing.async();
|
||||
|
||||
const f = document.createElement('iframe');
|
||||
f.srcdoc = '<p>first</p>';
|
||||
let loads = 0;
|
||||
f.onload = () => {
|
||||
loads += 1;
|
||||
if (loads === 1) {
|
||||
testing.expectEqual('first', f.contentDocument.querySelector('p').textContent);
|
||||
// setAttribute on a connected iframe re-navigates too
|
||||
f.setAttribute('srcdoc', '<p>second</p>');
|
||||
return;
|
||||
}
|
||||
state.resolve(f);
|
||||
};
|
||||
document.body.appendChild(f);
|
||||
|
||||
await state.done((f) => {
|
||||
testing.expectEqual('second', f.contentDocument.querySelector('p').textContent);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=remove_falls_back_to_src type=module>
|
||||
{
|
||||
const state = await testing.async();
|
||||
|
||||
const f = document.createElement('iframe');
|
||||
f.srcdoc = '<p>content</p>';
|
||||
let loads = 0;
|
||||
f.onload = () => {
|
||||
loads += 1;
|
||||
if (loads === 1) {
|
||||
f.removeAttribute('srcdoc');
|
||||
return;
|
||||
}
|
||||
state.resolve(f);
|
||||
};
|
||||
document.body.appendChild(f);
|
||||
|
||||
await state.done((f) => {
|
||||
// no src attribute, so removal navigates back to about:blank
|
||||
testing.expectEqual('about:blank', f.contentWindow.location.href);
|
||||
testing.expectEqual(null, f.contentDocument.querySelector('p'));
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=empty_srcdoc type=module>
|
||||
{
|
||||
const state = await testing.async();
|
||||
|
||||
const f = document.createElement('iframe');
|
||||
f.srcdoc = '';
|
||||
f.onload = () => state.resolve(f);
|
||||
document.body.appendChild(f);
|
||||
|
||||
await state.done((f) => {
|
||||
testing.expectEqual('about:srcdoc', f.contentWindow.location.href);
|
||||
testing.expectEqual('', f.contentDocument.body.textContent);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=referrer_is_parent type=module>
|
||||
{
|
||||
// a request from inside a srcdoc frame carries the parent's URL as the
|
||||
// Referer (about:srcdoc isn't a valid referrer source); the relative
|
||||
// fetch URL also proves base inheritance
|
||||
const state = await testing.async();
|
||||
const listener = (e) => {
|
||||
if (e.data && e.data.test === 'referrer') {
|
||||
window.removeEventListener('message', listener);
|
||||
state.resolve(e.data);
|
||||
}
|
||||
};
|
||||
window.addEventListener('message', listener);
|
||||
|
||||
const f = document.createElement('iframe');
|
||||
f.srcdoc = "<script>fetch('/echo_referer').then(r => r.text()).then(t => parent.postMessage({test: 'referrer', body: t}, '*'))<\/script>";
|
||||
document.body.appendChild(f);
|
||||
|
||||
await state.done((data) => {
|
||||
testing.expectEqual(true, data.body.includes('referer=' + location.href));
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=meta_referrer_in_srcdoc type=module>
|
||||
{
|
||||
// <meta name=referrer> inside the srcdoc content sets that frame's policy
|
||||
const state = await testing.async();
|
||||
const listener = (e) => {
|
||||
if (e.data && e.data.test === 'meta_referrer') {
|
||||
window.removeEventListener('message', listener);
|
||||
state.resolve(e.data);
|
||||
}
|
||||
};
|
||||
window.addEventListener('message', listener);
|
||||
|
||||
const f = document.createElement('iframe');
|
||||
f.srcdoc = "<meta name='referrer' content='no-referrer'><script>fetch('/echo_referer').then(r => r.text()).then(t => parent.postMessage({test: 'meta_referrer', body: t}, '*'))<\/script>";
|
||||
document.body.appendChild(f);
|
||||
|
||||
await state.done((data) => {
|
||||
testing.expectEqual(true, data.body.includes('referer=NONE'));
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -126,3 +126,44 @@
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- SVG <a> has the same link activation behavior as HTML <a>. SVGElement has
|
||||
no click(), so activate via a dispatched MouseEvent, bubbling up from the
|
||||
<text> child like a real click on the rendered link. -->
|
||||
<iframe name=frame7 id=f7></iframe>
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<a href="support/page.html" target=frame7><text id=svgtext y=10>svg link</text></a>
|
||||
</svg>
|
||||
|
||||
<script id=svg_anchor type=module>
|
||||
{
|
||||
const state = await testing.async();
|
||||
$('#svgtext').dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true}));
|
||||
$('#f7').onload = () => {
|
||||
state.resolve();
|
||||
};
|
||||
|
||||
await state.done(() => {
|
||||
testing.expectEqual('a-page\n', $('#f7').contentDocument.body.textContent);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<iframe name=frame8 id=f8></iframe>
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<a id=svglink2 xlink:href="support/page.html" target=frame8><text y=10>svg 1.1 link</text></a>
|
||||
</svg>
|
||||
|
||||
<script id=svg_anchor_xlink type=module>
|
||||
{
|
||||
const state = await testing.async();
|
||||
$('#svglink2').dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true}));
|
||||
$('#f8').onload = () => {
|
||||
state.resolve();
|
||||
};
|
||||
|
||||
await state.done(() => {
|
||||
testing.expectEqual('a-page\n', $('#f8').contentDocument.body.textContent);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -451,3 +451,20 @@
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Referer across redirects: a same-origin hop keeps the full referrer; a
|
||||
cross-origin hop (localhost alias) recomputes it at the redirect, which
|
||||
the default policy (strict-origin-when-cross-origin) strips to origin. -->
|
||||
<script id=fetch_redirect_referer type=module>
|
||||
{
|
||||
const state = await testing.async();
|
||||
const same = await (await fetch('/redirect_same_echo_referer')).text();
|
||||
const cross = await (await fetch('/redirect_cross_echo_referer')).text();
|
||||
state.resolve();
|
||||
|
||||
await state.done(() => {
|
||||
testing.expectEqual(`<html><body>referer=${location.href}</body></html>`, same);
|
||||
testing.expectEqual('<html><body>referer=http://127.0.0.1:9582/</body></html>', cross);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -326,3 +326,141 @@
|
||||
host.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="related_target_stops_path">
|
||||
{
|
||||
// DOM dispatch: the path stops at the node the relatedTarget retargets
|
||||
// onto, so the event never reaches the host or anything above it.
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const shadow = host.attachShadow({ mode: 'open' });
|
||||
const target = document.createElement('span');
|
||||
shadow.appendChild(target);
|
||||
|
||||
const seen = [];
|
||||
const watch = (node, name) => node.addEventListener('my-event', () => seen.push(name));
|
||||
watch(target, 'target');
|
||||
watch(shadow, 'shadow');
|
||||
watch(host, 'host');
|
||||
watch(document.body, 'body');
|
||||
watch(document, 'document');
|
||||
|
||||
target.dispatchEvent(new MouseEvent('my-event', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
relatedTarget: host,
|
||||
}));
|
||||
|
||||
testing.expectEqual('target,shadow', seen.join(','));
|
||||
|
||||
host.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="related_target_equal_to_target_skips_dispatch">
|
||||
{
|
||||
// When the relatedTarget retargets onto the target itself, the event is
|
||||
// not dispatched at all.
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const shadow = host.attachShadow({ mode: 'open' });
|
||||
const inner = document.createElement('span');
|
||||
shadow.appendChild(inner);
|
||||
|
||||
let hostCalled = false;
|
||||
host.addEventListener('my-event', () => hostCalled = true);
|
||||
|
||||
// relatedTarget lives in host's own shadow tree, so it retargets to host.
|
||||
host.dispatchEvent(new MouseEvent('my-event', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
relatedTarget: inner,
|
||||
}));
|
||||
|
||||
testing.expectEqual(false, hostCalled);
|
||||
|
||||
host.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="related_target_retargeted_per_listener">
|
||||
{
|
||||
// Each listener sees the relatedTarget retargeted against its own
|
||||
// currentTarget: the slot is inside the shadow tree and sees the real
|
||||
// node, everything in the document tree sees the host.
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const shadow = host.attachShadow({ mode: 'open' });
|
||||
const slot = document.createElement('slot');
|
||||
const related = document.createElement('i');
|
||||
shadow.appendChild(slot);
|
||||
shadow.appendChild(related);
|
||||
|
||||
const target = document.createElement('span');
|
||||
host.appendChild(target);
|
||||
|
||||
const seen = [];
|
||||
const watch = (node, name) => node.addEventListener('my-event', (e) => {
|
||||
seen.push(name + '=' + (e.relatedTarget === related ? 'related' : (e.relatedTarget === host ? 'host' : '?')));
|
||||
});
|
||||
watch(target, 'target');
|
||||
watch(slot, 'slot');
|
||||
watch(host, 'host');
|
||||
|
||||
target.dispatchEvent(new MouseEvent('my-event', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
relatedTarget: related,
|
||||
}));
|
||||
|
||||
testing.expectEqual('target=host,slot=related,host=host', seen.join(','));
|
||||
|
||||
host.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="related_target_post_dispatch">
|
||||
{
|
||||
// Post-dispatch the relatedTarget is left retargeted against the outermost
|
||||
// node the event reached, or cleared when it would expose a shadow tree.
|
||||
const makeHost = (tag) => {
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const shadow = host.attachShadow({ mode: 'open' });
|
||||
const inner = document.createElement(tag);
|
||||
shadow.appendChild(inner);
|
||||
return { host: host, inner: inner };
|
||||
};
|
||||
|
||||
// target and relatedTarget in different shadow trees: each is exposed as
|
||||
// its own host.
|
||||
const a = makeHost('span');
|
||||
const b = makeHost('i');
|
||||
const across = new MouseEvent('my-event', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
relatedTarget: b.inner,
|
||||
});
|
||||
a.inner.dispatchEvent(across);
|
||||
testing.expectEqual(a.host, across.target);
|
||||
testing.expectEqual(b.host, across.relatedTarget);
|
||||
|
||||
// target and relatedTarget in the same shadow tree: the event never leaves
|
||||
// it, so both are cleared rather than leaking a shadow node.
|
||||
const c = makeHost('span');
|
||||
const sibling = document.createElement('i');
|
||||
c.inner.parentNode.appendChild(sibling);
|
||||
const within = new MouseEvent('my-event', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
relatedTarget: sibling,
|
||||
});
|
||||
c.inner.dispatchEvent(within);
|
||||
testing.expectEqual(null, within.target);
|
||||
testing.expectEqual(null, within.relatedTarget);
|
||||
|
||||
a.host.remove();
|
||||
b.host.remove();
|
||||
c.host.remove();
|
||||
}
|
||||
</script>
|
||||
+119
-21
@@ -276,6 +276,16 @@ pub const Tool = enum {
|
||||
};
|
||||
}
|
||||
|
||||
/// Waits for page readiness on its own, letting the recorder downgrade a
|
||||
/// preceding `goto` to `domcontentloaded`. Exhaustive like the sibling
|
||||
/// predicates so a new wait tool makes an explicit choice here.
|
||||
pub fn waitsForReadiness(self: Tool) bool {
|
||||
return switch (self) {
|
||||
.waitForSelector, .waitForScript, .waitForState => true,
|
||||
.goto, .evaluate, .extract, .click, .fill, .scroll, .hover, .press, .selectOption, .setChecked, .search, .markdown, .html, .links, .tree, .nodeDetails, .interactiveElements, .structuredData, .detectForms, .findElement, .consoleLogs, .getUrl, .getCookies, .getEnv => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// A read tool that navigates when handed a `url`. The read isn't recorded,
|
||||
/// but the navigation is, so the recorder captures it as a `goto`. Excludes
|
||||
/// `evaluate` (carries its own `url`), `search` (derived engine URL), and
|
||||
@@ -329,7 +339,10 @@ pub const Tool = enum {
|
||||
\\ "type": "object",
|
||||
\\ "properties": {
|
||||
\\ "url": { "type": "string", "description": "The URL to navigate to, must be a valid URL." },
|
||||
\\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 10000." }
|
||||
\\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 10000." },
|
||||
\\ "waitUntil": { "type": "string", "enum":
|
||||
++ lp.Config.tagJsonArray(lp.Config.WaitUntil) ++
|
||||
\\, "description": "Event that completes the navigation. Defaults to 'load'. Prefer 'domcontentloaded' followed by waitForSelector on pages whose late scripts (ads) hold 'load' back. Avoid 'done' (full quiescence): on pages with constant background activity it is the slowest choice and can run to the timeout." }
|
||||
\\ },
|
||||
\\ "required": ["url"]
|
||||
\\}
|
||||
@@ -357,7 +370,7 @@ pub const Tool = enum {
|
||||
\\ "type": "object",
|
||||
\\ "properties": {
|
||||
\\ "selector": { "type": "string", "description": "Optional CSS selector. Render markdown for just that element's subtree." },
|
||||
\\ "backendNodeId": { "type": "integer", "description": "Optional backend node ID. Render markdown for just that node's subtree." },
|
||||
\\ "backendNodeId": { "type": "integer", "description": "Optional backend node ID. Render markdown for just that node's subtree. 0 is treated as omitted." },
|
||||
\\ "maxBytes": { "type": "integer", "description": "Optional soft cap on output size in bytes. Content is truncated at a UTF-8 boundary and a short '[truncated]' marker is appended past the cap." },
|
||||
\\ "url": { "type": "string", "description": "Optional URL to navigate to before rendering." },
|
||||
\\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 10000." }
|
||||
@@ -373,7 +386,7 @@ pub const Tool = enum {
|
||||
\\ "type": "object",
|
||||
\\ "properties": {
|
||||
\\ "selector": { "type": "string", "description": "Optional CSS selector. When set, dump only that element's outerHTML." },
|
||||
\\ "backendNodeId": { "type": "integer", "description": "Optional backend node ID. When set, dump only that node's outerHTML." },
|
||||
\\ "backendNodeId": { "type": "integer", "description": "Optional backend node ID. When set, dump only that node's outerHTML. 0 is treated as omitted." },
|
||||
\\ "url": { "type": "string", "description": "Optional URL to navigate to before dumping." },
|
||||
\\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 10000." }
|
||||
\\ }
|
||||
@@ -441,7 +454,7 @@ pub const Tool = enum {
|
||||
\\ "properties": {
|
||||
\\ "url": { "type": "string", "description": "Optional URL to navigate to before fetching the semantic tree." },
|
||||
\\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 10000." },
|
||||
\\ "backendNodeId": { "type": "integer", "description": "Optional backend node ID to get the tree for a specific element instead of the document root." },
|
||||
\\ "backendNodeId": { "type": "integer", "description": "Optional backend node ID to get the tree for a specific element instead of the document root. 0 is treated as omitted." },
|
||||
\\ "maxDepth": { "type": "integer", "description": "Optional maximum depth of the tree to return. Useful for exploring high-level structure first." }
|
||||
\\ }
|
||||
\\}
|
||||
@@ -510,7 +523,7 @@ pub const Tool = enum {
|
||||
\\{
|
||||
\\ "type": "object",
|
||||
\\ "properties": {
|
||||
\\ "backendNodeId": { "type": "integer", "description": "Optional: The backend node ID of the element to scroll. If omitted, scrolls the window." },
|
||||
\\ "backendNodeId": { "type": "integer", "description": "Optional: The backend node ID of the element to scroll. If omitted (or 0), scrolls the window." },
|
||||
\\ "x": { "type": "integer", "description": "Optional: The horizontal scroll offset." },
|
||||
\\ "y": { "type": "integer", "description": "Optional: The vertical scroll offset." }
|
||||
\\ }
|
||||
@@ -525,7 +538,7 @@ pub const Tool = enum {
|
||||
\\ "type": "object",
|
||||
\\ "properties": {
|
||||
\\ "selector": { "type": "string", "description": "The CSS selector to wait for." },
|
||||
\\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 5000." }
|
||||
\\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 5000, or 15000 when the page has not reached 'load' yet." }
|
||||
\\ },
|
||||
\\ "required": ["selector"]
|
||||
\\}
|
||||
@@ -539,7 +552,7 @@ pub const Tool = enum {
|
||||
\\ "type": "object",
|
||||
\\ "properties": {
|
||||
\\ "script": { "type": "string", "description": "JS expression evaluated each tick until truthy. Must be an expression (not a statement)." },
|
||||
\\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 5000." }
|
||||
\\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 5000, or 15000 when the page has not reached 'load' yet." }
|
||||
\\ },
|
||||
\\ "required": ["script"]
|
||||
\\}
|
||||
@@ -583,7 +596,7 @@ pub const Tool = enum {
|
||||
\\ "properties": {
|
||||
\\ "key": { "type": "string", "description": "The key to press (e.g. 'Enter', 'Tab', 'a')." },
|
||||
\\ "selector": { "type": "string", "description": "Optional CSS selector of the element to target. Preferred over backendNodeId." },
|
||||
\\ "backendNodeId": { "type": "integer", "description": "Optional backend node ID of the element to target. Defaults to the document when neither selector nor backendNodeId is provided." }
|
||||
\\ "backendNodeId": { "type": "integer", "description": "Optional backend node ID of the element to target. Defaults to the document when neither selector nor backendNodeId is provided; 0 is treated as omitted." }
|
||||
\\ },
|
||||
\\ "required": ["key"]
|
||||
\\}
|
||||
@@ -744,6 +757,16 @@ pub const ToolError = error{
|
||||
OutOfMemory,
|
||||
};
|
||||
|
||||
/// LLM-facing message for a tool failure. Bare error names leave the model
|
||||
/// retrying blind; spell out the recovery for errors it can act on.
|
||||
pub fn errorMessage(err: ToolError) []const u8 {
|
||||
return switch (err) {
|
||||
error.NodeNotFound => "NodeNotFound: the selector or backendNodeId matched nothing on the current page. Re-inspect the page (tree/interactiveElements) for fresh node ids, or omit backendNodeId to target the document root.",
|
||||
error.FrameNotLoaded => "FrameNotLoaded: no page is loaded — call goto (or pass a url) first.",
|
||||
else => @errorName(err),
|
||||
};
|
||||
}
|
||||
|
||||
/// Outcome of running a tool against the page. Operational failures (OOM,
|
||||
/// missing page, invalid params) come out as Zig errors on the enclosing
|
||||
/// `!ToolResult`; `is_error = true` is the in-band signal for a JS-level
|
||||
@@ -758,6 +781,7 @@ pub const ToolResult = struct {
|
||||
pub const GotoParams = struct {
|
||||
url: [:0]const u8,
|
||||
timeout: ?u32 = null,
|
||||
waitUntil: lp.Config.WaitUntil = default_nav_wait,
|
||||
};
|
||||
|
||||
pub const UrlParams = struct {
|
||||
@@ -786,7 +810,13 @@ pub fn call(
|
||||
tool_name: []const u8,
|
||||
arguments: ?std.json.Value,
|
||||
) ToolError!ToolResult {
|
||||
const tool = std.meta.stringToEnum(Tool, tool_name) orelse return ToolError.InvalidParams;
|
||||
// In-band so an LLM that invented a tool name (e.g. OpenAI's internal
|
||||
// `multi_tool_use.parallel` wrapper) learns the name is wrong instead of
|
||||
// retrying it with different arguments.
|
||||
const tool = std.meta.stringToEnum(Tool, tool_name) orelse return .{
|
||||
.text = try std.fmt.allocPrint(arena, "Unknown tool: {s}", .{tool_name}),
|
||||
.is_error = true,
|
||||
};
|
||||
if (diagnoseArgs(arena, arguments)) |msg|
|
||||
return .{ .text = msg, .is_error = true };
|
||||
// Must run before substituteStringArgs so the `key=="value"` secret-
|
||||
@@ -936,7 +966,7 @@ const schema_walker_suffix = ")";
|
||||
|
||||
fn execGoto(arena: std.mem.Allocator, session: *lp.Session, registry: *CDPNode.Registry, arguments: ?std.json.Value) ToolError![]const u8 {
|
||||
const args = try parseArgs(GotoParams, arena, arguments);
|
||||
return switch (try performGoto(session, registry, args.url, args.timeout)) {
|
||||
return switch (try performGoto(session, registry, args.url, .{ .timeout = args.timeout, .wait_until = args.waitUntil })) {
|
||||
.completed => "Navigated successfully.",
|
||||
.timeout => "Navigation started but the page did not finish loading before the timeout.",
|
||||
};
|
||||
@@ -988,7 +1018,7 @@ fn execSearch(arena: std.mem.Allocator, session: *lp.Session, registry: *CDPNode
|
||||
.{encoded},
|
||||
0,
|
||||
) catch return ToolError.OutOfMemory;
|
||||
_ = try performGoto(session, registry, ddg_url, args.timeout);
|
||||
_ = try performGoto(session, registry, ddg_url, .{ .timeout = args.timeout });
|
||||
const ddg_frame = try requireFrame(session);
|
||||
return .{ .text = try renderFrameMarkdown(arena, ddg_frame) };
|
||||
}
|
||||
@@ -1619,6 +1649,14 @@ fn execScroll(arena: std.mem.Allocator, session: *lp.Session, registry: *CDPNode
|
||||
/// already-loaded page rather than a full navigation (which uses 10000).
|
||||
const default_wait_timeout_ms: u32 = 5000;
|
||||
|
||||
/// A wait entered before `load` also inherits the nav budget: a post-load
|
||||
/// selector keeps the wall time it had when `goto` waited for `load` itself.
|
||||
/// Shared with CDP's `LP.waitForSelector`, which fronts the same action.
|
||||
pub fn defaultWaitTimeout(frame: *const lp.Frame) u32 {
|
||||
if (frame._load_state == .complete) return default_wait_timeout_ms;
|
||||
return default_nav_timeout_ms + default_wait_timeout_ms;
|
||||
}
|
||||
|
||||
fn execWaitForSelector(arena: std.mem.Allocator, session: *lp.Session, registry: *CDPNode.Registry, arguments: ?std.json.Value) ToolError![]const u8 {
|
||||
const Params = struct {
|
||||
selector: [:0]const u8,
|
||||
@@ -1628,7 +1666,7 @@ fn execWaitForSelector(arena: std.mem.Allocator, session: *lp.Session, registry:
|
||||
|
||||
const frame = try requireFrame(session);
|
||||
|
||||
const timeout_ms = args.timeout orelse default_wait_timeout_ms;
|
||||
const timeout_ms = args.timeout orelse defaultWaitTimeout(frame);
|
||||
|
||||
const node = lp.actions.waitForSelector(args.selector, timeout_ms, frame._frame_id, session) catch |err| switch (err) {
|
||||
error.InvalidSelector => return ToolError.InvalidParams,
|
||||
@@ -1655,7 +1693,7 @@ fn execWaitForScript(arena: std.mem.Allocator, session: *lp.Session, arguments:
|
||||
|
||||
const frame = try requireFrame(session);
|
||||
|
||||
const timeout_ms = args.timeout orelse default_wait_timeout_ms;
|
||||
const timeout_ms = args.timeout orelse defaultWaitTimeout(frame);
|
||||
|
||||
lp.actions.waitForScript(args.script, timeout_ms, frame._frame_id, session) catch |err| switch (err) {
|
||||
error.Cancelled => return ToolError.Cancelled,
|
||||
@@ -1921,7 +1959,7 @@ fn ensurePage(session: *lp.Session, registry: *CDPNode.Registry, url: ?[:0]const
|
||||
if (session.currentFrame()) |frame| {
|
||||
if (std.mem.eql(u8, frame.url, u)) return frame;
|
||||
}
|
||||
_ = try performGoto(session, registry, u, timeout);
|
||||
_ = try performGoto(session, registry, u, .{ .timeout = timeout });
|
||||
}
|
||||
return session.currentFrame() orelse ToolError.FrameNotLoaded;
|
||||
}
|
||||
@@ -1937,6 +1975,7 @@ const default_nav_timeout_ms: u32 = 10000;
|
||||
pub const StartedGoto = struct {
|
||||
frame_id: u32,
|
||||
timeout_ms: u32,
|
||||
until: lp.Config.WaitUntil,
|
||||
};
|
||||
|
||||
/// Open a fresh top-level page and start its navigation. The frame is non-null
|
||||
@@ -1971,10 +2010,19 @@ pub fn startGoto(
|
||||
}
|
||||
}
|
||||
const page = try openPage(session, args.url);
|
||||
return .{ .frame_id = page.frame_id, .timeout_ms = args.timeout orelse default_nav_timeout_ms };
|
||||
return .{
|
||||
.frame_id = page.frame_id,
|
||||
.timeout_ms = args.timeout orelse default_nav_timeout_ms,
|
||||
.until = args.waitUntil,
|
||||
};
|
||||
}
|
||||
|
||||
fn performGoto(session: *lp.Session, registry: *CDPNode.Registry, url: [:0]const u8, timeout: ?u32) ToolError!lp.Session.Runner.WaitResult {
|
||||
const PerformGotoOpts = struct {
|
||||
timeout: ?u32 = null,
|
||||
wait_until: lp.Config.WaitUntil = default_nav_wait,
|
||||
};
|
||||
|
||||
fn performGoto(session: *lp.Session, registry: *CDPNode.Registry, url: [:0]const u8, opts: PerformGotoOpts) ToolError!lp.Session.Runner.WaitResult {
|
||||
if (session.primaryPage()) |old_page| {
|
||||
registry.reset();
|
||||
old_page.close();
|
||||
@@ -1982,9 +2030,9 @@ fn performGoto(session: *lp.Session, registry: *CDPNode.Registry, url: [:0]const
|
||||
const page = try openPage(session, url);
|
||||
|
||||
var runner = session.runner(.{});
|
||||
const condition = lp.Session.Runner.WaitCondition{ .frame_id = page.frame_id, .until = default_nav_wait };
|
||||
const condition = lp.Session.Runner.WaitCondition{ .frame_id = page.frame_id, .until = opts.wait_until };
|
||||
var conditions = [_]lp.Session.Runner.WaitCondition{condition};
|
||||
const result = runner.waitResult(timeout orelse default_nav_timeout_ms, &conditions) catch |err| {
|
||||
const result = runner.waitResult(opts.timeout orelse default_nav_timeout_ms, &conditions) catch |err| {
|
||||
return if (err == error.Cancelled) ToolError.Cancelled else ToolError.NavigationFailed;
|
||||
};
|
||||
|
||||
@@ -2038,13 +2086,23 @@ fn formatEnumError(arena: std.mem.Allocator, field: []const u8, got: []const u8,
|
||||
}
|
||||
|
||||
pub fn parseValue(comptime T: type, arena: std.mem.Allocator, value: std.json.Value) ParseArgsError!T {
|
||||
return std.json.parseFromValueLeaky(T, arena, value, .{ .ignore_unknown_fields = true }) catch |err| switch (err) {
|
||||
error.OutOfMemory => error.OutOfMemory,
|
||||
var parsed = std.json.parseFromValueLeaky(T, arena, value, .{ .ignore_unknown_fields = true }) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
else => {
|
||||
log.debug(.browser, "parseValue rejected", .{ .err = @errorName(err), .type = @typeName(T) });
|
||||
return error.InvalidParams;
|
||||
},
|
||||
};
|
||||
// Schema contract: backendNodeId 0 means omitted — registry ids start at 1,
|
||||
// and zero-filling models (gpt-5.x) send 0 for "unset".
|
||||
if (comptime @typeInfo(T) == .@"struct" and @hasField(T, "backendNodeId") and
|
||||
@typeInfo(@FieldType(T, "backendNodeId")) == .optional)
|
||||
{
|
||||
if (parsed.backendNodeId) |nid| {
|
||||
if (nid == 0) parsed.backendNodeId = null;
|
||||
}
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/// For tools where every field is optional. Missing args → default `T`;
|
||||
@@ -2170,7 +2228,7 @@ pub fn reverseSubstituteEnvVars(arena: std.mem.Allocator, input: []const u8) err
|
||||
// before its full match is found, leaking a suffix into the recording.
|
||||
const Pair = struct { name: []const u8, value: []const u8 };
|
||||
var pairs: std.ArrayList(Pair) = .empty;
|
||||
try pairs.ensureTotalCapacity(arena, env_names.len);
|
||||
try pairs.ensureTotalCapacityPrecise(arena, env_names.len);
|
||||
for (env_names) |name| {
|
||||
const value = lookupLpEnv(name) orelse continue;
|
||||
if (value.len < 4) continue;
|
||||
@@ -2193,6 +2251,46 @@ pub fn reverseSubstituteEnvVars(arena: std.mem.Allocator, input: []const u8) err
|
||||
return if (changed) current else input;
|
||||
}
|
||||
|
||||
test "call: unknown tool name surfaces in-band" {
|
||||
var arena: std.heap.ArenaAllocator = .init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
// Session/registry are never touched on this branch; the name check is
|
||||
// the first thing `call` does.
|
||||
const r = try call(arena.allocator(), undefined, undefined, "multi_tool_use.parallel", null);
|
||||
try std.testing.expect(r.is_error);
|
||||
try std.testing.expectEqualStrings("Unknown tool: multi_tool_use.parallel", r.text);
|
||||
}
|
||||
|
||||
test "parseValue: zero-filled optional backendNodeId treated as omitted" {
|
||||
var arena: std.heap.ArenaAllocator = .init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const aa = arena.allocator();
|
||||
|
||||
const Params = struct {
|
||||
backendNodeId: ?CDPNode.Id = null,
|
||||
maxDepth: ?u32 = null,
|
||||
};
|
||||
const zeroed = try std.json.parseFromSliceLeaky(std.json.Value, aa,
|
||||
\\{"backendNodeId":0,"maxDepth":2}
|
||||
, .{});
|
||||
const args = try parseValue(Params, aa, zeroed);
|
||||
try std.testing.expectEqual(@as(?CDPNode.Id, null), args.backendNodeId);
|
||||
try std.testing.expectEqual(@as(?u32, 2), args.maxDepth);
|
||||
|
||||
const real = try std.json.parseFromSliceLeaky(std.json.Value, aa,
|
||||
\\{"backendNodeId":7}
|
||||
, .{});
|
||||
try std.testing.expectEqual(@as(?CDPNode.Id, 7), (try parseValue(Params, aa, real)).backendNodeId);
|
||||
|
||||
// Non-optional ids (nodeDetails) pass through untouched.
|
||||
const Required = struct { backendNodeId: CDPNode.Id };
|
||||
const zero_required = try std.json.parseFromSliceLeaky(std.json.Value, aa,
|
||||
\\{"backendNodeId":0}
|
||||
, .{});
|
||||
try std.testing.expectEqual(@as(CDPNode.Id, 0), (try parseValue(Required, aa, zero_required)).backendNodeId);
|
||||
}
|
||||
|
||||
test "substituteEnvVars resolves LP_* vars" {
|
||||
var arena: std.heap.ArenaAllocator = .init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
@@ -77,9 +77,9 @@ pub fn subtype(self: *const CData, comptime T: type) *T {
|
||||
// the arithmetic rides on factory-chain contiguity; the stored
|
||||
// back-pointer doubles as its canary
|
||||
if (comptime T == CDATASection) {
|
||||
std.debug.assert(sub._proto._proto == self);
|
||||
std.debug.assert(sub._proto_canary._proto_canary == self);
|
||||
} else {
|
||||
std.debug.assert(sub._proto == self);
|
||||
std.debug.assert(sub._proto_canary == self);
|
||||
}
|
||||
}
|
||||
return sub;
|
||||
|
||||
@@ -181,7 +181,9 @@ fn upgradeElement(self: *CustomElementRegistry, element: *Element, frame: *Frame
|
||||
return Custom.checkAndAttachBuiltIn(element, frame);
|
||||
};
|
||||
|
||||
if (custom._definition != null) return;
|
||||
if (custom._definition != null or custom._upgrade_failed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = custom._tag_name.str();
|
||||
const definition = self._definitions.get(name) orelse return;
|
||||
@@ -198,19 +200,49 @@ pub fn upgradeCustomElement(custom: *Custom, definition: *CustomElementDefinitio
|
||||
|
||||
const node = custom.asNode();
|
||||
const prev_upgrading = frame._upgrading_element;
|
||||
const prev_consumed = frame._upgrading_consumed;
|
||||
frame._upgrading_element = node;
|
||||
defer frame._upgrading_element = prev_upgrading;
|
||||
frame._upgrading_consumed = false;
|
||||
defer {
|
||||
frame._upgrading_element = prev_upgrading;
|
||||
frame._upgrading_consumed = prev_consumed;
|
||||
}
|
||||
|
||||
var ls: js.Local.Scope = undefined;
|
||||
frame.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
_ = ls.toLocal(definition.constructor).newInstance(&caught) catch |err| {
|
||||
log.warn(.js, "custom element upgrade", .{ .name = definition.name, .err = err, .caught = caught });
|
||||
const local = &ls.local;
|
||||
var try_catch: js.TryCatch = undefined;
|
||||
try_catch.init(local);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const object = ls.toLocal(definition.constructor).newInstanceThrow() catch |err| {
|
||||
if (err == error.ExecutionTerminated) {
|
||||
custom._definition = null;
|
||||
return err;
|
||||
}
|
||||
log.warn(.js, "custom element upgrade", .{ .name = definition.name, .err = err });
|
||||
upgradeFailed(custom);
|
||||
if (try_catch.exceptionValue()) |exc| {
|
||||
frame.window.reportError(exc, frame) catch {};
|
||||
}
|
||||
return error.CustomElementUpgradeFailed;
|
||||
};
|
||||
|
||||
const same = if (object.toZig(*Node)) |result| result == node else |_| false;
|
||||
if (!same) {
|
||||
// the construction result must be the element being upgraded.
|
||||
log.warn(.js, "custom element upgrade", .{ .name = definition.name, .reason = "constructor returned another value" });
|
||||
upgradeFailed(custom);
|
||||
const exc: js.Value = .{
|
||||
.local = local,
|
||||
.handle = local.isolate.createTypeError("custom element constructor must return the upgraded element"),
|
||||
};
|
||||
frame.window.reportError(exc, frame) catch {};
|
||||
return error.CustomElementUpgradeFailed;
|
||||
}
|
||||
|
||||
// Enqueue attributeChangedCallback for existing observed attributes
|
||||
const element = custom.asElement();
|
||||
for (element.attributeEntries()) |*attr| {
|
||||
@@ -227,6 +259,11 @@ pub fn upgradeCustomElement(custom: *Custom, definition: *CustomElementDefinitio
|
||||
}
|
||||
}
|
||||
|
||||
fn upgradeFailed(custom: *Custom) void {
|
||||
custom._definition = null;
|
||||
custom._upgrade_failed = true;
|
||||
}
|
||||
|
||||
fn validateName(name: []const u8) !void {
|
||||
if (name.len == 0) {
|
||||
return error.SyntaxError;
|
||||
@@ -288,5 +325,6 @@ pub const JsApi = struct {
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "WebApi: CustomElementRegistry" {
|
||||
testing.expectLog(&.{ .js, .js, .js, .js, .js, .js, .js, .js, .js, .js, .js, .js, .js, .js, .js, .js, .js, .js, .js, .js, .js, .js, .js });
|
||||
try testing.htmlRunner("custom_elements", .{});
|
||||
}
|
||||
@@ -81,7 +81,7 @@ pub fn getAsString(self: *const DataTransferItem, cb_: ?js.Function) !void {
|
||||
.string => |str| str,
|
||||
.file => return,
|
||||
};
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
var caught: js.TryCatch.Caught = .{};
|
||||
cb.tryCall(void, .{s}, &caught) catch {
|
||||
log.debug(.js, "getAsString callback", .{ .caught = caught, .source = "DataTransferItem" });
|
||||
};
|
||||
|
||||
@@ -212,6 +212,11 @@ pub fn getLastModified(self: *const Document, frame: *Frame) ![]const u8 {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn getReferrer(self: *const Document) []const u8 {
|
||||
const frame = self._frame orelse return "";
|
||||
return frame._referrer orelse "";
|
||||
}
|
||||
|
||||
pub fn getCharset(self: *const Document) []const u8 {
|
||||
if (self._charset) |charset| {
|
||||
return charset;
|
||||
@@ -723,7 +728,15 @@ pub fn getReadyState(self: *const Document) []const u8 {
|
||||
|
||||
pub fn getActiveElement(self: *Document) ?*Element {
|
||||
if (self._active_element) |el| {
|
||||
return el;
|
||||
// A focused element inside a shadow tree is exposed as its outermost
|
||||
// host; one in a detached tree isn't exposed at all.
|
||||
var candidate = el;
|
||||
while (candidate.asNode().containingShadowRoot()) |shadow| {
|
||||
candidate = shadow._host;
|
||||
}
|
||||
if (candidate.asNode().getRootNode(.{}) == self.asNode()) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// Default to body if it exists
|
||||
@@ -760,6 +773,9 @@ pub fn adoptNode(self: *Document, node: *Node, frame: *Frame) !*Node {
|
||||
if (node._type == .document) {
|
||||
return error.NotSupported;
|
||||
}
|
||||
if (node.is(Node.ShadowRoot) != null) {
|
||||
return error.HierarchyError;
|
||||
}
|
||||
|
||||
const old_owner = node.ownerDocument(frame) orelse frame.document;
|
||||
|
||||
@@ -1588,15 +1604,12 @@ pub const JsApi = struct {
|
||||
pub const hasFocus = bridge.function(Document.hasFocus, .{});
|
||||
|
||||
pub const prerendering = bridge.property(false, .{ .template = false });
|
||||
pub const characterSet = bridge.accessor(getCharacterSet, null, .{});
|
||||
pub const charset = bridge.accessor(getCharacterSet, null, .{});
|
||||
pub const inputEncoding = bridge.accessor(getCharacterSet, null, .{});
|
||||
pub const characterSet = bridge.accessor(Document.getCharset, null, .{});
|
||||
pub const charset = bridge.accessor(Document.getCharset, null, .{});
|
||||
pub const inputEncoding = bridge.accessor(Document.getCharset, null, .{});
|
||||
pub const compatMode = bridge.accessor(Document.getCompatMode, null, .{});
|
||||
pub const lastModified = bridge.accessor(Document.getLastModified, null, .{});
|
||||
fn getCharacterSet(self: *const Document) []const u8 {
|
||||
return self.getCharset();
|
||||
}
|
||||
pub const referrer = bridge.property("", .{ .template = false });
|
||||
pub const referrer = bridge.accessor(Document.getReferrer, null, .{});
|
||||
|
||||
// Generates a getter/setter pair backed by the frame's attribute-listener
|
||||
// map, like onclick above, for other document event handler properties.
|
||||
|
||||
+361
-260
@@ -51,6 +51,30 @@ pub const Proto = Node;
|
||||
|
||||
pub const DatasetLookup = std.AutoHashMapUnmanaged(*Element, *DOMStringMap);
|
||||
pub const StyleLookup = std.AutoHashMapUnmanaged(*Element, *CSSStyleProperties);
|
||||
pub const ComputedStyleLookup = std.AutoHashMapUnmanaged(ComputedStyleKey, *CSSStyleProperties);
|
||||
|
||||
pub const ComputedStyleKey = struct {
|
||||
element: *Element,
|
||||
pseudo: PseudoElement,
|
||||
};
|
||||
|
||||
pub const PseudoElement = enum {
|
||||
none,
|
||||
before,
|
||||
after,
|
||||
other,
|
||||
|
||||
pub fn parse(pseudo: []const u8) PseudoElement {
|
||||
if (pseudo.len == 0 or pseudo[0] != ':') {
|
||||
return .none;
|
||||
}
|
||||
const name = if (std.mem.startsWith(u8, pseudo, "::")) pseudo[2..] else pseudo[1..];
|
||||
if (std.ascii.eqlIgnoreCase(name, "before")) return .before;
|
||||
if (std.ascii.eqlIgnoreCase(name, "after")) return .after;
|
||||
return .other;
|
||||
}
|
||||
};
|
||||
|
||||
pub const ClassListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList);
|
||||
pub const RelListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList);
|
||||
pub const ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot);
|
||||
@@ -115,15 +139,35 @@ _attributes: Attribute.List = .{},
|
||||
// work to resolve the proto).
|
||||
_proto_canary: if (lp.IS_DEBUG) *Node else void = undefined,
|
||||
|
||||
pub const Type = union(enum) {
|
||||
html: *Html,
|
||||
svg: *Svg,
|
||||
pub const Type = enum(u8) {
|
||||
html,
|
||||
svg,
|
||||
};
|
||||
|
||||
pub fn Subtype(comptime tag: Type) type {
|
||||
return switch (tag) {
|
||||
.html => Html,
|
||||
.svg => Svg,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn subtype(self: *const Element, comptime T: type) *T {
|
||||
const offset = comptime Factory.chainOffsetOf(T, T) - Factory.chainOffsetOf(T, Element);
|
||||
const sub: *T = @ptrFromInt(@intFromPtr(self) + offset);
|
||||
if (comptime lp.IS_DEBUG) {
|
||||
// This pointer dance only works because the factory allocates the chain
|
||||
// in a contiguous block of memory. In debug, we assert this holds via
|
||||
// the _proto_canary back pointer.
|
||||
std.debug.assert(Factory.protoOf(sub) == self);
|
||||
}
|
||||
return sub;
|
||||
}
|
||||
|
||||
pub fn is(self: *Element, comptime T: type) ?*T {
|
||||
const type_name = @typeName(T);
|
||||
switch (self._type) {
|
||||
.html => |el| {
|
||||
.html => {
|
||||
const el = self.subtype(Html);
|
||||
if (T == Html) {
|
||||
return el;
|
||||
}
|
||||
@@ -131,7 +175,8 @@ pub fn is(self: *Element, comptime T: type) ?*T {
|
||||
return el.is(T);
|
||||
}
|
||||
},
|
||||
.svg => |svg| {
|
||||
.svg => {
|
||||
const svg = self.subtype(Svg);
|
||||
if (T == Svg) {
|
||||
return svg;
|
||||
}
|
||||
@@ -178,177 +223,183 @@ pub fn isEqualNode(self: *Element, other: *Element) bool {
|
||||
|
||||
pub fn getTagNameLower(self: *const Element) []const u8 {
|
||||
switch (self._type) {
|
||||
.html => |he| switch (he._type) {
|
||||
.custom => |ce| {
|
||||
@branchHint(.unlikely);
|
||||
return ce._tag_name.str();
|
||||
},
|
||||
else => return switch (he._type) {
|
||||
.anchor => "a",
|
||||
.area => "area",
|
||||
.base => "base",
|
||||
.body => "body",
|
||||
.br => "br",
|
||||
.button => "button",
|
||||
.canvas => "canvas",
|
||||
.custom => |e| e._tag_name.str(),
|
||||
.data => "data",
|
||||
.datalist => "datalist",
|
||||
.details => "details",
|
||||
.dialog => "dialog",
|
||||
.directory => "dir",
|
||||
.div => "div",
|
||||
.dl => "dl",
|
||||
.embed => "embed",
|
||||
.fieldset => "fieldset",
|
||||
.font => "font",
|
||||
.frameset => "frameset",
|
||||
.form => "form",
|
||||
.generic => |e| e._tag_name.str(),
|
||||
.heading => |e| e._tag_name.str(),
|
||||
.head => "head",
|
||||
.html => "html",
|
||||
.hr => "hr",
|
||||
.iframe => "iframe",
|
||||
.img => "img",
|
||||
.input => "input",
|
||||
.label => "label",
|
||||
.legend => "legend",
|
||||
.li => "li",
|
||||
.link => "link",
|
||||
.map => "map",
|
||||
.marquee => "marquee",
|
||||
.media => |m| switch (m._type) {
|
||||
.audio => "audio",
|
||||
.video => "video",
|
||||
.generic => "media",
|
||||
.html => {
|
||||
const he = self.subtype(Html);
|
||||
switch (he._type) {
|
||||
.custom => {
|
||||
@branchHint(.unlikely);
|
||||
return he.subtype(Html.Custom)._tag_name.str();
|
||||
},
|
||||
.meta => "meta",
|
||||
.meter => "meter",
|
||||
.mod => |e| e._tag_name.str(),
|
||||
.object => "object",
|
||||
.ol => "ol",
|
||||
.optgroup => "optgroup",
|
||||
.option => "option",
|
||||
.output => "output",
|
||||
.p => "p",
|
||||
.picture => "picture",
|
||||
.param => "param",
|
||||
.pre => "pre",
|
||||
.progress => "progress",
|
||||
.quote => |e| e._tag_name.str(),
|
||||
.script => "script",
|
||||
.select => "select",
|
||||
.slot => "slot",
|
||||
.source => "source",
|
||||
.span => "span",
|
||||
.style => "style",
|
||||
.table => "table",
|
||||
.table_caption => "caption",
|
||||
.table_cell => |e| e._tag_name.str(),
|
||||
.table_col => |e| e._tag_name.str(),
|
||||
.table_row => "tr",
|
||||
.table_section => |e| e._tag_name.str(),
|
||||
.template => "template",
|
||||
.textarea => "textarea",
|
||||
.time => "time",
|
||||
.title => "title",
|
||||
.track => "track",
|
||||
.ul => "ul",
|
||||
.unknown => |e| e._tag_name.str(),
|
||||
},
|
||||
else => return switch (he._type) {
|
||||
.anchor => "a",
|
||||
.area => "area",
|
||||
.base => "base",
|
||||
.body => "body",
|
||||
.br => "br",
|
||||
.button => "button",
|
||||
.canvas => "canvas",
|
||||
.custom => he.subtype(Html.Custom)._tag_name.str(),
|
||||
.data => "data",
|
||||
.datalist => "datalist",
|
||||
.details => "details",
|
||||
.dialog => "dialog",
|
||||
.directory => "dir",
|
||||
.div => "div",
|
||||
.dl => "dl",
|
||||
.embed => "embed",
|
||||
.fieldset => "fieldset",
|
||||
.font => "font",
|
||||
.frameset => "frameset",
|
||||
.form => "form",
|
||||
.generic => he.subtype(Html.Generic)._tag_name.str(),
|
||||
.heading => he.subtype(Html.Heading)._tag_name.str(),
|
||||
.head => "head",
|
||||
.html => "html",
|
||||
.hr => "hr",
|
||||
.iframe => "iframe",
|
||||
.img => "img",
|
||||
.input => "input",
|
||||
.label => "label",
|
||||
.legend => "legend",
|
||||
.li => "li",
|
||||
.link => "link",
|
||||
.map => "map",
|
||||
.marquee => "marquee",
|
||||
.media => switch (he.subtype(Html.Media)._type) {
|
||||
.audio => "audio",
|
||||
.video => "video",
|
||||
.generic => "media",
|
||||
},
|
||||
.meta => "meta",
|
||||
.meter => "meter",
|
||||
.mod => he.subtype(Html.Mod)._tag_name.str(),
|
||||
.object => "object",
|
||||
.ol => "ol",
|
||||
.optgroup => "optgroup",
|
||||
.option => "option",
|
||||
.output => "output",
|
||||
.p => "p",
|
||||
.picture => "picture",
|
||||
.param => "param",
|
||||
.pre => "pre",
|
||||
.progress => "progress",
|
||||
.quote => he.subtype(Html.Quote)._tag_name.str(),
|
||||
.script => "script",
|
||||
.select => "select",
|
||||
.slot => "slot",
|
||||
.source => "source",
|
||||
.span => "span",
|
||||
.style => "style",
|
||||
.table => "table",
|
||||
.table_caption => "caption",
|
||||
.table_cell => he.subtype(Html.TableCell)._tag_name.str(),
|
||||
.table_col => he.subtype(Html.TableCol)._tag_name.str(),
|
||||
.table_row => "tr",
|
||||
.table_section => he.subtype(Html.TableSection)._tag_name.str(),
|
||||
.template => "template",
|
||||
.textarea => "textarea",
|
||||
.time => "time",
|
||||
.title => "title",
|
||||
.track => "track",
|
||||
.ul => "ul",
|
||||
.unknown => he.subtype(Html.Unknown)._tag_name.str(),
|
||||
},
|
||||
}
|
||||
},
|
||||
.svg => |svg| return svg._tag_name.str(),
|
||||
.svg => return self.subtype(Svg)._tag_name.str(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 {
|
||||
return switch (self._type) {
|
||||
.html => |he| switch (he._type) {
|
||||
.anchor => "A",
|
||||
.area => "AREA",
|
||||
.base => "BASE",
|
||||
.body => "BODY",
|
||||
.br => "BR",
|
||||
.button => "BUTTON",
|
||||
.canvas => "CANVAS",
|
||||
.custom => |e| upperTagName(&e._tag_name, buf),
|
||||
.data => "DATA",
|
||||
.datalist => "DATALIST",
|
||||
.details => "DETAILS",
|
||||
.dialog => "DIALOG",
|
||||
.directory => "DIR",
|
||||
.div => "DIV",
|
||||
.dl => "DL",
|
||||
.embed => "EMBED",
|
||||
.fieldset => "FIELDSET",
|
||||
.font => "FONT",
|
||||
.frameset => "FRAMESET",
|
||||
.form => "FORM",
|
||||
.generic => |e| upperTagName(&e._tag_name, buf),
|
||||
.heading => |e| upperTagName(&e._tag_name, buf),
|
||||
.head => "HEAD",
|
||||
.html => "HTML",
|
||||
.hr => "HR",
|
||||
.iframe => "IFRAME",
|
||||
.img => "IMG",
|
||||
.input => "INPUT",
|
||||
.label => "LABEL",
|
||||
.legend => "LEGEND",
|
||||
.li => "LI",
|
||||
.link => "LINK",
|
||||
.map => "MAP",
|
||||
.marquee => "MARQUEE",
|
||||
.meta => "META",
|
||||
.media => |m| switch (m._type) {
|
||||
.audio => "AUDIO",
|
||||
.video => "VIDEO",
|
||||
.generic => "MEDIA",
|
||||
},
|
||||
.meter => "METER",
|
||||
.mod => |e| upperTagName(&e._tag_name, buf),
|
||||
.object => "OBJECT",
|
||||
.ol => "OL",
|
||||
.optgroup => "OPTGROUP",
|
||||
.option => "OPTION",
|
||||
.output => "OUTPUT",
|
||||
.p => "P",
|
||||
.picture => "PICTURE",
|
||||
.param => "PARAM",
|
||||
.pre => "PRE",
|
||||
.progress => "PROGRESS",
|
||||
.quote => |e| upperTagName(&e._tag_name, buf),
|
||||
.script => "SCRIPT",
|
||||
.select => "SELECT",
|
||||
.slot => "SLOT",
|
||||
.source => "SOURCE",
|
||||
.span => "SPAN",
|
||||
.style => "STYLE",
|
||||
.table => "TABLE",
|
||||
.table_caption => "CAPTION",
|
||||
.table_cell => |e| upperTagName(&e._tag_name, buf),
|
||||
.table_col => |e| upperTagName(&e._tag_name, buf),
|
||||
.table_row => "TR",
|
||||
.table_section => |e| upperTagName(&e._tag_name, buf),
|
||||
.template => "TEMPLATE",
|
||||
.textarea => "TEXTAREA",
|
||||
.time => "TIME",
|
||||
.title => "TITLE",
|
||||
.track => "TRACK",
|
||||
.ul => "UL",
|
||||
.unknown => |e| switch (self._namespace) {
|
||||
.html => upperTagName(&e._tag_name, buf),
|
||||
.svg, .xml, .mathml, .unknown, .null => e._tag_name.str(),
|
||||
},
|
||||
.html => blk: {
|
||||
const he = self.subtype(Html);
|
||||
break :blk switch (he._type) {
|
||||
.anchor => "A",
|
||||
.area => "AREA",
|
||||
.base => "BASE",
|
||||
.body => "BODY",
|
||||
.br => "BR",
|
||||
.button => "BUTTON",
|
||||
.canvas => "CANVAS",
|
||||
.custom => upperTagName(&he.subtype(Html.Custom)._tag_name, buf),
|
||||
.data => "DATA",
|
||||
.datalist => "DATALIST",
|
||||
.details => "DETAILS",
|
||||
.dialog => "DIALOG",
|
||||
.directory => "DIR",
|
||||
.div => "DIV",
|
||||
.dl => "DL",
|
||||
.embed => "EMBED",
|
||||
.fieldset => "FIELDSET",
|
||||
.font => "FONT",
|
||||
.frameset => "FRAMESET",
|
||||
.form => "FORM",
|
||||
.generic => upperTagName(&he.subtype(Html.Generic)._tag_name, buf),
|
||||
.heading => upperTagName(&he.subtype(Html.Heading)._tag_name, buf),
|
||||
.head => "HEAD",
|
||||
.html => "HTML",
|
||||
.hr => "HR",
|
||||
.iframe => "IFRAME",
|
||||
.img => "IMG",
|
||||
.input => "INPUT",
|
||||
.label => "LABEL",
|
||||
.legend => "LEGEND",
|
||||
.li => "LI",
|
||||
.link => "LINK",
|
||||
.map => "MAP",
|
||||
.marquee => "MARQUEE",
|
||||
.meta => "META",
|
||||
.media => switch (he.subtype(Html.Media)._type) {
|
||||
.audio => "AUDIO",
|
||||
.video => "VIDEO",
|
||||
.generic => "MEDIA",
|
||||
},
|
||||
.meter => "METER",
|
||||
.mod => upperTagName(&he.subtype(Html.Mod)._tag_name, buf),
|
||||
.object => "OBJECT",
|
||||
.ol => "OL",
|
||||
.optgroup => "OPTGROUP",
|
||||
.option => "OPTION",
|
||||
.output => "OUTPUT",
|
||||
.p => "P",
|
||||
.picture => "PICTURE",
|
||||
.param => "PARAM",
|
||||
.pre => "PRE",
|
||||
.progress => "PROGRESS",
|
||||
.quote => upperTagName(&he.subtype(Html.Quote)._tag_name, buf),
|
||||
.script => "SCRIPT",
|
||||
.select => "SELECT",
|
||||
.slot => "SLOT",
|
||||
.source => "SOURCE",
|
||||
.span => "SPAN",
|
||||
.style => "STYLE",
|
||||
.table => "TABLE",
|
||||
.table_caption => "CAPTION",
|
||||
.table_cell => upperTagName(&he.subtype(Html.TableCell)._tag_name, buf),
|
||||
.table_col => upperTagName(&he.subtype(Html.TableCol)._tag_name, buf),
|
||||
.table_row => "TR",
|
||||
.table_section => upperTagName(&he.subtype(Html.TableSection)._tag_name, buf),
|
||||
.template => "TEMPLATE",
|
||||
.textarea => "TEXTAREA",
|
||||
.time => "TIME",
|
||||
.title => "TITLE",
|
||||
.track => "TRACK",
|
||||
.ul => "UL",
|
||||
.unknown => switch (self._namespace) {
|
||||
.html => upperTagName(&he.subtype(Html.Unknown)._tag_name, buf),
|
||||
.svg, .xml, .mathml, .unknown, .null => he.subtype(Html.Unknown)._tag_name.str(),
|
||||
},
|
||||
};
|
||||
},
|
||||
.svg => |svg| svg._tag_name.str(),
|
||||
.svg => self.subtype(Svg)._tag_name.str(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getTagNameDump(self: *const Element) []const u8 {
|
||||
switch (self._type) {
|
||||
.html => return self.getTagNameLower(),
|
||||
.svg => |svg| return svg._tag_name.str(),
|
||||
.svg => return self.subtype(Svg)._tag_name.str(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1255,17 +1306,30 @@ pub fn checkVisibility(self: *Element, opts_: ?CheckVisibilityOpts, frame: *Fram
|
||||
});
|
||||
}
|
||||
|
||||
pub fn getElementDimensions(self: *Element, frame: *Frame) struct { width: f64, height: f64 } {
|
||||
var width: f64 = 5.0;
|
||||
var height: f64 = 5.0;
|
||||
pub const Dimensions = struct {
|
||||
width: f64,
|
||||
height: f64,
|
||||
// if the value is explicit (e.g. inline style, width attribute, ...) or defaulted
|
||||
explicit_width: bool = false,
|
||||
explicit_height: bool = false,
|
||||
};
|
||||
|
||||
pub fn getElementDimensions(self: *Element, frame: *Frame) Dimensions {
|
||||
var dims: Dimensions = .{ .width = 5.0, .height = 5.0 };
|
||||
|
||||
if (self.getStyle(frame)) |style| {
|
||||
const decl = style.asCSSStyleDeclaration();
|
||||
width = CSS.parseDimensionViewport(decl.getPropertyValue("width", frame), frame) orelse 5.0;
|
||||
height = CSS.parseDimensionViewport(decl.getPropertyValue("height", frame), frame) orelse 5.0;
|
||||
if (CSS.parseDimensionViewport(decl.getPropertyValue("width", frame), frame)) |w| {
|
||||
dims.width = w;
|
||||
dims.explicit_width = true;
|
||||
}
|
||||
if (CSS.parseDimensionViewport(decl.getPropertyValue("height", frame), frame)) |h| {
|
||||
dims.height = h;
|
||||
dims.explicit_height = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (width == 5.0 or height == 5.0) {
|
||||
if (dims.width == 5.0 or dims.height == 5.0) {
|
||||
const tag = self.getTag();
|
||||
|
||||
// Root containers get large default size to contain descendant positions.
|
||||
@@ -1273,35 +1337,69 @@ pub fn getElementDimensions(self: *Element, frame: *Frame) struct { width: f64,
|
||||
// even very deep trees (100 levels) stay within 10,000px.
|
||||
// 100M pixels is plausible for very long documents.
|
||||
if (tag == .html or tag == .body) {
|
||||
if (width == 5.0) width = 1920.0;
|
||||
if (height == 5.0) height = 100_000_000.0;
|
||||
if (dims.width == 5.0) dims.width = 1920.0;
|
||||
if (dims.height == 5.0) dims.height = 100_000_000.0;
|
||||
} else if (tag == .img or tag == .iframe) {
|
||||
if (self.getAttributeSafe(comptime .wrap("width"))) |w| {
|
||||
width = std.fmt.parseFloat(f64, w) catch width;
|
||||
if (std.fmt.parseFloat(f64, w)) |parsed| {
|
||||
dims.width = parsed;
|
||||
dims.explicit_width = true;
|
||||
} else |_| {}
|
||||
}
|
||||
if (self.getAttributeSafe(comptime .wrap("height"))) |h| {
|
||||
height = std.fmt.parseFloat(f64, h) catch height;
|
||||
if (std.fmt.parseFloat(f64, h)) |parsed| {
|
||||
dims.height = parsed;
|
||||
dims.explicit_height = true;
|
||||
} else |_| {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return .{ .width = width, .height = height };
|
||||
return dims;
|
||||
}
|
||||
|
||||
// We can't do this correctly without full styles and more rendering. We also
|
||||
// can't just ignore the children since some sites append nodes until a certain
|
||||
// width / height treshold is reached. If the size isn't explicit, we fallback
|
||||
// to contentWidth/contentHeight
|
||||
pub fn getClientWidth(self: *Element, frame: *Frame) f64 {
|
||||
if (!self.checkVisibilityCached(null, frame)) {
|
||||
var visibility_cache: VisibilityCache = .{};
|
||||
return self.getClientWidthWithCache(frame, &visibility_cache);
|
||||
}
|
||||
|
||||
pub fn getClientWidthWithCache(self: *Element, frame: *Frame, visibility_cache: *VisibilityCache) f64 {
|
||||
if (!self.checkVisibilityCached(visibility_cache, frame)) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
const dims = self.getElementDimensions(frame);
|
||||
return dims.width;
|
||||
|
||||
const tag = self.getTag();
|
||||
if (tag == .html or tag == .body or dims.explicit_width) {
|
||||
return dims.width;
|
||||
}
|
||||
|
||||
return @max(dims.width, self.contentWidth(frame, visibility_cache));
|
||||
}
|
||||
|
||||
pub fn getClientHeight(self: *Element, frame: *Frame) f64 {
|
||||
if (!self.checkVisibilityCached(null, frame)) {
|
||||
var visibility_cache: VisibilityCache = .{};
|
||||
return self.getClientHeightWithCache(frame, &visibility_cache);
|
||||
}
|
||||
|
||||
pub fn getClientHeightWithCache(self: *Element, frame: *Frame, visibility_cache: *VisibilityCache) f64 {
|
||||
if (!self.checkVisibilityCached(visibility_cache, frame)) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
const dims = self.getElementDimensions(frame);
|
||||
return dims.height;
|
||||
|
||||
const tag = self.getTag();
|
||||
if (tag == .html or tag == .body or dims.explicit_height) {
|
||||
return dims.height;
|
||||
}
|
||||
|
||||
return @max(dims.height, self.contentHeight(frame, visibility_cache));
|
||||
}
|
||||
|
||||
pub fn getBoundingClientRect(self: *Element, frame: *Frame) !*DOMRect {
|
||||
@@ -1916,81 +2014,84 @@ fn upperTagName(tag_name: *String, buf: []u8) []const u8 {
|
||||
|
||||
pub fn getTag(self: *const Element) Tag {
|
||||
return switch (self._type) {
|
||||
.html => |he| switch (he._type) {
|
||||
.anchor => .anchor,
|
||||
.area => .area,
|
||||
.base => .base,
|
||||
.div => .div,
|
||||
.dl => .dl,
|
||||
.embed => .embed,
|
||||
.form => .form,
|
||||
.p => .p,
|
||||
.custom => .custom,
|
||||
.data => .data,
|
||||
.datalist => .datalist,
|
||||
.details => .details,
|
||||
.dialog => .dialog,
|
||||
.directory => .directory,
|
||||
.iframe => .iframe,
|
||||
.img => .img,
|
||||
.br => .br,
|
||||
.button => .button,
|
||||
.canvas => .canvas,
|
||||
.fieldset => .fieldset,
|
||||
.font => .font,
|
||||
.frameset => .frameset,
|
||||
.heading => |h| h._tag,
|
||||
.label => .label,
|
||||
.legend => .legend,
|
||||
.li => .li,
|
||||
.map => .map,
|
||||
.marquee => .marquee,
|
||||
.ul => .ul,
|
||||
.ol => .ol,
|
||||
.object => .object,
|
||||
.optgroup => .optgroup,
|
||||
.output => .output,
|
||||
.picture => .picture,
|
||||
.param => .param,
|
||||
.pre => .pre,
|
||||
.generic => |g| g._tag,
|
||||
.media => |m| switch (m._type) {
|
||||
.audio => .audio,
|
||||
.video => .video,
|
||||
.generic => .media,
|
||||
},
|
||||
.meter => .meter,
|
||||
.mod => |m| m._tag,
|
||||
.progress => .progress,
|
||||
.quote => |q| q._tag,
|
||||
.script => .script,
|
||||
.select => .select,
|
||||
.slot => .slot,
|
||||
.source => .source,
|
||||
.span => .span,
|
||||
.option => .option,
|
||||
.table => .table,
|
||||
.table_caption => .caption,
|
||||
.table_cell => |tc| tc._tag,
|
||||
.table_col => |tc| tc._tag,
|
||||
.table_row => .tr,
|
||||
.table_section => |ts| ts._tag,
|
||||
.template => .template,
|
||||
.textarea => .textarea,
|
||||
.time => .time,
|
||||
.track => .track,
|
||||
.input => .input,
|
||||
.link => .link,
|
||||
.meta => .meta,
|
||||
.hr => .hr,
|
||||
.style => .style,
|
||||
.title => .title,
|
||||
.body => .body,
|
||||
.html => .html,
|
||||
.head => .head,
|
||||
.unknown => .unknown,
|
||||
.html => blk: {
|
||||
const he = self.subtype(Html);
|
||||
break :blk switch (he._type) {
|
||||
.anchor => .anchor,
|
||||
.area => .area,
|
||||
.base => .base,
|
||||
.div => .div,
|
||||
.dl => .dl,
|
||||
.embed => .embed,
|
||||
.form => .form,
|
||||
.p => .p,
|
||||
.custom => .custom,
|
||||
.data => .data,
|
||||
.datalist => .datalist,
|
||||
.details => .details,
|
||||
.dialog => .dialog,
|
||||
.directory => .directory,
|
||||
.iframe => .iframe,
|
||||
.img => .img,
|
||||
.br => .br,
|
||||
.button => .button,
|
||||
.canvas => .canvas,
|
||||
.fieldset => .fieldset,
|
||||
.font => .font,
|
||||
.frameset => .frameset,
|
||||
.heading => he.subtype(Html.Heading)._tag,
|
||||
.label => .label,
|
||||
.legend => .legend,
|
||||
.li => .li,
|
||||
.map => .map,
|
||||
.marquee => .marquee,
|
||||
.ul => .ul,
|
||||
.ol => .ol,
|
||||
.object => .object,
|
||||
.optgroup => .optgroup,
|
||||
.output => .output,
|
||||
.picture => .picture,
|
||||
.param => .param,
|
||||
.pre => .pre,
|
||||
.generic => he.subtype(Html.Generic)._tag,
|
||||
.media => switch (he.subtype(Html.Media)._type) {
|
||||
.audio => .audio,
|
||||
.video => .video,
|
||||
.generic => .media,
|
||||
},
|
||||
.meter => .meter,
|
||||
.mod => he.subtype(Html.Mod)._tag,
|
||||
.progress => .progress,
|
||||
.quote => he.subtype(Html.Quote)._tag,
|
||||
.script => .script,
|
||||
.select => .select,
|
||||
.slot => .slot,
|
||||
.source => .source,
|
||||
.span => .span,
|
||||
.option => .option,
|
||||
.table => .table,
|
||||
.table_caption => .caption,
|
||||
.table_cell => he.subtype(Html.TableCell)._tag,
|
||||
.table_col => he.subtype(Html.TableCol)._tag,
|
||||
.table_row => .tr,
|
||||
.table_section => he.subtype(Html.TableSection)._tag,
|
||||
.template => .template,
|
||||
.textarea => .textarea,
|
||||
.time => .time,
|
||||
.track => .track,
|
||||
.input => .input,
|
||||
.link => .link,
|
||||
.meta => .meta,
|
||||
.hr => .hr,
|
||||
.style => .style,
|
||||
.title => .title,
|
||||
.body => .body,
|
||||
.html => .html,
|
||||
.head => .head,
|
||||
.unknown => .unknown,
|
||||
};
|
||||
},
|
||||
.svg => |se| se.getTag(),
|
||||
.svg => self.subtype(Svg).getTag(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2315,6 +2416,7 @@ pub const JsApi = struct {
|
||||
pub const previousElementSibling = bridge.accessor(Element.previousElementSibling, null, .{});
|
||||
pub const childElementCount = bridge.accessor(Element.getChildElementCount, null, .{});
|
||||
pub const matches = bridge.function(Element.matches, .{});
|
||||
pub const webkitMatchesSelector = bridge.function(Element.matches, .{});
|
||||
pub const querySelector = bridge.function(Element.querySelector, .{});
|
||||
pub const querySelectorAll = bridge.function(Element.querySelectorAll, .{});
|
||||
pub const closest = bridge.function(Element.closest, .{});
|
||||
@@ -2353,22 +2455,21 @@ pub const Build = struct {
|
||||
// Calls `func_name` with `args` on the most specific type where it is
|
||||
// implement. This could be on the Element itself.
|
||||
pub fn call(self: *const Element, comptime func_name: []const u8, args: anytype) !bool {
|
||||
inline for (@typeInfo(Element.Type).@"union".fields) |f| {
|
||||
if (@field(Element.Type, f.name) == self._type) {
|
||||
// The inner type implements this function. Call it and we're done.
|
||||
const S = reflect.Struct(f.type);
|
||||
switch (self._type) {
|
||||
inline else => |tag| {
|
||||
const S = Subtype(tag);
|
||||
if (@hasDecl(S, "Build")) {
|
||||
// The inner type has its own "call" method. Defer to it.
|
||||
if (@hasDecl(S.Build, "call")) {
|
||||
const sub = @field(self._type, f.name);
|
||||
return S.Build.call(sub, func_name, args);
|
||||
return S.Build.call(self.subtype(S), func_name, args);
|
||||
}
|
||||
|
||||
// The inner type implements this function. Call it and we're done.
|
||||
if (@hasDecl(f.type, func_name)) {
|
||||
return @call(.auto, @field(f.type, func_name), args);
|
||||
if (@hasDecl(S, func_name)) {
|
||||
return @call(.auto, @field(S, func_name), args);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
if (@hasDecl(Element.Build, func_name)) {
|
||||
|
||||
@@ -21,6 +21,7 @@ const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
const EventManager = @import("../EventManager.zig");
|
||||
|
||||
const Node = @import("Node.zig");
|
||||
const EventTarget = @import("EventTarget.zig");
|
||||
@@ -40,6 +41,7 @@ _type_string: String,
|
||||
_target: ?*EventTarget = null,
|
||||
_current_target: ?*EventTarget = null,
|
||||
_dispatch_target: ?*EventTarget = null, // Original target for composedPath()
|
||||
_dispatch_related_target: ?*EventTarget = null,
|
||||
_prevent_default: bool = false,
|
||||
_stop_propagation: bool = false,
|
||||
_stop_immediate_propagation: bool = false,
|
||||
@@ -325,113 +327,61 @@ pub fn composedPath(self: *Event, exec: *Execution) ![]const *EventTarget {
|
||||
else => return &.{},
|
||||
};
|
||||
|
||||
// Build the path by walking up from target
|
||||
var path_len: usize = 0;
|
||||
var path_buffer: [128]*EventTarget = undefined;
|
||||
var stopped_at_shadow_boundary = false;
|
||||
|
||||
// Track closed shadow boundaries (position in path and host position)
|
||||
var closed_shadow_boundary: ?struct { shadow_end: usize, host_start: usize } = null;
|
||||
|
||||
const frame_ = switch (exec.js.global) {
|
||||
.frame => |frame| frame,
|
||||
else => null,
|
||||
};
|
||||
|
||||
const target_root = target_node.getRootNode(.{});
|
||||
var node: ?*Node = target_node;
|
||||
while (node) |n| {
|
||||
if (path_len >= path_buffer.len) {
|
||||
break;
|
||||
}
|
||||
path_buffer[path_len] = n.asEventTarget();
|
||||
path_len += 1;
|
||||
|
||||
// Check if this node is a shadow root
|
||||
if (n._type == .document_fragment) {
|
||||
const df = n.subtype(Node.DocumentFragment);
|
||||
if (df._type == .shadow_root) {
|
||||
const shadow = df._type.shadow_root;
|
||||
|
||||
if (!self._composed and n == target_root) {
|
||||
stopped_at_shadow_boundary = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Track the first closed shadow boundary we encounter
|
||||
if (shadow._mode == .closed and closed_shadow_boundary == null) {
|
||||
// Mark where the shadow root is in the path
|
||||
// The next element will be the host
|
||||
closed_shadow_boundary = .{
|
||||
.shadow_end = path_len - 1, // index of shadow root
|
||||
.host_start = path_len, // index where host will be
|
||||
};
|
||||
}
|
||||
|
||||
// Jump to the shadow host and continue
|
||||
node = shadow._host.asNode();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// an assigned slottable's event-path parent is its assigned slot,
|
||||
// routing the event into the slot's shadow tree
|
||||
if (frame_) |frame| {
|
||||
if (frame._assigned_slots.get(n)) |slot| {
|
||||
node = slot.asNode();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
node = n._parent;
|
||||
var path_buffer: [128]*EventTarget = undefined;
|
||||
var path_len = EventManager.buildEventPath(target_node, self, frame_, &path_buffer).len;
|
||||
if (path_len == 0) {
|
||||
return &.{};
|
||||
}
|
||||
|
||||
// Add window at the end. It only participates when propagation did not stop
|
||||
// at a shadow boundary...
|
||||
if (stopped_at_shadow_boundary == false) {
|
||||
// ... AND when the tree's root is a document
|
||||
const root_is_document = path_len > 0 and switch (path_buffer[path_len - 1]._type) {
|
||||
.node => |n| n._type == .document,
|
||||
else => false,
|
||||
// Window follows the document at the end of the path. A path that stopped
|
||||
// early — at a shadow boundary, or at the relatedTarget — doesn't end on
|
||||
// the document and so doesn't reach it.
|
||||
const root_is_document = switch (path_buffer[path_len - 1]._type) {
|
||||
.node => |n| n._type == .document,
|
||||
else => false,
|
||||
};
|
||||
if (root_is_document and path_len < path_buffer.len) {
|
||||
if (frame_) |frame| {
|
||||
path_buffer[path_len] = frame.window.asEventTarget();
|
||||
path_len += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// The host of the first closed shadow root on the path. Everything before
|
||||
// it is inside that root and hidden from a currentTarget outside it.
|
||||
var closed_host_index: ?usize = null;
|
||||
for (path_buffer[0..path_len], 0..) |entry, i| {
|
||||
const node = switch (entry._type) {
|
||||
.node => |n| n,
|
||||
else => continue,
|
||||
};
|
||||
if (root_is_document) {
|
||||
if (path_len < path_buffer.len) {
|
||||
switch (exec.js.global) {
|
||||
.worker => {},
|
||||
.frame => |frame| {
|
||||
path_buffer[path_len] = frame.window.asEventTarget();
|
||||
path_len += 1;
|
||||
},
|
||||
}
|
||||
}
|
||||
const shadow = node.is(Node.ShadowRoot) orelse continue;
|
||||
if (shadow._mode == .closed) {
|
||||
closed_host_index = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine visible path based on current_target and closed shadow boundaries
|
||||
var visible_start_index: usize = 0;
|
||||
|
||||
if (closed_shadow_boundary) |boundary| {
|
||||
// Check if current_target is outside the closed shadow
|
||||
// If current_target is null or is at/after the host position, hide shadow internals
|
||||
const current_target = self._current_target;
|
||||
|
||||
if (current_target) |ct| {
|
||||
// Find current_target in the path
|
||||
var ct_index: ?usize = null;
|
||||
if (closed_host_index) |host_index| {
|
||||
// Find current_target in the path; if it's at or after the host, it's
|
||||
// outside the closed shadow and must not see the nodes inside it.
|
||||
if (self._current_target) |ct| {
|
||||
for (path_buffer[0..path_len], 0..) |elem, i| {
|
||||
if (elem == ct) {
|
||||
ct_index = i;
|
||||
if (i >= host_index) {
|
||||
visible_start_index = host_index;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If current_target is at or after the host (outside the closed shadow),
|
||||
// hide everything from target up to the host
|
||||
if (ct_index) |idx| {
|
||||
if (idx >= boundary.host_start) {
|
||||
visible_start_index = boundary.host_start;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ pub fn forEach(self: *EventCounts, cb_: js.Function, js_this_: ?js.Object) !void
|
||||
const cb = if (js_this_) |js_this| try cb_.withThis(js_this) else cb_;
|
||||
|
||||
for (tracked_event_types, self._counts) |event_type, count| {
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
var caught: js.TryCatch.Caught = .{};
|
||||
cb.tryCall(void, .{ count, event_type, self }, &caught) catch {
|
||||
log.debug(.js, "forEach callback", .{ .caught = caught, .source = "EventCounts" });
|
||||
};
|
||||
|
||||
@@ -119,7 +119,7 @@ pub fn getTitle(self: *HTMLDocument, frame: *Frame) ![]const u8 {
|
||||
var started = false;
|
||||
var in_whitespace = false;
|
||||
var result: std.ArrayList(u8) = .empty;
|
||||
try result.ensureTotalCapacity(frame.local_arena, text.len);
|
||||
try result.ensureTotalCapacityPrecise(frame.local_arena, text.len);
|
||||
|
||||
for (text) |c| {
|
||||
const is_ascii_ws = c == ' ' or c == '\t' or c == '\n' or c == '\r' or c == '\x0C';
|
||||
|
||||
@@ -302,7 +302,7 @@ pub fn deliverEntries(self: *IntersectionObserver, frame: *Frame) !void {
|
||||
}
|
||||
|
||||
const entries = try self.takeRecords(frame);
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
var caught: js.TryCatch.Caught = .{};
|
||||
|
||||
var ls: js.Local.Scope = undefined;
|
||||
frame.js.localScope(&ls);
|
||||
|
||||
@@ -100,7 +100,7 @@ pub fn init() KeyValueList {
|
||||
}
|
||||
|
||||
pub fn ensureTotalCapacity(self: *KeyValueList, allocator: Allocator, n: usize) !void {
|
||||
return self._entries.ensureTotalCapacity(allocator, n);
|
||||
return self._entries.ensureTotalCapacityPrecise(allocator, n);
|
||||
}
|
||||
|
||||
pub fn get(self: *const KeyValueList, name: []const u8) ?[]const u8 {
|
||||
|
||||
@@ -195,7 +195,7 @@ pub const ModelContextClient = struct {
|
||||
defer ls.deinit();
|
||||
const resolver = ls.local.createPromiseResolver();
|
||||
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
var caught: js.TryCatch.Caught = .{};
|
||||
if (callback.tryCall(js.Value, .{}, &caught)) |result| {
|
||||
// The callback may itself return a thenable; resolving with its
|
||||
// value lets V8's promise resolution machinery unwrap it.
|
||||
|
||||
@@ -348,7 +348,7 @@ pub fn deliverRecords(self: *MutationObserver, frame: *Frame) !void {
|
||||
frame.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
var caught: js.TryCatch.Caught = .{};
|
||||
ls.toLocal(self._callback).tryCallWithThis(void, self, .{ records, self }, &caught) catch |err| {
|
||||
log.err(.frame, "MutObserver.deliverRecords", .{ .err = err, .caught = caught });
|
||||
return err;
|
||||
|
||||
+16
-12
@@ -464,7 +464,7 @@ pub fn getChildTextContent(self: *Node, writer: *std.Io.Writer) error{WriteFaile
|
||||
var it = self.childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
if (child.is(CData.Text)) |text| {
|
||||
try writer.writeAll(text._proto._data.str());
|
||||
try writer.writeAll(text.asCData()._data.str());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -476,7 +476,7 @@ pub fn childTextContentLen(self: *Node) usize {
|
||||
var it = self.childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
if (child.is(CData.Text)) |text| {
|
||||
len += text._proto._data.str().len;
|
||||
len += text.asCData()._data.str().len;
|
||||
}
|
||||
}
|
||||
return len;
|
||||
@@ -636,15 +636,15 @@ pub fn isEqualChildren(a: *Node, b: *Node) bool {
|
||||
return a_count == b_count;
|
||||
}
|
||||
|
||||
// The shadow root whose tree this node belongs to, or null when it belongs to
|
||||
// a document tree or a detached one. Inclusive: a shadow root is in its own
|
||||
// tree.
|
||||
pub fn containingShadowRoot(self: *Node) ?*ShadowRoot {
|
||||
return self.getRootNode(.{}).is(ShadowRoot);
|
||||
}
|
||||
|
||||
pub fn isInShadowTree(self: *Node) bool {
|
||||
var node = self._parent;
|
||||
while (node) |n| {
|
||||
if (n.is(ShadowRoot) != null) {
|
||||
return true;
|
||||
}
|
||||
node = n._parent;
|
||||
}
|
||||
return false;
|
||||
return self.containingShadowRoot() != null;
|
||||
}
|
||||
|
||||
pub fn isConnected(self: *const Node) bool {
|
||||
@@ -1209,6 +1209,10 @@ const CloneError = error{
|
||||
ExecutionTerminated,
|
||||
};
|
||||
pub fn cloneNode(self: *Node, deep_: ?bool, frame: *Frame) CloneError!*Node {
|
||||
if (self.is(ShadowRoot) != null) {
|
||||
return error.NotSupported;
|
||||
}
|
||||
|
||||
const deep = deep_ orelse false;
|
||||
switch (self._type) {
|
||||
.cdata => {
|
||||
@@ -1389,7 +1393,7 @@ fn _normalize(self: *Node, allocator: Allocator, buffer: *std.ArrayList(u8), fra
|
||||
continue;
|
||||
};
|
||||
|
||||
if (text_node._proto.getData().len == 0) {
|
||||
if (text_node.asCData().getData().len == 0) {
|
||||
frame.removeNode(self, current_node, .{ .will_be_reconnected = false });
|
||||
child = next_node;
|
||||
continue;
|
||||
@@ -1407,7 +1411,7 @@ fn _normalize(self: *Node, allocator: Allocator, buffer: *std.ArrayList(u8), fra
|
||||
next_node = node_to_merge.nextSibling();
|
||||
frame.removeNode(self, to_remove, .{ .will_be_reconnected = false });
|
||||
}
|
||||
text_node._proto._data = try frame.dupeSSO(buffer.items);
|
||||
text_node.asCData()._data = try frame.dupeSSO(buffer.items);
|
||||
buffer.clearRetainingCapacity();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ pub fn dispatch(self: *PerformanceObserver) !void {
|
||||
self._js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
var caught: js.TryCatch.Caught = .{};
|
||||
ls.toLocal(self._callback).tryCall(void, .{ EntryList{ ._entries = records }, self }, &caught) catch |err| {
|
||||
log.err(.frame, "PerfObserver.dispatch", .{ .err = err, .caught = caught });
|
||||
return err;
|
||||
|
||||
@@ -364,7 +364,7 @@ pub fn insertNode(self: *Range, node: *Node, frame: *Frame) !void {
|
||||
// records browsers do (one for the split-off node, one for
|
||||
// the inserted node).
|
||||
const second = try t.splitText(offset, frame);
|
||||
_ = try parent.insertBefore(node, second._proto.asNode(), frame);
|
||||
_ = try parent.insertBefore(node, second.asCData().asNode(), frame);
|
||||
} else {
|
||||
_ = try parent.insertBefore(node, container.nextSibling(), frame);
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ pub fn deliverEntries(self: *ResizeObserver, frame: *Frame) !void {
|
||||
return;
|
||||
}
|
||||
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
var caught: js.TryCatch.Caught = .{};
|
||||
|
||||
var ls: js.Local.Scope = undefined;
|
||||
frame.js.localScope(&ls);
|
||||
|
||||
@@ -124,6 +124,23 @@ pub fn setOnSlotChange(self: *ShadowRoot, callback: ?js.Function.Global, frame:
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getActiveElement(self: *ShadowRoot, frame: *Frame) ?*Element {
|
||||
const root = self.asNode();
|
||||
const document = root.ownerDocument(frame) orelse frame.document;
|
||||
|
||||
// This is answering two questions:
|
||||
// 1 - is the active element contained by me (if not, return null)
|
||||
// 2 - if it is, is there 1+ other shadowroot between us
|
||||
// a - if there is, return the nearest (to self) shadowroot's host
|
||||
// b - if there isn't, return the active element
|
||||
var candidate = document._active_element orelse return null;
|
||||
while (candidate.asNode().getRootNode(.{}) != root) {
|
||||
const shadow = candidate.asNode().containingShadowRoot() orelse return null;
|
||||
candidate = shadow._host;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
pub fn getElementById(self: *ShadowRoot, id: []const u8, frame: *Frame) ?*Element {
|
||||
if (id.len == 0) {
|
||||
return null;
|
||||
@@ -177,6 +194,7 @@ pub const JsApi = struct {
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const activeElement = bridge.accessor(ShadowRoot.getActiveElement, null, .{});
|
||||
pub const mode = bridge.accessor(ShadowRoot.getMode, null, .{});
|
||||
pub const host = bridge.accessor(ShadowRoot.getHost, null, .{});
|
||||
pub const delegatesFocus = bridge.accessor(ShadowRoot.getDelegatesFocus, null, .{});
|
||||
|
||||
@@ -212,7 +212,7 @@ fn httpHeaderCallback(transfer: *Transfer) !Transfer.HeaderResult {
|
||||
}
|
||||
|
||||
if (transfer.getContentLength()) |cl| {
|
||||
try self._script_buffer.ensureTotalCapacity(self._script_arena.?.allocator(), cl);
|
||||
try self._script_buffer.ensureTotalCapacityPrecise(self._script_arena.?.allocator(), cl);
|
||||
}
|
||||
|
||||
return .proceed;
|
||||
|
||||
@@ -60,8 +60,8 @@ pub fn getComputedLabel(_: *const WebDriver, element: *Element, frame: *Frame) !
|
||||
pub fn click(_: *const WebDriver, element: *Element, frame: *Frame) !void {
|
||||
if (element.is(Element.Html)) |html| {
|
||||
switch (html._type) {
|
||||
inline .button, .input, .textarea, .select => |i| {
|
||||
if (i.getDisabled()) {
|
||||
inline .button, .input, .textarea, .select => |tag| {
|
||||
if (html.subtype(Element.Html.Subtype(tag)).getDisabled()) {
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -639,6 +639,7 @@ pub fn reportError(self: *Window, err: js.Value, frame: *Frame) !void {
|
||||
// We still dispatch so that addEventListener('error', ...) listeners fire.
|
||||
try frame._event_manager.dispatchDirect(target, event, null, .{
|
||||
.context = "window.reportError",
|
||||
.run_microtasks = false,
|
||||
});
|
||||
|
||||
if (comptime lp.IS_TEST == false) {
|
||||
@@ -661,16 +662,15 @@ pub fn matchMedia(_: *const Window, query: []const u8, frame: *Frame) !*MediaQue
|
||||
}
|
||||
|
||||
pub fn getComputedStyle(_: *const Window, element: *Element, pseudo_element: ?[]const u8, frame: *Frame) !*CSSStyleProperties {
|
||||
if (pseudo_element) |pe| {
|
||||
if (pe.len != 0) {
|
||||
log.warn(.not_implemented, "window.GetComputedStyle", .{ .pseudo_element = pe });
|
||||
// Chrome hands out a distinct object per pseudo-element, so these
|
||||
// can't share the per-element cache entry.
|
||||
return CSSStyleProperties.init(element, true, frame);
|
||||
}
|
||||
}
|
||||
const gop = try frame._element_computed_styles.getOrPut(frame.arena, element);
|
||||
// :before/:after get their own cache entry and no warning: our answer
|
||||
// (the element's own computed style) is a reasonable default for the
|
||||
// common probes
|
||||
const pseudo = Element.PseudoElement.parse(pseudo_element orelse "");
|
||||
const gop = try frame._element_computed_styles.getOrPut(frame.arena, .{ .element = element, .pseudo = pseudo });
|
||||
if (!gop.found_existing) {
|
||||
if (pseudo == .other) {
|
||||
log.warn(.not_implemented, "window.GetComputedStyle", .{ .pseudo_element = pseudo_element.? });
|
||||
}
|
||||
gop.value_ptr.* = try CSSStyleProperties.init(element, true, frame);
|
||||
}
|
||||
return gop.value_ptr.*;
|
||||
|
||||
@@ -170,7 +170,7 @@ fn httpHeaderCallback(transfer: *Transfer) !Transfer.HeaderResult {
|
||||
}
|
||||
|
||||
if (transfer.getContentLength()) |cl| {
|
||||
try self._script_buffer.ensureTotalCapacity(self._script_arena.?.allocator(), cl);
|
||||
try self._script_buffer.ensureTotalCapacityPrecise(self._script_arena.?.allocator(), cl);
|
||||
}
|
||||
|
||||
return .proceed;
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../js/js.zig");
|
||||
|
||||
const Text = @import("Text.zig");
|
||||
@@ -24,7 +26,8 @@ const CDATASection = @This();
|
||||
|
||||
pub const Proto = Text;
|
||||
|
||||
_proto: *Text,
|
||||
_pad: bool = false,
|
||||
_proto_canary: if (lp.IS_DEBUG) *Text else void = undefined,
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(CDATASection);
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../js/js.zig");
|
||||
const Frame = @import("../../Frame.zig");
|
||||
|
||||
@@ -25,7 +27,8 @@ const Comment = @This();
|
||||
|
||||
pub const Proto = CData;
|
||||
|
||||
_proto: *CData,
|
||||
_pad: bool = false,
|
||||
_proto_canary: if (lp.IS_DEBUG) *CData else void = undefined,
|
||||
|
||||
pub fn init(str: ?js.NullableString, frame: *Frame) !*Comment {
|
||||
const node = try Frame.node_factory.createComment(frame, if (str) |s| s.value else "");
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../js/js.zig");
|
||||
|
||||
const CData = @import("../CData.zig");
|
||||
@@ -24,7 +26,7 @@ const ProcessingInstruction = @This();
|
||||
|
||||
pub const Proto = CData;
|
||||
|
||||
_proto: *CData,
|
||||
_proto_canary: if (lp.IS_DEBUG) *CData else void = undefined,
|
||||
_target: []const u8,
|
||||
|
||||
pub fn getTarget(self: *const ProcessingInstruction) []const u8 {
|
||||
|
||||
@@ -17,8 +17,10 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../js/js.zig");
|
||||
const Factory = @import("../../Factory.zig");
|
||||
const Frame = @import("../../Frame.zig");
|
||||
const Node = @import("../Node.zig");
|
||||
const CData = @import("../CData.zig");
|
||||
@@ -29,7 +31,13 @@ const Text = @This();
|
||||
|
||||
pub const Proto = CData;
|
||||
|
||||
_proto: *CData,
|
||||
_pad: bool = false,
|
||||
_proto_canary: if (lp.IS_DEBUG) *CData else void = undefined,
|
||||
|
||||
// Takes a const text but returns a mutable proto; see Node.subtype.
|
||||
pub fn asCData(self: *const Text) *CData {
|
||||
return Factory.protoOf(self);
|
||||
}
|
||||
|
||||
pub fn init(str: ?js.NullableString, frame: *Frame) !*Text {
|
||||
const node = try Frame.node_factory.createTextNode(frame, if (str) |s| s.value else "");
|
||||
@@ -38,13 +46,13 @@ pub fn init(str: ?js.NullableString, frame: *Frame) !*Text {
|
||||
|
||||
// This Text node's own data (getWholeText below spans adjacent Text nodes).
|
||||
pub fn ownData(self: *const Text) []const u8 {
|
||||
return self._proto._data.str();
|
||||
return Factory.protoOf(self)._data.str();
|
||||
}
|
||||
|
||||
// The concatenated data of the contiguous exclusive Text nodes (adjacent
|
||||
// Text siblings on both sides of this one), in tree order.
|
||||
pub fn getWholeText(self: *Text, frame: *Frame) ![]const u8 {
|
||||
const node = self._proto.asNode();
|
||||
const node = Factory.protoOf(self).asNode();
|
||||
|
||||
var first = node;
|
||||
while (first.previousSibling()) |prev| {
|
||||
@@ -57,7 +65,7 @@ pub fn getWholeText(self: *Text, frame: *Frame) ![]const u8 {
|
||||
// Common case: no adjacent text nodes, return our data directly.
|
||||
const has_next_text = if (node.nextSibling()) |next| isExclusiveTextNode(next) else false;
|
||||
if (first == node and !has_next_text) {
|
||||
return self._proto._data.str();
|
||||
return Factory.protoOf(self)._data.str();
|
||||
}
|
||||
|
||||
var buf: std.ArrayList(u8) = .empty;
|
||||
@@ -74,11 +82,11 @@ fn isExclusiveTextNode(node: *Node) bool {
|
||||
}
|
||||
|
||||
pub fn getAssignedSlot(self: *Text, frame: *Frame) ?*Slot {
|
||||
return slotting.findSlot(self._proto.asNode(), true, frame);
|
||||
return slotting.findSlot(Factory.protoOf(self).asNode(), true, frame);
|
||||
}
|
||||
|
||||
pub fn splitText(self: *Text, offset: usize, frame: *Frame) !*Text {
|
||||
const data = self._proto._data.str();
|
||||
const data = Factory.protoOf(self)._data.str();
|
||||
|
||||
const byte_offset = CData.utf16OffsetToUtf8(data, offset) catch return error.IndexSizeError;
|
||||
|
||||
@@ -86,7 +94,7 @@ pub fn splitText(self: *Text, offset: usize, frame: *Frame) !*Text {
|
||||
const new_node = try Frame.node_factory.createTextNode(frame, new_data);
|
||||
const new_text = new_node.as(Text);
|
||||
|
||||
const node = self._proto.asNode();
|
||||
const node = Factory.protoOf(self).asNode();
|
||||
|
||||
// Per DOM spec splitText: insert first (step 7a), then update ranges (7b-7e),
|
||||
// then truncate original node (step 8).
|
||||
@@ -103,8 +111,8 @@ pub fn splitText(self: *Text, offset: usize, frame: *Frame) !*Text {
|
||||
// Step 8: truncate original node via replaceData(offset, count, "").
|
||||
// Use replaceData instead of setData so live range updates fire
|
||||
// (matters for detached text nodes where steps 7b-7e were skipped).
|
||||
const length = self._proto.getLength();
|
||||
try self._proto.replaceData(offset, length - offset, "", frame);
|
||||
const length = Factory.protoOf(self).getLength();
|
||||
try Factory.protoOf(self).replaceData(offset, length - offset, "", frame);
|
||||
|
||||
return new_text;
|
||||
}
|
||||
|
||||
@@ -246,7 +246,7 @@ pub fn forEach(self: *DOMTokenList, cb_: js.Function, js_this_: ?js.Object, fram
|
||||
if (gop.found_existing) {
|
||||
continue;
|
||||
}
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
var caught: js.TryCatch.Caught = .{};
|
||||
cb.tryCall(void, .{ token, i, self }, &caught) catch |err| {
|
||||
frame._page.recordJsError(err);
|
||||
log.debug(.js, "forEach callback", .{ .caught = caught, .source = "DOMTokenList" });
|
||||
|
||||
@@ -97,7 +97,7 @@ pub fn forEach(self: *NodeList, cb: js.Function, frame: *Frame) !void {
|
||||
while (true) : (i += 1) {
|
||||
const node = try self.getAtIndex(@intCast(i), frame) orelse return;
|
||||
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
var caught: js.TryCatch.Caught = .{};
|
||||
cb.tryCall(void, .{ node, i, self }, &caught) catch |err| {
|
||||
frame._page.recordJsError(err);
|
||||
log.debug(.js, "forEach callback", .{ .caught = caught, .source = "nodelist" });
|
||||
|
||||
@@ -407,7 +407,7 @@ pub fn NodeLive(comptime mode: Mode) type {
|
||||
|
||||
fn isFormControl(el: *Element) bool {
|
||||
if (el._type != .html) return false;
|
||||
const html = el._type.html;
|
||||
const html = el.subtype(Element.Html);
|
||||
return switch (html._type) {
|
||||
.input, .button, .select, .textarea => true,
|
||||
else => false,
|
||||
|
||||
@@ -97,9 +97,11 @@ pub fn getPropertyValue(self: *const CSSStyleDeclaration, property_name: []const
|
||||
}
|
||||
|
||||
// Computed width/height must agree with the synthetic layout
|
||||
// metrics (offsetWidth/getBoundingClientRect). Returning ""
|
||||
// makes measurement code see contradictory sizes — jQuery's
|
||||
// "shrink text until it fits" loops then never terminate.
|
||||
// metrics. Returning "" makes measurement code see
|
||||
// contradictory sizes — jQuery's "shrink text until it fits"
|
||||
// loops then never terminate. jQuery's .width() reads this
|
||||
// value, so it must also carry clientWidth's content fallback
|
||||
// or append-until-wide marquee loops never terminate.
|
||||
if (wrapped.eql(comptime .wrap("width"))) {
|
||||
return resolvedDimension(element, .width, frame);
|
||||
}
|
||||
@@ -115,13 +117,13 @@ pub fn getPropertyValue(self: *const CSSStyleDeclaration, property_name: []const
|
||||
}
|
||||
|
||||
fn resolvedDimension(element: *Element, dimension: enum { width, height }, frame: *Frame) []const u8 {
|
||||
if (!element.checkVisibilityCached(null, frame)) {
|
||||
var visibility_cache: Element.VisibilityCache = .{};
|
||||
if (!element.checkVisibilityCached(&visibility_cache, frame)) {
|
||||
return "auto";
|
||||
}
|
||||
const dims = element.getElementDimensions(frame);
|
||||
const value = switch (dimension) {
|
||||
.width => dims.width,
|
||||
.height => dims.height,
|
||||
.width => element.getClientWidthWithCache(frame, &visibility_cache),
|
||||
.height => element.getClientHeightWithCache(frame, &visibility_cache),
|
||||
};
|
||||
return std.fmt.allocPrint(frame.local_arena, "{d}px", .{value}) catch "auto";
|
||||
}
|
||||
@@ -802,8 +804,8 @@ fn getDefaultPropertyValue(self: *const CSSStyleDeclaration, name: String) []con
|
||||
|
||||
fn getDefaultDisplay(element: *const Element) []const u8 {
|
||||
switch (element._type) {
|
||||
.html => |html| {
|
||||
return switch (html._type) {
|
||||
.html => {
|
||||
return switch (element.subtype(Element.Html)._type) {
|
||||
.anchor, .br, .span, .label, .time, .font, .mod, .quote => "inline",
|
||||
.body, .div, .dl, .p, .heading, .form, .button, .canvas, .details, .dialog, .embed, .head, .html, .hr, .iframe, .img, .input, .li, .link, .meta, .ol, .option, .script, .select, .slot, .style, .template, .textarea, .title, .ul, .media, .area, .base, .datalist, .directory, .fieldset, .frameset, .legend, .map, .marquee, .meter, .object, .optgroup, .output, .param, .picture, .pre, .progress, .source, .table, .table_caption, .table_cell, .table_col, .table_row, .table_section, .track => "block",
|
||||
.generic, .custom, .unknown, .data => blk: {
|
||||
@@ -835,8 +837,8 @@ fn isInlineTag(tag_name: []const u8) bool {
|
||||
|
||||
fn getDefaultColor(element: *const Element) []const u8 {
|
||||
switch (element._type) {
|
||||
.html => |html| {
|
||||
return switch (html._type) {
|
||||
.html => {
|
||||
return switch (element.subtype(Element.Html)._type) {
|
||||
.anchor => "rgb(0, 0, 238)", // blue
|
||||
else => "rgb(0, 0, 0)",
|
||||
};
|
||||
|
||||
@@ -353,7 +353,7 @@ pub const List = struct {
|
||||
|
||||
pub fn getNames(self: *const List, allocator: Allocator) ![][]const u8 {
|
||||
var arr: std.ArrayList([]const u8) = .empty;
|
||||
try arr.ensureTotalCapacity(allocator, self._len);
|
||||
try arr.ensureTotalCapacityPrecise(allocator, self._len);
|
||||
for (self.entries()) |*e| {
|
||||
arr.appendAssumeCapacity(e.name());
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ fn camelToKebab(arena: Allocator, camel: String) !String {
|
||||
|
||||
// Fallback: allocate for longer strings
|
||||
var result: std.ArrayList(u8) = .empty;
|
||||
try result.ensureTotalCapacity(arena, output_len);
|
||||
try result.ensureTotalCapacityPrecise(arena, output_len);
|
||||
result.appendSliceAssumeCapacity("data-");
|
||||
|
||||
for (camel_str, 0..) |c, i| {
|
||||
@@ -110,7 +110,7 @@ fn kebabToCamel(arena: Allocator, kebab: []const u8) !?[]const u8 {
|
||||
const data_part = kebab[5..]; // Skip "data-"
|
||||
|
||||
var result: std.ArrayList(u8) = .empty;
|
||||
try result.ensureTotalCapacity(arena, data_part.len);
|
||||
try result.ensureTotalCapacityPrecise(arena, data_part.len);
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < data_part.len) : (i += 1) {
|
||||
|
||||
@@ -116,6 +116,10 @@ _proto_canary: if (lp.IS_DEBUG) *Element else void = undefined,
|
||||
// which custom element class was invoked; look it up in the registry.
|
||||
pub fn construct(new_target: js.Function, frame: *Frame) !*Element {
|
||||
if (frame._upgrading_element) |node| {
|
||||
if (frame._upgrading_consumed) {
|
||||
return error.TypeError;
|
||||
}
|
||||
frame._upgrading_consumed = true;
|
||||
return node.is(Element) orelse return error.IllegalConstructor;
|
||||
}
|
||||
return Frame.node_factory.constructCustomElement(frame, new_target);
|
||||
@@ -127,90 +131,176 @@ pub fn construct(new_target: js.Function, frame: *Frame) !*Element {
|
||||
// constructors routed here.
|
||||
pub fn upgradeConstruct(frame: *Frame) !*Element {
|
||||
const node = frame._upgrading_element orelse return error.TypeError;
|
||||
if (frame._upgrading_consumed) {
|
||||
return error.TypeError;
|
||||
}
|
||||
frame._upgrading_consumed = true;
|
||||
return node.is(Element) orelse return error.TypeError;
|
||||
}
|
||||
|
||||
pub const Type = union(enum) {
|
||||
anchor: *Anchor,
|
||||
area: *Area,
|
||||
base: *Base,
|
||||
body: *Body,
|
||||
br: *BR,
|
||||
button: *Button,
|
||||
canvas: *Canvas,
|
||||
custom: *Custom,
|
||||
data: *Data,
|
||||
datalist: *DataList,
|
||||
details: *Details,
|
||||
dialog: *Dialog,
|
||||
directory: *Directory,
|
||||
div: *Div,
|
||||
dl: *DList,
|
||||
embed: *Embed,
|
||||
fieldset: *FieldSet,
|
||||
font: *Font,
|
||||
form: *Form,
|
||||
frameset: *FrameSet,
|
||||
generic: *Generic,
|
||||
heading: *Heading,
|
||||
head: *Head,
|
||||
html: *Html,
|
||||
hr: *HR,
|
||||
img: *Image,
|
||||
iframe: *IFrame,
|
||||
input: *Input,
|
||||
label: *Label,
|
||||
legend: *Legend,
|
||||
li: *LI,
|
||||
link: *Link,
|
||||
map: *Map,
|
||||
marquee: *Marquee,
|
||||
media: *Media,
|
||||
meta: *Meta,
|
||||
meter: *Meter,
|
||||
mod: *Mod,
|
||||
object: *Object,
|
||||
ol: *OL,
|
||||
optgroup: *OptGroup,
|
||||
option: *Option,
|
||||
output: *Output,
|
||||
p: *Paragraph,
|
||||
picture: *Picture,
|
||||
param: *Param,
|
||||
pre: *Pre,
|
||||
progress: *Progress,
|
||||
quote: *Quote,
|
||||
script: *Script,
|
||||
select: *Select,
|
||||
slot: *Slot,
|
||||
source: *Source,
|
||||
span: *Span,
|
||||
style: *Style,
|
||||
table: *Table,
|
||||
table_caption: *TableCaption,
|
||||
table_cell: *TableCell,
|
||||
table_col: *TableCol,
|
||||
table_row: *TableRow,
|
||||
table_section: *TableSection,
|
||||
template: *Template,
|
||||
textarea: *TextArea,
|
||||
time: *Time,
|
||||
title: *Title,
|
||||
track: *Track,
|
||||
ul: *UL,
|
||||
unknown: *Unknown,
|
||||
pub const Type = enum(u8) {
|
||||
anchor,
|
||||
area,
|
||||
base,
|
||||
body,
|
||||
br,
|
||||
button,
|
||||
canvas,
|
||||
custom,
|
||||
data,
|
||||
datalist,
|
||||
details,
|
||||
dialog,
|
||||
directory,
|
||||
div,
|
||||
dl,
|
||||
embed,
|
||||
fieldset,
|
||||
font,
|
||||
form,
|
||||
frameset,
|
||||
generic,
|
||||
heading,
|
||||
head,
|
||||
html,
|
||||
hr,
|
||||
img,
|
||||
iframe,
|
||||
input,
|
||||
label,
|
||||
legend,
|
||||
li,
|
||||
link,
|
||||
map,
|
||||
marquee,
|
||||
media,
|
||||
meta,
|
||||
meter,
|
||||
mod,
|
||||
object,
|
||||
ol,
|
||||
optgroup,
|
||||
option,
|
||||
output,
|
||||
p,
|
||||
picture,
|
||||
param,
|
||||
pre,
|
||||
progress,
|
||||
quote,
|
||||
script,
|
||||
select,
|
||||
slot,
|
||||
source,
|
||||
span,
|
||||
style,
|
||||
table,
|
||||
table_caption,
|
||||
table_cell,
|
||||
table_col,
|
||||
table_row,
|
||||
table_section,
|
||||
template,
|
||||
textarea,
|
||||
time,
|
||||
title,
|
||||
track,
|
||||
ul,
|
||||
unknown,
|
||||
};
|
||||
|
||||
pub fn Subtype(comptime tag: Type) type {
|
||||
return switch (tag) {
|
||||
.anchor => Anchor,
|
||||
.area => Area,
|
||||
.base => Base,
|
||||
.body => Body,
|
||||
.br => BR,
|
||||
.button => Button,
|
||||
.canvas => Canvas,
|
||||
.custom => Custom,
|
||||
.data => Data,
|
||||
.datalist => DataList,
|
||||
.details => Details,
|
||||
.dialog => Dialog,
|
||||
.directory => Directory,
|
||||
.div => Div,
|
||||
.dl => DList,
|
||||
.embed => Embed,
|
||||
.fieldset => FieldSet,
|
||||
.font => Font,
|
||||
.form => Form,
|
||||
.frameset => FrameSet,
|
||||
.generic => Generic,
|
||||
.heading => Heading,
|
||||
.head => Head,
|
||||
.html => Html,
|
||||
.hr => HR,
|
||||
.img => Image,
|
||||
.iframe => IFrame,
|
||||
.input => Input,
|
||||
.label => Label,
|
||||
.legend => Legend,
|
||||
.li => LI,
|
||||
.link => Link,
|
||||
.map => Map,
|
||||
.marquee => Marquee,
|
||||
.media => Media,
|
||||
.meta => Meta,
|
||||
.meter => Meter,
|
||||
.mod => Mod,
|
||||
.object => Object,
|
||||
.ol => OL,
|
||||
.optgroup => OptGroup,
|
||||
.option => Option,
|
||||
.output => Output,
|
||||
.p => Paragraph,
|
||||
.picture => Picture,
|
||||
.param => Param,
|
||||
.pre => Pre,
|
||||
.progress => Progress,
|
||||
.quote => Quote,
|
||||
.script => Script,
|
||||
.select => Select,
|
||||
.slot => Slot,
|
||||
.source => Source,
|
||||
.span => Span,
|
||||
.style => Style,
|
||||
.table => Table,
|
||||
.table_caption => TableCaption,
|
||||
.table_cell => TableCell,
|
||||
.table_col => TableCol,
|
||||
.table_row => TableRow,
|
||||
.table_section => TableSection,
|
||||
.template => Template,
|
||||
.textarea => TextArea,
|
||||
.time => Time,
|
||||
.title => Title,
|
||||
.track => Track,
|
||||
.ul => UL,
|
||||
.unknown => Unknown,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn subtype(self: *const HtmlElement, comptime T: type) *T {
|
||||
const offset = comptime Factory.chainOffsetOf(T, T) - Factory.chainOffsetOf(T, HtmlElement);
|
||||
const sub: *T = @ptrFromInt(@intFromPtr(self) + offset);
|
||||
if (comptime lp.IS_DEBUG) {
|
||||
// This pointer dance only works because the factory allocates the chain
|
||||
// in a contiguous block of memory. In debug, we assert this holds via
|
||||
// the _proto_canary back pointer.
|
||||
std.debug.assert(Factory.protoOf(sub) == self);
|
||||
}
|
||||
return sub;
|
||||
}
|
||||
|
||||
pub fn is(self: *HtmlElement, comptime T: type) ?*T {
|
||||
inline for (@typeInfo(Type).@"union".fields) |f| {
|
||||
if (@field(Type, f.name) == self._type) {
|
||||
if (f.type == T) {
|
||||
return &@field(self._type, f.name);
|
||||
switch (self._type) {
|
||||
inline else => |tag| {
|
||||
if (Subtype(tag) == T) {
|
||||
return self.subtype(T);
|
||||
}
|
||||
if (f.type == *T) {
|
||||
return @field(self._type, f.name);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -304,8 +394,8 @@ pub fn insertAdjacentHTML(
|
||||
|
||||
pub fn click(self: *HtmlElement, frame: *Frame) !void {
|
||||
switch (self._type) {
|
||||
inline .button, .input, .textarea, .select => |i| {
|
||||
if (i.getDisabled()) {
|
||||
inline .button, .input, .textarea, .select => |tag| {
|
||||
if (self.subtype(Subtype(tag)).getDisabled()) {
|
||||
return;
|
||||
}
|
||||
},
|
||||
@@ -330,8 +420,8 @@ pub fn click(self: *HtmlElement, frame: *Frame) !void {
|
||||
if (event._prevent_default == false) {
|
||||
// toggle the popover_target
|
||||
const explicit: ?*Element = switch (self._type) {
|
||||
.button => |b| b._popover_target,
|
||||
.input => |i| i._popover_target,
|
||||
.button => self.subtype(Button)._popover_target,
|
||||
.input => self.subtype(Input)._popover_target,
|
||||
else => null,
|
||||
};
|
||||
try popover.runInvokerActivation(self, explicit, frame);
|
||||
@@ -543,7 +633,7 @@ fn setAttributeListener(
|
||||
) !void {
|
||||
if (comptime lp.IS_DEBUG) {
|
||||
log.debug(.event, "Html.setAttributeListener", .{
|
||||
.type = std.meta.activeTag(self._type),
|
||||
.type = self._type,
|
||||
.listener_type = listener_type,
|
||||
});
|
||||
}
|
||||
@@ -1492,14 +1582,14 @@ fn collectInnerText(self: *HtmlElement, state: *InnerTextState) std.Io.Writer.Er
|
||||
const e = child.subtype(Node.Element);
|
||||
switch (e._type) {
|
||||
.svg => {},
|
||||
.html => |he| {
|
||||
.html => {
|
||||
const tag = e.getTag();
|
||||
switch (child_filter) {
|
||||
.none => {},
|
||||
.select => if (tag != .option and tag != .optgroup) continue,
|
||||
.optgroup => if (tag != .option) continue,
|
||||
}
|
||||
try handleChildElement(he, tag, state, &saw_cell, &saw_row);
|
||||
try handleChildElement(e.subtype(HtmlElement), tag, state, &saw_cell, &saw_row);
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -1870,17 +1960,17 @@ pub const Build = struct {
|
||||
// Calls `func_name` with `args` on the most specific type where it is
|
||||
// implement. This could be on the HtmlElement itself.
|
||||
pub fn call(self: *const HtmlElement, comptime func_name: []const u8, args: anytype) !bool {
|
||||
inline for (@typeInfo(HtmlElement.Type).@"union".fields) |f| {
|
||||
if (@field(HtmlElement.Type, f.name) == self._type) {
|
||||
switch (self._type) {
|
||||
inline else => |tag| {
|
||||
const S = Subtype(tag);
|
||||
// The inner type implements this function. Call it and we're done.
|
||||
const S = reflect.Struct(f.type);
|
||||
if (@hasDecl(S, "Build")) {
|
||||
if (@hasDecl(S.Build, func_name)) {
|
||||
try @call(.auto, @field(S.Build, func_name), args);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
if (@hasDecl(HtmlElement.Build, func_name)) {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../js/js.zig");
|
||||
@@ -47,64 +48,99 @@ _type: Type,
|
||||
_tag_name: String, // Svg elements are case-preserving
|
||||
_proto_canary: if (lp.IS_DEBUG) *Element else void = undefined,
|
||||
|
||||
pub const Type = union(enum) {
|
||||
graphics: *Graphics,
|
||||
view: *View,
|
||||
title: *Title,
|
||||
desc: *Desc,
|
||||
metadata: *Metadata,
|
||||
gradient: *GradientElement,
|
||||
clip_path: *ClipPath,
|
||||
marker: *Marker,
|
||||
mask: *Mask,
|
||||
pattern: *Pattern,
|
||||
stop: *Stop,
|
||||
generic: *Generic,
|
||||
pub const Type = enum(u8) {
|
||||
graphics,
|
||||
view,
|
||||
title,
|
||||
desc,
|
||||
metadata,
|
||||
gradient,
|
||||
clip_path,
|
||||
marker,
|
||||
mask,
|
||||
pattern,
|
||||
stop,
|
||||
generic,
|
||||
};
|
||||
|
||||
pub fn Subtype(comptime tag: Type) type {
|
||||
return switch (tag) {
|
||||
.graphics => Graphics,
|
||||
.view => View,
|
||||
.title => Title,
|
||||
.desc => Desc,
|
||||
.metadata => Metadata,
|
||||
.gradient => GradientElement,
|
||||
.clip_path => ClipPath,
|
||||
.marker => Marker,
|
||||
.mask => Mask,
|
||||
.pattern => Pattern,
|
||||
.stop => Stop,
|
||||
.generic => Generic,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn subtype(self: *const Svg, comptime T: type) *T {
|
||||
const offset = comptime Factory.chainOffsetOf(T, T) - Factory.chainOffsetOf(T, Svg);
|
||||
const sub: *T = @ptrFromInt(@intFromPtr(self) + offset);
|
||||
if (comptime lp.IS_DEBUG) {
|
||||
// This pointer dance only works because the factory allocates the chain
|
||||
// in a contiguous block of memory. In debug, we assert this holds via
|
||||
// the _proto_canary back pointer.
|
||||
std.debug.assert(Factory.protoOf(sub) == self);
|
||||
}
|
||||
return sub;
|
||||
}
|
||||
|
||||
pub fn is(self: *Svg, comptime T: type) ?*T {
|
||||
inline for (@typeInfo(Type).@"union".fields) |field| {
|
||||
if (@field(Type, field.name) == self._type) {
|
||||
if (field.type == *T) {
|
||||
return @field(self._type, field.name);
|
||||
switch (self._type) {
|
||||
inline else => |tag| {
|
||||
if (Subtype(tag) == T) {
|
||||
return self.subtype(T);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
if (self._type == .graphics) {
|
||||
return self._type.graphics.is(T);
|
||||
return self.subtype(Graphics).is(T);
|
||||
}
|
||||
if (self._type == .gradient) {
|
||||
return self._type.gradient.is(T);
|
||||
return self.subtype(GradientElement).is(T);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getTag(self: *const Svg) Element.Tag {
|
||||
return switch (self._type) {
|
||||
.graphics => |g| switch (g._type) {
|
||||
.svg => .svg,
|
||||
.g => .g,
|
||||
// No dedicated Element.Tag values; tag-name matching falls back
|
||||
// to _tag_name, like it does for generic SVG elements.
|
||||
.a, .use, .image, .defs, .symbol, .switch_element, .foreign_object => .unknown,
|
||||
.text_content => |content| switch (content._type) {
|
||||
.positioning => |positioning| switch (positioning._type) {
|
||||
.text => .text,
|
||||
.tspan => .unknown,
|
||||
.graphics => blk: {
|
||||
const g = self.subtype(Graphics);
|
||||
break :blk switch (g._type) {
|
||||
.svg => .svg,
|
||||
.g => .g,
|
||||
// No dedicated Element.Tag values; tag-name matching falls back
|
||||
// to _tag_name, like it does for generic SVG elements.
|
||||
.a, .use, .image, .defs, .symbol, .switch_element, .foreign_object => .unknown,
|
||||
.text_content => tc: {
|
||||
const content = g.subtype(Graphics.TextContent);
|
||||
break :tc switch (content._type) {
|
||||
.positioning => switch (content.subtype(Graphics.TextContent.TextPositioning)._type) {
|
||||
.text => .text,
|
||||
.tspan => .unknown,
|
||||
},
|
||||
.text_path => .unknown,
|
||||
};
|
||||
},
|
||||
.text_path => .unknown,
|
||||
},
|
||||
.geometry => |geo| switch (geo._type) {
|
||||
.rect => .rect,
|
||||
.circle => .circle,
|
||||
.ellipse => .ellipse,
|
||||
.line => .line,
|
||||
.path => .path,
|
||||
.polygon => .polygon,
|
||||
.polyline => .polyline,
|
||||
},
|
||||
.geometry => switch (g.subtype(Graphics.Geometry)._type) {
|
||||
.rect => .rect,
|
||||
.circle => .circle,
|
||||
.ellipse => .ellipse,
|
||||
.line => .line,
|
||||
.path => .path,
|
||||
.polygon => .polygon,
|
||||
.polyline => .polyline,
|
||||
},
|
||||
};
|
||||
},
|
||||
.generic => |g| g._tag,
|
||||
.generic => self.subtype(Generic)._tag,
|
||||
.title => .title,
|
||||
.view, .desc, .metadata, .gradient, .clip_path, .marker, .mask, .pattern, .stop => .unknown,
|
||||
};
|
||||
|
||||
@@ -16,8 +16,10 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const lp = @import("lightpanda");
|
||||
const std = @import("std");
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
|
||||
const URL = @import("../../../URL.zig");
|
||||
@@ -28,13 +30,14 @@ const HtmlElement = @import("../Html.zig");
|
||||
const Anchor = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
_proto: *HtmlElement,
|
||||
_pad: bool = false,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
|
||||
pub fn asElement(self: *Anchor) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asConstElement(self: *const Anchor) *const Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *Anchor) *Node {
|
||||
return self.asElement().asNode();
|
||||
|
||||
@@ -13,8 +13,10 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const lp = @import("lightpanda");
|
||||
const std = @import("std");
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
const URL = @import("../../../URL.zig");
|
||||
|
||||
@@ -28,13 +30,14 @@ const Area = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
|
||||
_proto: *HtmlElement,
|
||||
_pad: bool = false,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
|
||||
pub fn asElement(self: *Area) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asConstElement(self: *const Area) *const Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *Area) *Node {
|
||||
return self.asElement().asNode();
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
|
||||
const Node = @import("../../Node.zig");
|
||||
@@ -31,7 +32,8 @@ const Audio = @This();
|
||||
|
||||
pub const Proto = Media;
|
||||
|
||||
_proto: *Media,
|
||||
_pad: bool = false,
|
||||
_proto_canary: if (lp.IS_DEBUG) *Media else void = undefined,
|
||||
|
||||
pub fn constructor(maybe_url: ?String, frame: *Frame) !*Media {
|
||||
const node = try Frame.node_factory.createElementNS(frame, .html, "audio", null);
|
||||
@@ -48,11 +50,11 @@ pub fn constructor(maybe_url: ?String, frame: *Frame) !*Media {
|
||||
}
|
||||
|
||||
pub fn asMedia(self: *Audio) *Media {
|
||||
return self._proto;
|
||||
return Factory.protoOf(self);
|
||||
}
|
||||
|
||||
pub fn asElement(self: *Audio) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
|
||||
pub fn asNode(self: *Audio) *Node {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const lp = @import("lightpanda");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
@@ -7,10 +9,11 @@ const BR = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
|
||||
_proto: *HtmlElement,
|
||||
_pad: bool = false,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
|
||||
pub fn asElement(self: *BR) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *BR) *Node {
|
||||
return self.asElement().asNode();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const lp = @import("lightpanda");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const js = @import("../../../js/js.zig");
|
||||
const URL = @import("../../../URL.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
@@ -10,10 +12,11 @@ const Base = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
|
||||
_proto: *HtmlElement,
|
||||
_pad: bool = false,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
|
||||
pub fn asElement(self: *Base) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *Base) *Node {
|
||||
return self.asElement().asNode();
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
|
||||
const Node = @import("../../Node.zig");
|
||||
@@ -31,10 +32,11 @@ const Body = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
|
||||
_proto: *HtmlElement,
|
||||
_pad: bool = false,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
|
||||
pub fn asElement(self: *Body) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *Body) *Node {
|
||||
return self.asElement().asNode();
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const lp = @import("lightpanda");
|
||||
const std = @import("std");
|
||||
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
|
||||
const Node = @import("../../Node.zig");
|
||||
@@ -34,16 +36,16 @@ const Button = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
|
||||
_proto: *HtmlElement,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
_custom_validity: ?[]const u8 = null,
|
||||
_validity: ?*ValidityState = null,
|
||||
_popover_target: ?*Element = null,
|
||||
|
||||
pub fn asElement(self: *Button) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asConstElement(self: *const Button) *const Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *Button) *Node {
|
||||
return self.asElement().asNode();
|
||||
|
||||
@@ -16,8 +16,10 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const lp = @import("lightpanda");
|
||||
const std = @import("std");
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
@@ -32,14 +34,14 @@ const Execution = js.Execution;
|
||||
const Canvas = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
_proto: *HtmlElement,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
_cached: ?DrawingContext = null,
|
||||
|
||||
pub fn asElement(self: *Canvas) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asConstElement(self: *const Canvas) *const Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *Canvas) *Node {
|
||||
return self.asElement().asNode();
|
||||
|
||||
@@ -20,6 +20,7 @@ const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
|
||||
const Node = @import("../../Node.zig");
|
||||
@@ -35,14 +36,15 @@ const String = lp.String;
|
||||
const Custom = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
_proto: *HtmlElement,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
_tag_name: String,
|
||||
_definition: ?*CustomElementDefinition,
|
||||
_connected_callback_invoked: bool = false,
|
||||
_disconnected_callback_invoked: bool = false,
|
||||
_upgrade_failed: bool = false, // a failed upgrade is never retried
|
||||
|
||||
pub fn asElement(self: *Custom) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *Custom) *Node {
|
||||
return self.asElement().asNode();
|
||||
@@ -61,6 +63,9 @@ pub fn enqueueConnectedCallbackOnElement(comptime from_parser: bool, element: *E
|
||||
if (element.is(Custom)) |custom| {
|
||||
// Upgrade if a definition exists but isn't yet attached
|
||||
if (custom._definition == null) {
|
||||
if (custom._upgrade_failed) {
|
||||
return;
|
||||
}
|
||||
const name = custom._tag_name.str();
|
||||
if (frame.window._custom_elements._definitions.get(name)) |definition| {
|
||||
const CustomElementRegistry = @import("../../CustomElementRegistry.zig");
|
||||
@@ -256,29 +261,35 @@ pub fn checkAndAttachBuiltIn(element: *Element, frame: *Frame) !void {
|
||||
|
||||
// Invoke constructor
|
||||
const prev_upgrading = frame._upgrading_element;
|
||||
const prev_consumed = frame._upgrading_consumed;
|
||||
const node = element.asNode();
|
||||
frame._upgrading_element = node;
|
||||
defer frame._upgrading_element = prev_upgrading;
|
||||
frame._upgrading_consumed = false;
|
||||
defer {
|
||||
frame._upgrading_element = prev_upgrading;
|
||||
frame._upgrading_consumed = prev_consumed;
|
||||
}
|
||||
|
||||
// PERFORMANCE OPTIMIZATION: This pattern is discouraged in general code.
|
||||
// Used here because: (1) multiple early returns before needing Local,
|
||||
// (2) called from both V8 callbacks (Local exists) and parser (no Local).
|
||||
// Prefer either: requiring *const js.Local parameter, OR always creating
|
||||
// Local.Scope upfront.
|
||||
var ls: ?js.Local.Scope = null;
|
||||
var local = blk: {
|
||||
var ls: js.Local.Scope = undefined;
|
||||
var ls_open = false;
|
||||
const local = blk: {
|
||||
if (frame.js.local) |l| {
|
||||
break :blk l;
|
||||
}
|
||||
ls = undefined;
|
||||
frame.js.localScope(&ls.?);
|
||||
break :blk &ls.?.local;
|
||||
frame.js.localScope(&ls);
|
||||
ls_open = true;
|
||||
break :blk &ls.local;
|
||||
};
|
||||
defer if (ls) |*_ls| {
|
||||
_ls.deinit();
|
||||
defer if (ls_open) {
|
||||
ls.deinit();
|
||||
};
|
||||
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
var caught: js.TryCatch.Caught = .{};
|
||||
_ = local.toLocal(definition.constructor).newInstance(&caught) catch |err| {
|
||||
log.warn(.js, "custom builtin ctor", .{ .name = is_value, .err = err, .caught = caught });
|
||||
return;
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const lp = @import("lightpanda");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
@@ -24,10 +26,11 @@ const HtmlElement = @import("../Html.zig");
|
||||
const DList = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
_proto: *HtmlElement,
|
||||
_pad: bool = false,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
|
||||
pub fn asElement(self: *DList) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *DList) *Node {
|
||||
return self.asElement().asNode();
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const lp = @import("lightpanda");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
|
||||
@@ -27,10 +29,11 @@ const Data = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
|
||||
_proto: *HtmlElement,
|
||||
_pad: bool = false,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
|
||||
pub fn asElement(self: *Data) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
|
||||
pub fn asNode(self: *Data) *Node {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const lp = @import("lightpanda");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
@@ -7,10 +9,11 @@ const DataList = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
|
||||
_proto: *HtmlElement,
|
||||
_pad: bool = false,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
|
||||
pub fn asElement(self: *DataList) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *DataList) *Node {
|
||||
return self.asElement().asNode();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const lp = @import("lightpanda");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
|
||||
@@ -9,13 +11,14 @@ const Details = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
|
||||
_proto: *HtmlElement,
|
||||
_pad: bool = false,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
|
||||
pub fn asElement(self: *Details) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asConstElement(self: *const Details) *const Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *Details) *Node {
|
||||
return self.asElement().asNode();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const lp = @import("lightpanda");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
|
||||
@@ -10,13 +12,14 @@ const Dialog = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
|
||||
_proto: *HtmlElement,
|
||||
_pad: bool = false,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
|
||||
pub fn asElement(self: *Dialog) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asConstElement(self: *const Dialog) *const Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *Dialog) *Node {
|
||||
return self.asElement().asNode();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const lp = @import("lightpanda");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
@@ -8,10 +10,11 @@ const Directory = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
|
||||
_proto: *HtmlElement,
|
||||
_pad: bool = false,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
|
||||
pub fn asElement(self: *Directory) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *Directory) *Node {
|
||||
return self.asElement().asNode();
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const lp = @import("lightpanda");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
@@ -24,10 +26,11 @@ const HtmlElement = @import("../Html.zig");
|
||||
const Div = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
_proto: *HtmlElement,
|
||||
_pad: bool = false,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
|
||||
pub fn asElement(self: *Div) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *Div) *Node {
|
||||
return self.asElement().asNode();
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const lp = @import("lightpanda");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
@@ -25,13 +27,14 @@ const HtmlElement = @import("../Html.zig");
|
||||
const Embed = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
_proto: *HtmlElement,
|
||||
_pad: bool = false,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
|
||||
pub fn asElement(self: *Embed) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asConstElement(self: *const Embed) *const Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *Embed) *Node {
|
||||
return self.asElement().asNode();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const lp = @import("lightpanda");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
@@ -8,10 +10,11 @@ const FieldSet = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
|
||||
_proto: *HtmlElement,
|
||||
_pad: bool = false,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
|
||||
pub fn asElement(self: *FieldSet) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *FieldSet) *Node {
|
||||
return self.asElement().asNode();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const lp = @import("lightpanda");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
@@ -8,10 +10,11 @@ const Font = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
|
||||
_proto: *HtmlElement,
|
||||
_pad: bool = false,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
|
||||
pub fn asElement(self: *Font) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *Font) *Node {
|
||||
return self.asElement().asNode();
|
||||
|
||||
@@ -16,8 +16,10 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const lp = @import("lightpanda");
|
||||
const std = @import("std");
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
|
||||
const Node = @import("../../Node.zig");
|
||||
@@ -33,7 +35,7 @@ pub const TextArea = @import("TextArea.zig");
|
||||
const Form = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
_proto: *HtmlElement,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
|
||||
// Prevents submission of the form while we're in the process of submitting
|
||||
// the form. You can imagine an onsubmit = () => form.submit() endless loop.
|
||||
@@ -44,13 +46,13 @@ _firing_submission_events: bool = false,
|
||||
_constructing_entry_list: bool = false,
|
||||
|
||||
pub fn asHtmlElement(self: *Form) *HtmlElement {
|
||||
return self._proto;
|
||||
return Factory.protoOf(self);
|
||||
}
|
||||
fn asConstElement(self: *const Form) *const Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asElement(self: *Form) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *Form) *Node {
|
||||
return self.asElement().asNode();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
@@ -13,10 +14,11 @@ const FrameSet = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
|
||||
_proto: *HtmlElement,
|
||||
_pad: bool = false,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
|
||||
pub fn asElement(self: *FrameSet) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *FrameSet) *Node {
|
||||
return self.asElement().asNode();
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
@@ -31,10 +32,10 @@ const Generic = @This();
|
||||
pub const Proto = HtmlElement;
|
||||
_tag_name: String,
|
||||
_tag: Element.Tag,
|
||||
_proto: *HtmlElement,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
|
||||
pub fn asElement(self: *Generic) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *Generic) *Node {
|
||||
return self.asElement().asNode();
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const lp = @import("lightpanda");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
@@ -24,10 +26,11 @@ const HtmlElement = @import("../Html.zig");
|
||||
const HR = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
_proto: *HtmlElement,
|
||||
_pad: bool = false,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
|
||||
pub fn asElement(self: *HR) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *HR) *Node {
|
||||
return self.asElement().asNode();
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const lp = @import("lightpanda");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
@@ -24,10 +26,11 @@ const HtmlElement = @import("../Html.zig");
|
||||
const Head = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
_proto: *HtmlElement,
|
||||
_pad: bool = false,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
|
||||
pub fn asElement(self: *Head) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *Head) *Node {
|
||||
return self.asElement().asNode();
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
@@ -29,12 +30,12 @@ const String = lp.String;
|
||||
const Heading = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
_proto: *HtmlElement,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
_tag_name: String,
|
||||
_tag: Element.Tag,
|
||||
|
||||
pub fn asElement(self: *Heading) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *Heading) *Node {
|
||||
return self.asElement().asNode();
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const lp = @import("lightpanda");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
@@ -24,10 +26,11 @@ const HtmlElement = @import("../Html.zig");
|
||||
const Html = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
_proto: *HtmlElement,
|
||||
_pad: bool = false,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
|
||||
pub fn asElement(self: *Html) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *Html) *Node {
|
||||
return self.asElement().asNode();
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const lp = @import("lightpanda");
|
||||
const std = @import("std");
|
||||
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
|
||||
const Node = @import("../../Node.zig");
|
||||
@@ -29,16 +31,17 @@ const DOMTokenList = @import("../../collections.zig").DOMTokenList;
|
||||
|
||||
const HtmlElement = @import("../Html.zig");
|
||||
|
||||
const String = lp.String;
|
||||
const IFrame = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
_proto: *HtmlElement,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
_src: []const u8 = "",
|
||||
_executed: bool = false,
|
||||
_window: ?*Window = null,
|
||||
|
||||
pub fn asElement(self: *IFrame) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *IFrame) *Node {
|
||||
return self.asElement().asNode();
|
||||
@@ -77,6 +80,19 @@ pub fn setSrc(self: *IFrame, src: []const u8, frame: *Frame) !void {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hasSrcdoc(self: *IFrame) bool {
|
||||
return self.asElement().getAttributeSafe(comptime .wrap("srcdoc")) != null;
|
||||
}
|
||||
|
||||
pub fn getSrcdoc(self: *IFrame) []const u8 {
|
||||
return self.asElement().getAttributeSafe(comptime .wrap("srcdoc")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setSrcdoc(self: *IFrame, value: []const u8, frame: *Frame) !void {
|
||||
// Build.attributeChange triggers the (re)navigation.
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("srcdoc"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub fn getName(self: *IFrame) []const u8 {
|
||||
return self.asElement().getAttributeSafe(comptime .wrap("name")) orelse "";
|
||||
}
|
||||
@@ -103,6 +119,7 @@ pub const JsApi = struct {
|
||||
};
|
||||
|
||||
pub const src = bridge.accessor(IFrame.getSrc, IFrame.setSrc, .{ .ce_reactions = true });
|
||||
pub const srcdoc = bridge.accessor(IFrame.getSrcdoc, IFrame.setSrcdoc, .{ .ce_reactions = true });
|
||||
pub const name = bridge.accessor(IFrame.getName, IFrame.setName, .{ .ce_reactions = true });
|
||||
pub const contentWindow = bridge.accessor(IFrame.getContentWindow, null, .{});
|
||||
pub const contentDocument = bridge.accessor(IFrame.getContentDocument, null, .{});
|
||||
@@ -115,4 +132,28 @@ pub const Build = struct {
|
||||
const element = self.asElement();
|
||||
self._src = element.getAttributeSafe(comptime .wrap("src")) orelse "";
|
||||
}
|
||||
|
||||
pub fn attributeChange(element: *Element, name: String, _: String, frame: *Frame) !void {
|
||||
if (!name.eql(comptime .wrap("srcdoc"))) {
|
||||
return;
|
||||
}
|
||||
if (element.asNode().isConnected()) {
|
||||
// like src, setting srcdoc reloads the frame even if the value didn't change
|
||||
const self = element.as(IFrame);
|
||||
self._executed = false;
|
||||
try frame.iframeAddedCallback(self);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn attributeRemove(element: *Element, name: String, frame: *Frame) !void {
|
||||
if (!name.eql(comptime .wrap("srcdoc"))) {
|
||||
return;
|
||||
}
|
||||
if (element.asNode().isConnected()) {
|
||||
const self = element.as(IFrame);
|
||||
// removing srcdoc falls back to src (or about:blank)
|
||||
self._executed = false;
|
||||
try frame.iframeAddedCallback(self);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,5 +1,7 @@
|
||||
const lp = @import("lightpanda");
|
||||
const std = @import("std");
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
@@ -8,7 +10,8 @@ const HtmlElement = @import("../Html.zig");
|
||||
const Image = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
_proto: *HtmlElement,
|
||||
_pad: bool = false,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
|
||||
pub fn constructor(w_: ?u32, h_: ?u32, frame: *Frame) !*Image {
|
||||
const node = try Frame.node_factory.createElementNS(frame, .html, "img", null);
|
||||
@@ -26,10 +29,10 @@ pub fn constructor(w_: ?u32, h_: ?u32, frame: *Frame) !*Image {
|
||||
}
|
||||
|
||||
pub fn asElement(self: *Image) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asConstElement(self: *const Image) *const Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *Image) *Node {
|
||||
return self.asElement().asNode();
|
||||
@@ -130,7 +133,7 @@ pub fn imageAddedCallback(self: *Image, frame: *Frame) !void {
|
||||
const src = element.getAttributeSafe(comptime .wrap("src")) orelse return;
|
||||
if (src.len == 0) return;
|
||||
|
||||
try frame.queueLoad(self._proto);
|
||||
try frame.queueLoad(Factory.protoOf(self));
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
|
||||
@@ -20,6 +20,7 @@ const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
|
||||
const Node = @import("../../Node.zig");
|
||||
@@ -80,7 +81,7 @@ pub const Type = enum {
|
||||
}
|
||||
};
|
||||
|
||||
_proto: *HtmlElement,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
_default_value: ?[]const u8 = null,
|
||||
_default_checked: bool = false,
|
||||
_value: ?[]const u8 = null,
|
||||
@@ -122,10 +123,10 @@ fn dispatchInputEvent(self: *Input, data: ?[]const u8, input_type: []const u8, f
|
||||
}
|
||||
|
||||
pub fn asElement(self: *Input) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asConstElement(self: *const Input) *const Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *Input) *Node {
|
||||
return self.asElement().asNode();
|
||||
@@ -891,6 +892,20 @@ pub fn getLabels(self: *Input, frame: *Frame) !js.Array {
|
||||
return @import("Label.zig").getControlLabels(self.asElement(), frame);
|
||||
}
|
||||
|
||||
pub fn getList(self: *Input, frame: *Frame) ?*HtmlElement.DataList {
|
||||
switch (self._input_type) {
|
||||
.hidden, .password, .checkbox, .radio, .file, .submit, .image, .reset, .button => return null,
|
||||
else => {},
|
||||
}
|
||||
|
||||
const element = self.asElement();
|
||||
const list_id = element.getAttributeSafe(comptime .wrap("list")) orelse return null;
|
||||
|
||||
// list= resolves in the input's own tree (shadow root or document).
|
||||
const target = frame.getElementByIdFromNode(element.asNode(), list_id) orelse return null;
|
||||
return target.is(HtmlElement.DataList);
|
||||
}
|
||||
|
||||
pub fn getForm(self: *Input, frame: *Frame) ?*Form {
|
||||
const element = self.asElement();
|
||||
|
||||
@@ -1426,6 +1441,7 @@ pub const JsApi = struct {
|
||||
pub const size = bridge.accessor(Input.getSize, Input.setSize, .{ .ce_reactions = true });
|
||||
pub const src = bridge.accessor(Input.getSrc, Input.setSrc, .{ .ce_reactions = true });
|
||||
pub const form = bridge.accessor(Input.getForm, null, .{});
|
||||
pub const list = bridge.accessor(Input.getList, null, .{});
|
||||
pub const formAction = bridge.accessor(Input.getFormAction, Input.setFormAction, .{});
|
||||
pub const formEnctype = bridge.accessor(Input.getFormEnctype, Input.setFormEnctype, .{});
|
||||
pub const formMethod = bridge.accessor(Input.getFormMethod, Input.setFormMethod, .{});
|
||||
|
||||
@@ -16,8 +16,10 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const lp = @import("lightpanda");
|
||||
const std = @import("std");
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Factory = @import("../../../Factory.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
@@ -26,10 +28,11 @@ const HtmlElement = @import("../Html.zig");
|
||||
const LI = @This();
|
||||
|
||||
pub const Proto = HtmlElement;
|
||||
_proto: *HtmlElement,
|
||||
_pad: bool = false,
|
||||
_proto_canary: if (lp.IS_DEBUG) *HtmlElement else void = undefined,
|
||||
|
||||
pub fn asElement(self: *LI) *Element {
|
||||
return self._proto.asElement();
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
pub fn asNode(self: *LI) *Node {
|
||||
return self.asElement().asNode();
|
||||
|
||||
Loaded 100 of 209 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user