🎉 欢迎访问GreasyFork.Org 镜像站!本镜像站由公众号【爱吃馍】搭建,用于分享脚本。联系邮箱📮

Greasy fork 爱吃馍镜像

Readeasy (一键复制内容)

一键复制。内置你自定义的 Markdown 转换逻辑(支持中文引号转引用),使用 GM_addStyle 穿透 CSP。

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

// ==UserScript==
// @name         Readeasy (一键复制内容)
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  一键复制。内置你自定义的 Markdown 转换逻辑(支持中文引号转引用),使用 GM_addStyle 穿透 CSP。
// @author       _Sure.Lee
// @match        *://*/*
// @grant        GM_setClipboard
// @grant        GM_addStyle
// @run-at       document-end
// @require      https://unpkg.com/@mozilla/[email protected]/Readability.js
// ==/UserScript==

(function() {
    'use strict';

    function htmlToMarkdown(html) {
        const doc = new DOMParser().parseFromString(html, 'text/html');
        let lastTag = null;

        return traverse(doc.body);  // 不使用 trim,保留结构性换行

        function traverse(node) {
            let md = '';
            node.childNodes.forEach(child => {
                if (child.nodeType === Node.TEXT_NODE) {
                    // 如果去掉空白后长度为 0,就跳过
                    if (!/\S/.test(child.nodeValue)) return;
                    md += child.nodeValue.replace(/\s+/g, ' ');
                } else if (child.nodeType === Node.ELEMENT_NODE) {
                    const tag = child.tagName.toLowerCase();
                    const content = traverse(child).trim();
                    const className = (child.getAttribute('class') || '').toLowerCase();
                    const isQuoteLike = /quote/.test(className);
                    // 特有逻辑:全角引号内容自动变引用块
                    const isFullQuote = /^“[^”]{2,}”$/.test(content);
                    const parentTag = child.parentElement?.tagName?.toLowerCase() || '';

                    switch (tag) {
                        case 'h1': md += '# ' + content + '\n\n'; break;
                        case 'h2': md += '## ' + content + '\n\n'; break;
                        case 'h3': md += '### ' + content + '\n\n'; break;
                        case 'h4': md += '#### ' + content + '\n\n'; break;
                        case 'h5': md += '##### ' + content + '\n\n'; break;
                        case 'h6': md += '###### ' + content + '\n\n'; break;

                        case 'figure':
                            md += '\n' + content + '\n\n';  // figure 本身不渲染,只做结构隔断
                            lastTag = 'figure';
                            break;

                        case 'figcaption':
                            md += '\n> ' + content.replace(/\n/g, '\n> ') + '\n\n';
                            lastTag = 'figcaption';
                            break;

                        case 'p': {
                            if (lastTag === 'figcaption' && isFullQuote) {
                                let headingLevel = (parentTag === 'figure') ? 1 : 2;
                                const heading = '#'.repeat(headingLevel) + ' ' + content.trim();
                                md += '\n\n' + heading + '\n\n';  // 💡 强制从空行起始
                            } else if (isQuoteLike || isFullQuote) {
                                md += '\n> ' + content.replace(/\n/g, '\n> ') + '\n\n';
                            } else {
                                md += content + '\n\n';
                            }
                            lastTag = 'p';
                            break;
                        }
                        case 'br': md += '\n'; break;
                        case 'strong':
                        case 'b': md += '**' + content + '**'; break;
                        case 'em':
                        case 'i': md += '*' + content + '*'; break;
                        case 'code': md += '`' + content + '`'; break;
                        case 'pre': md += '\n```\n' + child.textContent.trim() + '\n```\n\n'; break;

                        case 'a': {
                            const href = child.getAttribute('href') || '';
                            md += '[' + content + '](' + href + ')';
                            break;
                        }

                        case 'img': {
                            const alt = child.getAttribute('alt') || '';
                            const src = child.getAttribute('src') || '';
                            md += '![' + alt + '](' + src + ')';
                            break;
                        }

                        case 'ul':
                            md += Array.from(child.children)
                                .map(li => '* ' + traverse(li).trim())
                                .join('\n') + '\n\n';
                            break;

                        case 'ol': {
                            let i = 1;
                            md += Array.from(child.children)
                                .map(li => (i++) + '. ' + traverse(li).trim())
                                .join('\n') + '\n\n';
                            break;
                        }

                        case 'li':
                            md += content + '\n';
                            break;

                        case 'blockquote':
                            md += '\n> ' + content.replace(/\n/g, '\n> ') + '\n\n';
                            break;

                        default:
                            if (isQuoteLike) {
                                md += '\n> ' + content.replace(/\n/g, '\n> ') + '\n\n';
                            } else {
                                md += content;
                            }
                    }
                }
            });
            return md;
        }
    }


    const css = `
        #copy-article-btn {
            position: fixed;
            top: 40%;
            right: 12px;
            z-index: 2147483647;
            width: 28px;
            height: 28px;
            border-radius: 50%;
            background: rgba(15, 23, 42, 0.4);
            border: 1px solid rgba(255, 255, 255, 0.1);
            backdrop-filter: blur(4px);
            box-shadow: 0 4px 12px rgba(0,0,0,0.1);
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            color: rgba(255, 255, 255, 0.6);
            opacity: 0.3;
            transform: scale(0.9);
            transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
            user-select: none;
            overflow: hidden;
            font-family: sans-serif;
            -webkit-font-smoothing: antialiased;
        }

        #copy-article-btn:hover {
            width: 42px;
            height: 42px;
            opacity: 1;
            transform: scale(1.1);
            right: 24px;
            background: linear-gradient(135deg, rgba(99, 102, 241, 0.9), rgba(34, 211, 238, 0.9));
            box-shadow: 0 8px 24px rgba(99, 102, 241, 0.4),
                        0 0 0 1px rgba(255,255,255,0.4) inset;
            color: #fff;
            border-color: transparent;
        }

        .copy-btn-icon {
            font-size: 14px;
            transition: transform 0.4s ease;
        }
        #copy-article-btn:hover .copy-btn-icon {
            font-size: 20px;
            transform: rotate(180deg);
        }

        /* 菜单样式 */
        #copy-format-menu {
            position: fixed;
            z-index: 2147483647;
            background: rgba(255, 255, 255, 0.9);
            backdrop-filter: blur(12px);
            box-shadow: 0 10px 30px rgba(0,0,0,0.15), 0 0 0 1px rgba(0,0,0,0.05);
            border-radius: 12px;
            padding: 5px;
            display: none;
            opacity: 0;
            transform: translateY(10px) scale(0.95);
            transition: all 0.2s ease;
            font-family: system-ui, -apple-system, sans-serif;
            min-width: 100px;
        }

        #copy-format-menu.show {
            opacity: 1;
            transform: translateY(0) scale(1);
        }

        #copy-format-menu button {
            display: block;
            width: 100%;
            padding: 8px 12px;
            background: transparent;
            border: none;
            border-radius: 8px;
            text-align: left;
            cursor: pointer;
            font-size: 13px;
            font-weight: 500;
            color: #333;
            transition: background 0.2s;
            margin-bottom: 2px;
        }

        #copy-format-menu button:hover {
            background: rgba(99, 102, 241, 0.1);
            color: #4f46e5;
        }

        @media (prefers-color-scheme: dark) {
            #copy-format-menu {
                background: rgba(30, 41, 59, 0.95);
                box-shadow: 0 10px 30px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.1);
            }
            #copy-format-menu button { color: #cbd5e1; }
            #copy-format-menu button:hover { background: rgba(255,255,255,0.1); color: #fff; }
        }
    `;

    GM_addStyle(css);

    if (document.getElementById('copy-article-btn')) return;

    const btn = document.createElement('button');
    btn.id = 'copy-article-btn';
    btn.title = '复制文章内容';
    btn.innerHTML = '<span class="copy-btn-icon">⚡</span>';

    // 拖动逻辑
    let isDragging = false, offsetX = 0, offsetY = 0, hasMoved = false;

    btn.addEventListener('mousedown', function(e) {
        isDragging = true;
        hasMoved = false;
        offsetX = e.clientX - btn.getBoundingClientRect().left;
        offsetY = e.clientY - btn.getBoundingClientRect().top;
        btn.style.transition = 'none';
    });

    document.addEventListener('mousemove', function(e) {
        if (isDragging) {
            hasMoved = true;
            btn.style.left = (e.clientX - offsetX) + 'px';
            btn.style.top = (e.clientY - offsetY) + 'px';
            btn.style.right = 'auto';
        }
    });

    document.addEventListener('mouseup', function() {
        if (isDragging) {
            isDragging = false;
            btn.style.transition = 'all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)';
        }
    });

    // 菜单
    const menu = document.createElement('div');
    menu.id = 'copy-format-menu';
    menu.innerHTML = `
        <button data-format="markdown">Markdown</button>
        <button data-format="text">Pure Text</button>
        <button data-format="html">HTML Code</button>
    `;
    document.body.appendChild(menu);

    function hideMenu() {
        menu.classList.remove('show');
        setTimeout(() => {
             if (!menu.classList.contains('show')) menu.style.display = 'none';
        }, 200);
    }

    btn.addEventListener('click', function(e) {
        if (hasMoved) return;
        e.stopPropagation();

        if (menu.style.display === 'block') {
            hideMenu();
        } else {
            const rect = btn.getBoundingClientRect();
            menu.style.display = 'block';
            const leftPos = rect.left - 110;
            menu.style.left = (leftPos > 0 ? leftPos : 10) + 'px';
            menu.style.top = (rect.bottom + 10) + 'px';
            requestAnimationFrame(() => menu.classList.add('show'));
        }
    });

    document.addEventListener('click', function(e) {
        if (!menu.contains(e.target) && e.target !== btn) {
            hideMenu();
        }
    });

    menu.addEventListener('click', function(e) {
        const format = e.target.getAttribute('data-format');
        if (!format) return;
        e.stopPropagation();
        hideMenu();
        copyArticle(format);
    });

    document.body.appendChild(btn);

    // 清理 HTML
    function cleanHtml(html) {
        const div = document.createElement('div');
        div.innerHTML = html;
        const elementsToRemove = div.querySelectorAll(
            'script, style, link, meta, iframe, button, input, form, nav, footer, [role="complementary"]'
        );
        elementsToRemove.forEach(el => el.remove());
        div.querySelectorAll('div:empty, span:empty, p:empty').forEach(el => el.remove());
        return div.innerHTML;
    }

    function formatPlainText(title, text) {
        text = text.replace(/\n{3,}/g, '\n\n');
        return `${title}\n\n${text}`;
    }

    async function copyArticle(format) {
        const originalIcon = btn.innerHTML;
        btn.innerHTML = '<span class="copy-btn-icon">⏳</span>';

        let title = document.title;
        let htmlContent = '';
        let textContent = '';
        let markdownContent = '';

        try {
            if (typeof Readability === 'undefined') {
                throw new Error('Readability library not loaded');
            }

            const documentClone = document.cloneNode(true);
            const article = new Readability(documentClone).parse();

            if (article) {
                title = article.title;
                htmlContent = cleanHtml(article.content);
                textContent = formatPlainText(title, article.textContent);


                markdownContent = `# ${title}\n\n` + htmlToMarkdown(htmlContent);
            } else {
                console.log("Readability parse failed, using fallback.");
                htmlContent = cleanHtml(document.body.innerHTML);
                textContent = formatPlainText(document.title, document.body.innerText);
                markdownContent = `# ${document.title}\n\n` + htmlToMarkdown(htmlContent);
            }

            let finalData = '';
            let mimeType = 'text/plain';

            if (format === 'markdown') {
                finalData = markdownContent;
            } else if (format === 'html') {
                finalData = `<h1>${title}</h1>${htmlContent}`;
                mimeType = 'text/html';
            } else {
                finalData = textContent;
            }

            try {
                if (navigator.clipboard && navigator.clipboard.writeText) {
                    await navigator.clipboard.writeText(finalData);
                } else {
                    throw new Error('Clipboard API not available');
                }
            } catch (clipboardErr) {
                if (typeof GM_setClipboard !== 'undefined') {
                    GM_setClipboard(finalData, { type: mimeType });
                } else {
                    throw clipboardErr;
                }
            }

            btn.innerHTML = '<span class="copy-btn-icon" style="color:#4ade80">✔</span>';
            btn.style.transform = 'scale(1.2)';

            setTimeout(() => {
                btn.innerHTML = originalIcon;
                btn.style.transform = '';
            }, 1200);

        } catch (e) {
            console.error('Copy failed:', e);
            btn.innerHTML = '<span class="copy-btn-icon">❌</span>';
            setTimeout(() => btn.innerHTML = originalIcon, 1200);
            alert('复制失败:' + e.message);
        }
    }

})();