diff options
| -rw-r--r-- | makefile | 2 | ||||
| -rw-r--r-- | src/exe.zig | 39 | ||||
| -rw-r--r-- | src/fs.zig | 116 | ||||
| -rw-r--r-- | src/gen.zig | 134 | ||||
| -rw-r--r-- | src/main.zig | 104 |
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); +} |