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 }