about summary refs log tree commit diff
path: root/src/main.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.zig')
-rw-r--r--src/main.zig519
1 files changed, 72 insertions, 447 deletions
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(
-            \\<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 joinIn(allocator, &.{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 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(
-        \\<!-- 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 = LICENSE_URL,
-        .PAGE_NAME = page_name,
-        .SITE_NAME = 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 = 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(
-        \\<a href="#top">[&#9206;]</a>
-        \\
-    );
 
-    try f.writer().writeAll(
-        \\</body>
-        \\</html>
-        \\
-    );
-}
-
-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(
-            \\<!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;
-}
-
-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(
-        \\<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 stripExt(p);
-        const dstpath = try joinIn(ctx.allocator, &.{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>
-        \\
-    );
-}
-
-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);
+        };
+    }
+};