mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-15 15:58:57 +00:00
various small api fixes/tweaks
This commit is contained in:
@@ -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) }));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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_ });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
13
src/log.zig
13
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());
|
||||
|
||||
22
src/main.zig
22
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user