const MAX_FILE_SIZE = 1024 * 1024 * 1024 * 8; const std = @import("std"); const crypto = std.crypto; const md = @import("koino"); const datetime = @import("datetime"); const Datetime = datetime.datetime.Datetime; const helper = @import("helper.zig"); const Data = @import("Data.zig"); const Faux = @import("Faux.zig"); pub const Visibility = enum { public, secret, draft }; pub const Parsed = @This(); // Properties allocator: ?std.mem.Allocator = null, id: Data.Id, title: []const u8, description: []const u8, author: []const u8, tags: []const []const u8, created: i64, visibility: Visibility, body: []const u8, html: ?[]const u8 = null, const Metadata = struct { id: ?[]const u8 = null, title: []const u8 = "A title", description: []const u8 = "A description", author: []const u8 = "Mx. Makko", tags: []const u8 = "", visibility: Visibility = .draft, created: ?[]const u8 = null, disable_templating: bool = false, }; // Lifecycle management pub fn deinit(self: Parsed) void { if (self.allocator) |allocator| { allocator.free(self.title); allocator.free(self.description); allocator.free(self.author); helper.freeStringList(allocator, self.tags); allocator.free(self.body); if (self.html) |html| allocator.free(html); } } // File processing implementation const Kind = enum { markdown, html }; const kind_map = std.StaticStringMap(Kind).initComptime(.{ .{ ".md", .markdown }, .{ ".markdown", .markdown }, .{ ".html-post", .html }, }); const FileContext = struct { dir: std.fs.Dir, path: []const u8, allocator: std.mem.Allocator, raw: []const u8, }; fn fromMarkdownFile(o: *Parsed) !Parsed { o.html = try md.markdownToHtml( o.allocator.?, o.body, .{ .extensions = .{ .strikethrough = true, .table = true, }, .render = .{ .lazy_load_images = true, .unsafe = true, }, }, ); return o.*; } fn fromHTMLFile(o: *Parsed) !Parsed { o.html = try o.allocator.?.dupe(u8, o.body); return o.*; } fn generateRandomId() Data.Id { var bytes: [8]u8 = undefined; crypto.random.bytes(&bytes); return std.mem.bytesToValue(Data.Id, &bytes); } fn writeUpdatedFrontmatterFile( ctx: FileContext, frontmatter: []const u8, data: []const u8, ) !void { var file = try ctx.dir.openFile(ctx.path, .{ .mode = .write_only }); defer file.close(); try file.writeAll("---\n"); try file.writeAll(frontmatter); try file.writeAll("---\n"); try file.writeAll(data); } fn processFrontmatter( ctx: FileContext, raw_front: []const u8, body: []const u8, data: anytype, ) !Parsed { var f = try Faux.parse(raw_front, ctx.allocator); defer f.deinit(); const front = f.toStructure(Metadata) catch return error.CouldNotParseFrontmatter; var buffer = std.ArrayList(u8).init(ctx.allocator); defer buffer.deinit(); var need_update = false; const actual_id_str: []const u8 = if (front.id) |id| try ctx.allocator.dupe(u8, id) else blk: { need_update = true; const random_id_val = generateRandomId(); const random_id_str = try helper.i64ToBase64(random_id_val, ctx.allocator); try buffer.writer().print("id: {s}\n", .{random_id_str}); break :blk random_id_str; }; defer ctx.allocator.free(actual_id_str); try buffer.appendSlice(raw_front); const created_timestamp: i64 = if (front.created) |created| helper.parseIso8601(created) catch return error.CouldNotParseISO8601 else blk: { need_update = true; const now = std.time.milliTimestamp(); const dt = Datetime.fromTimestamp(now); const created_str = try dt.formatISO8601(ctx.allocator, false); defer ctx.allocator.free(created_str); try buffer.writer().print("created: {s}\n", .{created_str}); break :blk now; }; if (need_update) try writeUpdatedFrontmatterFile(ctx, buffer.items, body); const id_val = if (actual_id_str.len == 11) helper.base64ToI64(actual_id_str) catch return error.InvalidId else std.fmt.parseInt(Data.Id, actual_id_str, 10) catch return error.InvalidId; const processed_body = if (front.disable_templating) try ctx.allocator.dupe(u8, body) else blk: { const json_data = try helper.valueToJson(ctx.allocator, data); defer json_data.deinit(); break :blk try helper.template(ctx.allocator, body, json_data.value); }; return Parsed{ .allocator = ctx.allocator, .body = processed_body, .title = try ctx.allocator.dupe(u8, front.title), .description = try ctx.allocator.dupe(u8, front.description), .author = try ctx.allocator.dupe(u8, front.author), .tags = try helper.toKebabWords(ctx.allocator, front.tags), .id = @bitCast(id_val), .visibility = front.visibility, .created = created_timestamp, }; } fn createDefaultFrontmatter( ctx: FileContext, ) !Parsed { const author = try helper.getFullUserName(ctx.allocator); defer ctx.allocator.free(author); const random_id_val = generateRandomId(); const random_id_str = try helper.i64ToBase64(random_id_val, ctx.allocator); defer ctx.allocator.free(random_id_str); const new_front = try std.fmt.allocPrint(ctx.allocator, \\id: {s} \\title: My cool post \\description: Here's a post I made that is cool! \\author: {s} \\#visibility: public \\ , .{ random_id_str, author }); defer ctx.allocator.free(new_front); try writeUpdatedFrontmatterFile(ctx, new_front, ctx.raw); return Parsed{ .visibility = .draft, .allocator = null, .id = undefined, .title = undefined, .description = undefined, .author = undefined, .tags = undefined, .created = undefined, .body = undefined, }; } // Public interface pub fn fromFile( dir: std.fs.Dir, path: []const u8, allocator: std.mem.Allocator, data: anytype, ) !?Parsed { const ext = std.fs.path.extension(path); const kind = kind_map.get(ext) orelse return null; const raw = try dir.readFileAlloc(allocator, path, MAX_FILE_SIZE); defer allocator.free(raw); const ctx = FileContext{ .dir = dir, .path = path, .allocator = allocator, .raw = raw, }; var parsed: Parsed = if (helper.extractFrontmatter(raw)) |raw_front| try processFrontmatter(ctx, raw_front, raw[raw_front.len + 8 ..], data) else try createDefaultFrontmatter(ctx); if (parsed.visibility == .draft) return parsed; return switch (kind) { .markdown => try fromMarkdownFile(&parsed), .html => try fromHTMLFile(&parsed), }; }