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