Copies ChatGPT responses as raw Markdown.
This script can be pased directly in your JavaScript console when on ChatGPT, or pasted to a script in tampermonkey.
It adds a little copy button next to the replies.
function elToText(el) { | |
return Array.from(el.childNodes) | |
.map((node) => { | |
switch (node.nodeType) { | |
case 1: { | |
// ELEMENT_NODE | |
const nextSibling = node.nextSibling?.nodeName; | |
const prevSibling = node.previousSibling?.nodeName; | |
switch (node.nodeName) { | |
case 'P': { | |
return `${elToText(node)}\n\n`; | |
} | |
case 'OL': | |
case 'UL': { | |
if (nextSibling === 'PRE') { | |
return `${elToText(node)}`; | |
} else { | |
return `${elToText(node)}\n\n`; | |
} | |
} | |
case 'LI': { | |
if (node.parentNode.nodeName === 'OL') { | |
return `- ${elToText(node)}`; | |
} else { | |
const index = nodeIndex(node); | |
return `${index + 1}. ${elToText(node)}`; | |
} | |
} | |
case 'CODE': | |
return '`' + elToText(node) + '`'; | |
case 'PRE': { | |
const code = node.querySelector( | |
'div > div.p-4.overflow-y-auto > code' | |
); | |
const language = Array.from(code.classList).find(c => c.startsWith('language-')).substring(9); | |
if (prevSibling === 'OL' || prevSibling === 'LI') { | |
return ' ```' + language + '\n ' + elToText(code) + ' ```\n\n'; | |
} else { | |
return '```' + language + '\n' + elToText(code) + '```\n\n'; | |
} | |
} | |
case 'STRONG': | |
case 'B': | |
return `**${elToText(node)}**`; | |
case 'EM': | |
case 'I': | |
return `*${elToText(node)}*`; | |
case 'DIV': | |
case 'SPAN': | |
return elToText(node); | |
case 'A': { | |
const link = node.getAttribute('href'); | |
const text = elToText(node); | |
return `[${text}](${link})`; | |
} | |
default: { | |
console.warn(`Unhandled node name: '${node.nodeName}'`); | |
return elToText(node); | |
} | |
} | |
} | |
case 3: // TEXT_NODE | |
return node.nodeValue; | |
default: | |
return ''; | |
} | |
}) | |
.join(''); | |
} | |
function nodeIndex(node) { | |
return Array.from(node.parentNode.children).indexOf(node); | |
} | |
function createElementFromHTML(htmlString) { | |
var div = document.createElement('div'); | |
div.innerHTML = htmlString.trim(); | |
// Change this to div.childNodes to support multiple top-level nodes. | |
return div.firstChild; | |
} | |
function fallbackCopyTextToClipboard(text) { | |
var textArea = document.createElement('textarea'); | |
textArea.value = text; | |
// Avoid scrolling to bottom | |
textArea.style.top = '0'; | |
textArea.style.left = '0'; | |
textArea.style.position = 'fixed'; | |
document.body.appendChild(textArea); | |
textArea.focus(); | |
textArea.select(); | |
try { | |
var successful = document.execCommand('copy'); | |
var msg = successful ? 'successful' : 'unsuccessful'; | |
console.log('Fallback: Copying text command was ' + msg); | |
} catch (err) { | |
console.error('Fallback: Oops, unable to copy', err); | |
} | |
document.body.removeChild(textArea); | |
} | |
function copyTextToClipboard(text) { | |
if (!navigator.clipboard) { | |
fallbackCopyTextToClipboard(text); | |
return; | |
} | |
navigator.clipboard.writeText(text).then( | |
function () { | |
console.log('Async: Copying to clipboard was successful!'); | |
}, | |
function (err) { | |
console.error('Async: Could not copy text: ', err); | |
} | |
); | |
} | |
function addCopyBtns() { | |
Array.from( | |
document.querySelectorAll( | |
'main > div.flex-1.overflow-hidden > div > div div.bg-gray-50 > div > div.relative.flex.w-\\[calc\\(100\\%-50px\\)\\].md\\:flex-col.lg\\:w-\\[calc\\(100\\%-115px\\)\\]' | |
) | |
).forEach((el) => { | |
if (el.dataset.copyAdded == 'true') { | |
return; | |
} | |
const content = el.querySelector(':scope > div:nth-child(1) .markdown'); | |
const btnsContainer = el.querySelector(':scope > div:nth-child(2)'); | |
if (btnsContainer != null) { | |
el.dataset.copyAdded = true; | |
} else { | |
const observer = new MutationObserver(() => { | |
addCopyBtns(); | |
if (el.dataset.copyAdded == 'true') { | |
observer.disconnect(); | |
} | |
}); | |
observer.observe(el, { childList: true }); | |
return; | |
} | |
const btn = createElementFromHTML(` | |
<button class="p-1 rounded-md hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400"> | |
<svg | |
version="1.1" | |
id="Layer_1" | |
xmlns="http://www.w3.org/2000/svg" | |
xmlns:xlink="http://www.w3.org/1999/xlink" | |
x="0px" | |
y="0px" | |
viewBox="0 0 460 460" | |
style="enable-background: new 0 0 460 460" | |
xml:space="preserve" | |
fill="currentColor" | |
class="h-4 w-4" | |
> | |
<g> | |
<g> | |
<g> | |
<path | |
d="M425.934,0H171.662c-18.122,0-32.864,14.743-32.864,32.864v77.134h30V32.864c0-1.579,1.285-2.864,2.864-2.864h254.272 | |
c1.579,0,2.864,1.285,2.864,2.864v254.272c0,1.58-1.285,2.865-2.864,2.865h-74.729v30h74.729 | |
c18.121,0,32.864-14.743,32.864-32.865V32.864C458.797,14.743,444.055,0,425.934,0z" | |
/> | |
<path | |
d="M288.339,139.998H34.068c-18.122,0-32.865,14.743-32.865,32.865v254.272C1.204,445.257,15.946,460,34.068,460h254.272 | |
c18.122,0,32.865-14.743,32.865-32.864V172.863C321.206,154.741,306.461,139.998,288.339,139.998z M288.341,430H34.068 | |
c-1.58,0-2.865-1.285-2.865-2.864V172.863c0-1.58,1.285-2.865,2.865-2.865h254.272c1.58,0,2.865,1.285,2.865,2.865v254.273h0.001 | |
C291.206,428.715,289.92,430,288.341,430z" | |
/> | |
</g> | |
</g> | |
</g> | |
</svg> | |
</button> | |
`); | |
btn.addEventListener('click', () => { | |
copyTextToClipboard(elToText(content).trim()); | |
}); | |
btnsContainer.appendChild(btn); | |
}); | |
} | |
const listContainer = document.querySelector( | |
'main > div.flex-1.overflow-hidden > div > div > div' | |
); | |
const observer = new MutationObserver(addCopyBtns); | |
observer.observe(listContainer, { childList: true }); | |
setTimeout(addCopyBtns, 500); |