Greasy Fork

Greasy Fork is available in English.

网页选中内容一键导出 Markdown(最终正式版 v4.3)

划词选中后弹出优美悬浮按钮,一键导出为 Markdown(支持丰富格式保留)。v4.3 已彻底解决按钮不显示问题,兼容各类广告屏蔽脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         网页选中内容一键导出 Markdown(最终正式版 v4.3)
// @namespace    http://tampermonkey.net/
// @version      4.3
// @description  划词选中后弹出优美悬浮按钮,一键导出为 Markdown(支持丰富格式保留)。v4.3 已彻底解决按钮不显示问题,兼容各类广告屏蔽脚本。
// @author       Gemini & xc(优化 by Grok)
// @license      GPL-3.0
// @match        *://*/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // 1. 注入少量 CSS(仅负责动画和悬停效果)
    const style = document.createElement('style');
    style.textContent = `
        #grok-md-export-btn {
            transition: all 0.3s cubic-bezier(0.68, -0.55, 0.27, 1.55) !important;
        }
        #grok-md-export-btn.show {
            animation: grokPop 0.4s cubic-bezier(0.68, -0.55, 0.27, 1.55) forwards !important;
        }
        @keyframes grokPop {
            0%   { opacity: 0; transform: translateY(20px) scale(0.85); }
            100% { opacity: 1; transform: translateY(0) scale(1); }
        }
        #grok-md-export-btn:hover {
            transform: translateY(-3px) scale(1.05) !important;
            box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3) !important;
        }
    `;
    document.head.appendChild(style);

    // 2. 创建按钮(关键样式全部内联,确保不被其他脚本覆盖)
    const exportBtn = document.createElement('div');
    exportBtn.id = 'grok-md-export-btn';
    exportBtn.innerHTML = `<span style="font-size:20px;">✨</span> 导出 Markdown`;
    exportBtn.style.cssText = `
        position: fixed !important;
        z-index: 2147483647 !important;
        opacity: 0;
        visibility: hidden;
        transform: translateY(20px) scale(0.85);
        background: rgba(30, 30, 32, 0.92) !important;
        backdrop-filter: blur(16px) !important;
        -webkit-backdrop-filter: blur(16px) !important;
        color: #ffffff !important;
        padding: 12px 24px !important;
        border-radius: 16px !important;
        font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif !important;
        font-size: 15.5px !important;
        font-weight: 600 !important;
        box-shadow: 0 12px 32px rgba(0, 0, 0, 0.25) !important;
        display: flex !important;
        align-items: center !important;
        gap: 9px !important;
        white-space: nowrap !important;
        cursor: pointer !important;
        user-select: none !important;
        pointer-events: auto !important;
        border: 1px solid rgba(255,255,255,0.15) !important;
    `;
    document.body.appendChild(exportBtn);

    let currentExportContent = '';

    // 3. Markdown 解析函数(智能保留格式)
    function nodeToMarkdown(node) {
        if (node.nodeType === Node.TEXT_NODE) return node.textContent;
        if (node.nodeType !== Node.ELEMENT_NODE) return '';
        const tag = node.tagName.toLowerCase();
        let inner = Array.from(node.childNodes).map(nodeToMarkdown).join('');
        switch (tag) {
            case 'h1': return `# ${inner.trim()}\n\n`;
            case 'h2': return `## ${inner.trim()}\n\n`;
            case 'h3': return `### ${inner.trim()}\n\n`;
            case 'h4': return `#### ${inner.trim()}\n\n`;
            case 'h5': return `##### ${inner.trim()}\n\n`;
            case 'h6': return `###### ${inner.trim()}\n\n`;
            case 'p': case 'div': case 'section': case 'article': return `${inner.trim()}\n\n`;
            case 'strong': case 'b': return `**${inner.trim()}**`;
            case 'em': case 'i': return `*${inner.trim()}*`;
            case 'a': {
                let href = node.getAttribute('href') || '';
                if (href) {
                    if (!href.startsWith('http')) href = href.startsWith('/') ? window.location.origin + href : new URL(href, window.location.href).href;
                    return `[${inner.trim()}](${href})`;
                }
                return inner.trim();
            }
            case 'ul': {
                let list = '\n';
                Array.from(node.children).forEach(child => { if (child.tagName.toLowerCase() === 'li') list += `- ${nodeToMarkdown(child).trim()}\n`; });
                return list + '\n';
            }
            case 'ol': {
                let list = '\n';
                Array.from(node.children).forEach((child, i) => { if (child.tagName.toLowerCase() === 'li') list += `${i + 1}. ${nodeToMarkdown(child).trim()}\n`; });
                return list + '\n';
            }
            case 'li': return inner.trim();
            case 'img': {
                let src = node.getAttribute('src') || node.getAttribute('data-src') || node.getAttribute('data-original') || '';
                if (src) {
                    if (src.startsWith('//')) src = 'https:' + src;
                    else if (!src.startsWith('http')) src = new URL(src, window.location.href).href;
                    return `![${node.getAttribute('alt') || '图片'}](${src})\n\n`;
                }
                return '';
            }
            case 'blockquote': return `> ${inner.replace(/\n/g, '\n> ')}\n\n`;
            case 'pre': return `\n\`\`\`\n${inner.trim()}\n\`\`\`\n\n`;
            case 'code': return node.parentElement?.tagName.toLowerCase() === 'pre' ? inner : `\`${inner.trim()}\``;
            case 'br': return '\n';
            default: return inner.trim() ? inner.trim() + '\n\n' : '';
        }
    }

    function parseSelection() {
        const sel = window.getSelection();
        if (sel.rangeCount === 0 || sel.toString().trim() === '') return null;

        const range = sel.getRangeAt(0);
        const frag = range.cloneContents();
        const tempDiv = document.createElement('div');
        tempDiv.appendChild(frag);

        let mdContent = sel.toString().trim();
        try {
            mdContent = nodeToMarkdown(tempDiv).replace(/(\n{3,})/g, '\n\n').trim();
        } catch(e) {}

        const metaData = `# [${document.title}](${window.location.href})\n> **抓取时间:** ${new Date().toLocaleString('zh-CN')}\n\n---\n\n`;
        return metaData + mdContent;
    }

    // 4. 鼠标事件
    document.addEventListener('mouseup', function(e) {
        if (e.target === exportBtn || exportBtn.contains(e.target)) return;

        setTimeout(() => {
            currentExportContent = parseSelection();
            if (currentExportContent) {
                let left = e.clientX + 25;
                let top = e.clientY + 25;

                const btnWidth = 200;
                const btnHeight = 55;
                if (left + btnWidth > window.innerWidth) left = e.clientX - btnWidth - 25;
                if (top + btnHeight > window.innerHeight) top = e.clientY - btnHeight - 35;

                exportBtn.style.left = `${left}px`;
                exportBtn.style.top = `${top}px`;
                exportBtn.style.opacity = '1';
                exportBtn.style.visibility = 'visible';
                exportBtn.style.transform = 'translateY(0) scale(1)';
                exportBtn.classList.add('show');
            }
        }, 80);
    });

    document.addEventListener('mousedown', () => {
        exportBtn.style.opacity = '0';
        exportBtn.style.visibility = 'hidden';
        exportBtn.classList.remove('show');
    });

    // 5. 点击导出 + 成功反馈
    exportBtn.addEventListener('click', function(e) {
        e.stopPropagation();
        if (!currentExportContent) return;

        const blob = new Blob([currentExportContent], { type: 'text/markdown;charset=utf-8' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        const shortTitle = document.title.substring(0, 25).replace(/[/\\?%*:|"<>]/g, '_');
        a.href = url;
        a.download = `摘录_${shortTitle}_${Date.now()}.md`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);

        // 成功反馈
        const originalHTML = exportBtn.innerHTML;
        exportBtn.style.background = 'rgba(16, 185, 129, 0.95) !important';
        exportBtn.innerHTML = `<span style="font-size:20px;">✅</span> 已成功导出!`;

        setTimeout(() => {
            exportBtn.style.opacity = '0';
            exportBtn.style.visibility = 'hidden';
            exportBtn.style.background = 'rgba(30, 30, 32, 0.92) !important';
            exportBtn.innerHTML = originalHTML;
            exportBtn.classList.remove('show');
            window.getSelection().removeAllRanges();
            currentExportContent = '';
        }, 1600);
    });
})();