lib/zen/src/footnote/scan.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 pub const DefinitionLine = struct {
4 key: []const u8,
5 text: []const u8,
6 };
7
8 pub const Event = union(enum) {
9 definition: DefinitionLine,
10 continuation: []const u8,
11 };
12
13 pub const Iterator = struct {
14 source: []const u8,
15 cursor: usize = 0,
16 fence: ?Fence = null,
17 current: bool = false,
18
19 pub fn init(source: []const u8) Iterator {
20 return .{ .source = source };
21 }
22
23 pub fn next(self: *Iterator) ?Event {
24 while (nextLine(self.source, &self.cursor)) |raw_line| {
25 const line = std.mem.trim(u8, raw_line, "\r");
26 if (self.fence) |open| {
27 if (closingFence(line, open)) self.fence = null;
28 continue;
29 }
30 if (parseFence(line)) |open| {
31 self.fence = open;
32 self.current = false;
33 continue;
34 }
35 if (std.mem.trim(u8, line, " \t").len == 0) {
36 self.current = false;
37 continue;
38 }
39 if (definition(line)) |parsed| {
40 self.current = true;
41 return .{ .definition = parsed };
42 }
43 if (self.current) {
44 if (continuation(line)) |text| return .{ .continuation = text };
45 }
46 self.current = false;
47 }
48 return null;
49 }
50 };
51
52 pub fn definition(line: []const u8) ?DefinitionLine {
53 const trimmed = trimLeft(line);
54 if (trimmed.len < 5 or trimmed[0] != '[' or trimmed[1] != '^') return null;
55 const key_end = std.mem.indexOfScalarPos(u8, trimmed, 2, ']') orelse return null;
56 if (key_end + 1 >= trimmed.len or trimmed[key_end + 1] != ':') return null;
57 const key = std.mem.trim(u8, trimmed[2..key_end], " \t");
58 if (key.len == 0) return null;
59 return .{
60 .key = key,
61 .text = std.mem.trim(u8, trimmed[key_end + 2 ..], " \t"),
62 };
63 }
64
65 pub fn continuation(line: []const u8) ?[]const u8 {
66 var index: usize = 0;
67 while (index < line.len and (line[index] == ' ' or line[index] == '\t')) : (index += 1) {}
68 if (index == 0) return null;
69 if (line[0] != '\t' and index < 2) return null;
70 const text = std.mem.trim(u8, line[index..], " \t");
71 if (text.len == 0) return null;
72 return text;
73 }
74
75 const Fence = struct {
76 marker: u8,
77 count: usize,
78 };
79
80 fn nextLine(source: []const u8, cursor: *usize) ?[]const u8 {
81 if (cursor.* >= source.len) return null;
82 const start = cursor.*;
83 if (std.mem.indexOfScalarPos(u8, source, start, '\n')) |end| {
84 cursor.* = end + 1;
85 return source[start..end];
86 }
87 cursor.* = source.len;
88 return source[start..];
89 }
90
91 fn parseFence(line: []const u8) ?Fence {
92 const trimmed = trimLeft(line);
93 if (trimmed.len < 3) return null;
94 const marker = trimmed[0];
95 if (marker != '`' and marker != '~') return null;
96 var count: usize = 0;
97 while (count < trimmed.len and trimmed[count] == marker) : (count += 1) {}
98 if (count < 3) return null;
99 return .{ .marker = marker, .count = count };
100 }
101
102 fn closingFence(line: []const u8, fence: Fence) bool {
103 const trimmed = std.mem.trim(u8, line, " \t");
104 var count: usize = 0;
105 while (count < trimmed.len and trimmed[count] == fence.marker) : (count += 1) {}
106 return count >= fence.count and std.mem.trim(u8, trimmed[count..], " \t").len == 0;
107 }
108
109 fn trimLeft(line: []const u8) []const u8 {
110 var index: usize = 0;
111 while (index < line.len and (line[index] == ' ' or line[index] == '\t')) : (index += 1) {}
112 return line[index..];
113 }
114
115 test "footnote scanner excludes fences and stops continuations" {
116 const source =
117 "[^one]: first\n" ++
118 " continued\n" ++
119 "paragraph\n" ++
120 " not continued\n" ++
121 "```markdown\n" ++
122 "[^hidden]: ignored\n" ++
123 "```\n" ++
124 "[^two]: second\n";
125 var iterator = Iterator.init(source);
126 const one = iterator.next().?.definition;
127 try std.testing.expectEqualStrings("one", one.key);
128 try std.testing.expectEqualStrings("first", one.text);
129 try std.testing.expectEqualStrings("continued", iterator.next().?.continuation);
130 const two = iterator.next().?.definition;
131 try std.testing.expectEqualStrings("two", two.key);
132 try std.testing.expect(iterator.next() == null);
133 }