Skip to content

Instantly share code, notes, and snippets.

@fxlrnrpt
Created March 21, 2026 17:14
Show Gist options
  • Select an option

  • Save fxlrnrpt/14f569a330d9c42ec7b320f9f2a72bbb to your computer and use it in GitHub Desktop.

Select an option

Save fxlrnrpt/14f569a330d9c42ec7b320f9f2a72bbb to your computer and use it in GitHub Desktop.
claude MD export
// ==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