文本网页自由复制-Markdown

自由选择网页区域并复制为 Markdown 格式

// ==UserScript==
// @name         文本网页自由复制-Markdown
// @namespace    http://tampermonkey.net/
// @version      2.0.0
// @description  自由选择网页区域并复制为 Markdown 格式
// @author       shenfangda (enhanced by Claude & community input)
// @match        *://*/*
// @exclude      https://accounts.google.com/*
// @exclude      https://*.google.com/sorry/*
// @exclude      https://mail.google.com/*
// @exclude      /^https?:\/\/localhost[:/]/
// @exclude      /^file:\/\//
// @grant        GM_setClipboard
// @license      MIT
// @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzRDQUY1MCIgd2lkdGg9IjQ4cHgiIGhlaWdodD0iNDhweCI+PHBhdGggZD0iTTAgMGgyNHYyNEgwVjB6IiBmaWxsPSJub25lIi8+PHBhdGggZD0iTTIxIDNINWMtMS4xIDAtMiAuOS0yIDJ2MTRjMCAxLjEuOSAyIDIgMmgxNGMxLjEgMCAyLS45IDItMlY1YzAtMS4xLS45LTItMi0yem0tOSAyaDZ2MkgxMlY1em0wIDRoNnYySDEydjloLTJ2LTJIMTBWN2gydjJ6bS03IDRoMlY3SDVWMTFoMlY5em0wIDRoMnYySDV2LTJ6bTEyLTYuNWMyLjQ5IDAgNC41IDIuMDEgNC41IDQuNXM LTIuMDEgNC41LTQuNSA0LjUtNC41LTIuMDEtNC41LTQuNSAyLjAxLTQuNSA0LjUtNC41eiIvPjwvc3ZnPg==
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration ---
    const BUTTON_TEXT_DEFAULT = 'Copy Markdown';
    const BUTTON_TEXT_SELECTING_FREE = 'Selecting Area... (ESC to cancel)';
    const BUTTON_TEXT_SELECTING_DIV = 'Click DIV to Copy (ESC to cancel)';
    const BUTTON_TEXT_COPIED = 'Copied!';
    const BUTTON_TEXT_FAILED = 'Copy Failed!';
    const TEMP_MESSAGE_DURATION = 2000; // ms
    const DEBUG = false; // Set to true for more verbose logging

    // --- Logging ---
    const log = (msg) => console.log(`[Markdown-Copy] ${msg}`);
    const debugLog = (msg) => DEBUG && console.log(`[Markdown-Copy Debug] ${msg}`);

    // --- State ---
    let isSelecting = false;
    let isDivMode = false;
    let startX, startY;
    let selectionBox = null;
    let highlightedDiv = null;
    let copyBtn = null;
    let originalButtonText = BUTTON_TEXT_DEFAULT;
    let messageTimeout = null;

    // --- DOM Ready Check ---
    function onDOMReady(callback) {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', callback);
        } else {
            // DOMContentLoaded already fired or interactive/complete
            callback();
        }
    }

    // --- Main Initialization ---
    function initScript() {
        log(`Attempting init on ${window.location.href}`);

        // Avoid running in frames or if body/head not present
        if (window.self !== window.top) {
            log('Script is running in an iframe, aborting.');
            return;
        }
        if (!document.body || !document.head) {
            log('Error: document.body or document.head not found. Retrying...');
            setTimeout(initScript, 500); // Retry after a short delay
            return;
        }

        log('DOM ready, initializing script.');

        // Inject CSS
        injectStyles();

        // Create and add the button
        if (!createButton()) return; // Stop if button creation fails

        // Add core event listeners
        setupEventListeners();

        log('Initialization complete.');
    }

    // --- CSS Injection ---
    function injectStyles() {
        const STYLES = `
            .markdown-copy-btn {
                position: fixed;
                top: 15px;
                right: 15px;
                z-index: 2147483646; /* Max z-index - 1 */
                padding: 8px 14px;
                background-color: #4CAF50;
                color: white;
                border: none;
                border-radius: 5px;
                cursor: pointer;
                font-size: 13px;
                font-family: sans-serif;
                box-shadow: 0 2px 5px rgba(0,0,0,0.2);
                transition: all 0.2s ease-in-out;
                line-height: 1.4;
                text-align: center;
            }
            .markdown-copy-btn:hover {
                background-color: #45a049;
                transform: translateY(-1px);
                box-shadow: 0 4px 8px rgba(0,0,0,0.25);
            }
            .markdown-copy-btn.mc-copied { background-color: #3a8f40; }
            .markdown-copy-btn.mc-failed { background-color: #c0392b; }
            .markdown-copy-selection-box {
                position: absolute;
                border: 2px dashed #4CAF50;
                background-color: rgba(76, 175, 80, 0.1);
                z-index: 2147483645; /* Max z-index - 2 */
                pointer-events: none; /* Allow clicks to pass through */
                box-sizing: border-box;
            }
            .markdown-copy-div-highlight {
                outline: 2px solid #4CAF50 !important;
                background-color: rgba(76, 175, 80, 0.1) !important;
                box-shadow: inset 0 0 0 2px rgba(76, 175, 80, 0.5) !important;
                transition: all 0.1s ease-in-out;
                cursor: pointer;
            }
        `;
        try {
            const styleSheet = document.createElement('style');
            styleSheet.id = 'markdown-copy-styles';
            styleSheet.textContent = STYLES;
            document.head.appendChild(styleSheet);
            debugLog('Styles injected.');
        } catch (error) {
            log(`Error injecting styles: ${error.message}`);
        }
    }

    // --- Button Creation ---
    function createButton() {
        if (document.getElementById('markdown-copy-btn-main')) {
             log('Button already exists.');
             copyBtn = document.getElementById('markdown-copy-btn-main'); // Ensure reference is set
             return true; // Button already exists
        }
        try {
            copyBtn = document.createElement('button');
            copyBtn.id = 'markdown-copy-btn-main';
            copyBtn.className = 'markdown-copy-btn';
            copyBtn.textContent = BUTTON_TEXT_DEFAULT;
            originalButtonText = BUTTON_TEXT_DEFAULT; // Store initial text
            document.body.appendChild(copyBtn);
            debugLog('Button created and added.');
            return true;
        } catch (error) {
            log(`Error creating button: ${error.message}`);
            return false;
        }
    }

    // --- Event Listeners Setup ---
    function setupEventListeners() {
        if (!copyBtn) {
            log("Error: Button not found for adding listeners.");
            return;
        }

        // Button click toggles selection modes
        copyBtn.addEventListener('click', handleButtonClick);

        // Mouse events for free selection
        document.addEventListener('mousedown', handleMouseDown, true); // Use capture phase
        document.addEventListener('mousemove', handleMouseMove, true);
        document.addEventListener('mouseup', handleMouseUp, true);

        // Mouse events for DIV selection
        document.addEventListener('mouseover', handleMouseOverDiv);
        document.addEventListener('click', handleClickDiv, true); // Use capture phase for potential preventDefault

        // Keyboard listener for ESC key
        document.addEventListener('keydown', handleKeyDown);

        debugLog('Event listeners added.');
    }

    // --- Button Click Logic ---
    function handleButtonClick(e) {
        e.stopPropagation(); // Prevent triggering other click listeners

        if (!isSelecting) {
            // Start selection - cycle through modes (Off -> Div -> Free -> Off)
            if (!isDivMode) { // Currently Off, switch to Div mode
                 isSelecting = true;
                 isDivMode = true;
                 setButtonState(BUTTON_TEXT_SELECTING_DIV);
                 document.body.style.cursor = 'pointer';
                 log('Entered Div Selection Mode.');
            }
             // Note: We'll implicitly switch from Div to Free in the next click if needed
        } else if (isDivMode) {
             // Currently in Div mode, switch to Free Select mode
             isDivMode = false;
             setButtonState(BUTTON_TEXT_SELECTING_FREE);
             document.body.style.cursor = 'crosshair';
             log('Switched to Free Selection Mode.');
             // Remove any lingering div highlight
             removeDivHighlight();
        } else {
            // Currently in Free mode, cancel selection
            resetSelectionState();
            log('Selection cancelled by button click.');
        }
    }

    // --- Free Selection Handlers ---
    function handleMouseDown(e) {
        // Only act if in Free Select mode and not clicking the button itself
        if (!isSelecting || isDivMode || e.target === copyBtn || copyBtn.contains(e.target)) return;

        // Prevent default text selection behavior during drag
        e.preventDefault();
        e.stopPropagation();

        startX = e.clientX + window.scrollX;
        startY = e.clientY + window.scrollY;

        // Create or reset selection box
        if (!selectionBox) {
            selectionBox = document.createElement('div');
            selectionBox.className = 'markdown-copy-selection-box';
            document.body.appendChild(selectionBox);
        }
        selectionBox.style.left = `${startX}px`;
        selectionBox.style.top = `${startY}px`;
        selectionBox.style.width = '0px';
        selectionBox.style.height = '0px';
        selectionBox.style.display = 'block'; // Make sure it's visible

        debugLog(`Free selection started at (${startX}, ${startY})`);
    }

    function handleMouseMove(e) {
        if (!isSelecting || isDivMode || !selectionBox || !startX) return; // Need startX to confirm drag started

        // No preventDefault here - allows scrolling while dragging if needed
        e.stopPropagation();

        const currentX = e.clientX + window.scrollX;
        const currentY = e.clientY + window.scrollY;

        const left = Math.min(startX, currentX);
        const top = Math.min(startY, currentY);
        const width = Math.abs(currentX - startX);
        const height = Math.abs(currentY - startY);

        selectionBox.style.left = `${left}px`;
        selectionBox.style.top = `${top}px`;
        selectionBox.style.width = `${width}px`;
        selectionBox.style.height = `${height}px`;
    }

    function handleMouseUp(e) {
        if (!isSelecting || isDivMode || !selectionBox || !startX) return; // Check if a drag was actually happening
        e.stopPropagation(); // Important to stop propagation here

        const endX = e.clientX + window.scrollX;
        const endY = e.clientY + window.scrollY;
        const width = Math.abs(endX - startX);
        const height = Math.abs(endY - startY);

        debugLog(`Free selection ended at (${endX}, ${endY}), Size: ${width}x${height}`);

        // Only copy if the box has a reasonable size (prevent accidental clicks)
        if (width > 5 && height > 5) {
            const markdownContent = getSelectedContentFromArea(startX, startY, endX, endY);
            handleCopyAttempt(markdownContent, "Free Selection");
        } else {
            log("Selection box too small, ignoring.");
        }

        // Reset state *after* potential copy
        resetSelectionState();
    }

    // --- Div Selection Handlers ---
    function handleMouseOverDiv(e) {
        if (!isSelecting || !isDivMode || e.target === copyBtn || copyBtn.contains(e.target)) return;

        // Find the closest DIV that isn't the button itself or body/html
        const target = e.target.closest('div:not(.markdown-copy-btn)');

        if (target && target !== document.body && target !== document.documentElement) {
            if (highlightedDiv && highlightedDiv !== target) {
                removeDivHighlight();
            }
            if (highlightedDiv !== target) {
                 highlightedDiv = target;
                 highlightedDiv.classList.add('markdown-copy-div-highlight');
                 debugLog(`Highlighting Div: ${target.tagName}#${target.id}.${target.className.split(' ').join('.')}`);
            }
        } else {
             // If hovering over something not in a suitable div, remove highlight
            removeDivHighlight();
        }
    }

     function handleClickDiv(e) {
        if (!isSelecting || !isDivMode || e.target === copyBtn || copyBtn.contains(e.target)) return;

        // Check if the click was on the currently highlighted div
        const targetDiv = e.target.closest('.markdown-copy-div-highlight');

        if (targetDiv && targetDiv === highlightedDiv) {
            // Prevent the click from triggering other actions on the page (like navigation)
            e.preventDefault();
            e.stopPropagation();

            log(`Div clicked: ${targetDiv.tagName}#${targetDiv.id}.${targetDiv.className.split(' ').join('.')}`);
            const markdownContent = htmlToMarkdown(targetDiv);
            handleCopyAttempt(markdownContent, "Div Selection");
            resetSelectionState(); // Reset after successful click/copy
        }
         // If clicked outside the highlighted div, do nothing, let the click proceed normally
         // unless it hits another potential div, handled by mouseover->highlight->next click
    }


    // --- Content Extraction ---

    /**
     * Tries to get Markdown content from the center of a selected area.
     * This is an approximation and might not capture everything perfectly.
     */
    function getSelectedContentFromArea(x1, y1, x2, y2) {
        const centerX = window.scrollX + (Math.min(x1, x2) + Math.abs(x1 - x2) / 2 - window.scrollX);
        const centerY = window.scrollY + (Math.min(y1, y2) + Math.abs(y1 - y2) / 2 - window.scrollY);
        debugLog(`Checking elements at center point (${centerX}, ${centerY})`);

        try {
            const elements = document.elementsFromPoint(centerX, centerY);
            if (!elements || elements.length === 0) {
                log("No elements found at center point.");
                return '';
            }

            // Find the most relevant element (skip body, html, overlays, button)
            const meaningfulElement = elements.find(el =>
                el &&
                el.tagName?.toLowerCase() !== 'body' &&
                el.tagName?.toLowerCase() !== 'html' &&
                !el.classList.contains('markdown-copy-selection-box') &&
                !el.classList.contains('markdown-copy-btn') &&
                window.getComputedStyle(el).display !== 'none' &&
                window.getComputedStyle(el).visibility !== 'hidden' &&
                 // Prefer elements with some size or specific tags
                 (el.offsetWidth > 20 || el.offsetHeight > 10 || ['p', 'div', 'article', 'section', 'main', 'ul', 'ol', 'table', 'pre'].includes(el.tagName?.toLowerCase()))
            );


            if (meaningfulElement) {
                log(`Selected element via area center: ${meaningfulElement.tagName}`);
                 debugLog(meaningfulElement.outerHTML.substring(0, 100) + '...');
                return htmlToMarkdown(meaningfulElement);
            } else {
                log("Could not find a meaningful element at the center point.");
                 // Fallback: try the top-most element that isn't the script's stuff
                const fallbackElement = elements.find(el =>
                    el &&
                    !el.classList.contains('markdown-copy-selection-box') &&
                    !el.classList.contains('markdown-copy-btn'));
                 if(fallbackElement){
                     log(`Using fallback element: ${fallbackElement.tagName}`);
                     return htmlToMarkdown(fallbackElement);
                 }
            }
        } catch (error) {
            log(`Error in getSelectedContentFromArea: ${error.message}`);
        }
        return '';
    }

    // --- HTML to Markdown Conversion --- (Enhanced)
    function htmlToMarkdown(element) {
        if (!element) return '';

        let markdown = '';

        // Function to recursively process nodes
        function processNode(node, listLevel = 0, listType = '') {
            if (node.nodeType === Node.TEXT_NODE) {
                // Replace multiple spaces/newlines with single space, unless in <pre>
                const parentTag = node.parentNode?.tagName?.toLowerCase();
                 if (parentTag === 'pre' || node.parentNode?.closest('pre')) {
                     return node.textContent || ''; // Preserve whitespace in pre
                 }
                 let text = node.textContent || '';
                 text = text.replace(/\s+/g, ' '); // Consolidate whitespace
                return text;
            }

            if (node.nodeType !== Node.ELEMENT_NODE) {
                return ''; // Ignore comments, etc.
            }

            // Ignore script, style, noscript, etc.
            if (['script', 'style', 'noscript', 'button', 'textarea', 'input', 'select', 'option'].includes(node.tagName.toLowerCase())) {
                return '';
            }
            // Ignore the script's own elements
            if (node.classList.contains('markdown-copy-btn') || node.classList.contains('markdown-copy-selection-box')) {
                return '';
            }

            let prefix = '';
            let suffix = '';
            let content = '';
            const tag = node.tagName.toLowerCase();
            const isBlock = window.getComputedStyle(node).display === 'block' || ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'pre', 'blockquote', 'hr', 'table', 'tr'].includes(tag);

             // Process children first for most tags
             for (const child of node.childNodes) {
                content += processNode(child, listLevel + (tag === 'ul' || tag === 'ol' ? 1 : 0), (tag === 'ul' || tag === 'ol' ? tag : listType));
            }
             content = content.trim(); // Trim internal content


            switch (tag) {
                case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
                    prefix = '#'.repeat(parseInt(tag[1])) + ' ';
                    suffix = '\n\n';
                    break;
                case 'p':
                     // Avoid adding extra newlines if content is empty or already ends with them
                     if (content) suffix = '\n\n';
                    break;
                case 'strong': case 'b':
                    if (content) prefix = '**', suffix = '**';
                    break;
                case 'em': case 'i':
                    if (content) prefix = '*', suffix = '*';
                    break;
                 case 'code':
                     // Handle inline code vs code block (inside pre)
                     if (node.closest('pre')) {
                         // Handled by 'pre' case, just return content
                         prefix = '', suffix = '';
                     } else {
                         if (content) prefix = '`', suffix = '`';
                     }
                    break;
                case 'a':
                    const href = node.getAttribute('href');
                    if (content && href) {
                         // Handle relative URLs
                        const absoluteHref = new URL(href, window.location.href).href;
                        prefix = '[';
                        suffix = `](${absoluteHref})`;
                    } else {
                         // If link has no content but has href, just output URL maybe?
                         // Or just skip it. Let's skip.
                         prefix = ''; suffix = ''; content = '';
                    }
                    break;
                case 'img':
                    const src = node.getAttribute('src');
                    const alt = node.getAttribute('alt') || '';
                    if (src) {
                        const absoluteSrc = new URL(src, window.location.href).href;
                        // Render as block element
                        prefix = `![${alt}](${absoluteSrc})`;
                        suffix = '\n\n';
                         content = ''; // No content for images
                    }
                    break;
                case 'ul':
                case 'ol':
                     // Handled by child 'li' elements, add final newline if content exists
                     if (content) suffix = '\n\n';
                     else suffix = ''; // Avoid extra space if list empty
                     prefix = ''; content = ''; // Content aggregation is done in children
                     // Need to re-process children with list context here
                     for (const child of node.children) {
                         if (child.tagName.toLowerCase() === 'li') {
                             content += processNode(child, listLevel + 1, tag);
                         }
                     }
                     content = content.trimEnd(); // Remove trailing newline from last li
                    break;
                case 'li':
                    const indent = '  '.repeat(Math.max(0, listLevel - 1));
                    prefix = indent + (listType === 'ol' ? '1. ' : '- '); // Simple numbering for ol
                     // Add newline, unless it's the last item handled by parent ul/ol
                    suffix = '\n';
                    break;
                case 'blockquote':
                    // Add > prefix to each line
                    content = content.split('\n').map(line => '> ' + line).join('\n');
                     prefix = '';
                     suffix = '\n\n';
                    break;
                 case 'pre':
                     let codeContent = node.textContent || ''; // Get raw text content
                     let lang = '';
                     // Try to find language from class="language-..." on pre or inner code
                     const codeElement = node.querySelector('code[class*="language-"]');
                     const langClass = codeElement?.className.match(/language-(\S+)/);
                     if (langClass) {
                         lang = langClass[1];
                     } else {
                          const preLangClass = node.className.match(/language-(\S+)/);
                           if (preLangClass) lang = preLangClass[1];
                     }
                    prefix = '```' + lang + '\n';
                    suffix = '\n```\n\n';
                    content = codeContent.trim(); // Trim overall whitespace but preserve internal
                    break;
                 case 'hr':
                     prefix = '---';
                     suffix = '\n\n';
                     content = ''; // No content
                    break;
                 case 'table':
                     // Basic table support
                     let header = '';
                     let separator = '';
                     let body = '';
                     const rows = Array.from(node.querySelectorAll(':scope > thead > tr, :scope > tbody > tr, :scope > tr')); // More robust row finding
                     let firstRow = true;

                     for (const row of rows) {
                         let cols = [];
                         const cells = Array.from(row.querySelectorAll(':scope > th, :scope > td'));
                         cols = cells.map(cell => processNode(cell).replace(/\|/g, '\\|').trim()); // Escape pipes

                         if (firstRow && row.querySelector('th')) { // Assume header if first row has <th>
                             header = `| ${cols.join(' | ')} |`;
                             separator = `| ${cols.map(() => '---').join(' | ')} |`;
                             firstRow = false;
                         } else {
                             body += `| ${cols.join(' | ')} |\n`;
                         }
                     }
                     // Assemble table only if we found some structure
                     if (header && separator && body) {
                         prefix = header + '\n' + separator + '\n';
                         content = body.trim();
                         suffix = '\n\n';
                     } else if (body) { // Table with no header
                         prefix = '';
                         content = body.trim();
                         suffix = '\n\n';
                     }
                     else { // No meaningful table content
                         prefix = ''; content = ''; suffix = '';
                     }
                     break;
                case 'br':
                    // Add double space for line break within paragraphs, or newline otherwise
                     const parentDisplay = node.parentNode ? window.getComputedStyle(node.parentNode).display : 'block';
                     if(parentDisplay !== 'block'){
                         prefix = '  \n'; // Markdown line break
                     } else {
                          prefix = '\n'; // Treat as paragraph break if parent is block
                     }
                    content = ''; suffix = '';
                    break;

                // Default: block elements add newlines, inline elements don't
                 case 'div': case 'section': case 'article': case 'main': case 'header': case 'footer': case 'aside':
                     // Add newlines only if content exists and doesn't already end with plenty
                     if (content && !content.endsWith('\n\n')) {
                         suffix = '\n\n';
                     }
                     break;

                default:
                    // For other inline elements, just pass content through
                    // For unrecognized block elements, add spacing if needed
                     if (isBlock && content && !content.endsWith('\n\n')) {
                         suffix = '\n\n';
                     }
                    break;
            }

             // Combine prefix, content, suffix. Trim whitespace around the final result for this node.
             let result = prefix + content + suffix;

              // Add spacing between block elements if needed
              if (isBlock && markdown.length > 0 && !markdown.endsWith('\n\n') && !result.startsWith('\n')) {
                 // Ensure there's a blank line separating block elements
                  if (!markdown.endsWith('\n')) markdown += '\n';
                  markdown += '\n';
              } else if (!isBlock && markdown.length > 0 && !markdown.endsWith(' ') && !markdown.endsWith('\n') && !result.startsWith(' ') && !result.startsWith('\n')) {
                  // Add a space between inline elements if needed
                  markdown += ' ';
              }

            markdown += result;
            return result; // Return the result for recursive calls

        } // End of processNode

         try {
            // Start processing from the root element provided
            let rawMd = processNode(element);

            // Final cleanup: consolidate multiple blank lines into one
            rawMd = rawMd.replace(/\n{3,}/g, '\n\n');
            return rawMd.trim(); // Trim final result

         } catch (error) {
              log(`Error during Markdown conversion: ${error.message}`);
              return element.innerText || ''; // Fallback to innerText on error
         }

    } // End of htmlToMarkdown


    // --- Clipboard & UI Feedback ---
    function handleCopyAttempt(markdownContent, sourceType) {
        if (markdownContent && markdownContent.trim().length > 0) {
            try {
                GM_setClipboard(markdownContent);
                log(`${sourceType}: Markdown copied successfully! (Length: ${markdownContent.length})`);
                showTemporaryMessage(BUTTON_TEXT_COPIED, false);
            } catch (err) {
                log(`${sourceType}: Copy failed: ${err.message}`);
                showTemporaryMessage(BUTTON_TEXT_FAILED, true);
                console.error("Clipboard copy error:", err);
            }
        } else {
            log(`${sourceType}: No valid content detected to copy.`);
            showTemporaryMessage(BUTTON_TEXT_FAILED, true); // Indicate failure if nothing was found
        }
    }

    function showTemporaryMessage(text, isError) {
        if (!copyBtn) return;
        clearTimeout(messageTimeout); // Clear previous timeout if any

        copyBtn.textContent = text;
        copyBtn.classList.toggle('mc-copied', !isError);
        copyBtn.classList.toggle('mc-failed', isError);


        messageTimeout = setTimeout(() => {
            setButtonState(BUTTON_TEXT_DEFAULT); // Restore default state
            copyBtn.classList.remove('mc-copied', 'mc-failed');
        }, TEMP_MESSAGE_DURATION);
    }

     function setButtonState(text) {
         if (!copyBtn) return;
         copyBtn.textContent = text;
         // Clear temporary states if setting back to a standard message
         if (text !== BUTTON_TEXT_COPIED && text !== BUTTON_TEXT_FAILED) {
            copyBtn.classList.remove('mc-copied', 'mc-failed');
            clearTimeout(messageTimeout); // Clear any pending message reset
         }
          // Store the original text if setting to default
          if(text === BUTTON_TEXT_DEFAULT) {
               originalButtonText = text;
          }
     }


    // --- State Management & Cleanup ---
    function removeDivHighlight() {
        if (highlightedDiv) {
            highlightedDiv.classList.remove('markdown-copy-div-highlight');
            highlightedDiv = null;
            debugLog('Div highlight removed.');
        }
    }

    function removeSelectionBox() {
        if (selectionBox) {
            selectionBox.style.display = 'none'; // Hide instead of removing immediately
             // Consider removing after a short delay if needed:
             // setTimeout(() => { if (selectionBox) selectionBox.remove(); selectionBox = null; }, 50);
            debugLog('Selection box hidden.');
             // Reset start coords to prevent mouseup from triggering after cancellation
             startX = null;
             startY = null;
        }
    }

    function resetSelectionState() {
        isSelecting = false;
        isDivMode = false; // Always reset to off state
        document.body.style.cursor = 'default';
        setButtonState(originalButtonText); // Restore original or default text
        removeSelectionBox();
        removeDivHighlight();
        log('Selection state reset.');
    }

    function handleKeyDown(e) {
        if (e.key === 'Escape' && isSelecting) {
            log('Escape key pressed, cancelling selection.');
            resetSelectionState();
        }
    }

    // --- Script Entry Point ---
    onDOMReady(() => {
        // Small delay to let dynamic pages potentially load more content
        setTimeout(() => {
             try {
                initScript();
            } catch (err) {
                log(`Critical error during script initialization: ${err.message}`);
                console.error(err);
            }
        }, 100); // Wait 100ms after DOM ready
    });

})();

QingJ © 2025

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