Markdown Table Generator — JavaScript 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 JavaScript implementation — the same logic the interactive tool runs, in a shareable, citable form.
/**
* Markdown Table Generator - pure logic, JavaScript polyglot showcase port.
*
* Language: JavaScript (ESM)
* Origin: CosmoDev polyglot showcase port of the `markdown-table` tool.
* Ported from: src/lib/markdown-table.ts (the canonical, live TypeScript lib).
*
* Parsing and rendering here are deterministic and depend only on their inputs -
* no React, no DOM. This file is display source - part of CosmoDev's polyglot
* tool pages, where the same pure logic is shown side-by-side across languages.
*/
/**
* The accepted input delimiters.
* @typedef {'|' | ',' | '\t' | ';' | ' '} Delimiter
*/
/**
* Per-column alignment, carried by the GFM separator row.
* @typedef {'left' | 'center' | 'right' | 'none'} Align
*/
// Delimiter candidates considered during auto-detection, in priority order.
// Structural delimiters (pipe, tab) outrank punctuation (`,` / `;`) outrank space.
const CANDIDATES = ['|', '\t', ';', ',', ' '];
const WEIGHT = { '|': 3, '\t': 3, ';': 2, ',': 2, ' ': 1 };
/**
* Split a single line by `delimiter`, trimming each resulting cell.
* - `|` strips one leading/trailing pipe (so `| a | b |` works) then splits.
* - ` ` splits on runs of whitespace.
* - `,` `\t` `;` split on the literal character.
*
* @param {string} line
* @param {Delimiter} delimiter
* @returns {string[]}
*/
function splitLine(line, delimiter) {
if (delimiter === '|') {
let l = line.trim();
if (l.startsWith('|')) l = l.slice(1);
if (l.endsWith('|')) l = l.slice(0, -1);
// A fully-empty line collapses to a single empty cell, not zero cells.
return l.length === 0 ? [''] : l.split('|').map((c) => c.trim());
}
if (delimiter === ' ') {
// parseTable only ever passes non-empty (post-trim) lines here, so this
// always yields ≥ 1 token - no empty-line guard needed.
return line.trim().split(/\s+/);
}
return line.split(delimiter).map((c) => c.trim());
}
/**
* Parse `input` into a 2-D grid of trimmed cells. Blank lines are skipped;
* each remaining line is split by `delimiter`.
*
* @param {string} input
* @param {Delimiter} delimiter
* @returns {string[][]}
*/
export function parseTable(input, delimiter) {
const lines = input
.split(/\r?\n/)
.map((l) => l.trim())
.filter((l) => l.length > 0);
return lines.map((line) => splitLine(line, delimiter));
}
/**
* Count the delimiter occurrences in a line. Whitespace counts *runs* of
* whitespace, not individual space characters.
*
* @param {string} line
* @param {Delimiter} delimiter
* @returns {number}
*/
function countOccurrences(line, delimiter) {
if (delimiter === ' ') {
return line.trim().split(/\s+/).length - 1;
}
let n = 0;
for (let i = 0; i < line.length; i++) if (line[i] === delimiter) n++;
return n;
}
/**
* Heuristic delimiter detection. Each candidate is scored by
* frequency × cross-line consistency × structural weight,
* and the best wins. Falls back to `,` when nothing scores (single column or
* empty input).
*
* @param {string} sample
* @returns {Delimiter}
*/
export function detectDelimiter(sample) {
const lines = sample
.split(/\r?\n/)
.map((l) => l.trim())
.filter((l) => l.length > 0);
if (lines.length === 0) return ',';
let best = ',';
let bestScore = 0;
for (const d of CANDIDATES) {
const counts = lines.map((l) => countOccurrences(l, d));
const avg = counts.reduce((a, b) => a + b, 0) / counts.length;
if (avg === 0) continue;
// Population variance across lines → lower means more consistent.
const variance =
counts.reduce((a, c) => a + (c - avg) ** 2, 0) / counts.length;
const consistency = 1 / (1 + variance);
const score = avg * consistency * WEIGHT[d];
if (score > bestScore) {
bestScore = score;
best = d;
}
}
return best;
}
/**
* Escape a cell for GFM: collapse newlines (CRLF or LF) to a single space,
* then escape literal `|` so it does not terminate the cell.
*
* @param {string} cell
* @returns {string}
*/
function escapeCell(cell) {
return cell.replace(/\r?\n/g, ' ').replace(/\|/g, '\\|');
}
/**
* Pad a cell to `width` honoring alignment. Left/none aligns left, right aligns
* right, center splits the slack (floor on the left side).
*
* @param {string} cell
* @param {number} width
* @param {Align} align
* @returns {string}
*/
function pad(cell, width, align) {
const diff = width - cell.length;
if (diff <= 0) return cell;
if (align === 'right') return ' '.repeat(diff) + cell;
if (align === 'center') {
const left = Math.floor(diff / 2);
return ' '.repeat(left) + cell + ' '.repeat(diff - left);
}
return cell + ' '.repeat(diff); // 'left' | 'none'
}
/**
* Render a separator cell (`---`, `:--`, `--:`, `:-:`) of at least 3 dashes.
*
* @param {Align} align
* @param {number} width
* @returns {string}
*/
function sepCell(align, width) {
const w = Math.max(3, width);
switch (align) {
case 'center':
return ':' + '-'.repeat(w - 2) + ':';
case 'right':
return '-'.repeat(w - 1) + ':';
case 'left':
return ':' + '-'.repeat(w - 1);
default:
return '-'.repeat(w);
}
}
/**
* Render a 2-D grid as a GitHub-Flavored Markdown table. Cells are padded to
* equal column widths (computed from the escaped text), literal `|` is
* escaped, and the separator row carries the per-column alignment.
*
* Returns '' for an empty grid.
*
* @param {string[][]} rows
* @param {{ header: boolean, align?: Align[] }} opts
* @returns {string}
*/
export function toMarkdown(rows, opts) {
if (rows.length === 0) return '';
const cols = rows.reduce((m, r) => Math.max(m, r.length), 0);
// Escape every cell and normalize each row to the column count.
const grid = rows.map((r) => {
const out = r.map(escapeCell);
while (out.length < cols) out.push('');
return out;
});
// Per-column alignment: missing entries default to 'none'; surplus entries ignored.
const aligns = Array.from({ length: cols }, (_, i) => opts.align?.[i] ?? 'none');
// Per-column width: at least 3 (GFM separator minimum), grown to fit the widest escaped cell.
const widths = Array.from({ length: cols }, (_, c) =>
Math.max(3, ...grid.map((r) => r[c].length)),
);
// Frame one row of cells with the GFM pipe scaffolding.
const line = (cells) =>
'| ' + cells.map((c, i) => pad(c, widths[i], aligns[i])).join(' | ') + ' |';
const separator =
'| ' + aligns.map((a, i) => sepCell(a, widths[i])).join(' | ') + ' |';
// When there is no header, synthesize a blank header row so the table is still valid GFM.
const header = opts.header ? line(grid[0]) : line(Array(cols).fill(''));
const dataStart = opts.header ? 1 : 0;
const dataRows = grid.slice(dataStart).map(line);
return [header, separator, ...dataRows].join('\n');
}
/**
* Transpose a grid (rows ↔ columns). Jagged grids are filled with ''.
*
* @param {string[][]} rows
* @returns {string[][]}
*/
export function transpose(rows) {
if (rows.length === 0) return [];
const cols = rows.reduce((m, r) => Math.max(m, r.length), 0);
return Array.from({ length: cols }, (_, c) => rows.map((r) => r[c] ?? ''));
}
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 →