Makko, the people-oriented static site generator made for blogging.
forge.starlightnet.work/Team/Makko
ssg
static-site-generator
makko
starlight-network
1const std = @import("std");
2
3const Makko = @import("Makko.zig");
4const Callbacks = @import("Callbacks.zig");
5const Data = @import("Data.zig");
6const Parsed = @import("Parsed.zig");
7
8const helper = @import("helper.zig");
9
10const MAX_FILE_SIZE = 1024 * 1024 * 1024 * 9;
11
12const Pass = @This();
13
14const ChangeList = std.ArrayList(Callbacks.Change);
15
16parent: *Makko,
17changes: ChangeList,
18changes_mutex: std.Thread.Mutex = .{},
19
20progress: std.Progress.Node,
21
22pub fn init(self: *Makko) Pass {
23 return Pass{
24 .parent = self,
25 .changes = ChangeList.init(self.allocator),
26 .progress = self.progress.start("Pass", 1),
27 };
28}
29
30pub fn deinit(pass: *Pass) void {
31 for (pass.changes.items) |changes|
32 changes.deinit(pass.parent.allocator);
33 pass.changes.deinit();
34
35 pass.progress.end();
36}
37
38fn overlapping(state: Makko, path: []const u8) bool {
39 const Feeds = @import("FeedGenerator.zig");
40
41 inline for (Feeds.generator_list) |generator| {
42 const feed_path = @field(state.origin.value.feeds, generator.name);
43
44 if (feed_path) |p|
45 if (std.mem.eql(u8, p, path))
46 return true;
47 }
48
49 return if (state.paths.source.access(path, .{}))
50 true
51 else |_|
52 false;
53}
54
55fn getURL(file_path: []const u8, allocator: std.mem.Allocator) ![]const u8 {
56 const ext_size = std.fs.path.extension(file_path).len;
57
58 return try std.mem.concat(allocator, u8, &.{
59 file_path[0 .. file_path.len - ext_size], ".html",
60 });
61}
62
63pub fn processFile(
64 pass: *Pass,
65 file: []const u8,
66) !?Parsed.Visibility {
67 const parent = pass.parent;
68
69 // TODO: maybe get rid of the thread_safe_allocator?
70 const allocator = parent.thread_safe_allocator.allocator();
71 const paths = parent.paths;
72 var data_copy = parent.data;
73
74 data_copy.pass.feed_template = 0;
75
76 const file_node = pass.progress.start(file, 3);
77 defer file_node.end();
78
79 // FIXME
80 //if (overlapping(parent.*, file))
81 // return error.OverlappingFiles;
82
83 /////////////////////////////////////////////////////////////////////
84
85 const parse_progress = file_node.start("Parse", 1);
86
87 const f_n = Parsed.fromFile(
88 paths.source,
89 file,
90 allocator,
91 data_copy,
92 ) catch |err| {
93 parent.log.err("Cannot process {s}! ({})", .{ file, err });
94 return err;
95 };
96
97 const f = f_n orelse {
98 const copy_progress = file_node.start("Copy", 1);
99 defer copy_progress.end();
100
101 parent.paths.output.deleteFile(file) catch {};
102
103 if (parent.symlinks_enabled) {
104 const source = try paths.source.realpathAlloc(
105 allocator,
106 file,
107 );
108 defer allocator.free(source);
109
110 const output_dir = try paths.output.realpathAlloc(
111 allocator,
112 ".",
113 );
114 defer allocator.free(output_dir);
115
116 const output = try std.fs.path.join(
117 allocator,
118 &.{ output_dir, file },
119 );
120 defer allocator.free(output);
121
122 try helper.relativeSymlink(source, output, allocator);
123 return null;
124 }
125
126 try paths.source.copyFile(
127 file,
128 paths.output,
129 file,
130 .{},
131 );
132
133 return null;
134 };
135 defer f.deinit();
136
137 defer parse_progress.end();
138
139 /////////////////////////////////////////////////////////////////////
140
141 if (f.visibility == .draft)
142 return f.visibility;
143
144 const url = try getURL(file, allocator);
145 defer allocator.free(url);
146
147 const body = f.html orelse f.body;
148
149 data_copy.post = Data.Post{
150 .id = f.id,
151
152 .title = f.title,
153 .description = f.description,
154 .author = f.author,
155
156 .tags = f.tags,
157
158 .created = Data.timeFromTimestamp(f.created),
159 .updated = Data.empty_time,
160
161 .is_secret = f.visibility == .secret,
162 .source = file,
163 .url = url,
164 .body = body,
165 };
166
167 {
168 parent.processed_posts_mutex.lock();
169 defer parent.processed_posts_mutex.unlock();
170
171 try parent.processed_posts.put(
172 f.id,
173 try allocator.dupe(u8, file),
174 );
175 }
176
177 /////////////////////////////////////////////////////////////////////
178
179 const hasher_progress = file_node.start("Hash", 1);
180
181 const hash = blk: {
182 const json_data = try helper.valueToJson(
183 allocator,
184 data_copy,
185 );
186 defer json_data.deinit();
187
188 break :blk helper.hashJsonMap(json_data.value.object);
189 };
190
191 var status: Callbacks.Status = .unchanged;
192
193 {
194 parent.hashes_mutex.lock();
195 defer parent.hashes_mutex.unlock();
196
197 const now = Data.timeFromTimestamp(std.time.milliTimestamp());
198
199 if (parent.hashes.getPtr(f.id)) |ptr| {
200 if (ptr.* != hash) {
201 status = .modified;
202 ptr.* = hash;
203 data_copy.post.?.updated = now;
204 }
205 } else {
206 status = .created;
207 data_copy.post.?.created = now;
208 data_copy.post.?.updated = now;
209 try parent.hashes.put(f.id, hash);
210 }
211 }
212
213 hasher_progress.end();
214
215 // CRITICAL TODO: IMPLEMENT CHANGES
216
217 /////////////////////////////////////////////////////////////////////
218
219 const writer_progress = file_node.start("Write", 1);
220 defer writer_progress.end();
221
222 var should_generate_origin = !helper.exists(parent.paths.output, file);
223 var should_generate_output = !helper.exists(parent.paths.output, url);
224
225 if (status != .unchanged) {
226 pass.changes_mutex.lock();
227 defer pass.changes_mutex.unlock();
228
229 const real_source = try parent.paths.source.realpathAlloc(allocator, file);
230 defer allocator.free(real_source);
231
232 const pre_real_output = try parent.paths.output.realpathAlloc(allocator, ".");
233 defer allocator.free(pre_real_output);
234
235 const real_output = try std.fs.path.join(allocator, &.{
236 pre_real_output, url,
237 });
238 defer allocator.free(real_output);
239
240 const change: Callbacks.Change = .{
241 .status = status,
242 .id = f.id,
243
244 .source = real_source,
245 .output = real_output,
246
247 .title = f.title,
248 .description = f.description,
249 .author = f.author,
250
251 .timestamp = data_copy.post.?.updated.raw,
252 };
253
254 should_generate_origin = true;
255 should_generate_output = true;
256 try pass.changes.append(try change.copy(allocator));
257 }
258
259 if (should_generate_origin)
260 try parent.paths.output.writeFile(.{
261 .sub_path = file,
262 .data = f.body,
263 });
264
265 if (should_generate_output) {
266 const json_data = try helper.valueToJson(
267 allocator,
268 data_copy,
269 );
270 defer json_data.deinit();
271
272 const second_pass = try helper.template(
273 allocator,
274 parent.templates.post.?,
275 json_data.value,
276 );
277 defer allocator.free(second_pass);
278
279 try parent.paths.output.writeFile(.{
280 .sub_path = url,
281 .data = second_pass,
282 });
283 }
284
285 if (f.visibility == .public) {
286 parent.posts_mutex.lock();
287 defer parent.posts_mutex.unlock();
288
289 const arena = parent.arena.allocator();
290
291 const new_post = data_copy.post.?;
292 const posts = parent.posts.items;
293
294 for (0..posts.len) |i| {
295 const post = posts[i];
296
297 if (post.id == new_post.id) {
298 Data.freePost(post, arena);
299 posts[i] = try Data.copyPost(new_post, arena);
300 return f.visibility;
301 }
302 }
303
304 try parent.posts.append(
305 try Data.copyPost(
306 new_post,
307 arena,
308 ),
309 );
310 }
311
312 return f.visibility;
313}
314
315pub fn markDeletedById(pass: *Pass, id: Data.Id) !void {
316 const parent = pass.parent;
317 const allocator = parent.allocator;
318
319 {
320 parent.hashes_mutex.lock();
321 defer parent.hashes_mutex.unlock();
322
323 _ = parent.hashes.remove(id);
324 }
325
326 {
327 parent.processed_posts_mutex.lock();
328 defer parent.processed_posts_mutex.unlock();
329
330 _ = parent.processed_posts.swapRemove(id);
331 }
332
333 {
334 pass.changes_mutex.lock();
335 defer pass.changes_mutex.unlock();
336
337 var source: ?[]const u8 = null;
338 if (pass.parent.processed_posts.get(id)) |path| {
339 source = try allocator.dupe(u8, path);
340 }
341
342 try pass.changes.append(.{
343 .timestamp = std.time.milliTimestamp(),
344 .status = .deleted,
345 .id = id,
346 .source = source,
347 });
348 }
349
350 {
351 parent.posts_mutex.lock();
352 defer parent.posts_mutex.unlock();
353
354 const posts = parent.posts.items;
355 for (0..posts.len) |i| {
356 const post = posts[i];
357
358 if (post.id == id) {
359 Data.freePost(
360 parent.posts.swapRemove(i),
361 parent.arena.allocator(),
362 );
363 break;
364 }
365 }
366 }
367}
368
369// Assumes file has already been parsed at least once per Makko state.
370pub fn deleteFileByPath(pass: *Pass, path: []const u8) !void {
371 const parent = pass.parent;
372 const paths = parent.paths;
373
374 const file_node = pass.progress.start(path, 3);
375 defer file_node.end();
376
377 const processed = parent.processed_posts;
378
379 paths.output.deleteTree(path) catch {};
380
381 var id: Data.Id = 0;
382 {
383 var maybe_id: ?Data.Id = null;
384 var iter = processed.iterator();
385 while (iter.next()) |entry| {
386 if (std.mem.eql(u8, entry.value_ptr.*, path))
387 maybe_id = entry.key_ptr.*;
388 }
389 id = maybe_id orelse return;
390
391 const url = try getURL(path, parent.allocator);
392 defer parent.allocator.free(url);
393
394 paths.output.deleteTree(url) catch {};
395 }
396
397 try pass.markDeletedById(id);
398}
399
400pub fn printChanges(pass: *Pass) !void {
401 if (pass.changes.items.len == 0)
402 return;
403
404 const allocator = pass.parent.allocator;
405
406 const log = pass.parent.log;
407 log.header("Changes");
408
409 const source_path = try pass.parent.paths.source.realpathAlloc(allocator, ".");
410 defer allocator.free(source_path);
411
412 for (pass.changes.items) |item| {
413 if (item.source) |src| {
414 log.raw("- '{s}' ", .{src[source_path.len + 1 ..]});
415 } else log.raw("- 'id: {}' ", .{item.id});
416
417 log.raw("[{s}]\n", .{@tagName(item.status)});
418 }
419}
420
421pub fn runCallbacks(pass: *Pass) !void {
422 const parent = pass.parent;
423 const changes = try pass.changes.toOwnedSlice();
424 defer {
425 for (changes) |change|
426 change.deinit(parent.allocator);
427 pass.changes.allocator.free(changes);
428 }
429 try parent.origin.value.callbacks.run(
430 changes,
431 pass.parent.paths.root,
432 pass.parent.log,
433 parent.allocator,
434 );
435}