From e99346fdf21a3d25d30945efaa626f8362dc6c27 Mon Sep 17 00:00:00 2001 From: fig <86081585+fig-eater@users.noreply.github.com> Date: Mon, 17 Nov 2025 22:54:25 -0600 Subject: [PATCH 1/5] fix: passing invalid reader/writer interface ptr --- src/main.zig | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main.zig b/src/main.zig index fe5c5a6..472a82e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -74,17 +74,19 @@ 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, .{}); } const cwd = std.fs.cwd; From 1de696d3f74695b413404e232ea96ed01cab8b84 Mon Sep 17 00:00:00 2001 From: fig <86081585+fig-eater@users.noreply.github.com> Date: Tue, 18 Nov 2025 00:07:37 -0600 Subject: [PATCH 2/5] fix: create output file if not existing, flush to file after writing text element. --- src/formatters/markdown.zig | 1 + src/main.zig | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/formatters/markdown.zig b/src/formatters/markdown.zig index 980702a..cc1550f 100644 --- a/src/formatters/markdown.zig +++ b/src/formatters/markdown.zig @@ -292,6 +292,7 @@ pub fn writeTextElement(node: Node, ctx: *const WriteContext) anyerror!void { defer ctx.allocator.free(text); try ctx.writer.writeAll(text); + try ctx.writer.flush(); } pub fn writeListItemElement(node: Node, ctx: *const WriteContext) anyerror!void { diff --git a/src/main.zig b/src/main.zig index 472a82e..edae0f8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -64,7 +64,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(); From 4238d1d80a0e8e5d8c35804a58148a7ac96b1d9f Mon Sep 17 00:00:00 2001 From: fig <86081585+fig-eater@users.noreply.github.com> Date: Tue, 18 Nov 2025 03:03:31 -0600 Subject: [PATCH 3/5] feat: add option to convert tabs into spaces --- src/formatters/markdown.zig | 45 ++++++++++++++++++++++++++++++++----- src/main.zig | 11 ++++++++- src/tokenizer.zig | 44 +++++++++++++++++++----------------- 3 files changed, 73 insertions(+), 27 deletions(-) diff --git a/src/formatters/markdown.zig b/src/formatters/markdown.zig index cc1550f..4c2a698 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, + + /// TODO: write docs :) + 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,28 @@ 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( + const replaced_newlines = try std.mem.replaceOwned( u8, ctx.allocator, try node.getText(), "\n", "\n\n", ); - defer ctx.allocator.free(text); - - try ctx.writer.writeAll(text); + defer ctx.allocator.free(replaced_newlines); + var t: []const u8 = 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, + replaced_newlines, + "\t", + spaces[0..convert_tab_size], + ); + t = replaced_tabs.?; + } + try ctx.writer.writeAll(t); try ctx.writer.flush(); } @@ -329,7 +347,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 = undefined; + 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, + try child.getText(), + "\t", + spaces[0..convert_tab_size], + ); + text = replaced_tabs.?; + } else { + text = try child.getText(); + } + try ctx.writer.writeAll(text); } } @@ -504,3 +537,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 edae0f8..db1d1cf 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", + .value_ref = r.mkRef(&config.convert_tab_size), + }, }), .target = cli.CommandTarget{ .action = cli.CommandAction{ .exec = processConfig }, @@ -52,6 +58,7 @@ 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 try std.fmt.parseInt(u8, config.convert_tab_size.?, 10); switch (input_source) { .file => { @@ -86,7 +93,9 @@ fn processConfig() !void { 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; diff --git a/src/tokenizer.zig b/src/tokenizer.zig index abb705b..521f054 100644 --- a/src/tokenizer.zig +++ b/src/tokenizer.zig @@ -313,23 +313,23 @@ fn canBeEscaped(byte: u8) bool { return std.mem.indexOfAny(u8, &.{byte}, escape_chars) != null; } -fn isNewlineString(str: []const u8) bool { - if (str.len != 2) { - return false; - } - - switch (str[0]) { - '\\' => {}, - else => { - return false; - }, - } - - switch (str[1]) { - 'n' => return true, - else => return false, - } -} +// fn isNewlineString(str: []const u8) bool { +// if (str.len != 2) { +// return false; +// } + +// switch (str[0]) { +// '\\' => {}, +// else => { +// return false; +// }, +// } + +// switch (str[1]) { +// 'n' => return true, +// else => return false, +// } +// } const State = enum { text, @@ -422,10 +422,12 @@ pub fn tokenize(allocator: std.mem.Allocator, reader: *std.io.Reader, options: O // append next byte byte = next_byte; escaped = true; - } else if (isNewlineString(&.{ byte, next_byte })) { - byte = '\n'; - escaped = true; - } else { + } + // else if (isNewlineString(&.{ byte, next_byte })) { + // byte = '\n'; + // escaped = true; + // } + else { // continue with next byte try parsed.buffer.append(allocator, byte); byte = next_byte; From 70c4c045afc9500862e2844f989edb6fc8cd4bbb Mon Sep 17 00:00:00 2001 From: fig <86081585+fig-eater@users.noreply.github.com> Date: Tue, 18 Nov 2025 03:30:34 -0600 Subject: [PATCH 4/5] chore: update doc comments, add cmd arg checking --- src/formatters/markdown.zig | 2 +- src/main.zig | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/formatters/markdown.zig b/src/formatters/markdown.zig index 4c2a698..3d3c0c4 100644 --- a/src/formatters/markdown.zig +++ b/src/formatters/markdown.zig @@ -101,7 +101,7 @@ pub const Options = struct { /// custom element handlers for application-specific rendering logic. user_data: ?*anyopaque = null, - /// TODO: write docs :) + /// Optional number of spaces to convert tabs to. convert_tab_size: ?u8 = null, }; diff --git a/src/main.zig b/src/main.zig index db1d1cf..a4818ab 100644 --- a/src/main.zig +++ b/src/main.zig @@ -37,7 +37,7 @@ pub fn main() !void { }, .{ .long_name = "convert_tab_size", - .help = "Convert tabs to given number of spaces", + .help = "Convert tabs to given number of spaces within [0, 255]", .value_ref = r.mkRef(&config.convert_tab_size), }, }), @@ -50,16 +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; - const convert_tab_size: ?u8 = if (config.convert_tab_size == null) null else try std.fmt.parseInt(u8, config.convert_tab_size.?, 10); - switch (input_source) { .file => { input_file = try cwd().openFile(config.input.?, .{}); From d437e805558ebccdfb9e0c773e909af89bb6763a Mon Sep 17 00:00:00 2001 From: fig <86081585+fig-eater@users.noreply.github.com> Date: Tue, 18 Nov 2025 03:38:53 -0600 Subject: [PATCH 5/5] chore: clean code --- src/formatters/markdown.zig | 22 +++++++++++-------- src/tokenizer.zig | 44 ++++++++++++++++++------------------- 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/src/formatters/markdown.zig b/src/formatters/markdown.zig index 3d3c0c4..630678a 100644 --- a/src/formatters/markdown.zig +++ b/src/formatters/markdown.zig @@ -288,28 +288,32 @@ pub fn writeListElement(node: Node, ctx: *const WriteContext) anyerror!void { } pub fn writeTextElement(node: Node, ctx: *const WriteContext) anyerror!void { + 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(replaced_newlines); - var t: []const u8 = 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, - replaced_newlines, + text, "\t", spaces[0..convert_tab_size], ); - t = replaced_tabs.?; + text = replaced_tabs.?; } - try ctx.writer.writeAll(t); + + try ctx.writer.writeAll(text); try ctx.writer.flush(); } @@ -347,21 +351,21 @@ 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| { - var text: []const u8 = undefined; + 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, - try child.getText(), + text, "\t", spaces[0..convert_tab_size], ); text = replaced_tabs.?; - } else { - text = try child.getText(); } + try ctx.writer.writeAll(text); } } diff --git a/src/tokenizer.zig b/src/tokenizer.zig index 521f054..abb705b 100644 --- a/src/tokenizer.zig +++ b/src/tokenizer.zig @@ -313,23 +313,23 @@ fn canBeEscaped(byte: u8) bool { return std.mem.indexOfAny(u8, &.{byte}, escape_chars) != null; } -// fn isNewlineString(str: []const u8) bool { -// if (str.len != 2) { -// return false; -// } - -// switch (str[0]) { -// '\\' => {}, -// else => { -// return false; -// }, -// } - -// switch (str[1]) { -// 'n' => return true, -// else => return false, -// } -// } +fn isNewlineString(str: []const u8) bool { + if (str.len != 2) { + return false; + } + + switch (str[0]) { + '\\' => {}, + else => { + return false; + }, + } + + switch (str[1]) { + 'n' => return true, + else => return false, + } +} const State = enum { text, @@ -422,12 +422,10 @@ pub fn tokenize(allocator: std.mem.Allocator, reader: *std.io.Reader, options: O // append next byte byte = next_byte; escaped = true; - } - // else if (isNewlineString(&.{ byte, next_byte })) { - // byte = '\n'; - // escaped = true; - // } - else { + } else if (isNewlineString(&.{ byte, next_byte })) { + byte = '\n'; + escaped = true; + } else { // continue with next byte try parsed.buffer.append(allocator, byte); byte = next_byte;