// ==UserScript== // @name Get Markdown // @namespace http://tampermonkey.net/ // @version 2.0 // @description Select any element to view HTML or convert to Markdown with Turndown // @author zabk // @match *://*/* // @grant GM_registerMenuCommand // @grant GM_addStyle // @require https://cdn.jsdelivr.net/npm/sweetalert2@11 // @require https://cdn.jsdelivr.net/npm/turndown@7.2.0/dist/turndown.js // @run-at document-end // ==/UserScript== (function() { 'use strict'; // CSS for highlighting and overlay const STYLES = ` #element-selector-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.01); z-index: 2147483646; cursor: crosshair; display: none; pointer-events: auto; } #element-selector-overlay.active { display: block; } .element-highlight { outline: 3px dashed #00d1b2 !important; outline-offset: -3px !important; box-shadow: 0 0 0 4px rgba(0, 209, 178, 0.2) !important; transition: none !important; } #element-selector-hint { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: #333; color: #fff; padding: 12px 24px; border-radius: 8px; font-size: 14px; z-index: 2147483647; box-shadow: 0 4px 12px rgba(0,0,0,0.3); display: none; max-width: 90vw; text-align: center; pointer-events: none; } #element-selector-hint.active { display: block; animation: slideDown 0.2s ease; } @keyframes slideDown { from { opacity: 0; transform: translate(-50%, -20px); } to { opacity: 1; transform: translate(-50%, 0); } } .swal2-html-container { text-align: left !important; max-height: 60vh !important; overflow: auto !important; } .swal2-html-container pre, .swal2-html-container textarea { margin: 0; font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; white-space: pre-wrap; word-break: break-all; background: #f6f8fa; padding: 12px; border-radius: 6px; border: 1px solid #e1e4e8; width: 100%; box-sizing: border-box; min-height: 200px; resize: vertical; } .swal2-html-container textarea { font-family: 'Consolas', 'Monaco', monospace; } .md-preview { background: #fff; padding: 12px; border-radius: 6px; border: 1px solid #e1e4e8; margin-top: 10px; max-height: 300px; overflow: auto; } .swal2-actions { flex-wrap: wrap !important; gap: 8px !important; } .swal2-actions button { margin: 2px !important; } `; let isActive = false; let currentHighlight = null; let overlay, hint; let turndownService = null; GM_addStyle(STYLES); // Initialize Turndown with common options function initTurndown() { if (typeof TurndownService === 'undefined') return null; const td = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced', fence: '```', emDelimiter: '*', bulletListMarker: '-', strongDelimiter: '**', linkStyle: 'inlined' }); // 🗑️ REMOVE style, script, noscript, iframe, etc. td.addRule('remove-unwanted', { filter: ['style', 'script', 'noscript', 'iframe', 'object', 'embed', 'canvas', 'svg'], replacement: () => '' }); // 🗑️ Remove elements with hidden/invisible attributes td.addRule('remove-hidden', { filter: (node) => { if (node.nodeType !== 1) return false; // Skip if hidden attribute or aria-hidden if (node.hasAttribute('hidden')) return true; if (node.getAttribute('aria-hidden') === 'true') return true; // Skip if style contains display:none or visibility:hidden const style = node.getAttribute('style') || ''; if (/display\s*:\s*none|visibility\s*:\s*hidden/i.test(style)) return true; return false; }, replacement: () => '' }); // ✅ Preserve
 blocks properly
    td.addRule('preCode', {
        filter: (node) => node.nodeName === 'PRE' && node.querySelector('code'),
        replacement: (content, node) => {
            const code = node.querySelector('code');
            const lang = code.className?.match(/language-(\w+)/)?.[1] || '';
            return `\n\`\`\`${lang}\n${code.textContent}\n\`\`\`\n\n`;
        }
    });

    // ✅ Handle images with alt text
    td.addRule('img', {
        filter: 'img',
        replacement: (content, node) => {
            const alt = node.alt || '';
            const src = node.src || '';
            const title = node.title ? ` "${node.title}"` : '';
            return src ? `![${alt}](${src}${title})` : '';
        }
    });

    return td;
}

    function createUI() {
        overlay = document.createElement('div');
        overlay.id = 'element-selector-overlay';
        document.documentElement.appendChild(overlay);

        hint = document.createElement('div');
        hint.id = 'element-selector-hint';
        hint.textContent = '🔍 Hover to highlight • Click to select • Press ESC to cancel';
        document.documentElement.appendChild(hint);

        overlay.addEventListener('click', handleClick, true);
        overlay.addEventListener('mousemove', handleMouseMove, true);
        overlay.addEventListener('mouseleave', clearHighlight, true);
        document.addEventListener('keydown', handleKeydown, true);
    }

    function handleKeydown(e) {
        if (e.key === 'Escape' && isActive) {
            e.preventDefault();
            e.stopPropagation();
            deactivate();
        }
    }

    function handleMouseMove(e) {
        e.preventDefault();
        e.stopPropagation();
        overlay.style.pointerEvents = 'none';
        const target = document.elementFromPoint(e.clientX, e.clientY);
        overlay.style.pointerEvents = 'auto';
        if (target && isValidElement(target)) highlightElement(target);
    }

    function handleClick(e) {
        e.preventDefault();
        e.stopPropagation();
        e.stopImmediatePropagation();
        overlay.style.pointerEvents = 'none';
        const target = document.elementFromPoint(e.clientX, e.clientY);
        overlay.style.pointerEvents = 'auto';
        if (target && isValidElement(target)) selectElement(target);
    }

    function isValidElement(el) {
        if (!el || el.nodeType !== 1) return false;
        if (el.id === 'element-selector-overlay' || el.id === 'element-selector-hint') return false;
        if (el.closest?.('#element-selector-overlay, #element-selector-hint')) return false;
        return true;
    }

    function highlightElement(el) {
        clearHighlight();
        if (el && isValidElement(el)) {
            if (el.classList) {
                el.classList.add('element-highlight');
            } else {
                el.setAttribute('data-element-highlight', 'true');
                el.style.outline = '3px dashed #00d1b2';
                el.style.boxShadow = '0 0 0 4px rgba(0, 209, 178, 0.2)';
            }
            currentHighlight = el;
        }
    }

    function clearHighlight() {
        if (currentHighlight) {
            if (currentHighlight.classList) {
                currentHighlight.classList.remove('element-highlight');
            } else {
                currentHighlight.removeAttribute('data-element-highlight');
                currentHighlight.style.outline = '';
                currentHighlight.style.boxShadow = '';
            }
            currentHighlight = null;
        }
    }

    function formatHTML(html) {
        let formatted = '';
        let indent = 0;
        const tab = '  ';
        const tokens = html.split(/(<\/?[^>]+>|)/g).filter(t => t.trim());

        for (let token of tokens) {
            const trimmed = token.trim();
            if (!trimmed) continue;
            if (trimmed.startsWith('') || isVoidElement(trimmed))) {
                formatted += tab.repeat(indent) + trimmed + '\n';
            } else if (trimmed.startsWith('<')) {
                formatted += tab.repeat(indent) + trimmed + '\n';
                if (!trimmed.endsWith('/>') && !isVoidElement(trimmed)) indent++;
            } else {
                const content = trimmed;
                if (content && !/^\s*$/.test(content)) {
                    formatted += tab.repeat(indent) + escapeHtml(content) + '\n';
                }
            }
        }
        return formatted.trim();
    }

    function isVoidElement(tag) {
        const voidEls = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
        const match = tag.match(/<(\w+)/i);
        return match && voidEls.includes(match[1].toLowerCase());
    }

    function escapeHtml(str) {
        const div = document.createElement('div');
        div.textContent = str;
        return div.innerHTML;
    }

    function copyToClipboard(text, successMsg = '📋 Copied!', failMsg = '❌ Copy failed') {
        return navigator.clipboard.writeText(text)
            .then(() => Swal.showValidationMessage(successMsg))
            .catch(() => Swal.showValidationMessage(failMsg));
    }

    function selectElement(el) {
        if (!el || !isValidElement(el)) return;
        deactivate();

        try {
            const html = el.outerHTML;
            const tagName = el.tagName.toLowerCase();
            const id = el.id ? `#${el.id}` : '';
            const className = el.className && typeof el.className === 'string'
                ? el.className.trim().split(/\s+/)[0] : null;
            const classSelector = className ? `.${className}` : '';
            const formattedHTML = formatHTML(html);

            // Lazy init Turndown
            if (turndownService === null) {
                turndownService = initTurndown();
            }

            Swal.fire({
                title: `<${tagName}${id}${classSelector}>`,
                html: `
                    
📄 Raw HTML
${escapeHtml(formattedHTML)}
`, width: '750px', showConfirmButton: true, confirmButtonText: '📋 Copy HTML', showCancelButton: true, cancelButtonText: '✕ Close', showDenyButton: true, denyButtonText: '🔄 Convert to Markdown', customClass: { htmlContainer: 'swal2-monospace' }, preConfirm: () => { const htmlContent = document.getElementById('swal-html-content')?.textContent || html; return copyToClipboard(htmlContent); }, preDeny: () => { if (!turndownService) { Swal.showValidationMessage('⚠️ Turndown not loaded'); return false; } try { const md = turndownService.turndown(html); console.log(md); Swal.fire({html:``}) } catch (err) { console.error('[Turndown Error]', err); Swal.showValidationMessage('❌ Conversion failed: ' + err.message); return false; } return false; // Keep dialog open } }); // Add toggle back to HTML after MD conversion Swal.getDenyButton()?.addEventListener('click', () => { if (Swal.getDenyButton()?.textContent?.includes('🔙')) { const mdSection = document.getElementById('swal-md-section'); if (mdSection) mdSection.style.display = 'none'; Swal.update({ confirmButtonText: '📋 Copy HTML', denyButtonText: '🔄 Convert to Markdown', preConfirm: () => { const htmlContent = document.getElementById('swal-html-content')?.textContent || html; return copyToClipboard(htmlContent); } }); } }); } catch (err) { console.error('[Element Viewer] Error:', err); Swal.fire({ icon: 'error', title: 'Error', text: 'Could not process element: ' + err.message }); } } function activate() { if (isActive) return; isActive = true; if (!overlay) createUI(); overlay.classList.add('active'); hint.classList.add('active'); document.documentElement.dataset.originalOverflow = document.documentElement.style.overflow; document.documentElement.style.overflow = 'hidden'; } function deactivate() { if (!isActive) return; isActive = false; clearHighlight(); if (overlay) overlay.classList.remove('active'); if (hint) hint.classList.remove('active'); if (document.documentElement.dataset.originalOverflow !== undefined) { document.documentElement.style.overflow = document.documentElement.dataset.originalOverflow; delete document.documentElement.dataset.originalOverflow; } } function toggleSelector() { isActive ? deactivate() : activate(); } GM_registerMenuCommand('🔍 Select Element (HTML/MD)', toggleSelector, 'S'); document.addEventListener('keydown', (e) => { if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'e' && !isActive) { e.preventDefault(); toggleSelector(); } }, true); window.addEventListener('beforeunload', () => deactivate()); })();