Compare commits

..

2 Commits

Author SHA1 Message Date
Karl Seguin
fec02850d4 Merge pull request #2068 from lightpanda-io/refactor/markdown-anchor-rendering
Some checks are pending
e2e-test / zig build release (push) Waiting to run
e2e-test / demo-scripts (push) Blocked by required conditions
e2e-test / wba-demo-scripts (push) Blocked by required conditions
e2e-test / wba-test (push) Blocked by required conditions
e2e-test / cdp-and-hyperfine-bench (push) Blocked by required conditions
e2e-test / perf-fmt (push) Blocked by required conditions
e2e-test / browser fetch (push) Blocked by required conditions
zig-test / zig fmt (push) Waiting to run
zig-test / zig test using v8 in debug mode (push) Waiting to run
zig-test / zig test (push) Waiting to run
zig-test / perf-fmt (push) Blocked by required conditions
markdown: simplify and optimize anchor rendering
2026-04-02 17:06:26 +08:00
Adrià Arrufat
71ac2e8c7f markdown: simplify and optimize anchor rendering 2026-04-02 09:11:26 +02:00
2 changed files with 51 additions and 62 deletions

View File

@@ -235,6 +235,10 @@ 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;
@@ -255,16 +259,12 @@ 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) {
// Even after an abort_all, we could still have transfers, but, at the std.debug.assert(self.in_use.first == null);
// very least, they should all be flagged as aborted.
var it = self.in_use.first; const running = self.handles.perform() catch |err| {
var leftover: usize = 0; lp.assert(false, "multi perform in abort", .{ .err = err });
while (it) |node| : (it = node.next) { };
const conn: *http.Connection = @fieldParentPtr("node", node); std.debug.assert(running == 0);
std.debug.assert((Transfer.fromConnection(conn) catch unreachable).aborted);
leftover += 1;
}
std.debug.assert(self.active == leftover);
} }
} }

View File

@@ -25,9 +25,7 @@ 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 };
@@ -39,7 +37,6 @@ 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,
@@ -100,27 +97,35 @@ 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"));
} }
fn hasBlockDescendant(root: *Node) bool { const ContentInfo = struct {
var tw = TreeWalker.FullExcludeSelf.Elements.init(root, .{}); has_visible: bool,
while (tw.next()) |el| { has_block: bool,
if (el.getTag().isBlock()) return true; };
}
return false;
}
fn hasVisibleContent(root: *Node) bool { fn analyzeContent(root: *Node) ContentInfo {
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)) return true; if (isSignificantText(node)) {
if (node.is(Element)) |el| { result.has_visible = true;
if (result.has_block) return result;
} else if (node.is(Element)) |el| {
if (!isVisibleElement(el)) { if (!isVisibleElement(el)) {
tw.skipChildren(); tw.skipChildren();
} else if (el.getTag() == .img) { } else {
return true; const tag = el.getTag();
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 false; return result;
} }
const Context = struct { const Context = struct {
@@ -170,9 +175,7 @@ const Context = struct {
if (!isVisibleElement(el)) return; if (!isVisibleElement(el)) return;
// --- Opening Tag Logic --- // Ensure block elements start on a new line
// 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)) {
@@ -182,7 +185,6 @@ 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("## "),
@@ -225,7 +227,6 @@ 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(' ');
}, },
@@ -241,7 +242,6 @@ 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,16 +286,15 @@ const Context = struct {
return; return;
}, },
.anchor => { .anchor => {
const has_content = hasVisibleContent(el.asNode()); const info = analyzeContent(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 (!has_content and label == null and href_raw == null) return; if (!info.has_visible 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 (has_block) { if (info.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');
@@ -307,25 +306,12 @@ const Context = struct {
return; return;
} }
if (isStandaloneAnchor(el)) { const standalone = 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 (has_content) { if (info.has_visible) {
try self.renderChildren(el.asNode()); try self.renderChildren(el.asNode());
} else { } else {
try self.writer.writeAll(label orelse ""); try self.writer.writeAll(label orelse "");
@@ -335,7 +321,12 @@ const Context = struct {
try self.writer.writeAll(h); try self.writer.writeAll(h);
} }
try self.writer.writeByte(')'); try self.writer.writeByte(')');
self.state.last_char_was_newline = false; if (standalone) {
try self.writer.writeByte('\n');
self.state.last_char_was_newline = true;
} else {
self.state.last_char_was_newline = false;
}
return; return;
}, },
.input => { .input => {
@@ -350,12 +341,8 @@ 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) {
@@ -368,7 +355,6 @@ 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;
} }
}, },
@@ -411,7 +397,6 @@ 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();
} }
@@ -454,15 +439,19 @@ const Context = struct {
} }
fn escape(self: *Context, text: []const u8) !void { fn escape(self: *Context, text: []const u8) !void {
for (text) |c| { var start: usize = 0;
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 => try self.writer.writeByte(c), else => {},
} }
} }
if (start < text.len) try self.writer.writeAll(text[start..]);
} }
}; };