DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

A Markdown table generator with no library — three pure functions: escape a cell, build the separator, pad to width

I built a Markdown table generator this week and my first instinct was to reach for a Markdown library. Then I actually looked at what a GitHub-Flavored table is — three kinds of line, cells fenced by pipes — and realized the whole engine is a plain data model plus a handful of pure functions. Maybe 70 lines, no dependencies. You fill an editable grid, set per-column alignment, paste CSV/TSV to auto-fill, and it emits properly padded Markdown with a live rendered preview. Here's how it fits together.

The data model is three flat arrays

Everything is driven by state you already understand — headers, rows, and one alignment per column:

const STATE = {
  headers: ["Feature", "Free", "Pro"],
  aligns:  ["left", "center", "right"],   // one per column
  rows: [["Projects","3","Unlimited"], ["Storage","1 GB","100 GB"]],
};
Enter fullscreen mode Exit fullscreen mode

Keeping the model this flat is what makes add-row, add-column, alignment and CSV-load each a one-line mutation. The grid only ever reads and writes this object; the emitter turns it into text.

Escape first, or user data breaks the row

Two characters structurally matter in a table: a pipe and a newline. A | inside cell text would start a new column and shift everything after it; a raw line-break would end the row early. So every value gets escaped before it's placed:

function escapeCell(v){
  return String(v ?? "")
    .replace(/\|/g, "\\|")        // pipe → \|  (would start a new column)
    .replace(/\r?\n|\r/g, " ");   // newline → space (a raw break ends the row)
}
Enter fullscreen mode Exit fullscreen mode

This one function is the difference between a tool that works on real data and one that only survives tidy demos.

Colons carry the alignment — nothing else does

The separator row is the line that actually makes it a table (a header alone is just a paragraph), and its colons are the only place alignment lives:

function sep(w, align){
  if (align === "left")   return ":" + "-".repeat(w - 1);   // :----
  if (align === "right")  return "-".repeat(w - 1) + ":";   // ----:
  if (align === "center") return ":" + "-".repeat(w - 2) + ":"; // :--:
  return "-".repeat(w);                                     // ----  (default)
}
Enter fullscreen mode Exit fullscreen mode

Aligning your source spaces does nothing to the rendered output — only these colons move the content. GitHub even reads the column count off this row, not the header.

Pad for humans; the renderer ignores it

To measure widths I walk each column and take the longest escaped string (header included), with a floor of 3 so --- always fits. Then I pad each cell on the side that matches its alignment. The thing to internalize: padding is purely cosmetic — every renderer trims leading/trailing whitespace, so a padded table and a crammed one render identically. It's there so the raw source lines up in a diff, which is why the tool offers it as a toggle. Assembling is then just glue: escape everything, measure, and emit the header row, the separator, and one line per data row joined with " | " and fenced by outer pipes.

The bonus: CSV paste is a real parser, and the preview is a self-test

You can't split(",") a CSV — a field can be quoted and legally contain commas, newlines and doubled "" quotes. So the paste handler is a tiny character-by-character state machine with one boolean, "are we inside quotes?", exactly how real CSV libraries work under the hood. Detect the delimiter first (a tab beats a comma → it's TSV), square up ragged rows, and the first row becomes the header.

The touch I'm most pleased with is the preview: instead of rendering the grid a second time, I build it by parsing the emitted Markdown back into a real <table>. If that round-trips cleanly, the emitter is provably correct — the preview is a live unit test I can see. The lesson underneath the whole thing: reach for a data model plus small pure functions before you reach for a dependency.

Fill the grid, set alignment, paste some CSV and watch the Markdown emit:
https://dev48v.infy.uk/solve/day46-markdown-table-generator.html

Top comments (0)