I Built a Markdown Table Generator in Pure Vanilla JS — Spreadsheet UI, CSV Import, HTML Output, 129 Tests
Markdown tables are a pain to write by hand. Getting the spacing right, escaping pipe characters, lining up the separator row — it's tedious and error-prone. So I built a browser-only Markdown Table Generator with a spreadsheet-style grid editor.
👉 Try it live → markdown-table-generator-50m.pages.dev
Source is a single 600-line HTML file. Zero dependencies. Works offline.
What it does
Grid editing
- Click any cell and type — Markdown and HTML outputs update instantly
- Tab / Shift+Tab to move between cells; Enter to move down; arrow keys for navigation
- Add rows with the
+ Rowbutton or by pressing Enter on the last row - Add columns with
+ Column
Row & column management
- ▲ / ▼ buttons to move rows up and down
- ◀ / ▶ buttons to move columns left and right
- ✕ button on any row or column to delete it
Column alignment
Each column has its own L / C / R alignment toggle. The alignment is reflected in both the Markdown separator (:-—, :—:, —:) and the HTML style="text-align: ..." attribute.
CSV import
Paste any CSV (including RFC 4180 quoted fields with embedded commas, newlines, and escaped double-quotes) and click Import CSV — the grid reloads with your data.
Trim empty
One click removes all all-blank rows and all-blank columns. Useful after deleting rows/columns or importing sparse CSVs.
Two outputs
-
Markdown table — pipe-delimited, padded to align columns, special characters escaped (
|→\|,\→\\) -
HTML table — with
<thead>/<tbody>,<th>for headers, and XSS-safe HTML escaping
Both have a Copy button that writes to the clipboard.
Example output
Given this grid:
| Name | Score | City |
|---|---|---|
| Alice | 95 | London |
| Bob | 87 | New York |
The Markdown output:
| Name | Score | City |
|:------|------:|:--------:|
| Alice | 95 | London |
| Bob | 87 | New York |
The HTML output:
<table border="1" cellspacing="0" cellpadding="6">
<thead>
<tr>
<th style="text-align: left;">Name</th>
<th style="text-align: right;">Score</th>
<th style="text-align: center;">City</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">Alice</td>
<td style="text-align: right;">95</td>
<td style="text-align: center;">London</td>
</tr>
<tr>
<td style="text-align: left;">Bob</td>
<td style="text-align: right;">87</td>
<td style="text-align: center;">New York</td>
</tr>
</tbody>
</table>
Technical details
CSV parser (RFC 4180 subset)
The tricky part of CSV parsing is quoted fields. The implementation handles:
function parseCSV(text) {
const src = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
let i = 0; const n = src.length;
const rows = [];
while (i < n) {
if (src[i] === '\n') { i++; continue; }
const row = [];
let expectMore = true;
while (expectMore) {
if (i < n && src[i] === '"') {
i++;
let val = '';
while (i < n) {
if (src[i] === '"') {
if (src[i+1] === '"') { val += '"'; i += 2; } // escaped quote
else { i++; break; } // closing quote
} else { val += src[i++]; }
}
row.push(val);
} else {
let val = '';
while (i < n && src[i] !== ',' && src[i] !== '\n') val += src[i++];
row.push(val);
}
if (i < n && src[i] === ',') i++;
else expectMore = false;
}
if (i < n && src[i] === '\n') i++;
if (row.length > 0) rows.push(row);
}
return rows;
}
The key insight: use a expectMore flag driven by whether we consumed a comma, rather than a character-lookahead while condition. This correctly handles trailing commas (e.g., a,b, → ['a','b','']) that naive parsers miss.
Markdown cell escaping
Two characters need escaping inside Markdown table cells:
function escapeMdCell(val) {
return val.replace(/\\/g, '\\\\').replace(/\|/g, '\\|');
}
Note: backslash must be escaped first, or the replacement itself would be double-escaped.
Column width padding
Columns are padded so everything lines up:
const widths = Array(cols).fill(0);
data.forEach(row => row.forEach((cell, ci) => {
widths[ci] = Math.max(widths[ci], escapeMdCell(cell).length,
mdAlignSep(effectiveAligns[ci]).length);
}));
The separator itself has a minimum length (:-— = 4, :—: = 5, —: = 4) that feeds into the width calculation.
XSS-safe HTML output
All cell content is escaped before insertion into HTML:
function escapeHtml(val) {
return val
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
Testing (129 tests, Node.js assert only)
No test framework — just const assert = require('assert') and a small runner:
let passed = 0, failed = 0;
function test(name, fn) {
try { fn(); console.log(` ✓ ${name}`); passed++; }
catch(e) { console.error(` ✗ ${name}\n ${e.message}`); failed++; }
}
Tests cover:
| Category | Tests |
|---|---|
| CSV parser | 18 |
| normalizeCells | 5 |
| isEmptyRow / isEmptyCol | 9 |
| trimEmpty | 9 |
| escapeMdCell | 8 |
| escapeHtml | 8 |
| mdAlignSep | 4 |
| generateMarkdown | 18 |
| generateHTML | 21 |
| Integration pipelines | 29 |
| Total | 129 |
Run with:
node test/test.js
Output:
Total: 129 ✓ 129 ✗ 0
All 129 tests passed!
Why no framework?
Every tool in the devnestio collection is a single HTML file with zero runtime dependencies. No build step, no bundler, no npm install in production. The file opens directly in a browser, works offline, and loads instantly.
For a tool this focused, vanilla JS is the right call. The total file size is under 25 KB.
Try it
👉 markdown-table-generator-50m.pages.dev
It's also available through the devnestio hub alongside other browser-only developer tools: devnestio.pages.dev/markdown-table-generator/
All processing is local. No data leaves your browser.
Part of the devnestio collection — free, browser-only developer tools.
Top comments (0)