Table Generator Implementation: From Data Editing to Multi-Format Output#

When writing technical documentation, I often need to insert tables. Hand-writing Markdown tables is manageable, but converting to HTML or CSV is tedious. Let’s build a tool that edits once and outputs in multiple formats.

The Essence of Three Table Formats#

Markdown Tables: Pipe-Separated#

| Name | Age | City |
| --- | --- | --- |
| John | 30 | NY |
| Jane | 25 | LA |

The core is using | to separate cells, with the second row as the separator line (alignment).

HTML Tables: Semantic Tags#

<table>
  <thead>
    <tr><th>Name</th><th>Age</th><th>City</th></tr>
  </thead>
  <tbody>
    <tr><td>John</td><td>30</td><td>NY</td></tr>
    <tr><td>Jane</td><td>25</td><td>LA</td></tr>
  </tbody>
</table>

Clear structure, but verbose to write by hand.

CSV: Comma-Separated Values#

Name,Age,City
John,30,NY
Jane,25,LA

Simplest format, Excel opens it directly.

Core Data Structure Design#

Two arrays represent the table data:

const [headers, setHeaders] = useState<string[]>(["Name", "Age", "City"])
const [data, setData] = useState<string[][]>([
  ["John", "30", "NY"],
  ["Jane", "25", "LA"],
])

Why separate storage? Because headers and data have different rendering logic:

  • Headers are editable with delete column buttons
  • Data rows have delete row buttons at the end
  • CSV first line is headers, followed by data

Format Conversion Algorithm Implementation#

Markdown Generator#

function generateMarkdown(headers: string[], data: string[][]): string {
  // Header row
  let md = "| " + headers.join(" | ") + " |\n"
  
  // Separator line (default left-aligned)
  md += "| " + headers.map(() => "---").join(" | ") + " |\n"
  
  // Data rows
  data.forEach(row => {
    md += "| " + row.join(" | ") + " |\n"
  })
  
  return md
}

Note that join(" | ") automatically inserts separators between elements, avoiding manual concatenation.

HTML Generator#

function generateHTML(headers: string[], data: string[][]): string {
  let html = "<table>\n  <thead>\n    <tr>\n"
  
  // Headers
  headers.forEach(h => {
    html += `      <th>${h}</th>\n`
  })
  html += "    </tr>\n  </thead>\n  <tbody>\n"
  
  // Data rows
  data.forEach(row => {
    html += "    <tr>\n"
    row.forEach(cell => {
      html += `      <td>${cell}</td>\n`
    })
    html += "    </tr>\n"
  })
  html += "  </tbody>\n</table>"
  
  return html
}

Spacing controls indentation for formatted output.

CSV Generator#

function generateCSV(headers: string[], data: string[][]): string {
  let csv = headers.join(",") + "\n"
  data.forEach(row => {
    csv += row.join(",") + "\n"
  })
  return csv
}

Simplest, but there’s a pitfall: comma and quote handling.

Edge Cases: Special Character Escaping#

Commas in CSV#

If a cell contains a comma, direct join(",") breaks the structure:

const row = ["Apple, Inc", "100"]
row.join(",")  // "Apple, Inc,100"  ❌ becomes 3 columns

The correct approach is to wrap with quotes:

function escapeCSV(cell: string): string {
  if (cell.includes(",") || cell.includes('"') || cell.includes("\n")) {
    return `"${cell.replace(/"/g, '""')}"`  // Escape quotes as double quotes
  }
  return cell
}

function generateCSV(headers: string[], data: string[][]): string {
  const escapedHeaders = headers.map(escapeCSV)
  let csv = escapedHeaders.join(",") + "\n"
  data.forEach(row => {
    csv += row.map(escapeCSV).join(",") + "\n"
  })
  return csv
}

Special Characters in HTML#

<, >, & need escaping:

function escapeHTML(str: string): string {
  return str
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
}

// When generating HTML
html += `      <td>${escapeHTML(cell)}</td>\n`

Newlines in Markdown Tables#

Markdown tables don’t support multiline content. Newlines break the structure:

function escapeMarkdown(cell: string): string {
  return cell.replace(/\n/g, " ")  // Replace newlines with spaces
}

Dynamic Table Editing Implementation#

Add Row#

const addRow = () => {
  setData([...data, new Array(cols).fill("")])
  setRows(rows + 1)
}

Adds an empty row with column count determined by current cols.

Delete Row#

const removeRow = (index: number) => {
  setData(data.filter((_, i) => i !== index))
  setRows(rows - 1)
}

filter removes the row at the specified index.

Add Column#

const addCol = () => {
  setHeaders([...headers, `Col ${cols + 1}`])
  setData(data.map(row => [...row, ""]))  // Append empty cell to each row
  setCols(cols + 1)
}

Note: both headers and data must be updated.

Delete Column#

const removeCol = (index: number) => {
  setHeaders(headers.filter((_, i) => i !== index))
  setData(data.map(row => row.filter((_, i) => i !== index)))
  setCols(cols - 1)
}

Iterate each row and filter out the specified column.

Real-Time Preview and Performance Optimization#

Use useMemo to cache output during editing:

const output = useMemo(() => {
  switch (format) {
    case "markdown":
      return generateMarkdown(headers, data)
    case "html":
      return generateHTML(headers, data)
    case "csv":
      return generateCSV(headers, data)
    default:
      return ""
  }
}, [format, headers, data])

Dependencies are format, headers, data - recalculates only when these change.

One-Click Copy Functionality#

const copyTable = async () => {
  try {
    await navigator.clipboard.writeText(output)
    // Show copy success feedback
  } catch (err) {
    // Fallback: use textarea
    const textarea = document.createElement("textarea")
    textarea.value = output
    document.body.appendChild(textarea)
    textarea.select()
    document.execCommand("copy")
    document.body.removeChild(textarea)
  }
}

Clipboard API works in HTTPS environments. HTTP requires a fallback solution.

Real-World Use Cases#

1. Technical Documentation Tables#

API docs commonly need parameter tables:

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| id | string | Yes | User ID |
| name | string | No | Username |

Generate quickly with the table generator, copy to Markdown editor.

2. Data Analysis Reports#

Convert Excel data to Markdown tables for technical blogs.

3. Frontend Component Documentation#

Props tables, Events tables, Slots tables - unified format.

Based on These Principles#

I built an online tool: Table Generator

Key features:

  • Three output formats: Markdown / HTML / CSV
  • Dynamic add/remove rows and columns
  • Real-time preview
  • One-click copy

The core algorithms aren’t complex, but handling edge cases makes it practical. Hope this helps.


Related tools: Markdown Table Generator | CSV Viewer