Last active
December 7, 2025 20:35
-
-
Save intellectronica/3d645807698b19c243b0d6caa816d777 to your computer and use it in GitHub Desktop.
Monkey Script for Copying an Element on a Web Page as Markdown (ported from AnswerDotAI/clipmd)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name ClipMD - Copy Element as Markdown | |
| // @namespace clipmd | |
| // @version 1.0.0 | |
| // @description Select any element on a page and copy its content as Markdown. Userscript port of the ClipMD Chrome extension. | |
| // @author Based on AnswerDotAI/clipmd | |
| // @match *://*/* | |
| // @grant GM_setClipboard | |
| // @grant GM_registerMenuCommand | |
| // @require https://unpkg.com/[email protected]/dist/turndown.js | |
| // @run-at document-idle | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| // ======================================== | |
| // Configuration | |
| // ======================================== | |
| const CONFIG = { | |
| highlightColor: 'rgba(111, 168, 220, 0.35)', | |
| borderColor: 'rgba(111, 168, 220, 0.9)', | |
| borderWidth: '2px', | |
| activationKey: { key: 'M', ctrlKey: true, shiftKey: true }, // Ctrl+Shift+M | |
| cancelKey: 'Escape', | |
| toastDuration: 2000 | |
| }; | |
| // ======================================== | |
| // State | |
| // ======================================== | |
| let isActive = false; | |
| let currentTarget = null; | |
| let overlay = null; | |
| let tooltip = null; | |
| let toast = null; | |
| // ======================================== | |
| // Turndown Setup | |
| // ======================================== | |
| const turndownService = new TurndownService({ | |
| headingStyle: 'atx', | |
| hr: '---', | |
| bulletListMarker: '-', | |
| codeBlockStyle: 'fenced', | |
| emDelimiter: '_' | |
| }); | |
| // Preserve code blocks better | |
| turndownService.addRule('preserveCode', { | |
| filter: ['pre'], | |
| replacement: function(content, node) { | |
| const code = node.querySelector('code'); | |
| const language = code?.className?.match(/language-(\w+)/)?.[1] || ''; | |
| const text = code?.textContent || node.textContent || content; | |
| return '\n\n```' + language + '\n' + text.trim() + '\n```\n\n'; | |
| } | |
| }); | |
| // Handle tables better | |
| turndownService.addRule('tables', { | |
| filter: 'table', | |
| replacement: function(content, node) { | |
| const rows = Array.from(node.querySelectorAll('tr')); | |
| if (rows.length === 0) return content; | |
| const result = []; | |
| rows.forEach((row, rowIndex) => { | |
| const cells = Array.from(row.querySelectorAll('th, td')); | |
| const cellContents = cells.map(cell => | |
| cell.textContent.trim().replace(/\|/g, '\\|').replace(/\n/g, ' ') | |
| ); | |
| result.push('| ' + cellContents.join(' | ') + ' |'); | |
| // Add header separator after first row if it contains th elements | |
| if (rowIndex === 0 && row.querySelector('th')) { | |
| result.push('| ' + cells.map(() => '---').join(' | ') + ' |'); | |
| } else if (rowIndex === 0) { | |
| // Assume first row is header even without th | |
| result.push('| ' + cells.map(() => '---').join(' | ') + ' |'); | |
| } | |
| }); | |
| return '\n\n' + result.join('\n') + '\n\n'; | |
| } | |
| }); | |
| // ======================================== | |
| // UI Components | |
| // ======================================== | |
| function createOverlay() { | |
| const el = document.createElement('div'); | |
| el.id = 'clipmd-overlay'; | |
| el.style.cssText = ` | |
| position: fixed; | |
| pointer-events: none; | |
| background: ${CONFIG.highlightColor}; | |
| border: ${CONFIG.borderWidth} solid ${CONFIG.borderColor}; | |
| border-radius: 3px; | |
| z-index: 2147483646; | |
| transition: all 0.05s ease-out; | |
| display: none; | |
| `; | |
| document.body.appendChild(el); | |
| return el; | |
| } | |
| function createTooltip() { | |
| const el = document.createElement('div'); | |
| el.id = 'clipmd-tooltip'; | |
| el.style.cssText = ` | |
| position: fixed; | |
| background: rgba(0, 0, 0, 0.85); | |
| color: white; | |
| padding: 6px 10px; | |
| border-radius: 4px; | |
| font-size: 12px; | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| z-index: 2147483647; | |
| pointer-events: none; | |
| display: none; | |
| max-width: 400px; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| `; | |
| document.body.appendChild(el); | |
| return el; | |
| } | |
| function createToast() { | |
| const el = document.createElement('div'); | |
| el.id = 'clipmd-toast'; | |
| el.style.cssText = ` | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| background: rgba(0, 0, 0, 0.9); | |
| color: white; | |
| padding: 12px 20px; | |
| border-radius: 8px; | |
| font-size: 14px; | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| z-index: 2147483647; | |
| pointer-events: none; | |
| display: none; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); | |
| transition: opacity 0.2s ease-out; | |
| `; | |
| document.body.appendChild(el); | |
| return el; | |
| } | |
| function showToast(message, isError = false) { | |
| if (!toast) toast = createToast(); | |
| toast.textContent = message; | |
| toast.style.background = isError ? 'rgba(180, 50, 50, 0.95)' : 'rgba(50, 120, 80, 0.95)'; | |
| toast.style.display = 'block'; | |
| toast.style.opacity = '1'; | |
| setTimeout(() => { | |
| toast.style.opacity = '0'; | |
| setTimeout(() => { | |
| toast.style.display = 'none'; | |
| }, 200); | |
| }, CONFIG.toastDuration); | |
| } | |
| function updateOverlay(element) { | |
| if (!overlay) overlay = createOverlay(); | |
| if (!tooltip) tooltip = createTooltip(); | |
| if (!element) { | |
| overlay.style.display = 'none'; | |
| tooltip.style.display = 'none'; | |
| return; | |
| } | |
| const rect = element.getBoundingClientRect(); | |
| overlay.style.display = 'block'; | |
| overlay.style.top = rect.top + 'px'; | |
| overlay.style.left = rect.left + 'px'; | |
| overlay.style.width = rect.width + 'px'; | |
| overlay.style.height = rect.height + 'px'; | |
| // Update tooltip | |
| const tagName = element.tagName.toLowerCase(); | |
| const id = element.id ? `#${element.id}` : ''; | |
| const classes = element.className && typeof element.className === 'string' | |
| ? '.' + element.className.trim().split(/\s+/).join('.') | |
| : ''; | |
| const selector = tagName + id + classes; | |
| tooltip.textContent = `📋 ${selector}`; | |
| tooltip.style.display = 'block'; | |
| tooltip.style.top = Math.max(5, rect.top - 30) + 'px'; | |
| tooltip.style.left = Math.max(5, rect.left) + 'px'; | |
| } | |
| // ======================================== | |
| // Core Functionality | |
| // ======================================== | |
| function activate() { | |
| if (isActive) return; | |
| isActive = true; | |
| document.body.style.cursor = 'crosshair'; | |
| showToast('🎯 Click an element to copy as Markdown (Esc to cancel)'); | |
| document.addEventListener('mouseover', handleMouseOver, true); | |
| document.addEventListener('mouseout', handleMouseOut, true); | |
| document.addEventListener('click', handleClick, true); | |
| document.addEventListener('keydown', handleKeyDown, true); | |
| } | |
| function deactivate() { | |
| if (!isActive) return; | |
| isActive = false; | |
| document.body.style.cursor = ''; | |
| currentTarget = null; | |
| updateOverlay(null); | |
| document.removeEventListener('mouseover', handleMouseOver, true); | |
| document.removeEventListener('mouseout', handleMouseOut, true); | |
| document.removeEventListener('click', handleClick, true); | |
| document.removeEventListener('keydown', handleKeyDown, true); | |
| } | |
| function handleMouseOver(e) { | |
| if (!isActive) return; | |
| // Ignore our own UI elements | |
| if (e.target.id?.startsWith('clipmd-')) return; | |
| currentTarget = e.target; | |
| updateOverlay(e.target); | |
| } | |
| function handleMouseOut(e) { | |
| if (!isActive) return; | |
| if (e.target === currentTarget) { | |
| currentTarget = null; | |
| updateOverlay(null); | |
| } | |
| } | |
| function handleClick(e) { | |
| if (!isActive) return; | |
| if (e.target.id?.startsWith('clipmd-')) return; | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| e.stopImmediatePropagation(); | |
| const target = currentTarget || e.target; | |
| if (!target) { | |
| deactivate(); | |
| return; | |
| } | |
| copyElementAsMarkdown(target); | |
| deactivate(); | |
| } | |
| function handleKeyDown(e) { | |
| if (e.key === CONFIG.cancelKey) { | |
| e.preventDefault(); | |
| deactivate(); | |
| showToast('❌ Cancelled'); | |
| } | |
| } | |
| function handleGlobalKeyDown(e) { | |
| // Check for activation shortcut: Ctrl+Shift+M | |
| if (e.key.toUpperCase() === CONFIG.activationKey.key && | |
| e.ctrlKey === CONFIG.activationKey.ctrlKey && | |
| e.shiftKey === CONFIG.activationKey.shiftKey && | |
| !e.altKey && !e.metaKey) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| if (isActive) { | |
| deactivate(); | |
| } else { | |
| activate(); | |
| } | |
| } | |
| } | |
| async function copyElementAsMarkdown(element) { | |
| try { | |
| const html = element.outerHTML; | |
| const markdown = turndownService.turndown(html); | |
| // Try GM_setClipboard first (more reliable in userscripts) | |
| if (typeof GM_setClipboard !== 'undefined') { | |
| GM_setClipboard(markdown, 'text'); | |
| showToast(`✅ Copied ${markdown.length} chars as Markdown`); | |
| } else { | |
| // Fallback to navigator.clipboard | |
| await navigator.clipboard.writeText(markdown); | |
| showToast(`✅ Copied ${markdown.length} chars as Markdown`); | |
| } | |
| } catch (err) { | |
| console.error('ClipMD error:', err); | |
| showToast('❌ Failed to copy: ' + err.message, true); | |
| } | |
| } | |
| // ======================================== | |
| // Initialization | |
| // ======================================== | |
| function init() { | |
| // Global keyboard shortcut | |
| document.addEventListener('keydown', handleGlobalKeyDown, true); | |
| // Register menu command if available | |
| if (typeof GM_registerMenuCommand !== 'undefined') { | |
| GM_registerMenuCommand('📋 Activate ClipMD (Ctrl+Shift+M)', activate); | |
| } | |
| console.log('[ClipMD] Userscript loaded. Press Ctrl+Shift+M to activate.'); | |
| } | |
| // Wait for page to be ready | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', init); | |
| } else { | |
| init(); | |
| } | |
| })(); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Based on the excellent extension https://github.com/AnswerDotAI/clipmd ; This only implements the Markdown functionality (CTRL-SHIFT-M to trigger). For the screenshotting you need a proper extension.