---
title: "Table to Spreadsheet action for ActionClip"
canonical_url: "https://actionclip.app/actions/table-to-spreadsheet"
document_type: "standalone_action"
official: true
action_count: 1
catalog_schema_version: 1
catalog_revision: "2026-10-15.5"
catalog_published_at: "2026-10-14T19:51:38Z"
---

# Table to Spreadsheet action for ActionClip

Convert rendered or Markdown tables into cells for Google Sheets or Excel.

## What it does

Captures selected rendered tables locally from their copied HTML, or converts Markdown and tab-separated tables to spreadsheet rows. Removes the header divider, bold, italic, and code formatting while preserving the first row and empty cells. Markdown links become their full URLs. Supports escaped pipes and quoted cells containing tabs, quotes, or line breaks. Select one complete table, run the action, copy the result, then paste into a spreadsheet cell. Rendered-table capture briefly copies the source selection and restores the previous clipboard before producing the result.

## Action details

| Field | Value |
| --- | --- |
| Category | Text Editing & Formatting |
| Action type | JavaScript |
| Default result | Copy the result |
| Internet | Not required |

## Action and outcome

| Action | Outcome |
| --- | --- |
| **Table to Spreadsheet** — Selected text: \| **Name** \| Website \| \| --- \| --- \| \| **Example** \| [Visit](https://example.com) \| | Name Website Example https://example.com |

## How to convert rendered or Markdown tables into cells for Google Sheets or Excel with ActionClip

1. Select the text you want to use.
2. Choose **Table to Spreadsheet** from ActionClip.
3. Review the extracted result, then copy it where you need it.

## Requirements

No setup required.

## Privacy

Runs locally in ActionClip’s JavaScript sandbox. Selected text does not leave your Mac.

## Action script

````javascript
function run(selected_text) {
  // Rendered tables may already arrive as spreadsheet clipboard text.
  if (selected_text.includes("\t") && !selected_text.trimStart().startsWith("|")) {
    const rows = [], row = [];
    let cell = "", quoted = false, closed = false, start = true;
    const finish = () => { row.push(cell); cell = ""; closed = false; start = true; };
    for (let index = 0; index < selected_text.length; index += 1) {
      const char = selected_text[index];
      if (quoted) {
        if (char === '"') {
          if (selected_text[index + 1] === '"') { cell += '"'; index += 1; }
          else { quoted = false; closed = true; }
        } else cell += char;
      } else if (char === "\t" || char === "\r" || char === "\n") {
        finish();
        if (char !== "\t") { rows.push(row.splice(0)); }
        if (char === "\r" && selected_text[index + 1] === "\n") index += 1;
      } else if (closed) {
        throw new Error("Invalid quoted spreadsheet cell. Select the table again and retry.");
      } else if (start && char === '"') { quoted = true; start = false; }
      else { cell += char; start = false; }
    }
    if (quoted) throw new Error("Incomplete quoted spreadsheet cell. Select the table again and retry.");
    if (row.length || cell.length || !/[\r\n]$/.test(selected_text)) { finish(); rows.push(row); }
    const width = rows[0].length;
    if (rows.some((values) => values.length !== width)) throw new Error("The selected rows have different numbers of columns.");
    const quote = (value) => /[\t\r\n"]/.test(value) ? '"' + value.replace(/"/g, '""') + '"' : value;
    return rows.map((values) => values.map(quote).join("\t")).join("\n");
  }
  const unescape = (text) => text.replace(/\\([^\w\s])/g, "$1");
  const entities = (text) => text.replace(/&(#x[\da-f]+|#\d+|amp|lt|gt|quot|apos|nbsp);/gi, (match, code) => {
    const names = {amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " "};
    if (code[0] !== "#") return names[code.toLowerCase()];
    const value = code[1].toLowerCase() === "x" ? parseInt(code.slice(2), 16) : parseInt(code.slice(1), 10);
    return value > 0 && value <= 0x10ffff && !(value >= 0xd800 && value <= 0xdfff)
      ? String.fromCodePoint(value) : match;
  });

  // Read inline Markdown links without truncating URLs containing parentheses.
  function linkAt(text, start) {
    if (text[start] !== "[") return null;
    let depth = 1, index = start + 1;
    for (; index < text.length && depth; index += 1) {
      if (text[index] === "\\") { index += 1; continue; }
      if (text[index] === "[") depth += 1;
      if (text[index] === "]") depth -= 1;
    }
    if (depth || text[index] !== "(") return null;
    index += 1;
    while (/\s/.test(text[index] || "") && index < text.length) index += 1;
    let url = "";
    if (text[index] === "<") {
      const end = text.indexOf(">", index + 1);
      if (end < 0) return null;
      url = text.slice(index + 1, end);
      index = end + 1;
    } else {
      depth = 0;
      for (; index < text.length; index += 1) {
        const char = text[index];
        if (char === "\\" && index + 1 < text.length) { url += char + text[++index]; continue; }
        if ((char === ")" || /\s/.test(char)) && depth === 0) break;
        if (char === "(") depth += 1;
        if (char === ")") depth -= 1;
        url += char;
      }
      if (depth) return null;
    }
    while (/\s/.test(text[index] || "") && index < text.length) index += 1;
    if (text[index] === '"' || text[index] === "'") {
      const quote = text[index++];
      while (index < text.length && text[index] !== quote) {
        if (text[index] === "\\") index += 1;
        index += 1;
      }
      if (text[index++] !== quote) return null;
      while (/\s/.test(text[index] || "") && index < text.length) index += 1;
    }
    return text[index] === ")" ? {end: index + 1, url: entities(unescape(url))} : null;
  }

  function codeAt(text, start) {
    if (text[start] !== "`") return null;
    const fence = text.slice(start).match(/^`+/)[0];
    let end = text.indexOf(fence, start + fence.length);
    while (end >= 0 && (text[end - 1] === "`" || text[end + fence.length] === "`")) {
      end = text.indexOf(fence, end + fence.length);
    }
    return end < 0 ? null : {end: end + fence.length, value: text.slice(start + fence.length, end)};
  }

  function splitRow(line) {
    const cells = [];
    let cell = "", separators = 0;
    for (let index = 0; index < line.length;) {
      const char = line[index];
      if (char === "\\" && index + 1 < line.length) { cell += line.slice(index, index + 2); index += 2; continue; }
      const token = codeAt(line, index) || linkAt(line, index);
      if (token) { cell += line.slice(index, token.end); index = token.end; continue; }
      if (char === "|") { cells.push(cell.trim()); cell = ""; separators += 1; }
      else cell += char;
      index += 1;
    }
    cells.push(cell.trim());
    if (!separators) throw new Error("Select a Markdown table with columns separated by | characters.");
    if (line.startsWith("|")) cells.shift();
    if (cells[cells.length - 1] === "" && line.endsWith("|") && cells.length > 1) cells.pop();
    return cells;
  }

  function plainCell(text) {
    let output = "";
    for (let index = 0; index < text.length;) {
      if (text[index] === "\\" && index + 1 < text.length) {
        output += unescape(text.slice(index, index + 2)); index += 2; continue;
      }
      const code = codeAt(text, index);
      if (code) { output += code.value; index = code.end; continue; }
      const isImage = text[index] === "!" && text[index + 1] === "[";
      const link = linkAt(text, index + (isImage ? 1 : 0));
      if (link) { output += link.url; index = link.end; continue; }
      const autolink = text.slice(index).match(/^<(https?:\/\/[^>]+|mailto:[^>]+)>/i);
      if (autolink) { output += entities(autolink[1]); index += autolink[0].length; continue; }
      const html = text.slice(index).match(/^<\/?(?:br|strong|b|em|i|s|del|code)(?:\s*\/?)>/i);
      if (html) { if (/^<br/i.test(html[0])) output += "\n"; index += html[0].length; continue; }
      const marker = ["**", "__", "~~", "*", "_"].find((value) => text.startsWith(value, index));
      if (marker && (marker !== "_" || index === 0 || !/\w/.test(text[index - 1]))) {
        const end = text.indexOf(marker, index + marker.length);
        const inner = end < 0 ? "" : text.slice(index + marker.length, end);
        if (inner && inner === inner.trim()) { output += plainCell(inner); index = end + marker.length; continue; }
      }
      output += text[index++];
    }
    return entities(output);
  }

  let lines = selected_text.replace(/\r\n|[\r\u2028\u2029]/g, "\n").trim().split("\n");
  // Accept a table copied with its enclosing Markdown code fence.
  const fence = (lines[0] || "").trim().match(/^(`{3,}|~{3,})(?:markdown|md|text)?\s*$/i);
  if (fence && lines.length > 1 && lines[lines.length - 1].trim() === fence[1]) lines = lines.slice(1, -1);
  const rows = lines.filter((line) => line.trim()).map((line) => splitRow(line.trim()));
  if (!rows.length) throw new Error("Select a Markdown table first.");
  if (rows.length > 1 && rows[1].every((value) => /^:?-{3,}:?$/.test(value))) rows.splice(1, 1);
  const width = rows[0].length;
  if (!width || rows.some((row) => row.length !== width)) {
    throw new Error("The table rows have different numbers of columns. Escape literal pipes as \\| and select one complete table.");
  }
  const quote = (text) => /[\t\r\n"]/.test(text) ? '"' + text.replace(/"/g, '""') + '"' : text;
  return rows.map((row) => row.map((cell) => quote(plainCell(cell))).join("\t")).join("\n");
}
````

## Links

- [HTML listing](https://actionclip.app/actions/table-to-spreadsheet)
- [Download action definition](https://actionclip.app/actions/table-to-spreadsheet.actionclip)
- [Browse Text Editing & Formatting actions](https://actionclip.app/marketplace#marketplace-textTools)
- [All Marketplace listings](https://actionclip.app/marketplace)
