DEV Community

Dev Nestio
Dev Nestio

Posted on

I Built a Markdown Table Generator in Pure Vanilla JS — Spreadsheet UI, CSV Import, HTML Output, 129 Tests

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 + Row button 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 |
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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, '\\|');
}
Enter fullscreen mode Exit fullscreen mode

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);
}));
Enter fullscreen mode Exit fullscreen mode

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, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;');
}
Enter fullscreen mode Exit fullscreen mode

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++; }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Output:

Total: 129  ✓ 129  ✗ 0
All 129 tests passed!
Enter fullscreen mode Exit fullscreen mode

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)