Regex Search & Highlight

Search and highlight text using regex patterns on any webpage

2025-11-21 기준 버전입니다. 최신 버전을 확인하세요.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Regex Search & Highlight
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Search and highlight text using regex patterns on any webpage
// @author       AnyPortInAHurricane & ClaudeAI
// @match        *://*/*
// @grant        GM_addStyle
// @run-at       document-idle
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // Add CSS styles
    GM_addStyle(`
        #regex-search-panel {
            position: fixed;
            top: 10px;
            right: 10px;
            z-index: 999999;
            background: #fff;
            border: 2px solid #333;
            border-radius: 8px;
            padding: 15px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.3);
            font-family: Arial, sans-serif;
            min-width: 320px;
        }
        #regex-search-panel.minimized {
            min-width: auto;
        }
        #regex-search-panel h3 {
            margin: 0 0 10px 0;
            font-size: 16px;
            color: #333;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        #regex-search-panel input[type="text"] {
            width: 100%;
            padding: 8px;
            margin-bottom: 8px;
            border: 1px solid #ccc;
            border-radius: 4px;
            font-family: monospace;
            box-sizing: border-box;
        }
        #regex-search-panel label {
            display: block;
            margin: 8px 0;
            font-size: 13px;
        }
        #regex-search-panel input[type="checkbox"] {
            margin-right: 5px;
        }
        #regex-search-panel button {
            padding: 8px 15px;
            margin: 5px 5px 0 0;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 13px;
            font-weight: bold;
        }
        #regex-search-btn {
            background: #4CAF50;
            color: white;
        }
        #regex-search-btn:hover {
            background: #45a049;
        }
        #regex-clear-btn {
            background: #f44336;
            color: white;
        }
        #regex-clear-btn:hover {
            background: #da190b;
        }
        #regex-toggle-btn {
            background: none;
            border: none;
            cursor: pointer;
            font-size: 18px;
            padding: 0;
            color: #666;
        }
        #regex-match-count {
            margin-top: 8px;
            font-size: 13px;
            color: #666;
            font-weight: bold;
        }
        .regex-highlight {
            background-color: yellow !important;
            color: black !important;
            padding: 2px 0;
            border-radius: 2px;
        }
        .regex-search-content {
            display: block;
        }
        .regex-search-content.hidden {
            display: none;
        }
    `);

    // Create the search panel
    const panel = document.createElement('div');
    panel.id = 'regex-search-panel';
    panel.innerHTML = `
        <h3>
            Regex Search
            <button id="regex-toggle-btn" title="Minimize/Maximize">−</button>
        </h3>
        <div class="regex-search-content">
            <input type="text" id="regex-pattern" placeholder="Enter regex pattern (e.g., \\b\\w+@\\w+\\.com\\b)" />
            <label>
                <input type="checkbox" id="regex-case-sensitive" />
                Case sensitive
            </label>
            <label>
                <input type="checkbox" id="regex-whole-word" />
                Whole word only
            </label>
            <div>
                <button id="regex-search-btn">Search & Highlight</button>
                <button id="regex-clear-btn">Clear</button>
            </div>
            <div id="regex-match-count"></div>
        </div>
    `;

    // Wait for body to be available
    const addPanel = () => {
        if (document.body) {
            document.body.appendChild(panel);
        } else {
            setTimeout(addPanel, 100);
        }
    };
    addPanel();

    // Store original content for restoration
    let modifiedElements = [];
    let matchCount = 0;

    // Toggle minimize/maximize
    document.addEventListener('click', (e) => {
        if (e.target.id === 'regex-toggle-btn') {
            const content = panel.querySelector('.regex-search-content');
            const btn = e.target;
            if (content.classList.contains('hidden')) {
                content.classList.remove('hidden');
                btn.textContent = '−';
                panel.classList.remove('minimized');
            } else {
                content.classList.add('hidden');
                btn.textContent = '+';
                panel.classList.add('minimized');
            }
        }
    });

    // Function to get all text nodes
    function getTextNodes(node) {
        const textNodes = [];
        const walker = document.createTreeWalker(
            node,
            NodeFilter.SHOW_TEXT,
            {
                acceptNode: (node) => {
                    // Skip script, style, and our panel
                    const parent = node.parentElement;
                    if (!parent) return NodeFilter.FILTER_REJECT;
                    const tag = parent.tagName;
                    if (tag === 'SCRIPT' || tag === 'STYLE' || tag === 'NOSCRIPT') {
                        return NodeFilter.FILTER_REJECT;
                    }
                    if (parent.closest('#regex-search-panel')) {
                        return NodeFilter.FILTER_REJECT;
                    }
                    // Only accept nodes with actual content
                    if (node.textContent.trim().length === 0) {
                        return NodeFilter.FILTER_REJECT;
                    }
                    return NodeFilter.FILTER_ACCEPT;
                }
            }
        );

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

    // Function to highlight matches
    function highlightMatches(pattern, caseSensitive, wholeWord) {
        clearHighlights();
        matchCount = 0;

        let flags = 'g';
        if (!caseSensitive) flags += 'i';

        let regex;
        try {
            if (wholeWord) {
                regex = new RegExp(`\\b(${pattern})\\b`, flags);
            } else {
                regex = new RegExp(pattern, flags);
            }
        } catch (e) {
            document.getElementById('regex-match-count').textContent = `Error: Invalid regex pattern`;
            return;
        }

        const textNodes = getTextNodes(document.body);

        textNodes.forEach(node => {
            const text = node.textContent;
            const matches = [...text.matchAll(regex)];

            if (matches.length > 0) {
                matchCount += matches.length;
                
                // Create a document fragment with highlighted text
                const fragment = document.createDocumentFragment();
                let lastIndex = 0;

                matches.forEach(match => {
                    // Add text before match
                    if (match.index > lastIndex) {
                        fragment.appendChild(document.createTextNode(text.substring(lastIndex, match.index)));
                    }

                    // Add highlighted match
                    const span = document.createElement('span');
                    span.className = 'regex-highlight';
                    span.textContent = match[0];
                    fragment.appendChild(span);

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

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

                // Store original node and replace
                modifiedElements.push({
                    node: node,
                    original: text,
                    parent: node.parentNode
                });

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

        document.getElementById('regex-match-count').textContent = 
            `Found ${matchCount} match${matchCount !== 1 ? 'es' : ''}`;

        // Scroll to first match
        if (matchCount > 0) {
            const firstHighlight = document.querySelector('.regex-highlight');
            if (firstHighlight) {
                firstHighlight.scrollIntoView({ behavior: 'smooth', block: 'center' });
            }
        }
    }

    // Function to clear highlights
    function clearHighlights() {
        // Remove all highlight spans
        document.querySelectorAll('.regex-highlight').forEach(span => {
            const parent = span.parentNode;
            parent.replaceChild(document.createTextNode(span.textContent), span);
            parent.normalize(); // Merge adjacent text nodes
        });

        modifiedElements = [];
        matchCount = 0;
        document.getElementById('regex-match-count').textContent = '';
    }

    // Event listeners
    document.getElementById('regex-search-btn').addEventListener('click', () => {
        const pattern = document.getElementById('regex-pattern').value;
        const caseSensitive = document.getElementById('regex-case-sensitive').checked;
        const wholeWord = document.getElementById('regex-whole-word').checked;

        if (!pattern) {
            alert('Please enter a regex pattern');
            return;
        }

        highlightMatches(pattern, caseSensitive, wholeWord);
    });

    document.getElementById('regex-clear-btn').addEventListener('click', () => {
        clearHighlights();
        document.getElementById('regex-pattern').value = '';
    });

    // Allow Enter key to search
    document.getElementById('regex-pattern').addEventListener('keypress', (e) => {
        if (e.key === 'Enter') {
            document.getElementById('regex-search-btn').click();
        }
    });

    // Handle dynamic content changes (for sites like Reddit)
    const observer = new MutationObserver(() => {
        // Re-apply highlights when DOM changes if there's an active search
        if (matchCount > 0) {
            const pattern = document.getElementById('regex-pattern').value;
            const caseSensitive = document.getElementById('regex-case-sensitive').checked;
            const wholeWord = document.getElementById('regex-whole-word').checked;
            if (pattern) {
                setTimeout(() => highlightMatches(pattern, caseSensitive, wholeWord), 100);
            }
        }
    });

    // Start observing with a delay to avoid initial page load chaos
    setTimeout(() => {
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }, 2000);

})();