Makko, the people-oriented static site generator made for blogging. forge.starlightnet.work/Team/Makko
ssg static-site-generator makko starlight-network
at main 7.2 kB view raw
1const MAX_FILE_SIZE = 1024 * 1024 * 1024 * 8; 2 3const std = @import("std"); 4const crypto = std.crypto; 5const md = @import("koino"); 6const datetime = @import("datetime"); 7const Datetime = datetime.datetime.Datetime; 8 9const helper = @import("helper.zig"); 10const Data = @import("Data.zig"); 11const Faux = @import("Faux.zig"); 12 13pub const Visibility = enum { public, secret, draft }; 14 15pub const Parsed = @This(); 16 17// Properties 18allocator: ?std.mem.Allocator = null, 19 20id: Data.Id, 21title: []const u8, 22description: []const u8, 23author: []const u8, 24tags: []const []const u8, 25created: i64, 26visibility: Visibility, 27body: []const u8, 28html: ?[]const u8 = null, 29 30const Metadata = struct { 31 id: ?[]const u8 = null, 32 title: []const u8 = "A title", 33 description: []const u8 = "A description", 34 author: []const u8 = "Mx. Makko", 35 tags: []const u8 = "", 36 visibility: Visibility = .draft, 37 created: ?[]const u8 = null, 38 disable_templating: bool = false, 39}; 40 41// Lifecycle management 42pub fn deinit(self: Parsed) void { 43 if (self.allocator) |allocator| { 44 allocator.free(self.title); 45 allocator.free(self.description); 46 allocator.free(self.author); 47 helper.freeStringList(allocator, self.tags); 48 allocator.free(self.body); 49 if (self.html) |html| allocator.free(html); 50 } 51} 52 53// File processing implementation 54const Kind = enum { markdown, html }; 55const kind_map = std.StaticStringMap(Kind).initComptime(.{ 56 .{ ".md", .markdown }, 57 .{ ".markdown", .markdown }, 58 .{ ".html-post", .html }, 59}); 60 61const FileContext = struct { 62 dir: std.fs.Dir, 63 path: []const u8, 64 allocator: std.mem.Allocator, 65 raw: []const u8, 66}; 67 68fn fromMarkdownFile(o: *Parsed) !Parsed { 69 o.html = try md.markdownToHtml( 70 o.allocator.?, 71 o.body, 72 .{ 73 .extensions = .{ 74 .strikethrough = true, 75 .table = true, 76 }, 77 .render = .{ 78 .lazy_load_images = true, 79 .unsafe = true, 80 }, 81 }, 82 ); 83 return o.*; 84} 85 86fn fromHTMLFile(o: *Parsed) !Parsed { 87 o.html = try o.allocator.?.dupe(u8, o.body); 88 return o.*; 89} 90 91fn generateRandomId() Data.Id { 92 var bytes: [8]u8 = undefined; 93 crypto.random.bytes(&bytes); 94 return std.mem.bytesToValue(Data.Id, &bytes); 95} 96 97fn writeUpdatedFrontmatterFile( 98 ctx: FileContext, 99 frontmatter: []const u8, 100 data: []const u8, 101) !void { 102 var file = try ctx.dir.openFile(ctx.path, .{ .mode = .write_only }); 103 defer file.close(); 104 105 try file.writeAll("---\n"); 106 try file.writeAll(frontmatter); 107 try file.writeAll("---\n"); 108 try file.writeAll(data); 109} 110 111fn processFrontmatter( 112 ctx: FileContext, 113 raw_front: []const u8, 114 body: []const u8, 115 data: anytype, 116) !Parsed { 117 var f = try Faux.parse(raw_front, ctx.allocator); 118 defer f.deinit(); 119 120 const front = f.toStructure(Metadata) catch 121 return error.CouldNotParseFrontmatter; 122 123 var buffer = std.ArrayList(u8).init(ctx.allocator); 124 defer buffer.deinit(); 125 var need_update = false; 126 127 const actual_id_str: []const u8 = 128 if (front.id) |id| 129 try ctx.allocator.dupe(u8, id) 130 else blk: { 131 need_update = true; 132 133 const random_id_val = generateRandomId(); 134 const random_id_str = try helper.i64ToBase64(random_id_val, ctx.allocator); 135 136 try buffer.writer().print("id: {s}\n", .{random_id_str}); 137 break :blk random_id_str; 138 }; 139 defer ctx.allocator.free(actual_id_str); 140 141 try buffer.appendSlice(raw_front); 142 143 const created_timestamp: i64 = 144 if (front.created) |created| 145 helper.parseIso8601(created) catch 146 return error.CouldNotParseISO8601 147 else blk: { 148 need_update = true; 149 150 const now = std.time.milliTimestamp(); 151 const dt = Datetime.fromTimestamp(now); 152 const created_str = try dt.formatISO8601(ctx.allocator, false); 153 defer ctx.allocator.free(created_str); 154 155 try buffer.writer().print("created: {s}\n", .{created_str}); 156 break :blk now; 157 }; 158 159 if (need_update) 160 try writeUpdatedFrontmatterFile(ctx, buffer.items, body); 161 162 const id_val = 163 if (actual_id_str.len == 11) 164 helper.base64ToI64(actual_id_str) catch 165 return error.InvalidId 166 else 167 std.fmt.parseInt(Data.Id, actual_id_str, 10) catch 168 return error.InvalidId; 169 170 const processed_body = 171 if (front.disable_templating) 172 try ctx.allocator.dupe(u8, body) 173 else blk: { 174 const json_data = try helper.valueToJson(ctx.allocator, data); 175 defer json_data.deinit(); 176 break :blk try helper.template(ctx.allocator, body, json_data.value); 177 }; 178 179 return Parsed{ 180 .allocator = ctx.allocator, 181 .body = processed_body, 182 .title = try ctx.allocator.dupe(u8, front.title), 183 .description = try ctx.allocator.dupe(u8, front.description), 184 .author = try ctx.allocator.dupe(u8, front.author), 185 .tags = try helper.toKebabWords(ctx.allocator, front.tags), 186 .id = @bitCast(id_val), 187 .visibility = front.visibility, 188 .created = created_timestamp, 189 }; 190} 191 192fn createDefaultFrontmatter( 193 ctx: FileContext, 194) !Parsed { 195 const author = try helper.getFullUserName(ctx.allocator); 196 defer ctx.allocator.free(author); 197 198 const random_id_val = generateRandomId(); 199 const random_id_str = try helper.i64ToBase64(random_id_val, ctx.allocator); 200 defer ctx.allocator.free(random_id_str); 201 202 const new_front = try std.fmt.allocPrint(ctx.allocator, 203 \\id: {s} 204 \\title: My cool post 205 \\description: Here's a post I made that is cool! 206 \\author: {s} 207 \\#visibility: public 208 \\ 209 , .{ random_id_str, author }); 210 defer ctx.allocator.free(new_front); 211 212 try writeUpdatedFrontmatterFile(ctx, new_front, ctx.raw); 213 214 return Parsed{ 215 .visibility = .draft, 216 .allocator = null, 217 .id = undefined, 218 .title = undefined, 219 .description = undefined, 220 .author = undefined, 221 .tags = undefined, 222 .created = undefined, 223 .body = undefined, 224 }; 225} 226 227// Public interface 228pub fn fromFile( 229 dir: std.fs.Dir, 230 path: []const u8, 231 allocator: std.mem.Allocator, 232 data: anytype, 233) !?Parsed { 234 const ext = std.fs.path.extension(path); 235 const kind = kind_map.get(ext) orelse return null; 236 237 const raw = try dir.readFileAlloc(allocator, path, MAX_FILE_SIZE); 238 defer allocator.free(raw); 239 240 const ctx = FileContext{ 241 .dir = dir, 242 .path = path, 243 .allocator = allocator, 244 .raw = raw, 245 }; 246 247 var parsed: Parsed = 248 if (helper.extractFrontmatter(raw)) |raw_front| 249 try processFrontmatter(ctx, raw_front, raw[raw_front.len + 8 ..], data) 250 else 251 try createDefaultFrontmatter(ctx); 252 253 if (parsed.visibility == .draft) 254 return parsed; 255 256 return switch (kind) { 257 .markdown => try fromMarkdownFile(&parsed), 258 .html => try fromHTMLFile(&parsed), 259 }; 260}