diff options
| author | Robert Günzler <r@gnzler.io> | 2026-06-24 11:57:02 +0900 |
|---|---|---|
| committer | Robert Günzler <r@gnzler.io> | 2026-09-22 18:01:30 +0200 |
| commit | c677e8873e0eae790a1b8b07844a10efa9f63a1e (patch) | |
| tree | 95bc9709c374d27962a73712d24807a98cc69bb4 /src/gen.zig | |
| parent | fcdf352af595759aa34e6ed037ee10810867a616 (diff) | |
| download | site-c677e8873e0eae790a1b8b07844a10efa9f63a1e.tar.gz | |
wip design for v2
Signed-off-by: Robert Günzler <r@gnzler.io>
Diffstat (limited to '')
| -rw-r--r-- | src/gen.zig | 404 |
1 files changed, 117 insertions, 287 deletions
diff --git a/src/gen.zig b/src/gen.zig index fd1dff7..8def5cf 100644 --- a/src/gen.zig +++ b/src/gen.zig @@ -1,131 +1,35 @@ const std = @import("std"); -const fs = @import("fs.zig"); -const exe = @import("exe.zig"); -const Context = @import("main.zig").Context; +const os = @import("os.zig"); const log = std.log.scoped(.gen); -pub fn generate(ctx: *Context) !void { - ctx.generated.clearRetainingCapacity(); - - ctx.progress_start("generate", true); - defer ctx.progress_end(); - - try index(ctx); - - log.info("Generated index.", .{}); - - try pages(ctx); - - log.info("Generated {d} files.", .{ctx.generated.items.len}); - log.info("Generated {d} tildes.", .{ctx.tilde}); -} - -fn index(ctx: *Context) !void { - ctx.progress_start("index.htm", false); - defer ctx.progress_end(); - - const srcpathIndex = try fs.joinPathAndFilename(ctx.allocator(), &.{Context.params.PAGE_PATH}, &.{"index.htm"}); - defer ctx.allocator().free(srcpathIndex); - - const f = try fs.FileSwapper.createAt(ctx.allocator(), srcpathIndex); - defer f.close(); - - var buffer: [1024]u8 = undefined; - var w = f.writer(&buffer); - - try w.interface.writeAll( - \\<ul class="col2 captial"> - \\ - ); - - for (ctx.sites.items) |s| { - for (s.pages.items) |p| { - if (p.kind == .Htm) { - const dstpath = try fs.joinPathAndFilename(ctx.allocator(), &.{Context.params.SITE_PATH}, &.{ p.name, ".html" }); - defer ctx.allocator().free(dstpath); - - try w.interface.print( - \\<li><a href="/{[LINK]s}">{[NAME]s}</a></li> - \\ - , .{ - .LINK = dstpath, - .NAME = p.name, - }); - } - } - } - - try w.interface.writeAll( - \\</ul> - \\ - ); - - try w.interface.flush(); -} - -fn pages(ctx: *Context) !void { - for (ctx.sites.items) |s| { - for (s.pages.items) |p| { - if (p.kind == .Meta) { - try meta(ctx, p); - } else if (p.kind == .Htm) { - try page(ctx, p); - } - } - } -} - -fn page(ctx: *Context, p: Context.Site.Page) !void { - ctx.progress_start(p.name, false); - defer ctx.progress_end(); - - const srcpath = try fs.joinPathAndFilename(ctx.allocator(), &.{Context.params.PAGE_PATH}, &.{ p.name, ".htm" }); - defer ctx.allocator().free(srcpath); - const dstpath = try fs.joinPathAndFilename(ctx.allocator(), &.{Context.params.SITE_PATH}, &.{ p.name, ".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); - - if (std.fs.path.dirname(rel_dstpath)) |d| { - const dstdir = try fs.joinPathAndFilename(ctx.allocator(), &.{Context.params.SITE_PATH}, &.{d}); - defer ctx.allocator().free(dstdir); - - try std.fs.cwd().makePath(dstdir); - } - - const srcmtime = try fs.getLastEdit(srcpath); - const dstmtime = try fs.getLastEdit(dstpath); - if (!ctx.force and srcmtime.sec < dstmtime.sec) return; - - log.debug("generating {s} at {s}", .{ p.path, rel_dstpath }); - - const f = try fs.FileSwapper.createAt(ctx.allocator(), dstpath); - - var buffer: [1024]u8 = undefined; - var w = f.writer(&buffer); - - const page_name = try std.mem.replaceOwned(u8, ctx.allocator(), p.name, "_", " "); - defer ctx.allocator().free(page_name); - - try build(ctx.allocator(), &w.interface, page_name, srcpath); - - f.close(); - - if (f.swapped.*) { - try ctx.generated.append( - ctx.allocator(), - try ctx.allocator().dupe(u8, dstpath), - ); - } -} +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 -fn build(allocator: std.mem.Allocator, w: *std.Io.Writer, page_name: []const u8, srcpath: []const u8) !void { - const mtime = try fs.getLastEdit(srcpath); - const mtime_str = try fs.formatTimespec(allocator, mtime, "%F"); - defer allocator.free(mtime_str); +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( @@ -148,18 +52,19 @@ fn build(allocator: std.mem.Allocator, w: *std.Io.Writer, page_name: []const u8, \\<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, + .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( \\<nav> - \\ ); - portal(allocator, w, "meta.nav", false) catch {}; // ignore error + template_portal(io, gpa, w, "meta.nav", opts) catch |err| { + if (err != error.FileNotFound) return err; + }; try w.writeAll( \\</nav> \\</header> @@ -167,11 +72,11 @@ fn build(allocator: std.mem.Allocator, w: *std.Io.Writer, page_name: []const u8, ); // MAIN - try w.print( + try w.writeAll( \\<main> \\ - , .{}); - try inject(allocator, w, srcpath); + ); + try inject(io, gpa, w, opts); try w.writeAll( \\</main> \\ @@ -190,14 +95,14 @@ fn build(allocator: std.mem.Allocator, w: *std.Io.Writer, page_name: []const u8, \\</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_str, - .PATH = srcpath, - .PUBLIC_INBOX = Context.params.PUBLIC_INBOX, - .SOURCE_URL = Context.params.SOURCE_URL, + .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( @@ -208,92 +113,26 @@ fn build(allocator: std.mem.Allocator, w: *std.Io.Writer, page_name: []const u8, try w.writeAll( \\</body> \\</html> - \\ ); try w.flush(); } -// 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: Context.Site.Page) !void { - ctx.progress_start(p.name, false); - defer ctx.progress_end(); - - ctx.meta += 1; - - if (std.mem.startsWith(u8, p.name, "meta.nav")) return; - - if (std.mem.startsWith(u8, p.name, "meta.tilde")) { - const srcpath = try fs.joinPathAndFilename(ctx.allocator(), &.{Context.params.PAGE_PATH}, &.{p.path}); - defer ctx.allocator().free(srcpath); - const dstname = p.name[11..]; // trim "meta.tilde." from the name - const dstpath = try fs.joinPathAndFilename(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 fs.FileSwapper.createAt(ctx.allocator(), dstpath); - defer f.close(); - - var buffer: [1024]u8 = undefined; - var w = f.writer(&buffer); - - try w.interface.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, - }); - try w.interface.flush(); - - ctx.tilde += 1; - return; - } - - log.err("unknown meta page type: {s}", .{p.name}); - return error.unknownMeta; -} - // parses srcpath, resolves refs and portals and writes everything to w -fn inject(allocator: std.mem.Allocator, w: *std.Io.Writer, srcpath: []const u8) !void { - var arena = std.heap.ArenaAllocator.init(allocator); +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 = std.fs.cwd().openFile(srcpath, .{ .mode = .read_only }) catch |err| { + const inc = opts.root_dir.openFile(io, opts.page_path, .{ .mode = .read_only }) catch |err| { if (err == error.FileNotFound) - log.warn("Missing {s} include.", .{srcpath}); + log.warn("Missing {s} include.", .{opts.page_path}); return err; }; - defer inc.close(); + defer inc.close(io); - var buffer: [1024]u8 = undefined; - var r = inc.readerStreaming(&buffer); + 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(); @@ -308,10 +147,10 @@ fn inject(allocator: std.mem.Allocator, w: *std.Io.Writer, srcpath: []const u8) var chunk = chunkw.written(); templating: while (true) { - if (std.mem.indexOfPosLinear(u8, chunk, 0, "{")) |start| { - if (std.mem.indexOfPosLinear(u8, chunk, start + 1, "}")) |end| { + 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(allocator, w, chunk[start + 1 .. end]) catch |err| switch (err) { + 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 @@ -320,7 +159,7 @@ fn inject(allocator: std.mem.Allocator, w: *std.Io.Writer, srcpath: []const u8) }, else => @panic("Failed to resolve template"), }; - chunk = chunk[end + 1 ..]; + chunk = chunk[end + DELIM_END.len ..]; } else break :templating; } else break :templating; } @@ -334,92 +173,83 @@ fn inject(allocator: std.mem.Allocator, w: *std.Io.Writer, srcpath: []const u8) } // implements various reference types that can be used in .htm templates -fn template(allocator: std.mem.Allocator, w: *std.Io.Writer, 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 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), } } -const Page = struct { - arena: std.heap.ArenaAllocator, - ref: []const u8, - name: []const u8, - html: []const u8, - htm: []const u8, - anchor: []const u8, - - fn from_ref(allocator: std.mem.Allocator, refname: []const u8) !Page { - var arena = std.heap.ArenaAllocator.init(allocator); - - const endofref = std.mem.lastIndexOf(u8, refname, "#") orelse refname.len; - const basename = try std.mem.replaceOwned(u8, arena.allocator(), refname[0..endofref], " ", "_"); - const html = try fs.joinPathAndFilename(arena.allocator(), &.{Context.params.SITE_PATH}, &.{ basename, ".html" }); - const htm = try fs.joinPathAndFilename(arena.allocator(), &.{Context.params.PAGE_PATH}, &.{ basename, ".htm" }); - - try std.fs.cwd().access(htm, .{ .mode = .read_only }); - - return .{ - .arena = arena, - .ref = refname[0..endofref], - .name = basename, - .html = html, - .htm = htm, - .anchor = if (endofref < refname.len) refname[endofref..] else "", - }; - } +// 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}); - fn deinit(self: Page) void { - self.arena.deinit(); - } -}; + const ref_path = try std.fmt.allocPrint(gpa, "{s}.html", .{ref_name}); + defer gpa.free(ref_path); -fn portal(allocator: std.mem.Allocator, w: *std.Io.Writer, refname: []const u8, insert_heading: bool) anyerror!void { - const p = try Page.from_ref(allocator, refname); - defer p.deinit(); + _ = io; + _ = opts; - if (insert_heading) - try w.print( - \\<h2 id="{[ID]s}"><a href="/{[PATH]s}">{[NAME]s}</a></2> - \\ - , .{ - .ID = p.ref, - .NAME = p.name, - .PATH = p.html, - }); - - try inject(allocator, w, p.htm); - - // XXX: track ref count? + try w.print( + \\<a href="{s}">{s}</a> + , .{ ref_path, ref_name }); } -fn ref(allocator: std.mem.Allocator, w: *std.Io.Writer, refname: []const u8) anyerror!void { - const p = try Page.from_ref(allocator, refname); - defer p.deinit(); +// 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}); - try w.print( - \\<a href="/{[PATH]s}{[ANCHOR]s}">{[NAME]s}</a> - , .{ - .NAME = p.name, - .PATH = p.html, - .ANCHOR = p.anchor, - }); + 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, .{}); - // XXX: track ref count? + var iopts = opts; + iopts.page_path = ref_path; + iopts.page_name = ref_name; + try inject(io, gpa, w, iopts); } -fn ref_link(allocator: std.mem.Allocator, w: *std.Io.Writer, refname: []const u8) anyerror!void { - const p = try Page.from_ref(allocator, refname); - defer p.deinit(); +// 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; - try w.print("/{s}{s}", .{ p.html, p.anchor }); -} + 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); -fn ref_name(allocator: std.mem.Allocator, w: *std.Io.Writer, refname: []const u8) anyerror!void { - const p = try Page.from_ref(allocator, refname); - defer p.deinit(); + try w.interface.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, + }); + try w.interface.flush(); + } - try w.print("{s}", .{p.ref}); + return error.UnsupportedMeta; } |