various small api fixes/tweaks

This commit is contained in:
Karl Seguin
2025-11-24 20:12:43 +08:00
parent 871fd46c89
commit e336c67857
17 changed files with 327 additions and 131 deletions

View File

@@ -63,7 +63,7 @@ _size_144_8: MemoryPoolAligned([144]u8, .@"8"),
_size_152_8: MemoryPoolAligned([152]u8, .@"8"),
_size_160_8: MemoryPoolAligned([160]u8, .@"8"),
_size_184_8: MemoryPoolAligned([184]u8, .@"8"),
_size_192_8: MemoryPoolAligned([192]u8, .@"8"),
_size_232_8: MemoryPoolAligned([232]u8, .@"8"),
_size_648_8: MemoryPoolAligned([648]u8, .@"8"),
pub fn init(page: *Page) Factory {
@@ -86,7 +86,7 @@ pub fn init(page: *Page) Factory {
._size_152_8 = MemoryPoolAligned([152]u8, .@"8").init(page.arena),
._size_160_8 = MemoryPoolAligned([160]u8, .@"8").init(page.arena),
._size_184_8 = MemoryPoolAligned([184]u8, .@"8").init(page.arena),
._size_192_8 = MemoryPoolAligned([192]u8, .@"8").init(page.arena),
._size_232_8 = MemoryPoolAligned([232]u8, .@"8").init(page.arena),
._size_648_8 = MemoryPoolAligned([648]u8, .@"8").init(page.arena),
};
}
@@ -265,7 +265,7 @@ pub fn createT(self: *Factory, comptime T: type) !*T {
if (comptime SO == 152) return @ptrCast(try self._size_152_8.create());
if (comptime SO == 160) return @ptrCast(try self._size_160_8.create());
if (comptime SO == 184) return @ptrCast(try self._size_184_8.create());
if (comptime SO == 192) return @ptrCast(try self._size_192_8.create());
if (comptime SO == 232) return @ptrCast(try self._size_232_8.create());
if (comptime SO == 648) return @ptrCast(try self._size_648_8.create());
@compileError(std.fmt.comptimePrint("No pool configured for @sizeOf({d}), @alignOf({d}): ({s})", .{ SO, @alignOf(T), @typeName(T) }));
}

View File

@@ -266,7 +266,7 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
try self.reset(false);
}
log.info(.http, "navigate", .{
log.info(.page, "navigate", .{
.url = request_url,
.method = opts.method,
.reason = opts.reason,
@@ -329,7 +329,7 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
.done_callback = pageDoneCallback,
.error_callback = pageErrorCallback,
}) catch |err| {
log.err(.http, "navigate request", .{ .url = self.url, .err = err });
log.err(.page, "navigate request", .{ .url = self.url, .err = err });
return err;
};
}
@@ -412,7 +412,7 @@ fn pageHeaderDoneCallback(transfer: *Http.Transfer) !void {
self.window._location = try Location.init(self.url, self);
self.document._location = self.window._location;
log.debug(.http, "navigate header", .{
log.debug(.page, "navigate header", .{
.url = self.url,
.status = header.status,
.content_type = header.contentType(),
@@ -433,7 +433,7 @@ fn pageDataCallback(transfer: *Http.Transfer, data: []const u8) !void {
} orelse .unknown;
if (comptime IS_DEBUG) {
log.debug(.http, "navigate first chunk", .{ .content_type = mime.content_type, .len = data.len });
log.debug(.page, "navigate first chunk", .{ .content_type = mime.content_type, .len = data.len });
}
switch (mime.content_type) {
@@ -475,7 +475,7 @@ fn pageDataCallback(transfer: *Http.Transfer, data: []const u8) !void {
fn pageDoneCallback(ctx: *anyopaque) !void {
if (comptime IS_DEBUG) {
log.debug(.http, "navigate done", .{});
log.debug(.page, "navigate done", .{});
}
var self: *Page = @ptrCast(@alignCast(ctx));
@@ -522,7 +522,7 @@ fn pageDoneCallback(ctx: *anyopaque) !void {
}
fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
log.err(.http, "navigate failed", .{ .err = err });
log.err(.page, "navigate failed", .{ .err = err });
var self: *Page = @ptrCast(@alignCast(ctx));
self.clearTransferArena();
@@ -624,7 +624,7 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult {
if (try_catch.hasCaught()) {
const msg = (try try_catch.err(self.arena)) orelse "unknown";
log.warn(.user_script, "page wait", .{ .err = msg, .src = "scheduler" });
log.warn(.js, "page wait", .{ .err = msg, .src = "scheduler" });
return error.JsError;
}

View File

@@ -757,7 +757,7 @@ const Script = struct {
// }
const msg = try_catch.err(page.arena) catch |err| @errorName(err) orelse "unknown";
log.warn(.user_script, "eval script", .{
log.warn(.js, "eval script", .{
.url = url,
.err = msg,
.cacheable = cacheable,

View File

@@ -17,24 +17,45 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Page = @import("Page.zig");
const Node = @import("webapi/Node.zig");
pub const Opts = struct {
// @ZIGDOM (none of these do anything)
pub const RootOpts = struct {
with_base: bool = false,
strip_mode: StripMode = .{},
strip: Opts.Strip = .{},
};
pub const StripMode = struct {
pub const Opts = struct {
strip: Strip = .{},
pub const Strip = struct {
js: bool = false,
ui: bool = false,
css: bool = false,
};
};
pub fn root(opts: RootOpts, writer: *std.Io.Writer, page: *Page) !void {
const doc = page.document;
if (opts.with_base) {
if (doc.is(Node.Document.HTMLDocument)) |html_doc| {
const parent = if (html_doc.getHead()) |head| head.asNode() else doc.asNode();
const base = try doc.createElement("base", null, page);
try base.setAttributeSafe("base", page.url, page);
_ = try parent.insertBefore(base.asNode(), parent.firstChild(), page);
}
}
return deep(doc.asNode(), .{.strip = opts.strip}, writer);
}
pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer) error{WriteFailed}!void {
switch (node._type) {
.cdata => |cd| try writer.writeAll(cd.getData()),
.element => |el| {
if (shouldStripElement(el, opts)) {
return;
}
try el.format(writer);
try children(node, opts, writer);
if (!isVoidElement(el)) {
@@ -106,3 +127,47 @@ fn isVoidElement(el: *const Node.Element) bool {
.svg => false,
};
}
fn shouldStripElement(el: *const Node.Element, opts: Opts) bool {
const tag_name = el.getTagNameDump();
if (opts.strip.js) {
if (std.mem.eql(u8, tag_name, "script")) return true;
if (std.mem.eql(u8, tag_name, "noscript")) return true;
if (std.mem.eql(u8, tag_name, "link")) {
if (el.getAttributeSafe("as")) |as| {
if (std.mem.eql(u8, as, "script")) return true;
}
if (el.getAttributeSafe("rel")) |rel| {
if (std.mem.eql(u8, rel, "modulepreload") or std.mem.eql(u8, rel, "preload")) {
if (el.getAttributeSafe("as")) |as| {
if (std.mem.eql(u8, as, "script")) return true;
}
}
}
}
}
if (opts.strip.css or opts.strip.ui) {
if (std.mem.eql(u8, tag_name, "style")) return true;
if (std.mem.eql(u8, tag_name, "link")) {
if (el.getAttributeSafe("rel")) |rel| {
if (std.mem.eql(u8, rel, "stylesheet")) return true;
}
}
}
if (opts.strip.ui) {
if (std.mem.eql(u8, tag_name, "img")) return true;
if (std.mem.eql(u8, tag_name, "picture")) return true;
if (std.mem.eql(u8, tag_name, "video")) return true;
if (std.mem.eql(u8, tag_name, "audio")) return true;
if (std.mem.eql(u8, tag_name, "svg")) return true;
if (std.mem.eql(u8, tag_name, "canvas")) return true;
if (std.mem.eql(u8, tag_name, "iframe")) return true;
}
return false;
}

View File

@@ -144,6 +144,7 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args
const result = self.func.castToFunction().call(context.v8_context, js_this, js_args);
if (result == null) {
std.debug.print("CB ERR: {s}\n", .{self.src() catch "???"});
return error.JSExecCallback;
}

View File

@@ -80,3 +80,11 @@
testing.expectTrue(whereResult.length >= 3);
}
</script>
<div id=escaped class=":popover-open"></div>
<script id="escaped">
{
const escaped = document.querySelector('.\\:popover-open');
testing.expectEqual('escaped', escaped.id);
}
</script>

View File

@@ -52,7 +52,7 @@ pub fn pushState(self: *History, state: js.Object, _title: []const u8, url: ?[]c
_ = url; // For minimal implementation, we don't actually navigate
_ = page;
self._state = state;
self._state = try state.persist();
self._length += 1;
}
@@ -60,7 +60,7 @@ pub fn replaceState(self: *History, state: js.Object, _title: []const u8, url: ?
_ = _title;
_ = url;
_ = page;
self._state = state;
self._state = try state.persist();
// Note: replaceState doesn't change length
}

View File

@@ -49,6 +49,7 @@ _performance: Performance,
_history: History,
_storage_bucket: *storage.Bucket,
_on_load: ?js.Function = null,
_on_error: ?js.Function = null, // TODO: invoke on error?
_location: *Location,
_timer_id: u30 = 0,
_timers: std.AutoHashMapUnmanaged(u32, *ScheduleCallback) = .{},
@@ -71,7 +72,6 @@ pub fn getDocument(self: *Window) *Document {
}
pub fn getConsole(self: *Window) *Console {
std.debug.print("getConsole\n", .{});
return &self._console;
}
@@ -119,6 +119,18 @@ pub fn setOnLoad(self: *Window, cb_: ?js.Function) !void {
}
}
pub fn getOnError(self: *const Window) ?js.Function {
return self._on_error;
}
pub fn setOnError(self: *Window, cb_: ?js.Function) !void {
if (cb_) |cb| {
self._on_error = cb;
} else {
self._on_error = null;
}
}
pub fn fetch(_: *const Window, input: Fetch.Input, page: *Page) !js.Promise {
return Fetch.init(input, page);
}
@@ -214,7 +226,7 @@ pub fn matchMedia(_: *const Window, query: []const u8, page: *Page) !*MediaQuery
});
}
pub fn getComputedStyle(_: *const Window, _: *Element, page: *Page) !@import("css/CSSStyleDeclaration.zig") {
pub fn getComputedStyle(_: *const Window, _: *Element, page: *Page) !*CSSStyleDeclaration {
return CSSStyleDeclaration.init(null, page);
}
@@ -362,6 +374,7 @@ pub const JsApi = struct {
pub const crypto = bridge.accessor(Window.getCrypto, null, .{ .cache = "crypto" });
pub const customElements = bridge.accessor(Window.getCustomElements, null, .{ .cache = "customElements" });
pub const onload = bridge.accessor(Window.getOnLoad, Window.setOnLoad, .{});
pub const onerror = bridge.accessor(Window.getOnError, Window.getOnError, .{});
pub const fetch = bridge.function(Window.fetch, .{});
pub const queueMicrotask = bridge.function(Window.queueMicrotask, .{});
pub const setTimeout = bridge.function(Window.setTimeout, .{});

View File

@@ -43,6 +43,9 @@ pub fn getMatches(_: *const MediaQueryList) bool {
return false;
}
pub fn addListener(_: *const MediaQueryList, _: js.Function) void {}
pub fn removeListener(_: *const MediaQueryList, _: js.Function) void {}
pub const JsApi = struct {
pub const bridge = js.Bridge(MediaQueryList);
@@ -54,6 +57,8 @@ pub const JsApi = struct {
pub const media = bridge.accessor(MediaQueryList.getMedia, null, .{});
pub const matches = bridge.accessor(MediaQueryList.getMatches, null, .{});
pub const addListener = bridge.function(MediaQueryList.addListener, .{});
pub const removeListener = bridge.function(MediaQueryList.removeListener, .{});
};
const testing = @import("../../../testing.zig");

View File

@@ -35,6 +35,11 @@ _executed: bool = false,
pub fn asElement(self: *Script) *Element {
return self._proto._proto;
}
pub fn asConstElement(self: *const Script) *const Element {
return self._proto._proto;
}
pub fn asNode(self: *Script) *Node {
return self.asElement().asNode();
}
@@ -76,6 +81,10 @@ pub fn setOnError(self: *Script, cb_: ?js.Function) !void {
}
}
pub fn getNoModule(self: *const Script) bool {
return self.asConstElement().getAttributeSafe("nomodule") != null;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(Script);
@@ -88,6 +97,7 @@ pub const JsApi = struct {
pub const src = bridge.accessor(Script.getSrc, Script.setSrc, .{});
pub const onload = bridge.accessor(Script.getOnLoad, Script.setOnLoad, .{});
pub const onerorr = bridge.accessor(Script.getOnError, Script.setOnError, .{});
pub const noModule = bridge.accessor(Script.getNoModule, null, .{});
};
pub const Build = struct {

View File

@@ -125,7 +125,7 @@ pub fn open(self: *XMLHttpRequest, method_: []const u8, url: [:0]const u8) !void
pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void {
if (comptime IS_DEBUG) {
log.debug(.xhr, "XMLHttpRequest.send", .{ .url = self._url });
log.debug(.http, "XMLHttpRequest.send", .{ .url = self._url });
}
if (body_) |b| {

View File

@@ -226,8 +226,8 @@ pub fn parse(arena: Allocator, input: []const u8, page: *Page) ParseError!Select
fn parsePart(self: *Parser, arena: Allocator, page: *Page) !Part {
return switch (self.peek()) {
'#' => .{ .id = try self.id() },
'.' => .{ .class = try self.class() },
'#' => .{ .id = try self.id(arena) },
'.' => .{ .class = try self.class(arena) },
'*' => blk: {
self.input = self.input[1..];
break :blk .universal;
@@ -655,7 +655,7 @@ fn parseNthPattern(self: *Parser) !Selector.NthPattern {
return .{ .a = a, .b = b };
}
pub fn id(self: *Parser) ![]const u8 {
pub fn id(self: *Parser, arena: Allocator) ![]const u8 {
// Must be called when we're at a '#'
std.debug.assert(self.peek() == '#');
@@ -667,26 +667,46 @@ pub fn id(self: *Parser) ![]const u8 {
return error.InvalidIDSelector;
}
// First character: must be letter, underscore, or non-ASCII (>= 0x80)
// Can also be hyphen if not followed by digit or another hyphen
const first = input[0];
if (first == '-') {
if (input.len < 2) {
@branchHint(.cold);
return error.InvalidIDSelector;
}
const second = input[1];
if (second == '-' or std.ascii.isDigit(second)) {
@branchHint(.cold);
return error.InvalidIDSelector;
}
} else if (!std.ascii.isAlphabetic(first) and first != '_' and first < 0x80) {
@branchHint(.cold);
return error.InvalidIDSelector;
}
// First pass: find the end of the id and check if there are escape sequences
var i: usize = 0;
var has_escape = false;
var first_char_validated = false;
var i: usize = 1;
for (input[1..]) |b| {
while (i < input.len) {
const b = input[i];
if (b == '\\') {
// Escape sequence
if (i + 1 >= input.len) {
@branchHint(.cold);
return error.InvalidIDSelector;
}
has_escape = true;
i += 2; // Skip backslash and escaped char
first_char_validated = true;
continue;
}
// Validate first character if not yet validated
if (!first_char_validated) {
if (b == '-') {
if (i + 1 >= input.len) {
@branchHint(.cold);
return error.InvalidIDSelector;
}
const second = input[i + 1];
if (second == '-' or std.ascii.isDigit(second)) {
@branchHint(.cold);
return error.InvalidIDSelector;
}
} else if (!std.ascii.isAlphabetic(b) and b != '_' and b < 0x80) {
@branchHint(.cold);
return error.InvalidIDSelector;
}
first_char_validated = true;
}
// Check if this is a valid id character
switch (b) {
'a'...'z', 'A'...'Z', '0'...'9', '-', '_' => {},
0x80...0xFF => {}, // non-ASCII characters
@@ -701,11 +721,39 @@ pub fn id(self: *Parser) ![]const u8 {
i += 1;
}
if (i == 0) {
@branchHint(.cold);
return error.InvalidIDSelector;
}
const raw = input[0..i];
self.input = input[i..];
return input[0..i];
// If no escape sequences, return the slice as-is
if (!has_escape) {
return raw;
}
// Build unescaped string
var result = try std.ArrayList(u8).initCapacity(arena, raw.len);
var j: usize = 0;
while (j < raw.len) {
if (raw[j] == '\\') {
j += 1; // Skip backslash
if (j < raw.len) {
try result.append(arena, raw[j]); // Add escaped char
j += 1;
}
} else {
try result.append(arena, raw[j]);
j += 1;
}
}
return result.items;
}
fn class(self: *Parser) ![]const u8 {
fn class(self: *Parser, arena: Allocator) ![]const u8 {
// Must be called when we're at a '.'
std.debug.assert(self.peek() == '.');
@@ -717,26 +765,46 @@ fn class(self: *Parser) ![]const u8 {
return error.InvalidClassSelector;
}
// First character: must be letter, underscore, or non-ASCII (>= 0x80)
// Can also be hyphen if not followed by digit or another hyphen
const first = input[0];
if (first == '-') {
if (input.len < 2) {
@branchHint(.cold);
return error.InvalidClassSelector;
}
const second = input[1];
if (second == '-' or std.ascii.isDigit(second)) {
@branchHint(.cold);
return error.InvalidClassSelector;
}
} else if (!std.ascii.isAlphabetic(first) and first != '_' and first < 0x80) {
@branchHint(.cold);
return error.InvalidClassSelector;
}
// First pass: find the end of the class name and check if there are escape sequences
var i: usize = 0;
var has_escape = false;
var first_char_validated = false;
var i: usize = 1;
for (input[1..]) |b| {
while (i < input.len) {
const b = input[i];
if (b == '\\') {
// Escape sequence
if (i + 1 >= input.len) {
@branchHint(.cold);
return error.InvalidClassSelector;
}
has_escape = true;
i += 2; // Skip backslash and escaped char
first_char_validated = true;
continue;
}
// Validate first character if not yet validated
if (!first_char_validated) {
if (b == '-') {
if (i + 1 >= input.len) {
@branchHint(.cold);
return error.InvalidClassSelector;
}
const second = input[i + 1];
if (second == '-' or std.ascii.isDigit(second)) {
@branchHint(.cold);
return error.InvalidClassSelector;
}
} else if (!std.ascii.isAlphabetic(b) and b != '_' and b < 0x80) {
@branchHint(.cold);
return error.InvalidClassSelector;
}
first_char_validated = true;
}
// Check if this is a valid class name character
switch (b) {
'a'...'z', 'A'...'Z', '0'...'9', '-', '_' => {},
0x80...0xFF => {}, // non-ASCII characters
@@ -751,8 +819,36 @@ fn class(self: *Parser) ![]const u8 {
i += 1;
}
if (i == 0) {
@branchHint(.cold);
return error.InvalidClassSelector;
}
const raw = input[0..i];
self.input = input[i..];
return input[0..i];
// If no escape sequences, return the slice as-is
if (!has_escape) {
return raw;
}
// Build unescaped string
var result = try std.ArrayList(u8).initCapacity(arena, raw.len);
var j: usize = 0;
while (j < raw.len) {
if (raw[j] == '\\') {
j += 1; // Skip backslash
if (j < raw.len) {
try result.append(arena, raw[j]); // Add escaped char
j += 1;
}
} else {
try result.append(arena, raw[j]);
j += 1;
}
}
return result.items;
}
fn tag(self: *Parser) ![]const u8 {
@@ -941,227 +1037,231 @@ fn fastEql(a: []const u8, comptime b: []const u8) bool {
const testing = @import("../../../testing.zig");
test "Selector: Parser.ID" {
const arena = testing.allocator;
{
var parser = Parser{ .input = "#" };
try testing.expectError(error.InvalidIDSelector, parser.id());
try testing.expectError(error.InvalidIDSelector, parser.id(arena));
}
{
var parser = Parser{ .input = "# " };
try testing.expectError(error.InvalidIDSelector, parser.id());
try testing.expectError(error.InvalidIDSelector, parser.id(arena));
}
{
var parser = Parser{ .input = "#1" };
try testing.expectError(error.InvalidIDSelector, parser.id());
try testing.expectError(error.InvalidIDSelector, parser.id(arena));
}
{
var parser = Parser{ .input = "#9abc" };
try testing.expectError(error.InvalidIDSelector, parser.id());
try testing.expectError(error.InvalidIDSelector, parser.id(arena));
}
{
var parser = Parser{ .input = "#-1" };
try testing.expectError(error.InvalidIDSelector, parser.id());
try testing.expectError(error.InvalidIDSelector, parser.id(arena));
}
{
var parser = Parser{ .input = "#-5abc" };
try testing.expectError(error.InvalidIDSelector, parser.id());
try testing.expectError(error.InvalidIDSelector, parser.id(arena));
}
{
var parser = Parser{ .input = "#--" };
try testing.expectError(error.InvalidIDSelector, parser.id());
try testing.expectError(error.InvalidIDSelector, parser.id(arena));
}
{
var parser = Parser{ .input = "#--test" };
try testing.expectError(error.InvalidIDSelector, parser.id());
try testing.expectError(error.InvalidIDSelector, parser.id(arena));
}
{
var parser = Parser{ .input = "#-" };
try testing.expectError(error.InvalidIDSelector, parser.id());
try testing.expectError(error.InvalidIDSelector, parser.id(arena));
}
{
var parser = Parser{ .input = "#over" };
try testing.expectEqual("over", try parser.id());
try testing.expectEqual("over", try parser.id(arena));
try testing.expectEqual("", parser.input);
}
{
var parser = Parser{ .input = "#myID123" };
try testing.expectEqual("myID123", try parser.id());
try testing.expectEqual("myID123", try parser.id(arena));
try testing.expectEqual("", parser.input);
}
{
var parser = Parser{ .input = "#_test" };
try testing.expectEqual("_test", try parser.id());
try testing.expectEqual("_test", try parser.id(arena));
try testing.expectEqual("", parser.input);
}
{
var parser = Parser{ .input = "#test_123" };
try testing.expectEqual("test_123", try parser.id());
try testing.expectEqual("test_123", try parser.id(arena));
try testing.expectEqual("", parser.input);
}
{
var parser = Parser{ .input = "#-test" };
try testing.expectEqual("-test", try parser.id());
try testing.expectEqual("-test", try parser.id(arena));
try testing.expectEqual("", parser.input);
}
{
var parser = Parser{ .input = "#my-id" };
try testing.expectEqual("my-id", try parser.id());
try testing.expectEqual("my-id", try parser.id(arena));
try testing.expectEqual("", parser.input);
}
{
var parser = Parser{ .input = "#test other" };
try testing.expectEqual("test", try parser.id());
try testing.expectEqual("test", try parser.id(arena));
try testing.expectEqual(" other", parser.input);
}
{
var parser = Parser{ .input = "#id.class" };
try testing.expectEqual("id", try parser.id());
try testing.expectEqual("id", try parser.id(arena));
try testing.expectEqual(".class", parser.input);
}
{
var parser = Parser{ .input = "#id:hover" };
try testing.expectEqual("id", try parser.id());
try testing.expectEqual("id", try parser.id(arena));
try testing.expectEqual(":hover", parser.input);
}
{
var parser = Parser{ .input = "#id>child" };
try testing.expectEqual("id", try parser.id());
try testing.expectEqual("id", try parser.id(arena));
try testing.expectEqual(">child", parser.input);
}
{
var parser = Parser{ .input = "#id[attr]" };
try testing.expectEqual("id", try parser.id());
try testing.expectEqual("id", try parser.id(arena));
try testing.expectEqual("[attr]", parser.input);
}
}
test "Selector: Parser.class" {
const arena = testing.allocator;
{
var parser = Parser{ .input = "." };
try testing.expectError(error.InvalidClassSelector, parser.class());
try testing.expectError(error.InvalidClassSelector, parser.class(arena));
}
{
var parser = Parser{ .input = ". " };
try testing.expectError(error.InvalidClassSelector, parser.class());
try testing.expectError(error.InvalidClassSelector, parser.class(arena));
}
{
var parser = Parser{ .input = ".1" };
try testing.expectError(error.InvalidClassSelector, parser.class());
try testing.expectError(error.InvalidClassSelector, parser.class(arena));
}
{
var parser = Parser{ .input = ".9abc" };
try testing.expectError(error.InvalidClassSelector, parser.class());
try testing.expectError(error.InvalidClassSelector, parser.class(arena));
}
{
var parser = Parser{ .input = ".-1" };
try testing.expectError(error.InvalidClassSelector, parser.class());
try testing.expectError(error.InvalidClassSelector, parser.class(arena));
}
{
var parser = Parser{ .input = ".-5abc" };
try testing.expectError(error.InvalidClassSelector, parser.class());
try testing.expectError(error.InvalidClassSelector, parser.class(arena));
}
{
var parser = Parser{ .input = ".--" };
try testing.expectError(error.InvalidClassSelector, parser.class());
try testing.expectError(error.InvalidClassSelector, parser.class(arena));
}
{
var parser = Parser{ .input = ".--test" };
try testing.expectError(error.InvalidClassSelector, parser.class());
try testing.expectError(error.InvalidClassSelector, parser.class(arena));
}
{
var parser = Parser{ .input = ".-" };
try testing.expectError(error.InvalidClassSelector, parser.class());
try testing.expectError(error.InvalidClassSelector, parser.class(arena));
}
{
var parser = Parser{ .input = ".active" };
try testing.expectEqual("active", try parser.class());
try testing.expectEqual("active", try parser.class(arena));
try testing.expectEqual("", parser.input);
}
{
var parser = Parser{ .input = ".myClass123" };
try testing.expectEqual("myClass123", try parser.class());
try testing.expectEqual("myClass123", try parser.class(arena));
try testing.expectEqual("", parser.input);
}
{
var parser = Parser{ .input = "._test" };
try testing.expectEqual("_test", try parser.class());
try testing.expectEqual("_test", try parser.class(arena));
try testing.expectEqual("", parser.input);
}
{
var parser = Parser{ .input = ".test_123" };
try testing.expectEqual("test_123", try parser.class());
try testing.expectEqual("test_123", try parser.class(arena));
try testing.expectEqual("", parser.input);
}
{
var parser = Parser{ .input = ".-test" };
try testing.expectEqual("-test", try parser.class());
try testing.expectEqual("-test", try parser.class(arena));
try testing.expectEqual("", parser.input);
}
{
var parser = Parser{ .input = ".my-class" };
try testing.expectEqual("my-class", try parser.class());
try testing.expectEqual("my-class", try parser.class(arena));
try testing.expectEqual("", parser.input);
}
{
var parser = Parser{ .input = ".test other" };
try testing.expectEqual("test", try parser.class());
try testing.expectEqual("test", try parser.class(arena));
try testing.expectEqual(" other", parser.input);
}
{
var parser = Parser{ .input = ".class1.class2" };
try testing.expectEqual("class1", try parser.class());
try testing.expectEqual("class1", try parser.class(arena));
try testing.expectEqual(".class2", parser.input);
}
{
var parser = Parser{ .input = ".class:hover" };
try testing.expectEqual("class", try parser.class());
try testing.expectEqual("class", try parser.class(arena));
try testing.expectEqual(":hover", parser.input);
}
{
var parser = Parser{ .input = ".class>child" };
try testing.expectEqual("class", try parser.class());
try testing.expectEqual("class", try parser.class(arena));
try testing.expectEqual(">child", parser.input);
}
{
var parser = Parser{ .input = ".class[attr]" };
try testing.expectEqual("class", try parser.class());
try testing.expectEqual("class", try parser.class(arena));
try testing.expectEqual("[attr]", parser.input);
}
}
@@ -1354,3 +1454,4 @@ test "Selector: Parser.parseNthPattern" {
try testing.expectEqual(" )", parser.input);
}
}

View File

@@ -129,7 +129,7 @@ pub const Jar = struct {
pub fn populateFromResponse(self: *Jar, uri: *const Uri, set_cookie: []const u8) !void {
const c = Cookie.parse(self.allocator, uri, set_cookie) catch |err| {
log.warn(.web_api, "cookie parse failed", .{ .raw = set_cookie, .err = err });
log.warn(.page, "cookie parse failed", .{ .raw = set_cookie, .err = err });
return;
};
@@ -312,7 +312,7 @@ pub const Cookie = struct {
// Algolia, for example, will call document.setCookie with
// an expired value which is literally 'Invalid Date'
// (it's trying to do something like: `new Date() + undefined`).
log.debug(.web_api, "cookie expires date", .{ .date = expires_ });
log.debug(.page, "cookie expires date", .{ .date = expires_ });
}
}
}

View File

@@ -88,8 +88,8 @@ pub fn LogInterceptor(comptime BC: type) type {
self.bc.cdp.sendEvent("Log.entryAdded", .{
.entry = .{
.source = switch (scope) {
.js, .user_script, .console, .web_api, .script_event => "javascript",
.http, .fetch, .xhr => "network",
.js, .console => "javascript",
.http => "network",
.telemetry, .unknown_prop, .interceptor => unreachable, // filtered out in writer above
else => "other",
},

View File

@@ -32,7 +32,7 @@ const Allocator = std.mem.Allocator;
pub const FetchOpts = struct {
wait_ms: u32 = 5000,
dump: dump.Opts,
dump: dump.RootOpts,
writer: ?*std.Io.Writer = null,
};
pub fn fetch(app: *App, url: [:0]const u8, opts: FetchOpts) !void {
@@ -64,7 +64,7 @@ pub fn fetch(app: *App, url: [:0]const u8, opts: FetchOpts) !void {
_ = session.fetchWait(opts.wait_ms);
const writer = opts.writer orelse return;
try dump.deep(page.document.asNode(), opts.dump, writer);
try dump.root(opts.dump, writer, page);
try writer.flush();
}

View File

@@ -33,19 +33,12 @@ pub const Scope = enum {
http,
page,
js,
loop,
event,
scheduler,
not_implemented,
script_event,
telemetry,
user_script,
unknown_prop,
web_api,
xhr,
fetch,
polyfill,
interceptor,
unknown_prop,
};
const Opts = struct {
@@ -394,7 +387,7 @@ test "log: data" {
const string = try testing.allocator.dupe(u8, "spice_must_flow");
defer testing.allocator.free(string);
try logTo(.http, .warn, "a msg", .{
try logTo(.page, .warn, "a msg", .{
.cint = 5,
.cfloat = 3.43,
.int = @as(i16, -49),
@@ -409,7 +402,7 @@ test "log: data" {
.level = Level.warn,
}, &aw.writer);
try testing.expectEqual("$time=1739795092929 $scope=http $level=warn $msg=\"a msg\" " ++
try testing.expectEqual("$time=1739795092929 $scope=page $level=warn $msg=\"a msg\" " ++
"cint=5 cfloat=3.43 int=-49 float=0.0003232 bt=true bf=false " ++
"nn=33 n=null lit=over9000! slice=spice_must_flow " ++
"err=Nope level=warn\n", aw.written());

View File

@@ -125,8 +125,8 @@ fn run(allocator: Allocator, main_arena: Allocator) !void {
var fetch_opts = lp.FetchOpts{
.wait_ms = 5000,
.dump = .{
.strip = opts.strip,
.with_base = opts.withbase,
.strip_mode = opts.strip_mode,
},
};
@@ -245,7 +245,7 @@ const Command = struct {
dump: bool = false,
common: Common,
withbase: bool = false,
strip_mode: lp.dump.Opts.StripMode = .{},
strip: lp.dump.Opts.Strip = .{},
};
const Common = struct {
@@ -511,7 +511,7 @@ fn parseFetchArgs(
var withbase: bool = false;
var url: ?[:0]const u8 = null;
var common: Command.Common = .{};
var strip_mode: lp.dump.Opts.StripMode = .{};
var strip: lp.dump.Opts.Strip = .{};
while (args.next()) |opt| {
if (std.mem.eql(u8, "--dump", opt)) {
@@ -524,7 +524,7 @@ fn parseFetchArgs(
.feature = "--noscript argument",
.hint = "use '--strip_mode js' instead",
});
strip_mode.js = true;
strip.js = true;
continue;
}
@@ -543,15 +543,15 @@ fn parseFetchArgs(
while (it.next()) |part| {
const trimmed = std.mem.trim(u8, part, &std.ascii.whitespace);
if (std.mem.eql(u8, trimmed, "js")) {
strip_mode.js = true;
strip.js = true;
} else if (std.mem.eql(u8, trimmed, "ui")) {
strip_mode.ui = true;
strip.ui = true;
} else if (std.mem.eql(u8, trimmed, "css")) {
strip_mode.css = true;
strip.css = true;
} else if (std.mem.eql(u8, trimmed, "full")) {
strip_mode.js = true;
strip_mode.ui = true;
strip_mode.css = true;
strip.js = true;
strip.ui = true;
strip.css = true;
} else {
log.fatal(.app, "invalid option choice", .{ .arg = "--strip_mode", .value = trimmed });
}
@@ -583,9 +583,9 @@ fn parseFetchArgs(
return .{
.url = url.?,
.dump = dump,
.strip = strip,
.common = common,
.withbase = withbase,
.strip_mode = strip_mode,
};
}