mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-10-06 11:21:36 -04:00
Merge branch 'screenshot-kitty' of https://github.com/lightpanda-io/browser into screenshot-kitty
This commit is contained in:
commit
55d092d6b6
36 files changed
+722
-520
No files matched your search
@@ -65,6 +65,7 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
LPD_PATH: ${{ github.workspace }}/bin/lightpanda
|
||||
LPD_ARGS: --load-resources iframe --load-resources worker
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
LP_MODEL: ${{ github.event.inputs.model }}
|
||||
# Optional: news.ycombinator.com often blocks datacenter IPs. Reuse
|
||||
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
- id: args
|
||||
name: build LP args
|
||||
run: |
|
||||
args="--http-cache-dir /tmp/lp-cache"
|
||||
args="--http-cache-dir /tmp/lp-cache --load-resources worker --load-resources iframe"
|
||||
[ "${{ matrix.proxy }}" = "true" ] && args="$args --http-proxy http://127.0.0.1:3000"
|
||||
[ "${{ matrix.robotstxt }}" = "true" ] && args="$args --obey-robots"
|
||||
[ "${{ matrix.wba }}" = "true" ] && args="$args --web-bot-auth-key-file private_key.pem"
|
||||
@@ -162,7 +162,7 @@ jobs:
|
||||
- id: args
|
||||
name: build LP args
|
||||
run: |
|
||||
args="--http-cache-dir /tmp/lp-cache"
|
||||
args="--http-cache-dir /tmp/lp-cache --load-resources worker --load-resources iframe"
|
||||
[ "${{ matrix.robotstxt }}" = "true" ] && args="$args --obey-robots"
|
||||
[ "${{ matrix.wba }}" = "true" ] && args="$args --web-bot-auth-key-file private_key.pem"
|
||||
[ "${{ matrix.wba }}" = "true" ] && args="$args --web-bot-auth-domain ${{ vars.WBA_DOMAIN }}"
|
||||
@@ -429,6 +429,7 @@ jobs:
|
||||
- name: deterministic agent replay
|
||||
env:
|
||||
LPD_PATH: ${{ github.workspace }}/bin/lightpanda
|
||||
LPD_ARGS: --load-resources iframe --load-resources worker
|
||||
run: ./agent/run.sh deterministic
|
||||
|
||||
mcp-smoke:
|
||||
|
||||
+57
-62
@@ -40,9 +40,11 @@ pub const CDP_TCP_USER_TIMEOUT_MS: c_int = 10_000;
|
||||
|
||||
const Config = @This();
|
||||
|
||||
fn logFilterScopesValidator(allocator: Allocator, args: *std.process.Args.Iterator, list: *std.ArrayList(log.FilterRule)) !void {
|
||||
fn logFilterValidator(allocator: Allocator, args: *std.process.Args.Iterator, list: *std.ArrayList(log.FilterRule)) !void {
|
||||
const str = args.next() orelse return error.InvalidOption;
|
||||
|
||||
defer log.opts.scope_enabled = log.resolveFilters(list.items);
|
||||
|
||||
var it = std.mem.splitScalar(u8, str, ',');
|
||||
while (it.next()) |part| {
|
||||
if (part.len == 0) continue;
|
||||
@@ -66,7 +68,7 @@ fn logFilterScopesValidator(allocator: Allocator, args: *std.process.Args.Iterat
|
||||
}
|
||||
|
||||
const v = std.meta.stringToEnum(log.Scope, name) orelse {
|
||||
log.fatal(.app, "invalid option choice", .{ .arg = "--log-filter-scopes", .value = part });
|
||||
log.fatal(.app, "invalid option choice", .{ .arg = "--log-filter", .value = part });
|
||||
return error.InvalidOption;
|
||||
};
|
||||
|
||||
@@ -78,6 +80,7 @@ fn logLevelValidator(_: Allocator, args: *std.process.Args.Iterator, target: *?l
|
||||
const str = args.next() orelse return error.MissingArgument;
|
||||
if (std.mem.eql(u8, str, "error")) {
|
||||
target.* = .err;
|
||||
log.opts.level = .err;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -85,6 +88,24 @@ fn logLevelValidator(_: Allocator, args: *std.process.Args.Iterator, target: *?l
|
||||
log.fatal(.app, "invalid option choice", .{ .arg = "--log-level", .value = str });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
log.opts.level = target.*.?;
|
||||
}
|
||||
|
||||
// The MCP host captures stderr into a log file, where pretty's ANSI
|
||||
// escapes and multi-line entries are noise. Runs before any option is
|
||||
// read so parse-time lines match; --log-format still overrides.
|
||||
fn mcpLogDefaults() void {
|
||||
log.opts.format = .logfmt;
|
||||
}
|
||||
|
||||
fn logFormatValidator(_: Allocator, args: *std.process.Args.Iterator, target: *?log.Format) !void {
|
||||
const str = args.next() orelse return error.MissingArgument;
|
||||
const format = std.meta.stringToEnum(log.Format, str) orelse {
|
||||
log.fatal(.app, "invalid option choice", .{ .arg = "--log-format", .value = str });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
target.* = format;
|
||||
log.opts.format = format;
|
||||
}
|
||||
|
||||
fn httpHeaderValidator(allocator: Allocator, args: *std.process.Args.Iterator, list: *std.ArrayList(HttpHeader)) !void {
|
||||
@@ -211,8 +232,11 @@ fn caPathValidator(
|
||||
}
|
||||
}
|
||||
|
||||
pub const LoadResources = packed struct(u1) {
|
||||
pub const LoadResources = packed struct(u4) {
|
||||
image: bool = false,
|
||||
iframe: bool = false,
|
||||
worker: bool = false,
|
||||
stylesheet: bool = false,
|
||||
};
|
||||
|
||||
/// Common CLI args.
|
||||
@@ -231,8 +255,9 @@ const CommonOptions = .{
|
||||
.{ .name = "ws_max_concurrent", .type = ?u8 },
|
||||
.{ .name = "insecure_disable_tls_host_verification", .type = bool },
|
||||
.{ .name = "log_level", .type = ?log.Level, .validator = logLevelValidator },
|
||||
.{ .name = "log_format", .type = ?log.Format },
|
||||
.{ .name = "log_filter_scopes", .type = log.FilterRule, .multiple = true, .validator = logFilterScopesValidator },
|
||||
.{ .name = "log_format", .type = ?log.Format, .validator = logFormatValidator },
|
||||
.{ .name = "log_filter", .type = log.FilterRule, .multiple = true, .validator = logFilterValidator },
|
||||
.{ .name = "log_filter_scopes", .type = log.FilterRule, .multiple = true, .validator = logFilterValidator, .deprecated = "use --log-filter" },
|
||||
.{ .name = "user_agent_suffix", .type = ?[]const u8 },
|
||||
.{ .name = "http_cache_dir", .type = ?[]const u8 },
|
||||
.{ .name = "http_cache_entry_limit", .type = ?u32, .default = 1000 },
|
||||
@@ -246,9 +271,9 @@ const CommonOptions = .{
|
||||
.{ .name = "adblock_lists", .type = ?[]const u8 },
|
||||
.{ .name = "cookie", .type = ?[]const u8 },
|
||||
.{ .name = "cookie_jar", .type = ?[]const u8 },
|
||||
.{ .name = "disable_subframes", .type = bool },
|
||||
.{ .name = "disable_workers", .type = bool },
|
||||
.{ .name = "enable_external_stylesheets", .type = bool },
|
||||
.{ .name = "disable_subframes", .type = bool, .deprecated = "subframes are now disabled by default, use \"--load-resources iframe\" to enable" },
|
||||
.{ .name = "disable_workers", .type = bool, .deprecated = "workers are now disabled by default, use \"--load-resources worker\" to enable" },
|
||||
.{ .name = "enable_external_stylesheets", .type = bool, .deprecated = "use \"--load-resources stylesheet\" to enable" },
|
||||
.{ .name = "load_resources", .type = LoadResources, .default = LoadResources{} },
|
||||
.{ .name = "v8_flags_unsafe", .type = ?[]const u8 },
|
||||
.{ .name = "v8_max_heap_mb", .type = ?u32 },
|
||||
@@ -355,7 +380,6 @@ const Commands = cli.Builder(.{
|
||||
.{ .name = "host", .type = []const u8, .default = "127.0.0.1" },
|
||||
.{ .name = "port", .type = u16, .default = 9222 },
|
||||
.{ .name = "advertise_host", .type = ?[]const u8 },
|
||||
.{ .name = "timeout", .type = ?u31 },
|
||||
.{ .name = "cdp_max_connections", .type = u16, .default = 16 },
|
||||
.{ .name = "cdp_max_pending_connections", .type = u16, .default = 128 },
|
||||
.{ .name = "cdp_max_message_size", .type = u32, .default = 1024 * 1024 },
|
||||
@@ -403,6 +427,7 @@ const Commands = cli.Builder(.{
|
||||
},
|
||||
.{
|
||||
.name = "mcp",
|
||||
.before_parse = mcpLogDefaults,
|
||||
.options = .{
|
||||
.{ .name = "port", .type = ?u16 },
|
||||
.{ .name = "host", .type = []const u8, .default = "127.0.0.1" },
|
||||
@@ -469,6 +494,18 @@ pub fn init(allocator: Allocator, exec_name: []const u8, mode: Mode) !Config {
|
||||
if (modeNeedsHttp(mode)) {
|
||||
config.http_headers = try HttpHeaders.init(allocator, &config);
|
||||
}
|
||||
|
||||
switch (config.mode) {
|
||||
inline else => |*m| {
|
||||
if (@hasField(@TypeOf(m.*), "enable_external_stylesheets")) {
|
||||
if (m.enable_external_stylesheets) {
|
||||
// map deprecated property onto updated one
|
||||
m.load_resources.stylesheet = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -503,20 +540,6 @@ pub fn obeyRobots(self: *const Config) bool {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn disableSubframes(self: *const Config) bool {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch, .mcp, .agent => |opts| opts.disable_subframes,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn disableWorkers(self: *const Config) bool {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch, .mcp, .agent => |opts| opts.disable_workers,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn watchdogMs(self: *const Config) ?u32 {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch, .mcp, .agent => |opts| {
|
||||
@@ -527,13 +550,6 @@ pub fn watchdogMs(self: *const Config) ?u32 {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn enableExternalStylesheets(self: *const Config) bool {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch, .mcp, .agent => |opts| opts.enable_external_stylesheets,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn loadResources(self: *const Config) LoadResources {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch, .mcp, .agent => |opts| opts.load_resources,
|
||||
@@ -641,18 +657,6 @@ pub fn wsMaxConcurrent(self: *const Config) u8 {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn logLevel(self: *const Config) ?log.Level {
|
||||
return switch (self.mode) {
|
||||
// Agent mode quiets page-driven `console.error` noise unless verbosity=high.
|
||||
.agent => |opts| opts.log_level orelse switch (agentVerbosity(opts)) {
|
||||
.low, .medium => .err,
|
||||
.high => null,
|
||||
},
|
||||
inline .serve, .fetch, .mcp => |opts| opts.log_level,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
/// Resolve --verbosity. Explicit value wins. Else: --task with stderr
|
||||
/// captured (pipe/file) defaults to .high so benchmark harnesses and
|
||||
/// other programmatic consumers get the [tool/result] trace; REPL and
|
||||
@@ -676,23 +680,6 @@ fn stderrIsTty() bool {
|
||||
return stderr_tty_cached;
|
||||
}
|
||||
|
||||
pub fn logFormat(self: *const Config) ?log.Format {
|
||||
return switch (self.mode) {
|
||||
// The MCP host captures stderr into a log file, where pretty's ANSI
|
||||
// escapes and multi-line entries are noise.
|
||||
.mcp => |opts| opts.log_format orelse .logfmt,
|
||||
inline .serve, .fetch, .agent => |opts| opts.log_format,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn logFilterScopes(self: *const Config) std.ArrayList(log.FilterRule) {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch, .mcp, .agent => |opts| opts.log_filter_scopes,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn userAgentSuffix(self: *const Config) ?[]const u8 {
|
||||
return switch (self.mode) {
|
||||
inline .serve, .fetch, .mcp, .agent => |opts| opts.user_agent_suffix,
|
||||
@@ -1075,9 +1062,6 @@ fn printPaged(allocator: Allocator, text: []const u8) void {
|
||||
|
||||
pub fn parseArgs(allocator: Allocator, proc_args: std.process.Args) !Config {
|
||||
const exec_name, var command = try Commands.parse(allocator, proc_args);
|
||||
if (command == .serve and command.serve.timeout != null) {
|
||||
log.warn(.app, "--timeout is deprecated", .{});
|
||||
}
|
||||
const invoked = std.meta.activeTag(command);
|
||||
// Rewrite `run` to `.agent` so nothing downstream needs a `.run` case.
|
||||
if (command == .run) {
|
||||
@@ -1093,6 +1077,17 @@ pub fn parseArgs(allocator: Allocator, proc_args: std.process.Args) !Config {
|
||||
}
|
||||
command = .{ .agent = agent_opts };
|
||||
}
|
||||
|
||||
// Agent mode quiets page-driven `console.error` noise unless
|
||||
// verbosity=high. Depends on --verbosity/--task, so it can only be
|
||||
// resolved after the options are parsed; an explicit --log-level wins.
|
||||
if (command == .agent) {
|
||||
const opts = command.agent;
|
||||
if (opts.log_level == null and agentVerbosity(opts) != .high) {
|
||||
log.opts.level = .err;
|
||||
}
|
||||
}
|
||||
|
||||
var config = try Config.init(allocator, exec_name, command);
|
||||
config.command = invoked;
|
||||
return config;
|
||||
|
||||
@@ -1094,7 +1094,7 @@ pub fn makeRequest(self: *Frame, req: HttpClient.Request) !void {
|
||||
errdefer transfer.deinit();
|
||||
try self.headersForRequest(transfer);
|
||||
}
|
||||
return transfer.submit();
|
||||
transfer.submit() catch {};
|
||||
}
|
||||
|
||||
// Two-phase variant; see HttpClient.newRequest for the ownership contract.
|
||||
@@ -1855,8 +1855,8 @@ pub fn iframeAddedCallback(self: *Frame, iframe: *IFrame) !void {
|
||||
if (iframe._executed) {
|
||||
return;
|
||||
}
|
||||
if (!self._session.subframe_loading_enabled) {
|
||||
// configured not to load frames
|
||||
if (self._session.load_resources.iframe == false) {
|
||||
log.warnDisabledIFrame();
|
||||
iframe._executed = true;
|
||||
return;
|
||||
}
|
||||
@@ -2269,9 +2269,7 @@ pub fn loadExternalStylesheet(self: *Frame, link: *Element.Html.Link, href: []co
|
||||
|
||||
const session = self._session;
|
||||
|
||||
// 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) {
|
||||
if (session.load_resources.stylesheet == false) {
|
||||
return self.queueLoad(Factory.protoOf(link));
|
||||
}
|
||||
|
||||
@@ -3796,9 +3794,9 @@ test "Frame: iframeAddedCallback does not create a frame when termination is pen
|
||||
defer testing.test_session.closeAllPages();
|
||||
|
||||
const session = frame._session;
|
||||
const subframe_loading_enabled = session.subframe_loading_enabled;
|
||||
session.subframe_loading_enabled = true;
|
||||
defer session.subframe_loading_enabled = subframe_loading_enabled;
|
||||
const subframe_loading_enabled = session.load_resources.iframe;
|
||||
session.load_resources.iframe = true;
|
||||
defer session.load_resources.iframe = subframe_loading_enabled;
|
||||
|
||||
const element = try frame.document.createElement("iframe", null, frame);
|
||||
const iframe = element.as(HtmlElement.IFrame);
|
||||
|
||||
@@ -671,3 +671,23 @@ test "ScriptManager: async script whose submit fails synchronously releases its
|
||||
\\document.head.appendChild(s);
|
||||
, null);
|
||||
}
|
||||
|
||||
test "ScriptManager: preload whose submit fails synchronously releases its arena once" {
|
||||
const page = try testing.pageTest("mcp_nav.html", .{});
|
||||
defer page.close();
|
||||
|
||||
const frame = page.frame().?;
|
||||
const sm = &frame._script_manager;
|
||||
const client = sm.base.client;
|
||||
client.test_fail_submit = error.TestSubmitFailure;
|
||||
defer client.test_fail_submit = null;
|
||||
|
||||
// PreloadedScript.errorCallback logs the fetch error.
|
||||
testing.expectLog(&.{.http});
|
||||
|
||||
const url = "http://127.0.0.1:9582/fails-at-submit.js";
|
||||
// A fetch was started (and failed), so the hint's error event fires.
|
||||
try testing.expectEqual(true, try sm.preloadScript(null, url));
|
||||
// errorCallback consumed the entry; nothing dangles in the map.
|
||||
try testing.expectEqual(false, sm.preloaded_scripts.contains(url));
|
||||
}
|
||||
@@ -1120,6 +1120,58 @@ test "ScriptManagerBase: shutdownCallback fails a .loading module" {
|
||||
try testing.expectError(error.Failed, sm.waitForImport(url));
|
||||
}
|
||||
|
||||
test "ScriptManagerBase: import whose submit fails synchronously releases its arena once" {
|
||||
const page = try testing.pageTest("mcp_nav.html", .{});
|
||||
defer page.close();
|
||||
const frame = page.frame().?;
|
||||
|
||||
const sm = &frame._script_manager.base;
|
||||
const client = sm.client;
|
||||
client.test_fail_submit = error.TestSubmitFailure;
|
||||
defer client.test_fail_submit = null;
|
||||
|
||||
// Script.errorCallback logs the fetch error.
|
||||
testing.expectLog(&.{.http});
|
||||
|
||||
const url: [:0]const u8 = "http://127.0.0.1:9582/fails-at-submit.js";
|
||||
try sm.preloadImport(url, frame.url, .{});
|
||||
|
||||
// The failure is delivered through the entry, same as an async one.
|
||||
try testing.expect(sm.async_scripts.first == null);
|
||||
try testing.expect(sm.imported_modules.getPtr(url).?.state == .err);
|
||||
try testing.expectError(error.Failed, sm.waitForImport(url));
|
||||
}
|
||||
|
||||
test "ScriptManagerBase: dynamic import whose submit fails synchronously rejects once" {
|
||||
const page = try testing.pageTest("mcp_nav.html", .{});
|
||||
defer page.close();
|
||||
const frame = page.frame().?;
|
||||
|
||||
const sm = &frame._script_manager.base;
|
||||
const client = sm.client;
|
||||
client.test_fail_submit = error.TestSubmitFailure;
|
||||
defer client.test_fail_submit = null;
|
||||
|
||||
// Script.errorCallback logs the fetch error.
|
||||
testing.expectLog(&.{.http});
|
||||
|
||||
var ls: js.Local.Scope = undefined;
|
||||
frame.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
try ls.local.eval(
|
||||
\\globalThis.__dyn = 'pending';
|
||||
\\import('http://127.0.0.1:9582/fails-at-submit.js').then(
|
||||
\\ () => { globalThis.__dyn = 'resolved'; },
|
||||
\\ (e) => { globalThis.__dyn = String(e); },
|
||||
\\);
|
||||
, frame.url); // the resource name is the import's base url
|
||||
ls.local.runMicrotasks();
|
||||
|
||||
try testing.expect(sm.async_scripts.first == null);
|
||||
try testing.expectEqual(true, (try ls.local.exec("globalThis.__dyn === 'TestSubmitFailure'", null)).toBool());
|
||||
}
|
||||
|
||||
test "ScriptManagerBase: waitForImport stops when teardown is pending" {
|
||||
const page = try testing.pageTest("mcp_nav.html", .{});
|
||||
defer page.close();
|
||||
|
||||
+2
-25
@@ -92,33 +92,14 @@ _tool_frame_override: ?u32 = null,
|
||||
// connection (see `Browser.frame_id_gen` and issue #2472).
|
||||
loader_id_gen: u32 = 0,
|
||||
|
||||
// configuration (or CDP command) to disable iframe loading
|
||||
subframe_loading_enabled: bool = true,
|
||||
|
||||
// configuration (or CDP command) to disable Web Worker loading. When false,
|
||||
// `new Worker(url)` returns a Worker object whose script is never fetched
|
||||
// and never evaluated. Set from the `--disable-workers` CLI flag at
|
||||
// session init; the LP.configureLoading CDP method can flip it per-session.
|
||||
worker_loading_enabled: bool = true,
|
||||
|
||||
// Console.* capture for the `consoleLogs` tool, capped at `max_console_bytes`.
|
||||
// Opt-in via `enableConsoleCapture`: plain CDP `serve` never drains it, so
|
||||
// leaving the listener off keeps the buffer at zero bytes.
|
||||
_console_messages: std.Io.Writer.Allocating,
|
||||
_console_capture: bool = false,
|
||||
|
||||
// Opt-in fetch of external <link rel=stylesheet> resources. Defaults to
|
||||
// false to preserve the current rendering-free fast path: drivers that
|
||||
// don't need accurate visibility checks pay nothing. Set from the
|
||||
// `--enable-external-stylesheets` CLI flag at session init; the
|
||||
// LP.configureLoading CDP method can flip it per-session. When true,
|
||||
// `Link.linkAddedCallback` routes to `Frame.loadExternalStylesheet`
|
||||
// (synchronous fetch + parse + register on `document.styleSheets`).
|
||||
load_external_stylesheets: bool = false,
|
||||
|
||||
// Sub-resources to actually request. Off by default: a driver that only
|
||||
// reads the DOM shouldn't pay for bytes it never looks at.
|
||||
load_resources: Config.LoadResources = .{},
|
||||
// configured external resources (images, stylesheet, worker, iframe) to load
|
||||
load_resources: Config.LoadResources,
|
||||
|
||||
/// Caller-supplied cancellation probe. `Runner._wait` polls it between
|
||||
/// ticks; once `check` returns true the wait returns `error.Cancelled`.
|
||||
@@ -180,11 +161,7 @@ pub fn init(self: *Session, browser: *Browser, notification: *Notification) !voi
|
||||
.browser = browser,
|
||||
.notification = notification,
|
||||
.cookie_jar = storage.Cookie.Jar.init(allocator, notification),
|
||||
// CLI defaults; LP.configureLoading can flip these per-session.
|
||||
.subframe_loading_enabled = !browser.app.config.disableSubframes(),
|
||||
.worker_loading_enabled = !browser.app.config.disableWorkers(),
|
||||
._console_messages = .init(allocator),
|
||||
.load_external_stylesheets = browser.app.config.enableExternalStylesheets(),
|
||||
.load_resources = browser.app.config.loadResources(),
|
||||
};
|
||||
errdefer self._console_messages.deinit();
|
||||
|
||||
@@ -201,7 +201,7 @@ pub const RGBA = packed struct(u32) {
|
||||
|
||||
pub fn init(r: u8, g: u8, b: u8, a: f32) RGBA {
|
||||
const clamped = std.math.clamp(a, 0, 1);
|
||||
return .{ .r = r, .g = g, .b = b, .a = @intFromFloat(clamped * 255) };
|
||||
return .{ .r = r, .g = g, .b = b, .a = @trunc(clamped * 255) };
|
||||
}
|
||||
|
||||
/// Finds a color by its name.
|
||||
|
||||
@@ -612,7 +612,7 @@ fn consumeNumeric(self: *Tokenizer) Token {
|
||||
break :blk std.math.minInt(i32);
|
||||
}
|
||||
|
||||
break :blk @as(i32, @intFromFloat(value));
|
||||
break :blk @as(i32, @trunc(value));
|
||||
} else null;
|
||||
|
||||
if (!self.isEof() and self.nextByteUnchecked() == '%') {
|
||||
|
||||
+31
-8
@@ -89,7 +89,7 @@ fn rootUncapped(doc: *Node.Document, opts: Opts, writer: *std.Io.Writer, frame:
|
||||
if (opts.with_base) {
|
||||
const parent = if (html_doc.getHead()) |head| head.asNode() else doc.asNode();
|
||||
const base = try doc.createElement("base", null, frame);
|
||||
try base.setAttributeSafe(comptime .wrap("base"), .wrap(frame.base()), frame);
|
||||
try base.setAttributeSafe(comptime .wrap("href"), .wrap(frame.base()), frame);
|
||||
_ = try parent.insertBefore(base.asNode(), parent.firstChild(), frame);
|
||||
}
|
||||
}
|
||||
@@ -348,13 +348,17 @@ fn dumpSlotContent(slot: *Slot, opts: Opts, writer: *std.Io.Writer, frame: *Fram
|
||||
}
|
||||
}
|
||||
|
||||
fn isVoidElement(el: *const Node.Element) bool {
|
||||
return switch (el._type) {
|
||||
.html => switch (el.subtype(Node.Element.Html)._type) {
|
||||
.br, .hr, .img, .input, .link, .meta => true,
|
||||
else => false,
|
||||
fn isVoidElement(el: *Node.Element) bool {
|
||||
return switch (el.getTag()) {
|
||||
.area, .base, .br, .col, .embed, .hr, .img, .input, .link, .meta, .param, .source, .track => true,
|
||||
.unknown => {
|
||||
const unknown = el.as(Node.Element.Html.Unknown);
|
||||
if (unknown._tag_name.eql(comptime .wrap("wbr"))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
.svg => false,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -494,10 +498,29 @@ test "dump: default dumps the whole document" {
|
||||
test "dump: with_base injects a <base> element" {
|
||||
try expectDump(.{ .with_base = true },
|
||||
\\<!DOCTYPE html>
|
||||
\\<html><head><base base="http://127.0.0.1:9582/src/browser/tests/dump.html"></base><style>.hidden{display:none}</style><link rel="stylesheet" href="data:text/css,"><script>var a=1;</script></head><body><h1>Title</h1><p class="hidden">secret</p><img><svg></svg><noscript>nojs</noscript><p>visible & well</p></body></html>
|
||||
\\<html><head><base href="http://127.0.0.1:9582/src/browser/tests/dump.html"><style>.hidden{display:none}</style><link rel="stylesheet" href="data:text/css,"><script>var a=1;</script></head><body><h1>Title</h1><p class="hidden">secret</p><img><svg></svg><noscript>nojs</noscript><p>visible & well</p></body></html>
|
||||
);
|
||||
}
|
||||
|
||||
test "dump: void elements have no end tag" {
|
||||
const frame = try testing.createFrame();
|
||||
defer testing.test_session.closeAllPages();
|
||||
|
||||
const doc = frame.window._document;
|
||||
const div = try doc.createElement("div", null, frame);
|
||||
try Frame.parse.htmlAsChildren(frame, div.asNode(),
|
||||
\\<video><source src="a.mp4"><track kind="captions"></video><map><area shape="rect"></map><embed src="e.swf"><p>a<wbr>b</p><table><colgroup><col span="2"></colgroup></table>
|
||||
);
|
||||
|
||||
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||
defer aw.deinit();
|
||||
try deep(div.asNode(), .{}, &aw.writer, frame);
|
||||
|
||||
try testing.expectString(
|
||||
\\<div><video><source src="a.mp4"><track kind="captions"></video><map><area shape="rect"></map><embed src="e.swf"><p>a<wbr>b</p><table><colgroup><col span="2"></colgroup></table></div>
|
||||
, aw.written());
|
||||
}
|
||||
|
||||
test "dump: strip.js removes script and noscript" {
|
||||
try expectDump(.{ .strip = .{ .js = true } },
|
||||
\\<!DOCTYPE html>
|
||||
|
||||
@@ -32,6 +32,8 @@ const Node = @import("../webapi/Node.zig");
|
||||
const Event = @import("../webapi/Event.zig");
|
||||
const Element = @import("../webapi/Element.zig");
|
||||
const TreeWalker = @import("../webapi/TreeWalker.zig");
|
||||
const TextEvent = @import("../webapi/event/TextEvent.zig");
|
||||
const InputEvent = @import("../webapi/event/InputEvent.zig");
|
||||
const MouseEvent = @import("../webapi/event/MouseEvent.zig");
|
||||
const WheelEvent = @import("../webapi/event/WheelEvent.zig");
|
||||
const PointerEvent = @import("../webapi/event/PointerEvent.zig");
|
||||
@@ -312,7 +314,7 @@ pub fn triggerMouseWheel(frame: *Frame, x: f64, y: f64, delta_x: f64, delta_y: f
|
||||
|
||||
fn deltaToScroll(d: f64) i32 {
|
||||
if (std.math.isNan(d)) return 0;
|
||||
return @intFromFloat(std.math.clamp(d, std.math.minInt(i32), std.math.maxInt(i32)));
|
||||
return @trunc(std.math.clamp(d, std.math.minInt(i32), std.math.maxInt(i32)));
|
||||
}
|
||||
|
||||
// callback when the "click" event reaches the frame.
|
||||
@@ -628,22 +630,79 @@ pub fn handleKeydown(frame: *Frame, target: *Node, event: *Event) !void {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle printable characters
|
||||
if (key.isPrintable()) {
|
||||
try input.innerInsert(key.asString(), frame);
|
||||
return editKey(frame, event, input, key);
|
||||
}
|
||||
|
||||
if (target.is(Element.Html.TextArea)) |textarea| {
|
||||
if (key == .Enter) {
|
||||
if (try allowEdit(frame, event, textarea.asElement(), null, "\n", "insertLineBreak")) {
|
||||
try textarea.innerInsert("\n", frame);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
return editKey(frame, event, textarea, key);
|
||||
}
|
||||
}
|
||||
|
||||
// edit keys are handled by Input and TextArea the same
|
||||
fn editKey(frame: *Frame, event: *Event, ctl: anytype, key: KeyboardEvent.Key) !void {
|
||||
if (key == .Backspace or key == .Delete) {
|
||||
const forward = key == .Delete;
|
||||
if (try allowEdit(frame, event, ctl.asElement(), null, null, deleteInputType(forward))) {
|
||||
try ctl.innerDelete(forward, frame);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.is(Element.Html.TextArea)) |textarea| {
|
||||
// zig fmt: off
|
||||
const append =
|
||||
if (key == .Enter) "\n"
|
||||
else if (key.isPrintable()) key.asString()
|
||||
else return
|
||||
;
|
||||
// zig fmt: on
|
||||
return textarea.innerInsert(append, frame);
|
||||
if (key.isPrintable()) {
|
||||
if (try allowEdit(frame, event, ctl.asElement(), key.asString(), key.asString(), "insertText")) {
|
||||
try ctl.innerInsert(key.asString(), frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn deleteInputType(forward: bool) []const u8 {
|
||||
return if (forward) "deleteContentForward" else "deleteContentBackward";
|
||||
}
|
||||
|
||||
// pre-edit events for a key's default action, can cancel the edit (i.e. by
|
||||
// returning false)
|
||||
fn allowEdit(frame: *Frame, keydown: *Event, target: *Element, before_data: ?[]const u8, text_data: ?[]const u8, input_type: []const u8) !bool {
|
||||
if (keydown.getIsTrusted() == false) {
|
||||
// only trusted events fire these events, so for a untrusted event, the
|
||||
// edit isn't cancelled.
|
||||
return true;
|
||||
}
|
||||
|
||||
{
|
||||
const before = (try InputEvent.initTrusted(comptime .wrap("beforeinput"), .{
|
||||
.bubbles = true,
|
||||
.cancelable = true,
|
||||
.composed = true,
|
||||
.data = before_data,
|
||||
.inputType = input_type,
|
||||
}, frame)).asEvent();
|
||||
before.acquireRef(); // need to check its _prevent_default
|
||||
defer _ = before.releaseRef(frame._page);
|
||||
try frame._event_manager.dispatch(target.asEventTarget(), before);
|
||||
if (before._prevent_default) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const data = text_data orelse return true;
|
||||
const text_event = (try TextEvent.initTrusted("textInput", .{
|
||||
.bubbles = true,
|
||||
.cancelable = true,
|
||||
.view = frame.window,
|
||||
.data = data,
|
||||
}, frame)).asEvent();
|
||||
text_event.acquireRef(); // need to check its _prevent_default
|
||||
defer _ = text_event.releaseRef(frame._page);
|
||||
try frame._event_manager.dispatch(target.asEventTarget(), text_event);
|
||||
return text_event._prevent_default == false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
let evt = document.createEvent('TextEvent');
|
||||
evt.initTextEvent('foo');
|
||||
testing.expectEqual('foo', evt.type);
|
||||
testing.expectEqual('', evt.data);
|
||||
// legacy IDL default: optional DOMString dataArg = "undefined"
|
||||
testing.expectEqual('undefined', evt.data);
|
||||
testing.expectEqual(false, evt.bubbles);
|
||||
testing.expectEqual(false, evt.cancelable);
|
||||
}
|
||||
|
||||
@@ -1361,3 +1361,41 @@
|
||||
testing.expectEqual('sc:///', url.href);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=searchParamsQuerySync>
|
||||
{
|
||||
// Accessing searchParams must not rewrite the query...
|
||||
const url = new URL('http://www.example.com/?a=b,c');
|
||||
url.searchParams;
|
||||
testing.expectEqual('http://www.example.com/?a=b,c', url.toString());
|
||||
testing.expectEqual('?a=b,c', url.search);
|
||||
testing.expectEqual('a=b%2Cc', url.searchParams.toString());
|
||||
|
||||
// ...but mutations push the serialized list into it.
|
||||
url.searchParams.append('x', 'y');
|
||||
testing.expectEqual('http://www.example.com/?a=b%2Cc&x=y', url.toString());
|
||||
|
||||
// Deleting the last param drops the '?'.
|
||||
const url2 = new URL('http://example.com/?p1&p2');
|
||||
url2.searchParams.delete('p1');
|
||||
url2.searchParams.delete('p2');
|
||||
testing.expectEqual('http://example.com/', url2.href);
|
||||
testing.expectEqual('', url2.search);
|
||||
|
||||
// A double-'?' query reads back raw via url.search.
|
||||
const url3 = new URL('http://example.org/file??a=b&c=d');
|
||||
testing.expectEqual('??a=b&c=d', url3.search);
|
||||
testing.expectEqual('%3Fa=b&c=d', url3.searchParams.toString());
|
||||
|
||||
// set() keeps the first entry's position when pushing back.
|
||||
const url4 = new URL('http://example.org/file?e=f&g=h');
|
||||
const sp = url4.searchParams;
|
||||
sp.append('i', ' j ');
|
||||
sp.set('e', 'updated');
|
||||
testing.expectEqual('?e=updated&g=h&i=+j+', url4.search);
|
||||
|
||||
// search setter still propagates into an existing searchParams.
|
||||
url4.search = 'n=1';
|
||||
testing.expectEqual('n=1', sp.toString());
|
||||
}
|
||||
</script>
|
||||
@@ -405,7 +405,7 @@ fn clampLongLong(value: f64) i64 {
|
||||
if (@abs(value - @trunc(value)) == 0.5 and @mod(rounded, 2) != 0) {
|
||||
rounded -= std.math.sign(value);
|
||||
}
|
||||
return @intFromFloat(rounded);
|
||||
return @trunc(rounded);
|
||||
}
|
||||
|
||||
/// Returns the size of the Blob in bytes.
|
||||
|
||||
@@ -1839,7 +1839,7 @@ pub fn scrollIntoView(self: *Element, opts: ?ScrollIntoViewOpts, frame: *Frame)
|
||||
// Positions come from the faux-layout document position (top = preorder
|
||||
// depth-scaled y), the same source getBoundingClientRect uses.
|
||||
const y = calculateDocumentPosition(self.asNode());
|
||||
frame.window.scrollTo(.{ .x = 0 }, @intFromFloat(@max(0, y)), frame) catch {};
|
||||
frame.window.scrollTo(.{ .x = 0 }, @trunc(@max(0, y)), frame) catch {};
|
||||
}
|
||||
|
||||
const ScrollToOpts = union(enum) {
|
||||
|
||||
@@ -92,8 +92,8 @@ pub fn init(frame: *Frame, url: [:0]const u8, name: []const u8, worker_type: Wor
|
||||
const proto = self._proto;
|
||||
errdefer proto.deinit();
|
||||
|
||||
if (!session.worker_loading_enabled) {
|
||||
log.debug(.browser, "shared worker disabled", .{ .url = owned_url });
|
||||
if (session.load_resources.worker == false) {
|
||||
log.warnDisabledWorker();
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
+23
-28
@@ -60,6 +60,9 @@ pub fn parse(url: []const u8, maybe_base: ?[]const u8, exec: *const Execution) ?
|
||||
|
||||
pub fn deinit(self: *URL, page: *Page) void {
|
||||
if (self._search_params) |search_params| {
|
||||
// The params can outlive the URL. Unlink it.
|
||||
search_params._url = null;
|
||||
// And, remove the RC that we (URL) were holding on it.
|
||||
search_params.releaseRef(page);
|
||||
}
|
||||
// Not tracked by arena.
|
||||
@@ -171,18 +174,8 @@ pub fn setPort(self: *URL, maybe_value: ?[]const u8) void {
|
||||
_ = U.url_set_port(self._url, port);
|
||||
}
|
||||
|
||||
pub fn getSearch(self: *const URL, exec: *const Execution) ![]const u8 {
|
||||
if (self._search_params) |search_params| {
|
||||
if (search_params.getSize() == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
var buf = std.Io.Writer.Allocating.init(exec.local_arena);
|
||||
try buf.writer.writeByte('?');
|
||||
try search_params.toString(&buf.writer);
|
||||
return buf.written();
|
||||
}
|
||||
|
||||
// searchParam pushes its mutations to URL, so self._url is always in sync
|
||||
pub fn getSearch(self: *const URL, _: *const Execution) ![]const u8 {
|
||||
var out: [*]const u8 = undefined;
|
||||
var len: usize = 0;
|
||||
const res = U.url_get_query(self._url, &out, &len);
|
||||
@@ -257,12 +250,28 @@ pub fn getSearchParams(self: *URL, exec: *const Execution) !*URLSearchParams {
|
||||
const search_value = if (U.url_get_query(self._url, &out, &len) == 0) (out - 1)[0 .. len + 1] else "";
|
||||
|
||||
const params = try URLSearchParams.init(.{ .query_string = search_value }, exec);
|
||||
// Released in deinit; the cached params must outlive their JS wrapper.
|
||||
params.acquireRef();
|
||||
params._url = self;
|
||||
self._search_params = params;
|
||||
return params;
|
||||
}
|
||||
|
||||
// Every update to the url's _search_params needs to keep the url in sync
|
||||
pub fn syncQueryFromParams(self: *URL, exec: *const Execution) !void {
|
||||
const params = self._search_params orelse return;
|
||||
if (params.getSize() == 0) {
|
||||
U.url_set_query_to_null(self._url);
|
||||
return;
|
||||
}
|
||||
|
||||
var buf = std.Io.Writer.Allocating.init(exec.local_arena);
|
||||
try params.toString(&buf.writer);
|
||||
const query = buf.written();
|
||||
if (U.url_set_query(self._url, query.ptr, query.len) != 0) {
|
||||
return error.TypeError;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getOrigin(self: *const URL, exec: *const Execution) ![]const u8 {
|
||||
const origin = U.url_get_origin(self._url);
|
||||
defer origin.deinit();
|
||||
@@ -288,21 +297,7 @@ pub fn setHref(self: *URL, value: []const u8, exec: *const Execution) !void {
|
||||
try search_params.updateFromString(search_value, exec);
|
||||
}
|
||||
|
||||
pub fn toString(self: *const URL, exec: *const Execution) ![]const u8 {
|
||||
if (self._search_params) |search_params| {
|
||||
if (search_params.getSize() == 0) {
|
||||
U.url_set_query_to_null(self._url);
|
||||
} else {
|
||||
var buf = std.Io.Writer.Allocating.init(exec.local_arena);
|
||||
defer buf.deinit();
|
||||
try search_params.toString(&buf.writer);
|
||||
const query = buf.written();
|
||||
if (U.url_set_query(self._url, query.ptr, query.len) != 0) {
|
||||
return error.ToString;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toString(self: *const URL, _: *const Execution) ![]const u8 {
|
||||
var out: [*]const u8 = undefined;
|
||||
var len: usize = 0;
|
||||
U.url_to_string(self._url, &out, &len);
|
||||
|
||||
@@ -91,14 +91,8 @@ pub fn init(url: []const u8, options: ?WorkerOptions, frame: *Frame) !*Worker {
|
||||
self._worker_scope = dedicated_worker;
|
||||
try frame.trackWorker(self);
|
||||
|
||||
// `--disable-workers` (or `LP.configureLoading { worker: false }`):
|
||||
// skip the script fetch and eval. The Worker object is still
|
||||
// constructed so JS `new Worker(url)` does not throw, but the
|
||||
// worker's eval never runs (postMessage from the page is queued
|
||||
// indefinitely with no handler to drain it). Mirrors the
|
||||
// `subframe_loading_enabled` pattern for iframes.
|
||||
if (!session.worker_loading_enabled) {
|
||||
log.debug(.browser, "worker disabled", .{ .url = resolved_url });
|
||||
if (session.load_resources.worker == false) {
|
||||
log.warnDisabledWorker();
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
@@ -270,7 +270,7 @@ pub fn makeRequest(self: *WorkerGlobalScope, req: HttpClient.Request) !void {
|
||||
errdefer transfer.deinit();
|
||||
try self.headersForRequest(transfer);
|
||||
}
|
||||
return transfer.submit();
|
||||
transfer.submit() catch {};
|
||||
}
|
||||
|
||||
// Two-phase variant; see HttpClient.newRequest for the ownership contract.
|
||||
|
||||
@@ -29,12 +29,12 @@ const HtmlElement = @import("../Html.zig");
|
||||
const Form = @import("Form.zig");
|
||||
const Selection = @import("../../Selection.zig");
|
||||
const Event = @import("../../Event.zig");
|
||||
const InputEvent = @import("../../event/InputEvent.zig");
|
||||
const ValidityState = @import("ValidityState.zig");
|
||||
const popover = @import("../popover.zig");
|
||||
const File = @import("../../File.zig");
|
||||
const FileList = @import("../../FileList.zig");
|
||||
const reflection = @import("../reflection.zig");
|
||||
const text_entry = @import("../text_entry.zig");
|
||||
|
||||
const String = lp.String;
|
||||
|
||||
@@ -113,16 +113,6 @@ pub fn setOnSelectionChange(self: *Input, listener: ?js.Function) !void {
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatchSelectionChangeEvent(self: *Input, frame: *Frame) !void {
|
||||
const event = try Event.init("selectionchange", .{ .bubbles = true }, frame._page);
|
||||
try frame._event_manager.dispatch(self.asElement().asEventTarget(), event);
|
||||
}
|
||||
|
||||
fn dispatchInputEvent(self: *Input, data: ?[]const u8, input_type: []const u8, frame: *Frame) !void {
|
||||
const event = try InputEvent.initTrusted(comptime .wrap("input"), .{ .data = data, .inputType = input_type }, frame);
|
||||
try frame._event_manager.dispatch(self.asElement().asEventTarget(), event.asEvent());
|
||||
}
|
||||
|
||||
pub fn asElement(self: *Input) *Element {
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
@@ -623,137 +613,34 @@ pub fn setSrc(self: *Input, src: []const u8, frame: *Frame) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("src"), .wrap(trimmed), frame);
|
||||
}
|
||||
|
||||
pub fn select(self: *Input, frame: *Frame) !void {
|
||||
const len = if (self._value) |v| @as(u32, @intCast(v.len)) else 0;
|
||||
try self.setSelectionRange(0, len, null, frame);
|
||||
const event = try Event.init("select", .{ .bubbles = true }, frame._page);
|
||||
try frame._event_manager.dispatch(self.asElement().asEventTarget(), event);
|
||||
}
|
||||
const entry = text_entry.TextEntry(Input);
|
||||
|
||||
fn selectionAvailable(self: *const Input) bool {
|
||||
pub const select = entry.select;
|
||||
pub const innerInsert = entry.innerInsert;
|
||||
pub const innerDelete = entry.innerDelete;
|
||||
pub const getSelectionDirection = entry.getSelectionDirection;
|
||||
pub const setSelectionStart = entry.setSelectionStart;
|
||||
pub const setSelectionEnd = entry.setSelectionEnd;
|
||||
pub const setSelectionRange = entry.setSelectionRange;
|
||||
|
||||
pub fn selectionAvailable(self: *const Input) bool {
|
||||
switch (self._input_type) {
|
||||
.text, .search, .url, .tel, .password => return true,
|
||||
else => return false,
|
||||
}
|
||||
}
|
||||
|
||||
const HowSelected = union(enum) { partial: struct { u32, u32 }, full, none };
|
||||
|
||||
fn howSelected(self: *const Input) HowSelected {
|
||||
if (!self.selectionAvailable()) return .none;
|
||||
const value = self._value orelse return .none;
|
||||
|
||||
if (self._selection_start == self._selection_end) return .none;
|
||||
if (self._selection_start == 0 and self._selection_end == value.len) return .full;
|
||||
return .{ .partial = .{ self._selection_start, self._selection_end } };
|
||||
}
|
||||
|
||||
pub fn innerInsert(self: *Input, str: []const u8, frame: *Frame) !void {
|
||||
const arena = frame.arena;
|
||||
|
||||
switch (self.howSelected()) {
|
||||
.full => {
|
||||
// if the input is fully selected, replace the content.
|
||||
const new_value = try arena.dupe(u8, str);
|
||||
try self.setValue(new_value, frame);
|
||||
self._selection_start = @intCast(new_value.len);
|
||||
self._selection_end = @intCast(new_value.len);
|
||||
self._selection_direction = .none;
|
||||
try self.dispatchSelectionChangeEvent(frame);
|
||||
},
|
||||
.partial => |range| {
|
||||
// if the input is partially selected, replace the selected content.
|
||||
const current_value = self.getValue();
|
||||
const before = current_value[0..range[0]];
|
||||
const remaining = current_value[range[1]..];
|
||||
|
||||
const new_value = try std.mem.concat(
|
||||
arena,
|
||||
u8,
|
||||
&.{ before, str, remaining },
|
||||
);
|
||||
try self.setValue(new_value, frame);
|
||||
|
||||
const new_pos = range[0] + str.len;
|
||||
self._selection_start = @intCast(new_pos);
|
||||
self._selection_end = @intCast(new_pos);
|
||||
self._selection_direction = .none;
|
||||
try self.dispatchSelectionChangeEvent(frame);
|
||||
},
|
||||
.none => {
|
||||
// if the input is not selected, just insert at cursor.
|
||||
const current_value = self.getValue();
|
||||
const new_value = try std.mem.concat(arena, u8, &.{ current_value, str });
|
||||
try self.setValue(new_value, frame);
|
||||
},
|
||||
}
|
||||
try self.dispatchInputEvent(str, "insertText", frame);
|
||||
}
|
||||
|
||||
pub fn getSelectionDirection(self: *const Input) []const u8 {
|
||||
return @tagName(self._selection_direction);
|
||||
}
|
||||
|
||||
// Nullable here, unlike <textarea>'s, which is why these two aren't shared.
|
||||
pub fn getSelectionStart(self: *const Input) !?u32 {
|
||||
if (!self.selectionAvailable()) return null;
|
||||
return self._selection_start;
|
||||
}
|
||||
|
||||
pub fn setSelectionStart(self: *Input, value: u32, frame: *Frame) !void {
|
||||
if (!self.selectionAvailable()) return error.InvalidStateError;
|
||||
self._selection_start = value;
|
||||
try self.dispatchSelectionChangeEvent(frame);
|
||||
}
|
||||
|
||||
pub fn getSelectionEnd(self: *const Input) !?u32 {
|
||||
if (!self.selectionAvailable()) return null;
|
||||
return self._selection_end;
|
||||
}
|
||||
|
||||
pub fn setSelectionEnd(self: *Input, value: u32, frame: *Frame) !void {
|
||||
if (!self.selectionAvailable()) return error.InvalidStateError;
|
||||
self._selection_end = value;
|
||||
try self.dispatchSelectionChangeEvent(frame);
|
||||
}
|
||||
|
||||
pub fn setSelectionRange(
|
||||
self: *Input,
|
||||
selection_start: u32,
|
||||
selection_end: u32,
|
||||
selection_dir: ?[]const u8,
|
||||
frame: *Frame,
|
||||
) !void {
|
||||
if (!self.selectionAvailable()) return error.InvalidStateError;
|
||||
|
||||
const direction = blk: {
|
||||
if (selection_dir) |sd| {
|
||||
break :blk std.meta.stringToEnum(Selection.SelectionDirection, sd) orelse .none;
|
||||
} else break :blk .none;
|
||||
};
|
||||
|
||||
const value = self._value orelse {
|
||||
self._selection_start = 0;
|
||||
self._selection_end = 0;
|
||||
self._selection_direction = .none;
|
||||
return;
|
||||
};
|
||||
|
||||
const len_u32: u32 = @intCast(value.len);
|
||||
var start: u32 = if (selection_start > len_u32) len_u32 else selection_start;
|
||||
const end: u32 = if (selection_end > len_u32) len_u32 else selection_end;
|
||||
|
||||
// If end is less than start, both are equal to end.
|
||||
if (end < start) {
|
||||
start = end;
|
||||
}
|
||||
|
||||
self._selection_direction = direction;
|
||||
self._selection_start = start;
|
||||
self._selection_end = end;
|
||||
|
||||
try self.dispatchSelectionChangeEvent(frame);
|
||||
}
|
||||
|
||||
pub fn getLabels(self: *Input, frame: *Frame) !js.Array {
|
||||
if (self._input_type == .hidden) {
|
||||
return frame.js.local.?.newArray(0);
|
||||
|
||||
@@ -193,7 +193,7 @@ test "WebApi: HTML.Link" {
|
||||
|
||||
test "WebApi: HTML.Link external stylesheet" {
|
||||
testing.silenceLog(&.{.http});
|
||||
try testing.htmlRunner("css/external_stylesheet.html", .{ .load_external_stylesheets = true });
|
||||
try testing.htmlRunner("css/external_stylesheet.html", .{ .load_resources = .{ .stylesheet = true } });
|
||||
}
|
||||
|
||||
// Regression: a synchronous external-stylesheet fetch must not strand the
|
||||
@@ -202,5 +202,5 @@ test "WebApi: HTML.Link external stylesheet" {
|
||||
// never drains and the document is stuck at readyState "loading".
|
||||
test "WebApi: HTML.Link deferred script then external stylesheet" {
|
||||
testing.silenceLog(&.{.http});
|
||||
try testing.htmlRunner("css/deferred_script_then_stylesheet.html", .{ .load_external_stylesheets = true });
|
||||
try testing.htmlRunner("css/deferred_script_then_stylesheet.html", .{ .load_resources = .{ .stylesheet = true } });
|
||||
}
|
||||
@@ -28,9 +28,9 @@ const HtmlElement = @import("../Html.zig");
|
||||
const Form = @import("Form.zig");
|
||||
const Selection = @import("../../Selection.zig");
|
||||
const Event = @import("../../Event.zig");
|
||||
const InputEvent = @import("../../event/InputEvent.zig");
|
||||
const ValidityState = @import("ValidityState.zig");
|
||||
const reflection = @import("../reflection.zig");
|
||||
const text_entry = @import("../text_entry.zig");
|
||||
|
||||
const TextArea = @This();
|
||||
|
||||
@@ -59,16 +59,6 @@ pub fn setOnSelectionChange(self: *TextArea, listener: ?js.Function) !void {
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatchSelectionChangeEvent(self: *TextArea, frame: *Frame) !void {
|
||||
const event = try Event.init("selectionchange", .{ .bubbles = true }, frame._page);
|
||||
try frame._event_manager.dispatch(self.asElement().asEventTarget(), event);
|
||||
}
|
||||
|
||||
fn dispatchInputEvent(self: *TextArea, data: ?[]const u8, input_type: []const u8, frame: *Frame) !void {
|
||||
const event = try InputEvent.initTrusted(comptime .wrap("input"), .{ .data = data, .inputType = input_type }, frame);
|
||||
try frame._event_manager.dispatch(self.asElement().asEventTarget(), event.asEvent());
|
||||
}
|
||||
|
||||
pub fn asElement(self: *TextArea) *Element {
|
||||
return Factory.protoOf(self).asElement();
|
||||
}
|
||||
@@ -123,123 +113,30 @@ pub fn getMinLength(self: *const TextArea) i32 {
|
||||
return reflection.getLimitedLong(self.asConstElement(), comptime .wrap("minlength"));
|
||||
}
|
||||
|
||||
pub fn select(self: *TextArea, frame: *Frame) !void {
|
||||
const len = if (self._value) |v| @as(u32, @intCast(v.len)) else 0;
|
||||
try self.setSelectionRange(0, len, null, frame);
|
||||
const event = try Event.init("select", .{ .bubbles = true }, frame._page);
|
||||
try frame._event_manager.dispatch(self.asElement().asEventTarget(), event);
|
||||
}
|
||||
|
||||
const HowSelected = union(enum) { partial: struct { u32, u32 }, full, none };
|
||||
|
||||
fn howSelected(self: *const TextArea) HowSelected {
|
||||
const value = self._value orelse return .none;
|
||||
|
||||
if (self._selection_start == self._selection_end) return .none;
|
||||
if (self._selection_start == 0 and self._selection_end == value.len) return .full;
|
||||
return .{ .partial = .{ self._selection_start, self._selection_end } };
|
||||
}
|
||||
|
||||
pub fn innerInsert(self: *TextArea, str: []const u8, frame: *Frame) !void {
|
||||
const arena = frame.arena;
|
||||
|
||||
switch (self.howSelected()) {
|
||||
.full => {
|
||||
// if the text area is fully selected, replace the content.
|
||||
const new_value = try arena.dupe(u8, str);
|
||||
try self.setValue(new_value, frame);
|
||||
self._selection_start = @intCast(new_value.len);
|
||||
self._selection_end = @intCast(new_value.len);
|
||||
self._selection_direction = .none;
|
||||
try self.dispatchSelectionChangeEvent(frame);
|
||||
},
|
||||
.partial => |range| {
|
||||
// if the text area is partially selected, replace the selected content.
|
||||
const current_value = self.getValue();
|
||||
const before = current_value[0..range[0]];
|
||||
const remaining = current_value[range[1]..];
|
||||
|
||||
const new_value = try std.mem.concat(
|
||||
arena,
|
||||
u8,
|
||||
&.{ before, str, remaining },
|
||||
);
|
||||
try self.setValue(new_value, frame);
|
||||
|
||||
const new_pos = range[0] + str.len;
|
||||
self._selection_start = @intCast(new_pos);
|
||||
self._selection_end = @intCast(new_pos);
|
||||
self._selection_direction = .none;
|
||||
try self.dispatchSelectionChangeEvent(frame);
|
||||
},
|
||||
.none => {
|
||||
// if the text area is not selected, just insert at cursor.
|
||||
const current_value = self.getValue();
|
||||
const new_value = try std.mem.concat(arena, u8, &.{ current_value, str });
|
||||
try self.setValue(new_value, frame);
|
||||
},
|
||||
}
|
||||
try self.dispatchInputEvent(str, "insertText", frame);
|
||||
}
|
||||
|
||||
pub fn getSelectionDirection(self: *const TextArea) []const u8 {
|
||||
return @tagName(self._selection_direction);
|
||||
const entry = text_entry.TextEntry(TextArea);
|
||||
|
||||
pub const select = entry.select;
|
||||
pub const innerInsert = entry.innerInsert;
|
||||
pub const innerDelete = entry.innerDelete;
|
||||
pub const getSelectionDirection = entry.getSelectionDirection;
|
||||
pub const setSelectionStart = entry.setSelectionStart;
|
||||
pub const setSelectionEnd = entry.setSelectionEnd;
|
||||
pub const setSelectionRange = entry.setSelectionRange;
|
||||
|
||||
// <textarea> always supports selection; <input> only does for some types.
|
||||
pub fn selectionAvailable(_: *const TextArea) bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Non-null unlike input
|
||||
pub fn getSelectionStart(self: *const TextArea) u32 {
|
||||
return self._selection_start;
|
||||
}
|
||||
|
||||
pub fn setSelectionStart(self: *TextArea, value: u32, frame: *Frame) !void {
|
||||
self._selection_start = value;
|
||||
try self.dispatchSelectionChangeEvent(frame);
|
||||
}
|
||||
|
||||
pub fn getSelectionEnd(self: *const TextArea) u32 {
|
||||
return self._selection_end;
|
||||
}
|
||||
|
||||
pub fn setSelectionEnd(self: *TextArea, value: u32, frame: *Frame) !void {
|
||||
self._selection_end = value;
|
||||
try self.dispatchSelectionChangeEvent(frame);
|
||||
}
|
||||
|
||||
pub fn setSelectionRange(
|
||||
self: *TextArea,
|
||||
selection_start: u32,
|
||||
selection_end: u32,
|
||||
selection_dir: ?[]const u8,
|
||||
frame: *Frame,
|
||||
) !void {
|
||||
const direction = blk: {
|
||||
if (selection_dir) |sd| {
|
||||
break :blk std.meta.stringToEnum(Selection.SelectionDirection, sd) orelse .none;
|
||||
} else break :blk .none;
|
||||
};
|
||||
|
||||
const value = self._value orelse {
|
||||
self._selection_start = 0;
|
||||
self._selection_end = 0;
|
||||
self._selection_direction = .none;
|
||||
return;
|
||||
};
|
||||
|
||||
const len_u32: u32 = @intCast(value.len);
|
||||
var start: u32 = if (selection_start > len_u32) len_u32 else selection_start;
|
||||
const end: u32 = if (selection_end > len_u32) len_u32 else selection_end;
|
||||
|
||||
// If end is less than start, both are equal to end.
|
||||
if (end < start) {
|
||||
start = end;
|
||||
}
|
||||
|
||||
self._selection_direction = direction;
|
||||
self._selection_start = start;
|
||||
self._selection_end = end;
|
||||
|
||||
try self.dispatchSelectionChangeEvent(frame);
|
||||
}
|
||||
|
||||
pub fn getForm(self: *TextArea, frame: *Frame) ?*Form {
|
||||
const element = self.asElement();
|
||||
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
// 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/>.
|
||||
|
||||
// Input and TextArea share much in common. All shared logic between Input and
|
||||
// TextArea sits here.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const Frame = @import("../../Frame.zig");
|
||||
|
||||
const Event = @import("../Event.zig");
|
||||
const Selection = @import("../Selection.zig");
|
||||
const InputEvent = @import("../event/InputEvent.zig");
|
||||
|
||||
pub fn TextEntry(comptime T: type) type {
|
||||
return struct {
|
||||
pub fn select(self: *T, frame: *Frame) !void {
|
||||
const len = if (self._value) |v| @as(u32, @intCast(v.len)) else 0;
|
||||
try setSelectionRange(self, 0, len, null, frame);
|
||||
const event = try Event.init("select", .{ .bubbles = true }, frame._page);
|
||||
try frame._event_manager.dispatch(self.asElement().asEventTarget(), event);
|
||||
}
|
||||
|
||||
pub fn innerInsert(self: *T, str: []const u8, frame: *Frame) !void {
|
||||
const arena = frame.arena;
|
||||
|
||||
switch (howSelected(self)) {
|
||||
.full => {
|
||||
// fully selected, replace the content.
|
||||
const new_value = try arena.dupe(u8, str);
|
||||
try self.setValue(new_value, frame);
|
||||
self._selection_start = @intCast(new_value.len);
|
||||
self._selection_end = @intCast(new_value.len);
|
||||
self._selection_direction = .none;
|
||||
try dispatchSelectionChangeEvent(self, frame);
|
||||
},
|
||||
.partial => |range| {
|
||||
// partially selected, replace the selected content.
|
||||
const current_value = self.getValue();
|
||||
const before = current_value[0..range[0]];
|
||||
const remaining = current_value[range[1]..];
|
||||
|
||||
const new_value = try std.mem.concat(
|
||||
arena,
|
||||
u8,
|
||||
&.{ before, str, remaining },
|
||||
);
|
||||
try self.setValue(new_value, frame);
|
||||
|
||||
const new_pos = range[0] + str.len;
|
||||
self._selection_start = @intCast(new_pos);
|
||||
self._selection_end = @intCast(new_pos);
|
||||
self._selection_direction = .none;
|
||||
try dispatchSelectionChangeEvent(self, frame);
|
||||
},
|
||||
.none => {
|
||||
// nothing selected, just insert at cursor.
|
||||
const current_value = self.getValue();
|
||||
const new_value = try std.mem.concat(arena, u8, &.{ current_value, str });
|
||||
try self.setValue(new_value, frame);
|
||||
},
|
||||
}
|
||||
try dispatchInputEvent(self, str, "insertText", frame);
|
||||
}
|
||||
|
||||
// forward == delete
|
||||
// !forward == backspace
|
||||
pub fn innerDelete(self: *T, forward: bool, frame: *Frame) !void {
|
||||
const current_value = self.getValue();
|
||||
const value_len: u32 = @intCast(current_value.len);
|
||||
|
||||
var start: u32 = undefined;
|
||||
var end: u32 = undefined;
|
||||
switch (howSelected(self)) {
|
||||
.full => {
|
||||
start = 0;
|
||||
end = value_len;
|
||||
},
|
||||
.partial => |range| {
|
||||
start, end = range;
|
||||
},
|
||||
.none => {
|
||||
// Controls without selection support keep no caret; edit at the end.
|
||||
const caret = if (self.selectionAvailable()) @min(self._selection_start, value_len) else value_len;
|
||||
if (forward) {
|
||||
if (caret >= value_len) {
|
||||
return;
|
||||
}
|
||||
start = caret;
|
||||
end = caret + (std.unicode.utf8ByteSequenceLength(current_value[caret]) catch 1);
|
||||
} else {
|
||||
if (caret == 0) {
|
||||
return;
|
||||
}
|
||||
end = caret;
|
||||
start = caret - 1;
|
||||
while (start > 0 and (current_value[start] & 0xC0) == 0x80) {
|
||||
start -= 1;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const new_value = try std.mem.concat(frame.arena, u8, &.{
|
||||
current_value[0..start],
|
||||
current_value[@min(end, value_len)..],
|
||||
});
|
||||
try self.setValue(new_value, frame);
|
||||
self._selection_start = start;
|
||||
self._selection_end = start;
|
||||
self._selection_direction = .none;
|
||||
try dispatchSelectionChangeEvent(self, frame);
|
||||
try dispatchInputEvent(self, null, if (forward) "deleteContentForward" else "deleteContentBackward", frame);
|
||||
}
|
||||
|
||||
pub fn getSelectionDirection(self: *const T) []const u8 {
|
||||
return @tagName(self._selection_direction);
|
||||
}
|
||||
|
||||
pub fn setSelectionStart(self: *T, value: u32, frame: *Frame) !void {
|
||||
if (self.selectionAvailable() == false) {
|
||||
return error.InvalidStateError;
|
||||
}
|
||||
self._selection_start = value;
|
||||
try dispatchSelectionChangeEvent(self, frame);
|
||||
}
|
||||
|
||||
pub fn setSelectionEnd(self: *T, value: u32, frame: *Frame) !void {
|
||||
if (self.selectionAvailable() == false) {
|
||||
return error.InvalidStateError;
|
||||
}
|
||||
self._selection_end = value;
|
||||
try dispatchSelectionChangeEvent(self, frame);
|
||||
}
|
||||
|
||||
pub fn setSelectionRange(
|
||||
self: *T,
|
||||
selection_start: u32,
|
||||
selection_end: u32,
|
||||
selection_dir: ?[]const u8,
|
||||
frame: *Frame,
|
||||
) !void {
|
||||
if (self.selectionAvailable() == false) {
|
||||
return error.InvalidStateError;
|
||||
}
|
||||
|
||||
const direction = blk: {
|
||||
if (selection_dir) |sd| {
|
||||
break :blk std.meta.stringToEnum(Selection.SelectionDirection, sd) orelse .none;
|
||||
} else break :blk .none;
|
||||
};
|
||||
|
||||
const value = self._value orelse {
|
||||
self._selection_start = 0;
|
||||
self._selection_end = 0;
|
||||
self._selection_direction = .none;
|
||||
return;
|
||||
};
|
||||
|
||||
const len_u32: u32 = @intCast(value.len);
|
||||
var start: u32 = if (selection_start > len_u32) len_u32 else selection_start;
|
||||
const end: u32 = if (selection_end > len_u32) len_u32 else selection_end;
|
||||
|
||||
// If end is less than start, both are equal to end.
|
||||
if (end < start) {
|
||||
start = end;
|
||||
}
|
||||
|
||||
self._selection_direction = direction;
|
||||
self._selection_start = start;
|
||||
self._selection_end = end;
|
||||
|
||||
try dispatchSelectionChangeEvent(self, frame);
|
||||
}
|
||||
|
||||
const HowSelected = union(enum) { partial: struct { u32, u32 }, full, none };
|
||||
|
||||
fn howSelected(self: *const T) HowSelected {
|
||||
if (self.selectionAvailable() == false) {
|
||||
return .none;
|
||||
}
|
||||
const value = self._value orelse return .none;
|
||||
|
||||
if (self._selection_start == self._selection_end) {
|
||||
return .none;
|
||||
}
|
||||
if (self._selection_start == 0 and self._selection_end == value.len) {
|
||||
return .full;
|
||||
}
|
||||
return .{ .partial = .{ self._selection_start, self._selection_end } };
|
||||
}
|
||||
|
||||
fn dispatchSelectionChangeEvent(self: *T, frame: *Frame) !void {
|
||||
const event = try Event.init("selectionchange", .{ .bubbles = true }, frame._page);
|
||||
try frame._event_manager.dispatch(self.asElement().asEventTarget(), event);
|
||||
}
|
||||
|
||||
fn dispatchInputEvent(self: *T, data: ?[]const u8, input_type: []const u8, frame: *Frame) !void {
|
||||
const event = try InputEvent.initTrusted(comptime .wrap("input"), .{ .data = data, .inputType = input_type }, frame);
|
||||
try frame._event_manager.dispatch(self.asElement().asEventTarget(), event.asEvent());
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -43,6 +43,14 @@ pub const Options = Event.inheritOptions(
|
||||
);
|
||||
|
||||
pub fn init(typ: []const u8, _opts: ?Options, frame: *Frame) !*TextEvent {
|
||||
return initWithTrusted(typ, _opts, false, frame);
|
||||
}
|
||||
|
||||
pub fn initTrusted(typ: []const u8, _opts: ?Options, frame: *Frame) !*TextEvent {
|
||||
return initWithTrusted(typ, _opts, true, frame);
|
||||
}
|
||||
|
||||
fn initWithTrusted(typ: []const u8, _opts: ?Options, trusted: bool, frame: *Frame) !*TextEvent {
|
||||
const arena = try frame.getArena(.tiny, "TextEvent");
|
||||
errdefer arena.release();
|
||||
const type_string = try String.init(arena.allocator(), typ, .{});
|
||||
@@ -58,7 +66,7 @@ pub fn init(typ: []const u8, _opts: ?Options, frame: *Frame) !*TextEvent {
|
||||
},
|
||||
);
|
||||
|
||||
Event.populatePrototypes(event, opts, false);
|
||||
Event.populatePrototypes(event, opts, trusted);
|
||||
return event;
|
||||
}
|
||||
|
||||
@@ -90,7 +98,8 @@ pub fn initTextEvent(
|
||||
event._bubbles = bubbles orelse false;
|
||||
event._cancelable = cancelable orelse false;
|
||||
ui._view = view;
|
||||
self._data = if (data) |d| try arena.dupe(u8, d) else "";
|
||||
// yup, the literal string "undefined" (not undefined) is the default
|
||||
self._data = if (data) |d| try arena.dupe(u8, d) else "undefined";
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
|
||||
@@ -22,6 +22,7 @@ const lp = @import("lightpanda");
|
||||
const js = @import("../../js/js.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
|
||||
const URL = @import("../URL.zig");
|
||||
const FormData = @import("FormData.zig");
|
||||
const KeyValueList = @import("../KeyValueList.zig");
|
||||
|
||||
@@ -44,6 +45,7 @@ const URLSearchParams = @This();
|
||||
_rc: lp.RC = .{},
|
||||
_arena: *lp.Arena,
|
||||
_params: KeyValueList,
|
||||
_url: ?*URL = null, // Set when created via the url.searchParams getter
|
||||
|
||||
const InitOpts = union(enum) {
|
||||
form_data: *FormData,
|
||||
@@ -62,9 +64,12 @@ pub fn init(opts_: ?InitOpts, exec: *const Execution) !*URLSearchParams {
|
||||
.form_data => |fd| break :blk try fd.toKeyValueList(arena.allocator()),
|
||||
.value => |js_val| {
|
||||
if (js_val.isObject()) {
|
||||
// Per Web IDL, an object with @@iterator converts as a
|
||||
// sequence of [name, value] pairs. This covers arrays,
|
||||
// Maps, generators and other URLSearchParams - including
|
||||
// ones with a patched @@iterator, so no instanceof
|
||||
// fast-path here.
|
||||
if (try js_val.iterator()) |it| {
|
||||
// value satisfies the @@iterator protocol, so we're
|
||||
// expecting [name, value] tuples
|
||||
break :blk try paramsFromIterator(arena.allocator(), it);
|
||||
}
|
||||
// normalizer is null, so frame won't be used
|
||||
@@ -118,16 +123,19 @@ pub fn has(self: *const URLSearchParams, name: []const u8, value: ?[]const u8) b
|
||||
return self._params.has(name, value);
|
||||
}
|
||||
|
||||
pub fn set(self: *URLSearchParams, name: []const u8, value: []const u8) !void {
|
||||
return self._params.set(self._arena.allocator(), name, value);
|
||||
pub fn set(self: *URLSearchParams, name: []const u8, value: []const u8, exec: *const Execution) !void {
|
||||
try self._params.set(self._arena.allocator(), name, value);
|
||||
try self.postUpdate(exec);
|
||||
}
|
||||
|
||||
pub fn append(self: *URLSearchParams, name: []const u8, value: []const u8) !void {
|
||||
return self._params.append(self._arena.allocator(), name, value);
|
||||
pub fn append(self: *URLSearchParams, name: []const u8, value: []const u8, exec: *const Execution) !void {
|
||||
try self._params.append(self._arena.allocator(), name, value);
|
||||
try self.postUpdate(exec);
|
||||
}
|
||||
|
||||
pub fn delete(self: *URLSearchParams, name: []const u8, value: ?[]const u8) void {
|
||||
pub fn delete(self: *URLSearchParams, name: []const u8, value: ?[]const u8, exec: *const Execution) !void {
|
||||
self._params.delete(name, value);
|
||||
try self.postUpdate(exec);
|
||||
}
|
||||
|
||||
pub fn keys(self: *URLSearchParams, exec: *const Execution) !*KeyIterator {
|
||||
@@ -154,7 +162,10 @@ pub fn format(self: *const URLSearchParams, writer: *std.Io.Writer) !void {
|
||||
pub fn forEach(self: *URLSearchParams, cb_: js.Function, js_this_: ?js.Object) !void {
|
||||
const cb = if (js_this_) |js_this| try cb_.withThis(js_this) else cb_;
|
||||
|
||||
// the callback can mutate the list
|
||||
// Index-based on purpose: the callback can mutate the list (delete,
|
||||
// append, ...), which both changes its length and can reallocate the
|
||||
// backing slice, so neither a captured slice nor a pointer into it
|
||||
// survives the call.
|
||||
var i: usize = 0;
|
||||
while (i < self._params._entries.items.len) : (i += 1) {
|
||||
const entry = self._params._entries.items[i];
|
||||
@@ -165,15 +176,27 @@ pub fn forEach(self: *URLSearchParams, cb_: js.Function, js_this_: ?js.Object) !
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sort(self: *URLSearchParams) void {
|
||||
// std.mem.sort is stable (as required by the spec)
|
||||
pub fn sort(self: *URLSearchParams, exec: *const Execution) !void {
|
||||
// std.mem.sort (block sort) is stable, which the spec requires: entries
|
||||
// with equal names keep their relative order.
|
||||
std.mem.sort(KeyValueList.Entry, self._params._entries.items, {}, struct {
|
||||
fn cmp(_: void, a: KeyValueList.Entry, b: KeyValueList.Entry) bool {
|
||||
return utf16Order(a.name.str(), b.name.str()) == .lt;
|
||||
}
|
||||
}.cmp);
|
||||
try self.postUpdate(exec);
|
||||
}
|
||||
|
||||
// we need to keep the linked url in sync
|
||||
fn postUpdate(self: *URLSearchParams, exec: *const Execution) !void {
|
||||
const url = self._url orelse return;
|
||||
try url.syncQueryFromParams(exec);
|
||||
}
|
||||
|
||||
// The URL spec sorts by UTF-16 code units, not bytes or code points. The
|
||||
// difference is observable for supplementary-plane characters: 🌈 (U+1F308)
|
||||
// is a higher code point than ffi (U+FB03), but its UTF-16 lead surrogate
|
||||
// (0xD83C) is a lower code unit.
|
||||
fn utf16Order(a: []const u8, b: []const u8) std.math.Order {
|
||||
var ia: usize = 0;
|
||||
var ib: usize = 0;
|
||||
@@ -231,21 +254,17 @@ fn paramsFromIterator(allocator: Allocator, it: js.Value.Iterator) !KeyValueList
|
||||
|
||||
if (item.isArray()) {
|
||||
const as_array = item.toArray();
|
||||
if (as_array.len() != 2) {
|
||||
// we should be getting [name, value]
|
||||
return error.TypeError;
|
||||
}
|
||||
// Each pair must have exactly 2 items.
|
||||
if (as_array.len() != 2) return error.TypeError;
|
||||
name_val = try as_array.get(0);
|
||||
value_val = try as_array.get(1);
|
||||
} else {
|
||||
// Not an array, but it could itself be an iterator
|
||||
// A non-array pair (e.g. what a Map entries iterator or a custom
|
||||
// generator yields) converts through its own @@iterator.
|
||||
var pair_it = (try item.iterator()) orelse return error.TypeError;
|
||||
name_val = (try pair_it.next()) orelse return error.TypeError;
|
||||
value_val = (try pair_it.next()) orelse return error.TypeError;
|
||||
if (try pair_it.next() != null) {
|
||||
// should only have a name and a value, anything else is wrong
|
||||
return error.TypeError;
|
||||
}
|
||||
if (try pair_it.next() != null) return error.TypeError;
|
||||
}
|
||||
|
||||
try params._entries.append(allocator, .{
|
||||
@@ -301,6 +320,13 @@ fn paramsFromString(allocator: Allocator, input_: []const u8, buf: []u8) !KeyVal
|
||||
return params;
|
||||
}
|
||||
|
||||
// True when value[i] starts a valid %XX escape. A '%' not followed by two
|
||||
// hex digits is not an error: percent-decode passes it through literally
|
||||
// ("b=%2sf" parses to "%2sf").
|
||||
fn isEscapeTriplet(value: []const u8, i: usize) bool {
|
||||
return i + 2 < value.len and std.ascii.isHex(value[i + 1]) and std.ascii.isHex(value[i + 2]);
|
||||
}
|
||||
|
||||
fn unescape(arena: Allocator, value: []const u8, buf: []u8) !String {
|
||||
if (value.len == 0) {
|
||||
return comptime .wrap("");
|
||||
@@ -390,11 +416,6 @@ pub const Iterator = struct {
|
||||
}
|
||||
};
|
||||
|
||||
// True when value[i] starts a valid %XX escape
|
||||
fn isEscapeTriplet(value: []const u8, i: usize) bool {
|
||||
return i + 2 < value.len and std.ascii.isHex(value[i + 1]) and std.ascii.isHex(value[i + 2]);
|
||||
}
|
||||
|
||||
const GenericIterator = @import("../collections/iterator.zig").Entry;
|
||||
pub const KeyIterator = GenericIterator(Iterator, "0");
|
||||
pub const ValueIterator = GenericIterator(Iterator, "1");
|
||||
|
||||
@@ -154,8 +154,8 @@ test "BodyInit: URLSearchParams emit urlencoded body + content-type" {
|
||||
|
||||
const usp = try arena.create(URLSearchParams);
|
||||
usp.* = .{ ._arena = arena, ._params = .empty };
|
||||
try usp.append("a", "1");
|
||||
try usp.append("b", "2");
|
||||
try usp._params.append(arena.allocator(), "a", "1");
|
||||
try usp._params.append(arena.allocator(), "b", "2");
|
||||
|
||||
const r = try (BodyInit{ .url_search_params = usp }).extract(arena.allocator());
|
||||
try testing.expectString("a=1&b=2", r.bytes);
|
||||
|
||||
@@ -261,7 +261,7 @@ pub fn maybeBumpGenerator(self: *Engine, store_id: i64, key: f64) !void {
|
||||
|
||||
// Cap at 2^53, the largest integer the generator tracks per spec.
|
||||
const capped = @min(@floor(key), 9007199254740992);
|
||||
const want: i64 = @intFromFloat(capped + 1);
|
||||
const want: i64 = @trunc(capped + 1);
|
||||
try self.conn.exec(
|
||||
"update idb_object_stores set key_generator = ?2 where id = ?1 and key_generator < ?2",
|
||||
.{ store_id, want },
|
||||
|
||||
@@ -231,19 +231,19 @@ fn substringFn(arena: Allocator, args: []const result.Result) Error![]const u8 {
|
||||
if (std.math.isNan(len_raw)) return "";
|
||||
const len = roundHalfToPosInf(len_raw);
|
||||
const sum = start - 1 + len;
|
||||
// -inf + inf is NaN; @intFromFloat(NaN) is illegal behavior.
|
||||
// -inf + inf is NaN, and NaN to integer is illegal behavior.
|
||||
if (std.math.isNan(sum)) return "";
|
||||
const si_f = @max(start - 1, 0);
|
||||
const ei_f = @min(sum, s_len);
|
||||
if (si_f >= ei_f) return "";
|
||||
const si: usize = @intFromFloat(si_f);
|
||||
const ei: usize = @intFromFloat(ei_f);
|
||||
const si: usize = @trunc(si_f);
|
||||
const ei: usize = @trunc(ei_f);
|
||||
return s[si..ei];
|
||||
}
|
||||
|
||||
const si_f = @max(start - 1, 0);
|
||||
if (si_f >= s_len) return "";
|
||||
const si: usize = @intFromFloat(si_f);
|
||||
const si: usize = @trunc(si_f);
|
||||
return s[si..];
|
||||
}
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ pub fn numberToString(arena: Allocator, n: f64) error{OutOfMemory}![]const u8 {
|
||||
if (std.math.isNegativeInf(n)) return "-Infinity";
|
||||
if (n == 0) return "0"; // covers +0 and -0
|
||||
if (@trunc(n) == n and n >= -9.007199254740992e15 and n <= 9.007199254740992e15) {
|
||||
return std.fmt.allocPrint(arena, "{d}", .{@as(i64, @intFromFloat(n))});
|
||||
return std.fmt.allocPrint(arena, "{d}", .{@as(i64, @trunc(n))});
|
||||
}
|
||||
return std.fmt.allocPrint(arena, "{d}", .{n});
|
||||
}
|
||||
|
||||
@@ -537,8 +537,8 @@ fn getBoxModel(cmd: *CDP.Command) !void {
|
||||
.padding = zero,
|
||||
.border = zero,
|
||||
.margin = zero,
|
||||
.width = @intFromFloat(rect.width),
|
||||
.height = @intFromFloat(rect.height),
|
||||
.width = @trunc(rect.width),
|
||||
.height = @trunc(rect.height),
|
||||
} }, .{});
|
||||
}
|
||||
|
||||
|
||||
+18
-18
@@ -102,10 +102,10 @@ fn configureLoading(cmd: *CDP.Command) !void {
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.NoBrowserContext;
|
||||
if (params.subFrame) |v| bc.session.subframe_loading_enabled = v;
|
||||
if (params.worker) |v| bc.session.worker_loading_enabled = v;
|
||||
if (params.externalStylesheets) |v| bc.session.load_external_stylesheets = v;
|
||||
if (params.images) |v| bc.session.load_resources.image = v;
|
||||
if (params.worker) |v| bc.session.load_resources.worker = v;
|
||||
if (params.subFrame) |v| bc.session.load_resources.iframe = v;
|
||||
if (params.externalStylesheets) |v| bc.session.load_resources.stylesheet = v;
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
@@ -771,8 +771,8 @@ test "cdp.lp: configureLoading toggles subFrame and worker independently" {
|
||||
_ = try bc.session.createPage();
|
||||
|
||||
// Defaults: both loading types enabled.
|
||||
try testing.expectEqual(true, bc.session.subframe_loading_enabled);
|
||||
try testing.expectEqual(true, bc.session.worker_loading_enabled);
|
||||
try testing.expectEqual(true, bc.session.load_resources.iframe);
|
||||
try testing.expectEqual(true, bc.session.load_resources.worker);
|
||||
|
||||
// subFrame-only: leaves worker untouched.
|
||||
try ctx.processMessage(.{
|
||||
@@ -781,8 +781,8 @@ test "cdp.lp: configureLoading toggles subFrame and worker independently" {
|
||||
.params = .{ .subFrame = false },
|
||||
});
|
||||
try ctx.expectSentResult(null, .{ .id = 1 });
|
||||
try testing.expectEqual(false, bc.session.subframe_loading_enabled);
|
||||
try testing.expectEqual(true, bc.session.worker_loading_enabled);
|
||||
try testing.expectEqual(false, bc.session.load_resources.iframe);
|
||||
try testing.expectEqual(true, bc.session.load_resources.worker);
|
||||
|
||||
// worker-only: leaves subFrame untouched.
|
||||
try ctx.processMessage(.{
|
||||
@@ -791,8 +791,8 @@ test "cdp.lp: configureLoading toggles subFrame and worker independently" {
|
||||
.params = .{ .worker = false },
|
||||
});
|
||||
try ctx.expectSentResult(null, .{ .id = 2 });
|
||||
try testing.expectEqual(false, bc.session.subframe_loading_enabled);
|
||||
try testing.expectEqual(false, bc.session.worker_loading_enabled);
|
||||
try testing.expectEqual(false, bc.session.load_resources.iframe);
|
||||
try testing.expectEqual(false, bc.session.load_resources.worker);
|
||||
|
||||
// Both at once.
|
||||
try ctx.processMessage(.{
|
||||
@@ -801,8 +801,8 @@ test "cdp.lp: configureLoading toggles subFrame and worker independently" {
|
||||
.params = .{ .subFrame = true, .worker = true },
|
||||
});
|
||||
try ctx.expectSentResult(null, .{ .id = 3 });
|
||||
try testing.expectEqual(true, bc.session.subframe_loading_enabled);
|
||||
try testing.expectEqual(true, bc.session.worker_loading_enabled);
|
||||
try testing.expectEqual(true, bc.session.load_resources.iframe);
|
||||
try testing.expectEqual(true, bc.session.load_resources.worker);
|
||||
}
|
||||
|
||||
test "cdp.lp: configureLoading toggles externalStylesheets independently" {
|
||||
@@ -813,7 +813,7 @@ test "cdp.lp: configureLoading toggles externalStylesheets independently" {
|
||||
_ = try bc.session.createPage();
|
||||
|
||||
// Default is opt-in: off unless the CLI flag or CDP toggle enables it.
|
||||
try testing.expectEqual(false, bc.session.load_external_stylesheets);
|
||||
try testing.expectEqual(false, bc.session.load_resources.stylesheet);
|
||||
|
||||
// Enable via CDP; the other two loading toggles stay at their defaults.
|
||||
try ctx.processMessage(.{
|
||||
@@ -822,9 +822,9 @@ test "cdp.lp: configureLoading toggles externalStylesheets independently" {
|
||||
.params = .{ .externalStylesheets = true },
|
||||
});
|
||||
try ctx.expectSentResult(null, .{ .id = 1 });
|
||||
try testing.expectEqual(true, bc.session.load_external_stylesheets);
|
||||
try testing.expectEqual(true, bc.session.subframe_loading_enabled);
|
||||
try testing.expectEqual(true, bc.session.worker_loading_enabled);
|
||||
try testing.expectEqual(true, bc.session.load_resources.stylesheet);
|
||||
try testing.expectEqual(true, bc.session.load_resources.iframe);
|
||||
try testing.expectEqual(true, bc.session.load_resources.worker);
|
||||
|
||||
// Flip back off; partial params must not reset the other fields.
|
||||
try ctx.processMessage(.{
|
||||
@@ -833,7 +833,7 @@ test "cdp.lp: configureLoading toggles externalStylesheets independently" {
|
||||
.params = .{ .externalStylesheets = false },
|
||||
});
|
||||
try ctx.expectSentResult(null, .{ .id = 2 });
|
||||
try testing.expectEqual(false, bc.session.load_external_stylesheets);
|
||||
try testing.expectEqual(true, bc.session.subframe_loading_enabled);
|
||||
try testing.expectEqual(true, bc.session.worker_loading_enabled);
|
||||
try testing.expectEqual(false, bc.session.load_resources.stylesheet);
|
||||
try testing.expectEqual(true, bc.session.load_resources.iframe);
|
||||
try testing.expectEqual(true, bc.session.load_resources.worker);
|
||||
}
|
||||
+13
-1
@@ -52,6 +52,11 @@ const log = lp.log;
|
||||
/// that appears in both with the same field name and type is collapsed
|
||||
/// into one field (the command's own option wins); reusing a name with
|
||||
/// a different type is a compile error.
|
||||
/// - `before_parse: fn () void` (optional) — called once the command is
|
||||
/// known (by name or sniffed from a legacy flag), before any option is
|
||||
/// read. For mode-level process defaults that must already hold while
|
||||
/// the options themselves are parsed, e.g. log settings; an explicit
|
||||
/// option parsed later still wins.
|
||||
/// - `positional: struct` (optional) — a positional argument with `.name`
|
||||
/// and `.type` that may appear anywhere in argv. By default it holds a
|
||||
/// single value: `.type` must be an optional pointer-to-u8 slice (e.g.
|
||||
@@ -79,6 +84,8 @@ const log = lp.log;
|
||||
/// built-in type switch. See the validator section below.
|
||||
/// - `variants: tuple` (optional) — alternate flag names that write into
|
||||
/// the same field. See the variants section below.
|
||||
/// - `deprecated: []const u8` (optional) — the option still parses, but
|
||||
/// each use logs a warning carrying this note.
|
||||
///
|
||||
/// ## Supported types and their defaults
|
||||
///
|
||||
@@ -470,7 +477,6 @@ pub fn Builder(comptime commands: anytype) type {
|
||||
inline for (.{
|
||||
"--host",
|
||||
"--port",
|
||||
"--timeout",
|
||||
}) |heuristic| {
|
||||
if (std.mem.eql(u8, cmd_str, heuristic)) {
|
||||
return .serve;
|
||||
@@ -526,6 +532,9 @@ pub fn Builder(comptime commands: anytype) type {
|
||||
const OptionType = @TypeOf(option);
|
||||
const is_multiple = @hasField(OptionType, "multiple") and option.multiple;
|
||||
const has_validator = @hasField(OptionType, "validator");
|
||||
if (@hasField(OptionType, "deprecated")) {
|
||||
log.warn(.app, "deprecated CLI parameter", .{ .name = option.name, .note = option.deprecated });
|
||||
}
|
||||
|
||||
// Prefer validator for parsing if provided. The validator writes
|
||||
// through the field pointer (the list itself for multiples).
|
||||
@@ -682,6 +691,9 @@ pub fn Builder(comptime commands: anytype) type {
|
||||
args: *std.process.Args.Iterator,
|
||||
) !Union {
|
||||
const Command = @FieldType(Union, command.name);
|
||||
if (@hasField(@TypeOf(command), "before_parse")) {
|
||||
command.before_parse();
|
||||
}
|
||||
var c = Command{};
|
||||
|
||||
const options = blk: {
|
||||
|
||||
+19
-21
@@ -351,28 +351,26 @@
|
||||
\\ --cookie-jar <PATH>
|
||||
\\ Path to a JSON file to save cookies to on exit (write-only).
|
||||
\\ Defaults to no cookie saving.
|
||||
\\ --disable-subframes
|
||||
\\ Skip loading <iframe> elements. The parser still registers them in the
|
||||
\\ DOM, but no child frame or Page.frameAttached events are produced.
|
||||
\\ Defaults to false.
|
||||
\\ --disable-workers
|
||||
\\ Skip loading dedicated Web Workers. The Worker constructor still
|
||||
\\ returns a Worker object, but no script fetch is initiated and its scope
|
||||
\\ never runs.
|
||||
\\ Defaults to false.
|
||||
\\ --enable-external-stylesheets
|
||||
\\ Fetch external <link rel=stylesheet> resources so their rules
|
||||
\\ contribute to computed styles (and therefore to visibility checks like
|
||||
\\ display, visibility, opacity, pointer-events).
|
||||
\\ Defaults to false, except in agent mode with an LLM, where it is on.
|
||||
\\ --load-resources <RESOURCE>
|
||||
\\ Sub-resource to actually request. Can be passed multiple times.
|
||||
\\ Defaults to requesting none of them.
|
||||
\\ Allowed values:
|
||||
\\ image <img> sources, so that load/error reflects the real
|
||||
\\ HTTP status. Only the response headers are read;
|
||||
\\ images are never decoded, so naturalWidth and
|
||||
\\ naturalHeight stay 0. Delays the window load event.
|
||||
\\ iframe When enabled, <iframe> elements are fully loaded.
|
||||
\\
|
||||
\\ image <img> sources, so that load/error reflects the real
|
||||
\\ HTTP status. Only the response headers are read;
|
||||
\\ images are never decoded, so naturalWidth and
|
||||
\\ naturalHeight stay 0. Delays the window load event.
|
||||
\\
|
||||
\\ stylesheet Fetch external <link rel=stylesheet> resources so
|
||||
\\ their rules contribute to computed styles (and
|
||||
\\ therefore to visibility checks like display,
|
||||
\\ visibility, opacity, pointer-events).
|
||||
\\
|
||||
\\ worker Enable loading dedicated and shared workers. When
|
||||
\\ disabled, the Worker constructor still returns a
|
||||
\\ Worker, but no script fetch is initiated and the
|
||||
\\ Worker never runs.
|
||||
\\ --http-cache-dir <PATH>
|
||||
\\ Directory used as a filesystem cache for network resources. Omitting
|
||||
\\ this disables caching.
|
||||
@@ -423,13 +421,13 @@
|
||||
\\ --insecure-disable-tls-host-verification
|
||||
\\ Disables host verification on all HTTP requests.
|
||||
\\ Only set this if you understand and accept the risk.
|
||||
\\ --log-filter-scopes <SCOPE>
|
||||
\\ --log-filter <SCOPE>
|
||||
\\ Filter logs per scope, applied first-to-last. Can be passed multiple times.
|
||||
\\ "-X" (or bare "X") filters out a scope, "+X" filters it in, and
|
||||
\\ "all" targets every scope.
|
||||
\\ e.g. --log-filter-scopes http --log-filter-scopes unknown_prop
|
||||
\\ e.g. --log-filter http --log-filter unknown_prop
|
||||
\\ hides those two.
|
||||
\\ --log-filter-scopes -all --log-filter-scopes +cdp
|
||||
\\ --log-filter -all --log-filter +cdp
|
||||
\\ hides everything except cdp.
|
||||
\\ --log-format <FORMAT>
|
||||
\\ The log format.
|
||||
|
||||
+32
-20
@@ -21,23 +21,24 @@ const lp = @import("lightpanda");
|
||||
|
||||
pub const Scope = enum {
|
||||
app,
|
||||
dom,
|
||||
bug,
|
||||
browser,
|
||||
bug,
|
||||
cache,
|
||||
cdp,
|
||||
console,
|
||||
http,
|
||||
frame,
|
||||
js,
|
||||
disabled,
|
||||
dom,
|
||||
event,
|
||||
scheduler,
|
||||
frame,
|
||||
http,
|
||||
js,
|
||||
mcp,
|
||||
not_implemented,
|
||||
scheduler,
|
||||
storage,
|
||||
telemetry,
|
||||
unknown_prop,
|
||||
mcp,
|
||||
cache,
|
||||
websocket,
|
||||
storage,
|
||||
};
|
||||
|
||||
pub const num_scopes = @typeInfo(Scope).@"enum".fields.len;
|
||||
@@ -54,7 +55,7 @@ pub const FilterRule = struct {
|
||||
/// array. Directives apply left-to-right, so `-all,+cdp` disables every
|
||||
/// scope then re-enables `cdp`. Scopes untouched by any directive stay
|
||||
/// enabled.
|
||||
pub fn resolveFilterScopes(rules: []const FilterRule) [num_scopes]bool {
|
||||
pub fn resolveFilters(rules: []const FilterRule) [num_scopes]bool {
|
||||
var scope_enabled = [_]bool{true} ** num_scopes;
|
||||
for (rules) |rule| {
|
||||
if (rule.scope) |scope| {
|
||||
@@ -70,7 +71,6 @@ const Opts = struct {
|
||||
format: Format = if (lp.IS_DEBUG) .pretty else .logfmt,
|
||||
level: Level = if (lp.IS_DEBUG) .info else .warn,
|
||||
// Per-scope enabled flags; a `false` entry suppresses that scope's logs.
|
||||
// Only consulted in Debug builds. Default: everything enabled.
|
||||
scope_enabled: [num_scopes]bool = [_]bool{true} ** num_scopes,
|
||||
};
|
||||
|
||||
@@ -86,10 +86,8 @@ pub fn enabled(scope: Scope, level: Level) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (comptime lp.IS_DEBUG) {
|
||||
if (opts.scope_enabled[@intFromEnum(scope)] == false) {
|
||||
return false;
|
||||
}
|
||||
if (opts.scope_enabled[@intFromEnum(scope)] == false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -164,6 +162,20 @@ pub fn note(scope: Scope, msg: []const u8, data: anytype) void {
|
||||
}
|
||||
}
|
||||
|
||||
var warned_disabled_worker = std.atomic.Value(bool).init(false);
|
||||
pub fn warnDisabledWorker() void {
|
||||
if (warned_disabled_worker.swap(true, .monotonic) == false) {
|
||||
warn(.disabled, "workers disabled", .{ .hint = "enable via --load-resources worker" });
|
||||
}
|
||||
}
|
||||
|
||||
var warned_disabled_iframe = std.atomic.Value(bool).init(false);
|
||||
pub fn warnDisabledIFrame() void {
|
||||
if (warned_disabled_iframe.swap(true, .monotonic) == false) {
|
||||
warn(.disabled, "iframes disabled", .{ .hint = "enable via --load-resources iframe" });
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log(scope: Scope, level: Level, msg: []const u8, data: anytype) void {
|
||||
if (enabled(scope, level) == false) {
|
||||
return;
|
||||
@@ -602,17 +614,17 @@ test "log: string escape" {
|
||||
}
|
||||
}
|
||||
|
||||
test "log: resolveFilterScopes" {
|
||||
test "log: resolveFilters" {
|
||||
// No directives: everything enabled.
|
||||
{
|
||||
const se = resolveFilterScopes(&.{});
|
||||
const se = resolveFilters(&.{});
|
||||
try testing.expectEqual(true, se[@intFromEnum(Scope.cdp)]);
|
||||
try testing.expectEqual(true, se[@intFromEnum(Scope.http)]);
|
||||
}
|
||||
|
||||
// Backward compatible: bare/`-` scope filters that scope out, rest stay in.
|
||||
{
|
||||
const se = resolveFilterScopes(&.{
|
||||
const se = resolveFilters(&.{
|
||||
.{ .scope = .cdp, .enable = false },
|
||||
.{ .scope = .http, .enable = false },
|
||||
});
|
||||
@@ -623,7 +635,7 @@ test "log: resolveFilterScopes" {
|
||||
|
||||
// `-all,+cdp`: disable everything, then re-enable cdp.
|
||||
{
|
||||
const se = resolveFilterScopes(&.{
|
||||
const se = resolveFilters(&.{
|
||||
.{ .scope = null, .enable = false },
|
||||
.{ .scope = .cdp, .enable = true },
|
||||
});
|
||||
@@ -634,7 +646,7 @@ test "log: resolveFilterScopes" {
|
||||
|
||||
// `+all,-cdp`: enable everything, then disable cdp. Order matters.
|
||||
{
|
||||
const se = resolveFilterScopes(&.{
|
||||
const se = resolveFilters(&.{
|
||||
.{ .scope = null, .enable = true },
|
||||
.{ .scope = .cdp, .enable = false },
|
||||
});
|
||||
|
||||
@@ -83,16 +83,6 @@ fn run(allocator: Allocator, main_arena: Allocator, proc_args: std.process.Args)
|
||||
else => {},
|
||||
}
|
||||
|
||||
if (args.logLevel()) |ll| {
|
||||
log.opts.level = ll;
|
||||
}
|
||||
if (args.logFormat()) |lf| {
|
||||
log.opts.format = lf;
|
||||
}
|
||||
|
||||
// Set log filter scopes.
|
||||
log.opts.scope_enabled = log.resolveFilterScopes(args.logFilterScopes().items);
|
||||
|
||||
// must be installed before any other threads
|
||||
const sighandler = try main_arena.create(SigHandler);
|
||||
sighandler.* = .{ .arena = main_arena };
|
||||
|
||||
+10
-6
@@ -341,8 +341,10 @@ const WEB_API_TEST_ROOT = "src/browser/tests/";
|
||||
const HtmlRunnerOpts = struct {
|
||||
timeout_ms: u32 = 2000,
|
||||
inject_script: ?[]const u8 = null,
|
||||
load_external_stylesheets: bool = false,
|
||||
load_resources: Config.LoadResources = .{},
|
||||
load_resources: Config.LoadResources = .{
|
||||
.worker = true,
|
||||
.iframe = true,
|
||||
},
|
||||
};
|
||||
|
||||
// Create a fresh page on `test_session` and return its root frame — for tests
|
||||
@@ -368,11 +370,12 @@ pub fn htmlRunner(comptime path: []const u8, opts: HtmlRunnerOpts) !void {
|
||||
}
|
||||
defer test_session.inject_scripts = &.{};
|
||||
|
||||
test_session.load_external_stylesheets = opts.load_external_stylesheets;
|
||||
defer test_session.load_external_stylesheets = false;
|
||||
|
||||
test_session.load_resources = opts.load_resources;
|
||||
defer test_session.load_resources = .{};
|
||||
defer test_session.load_resources = .{
|
||||
// original defaults, tests expect these to be on
|
||||
.worker = true,
|
||||
.iframe = true,
|
||||
};
|
||||
|
||||
const root = try std.fs.path.joinZ(arena_allocator, &.{ WEB_API_TEST_ROOT, path });
|
||||
const stat = std.Io.Dir.cwd().statFile(io, root, .{}) catch |err| {
|
||||
@@ -533,6 +536,7 @@ test "tests:beforeAll" {
|
||||
.insecure_disable_tls_host_verification = true,
|
||||
.user_agent_suffix = "internal-tester",
|
||||
.ws_max_concurrent = 50,
|
||||
.load_resources = .{ .worker = true, .iframe = true },
|
||||
} });
|
||||
|
||||
test_app = try App.init(test_allocator, &test_config);
|
||||
|
||||
Reference in new issue
Block a user