commit c641d71ac828a0e83f74777e206b9fc8d2828427
Author: tri <tri@thac.loan>
Date:   Sat Oct 11 11:21:35 2025 +0700

    first slice: parse .dj files with frontmatter

diff --git a/.luarc.json b/.luarc.json
new file mode 100644
index 0000000..625f331
--- /dev/null
+++ b/.luarc.json
@@ -0,0 +1,3 @@
+{
+  "diagnostics.disable": ["lowercase-global"]
+}
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..5e36e86
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,5 @@
+test:
+	zig build test --watch
+
+clean:
+	rm -rf .zig-cache zig-out
diff --git a/sample/first/index.dj b/sample/first/index.dj
new file mode 100644
index 0000000..0e7f140
--- /dev/null
+++ b/sample/first/index.dj
@@ -0,0 +1,8 @@
+```
+title: First
+published_at: 2025-10-09T21:13:03+07:00
+```
+
+## First!
+
+Hello _friend_.
diff --git a/sample/first/index.html b/sample/first/index.html
new file mode 100644
index 0000000..af32d2e
--- /dev/null
+++ b/sample/first/index.html
@@ -0,0 +1,4 @@
+<section id="First">
+<h2>First!</h2>
+<p>Hello <em>friend</em>.</p>
+</section>
diff --git a/sample/meta.json b/sample/meta.json
new file mode 100644
index 0000000..f1b193f
--- /dev/null
+++ b/sample/meta.json
@@ -0,0 +1,4 @@
+{
+  "site_name": "Sample Site",
+  "tagline": "Who samples the samplers?"
+}
diff --git a/sample/second/index.dj b/sample/second/index.dj
new file mode 100644
index 0000000..22e6cfa
--- /dev/null
+++ b/sample/second/index.dj
@@ -0,0 +1,6 @@
+```
+title: Second post
+published_at: 2025-10-11T09:04:40+07:00
+```
+
+Look ma, second post!
diff --git a/sample/second/index.html b/sample/second/index.html
new file mode 100644
index 0000000..21750bd
--- /dev/null
+++ b/sample/second/index.html
@@ -0,0 +1 @@
+<p>Look ma, second post!</p>
diff --git a/sample/second/meta.json b/sample/second/meta.json
new file mode 100644
index 0000000..9261e08
--- /dev/null
+++ b/sample/second/meta.json
@@ -0,0 +1,4 @@
+{
+  "title": "Secondly",
+  "published_at": "2025-10-09T23:51:45+07:00"
+}
diff --git a/src/Djot.lua b/src/Djot.lua
new file mode 100644
index 0000000..ee6cc40
--- /dev/null
+++ b/src/Djot.lua
@@ -0,0 +1,24 @@
+local djot = require("djot");
+
+--ignore_frontmatter = { {
+--    code_block = {
+--        enter = function(e)
+--            if e.lang == "frontmatter" then
+--                e.text = "shit"
+--            end
+--        end
+--        ,
+--        exit = function(e)
+--            if e.lang == "frontmatter" then
+--                e.attr = e.attr or djot.ast.new_attributes()
+--                e.attr.style = "display:none"
+--            end
+--        end
+--    }
+--} }
+
+function djot_to_html(input)
+    local doc = djot.parse(input)
+    --djot.filter.apply_filter(doc, ignore_frontmatter)
+    return djot.render_html(doc)
+end
diff --git a/src/Djot.zig b/src/Djot.zig
index ba81db4..b43c77b 100644
--- a/src/Djot.zig
+++ b/src/Djot.zig
@@ -22,12 +22,7 @@ pub fn init(gpa: mem.Allocator) !Djot {
         return err;
     };
 
-    lua.doString(
-        \\djot = require("djot")
-        \\function djot_to_html(input)
-        \\  return djot.render_html(djot.parse(input))
-        \\end
-    ) catch |err| {
+    lua.doString(@embedFile("Djot.lua")) catch |err| {
         try djot.printError();
         return err;
     };
@@ -62,7 +57,6 @@ pub fn toHtml(self: *const Djot, gpa: mem.Allocator, djot_text: []const u8) ![]c
     return result;
 }
 
-/// Caller owns returned memory
 pub fn writeHtml(self: *const Djot, writer: *Io.Writer, djot_text: []const u8) !void {
     // call the global function
     assert(try self.lua.getGlobal("djot_to_html") == .function);
diff --git a/src/Post.zig b/src/Post.zig
new file mode 100644
index 0000000..09bca06
--- /dev/null
+++ b/src/Post.zig
@@ -0,0 +1,47 @@
+const std = @import("std");
+const mem = std.mem;
+const println = @import("utils.zig").println;
+const Djot = @import("Djot.zig");
+
+pub const Post = @This();
+
+title: []const u8 = "",
+published_at: []const u8 = "",
+djot_text: []const u8 = "",
+
+pub fn init(gpa: mem.Allocator, input: []const u8) !Post {
+    const fence = "```";
+    var post = Post{};
+
+    var lines = mem.splitScalar(u8, input, '\n');
+    var idx: usize = 0;
+
+    const first_line = lines.next().?;
+    if (!mem.eql(u8, first_line, fence)) {
+        @panic("First line must be " ++ fence ++ " to start frontmatter");
+    }
+    idx += first_line.len + 1;
+
+    // Read frontmatter line by line
+    while (lines.next()) |line| {
+        defer idx += line.len + 1;
+
+        if (mem.eql(u8, line, fence)) {
+            break;
+        }
+        if (mem.startsWith(u8, line, "title: ")) {
+            post.title = try gpa.dupe(u8, line["title: ".len..]);
+        } else if (mem.startsWith(u8, line, "published_at: ")) {
+            post.published_at = try gpa.dupe(u8, line["published_at: ".len..]);
+        }
+    }
+
+    post.djot_text = try gpa.dupe(u8, input[idx..]);
+    return post;
+}
+
+pub fn deinit(self: Post, gpa: mem.Allocator) void {
+    gpa.free(self.title);
+    gpa.free(self.published_at);
+    gpa.free(self.djot_text);
+}
diff --git a/src/main.zig b/src/main.zig
index d90905b..1347cf5 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -1,7 +1,29 @@
 const std = @import("std");
+const fs = std.fs;
+const os = std.os;
+const mem = std.mem;
+
+const println = @import("utils.zig").println;
 const Djot = @import("Djot.zig");
+const Post = @import("Post.zig");
+
+pub fn main() !u8 {
+    var target_dir: [*:0]const u8 = undefined;
+    switch (os.argv.len) {
+        1 => target_dir = ".",
+        2 => target_dir = os.argv[1],
+        else => {
+            std.debug.print("Usage: loa [target_dir]\n", .{});
+            return 1;
+        },
+    }
 
-pub fn main() !void {
+    try entrypoint(target_dir);
+
+    return 0;
+}
+
+pub fn entrypoint(target_dir: [*:0]const u8) !void {
     var gpa_state = std.heap.DebugAllocator(.{}){};
     const gpa = gpa_state.allocator();
     defer _ = gpa_state.deinit();
@@ -9,11 +31,45 @@ pub fn main() !void {
     const djot = try Djot.init(gpa);
     defer djot.deinit();
 
-    const html = try djot.toHtml(gpa, "# foo");
-    defer gpa.free(html);
-    std.debug.print("--\n{s}\n--", .{html});
+    var dir = try fs.cwd().openDirZ(target_dir, .{ .iterate = true });
+    defer dir.close();
+
+    var walker = try dir.walk(gpa);
+    defer walker.deinit();
+
+    var posts: std.ArrayList(Post) = try .initCapacity(gpa, 16);
+    defer {
+        for (posts.items) |p| {
+            p.deinit(gpa);
+        }
+        posts.deinit(gpa);
+    }
+
+    var djot_buf: [1024 * 16]u8 = undefined;
+    var writer_buf: [1024 * 4]u8 = undefined;
+    while (try walker.next()) |entry| {
+        if (entry.kind == .file and mem.eql(u8, entry.basename, "index.dj")) {
+            const djot_text = try entry.dir.readFile(entry.basename, &djot_buf);
+
+            const p = try Post.init(gpa, djot_text);
+            try posts.append(gpa, p);
+
+            var html_file = try entry.dir.createFile("index.html", .{});
+            defer html_file.close();
+
+            var html_writer = html_file.writer(&writer_buf);
+            try djot.writeHtml(&html_writer.interface, p.djot_text);
+            try html_writer.interface.flush();
+        }
+    }
+
+    println("Found {d} posts", .{posts.items.len});
 }
 
 test {
     std.testing.refAllDeclsRecursive(@This());
 }
+
+test entrypoint {
+    try entrypoint("sample");
+}
diff --git a/src/utils.zig b/src/utils.zig
new file mode 100644
index 0000000..1e03cdc
--- /dev/null
+++ b/src/utils.zig
@@ -0,0 +1,6 @@
+const std = @import("std");
+const print = std.debug.print;
+
+pub fn println(comptime fmt: []const u8, args: anytype) void {
+    return print(fmt ++ "\n", args);
+}