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 --- 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 ------ 17 files changed, 767 insertions(+), 1027 deletions(-) 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 (limited to 'src') 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} - \\ - \\ - \\
- \\{[SITE_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( - \\ - \\
- \\ - ); - - // 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( - \\ - \\ - \\ - \\ - \\ - \\ - \\ - \\ - \\ - \\ - \\ - \\ - \\ - \\ - , .{ - .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" />

    train