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