~subsetpark/erasmus

ff3012b47e72a6091b16ad3e745096c2976b8e6d — Zach Smith 2 months ago 4f48451 nested-braces
Break out note.zig and assemble all metadata before generating body
3 files changed, 296 insertions(+), 278 deletions(-)

M src/main.zig
A src/note.zig
M src/util.zig
M src/main.zig => src/main.zig +22 -278
@@ 1,273 1,16 @@
const std = @import("std");
const util = @import("./util.zig");
const Note = @import("./note.zig").Note;

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{
    IllegalCharacterInBrackets,
};

const begin_braces = '{';
const end_braces = '}';

const CharBuffer = std.ArrayList(u8);

const BufferAndBraces = struct {
    buffer: CharBuffer,
    offset: usize,
    slice_begins: std.ArrayList(usize),

    fn init(offset: usize, allocator: mem.Allocator) @This() {
        return @This(){
            .buffer = CharBuffer.init(allocator),
            .offset = offset,
            .slice_begins = std.ArrayList(usize).init(allocator),
        };
    }

    fn newBrace(self: *@This(), body_offset: usize) !void {
        try self.slice_begins.append(body_offset - self.offset);
    }

    fn currentBracesCount(self: @This()) usize {
        return self.slice_begins.items.len;
    }

    fn pop(self: *@This()) usize {
        return self.slice_begins.pop();
    }

    fn deinit(self: @This()) void {
        self.buffer.deinit();
        self.slice_begins.deinit();
    }
};

const EscapedString = struct {
    escaped: CharBuffer,
    escape_error: ?EscapeError = null,

    const EscapeError = enum { TerminalPeriod };

    fn init(to_escape: []const u8, allocator: mem.Allocator) !@This() {
        return EscapedString{
            .escaped = try CharBuffer.initCapacity(allocator, to_escape.len * 2),
        };
    }

    // Process a string and escape any delimiters for reference creation.
    fn escape(self: *@This(), s: []const u8) 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 and s[s.len - 1] == '.') {
            self.escape_error = EscapeError.TerminalPeriod;
            return;
        }
        for (s) |char| {
            switch (char) {
                ' ' => {
                    self.escaped.appendAssumeCapacity('\\');
                },
                else => {},
            }
            const lower_char = std.ascii.toLower(char);
            self.escaped.appendAssumeCapacity(lower_char);
        }
    }

    fn handleErrors(self: @This()) void {
        if (self.escape_error) |escape_error| {
            switch (escape_error) {
                EscapeError.TerminalPeriod => {
                    std.debug.print("Note references can't end with a period", .{});
                    std.os.exit(1);
                },
            }
        }
    }
};

// Iterate through a slice and populate an ArrayList with subslices capturing
// "{<reference>}"
fn findBracedStrings(
    note_body: []const u8,
    references: *std.ArrayList([]const u8),
    allocator: mem.Allocator,
) !void {
    var brace_context: ?BufferAndBraces = null;

    var i: usize = 0;
    while (i < note_body.len) : (i += 1) {
        const char = note_body[i];
        switch (char) {
            begin_braces => {
                // Use a BufferAndBraces, which can keep track of multiple open
                // braces, to handle nested braces. We'll copy the underlying
                // contents of an outer brace into a buffer while noting (but
                // not copying) any nested braces inside it.
                if (brace_context == null) {
                    brace_context = BufferAndBraces.init(i, allocator);
                }
                if (brace_context) |*ctx| {
                    try ctx.newBrace(i);
                }
            },
            end_braces => {
                if (brace_context) |*ctx| {
                    // Subtract the number of current open braces, because
                    // those won't have been included in the current buffer.
                    const begin = ctx.pop() - ctx.currentBracesCount();
                    const reference_text = ctx.buffer.items[begin..];

                    const new_string = try allocator.alloc(u8, reference_text.len);
                    std.mem.copy(u8, new_string, reference_text);

                    try references.append(new_string);

                    if (ctx.slice_begins.items.len == 0) {
                        ctx.deinit();
                        brace_context = null;
                    }
                }
            },
            '\n' => {
                // References can't cross over lines.
                if (brace_context) |ctx| {
                    ctx.deinit();
                    brace_context = null;
                }
            },
            else => {
                // While we're currently in open braces, collect the non-brace
                // strings.
                if (brace_context) |*ctx| {
                    try ctx.buffer.append(char);
                }
            },
        }
    }
}

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

    fn init(name: []const u8, allocator: mem.Allocator) @This() {
        return @This(){
            .name = name,
            .body = CharBuffer.init(allocator),
            .new_body = CharBuffer.init(allocator),
            .backlinks = std.ArrayList([]const u8).init(allocator),
            .references = std.ArrayList([]const u8).init(allocator),
        };
    }

    fn deinit(self: @This()) void {
        self.new_body.deinit();
        self.backlinks.deinit();
        self.references.deinit();
    }

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

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

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

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

        for (notes.items) |other_note| {
            const items = other_note.body.items;
            const lower_items = try std.ascii.allocLowerString(allocator, items);
            if (mem.indexOf(u8, lower_items, link_here) != null) {
                found_one = true;

                var escaped = try EscapedString.init(other_note.name, allocator);
                defer escaped.escaped.deinit();

                escaped.escape(other_note.name);
                escaped.handleErrors();

                try self.appendRef(escaped.escaped.items, allocator);
            }
            allocator.free(lower_items);
        }

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

    // References: gather every reference in the body of the note.
    fn appendReferences(
        self: *@This(),
        allocator: mem.Allocator,
    ) !void {
        var braces = std.ArrayList([]const u8).init(allocator);
        defer braces.deinit();

        try findBracedStrings(self.body.items, &braces, allocator);

        // References padding.
        if (braces.items.len > 0) {
            try self.append('\n');
        }

        var seen = std.BufSet.init(allocator);

        // Escape and add forward references to note.
        for (braces.items) |phrase| {
            var escaped = try EscapedString.init(phrase, allocator);
            defer escaped.escaped.deinit();

            escaped.escape(phrase);
            escaped.handleErrors();

            const str = escaped.escaped.items;
            if (!seen.contains(str)) {
                try seen.insert(str);
                try self.appendRef(str, allocator);
            }
        }
    }
};

const help_text =
    \\Usage: er [-h] [-d] [DIRECTORY_NAME]
    \\


@@ 279,7 22,7 @@ const help_text =

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



@@ 291,10 34,10 @@ fn readBody(entry_name: []const u8, contents: *CharBuffer) !void {
        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(
            if (line.len > util.ref_prefix.len and mem.eql(
                u8,
                ref_prefix,
                line[0..ref_prefix.len],
                util.ref_prefix,
                line[0..util.ref_prefix.len],
            )) {
                // Filter out a ref line.
                continue;


@@ 367,34 110,35 @@ pub fn main() !void {
        }
    }

    // For each note, gather backlinks and references, and generate
    // contents.
    // Populate all note metadata required to generate a new note body.
    for (notes.items) |*note| {
        try note.appendBacklinks(&notes, allocator);
        try note.appendSlice(note.body.items);
        try note.appendReferences(allocator);
        try note.populateNoteData(notes, allocator);
    }

    // Assemble new note bodies.
    var note_bodies = std.ArrayList(struct { name: []const u8, body: util.CharBuffer }).init(allocator);
    for (notes.items) |*note| {
        var buf = util.CharBuffer.init(allocator);
        try note.generateBody(&buf, allocator);

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

    // Free the original note bodies.
    for (notes.items) |note| {
        note.body.deinit();
        try note_bodies.append(.{ .name = note.name, .body = buf });
        note.deinit();
    }

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

A src/note.zig => src/note.zig +270 -0
@@ 0,0 1,270 @@
const std = @import("std");
const util = @import("./util.zig");

const mem = std.mem;
const fmt = std.fmt;

const EscapedString = struct {
    escaped: util.CharBuffer,
    escape_error: ?EscapeError = null,

    const EscapeError = enum { TerminalPeriod };

    fn init(to_escape: []const u8, allocator: mem.Allocator) !@This() {
        return EscapedString{
            .escaped = try util.CharBuffer.initCapacity(allocator, to_escape.len * 2),
        };
    }

    // Process a string and escape any delimiters for reference creation.
    fn escapeAndLower(self: *@This(), s: []const u8) 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 and s[s.len - 1] == '.') {
            self.escape_error = EscapeError.TerminalPeriod;
            return;
        }
        for (s) |char| {
            switch (char) {
                ' ' => {
                    self.escaped.appendAssumeCapacity('\\');
                },
                else => {},
            }
            const lower_char = std.ascii.toLower(char);
            self.escaped.appendAssumeCapacity(lower_char);
        }
    }

    fn handleErrors(self: @This()) void {
        if (self.escape_error) |escape_error| {
            switch (escape_error) {
                EscapeError.TerminalPeriod => {
                    std.debug.print("Note references can't end with a period", .{});
                    std.os.exit(1);
                },
            }
        }
    }
};

const BufferAndBraces = struct {
    buffer: util.CharBuffer,
    offset: usize,
    slice_begins: std.ArrayList(usize),

    fn init(offset: usize, allocator: mem.Allocator) @This() {
        return @This(){
            .buffer = util.CharBuffer.init(allocator),
            .offset = offset,
            .slice_begins = std.ArrayList(usize).init(allocator),
        };
    }

    fn newBrace(self: *@This(), body_offset: usize) !void {
        try self.slice_begins.append(body_offset - self.offset);
    }

    fn currentBracesCount(self: @This()) usize {
        return self.slice_begins.items.len;
    }

    fn pop(self: *@This()) usize {
        return self.slice_begins.pop();
    }

    fn deinit(self: @This()) void {
        self.buffer.deinit();
        self.slice_begins.deinit();
    }
};

const begin_braces = '{';
const end_braces = '}';

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

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

    pub fn init(name: []const u8, allocator: mem.Allocator) @This() {
        return @This(){
            .name = name,
            .body = util.CharBuffer.init(allocator),
            .backlinks = std.ArrayList([]const u8).init(allocator),
            .references = std.ArrayList([]const u8).init(allocator),
        };
    }

    pub fn deinit(self: @This()) void {
        self.body.deinit();
        self.references.deinit();
    }

    // Iterate through the rest of the notes populate self.backlinks with names
    // of notes containing "{<self.name>}"
    pub fn populateBacklinks(
        self: *@This(),
        notes: std.ArrayList(Note),
        allocator: mem.Allocator,
    ) !void {
        const self_lowered = try std.ascii.allocLowerString(allocator, self.name);
        defer allocator.free(self_lowered);

        for (notes.items) |other_note| {
            for (other_note.references.items) |reference| {
                const reference_lowered = try std.ascii.allocLowerString(allocator, reference);
                defer allocator.free(reference_lowered);

                if (mem.eql(u8, reference_lowered, self_lowered)) {
                    var escaped = try EscapedString.init(other_note.name, allocator);
                    defer escaped.escaped.deinit();

                    escaped.escapeAndLower(other_note.name);
                    escaped.handleErrors();

                    try self.backlinks.append(escaped.escaped.items);
                    break;
                }
            }
        }
    }

    // Iterate through the note body and populate self.references with subslices
    // capturing "{<reference>}"
    pub fn populateReferences(self: *@This(), allocator: mem.Allocator) !void {
        var brace_context: ?BufferAndBraces = null;

        var i: usize = 0;
        while (i < self.body.items.len) : (i += 1) {
            const char = self.body.items[i];
            switch (char) {
                begin_braces => {
                    // Use a BufferAndBraces, which can keep track of multiple open
                    // braces, to handle nested braces. We'll copy the underlying
                    // contents of an outer brace into a buffer while noting (but
                    // not copying) any nested braces inside it.
                    if (brace_context == null) {
                        brace_context = BufferAndBraces.init(i, allocator);
                    }
                    if (brace_context) |*ctx| {
                        try ctx.newBrace(i);
                    }
                },
                end_braces => {
                    if (brace_context) |*ctx| {
                        // Subtract the number of current open braces, because
                        // those won't have been included in the current buffer.
                        const begin = ctx.pop() - ctx.currentBracesCount();
                        const reference_text = ctx.buffer.items[begin..];

                        const new_string = try allocator.alloc(u8, reference_text.len);
                        mem.copy(u8, new_string, reference_text);

                        try self.references.append(new_string);

                        if (ctx.slice_begins.items.len == 0) {
                            ctx.deinit();
                            brace_context = null;
                        }
                    }
                },
                '\n' => {
                    // References can't cross over lines.
                    if (brace_context) |ctx| {
                        ctx.deinit();
                        brace_context = null;
                    }
                },
                else => {
                    // While we're currently in open braces, collect the non-brace
                    // strings.
                    if (brace_context) |*ctx| {
                        try ctx.buffer.append(char);
                    }
                },
            }
        }
    }

    // Backlinks: if any other note body refers to this one, add a reference to
    // that body's name.
    fn appendBacklinks(
        self: *@This(),
        buf: *util.CharBuffer,
        allocator: mem.Allocator,
    ) !void {
        var found_one = false;

        for (self.backlinks.items) |backlink| {
            found_one = true;
            try appendRef(buf, backlink, allocator);
        }

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

    pub fn appendBody(self: *@This(), buf: *util.CharBuffer) !void {
        try buf.appendSlice(self.body.items);
    }

    // References: gather every reference in the body of the note.
    pub fn appendReferences(
        self: *@This(),
        buf: *util.CharBuffer,
        allocator: mem.Allocator,
    ) !void {
        // References padding.
        if (self.references.items.len > 0) {
            try buf.append('\n');
        }

        // Dedupe lowered and escaped references so we only link out once per
        // reference.
        var seen = std.BufSet.init(allocator);

        // Escape and add forward references to note.
        for (self.references.items) |phrase| {
            var escaped = try EscapedString.init(phrase, allocator);
            defer escaped.escaped.deinit();

            escaped.escapeAndLower(phrase);
            escaped.handleErrors();

            const str = escaped.escaped.items;
            if (!seen.contains(str)) {
                try seen.insert(str);
                try appendRef(buf, str, allocator);
            }
        }
    }

    pub fn populateNoteData(self: *@This(), notes: std.ArrayList(Note), allocator: mem.Allocator) !void {
        try self.populateBacklinks(notes, allocator);
        try self.populateReferences(allocator);
    }

    pub fn generateBody(
        self: *@This(),
        buf: *util.CharBuffer,
        allocator: mem.Allocator,
    ) !void {
        try self.appendBacklinks(buf, allocator);
        try self.appendBody(buf);
        try self.appendReferences(buf, allocator);
    }
};

M src/util.zig => src/util.zig +4 -0
@@ 12,3 12,7 @@ pub fn nextLine(reader: anytype, buffer: []u8) !?[]const u8 {
        return line;
    }
}

pub const CharBuffer = std.ArrayList(u8);

pub const ref_prefix = "%ref";