LeetCode Copy Title & Markdown

Adds buttons to copy the title/description to Markdown, and makes the title selectable

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         LeetCode Copy Title & Markdown
// @namespace    https://github.com/wilmtang/tampermonkey-scripts
// @version      2.3
// @description  Adds buttons to copy the title/description to Markdown, and makes the title selectable
// @author       wilmtang
// @license      MIT
// @homepageURL  https://github.com/wilmtang/tampermonkey-scripts/tree/main/Leetcode
// @supportURL   https://github.com/wilmtang/tampermonkey-scripts/issues
// @match        *://leetcode.com/problems/*/
// @match        *://leetcode.com/problems/*/description/
// @match        *://leetcode.com/problems/*/*
// @match        *://www.leetcode.com/problems/*/
// @match        *://www.leetcode.com/problems/*/description/
// @match        *://www.leetcode.com/problems/*/*
// @grant        GM_setClipboard
// @require      https://unpkg.com/[email protected]/dist/turndown.js
// ==/UserScript==

(function () {
    'use strict';

    const CONTAINER_ID = 'lc-copy-btns-container';
    let lastInjectedTitle = null;
    let lastPathname = null;

    // --- Selectability CSS & Event Blockers ---
    function makeTitleSelectable() {
        if (document.getElementById('lc-selectable-style')) return;
        
        const style = document.createElement('style');
        style.id = 'lc-selectable-style';
        style.innerHTML = `
            .text-title-large,
            .text-title-large * {
                -webkit-user-select: text !important;
                -moz-user-select: text !important;
                -ms-user-select: text !important;
                user-select: text !important;
                cursor: text !important;
                pointer-events: auto !important;
            }
        `;
        document.head.appendChild(style);

        const stopDragCancel = function(e) {
            let target = e.target;
            while (target && target !== document.body) {
                if (target.classList && target.classList.contains('text-title-large')) {
                    if (e.type === 'dragstart') e.preventDefault(); 
                    else e.stopPropagation(); 
                    break;
                }
                target = target.parentNode;
            }
        };

        document.addEventListener('mousedown', stopDragCancel, true);
        document.addEventListener('selectstart', stopDragCancel, true);
        document.addEventListener('dragstart', stopDragCancel, true);
    }

    makeTitleSelectable();

    // --- NEW in 2.1: Destroy Link Behavior ---
    // This stops the browser from treating the middle of the text as a draggable URL
    function neutralizeLink(el) {
        if (!el) return;
        
        // If the element itself is the anchor tag
        if (el.tagName === 'A') {
            el.removeAttribute('href');
        } 
        
        // If the element contains an anchor tag
        const childA = el.querySelector('a');
        if (childA) {
            childA.removeAttribute('href');
        }
    }


    // --- Helper: Creates the standardized buttons ---
    function createButtonBase(title, text) {
        const btn = document.createElement('button');
        btn.title = title;
        btn.textContent = text;
        btn.style.cssText = `
            padding: 2px 8px;
            font-size: 14px;
            cursor: pointer;
            background: transparent;
            border: 1px solid #888;
            border-radius: 6px;
            color: inherit;
            transition: background 0.15s, border-color 0.15s;
            line-height: 1.4;
            opacity: 0.7;
        `;

        btn.addEventListener('mouseenter', () => {
            btn.style.opacity = '1';
            btn.style.borderColor = '#ffa116';
        });
        btn.addEventListener('mouseleave', () => {
            btn.style.opacity = '0.7';
            btn.style.borderColor = '#888';
        });

        return btn;
    }

    // --- Helper: Success Animation ---
    function triggerSuccessAction(btn, originalText) {
        btn.textContent = '✅';
        btn.style.borderColor = '#00b894';
        btn.style.opacity = '1';
        setTimeout(() => {
            btn.textContent = originalText;
            btn.style.borderColor = '#888';
            btn.style.opacity = '0.7';
        }, 1500);
    }

    // --- Helper: Copy logic fallback ---
    function copyToClipboard(text) {
        if (typeof GM_setClipboard !== 'undefined') {
            GM_setClipboard(text);
        } else {
            navigator.clipboard.writeText(text).catch(() => {
                const ta = document.createElement('textarea');
                ta.value = text;
                document.body.appendChild(ta);
                ta.select();
                document.execCommand('copy');
                document.body.removeChild(ta);
            });
        }
    }

    function stripProblemNumber(title) {
        return title.replace(/^\d+\.\s*/, '').trim();
    }

    // --- Button 1: Copy Title ---
    function createCopyTitleButton() {
        const btn = createButtonBase('Copy title to clipboard', '📋');
        btn.id = 'lc-copy-title-btn';

        btn.addEventListener('click', (e) => {
            e.stopPropagation();
            const currentTitle = stripProblemNumber((findTitleEl()?.textContent ?? '').trim());
            if (!currentTitle) return;

            copyToClipboard(currentTitle);
            triggerSuccessAction(btn, '📋');
        });

        return btn;
    }

    // --- Button 2: Copy Full Markdown ---
    function createCopyMdButton() {
        const btn = createButtonBase('Copy problem to Markdown', 'M↓');
        btn.id = 'lc-copy-md-btn';

        btn.addEventListener('click', (e) => {
            e.stopPropagation();

            const titleEl = findTitleEl();
            const descEl = findDescriptionEl();
            if (!titleEl || !descEl) {
                alert("Could not find the problem description on the page.");
                return;
            }

            const titleText = titleEl.textContent.trim();

            const cleanPathname = window.location.pathname.split('/description')[0] + '/description/';
            const url = window.location.origin + cleanPathname;

            let mdContent = convertToMarkdown(descEl.innerHTML);
            mdContent = mdContent.replace(/\u200B/g, '');
            mdContent = mdContent.split('\n').map(line => '> ' + line).join('\n');

            const finalMd = `# ${titleText}\n${url}\n${mdContent}\n`;

            copyToClipboard(finalMd);
            triggerSuccessAction(btn, 'M↓');
        });

        return btn;
    }

    // --- HTML to Markdown Conversion Logic ---
    function convertToMarkdown(html) {
        if (typeof TurndownService === 'undefined') {
            alert("Markdown library failed to load. Check Tampermonkey permissions for @require.");
            return 'Error: Turndown library failed to load.';
        }

        const turndownService = new TurndownService({
            headingStyle: 'atx',
            codeBlockStyle: 'fenced'
        });

        turndownService.addRule('katex', {
            filter: function (node) {
                return node.classList && node.classList.contains('katex');
            },
            replacement: function (content, node) {
                const annotation = node.querySelector('annotation[encoding="application/x-tex"]');
                if (annotation) {
                    const tex = annotation.textContent.trim();
                    const isDisplay = node.classList.contains('katex-display') ||
                                      (node.parentNode && node.parentNode.classList.contains('katex-display'));
                    return isDisplay ? `\n$$\n${tex}\n$$\n` : `$${tex}$`;
                }
                return node.textContent;
            }
        });

        turndownService.addRule('sup', {
            filter: 'sup',
            replacement: function (content) { return `^${content}`; }
        });
        turndownService.addRule('sub', {
            filter: 'sub',
            replacement: function (content) { return `_${content}`; }
        });

        turndownService.addRule('pre', {
            filter: 'pre',
            replacement: function (content, node) {
                const code = node.textContent || '';
                return `\n\`\`\`\n${code.trim()}\n\`\`\`\n`;
            }
        });

        turndownService.addRule('hidden', {
            filter: function(node) {
                return node.classList && (node.classList.contains('hide') || node.classList.contains('sr-only'));
            },
            replacement: function() { return ''; }
        });

        return turndownService.turndown(html);
    }

    // --- Ultra-Aggressive Element Selectors ---
    function findTitleEl() {
        let el = document.querySelector('.text-title-large a') || document.querySelector('.text-title-large');
        if (el) return el;

        const selectors = [
            'div[data-cy="question-title"]',
            'h1[data-cy="question-title"]',
            'h1.mr-2',
            '.question-title h3'
        ];
        for (const sel of selectors) {
            el = document.querySelector(sel);
            if (el) return el;
        }

        const allHeadings = document.querySelectorAll('h1, a, div');
        for (const elem of allHeadings) {
            if (/^\d+\.\s/.test(elem.textContent.trim())) {
                if (elem.tagName === 'H1' || elem.classList.contains('text-title-large')) {
                    return elem;
                }
            }
        }

        return null;
    }

    function findDescriptionEl() {
        const selectors = [
            'div[data-track-load="description_content"]',
            '.content__u3I1',
            '.question-content',
            '[data-track-load="description_content"]'
        ];
        for (const sel of selectors) {
            const el = document.querySelector(sel);
            if (el) return el;
        }
        return null;
    }

    // --- Core Injection Logic ---
    function injectButtons() {
        if (!/\/problems\/.+/.test(location.pathname)) return;

        const titleEl = findTitleEl();
        if (!titleEl) return;
        
        // Strip the link behavior so the middle is selectable
        neutralizeLink(titleEl);

        const titleText = titleEl.textContent.trim();
        if (!titleText) return;

        let container = document.getElementById(CONTAINER_ID);

        if (container && lastInjectedTitle === titleText) return;
        if (container) container.remove();

        container = document.createElement('div');
        container.id = CONTAINER_ID;
        container.style.cssText = `
            display: inline-flex;
            gap: 8px;
            margin-left: 10px;
            vertical-align: middle;
        `;

        const titleBtn = createCopyTitleButton();
        const mdBtn = createCopyMdButton();

        container.appendChild(titleBtn);
        container.appendChild(mdBtn);

        titleEl.insertAdjacentElement('afterend', container);

        lastInjectedTitle = titleText;
        lastPathname = location.pathname;
    }

    // Watch for SPA navigations via URL changes
    function onUrlChange() {
        if (location.pathname !== lastPathname) {
            lastInjectedTitle = null;
            lastPathname = location.pathname;
            injectButtons(); 
        }
    }

    const _pushState = history.pushState.bind(history);
    history.pushState = function (...args) {
        _pushState(...args);
        onUrlChange();
    };
    window.addEventListener('popstate', onUrlChange);

    const observer = new MutationObserver(() => injectButtons());
    observer.observe(document.body, { childList: true, subtree: true });

    window.addEventListener('load', injectButtons);
    setTimeout(injectButtons, 1000);
    setTimeout(injectButtons, 2500);
    setTimeout(injectButtons, 5000);
})();