Regex Search & Highlight

Search and highlight text using regex patterns on any webpage

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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.className = 'minimized';
    panel.innerHTML = `
        <h3>
            Regex Search
            <button id="regex-toggle-btn" title="Minimize/Maximize">+</button>
        </h3>
        <div class="regex-search-content hidden">
            <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);

})();