// TODO: just let compiler infer error set through recursive calls // depends on: https://github.com/ziglang/zig/issues/2971 const std = @import("std"); const c = @cImport({ @cInclude("time.h"); }); const PAGE_PATH = "src/inc"; const SITE_PATH = "site"; const DOMAIN_URL = "https://example.com"; const SITE_NAME = "example"; const LICENSE_URL = "https://example.com/license"; const SOURCE_URL = "https://example.com/source"; const BuildErrors = (std.fs.File.OpenError || std.fs.File.WriteError || std.mem.Allocator.Error); // also trims off base paths fn stripExt(path: []const u8) ![]const u8 { const basename = std.fs.path.basename(path); const ext = std.fs.path.extension(path); const ext_idx = std.mem.indexOfPosLinear(u8, basename, 0, ext); std.debug.assert(ext_idx != null); return basename[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 portal(allocator: std.mem.Allocator, f: std.fs.File, refname: []const u8, insert_heading: bool) BuildErrors!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 f.writer().print( \\

{[NAME]s} \\ , .{ .ID = basename, .NAME = refname, .PATH = refpathSite, }); try inject(allocator, f, refpathPage); // XXX: track ref count? } fn template(allocator: std.mem.Allocator, f: std.fs.File, refname: []const u8) BuildErrors!void { if (std.mem.startsWith(u8, refname, "/")) return try portal(allocator, f, refname[1..], false); 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 f.writer().print( \\{[NAME]s} \\ , .{ .NAME = refname, .PATH = refpathSite, }); // XXX: track ref count? } fn inject(allocator: std.mem.Allocator, f: std.fs.File, srcpath: []const u8) BuildErrors!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; }; defer inc.close(); var buf_writer = std.io.bufferedWriter(f.writer()); 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| { if (std.mem.indexOfPosLinear(u8, chunk, 0, "{")) |start| { try out_stream.writeAll(chunk[0..start]); try buf_writer.flush(); if (std.mem.indexOfPosLinear(u8, chunk, start + 1, "}")) |end| { try template(allocator, f, chunk[start + 1 .. end]); try buf_writer.flush(); try out_stream.writeAll(chunk[end + 1 ..]); try buf_writer.flush(); } else { std.log.err("Templating error in {s}", .{srcpath}); std.os.linux.exit(1); } } else { try out_stream.writeAll(chunk); try out_stream.writeByte('\n'); } _ = arena.reset(.free_all); } try buf_writer.flush(); } fn build(allocator: std.mem.Allocator, f: std.fs.File, page_name: []const u8, srcpath: []const u8) BuildErrors!void { const mtime = blk: { 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 = c.ctime(&mtime.tv_sec); break :blk std.mem.span(mtime_str); }; // HEAD try f.writer().print( \\ \\ \\ \\ \\ \\ \\ \\ \\{[SITE_NAME]s} — {[PAGE_NAME]s} \\ \\ \\
\\{[SITE_NAME]s} \\

gzr.im

\\
\\
\\ , .{ .DOMAIN = DOMAIN_URL, .SITE_NAME = SITE_NAME, .PAGE_NAME = page_name, }); // NAV try f.writer().writeAll( \\ \\ ); // MAIN try f.writer().print( \\
\\ \\ \\ \\

{[PAGE_NAME]s}

\\ , .{ .PAGE_NAME = page_name, }); try inject(allocator, f, srcpath); try f.writer().writeAll( \\ \\
\\ ); // FOOTER try f.writer().print( \\ \\ , .{ .MTIME = mtime, .PATH = srcpath, .LICENSE = LICENSE_URL, .SOURCE = SOURCE_URL, }); } fn generate(allocator: std.mem.Allocator, pages: [][]const u8) !void { for (pages) |p| { const dstname = try stripExt(p); const srcpath = try joinIn(allocator, &.{PAGE_PATH}, &.{ dstname, ".htm" }); defer allocator.free(srcpath); const dstpath = try joinIn(allocator, &.{SITE_PATH}, &.{ dstname, ".html" }); defer allocator.free(dstpath); const f = try std.fs.cwd().createFile(dstpath, .{}); const page_name = try std.mem.replaceOwned(u8, allocator, dstname, "_", " "); defer allocator.free(page_name); try build(allocator, f, page_name, srcpath); } std.log.info("Generated {d} files.", .{pages.len}); } fn genindex(allocator: std.mem.Allocator, pages: [][]const u8) !void { const srcpathIndex = try joinIn(allocator, &.{PAGE_PATH}, &.{"index.htm"}); defer allocator.free(srcpathIndex); const f = try std.fs.cwd().createFile(srcpathIndex, .{}); defer f.close(); try f.writeAll( \\ \\ ); std.log.info("Generated index.", .{}); } fn index(allocator: std.mem.Allocator, path: []const u8) ![][]const u8 { var dir = std.fs.cwd().openDir(path, .{ .iterate = true }) catch |err| { if (err == error.FileNotFound) { std.log.err("Missing {s} folder.", .{PAGE_PATH}); std.os.linux.exit(1); } else return err; }; defer dir.close(); var pages = std.ArrayList([]const u8).init(allocator); defer pages.deinit(); var dirIter = dir.iterate(); while (try dirIter.next()) |e| if (e.kind == .file) if (std.mem.endsWith(u8, e.name, ".htm")) try pages.append(try allocator.dupe(u8, e.name)); std.log.info("Indexed {d} pages.", .{pages.items.len}); return try pages.toOwnedSlice(); } pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const allocator = gpa.allocator(); defer _ = gpa.deinit(); const pages = index(allocator, PAGE_PATH) catch { std.log.err("Failed to index pages.", .{}); std.os.linux.exit(1); }; defer { for (pages) |p| allocator.free(p); allocator.free(pages); } try genindex(allocator, pages); try generate(allocator, pages); }