From e336c67857b92d6a5ec5fcbfad7f159f8937b3eb Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 24 Nov 2025 20:12:43 +0800 Subject: [PATCH] various small api fixes/tweaks --- src/browser/Factory.zig | 6 +- src/browser/Page.zig | 14 +- src/browser/ScriptManager.zig | 2 +- src/browser/dump.zig | 73 ++++- src/browser/js/Function.zig | 1 + src/browser/tests/element/pseudo_classes.html | 8 + src/browser/webapi/History.zig | 4 +- src/browser/webapi/Window.zig | 17 +- src/browser/webapi/css/MediaQueryList.zig | 5 + src/browser/webapi/element/html/Script.zig | 10 + src/browser/webapi/net/XMLHttpRequest.zig | 2 +- src/browser/webapi/selector/Parser.zig | 269 ++++++++++++------ src/browser/webapi/storage/cookie.zig | 4 +- src/cdp/domains/log.zig | 4 +- src/lightpanda.zig | 4 +- src/log.zig | 13 +- src/main.zig | 22 +- 17 files changed, 327 insertions(+), 131 deletions(-) diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index 696ae98c..8c9c3b58 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -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) })); } diff --git a/src/browser/Page.zig b/src/browser/Page.zig index e9eda3f2..b93a9f94 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -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; } diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index 0d8740c8..632be5f2 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -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, diff --git a/src/browser/dump.zig b/src/browser/dump.zig index 8efc8da4..2e00ba39 100644 --- a/src/browser/dump.zig +++ b/src/browser/dump.zig @@ -17,24 +17,45 @@ // along with this program. If not, see . 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; +} diff --git a/src/browser/js/Function.zig b/src/browser/js/Function.zig index 73c02911..41d8fa2c 100644 --- a/src/browser/js/Function.zig +++ b/src/browser/js/Function.zig @@ -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; } diff --git a/src/browser/tests/element/pseudo_classes.html b/src/browser/tests/element/pseudo_classes.html index 8114cae0..fe75ab84 100644 --- a/src/browser/tests/element/pseudo_classes.html +++ b/src/browser/tests/element/pseudo_classes.html @@ -80,3 +80,11 @@ testing.expectTrue(whereResult.length >= 3); } + +
+ diff --git a/src/browser/webapi/History.zig b/src/browser/webapi/History.zig index 3bc56866..d80fe3ba 100644 --- a/src/browser/webapi/History.zig +++ b/src/browser/webapi/History.zig @@ -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 } diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index ecf3793d..b65359e4 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -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, .{}); diff --git a/src/browser/webapi/css/MediaQueryList.zig b/src/browser/webapi/css/MediaQueryList.zig index 4e0da971..46304ccc 100644 --- a/src/browser/webapi/css/MediaQueryList.zig +++ b/src/browser/webapi/css/MediaQueryList.zig @@ -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"); diff --git a/src/browser/webapi/element/html/Script.zig b/src/browser/webapi/element/html/Script.zig index 1e548c4e..c12038a6 100644 --- a/src/browser/webapi/element/html/Script.zig +++ b/src/browser/webapi/element/html/Script.zig @@ -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 { diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig index dfb848e6..6239ddc4 100644 --- a/src/browser/webapi/net/XMLHttpRequest.zig +++ b/src/browser/webapi/net/XMLHttpRequest.zig @@ -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| { diff --git a/src/browser/webapi/selector/Parser.zig b/src/browser/webapi/selector/Parser.zig index 41075ca5..b97f7c00 100644 --- a/src/browser/webapi/selector/Parser.zig +++ b/src/browser/webapi/selector/Parser.zig @@ -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); } } + diff --git a/src/browser/webapi/storage/cookie.zig b/src/browser/webapi/storage/cookie.zig index 25d6f51d..436d258b 100644 --- a/src/browser/webapi/storage/cookie.zig +++ b/src/browser/webapi/storage/cookie.zig @@ -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_ }); } } } diff --git a/src/cdp/domains/log.zig b/src/cdp/domains/log.zig index 07d3c6d6..66b8b79f 100644 --- a/src/cdp/domains/log.zig +++ b/src/cdp/domains/log.zig @@ -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", }, diff --git a/src/lightpanda.zig b/src/lightpanda.zig index 9c15f722..ddc815fd 100644 --- a/src/lightpanda.zig +++ b/src/lightpanda.zig @@ -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(); } diff --git a/src/log.zig b/src/log.zig index e34329e8..d3ab7c76 100644 --- a/src/log.zig +++ b/src/log.zig @@ -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()); diff --git a/src/main.zig b/src/main.zig index 42ad8d0f..1f7bd57e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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, }; }