☰

πŸ’¬ Quote Highlighter

Highlights quoted text with customizable colors, styles, and features

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name           πŸ’¬ Quote Highlighter
// @version        1.3
// @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',                           // #b92b27 | #EA9D9C | #b92b27
        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,

        // Startup delay
        startupDelay: 1000,

        // 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  // /(?<!\w)(["β€œβ€β€œβ€œ'β€˜`"])(.*?)(["””””'’`.])(?!\w)/g
    };

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

    GM_addStyle(`
        .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;
        }

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

        ${
            CONFIG.underline
                ? `
            .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('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 = '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...');

    setTimeout(() => {
        scan(document.body);

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

        log('Observer started');
        }, CONFIG.startupDelay);
    }

    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
    };

})();