about summary refs log tree commit diff
diff options
context:
space:
mode:
authorRobert Günzler <r@gnzler.io>2026-04-08 13:29:17 +0900
committerRobert Günzler <r@gnzler.io>2026-04-08 13:50:39 +0900
commit2200e168cfc35134aec2ccc656ad5419663a973c (patch)
tree3b85ec1e701ffad2a519fd5d9c54321be5c3eedf
parent8bca0ebd8645bf2714035ed61ac140956a83fc21 (diff)
downloadsite-2200e168cfc35134aec2ccc656ad5419663a973c.tar.gz
start refactoring to support multiple page roots
Signed-off-by: Robert Günzler <r@gnzler.io>
Diffstat (limited to '')
-rw-r--r--makefile4
-rw-r--r--src/exe.zig21
-rw-r--r--src/fs.zig1
-rw-r--r--src/gen.zig127
-rw-r--r--src/main.zig146
5 files changed, 179 insertions, 120 deletions
diff --git a/makefile b/makefile
index c3d0745..d293307 100644
--- a/makefile
+++ b/makefile
@@ -14,7 +14,9 @@ clean:
 	@ rm -rf site/* $(rbc_BIN) $(md_SRC:%.md=%.htm)
 
 gen: $(rbc_BIN)
-	@ $(rbc_BIN) md gen
+	# TODO: fix the panic
+	@ $(rbc_BIN) md
+	@ $(rbc_BIN) gen
 
 watch:
 	@ $(rbc_BIN) watch
diff --git a/src/exe.zig b/src/exe.zig
index 998ea3b..ad3a7aa 100644
--- a/src/exe.zig
+++ b/src/exe.zig
@@ -57,20 +57,29 @@ pub fn WaitPool(
 pub fn runPandoc(ctx: *Context) !void {
     ctx.generated.clearRetainingCapacity();
 
-    ctx.progress_start("Processing markdown files");
+    ctx.progress_start("Processing markdown files", true);
     defer ctx.progress_end();
 
-    // TODO: check if pandoc is installed and exit gracefully
+    var md = try std.ArrayList([]const u8).initCapacity(ctx.allocator(), 1);
+    defer md.deinit(ctx.allocator());
+
+    // TODO: support more than one site
+    for (ctx.sites.getLast().pages.items) |p|
+        if (p.kind == .Markdown) try md.append(ctx.allocator(), p.path);
 
+    // TODO: check if pandoc is installed and exit gracefully
     try WaitPool(
         []const u8,
         pandocWorker,
-    ).work(ctx, ctx.markdown.items, .{});
+    ).work(ctx, md.items, .{});
 
-    log.info("Processed {d} markdown files.", .{ctx.markdown.items.len});
+    log.info("Processed {d} markdown files.", .{md.items.len});
 }
 
 fn pandocWorker(ctx: *Context, page: []const u8) !void {
+    ctx.progress_start(page, true);
+    defer ctx.progress_end();
+
     const source_path = try std.fs.path.join(ctx.allocator(), &.{ Context.params.PAGE_PATH, page });
     defer ctx.allocator().free(source_path);
     const target_path = try fs.replaceExtension(ctx.allocator(), source_path, "htm");
@@ -159,7 +168,7 @@ pub fn captureStdout(ctx: *Context, argv: []const []const u8, env_map: ?*std.pro
     const label = try std.fmt.allocPrint(ctx.allocator(), "running {s}", .{command});
     defer ctx.allocator().free(label);
 
-    ctx.progress_start(label);
+    ctx.progress_start(label, true);
     defer ctx.progress_end();
 
     log.debug("running {s}", .{command});
@@ -187,7 +196,7 @@ pub fn shell(ctx: *Context, command: []const u8, env_map: ?*std.process.EnvMap)
     const label = try std.fmt.allocPrint(ctx.allocator(), "running {s}", .{command});
     defer ctx.allocator().free(label);
 
-    ctx.progress_start(label);
+    ctx.progress_start(label, true);
     defer ctx.progress_end();
 
     var p = std.process.Child.init(&.{
diff --git a/src/fs.zig b/src/fs.zig
index 08ad7ee..f139653 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -124,7 +124,6 @@ pub fn joinPathAndFilename(alloc: std.mem.Allocator, paths: []const []const u8,
     const filename = try std.mem.concat(fba.allocator(), u8, filename_parts);
 
     var p = try std.ArrayList([]const u8).initCapacity(fba.allocator(), paths.len + 1);
-
     p.appendSliceAssumeCapacity(paths);
     p.appendAssumeCapacity(filename);
 
diff --git a/src/gen.zig b/src/gen.zig
index 449342e..fd1dff7 100644
--- a/src/gen.zig
+++ b/src/gen.zig
@@ -9,21 +9,21 @@ const log = std.log.scoped(.gen);
 pub fn generate(ctx: *Context) !void {
     ctx.generated.clearRetainingCapacity();
 
-    ctx.progress_start("generate");
+    ctx.progress_start("generate", true);
     defer ctx.progress_end();
 
-    try genIndex(ctx);
+    try index(ctx);
 
     log.info("Generated index.", .{});
 
-    try genSites(ctx);
+    try pages(ctx);
 
     log.info("Generated {d} files.", .{ctx.generated.items.len});
     log.info("Generated {d} tildes.", .{ctx.tilde});
 }
 
-fn genIndex(ctx: *Context) !void {
-    ctx.progress_start("index.htm");
+fn index(ctx: *Context) !void {
+    ctx.progress_start("index.htm", false);
     defer ctx.progress_end();
 
     const srcpathIndex = try fs.joinPathAndFilename(ctx.allocator(), &.{Context.params.PAGE_PATH}, &.{"index.htm"});
@@ -40,32 +40,21 @@ fn genIndex(ctx: *Context) !void {
         \\
     );
 
-    // alphanum ascii sort
-    std.mem.sort(
-        []const u8,
-        ctx.pages.items,
-        {},
-        struct {
-            fn lessthan(_: void, a: []const u8, b: []const u8) bool {
-                return std.ascii.lessThanIgnoreCase(a, b);
+    for (ctx.sites.items) |s| {
+        for (s.pages.items) |p| {
+            if (p.kind == .Htm) {
+                const dstpath = try fs.joinPathAndFilename(ctx.allocator(), &.{Context.params.SITE_PATH}, &.{ p.name, ".html" });
+                defer ctx.allocator().free(dstpath);
+
+                try w.interface.print(
+                    \\<li><a href="/{[LINK]s}">{[NAME]s}</a></li>
+                    \\
+                , .{
+                    .LINK = dstpath,
+                    .NAME = p.name,
+                });
             }
-        }.lessthan,
-    );
-
-    for (ctx.pages.items) |p| {
-        if (std.mem.startsWith(u8, p, "meta.")) continue;
-
-        const dstname = try fs.stripExtension(p);
-        const dstpath = try fs.joinPathAndFilename(ctx.allocator(), &.{Context.params.SITE_PATH}, &.{ dstname, ".html" });
-        defer ctx.allocator().free(dstpath);
-
-        try w.interface.print(
-            \\<li><a href="/{[LINK]s}">{[NAME]s}</a></li>
-            \\
-        , .{
-            .LINK = dstpath,
-            .NAME = dstname,
-        });
+        }
     }
 
     try w.interface.writeAll(
@@ -76,27 +65,25 @@ fn genIndex(ctx: *Context) !void {
     try w.interface.flush();
 }
 
-fn genSites(ctx: *Context) !void {
-    for (ctx.pages.items) |p| {
-        if (std.mem.startsWith(u8, std.fs.path.basename(p), "meta.")) {
-            // meta pages
-            try meta(ctx, p);
-            continue;
+fn pages(ctx: *Context) !void {
+    for (ctx.sites.items) |s| {
+        for (s.pages.items) |p| {
+            if (p.kind == .Meta) {
+                try meta(ctx, p);
+            } else if (p.kind == .Htm) {
+                try page(ctx, p);
+            }
         }
-
-        // regular pages
-        try genSite(ctx, p);
     }
 }
 
-fn genSite(ctx: *Context, p: []const u8) !void {
-    ctx.progress_start(std.fs.path.basename(p));
+fn page(ctx: *Context, p: Context.Site.Page) !void {
+    ctx.progress_start(p.name, false);
     defer ctx.progress_end();
 
-    const dstname = try fs.stripExtension(p);
-    const srcpath = try fs.joinPathAndFilename(ctx.allocator(), &.{Context.params.PAGE_PATH}, &.{ dstname, ".htm" });
+    const srcpath = try fs.joinPathAndFilename(ctx.allocator(), &.{Context.params.PAGE_PATH}, &.{ p.name, ".htm" });
     defer ctx.allocator().free(srcpath);
-    const dstpath = try fs.joinPathAndFilename(ctx.allocator(), &.{Context.params.SITE_PATH}, &.{ dstname, ".html" });
+    const dstpath = try fs.joinPathAndFilename(ctx.allocator(), &.{Context.params.SITE_PATH}, &.{ p.name, ".html" });
     defer ctx.allocator().free(dstpath);
     const rel_dstpath = try std.fs.path.relative(ctx.allocator(), Context.params.SITE_PATH, dstpath);
     defer ctx.allocator().free(rel_dstpath);
@@ -112,14 +99,14 @@ fn genSite(ctx: *Context, p: []const u8) !void {
     const dstmtime = try fs.getLastEdit(dstpath);
     if (!ctx.force and srcmtime.sec < dstmtime.sec) return;
 
-    log.debug("generating {s} at {s}", .{ p, rel_dstpath });
+    log.debug("generating {s} at {s}", .{ p.path, rel_dstpath });
 
     const f = try fs.FileSwapper.createAt(ctx.allocator(), dstpath);
 
     var buffer: [1024]u8 = undefined;
     var w = f.writer(&buffer);
 
-    const page_name = try std.mem.replaceOwned(u8, ctx.allocator(), dstname, "_", " ");
+    const page_name = try std.mem.replaceOwned(u8, ctx.allocator(), p.name, "_", " ");
     defer ctx.allocator().free(page_name);
 
     try build(ctx.allocator(), &w.interface, page_name, srcpath);
@@ -230,19 +217,18 @@ fn build(allocator: std.mem.Allocator, w: *std.Io.Writer, page_name: []const u8,
 // build a virtual html file, currently either:
 // - meta.nav: used for populating the global navigation menu
 // - meta.tilde: used to create ~foobar files
-fn meta(ctx: *Context, p: []const u8) !void {
-    ctx.progress_start(std.fs.path.basename(p));
+fn meta(ctx: *Context, p: Context.Site.Page) !void {
+    ctx.progress_start(p.name, false);
     defer ctx.progress_end();
 
     ctx.meta += 1;
 
-    if (std.mem.startsWith(u8, p, "meta.nav.")) return;
+    if (std.mem.startsWith(u8, p.name, "meta.nav")) return;
 
-    if (std.mem.startsWith(u8, p, "meta.tilde.")) {
-        const dot = std.mem.indexOfPosLinear(u8, p, 11, ".");
-        const dstname = p[11..dot.?];
-        const srcpath = try fs.joinPathAndFilename(ctx.allocator(), &.{Context.params.PAGE_PATH}, &.{p});
+    if (std.mem.startsWith(u8, p.name, "meta.tilde")) {
+        const srcpath = try fs.joinPathAndFilename(ctx.allocator(), &.{Context.params.PAGE_PATH}, &.{p.path});
         defer ctx.allocator().free(srcpath);
+        const dstname = p.name[11..]; // trim "meta.tilde." from the name
         const dstpath = try fs.joinPathAndFilename(ctx.allocator(), &.{Context.params.LINK_PATH}, &.{ "~", dstname });
         defer ctx.allocator().free(dstpath);
 
@@ -289,6 +275,7 @@ fn meta(ctx: *Context, p: []const u8) !void {
         return;
     }
 
+    log.err("unknown meta page type: {s}", .{p.name});
     return error.unknownMeta;
 }
 
@@ -390,49 +377,49 @@ const Page = struct {
 };
 
 fn portal(allocator: std.mem.Allocator, w: *std.Io.Writer, refname: []const u8, insert_heading: bool) anyerror!void {
-    const page = try Page.from_ref(allocator, refname);
-    defer page.deinit();
+    const p = try Page.from_ref(allocator, refname);
+    defer p.deinit();
 
     if (insert_heading)
         try w.print(
             \\<h2 id="{[ID]s}"><a href="/{[PATH]s}">{[NAME]s}</a></2>
             \\
         , .{
-            .ID = page.ref,
-            .NAME = page.name,
-            .PATH = page.html,
+            .ID = p.ref,
+            .NAME = p.name,
+            .PATH = p.html,
         });
 
-    try inject(allocator, w, page.htm);
+    try inject(allocator, w, p.htm);
 
     // XXX: track ref count?
 }
 
 fn ref(allocator: std.mem.Allocator, w: *std.Io.Writer, refname: []const u8) anyerror!void {
-    const page = try Page.from_ref(allocator, refname);
-    defer page.deinit();
+    const p = try Page.from_ref(allocator, refname);
+    defer p.deinit();
 
     try w.print(
         \\<a href="/{[PATH]s}{[ANCHOR]s}">{[NAME]s}</a>
     , .{
-        .NAME = page.name,
-        .PATH = page.html,
-        .ANCHOR = page.anchor,
+        .NAME = p.name,
+        .PATH = p.html,
+        .ANCHOR = p.anchor,
     });
 
     // XXX: track ref count?
 }
 
 fn ref_link(allocator: std.mem.Allocator, w: *std.Io.Writer, refname: []const u8) anyerror!void {
-    const page = try Page.from_ref(allocator, refname);
-    defer page.deinit();
+    const p = try Page.from_ref(allocator, refname);
+    defer p.deinit();
 
-    try w.print("/{s}{s}", .{ page.html, page.anchor });
+    try w.print("/{s}{s}", .{ p.html, p.anchor });
 }
 
 fn ref_name(allocator: std.mem.Allocator, w: *std.Io.Writer, refname: []const u8) anyerror!void {
-    const page = try Page.from_ref(allocator, refname);
-    defer page.deinit();
+    const p = try Page.from_ref(allocator, refname);
+    defer p.deinit();
 
-    try w.print("{s}", .{page.ref});
+    try w.print("{s}", .{p.ref});
 }
diff --git a/src/main.zig b/src/main.zig
index 3edfb1c..fb8e687 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -7,6 +7,7 @@
 
 const exe = @import("exe.zig");
 const gen = @import("gen.zig");
+const fs = @import("fs.zig");
 
 const std = @import("std");
 
@@ -14,20 +15,34 @@ const log = std.log.scoped(.rbc);
 
 // tune std.log options
 pub const std_options = std.Options{
-    .log_level = .info,
+    .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,
 
-    pages: std.ArrayListUnmanaged([]const u8) = .{},
-    markdown: std.ArrayListUnmanaged([]const u8) = .{},
-
+    sites: std.ArrayListUnmanaged(Site) = .{},
     generated: std.ArrayListUnmanaged([]const u8) = .{},
     meta: usize = 0,
     tilde: usize = 0,
@@ -62,11 +77,6 @@ pub const Context = struct {
             .child_allocator = self.arena.allocator(),
             .mutex = self.arena_mx,
         };
-        // create root progress node
-        try self.progress_nodes.append(
-            self.allocator(),
-            std.Progress.start(.{ .root_name = "" }),
-        );
         return self;
     }
 
@@ -82,17 +92,25 @@ pub const Context = struct {
     }
 
     // progress handling
-    pub fn progress_start(self: *Context, name: []const u8) void {
+    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();
-        const new = self.progress_nodes.getLast().start(name, 0);
-        self.progress_nodes.append(self.allocator(), new) catch @panic("insufficient memory");
+        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 current = self.progress_nodes.pop() orelse return;
+        const node = self.progress_nodes.pop() orelse return;
         self.progress_nodes_mx.unlock();
-        current.end();
+        node.end();
     }
 };
 
@@ -102,14 +120,14 @@ pub const Context = struct {
 //
 fn index(ctx: *Context) !void {
     // avoid reindexing
-    if ((ctx.pages.items.len != 0) and (ctx.markdown.items.len != 0)) return;
+    if (ctx.sites.items.len != 0) return;
 
-    ctx.progress_start("index");
+    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 => {
-            std.log.err("Missing {s} folder.", .{Context.params.PAGE_PATH});
+            log.err("Missing {s} folder.", .{Context.params.PAGE_PATH});
             std.os.linux.exit(1);
         },
         else => |e| return e,
@@ -119,19 +137,45 @@ fn index(ctx: *Context) !void {
     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);
+                ctx.progress_start(e.basename, false);
                 defer ctx.progress_end();
 
-                if (std.mem.endsWith(u8, e.basename, ".htm")) {
-                    std.log.debug("Indexing {s}", .{e.path});
-                    try ctx.pages.append(ctx.allocator(), try ctx.allocator().dupe(u8, e.path));
-                }
-                if (std.mem.endsWith(u8, e.basename, ".md")) {
-                    std.log.debug("Indexing {s}", .{e.path});
-                    try ctx.markdown.append(ctx.allocator(), try ctx.allocator().dupe(u8, e.path));
+                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?
@@ -140,8 +184,26 @@ fn index(ctx: *Context) !void {
         }
     }
 
-    log.info("Indexed {d} pages.", .{ctx.pages.items.len});
-    log.info("Indexed {d} markdown files.", .{ctx.markdown.items.len});
+    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;
 }
@@ -206,11 +268,11 @@ const Cli = struct {
     }
 
     fn mdCmd(ctx: *Context, argv: ?*std.process.ArgIterator) !void {
-        try index(ctx);
-        try exe.runPandoc(ctx);
+            try index(ctx);
+            try exe.runPandoc(ctx);
 
-        if (checkOpts(argv, &.{ "-c", "--commit" }))
-            try exe.commit(ctx, ctx.generated.items);
+            if (checkOpts(argv, &.{ "-c", "--commit" }))
+                try exe.commit(ctx, ctx.generated.items);
     }
 
     fn watchCmd(ctx: *Context) !void {
@@ -223,19 +285,19 @@ const Cli = struct {
     }
 
     fn serveCmd(ctx: *Context) !void {
-        try exe.shell(ctx,
-            \\python -m http.server 1414
-        , null);
+        // try exe.shell(ctx,
+        //     \\python -m http.server 1414
+        // , null);
 
-        // var env_map = try std.process.getEnvMap(ctx.allocator());
-        // defer env_map.deinit();
+        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);
+        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);
     }
 };