Quick start (Tampermonkey + ChatGPT copy)
- Install Tampermonkey in your browser.
- Create a new userscript and paste the script below.
- Open https://chatgpt.com, click "Copy clean" on a response, and paste into PromptPress.
- Keep "ON" enabled for best results.
// ==UserScript==
// @name ChatGPT Copy Clean (tables + math)
// @match https://chatgpt.com/*
// @grant GM_setClipboard
// ==/UserScript==
(function () {
const stripZeroWidth = (text) =>
text.replace(/[\u200B\u200C\u200D\u2060\uFEFF\u00AD\u202A-\u202E\u2066-\u2069]/g, "");
const escapePipes = (text) =>
text.replace(/\|/g, (m, offset, str) =>
offset > 0 && str[offset - 1] === "\\" ? "|" : "\\|",
);
const normalizeMatrixRows = (tex) => {
if (!/\\begin\{(bmatrix|pmatrix|matrix|aligned|align\*?|cases|array)\}/.test(tex)) {
return tex;
}
return tex.replace(/(?<!\\)\\\\s*(?=[0-9+\-\s\]])/g, "\\\\");
};
const flattenMath = (tex) =>
tex
.replace(/\r\n?/g, "\n")
.split("\n")
.map((l) => l.trim())
.filter(Boolean)
.join(" ");
const wrapMath = (tex, displayHint) => {
let body = normalizeMatrixRows(tex);
body = flattenMath(body);
const wantsDisplay =
displayHint ||
/\\begin\{[^}]+\}/.test(body) ||
/\\end\{[^}]+\}/.test(body);
if (wantsDisplay) return `\n\n$$\n${body}\n$$\n\n`;
return `$${body}$`;
};
const isNoise = (el) => {
const tag = el.tagName.toLowerCase();
if (tag === "button") return true;
const aria = (el.getAttribute("aria-label") || "").toLowerCase();
if (aria.includes("copy code")) return true;
const testId = (el.getAttribute("data-testid") || "").toLowerCase();
if (testId.includes("copy") && testId.includes("code")) return true;
return false;
};
const renderTable = (tableEl) => {
const rows = Array.from(tableEl.querySelectorAll("tr"));
if (!rows.length) return "";
const headerRow = tableEl.querySelector("thead tr") || rows[0];
const headerCells = Array.from(headerRow.querySelectorAll("th, td"));
if (!headerCells.length) return "";
const colCount = headerCells.length;
const bodyRows =
headerRow === rows[0] ? rows.slice(1) : rows.filter((r) => r !== headerRow);
const renderCell = (cell) => {
const walkCell = (node) => {
if (node.nodeType === Node.TEXT_NODE) {
return stripZeroWidth(node.textContent || "");
}
if (node.nodeType !== Node.ELEMENT_NODE) return "";
const el = node;
if (el.classList.contains("katex") || el.classList.contains("katex-display")) {
const ann = el.querySelector('annotation[encoding="application/x-tex"]');
const tex = ann ? ann.textContent.trim() : (el.textContent || "").trim();
const display = el.classList.contains("katex-display");
return wrapMath(tex, display);
}
const tag = el.tagName.toLowerCase();
if (tag === "br") return "\n";
return Array.from(el.childNodes).map(walkCell).join("");
};
const raw = Array.from(cell.childNodes).map(walkCell).join("");
let text = raw
.replace(/\n\s*\$\$\s*\n([\s\S]*?)\n\s*\$\$\s*\n/g, (_, math) => `$${String(math).trim()}$`)
.replace(/\n+/g, " ")
.trim();
text = escapePipes(text);
return text.length ? text : " ";
};
const buildRow = (cells) => {
const values = [];
for (let i = 0; i < colCount; i += 1) {
values.push(cells[i] ? renderCell(cells[i]) : " ");
}
return `| ${values.join(" | ")} |`;
};
const headerLine = buildRow(headerCells);
const separator = `| ${Array(colCount).fill("---").join(" | ")} |`;
const bodyLines = bodyRows
.map((row) => buildRow(Array.from(row.querySelectorAll("th, td"))))
.join("\n");
return `\n\n${headerLine}\n${separator}${bodyLines ? `\n${bodyLines}` : ""}\n\n`;
};
const htmlToMarkdown = (root) => {
const out = [];
const walk = (node) => {
if (node.nodeType === Node.TEXT_NODE) {
out.push(stripZeroWidth(node.textContent || ""));
return;
}
if (node.nodeType !== Node.ELEMENT_NODE) return;
const el = node;
if (isNoise(el)) return;
if (el.classList.contains("katex") || el.classList.contains("katex-display")) {
const ann = el.querySelector('annotation[encoding="application/x-tex"]');
const tex = ann ? ann.textContent.trim() : (el.textContent || "").trim();
const display = el.classList.contains("katex-display");
out.push(wrapMath(tex, display));
return;
}
const tag = el.tagName.toLowerCase();
if (tag === "table") {
out.push(renderTable(el));
return;
}
if (tag === "pre") {
const code = el.textContent || "";
out.push(`\n\n\`\`\`\n${code.replace(/\n$/, "")}\n\`\`\`\n\n`);
return;
}
if (tag === "br") {
out.push("\n");
return;
}
const children = Array.from(el.childNodes);
children.forEach(walk);
if (tag === "p" || tag === "div") out.push("\n\n");
if (/^h[1-6]$/.test(tag)) out.push("\n\n");
};
walk(root);
return out.join("").replace(/\n{3,}/g, "\n\n").trim();
};
const addButtons = () => {
document.querySelectorAll('[data-message-id]').forEach((msg) => {
if (msg.querySelector('.copy-clean-btn')) return;
const btn = document.createElement('button');
btn.textContent = 'Copy clean';
btn.className = 'copy-clean-btn';
btn.style.marginLeft = '8px';
btn.onclick = () => copyClean(msg);
const toolbar = msg.querySelector('[data-testid="toolbox"]') || msg;
toolbar.appendChild(btn);
});
};
const copyClean = (msg) => {
const html = msg.innerHTML;
const div = document.createElement('div');
div.innerHTML = html;
const md = htmlToMarkdown(div);
GM_setClipboard(md, 'text');
alert('Copied clean');
};
setInterval(addButtons, 1000);
})();