about summary refs log tree commit diff
path: root/src/gen.zig
diff options
context:
space:
mode:
authorRobert Günzler <r@gnzler.io>2025-07-17 10:33:24 +0200
committerRobert Günzler <r@gnzler.io>2025-07-17 10:33:53 +0200
commitc36b9cb651dccec5defc0826b3c43e864fd00ddf (patch)
tree7adb7d5119eb650ab598047da5bdb69c02c8b47f /src/gen.zig
parentd50c54e23412febc1a550a15b574a02ff2907eed (diff)
downloadsite-c36b9cb651dccec5defc0826b3c43e864fd00ddf.tar.gz
refactor into cli
Signed-off-by: Robert Günzler <r@gnzler.io>
Diffstat (limited to '')
-rw-r--r--src/gen.zig440
1 files changed, 440 insertions, 0 deletions
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(
+        \\<ul class="col2 captial">
+        \\
+    );
+
+    // 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 util.stripExt(p);
+        const dstpath = try util.joinIn(ctx.allocator(), &.{Context.params.SITE_PATH}, &.{ dstname, ".html" });
+        defer ctx.allocator().free(dstpath);
+
+        try f.writer().print(
+            \\<li><a href="/{[LINK]s}">{[NAME]s}</a></li>
+            \\
+        , .{
+            .LINK = dstpath,
+            .NAME = dstname,
+        });
+    }
+
+    try f.writer().writeAll(
+        \\</ul>
+        \\
+    );
+}
+
+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(
+        \\<!-- Generated file, do not edit, you may edit and discard {[SOURCE]s} :) -->
+        \\<!DOCTYPE html>
+        \\<html lang="en">
+        \\<head>
+        \\<meta charset="utf-8">
+        \\<meta name="thumbnail" content="/media/services/thumbnail.jpg">
+        \\<meta name="viewport" content="width=device-width,initial-scale=1">
+        \\<link rel="alternate" type="application/rss+xml" title="RSS Feed" href="/links/rss.xml">
+        \\<link rel="stylesheet" type="text/css" href="/links/main.css">
+        \\<link rel="shortcut icon" type="image/png" href="/media/services/icon.png">
+        \\<link rel="license" href="{[LICENSE_URL]s}">
+        \\<title>{[SITE_NAME]s} &mdash; {[PAGE_NAME]s}</title>
+        \\</head>
+        \\<body>
+        \\<header>
+        \\<a href="/"><img src="/media/interface/logo.svg" alt="{[SITE_NAME]s}"></a>
+        \\
+    , .{
+        .SOURCE = srcpath,
+        .LICENSE_URL = Context.params.LICENSE_URL,
+        .PAGE_NAME = page_name,
+        .SITE_NAME = Context.params.SITE_NAME,
+    });
+
+    // NAV
+    try f.writer().writeAll(
+        \\<nav>
+        \\
+    );
+    portal(allocator, f.writer().any(), "meta.nav", false) catch {}; // ignore error
+    try f.writer().writeAll(
+        \\</nav>
+        \\</header>
+        \\
+    );
+
+    // MAIN
+    try f.writer().print(
+        \\<main>
+        \\
+    , .{});
+    try inject(allocator, f.writer().any(), srcpath);
+    try f.writer().writeAll(
+        \\</main>
+        \\
+    );
+
+    // FOOTER
+    try f.writer().print(
+        \\<footer>
+        \\Edited on {[MTIME]s} <a href="{[SOURCE_URL]s}/{[PATH]s}" target="_blank">[src]</a>
+        \\<a href="mailto:{[PUBLIC_INBOX]s}">[comments]</a>
+        \\<br>
+        \\<b>{[LICENSE_HOLDER]s}</b>
+        \\&copy; {[LICENSE_YEAR]s}
+        \\&mdash;
+        \\<a href="{[LICENSE_URL]s}" target="_blank">{[LICENSE_NAME]s}</a>
+        \\</footer>
+        \\
+    , .{
+        .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(
+        \\<a href="#top">[&#9206;]</a>
+        \\
+    );
+
+    try f.writer().writeAll(
+        \\</body>
+        \\</html>
+        \\
+    );
+}
+
+// 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(
+            \\<!DOCTYPE html>
+            \\<html lang="en">
+            \\<head>
+            \\<meta charset="utf-8">
+            \\<meta http-equiv="refresh" content="0; url={[LINK]s}">
+            \\<meta name="fediverse:creator" content="@robertgzr@toot.berlin">
+            \\<link rel="stylesheet" type="text/css" href="links/main.css">
+            \\</head>
+            \\<body>
+            \\<noscript>Continue to <a href="{[LINK]s}">{[LINK]s}</a>.</noscript>
+            \\<script>
+            \\'use strict'
+            \\window.location.replace("{[LINK]s}")
+            \\</script>
+            \\</body>
+            \\</html>
+            \\
+        , .{
+            .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(
+            \\<h2 id="{[ID]s}"><a href="/{[PATH]s}">{[NAME]s}</a></2>
+            \\
+        , .{
+            .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(
+        \\<a href="/{[PATH]s}">{[NAME]s}</a>
+    , .{
+        .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);
+    }
+};