☰

πŸ’¬ Quote Highlighter

Highlights quoted text with customizable colors, styles, and features

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ этот скрипт, Π²Ρ‹ сначала Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π°, Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€ Tampermonkey, Greasemonkey ΠΈΠ»ΠΈ Violentmonkey.

Для установки этого скрипта Π²Π°ΠΌ Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΠΎ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅, Ρ‚Π°ΠΊΠΎΠ΅ ΠΊΠ°ΠΊ Tampermonkey.

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ этот скрипт, Π²Ρ‹ сначала Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π°, Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€ Tampermonkey ΠΈΠ»ΠΈ Violentmonkey.

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ этот скрипт, Π²Ρ‹ сначала Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π°, Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€ Tampermonkey ΠΈΠ»ΠΈ Userscripts.

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ этот скрипт, сначала Π²Ρ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π°, Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€ Tampermonkey.

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ этот скрипт, Π²Ρ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ β€” ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ€ скриптов.

(Ρƒ мСня ΡƒΠΆΠ΅ Π΅ΡΡ‚ΡŒ ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ€ скриптов, Π΄Π°ΠΉΡ‚Π΅ ΠΌΠ½Π΅ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ скрипт!)

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ этот ΡΡ‚ΠΈΠ»ΡŒ, сначала Π²Ρ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π°, Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€ Stylus.

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ этот ΡΡ‚ΠΈΠ»ΡŒ, сначала Π²Ρ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π°, Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€ Stylus.

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ этот ΡΡ‚ΠΈΠ»ΡŒ, сначала Π²Ρ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π°, Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€ Stylus.

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ этот ΡΡ‚ΠΈΠ»ΡŒ, сначала Π²Ρ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ β€” ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ€ стилСй.

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ этот ΡΡ‚ΠΈΠ»ΡŒ, сначала Π²Ρ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ β€” ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ€ стилСй.

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ этот ΡΡ‚ΠΈΠ»ΡŒ, сначала Π²Ρ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ β€” ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ€ стилСй.

(Ρƒ мСня ΡƒΠΆΠ΅ Π΅ΡΡ‚ΡŒ ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ€ стилСй, Π΄Π°ΠΉΡ‚Π΅ ΠΌΠ½Π΅ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ скрипт!)

// ==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);
})();


*/