文本网页自由复制-Markdown

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

  1. // ==UserScript==
  2. // @name 文本网页自由复制-Markdown
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.0.0
  5. // @description 自由选择网页区域并复制为 Markdown 格式
  6. // @author shenfangda (enhanced by Claude & community input)
  7. // @match *://*/*
  8. // @exclude https://accounts.google.com/*
  9. // @exclude https://*.google.com/sorry/*
  10. // @exclude https://mail.google.com/*
  11. // @exclude /^https?:\/\/localhost[:/]/
  12. // @exclude /^file:\/\//
  13. // @grant GM_setClipboard
  14. // @license MIT
  15. // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzRDQUY1MCIgd2lkdGg9IjQ4cHgiIGhlaWdodD0iNDhweCI+PHBhdGggZD0iTTAgMGgyNHYyNEgwVjB6IiBmaWxsPSJub25lIi8+PHBhdGggZD0iTTIxIDNINWMtMS4xIDAtMiAuOS0yIDJ2MTRjMCAxLjEuOSAyIDIgMmgxNGMxLjEgMCAyLS45IDItMlY1YzAtMS4xLS45LTItMi0yem0tOSAyaDZ2MkgxMlY1em0wIDRoNnYySDEydjloLTJ2LTJIMTBWN2gydjJ6bS03IDRoMlY3SDVWMTFoMlY5em0wIDRoMnYySDV2LTJ6bTEyLTYuNWMyLjQ5IDAgNC41IDIuMDEgNC41IDQuNXM LTIuMDEgNC41LTQuNSA0LjUtNC41LTIuMDEtNC41LTQuNSAyLjAxLTQuNSA0LjUtNC41eiIvPjwvc3ZnPg==
  16. // ==/UserScript==
  17.  
  18. (function() {
  19. 'use strict';
  20.  
  21. // --- Configuration ---
  22. const BUTTON_TEXT_DEFAULT = 'Copy Markdown';
  23. const BUTTON_TEXT_SELECTING_FREE = 'Selecting Area... (ESC to cancel)';
  24. const BUTTON_TEXT_SELECTING_DIV = 'Click DIV to Copy (ESC to cancel)';
  25. const BUTTON_TEXT_COPIED = 'Copied!';
  26. const BUTTON_TEXT_FAILED = 'Copy Failed!';
  27. const TEMP_MESSAGE_DURATION = 2000; // ms
  28. const DEBUG = false; // Set to true for more verbose logging
  29.  
  30. // --- Logging ---
  31. const log = (msg) => console.log(`[Markdown-Copy] ${msg}`);
  32. const debugLog = (msg) => DEBUG && console.log(`[Markdown-Copy Debug] ${msg}`);
  33.  
  34. // --- State ---
  35. let isSelecting = false;
  36. let isDivMode = false;
  37. let startX, startY;
  38. let selectionBox = null;
  39. let highlightedDiv = null;
  40. let copyBtn = null;
  41. let originalButtonText = BUTTON_TEXT_DEFAULT;
  42. let messageTimeout = null;
  43.  
  44. // --- DOM Ready Check ---
  45. function onDOMReady(callback) {
  46. if (document.readyState === 'loading') {
  47. document.addEventListener('DOMContentLoaded', callback);
  48. } else {
  49. // DOMContentLoaded already fired or interactive/complete
  50. callback();
  51. }
  52. }
  53.  
  54. // --- Main Initialization ---
  55. function initScript() {
  56. log(`Attempting init on ${window.location.href}`);
  57.  
  58. // Avoid running in frames or if body/head not present
  59. if (window.self !== window.top) {
  60. log('Script is running in an iframe, aborting.');
  61. return;
  62. }
  63. if (!document.body || !document.head) {
  64. log('Error: document.body or document.head not found. Retrying...');
  65. setTimeout(initScript, 500); // Retry after a short delay
  66. return;
  67. }
  68.  
  69. log('DOM ready, initializing script.');
  70.  
  71. // Inject CSS
  72. injectStyles();
  73.  
  74. // Create and add the button
  75. if (!createButton()) return; // Stop if button creation fails
  76.  
  77. // Add core event listeners
  78. setupEventListeners();
  79.  
  80. log('Initialization complete.');
  81. }
  82.  
  83. // --- CSS Injection ---
  84. function injectStyles() {
  85. const STYLES = `
  86. .markdown-copy-btn {
  87. position: fixed;
  88. top: 15px;
  89. right: 15px;
  90. z-index: 2147483646; /* Max z-index - 1 */
  91. padding: 8px 14px;
  92. background-color: #4CAF50;
  93. color: white;
  94. border: none;
  95. border-radius: 5px;
  96. cursor: pointer;
  97. font-size: 13px;
  98. font-family: sans-serif;
  99. box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  100. transition: all 0.2s ease-in-out;
  101. line-height: 1.4;
  102. text-align: center;
  103. }
  104. .markdown-copy-btn:hover {
  105. background-color: #45a049;
  106. transform: translateY(-1px);
  107. box-shadow: 0 4px 8px rgba(0,0,0,0.25);
  108. }
  109. .markdown-copy-btn.mc-copied { background-color: #3a8f40; }
  110. .markdown-copy-btn.mc-failed { background-color: #c0392b; }
  111. .markdown-copy-selection-box {
  112. position: absolute;
  113. border: 2px dashed #4CAF50;
  114. background-color: rgba(76, 175, 80, 0.1);
  115. z-index: 2147483645; /* Max z-index - 2 */
  116. pointer-events: none; /* Allow clicks to pass through */
  117. box-sizing: border-box;
  118. }
  119. .markdown-copy-div-highlight {
  120. outline: 2px solid #4CAF50 !important;
  121. background-color: rgba(76, 175, 80, 0.1) !important;
  122. box-shadow: inset 0 0 0 2px rgba(76, 175, 80, 0.5) !important;
  123. transition: all 0.1s ease-in-out;
  124. cursor: pointer;
  125. }
  126. `;
  127. try {
  128. const styleSheet = document.createElement('style');
  129. styleSheet.id = 'markdown-copy-styles';
  130. styleSheet.textContent = STYLES;
  131. document.head.appendChild(styleSheet);
  132. debugLog('Styles injected.');
  133. } catch (error) {
  134. log(`Error injecting styles: ${error.message}`);
  135. }
  136. }
  137.  
  138. // --- Button Creation ---
  139. function createButton() {
  140. if (document.getElementById('markdown-copy-btn-main')) {
  141. log('Button already exists.');
  142. copyBtn = document.getElementById('markdown-copy-btn-main'); // Ensure reference is set
  143. return true; // Button already exists
  144. }
  145. try {
  146. copyBtn = document.createElement('button');
  147. copyBtn.id = 'markdown-copy-btn-main';
  148. copyBtn.className = 'markdown-copy-btn';
  149. copyBtn.textContent = BUTTON_TEXT_DEFAULT;
  150. originalButtonText = BUTTON_TEXT_DEFAULT; // Store initial text
  151. document.body.appendChild(copyBtn);
  152. debugLog('Button created and added.');
  153. return true;
  154. } catch (error) {
  155. log(`Error creating button: ${error.message}`);
  156. return false;
  157. }
  158. }
  159.  
  160. // --- Event Listeners Setup ---
  161. function setupEventListeners() {
  162. if (!copyBtn) {
  163. log("Error: Button not found for adding listeners.");
  164. return;
  165. }
  166.  
  167. // Button click toggles selection modes
  168. copyBtn.addEventListener('click', handleButtonClick);
  169.  
  170. // Mouse events for free selection
  171. document.addEventListener('mousedown', handleMouseDown, true); // Use capture phase
  172. document.addEventListener('mousemove', handleMouseMove, true);
  173. document.addEventListener('mouseup', handleMouseUp, true);
  174.  
  175. // Mouse events for DIV selection
  176. document.addEventListener('mouseover', handleMouseOverDiv);
  177. document.addEventListener('click', handleClickDiv, true); // Use capture phase for potential preventDefault
  178.  
  179. // Keyboard listener for ESC key
  180. document.addEventListener('keydown', handleKeyDown);
  181.  
  182. debugLog('Event listeners added.');
  183. }
  184.  
  185. // --- Button Click Logic ---
  186. function handleButtonClick(e) {
  187. e.stopPropagation(); // Prevent triggering other click listeners
  188.  
  189. if (!isSelecting) {
  190. // Start selection - cycle through modes (Off -> Div -> Free -> Off)
  191. if (!isDivMode) { // Currently Off, switch to Div mode
  192. isSelecting = true;
  193. isDivMode = true;
  194. setButtonState(BUTTON_TEXT_SELECTING_DIV);
  195. document.body.style.cursor = 'pointer';
  196. log('Entered Div Selection Mode.');
  197. }
  198. // Note: We'll implicitly switch from Div to Free in the next click if needed
  199. } else if (isDivMode) {
  200. // Currently in Div mode, switch to Free Select mode
  201. isDivMode = false;
  202. setButtonState(BUTTON_TEXT_SELECTING_FREE);
  203. document.body.style.cursor = 'crosshair';
  204. log('Switched to Free Selection Mode.');
  205. // Remove any lingering div highlight
  206. removeDivHighlight();
  207. } else {
  208. // Currently in Free mode, cancel selection
  209. resetSelectionState();
  210. log('Selection cancelled by button click.');
  211. }
  212. }
  213.  
  214. // --- Free Selection Handlers ---
  215. function handleMouseDown(e) {
  216. // Only act if in Free Select mode and not clicking the button itself
  217. if (!isSelecting || isDivMode || e.target === copyBtn || copyBtn.contains(e.target)) return;
  218.  
  219. // Prevent default text selection behavior during drag
  220. e.preventDefault();
  221. e.stopPropagation();
  222.  
  223. startX = e.clientX + window.scrollX;
  224. startY = e.clientY + window.scrollY;
  225.  
  226. // Create or reset selection box
  227. if (!selectionBox) {
  228. selectionBox = document.createElement('div');
  229. selectionBox.className = 'markdown-copy-selection-box';
  230. document.body.appendChild(selectionBox);
  231. }
  232. selectionBox.style.left = `${startX}px`;
  233. selectionBox.style.top = `${startY}px`;
  234. selectionBox.style.width = '0px';
  235. selectionBox.style.height = '0px';
  236. selectionBox.style.display = 'block'; // Make sure it's visible
  237.  
  238. debugLog(`Free selection started at (${startX}, ${startY})`);
  239. }
  240.  
  241. function handleMouseMove(e) {
  242. if (!isSelecting || isDivMode || !selectionBox || !startX) return; // Need startX to confirm drag started
  243.  
  244. // No preventDefault here - allows scrolling while dragging if needed
  245. e.stopPropagation();
  246.  
  247. const currentX = e.clientX + window.scrollX;
  248. const currentY = e.clientY + window.scrollY;
  249.  
  250. const left = Math.min(startX, currentX);
  251. const top = Math.min(startY, currentY);
  252. const width = Math.abs(currentX - startX);
  253. const height = Math.abs(currentY - startY);
  254.  
  255. selectionBox.style.left = `${left}px`;
  256. selectionBox.style.top = `${top}px`;
  257. selectionBox.style.width = `${width}px`;
  258. selectionBox.style.height = `${height}px`;
  259. }
  260.  
  261. function handleMouseUp(e) {
  262. if (!isSelecting || isDivMode || !selectionBox || !startX) return; // Check if a drag was actually happening
  263. e.stopPropagation(); // Important to stop propagation here
  264.  
  265. const endX = e.clientX + window.scrollX;
  266. const endY = e.clientY + window.scrollY;
  267. const width = Math.abs(endX - startX);
  268. const height = Math.abs(endY - startY);
  269.  
  270. debugLog(`Free selection ended at (${endX}, ${endY}), Size: ${width}x${height}`);
  271.  
  272. // Only copy if the box has a reasonable size (prevent accidental clicks)
  273. if (width > 5 && height > 5) {
  274. const markdownContent = getSelectedContentFromArea(startX, startY, endX, endY);
  275. handleCopyAttempt(markdownContent, "Free Selection");
  276. } else {
  277. log("Selection box too small, ignoring.");
  278. }
  279.  
  280. // Reset state *after* potential copy
  281. resetSelectionState();
  282. }
  283.  
  284. // --- Div Selection Handlers ---
  285. function handleMouseOverDiv(e) {
  286. if (!isSelecting || !isDivMode || e.target === copyBtn || copyBtn.contains(e.target)) return;
  287.  
  288. // Find the closest DIV that isn't the button itself or body/html
  289. const target = e.target.closest('div:not(.markdown-copy-btn)');
  290.  
  291. if (target && target !== document.body && target !== document.documentElement) {
  292. if (highlightedDiv && highlightedDiv !== target) {
  293. removeDivHighlight();
  294. }
  295. if (highlightedDiv !== target) {
  296. highlightedDiv = target;
  297. highlightedDiv.classList.add('markdown-copy-div-highlight');
  298. debugLog(`Highlighting Div: ${target.tagName}#${target.id}.${target.className.split(' ').join('.')}`);
  299. }
  300. } else {
  301. // If hovering over something not in a suitable div, remove highlight
  302. removeDivHighlight();
  303. }
  304. }
  305.  
  306. function handleClickDiv(e) {
  307. if (!isSelecting || !isDivMode || e.target === copyBtn || copyBtn.contains(e.target)) return;
  308.  
  309. // Check if the click was on the currently highlighted div
  310. const targetDiv = e.target.closest('.markdown-copy-div-highlight');
  311.  
  312. if (targetDiv && targetDiv === highlightedDiv) {
  313. // Prevent the click from triggering other actions on the page (like navigation)
  314. e.preventDefault();
  315. e.stopPropagation();
  316.  
  317. log(`Div clicked: ${targetDiv.tagName}#${targetDiv.id}.${targetDiv.className.split(' ').join('.')}`);
  318. const markdownContent = htmlToMarkdown(targetDiv);
  319. handleCopyAttempt(markdownContent, "Div Selection");
  320. resetSelectionState(); // Reset after successful click/copy
  321. }
  322. // If clicked outside the highlighted div, do nothing, let the click proceed normally
  323. // unless it hits another potential div, handled by mouseover->highlight->next click
  324. }
  325.  
  326.  
  327. // --- Content Extraction ---
  328.  
  329. /**
  330. * Tries to get Markdown content from the center of a selected area.
  331. * This is an approximation and might not capture everything perfectly.
  332. */
  333. function getSelectedContentFromArea(x1, y1, x2, y2) {
  334. const centerX = window.scrollX + (Math.min(x1, x2) + Math.abs(x1 - x2) / 2 - window.scrollX);
  335. const centerY = window.scrollY + (Math.min(y1, y2) + Math.abs(y1 - y2) / 2 - window.scrollY);
  336. debugLog(`Checking elements at center point (${centerX}, ${centerY})`);
  337.  
  338. try {
  339. const elements = document.elementsFromPoint(centerX, centerY);
  340. if (!elements || elements.length === 0) {
  341. log("No elements found at center point.");
  342. return '';
  343. }
  344.  
  345. // Find the most relevant element (skip body, html, overlays, button)
  346. const meaningfulElement = elements.find(el =>
  347. el &&
  348. el.tagName?.toLowerCase() !== 'body' &&
  349. el.tagName?.toLowerCase() !== 'html' &&
  350. !el.classList.contains('markdown-copy-selection-box') &&
  351. !el.classList.contains('markdown-copy-btn') &&
  352. window.getComputedStyle(el).display !== 'none' &&
  353. window.getComputedStyle(el).visibility !== 'hidden' &&
  354. // Prefer elements with some size or specific tags
  355. (el.offsetWidth > 20 || el.offsetHeight > 10 || ['p', 'div', 'article', 'section', 'main', 'ul', 'ol', 'table', 'pre'].includes(el.tagName?.toLowerCase()))
  356. );
  357.  
  358.  
  359. if (meaningfulElement) {
  360. log(`Selected element via area center: ${meaningfulElement.tagName}`);
  361. debugLog(meaningfulElement.outerHTML.substring(0, 100) + '...');
  362. return htmlToMarkdown(meaningfulElement);
  363. } else {
  364. log("Could not find a meaningful element at the center point.");
  365. // Fallback: try the top-most element that isn't the script's stuff
  366. const fallbackElement = elements.find(el =>
  367. el &&
  368. !el.classList.contains('markdown-copy-selection-box') &&
  369. !el.classList.contains('markdown-copy-btn'));
  370. if(fallbackElement){
  371. log(`Using fallback element: ${fallbackElement.tagName}`);
  372. return htmlToMarkdown(fallbackElement);
  373. }
  374. }
  375. } catch (error) {
  376. log(`Error in getSelectedContentFromArea: ${error.message}`);
  377. }
  378. return '';
  379. }
  380.  
  381. // --- HTML to Markdown Conversion --- (Enhanced)
  382. function htmlToMarkdown(element) {
  383. if (!element) return '';
  384.  
  385. let markdown = '';
  386.  
  387. // Function to recursively process nodes
  388. function processNode(node, listLevel = 0, listType = '') {
  389. if (node.nodeType === Node.TEXT_NODE) {
  390. // Replace multiple spaces/newlines with single space, unless in <pre>
  391. const parentTag = node.parentNode?.tagName?.toLowerCase();
  392. if (parentTag === 'pre' || node.parentNode?.closest('pre')) {
  393. return node.textContent || ''; // Preserve whitespace in pre
  394. }
  395. let text = node.textContent || '';
  396. text = text.replace(/\s+/g, ' '); // Consolidate whitespace
  397. return text;
  398. }
  399.  
  400. if (node.nodeType !== Node.ELEMENT_NODE) {
  401. return ''; // Ignore comments, etc.
  402. }
  403.  
  404. // Ignore script, style, noscript, etc.
  405. if (['script', 'style', 'noscript', 'button', 'textarea', 'input', 'select', 'option'].includes(node.tagName.toLowerCase())) {
  406. return '';
  407. }
  408. // Ignore the script's own elements
  409. if (node.classList.contains('markdown-copy-btn') || node.classList.contains('markdown-copy-selection-box')) {
  410. return '';
  411. }
  412.  
  413. let prefix = '';
  414. let suffix = '';
  415. let content = '';
  416. const tag = node.tagName.toLowerCase();
  417. const isBlock = window.getComputedStyle(node).display === 'block' || ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'pre', 'blockquote', 'hr', 'table', 'tr'].includes(tag);
  418.  
  419. // Process children first for most tags
  420. for (const child of node.childNodes) {
  421. content += processNode(child, listLevel + (tag === 'ul' || tag === 'ol' ? 1 : 0), (tag === 'ul' || tag === 'ol' ? tag : listType));
  422. }
  423. content = content.trim(); // Trim internal content
  424.  
  425.  
  426. switch (tag) {
  427. case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
  428. prefix = '#'.repeat(parseInt(tag[1])) + ' ';
  429. suffix = '\n\n';
  430. break;
  431. case 'p':
  432. // Avoid adding extra newlines if content is empty or already ends with them
  433. if (content) suffix = '\n\n';
  434. break;
  435. case 'strong': case 'b':
  436. if (content) prefix = '**', suffix = '**';
  437. break;
  438. case 'em': case 'i':
  439. if (content) prefix = '*', suffix = '*';
  440. break;
  441. case 'code':
  442. // Handle inline code vs code block (inside pre)
  443. if (node.closest('pre')) {
  444. // Handled by 'pre' case, just return content
  445. prefix = '', suffix = '';
  446. } else {
  447. if (content) prefix = '`', suffix = '`';
  448. }
  449. break;
  450. case 'a':
  451. const href = node.getAttribute('href');
  452. if (content && href) {
  453. // Handle relative URLs
  454. const absoluteHref = new URL(href, window.location.href).href;
  455. prefix = '[';
  456. suffix = `](${absoluteHref})`;
  457. } else {
  458. // If link has no content but has href, just output URL maybe?
  459. // Or just skip it. Let's skip.
  460. prefix = ''; suffix = ''; content = '';
  461. }
  462. break;
  463. case 'img':
  464. const src = node.getAttribute('src');
  465. const alt = node.getAttribute('alt') || '';
  466. if (src) {
  467. const absoluteSrc = new URL(src, window.location.href).href;
  468. // Render as block element
  469. prefix = `![${alt}](${absoluteSrc})`;
  470. suffix = '\n\n';
  471. content = ''; // No content for images
  472. }
  473. break;
  474. case 'ul':
  475. case 'ol':
  476. // Handled by child 'li' elements, add final newline if content exists
  477. if (content) suffix = '\n\n';
  478. else suffix = ''; // Avoid extra space if list empty
  479. prefix = ''; content = ''; // Content aggregation is done in children
  480. // Need to re-process children with list context here
  481. for (const child of node.children) {
  482. if (child.tagName.toLowerCase() === 'li') {
  483. content += processNode(child, listLevel + 1, tag);
  484. }
  485. }
  486. content = content.trimEnd(); // Remove trailing newline from last li
  487. break;
  488. case 'li':
  489. const indent = ' '.repeat(Math.max(0, listLevel - 1));
  490. prefix = indent + (listType === 'ol' ? '1. ' : '- '); // Simple numbering for ol
  491. // Add newline, unless it's the last item handled by parent ul/ol
  492. suffix = '\n';
  493. break;
  494. case 'blockquote':
  495. // Add > prefix to each line
  496. content = content.split('\n').map(line => '> ' + line).join('\n');
  497. prefix = '';
  498. suffix = '\n\n';
  499. break;
  500. case 'pre':
  501. let codeContent = node.textContent || ''; // Get raw text content
  502. let lang = '';
  503. // Try to find language from class="language-..." on pre or inner code
  504. const codeElement = node.querySelector('code[class*="language-"]');
  505. const langClass = codeElement?.className.match(/language-(\S+)/);
  506. if (langClass) {
  507. lang = langClass[1];
  508. } else {
  509. const preLangClass = node.className.match(/language-(\S+)/);
  510. if (preLangClass) lang = preLangClass[1];
  511. }
  512. prefix = '```' + lang + '\n';
  513. suffix = '\n```\n\n';
  514. content = codeContent.trim(); // Trim overall whitespace but preserve internal
  515. break;
  516. case 'hr':
  517. prefix = '---';
  518. suffix = '\n\n';
  519. content = ''; // No content
  520. break;
  521. case 'table':
  522. // Basic table support
  523. let header = '';
  524. let separator = '';
  525. let body = '';
  526. const rows = Array.from(node.querySelectorAll(':scope > thead > tr, :scope > tbody > tr, :scope > tr')); // More robust row finding
  527. let firstRow = true;
  528.  
  529. for (const row of rows) {
  530. let cols = [];
  531. const cells = Array.from(row.querySelectorAll(':scope > th, :scope > td'));
  532. cols = cells.map(cell => processNode(cell).replace(/\|/g, '\\|').trim()); // Escape pipes
  533.  
  534. if (firstRow && row.querySelector('th')) { // Assume header if first row has <th>
  535. header = `| ${cols.join(' | ')} |`;
  536. separator = `| ${cols.map(() => '---').join(' | ')} |`;
  537. firstRow = false;
  538. } else {
  539. body += `| ${cols.join(' | ')} |\n`;
  540. }
  541. }
  542. // Assemble table only if we found some structure
  543. if (header && separator && body) {
  544. prefix = header + '\n' + separator + '\n';
  545. content = body.trim();
  546. suffix = '\n\n';
  547. } else if (body) { // Table with no header
  548. prefix = '';
  549. content = body.trim();
  550. suffix = '\n\n';
  551. }
  552. else { // No meaningful table content
  553. prefix = ''; content = ''; suffix = '';
  554. }
  555. break;
  556. case 'br':
  557. // Add double space for line break within paragraphs, or newline otherwise
  558. const parentDisplay = node.parentNode ? window.getComputedStyle(node.parentNode).display : 'block';
  559. if(parentDisplay !== 'block'){
  560. prefix = ' \n'; // Markdown line break
  561. } else {
  562. prefix = '\n'; // Treat as paragraph break if parent is block
  563. }
  564. content = ''; suffix = '';
  565. break;
  566.  
  567. // Default: block elements add newlines, inline elements don't
  568. case 'div': case 'section': case 'article': case 'main': case 'header': case 'footer': case 'aside':
  569. // Add newlines only if content exists and doesn't already end with plenty
  570. if (content && !content.endsWith('\n\n')) {
  571. suffix = '\n\n';
  572. }
  573. break;
  574.  
  575. default:
  576. // For other inline elements, just pass content through
  577. // For unrecognized block elements, add spacing if needed
  578. if (isBlock && content && !content.endsWith('\n\n')) {
  579. suffix = '\n\n';
  580. }
  581. break;
  582. }
  583.  
  584. // Combine prefix, content, suffix. Trim whitespace around the final result for this node.
  585. let result = prefix + content + suffix;
  586.  
  587. // Add spacing between block elements if needed
  588. if (isBlock && markdown.length > 0 && !markdown.endsWith('\n\n') && !result.startsWith('\n')) {
  589. // Ensure there's a blank line separating block elements
  590. if (!markdown.endsWith('\n')) markdown += '\n';
  591. markdown += '\n';
  592. } else if (!isBlock && markdown.length > 0 && !markdown.endsWith(' ') && !markdown.endsWith('\n') && !result.startsWith(' ') && !result.startsWith('\n')) {
  593. // Add a space between inline elements if needed
  594. markdown += ' ';
  595. }
  596.  
  597. markdown += result;
  598. return result; // Return the result for recursive calls
  599.  
  600. } // End of processNode
  601.  
  602. try {
  603. // Start processing from the root element provided
  604. let rawMd = processNode(element);
  605.  
  606. // Final cleanup: consolidate multiple blank lines into one
  607. rawMd = rawMd.replace(/\n{3,}/g, '\n\n');
  608. return rawMd.trim(); // Trim final result
  609.  
  610. } catch (error) {
  611. log(`Error during Markdown conversion: ${error.message}`);
  612. return element.innerText || ''; // Fallback to innerText on error
  613. }
  614.  
  615. } // End of htmlToMarkdown
  616.  
  617.  
  618. // --- Clipboard & UI Feedback ---
  619. function handleCopyAttempt(markdownContent, sourceType) {
  620. if (markdownContent && markdownContent.trim().length > 0) {
  621. try {
  622. GM_setClipboard(markdownContent);
  623. log(`${sourceType}: Markdown copied successfully! (Length: ${markdownContent.length})`);
  624. showTemporaryMessage(BUTTON_TEXT_COPIED, false);
  625. } catch (err) {
  626. log(`${sourceType}: Copy failed: ${err.message}`);
  627. showTemporaryMessage(BUTTON_TEXT_FAILED, true);
  628. console.error("Clipboard copy error:", err);
  629. }
  630. } else {
  631. log(`${sourceType}: No valid content detected to copy.`);
  632. showTemporaryMessage(BUTTON_TEXT_FAILED, true); // Indicate failure if nothing was found
  633. }
  634. }
  635.  
  636. function showTemporaryMessage(text, isError) {
  637. if (!copyBtn) return;
  638. clearTimeout(messageTimeout); // Clear previous timeout if any
  639.  
  640. copyBtn.textContent = text;
  641. copyBtn.classList.toggle('mc-copied', !isError);
  642. copyBtn.classList.toggle('mc-failed', isError);
  643.  
  644.  
  645. messageTimeout = setTimeout(() => {
  646. setButtonState(BUTTON_TEXT_DEFAULT); // Restore default state
  647. copyBtn.classList.remove('mc-copied', 'mc-failed');
  648. }, TEMP_MESSAGE_DURATION);
  649. }
  650.  
  651. function setButtonState(text) {
  652. if (!copyBtn) return;
  653. copyBtn.textContent = text;
  654. // Clear temporary states if setting back to a standard message
  655. if (text !== BUTTON_TEXT_COPIED && text !== BUTTON_TEXT_FAILED) {
  656. copyBtn.classList.remove('mc-copied', 'mc-failed');
  657. clearTimeout(messageTimeout); // Clear any pending message reset
  658. }
  659. // Store the original text if setting to default
  660. if(text === BUTTON_TEXT_DEFAULT) {
  661. originalButtonText = text;
  662. }
  663. }
  664.  
  665.  
  666. // --- State Management & Cleanup ---
  667. function removeDivHighlight() {
  668. if (highlightedDiv) {
  669. highlightedDiv.classList.remove('markdown-copy-div-highlight');
  670. highlightedDiv = null;
  671. debugLog('Div highlight removed.');
  672. }
  673. }
  674.  
  675. function removeSelectionBox() {
  676. if (selectionBox) {
  677. selectionBox.style.display = 'none'; // Hide instead of removing immediately
  678. // Consider removing after a short delay if needed:
  679. // setTimeout(() => { if (selectionBox) selectionBox.remove(); selectionBox = null; }, 50);
  680. debugLog('Selection box hidden.');
  681. // Reset start coords to prevent mouseup from triggering after cancellation
  682. startX = null;
  683. startY = null;
  684. }
  685. }
  686.  
  687. function resetSelectionState() {
  688. isSelecting = false;
  689. isDivMode = false; // Always reset to off state
  690. document.body.style.cursor = 'default';
  691. setButtonState(originalButtonText); // Restore original or default text
  692. removeSelectionBox();
  693. removeDivHighlight();
  694. log('Selection state reset.');
  695. }
  696.  
  697. function handleKeyDown(e) {
  698. if (e.key === 'Escape' && isSelecting) {
  699. log('Escape key pressed, cancelling selection.');
  700. resetSelectionState();
  701. }
  702. }
  703.  
  704. // --- Script Entry Point ---
  705. onDOMReady(() => {
  706. // Small delay to let dynamic pages potentially load more content
  707. setTimeout(() => {
  708. try {
  709. initScript();
  710. } catch (err) {
  711. log(`Critical error during script initialization: ${err.message}`);
  712. console.error(err);
  713. }
  714. }, 100); // Wait 100ms after DOM ready
  715. });
  716.  
  717. })();

QingJ © 2025

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