复制格式转换(Markdown)

选中内容后浮现按钮(固定右上角),点击自动复制为完整 Markdown 格式,确保排版与原文一致。支持数学公式、代码块语言标识、表格对齐。简化浮现逻辑。

// ==UserScript==
// @name         复制格式转换(Markdown)
// @namespace    http://tampermonkey.net/
// @version      2.2
// @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();

    /**
     * 创建浮动按钮元素 (固定位置)
     * @returns {HTMLElement} 按钮元素
     */
    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: '8px 15px',
            backgroundColor: '#007bff',
            color: '#fff',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer',
            zIndex: '10000',
            display: 'none', // 初始隐藏
            boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
            transition: 'opacity 0.2s ease-in-out',
            fontSize: '13px',
            whiteSpace: 'nowrap'
        });
        document.body.appendChild(button);
        return button;
    }

    /**
     * 创建预览窗口元素 (固定在按钮下方)
     * @returns {HTMLElement} 预览窗口元素
     */
    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',
            overflowX: 'auto',
            padding: '10px',
            backgroundColor: '#fff',
            border: '1px solid #ddd',
            boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
            zIndex: '9999',
            display: 'none', // 初始隐藏
            fontFamily: 'monospace',
            fontSize: '13px',
            whiteSpace: 'pre-wrap',
            wordWrap: 'break-word',
            boxSizing: 'border-box'
        });
        document.body.appendChild(preview);
        return preview;
    }

    /**
     * 显示浮动按钮 (简单显示)
     */
    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) {
            let text = node.textContent;
            return config.preserveEmptyLines ? text : text.trim() || '';
        }
        if (node.nodeType === Node.ELEMENT_NODE) {
            const tagName = node.tagName.toLowerCase();
            switch (tagName) {
                case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
                    return `\n${formatHeader(node)}\n`;
                case 'br':
                    return '\n';
                case 'p':
                    return `\n${formatParagraph(node)}\n`;
                case 'ul': case 'ol':
                    return `\n${formatList(node, tagName === 'ol')}\n`;
                case 'blockquote':
                    return `\n${formatBlockquote(node)}\n`;
                case 'pre':
                    return `\n${formatCodeBlock(node)}\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';
                case 'table':
                    return `\n${formatTable(node)}\n`;
                case 'span': case 'div':
                    if (node.matches(config.mathSelector)) {
                        return `\n${formatMath(node)}\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`;
    }

    // 改进代码块语言检测
    function formatCodeBlock(node) {
        let langMatch = node.className.match(/language-(\w+)/);
        let lang = langMatch ? langMatch[1] : '';
        if (!lang) {
            const codeEl = node.querySelector('code');
            if (codeEl) {
                langMatch = codeEl.className.match(/language-(\w+)/);
                lang = langMatch ? langMatch[1] : '';
            }
        }
        const code = node.textContent.trim();
        return `\`\`\`${lang}\n${code}\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 `![${alt}](${src})`;
    }

    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 separatorCells = headers.map(cell => {
            const style = window.getComputedStyle(cell);
            const align = style.textAlign;
            if (align === 'right') return '---:';
            if (align === 'center') return ':---:';
            return '---';
        });
        const separator = `| ${separatorCells.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}`;
    }

    function formatMath(node) {
        const formula = node.textContent.trim();
        if (config.addSeparators) {
            return `$$\n${formula}\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;
        }
    }

    /**
     * 显示提示信息 (修复动画)
     * @param {string} message 提示内容
     */
    function showToast(message) {
        // 动态注入关键帧动画样式
        if (!document.getElementById('markdown-copy-toast-styles')) {
            const style = document.createElement('style');
            style.id = 'markdown-copy-toast-styles';
            style.textContent = `
                @keyframes fadeInOut {
                    0% { opacity: 0; transform: translateY(20px); }
                    10% { opacity: 1; transform: translateY(0); }
                    90% { opacity: 1; transform: translateY(0); }
                    100% { opacity: 0; transform: translateY(20px); }
                }
            `;
            document.head.appendChild(style);
        }

        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: '10001',
            boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
            animation: 'fadeInOut 2.5s ease-in-out forwards',
            pointerEvents: 'none'
        });
        toast.innerText = message;
        document.body.appendChild(toast);
        setTimeout(() => {
             if (toast.parentNode) { toast.remove(); }
        }, 2500);
    }

    // --- 事件监听器 ---
    document.addEventListener('mouseup', handleMouseUp);
    document.addEventListener('keydown', handleKeyDown);
    document.addEventListener('mousedown', handleMouseDown);

    /**
     * 鼠标释放事件处理 (简化逻辑)
     * @param {Event} event 鼠标事件
     */
    function handleMouseUp(event) {
        // 添加小延迟,确保 selection 状态是最新的
        setTimeout(() => {
            const selection = window.getSelection();
            // 简化的浮现逻辑:只要选区存在且未折叠,就显示按钮
            if (selection && !selection.isCollapsed) {
                showFloatingButton();
                floatingButton.onclick = async () => {
                    try {
                        let currentRange;
                        try {
                            currentRange = selection.getRangeAt(0);
                        } catch (e) {
                            throw new Error("选区在复制时已丢失,请重新选择内容。");
                        }

                        const markdownContent = extractAndConvertToMarkdown(currentRange);

                        // 显示预览窗口
                        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 {
                        // 不再立即隐藏
                    }
                };
            } else {
                // 如果没有选区,则隐藏按钮
                hideFloatingButton();
            }
        }, 10); // 10ms 延迟
    }

    /**
     * 键盘事件处理
     * @param {Event} event 键盘事件
     */
    function handleKeyDown(event) {
        if (event.ctrlKey && event.shiftKey && event.code === 'KeyC') {
            event.preventDefault(); // 阻止默认行为
            handleMouseUp(event); // 触发复制逻辑
        }
    }

    /**
     * 鼠标按下事件处理(用于点击外部区域隐藏按钮)
     * @param {Event} event 鼠标事件
     */
    function handleMouseDown(event) {
        // 如果点击的不是按钮或预览窗口本身,则隐藏它们
        if (!floatingButton.contains(event.target) && !previewWindow.contains(event.target)) {
            hideFloatingButton();
        }
    }

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址