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");
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}