lib/zen/src/frontmatter/scan.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const model = @import("model.zig");
3
4 pub const Sections = struct {
5 metadata_start: usize,
6 metadata_end: usize,
7 body_start: usize,
8 };
9
10 pub const Field = union(enum) {
11 pair: struct {
12 key: []const u8,
13 value: []const u8,
14 },
15 continuation: []const u8,
16 };
17
18 pub const Iterator = struct {
19 lines: std.mem.SplitIterator(u8, .scalar),
20
21 pub fn init(bytes: []const u8) Iterator {
22 return .{ .lines = std.mem.splitScalar(u8, bytes, '\n') };
23 }
24
25 pub fn next(self: *Iterator) ?Field {
26 while (self.lines.next()) |raw| {
27 const continuation = raw.len != 0 and (raw[0] == ' ' or raw[0] == '\t');
28 if (continuation) {
29 const value = std.mem.trim(u8, raw, " \t\r");
30 if (value.len != 0) return .{ .continuation = value };
31 continue;
32 }
33 const line = std.mem.trim(u8, raw, " \t\r");
34 if (line.len == 0) continue;
35 const colon = std.mem.indexOfScalar(u8, line, ':') orelse continue;
36 const key = std.mem.trim(u8, line[0..colon], " \t");
37 if (key.len == 0) continue;
38 return .{ .pair = .{
39 .key = key,
40 .value = stripQuotes(std.mem.trim(u8, line[colon + 1 ..], " \t")),
41 } };
42 }
43 return null;
44 }
45 };
46
47 pub fn sections(markdown: []const u8) model.ParseError!Sections {
48 const first_end = std.mem.indexOfScalar(u8, markdown, '\n') orelse return .{
49 .metadata_start = 0,
50 .metadata_end = 0,
51 .body_start = 0,
52 };
53 const first = std.mem.trim(u8, markdown[0..first_end], "\r");
54 if (!std.mem.eql(u8, first, "---")) return .{
55 .metadata_start = 0,
56 .metadata_end = 0,
57 .body_start = 0,
58 };
59
60 var cursor = first_end + 1;
61 while (cursor <= markdown.len) {
62 const start = cursor;
63 const end = std.mem.indexOfScalarPos(u8, markdown, start, '\n') orelse markdown.len;
64 const line = std.mem.trim(u8, std.mem.trim(u8, markdown[start..end], "\r"), " \t");
65 if (std.mem.eql(u8, line, "---")) return .{
66 .metadata_start = first_end + 1,
67 .metadata_end = start,
68 .body_start = if (end < markdown.len) end + 1 else end,
69 };
70 if (end == markdown.len) break;
71 cursor = end + 1;
72 }
73 return error.UnclosedFrontmatter;
74 }
75
76 fn stripQuotes(value: []const u8) []const u8 {
77 if (value.len >= 2 and value[0] == '"' and value[value.len - 1] == '"') {
78 return value[1 .. value.len - 1];
79 }
80 if (value.len >= 2 and value[0] == '\'' and value[value.len - 1] == '\'') {
81 return value[1 .. value.len - 1];
82 }
83 return value;
84 }