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 --- src/gen.zig | 440 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 440 insertions(+) create mode 100644 src/gen.zig (limited to 'src/gen.zig') 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); + } +}; -- cgit 1.4.1