const std = @import("std"); const Io = std.Io; const Allocator = std.mem.Allocator; const fern = @import("fern"); const zlua = @import("zlua"); const Lua = zlua.Lua; const maivi = @import("root.zig"); const lua = maivi.lua; // init ======================================================================= var arena: std.heap.ArenaAllocator = undefined; var allocator: Allocator = undefined; pub fn init() void { arena = .init(maivi.allocator); allocator = arena.allocator(); } pub fn deinit() void { arena.deinit(); } // Data ======================================================================= pub const Frontmatter = struct { pub const Date = packed struct { year: c_int = 0, month: c_int = 0, day: c_int = 0, // needs format YYYY-MM-DD pub fn parse(str: []const u8) !Frontmatter.Date { if (str.len != 10) return error.InvalidLength; return .{ .year = try std.fmt.parseInt(c_int, str[0..4], 10), .month = try std.fmt.parseInt(c_int, str[5..7], 10), .day = try std.fmt.parseInt(c_int, str[8..10], 10), }; } }; pub const Item = union(enum) { str: []const u8, date: Date, list: [][]const u8 }; const KV = struct { k: []const u8, v: Item }; pub const KeyPairs = std.ArrayList(KV); /// parse if the first line == '+++' until first next line that == '+++' pub fn parse(data_allocator: Allocator, reader: *Io.Reader) !Frontmatter.KeyPairs { var props: Frontmatter.KeyPairs = try .initCapacity(data_allocator, 16); const first = reader.take(3) catch |e| switch (e) { error.EndOfStream => return error.NoFrontMatter, else => return e, }; if (!std.mem.eql(u8, first, "+++")) return error.NoFrontMatter; w: while (true) { const line = reader.takeDelimiterExclusive('\n') catch break :w; p: { _ = reader.peek(1) catch break :p; reader.toss(1); } if (std.mem.eql(u8, line, "+++")) break :w; var it = std.mem.splitScalar(u8, line, '='); const key = std.mem.trim(u8, it.first(), " "); const value = if (it.peek()) |v| std.mem.trim(u8, v, " ") else continue :w; if (value.len == 0) continue :w; if (value[0] == '"') { if (value[value.len - 1] != '"') continue :w; const str = value[1 .. value.len - 1]; try props.append(data_allocator, .{ .k = key, .v = .{ .str = str } }); continue :w; } if (std.mem.findScalar(u8, value, ',')) |_| { var list: std.ArrayList([]const u8) = try .initCapacity(data_allocator, 16); var l_it = std.mem.splitScalar(u8, value, ','); while (l_it.next()) |item| if (item.len > 0) try list.append(data_allocator, item); try props.append(data_allocator, .{ .k = key, .v = .{ .list = try list.toOwnedSlice(data_allocator) } }); continue :w; } const date = Date.parse(value) catch |err| { fern.err().ctx("data.Data.parse") .msg("date parse failed") .str("v", value) .err(err).log(); continue :w; }; try props.append(data_allocator, .{ .k = key, .v = .{ .date = date } }); } return props; } }; // load ======================================================================= pub fn load(io: Io) !void { fern.trace().ctx("data.load").log(); const rootdir = try Io.Dir.openDirAbsolute(io, maivi.config.root, .{}); defer rootdir.close(io); const data_dir = Io.Dir.openDir(rootdir, io, "data", .{ .iterate = true, .access_sub_paths = true }) catch |err| switch (err) { error.FileNotFound, error.NotDir => { fern.warn().ctx("data") .msg("could not parse data/ directory; it is either missing or not a directory.") .log(); return; }, else => return err, }; defer data_dir.close(io); try parseDir(io, data_dir); } fn parseFile(io: Io, file: Io.File) !void { var real_path: [0x400]u8 = undefined; const i = try file.realPath(io, &real_path); const path = std.mem.cutPrefix(u8, real_path[0..i], maivi.config.root) orelse return error.Unexpected; var buf: [0x400]u8 = undefined; var file_reader = file.reader(io, &buf); const reader = &file_reader.interface; var a: std.heap.ArenaAllocator = .init(maivi.allocator); defer a.deinit(); const data_allocator = a.allocator(); var data: ?Frontmatter.KeyPairs = Frontmatter.parse(data_allocator, reader) catch |err| switch (err) { error.NoFrontMatter => null, else => return err, }; defer if (data != null) data.?.deinit(data_allocator); fern.debug().ctx("data.parse_file") .msg("adding data") .str("path", path).log(); try setData(maivi.lua.lua, path, data); } fn parseDir(io: Io, dir: Io.Dir) !void { var it = dir.iterate(); return while (try it.next(io)) |x| switch (x.kind) { .file => { const file = try dir.openFile(io, x.name, .{}); defer file.close(io); try parseFile(io, file); }, .directory => { const sub_dir = try dir.openDir(io, x.name, .{ .iterate = true, .access_sub_paths = true }); defer sub_dir.close(io); try parseDir(io, sub_dir); }, else => continue, }; } // lua ======================================================================== pub fn setData(l: *Lua, path: []const u8, fmtable: ?Frontmatter.KeyPairs) !void { _ = l.getGlobal("this"); _ = l.getField(-1, "data"); defer l.pop(2); // pops "data" and "this" _ = l.pushString(path); if (fmtable) |t| _ = try pushData(l, t) else l.newTable(); l.setTable(-3); } // helpers ==================================================================== fn pushData(l: *Lua, d: Frontmatter.KeyPairs) !i32 { l.createTable(0, @as(i32, @intCast(d.items.len))); for (d.items) |kv| { _ = l.pushString(kv.k); errdefer l.pop(1); defer l.setTable(-3); _ = switch (kv.v) { .str => l.pushString(kv.v.str), .list => try pushList(l, []const u8, kv.v.list), .date => try pushDate(l, kv.v.date), }; } return 1; } /// * Pops from Stack: `0` /// * Pushes to Stack: `1` fn pushList(l: *Lua, comptime T: anytype, lst: []T) !i32 { l.createTable(@as(i32, @intCast(lst.len)), 0); errdefer l.pop(1); for (lst, 1..lst.len + 1) |item, n| { _ = try l.pushAny(item); l.setIndexRaw(-2, @as(c_int, @intCast(n))); } return 1; } /// * Pops from Stack: `0` /// * Pushes to Stack: `1` fn pushDate(l: *Lua, date: Frontmatter.Date) !i32 { _ = l.getGlobal("os"); _ = l.getField(-1, "time"); l.remove(l.absIndex(-2)); if (!l.isFunction(-1)) return error.Unexpected; // { year = ..., month = ..., day = ... } l.createTable(0, 3); l.pushInteger(date.year); l.setField(-2, "year"); l.pushInteger(date.month); l.setField(-2, "month"); l.pushInteger(date.day); l.setField(-2, "day"); try lua.pcall(l, .{ .args = 1, .results = 1 }); return 1; }