// ==UserScript==
// @name 复制格式转换(Markdown)
// @namespace http://tampermonkey.net/
// @version 2.1
// @description 选中内容后浮现按钮,点击自动复制为完整 Markdown 格式,确保排版与原文一致。支持数学公式、代码块语言标识、表格对齐。
// @author KiwiFruit
// @match *://*/*
// @grant GM_setClipboard
// @license MIT
// @downloadURL https://update.greasyfork.icu/scripts/532490/%E5%A4%8D%E5%88%B6%E6%A0%BC%E5%BC%8F%E8%BD%AC%E6%8D%A2%EF%BC%88Markdown%EF%BC%89.user.js
// @updateURL https://update.greasyfork.icu/scripts/532490/%E5%A4%8D%E5%88%B6%E6%A0%BC%E5%BC%8F%E8%BD%AC%E6%8D%A2%EF%BC%88Markdown%EF%BC%89.meta.js
// ==/UserScript==
(function () {
'use strict';
const config = {
preserveEmptyLines: false, // 是否保留空行
addSeparators: true, // 数学公式使用 $$ 分隔符
mathSelector: '.math-formula', // 数学公式自定义选择器
};
// 创建浮动按钮
const floatingButton = createFloatingButton();
// 创建预览窗口
const previewWindow = createPreviewWindow();
function createFloatingButton() {
const button = document.createElement('button');
button.id = 'markdownCopyButton';
button.innerText = '复制为 MD (Ctrl+Shift+C)';
Object.assign(button.style, {
position: 'fixed',
top: '20px',
right: '20px',
padding: '10px 20px',
backgroundColor: '#007bff',
color: '#fff',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
zIndex: '9999',
display: 'none',
boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
transition: 'opacity 0.3s ease-in-out'
});
document.body.appendChild(button);
return button;
}
function createPreviewWindow() {
const preview = document.createElement('div');
preview.id = 'markdownPreview';
Object.assign(preview.style, {
position: 'fixed',
top: '60px',
right: '20px',
width: '300px',
maxHeight: '400px',
overflowY: 'auto',
padding: '10px',
backgroundColor: '#fff',
border: '1px solid #ddd',
boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
zIndex: '9999',
display: 'none',
fontFamily: 'monospace',
fontSize: '14px',
whiteSpace: 'pre-wrap',
wordWrap: 'break-word'
});
document.body.appendChild(preview);
return preview;
}
function showFloatingButton() {
floatingButton.style.display = 'block';
}
function hideFloatingButton() {
floatingButton.style.display = 'none';
previewWindow.style.display = 'none';
}
function convertToMarkdown(node) {
if (node.nodeType === Node.TEXT_NODE) {
let text = node.textContent;
return config.preserveEmptyLines ? text : text.trim() || '';
}
if (node.nodeType === Node.ELEMENT_NODE) {
const tagName = node.tagName.toLowerCase();
switch (tagName) {
// 标题(h1-h6):前后加空行
case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
return `\n\n${formatHeader(node)}\n\n`;
// 换行符:
转换为单个换行
case 'br':
return '\n';
// 段落:
前后加空行 case 'p': return `\n\n${formatParagraph(node)}\n\n`; // 列表:
前后加空行 case 'blockquote': return `\n\n${formatBlockquote(node)}\n\n`; // 代码块:前后加空行 case 'pre': return `\n\n${formatCodeBlock(node)}\n\n`; // 内联代码:直接返回,不加空行 case 'code': return formatInlineCode(node); // 链接: 直接返回 case 'a': return formatLink(node); // 图片:
直接返回 case 'img': return formatImage(node); // 加粗:/ 直接返回 case 'strong': case 'b': return formatBold(node); // 斜体:/ 直接返回 case 'em': case 'i': return formatItalic(node); // 删除线:
直接返回 case 'del': return formatStrikethrough(node); // 水平线:
前后加空行 case 'hr': return '\n\n---\n\n'; // 表格:前后加空行 case 'table': return `\n\n${formatTable(node)}\n\n`; // 数学公式:
前后加空行 case 'span': case 'div': if (node.matches(config.mathSelector)) { return `\n\n${formatMath(node)}\n\n`; } return processChildren(node); // 默认处理:递归子节点 default: return processChildren(node); } } return ''; } function formatHeader(node) { const level = parseInt(node.tagName.slice(1)); const content = processChildren(node).trim(); return `${'#'.repeat(level)} ${content}`; } function formatParagraph(node) { const content = processChildren(node).trim(); return `${content}`; } function formatList(node, isOrdered) { let markdown = ''; let index = 1; for (const li of node.children) { const content = processChildren(li).trim(); markdown += `${isOrdered ? `${index++}.` : '-' } ${content}\n`; } return `\n${markdown}\n`; } function formatBlockquote(node) { const content = processChildren(node).trim(); return `> ${content.replace(/\n/g, '\n> ')}\n\n`; } function formatCodeBlock(node) { const langMatch = node.className.match(/language-(\w+)/); const lang = langMatch ? langMatch[1] : ''; const code = node.textContent.trim(); return `\`\`\`${lang}\n${code}\n\`\`\`\n\n`; } function formatInlineCode(node) { return `\`${node.textContent.trim()}\``; } function formatLink(node) { const text = processChildren(node).trim(); const href = node.href || ''; return `[${text}](${href})`; } function formatImage(node) { const alt = node.alt || 'Image'; const src = node.src || ''; return ``; } function formatBold(node) { const content = processChildren(node).trim(); return ` **${content}** `; } function formatItalic(node) { const content = processChildren(node).trim(); return ` *${content}* `; } function formatStrikethrough(node) { const content = processChildren(node).trim(); return `~~${content}~~`; } function formatTable(node) { const rows = Array.from(node.rows); if (rows.length === 0) return ''; const headers = Array.from(rows[0].cells); const separator = headers.map(() => '---').join('|'); const headerRow = `| ${headers.map(cell => cell.textContent.trim()).join(' | ')} |`; const dataRows = rows.slice(1).map(row => { const cells = Array.from(row.cells).map(cell => { return processChildren(cell).trim().replace(/\n/g, ' '); }); return `| ${cells.join(' | ')} |`; }).join('\n'); return `${headerRow}\n| ${separator} |\n${dataRows}\n\n`; } function formatMath(node) { const formula = node.textContent.trim(); if (config.addSeparators) { return `$$\n${formula}\n$$\n\n`; } else { return `$${formula}$ `; } } function processChildren(node) { let result = ''; for (const child of node.childNodes) { result += convertToMarkdown(child); } return result; } function extractAndConvertToMarkdown(range) { const fragment = range.cloneContents(); const nodes = Array.from(fragment.childNodes); return nodes.map(node => convertToMarkdown(node)).join('').trim(); } async function copyToClipboard(text) { try { if (typeof GM_setClipboard === 'function') { GM_setClipboard(text); return true; } else { await navigator.clipboard.writeText(text); console.log('Markdown 已复制到剪贴板'); return true; } } catch (err) { console.error('复制失败:', err); return false; } } function showToast(message) { const toast = document.createElement('div'); Object.assign(toast.style, { position: 'fixed', bottom: '20px', right: '20px', padding: '10px 20px', backgroundColor: '#333', color: '#fff', borderRadius: '5px', zIndex: '9999', boxShadow: '0 4px 6px rgba(0,0,0,0.1)', animation: 'fadeInOut 2s ease-in-out' }); toast.innerText = message; document.body.appendChild(toast); setTimeout(() => toast.remove(), 2000); } document.addEventListener('mouseup', handleMouseUp); document.addEventListener('keydown', handleKeyDown); document.addEventListener('mousedown', handleMouseDown); function handleMouseUp(event) { const selection = window.getSelection(); if (selection && !selection.isCollapsed) { showFloatingButton(); floatingButton.onclick = async () => { try { const range = selection.getRangeAt(0); const markdownContent = extractAndConvertToMarkdown(range); previewWindow.innerText = markdownContent; previewWindow.style.display = 'block'; const success = await copyToClipboard(markdownContent); if (success) { showToast('内容已复制为 Markdown 格式!'); } else { showToast('复制失败,请重试!'); } } catch (err) { console.error('处理内容时出错:', err); showToast(`发生错误:${err.message}`); } finally { hideFloatingButton(); } }; } else { hideFloatingButton(); } } function handleKeyDown(event) { if (event.ctrlKey && event.shiftKey && event.code === 'KeyC') { handleMouseUp(event); } } function handleMouseDown(event) { if (!floatingButton.contains(event.target) && !previewWindow.contains(event.target)) { hideFloatingButton(); } } })();