diff options
| author | Robert Günzler <r@gnzler.io> | 2025-07-17 10:33:24 +0200 |
|---|---|---|
| committer | Robert Günzler <r@gnzler.io> | 2025-07-17 10:33:53 +0200 |
| commit | c36b9cb651dccec5defc0826b3c43e864fd00ddf (patch) | |
| tree | 7adb7d5119eb650ab598047da5bdb69c02c8b47f /src/gen.zig | |
| parent | d50c54e23412febc1a550a15b574a02ff2907eed (diff) | |
| download | site-c36b9cb651dccec5defc0826b3c43e864fd00ddf.tar.gz | |
refactor into cli
Signed-off-by: Robert Günzler <r@gnzler.io>
Diffstat (limited to 'src/gen.zig')
| -rw-r--r-- | src/gen.zig | 440 |
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} — {[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> + \\© {[LICENSE_YEAR]s} + \\— + \\<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">[⏶]</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); + } +}; |