const std = @import("std"); const fs = @import("fs.zig"); const exe = @import("exe.zig"); const Context = @import("main.zig").Context; const log = std.log.scoped(.gen); pub fn generate(ctx: *Context) !void { ctx.progress_start("generate"); defer ctx.progress_end(); try genIndex(ctx); log.info("Generated index.", .{}); try genSite(ctx); log.info("Generated {d} files.", .{ctx.generated}); log.info("Generated {d} tildes.", .{ctx.tilde}); } fn genIndex(ctx: *Context) !void { ctx.progress_start("index.htm"); 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( \\ \\ ); try w.interface.flush(); if (!try f.diff()) try f.undo(); } fn genSite(ctx: *Context) !void { for (ctx.pages.items) |p| { const pname = std.fs.path.basename(p); ctx.progress_start(pname); defer ctx.progress_end(); if (std.mem.startsWith(u8, pname, "meta.")) { try meta(ctx, p); continue; } const dstname = try fs.stripExtension(p); const srcpath = try fs.joinPathAndFilename(ctx.allocator(), &.{Context.params.PAGE_PATH}, &.{ dstname, ".htm" }); defer ctx.allocator().free(srcpath); const dstpath = try fs.joinPathAndFilename(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 fs.joinPathAndFilename(ctx.allocator(), &.{Context.params.SITE_PATH}, &.{d}); defer ctx.allocator().free(dstdir); try std.fs.cwd().makePath(dstdir); } const f = try fs.FileSwapper.createAt(ctx.allocator(), dstpath); defer f.close(); var buffer: [1024]u8 = undefined; var w = f.writer(&buffer); const page_name = try std.mem.replaceOwned(u8, ctx.allocator(), dstname, "_", " "); defer ctx.allocator().free(page_name); try build(ctx.allocator(), &w.interface, page_name, srcpath); ctx.generated += 1; } } // 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); // HEAD try w.print( \\ \\ \\ \\ \\ \\ \\ \\ \\ \\ \\ \\ \\{[SITE_NAME]s} — {[PAGE_NAME]s} \\ \\ \\
\\{[SITE_NAME]s} \\ , .{ .SOURCE = srcpath, .LICENSE_URL = Context.params.LICENSE_URL, .PAGE_NAME = page_name, .SITE_NAME = Context.params.SITE_NAME, }); // NAV try w.writeAll( \\ \\
\\ ); // MAIN try w.print( \\
\\ , .{}); try inject(allocator, w, srcpath); try w.writeAll( \\
\\ ); // FOOTER try w.print( \\ \\ , .{ .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, }); try w.writeAll( \\[⏶] \\ ); try w.writeAll( \\ \\ \\ ); 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: []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 fs.joinPathAndFilename(ctx.allocator(), &.{Context.params.PAGE_PATH}, &.{p}); defer ctx.allocator().free(srcpath); 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( \\ \\ \\ \\ \\ \\ \\ \\ \\ \\ \\ \\ \\ \\ , .{ .LINK = link, }); try w.interface.flush(); 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.Writer, srcpath: []const u8) !void { var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); 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 buffer: [1024]u8 = undefined; var r = inc.readerStreaming(&buffer); 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, "{")) |start| { if (std.mem.indexOfPosLinear(u8, chunk, start + 1, "}")) |end| { try w.writeAll(chunk[0..start]); template(allocator, w, chunk[start + 1 .. end]) 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 + 1 ..]; } 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(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), } } 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 "", }; } fn deinit(self: Page) void { self.arena.deinit(); } }; fn portal(allocator: std.mem.Allocator, w: *std.Io.Writer, refname: []const u8, insert_heading: bool) anyerror!void { const page = try Page.from_ref(allocator, refname); defer page.deinit(); if (insert_heading) try w.print( \\

{[NAME]s} \\ , .{ .ID = page.ref, .NAME = page.name, .PATH = page.html, }); try inject(allocator, w, page.htm); // XXX: track ref count? } fn ref(allocator: std.mem.Allocator, w: *std.Io.Writer, refname: []const u8) anyerror!void { const page = try Page.from_ref(allocator, refname); defer page.deinit(); try w.print( \\{[NAME]s} , .{ .NAME = page.name, .PATH = page.html, .ANCHOR = page.anchor, }); // XXX: track ref count? } fn ref_link(allocator: std.mem.Allocator, w: *std.Io.Writer, refname: []const u8) anyerror!void { const page = try Page.from_ref(allocator, refname); defer page.deinit(); try w.print("/{s}{s}", .{ page.html, page.anchor }); } fn ref_name(allocator: std.mem.Allocator, w: *std.Io.Writer, refname: []const u8) anyerror!void { const page = try Page.from_ref(allocator, refname); defer page.deinit(); try w.print("{s}", .{page.ref}); }