Turn a CSV into a Markdown pipe table you can paste into a README, a pull request or a wiki page. Commas inside quotes stay in their cell, stray pipes get escaped, and Pad columns lines up the columns so the raw file is readable too. The CSV is parsed in this tab and never uploaded.
package,version,license,notes
astro,5.2.0,MIT,"Static sites, islands"
tailwindcss,4.0.1,MIT,Utility-first CSS
left-pad,1.3.0,WTFPL,"Unpublished in 2016, broke half of npm"
zod,3.24.1,MIT,Schema | validation | package | version | license | notes |
| ----------- | ------- | ------- | -------------------------------------- |
| astro | 5.2.0 | MIT | Static sites, islands |
| tailwindcss | 4.0.1 | MIT | Utility-first CSS |
| left-pad | 1.3.0 | WTFPL | Unpublished in 2016, broke half of npm |
| zod | 3.24.1 | MIT | Schema \| validation |
The quoted commas in the notes column stayed inside their
cells, and the pipe in the last row became \|
so it does not start a fifth column.
Alignment lives in the separator row under the header. A colon on the left means left-aligned, a colon on the right means right-aligned, colons on both sides mean centered, and no colon leaves it to the renderer (GitHub uses left).
| left | center | right | none |
| :--- | :----: | ----: | ---- |
| a | b | c | d | Align: numbers right checks each column and
right-aligns it only when every non-empty value looks like a
number (42, -3.5,
1,200.50, 15%). Versions such as
5.2.0 are not numbers and stay as they are.
Untick Pad columns when the table lives in a file that changes often. Renderers ignore the padding, and without it, making one cell the longest in its column changes one line in the diff instead of every row in the table.
Every row has to fit on one line. The tool writes line breaks inside a quoted CSV field as <br>, which GitHub, GitLab and most renderers display as a break.
GFM tables need a header row. Untick First row is header and the tool writes Column 1, Column 2 and so on, then treats every CSV line as data.
There is no colspan or rowspan. Short rows are padded with empty cells so every row has the same count.
Pipe tables are a GitHub Flavored Markdown extension. Original Markdown and plain CommonMark render them as text with pipes in it.
const csv = `name,notes
Ada,"Wrote the first program, 1843"
Zod,Schema | validation`;
// Minimal RFC 4180 parser: quotes, "" escapes, separators and newlines inside quotes
function parseCsv(text, sep = ",") {
const rows = [];
let row = [], field = "", q = false;
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (q) {
if (c === '"' && text[i + 1] === '"') { field += '"'; i++; }
else if (c === '"') q = false;
else field += c;
} else if (c === '"') q = true;
else if (c === sep) { row.push(field); field = ""; }
else if (c === "\n") { row.push(field); rows.push(row); row = []; field = ""; }
else if (c !== "\r") field += c;
}
row.push(field); rows.push(row);
return rows;
}
// Escape backslashes and pipes, and keep each row on one line
const cell = (s) => s.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
const line = (r) => `| ${r.map(cell).join(" | ")} |`;
const [head, ...body] = parseCsv(csv);
const md = [line(head), `| ${head.map(() => "---").join(" | ")} |`, ...body.map(line)].join("\n");
console.log(md); The cell() escape order matters: backslashes first, then pipes, or the backslash added in front of a pipe gets doubled.
import csv
import io
data = """name,notes
Ada,"Wrote the first program, 1843"
Zod,Schema | validation
"""
def cell(s):
return s.replace("\\", "\\\\").replace("|", "\\|").replace("\r\n", "<br>").replace("\n", "<br>")
rows = list(csv.reader(io.StringIO(data)))
head, body = rows[0], rows[1:]
lines = ["| " + " | ".join(cell(c) for c in head) + " |",
"| " + " | ".join("---" for _ in head) + " |"]
lines += ["| " + " | ".join(cell(c) for c in r) + " |" for r in body]
print("\n".join(lines)) csv.reader already handles quoted fields, doubled quotes and line breaks inside quotes.
<?php
$csv = "name,notes\nAda,\"Wrote the first program, 1843\"\nZod,Schema | validation\n";
// fgetcsv on a memory stream handles quoted fields that span lines, str_getcsv does not
$fh = fopen('php://memory', 'r+');
fwrite($fh, $csv);
rewind($fh);
$rows = [];
while (($r = fgetcsv($fh, null, ',', '"', '')) !== false) {
if ($r !== [null]) $rows[] = $r;
}
$cell = fn($s) => str_replace(["\\", '|', "\r\n", "\n"], ["\\\\", '\|', '<br>', '<br>'], $s);
$line = fn($r) => '| ' . implode(' | ', array_map($cell, $r)) . ' |';
$head = array_shift($rows);
$out = [$line($head), '| ' . implode(' | ', array_fill(0, count($head), '---')) . ' |'];
foreach ($rows as $r) $out[] = $line($r);
echo implode("\n", $out), "\n"; The empty fifth argument to fgetcsv turns off PHP's non-standard backslash escape, which otherwise misreads a field ending in a backslash.
Pipe tables are defined by the GitHub Flavored Markdown spec, not by CommonMark itself. GitLab, Gitea, Obsidian, most static site generators and pandoc (as pipe_tables) accept the same syntax.
This tool follows the specifications below. Read them when you need the exact normative wording, for example to settle an edge case between two implementations.
Paste the CSV or click Upload CSV. The Markdown pane fills in as you type; click Copy Markdown and paste it into a README, issue or wiki page.
A field wrapped in double quotes keeps its commas, and a doubled quote ("") becomes one quote. A literal | in a cell is written as \| so it does not start a new column.
They become <br> tags, because a Markdown table row has to stay on one line. GitHub, GitLab and most other renderers display <br> as a line break inside the cell.
Pick an option from the Align dropdown. The alignment goes in the separator row as colons (:---, :---:, ---:). Align: numbers right only right-aligns columns where every value is a number.
Only to people reading the raw Markdown. Renderers ignore the extra spaces, so untick Pad columns for the shortest output. Without padding, making one cell the longest in its column changes one line in a diff instead of every row.
For email or a web page use CSV to Table, which copies rich text or HTML. Starting from JSON, there is JSON to Markdown Table, and cells copied out of a spreadsheet go into Excel to HTML Table. To write the rest of the README, open the Markdown editor.