const std = @import("std");
const log = std.log.scoped(.gen);
const Context = @import("main.zig").Context;
const fs = @import("fs.zig");
const exe = @import("exe.zig");
pub fn fixMtimes(ctx: *Context) !void {
var buf: [2 * std.os.linux.PATH_MAX]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
ctx.progress_start("Fix mtimes");
defer ctx.progress_end();
var counter: usize = 0;
for (ctx.pages.items) |p| {
ctx.progress_start(p);
defer ctx.progress_end();
const path = try fs.joinPathAndFilename(fba.allocator(), &.{Context.params.PAGE_PATH}, &.{p});
const mdpath = try fs.replaceExtension(fba.allocator(), path, "md");
defer fba.reset();
std.fs.cwd().access(mdpath, .{}) catch {
// only set mtime from git if no markdown source is used
const ts = fs.getGitTime(path) catch continue;
try fs.setMtime(path, ts);
counter += 1;
};
}
log.info("Retimed {d} pages.", .{counter});
}
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();
try f.writer().writeAll(
\\
\\
);
// alphanum ascii sort
std.mem.sort(
[]const u8,
ctx.pages.items,
{},
struct {
fn lessthan(_: void, a: []const u8, b: []const u8) bool {
return std.ascii.lessThanIgnoreCase(a, b);
}
}.lessthan,
);
for (ctx.pages.items) |p| {
if (std.mem.startsWith(u8, p, "meta.")) continue;
const dstname = try fs.stripExtension(p);
const dstpath = try fs.joinPathAndFilename(ctx.allocator(), &.{Context.params.SITE_PATH}, &.{ dstname, ".html" });
defer ctx.allocator().free(dstpath);
try f.writer().print(
\\- {[NAME]s}
\\
, .{
.LINK = dstpath,
.NAME = dstname,
});
}
try f.writer().writeAll(
\\
\\
);
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();
const page_name = try std.mem.replaceOwned(u8, ctx.allocator(), dstname, "_", " ");
defer ctx.allocator().free(page_name);
try build(ctx.allocator(), f.writer().any(), page_name, srcpath);
ctx.generated += 1;
}
}
// build a single html file
fn build(allocator: std.mem.Allocator, w: std.io.AnyWriter, page_name: []const u8, srcpath: []const u8) !void {
const mtime = try fs.getMtime(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}
\\
\\
\\
\\
\\
, .{
.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(
\\
\\
\\
);
}
// 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();
try f.writer().print(
\\
\\
\\
\\
\\
\\
\\
\\
\\
\\
\\
\\
\\
\\
, .{
.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 |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 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),
}
}
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.AnyWriter, 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}2>
\\
, .{
.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.AnyWriter, 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.AnyWriter, 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.AnyWriter, refname: []const u8) anyerror!void {
const page = try Page.from_ref(allocator, refname);
defer page.deinit();
try w.print("{s}", .{page.ref});
}