~clarity/sitegen

fe2003c0dc83ffe52f3f537f57e7be5499b9fac7 — Clarity Flowers a month ago 78a770d
Data-driven templating
1 files changed, 186 insertions(+), 215 deletions(-)

M src/main.zig
M src/main.zig => src/main.zig +186 -215
@@ 288,12 288,12 @@ fn renderFile(
        );
        defer out_file.close();
        const writer = out_file.writer();
        try Template.format(template.header, doc.info, file_info, writer);
        try Template.format(template.header, template.text, doc.info, file_info, writer);
        for (doc.blocks) |block| switch (ext) {
            .html => try formatBlockHtml(block, writer),
            .gmi => try formatBlockGmi(block, writer),
        };
        try Template.format(template.footer, doc.info, file_info, writer);
        try Template.format(template.footer, template.text, doc.info, file_info, writer);
    }
}



@@ 1207,257 1207,228 @@ fn formatIndexMarkup(writer: anytype, pages: []const IndexEntry) !void {
// ---- TEMPLATES ----

const Template = struct {
    header: []const Node,
    footer: []const Node,
    header: Tokens,
    footer: Tokens,
    text: []const u8,

    const Node = union(enum) {
        text: []const u8,
        variable: Variable,
        conditional: Conditional,
    };
    const Token = struct {
        const Type = enum { variable, format, conditional, end, text };

    const VariableName = enum {
        title,
        file,
        dir,
        written,
        updated,
        back_text,
        back,
        parent_name,
        parent,
        type: Type,
        position: usize,
        value: union {
            variable: Variable,
            jump: usize,
        } = undefined,
    };

    const Variable = struct {
        name: VariableName,
        format: ?[]const u8 = null,
    };
    const Variable = enum { title, file, dir, written, updated, back_text, back, parent_name, parent };

    const Conditional = struct {
        name: VariableName,
        output: []const Node,
    };
    const Tokens = std.MultiArrayList(Token);

    fn get(cwd: *std.fs.Dir, allocator: *std.mem.Allocator, template_file: ?[]const u8, comptime ext: Ext) !Template {
        return try parse(
            if (template_file) |file|
                try cwd.readFileAlloc(
                    allocator,
                    file,
                    1024 * 1024 * 1024,
                )
            else
                @embedFile("default_template." ++ @tagName(ext)),
            allocator,
        );
    fn readByte(reader: anytype) !?u8 {
        return reader.readByte() catch |err| switch (err) {
            error.EndOfStream => null,
            else => |other_err| return other_err,
        };
    }

    fn parseVariableStart(text: []const u8, start: usize) ?ParseResult(Variable) {
        if (parseLiteral(text, start, "{{")) |index| {
            if (index >= text.len) return null;
            inline for (@typeInfo(Variable).Enum.fields) |fld| {
                if (parseLiteral(text, index, fld.name)) |new_index| {
                    return ok(@field(Variable, fld.name), new_index);
                }
            }
        }
        return null;
    }

    // Caller must call deinit() on result
    fn parse(text: []const u8, allocator: *std.mem.Allocator) !Template {
        var arena = std.heap.ArenaAllocator.init(allocator);
        errdefer arena.deinit();
        var header_tokens = Tokens{};
        errdefer header_tokens.deinit(allocator);
        var footer_tokens = Tokens{};
        errdefer footer_tokens.deinit(allocator);

        var conditionals_buffer: [16]usize = undefined;
        var conditionals: []usize = conditionals_buffer[0..0];

        var tokens = &header_tokens;
        var index: usize = 0;
        var text_start = index;
        var header = std.ArrayList(Node).init(&arena.allocator);
        var footer = std.ArrayList(Node).init(&arena.allocator);

        try tokens.append(allocator, .{ .type = .text, .position = 0 });

        while (index < text.len) {
            if (parseLiteral(text, index, "{{content}}")) |new_pos| {
                if (index > text_start) {
                    try header.append(.{ .text = text[text_start..index] });
                }
                index = new_pos;
                text_start = index;
                break;
            } else if (try parseVariable(
                text,
                index,
                &arena.allocator,
            )) |res| {
                if (index > text_start) {
                    try header.append(.{ .text = text[text_start..index] });
            if (conditionals.len == 0 and tokens != &footer_tokens) {
                if (parseLiteral(text, index, "{{content}}")) |new_index| {
                    try tokens.append(allocator, .{ .type = .end, .position = index });
                    tokens = &footer_tokens;
                    index = new_index;
                    try tokens.append(allocator, .{ .type = .text, .position = index });
                    continue;
                }
                index = res.new_pos;
                text_start = index;
                try header.append(res.data);
            } else {
                index += 1;
            }
        }
        if (index > text_start) {
            try header.append(.{ .text = text[text_start..index] });
        }
        while (index < text.len) {
            if (try parseVariable(
                text,
                index,
                &arena.allocator,
            )) |res| {
                if (index > text_start) {
                    try footer.append(.{ .text = text[text_start..index] });
            if (parseVariableStart(text, index)) |res| {
                if (parseLiteral(text, res.new_pos, "|") != null) {
                    if (std.mem.indexOfPos(u8, text, res.new_pos + 1, "}}")) |end| {
                        try tokens.append(allocator, .{ .type = .variable, .position = index, .value = .{ .variable = res.data } });
                        try tokens.append(allocator, .{ .type = .format, .position = res.new_pos + 1 });
                        try tokens.append(allocator, .{ .type = .end, .position = end });
                        try tokens.append(allocator, .{ .type = .text, .position = end + 2 });
                        index = end + 2;
                        continue;
                    }
                }
                if (parseLiteral(text, res.new_pos, "}}") != null) {
                    try tokens.append(allocator, .{ .type = .variable, .position = index, .value = .{ .variable = res.data } });
                    index = res.new_pos + 2;
                    try tokens.append(allocator, .{ .type = .text, .position = index });
                    continue;
                } else if (conditionals.len < conditionals_buffer.len and parseLiteral(text, res.new_pos, "?") != null) {
                    conditionals.len = conditionals.len + 1;
                    conditionals[conditionals.len - 1] = tokens.len;
                    try tokens.append(allocator, .{ .type = .conditional, .position = index });
                    try tokens.append(allocator, .{ .type = .variable, .position = index, .value = .{ .variable = res.data } });
                    index = res.new_pos + 1;
                    try tokens.append(allocator, .{ .type = .text, .position = index });
                    continue;
                }
                index = res.new_pos;
                text_start = index;
                try footer.append(res.data);
            } else {
                index += 1;
            }
        }
        if (index > text_start) {
            try footer.append(.{ .text = text[text_start..index] });
            if (conditionals.len > 0 and parseLiteral(text, index, "}}") != null) {
                try tokens.append(allocator, .{ .type = .end, .position = index });
                tokens.items(.value)[conditionals[conditionals.len - 1]] = .{ .jump = tokens.len };
                conditionals.len = conditionals.len - 1;
                index += 2;
                try tokens.append(allocator, .{ .type = .text, .position = index });
                continue;
            }
            index += 1;
        }
        return Template{
            .header = header.toOwnedSlice(),
            .footer = footer.toOwnedSlice(),
            .header = header_tokens,
            .footer = footer_tokens,
            .text = text,
        };
    }

    fn parseVariableName(
        text: []const u8,
        start: usize,
    ) ?ParseResult(VariableName) {
        if (start >= text.len) return null;
        inline for (@typeInfo(VariableName).Enum.fields) |fld| {
            if (parseLiteral(text, start, fld.name)) |index| {
                return ok(@field(VariableName, fld.name), index);
            }
        }
        return null;
    }

    fn parseVariableFormat(
        text: []const u8,
        start: usize,
    ) ?ParseResult([]const u8) {
        if (start >= text.len) return null;
        const text_start = parseLiteral(text, start, "|") orelse return null;
        const text_end = std.mem.indexOfPos(u8, text, text_start, "}}") orelse
            return null;
        return ok(text[text_start..text_end], text_end + 2);
    fn get(cwd: *std.fs.Dir, allocator: *std.mem.Allocator, template_file: ?[]const u8, comptime ext: Ext) !Template {
        return try parse(
            if (template_file) |file|
                try cwd.readFileAlloc(
                    allocator,
                    file,
                    1024 * 1024 * 1024,
                )
            else
                @embedFile("default_template." ++ @tagName(ext)),
            allocator,
        );
    }

    fn parseConditional(
        text: []const u8,
        start: usize,
        allocator: *std.mem.Allocator,
    ) std.mem.Allocator.Error!?ParseResult([]const Node) {
        var index = parseLiteral(text, start, "?") orelse return null;
        var arena = std.heap.ArenaAllocator.init(allocator);
        errdefer arena.deinit();
        var text_start = index;
        var result = std.ArrayList(Node).init(&arena.allocator);
        while (index < text.len) {
            if (parseLiteral(text, index, "}}")) |new_index| {
                if (index > text_start) {
                    try result.append(.{ .text = text[text_start..index] });
                }
                return ok(
                    @as([]const Node, result.toOwnedSlice()),
                    new_index,
                );
            } else if (try parseVariable(
                text,
                index,
                &arena.allocator,
            )) |res| {
                if (index > text_start) {
                    try result.append(.{ .text = text[text_start..index] });
                }
                index = res.new_pos;
                text_start = index;
                try result.append(res.data);
            } else {
                index += 1;
            }
    fn content(positions: []const usize, index: usize, text: []const u8) []const u8 {
        if (index + 1 >= positions.len) {
            return text[positions[index]..];
        } else {
            return text[positions[index]..positions[index + 1]];
        }
        arena.deinit();
        return null;
    }

    fn parseVariable(
        text: []const u8,
        start: usize,
        allocator: *std.mem.Allocator,
    ) std.mem.Allocator.Error!?ParseResult(Node) {
        var index = start;
        index = parseLiteral(text, index, "{{") orelse return null;
        const name_res = parseVariableName(text, index) orelse return null;

        index = name_res.new_pos;
        if (parseVariableFormat(text, index)) |format_res| {
            return ok(Node{
                .variable = .{
                    .name = name_res.data,
                    .format = format_res.data,
    fn logTokens(tokens: Tokens, text: []const u8) void {
        const positions = tokens.items(.position);
        const values = tokens.items(.value);
        for (tokens.items(.type)) |tag, i| {
            std.debug.print("{d: >2} {d: >3} {s}", .{ i, positions[i], @tagName(tag) });
            switch (tag) {
                .variable => {
                    std.debug.print(" {s}", .{@tagName(values[i].variable)});
                },
            }, format_res.new_pos);
        } else if (try parseConditional(
            text,
            index,
            allocator,
        )) |cond_res| {
            return ok(Node{
                .conditional = .{
                    .name = name_res.data,
                    .output = cond_res.data,
                .text => {
                    const preview = content(positions, i, text);
                    std.debug.print(" \x1B[37m{s}\x1B[0m", .{preview});
                },
            }, cond_res.new_pos);
                .conditional => {
                    std.debug.print("->{d}", .{values[i].jump});
                },
                else => {},
            }
            std.debug.print("\n", .{});
        }
        const end_index = parseLiteral(text, index, "}}") orelse return null;
        return ok(Node{ .variable = .{ .name = name_res.data } }, end_index);
    }

    fn format(
        template: []const Node,
        template: Tokens,
        text: []const u8,
        info: Info,
        file: FileInfo,
        writer: anytype,
    ) @TypeOf(writer).Error!void {
        for (template) |node| switch (node) {
            .text => |text| try writer.writeAll(text),
            .variable => |variable| switch (variable.name) {
                .written => {
                    try info.created.formatRuntime(
                        variable.format orelse "",
                        writer,
                    );
                },
                .updated => {
                    if (info.changes.len > 0) {
                        try info.changes[info.changes.len - 1].date.formatRuntime(
                            variable.format orelse "",
                            writer,
                        );
        const tags = template.items(.type);
        const values = template.items(.value);
        const positions = template.items(.position);
        var i: usize = 0;
        while (i < tags.len) {
            switch (tags[i]) {
                .text => try writer.writeAll(content(positions, i, text)),
                .conditional => {
                    if (switch (values[i + 1].variable) {
                        .written, .title, .file, .back_text => true,
                        .dir => file.dir != null,
                        .back, .parent => file.dir != null or
                            !std.mem.eql(u8, file.name, "index"),
                        .updated => info.changes.len > 0,
                        .parent_name => file.parent_title != null,
                    }) {
                        i += 1;
                    } else {
                        i = values[i].jump;
                        continue;
                    }
                },
                .title => try writer.writeAll(info.title),
                .file => try writer.writeAll(file.name),
                .dir => if (file.dir) |dir| try writer.writeAll(dir),
                .back, .parent => {
                    try writer.writeByte('.');
                    if (file.dir != null) {
                        if (std.mem.eql(u8, file.name, "index")) {
                            try writer.writeByte('.');
                .end, .format => {},
                .variable => switch (values[i].variable) {
                    .written => {
                        if (i + 1 < tags.len and tags[i + 1] == .format) {
                            try info.created.formatRuntime(
                                content(positions, i + 1, text),
                                writer,
                            );
                        } else {
                            try info.created.formatRuntime("", writer);
                        }
                    }
                },
                .back_text, .parent_name => {
                    if (file.parent_title) |name| {
                        try writer.writeAll(name);
                    }
                    },
                    .updated => {
                        if (info.changes.len > 0) {
                            if (i + 1 < tags.len and tags[i + 1] == .format) {
                                try info.changes[info.changes.len - 1].date.formatRuntime(
                                    content(positions, i + 1, text),
                                    writer,
                                );
                            } else {
                                try info.changes[info.changes.len - 1].date.formatRuntime("", writer);
                            }
                        }
                    },
                    .title => try writer.writeAll(info.title),
                    .file => try writer.writeAll(file.name),
                    .dir => if (file.dir) |dir| try writer.writeAll(dir),
                    .back, .parent => {
                        try writer.writeByte('.');
                        if (file.dir != null) {
                            if (std.mem.eql(u8, file.name, "index")) {
                                try writer.writeByte('.');
                            }
                        }
                    },
                    .back_text, .parent_name => {
                        if (file.parent_title) |name| {
                            try writer.writeAll(name);
                        }
                    },
                },
            },
            .conditional => |conditional| {
                if (switch (conditional.name) {
                    .written, .title, .file, .back_text => true,
                    .dir => file.dir != null,
                    .back, .parent => file.dir != null or
                        !std.mem.eql(u8, file.name, "index"),
                    .updated => info.changes.len > 0,
                    .parent_name => file.parent_title != null,
                }) {
                    try format(conditional.output, info, file, writer);
                }
            },
        };
            }
            i += 1;
        }
    }
};