Overview
What Table to Spreadsheet 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.
| Category | Text Editing & Formatting |
|---|---|
| Action type | JavaScript |
| Default result | Copy the result |
| Internet | Not required |
Example
Action and outcome
| Action | Outcome |
|---|---|
| Table to SpreadsheetSelected text: | **Name** | Website | | --- | --- | | **Example** | [Visit](https://example.com) | | Name Website Example https://example.com |
Workflow
Use Table to Spreadsheet
Follow these steps to convert rendered or Markdown tables into cells for Google Sheets or Excel with ActionClip.
- Select the text you want to use.
- Choose Table to Spreadsheet from ActionClip.
- Review the extracted result, then copy it where you need it.
Before you add it
Requirements and privacy
- Before you start: No setup required.
- Your selected text: Runs locally in ActionClip’s JavaScript sandbox. Selected text does not leave your Mac.
Inspect before installing
Action script
The script below is included so you can inspect how the action works before downloading it.
JavaScript definition
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");
}