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 }