// ==UserScript==
// @name ChatGPT对话Markdown导出
// @name:en ChatGPT to Markdown Exporter
// @version 1.2
// @description 导出ChatGPT和Grok网站上的对话为Markdown格式.
// @description:en Export chat history from ChatGPT and Grok websites to Markdown format.
// @author ChingyuanCheng //origin from: Marverlises
// @license MIT
// @match https://chatgpt.com/*
// @match https://*.openai.com/*
// @match https://grok.com/*
// @grant none
// @namespace https://greasyfork.org/users/1435416
// ==/UserScript==
(function() {
// Select chat elements based on the website
function getConversationElements() {
const currentUrl = window.location.href;
if (currentUrl.includes("openai.com") || currentUrl.includes("chatgpt.com")) {
return document.querySelectorAll('div.flex.flex-grow.flex-col.max-w-full');
} else if (currentUrl.includes("grok.com")) {
return document.querySelectorAll('div.message-bubble');
}
return [];
}
// Convert HTML to Markdown
function htmlToMarkdown(html) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Handle formulas
doc.querySelectorAll('span.katex-html').forEach(element => element.remove());
doc.querySelectorAll('mrow').forEach(mrow => mrow.remove());
doc.querySelectorAll('annotation[encoding="application/x-tex"]').forEach(element => {
if (element.closest('.katex-display')) {
const latex = element.textContent;
element.replaceWith(`\n$$\n${latex}\n$$\n`);
} else {
const latex = element.textContent;
element.replaceWith(`$${latex}$`);
}
});
// Bold text
doc.querySelectorAll('strong, b').forEach(bold => {
bold.parentNode.replaceChild(document.createTextNode(`**${bold.textContent}**`), bold);
});
// Italic text
doc.querySelectorAll('em, i').forEach(italic => {
italic.parentNode.replaceChild(document.createTextNode(`*${italic.textContent}*`), italic);
});
// Inline code
doc.querySelectorAll('p code').forEach(code => {
code.parentNode.replaceChild(document.createTextNode(`\`${code.textContent}\``), code);
});
// Links
doc.querySelectorAll('a').forEach(link => {
link.parentNode.replaceChild(document.createTextNode(`[${link.textContent}](${link.href})`), link);
});
// Images
doc.querySelectorAll('img').forEach(img => {
img.parentNode.replaceChild(document.createTextNode(``), img);
});
// Code blocks
doc.querySelectorAll('pre').forEach(pre => {
const codeType = pre.querySelector('div > div:first-child')?.textContent || '';
const markdownCode = pre.querySelector('div > div:nth-child(3) > code')?.textContent || pre.textContent;
pre.innerHTML = `\n\`\`\`${codeType}\n${markdownCode}\`\`\`\n`;
});
// Unordered lists
doc.querySelectorAll('ul').forEach(ul => {
let markdown = '';
ul.querySelectorAll(':scope > li').forEach(li => {
markdown += `- ${li.textContent.trim()}\n`;
});
ul.parentNode.replaceChild(document.createTextNode('\n' + markdown.trim()), ul);
});
// Ordered lists
doc.querySelectorAll('ol').forEach(ol => {
let markdown = '';
ol.querySelectorAll(':scope > li').forEach((li, index) => {
markdown += `${index + 1}. ${li.textContent.trim()}\n`;
});
ol.parentNode.replaceChild(document.createTextNode('\n' + markdown.trim()), ol);
});
// Headers
for (let i = 1; i <= 6; i++) {
doc.querySelectorAll(`h${i}`).forEach(header => {
header.parentNode.replaceChild(document.createTextNode('\n' + `${'#'.repeat(i)} ${header.textContent}\n`), header);
});
}
// Paragraphs
doc.querySelectorAll('p').forEach(p => {
p.parentNode.replaceChild(document.createTextNode('\n' + p.textContent + '\n'), p);
});
// Tables
doc.querySelectorAll('table').forEach(table => {
let markdown = '';
table.querySelectorAll('thead tr').forEach(tr => {
tr.querySelectorAll('th').forEach(th => {
markdown += `| ${th.textContent} `;
});
markdown += '|\n';
tr.querySelectorAll('th').forEach(() => {
markdown += '| ---- ';
});
markdown += '|\n';
});
table.querySelectorAll('tbody tr').forEach(tr => {
tr.querySelectorAll('td').forEach(td => {
markdown += `| ${td.textContent} `;
});
markdown += '|\n';
});
table.parentNode.replaceChild(document.createTextNode('\n' + markdown.trim() + '\n'), table);
});
let markdown = doc.body.innerHTML.replace(/<[^>]*>/g, '');
markdown = markdown.replaceAll(/- >/g, '- $\\gt$')
.replaceAll(/>/g, '>')
.replaceAll(/</g, '<')
.replaceAll(/≥/g, '>=')
.replaceAll(/≤/g, '<=')
.replaceAll(/≠/g, '\\neq');
return markdown.trim();
}
// Download content as a file
function download(data, filename, type) {
const file = new Blob([data], { type: type });
const a = document.createElement('a');
const url = URL.createObjectURL(file);
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 0);
}
// Show export modal with Markdown content
function showExportModal() {
let markdownContent = "";
const allElements = getConversationElements();
for (let i = 0; i < allElements.length; i += 2) {
if (!allElements[i + 1]) break;
let userText = allElements[i].textContent.trim();
let answerHtml = allElements[i + 1].innerHTML.trim();
userText = htmlToMarkdown(userText);
answerHtml = htmlToMarkdown(answerHtml);
const isGrok = window.location.href.includes("grok.com");
markdownContent += `\n# User Question\n${userText}\n# ${isGrok ? 'Grok' : 'ChatGPT'}\n${answerHtml}`;
}
markdownContent = markdownContent.replace(/&/g, '&');
if (!markdownContent) {
alert("No conversation content found.");
return;
}
// Create modal
const modal = document.createElement('div');
modal.id = 'markdown-modal';
Object.assign(modal.style, {
position: 'fixed',
top: '0',
left: '0',
width: '100%',
height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: '1000'
});
const modalContent = document.createElement('div');
Object.assign(modalContent.style, {
backgroundColor: '#fff',
color: '#000',
padding: '20px',
borderRadius: '8px',
width: '50%',
height: '80%',
display: 'flex',
flexDirection: 'column',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
overflow: 'hidden'
});
const textarea = document.createElement('textarea');
textarea.value = markdownContent;
Object.assign(textarea.style, {
flex: '1',
resize: 'none',
width: '100%',
padding: '10px',
fontSize: '14px',
fontFamily: 'monospace',
marginBottom: '10px',
boxSizing: 'border-box',
color: '#000',
backgroundColor: '#f9f9f9',
border: '1px solid #ccc',
borderRadius: '4px'
});
textarea.setAttribute('readonly', true);
const buttonContainer = document.createElement('div');
Object.assign(buttonContainer.style, {
display: 'flex',
justifyContent: 'flex-end'
});
const copyButton = document.createElement('button');
copyButton.textContent = 'Copy';
Object.assign(copyButton.style, {
padding: '8px 16px',
fontSize: '14px',
cursor: 'pointer',
backgroundColor: '#28A745',
color: '#fff',
border: 'none',
borderRadius: '4px',
marginRight: '10px'
});
const downloadButton = document.createElement('button');
downloadButton.textContent = 'Download';
Object.assign(downloadButton.style, {
padding: '8px 16px',
fontSize: '14px',
cursor: 'pointer',
backgroundColor: '#007BFF',
color: '#fff',
border: 'none',
borderRadius: '4px',
marginRight: '10px'
});
const closeButton = document.createElement('button');
closeButton.textContent = 'Close';
Object.assign(closeButton.style, {
padding: '8px 16px',
fontSize: '14px',
cursor: 'pointer',
backgroundColor: '#DC3545',
color: '#fff',
border: 'none',
borderRadius: '4px'
});
buttonContainer.appendChild(copyButton);
buttonContainer.appendChild(downloadButton);
buttonContainer.appendChild(closeButton);
modalContent.appendChild(textarea);
modalContent.appendChild(buttonContainer);
modal.appendChild(modalContent);
document.body.appendChild(modal);
// Event listeners for buttons
copyButton.addEventListener('click', () => {
textarea.select();
navigator.clipboard.writeText(textarea.value)
.then(() => {
copyButton.textContent = 'Copied';
setTimeout(() => copyButton.textContent = 'Copy', 2000);
})
.catch(err => console.error('Copy failed', err));
});
downloadButton.addEventListener('click', () => {
download(markdownContent, 'chat-export.md', 'text/markdown');
});
closeButton.addEventListener('click', () => {
document.body.removeChild(modal);
});
// Close modal with Escape key or click outside
const escListener = (e) => {
if (e.key === 'Escape' && document.getElementById('markdown-modal')) {
document.body.removeChild(modal);
document.removeEventListener('keydown', escListener);
}
};
document.addEventListener('keydown', escListener);
modal.addEventListener('click', (e) => {
if (e.target === modal) {
document.body.removeChild(modal);
document.removeEventListener('keydown', escListener);
}
});
}
// Create the export button on the page
function createExportButton() {
const exportButton = document.createElement('button');
exportButton.textContent = 'Export Chat';
exportButton.id = 'export-chat';
const styles = {
position: 'fixed',
height: '36px',
top: '10px',
right: '35%',
zIndex: '10000',
padding: '10px',
backgroundColor: '#000000',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
textAlign: 'center',
lineHeight: '16px'
};
Object.assign(exportButton.style, styles);
document.body.appendChild(exportButton);
exportButton.addEventListener('click', showExportModal);
}
// Initialize button and periodically check its presence
createExportButton();
setInterval(() => {
if (!document.getElementById('export-chat')) {
createExportButton();
}
}, 1000);
})();