~gpanders/zet

e506d6d43e6fb9b48529b9ca1fac70187f36798f — Greg Anders 13 days ago 5ea9f96
Write backlinks at bottom of file

Before, backlinks would be written at the end of the file but before the
last "---", if it existed. This was because a "---" was used to begin
the references section in notes. However, it is much easier and
generally more readable to simply put references under their own "##
References" section and keep backlinks at the bottom.
1 files changed, 29 insertions(+), 45 deletions(-)

M src/zettel.zig
M src/zettel.zig => src/zettel.zig +29 -45
@@ 8,11 8,9 @@ const warn = std.debug.warn;

const c = @cImport(@cInclude("time.h"));

const id_length = "20200106121000".len;
const id_len = "20200106121000".len;
const extension = ".md";
const new_template = "---\ntitle: {}\ndate:  {}\ntags:\n...\n";
const backlinks_template = "- [[{}]] {}";
const backlinks_header = "## Backlinks";
const bl_header = "## Backlinks";

pub const Zettel = struct {
    allocator: *mem.Allocator,


@@ 43,14 41,15 @@ pub const Zettel = struct {
        var file = try fs.cwd().createFile(fname, .{});
        defer file.close();

        const contents = try fmt.allocPrint(allocator, new_template, .{ title, date });
        comptime var template = "---\ntitle: {}\ndate:  {}\ntags:\n...\n";
        const contents = try fmt.allocPrint(allocator, template, .{ title, date });
        try file.writeAll(contents);

        return Zettel{
            .allocator = allocator,
            .basename = fname[0 .. fname.len - extension.len],
            .fname = fname,
            .id = fname[0..id_length],
            .id = fname[0..id_len],
            .tags = &[_][]const u8{},
            .title = try mem.dupe(allocator, u8, title),
            .mtime = (try file.stat()).mtime,


@@ 91,8 90,8 @@ pub const Zettel = struct {

            // Also try id + title
            if (mem.startsWith(u8, keyword, self.id) and
                keyword.len > id_length + 1 and keyword[id_length] == ' ' and
                ascii.eqlIgnoreCase(keyword[id_length + 1 ..], self.title))
                keyword.len > id_len + 1 and keyword[id_len] == ' ' and
                ascii.eqlIgnoreCase(keyword[id_len + 1 ..], self.title))
            {
                return true;
            }


@@ 106,17 105,19 @@ pub const Zettel = struct {
        var links_list = std.ArrayList([]const u8).init(self.allocator);

        // Ignore any links beneath a preexisting backlinks header
        const end_index = mem.indexOf(u8, self.contents, backlinks_header) orelse self.contents.len;
        const end_index = mem.indexOf(u8, self.contents, bl_header) orelse
            self.contents.len;

        var start_index: usize = 0;
        outer: while (mem.indexOfPos(u8, self.contents[0..end_index], start_index, "[[")) |index| : (start_index = index + "[[".len) {
        outer: while (mem.indexOfPos(u8, self.contents[0..end_index], start_index, "[[")) |index| : (start_index = index + 1) {
            // Check for valid link and ID
            const link = self.contents[index .. index + "[[".len + self.id.len + "]]".len];
            comptime var link_len = "[[".len + id_len + "]]".len;
            const link = self.contents[index .. index + link_len];
            if (!mem.startsWith(u8, link, "[[") or !mem.endsWith(u8, link, "]]")) {
                continue;
            }

            const id = link["[[".len .. link.len - "]]".len];
            const id = link["[[".len .. "[[".len + id_len];
            for (id) |char| {
                if (!ascii.isDigit(char)) {
                    continue :outer;


@@ 133,41 134,24 @@ pub const Zettel = struct {
    pub fn writeBacklinks(self: *Zettel) !void {
        if (self.backlinks.items.len == 0) return;

        // Find the index of the end of the YAML metadata block, since that
        // interferes with detection of horizontal rules ("---") in the actual
        // note contents.
        // Note that both "---" and "..." are valid closing delimiters of the
        // metadata block.
        const metadata_start = "---".len + 1;
        const metadata_end = mem.min(usize, &[_]usize{
            mem.indexOf(u8, self.contents[metadata_start..], "...") orelse self.contents.len,
            mem.indexOf(u8, self.contents[metadata_start..], "---") orelse self.contents.len,
        });

        const end_index = blk: {
            if (mem.lastIndexOf(u8, self.contents, "---")) |i| {
                if (i <= metadata_end) {
                    // If last instance of "---" is in the metadata block, ignore it
                    break :blk self.contents.len;
                } else {
                    break :blk i - 1;
                }
            } else unreachable;
        };

        const start_index = if (mem.indexOf(u8, self.contents, backlinks_header)) |s| s - 1 else end_index;
        const start_index = if (mem.indexOf(u8, self.contents, bl_header)) |s|
            s - 1
        else
            self.contents.len;

        var backlinks = try std.ArrayList([]const u8).initCapacity(self.allocator, self.backlinks.items.len);
        for (self.backlinks.items) |backlink| {
            const item = try fmt.allocPrint(self.allocator, backlinks_template, .{ backlink.id, backlink.title });
        for (self.backlinks.items) |bl| {
            comptime var template = "- [[{}]] {}";
            const item = try fmt.allocPrint(self.allocator, template, .{ bl.id, bl.title });
            backlinks.appendAssumeCapacity(item);
        }

        const new_contents = try fmt.allocPrint(self.allocator, "{}\n\n{}\n\n{}\n{}", .{
        const new_contents = try mem.join(self.allocator, "\n", &[_][]const u8{
            self.contents[0 .. start_index - 1],
            backlinks_header,
            "",
            bl_header,
            "",
            try mem.join(self.allocator, "\n", backlinks.items),
            self.contents[end_index..],
        });

        if (!mem.eql(u8, new_contents, self.contents)) {


@@ 248,7 232,7 @@ fn fromEntry(allocator: *mem.Allocator, entry: fs.Dir.Entry) !Zettel {

    var line_it = mem.split(contents, "\n");
    if (line_it.next()) |line| {
        // If first line is not a metadata delimiter, skip this entry
        // If first line is not a front matter fence, skip this entry
        if (!mem.eql(u8, line, "---")) {
            return error.InvalidFormat;
        }


@@ 293,7 277,7 @@ fn fromEntry(allocator: *mem.Allocator, entry: fs.Dir.Entry) !Zettel {
            }
        }
    } else {
        // Reached EOF before finding closing metadata delimiter
        // Reached EOF before finding closing front matter fence
        return error.InvalidFormat;
    }



@@ 306,7 290,7 @@ fn fromEntry(allocator: *mem.Allocator, entry: fs.Dir.Entry) !Zettel {
        .allocator = allocator,
        .basename = fname[0 .. fname.len - extension.len],
        .fname = fname,
        .id = fname[0..id_length],
        .id = fname[0..id_len],
        .tags = tags.toOwnedSlice(),
        .title = title.?,
        .mtime = (try file.stat()).mtime,


@@ 316,9 300,9 @@ fn fromEntry(allocator: *mem.Allocator, entry: fs.Dir.Entry) !Zettel {
}

fn hasId(name: []const u8) bool {
    if (name.len < id_length) return false;
    if (name.len < id_len) return false;

    for (name[0..id_length]) |char| {
    for (name[0..id_len]) |char| {
        if (!ascii.isDigit(char)) return false;
    }