复制格式转换 (Markdown)

选中内容后浮现按钮在右上角,点击按钮自动复制为完整的 Markdown 格式,并确保排版正确。新增支持数学公式的复制。

// ==UserScript==
// @name         复制格式转换 (Markdown)
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  选中内容后浮现按钮在右上角,点击按钮自动复制为完整的 Markdown 格式,并确保排版正确。新增支持数学公式的复制。
// @author       KiwiFruit
// @match        *://*/*
// @grant        GM_setClipboard
// @license      MIT
// ==/UserScript==
(function () {
    'use strict';
    // 配置选项
    const config = {
        preserveEmptyLines: false, // 是否保留空行
        addSeparators: true, // 是否自动添加分隔符
    };

    // 创建浮动按钮
    function createFloatingButton() {
        const button = document.createElement('button');
        button.id = 'markdownCopyButton';
        button.innerText = '复制为 Markdown 格式 (Ctrl+Shift+C)';
        button.style.position = 'fixed';
        button.style.top = '20px';
        button.style.right = '20px';
        button.style.padding = '10px 20px';
        button.style.backgroundColor = '#007bff';
        button.style.color = '#fff';
        button.style.border = 'none';
        button.style.borderRadius = '5px';
        button.style.cursor = 'pointer';
        button.style.zIndex = '9999';
        button.style.display = 'none';
        button.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)';
        document.body.appendChild(button);
        return button;
    }
    let floatingButton = createFloatingButton();

    // 创建预览窗口
    function createPreviewWindow() {
        const preview = document.createElement('div');
        preview.id = 'markdownPreview';
        preview.style.position = 'fixed';
        preview.style.top = '60px';
        preview.style.right = '20px';
        preview.style.width = '300px';
        preview.style.maxHeight = '400px';
        preview.style.overflowY = 'auto';
        preview.style.padding = '10px';
        preview.style.backgroundColor = '#fff';
        preview.style.border = '1px solid #ddd';
        preview.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)';
        preview.style.zIndex = '9999';
        preview.style.display = 'none';
        document.body.appendChild(preview);
        return preview;
    }
    let previewWindow = createPreviewWindow();

    // 显示浮动按钮
    function showFloatingButton() {
        floatingButton.style.display = 'block';
    }

    // 隐藏浮动按钮
    function hideFloatingButton() {
        floatingButton.style.display = 'none';
        previewWindow.style.display = 'none'; // 同时隐藏预览窗口
    }

    // 递归提取并转换为 Markdown
    function convertToMarkdown(node) {
        if (node.nodeType === Node.TEXT_NODE) {
            const text = node.textContent.trim();
            return config.preserveEmptyLines ? text : text || '';
        }
        if (node.nodeType === Node.ELEMENT_NODE) {
            switch (node.tagName.toLowerCase()) {
                case 'h1':
                case 'h2':
                case 'h3':
                case 'h4':
                case 'h5':
                case 'h6': {
                    const level = node.tagName.toLowerCase().replace('h', '');
                    const content = processChildren(node);
                    return `${'#'.repeat(parseInt(level))} ${content}
`;
                }
                case 'p': {
                    return `${processChildren(node)}
`;
                }
                case 'ul': {
                    let markdown = '';
                    for (const li of node.children) {
                        markdown += `- ${processChildren(li)}
`;
                    }
                    return markdown + '\n';
                }
                case 'ol': {
                    let markdown = '';
                    let index = 1;
                    for (const li of node.children) {
                        markdown += `${index++}. ${processChildren(li)}
`;
                    }
                    return markdown + '\n';
                }
                case 'blockquote': {
                    const content = processChildren(node);
                    return `> ${content.split('\n').join('\n> ')}
`;
                }
                case 'pre': {
                    return `\`\`\`
${node.textContent.trim()}
\`\`\`
`;
                }
                case 'code': {
                    return `\`${node.textContent.trim()}\``;
                }
                case 'a': {
                    const text = processChildren(node);
                    const href = node.href || '';
                    return `[${text}](${href})`;
                }
                case 'img': {
                    const alt = node.alt || 'Image';
                    const src = node.src || '';
                    return `![${alt}](${src})`;
                }
                case 'strong':
                case 'b': {
                    return `**${processChildren(node)}**`;
                }
                case 'em':
                case 'i': {
                    return `*${processChildren(node)}*`;
                }
                case 'del': {
                    return `~~${processChildren(node)}~~`;
                }
                case 'hr': {
                    return '---\n';
                }
                case 'table': {
                    const rows = Array.from(node.rows).map(row => {
                        const cells = Array.from(row.cells).map(cell => `| ${processChildren(cell).trim()} `);
                        return cells.join('') + '|';
                    });
                    return `${rows.join('\n')}\n`;
                }
                case 'span': // 新增:处理公式
                case 'div': { // 新增:处理公式
                    if (node.classList.contains('math-formula')) { // 假设公式有一个特定的类名
                        const formulaText = node.textContent.trim();
                        return config.addSeparators ? `$$\n${formulaText}\n$$` : `$${formulaText}$`; // 根据需要调整
                    }
                    return processChildren(node);
                }
                default: {
                    return processChildren(node);
                }
            }
        }
        return ''; // 忽略其他类型的节点
    }

    // 辅助函数:处理子节点
    function processChildren(node) {
        let result = '';
        for (const child of node.childNodes) {
            result += convertToMarkdown(child);
        }
        return result.trim();
    }

    // 提取选中内容并转换为 Markdown
    function extractAndConvertToMarkdown(range) {
        return Array.from(range.cloneContents().childNodes)
            .map(node => convertToMarkdown(node))
            .join('')
            .trim();
    }

    // 复制到剪贴板
    async function copyToClipboard(text) {
        try {
            if (typeof GM_setClipboard === 'function') {
                GM_setClipboard(text); // 使用 Tampermonkey 提供的剪贴板功能
                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');
        toast.style.position = 'fixed';
        toast.style.bottom = '20px';
        toast.style.right = '20px';
        toast.style.padding = '10px 20px';
        toast.style.backgroundColor = '#333';
        toast.style.color = '#fff';
        toast.style.borderRadius = '5px';
        toast.style.zIndex = '9999';
        toast.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)';
        toast.innerText = message;
        document.body.appendChild(toast);
        setTimeout(() => {
            toast.remove();
        }, 2000);
    }

    // 监听鼠标释放事件
    document.addEventListener('mouseup', handleMouseUp);

    // 处理鼠标释放事件
    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();
        }
    }

    // 添加快捷键支持
    document.addEventListener('keydown', (event) => {
        if (event.ctrlKey && event.shiftKey && event.code === 'KeyC') {
            handleMouseUp(event); // 模拟鼠标释放事件
        }
    });

    // 监听页面点击事件,点击空白区域时隐藏按钮
    document.addEventListener('mousedown', (event) => {
        if (!floatingButton.contains(event.target) && !previewWindow.contains(event.target)) {
            hideFloatingButton();
        }
    });
})();