Makko, the people-oriented static site generator made for blogging.
forge.starlightnet.work/Team/Makko
ssg
static-site-generator
makko
starlight-network
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}