diff --git a/src/formatters/markdown.zig b/src/formatters/markdown.zig index 980702a..630678a 100644 --- a/src/formatters/markdown.zig +++ b/src/formatters/markdown.zig @@ -79,6 +79,8 @@ pub const WriteContext = struct { write_element_fn: ?WriteElementFunction = null, /// Optional user data for custom handlers user_data: ?*anyopaque = null, + /// Optional number of spaces to convert tabs to. + convert_tab_size: ?u8 = null, }; /// Configuration options for Markdown rendering. @@ -98,6 +100,9 @@ pub const Options = struct { /// This data is available in the WriteContext and can be used by /// custom element handlers for application-specific rendering logic. user_data: ?*anyopaque = null, + + /// Optional number of spaces to convert tabs to. + convert_tab_size: ?u8 = null, }; pub const MarkdownElement = enum { @@ -153,6 +158,7 @@ pub fn renderDocument(allocator: Allocator, doc: Document, writer: *std.io.Write .writer = writer, .write_element_fn = options.write_element_fn, .user_data = options.user_data, + .convert_tab_size = options.convert_tab_size, }; try render(doc.root, &ctx); @@ -282,16 +288,33 @@ pub fn writeListElement(node: Node, ctx: *const WriteContext) anyerror!void { } pub fn writeTextElement(node: Node, ctx: *const WriteContext) anyerror!void { - const text = try std.mem.replaceOwned( + var text: []const u8 = try node.getText(); + + const replaced_newlines = try std.mem.replaceOwned( u8, ctx.allocator, - try node.getText(), + text, "\n", "\n\n", ); - defer ctx.allocator.free(text); + defer ctx.allocator.free(replaced_newlines); + text = replaced_newlines; + + var replaced_tabs: ?[]const u8 = null; + defer if (replaced_tabs != null) ctx.allocator.free(replaced_tabs.?); + if (ctx.convert_tab_size) |convert_tab_size| { + replaced_tabs = try std.mem.replaceOwned( + u8, + ctx.allocator, + text, + "\t", + spaces[0..convert_tab_size], + ); + text = replaced_tabs.?; + } try ctx.writer.writeAll(text); + try ctx.writer.flush(); } pub fn writeListItemElement(node: Node, ctx: *const WriteContext) anyerror!void { @@ -328,7 +351,22 @@ pub fn writeCodeElement(node: Node, ctx: *const WriteContext) !void { pub fn writeAllChildrenText(node: Node, ctx: *const WriteContext) !void { var it = node.iterator(.{ .type = .text }); while (it.next()) |child| { - try ctx.writer.writeAll(try child.getText()); + var text: []const u8 = try child.getText(); + + var replaced_tabs: ?[]const u8 = null; + defer if (replaced_tabs != null) ctx.allocator.free(replaced_tabs.?); + if (ctx.convert_tab_size) |convert_tab_size| { + replaced_tabs = try std.mem.replaceOwned( + u8, + ctx.allocator, + text, + "\t", + spaces[0..convert_tab_size], + ); + text = replaced_tabs.?; + } + + try ctx.writer.writeAll(text); } } @@ -503,3 +541,5 @@ const Document = @import("../Document.zig"); const std = @import("std"); const testing = std.testing; const logger = std.log.scoped(.markdown_formatter); + +const spaces: []const u8 = " " ** 255; diff --git a/src/main.zig b/src/main.zig index fe5c5a6..a4818ab 100644 --- a/src/main.zig +++ b/src/main.zig @@ -5,6 +5,7 @@ pub const std_options: Options = .{ var config = struct { input: ?[]const u8 = null, output: ?[]const u8 = null, + convert_tab_size: ?[]const u8 = null, }{}; const StreamSource = enum { @@ -34,6 +35,11 @@ pub fn main() !void { .help = "output file", .value_ref = r.mkRef(&config.output), }, + .{ + .long_name = "convert_tab_size", + .help = "Convert tabs to given number of spaces within [0, 255]", + .value_ref = r.mkRef(&config.convert_tab_size), + }, }), .target = cli.CommandTarget{ .action = cli.CommandAction{ .exec = processConfig }, @@ -44,15 +50,22 @@ pub fn main() !void { } fn processConfig() !void { + const input_source: StreamSource = if (config.input == null) .stdin else .file; + const output_source: StreamDestination = if (config.output == null) .stdout else .file; + const convert_tab_size: ?u8 = if (config.convert_tab_size == null) + null + else + std.fmt.parseInt(u8, config.convert_tab_size.?, 10) catch { + std.log.err("convert_tab_size must be an integer in range of [0, 255]", .{}); + return error.ConvertTabSizeInvalid; + }; + var input_file: File = undefined; defer input_file.close(); var output_file: File = undefined; defer output_file.close(); - const input_source: StreamSource = if (config.input == null) .stdin else .file; - const output_source: StreamDestination = if (config.output == null) .stdout else .file; - switch (input_source) { .file => { input_file = try cwd().openFile(config.input.?, .{}); @@ -64,7 +77,7 @@ fn processConfig() !void { switch (output_source) { .file => { - output_file = try cwd().openFile(config.output.?, .{}); + output_file = try cwd().createFile(config.output.?, .{}); }, .stdout => { output_file = std.fs.File.stdout(); @@ -74,17 +87,21 @@ fn processConfig() !void { var in_buf: [1024]u8 = undefined; var out_buf: [1024]u8 = undefined; - var reader = input_file.reader(&in_buf).interface; - var writer = output_file.writer(&out_buf).interface; + var file_reader = input_file.reader(&in_buf); + const reader = &file_reader.interface; + var file_writer = output_file.writer(&out_buf); + const writer = &file_writer.interface; var arena = ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); - const document = try lib.load(allocator, &reader, .{}); + const document = try lib.load(allocator, reader, .{}); defer document.deinit(); - try renderDocument(allocator, document, &writer, .{}); + try renderDocument(allocator, document, writer, .{ + .convert_tab_size = convert_tab_size, + }); } const cwd = std.fs.cwd;