Skip to content

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 →