summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/markdown.zig83
-rw-r--r--src/tui_components.zig35
2 files changed, 110 insertions, 8 deletions
diff --git a/src/markdown.zig b/src/markdown.zig
index a029756..fad1676 100644
--- a/src/markdown.zig
+++ b/src/markdown.zig
@@ -136,6 +136,11 @@ pub const Renderer = struct {
buf: std.ArrayList(u8) = .empty,
in_code_block: bool = false,
list_depth: usize = 0,
+ list_item_depth: usize = 0,
+ list_item_first_paragraph: bool = false,
+ list_item_depth_stack: [64]usize = [_]usize{0} ** 64,
+ list_item_first_stack: [64]bool = [_]bool{false} ** 64,
+ list_item_stack_len: usize = 0,
heading_level: usize = 0,
err: ?anyerror = null,
@@ -180,11 +185,44 @@ pub const Renderer = struct {
fn flushParagraph(self: *Renderer) !void {
const text = std.mem.trim(u8, self.buf.items, " \t\n");
if (text.len == 0) { self.buf.clearRetainingCapacity(); return; }
- var wrapped: std.ArrayList(u8) = .empty;
- defer wrapped.deinit(self.alloc);
- try wrapStyled(text, self.width, &wrapped, self.alloc);
- var it = std.mem.splitScalar(u8, wrapped.items, '\n');
- while (it.next()) |line| try self.appendLine(line);
+
+ if (self.list_item_depth > 0) {
+ var first_prefix: std.ArrayList(u8) = .empty;
+ defer first_prefix.deinit(self.alloc);
+ var cont_prefix: std.ArrayList(u8) = .empty;
+ defer cont_prefix.deinit(self.alloc);
+
+ const indent = (self.list_item_depth - 1) * 2;
+ try first_prefix.appendNTimes(self.alloc, ' ', indent);
+ try cont_prefix.appendNTimes(self.alloc, ' ', indent + 2);
+ if (self.list_item_first_paragraph) {
+ try first_prefix.appendSlice(self.alloc, "• ");
+ } else {
+ try first_prefix.appendNTimes(self.alloc, ' ', 2);
+ }
+
+ var wrapped: std.ArrayList(u8) = .empty;
+ defer wrapped.deinit(self.alloc);
+ const wrap_width = if (self.width > visibleWidth(first_prefix.items)) self.width - visibleWidth(first_prefix.items) else 1;
+ try wrapStyled(text, wrap_width, &wrapped, self.alloc);
+ var it = std.mem.splitScalar(u8, wrapped.items, '\n');
+ var first = true;
+ while (it.next()) |line| {
+ var tmp: std.ArrayList(u8) = .empty;
+ defer tmp.deinit(self.alloc);
+ try tmp.appendSlice(self.alloc, if (first) first_prefix.items else cont_prefix.items);
+ try tmp.appendSlice(self.alloc, line);
+ try self.appendLine(tmp.items);
+ first = false;
+ }
+ self.list_item_first_paragraph = false;
+ } else {
+ var wrapped: std.ArrayList(u8) = .empty;
+ defer wrapped.deinit(self.alloc);
+ try wrapStyled(text, self.width, &wrapped, self.alloc);
+ var it = std.mem.splitScalar(u8, wrapped.items, '\n');
+ while (it.next()) |line| try self.appendLine(line);
+ }
self.buf.clearRetainingCapacity();
}
@@ -222,7 +260,13 @@ pub const Renderer = struct {
c.MD_BLOCK_UL, c.MD_BLOCK_OL => self.list_depth += 1,
c.MD_BLOCK_LI => {
self.flushParagraph() catch |e| return self.fail(e);
- self.add("• ") catch |e| return self.fail(e);
+ if (self.list_item_stack_len < self.list_item_depth_stack.len) {
+ self.list_item_depth_stack[self.list_item_stack_len] = self.list_item_depth;
+ self.list_item_first_stack[self.list_item_stack_len] = self.list_item_first_paragraph;
+ self.list_item_stack_len += 1;
+ }
+ self.list_item_depth = self.list_depth;
+ self.list_item_first_paragraph = true;
},
c.MD_BLOCK_HR => self.appendLine("────────") catch |e| return self.fail(e),
else => {},
@@ -240,9 +284,20 @@ pub const Renderer = struct {
self.appendBlank() catch |e| return self.fail(e);
self.heading_level = 0;
},
- c.MD_BLOCK_P, c.MD_BLOCK_LI => {
+ c.MD_BLOCK_P => {
self.flushParagraph() catch |e| return self.fail(e);
},
+ c.MD_BLOCK_LI => {
+ self.flushParagraph() catch |e| return self.fail(e);
+ if (self.list_item_stack_len > 0) {
+ self.list_item_stack_len -= 1;
+ self.list_item_depth = self.list_item_depth_stack[self.list_item_stack_len];
+ self.list_item_first_paragraph = self.list_item_first_stack[self.list_item_stack_len];
+ } else {
+ self.list_item_depth = 0;
+ self.list_item_first_paragraph = false;
+ }
+ },
c.MD_BLOCK_CODE => {
self.flushCodeBlock() catch |e| return self.fail(e);
self.appendBlank() catch |e| return self.fail(e);
@@ -330,3 +385,17 @@ test "Renderer uses MD4C for common markdown" {
}
try testing.expect(found_code);
}
+
+test "Renderer preserves nested list indentation" {
+ var out: std.ArrayList([]const u8) = .empty;
+ defer {
+ for (out.items) |l| testing.allocator.free(l);
+ out.deinit(testing.allocator);
+ }
+ var r: Renderer = .{ .alloc = testing.allocator, .width = 80, .out_lines = &out };
+ try r.render("- top\n - child\n- next\n");
+ try testing.expectEqual(@as(usize, 3), out.items.len);
+ try testing.expectEqualStrings("• top", out.items[0]);
+ try testing.expectEqualStrings(" • child", out.items[1]);
+ try testing.expectEqualStrings("• next", out.items[2]);
+}
diff --git a/src/tui_components.zig b/src/tui_components.zig
index 4f74210..f85c294 100644
--- a/src/tui_components.zig
+++ b/src/tui_components.zig
@@ -156,6 +156,30 @@ pub fn displayWidthStyled(text: []const u8) usize {
return cols;
}
+fn reapplyAfterReset(alloc: std.mem.Allocator, text: []const u8, bg_open: []const u8, fg_open: []const u8) ![]const u8 {
+ const reset = theme.reset;
+ var count: usize = 0;
+ var pos: usize = 0;
+ while (std.mem.indexOfPos(u8, text, pos, reset)) |idx| {
+ count += 1;
+ pos = idx + reset.len;
+ }
+ if (count == 0) return alloc.dupe(u8, text);
+
+ const extra = count * (bg_open.len + fg_open.len);
+ var out = try std.ArrayList(u8).initCapacity(alloc, text.len + extra);
+ errdefer out.deinit(alloc);
+ pos = 0;
+ while (std.mem.indexOfPos(u8, text, pos, reset)) |idx| {
+ try out.appendSlice(alloc, text[pos .. idx + reset.len]);
+ try out.appendSlice(alloc, bg_open);
+ try out.appendSlice(alloc, fg_open);
+ pos = idx + reset.len;
+ }
+ try out.appendSlice(alloc, text[pos..]);
+ return try out.toOwnedSlice(alloc);
+}
+
/// ANSI-aware version of `truncateToCols`; never cuts inside a CSI sequence and
/// does not count escapes toward the column budget.
pub fn truncateStyledToCols(text: []const u8, max_cols: usize) []const u8 {
@@ -755,10 +779,19 @@ pub const UserText = struct {
const pad_str = try a.alloc(u8, right_pad);
defer a.free(pad_str);
@memset(pad_str, ' ');
+
+ // Markdown inline styles currently close with a full SGR reset.
+ // Inside the user-message block that reset must be immediately
+ // followed by the block's bg+fg again, otherwise the faint user
+ // background disappears for the rest of the visual line (including
+ // right padding) after `code`, *em*, **strong**, etc.
+ const inner_user = try reapplyAfterReset(a, inner, bg.open(), fg.open());
+ defer a.free(inner_user);
+
const composed = try std.fmt.allocPrint(
a,
"{s}{s}{s}{s}{s}{s}",
- .{ bg.open(), fg.open(), indent, inner, pad_str, theme.reset },
+ .{ bg.open(), fg.open(), indent, inner_user, pad_str, theme.reset },
);
try out.append(a, composed);
}