Makko, the people-oriented static site generator made for blogging. forge.starlightnet.work/Team/Makko
ssg static-site-generator makko starlight-network
at main 12 kB view raw
1const std = @import("std"); 2const builtin = @import("builtin"); 3const mustache = @import("mustache"); 4const datetime = @import("datetime"); 5const json = std.json; 6 7const Data = @import("Data.zig"); 8const Datetime = datetime.datetime.Datetime; 9const Timezone = datetime.timezones; 10 11pub const supports_symlinks = 12 switch (builtin.target.os.tag) { 13 .windows, 14 .plan9, 15 .fuchsia, 16 .wasi, 17 .hermit, 18 .driverkit, 19 .zos, 20 .ps3, 21 .ps4, 22 .ps5, 23 .uefi, 24 .rtems, 25 .contiki, 26 .elfiamcu, 27 .freestanding, 28 .other, 29 => false, 30 else => true, 31 }; 32 33pub fn relativeSymlink(from: []const u8, to: []const u8, allocator: std.mem.Allocator) !void { 34 const to_dir = std.fs.path.dirname(to) orelse 35 return error.CouldNotGetDirname; 36 37 const relative_target = try std.fs.path.relative(allocator, to_dir, from); 38 defer allocator.free(relative_target); 39 40 var dir = try std.fs.openDirAbsolute(to_dir, .{}); 41 defer dir.close(); 42 43 const to_basename = std.fs.path.basename(to); 44 45 try dir.symLink(relative_target, to_basename, .{}); 46} 47 48pub fn formatTwoDigitInt(value: u8, arena: std.mem.Allocator) ![]const u8 { 49 return try std.fmt.allocPrint(arena, "{:0>2}", .{value}); 50} 51 52pub fn parseTwoDigitInt(s: []const u8) !u32 { 53 if (s.len != 2) 54 return error.InvalidFormat; 55 return try std.fmt.parseInt(u32, s, 10); 56} 57 58pub fn parseSignedOffset(s: []const u8) !i32 { 59 // Parse timezone offset in format +HH:mm or -HH:mm 60 if (s.len != 6) return error.InvalidFormat; 61 const sign: i32 = switch (s[0]) { 62 '+' => 1, 63 '-' => -1, 64 else => return error.InvalidFormat, 65 }; 66 67 const hour = try parseTwoDigitInt(s[1..3]); 68 if (s[3] != ':') return error.InvalidFormat; 69 70 const minute = try parseTwoDigitInt(s[4..6]); 71 return sign * @as(i32, @bitCast((hour * 60) + minute)); 72} 73 74pub fn parseIso8601(input: []const u8) !i64 { 75 // Minimal length: 19 (YYYY-MM-DDTHH:mm:ss) 76 if (input.len < 19) return error.InvalidFormat; 77 78 const year = try std.fmt.parseInt(u16, input[0..4], 10); 79 if (input[4] != '-') return error.InvalidFormat; 80 81 const month = try parseTwoDigitInt(input[5..7]); 82 if (input[7] != '-') return error.InvalidFormat; 83 84 const day = try parseTwoDigitInt(input[8..10]); 85 if (input[10] != 'T' and input[10] != 't') return error.InvalidFormat; 86 87 const hour = try parseTwoDigitInt(input[11..13]); 88 if (input[13] != ':') return error.InvalidFormat; 89 90 const minute = try parseTwoDigitInt(input[14..16]); 91 if (input[16] != ':') return error.InvalidFormat; 92 93 const second = try parseTwoDigitInt(input[17..19]); 94 95 var offset_minutes: i32 = 0; 96 const rest = input[19..]; 97 98 if (rest.len == 0) { 99 // No timezone provided, assume local or UTC? Here assume UTC 100 offset_minutes = 0; 101 } else if (rest.len == 1 and rest[0] == 'Z') { 102 offset_minutes = 0; 103 } else if (rest.len == 6) { 104 offset_minutes = try parseSignedOffset(rest); 105 } else { 106 return error.InvalidFormat; 107 } 108 109 const dt = try Datetime.create( 110 year, 111 month, 112 day, 113 hour, 114 minute, 115 second, 116 0, 117 Timezone.UTC, 118 ); 119 120 return @truncate(dt.shiftMinutes(offset_minutes).toTimestamp()); 121} 122 123pub fn template( 124 allocator: std.mem.Allocator, 125 source: []const u8, 126 data: anytype, 127) ![]const u8 { 128 if (mustache.allocRenderText( 129 allocator, 130 source, 131 data, 132 )) |output| { 133 if (output.len > 0) 134 return output; 135 allocator.free(output); 136 } else |_| {} 137 138 return try allocator.dupe(u8, source); 139} 140 141pub inline fn toBytes(num: anytype) []const u8 { 142 return &@as([@sizeOf(@TypeOf(num))]u8, @bitCast(num)); 143} 144 145pub fn compareText(_: void, lhs: []const u8, rhs: []const u8) bool { 146 return std.mem.order(u8, lhs, rhs) == .lt; 147} 148 149fn parseGECOS(user: []const u8, allocator: std.mem.Allocator) ?[]const u8 { 150 var file = std.fs.openFileAbsolute("/etc/passwd", .{}) catch return null; 151 defer file.close(); 152 153 var buf_reader = std.io.bufferedReader(file.reader()); 154 var in_stream = buf_reader.reader(); 155 var buf: [128]u8 = undefined; 156 157 while (in_stream.readUntilDelimiterOrEof(&buf, '\n') catch return null) |line| { 158 if (!std.mem.startsWith(u8, line, user)) 159 continue; 160 161 const s = std.mem.indexOfScalar(u8, line, ':') orelse continue; 162 if (s != user.len) 163 continue; 164 165 var i: usize = 0; 166 167 var token_iter = std.mem.splitScalar(u8, line, ':'); 168 while (token_iter.next()) |token| { 169 if (i == 4) { 170 if (token.len == 0) 171 return null; 172 173 return allocator.dupe(u8, token) catch null; 174 } 175 i = i + 1; 176 } 177 } 178 179 return null; 180} 181 182const fallback = "Mx. Makko"; 183 184pub fn getFullUserName(allocator: std.mem.Allocator) ![]const u8 { 185 var envmap = try std.process.getEnvMap(allocator); 186 defer envmap.deinit(); 187 188 if (envmap.get("USER")) |user| { 189 if (parseGECOS(user, allocator)) |full_name| 190 return full_name; 191 192 return try allocator.dupe(u8, user); 193 } 194 195 // Make sure that the caller owns the memory! 196 return try allocator.dupe(u8, fallback); 197} 198 199pub fn exists(dir: std.fs.Dir, path: []const u8) bool { 200 _ = dir.access(path, .{}) catch return false; 201 return true; 202} 203 204pub fn isDir(dir: std.fs.Dir, path: []const u8) bool { 205 var f = dir.openDir(path, .{}) catch return false; 206 f.close(); 207 208 return true; 209} 210 211pub fn extractFrontmatter(text: []const u8) ?[]const u8 { 212 var start_pos: usize = 0; 213 var end_pos: usize = 0; 214 var in_frontmatter = false; 215 216 var lines = std.mem.tokenizeScalar(u8, text, '\n'); 217 var cursor: usize = 0; 218 219 while (lines.next()) |line| { 220 const trimmed = std.mem.trim(u8, line, " \t\r"); 221 const line_start = cursor; 222 cursor += line.len + 1; // +1 for '\n' 223 224 if (!in_frontmatter) { 225 if (std.mem.eql(u8, trimmed[0..3], "---")) { 226 in_frontmatter = true; 227 start_pos = cursor; 228 } else if (trimmed.len != 0) 229 return null; 230 } else { 231 if (std.mem.eql(u8, trimmed[0..3], "---")) { 232 end_pos = line_start; 233 return text[start_pos..end_pos]; 234 } 235 } 236 } 237 238 return null; 239} 240 241const hash_seed = 0x12345678; // don't you dare change this. 242const Hasher = std.hash.Wyhash; 243 244pub fn hashValue( 245 hasher: *Hasher, 246 obj: *std.json.Value, 247) void { 248 switch (obj.*) { 249 .null => hasher.update("nil\x00"), 250 .bool => |b| hasher.update(if (b) "tru\x00" else "fal\x00"), 251 252 .float => |f| { 253 hasher.update("flt\x00"); 254 hasher.update(toBytes(f)); 255 }, 256 257 .integer => |i| { 258 hasher.update("int\x00"); 259 hasher.update(toBytes(i)); 260 }, 261 262 .string, .number_string => |s| { 263 hasher.update("str\x00"); 264 hasher.update(s); 265 }, 266 267 .array => |a| { 268 hasher.update("arr\x00"); 269 for (a.items) |*element| 270 hashValue(hasher, element); 271 hasher.update("arr\x01"); 272 }, 273 274 .object => |*o| { 275 hasher.update("obj\x00"); 276 277 const keys = o.keys(); 278 279 const C = struct { 280 keys: [][]const u8, 281 282 pub fn lessThan(ctx: @This(), a: usize, b: usize) bool { 283 return compareText({}, ctx.keys[a], ctx.keys[b]); 284 } 285 }; 286 287 o.sort(C{ .keys = keys }); 288 289 for (o.keys()) |name| { 290 hasher.update("prp\x00"); 291 hasher.update(name); 292 const n = o.getPtr(name).?; 293 hashValue(hasher, n); 294 hasher.update("prp\x01"); 295 } 296 hasher.update("obj\x01"); 297 }, 298 } 299} 300 301pub fn hashJsonMap(map: std.json.ObjectMap) Data.Hash { 302 var hasher = Hasher.init(hash_seed); 303 var value: std.json.Value = .{ .object = map }; 304 hashValue(&hasher, &value); 305 const h: Data.Hash = @bitCast(hasher.final()); 306 return h; 307} 308 309pub fn hashString(data: []const u8) Data.Hash { 310 var hasher = Hasher.init(hash_seed); 311 hasher.update(data); 312 const h: Data.Hash = @bitCast(hasher.final()); 313 return h; 314} 315 316pub fn hash(data: anytype) Data.Hash { 317 return hashString(toBytes(data)); 318} 319 320// todo: implement this properly (it's very very very wasteful.) 321pub fn valueToJson( 322 arena: std.mem.Allocator, 323 value: anytype, 324) !std.json.Parsed(json.Value) { 325 const j = try std.json.stringifyAlloc( 326 arena, 327 value, 328 .{ .whitespace = .minified }, 329 ); 330 defer arena.free(j); 331 332 return try std.json.parseFromSlice( 333 json.Value, 334 arena, 335 j, 336 .{ .allocate = .alloc_always }, 337 ); 338} 339 340const base_encoder = std.base64.Base64Encoder; 341const base_decoder = std.base64.Base64Decoder; 342const base_alphabet = std.base64.url_safe_alphabet_chars; 343 344pub fn i64ToBase64(i: i64, allocator: std.mem.Allocator) ![]u8 { 345 var buffer: [@sizeOf(i64)]u8 = undefined; 346 var dest: [32]u8 = undefined; 347 std.mem.writeInt(i64, &buffer, i, .big); 348 349 const encoder = base_encoder.init(base_alphabet, null); 350 351 return try allocator.dupe(u8, encoder.encode(&dest, &buffer)); 352} 353 354pub fn base64ToI64(b64: []const u8) !i64 { 355 const decoder = base_decoder.init(base_alphabet, null); 356 357 var dest: [@sizeOf(i64)]u8 = undefined; 358 try decoder.decode(&dest, b64); 359 360 return std.mem.readInt(i64, &dest, .big); 361} 362 363pub fn isValidBase64(s: []const u8) bool { 364 str: for (s) |c| { 365 for (base_alphabet) |valid_char| 366 if (c == valid_char) 367 continue :str; 368 return false; 369 } 370 return true; 371} 372 373pub fn toKebabWords(allocator: std.mem.Allocator, input: []const u8) ![]const []const u8 { 374 var words = std.ArrayList([]const u8).init(allocator); 375 376 var it = std.mem.splitAny(u8, input, " \t"); 377 while (it.next()) |word| { 378 const kebab = try toKebabCase(allocator, word); 379 if (kebab.len == 0) { 380 allocator.free(kebab); 381 continue; 382 } 383 try words.append(kebab); 384 } 385 386 return try words.toOwnedSlice(); 387} 388 389// todo: handle utf8 390fn toKebabCase(allocator: std.mem.Allocator, input: []const u8) ![]const u8 { 391 var buf = std.ArrayList(u8).init(allocator); 392 393 var i: usize = 0; 394 while (i < input.len) { 395 const c = input[i]; 396 397 if (c == '_') { 398 try appendDashIfNeeded(&buf); 399 i += 1; 400 continue; 401 } 402 403 if (std.ascii.isDigit(c)) { 404 try appendDashIfNeeded(&buf); 405 try buf.append(c); 406 i += 1; 407 continue; 408 } 409 410 if (std.ascii.isUpper(c)) { 411 const next = 412 if (i + 1 < input.len) 413 input[i + 1] 414 else 415 0; 416 const is_acronym = 417 (i + 1 < input.len and std.ascii.isUpper(next)) or (i + 1 == input.len); 418 419 try appendDashIfNeeded(&buf); 420 try buf.append(std.ascii.toLower(c)); 421 i += 1; 422 423 if (is_acronym) { 424 while (i < input.len and std.ascii.isUpper(input[i])) { 425 try buf.append(std.ascii.toLower(input[i])); 426 i += 1; 427 } 428 } 429 430 continue; 431 } 432 433 try buf.append(c); 434 i += 1; 435 } 436 437 return buf.toOwnedSlice(); 438} 439 440fn appendDashIfNeeded(buf: *std.ArrayList(u8)) !void { 441 if (buf.items.len > 0 and buf.items[buf.items.len - 1] != '-') { 442 try buf.append('-'); 443 } 444} 445 446pub fn freeStringList( 447 allocator: std.mem.Allocator, 448 list: []const []const u8, 449) void { 450 defer allocator.free(list); 451 for (list) |str| 452 allocator.free(str); 453} 454 455pub fn copyStringList( 456 allocator: std.mem.Allocator, 457 list: []const []const u8, 458) ![]const []const u8 { 459 const copy = try allocator.alloc([]const u8, list.len); 460 for (list, 0..) |str, i| 461 copy[i] = try allocator.dupe(u8, str); 462 463 return copy; 464} 465 466pub fn fold64to32(h: u64) u32 { 467 var k = h; 468 k ^= k >> 33; 469 k *= 0xff51afd7ed558ccd; 470 k ^= k >> 33; 471 k *= 0xc4ceb9fe1a85ec53; 472 k ^= k >> 33; 473 return @intCast(k ^ (k >> 32)); 474}