From c36b9cb651dccec5defc0826b3c43e864fd00ddf Mon Sep 17 00:00:00 2001 From: Robert Günzler Date: Thu, 17 Jul 2025 10:33:24 +0200 Subject: refactor into cli MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Robert Günzler --- makefile | 14 +- src/gen.zig | 440 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 519 +++++++++-------------------------------------------------- 3 files changed, 519 insertions(+), 454 deletions(-) create mode 100644 src/gen.zig diff --git a/makefile b/makefile index 8a24e90..37390b1 100644 --- a/makefile +++ b/makefile @@ -14,18 +14,18 @@ SRC := $(htm_SRC) $(md_SRC:%.md=%.htm) # generator ZIG ?= zig -gen_SRC := src/main.zig -gen_BIN := bin/gen +rbl_SRC := src/main.zig src/gen.zig +rbl_BIN := bin/rbl -$(gen_BIN): $(gen_SRC) +$(rbl_BIN): $(rbl_SRC) $(ZIG) build-exe -lc -O Debug -freference-trace -femit-bin=$@ $< clean: - @ rm -rf site/* bin/* $(md_SRC:%.md=%.htm) + @ rm -rf site/* $(rbl_BIN) $(md_SRC:%.md=%.htm) -gen: $(gen_BIN) $(SRC) - @ mkdir -p site/ && $(gen_BIN) +gen: $(rbl_BIN) $(SRC) + @ mkdir -p site/ && $(rbl_BIN) gen watch: @ find . -print | entr -nc -dd -- $(MAKE) @@ -35,7 +35,7 @@ watch-and-serve: was: watch-and-serve -.PHONY: all clean serve watch watch-and-serve was +.PHONY: all gen clean serve watch watch-and-serve was include serve.mk include webdeploy.mk diff --git a/src/gen.zig b/src/gen.zig new file mode 100644 index 0000000..721b08a --- /dev/null +++ b/src/gen.zig @@ -0,0 +1,440 @@ +const std = @import("std"); +const c = @cImport({ + @cInclude("time.h"); +}); + +const log = std.log.scoped(.gen); +const Context = @import("main.zig").Context; + +pub fn generate(ctx: *Context) !void { + try genIndexHtml(ctx); + + log.info("Generated index.", .{}); + + try genSite(ctx); + + log.info("Generated {d} files.", .{ctx.generated}); + log.info("Generated {d} tildes.", .{ctx.tilde}); +} + +fn genIndexHtml(ctx: *Context) !void { + const srcpathIndex = try util.joinIn(ctx.allocator(), &.{Context.params.PAGE_PATH}, &.{"index.htm"}); + defer ctx.allocator().free(srcpathIndex); + + const f = try FileSwapper.createAt(ctx.allocator(), srcpathIndex); + defer f.close(); + + try f.writer().writeAll( + \\ + \\ + ); +} + +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| { + const pname = std.fs.path.basename(p); + 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" }); + defer ctx.allocator().free(srcpath); + const dstpath = try util.joinIn(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); + + 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}); + defer ctx.allocator().free(dstdir); + + try std.fs.cwd().makePath(dstdir); + } + + const f = try std.fs.cwd().createFile(dstpath, .{}); + + const page_name = try std.mem.replaceOwned(u8, ctx.allocator(), dstname, "_", " "); + defer ctx.allocator().free(page_name); + + try build(ctx.allocator(), f, page_name, srcpath); + ctx.generated += 1; + } +} + +// 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); + defer allocator.free(mtime); + + // HEAD + try f.writer().print( + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\{[SITE_NAME]s} — {[PAGE_NAME]s} + \\ + \\ + \\
+ \\{[SITE_NAME]s} + \\ + , .{ + .SOURCE = srcpath, + .LICENSE_URL = Context.params.LICENSE_URL, + .PAGE_NAME = page_name, + .SITE_NAME = Context.params.SITE_NAME, + }); + + // NAV + try f.writer().writeAll( + \\ + \\
+ \\ + ); + + // MAIN + try f.writer().print( + \\
+ \\ + , .{}); + try inject(allocator, f.writer().any(), srcpath); + try f.writer().writeAll( + \\
+ \\ + ); + + // FOOTER + try f.writer().print( + \\ + \\ + , .{ + .LICENSE_HOLDER = Context.params.LICENSE_HOLDER, + .LICENSE_NAME = Context.params.LICENSE_NAME, + .LICENSE_URL = Context.params.LICENSE_URL, + .LICENSE_YEAR = Context.params.LICENSE_YEAR, + .MTIME = mtime, + .PATH = srcpath, + .PUBLIC_INBOX = Context.params.PUBLIC_INBOX, + .SOURCE_URL = Context.params.SOURCE_URL, + }); + + try f.writer().writeAll( + \\[⏶] + \\ + ); + + try f.writer().writeAll( + \\ + \\ + \\ + ); +} + +// 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.meta += 1; + + if (std.mem.startsWith(u8, p, "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 util.joinIn(ctx.allocator(), &.{Context.params.PAGE_PATH}, &.{p}); + defer ctx.allocator().free(srcpath); + const dstpath = try util.joinIn(ctx.allocator(), &.{Context.params.LINK_PATH}, &.{ "~", dstname }); + defer ctx.allocator().free(dstpath); + + const link = blk: { + const f = try std.fs.cwd().openFile(srcpath, .{ .mode = .read_only }); + defer f.close(); + const content = try f.readToEndAlloc(ctx.allocator(), std.os.linux.PATH_MAX); + defer ctx.allocator().free(content); + const trimmed = std.mem.trim(u8, content, " \n\t\r"); + break :blk try ctx.allocator().dupe(u8, trimmed); + }; + defer ctx.allocator().free(link); + + const f = try std.fs.cwd().createFile(dstpath, .{}); + defer f.close(); + + try f.writer().print( + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + , .{ + .LINK = link, + }); + + ctx.tilde += 1; + return; + } + + return error.unknownMeta; +} + +// parses srcpath, resolves refs and portals and writes everything to w +fn inject(allocator: std.mem.Allocator, w: std.io.AnyWriter, srcpath: []const u8) !void { + const inc = std.fs.cwd().openFile(srcpath, .{ .mode = .read_only }) catch |err| { + if (err == error.FileNotFound) + log.warn("Missing {s} include.", .{srcpath}); + + return err; + }; + defer inc.close(); + + var buf_writer = std.io.bufferedWriter(w); + var out_stream = buf_writer.writer(); + + var buf_reader = std.io.bufferedReader(inc.reader()); + var in_stream = buf_reader.reader(); + + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + + while (in_stream.readUntilDelimiterOrEofAlloc( + arena.allocator(), + '\n', + buf_reader.buf.len, + ) catch { + @panic("Failed to read from stream"); + }) |chunk_| { + defer _ = arena.reset(.free_all); + + var chunk = chunk_; + templating: while (true) { + if (std.mem.indexOfPosLinear(u8, chunk, 0, "{")) |start| { + if (std.mem.indexOfPosLinear(u8, chunk, start + 1, "}")) |end| { + try out_stream.writeAll(chunk[0..start]); + template(allocator, out_stream.any(), chunk[start + 1 .. end]) catch + @panic("Failed to resolve template"); + chunk = chunk[end + 1 ..]; + } else break :templating; + } else break :templating; + } + + try out_stream.writeAll(chunk); + try out_stream.writeByte('\n'); + try buf_writer.flush(); + } + + try buf_writer.flush(); +} + +// implements various reference types that can be used in .htm templates +fn template(allocator: std.mem.Allocator, w: std.io.AnyWriter, refname: []const u8) anyerror!void { + switch (refname[0]) { + '/' => return try portal(allocator, w, refname[1..], false), + '@' => return try ref_link(allocator, w, refname[1..]), + '+' => return try ref_name(allocator, w, refname[1..]), + else => return try ref(allocator, w, refname), + } +} + +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" }); + defer allocator.free(refpathPage); + const refpathSite = try util.joinIn(allocator, &.{Context.params.SITE_PATH}, &.{ basename, ".html" }); + defer allocator.free(refpathSite); + + if (insert_heading) + try w.print( + \\

{[NAME]s} + \\ + , .{ + .ID = basename, + .NAME = refname, + .PATH = refpathSite, + }); + + try inject(allocator, w, refpathPage); + + // XXX: track ref count? +} + +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" }); + defer allocator.free(refpathSite); + + try w.print( + \\{[NAME]s} + , .{ + .NAME = refname, + .PATH = refpathSite, + }); + + // XXX: track ref count? +} + +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" }); + defer allocator.free(refpathSite); + + try w.print("/{s}", .{refpathSite}); +} + +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 e6ed977..8e515ee 100644 --- a/src/main.zig +++ b/src/main.zig @@ -6,464 +6,69 @@ // personal site https://github.com/XXIIVV/Oscean const std = @import("std"); -const c = @cImport({ - @cInclude("time.h"); -}); +const gen = @import("gen.zig"); -const PAGE_PATH = "src/inc"; -const SITE_PATH = "site"; -const LINK_PATH = "links"; - -const SITE_NAME = "gzr.im"; -const LICENSE_HOLDER = "robertgzr"; -const LICENSE_YEAR = "2025"; -const LICENSE_NAME = "BY-NC-SA 4.0"; -const LICENSE_URL = "https://git.sr.ht/~robertgzr/site/blob/master/LICENSE.content"; -const SOURCE_URL = "https://git.sr.ht/~robertgzr/site/tree/master"; -const PUBLIC_INBOX = "~robertgzr/public-inbox@lists.sr.ht"; +const log = std.log.scoped(.root); // tune std.log options pub const std_options = std.Options{ .log_level = .info, }; -const Context = struct { - allocator: std.mem.Allocator, - pages: [][]const u8, +pub const Context = struct { + arena: std.heap.ArenaAllocator, + + pages: [][]const u8 = undefined, generated: usize = 0, meta: usize = 0, tilde: usize = 0, -}; - -// 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); -} - -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 joinIn(allocator, &.{PAGE_PATH}, &.{ basename, ".htm" }); - defer allocator.free(refpathPage); - const refpathSite = try joinIn(allocator, &.{SITE_PATH}, &.{ basename, ".html" }); - defer allocator.free(refpathSite); - - if (insert_heading) - try w.print( - \\

{[NAME]s} - \\ - , .{ - .ID = basename, - .NAME = refname, - .PATH = refpathSite, - }); - - try inject(allocator, w, refpathPage); - - // XXX: track ref count? -} - -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 joinIn(allocator, &.{SITE_PATH}, &.{ basename, ".html" }); - defer allocator.free(refpathSite); - - try w.print( - \\{[NAME]s} - , .{ - .NAME = refname, - .PATH = refpathSite, - }); - - // XXX: track ref count? -} - -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 joinIn(allocator, &.{SITE_PATH}, &.{ basename, ".html" }); - defer allocator.free(refpathSite); - - try w.print("/{s}", .{refpathSite}); -} - -fn ref_name(_: std.mem.Allocator, w: std.io.AnyWriter, refname: []const u8) anyerror!void { - try w.print("{s}", .{refname}); -} -fn template(allocator: std.mem.Allocator, w: std.io.AnyWriter, refname: []const u8) anyerror!void { - switch (refname[0]) { - '/' => return try portal(allocator, w, refname[1..], false), - '@' => return try ref_link(allocator, w, refname[1..]), - '+' => return try ref_name(allocator, w, refname[1..]), - else => return try ref(allocator, w, refname), - } -} - -// Parses srcpath, resolves refs and portals and writes everything to w -fn inject(allocator: std.mem.Allocator, w: std.io.AnyWriter, srcpath: []const u8) !void { - const inc = std.fs.cwd().openFile(srcpath, .{ .mode = .read_only }) catch |err| { - if (err == error.FileNotFound) - std.log.warn("Missing {s} include.", .{srcpath}); - - return err; + 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"; }; - defer inc.close(); - - var buf_writer = std.io.bufferedWriter(w); - var out_stream = buf_writer.writer(); - - var buf_reader = std.io.bufferedReader(inc.reader()); - var in_stream = buf_reader.reader(); - - var arena = std.heap.ArenaAllocator.init(allocator); - defer arena.deinit(); - - while (in_stream.readUntilDelimiterOrEofAlloc( - arena.allocator(), - '\n', - buf_reader.buf.len, - ) catch { - @panic("Failed to read from stream"); - }) |chunk_| { - defer _ = arena.reset(.free_all); - - var chunk = chunk_; - templating: while (true) { - if (std.mem.indexOfPosLinear(u8, chunk, 0, "{")) |start| { - if (std.mem.indexOfPosLinear(u8, chunk, start + 1, "}")) |end| { - try out_stream.writeAll(chunk[0..start]); - template(allocator, out_stream.any(), chunk[start + 1 .. end]) catch - @panic("Failed to resolve template"); - chunk = chunk[end + 1 ..]; - } else break :templating; - } else break :templating; - } - - try out_stream.writeAll(chunk); - try out_stream.writeByte('\n'); - try buf_writer.flush(); - } - - try buf_writer.flush(); -} - -fn build(allocator: std.mem.Allocator, f: std.fs.File, page_name: []const u8, srcpath: []const u8) !void { - const mtime = try getMtime(allocator, srcpath); - defer allocator.free(mtime); - - // HEAD - try f.writer().print( - \\ - \\ - \\ - \\ - \\ - \\ - \\ - \\ - \\ - \\ - \\ - \\{[SITE_NAME]s} — {[PAGE_NAME]s} - \\ - \\ - \\
- \\{[SITE_NAME]s} - \\ - , .{ - .SOURCE = srcpath, - .LICENSE_URL = LICENSE_URL, - .PAGE_NAME = page_name, - .SITE_NAME = SITE_NAME, - }); - - // NAV - try f.writer().writeAll( - \\ - \\
- \\ - ); - - // MAIN - try f.writer().print( - \\
- \\ - , .{}); - try inject(allocator, f.writer().any(), srcpath); - try f.writer().writeAll( - \\
- \\ - ); - - // FOOTER - try f.writer().print( - \\ - \\ - , .{ - .LICENSE_HOLDER = LICENSE_HOLDER, - .LICENSE_NAME = LICENSE_NAME, - .LICENSE_URL = LICENSE_URL, - .LICENSE_YEAR = LICENSE_YEAR, - .MTIME = mtime, - .PATH = srcpath, - .PUBLIC_INBOX = PUBLIC_INBOX, - .SOURCE_URL = SOURCE_URL, - }); - - try f.writer().writeAll( - \\[⏶] - \\ - ); - try f.writer().writeAll( - \\ - \\ - \\ - ); -} - -fn meta(ctx: *Context, p: []const u8) !void { - ctx.meta += 1; - - if (std.mem.startsWith(u8, p, "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 joinIn(ctx.allocator, &.{PAGE_PATH}, &.{p}); - defer ctx.allocator.free(srcpath); - const dstpath = try joinIn(ctx.allocator, &.{LINK_PATH}, &.{ "~", dstname }); - defer ctx.allocator.free(dstpath); - - const link = blk: { - const f = try std.fs.cwd().openFile(srcpath, .{ .mode = .read_only }); - defer f.close(); - const content = try f.readToEndAlloc(ctx.allocator, std.os.linux.PATH_MAX); - defer ctx.allocator.free(content); - const trimmed = std.mem.trim(u8, content, " \n\t\r"); - break :blk try ctx.allocator.dupe(u8, trimmed); + fn new(parent_alloc: std.mem.Allocator) *Context { + const ctx = parent_alloc.create(Context) catch |err| @panic(@errorName(err)); + ctx.* = .{ + .arena = std.heap.ArenaAllocator.init(parent_alloc), }; - defer ctx.allocator.free(link); - - const f = try std.fs.cwd().createFile(dstpath, .{}); - defer f.close(); - - try f.writer().print( - \\ - \\ - \\ - \\ - \\ - \\ - \\ - \\ - \\ - \\ - \\ - \\ - \\ - \\ - , .{ - .LINK = link, - }); - - ctx.tilde += 1; - return; - } - - return error.unknownMeta; -} - -fn genSite(ctx: *Context) !void { - for (ctx.pages) |p| { - const pname = std.fs.path.basename(p); - if (std.mem.startsWith(u8, pname, "meta.")) { - try meta(ctx, p); - continue; - } - - const dstname = try stripExt(p); - const srcpath = try joinIn(ctx.allocator, &.{PAGE_PATH}, &.{ dstname, ".htm" }); - defer ctx.allocator.free(srcpath); - const dstpath = try joinIn(ctx.allocator, &.{SITE_PATH}, &.{ dstname, ".html" }); - defer ctx.allocator.free(dstpath); - const rel_dstpath = try std.fs.path.relative(ctx.allocator, SITE_PATH, dstpath); - defer ctx.allocator.free(rel_dstpath); - - std.log.debug("generating {s} at {s}", .{ p, rel_dstpath }); - - if (std.fs.path.dirname(rel_dstpath)) |d| { - const dstdir = try joinIn(ctx.allocator, &.{SITE_PATH}, &.{d}); - defer ctx.allocator.free(dstdir); - - try std.fs.cwd().makePath(dstdir); - } - - const f = try std.fs.cwd().createFile(dstpath, .{}); - - const page_name = try std.mem.replaceOwned(u8, ctx.allocator, dstname, "_", " "); - defer ctx.allocator.free(page_name); - - try build(ctx.allocator, f, page_name, srcpath); - ctx.generated += 1; + return ctx; } -} - -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 deinit(self: *Context) void { + self.arena.deinit(); + self.arena.child_allocator.destroy(self); } - 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"); + pub fn allocator(self: *Context) std.mem.Allocator { + return self.arena.allocator(); } }; -fn genIndexHtml(ctx: *Context) !void { - const srcpathIndex = try joinIn(ctx.allocator, &.{PAGE_PATH}, &.{"index.htm"}); - defer ctx.allocator.free(srcpathIndex); - - const f = try FileSwapper.createAt(ctx.allocator, srcpathIndex); - defer f.close(); - - try f.writer().writeAll( - \\
    - \\ - ); - - // alphanum ascii sort - std.mem.sort( - []const u8, - ctx.pages, - {}, - struct { - fn lessthan(_: void, a: []const u8, b: []const u8) bool { - return std.ascii.lessThanIgnoreCase(a, b); - } - }.lessthan, - ); - - for (ctx.pages) |p| { - if (std.mem.startsWith(u8, p, "meta.")) continue; - - const dstname = try stripExt(p); - const dstpath = try joinIn(ctx.allocator, &.{SITE_PATH}, &.{ dstname, ".html" }); - defer ctx.allocator.free(dstpath); - - try f.writer().print( - \\
  • {[NAME]s}
  • - \\ - , .{ - .LINK = dstpath, - .NAME = dstname, - }); - } - - try f.writer().writeAll( - \\
- \\ - ); -} - -fn index(allocator: std.mem.Allocator, rootpath: []const u8) ![][]const u8 { - var root = std.fs.cwd().openDir(rootpath, .{ .iterate = true }) catch |err| switch (err) { +fn index(ctx: *Context) !void { + var root = std.fs.cwd().openDir(Context.params.PAGE_PATH, .{ .iterate = true }) catch |err| switch (err) { error.FileNotFound => { - std.log.err("Missing {s} folder.", .{PAGE_PATH}); + std.log.err("Missing {s} folder.", .{Context.params.PAGE_PATH}); std.os.linux.exit(1); }, else => |e| return e, }; defer root.close(); - var pages = std.ArrayList([]const u8).init(allocator); + var pages = std.ArrayList([]const u8).init(ctx.allocator()); defer pages.deinit(); - var walk = try root.walk(allocator); + var walk = try root.walk(ctx.allocator()); defer walk.deinit(); while (try walk.next()) |e| { @@ -471,14 +76,16 @@ fn index(allocator: std.mem.Allocator, rootpath: []const u8) ![][]const u8 { .file => { if (std.mem.endsWith(u8, e.basename, ".htm")) { std.log.debug("Indexing {s}", .{e.path}); - try pages.append(try allocator.dupe(u8, e.path)); + try pages.append(try ctx.allocator().dupe(u8, e.path)); } }, else => {}, } } - return try pages.toOwnedSlice(); + ctx.pages = try pages.toOwnedSlice(); + + return; } pub fn main() !void { @@ -486,28 +93,46 @@ pub fn main() !void { const allocator = gpa.allocator(); defer _ = gpa.deinit(); - const pages = index(allocator, PAGE_PATH) catch { - std.log.err("Failed to index pages.", .{}); + var ctx = Context.new(allocator); + defer ctx.deinit(); + + index(ctx) catch { + log.err("Failed to index pages.", .{}); std.os.linux.exit(1); }; - defer { - for (pages) |p| allocator.free(p); - allocator.free(pages); - } - std.log.info("Indexed {d} pages.", .{pages.len}); + log.info("Indexed {d} pages.", .{ctx.pages.len}); - var ctx = Context{ - .allocator = allocator, - .pages = pages, - }; + Cli.parse()(ctx); +} - try genIndexHtml(&ctx); +const RunFn = fn (*Context) void; - std.log.info("Generated index.", .{}); +const Cli = struct { + fn parse() *const RunFn { + var argv = std.process.args(); - try genSite(&ctx); + const more = argv.skip(); // skip arg0 + if (!more) return &Cli.help_cmd; - std.log.info("Generated {d} files.", .{ctx.generated}); - std.log.info("Generated {d} tildes.", .{ctx.tilde}); -} + const next_arg = argv.next() orelse return &Cli.help_cmd; + + if (std.mem.eql(u8, next_arg, "gen")) { + return &Cli.gen_cmd; + } else { + return &Cli.help_cmd; + } + } + + fn help_cmd(_: *Context) void { + std.debug.print("usage: rbl [gen]\n", .{}); + std.os.linux.exit(0); + } + + fn gen_cmd(ctx: *Context) void { + gen.generate(ctx) catch { + log.err("Failed to generate site.", .{}); + std.os.linux.exit(1); + }; + } +}; -- cgit 1.4.1