Twitter sol/bsc to gmgn (Optimized)

More stable and wider range recognition of sol/bsc addresses on Twitter/X and turns them into gmgn.ai links.

// ==UserScript==
// @name         Twitter sol/bsc to gmgn (Optimized)
// @namespace    http://tampermonkey.net/
// @version      0.8
// @description  More stable and wider range recognition of sol/bsc addresses on Twitter/X and turns them into gmgn.ai links.
// @author       mqtt
// @match        https://x.com/*
// @match        https://twitter.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=twitter.com
// @grant        GM_addStyle
// ==/UserScript==

(function () {
    'use strict';

    const solAddressRegex = /\b([1-9A-HJ-NP-Za-km-z]{32,44})\b/g;
    const evmAddressRegex = /\b(0x[a-fA-F0-9]{40})\b/g;

    const TARGET_SELECTORS = [
        '[data-testid="tweetText"]',                  
        '[data-testid="userProfileHeader_bio"]'       
    ].join(', ');

    const SOL_LINK_CLASS = 'sol-address-link-gemini';
    const EVM_LINK_CLASS = 'evm-address-link-gemini';
    const PROCESSED_MARKER_CLASS = 'address-link-processed-gemini';

    let processQueue = new Set();
    let processTimer = null;

    GM_addStyle(`
        a.${SOL_LINK_CLASS}, a.${EVM_LINK_CLASS} {
            color: #FF4500 !important; 
            text-decoration: underline !important;
            font-weight: bold;
            transition: all 0.2s ease;
        }
        a.${SOL_LINK_CLASS}:hover, a.${EVM_LINK_CLASS}:hover {
            color: #FF6347 !important;
            text-shadow: 0 0 5px rgba(255, 69, 0, 0.7);
        }
    `);

    function processNodes() {
        if (processQueue.size === 0) return;
        const nodesToProcess = [...processQueue];
        processQueue.clear();

        nodesToProcess.forEach(element => {
            if (element.classList.contains(PROCESSED_MARKER_CLASS)) {
                return;
            }

            const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, {
                acceptNode: (node) => {
                    if (node.parentElement.closest('script, style, a, textarea')) {
                        return NodeFilter.FILTER_REJECT;
                    }
                    if (solAddressRegex.test(node.textContent) || evmAddressRegex.test(node.textContent)) {
                        solAddressRegex.lastIndex = 0; 
                        evmAddressRegex.lastIndex = 0;
                        return NodeFilter.FILTER_ACCEPT;
                    }
                    return NodeFilter.FILTER_SKIP;
                }
            });

            let changed = false;
            const textNodes = [];
            while (walker.nextNode()) textNodes.push(walker.currentNode);

            textNodes.forEach(textNode => {
                const parent = textNode.parentElement;
                if (!parent) return;

                const text = textNode.textContent;
                const fragment = document.createDocumentFragment();
                let lastIndex = 0;

                const matches = [];
                let match;
                while ((match = solAddressRegex.exec(text)) !== null) {
                    matches.push({ type: 'sol', index: match.index, address: match[1] });
                }
                while ((match = evmAddressRegex.exec(text)) !== null) {
                    matches.push({ type: 'evm', index: match.index, address: match[1] });
                }

                if (matches.length === 0) return;

                changed = true;
                matches.sort((a, b) => a.index - b.index);

                matches.forEach(m => {
                    if (m.index > lastIndex) {
                        fragment.appendChild(document.createTextNode(text.substring(lastIndex, m.index)));
                    }
                    const link = document.createElement('a');
                    link.textContent = m.address;
                    if (m.type === 'sol') {
                        link.href = `https://gmgn.ai/sol/token/ufeXUTjX_${m.address}?filter=All`;
                        link.className = SOL_LINK_CLASS;
                    } else { // evm
                        link.href = `https://gmgn.ai/bsc/token/${m.address}`;
                        link.className = EVM_LINK_CLASS;
                    }
                    link.target = '_blank';
                    link.rel = 'noopener noreferrer';
                    link.onclick = (e) => e.stopPropagation(); // 防止点击链接触发行推文的导航事件

                    fragment.appendChild(link);
                    lastIndex = m.index + m.address.length;
                });

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

                parent.replaceChild(fragment, textNode);
            });

            if (changed) {
                element.classList.add(PROCESSED_MARKER_CLASS);
            }
        });
    }

    function enqueueNode(node) {
        if (node.nodeType === 1 && !processQueue.has(node) && !node.classList.contains(PROCESSED_MARKER_CLASS)) {
            if (node.matches(TARGET_SELECTORS)) {
                 processQueue.add(node);
            } else {
                node.querySelectorAll(TARGET_SELECTORS).forEach(innerNode => {
                     if (!processQueue.has(innerNode) && !innerNode.classList.contains(PROCESSED_MARKER_CLASS)) {
                        processQueue.add(innerNode);
                    }
                });
            }
        }

        if (processQueue.size > 0 && !processTimer) {
            processTimer = setTimeout(() => {
                processNodes();
                processTimer = null;
            }, 200); 
        }
    }

    function setupObserver() {
        const observer = new MutationObserver(mutations => {
            for (const mutation of mutations) {
                if (mutation.type === 'childList') {
                    mutation.addedNodes.forEach(node => enqueueNode(node));
                }
                if (mutation.type === 'attributes' && mutation.target.nodeType === 1) {
                    if (!mutation.target.classList.contains(PROCESSED_MARKER_CLASS)) {
                        enqueueNode(mutation.target);
                    }
                }
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: true, 
            attributeFilter: ['class'] 
        });
    }

    function initialScan() {
        document.querySelectorAll(TARGET_SELECTORS).forEach(node => enqueueNode(node));
        if (processQueue.size > 0) {
            processNodes();
        }
    }

    if (document.readyState === 'loading') {
        window.addEventListener('DOMContentLoaded', initialScan);
    } else {
        initialScan();
    }

    setupObserver();
}());