LeetCode Copy Cleaner (去除复制的代码作者信息)

点击 LeetCode 代码块的复制按钮时,只复制纯代码,去除末尾附加的作者和题目链接等信息。

// ==UserScript==
// @name         LeetCode Copy Cleaner (去除复制的代码作者信息)
// @namespace    https://github.com/lesir831/UserScript
// @version      1.3
// @description  点击 LeetCode 代码块的复制按钮时,只复制纯代码,去除末尾附加的作者和题目链接等信息。
// @author       lesir
// @match        https://leetcode.com/problems/*
// @match        https://leetcode.cn/problems/*
// @match        https://leetcode.com/explore/interview/*
// @match        https://leetcode.cn/explore/interview/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=leetcode.com
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';

    console.log('LeetCode Copy Code Cleaner script loaded.');

    // 添加一个防抖函数,避免事件多次触发导致的问题
    function debounce(func, wait) {
        let timeout;
        return function (...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    }

    // 保存原始的 Clipboard API 方法
    const originalWriteText = navigator.clipboard.writeText;

    // 覆盖剪贴板 API 以拦截所有复制操作
    navigator.clipboard.writeText = function (text) {
        // 检查文本是否包含 LeetCode 的特征文本
        if (text && (text.includes('\nAuthor: ') || text.includes('\n作者:') ||
            text.includes('https://leetcode.com') || text.includes('https://leetcode.cn'))) {
            console.log('Detected LeetCode copyright text, cleaning...');

            // 查找并删除版权信息
            // 匹配多种可能的版权格式
            const cleanedText = text.split(/\n(Author: |作者:)/)[0].trim();
            console.log('Cleaned code copied to clipboard');
            return originalWriteText.call(this, cleanedText);
        }

        // 如果不是 LeetCode 代码,正常执行
        return originalWriteText.call(this, text);
    };

    // 主要的按钮点击拦截功能
    const handleButtonClick = function (event) {
        let copyButtonClickTarget = null; // 将被认为是“复制按钮”的元素

        // 1. 优先检查新的按钮结构 (最具体)
        const newButtonElement = event.target.closest('div.CODEBLOCK_COPY_BUTTON');
        if (newButtonElement) {
            copyButtonClickTarget = newButtonElement;
            console.log('New button structure div.CODEBLOCK_COPY_BUTTON identified.');
        } else {
            // 2. 如果找不到新结构,回退到旧的按钮/图标识别逻辑
            const olderIconOrButton = event.target.closest('svg.fa-clone, svg[class*="copy"], button[class*="copy"]');
            if (olderIconOrButton) {
                // 尝试为旧结构找到可点击的父元素
                copyButtonClickTarget = olderIconOrButton.closest('div[class*="cursor-pointer"], button[class*="copy"]');
                if (copyButtonClickTarget) {
                    console.log('Older button structure identified via icon/button content and specific parent.');
                } else {
                    // 如果没有特定的可点击父元素,直接使用图标本身或其直接父级(如果它是按钮)
                    copyButtonClickTarget = olderIconOrButton.closest('button') || olderIconOrButton;
                    console.log('Older icon/button found, using it or its button parent as target.');
                }
            }
        }

        if (copyButtonClickTarget) {
            console.log('Potential LeetCode copy button interaction detected. Target:', copyButtonClickTarget);

            // 找到代码容器。这是识别按钮后最关键的部分。
            // 按钮和代码文本区域之间的关系可能会有所不同。
            let codeContainer = null;

            if (copyButtonClickTarget.classList.contains('CODEBLOCK_COPY_BUTTON')) {
                // 对于新按钮,我们需要找到其关联的代码块。
                // 策略:向上查找几层父元素,寻找常见的代码编辑器容器或 pre 标签。
                let parent = copyButtonClickTarget.parentElement;
                for (let i = 0; i < 4 && parent; i++) { // 最多检查4层父元素
                    // 查找 Monaco 编辑器, CodeMirror, 或通用的 pre 标签。
                    // LeetCode 通常用包含 'code-block' 或类似类名的 div 包装代码块。
                    // 同时检查父元素自身是否为代码区域的直接容器
                    const potentialContainer = parent.querySelector('pre, div.monaco-editor, div.react-codemirror2, div[class*="language-"], div.view-lines, div.CodeMirror-code, textarea.cm-content');
                    if (potentialContainer) {
                        codeContainer = parent; // 假设父元素是这些代码元素的容器
                        console.log('Found code container for new button by searching upwards from button parent:', codeContainer);
                        break;
                    }
                    // 检查父元素本身是否是已知的包装器 (优先级稍低)
                    if (parent.matches('[class*="code-block"], [class*="code-editor"], [class*="sample-code"], [class*="monaco-editor"], [class*="react-codemirror2"]')) {
                        codeContainer = parent;
                        console.log('Found code container for new button by matching parent class:', codeContainer);
                        break;
                    }
                    parent = parent.parentElement;
                }
                if (!codeContainer) {
                    console.warn('Could not reliably find code container for new button structure. Falling back to button\'s parent or grandparent.');
                    codeContainer = copyButtonClickTarget.parentElement?.parentElement || copyButtonClickTarget.parentElement; // 回退到按钮的父元素或祖父元素
                }
            } else {
                // 旧按钮的原始逻辑
                codeContainer = copyButtonClickTarget.closest('.group.relative, [class*="code-block"], [class*="monaco-editor-background"], .monaco-editor, .react-codemirror2');
                console.log('Attempting to find code container for older button structure:', codeContainer);
            }

            if (!codeContainer) {
                console.error('Failed to find code container for the button:', copyButtonClickTarget, 'DOM structure might have changed significantly.');
                return; // 如果找不到容器,则停止处理,让默认行为(可能被剪贴板API覆盖逻辑清理)发生
            }

            // 查找实际的代码元素
            // code:not(span>code) 避免选中行内代码片段 (如果 pre code 未找到)
            let codeElement = codeContainer.querySelector('pre code, code:not(span > code), div.view-lines, div.CodeMirror-code, textarea.cm-content');

            if (codeElement) {
                event.preventDefault();
                event.stopPropagation();
                console.log('Code element found:', codeElement);

                let pureCode = '';
                // 处理 Monaco 编辑器 (它使用 div.view-lines 和单独的行 div)
                if (codeElement.classList.contains('view-lines') || codeContainer.querySelector('div.view-lines')) {
                    const linesHost = codeContainer.querySelector('div.view-lines') || codeElement;
                    const lines = linesHost.querySelectorAll('div[class*="view-line"]'); // 更通用的类匹配
                    lines.forEach(line => {
                        pureCode += (line.textContent || line.innerText) + '\n';
                    });
                    pureCode = pureCode.replace(/\n$/, ""); // 移除末尾的换行符
                } else if (codeElement.matches('div.CodeMirror-code')) { // 处理 CodeMirror
                    const lines = codeElement.querySelectorAll('.CodeMirror-line');
                    lines.forEach(line => {
                        pureCode += (line.textContent || line.innerText) + '\n';
                    });
                    pureCode = pureCode.replace(/\n$/, "");
                } else {
                    pureCode = codeElement.textContent || codeElement.innerText;
                }

                pureCode = pureCode.trim(); // 通用清理

                if (!pureCode && codeElement.tagName === 'TEXTAREA') { // 特别处理 textarea (例如 CodeMirror 6 的 cm-content)
                    pureCode = codeElement.value;
                }

                if (!pureCode) {
                    console.warn("Extracted pure code is empty. Code element:", codeElement, "Container:", codeContainer, "Button:", copyButtonClickTarget);
                    // 如果提取的代码为空,可能意味着代码元素选择器仍需调整,
                    // 或者页面结构确实没有文本。为避免复制空内容,可以考虑不执行复制。
                    // 但目前还是尝试复制(如果为空,则剪贴板API的清理逻辑是最后的防线)
                }

                navigator.clipboard.writeText(pureCode).then(() => {
                    console.log('Pure code copied to clipboard successfully! Content snippet:', pureCode.substring(0, 100) + "...");
                    // 视觉反馈逻辑 (来自原脚本)
                    const feedbackSpan = document.createElement('span');
                    feedbackSpan.textContent = '已复制';
                    feedbackSpan.style.position = 'fixed'; // 使用 fixed 以便在滚动时也能正确定位
                    feedbackSpan.style.backgroundColor = '#4CAF50';
                    feedbackSpan.style.color = 'white';
                    feedbackSpan.style.padding = '3px 6px';
                    feedbackSpan.style.borderRadius = '3px';
                    feedbackSpan.style.fontSize = '12px';
                    feedbackSpan.style.zIndex = '9999';
                    feedbackSpan.style.pointerEvents = 'none'; // 避免反馈元素自身拦截鼠标事件
                    feedbackSpan.style.opacity = '0.9';
                    feedbackSpan.style.transition = 'opacity 0.5s ease-out';


                    const buttonRect = copyButtonClickTarget.getBoundingClientRect();
                    // 定位在按钮上方
                    // getBoundingClientRect 的 top/left 是相对于视口的
                    // feedbackSpan.offsetHeight 可能在元素添加到DOM之前不准确,但这里通常可以接受
                    let topPosition = buttonRect.top - (feedbackSpan.offsetHeight || 20) - 5; // 减去估算的高度和一些间距
                    let leftPosition = buttonRect.left + (buttonRect.width / 2) - (feedbackSpan.offsetWidth / 2 || 20); // 按钮中心

                    // 确保反馈在视口内
                    topPosition = Math.max(5, topPosition); // 至少离顶部5px
                    leftPosition = Math.max(5, Math.min(leftPosition, window.innerWidth - (feedbackSpan.offsetWidth || 40) - 5));


                    feedbackSpan.style.top = `${topPosition}px`;
                    feedbackSpan.style.left = `${leftPosition}px`;

                    document.body.appendChild(feedbackSpan);
                    setTimeout(() => {
                        feedbackSpan.style.opacity = '0';
                        setTimeout(() => feedbackSpan.remove(), 500);
                    }, 1500);
                }).catch(err => {
                    console.error('Failed to copy pure code: ', err);
                    alert('复制代码失败,请尝试手动选中复制。\n错误信息: ' + err.message);
                });

                return false; // 确保事件不继续传播
            } else {
                console.error('Could not find the code element within the container:', codeContainer, 'for button:', copyButtonClickTarget);
            }
        }
        // 如果不是已识别的复制按钮,或者逻辑未能找到元素,则让事件继续传播。
        // navigator.clipboard.writeText 的覆盖逻辑将是最后的防线。
        return true;
    };

    // 使用事件委托监听所有点击事件,采用捕获阶段
    document.addEventListener('click', handleButtonClick, true);

    // 使用 MutationObserver 监听 DOM 变化,处理动态加载的内容
    const observer = new MutationObserver(debounce(function (mutations) {
        // 检查是否有新的代码块被添加
        for (const mutation of mutations) {
            if (mutation.type === 'childList' && mutation.addedNodes.length) {
                // DOM 变化,可能需要重新检查按钮
                console.log('DOM changed, looking for new copy buttons');
            }
        }
    }, 200));

    // 开始监听整个文档的变化
    observer.observe(document.documentElement, {
        childList: true,
        subtree: true
    });

})();