Last active
April 7, 2025 00:25
-
-
Save richdrummer33/6a22cc4d73b3ce0a2a48ca8e6a741d92 to your computer and use it in GitHub Desktop.
Downloads Claude.ai conversations as markdown
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
javascript:(function() { | |
console.log("Claude to Markdown bookmarklet started"); | |
function waitForClaudeMessages(retries = 10) { | |
const messages = document.querySelectorAll(".p-4.text-base"); | |
if (messages.length === 0 && retries > 0) { | |
console.log("Waiting for messages to load..."); | |
setTimeout(() => waitForClaudeMessages(retries - 1), 500); | |
return; | |
} | |
console.log("Messages found:", messages.length); | |
extractClaudeMarkdown(messages); | |
} | |
// Basic HTML to Markdown conversion functions | |
function htmlToMarkdown(html) { | |
// Create a temporary div to parse HTML | |
const tempDiv = document.createElement('div'); | |
tempDiv.innerHTML = html; | |
// Process code blocks | |
tempDiv.querySelectorAll('pre').forEach(pre => { | |
const codeElement = pre.querySelector('code'); | |
const language = codeElement && codeElement.className.match(/language-(\w+)/) ? | |
codeElement.className.match(/language-(\w+)/)[1] : ''; | |
const code = codeElement ? codeElement.innerText : pre.innerText; | |
pre.outerHTML = `\n\`\`\`${language}\n${code}\n\`\`\`\n`; | |
}); | |
// Process inline code | |
tempDiv.querySelectorAll('code:not(pre code)').forEach(code => { | |
code.outerHTML = '`' + code.innerText + '`'; | |
}); | |
// Process strong/bold | |
tempDiv.querySelectorAll('strong, b').forEach(strong => { | |
strong.outerHTML = '**' + strong.innerText + '**'; | |
}); | |
// Process emphasis/italic | |
tempDiv.querySelectorAll('em, i').forEach(em => { | |
em.outerHTML = '*' + em.innerText + '*'; | |
}); | |
// Process links | |
tempDiv.querySelectorAll('a').forEach(link => { | |
link.outerHTML = '[' + link.innerText + '](' + link.href + ')'; | |
}); | |
// Process lists | |
tempDiv.querySelectorAll('ul, ol').forEach(list => { | |
const isOrdered = list.tagName === 'OL'; | |
const items = Array.from(list.querySelectorAll('li')); | |
items.forEach((item, index) => { | |
const prefix = isOrdered ? `${index + 1}. ` : '- '; | |
item.innerHTML = prefix + item.innerHTML; | |
}); | |
list.outerHTML = '\n' + Array.from(items).map(i => i.innerHTML).join('\n') + '\n'; | |
}); | |
// Process headings | |
for (let i = 1; i <= 6; i++) { | |
tempDiv.querySelectorAll(`h${i}`).forEach(heading => { | |
const hashes = '#'.repeat(i); | |
heading.outerHTML = `\n${hashes} ${heading.innerText}\n`; | |
}); | |
} | |
// Process paragraphs | |
tempDiv.querySelectorAll('p').forEach(p => { | |
p.outerHTML = '\n' + p.innerHTML + '\n'; | |
}); | |
// Process line breaks | |
tempDiv.querySelectorAll('br').forEach(br => { | |
br.outerHTML = '\n'; | |
}); | |
// Clean up the text | |
let markdown = tempDiv.innerText || tempDiv.textContent; | |
markdown = markdown.replace(/\n{3,}/g, '\n\n'); // Replace multiple newlines with just two | |
return markdown; | |
} | |
// Wait for Claude messages to load | |
waitForClaudeMessages(); | |
// Clone the body to work with | |
const bodyClone = document.body.cloneNode(true); | |
console.log("Body cloned for processing"); | |
// Get the title of the conversation | |
const title = document.title || "Claude Conversation"; | |
let markdownContent = `# ${title}\n\n`; | |
console.log("Processing title:", title); | |
// Find user messages | |
const userMessages = bodyClone.querySelectorAll('[data-testid="user-message"]'); | |
console.log(`Found ${userMessages.length} user messages`); | |
// Find Claude's messages | |
const claudeMessages = bodyClone.querySelectorAll('.font-claude-message'); | |
console.log(`Found ${claudeMessages.length} Claude messages`); | |
// Determine the total number of message exchanges | |
const messageCount = Math.max(userMessages.length, claudeMessages.length); | |
console.log(`Processing ${messageCount} message exchanges`); | |
// Process each message pair | |
for (let i = 0; i < messageCount; i++) { | |
const userMessage = userMessages[i]; | |
if (userMessage) { | |
markdownContent += `## Human: ${i+1}\n\n`; | |
markdownContent += htmlToMarkdown(userMessage.innerHTML); | |
markdownContent += '\n\n'; | |
} | |
const claudeMessage = claudeMessages[i]; | |
if (claudeMessage) { | |
markdownContent += `## Claude: ${i+1}\n\n`; | |
// Get the main content from Claude's message | |
const claudeContent = claudeMessage.querySelector('[data-testid="message-content"]') || | |
claudeMessage.querySelector('.grid-cols-1') || | |
claudeMessage; | |
markdownContent += htmlToMarkdown(claudeContent.innerHTML); | |
markdownContent += '\n\n'; | |
} | |
} | |
// Clean up the markdown content | |
markdownContent = markdownContent | |
.replace(/\n{3,}/g, '\n\n') | |
.replace(/>/g, '>') | |
.replace(/</g, '<') | |
.replace(/&/g, '&'); | |
console.log("Markdown content generated"); | |
// Create a download link for the markdown file | |
const downloadLink = document.createElement('a'); | |
downloadLink.download = `${title.replace(/[^\w\s]/gi, '')}.md`; | |
downloadLink.href = URL.createObjectURL(new Blob([markdownContent], {type: 'text/markdown'})); | |
downloadLink.style.display = "none"; | |
// Add the link to the document and click it to start the download | |
document.body.appendChild(downloadLink); | |
console.log("Triggering download"); | |
downloadLink.click(); | |
// Clean up | |
document.body.removeChild(downloadLink); | |
console.log("Bookmarklet execution completed"); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment