💬 Quote Highlighter

Highlights quoted text with customizable colors, styles, and features

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

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

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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

})();