Markdown Table Generator — Zig source
Turn pipe, CSV, tab, semicolon, or space-separated data into a clean GitHub-Flavored Markdown table. Auto-detects the delimiter, pads columns, escapes pipes, and supports per-column alignment - all in your browser.
This is the Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.
//! markdown-table — Zig port: split delimiter-separated rows and render a padded GFM table, ported from src/lib/markdown-table.ts.
const std = @import("std");
pub const Align = enum { none, left, center, right };
/// splitLine: literal-delimiter split (',', '\t', ';'), each cell trimmed.
fn splitLine(alloc: std.mem.Allocator, line: []const u8, delim: u8) ![][]const u8 {
var cells = std.ArrayList([]const u8).init(alloc);
defer cells.deinit();
var it = std.mem.splitScalar(u8, line, delim);
while (it.next()) |cell| try cells.append(std.mem.trim(u8, cell, " \t\r"));
return cells.toOwnedSlice();
}
/// parseTable: non-empty (post-trim) lines, each split by the delimiter.
/// The trailing \r of a CRLF pair is absorbed by the line trim.
fn parseTable(alloc: std.mem.Allocator, input: []const u8, delim: u8) ![][][]const u8 {
var rows = std.ArrayList([][]const u8).init(alloc);
defer rows.deinit();
var lines = std.mem.splitScalar(u8, input, '\n');
while (lines.next()) |raw| {
const line = std.mem.trim(u8, raw, " \t\r");
if (line.len == 0) continue;
try rows.append(try splitLine(alloc, line, delim));
}
return rows.toOwnedSlice();
}
/// escapeCell: newlines collapse to spaces, a literal '|' renders as '\|'.
fn escapeCell(alloc: std.mem.Allocator, cell: []const u8) ![]u8 {
var out = std.ArrayList(u8).init(alloc);
for (cell) |c| switch (c) {
'\r', '\n' => try out.append(' '),
'|' => try out.appendSlice("\\|"),
else => try out.append(c),
};
return out.toOwnedSlice();
}
/// pad: append a cell to `out`, filling to `width` per alignment.
fn appendPadded(writer: anytype, cell: []const u8, width: usize, align: Align) !void {
const diff = if (cell.len >= width) 0 else width - cell.len;
const left = switch (align) {
.right => diff,
.center => diff / 2,
.left, .none => 0, // left-aligned
};
try writer.writeByteNTimes(' ', left);
try writer.writeAll(cell);
try writer.writeByteNTimes(' ', diff - left);
}
/// sepCell: ---, :--, --:, :-: — the alignment separator, min width 3.
fn appendSep(writer: anytype, align: Align, width: usize) !void {
const w = @max(width, 3);
switch (align) {
.center => {
try writer.writeAll(":");
try writer.writeByteNTimes('-', w - 2);
try writer.writeAll(":");
},
.right => {
try writer.writeByteNTimes('-', w - 1);
try writer.writeAll(":");
},
.left => {
try writer.writeAll(":");
try writer.writeByteNTimes('-', w - 1);
},
.none => try writer.writeByteNTimes('-', w),
}
}
/// toMarkdown: pad every column to its widest (escaped) cell and lay rows out
/// as '| a | b |', with the separator row under the header row.
fn toMarkdown(alloc: std.mem.Allocator, rows: []const []const []const u8, header: bool, aligns: []const Align) ![]u8 {
if (rows.len == 0) return alloc.dupe(u8, "");
var cols: usize = 0;
for (rows) |r| cols = @max(cols, r.len);
// Escape every cell and pad jagged rows with '' so the grid is rectangular.
var grid = std.ArrayList([]const u8).init(alloc);
defer grid.deinit();
for (rows) |r| {
for (r) |c| try grid.append(try escapeCell(alloc, c));
for (r.len..cols) |_| try grid.append("");
}
const widths = try alloc.alloc(usize, cols);
const align = try alloc.alloc(Align, cols);
for (0..cols) |c| {
widths[c] = 3; // GFM minimum
align[c] = if (c < aligns.len) aligns[c] else .none;
var idx = c; // the grid is flat: stride `cols` walks column `c`
while (idx < grid.items.len) : (idx += cols) widths[c] = @max(widths[c], grid.items[idx].len);
}
// Row layout: '| ' + padded cells joined ' | ' + ' |\n'. The header row
// is row 0, or a blank row when row 0 is data — still valid GFM.
var out = std.ArrayList(u8).init(alloc);
defer out.deinit();
const w = out.writer();
try w.writeAll("| ");
for (0..cols) |c| {
if (c > 0) try w.writeAll(" | ");
const cell = if (header) grid.items[c] else "";
try appendPadded(w, cell, widths[c], align[c]);
}
try w.writeAll(" |\n| ");
for (0..cols) |c| {
if (c > 0) try w.writeAll(" | ");
try appendSep(w, align[c], widths[c]);
}
try w.writeAll(" |\n");
const start = @intFromBool(header) * cols; // skip row 0 when it is the header
var i = start;
while (i < grid.items.len) : (i += cols) {
try w.writeAll("| ");
for (grid.items[i .. i + cols], 0..) |cell, c| {
if (c > 0) try w.writeAll(" | ");
try appendPadded(w, cell, widths[c], align[c]);
}
try w.writeAll(" |\n");
}
return out.toOwnedSlice();
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
const rows = try parseTable(alloc, "Name,Price,Qty\nWire | cable,2 m,1\nLED,3 mm,25", ',');
const aligns = [_]Align{ .none, .right, .center };
const out = try toMarkdown(alloc, rows, true, &aligns);
defer alloc.free(out);
try std.io.getStdOut().writeAll(out);
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →