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); |
Thanks!