about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--makefile2
-rw-r--r--src/exe.zig39
-rw-r--r--src/fs.zig116
-rw-r--r--src/gen.zig134
-rw-r--r--src/main.zig104
5 files changed, 258 insertions, 137 deletions
diff --git a/makefile b/makefile
index 37390b1..2cdd5e4 100644
--- a/makefile
+++ b/makefile
@@ -14,7 +14,7 @@ SRC     := $(htm_SRC) $(md_SRC:%.md=%.htm)
 
 # generator
 ZIG     ?= zig
-rbl_SRC := src/main.zig src/gen.zig
+rbl_SRC := src/main.zig src/fs.zig src/gen.zig src/exe.zig
 rbl_BIN := bin/rbl
 
 $(rbl_BIN): $(rbl_SRC)
diff --git a/src/exe.zig b/src/exe.zig
new file mode 100644
index 0000000..ef38a67
--- /dev/null
+++ b/src/exe.zig
@@ -0,0 +1,39 @@
+const std = @import("std");
+
+const log = std.log.scoped(.exe);
+const Context = @import("main.zig").Context;
+const fs = @import("fs.zig");
+
+pub fn run_pandoc(ctx: *Context) !void {
+    ctx.progress_start("Processing markdown files");
+    defer ctx.progress_end();
+
+    for (ctx.markdown.items) |page| {
+        const targetpath = try fs.replace_extension(ctx.allocator(), page, "md");
+        defer ctx.allocator().free(targetpath);
+
+        const f = try fs.FileSwapper.createAt(ctx.allocator(), targetpath);
+        defer f.close();
+
+        try run(ctx, &.{ "notify-send", "hi", page });
+    }
+    // @ echo "<!-- Generated file, you may edit and discard $< :) -->" >$@
+    // $(PANDOC) -fmarkdown+wikilinks_title_after_pipe+autolink_bare_uris -thtml5 --lua-filter=$(PANDOC_PREPROC) --strip-comments $< >>$@
+}
+
+pub fn run(ctx: *Context, argv: []const []const u8) !void {
+    const command = try std.mem.join(ctx.allocator(), " ", argv);
+    defer ctx.allocator().free(command);
+    const label = try std.fmt.allocPrint(ctx.allocator(), "running {s}", .{command});
+    defer ctx.allocator().free(label);
+
+    ctx.progress_start(label);
+    defer ctx.progress_end();
+
+    const result = try std.process.Child.run(.{
+        .argv = argv,
+        .allocator = std.heap.page_allocator,
+        .progress_node = ctx.progress_nodes.getLast(),
+    });
+    _ = result;
+}
diff --git a/src/fs.zig b/src/fs.zig
new file mode 100644
index 0000000..c009afd
--- /dev/null
+++ b/src/fs.zig
@@ -0,0 +1,116 @@
+const std = @import("std");
+const c = @cImport({
+    @cInclude("time.h");
+});
+
+pub const FileSwapper = struct {
+    allocator: std.mem.Allocator,
+    target_path: []const u8,
+    tmp_path: []const u8,
+    tmp: std.fs.File,
+
+    pub fn createAt(alloc: std.mem.Allocator, path: []const u8) !FileSwapper {
+        const target_dir = std.fs.path.dirname(path) orelse ".";
+        const target_name = std.fs.path.basename(path);
+
+        const tmp_name = try std.fmt.allocPrint(
+            alloc,
+            ".{s}",
+            .{target_name},
+        );
+        defer alloc.free(tmp_name);
+
+        const tmp_path = try std.fs.path.join(
+            alloc,
+            &.{ target_dir, tmp_name },
+        );
+
+        const f = try std.fs.cwd().createFile(tmp_path, .{});
+
+        return .{
+            .allocator = alloc,
+            .target_path = path,
+            .tmp_path = tmp_path,
+            .tmp = f,
+        };
+    }
+
+    pub fn writer(self: FileSwapper) std.fs.File.Writer {
+        return self.tmp.writer();
+    }
+
+    pub fn close(self: FileSwapper) void {
+        defer self.allocator.free(self.tmp_path);
+        defer self.tmp.close();
+
+        // flush writes
+        self.tmp.sync() catch
+            @panic("sync tmp file");
+
+        // swap file into final location
+        std.fs.rename(
+            std.fs.cwd(),
+            self.tmp_path,
+            std.fs.cwd(),
+            self.target_path,
+        ) catch
+            @panic("rename tmp file");
+    }
+};
+
+pub fn replace_extension(alloc: std.mem.Allocator, p: []const u8, new: []const u8) ![]const u8 {
+    const dirname = std.fs.path.dirname(p) orelse ".";
+    const basename = try strip_extension(p);
+    return try join_path_and_filename(
+        alloc,
+        &.{dirname},
+        &.{ basename, ".", new },
+    );
+}
+
+// also trims off base paths
+pub fn strip_extension(path: []const u8) ![]const u8 {
+    const ext = std.fs.path.extension(path);
+    const ext_idx = std.mem.indexOfPosLinear(u8, path, 0, ext);
+    std.debug.assert(ext_idx != null);
+    return path[0..ext_idx.?];
+}
+
+pub fn join_path_and_filename(alloc: std.mem.Allocator, paths: []const []const u8, filename_parts: []const []const u8) ![]const u8 {
+    const filename = try std.mem.concat(alloc, u8, filename_parts);
+    defer alloc.free(filename);
+
+    var p = try std.ArrayList([]const u8).initCapacity(alloc, paths.len + 1);
+    defer p.deinit();
+
+    p.appendSliceAssumeCapacity(paths);
+    p.appendAssumeCapacity(filename);
+
+    const paths2 = try p.toOwnedSlice();
+    defer alloc.free(paths2);
+
+    std.debug.assert(paths.len + 1 == paths2.len);
+    return try std.fs.path.join(alloc, paths2);
+}
+
+pub fn get_mtime(alloc: std.mem.Allocator, path: []const u8) ![]const u8 {
+    const attr = try alloc.create(std.os.linux.Stat);
+    defer alloc.destroy(attr);
+
+    const pathZ = try alloc.dupeZ(u8, path);
+    defer alloc.free(pathZ);
+
+    // use stat to get mtime
+    _ = std.os.linux.stat(pathZ, attr);
+
+    // use libc strftime to format time string
+    const mtime = attr.mtime();
+    const mtime_str = try alloc.alloc(u8, 512);
+    const n = c.strftime(
+        mtime_str.ptr,
+        mtime_str.len,
+        "%F",
+        c.localtime(&mtime.sec),
+    );
+    return try alloc.realloc(mtime_str, n);
+}
diff --git a/src/gen.zig b/src/gen.zig
index 721b08a..3ea0bc2 100644
--- a/src/gen.zig
+++ b/src/gen.zig
@@ -1,12 +1,13 @@
 const std = @import("std");
-const c = @cImport({
-    @cInclude("time.h");
-});
 
 const log = std.log.scoped(.gen);
 const Context = @import("main.zig").Context;
+const fs = @import("fs.zig");
 
 pub fn generate(ctx: *Context) !void {
+    ctx.progress_start("generate");
+    defer ctx.progress_end();
+
     try genIndexHtml(ctx);
 
     log.info("Generated index.", .{});
@@ -18,10 +19,13 @@ pub fn generate(ctx: *Context) !void {
 }
 
 fn genIndexHtml(ctx: *Context) !void {
-    const srcpathIndex = try util.joinIn(ctx.allocator(), &.{Context.params.PAGE_PATH}, &.{"index.htm"});
+    ctx.progress_start("index.html");
+    defer ctx.progress_end();
+
+    const srcpathIndex = try fs.join_path_and_filename(ctx.allocator(), &.{Context.params.PAGE_PATH}, &.{"index.htm"});
     defer ctx.allocator().free(srcpathIndex);
 
-    const f = try FileSwapper.createAt(ctx.allocator(), srcpathIndex);
+    const f = try fs.FileSwapper.createAt(ctx.allocator(), srcpathIndex);
     defer f.close();
 
     try f.writer().writeAll(
@@ -32,7 +36,7 @@ fn genIndexHtml(ctx: *Context) !void {
     // alphanum ascii sort
     std.mem.sort(
         []const u8,
-        ctx.pages,
+        ctx.pages.items,
         {},
         struct {
             fn lessthan(_: void, a: []const u8, b: []const u8) bool {
@@ -41,11 +45,11 @@ fn genIndexHtml(ctx: *Context) !void {
         }.lessthan,
     );
 
-    for (ctx.pages) |p| {
+    for (ctx.pages.items) |p| {
         if (std.mem.startsWith(u8, p, "meta.")) continue;
 
-        const dstname = try util.stripExt(p);
-        const dstpath = try util.joinIn(ctx.allocator(), &.{Context.params.SITE_PATH}, &.{ dstname, ".html" });
+        const dstname = try fs.strip_extension(p);
+        const dstpath = try fs.join_path_and_filename(ctx.allocator(), &.{Context.params.SITE_PATH}, &.{ dstname, ".html" });
         defer ctx.allocator().free(dstpath);
 
         try f.writer().print(
@@ -63,58 +67,22 @@ fn genIndexHtml(ctx: *Context) !void {
     );
 }
 
-const FileSwapper = struct {
-    allocator: std.mem.Allocator,
-    target_path: []const u8,
-    tmp_path: []const u8,
-    tmp: std.fs.File,
-
-    fn createAt(allocator: std.mem.Allocator, path: []const u8) !FileSwapper {
-        const target_dir = std.fs.path.dirname(path) orelse ".";
-        const target_name = std.fs.path.basename(path);
-
-        const tmp_name = try std.fmt.allocPrint(allocator, ".{s}", .{target_name});
-        defer allocator.free(tmp_name);
-
-        const tmp_path = try std.fs.path.join(allocator, &.{ target_dir, tmp_name });
-
-        const f = try std.fs.cwd().createFile(tmp_path, .{});
-
-        return .{
-            .allocator = allocator,
-            .target_path = path,
-            .tmp_path = tmp_path,
-            .tmp = f,
-        };
-    }
-
-    fn writer(self: FileSwapper) std.fs.File.Writer {
-        return self.tmp.writer();
-    }
-
-    fn close(self: FileSwapper) void {
-        defer self.allocator.free(self.tmp_path);
-        defer self.tmp.close();
-
-        self.tmp.sync() catch @panic("sync tmp file");
-
-        std.debug.print("??? {s} => {s}\n", .{ self.tmp_path, self.target_path });
-        std.fs.rename(std.fs.cwd(), self.tmp_path, std.fs.cwd(), self.target_path) catch @panic("rename tmp file");
-    }
-};
-
 fn genSite(ctx: *Context) !void {
-    for (ctx.pages) |p| {
+    for (ctx.pages.items) |p| {
         const pname = std.fs.path.basename(p);
+
+        ctx.progress_start(pname);
+        defer ctx.progress_end();
+
         if (std.mem.startsWith(u8, pname, "meta.")) {
             try meta(ctx, p);
             continue;
         }
 
-        const dstname = try util.stripExt(p);
-        const srcpath = try util.joinIn(ctx.allocator(), &.{Context.params.PAGE_PATH}, &.{ dstname, ".htm" });
+        const dstname = try fs.strip_extension(p);
+        const srcpath = try fs.join_path_and_filename(ctx.allocator(), &.{Context.params.PAGE_PATH}, &.{ dstname, ".htm" });
         defer ctx.allocator().free(srcpath);
-        const dstpath = try util.joinIn(ctx.allocator(), &.{Context.params.SITE_PATH}, &.{ dstname, ".html" });
+        const dstpath = try fs.join_path_and_filename(ctx.allocator(), &.{Context.params.SITE_PATH}, &.{ dstname, ".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);
@@ -122,7 +90,7 @@ fn genSite(ctx: *Context) !void {
         log.debug("generating {s} at {s}", .{ p, rel_dstpath });
 
         if (std.fs.path.dirname(rel_dstpath)) |d| {
-            const dstdir = try util.joinIn(ctx.allocator(), &.{Context.params.SITE_PATH}, &.{d});
+            const dstdir = try fs.join_path_and_filename(ctx.allocator(), &.{Context.params.SITE_PATH}, &.{d});
             defer ctx.allocator().free(dstdir);
 
             try std.fs.cwd().makePath(dstdir);
@@ -140,7 +108,7 @@ fn genSite(ctx: *Context) !void {
 
 // build a single html file
 fn build(allocator: std.mem.Allocator, f: std.fs.File, page_name: []const u8, srcpath: []const u8) !void {
-    const mtime = try util.getMtime(allocator, srcpath);
+    const mtime = try fs.get_mtime(allocator, srcpath);
     defer allocator.free(mtime);
 
     // HEAD
@@ -238,9 +206,9 @@ fn meta(ctx: *Context, p: []const u8) !void {
     if (std.mem.startsWith(u8, p, "meta.tilde.")) {
         const dot = std.mem.indexOfPosLinear(u8, p, 11, ".");
         const dstname = p[11..dot.?];
-        const srcpath = try util.joinIn(ctx.allocator(), &.{Context.params.PAGE_PATH}, &.{p});
+        const srcpath = try fs.join_path_and_filename(ctx.allocator(), &.{Context.params.PAGE_PATH}, &.{p});
         defer ctx.allocator().free(srcpath);
-        const dstpath = try util.joinIn(ctx.allocator(), &.{Context.params.LINK_PATH}, &.{ "~", dstname });
+        const dstpath = try fs.join_path_and_filename(ctx.allocator(), &.{Context.params.LINK_PATH}, &.{ "~", dstname });
         defer ctx.allocator().free(dstpath);
 
         const link = blk: {
@@ -346,9 +314,9 @@ fn template(allocator: std.mem.Allocator, w: std.io.AnyWriter, refname: []const
 fn portal(allocator: std.mem.Allocator, w: std.io.AnyWriter, refname: []const u8, insert_heading: bool) anyerror!void {
     const basename = try std.mem.replaceOwned(u8, allocator, refname, " ", "_");
     defer allocator.free(basename);
-    const refpathPage = try util.joinIn(allocator, &.{Context.params.PAGE_PATH}, &.{ basename, ".htm" });
+    const refpathPage = try fs.join_path_and_filename(allocator, &.{Context.params.PAGE_PATH}, &.{ basename, ".htm" });
     defer allocator.free(refpathPage);
-    const refpathSite = try util.joinIn(allocator, &.{Context.params.SITE_PATH}, &.{ basename, ".html" });
+    const refpathSite = try fs.join_path_and_filename(allocator, &.{Context.params.SITE_PATH}, &.{ basename, ".html" });
     defer allocator.free(refpathSite);
 
     if (insert_heading)
@@ -369,7 +337,7 @@ fn portal(allocator: std.mem.Allocator, w: std.io.AnyWriter, refname: []const u8
 fn ref(allocator: std.mem.Allocator, w: std.io.AnyWriter, refname: []const u8) anyerror!void {
     const basename = try std.mem.replaceOwned(u8, allocator, refname, " ", "_");
     defer allocator.free(basename);
-    const refpathSite = try util.joinIn(allocator, &.{Context.params.SITE_PATH}, &.{ basename, ".html" });
+    const refpathSite = try fs.join_path_and_filename(allocator, &.{Context.params.SITE_PATH}, &.{ basename, ".html" });
     defer allocator.free(refpathSite);
 
     try w.print(
@@ -385,7 +353,7 @@ fn ref(allocator: std.mem.Allocator, w: std.io.AnyWriter, refname: []const u8) a
 fn ref_link(allocator: std.mem.Allocator, w: std.io.AnyWriter, refname: []const u8) anyerror!void {
     const basename = try std.mem.replaceOwned(u8, allocator, refname, " ", "_");
     defer allocator.free(basename);
-    const refpathSite = try util.joinIn(allocator, &.{Context.params.SITE_PATH}, &.{ basename, ".html" });
+    const refpathSite = try fs.join_path_and_filename(allocator, &.{Context.params.SITE_PATH}, &.{ basename, ".html" });
     defer allocator.free(refpathSite);
 
     try w.print("/{s}", .{refpathSite});
@@ -394,47 +362,3 @@ fn ref_link(allocator: std.mem.Allocator, w: std.io.AnyWriter, refname: []const
 fn ref_name(_: std.mem.Allocator, w: std.io.AnyWriter, refname: []const u8) anyerror!void {
     try w.print("{s}", .{refname});
 }
-
-const util = struct {
-    // also trims off base paths
-    fn stripExt(path: []const u8) ![]const u8 {
-        const ext = std.fs.path.extension(path);
-        const ext_idx = std.mem.indexOfPosLinear(u8, path, 0, ext);
-        std.debug.assert(ext_idx != null);
-        return path[0..ext_idx.?];
-    }
-
-    fn joinIn(allocator: std.mem.Allocator, paths: []const []const u8, filename_parts: []const []const u8) ![]u8 {
-        const filename = try std.mem.concat(allocator, u8, filename_parts);
-        defer allocator.free(filename);
-
-        var p = try std.ArrayList([]const u8).initCapacity(allocator, paths.len + 1);
-        defer p.deinit();
-
-        p.appendSliceAssumeCapacity(paths);
-        p.appendAssumeCapacity(filename);
-
-        const paths2 = try p.toOwnedSlice();
-        defer allocator.free(paths2);
-
-        std.debug.assert(paths.len + 1 == paths2.len);
-        return try std.fs.path.join(allocator, paths2);
-    }
-
-    fn getMtime(allocator: std.mem.Allocator, srcpath: []const u8) ![]u8 {
-        const attr = try allocator.create(std.os.linux.Stat);
-        defer allocator.destroy(attr);
-        const srcpathZ = try allocator.dupeZ(u8, srcpath);
-        defer allocator.free(srcpathZ);
-        _ = std.os.linux.stat(srcpathZ, attr);
-        const mtime = attr.mtime();
-        const mtime_str = try allocator.alloc(u8, 512);
-        const n = c.strftime(
-            mtime_str.ptr,
-            mtime_str.len,
-            "%F",
-            c.localtime(&mtime.sec),
-        );
-        return try allocator.realloc(mtime_str, n);
-    }
-};
diff --git a/src/main.zig b/src/main.zig
index 8e515ee..a32d968 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -7,6 +7,7 @@
 
 const std = @import("std");
 const gen = @import("gen.zig");
+const exe = @import("exe.zig");
 
 const log = std.log.scoped(.root);
 
@@ -17,8 +18,11 @@ pub const std_options = std.Options{
 
 pub const Context = struct {
     arena: std.heap.ArenaAllocator,
+    progress_nodes: std.ArrayList(std.Progress.Node),
+
+    pages: std.ArrayListUnmanaged([]const u8),
+    markdown: std.ArrayListUnmanaged([]const u8),
 
-    pages: [][]const u8 = undefined,
     generated: usize = 0,
     meta: usize = 0,
     tilde: usize = 0,
@@ -37,15 +41,24 @@ pub const Context = struct {
         pub const PUBLIC_INBOX = "~robertgzr/public-inbox@lists.sr.ht";
     };
 
-    fn new(parent_alloc: std.mem.Allocator) *Context {
-        const ctx = parent_alloc.create(Context) catch |err| @panic(@errorName(err));
-        ctx.* = .{
+    fn create(parent_alloc: std.mem.Allocator) !*Context {
+        const self = try parent_alloc.create(Context);
+        self.* = .{
             .arena = std.heap.ArenaAllocator.init(parent_alloc),
+            .progress_nodes = try .initCapacity(self.allocator(), 10),
+            .pages = .{},
+            .markdown = .{},
         };
-        return ctx;
+        // create root progress node
+        self.progress_nodes.appendAssumeCapacity(
+            std.Progress.start(.{ .root_name = "" }),
+        );
+        return self;
     }
 
-    fn deinit(self: *Context) void {
+    fn destroy(self: *Context) void {
+        self.progress_end();
+        self.progress_nodes.deinit();
         self.arena.deinit();
         self.arena.child_allocator.destroy(self);
     }
@@ -53,9 +66,22 @@ pub const Context = struct {
     pub fn allocator(self: *Context) std.mem.Allocator {
         return self.arena.allocator();
     }
+
+    // progress handling
+    pub fn progress_start(self: *Context, name: []const u8) void {
+        const new = self.progress_nodes.getLast().start(name, 0);
+        self.progress_nodes.appendAssumeCapacity(new);
+    }
+    pub fn progress_end(self: *Context) void {
+        const current = self.progress_nodes.pop() orelse return;
+        current.end();
+    }
 };
 
 fn index(ctx: *Context) !void {
+    ctx.progress_start("index");
+    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});
@@ -65,47 +91,34 @@ fn index(ctx: *Context) !void {
     };
     defer root.close();
 
-    var pages = std.ArrayList([]const u8).init(ctx.allocator());
-    defer pages.deinit();
-
     var walk = try root.walk(ctx.allocator());
     defer walk.deinit();
 
     while (try walk.next()) |e| {
         switch (e.kind) {
             .file => {
+                ctx.progress_start(e.basename);
+                defer ctx.progress_end();
+
                 if (std.mem.endsWith(u8, e.basename, ".htm")) {
                     std.log.debug("Indexing {s}", .{e.path});
-                    try pages.append(try ctx.allocator().dupe(u8, 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));
                 }
             },
             else => {},
         }
     }
 
-    ctx.pages = try pages.toOwnedSlice();
+    log.info("Indexed {d} pages.", .{ctx.pages.items.len});
+    log.info("Indexed {d} markdown files.", .{ctx.markdown.items.len});
 
     return;
 }
 
-pub fn main() !void {
-    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
-    const allocator = gpa.allocator();
-    defer _ = gpa.deinit();
-
-    var ctx = Context.new(allocator);
-    defer ctx.deinit();
-
-    index(ctx) catch {
-        log.err("Failed to index pages.", .{});
-        std.os.linux.exit(1);
-    };
-
-    log.info("Indexed {d} pages.", .{ctx.pages.len});
-
-    Cli.parse()(ctx);
-}
-
 const RunFn = fn (*Context) void;
 
 const Cli = struct {
@@ -119,20 +132,49 @@ const Cli = struct {
 
         if (std.mem.eql(u8, next_arg, "gen")) {
             return &Cli.gen_cmd;
+        } else if (std.mem.eql(u8, next_arg, "md")) {
+            return &Cli.gen_md_cmd;
         } else {
             return &Cli.help_cmd;
         }
     }
 
     fn help_cmd(_: *Context) void {
-        std.debug.print("usage: rbl [gen]\n", .{});
-        std.os.linux.exit(0);
+        std.debug.print("usage: rbl [gen|md]\n\n", .{});
     }
 
     fn gen_cmd(ctx: *Context) void {
+        index(ctx) catch {
+            log.err("Failed to index pages.", .{});
+            std.os.linux.exit(1);
+        };
+
         gen.generate(ctx) catch {
             log.err("Failed to generate site.", .{});
             std.os.linux.exit(1);
         };
     }
+
+    fn gen_md_cmd(ctx: *Context) void {
+        index(ctx) catch {
+            log.err("Failed to index pages.", .{});
+            std.os.linux.exit(1);
+        };
+
+        exe.run_pandoc(ctx) catch {
+            log.err("Failed to process markdown files.", .{});
+            std.os.linux.exit(1);
+        };
+    }
 };
+
+pub fn main() !void {
+    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+    const allocator = gpa.allocator();
+    defer _ = gpa.deinit();
+
+    var ctx = try Context.create(allocator);
+    defer ctx.destroy();
+
+    Cli.parse()(ctx);
+}