Markdown Table Generator — Go 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 Go implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Package markdowntable is the Go twin of CosmoDev's src/lib/markdown-table.ts
// (dual source: the web lib is TypeScript, the CLI lib is Go — kept in lock-step).
// Pure + deterministic, never panics. The table-driven tests in
// markdown-table_test.go share vectors with src/lib/markdown-table.test.ts so the
// two implementations are held to the same contract.
//
// The package mirrors the TS lib exactly: parseTable splits a delimited blob into a
// 2-D grid of trimmed cells, detectDelimiter heuristically scores candidate
// separators, toMarkdown renders a grid as a padded GFM table, and transpose swaps
// rows with columns.
package markdowntable
import (
"regexp"
"strings"
"unicode/utf8"
)
// Delimiter is the field separator used to split a row into cells. It mirrors the
// TS Delimiter union ('|' | ',' | '\t' | ';' | ' '). Space is special-cased: it
// splits on runs of whitespace rather than a single literal character.
type Delimiter byte
const (
Pipe Delimiter = '|'
Comma Delimiter = ','
Tab Delimiter = '\t'
Semicolon Delimiter = ';'
Space Delimiter = ' '
)
// Align is the horizontal alignment of a column. It mirrors the TS Align union.
// The zero value AlignNone matches the TS default 'none' (left-rendered, no marker).
type Align int
const (
AlignNone Align = iota // left-rendered, plain separator (TS 'none')
AlignLeft // TS 'left' → ':---'
AlignCenter // TS 'center'→ ':-:'
AlignRight // TS 'right'→ '---:'
)
// ToMarkdownOptions configures ToMarkdown. It mirrors ToMarkdownOptions in the TS
// lib. Align is a slice (nil → every column defaults to AlignNone), and entries
// beyond the column count are ignored, exactly like the TS `align?` field.
type ToMarkdownOptions struct {
Header bool
Align []Align
}
// candidates is the ordered set of separators detectDelimiter scores. The order
// matters: when two candidates score equally the earlier one wins (strict >
// comparison), so ';' outranks ',' — see the "breaks ties in candidate order"
// vector shared with the TS suite.
var candidates = []Delimiter{Pipe, Tab, Semicolon, Comma, Space}
// weight ranks structural separators above punctuation above space. Mirrors WEIGHT
// in the TS lib: pipe/tab = 3, semicolon/comma = 2, space = 1.
var weight = map[Delimiter]float64{Pipe: 3, Tab: 3, Semicolon: 2, Comma: 2, Space: 1}
var newlineRe = regexp.MustCompile(`\r?\n`)
// nonEmptyLines splits input on \r?\n, trims each line, and drops blank lines. It is
// the shared front-end of parseTable and detectDelimiter (both use the identical
// `.split(/\r?\n/).map(trim).filter(non-empty)` pipeline in the TS lib).
func nonEmptyLines(input string) []string {
raw := newlineRe.Split(input, -1)
lines := make([]string, 0, len(raw))
for _, l := range raw {
t := strings.TrimSpace(l)
if len(t) > 0 {
lines = append(lines, t)
}
}
return lines
}
// splitLine splits a single (non-empty, trimmed) line by delimiter, trimming each
// resulting cell. It mirrors splitLine() in the TS lib:
// - Pipe strips one leading/trailing pipe (so `| a | b |` works) then splits.
// - Space splits on runs of whitespace.
// - Comma/Tab/Semicolon split on the literal character.
func splitLine(line string, d Delimiter) []string {
switch d {
case Pipe:
l := strings.TrimSpace(line)
l = strings.TrimPrefix(l, "|")
l = strings.TrimSuffix(l, "|")
if l == "" {
return []string{""}
}
parts := strings.Split(l, "|")
for i, c := range parts {
parts[i] = strings.TrimSpace(c)
}
return parts
case Space:
// parseTable only passes non-empty (post-trim) lines, so Fields always
// yields ≥1 token — no empty-line guard needed here, matching the TS lib.
return strings.Fields(strings.TrimSpace(line))
default: // Comma, Tab, Semicolon
parts := strings.Split(line, string(d))
for i, c := range parts {
parts[i] = strings.TrimSpace(c)
}
return parts
}
}
// ParseTable parses input into a 2-D grid of trimmed cells. Blank lines are
// skipped and each non-empty line is split by delimiter. It is the Go twin of
// parseTable() in src/lib/markdown-table.ts.
func ParseTable(input string, d Delimiter) [][]string {
lines := nonEmptyLines(input)
grid := make([][]string, len(lines))
for i, line := range lines {
grid[i] = splitLine(line, d)
}
return grid
}
// countOccurrences counts delimiter occurrences in a line. For Space it counts
// whitespace runs (tokens − 1), not literal characters — mirroring countOccurrences
// in the TS lib. The line is assumed non-empty and post-trim (detectDelimiter only
// passes such lines).
func countOccurrences(line string, d Delimiter) int {
if d == Space {
return len(strings.Fields(strings.TrimSpace(line))) - 1
}
return strings.Count(line, string(d))
}
// DetectDelimiter heuristically detects the most likely delimiter in sample. It
// scores each candidate by frequency × cross-line consistency × weight and returns
// the best, falling back to Comma when nothing scores (single column / empty input).
// It is the Go twin of detectDelimiter() in src/lib/markdown-table.ts.
func DetectDelimiter(sample string) Delimiter {
lines := nonEmptyLines(sample)
if len(lines) == 0 {
return Comma
}
best := Comma
bestScore := 0.0
for _, d := range candidates {
counts := make([]float64, len(lines))
sum := 0.0
for i, l := range lines {
c := float64(countOccurrences(l, d))
counts[i] = c
sum += c
}
avg := sum / float64(len(lines))
if avg == 0 {
continue
}
varSq := 0.0
for _, c := range counts {
varSq += (c - avg) * (c - avg)
}
variance := varSq / float64(len(lines))
consistency := 1 / (1 + variance)
score := avg * consistency * weight[d]
// Strict >: on a tie the earlier candidate (already held) wins, so ';'
// outranks ',' — matching the TS tie-break contract.
if score > bestScore {
bestScore = score
best = d
}
}
return best
}
// escapeCell escapes a cell for GFM: collapse newlines to spaces, then escape a
// literal pipe as `\|`. Mirrors escapeCell() in the TS lib.
func escapeCell(cell string) string {
s := newlineRe.ReplaceAllString(cell, " ")
return strings.ReplaceAll(s, "|", "\\|")
}
// pad pads cell to width honoring alignment. Width is measured in runes (the TS
// uses .length, which for the ASCII/bmp cells this tool handles agrees with the
// rune count). Mirrors pad() in the TS lib.
func pad(cell string, width int, a Align) string {
diff := width - utf8.RuneCountInString(cell)
if diff <= 0 {
return cell
}
spaces := strings.Repeat(" ", diff)
switch a {
case AlignRight:
return spaces + cell
case AlignCenter:
left := diff / 2 // integer division floors, matching Math.floor
return strings.Repeat(" ", left) + cell + strings.Repeat(" ", diff-left)
default: // AlignLeft, AlignNone → left-aligned
return cell + spaces
}
}
// sepCell renders a separator cell ('---', ':--', '--:', ':-:') of the given width,
// with a minimum of three dashes. Mirrors sepCell() in the TS lib.
func sepCell(a Align, width int) string {
w := max(width, 3) // minimum three dashes, mirroring Math.max(3, width)
switch a {
case AlignCenter:
return ":" + strings.Repeat("-", w-2) + ":"
case AlignRight:
return strings.Repeat("-", w-1) + ":"
case AlignLeft:
return ":" + strings.Repeat("-", w-1)
default:
return strings.Repeat("-", w)
}
}
// ToMarkdown renders a 2-D grid as a GitHub-Flavored Markdown table. Cells are
// escaped, padded to equal column widths, and a separator row carries the
// per-column alignment. It returns "" for an empty grid. It is the Go twin of
// toMarkdown() in src/lib/markdown-table.ts.
func ToMarkdown(rows [][]string, opts ToMarkdownOptions) string {
if len(rows) == 0 {
return ""
}
cols := 0
for _, r := range rows {
cols = max(cols, len(r))
}
// Escape every cell and normalize each row to exactly `cols` cells.
grid := make([][]string, len(rows))
for i, r := range rows {
out := make([]string, cols)
for j := 0; j < cols; j++ {
if j < len(r) {
out[j] = escapeCell(r[j])
} // else: leave "" (jagged rows / short rows padded with "")
}
grid[i] = out
}
aligns := make([]Align, cols)
for i := range aligns {
aligns[i] = AlignNone
if opts.Align != nil && i < len(opts.Align) {
aligns[i] = opts.Align[i]
}
}
widths := make([]int, cols)
for c := 0; c < cols; c++ {
w := 3
for _, r := range grid {
w = max(w, utf8.RuneCountInString(r[c]))
}
widths[c] = w
}
line := func(cells []string) string {
padded := make([]string, len(cells))
for i, c := range cells {
padded[i] = pad(c, widths[i], aligns[i])
}
return "| " + strings.Join(padded, " | ") + " |"
}
sepCells := make([]string, cols)
for i, a := range aligns {
sepCells[i] = sepCell(a, widths[i])
}
separator := "| " + strings.Join(sepCells, " | ") + " |"
var header string
var dataRows [][]string
if opts.Header {
header = line(grid[0])
dataRows = grid[1:]
} else {
header = line(make([]string, cols)) // blank synthesized header row
dataRows = grid
}
parts := make([]string, 0, 2+len(dataRows))
parts = append(parts, header, separator)
for _, r := range dataRows {
parts = append(parts, line(r))
}
return strings.Join(parts, "\n")
}
// Transpose transposes a grid (rows ↔ columns). Jagged grids are filled with "".
// It is the Go twin of transpose() in src/lib/markdown-table.ts.
func Transpose(rows [][]string) [][]string {
if len(rows) == 0 {
return [][]string{}
}
cols := 0
for _, r := range rows {
cols = max(cols, len(r))
}
out := make([][]string, cols)
for c := 0; c < cols; c++ {
row := make([]string, len(rows))
for i, r := range rows {
if c < len(r) {
row[i] = r[c]
} // else: "" — jagged grids fill with empty strings
}
out[c] = row
}
return 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 →