💬 Quote Highlighter

Highlights quoted text with customizable colors, styles, and features

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

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

})();