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 Brackets = struct {
brackets: std.ArrayList([]const u8),
error_slice: ?[]const u8,
};
const EscapedString = struct {
escaped: std.ArrayList(u8),
error_slice: ?[]const u8,
};
const CharBuffer = std.ArrayList(u8);
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_body: *CharBuffer, 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);
},
}
}
}
// Backlinks: if any other note body refers to this one, add a reference to
// that body's name.
fn appendBacklinks(
note_name: []const u8,
note_body: *CharBuffer,
notes: std.StringHashMap(CharBuffer),
allocator: *mem.Allocator,
) !void {
var found_one = false;
var all_notes = notes.iterator();
const link_here = try fmt.allocPrint(allocator, "{c}{s}{c}", .{
begin_brackets,
note_name,
end_brackets,
});
while (all_notes.next()) |entry| {
const items = entry.value_ptr.*.items;
if (mem.indexOf(u8, items, link_here) != null) {
found_one = true;
try appendRef(note_body, entry.key_ptr.*, allocator);
}
}
// Backlinks padding.
if (found_one) {
try note_body.append('\n');
}
}
// References: gather every reference in the body of the note.
fn appendReferences(
note_name: []const u8,
note_body: *CharBuffer,
notes: std.StringHashMap(CharBuffer),
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_body.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 |err| {
std.debug.print("Found illegal character in brackets: `{s}`\n", .{
escaped.error_slice,
});
std.os.exit(1);
};
try appendRef(note_body, 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.StringHashMap(CharBuffer).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 = CharBuffer.init(allocator);
try readBody(path, &contents);
try notes.put(entry.name, contents);
}
}
var next_generation = std.StringHashMap(CharBuffer).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 maybe_note_body = notes.get(note_name);
if (maybe_note_body) |note_body| {
var new_note_body = CharBuffer.init(allocator);
try appendBacklinks(note_name, &new_note_body, notes, allocator);
try new_note_body.appendSlice(note_body.items);
try appendReferences(note_name, &new_note_body, notes, 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);
}
}