lib/zen/src/frontmatter/plan.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const model = @import("model.zig");
3 const scan = @import("scan.zig");
4
5 pub const Limits = struct {
6 max_pairs: usize,
7 max_continuation_bytes: usize,
8 };
9
10 pub const MetadataProjection = struct {
11 title_bytes: ?usize = null,
12 date_bytes: ?usize = null,
13 type_bytes: ?usize = null,
14 draft: bool = false,
15 curated: bool = false,
16 };
17
18 pub const Plan = struct {
19 input_bytes: usize,
20 metadata_start: usize,
21 metadata_end: usize,
22 body_start: usize,
23 pairs: usize,
24 continuation_bytes: usize,
25 projection: MetadataProjection,
26
27 pub fn inspect(markdown: []const u8, limits: Limits) model.Error!Plan {
28 const split = try scan.sections(markdown);
29 std.debug.assert(split.metadata_start <= split.metadata_end);
30 std.debug.assert(split.metadata_end <= split.body_start);
31 std.debug.assert(split.body_start <= markdown.len);
32 var iterator = scan.Iterator.init(markdown[split.metadata_start..split.metadata_end]);
33 var pair_count: usize = 0;
34 var finalized_bytes: usize = 0;
35 var current: ?Current = null;
36 var projection: Projection = .{};
37
38 while (iterator.next()) |field| switch (field) {
39 .pair => |pair| {
40 finalized_bytes = try finalize(finalized_bytes, current);
41 projection.finish(current);
42 pair_count = try added(pair_count, 1);
43 if (pair_count > limits.max_pairs) return error.PairCapacityExceeded;
44 current = Current.init(pair.key, pair.value);
45 },
46 .continuation => |value| if (current) |*active| {
47 try active.continueWith(value);
48 const admitted = try added(finalized_bytes, active.joined_bytes);
49 if (admitted > limits.max_continuation_bytes) {
50 return error.ContinuationByteCapacityExceeded;
51 }
52 },
53 };
54 finalized_bytes = try finalize(finalized_bytes, current);
55 projection.finish(current);
56 std.debug.assert(pair_count <= limits.max_pairs);
57 std.debug.assert(finalized_bytes <= limits.max_continuation_bytes);
58 const result = Plan{
59 .input_bytes = markdown.len,
60 .metadata_start = split.metadata_start,
61 .metadata_end = split.metadata_end,
62 .body_start = split.body_start,
63 .pairs = pair_count,
64 .continuation_bytes = finalized_bytes,
65 .projection = projection.value,
66 };
67 std.debug.assert(result.input_bytes == markdown.len);
68 std.debug.assert(result.pairs == pair_count);
69 std.debug.assert(result.continuation_bytes == finalized_bytes);
70 return result;
71 }
72
73 pub fn exactLimits(self: Plan) Limits {
74 const limits = Limits{
75 .max_pairs = self.pairs,
76 .max_continuation_bytes = self.continuation_bytes,
77 };
78 std.debug.assert(limits.max_pairs == self.pairs);
79 std.debug.assert(limits.max_continuation_bytes == self.continuation_bytes);
80 return limits;
81 }
82 };
83
84 const Current = struct {
85 key: []const u8,
86 initial_bytes: usize,
87 joined_bytes: usize = 0,
88 continued: bool = false,
89 truth: Truth,
90
91 fn init(key: []const u8, value: []const u8) Current {
92 std.debug.assert(key.len != 0);
93 var truth: Truth = .{};
94 truth.append(value);
95 const current = Current{ .key = key, .initial_bytes = value.len, .truth = truth };
96 std.debug.assert(current.initial_bytes == value.len);
97 std.debug.assert(!current.continued);
98 return current;
99 }
100
101 fn continueWith(self: *Current, value: []const u8) error{CapacityOverflow}!void {
102 std.debug.assert(value.len != 0);
103 const before = self.valueBytes();
104 if (!self.continued) {
105 self.joined_bytes = self.initial_bytes;
106 if (self.initial_bytes != 0) {
107 self.joined_bytes = try added(self.joined_bytes, 1);
108 self.truth.append(" ");
109 }
110 self.continued = true;
111 } else {
112 self.joined_bytes = try added(self.joined_bytes, 1);
113 self.truth.append(" ");
114 }
115 self.joined_bytes = try added(self.joined_bytes, value.len);
116 self.truth.append(value);
117 std.debug.assert(self.continued);
118 std.debug.assert(self.joined_bytes >= before);
119 std.debug.assert(self.joined_bytes >= value.len);
120 }
121
122 fn valueBytes(self: Current) usize {
123 const bytes = if (self.continued) self.joined_bytes else self.initial_bytes;
124 if (self.continued) std.debug.assert(bytes >= self.initial_bytes);
125 return bytes;
126 }
127 };
128
129 const Projection = struct {
130 value: MetadataProjection = .{},
131 title_seen: bool = false,
132 date_seen: bool = false,
133 type_seen: bool = false,
134 draft_seen: bool = false,
135 curated_seen: bool = false,
136
137 fn finish(self: *Projection, maybe_current: ?Current) void {
138 const current = maybe_current orelse return;
139 std.debug.assert(current.key.len != 0);
140 if (std.mem.eql(u8, current.key, "title") and !self.title_seen) {
141 self.title_seen = true;
142 self.value.title_bytes = current.valueBytes();
143 } else if (std.mem.eql(u8, current.key, "date") and !self.date_seen) {
144 self.date_seen = true;
145 self.value.date_bytes = current.valueBytes();
146 } else if (std.mem.eql(u8, current.key, "type") and !self.type_seen) {
147 self.type_seen = true;
148 self.value.type_bytes = current.valueBytes();
149 } else if (std.mem.eql(u8, current.key, "draft") and !self.draft_seen) {
150 self.draft_seen = true;
151 self.value.draft = current.truth.isTrue();
152 } else if (std.mem.eql(u8, current.key, "curated") and !self.curated_seen) {
153 self.curated_seen = true;
154 self.value.curated = current.truth.isTrue();
155 }
156 }
157 };
158
159 const Truth = struct {
160 bytes: [4]u8 = undefined,
161 len: usize = 0,
162 possible: bool = true,
163
164 fn append(self: *Truth, value: []const u8) void {
165 if (!self.possible) return;
166 std.debug.assert(self.len <= self.bytes.len);
167 if (value.len > self.bytes.len - self.len) {
168 self.possible = false;
169 return;
170 }
171 @memcpy(self.bytes[self.len..][0..value.len], value);
172 self.len += value.len;
173 std.debug.assert(self.len <= self.bytes.len);
174 }
175
176 fn isTrue(self: Truth) bool {
177 return self.possible and self.len == 4 and std.mem.eql(u8, self.bytes[0..4], "true");
178 }
179 };
180
181 fn finalize(total: usize, current: ?Current) error{CapacityOverflow}!usize {
182 const active = current orelse return total;
183 if (!active.continued) return total;
184 return added(total, active.joined_bytes);
185 }
186
187 fn added(left: usize, right: usize) error{CapacityOverflow}!usize {
188 return std.math.add(usize, left, right) catch error.CapacityOverflow;
189 }