lib/zen/src/footnote/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_definitions: usize,
7 max_joined_text_bytes: usize,
8 };
9
10 pub const default_limits: Limits = .{
11 .max_definitions = 512,
12 .max_joined_text_bytes = 64 * 1024,
13 };
14
15 pub const Plan = struct {
16 source_bytes: usize,
17 definitions: usize,
18 joined_text_bytes: usize,
19
20 pub fn inspect(source: []const u8, limits: Limits) model.Error!Plan {
21 var state = State{};
22 var iterator = scan.Iterator.init(source);
23 while (iterator.next()) |event| switch (event) {
24 .definition => |definition| {
25 state.definitions = try added(state.definitions, 1);
26 if (state.definitions > limits.max_definitions) {
27 return error.FootnoteDefinitionCapacityExceeded;
28 }
29 state.current_text_bytes = definition.text.len;
30 state.current_joined = false;
31 },
32 .continuation => |text| try state.join(text, limits.max_joined_text_bytes),
33 };
34 std.debug.assert(state.definitions <= limits.max_definitions);
35 std.debug.assert(state.joined_text_bytes <= limits.max_joined_text_bytes);
36 return .{
37 .source_bytes = source.len,
38 .definitions = state.definitions,
39 .joined_text_bytes = state.joined_text_bytes,
40 };
41 }
42
43 pub fn exactLimits(self: Plan) Limits {
44 return .{
45 .max_definitions = self.definitions,
46 .max_joined_text_bytes = self.joined_text_bytes,
47 };
48 }
49 };
50
51 const State = struct {
52 definitions: usize = 0,
53 joined_text_bytes: usize = 0,
54 current_text_bytes: usize = 0,
55 current_joined: bool = false,
56
57 fn join(self: *State, text: []const u8, max_joined_text_bytes: usize) model.Error!void {
58 var joined_text_bytes = self.joined_text_bytes;
59 if (!self.current_joined) {
60 joined_text_bytes = try added(joined_text_bytes, self.current_text_bytes);
61 if (self.current_text_bytes != 0) joined_text_bytes = try added(joined_text_bytes, 1);
62 } else {
63 joined_text_bytes = try added(joined_text_bytes, 1);
64 }
65 joined_text_bytes = try added(joined_text_bytes, text.len);
66 if (joined_text_bytes > max_joined_text_bytes) {
67 return error.FootnoteJoinedTextByteCapacityExceeded;
68 }
69 self.joined_text_bytes = joined_text_bytes;
70 self.current_text_bytes = if (self.current_text_bytes == 0)
71 text.len
72 else
73 try added(try added(self.current_text_bytes, 1), text.len);
74 self.current_joined = true;
75 }
76 };
77
78 fn added(left: usize, right: usize) error{CapacityOverflow}!usize {
79 return std.math.add(usize, left, right) catch error.CapacityOverflow;
80 }
81
82 test "footnote plan counts only text requiring a join" {
83 const source =
84 "[^paper]: A [paper](/paper)\n" ++
85 " with `code`.\n" ++
86 "[^block]:\n" ++
87 " Block style.\n" ++
88 "[^unused]: Hidden.\n";
89 const plan = try Plan.inspect(source, default_limits);
90 try std.testing.expectEqual(@as(usize, 3), plan.definitions);
91 try std.testing.expectEqual(@as(usize, 42), plan.joined_text_bytes);
92 try std.testing.expectEqual(Limits{
93 .max_definitions = 3,
94 .max_joined_text_bytes = 42,
95 }, plan.exactLimits());
96 }
97
98 test "footnote plan borrows every uncontinued definition" {
99 const plan = try Plan.inspect("[^one]: First\n[^two]: Second\n", default_limits);
100 try std.testing.expectEqual(@as(usize, 2), plan.definitions);
101 try std.testing.expectEqual(@as(usize, 0), plan.joined_text_bytes);
102 }