From 5bc242742e1225c7c3b87a31c2c2a3998ac18685 Mon Sep 17 00:00:00 2001
From: Robert Günzler
Date: Fri, 18 Sep 2026 04:00:18 +0200
Subject: v2
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: Robert Günzler
---
.webdeployignore | 1 +
makefile | 54 +--
rbc.mk | 28 ++
src/Content.zig | 210 ------------
src/Context.zig | 242 --------------
src/FileSwapper.zig | 101 ------
src/gen.zig | 255 ---------------
src/inc/2025.htm | 12 +-
src/inc/2025.md | 12 +-
src/inc/2026.htm | 2 +-
src/inc/2026.md | 2 +-
src/inc/japan.htm | 16 +-
src/inc/meta.baseof.htm | 36 ++
src/inc/meta.nav.htm | 12 +-
src/inc/nikko.htm | 2 +-
src/inc/nikko.md | 2 +-
src/inc/notes.htm | 12 +-
src/inc/notes.md | 12 +-
src/main.zig | 782 ++++++++++++++++++++++++++++++++++++++------
src/os.zig | 84 -----
testdata/fragment.htm | 3 +
testdata/meta.baseof.htm | 15 +
testdata/meta.nav.htm | 6 +
testdata/meta.tilde.foo.htm | 1 +
testdata/only-copy.html | 3 +
testdata/page.htm | 12 +
26 files changed, 864 insertions(+), 1053 deletions(-)
create mode 100644 rbc.mk
delete mode 100644 src/Content.zig
delete mode 100644 src/Context.zig
delete mode 100644 src/FileSwapper.zig
delete mode 100644 src/gen.zig
create mode 100644 src/inc/meta.baseof.htm
delete mode 100644 src/os.zig
create mode 100644 testdata/fragment.htm
create mode 100644 testdata/meta.baseof.htm
create mode 100644 testdata/meta.nav.htm
create mode 100644 testdata/meta.tilde.foo.htm
create mode 100644 testdata/only-copy.html
create mode 100644 testdata/page.htm
diff --git a/.webdeployignore b/.webdeployignore
index 753fee2..547ab76 100644
--- a/.webdeployignore
+++ b/.webdeployignore
@@ -4,6 +4,7 @@
- bin/
- src/
+- testdata/
- makefile
- *.mk
- vcs
diff --git a/makefile b/makefile
index 0348ce2..b495f54 100644
--- a/makefile
+++ b/makefile
@@ -1,41 +1,38 @@
+PANDOC := pandoc
+ENTR := entr
+CADDY := caddy
+
all: gen
-# generator
-ZIG ?= /usr/bin/zig-bin-0.16.0
-rbc_SRC := src/main.zig $(wildcard src/*.zig)
-rbc_BIN := bin/rbc
+site_DIR := site
+htm_DIR := src/inc
-ifdef REL
-rbc_OPT := ReleaseFast
-else
-rbc_OPT := Debug
-endif
+htm_SRC := $(shell find $(htm_DIR) -name '*.htm' -not -name 'meta.*' -print)
+htm_OUT := $(subst $(htm_DIR),$(site_DIR),$(htm_SRC:%.htm=%.html))
-$(rbc_BIN): $(rbc_SRC)
- @ mkdir -p $(dir $@)
- $(ZIG) build-exe -lc -O $(rbc_OPT) -freference-trace -femit-bin=$@ $<
+mdn_SRC := $(wildcard $(htm_DIR)/*.md)
+mdn_OUT := $(mdn_SRC:%.md=%.htm)
-rbc: $(rbc_BIN)
+gen: $(rbc_BIN) $(mdn_OUT) $(htm_OUT)
-check:
- $(ZIG) test -lc -O Debug -freference-trace --test-filter Context src/Context.zig
+gen-htm: $(htm_OUT)
+ $(MAKE) $(htm_OUT)
-clean:
- @ rm -rf $(rbc_BIN)
+$(site_DIR)/%.html: $(htm_DIR)/%.htm
+ $(rbc_BIN) $< $(site_DIR)
-site-clean:
- @ rm -rf site/* $(md_SRC:%.md=%.htm)
+gen-mdn: $(mdn_OUT)
+ $(MAKE) $(mdn_OUT)
-gen: $(rbc_BIN)
- # TODO: fix the panic
- @ $(rbc_BIN) md
- @ $(rbc_BIN) gen
+$(mdn_OUT): %.htm: %.md
+ $(PANDOC) --from=markdown+wikilinks_title_after_pipe+autolink_bare_uris --to=html5 --lua-filter=./preproc.lua --strip-comments --output=$@ $<
watch:
- @ $(rbc_BIN) watch
+ @ printf '%s\n' $(htm_SRC) $(mdn_SRC) | $(ENTR) -cd -- $(MAKE) /_
serve:
- @ $(rbc_BIN) serve
+ @ $(CADDY) fmt --overwrite
+ @ $(CADDY) run --watch
watch-and-serve:
@ ${MAKE} -j2 watch serve
@@ -43,7 +40,12 @@ watch-and-serve:
was: watch-and-serve
dev: watch-and-serve
-.PHONY: gen clean serve watch watch-and-serve was
+site-clean:
+ rm -rf site/*
+ rm -rf $(mdn_SRC:%.md=%.htm)
+
+.PHONY: gen serve watch watch-and-serve was site-clean
include webdeploy.mk
include edit.mk
+include rbc.mk
diff --git a/rbc.mk b/rbc.mk
new file mode 100644
index 0000000..d0c7b35
--- /dev/null
+++ b/rbc.mk
@@ -0,0 +1,28 @@
+ZIG ?= /usr/bin/zig-bin-0.16.0
+
+# generator
+rbc_SRC := src/main.zig
+rbc_BIN := bin/rbc
+
+ifdef REL
+rbc_OPT := ReleaseFast
+else
+rbc_OPT := Debug
+endif
+
+$(rbc_BIN): $(rbc_SRC)
+ @ mkdir -p $(dir $@)
+ $(ZIG) build-exe -lc -O $(rbc_OPT) -freference-trace -femit-bin=$@ $<
+
+
+build-rbc: $(rbc_BIN)
+
+check-rbc: FILTER ?= e2e
+check-rbc:
+ $(ZIG) test -lc -O Debug -freference-trace $(rbc_SRC) --test-filter '$(FILTER)'
+
+clean-rbc:
+ @ rm -rf $(rbc_BIN)
+
+
+.PHONY: build-rbc check-rbc clean-rbc
diff --git a/src/Content.zig b/src/Content.zig
deleted file mode 100644
index 120d8b5..0000000
--- a/src/Content.zig
+++ /dev/null
@@ -1,210 +0,0 @@
-const std = @import("std");
-const os = @import("os.zig");
-
-const Content = @This();
-const log = std.log.scoped(.Content);
-
-pub const Digest = [64]u8;
-
-io: std.Io,
-arena: *std.heap.ArenaAllocator,
-root: std.Io.Dir,
-
-pub fn init(io: std.Io, allocator: std.mem.Allocator, root_dir: std.Io.Dir) !Content {
- const arena = try allocator.create(std.heap.ArenaAllocator);
- errdefer allocator.destroy(arena);
- arena.* = std.heap.ArenaAllocator.init(allocator);
-
- return .{
- .io = io,
- .arena = arena,
- .root = root_dir,
- };
-}
-
-pub fn deinit(self: *Content) void {
- self.root.close(self.io);
- const allocator = self.arena.child_allocator;
- self.arena.deinit();
- allocator.destroy(self.arena);
-}
-
-pub fn digest(self: *Content, dir: std.Io.Dir, sub_path: []const u8) !Digest {
- // generate sha256 digest
- // TODO: use zig-based hashing?
- const command = blk: {
- var buf: [std.os.linux.PATH_MAX + 10]u8 = undefined;
- @memcpy(buf[0..10], "sha256sum ");
- @memcpy(buf[10 .. 10 + sub_path.len], sub_path);
- break :blk buf[0 .. 10 + sub_path.len];
- };
- var buf: [std.os.linux.PATH_MAX + 64]u8 = undefined;
- var w = std.Io.Writer.fixed(&buf);
- try os.captureStdout(self.io, command, .{ .cwd = .{ .dir = dir } }, &w);
-
- // take only the digest
- return buf[0..64].*;
-}
-
-pub fn get(self: *Content, dgst: Digest) !?Digest {
- const found = blk: {
- self.root.access(self.io, &dgst, .{}) catch |err| switch (err) {
- error.FileNotFound => break :blk false,
- else => return err,
- };
- break :blk true;
- };
- if (found) {
- return dgst;
- } else {
- return null;
- }
-}
-
-pub fn add(self: *Content, dir: std.Io.Dir, sub_path: []const u8) !Digest {
- const dgst = try self.digest(dir, sub_path);
-
- return (try self.get(dgst)) orelse {
- // create digest directory
- try self.root.createDirPath(self.io, &dgst);
-
- log.debug("Added {s}", .{dgst});
- return dgst;
- };
-}
-
-pub fn del(self: *Content, dgst: Digest) !void {
- try self.root.deleteTree(self.io, &dgst);
-}
-
-const Enumerator = struct {
- content: *Content,
- walker: std.Io.Dir.SelectiveWalker,
-
- pub fn deinit(self: *Enumerator) void {
- self.walker.deinit();
- }
-
- pub fn next(self: *Enumerator) !?Digest {
- if (try self.walker.next(self.content.io)) |e| {
- switch (e.kind) {
- // TODO: we should probably check that this is actually a digest
- .directory => return e.basename[0..64].*,
- else => return null,
- }
- } else {
- return null;
- }
- }
-};
-
-pub fn enumerate(self: *Content, allocator: std.mem.Allocator) !Enumerator {
- return .{
- .walker = try self.root.walkSelectively(allocator),
- .content = self,
- };
-}
-
-pub fn propSet(self: *Content, dgst: Digest, key: []const u8, val: ?[]const u8) !void {
- const digest_dir = try self.root.openDir(self.io, &dgst, .{});
- defer digest_dir.close(self.io);
-
- if (val == null) {
- return try digest_dir.deleteFile(self.io, key);
- }
-
- return try digest_dir.writeFile(self.io, .{
- .sub_path = key,
- .data = val.?,
- });
-}
-
-pub fn propGet(self: *Content, dgst: Digest, key: []const u8, allocator: std.mem.Allocator) !?[]const u8 {
- const digest_dir = try self.root.openDir(self.io, &dgst, .{});
- defer digest_dir.close(self.io);
-
- if (digest_dir.readFileAlloc(self.io, key, allocator, .unlimited)) |val| {
- return val;
- } else |err| switch (err) {
- error.FileNotFound => return null,
- else => return err,
- }
-}
-
-// fn checkLink(self: *Content, allocator: std.mem.Allocator, dgst: Digest) ![]const u8 {
-// const link_target = try self.propGet(dgst, "link", allocator);
-// // check target exists
-// try std.Io.Dir.cwd().access(self.io, link_target, .{ .read = true });
-// return link_target;
-// }
-
-test "e2e" {
- std.testing.log_level = .debug;
-
- const DEMO_DIR: []const u8 = "/home/robert/tmp/rbc-demo/";
- const CONTENT_DIR: []const u8 = DEMO_DIR ++ ".content";
-
- try std.Io.Dir.cwd().deleteTree(std.testing.io, CONTENT_DIR);
-
- const demo_dir = try std.Io.Dir.cwd().openDir(std.testing.io, DEMO_DIR, .{ .iterate = true });
- const content_dir = try std.Io.Dir.cwd().createDirPathOpen(std.testing.io, CONTENT_DIR, .{ .open_options = .{ .iterate = true } });
-
- var cnt = try Content.init(std.testing.io, std.testing.allocator, content_dir);
- defer cnt.deinit();
-
- {
- var walker = try demo_dir.walkSelectively(std.testing.allocator);
- defer walker.deinit();
-
- while (try walker.next(std.testing.io)) |e| {
- switch (e.kind) {
- .file => {
- log.info("+ {s}", .{e.path});
- const dgst = try cnt.add(demo_dir, e.path);
- try content_dir.access(std.testing.io, &dgst, .{});
- const rel = try std.fs.path.relativePosix(std.testing.allocator, DEMO_DIR, CONTENT_DIR, e.path);
- defer std.testing.allocator.free(rel);
- try cnt.propSet(dgst, "link", rel);
- },
- else => {},
- }
- }
- }
-
- var etor = try cnt.enumerate(std.testing.allocator);
- defer etor.deinit();
-
- {
- const dgst = try etor.next();
- try std.testing.expect(dgst != null);
-
- try cnt.propSet(dgst.?, "foo", "bar");
- const val1 = try cnt.propGet(dgst.?, "foo", std.testing.allocator);
- try std.testing.expect(val1 != null);
- defer std.testing.allocator.free(val1);
- try std.testing.expect(std.mem.eql(u8, "bar", val1.?));
-
- try cnt.propSet(dgst.?, "foo", null);
- const val2 = try cnt.propGet(dgst.?, "foo", std.testing.allocator);
- try std.testing.expect(val2 == null);
- }
-
- {
- const dgst = try etor.next();
- try std.testing.expect(dgst != null);
-
- try cnt.del(dgst.?);
- const err = content_dir.access(std.testing.io, &dgst.?, .{});
- try std.testing.expectError(error.FileNotFound, err);
- }
-
- while (try etor.next()) |dgst| {
- log.info("? {s}", .{dgst});
- try content_dir.access(std.testing.io, &dgst, .{});
- const link = try cnt.propGet(dgst, "link", std.testing.allocator);
- try std.testing.expect(link != null);
- defer std.testing.allocator.free(link.?);
- try content_dir.access(std.testing.io, link.?, .{});
- log.info("l {s}", .{link.?});
- }
-}
diff --git a/src/Context.zig b/src/Context.zig
deleted file mode 100644
index f5a638e..0000000
--- a/src/Context.zig
+++ /dev/null
@@ -1,242 +0,0 @@
-const std = @import("std");
-
-const Content = @import("Content.zig");
-const FileSwapper = @import("FileSwapper.zig");
-const gen = @import("gen.zig");
-
-const Context = @This();
-const log = std.log.scoped(.Context);
-
-pub const Defaults = struct {
- // TODO: should this be next to the root_path of Context.index?
- pub const CONTENT_PATH = ".rbc/content";
- pub const CONTENT_REF = "../..";
-
- 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";
-};
-
-io: std.Io,
-arena: std.heap.ArenaAllocator,
-content: ?Content = null,
-root: ?std.Io.Dir = null,
-
-pub fn init(io: std.Io, gpa: std.mem.Allocator) !Context {
- return .{
- .io = io,
- .arena = std.heap.ArenaAllocator.init(gpa),
- };
-}
-
-pub fn deinit(self: *Context) void {
- if (self.content != null) {
- self.content.?.deinit();
- }
- self.arena.deinit();
-}
-
-pub fn index(self: *Context, root_dir: std.Io.Dir) !void {
- if (self.content == null) {
- // setup content store
- const content_dir = try root_dir.createDirPathOpen(
- self.io,
- Defaults.CONTENT_PATH,
- .{ .open_options = .{ .iterate = true } },
- );
- self.content = try Content.init(self.io, self.arena.allocator(), content_dir);
- self.root = root_dir;
- } else {
- try self.gc();
- }
-
- var walk = try root_dir.walkSelectively(self.arena.allocator());
- defer walk.deinit();
-
- var digests: std.ArrayList(Content.Digest) = .empty;
- defer digests.deinit(self.arena.allocator());
-
- while (try walk.next(self.io)) |e| {
- switch (e.kind) {
- .file => {
- if (std.mem.endsWith(u8, e.basename, ".htm")) {
- const dgst = try self.content.?.add(root_dir, e.path);
- try self.content.?.propSet(dgst, "link", e.path);
-
- try digests.append(self.arena.allocator(), dgst);
- log.debug("Indexing page {s} (digest={s})", .{ e.path, dgst });
- }
- },
- else => {},
- }
- }
-
- log.info("Indexed {d} pages", .{digests.items.len});
-}
-
-// garbage collect
-fn gc(self: *Context) !void {
- if (self.content == null) {
- return error.NoIndex;
- }
-
- var etor = try self.content.?.enumerate(self.arena.allocator());
- defer etor.deinit();
-
- var gc_n: usize = 0;
- while (try etor.next()) |dgst| {
- if (try self.content.?.propGet(dgst, "link", self.arena.allocator())) |link| {
- defer self.arena.allocator().free(link);
-
- // cleanup entries with missing link
- self.root.?.access(self.io, link, .{}) catch |err| switch (err) {
- error.FileNotFound => {
- try self.content.?.del(dgst);
- gc_n += 1;
- },
- else => return err,
- };
- }
- }
-
- log.info("Garbage collected {d} pages", .{gc_n});
-}
-
-pub fn render(self: *Context, site_dir: std.Io.Dir, force: bool) !void {
- if (self.content == null) {
- return error.NoIndex;
- }
-
- log.info("Rendering site", .{});
-
- var page_n: u32 = 0;
-
- var etor = try self.content.?.enumerate(self.arena.allocator());
- defer etor.deinit();
-
- // render pages
- while (try etor.next()) |dgst| {
- if (!force and try self.hasRenderPage(dgst, site_dir)) {
- continue; // no need to re-render
- }
-
- try self.renderPage(site_dir, dgst);
- page_n += 1;
- }
-
- log.info("Rendered {d} pages", .{page_n});
-}
-
-fn hasRenderPage(self: *Context, dgst: Content.Digest, site_dir: std.Io.Dir) !bool {
- const render_path = try self.content.?.propGet(dgst, "render", self.arena.allocator());
- if (render_path == null) {
- return false;
- }
- defer self.arena.allocator().free(render_path.?);
-
- site_dir.access(self.io, render_path.?, .{}) catch |err| switch (err) {
- error.FileNotFound => return false,
- else => return err,
- };
-
- log.debug("Skipping {s}", .{render_path.?});
- return true;
-}
-
-fn renderPage(self: *Context, site_dir: std.Io.Dir, dgst: Content.Digest) !void {
- var arena = std.heap.ArenaAllocator.init(self.arena.allocator());
- defer arena.deinit();
- const gpa = arena.allocator();
-
- const page_path = try self.content.?.propGet(dgst, "link", self.arena.allocator());
- if (page_path == null) {
- return error.NoLink;
- }
-
- const page_name = std.mem.cutSuffix(u8, std.fs.path.basename(page_path.?), ".htm").?;
-
- if (std.mem.startsWith(u8, page_name, "meta.")) {
- log.warn("skipping meta page => {s}", .{page_name});
- return;
- } else {
- log.debug("Rendering page {s} (name={s})", .{ page_path.?, page_name });
-
- const render_name = try std.mem.concat(gpa, u8, &.{ page_name, ".html" });
- const pretty_name = try std.mem.replaceOwned(u8, gpa, page_name, "_", " ");
-
- const sw = try FileSwapper.createAt(self.io, gpa, site_dir, render_name);
- defer sw.close();
- var w = sw.writer();
-
- try gen.build_htm(self.io, gpa, &w.interface, .{
- .root_dir = self.root.?,
- .site_dir = site_dir,
- .page_path = page_path.?,
- .page_name = pretty_name,
- .meta = .{
- .site_name = Defaults.SITE_NAME,
- .license_url = Defaults.LICENSE_URL,
- .license_holder = Defaults.LICENSE_HOLDER,
- .license_year = Defaults.LICENSE_YEAR,
- .license_name = Defaults.LICENSE_NAME,
- .public_inbox = Defaults.PUBLIC_INBOX,
- .source_url = Defaults.SOURCE_URL,
- },
- });
-
- try self.content.?.propSet(dgst, "render", render_name);
- try self.content.?.propSet(dgst, "name", pretty_name);
- }
-}
-
-// test "e2e" {
-// std.testing.log_level = .debug;
-//
-// const DEMO_DIR: []const u8 = "/home/robert/tmp/rbc-demo/";
-// const SITE_DIR: []const u8 = DEMO_DIR ++ "site/";
-//
-// const demo_dir = try std.Io.Dir.cwd().openDir(std.testing.io, DEMO_DIR, .{ .iterate = true });
-// defer demo_dir.close(std.testing.io);
-// const site_dir = try std.Io.Dir.cwd().createDirPathOpen(std.testing.io, SITE_DIR, .{});
-// defer site_dir.close(std.testing.io);
-//
-// var ctx = try Context.init(std.testing.io, std.testing.allocator);
-// defer ctx.deinit();
-//
-// try ctx.index(demo_dir);
-// try ctx.render(site_dir);
-//
-// // TODO:
-// // index; build a list of input files
-// // parse; detect backlinks, dependencies, etc.
-// // render; write output
-// }
-
-test "gc" {
- std.testing.log_level = .debug;
-
- var tmp = std.testing.tmpDir(.{ .iterate = true });
- defer tmp.cleanup();
-
- const tmp_src = try tmp.dir.createDirPathOpen(std.testing.io, "src", .{ .open_options = .{ .iterate = true } });
- // const tmp_dst = try tmp.dir.createDirPathOpen(std.testing.io, "dst", .{});
-
- try tmp_src.writeFile(std.testing.io, .{ .sub_path = "foo.htm", .data = "hello world" });
-
- var ctx = try Context.init(std.testing.io, std.testing.allocator);
- defer ctx.deinit();
-
- try ctx.index(tmp_src);
- try tmp_src.deleteFile(std.testing.io, "foo.htm");
-
- try ctx.index(tmp_src);
- var etor = try ctx.content.?.enumerate(std.testing.allocator);
- defer etor.deinit();
- var seen: u32 = 0;
- while (try etor.next()) |_| seen += 1;
- try std.testing.expect(seen == 0);
-}
diff --git a/src/FileSwapper.zig b/src/FileSwapper.zig
deleted file mode 100644
index 13bafe4..0000000
--- a/src/FileSwapper.zig
+++ /dev/null
@@ -1,101 +0,0 @@
-const std = @import("std");
-
-const FileSwapper = @This();
-const log = std.log.scoped(.FileSwapper);
-
-io: std.Io,
-arena: *std.heap.ArenaAllocator,
-dir: std.Io.Dir,
-target_path: []const u8,
-tmp_path: []const u8,
-tmp: std.Io.File,
-
-pub fn createAt(io: std.Io, allocator: std.mem.Allocator, dir: std.Io.Dir, sub_path: []const u8) !FileSwapper {
- const arena = try allocator.create(std.heap.ArenaAllocator);
- errdefer allocator.destroy(arena);
- arena.* = std.heap.ArenaAllocator.init(allocator);
-
- const tmp_path = try std.fmt.allocPrint(
- arena.allocator(),
- ".{s}",
- .{sub_path},
- );
-
- log.debug("Creating {s}", .{tmp_path});
-
- const f = try dir.createFile(io, tmp_path, .{});
-
- return .{
- .io = io,
- .arena = arena,
- .dir = dir,
- .target_path = try arena.allocator().dupe(u8, sub_path),
- .tmp_path = tmp_path,
- .tmp = f,
- };
-}
-
-fn deinit(self: FileSwapper) void {
- self.tmp.close(self.io);
- const allocator = self.arena.child_allocator;
- self.arena.deinit();
- allocator.destroy(self.arena);
-}
-
-pub fn file(self: FileSwapper) std.fs.File {
- return self.tmp;
-}
-
-pub fn writer(self: FileSwapper) std.Io.File.Writer {
- const buf = self.arena.allocator().alloc(u8, 1024) catch
- @panic("oom");
- return self.tmp.writer(self.io, buf);
-}
-
-pub fn swap(self: FileSwapper) !void {
- log.debug("Swapping {s} -> {s}", .{ self.tmp_path, self.target_path });
-
- // flush writes
- try self.tmp.sync(self.io);
-
- // swap file into final location
- // NOTE: the tmp fd is still valid after this
- try std.Io.Dir.rename(
- self.dir,
- self.tmp_path,
- self.dir,
- self.target_path,
- self.io,
- );
-}
-
-pub fn close(self: FileSwapper) void {
- self.swap() catch
- @panic("failed to swap target file");
-
- self.deinit();
-}
-
-test "smoke" {
- std.testing.log_level = .debug;
-
- const alloc = std.testing.allocator;
-
- var tmp = std.testing.tmpDir(.{});
- defer tmp.cleanup();
-
- const fileswapper = try FileSwapper.createAt(std.testing.io, alloc, tmp.dir, "test.txt");
- errdefer fileswapper.deinit();
-
- var w = fileswapper.writer();
- try w.interface.writeAll("hi");
-
- try tmp.dir.access(std.testing.io, fileswapper.tmp_path, .{ .read = true, .write = true });
-
- try fileswapper.swap();
-
- const err = tmp.dir.access(std.testing.io, fileswapper.tmp_path, .{ .read = true, .write = true });
- try std.testing.expectError(error.FileNotFound, err);
-
- fileswapper.deinit();
-}
diff --git a/src/gen.zig b/src/gen.zig
deleted file mode 100644
index 8def5cf..0000000
--- a/src/gen.zig
+++ /dev/null
@@ -1,255 +0,0 @@
-const std = @import("std");
-
-const os = @import("os.zig");
-
-const log = std.log.scoped(.gen);
-
-const DELIM_START: []const u8 = "{[";
-const DELIM_END: []const u8 = "]}";
-
-pub const BuildOpts = struct {
- root_dir: std.Io.Dir,
- site_dir: std.Io.Dir,
-
- page_path: []const u8,
- page_name: []const u8,
-
- meta: struct {
- source_url: []const u8,
- site_name: []const u8,
- license_url: []const u8,
- license_holder: []const u8,
- license_name: []const u8,
- license_year: []const u8,
- public_inbox: []const u8,
- },
-};
-
-// build a single html file
-pub fn build_htm(io: std.Io, gpa: std.mem.Allocator, w: *std.Io.Writer, opts: BuildOpts) !void {
- const stat = try opts.root_dir.statFile(io, opts.page_path, .{});
- const mtime_str = try os.formatTimestamp(gpa, stat.mtime, "%F");
- defer gpa.free(mtime_str);
-
- // HEAD
- try w.print(
- \\
- \\
- \\
- \\
- \\
- \\
- \\
- \\
- \\
- \\
- \\
- \\
- \\{[SITE_NAME]s} — {[PAGE_NAME]s}
- \\
- \\
- \\
- \\
- \\
- , .{
- .SOURCE = opts.page_path,
- .PAGE_NAME = opts.page_name,
- .LICENSE_URL = opts.meta.license_url,
- .SITE_NAME = opts.meta.site_name,
- });
-
- // NAV
- try w.writeAll(
- \\
- );
- template_portal(io, gpa, w, "meta.nav", opts) catch |err| {
- if (err != error.FileNotFound) return err;
- };
- try w.writeAll(
- \\
- \\
- \\
- );
-
- // MAIN
- try w.writeAll(
- \\
- \\
- );
- try inject(io, gpa, w, opts);
- try w.writeAll(
- \\
- \\
- );
-
- // FOOTER
- try w.print(
- \\
- \\
- , .{
- .MTIME = mtime_str,
- .PATH = opts.page_path,
- .LICENSE_HOLDER = opts.meta.license_holder,
- .LICENSE_NAME = opts.meta.license_name,
- .LICENSE_URL = opts.meta.license_url,
- .LICENSE_YEAR = opts.meta.license_year,
- .PUBLIC_INBOX = opts.meta.public_inbox,
- .SOURCE_URL = opts.meta.source_url,
- });
-
- try w.writeAll(
- \\[⏶]
- \\
- );
-
- try w.writeAll(
- \\
- \\
- );
-
- try w.flush();
-}
-
-// parses srcpath, resolves refs and portals and writes everything to w
-fn inject(io: std.Io, gpa: std.mem.Allocator, w: *std.Io.Writer, opts: BuildOpts) !void {
- var arena = std.heap.ArenaAllocator.init(gpa);
- defer arena.deinit();
-
- const inc = opts.root_dir.openFile(io, opts.page_path, .{ .mode = .read_only }) catch |err| {
- if (err == error.FileNotFound)
- log.warn("Missing {s} include.", .{opts.page_path});
-
- return err;
- };
- defer inc.close(io);
-
- const buf = try arena.allocator().alloc(u8, 1024);
- var r = inc.readerStreaming(io, buf);
-
- var chunkw = std.Io.Writer.Allocating.init(arena.allocator());
- defer chunkw.deinit();
-
- while (true) {
- defer chunkw.clearRetainingCapacity(); // reset chunk
-
- _ = r.interface.streamDelimiter(&chunkw.writer, '\n') catch |err|
- if (err == error.EndOfStream) break else return err;
-
- _ = r.interface.toss(1); // skip newline delimiter
-
- var chunk = chunkw.written();
- templating: while (true) {
- if (std.mem.indexOfPosLinear(u8, chunk, 0, DELIM_START)) |start| {
- if (std.mem.indexOfPosLinear(u8, chunk, start + 1, DELIM_END)) |end| {
- try w.writeAll(chunk[0..start]);
- template(io, gpa, w, chunk[(start + DELIM_START.len)..end], opts) catch |err| switch (err) {
- error.FileNotFound => {
- // log.warn("ref {s} not found", .{chunk[start + 1 .. end]});
- // trim already written chunk and exit templating
- chunk = chunk[start..];
- break :templating;
- },
- else => @panic("Failed to resolve template"),
- };
- chunk = chunk[end + DELIM_END.len ..];
- } else break :templating;
- } else break :templating;
- }
-
- try w.writeAll(chunk);
- try w.writeByte('\n');
- try w.flush();
- }
-
- try w.flush();
-}
-
-// implements various reference types that can be used in .htm templates
-fn template(io: std.Io, gpa: std.mem.Allocator, w: *std.Io.Writer, ref_name: []const u8, opts: BuildOpts) anyerror!void {
- switch (ref_name[0]) {
- '/' => return try template_portal(io, gpa, w, ref_name[1..], opts),
- // '$' => return try ref_link(allocator, w, refname[1..]),
- // '+' => return try ref_name(allocator, w, refname[1..]),
- else => return try template_ref(io, gpa, w, ref_name, opts),
- }
-}
-
-// insert link
-fn template_ref(io: std.Io, gpa: std.mem.Allocator, w: *std.Io.Writer, ref_name: []const u8, opts: BuildOpts) anyerror!void {
- log.debug("template:ref (ref={s})", .{ref_name});
-
- const ref_path = try std.fmt.allocPrint(gpa, "{s}.html", .{ref_name});
- defer gpa.free(ref_path);
-
- _ = io;
- _ = opts;
-
- try w.print(
- \\{s}
- , .{ ref_path, ref_name });
-}
-
-// insert contents
-fn template_portal(io: std.Io, gpa: std.mem.Allocator, w: *std.Io.Writer, ref_name: []const u8, opts: BuildOpts) anyerror!void {
- log.debug("template:portal (ref={s})", .{ref_name});
-
- const ref_path = try std.fmt.allocPrint(gpa, "{s}.htm", .{ref_name});
- defer gpa.free(ref_path);
-
- try opts.root_dir.access(io, ref_path, .{});
-
- var iopts = opts;
- iopts.page_path = ref_path;
- iopts.page_name = ref_name;
- try inject(io, gpa, w, iopts);
-}
-
-// build a meta file
-pub fn build_meta(io: std.Io, gpa: std.mem.Allocator, w: *std.Io.Writer, opts: BuildOpts) !void {
- if (std.mem.startsWith(u8, opts.page_name, "meta.nav")) return;
-
- if (std.mem.startsWith(u8, opts.page_name, "meta.tilde")) {
- const name = std.mem.cutPrefix(u8, opts.page_name, "meta.tilde.").?;
- const path = try std.fmt.allocPrint(gpa, "~{s}", .{name});
- defer gpa.free(path);
-
- const link = blk: {
- const content = try opts.root_dir.readFileAlloc(io, opts.page_path, gpa, .unlimited);
- break :blk std.mem.trim(u8, content, " \n\t\r");
- };
- defer gpa.free(link);
-
- try w.interface.print(
- \\
- \\
- \\
- \\
- \\
- \\
- \\
- \\
- \\
- \\Continue to {[LINK]s} .
- \\
- \\
- \\
- \\
- , .{
- .LINK = link,
- });
- try w.interface.flush();
- }
-
- return error.UnsupportedMeta;
-}
diff --git a/src/inc/2025.htm b/src/inc/2025.htm
index e1f3933..f867484 100644
--- a/src/inc/2025.htm
+++ b/src/inc/2025.htm
@@ -86,7 +86,7 @@ target="_blank">all-systems-go
saw parcels live
visited amsterdam
took the sleeper train
-got really into the passage for two weeks ↪ {read#sep25}
+got really into the passage for two weeks ↪ {[read#sep25]}
had delicious coffee in linden
played chunithm @ hi-score (again)
went to hannover (again)
@@ -143,7 +143,7 @@ class="i size-m" alt="coffee" />