// ==UserScript==
// @name 复制格式转换(Markdown)
// @namespace http://tampermonkey.net/
// @version 2.0
// @description 选中内容后浮现按钮,点击自动复制为完整 Markdown 格式,确保排版与原文一致。支持数学公式、代码块语言标识、表格对齐。
// @author KiwiFruit
// @match *://*/*
// @grant GM_setClipboard
// @license MIT
// ==/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`;
// 换行符:<br> 转换为单个换行
case 'br':
return '\n';
// 段落:<p> 前后加空行
case 'p':
return `\n\n${formatParagraph(node)}\n\n`;
// 列表:<ul>/<ol> 前后加空行
case 'ul': case 'ol':
return `\n\n${formatList(node, tagName === 'ol')}\n\n`;
// 引用:<blockquote> 前后加空行
case 'blockquote':
return `\n\n${formatBlockquote(node)}\n\n`;
// 代码块:<pre> 前后加空行
case 'pre':
return `\n\n${formatCodeBlock(node)}\n\n`;
// 内联代码:<code> 直接返回,不加空行
case 'code':
return formatInlineCode(node);
// 链接:<a> 直接返回
case 'a':
return formatLink(node);
// 图片:<img> 直接返回
case 'img':
return formatImage(node);
// 加粗:<strong>/<b> 直接返回
case 'strong': case 'b':
return formatBold(node);
// 斜体:<em>/<i> 直接返回
case 'em': case 'i':
return formatItalic(node);
// 删除线:<del> 直接返回
case 'del':
return formatStrikethrough(node);
// 水平线:<hr> 前后加空行
case 'hr':
return '\n\n---\n\n';
// 表格:<table> 前后加空行
case 'table':
return `\n\n${formatTable(node)}\n\n`;
// 数学公式:<span.math-formula> 前后加空行
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();
}
}
})();