~subsetpark/erasmus

ref: 3013b697be5cc29c9d2b347735bb8b9933b3eec7 erasmus/src/main.zig -rw-r--r-- 10.0 KiB
3013b697 — Zach Smith Improve pant doc. 3 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
const std = @import("std");
const fs = std.fs;
const mem = std.mem;
const fmt = std.fmt;
const util = @import("./util.zig");

pub const io_mode = .evented;
var debug_mode = false;

const ref_prefix = "%ref";
// Presumably this should be longer?
const max_line_length = 1024;

const RuntimeError = error{
    BracketInBrackets,
    IllegalCharacterInBrackets,
};

const begin_brackets = '{';
const end_brackets = '}';

const CharBuffer = std.ArrayList(u8);

const Brackets = struct {
    brackets: std.ArrayList([]const u8),
    error_slice: ?[]const u8,
};

const EscapedString = struct {
    escaped: CharBuffer,
    error_slice: ?[]const u8,
};

const Note = struct {
    name: []const u8,
    body: CharBuffer,
    new_body: CharBuffer,
    backlinks: std.ArrayList([]const u8),
    references: std.ArrayList([]const u8),

    fn append(self: *Note, c: u8) !void {
        try self.new_body.append(c);
    }

    fn appendSlice(self: *Note, slice: []u8) !void {
        try self.new_body.appendSlice(slice);
    }
};

const help_text =
    \\Usage: er [-h] [-d] [DIRECTORY_NAME]
    \\
    \\  -h                Display this help message and quit.
    \\  -d                Enable debug mode.
    \\  DIRECTORY_NAME    Specify the directory `er` runs in. Defaults to ".".
    \\
;

// Read a note from disk, filtering out any lines
// inserted by the indexing process.
fn readBody(entry_name: []const u8, contents: *CharBuffer) !void {
    const file = try fs.cwd().openFile(entry_name, .{});
    defer file.close();

    const reader = file.reader();
    var started_body = false;
    var newline_counter: usize = 0;

    while (true) {
        var buf: [max_line_length]u8 = undefined;
        var maybe_line = try util.nextLine(reader, &buf);
        if (maybe_line) |line| {
            if (line.len > ref_prefix.len and mem.eql(
                u8,
                ref_prefix,
                line[0..ref_prefix.len],
            )) {
                // Filter out a ref line.
                continue;
            }
            if (!started_body and line.len == 0) {
                // Filter out padding before body.
                continue;
            }

            started_body = true;

            if (line.len == 0) {
                // If we are in the note body and encounter an empty line, fill
                // the newline buffer.
                newline_counter += 1;
            } else {
                // If we encounter a non-empty line, we're still in the note
                // body, so be sure to flush the newline buffer.
                try contents.appendNTimes('\n', newline_counter);
                newline_counter = 0;

                try contents.appendSlice(line);
                try contents.append('\n');
            }
        } else {
            break;
        }
    }
}

// Iterate through a slice and populate an ArrayList with subslices capturing
// "{<reference>}"
fn findBracketedStrings(note_body: []const u8, brackets: *Brackets) !bool {
    var found = false;
    var i: usize = 0;
    var current_slice: ?usize = null;
    while (i < note_body.len) : (i += 1) {
        if (note_body[i] == begin_brackets) {
            if (current_slice) |slice_start| {
                // We tried to make a reference inside an existing one.
                brackets.error_slice = note_body[slice_start .. i + 1];
                return RuntimeError.BracketInBrackets;
            } else {
                // Open a new reference.
                current_slice = i;
            }
        } else if (note_body[i] == end_brackets) {
            if (current_slice) |slice_start| {
                if (i - slice_start > 1) {
                    // If we are closing an existing reference and the brackets
                    // aren't empty, push a new slice.
                    found = true;
                    try brackets.brackets.append(note_body[slice_start + 1 .. i]);
                }
                current_slice = null;
            }
        } else if (note_body[i] == '\n') {
            // References can't cross over lines.
            current_slice = null;
        }
    }
    return found;
}

// Create a reference line and add it to a note.
fn appendRef(note: *Note, referent: []const u8, allocator: mem.Allocator) !void {
    const ref = try fmt.allocPrint(allocator, "{s}:{s}", .{
        ref_prefix,
        referent,
    });
    try note.appendSlice(ref);
    try note.append('\n');
}

// Process a string and escape any delimiters for reference creation.
fn escape(s: []const u8, res: *EscapedString) RuntimeError!void {
    // Check for illegal characters. Right now the only illegal character is a
    // terminal period, which confuses vim, so this is a bit particular.
    if (s.len > 0) {
        switch (s[s.len - 1]) {
            '.' => {
                res.error_slice = s;
                return RuntimeError.IllegalCharacterInBrackets;
            },
            else => {},
        }
    }
    for (s) |char| {
        switch (char) {
            ' ' => {
                res.escaped.appendAssumeCapacity('\\');
                res.escaped.appendAssumeCapacity(char);
            },
            else => {
                res.escaped.appendAssumeCapacity(char);
            },
        }
    }
}

// Backlinks: if any other note body refers to this one, add a reference to
// that body's name.
fn appendBacklinks(
    note: *Note,
    notes: *std.ArrayList(Note),
    allocator: mem.Allocator,
) !void {
    var found_one = false;
    const link_here = try fmt.allocPrint(allocator, "{c}{s}{c}", .{
        begin_brackets,
        note.name,
        end_brackets,
    });

    for (notes.items) |other_note| {
        const items = other_note.body.items;
        if (mem.indexOf(u8, items, link_here) != null) {
            found_one = true;

            var escaped = EscapedString{
                .escaped = try CharBuffer.initCapacity(allocator, other_note.name.len * 2),
                .error_slice = undefined,
            };
            defer escaped.escaped.deinit();

            escape(other_note.name, &escaped) catch {
                std.debug.print("Found illegal character in brackets: `{s}`\n", .{
                    escaped.error_slice,
                });
                std.os.exit(1);
            };
            try appendRef(note, escaped.escaped.items, allocator);
        }
    }

    // Backlinks padding.
    if (found_one) {
        try note.append('\n');
    }
}

// References: gather every reference in the body of the note.
fn appendReferences(
    note: *Note,
    allocator: mem.Allocator,
) !void {
    var brackets = Brackets{
        .brackets = std.ArrayList([]const u8).init(allocator),
        .error_slice = undefined,
    };
    defer brackets.brackets.deinit();

    const found_one = findBracketedStrings(note.body.items, &brackets) catch |err| switch (err) {
        RuntimeError.BracketInBrackets => {
            std.debug.print("Found improperly nested brackets: {s}.\n", .{
                brackets.error_slice,
            });
            std.os.exit(1);
        },
        else => {
            return err;
        },
    };
    // References padding.
    if (found_one) {
        try note.append('\n');
    }
    // Escape and add forward references to note.
    for (brackets.brackets.items) |phrase| {
        var escaped = EscapedString{
            .escaped = try CharBuffer.initCapacity(allocator, phrase.len * 2),
            .error_slice = undefined,
        };
        defer escaped.escaped.deinit();

        escape(phrase, &escaped) catch {
            std.debug.print("Found illegal character in brackets: `{s}`\n", .{
                escaped.error_slice,
            });
            std.os.exit(1);
        };
        try appendRef(note, escaped.escaped.items, allocator);
    }
}

pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const allocator = arena.allocator();

    // Process CLI args.
    var maybe_directory_name: ?[]const u8 = null;
    var args = std.process.args();
    // Skip the program name.
    _ = args.skip();
    while (args.next(allocator)) |arg| {
        const inner_arg = try arg;
        if (mem.eql(u8, inner_arg, "-d")) {
            debug_mode = true;
        } else if (mem.eql(u8, inner_arg, "-h")) {
            std.debug.print(help_text, .{});
            std.os.exit(0);
        } else if (maybe_directory_name == null) {
            maybe_directory_name = inner_arg;
        }
    }
    const directory_name = maybe_directory_name orelse ".";

    var notes = std.ArrayList(Note).init(allocator);
    defer notes.deinit();

    // Load note bodies into memory.
    const dir = try fs.cwd().openDir(directory_name, .{ .iterate = true });
    var iter = dir.iterate();
    while (try iter.next()) |entry| {
        if (entry.kind == .File) {
            const path = try fs.path.join(allocator, &[_][]const u8{
                directory_name,
                entry.name,
            });
            var note = Note{
                .name = entry.name,
                .body = CharBuffer.init(allocator),
                .new_body = CharBuffer.init(allocator),
                .backlinks = std.ArrayList([]const u8).init(allocator),
                .references = std.ArrayList([]const u8).init(allocator),
            };
            try readBody(path, &note.body);
            try notes.append(note);
        }
    }

    // For each note, gather backlinks and references, and generate
    // contents.
    for (notes.items) |*note| {
        try appendBacklinks(note, &notes, allocator);
        try note.appendSlice(note.body.items);
        try appendReferences(note, allocator);

        // Optional debugging.
        if (debug_mode) {
            std.debug.print("{s}:\n\n{s}\n", .{
                note.name,
                note.new_body.items,
            });
        }
    }

    // Free the original note bodies.
    for (notes.items) |note| {
        note.body.deinit();
    }

    // Write to disk.
    for (notes.items) |note| {
        const path = try fs.path.join(allocator, &[_][]const u8{
            directory_name,
            note.name,
        });
        try fs.cwd().writeFile(path, note.new_body.items);
    }
}