diff options
Diffstat (limited to '')
| -rw-r--r-- | src/main.zig | 519 |
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} — {[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> - \\© {[LICENSE_YEAR]s} - \\— - \\<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">[⏶]</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); + }; + } +}; |