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 | |
| parent | d50c54e23412febc1a550a15b574a02ff2907eed (diff) | |
| download | site-c36b9cb651dccec5defc0826b3c43e864fd00ddf.tar.gz | |
refactor into cli
Signed-off-by: Robert Günzler <r@gnzler.io>
Diffstat (limited to '')
| -rw-r--r-- | src/gen.zig | 440 | ||||
| -rw-r--r-- | src/main.zig | 519 |
2 files changed, 512 insertions, 447 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); + } +}; 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); + }; + } +}; |