lib/markdown/src/block.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 pub const BlockType = enum {
4 paragraph,
5 heading1,
6 heading2,
7 heading3,
8 heading4,
9 code_block,
10 bullet_list,
11 numbered_list,
12 blockquote,
13 horizontal_rule,
14 };
15
16 pub const Block = struct {
17 block_type: BlockType = .paragraph,
18 content: []const u8,
19 indent: u8 = 0,
20 number: ?u16 = null,
21 language: ?[]const u8 = null,
22 };
23
24 pub const Parsed = struct {
25 block_type: BlockType,
26 content: []const u8,
27 number: ?u16,
28 indent: u8,
29 };
30
31 pub fn parseType(line: []const u8) Parsed {
32 const indent = markdownIndent(line);
33 const trimmed = trimLeft(line, " \t");
34
35 if (trimmed.len == 0) {
36 return .{ .block_type = .paragraph, .content = "", .number = null, .indent = indent };
37 }
38
39 if (trimmed.len >= 3 and (std.mem.startsWith(u8, trimmed, "---") or std.mem.startsWith(u8, trimmed, "***") or std.mem.startsWith(u8, trimmed, "___"))) {
40 var all_same = true;
41 const first = trimmed[0];
42 for (trimmed) |c| {
43 if (c != first and c != ' ') {
44 all_same = false;
45 break;
46 }
47 }
48 if (all_same) {
49 return .{ .block_type = .horizontal_rule, .content = "", .number = null, .indent = indent };
50 }
51 }
52
53 if (std.mem.startsWith(u8, trimmed, "#### ")) {
54 return .{ .block_type = .heading4, .content = trimmed[5..], .number = null, .indent = indent };
55 }
56 if (std.mem.startsWith(u8, trimmed, "### ")) {
57 return .{ .block_type = .heading3, .content = trimmed[4..], .number = null, .indent = indent };
58 }
59 if (std.mem.startsWith(u8, trimmed, "## ")) {
60 return .{ .block_type = .heading2, .content = trimmed[3..], .number = null, .indent = indent };
61 }
62 if (std.mem.startsWith(u8, trimmed, "# ")) {
63 return .{ .block_type = .heading1, .content = trimmed[2..], .number = null, .indent = indent };
64 }
65
66 if (std.mem.startsWith(u8, trimmed, "> ")) {
67 return .{ .block_type = .blockquote, .content = trimmed[2..], .number = null, .indent = indent };
68 }
69 if (std.mem.startsWith(u8, trimmed, ">")) {
70 return .{ .block_type = .blockquote, .content = trimmed[1..], .number = null, .indent = indent };
71 }
72
73 if (trimmed.len >= 2 and (trimmed[0] == '-' or trimmed[0] == '*' or trimmed[0] == '+') and trimmed[1] == ' ') {
74 return .{ .block_type = .bullet_list, .content = trimmed[2..], .number = null, .indent = indent };
75 }
76
77 if (trimmed.len >= 3) {
78 var num_end: usize = 0;
79 while (num_end < trimmed.len and trimmed[num_end] >= '0' and trimmed[num_end] <= '9') {
80 num_end += 1;
81 }
82 if (num_end > 0 and num_end < trimmed.len and trimmed[num_end] == '.' and num_end + 1 < trimmed.len and trimmed[num_end + 1] == ' ') {
83 const num = std.fmt.parseInt(u16, trimmed[0..num_end], 10) catch 1;
84 return .{ .block_type = .numbered_list, .content = trimmed[num_end + 2 ..], .number = num, .indent = indent };
85 }
86 }
87
88 if (std.mem.startsWith(u8, trimmed, "```")) {
89 return .{ .block_type = .code_block, .content = trimmed[3..], .number = null, .indent = indent };
90 }
91
92 return .{ .block_type = .paragraph, .content = trimmed, .number = null, .indent = indent };
93 }
94
95 fn markdownIndent(line: []const u8) u8 {
96 var columns: usize = 0;
97 for (line) |char| {
98 switch (char) {
99 ' ' => columns += 1,
100 '\t' => columns += 4,
101 else => break,
102 }
103 }
104 return @intCast(@min(columns / 2, @as(usize, std.math.maxInt(u8))));
105 }
106
107 fn trimLeft(bytes: []const u8, values: []const u8) []const u8 {
108 var start: usize = 0;
109 while (start < bytes.len and std.mem.indexOfScalar(u8, values, bytes[start]) != null) start += 1;
110 return bytes[start..];
111 }
112
113 test "parseType heading" {
114 const h1 = parseType("# Title");
115 try std.testing.expectEqual(BlockType.heading1, h1.block_type);
116 try std.testing.expectEqualStrings("Title", h1.content);
117
118 const h2 = parseType("## Subtitle");
119 try std.testing.expectEqual(BlockType.heading2, h2.block_type);
120
121 const h3 = parseType("### Section");
122 try std.testing.expectEqual(BlockType.heading3, h3.block_type);
123 }
124
125 test "parseType bullet list" {
126 const bullet = parseType("- Item");
127 try std.testing.expectEqual(BlockType.bullet_list, bullet.block_type);
128 try std.testing.expectEqualStrings("Item", bullet.content);
129 try std.testing.expectEqual(@as(u8, 0), bullet.indent);
130
131 const star = parseType("* Another");
132 try std.testing.expectEqual(BlockType.bullet_list, star.block_type);
133
134 const nested = parseType(" - Nested");
135 try std.testing.expectEqual(BlockType.bullet_list, nested.block_type);
136 try std.testing.expectEqual(@as(u8, 1), nested.indent);
137 }
138
139 test "parseType numbered list" {
140 const num = parseType("1. First");
141 try std.testing.expectEqual(BlockType.numbered_list, num.block_type);
142 try std.testing.expectEqualStrings("First", num.content);
143 try std.testing.expectEqual(@as(?u16, 1), num.number);
144
145 const num2 = parseType("42. Item");
146 try std.testing.expectEqual(@as(?u16, 42), num2.number);
147
148 const nested = parseType("\t3. Nested");
149 try std.testing.expectEqual(@as(?u16, 3), nested.number);
150 try std.testing.expectEqual(@as(u8, 2), nested.indent);
151 }
152
153 test "parseType blockquote" {
154 const quote = parseType("> Quote text");
155 try std.testing.expectEqual(BlockType.blockquote, quote.block_type);
156 try std.testing.expectEqualStrings("Quote text", quote.content);
157 }
158
159 test "parseType horizontal rule" {
160 const hr1 = parseType("---");
161 try std.testing.expectEqual(BlockType.horizontal_rule, hr1.block_type);
162
163 const hr2 = parseType("***");
164 try std.testing.expectEqual(BlockType.horizontal_rule, hr2.block_type);
165 }
166
167 test "parseType paragraph" {
168 const para = parseType("Normal text");
169 try std.testing.expectEqual(BlockType.paragraph, para.block_type);
170 try std.testing.expectEqualStrings("Normal text", para.content);
171 }
172
173 test "BlockType values" {
174 try std.testing.expectEqual(BlockType.paragraph, BlockType.paragraph);
175 try std.testing.expectEqual(BlockType.heading1, BlockType.heading1);
176 try std.testing.expectEqual(BlockType.code_block, BlockType.code_block);
177 }