diff options
Diffstat (limited to 'src/Context.zig')
| -rw-r--r-- | src/Context.zig | 242 |
1 files changed, 242 insertions, 0 deletions
diff --git a/src/Context.zig b/src/Context.zig new file mode 100644 index 0000000..f5a638e --- /dev/null +++ b/src/Context.zig @@ -0,0 +1,242 @@ +const std = @import("std"); + +const Content = @import("Content.zig"); +const FileSwapper = @import("FileSwapper.zig"); +const gen = @import("gen.zig"); + +const Context = @This(); +const log = std.log.scoped(.Context); + +pub const Defaults = struct { + // TODO: should this be next to the root_path of Context.index? + pub const CONTENT_PATH = ".rbc/content"; + pub const CONTENT_REF = "../.."; + + pub const SITE_NAME = "gzr.im"; + pub const LICENSE_HOLDER = "robertgzr"; + pub const LICENSE_YEAR = "2025"; + pub const LICENSE_NAME = "BY-NC-SA 4.0"; + pub const LICENSE_URL = "https://git.sr.ht/~robertgzr/site/blob/master/LICENSE.content"; + pub const SOURCE_URL = "https://git.sr.ht/~robertgzr/site/tree/master"; + pub const PUBLIC_INBOX = "~robertgzr/public-inbox@lists.sr.ht"; +}; + +io: std.Io, +arena: std.heap.ArenaAllocator, +content: ?Content = null, +root: ?std.Io.Dir = null, + +pub fn init(io: std.Io, gpa: std.mem.Allocator) !Context { + return .{ + .io = io, + .arena = std.heap.ArenaAllocator.init(gpa), + }; +} + +pub fn deinit(self: *Context) void { + if (self.content != null) { + self.content.?.deinit(); + } + self.arena.deinit(); +} + +pub fn index(self: *Context, root_dir: std.Io.Dir) !void { + if (self.content == null) { + // setup content store + const content_dir = try root_dir.createDirPathOpen( + self.io, + Defaults.CONTENT_PATH, + .{ .open_options = .{ .iterate = true } }, + ); + self.content = try Content.init(self.io, self.arena.allocator(), content_dir); + self.root = root_dir; + } else { + try self.gc(); + } + + var walk = try root_dir.walkSelectively(self.arena.allocator()); + defer walk.deinit(); + + var digests: std.ArrayList(Content.Digest) = .empty; + defer digests.deinit(self.arena.allocator()); + + while (try walk.next(self.io)) |e| { + switch (e.kind) { + .file => { + if (std.mem.endsWith(u8, e.basename, ".htm")) { + const dgst = try self.content.?.add(root_dir, e.path); + try self.content.?.propSet(dgst, "link", e.path); + + try digests.append(self.arena.allocator(), dgst); + log.debug("Indexing page {s} (digest={s})", .{ e.path, dgst }); + } + }, + else => {}, + } + } + + log.info("Indexed {d} pages", .{digests.items.len}); +} + +// garbage collect +fn gc(self: *Context) !void { + if (self.content == null) { + return error.NoIndex; + } + + var etor = try self.content.?.enumerate(self.arena.allocator()); + defer etor.deinit(); + + var gc_n: usize = 0; + while (try etor.next()) |dgst| { + if (try self.content.?.propGet(dgst, "link", self.arena.allocator())) |link| { + defer self.arena.allocator().free(link); + + // cleanup entries with missing link + self.root.?.access(self.io, link, .{}) catch |err| switch (err) { + error.FileNotFound => { + try self.content.?.del(dgst); + gc_n += 1; + }, + else => return err, + }; + } + } + + log.info("Garbage collected {d} pages", .{gc_n}); +} + +pub fn render(self: *Context, site_dir: std.Io.Dir, force: bool) !void { + if (self.content == null) { + return error.NoIndex; + } + + log.info("Rendering site", .{}); + + var page_n: u32 = 0; + + var etor = try self.content.?.enumerate(self.arena.allocator()); + defer etor.deinit(); + + // render pages + while (try etor.next()) |dgst| { + if (!force and try self.hasRenderPage(dgst, site_dir)) { + continue; // no need to re-render + } + + try self.renderPage(site_dir, dgst); + page_n += 1; + } + + log.info("Rendered {d} pages", .{page_n}); +} + +fn hasRenderPage(self: *Context, dgst: Content.Digest, site_dir: std.Io.Dir) !bool { + const render_path = try self.content.?.propGet(dgst, "render", self.arena.allocator()); + if (render_path == null) { + return false; + } + defer self.arena.allocator().free(render_path.?); + + site_dir.access(self.io, render_path.?, .{}) catch |err| switch (err) { + error.FileNotFound => return false, + else => return err, + }; + + log.debug("Skipping {s}", .{render_path.?}); + return true; +} + +fn renderPage(self: *Context, site_dir: std.Io.Dir, dgst: Content.Digest) !void { + var arena = std.heap.ArenaAllocator.init(self.arena.allocator()); + defer arena.deinit(); + const gpa = arena.allocator(); + + const page_path = try self.content.?.propGet(dgst, "link", self.arena.allocator()); + if (page_path == null) { + return error.NoLink; + } + + const page_name = std.mem.cutSuffix(u8, std.fs.path.basename(page_path.?), ".htm").?; + + if (std.mem.startsWith(u8, page_name, "meta.")) { + log.warn("skipping meta page => {s}", .{page_name}); + return; + } else { + log.debug("Rendering page {s} (name={s})", .{ page_path.?, page_name }); + + const render_name = try std.mem.concat(gpa, u8, &.{ page_name, ".html" }); + const pretty_name = try std.mem.replaceOwned(u8, gpa, page_name, "_", " "); + + const sw = try FileSwapper.createAt(self.io, gpa, site_dir, render_name); + defer sw.close(); + var w = sw.writer(); + + try gen.build_htm(self.io, gpa, &w.interface, .{ + .root_dir = self.root.?, + .site_dir = site_dir, + .page_path = page_path.?, + .page_name = pretty_name, + .meta = .{ + .site_name = Defaults.SITE_NAME, + .license_url = Defaults.LICENSE_URL, + .license_holder = Defaults.LICENSE_HOLDER, + .license_year = Defaults.LICENSE_YEAR, + .license_name = Defaults.LICENSE_NAME, + .public_inbox = Defaults.PUBLIC_INBOX, + .source_url = Defaults.SOURCE_URL, + }, + }); + + try self.content.?.propSet(dgst, "render", render_name); + try self.content.?.propSet(dgst, "name", pretty_name); + } +} + +// test "e2e" { +// std.testing.log_level = .debug; +// +// const DEMO_DIR: []const u8 = "/home/robert/tmp/rbc-demo/"; +// const SITE_DIR: []const u8 = DEMO_DIR ++ "site/"; +// +// const demo_dir = try std.Io.Dir.cwd().openDir(std.testing.io, DEMO_DIR, .{ .iterate = true }); +// defer demo_dir.close(std.testing.io); +// const site_dir = try std.Io.Dir.cwd().createDirPathOpen(std.testing.io, SITE_DIR, .{}); +// defer site_dir.close(std.testing.io); +// +// var ctx = try Context.init(std.testing.io, std.testing.allocator); +// defer ctx.deinit(); +// +// try ctx.index(demo_dir); +// try ctx.render(site_dir); +// +// // TODO: +// // index; build a list of input files +// // parse; detect backlinks, dependencies, etc. +// // render; write output +// } + +test "gc" { + std.testing.log_level = .debug; + + var tmp = std.testing.tmpDir(.{ .iterate = true }); + defer tmp.cleanup(); + + const tmp_src = try tmp.dir.createDirPathOpen(std.testing.io, "src", .{ .open_options = .{ .iterate = true } }); + // const tmp_dst = try tmp.dir.createDirPathOpen(std.testing.io, "dst", .{}); + + try tmp_src.writeFile(std.testing.io, .{ .sub_path = "foo.htm", .data = "hello world" }); + + var ctx = try Context.init(std.testing.io, std.testing.allocator); + defer ctx.deinit(); + + try ctx.index(tmp_src); + try tmp_src.deleteFile(std.testing.io, "foo.htm"); + + try ctx.index(tmp_src); + var etor = try ctx.content.?.enumerate(std.testing.allocator); + defer etor.deinit(); + var seen: u32 = 0; + while (try etor.next()) |_| seen += 1; + try std.testing.expect(seen == 0); +} |