~subsetpark/erasmus

ref: a1d5f13fc784f9ef77a75be1541a139d32e87bd2 erasmus/src/main.zig -rw-r--r-- 10.2 KiB
a1d5f13f — Zach Smith Use catch directly when handling errors 4 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
const std = @import("std");
const fs = std.fs;
const mem = std.mem;
const fmt = std.fmt;

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 Brackets = struct {
    brackets: std.ArrayList([]const u8),
    error_slice: ?[]const u8,
};

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

fn nextLine(reader: anytype, buffer: []u8) !?[]const u8 {
    var line = (try reader.readUntilDelimiterOrEof(
        buffer,
        '\n',
    )) orelse return null;
    // trim annoying windows-only carriage return character
    if (@import("builtin").os.tag == .windows) {
        return mem.trimRight(u8, line, "\r");
    } else {
        return line;
    }
}

// Read a note from disk, filtering out any lines
// inserted by the indexing process.
fn readBody(entry_name: []const u8, contents: *std.ArrayList(u8)) !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 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) !void {
    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.
                    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;
        }
    }
}

// Create a reference line and add it to a note.
fn appendRef(note_body: *std.ArrayList(u8), referent: []const u8, allocator: *mem.Allocator) !void {
    const ref = try fmt.allocPrint(allocator, "{s}:{s}", .{
        ref_prefix,
        referent,
    });
    try note_body.appendSlice(ref);
    try note_body.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);
            },
        }
    }
}

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 ".".
    \\
;

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.StringHashMap(std.ArrayList(u8)).init(allocator);

    // 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 contents = std.ArrayList(u8).init(allocator);
            try readBody(path, &contents);
            try notes.put(entry.name, contents);
        }
    }

    var next_generation = std.StringHashMap(std.ArrayList(u8)).init(allocator);
    defer next_generation.deinit();

    // For each note, gather backlinks and references, and generate
    // contents.
    var note_names = notes.keyIterator();
    while (note_names.next()) |note_name_ptr| {
        const note_name = note_name_ptr.*;
        const link_here = try fmt.allocPrint(allocator, "{c}{s}{c}", .{
            begin_brackets,
            note_name,
            end_brackets,
        });

        const maybe_note_body = notes.get(note_name);
        if (maybe_note_body) |note_body| {
            var new_note_body = std.ArrayList(u8).init(allocator);
            // Backlinks: if any other note body refers to this one, add a
            // reference to that body's name.
            var found_a_backlink = false;
            var all_notes = notes.iterator();
            while (all_notes.next()) |entry| {
                const items = entry.value_ptr.*.items;
                if (mem.indexOf(u8, items, link_here) != null) {
                    found_a_backlink = true;
                    try appendRef(&new_note_body, entry.key_ptr.*, allocator);
                }
            }
            // Backlinks padding.
            if (found_a_backlink) {
                try new_note_body.append('\n');
            }
            // Body: Add main note body.
            try new_note_body.appendSlice(note_body.items);
            // References: gather every reference in the body of the note.
            var brackets = Brackets{
                .brackets = std.ArrayList([]const u8).init(allocator),
                .error_slice = undefined,
            };
            defer brackets.brackets.deinit();
            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 (brackets.brackets.items.len > 0) {
                try new_note_body.append('\n');
            }
            // References: escape and add forward references to note.
            for (brackets.brackets.items) |phrase| {
                var escaped = EscapedString{
                    .escaped = try std.ArrayList(u8).initCapacity(allocator, phrase.len * 2),
                    .error_slice = undefined,
                };
                defer escaped.escaped.deinit();

                escape(phrase, &escaped) catch |err| {
                    std.debug.print("Found illegal character in brackets: `{s}`\n", .{
                        escaped.error_slice,
                    });
                    std.os.exit(1);
                };
                try appendRef(&new_note_body, escaped.escaped.items, allocator);
            }
            // Optional debugging.
            if (debug_mode) {
                std.debug.print("{s}:\n\n{s}\n", .{
                    note_name,
                    new_note_body.items,
                });
            }

            try next_generation.put(note_name, new_note_body);
        } else unreachable;
    }
    // Free the original note bodies.
    var all_notes = notes.iterator();
    while (all_notes.next()) |entry| {
        entry.value_ptr.deinit();
    }
    notes.deinit();
    // Write to disk.
    var notes_to_write = next_generation.iterator();
    while (notes_to_write.next()) |entry| {
        const path = try fs.path.join(allocator, &[_][]const u8{
            directory_name,
            entry.key_ptr.*,
        });
        const note_body = entry.value_ptr.*.items;
        try fs.cwd().writeFile(path, note_body);
    }
}