// ==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== // @downloadURL https://update.greasyfork.cloud/scripts/529449/%E6%96%87%E6%9C%AC%E7%BD%91%E9%A1%B5%E8%87%AA%E7%94%B1%E5%A4%8D%E5%88%B6-Markdown.user.js // @updateURL https://update.greasyfork.cloud/scripts/529449/%E6%96%87%E6%9C%AC%E7%BD%91%E9%A1%B5%E8%87%AA%E7%94%B1%E5%A4%8D%E5%88%B6-Markdown.meta.js // ==/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
                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 
                             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
    });

})();