Compare commits

..

1 Commits

Author SHA1 Message Date
Karl Seguin
de0a04a58e Relax assertion on httpclient abort
It's ok to still have transfers, as long as whatever transfers still exists
are in an aborted state.
2026-04-02 17:59:17 +08:00
2 changed files with 62 additions and 51 deletions

View File

@@ -235,10 +235,6 @@ fn _abort(self: *Client, comptime abort_all: bool, frame_id: u32) void {
} }
} }
if (comptime IS_DEBUG and abort_all) {
std.debug.assert(self.active == 0);
}
{ {
var q = &self.queue; var q = &self.queue;
var n = q.first; var n = q.first;
@@ -259,12 +255,16 @@ fn _abort(self: *Client, comptime abort_all: bool, frame_id: u32) void {
} }
if (comptime IS_DEBUG and abort_all) { if (comptime IS_DEBUG and abort_all) {
std.debug.assert(self.in_use.first == null); // Even after an abort_all, we could still have transfers, but, at the
// very least, they should all be flagged as aborted.
const running = self.handles.perform() catch |err| { var it = self.in_use.first;
lp.assert(false, "multi perform in abort", .{ .err = err }); var leftover: usize = 0;
}; while (it) |node| : (it = node.next) {
std.debug.assert(running == 0); const conn: *http.Connection = @fieldParentPtr("node", node);
std.debug.assert((Transfer.fromConnection(conn) catch unreachable).aborted);
leftover += 1;
}
std.debug.assert(self.active == leftover);
} }
} }

View File

@@ -25,7 +25,9 @@ const Element = @import("webapi/Element.zig");
const Node = @import("webapi/Node.zig"); const Node = @import("webapi/Node.zig");
const isAllWhitespace = @import("../string.zig").isAllWhitespace; const isAllWhitespace = @import("../string.zig").isAllWhitespace;
pub const Opts = struct {}; pub const Opts = struct {
// Options for future customization (e.g., dialect)
};
const State = struct { const State = struct {
const ListType = enum { ordered, unordered }; const ListType = enum { ordered, unordered };
@@ -37,6 +39,7 @@ const State = struct {
list_depth: usize = 0, list_depth: usize = 0,
list_stack: [32]ListState = undefined, list_stack: [32]ListState = undefined,
pre_node: ?*Node = null, pre_node: ?*Node = null,
in_code: bool = false,
in_table: bool = false, in_table: bool = false,
table_row_index: usize = 0, table_row_index: usize = 0,
table_col_count: usize = 0, table_col_count: usize = 0,
@@ -97,35 +100,27 @@ fn getAnchorLabel(el: *Element) ?[]const u8 {
return el.getAttributeSafe(comptime .wrap("aria-label")) orelse el.getAttributeSafe(comptime .wrap("title")); return el.getAttributeSafe(comptime .wrap("aria-label")) orelse el.getAttributeSafe(comptime .wrap("title"));
} }
const ContentInfo = struct { fn hasBlockDescendant(root: *Node) bool {
has_visible: bool, var tw = TreeWalker.FullExcludeSelf.Elements.init(root, .{});
has_block: bool, while (tw.next()) |el| {
}; if (el.getTag().isBlock()) return true;
}
return false;
}
fn analyzeContent(root: *Node) ContentInfo { fn hasVisibleContent(root: *Node) bool {
var result: ContentInfo = .{ .has_visible = false, .has_block = false };
var tw = TreeWalker.FullExcludeSelf.init(root, .{}); var tw = TreeWalker.FullExcludeSelf.init(root, .{});
while (tw.next()) |node| { while (tw.next()) |node| {
if (isSignificantText(node)) { if (isSignificantText(node)) return true;
result.has_visible = true; if (node.is(Element)) |el| {
if (result.has_block) return result;
} else if (node.is(Element)) |el| {
if (!isVisibleElement(el)) { if (!isVisibleElement(el)) {
tw.skipChildren(); tw.skipChildren();
} else { } else if (el.getTag() == .img) {
const tag = el.getTag(); return true;
if (tag == .img) {
result.has_visible = true;
if (result.has_block) return result;
}
if (tag.isBlock()) {
result.has_block = true;
if (result.has_visible) return result;
}
} }
} }
} }
return result; return false;
} }
const Context = struct { const Context = struct {
@@ -175,7 +170,9 @@ const Context = struct {
if (!isVisibleElement(el)) return; if (!isVisibleElement(el)) return;
// Ensure block elements start on a new line // --- Opening Tag Logic ---
// Ensure block elements start on a new line (double newline for paragraphs etc)
if (tag.isBlock() and !self.state.in_table) { if (tag.isBlock() and !self.state.in_table) {
try self.ensureNewline(); try self.ensureNewline();
if (shouldAddSpacing(tag)) { if (shouldAddSpacing(tag)) {
@@ -185,6 +182,7 @@ const Context = struct {
try self.ensureNewline(); try self.ensureNewline();
} }
// Prefixes
switch (tag) { switch (tag) {
.h1 => try self.writer.writeAll("# "), .h1 => try self.writer.writeAll("# "),
.h2 => try self.writer.writeAll("## "), .h2 => try self.writer.writeAll("## "),
@@ -227,6 +225,7 @@ const Context = struct {
try self.writer.writeByte('|'); try self.writer.writeByte('|');
}, },
.td, .th => { .td, .th => {
// Note: leading pipe handled by previous cell closing or tr opening
self.state.last_char_was_newline = false; self.state.last_char_was_newline = false;
try self.writer.writeByte(' '); try self.writer.writeByte(' ');
}, },
@@ -242,6 +241,7 @@ const Context = struct {
.code => { .code => {
if (self.state.pre_node == null) { if (self.state.pre_node == null) {
try self.writer.writeByte('`'); try self.writer.writeByte('`');
self.state.in_code = true;
self.state.last_char_was_newline = false; self.state.last_char_was_newline = false;
} }
}, },
@@ -286,15 +286,16 @@ const Context = struct {
return; return;
}, },
.anchor => { .anchor => {
const info = analyzeContent(el.asNode()); const has_content = hasVisibleContent(el.asNode());
const label = getAnchorLabel(el); const label = getAnchorLabel(el);
const href_raw = el.getAttributeSafe(comptime .wrap("href")); const href_raw = el.getAttributeSafe(comptime .wrap("href"));
if (!info.has_visible and label == null and href_raw == null) return; if (!has_content and label == null and href_raw == null) return;
const has_block = hasBlockDescendant(el.asNode());
const href = if (href_raw) |h| URL.resolve(self.page.call_arena, self.page.base(), h, .{ .encode = true }) catch h else null; const href = if (href_raw) |h| URL.resolve(self.page.call_arena, self.page.base(), h, .{ .encode = true }) catch h else null;
if (info.has_block) { if (has_block) {
try self.renderChildren(el.asNode()); try self.renderChildren(el.asNode());
if (href) |h| { if (href) |h| {
if (!self.state.last_char_was_newline) try self.writer.writeByte('\n'); if (!self.state.last_char_was_newline) try self.writer.writeByte('\n');
@@ -306,12 +307,25 @@ const Context = struct {
return; return;
} }
const standalone = isStandaloneAnchor(el); if (isStandaloneAnchor(el)) {
if (standalone) {
if (!self.state.last_char_was_newline) try self.writer.writeByte('\n'); if (!self.state.last_char_was_newline) try self.writer.writeByte('\n');
try self.writer.writeByte('[');
if (has_content) {
try self.renderChildren(el.asNode());
} else {
try self.writer.writeAll(label orelse "");
}
try self.writer.writeAll("](");
if (href) |h| {
try self.writer.writeAll(h);
}
try self.writer.writeAll(")\n");
self.state.last_char_was_newline = true;
return;
} }
try self.writer.writeByte('['); try self.writer.writeByte('[');
if (info.has_visible) { if (has_content) {
try self.renderChildren(el.asNode()); try self.renderChildren(el.asNode());
} else { } else {
try self.writer.writeAll(label orelse ""); try self.writer.writeAll(label orelse "");
@@ -321,12 +335,7 @@ const Context = struct {
try self.writer.writeAll(h); try self.writer.writeAll(h);
} }
try self.writer.writeByte(')'); try self.writer.writeByte(')');
if (standalone) { self.state.last_char_was_newline = false;
try self.writer.writeByte('\n');
self.state.last_char_was_newline = true;
} else {
self.state.last_char_was_newline = false;
}
return; return;
}, },
.input => { .input => {
@@ -341,8 +350,12 @@ const Context = struct {
else => {}, else => {},
} }
// --- Render Children ---
try self.renderChildren(el.asNode()); try self.renderChildren(el.asNode());
// --- Closing Tag Logic ---
// Suffixes
switch (tag) { switch (tag) {
.pre => { .pre => {
if (!self.state.last_char_was_newline) { if (!self.state.last_char_was_newline) {
@@ -355,6 +368,7 @@ const Context = struct {
.code => { .code => {
if (self.state.pre_node == null) { if (self.state.pre_node == null) {
try self.writer.writeByte('`'); try self.writer.writeByte('`');
self.state.in_code = false;
self.state.last_char_was_newline = false; self.state.last_char_was_newline = false;
} }
}, },
@@ -397,6 +411,7 @@ const Context = struct {
else => {}, else => {},
} }
// Post-block newlines
if (tag.isBlock() and !self.state.in_table) { if (tag.isBlock() and !self.state.in_table) {
try self.ensureNewline(); try self.ensureNewline();
} }
@@ -439,19 +454,15 @@ const Context = struct {
} }
fn escape(self: *Context, text: []const u8) !void { fn escape(self: *Context, text: []const u8) !void {
var start: usize = 0; for (text) |c| {
for (text, 0..) |c, i| {
switch (c) { switch (c) {
'\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '#', '+', '-', '!', '|' => { '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '#', '+', '-', '!', '|' => {
if (i > start) try self.writer.writeAll(text[start..i]);
try self.writer.writeByte('\\'); try self.writer.writeByte('\\');
try self.writer.writeByte(c); try self.writer.writeByte(c);
start = i + 1;
}, },
else => {}, else => try self.writer.writeByte(c),
} }
} }
if (start < text.len) try self.writer.writeAll(text[start..]);
} }
}; };