about summary refs log tree commit diff
path: root/src/main.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.zig')
-rw-r--r--src/main.zig323
1 files changed, 323 insertions, 0 deletions
diff --git a/src/main.zig b/src/main.zig
new file mode 100644
index 0000000..56dc7a1
--- /dev/null
+++ b/src/main.zig
@@ -0,0 +1,323 @@
+// 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(
+            \\<h2 id="{[ID]s}"><a href="/{[PATH]s}">{[NAME]s}</a></2>
+            \\
+        , .{
+            .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(
+        \\<a href="/{[PATH]s}">{[NAME]s}</a>
+        \\
+    , .{
+        .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(
+        \\<!DOCTYPE html><html lang="en">
+        \\<head>
+        \\<meta charset="utf-8">
+        \\<meta name="thumbnail" content="{[DOMAIN]s}/media/services/thumbnail.jpg">
+        \\<meta name="viewport" content="width=device-width,initial-scale=1">
+        \\<link rel="alternate" type="application/rss+xml" title="RSS Feed" href="../links/rss.xml">
+        \\<link rel="stylesheet" type="text/css" href="../links/main.css">
+        \\<link rel="shortcut icon" type="image/png" href="../media/services/icon.png">
+        \\<title>{[SITE_NAME]s} &mdash; {[PAGE_NAME]s}</title>
+        \\</head>
+        \\<body>
+        \\<header>
+        \\<a href="/"><img src="../media/interface/logo.svg" alt="{[SITE_NAME]s}" height="50">
+        \\<h1>gzr.im</h1>
+        \\</a>
+        \\</header>
+        \\
+    , .{
+        .DOMAIN = DOMAIN_URL,
+        .SITE_NAME = SITE_NAME,
+        .PAGE_NAME = page_name,
+    });
+
+    // NAV
+    try f.writer().writeAll(
+        \\<nav>
+        \\
+    );
+    portal(allocator, f, "meta.nav", false) catch {}; // ignore error
+    try f.writer().writeAll(
+        \\</nav>
+        \\
+    );
+
+    // MAIN
+    try f.writer().print(
+        \\<main>
+        \\
+        \\<!-- Generated file, do not edit -->
+        \\
+        \\<h1>{[PAGE_NAME]s}</h1>
+        \\
+    , .{
+        .PAGE_NAME = page_name,
+    });
+    try inject(allocator, f, srcpath);
+    try f.writer().writeAll(
+        \\
+        \\</main>
+        \\
+    );
+
+    // FOOTER
+    try f.writer().print(
+        \\<footer>
+        \\<span style="">
+        \\Edited on {[MTIME]s} <a href="{[SOURCE]s}/{[PATH]s}">[edit]</a><br/>
+        \\</span>
+        \\<b>robertgzr</b> &copy; &mdash;
+        \\<a href="{[LICENSE]s}" target="_blank">BY-NC-SA 4.0</a>
+        \\</footer>
+        \\</body></html>
+    , .{
+        .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(
+        \\<ul class="col2 captial">
+        \\
+    );
+
+    // alphanum ascii sort
+    std.mem.sort(
+        []const u8,
+        pages,
+        {},
+        struct {
+            fn lessthan(_: void, a: []const u8, b: []const u8) bool {
+                return std.ascii.lessThanIgnoreCase(a, b);
+            }
+        }.lessthan,
+    );
+
+    for (pages) |p| {
+        if (std.mem.startsWith(u8, p, "meta.")) continue;
+
+        const dstname = try stripExt(p);
+        const dstpath = try joinIn(allocator, &.{SITE_PATH}, &.{ dstname, ".html" });
+        defer allocator.free(dstpath);
+
+        try f.writer().print(
+            \\<li><a href="/{[HREF]s}">{[LINK]s}</a></li>
+            \\
+        , .{
+            .HREF = dstpath,
+            .LINK = dstname,
+        });
+    }
+
+    try f.writeAll(
+        \\</ul>
+        \\
+    );
+
+    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);
+}