💬 Quote Highlighter

Highlights quoted text with customizable colors, styles, and features

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           💬 Quote Highlighter
// @version        1.2
// @description    Highlights quoted text with customizable colors, styles, and features
// @author         Misspent
// @namespace      ChatGPT / Grok AI
// @icon           https://i.imgur.com/bhdcGzd.png

// @match          *://*/*

// @exclude        /^https?:\/\/(www\.)?(x\.com|youtube\.com|github\.com|twitter\.com|steam\.com|search.brave\.com|twitch\.tv)(\/.*)?$/

// @grant          GM_addStyle
// @run-at         document-idle
// @license        MIT
// ==/UserScript==

// https://www.iconsdb.com/icons/preview/orange/quote-xxl.png


(function () {
    'use strict';

    // ================== CONFIGURATION ==================

    const CONFIG = {
        // Highlight appearance
        textColor: '#EA9D9C',
        backgroundColor: 'rgba(234, 157, 156, 0.12)',
        borderRadius: '0px',
        padding: '0 2px',
        // fontWeight: '500',

        // Enable subtle underline instead of bg
        underline: false,

        // Ignore short quoted text
        minimumLength: 2,

        // Prevent absurdly large matches
        maximumLength: 500,

        // Ignore these tags entirely
        ignoredTags: new Set([
            'SCRIPT',
            'STYLE',
            'TEXTAREA',
            'INPUT',
            'CODE',
            'PRE',
            'NOSCRIPT',
            'OPTION',
            // Mine
            'a',
            'video',
            '[href]',
            '.flair',
            '.subtitle',
            '#subtitle',
            '#ace-editor',
            '.no-highlight',
            '.flair-content',
            '#TitleClipboard',
            'clipboard-notification'
        ]),

        // Ignore editable areas
        ignoreContentEditable: true,

        // MutationObserver debounce
        observerDebounce: 150,

        // Debug mode
        debug: false,

        // Your regex
        quoteRegex:
            /(?<!\w)(["“”““'‘`"])(.*?)(["””””'’`.])(?!\w)/g
    };

     // ================== STYLES ==================

    GM_addStyle(`
        .tm-quote-highlight {
            color: ${CONFIG.textColor} !important;
            background: ${CONFIG.underline ? 'transparent' : CONFIG.backgroundColor};
            border-radius: ${CONFIG.borderRadius};
            padding: ${CONFIG.padding};
            font-weight: ${CONFIG.fontWeight};
            transition: all 0.15s ease;
            cursor: text;
        }

        .tm-quote-highlight:hover {
            filter: brightness(1.08);
        }

        ${
            CONFIG.underline
                ? `
            .tm-quote-highlight {
                text-decoration: underline;
                text-decoration-color: ${CONFIG.textColor};
                text-decoration-thickness: 2px;
            }
        `
                : ''
        }
    `);

     // ================== HELPERS ==================

    function log(...args) {
        if (CONFIG.debug) {
            console.log('[QuoteHighlighter]', ...args);
        }
    }

    function shouldSkipNode(node) {
        if (!node || !node.parentNode) return true;

        const parent = node.parentNode;

        if (parent.classList?.contains('tm-quote-highlight')) {
            return true;
        }

        if (CONFIG.ignoredTags.has(parent.nodeName)) {
            return true;
        }

        if (
            CONFIG.ignoreContentEditable &&
            parent.closest('[contenteditable="true"]')
        ) {
            return true;
        }

        return false;
    }

     // ================== MAIN HIGHLIGHT FUNCTION ==================

    function processTextNode(textNode) {
        if (shouldSkipNode(textNode)) return;

        const text = textNode.nodeValue;

        if (!text || !text.trim()) return;

        CONFIG.quoteRegex.lastIndex = 0;

        const matches = [...text.matchAll(CONFIG.quoteRegex)];

        if (!matches.length) return;

        const fragment = document.createDocumentFragment();
        let lastIndex = 0;

        for (const match of matches) {
            const fullMatch = match[0];

            if (
                fullMatch.length < CONFIG.minimumLength ||
                fullMatch.length > CONFIG.maximumLength
            ) {
                continue;
            }

            const start = match.index;

            // Normal text before match
            if (start > lastIndex) {
                fragment.appendChild(
                    document.createTextNode(
                        text.slice(lastIndex, start)
                    )
                );
            }

            // Highlighted quote
            const span = document.createElement('span');
            span.className = 'tm-quote-highlight';
            span.textContent = fullMatch;

            // Tooltip
            span.title = 'Quoted from Script';

            fragment.appendChild(span);

            lastIndex = start + fullMatch.length;
        }

        // Remaining text
        if (lastIndex < text.length) {
            fragment.appendChild(
                document.createTextNode(text.slice(lastIndex))
            );
        }

        // Replace only if something changed
        if (fragment.childNodes.length) {
            textNode.parentNode.replaceChild(fragment, textNode);
        }
    }

     // ================== TREE WALKER ==================

    function scan(root = document.body) {
        const walker = document.createTreeWalker(
            root,
            NodeFilter.SHOW_TEXT,
            null
        );

        const textNodes = [];

        while (walker.nextNode()) {
            textNodes.push(walker.currentNode);
        }

        for (const node of textNodes) {
            processTextNode(node);
        }

        log(`Processed ${textNodes.length} text nodes`);
    }

     // ================== LIVE PAGE SUPPORT ==================

    let debounceTimer;

    const observer = new MutationObserver((mutations) => {
        clearTimeout(debounceTimer);

        debounceTimer = setTimeout(() => {
            for (const mutation of mutations) {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType === Node.TEXT_NODE) {
                        processTextNode(node);
                    } else if (node.nodeType === Node.ELEMENT_NODE) {
                        scan(node);
                    }
                }
            }
        }, CONFIG.observerDebounce);
    });

     // ================== START ==================

    function init() {
        log('Initializing...');

        scan(document.body);

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        log('Observer started');
    }

    if (document.readyState === 'loading') {
        window.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

     // ================== OPTIONAL GLOBAL API (Useful for debugging in DevTools) ==================

    window.QuoteHighlighter = {
        rescan: () => scan(document.body),
        disconnect: () => observer.disconnect(),
        config: CONFIG
    };

})();







/* 📌 Grok Version 1:

// ⭐ Features:

- Customizable highlight color (default `#EA9D9C`)
- Multiple highlight styles (text color, background, both, underline)
- Click to copy quote
- Tooltip on hover
- Persistent settings via TamperMonkey storage
- Settings menu (accessible via TamperMonkey dashboard)
- Smart mutation observer for dynamic content (Twitter/X, Reddit, news sites, etc.)
- Debounced processing to avoid performance issues
- Ignores script/style tags


// off
// GM_addStyle
// GM_getValue
// GM_setValue
// GM_registerMenuCommand
// document-end




(() => {
    'use strict';

    // ================== CONFIGURATION ==================
    const CONFIG = {
        enabled: GM_getValue('enabled', true),
        color: GM_getValue('color', '#EA9D9C'),
        background: GM_getValue('background', 'rgba(234, 157, 156, 0.15)'),
        style: GM_getValue('style', 'color'), // 'color', 'background', 'both', 'underline'
        borderRadius: GM_getValue('borderRadius', '0px'),
        padding: GM_getValue('padding', '0px 2px'),
        showTooltip: GM_getValue('showTooltip', true),
        copyOnClick: GM_getValue('copyOnClick', true),
        debounceTime: 800,
    };

    // Regex for various quote types
    const QUOTE_REGEX = /(?<!\w)(["“”'"‘’`])(.*?)(["”'"’`])/g;

    let observer = null;

    // ================== STYLES ==================
    function addGlobalStyles() {
        GM_addStyle(`
            .quote-highlight {
                color: ${CONFIG.color};
                background: ${CONFIG.style === 'background' || CONFIG.style === 'both' ? CONFIG.background : 'transparent'};
                padding: ${CONFIG.padding};
                border-radius: ${CONFIG.borderRadius};
                ${CONFIG.style === 'underline' ? 'text-decoration: underline wavy #EA9D9C;' : ''}
                transition: all 0.2s ease;
                cursor: pointer;
            }
            .quote-highlight:hover {
                filter: brightness(1.1);
                box-shadow: 0 0 0 2px rgba(234, 157, 156, 0.3);
            }
        `);
    }

    // ================== CORE FUNCTIONS ==================
    function highlightQuotes(node) {
        if (!CONFIG.enabled) return;

        const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null, false);
        const nodesToProcess = [];

        let textNode;
        while ((textNode = walker.nextNode())) {
            if (textNode.parentNode.tagName === 'SCRIPT' ||
                textNode.parentNode.tagName === 'STYLE' ||
                textNode.parentNode.classList.contains('quote-highlight')) {
                continue;
            }
            if (QUOTE_REGEX.test(textNode.textContent)) {
                nodesToProcess.push(textNode);
            }
        }

        nodesToProcess.forEach(textNode => {
            const fragment = document.createDocumentFragment();
            let lastIndex = 0;
            let match;

            // Reset regex
            QUOTE_REGEX.lastIndex = 0;

            while ((match = QUOTE_REGEX.exec(textNode.textContent)) !== null) {
                const [full, openQuote, content, closeQuote] = match;
                const start = match.index;

                // Add text before match
                if (start > lastIndex) {
                    fragment.appendChild(document.createTextNode(
                        textNode.textContent.slice(lastIndex, start)
                    ));
                }

                // Create highlight span
                const span = document.createElement('span');
                span.className = 'quote-highlight';
                span.textContent = full;

                if (CONFIG.showTooltip) {
                    span.title = `Quoted text • Click to copy`;
                }

                if (CONFIG.copyOnClick) {
                    span.addEventListener('click', (e) => {
                        e.stopImmediatePropagation();
                        navigator.clipboard.writeText(full).then(() => {
                            const original = span.style.background;
                            span.style.background = '#A8E6A8';
                            setTimeout(() => span.style.background = original, 600);
                        });
                    });
                }

                fragment.appendChild(span);
                lastIndex = start + full.length;
            }

            // Add remaining text
            if (lastIndex < textNode.textContent.length) {
                fragment.appendChild(document.createTextNode(
                    textNode.textContent.slice(lastIndex)
                ));
            }

            textNode.parentNode.replaceChild(fragment, textNode);
        });
    }

    // ================== MUTATION OBSERVER ==================
    function startObserver() {
        if (observer) observer.disconnect();

        observer = new MutationObserver(debounce((mutations) => {
            mutations.forEach(mutation => {
                if (mutation.addedNodes.length) {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === 1) highlightQuotes(node);
                    });
                }
            });
        }, CONFIG.debounceTime));

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    // ================== SETTINGS MENU ==================
    function registerMenuCommands() {
        GM_registerMenuCommand(`Quote Highlighter: ${CONFIG.enabled ? 'ON' : 'OFF'}`, () => {
            CONFIG.enabled = !CONFIG.enabled;
            GM_setValue('enabled', CONFIG.enabled);
            location.reload();
        });

        GM_registerMenuCommand("Open Settings", openSettings);
    }

    function openSettings() {
        const settingsHTML = `
        <div style="position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#222;color:#eee;padding:20px;border-radius:12px;z-index:999999;width:380px;border:2px solid #EA9D9C;">
            <h2 style="margin:0 0 15px 0;color:#EA9D9C;">Quote Highlighter Settings</h2>

            <label>Highlight Color: <input type="color" id="color" value="${CONFIG.color}"></label><br><br>
            <label>Background: <input type="color" id="bg" value="${CONFIG.background.replace('rgba(234, 157, 156, 0.15)', '#EA9D9C')}"></label><br><br>

            <label>Style:
                <select id="style">
                    <option value="color" ${CONFIG.style==='color'?'selected':''}>Text Color Only</option>
                    <option value="background" ${CONFIG.style==='background'?'selected':''}>Background</option>
                    <option value="both" ${CONFIG.style==='both'?'selected':''}>Both</option>
                    <option value="underline" ${CONFIG.style==='underline'?'selected':''}>Underline</option>
                </select>
            </label><br><br>

            <label><input type="checkbox" id="tooltip" ${CONFIG.showTooltip?'checked':''}> Show tooltip</label><br>
            <label><input type="checkbox" id="copy" ${CONFIG.copyOnClick?'checked':''}> Click to copy quote</label><br><br>

            <button id="save" style="background:#EA9D9C;color:black;padding:8px 16px;border:none;border-radius:6px;cursor:pointer;">Save & Refresh</button>
            <button id="close" style="margin-left:10px;padding:8px 16px;">Close</button>
        </div>`;

        const div = document.createElement('div');
        div.innerHTML = settingsHTML;
        document.body.appendChild(div);

        div.querySelector('#save').onclick = () => {
            CONFIG.color = div.querySelector('#color').value;
            CONFIG.background = div.querySelector('#bg').value + '33'; // add slight transparency
            CONFIG.style = div.querySelector('#style').value;
            CONFIG.showTooltip = div.querySelector('#tooltip').checked;
            CONFIG.copyOnClick = div.querySelector('#copy').checked;

            Object.keys(CONFIG).forEach(key => {
                if (typeof CONFIG[key] !== 'function') GM_setValue(key, CONFIG[key]);
            });

            location.reload();
        };

        div.querySelector('#close').onclick = () => div.remove();
    }

    // ================== INIT ==================
    function init() {
        addGlobalStyles();
        highlightQuotes(document.body);
        startObserver();
        registerMenuCommands();

        console.log('%cQuote Highlighter initialized ✓', 'color:#EA9D9C;font-weight:bold');
    }

    init();
})();


*/








/* 📌 CHATGPT Version 1:



(function() {
    'use strict';

    // --- CONFIGURATION ---
    const highlightColor = '#EA9D9C'; // Color for highlighting
    const quoteRegex = /(?<!\w)(["“”““'‘`"])(.*?)(["””””'’`.])(?!\w)/g;
    // You can adjust the regex above to change what counts as "quotes"

    // --- FUNCTION TO HIGHLIGHT QUOTED TEXT ---
    function highlightQuotes(node) {
        if (node.nodeType === Node.TEXT_NODE) {
            const parent = node.parentNode;
            let text = node.nodeValue;
            let match;
            let lastIndex = 0;
            const frag = document.createDocumentFragment();

            while ((match = quoteRegex.exec(text)) !== null) {
                // Add text before the match
                frag.appendChild(document.createTextNode(text.slice(lastIndex, match.index)));

                // Create highlighted span
                const span = document.createElement('span');
                span.textContent = match[0];
                span.style.backgroundColor = highlightColor;
                frag.appendChild(span);

                lastIndex = match.index + match[0].length;
            }

            // Add remaining text
            frag.appendChild(document.createTextNode(text.slice(lastIndex)));

            parent.replaceChild(frag, node);
        } else if (node.nodeType === Node.ELEMENT_NODE && node.nodeName !== "SCRIPT" && node.nodeName !== "STYLE") {
            for (let child of Array.from(node.childNodes)) {
                highlightQuotes(child);
            }
        }
    }

    // Run the highlighter on the whole page
    highlightQuotes(document.body);
})();


*/