const std = @import("std");
const os = @import("os.zig");
const log = std.log.scoped(.gen);
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
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(
\\
\\
\\
\\
\\
\\
\\
\\
\\
\\
\\
\\
\\{[SITE_NAME]s} — {[PAGE_NAME]s}
\\
\\
\\
\\
\\
, .{
.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(
\\
\\
\\
);
// MAIN
try w.writeAll(
\\
\\
);
try inject(io, gpa, w, opts);
try w.writeAll(
\\
\\
);
// FOOTER
try w.print(
\\
\\
, .{
.MTIME = mtime_str,
.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(
\\[⏶]
\\
);
try w.writeAll(
\\
\\
);
try w.flush();
}
// parses srcpath, resolves refs and portals and writes everything to w
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 = opts.root_dir.openFile(io, opts.page_path, .{ .mode = .read_only }) catch |err| {
if (err == error.FileNotFound)
log.warn("Missing {s} include.", .{opts.page_path});
return err;
};
defer inc.close(io);
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();
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, DELIM_START)) |start| {
if (std.mem.indexOfPosLinear(u8, chunk, start + 1, DELIM_END)) |end| {
try w.writeAll(chunk[0..start]);
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
chunk = chunk[start..];
break :templating;
},
else => @panic("Failed to resolve template"),
};
chunk = chunk[end + DELIM_END.len ..];
} 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(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),
}
}
// 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});
const ref_path = try std.fmt.allocPrint(gpa, "{s}.html", .{ref_name});
defer gpa.free(ref_path);
_ = io;
_ = opts;
try w.print(
\\{s}
, .{ ref_path, ref_name });
}
// 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});
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, .{});
var iopts = opts;
iopts.page_path = ref_path;
iopts.page_name = ref_name;
try inject(io, gpa, w, iopts);
}
// 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;
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);
try w.interface.print(
\\
\\
\\
\\
\\
\\
\\
\\
\\
\\
\\
\\
\\
\\
, .{
.LINK = link,
});
try w.interface.flush();
}
return error.UnsupportedMeta;
}