Skip to content

Instantly share code, notes, and snippets.

@spion
Last active April 16, 2023 21:22
Show Gist options
  • Save spion/667e720554b80e70e5fa008234e34cf0 to your computer and use it in GitHub Desktop.
Save spion/667e720554b80e70e5fa008234e34cf0 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name ChatGPT save to Markdown button
// @author spion, avosirenfal
// @description Adds an export button for exporting the doc to markdown
// @namespace chatgpt
// @version 1.0.0
// @match https://chat.openai.com/*
// @grant none
// @run-at document-start
// ==/UserScript==
// Inspired by https://github.com/avosirenfal/chatgpt-export/blob/main/chatgpt-export.user.js
(function () {
'use strict';
const turndownModule = import('https://cdn.skypack.dev/turndown');
const Characters = {
Me: 'User',
Gpt: 'ChatGPT'
}
const callback = function (_mutationsList, _observer) {
const nav = document.querySelector("nav");
if (nav.querySelector("#custom_save_button")) {
return;
}
const el = nav.lastElementChild.cloneNode(false);
el.id = 'custom_save_button';
el.innerHTML = "Export Markdown";
el.onclick = async function () {
const turndown = (await turndownModule).default;
const td = new turndown();
td.addRule('fencedCodeBlock', {
filter: function (node, options) {
return (
// options.codeBlockStyle === 'fenced' &&
node.nodeName === 'PRE' &&
node.querySelector('code.hljs') != null
)
},
replacement: function (content, node, options) {
let codeBlock = node.querySelector('code.hljs');
var className = codeBlock.getAttribute('class') || ''
var language = (className.match(/language-(\S+)/) || [null, ''])[1]
return `\n\n${'```'}${language}\n${codeBlock.textContent}\n${'```'}\n\n`;
}
})
const html2markdown = html => td.turndown(html);
const markdownLink = document.createElement('a');
let firstConversationItem = null;
let conversation = `# ${document.title}\n\n`
let currentCharacter = Characters.Me;
for (let item of Array.from(document.querySelectorAll('div:has(img) + div .items-start'))) {
firstConversationItem = item;
conversation += `### ${currentCharacter}\n\n`
if (item.querySelector('.markdown')) {
let gptHtml = item.querySelector('.markdown').innerHTML;
let gptMarkdown = html2markdown(gptHtml);
// Remove "Copy code" lines.
// Todo: convert small code blocks to larger code blocks with language
gptMarkdown = gptMarkdown.split("\n").filter(l => !/^[a-z]*Copy code$/.test(l)).join("\n");
conversation += gptMarkdown;
}
else {
conversation += item.innerText;
}
conversation += "\n\n";
if (currentCharacter == Characters.Me) {
currentCharacter = Characters.Gpt
}
else {
currentCharacter = Characters.Me
}
}
if (firstConversationItem === null) {
alert("Failed to get text for the items. The conversation is empty, or the CSS selector is no longer correct");
return;
}
markdownLink.href = URL.createObjectURL(new Blob([conversation], {
type: 'text/markdown'
}));
markdownLink.download = `GPT Conversation - ${document.title}.md`;
document.body.appendChild(markdownLink);
markdownLink.click();
document.body.removeChild(markdownLink);
URL.revokeObjectURL(markdownLink.href);
}
nav.appendChild(el);
};
window.addEventListener('load', (event) => {
const observer = new MutationObserver(callback);
observer.observe(document.querySelector('body'), { attributes: true, childList: true, subtree: true });
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment