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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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
    });

})();