Created
March 21, 2026 17:14
-
-
Save fxlrnrpt/14f569a330d9c42ec7b320f9f2a72bbb to your computer and use it in GitHub Desktop.
claude MD export
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
| // ==UserScript== | |
| // @name Claude MD Exporter (DOM) | |
| // @namespace http://tampermonkey.net/ | |
| // @version 2.2.0 | |
| // @description Minimal Markdown exporter for Claude conversations. DOM-based, no API interception. | |
| // @author Rewrite | |
| // @match https://claude.ai/chat/* | |
| // @grant none | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| const MAX_TOC_LEN = 80; | |
| function truncate(text, max) { | |
| const clean = text.replace(/\s+/g, ' ').trim(); | |
| if (clean.length <= max) return clean; | |
| return clean.slice(0, max).replace(/\s+\S*$/, '') + '…'; | |
| } | |
| // GitHub-style heading → anchor slug | |
| function slugify(text) { | |
| return text.toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-'); | |
| } | |
| function extractConversation() { | |
| const title = document.title.replace(/\s*[-–]\s*Claude\s*$/, '').trim() || 'Conversation'; | |
| const turns = document.querySelectorAll('div[data-test-render-count]'); | |
| if (!turns.length) return null; | |
| const toc = []; // {label, slug} | |
| const body = []; | |
| let userCount = 0; | |
| let assistantCount = 0; | |
| turns.forEach(turn => { | |
| const userMsg = turn.querySelector('[data-testid="user-message"]'); | |
| const assistantMsg = turn.querySelector('div[data-is-streaming]'); | |
| if (userMsg) { | |
| userCount++; | |
| const heading = `User ${userCount}`; | |
| const rawText = userMsg.textContent.trim(); | |
| const preview = truncate(rawText, MAX_TOC_LEN); | |
| toc.push({ label: `**${heading}:** ${preview}`, slug: slugify(heading) }); | |
| body.push(`## ${heading}\n`); | |
| body.push(nodeToMd(userMsg, 0)); | |
| body.push(''); | |
| } else if (assistantMsg) { | |
| assistantCount++; | |
| body.push(`## Assistant ${assistantCount}\n`); | |
| const md = assistantMsg.querySelector('.standard-markdown'); | |
| if (md) body.push(nodeToMd(md, 1)); | |
| body.push(''); | |
| } | |
| }); | |
| // Assemble | |
| const lines = [`# ${title}\n`]; | |
| if (toc.length) { | |
| lines.push(`## Table of Contents\n`); | |
| toc.forEach(t => lines.push(`- [${t.label}](#${t.slug})`)); | |
| lines.push(''); | |
| } | |
| lines.push(body.join('\n')); | |
| return lines.join('\n'); | |
| } | |
| // ── DOM node → Markdown ──────────────────────────────────────────── | |
| function nodeToMd(root, headingOffset) { | |
| if (!root) return ''; | |
| const off = headingOffset || 0; | |
| const out = []; | |
| function walk(node) { | |
| if (node.nodeType === 3) { out.push(node.textContent); return; } | |
| if (node.nodeType !== 1) return; | |
| const tag = node.tagName; | |
| if (tag === 'BUTTON' || tag === 'SVG' || tag === 'svg') return; | |
| if (node.getAttribute('aria-hidden') === 'true') return; | |
| if (node.classList?.contains('sr-only')) return; | |
| if (node.getAttribute('role') === 'group' && node.getAttribute('aria-label')?.includes('Message actions')) return; | |
| if (tag === 'PRE') { | |
| const code = node.querySelector('code'); | |
| const lang = [...(code?.classList || [])].find(c => c.startsWith('language-'))?.replace('language-', '') || ''; | |
| out.push(`\n\`\`\`${lang}\n${(code || node).textContent.trim()}\n\`\`\`\n`); | |
| return; | |
| } | |
| if (tag === 'CODE') { out.push(`\`${node.textContent}\``); return; } | |
| const hMatch = tag.match(/^H([1-6])$/); | |
| if (hMatch) { | |
| const level = Math.min(+hMatch[1] + off, 6); | |
| out.push(`\n${'#'.repeat(level)} ${node.textContent.trim()}\n`); | |
| return; | |
| } | |
| if (tag === 'HR') { out.push('\n---\n'); return; } | |
| if (tag === 'UL' || tag === 'OL') { | |
| node.querySelectorAll(':scope > li').forEach((li, i) => { | |
| out.push(`${tag === 'OL' ? `${i + 1}. ` : '- '}${li.textContent.trim()}`); | |
| }); | |
| out.push(''); | |
| return; | |
| } | |
| if (tag === 'STRONG' || tag === 'B') { out.push('**'); node.childNodes.forEach(walk); out.push('**'); return; } | |
| if (tag === 'EM' || tag === 'I') { out.push('*'); node.childNodes.forEach(walk); out.push('*'); return; } | |
| if (tag === 'A') { out.push('['); node.childNodes.forEach(walk); out.push(`](${node.getAttribute('href') || ''})`); return; } | |
| if (tag === 'P') { node.childNodes.forEach(walk); out.push('\n\n'); return; } | |
| if (tag === 'BLOCKQUOTE') { out.push('\n> '); node.childNodes.forEach(walk); out.push('\n'); return; } | |
| if (tag === 'BR') { out.push('\n'); return; } | |
| if (tag === 'TABLE') { out.push(tableToMd(node)); return; } | |
| node.childNodes.forEach(walk); | |
| } | |
| walk(root); | |
| return out.join('').replace(/\n{3,}/g, '\n\n').trim(); | |
| } | |
| function tableToMd(table) { | |
| const rows = [...table.querySelectorAll('tr')]; | |
| if (!rows.length) return ''; | |
| const matrix = rows.map(r => [...r.querySelectorAll('th, td')].map(c => c.textContent.trim())); | |
| const colW = matrix[0].map((_, i) => Math.max(...matrix.map(r => (r[i] || '').length), 3)); | |
| const pad = (s, w) => (s || '').padEnd(w); | |
| const lines = []; | |
| lines.push('| ' + matrix[0].map((c, i) => pad(c, colW[i])).join(' | ') + ' |'); | |
| lines.push('| ' + colW.map(w => '-'.repeat(w)).join(' | ') + ' |'); | |
| matrix.slice(1).forEach(r => lines.push('| ' + r.map((c, i) => pad(c, colW[i])).join(' | ') + ' |')); | |
| return '\n' + lines.join('\n') + '\n'; | |
| } | |
| function downloadMd() { | |
| const md = extractConversation(); | |
| if (!md) { alert('No conversation found.'); return; } | |
| const ts = new Date().toISOString().slice(0, 16).replace(/[:.]/g, '-'); | |
| const name = document.title.split(' - ')[0].replace(/[\\/:*?"<>|]/g, '_').slice(0, 40); | |
| const blob = new Blob([md], { type: 'text/markdown;charset=utf-8' }); | |
| const a = document.createElement('a'); | |
| a.href = URL.createObjectURL(blob); | |
| a.download = `Claude_${name}_${ts}.md`; | |
| a.click(); | |
| URL.revokeObjectURL(a.href); | |
| } | |
| function injectUI() { | |
| if (document.getElementById('claude-md-export')) return; | |
| const btn = document.createElement('button'); | |
| btn.id = 'claude-md-export'; | |
| btn.textContent = '⬇ MD'; | |
| Object.assign(btn.style, { | |
| position: 'fixed', bottom: '20px', right: '20px', zIndex: '10000', | |
| padding: '6px 14px', border: 'none', borderRadius: '6px', | |
| background: '#2563eb', color: '#fff', cursor: 'pointer', | |
| fontSize: '12px', fontWeight: '600', fontFamily: 'system-ui, sans-serif', | |
| boxShadow: '0 2px 6px rgba(0,0,0,.25)', | |
| opacity: '0.45', transition: 'opacity .2s', | |
| }); | |
| btn.onmouseenter = () => btn.style.opacity = '1'; | |
| btn.onmouseleave = () => btn.style.opacity = '0.45'; | |
| btn.onclick = downloadMd; | |
| document.body.appendChild(btn); | |
| } | |
| if (document.readyState === 'complete') injectUI(); | |
| else window.addEventListener('load', injectUI); | |
| setInterval(() => { if (!document.getElementById('claude-md-export')) injectUI(); }, 3000); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment