Markdown Table Generator: From String Concatenation to Real-time Preview#

What’s the most annoying part of writing technical documentation? Tables. Markdown’s table syntax is simple but tedious. Manually typing | and --- gets error-prone quickly. Recently added a table generator to JsonKit, and here’s how it works.

Markdown Table Syntax Basics#

Let’s review the basic structure:

| Name  | Age | City    |
|-------|-----|---------|
| Alice | 28  | Beijing |
| Bob   | 35  | Shanghai|

Three parts:

  1. Header row: Column names separated by |
  2. Separator row: --- for each column (at least 3 dashes)
  3. Data rows: Values wrapped in |

Alignment is controlled by the separator row:

| Left | Center | Right |
|:-----|:------:|------:|
| Left | Center | Right |
  • :--- for left align
  • :---: for center
  • ---: for right align

Core Implementation: String Concatenation#

Generating Markdown tables is essentially string concatenation, but with some edge cases:

function generateMarkdownTable(
  headers: string[],
  rows: string[][],
  alignments: ('left' | 'center' | 'right')[]
): string {
  // Header row
  const headerLine = '| ' + headers.join(' | ') + ' |'
  
  // Separator row (with alignment)
  const separatorLine = '| ' + alignments.map(a => {
    if (a === 'left') return ':---'
    if (a === 'center') return ':---:'
    return '---:'
  }).join(' | ') + ' |'
  
  // Data rows
  const dataLines = rows.map(row => '| ' + row.join(' | ') + ' |')
  
  return [headerLine, separatorLine, ...dataLines].join('\n')
}

Looks simple, but real-world usage brings edge cases.

Edge Case 1: Empty Cells#

Users might delete cell content. Don’t omit it - keep the empty string:

// Wrong: omitting empty cells breaks column count
'| Alice | 28 | |'  // Correct
'| Alice | 28 |'    // Wrong, missing a column

Edge Case 2: Special Characters#

Cell content containing | needs escaping:

function escapeCell(text: string): string {
  return text.replace(/\|/g, '\\|')
}

Edge Case 3: Newlines#

Markdown table cells don’t support newlines. Replace with <br> or space:

function sanitizeCell(text: string): string {
  return text.replace(/\n/g, ' ').replace(/\|/g, '\\|')
}

Performance: useMemo Caching#

When table data changes, Markdown output needs recalculation. Use useMemo to avoid unnecessary recomputation:

const markdown = useMemo(() => {
  const headerLine = '| ' + headers.join(' | ') + ' |'
  const separatorLine = '| ' + alignments.map(a => {
    if (a === 'left') return ':---'
    if (a === 'center') return ':---:'
    return '---:'
  }).join(' | ') + ' |'
  const dataLines = rows.map(row => '| ' + row.join(' | ') + ' |')
  return [headerLine, separatorLine, ...dataLines].join('\n')
}, [headers, rows, alignments])

Dependencies are headers, rows, alignments - only regenerate when these change.

Dynamic Row/Column Operations#

The core feature is dynamically adding/removing rows and columns:

// Add row
const addRow = useCallback(() => {
  setRows(prev => [...prev, headers.map(() => '')])
}, [headers])

// Remove row
const removeRow = useCallback(() => {
  setRows(prev => prev.length > 1 ? prev.slice(0, -1) : prev)
}, [])

// Add column
const addCol = useCallback(() => {
  setHeaders(prev => [...prev, `Header ${prev.length + 1}`])
  setRows(prev => prev.map(row => [...row, '']))
  setAlignments(prev => [...prev, 'left'])
}, [])

// Remove column
const removeCol = useCallback(() => {
  if (headers.length <= 1) return  // Keep at least one column
  setHeaders(prev => prev.slice(0, -1))
  setRows(prev => prev.map(row => row.slice(0, -1)))
  setAlignments(prev => prev.slice(0, -1))
}, [headers.length])

Note that removing a column updates three states: headers, rows, alignments. Missing any causes data misalignment.

Alignment Toggle Cycle#

Clicking alignment button cycles through left, center, right:

const toggleAlignment = useCallback((index: number) => {
  setAlignments(prev => prev.map((a, i) => {
    if (i !== index) return a
    if (a === 'left') return 'center'
    if (a === 'center') return 'right'
    return 'left'
  }))
}, [])

Using map to return a new array maintains React state immutability.

Real-time Preview Implementation#

Preview renders HTML table directly, alignment controlled by className:

<table className="w-full border-collapse">
  <thead>
    <tr>
      {headers.map((h, i) => (
        <th
          key={i}
          className={`
            px-4 py-2 border border-border
            ${alignments[i] === 'center' ? 'text-center' : 
              alignments[i] === 'right' ? 'text-right' : 'text-left'}
          `}
        >
          {h}
        </th>
      ))}
    </tr>
  </thead>
  <tbody>
    {rows.map((row, ri) => (
      <tr key={ri}>
        {row.map((cell, ci) => (
          <td
            key={ci}
            className={`
              px-4 py-2 border border-border
              ${alignments[ci] === 'center' ? 'text-center' : 
                alignments[ci] === 'right' ? 'text-right' : 'text-left'}
            `}
          >
            {cell}
          </td>
        ))}
      </tr>
    ))}
  </tbody>
</table>

Preview and Markdown output share the same data source - true WYSIWYG.

Practical Details#

1. Copy to Clipboard#

const handleCopy = useCallback(() => {
  navigator.clipboard.writeText(markdown)
  setCopied(true)
  setTimeout(() => setCopied(false), 2000)
}, [markdown])

Reset copy state after 2 seconds for visual feedback.

2. Minimum Column Limit#

Check if only one column remains before deletion:

if (headers.length <= 1) return

Prevents empty table structure.

3. Default Data#

Initialize with sample data to reduce learning curve:

const [headers, setHeaders] = useState(['Header 1', 'Header 2', 'Header 3'])
const [rows, setRows] = useState([
  ['Cell 1', 'Cell 2', 'Cell 3'],
  ['Cell 4', 'Cell 5', 'Cell 6'],
])

The Result#

Based on this implementation: Markdown Table Generator

Features:

  • Visual table editing
  • One-click column alignment toggle
  • Dynamic row/column add/remove
  • Real-time preview and Markdown output
  • One-click copy to clipboard

Not much code, but getting the interaction details right takes effort. Hope this helps.


Related: Markdown to HTML | Code Formatter