diff options
| author | Robert Günzler <r@gnzler.io> | 2024-10-15 02:50:55 +0200 |
|---|---|---|
| committer | Robert Günzler <r@gnzler.io> | 2024-10-16 15:40:14 +0200 |
| commit | 398874a3dd76193122472cd015bd7d6cd0b9300b (patch) | |
| tree | 00e522d14c42856389c814f31062136191ad0019 /src/main.zig | |
| download | site-398874a3dd76193122472cd015bd7d6cd0b9300b.tar.gz | |
commit mvp
Signed-off-by: Robert Günzler <r@gnzler.io>
Diffstat (limited to 'src/main.zig')
| -rw-r--r-- | src/main.zig | 323 |
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} — {[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> © — + \\<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); +} |