Skip to content

Instantly share code, notes, and snippets.

@intellectronica
Last active December 7, 2025 20:35
Show Gist options
  • Select an option

  • Save intellectronica/3d645807698b19c243b0d6caa816d777 to your computer and use it in GitHub Desktop.

Select an option

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)
// ==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();
}
})();
@intellectronica
Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment