From 0efda28ab5edb9ab90f648410c5867140d2868f3 Mon Sep 17 00:00:00 2001 From: t Date: Tue, 16 Jun 2026 11:34:47 -0600 Subject: preserve indentation of sub-lists, reset formatting for the component after closing formatting --- src/markdown.zig | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 76 insertions(+), 7 deletions(-) (limited to 'src/markdown.zig') 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,8 +284,19 @@ 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); @@ -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]); +} -- cgit v1.3