// TODO: just let compiler infer error set through recursive calls // depends on: https://github.com/ziglang/zig/issues/2971 // This code is heavily insprired by Devine's implementation in C: // https://github.com/hundredrabbits/100r.co/blob/main/src/main.c and his // personal site https://github.com/XXIIVV/Oscean const exe = @import("exe.zig"); const gen = @import("gen.zig"); const fs = @import("fs.zig"); const std = @import("std"); const log = std.log.scoped(.rbc); // tune std.log options pub const std_options = std.Options{ .log_level = .debug, }; pub const Context = struct { pub const Site = struct { pub const Page = struct { kind: enum { Meta, Htm, Markdown, }, path: []const u8, name: []const u8, }; path: []const u8, pages: std.ArrayListUnmanaged(Page) = .{}, }; arena: std.heap.ArenaAllocator, arena_mx: std.Thread.Mutex = .{}, alloc: std.heap.ThreadSafeAllocator = undefined, progress_nodes: std.ArrayListUnmanaged(std.Progress.Node) = .{}, progress_nodes_mx: std.Thread.Mutex = .{}, progress_nodes_tl: ?*const std.Progress.Node = null, sites: std.ArrayListUnmanaged(Site) = .{}, generated: std.ArrayListUnmanaged([]const u8) = .{}, meta: usize = 0, tilde: usize = 0, // opts force: bool = false, pub const params = struct { pub const PAGE_PATH = "src/inc"; pub const SITE_PATH = "site"; pub const LINK_PATH = "links"; pub const SITE_NAME = "gzr.im"; pub const LICENSE_HOLDER = "robertgzr"; pub const LICENSE_YEAR = "2025"; pub const LICENSE_NAME = "BY-NC-SA 4.0"; pub const LICENSE_URL = "https://git.sr.ht/~robertgzr/site/blob/master/LICENSE.content"; pub const SOURCE_URL = "https://git.sr.ht/~robertgzr/site/tree/master"; pub const PUBLIC_INBOX = "~robertgzr/public-inbox@lists.sr.ht"; pub const PANDOC = "pandoc"; pub const PANDOC_PREPROC = "preproc.lua"; }; fn create(parent_alloc: std.mem.Allocator) !*Context { const self = try parent_alloc.create(Context); self.* = .{ .arena = std.heap.ArenaAllocator.init(parent_alloc), }; // create thread safe allocator self.alloc = std.heap.ThreadSafeAllocator{ .child_allocator = self.arena.allocator(), .mutex = self.arena_mx, }; return self; } fn destroy(self: *Context) void { self.progress_end(); self.progress_nodes.deinit(self.allocator()); self.arena.deinit(); self.arena.child_allocator.destroy(self); } pub fn allocator(self: *Context) std.mem.Allocator { return self.alloc.allocator(); } // progress handling pub fn progress_start(self: *Context, name: []const u8, become_toplevel: bool) void { const node = blk: { if (self.progress_nodes_tl) |tl| break :blk tl.start(name, 0); break :blk std.Progress.start(.{ .root_name = name }); }; if (become_toplevel) self.progress_nodes_tl = &node; self.progress_nodes_mx.lock(); self.progress_nodes.append(self.allocator(), node) catch @panic("insufficient memory"); self.progress_nodes_mx.unlock(); } pub fn progress_end(self: *Context) void { self.progress_nodes_mx.lock(); const node = self.progress_nodes.pop() orelse return; self.progress_nodes_mx.unlock(); node.end(); } }; // TODO: make index smarter, it should know which files reference each other. // with this information, we could make better decisions whether to generate // output. // fn index(ctx: *Context) !void { // avoid reindexing if (ctx.sites.items.len != 0) return; ctx.progress_start("index", true); defer ctx.progress_end(); var root = std.fs.cwd().openDir(Context.params.PAGE_PATH, .{ .iterate = true }) catch |err| switch (err) { error.FileNotFound => { log.err("Missing {s} folder.", .{Context.params.PAGE_PATH}); std.os.linux.exit(1); }, else => |e| return e, }; defer root.close(); var walk = try root.walk(ctx.allocator()); defer walk.deinit(); var meta: u32 = 0; var htm: u32 = 0; var md: u32 = 0; // root site log.debug("Indexing site root", .{}); var site = Context.Site{ .path = "." }; while (try walk.next()) |e| { switch (e.kind) { .file => { ctx.progress_start(e.basename, false); defer ctx.progress_end(); if (std.mem.startsWith(u8, e.basename, "meta.")) { log.debug("Indexing meta {s}", .{e.path}); try site.pages.append(ctx.allocator(), .{ .kind = .Meta, .name = try ctx.allocator().dupe(u8, try fs.stripExtension(e.basename)), .path = try ctx.allocator().dupe(u8, e.path), }); meta += 1; } else if (std.mem.endsWith(u8, e.basename, ".htm")) { log.debug("--> {s}", .{std.mem.trimEnd(u8, e.basename, ".htm")}); log.debug("Indexing page {s}", .{e.path}); try site.pages.append(ctx.allocator(), .{ .kind = .Htm, .name = try ctx.allocator().dupe(u8, try fs.stripExtension(e.basename)), .path = try ctx.allocator().dupe(u8, e.path), }); htm += 1; } else if (std.mem.endsWith(u8, e.basename, ".md")) { log.debug("Indexing page {s}", .{e.path}); try site.pages.append(ctx.allocator(), .{ .kind = .Markdown, .name = try ctx.allocator().dupe(u8, try fs.stripExtension(e.basename)), .path = try ctx.allocator().dupe(u8, e.path), }); md += 1; } // XXX: scan for refs here? }, else => {}, } } try ctx.sites.append(ctx.allocator(), site); // sort pages by name for (ctx.sites.items) |s| { std.mem.sort( Context.Site.Page, s.pages.items, {}, struct { fn lessthan(_: void, a: Context.Site.Page, b: Context.Site.Page) bool { return std.ascii.lessThanIgnoreCase(a.name, b.name); } }.lessthan, ); } log.info("Indexed {d} sites.", .{ctx.sites.items.len}); log.info("Indexed {d} meta pages.", .{meta}); log.info("Indexed {d} htm pages.", .{htm}); log.info("Indexed {d} markdown pages.", .{md}); return; } const Cli = struct { fn run(ctx: *Context) !void { var argv = std.process.args(); _ = argv.skip(); // skip arg0 if (argv.inner.count == 1) { try Cli.mdCmd(ctx, null); try Cli.genCmd(ctx, null); } while (argv.next()) |arg| { if (std.mem.eql(u8, arg, "help")) { Cli.helpCmd(ctx); } else if (std.mem.eql(u8, arg, "gen")) { try Cli.genCmd(ctx, &argv); } else if (std.mem.eql(u8, arg, "md")) { try Cli.mdCmd(ctx, &argv); } else if (std.mem.eql(u8, arg, "watch")) { try Cli.watchCmd(ctx); } else if (std.mem.eql(u8, arg, "serve")) { try Cli.serveCmd(ctx); } else { Cli.helpCmd(ctx); } } } fn checkOpts(maybe_argv: ?*std.process.ArgIterator, opts: []const []const u8) bool { if (maybe_argv) |argv| { const arg = argv.next() orelse return false; // check opts for (opts) |opt| { if (std.mem.eql(u8, arg, opt)) return true; } // rewind argv argv.*.inner.index -= 1; return false; } else { return false; } } fn helpCmd(_: *Context) void { std.debug.print("usage: rbl [gen|md|watch|serve]\n\n", .{}); } fn genCmd(ctx: *Context, argv: ?*std.process.ArgIterator) !void { try index(ctx); if (checkOpts(argv, &.{ "-f", "--force" })) ctx.force = true; try gen.generate(ctx); if (checkOpts(argv, &.{ "-c", "--commit" })) try exe.commit(ctx, ctx.generated.items); } fn mdCmd(ctx: *Context, argv: ?*std.process.ArgIterator) !void { try index(ctx); try exe.runPandoc(ctx); if (checkOpts(argv, &.{ "-c", "--commit" })) try exe.commit(ctx, ctx.generated.items); } fn watchCmd(ctx: *Context) !void { const selfExe = try std.fs.selfExePathAlloc(ctx.allocator()); const command = try std.fmt.allocPrint(ctx.allocator(), \\find ./src/inc/ -type f -print | entr -nc -dd -- {s} md gen , .{selfExe}); try exe.shell(ctx, command, null); } fn serveCmd(ctx: *Context) !void { // try exe.shell(ctx, // \\python -m http.server 1414 // , null); var env_map = try std.process.getEnvMap(ctx.allocator()); defer env_map.deinit(); try env_map.put("CADDY_HTTP_PORT", "1414"); try env_map.put("CADDY_HOST", "localhost"); try exe.shell(ctx, \\caddy fmt --overwrite && caddy run --watch , &env_map); } }; pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}).init; const allocator = gpa.allocator(); defer _ = gpa.deinit(); var ctx = try Context.create(allocator); defer ctx.destroy(); try Cli.run(ctx); }