XCancel.com to X.com Redirector

Redirects XCancel.com links to X.com

// ==UserScript==
// @name         XCancel.com to X.com Redirector
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Redirects XCancel.com links to X.com
// @author       drowned1
// @match        *://*/*
// @grant        none
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const sourceDomain = 'xcancel.com';
    const targetDomain = 'x.com';
    const sourceHrefSelector = `a[href*="${sourceDomain}"]`;

    // Function to replace links and text within a specific element or node list
    function replaceInElements(elements) {
        if (!elements) return;

        // Ensure elements is iterable (NodeList, Array, or single element)
        const elementList = (elements instanceof Node || typeof elements.forEach !== 'function') ? [elements] : elements;

        elementList.forEach(element => {
            // Only process element nodes
            if (element.nodeType !== Node.ELEMENT_NODE) return;

            // Replace href attributes in anchor tags within the element or if it is an anchor itself
            if (element.matches && element.matches(sourceHrefSelector)) {
                 if (element.href.includes(sourceDomain)) {
                    element.href = element.href.replace(sourceDomain, targetDomain);
                 }
            }
            const links = element.querySelectorAll(sourceHrefSelector);
            links.forEach(link => {
                if (link.href.includes(sourceDomain)) {
                    link.href = link.href.replace(sourceDomain, targetDomain);
                }
            });

            // --- Optional Text Replacement (More Performant but still potentially heavy) ---
            // Consider if text replacement is truly necessary, as it's more complex and performance-intensive.
            // This version uses TreeWalker scoped to the current element.
            /*
            const walker = document.createTreeWalker(
                element,
                NodeFilter.SHOW_TEXT,
                { acceptNode: function(node) {
                    // Skip nodes inside script/style tags and check for the source domain
                    const parentTag = node.parentElement?.tagName?.toLowerCase();
                    if (parentTag !== 'script' && parentTag !== 'style' && node.nodeValue.includes(sourceDomain)) {
                        return NodeFilter.FILTER_ACCEPT;
                    }
                    return NodeFilter.FILTER_REJECT;
                  }
                },
                false
            );

            let node;
            while (node = walker.nextNode()) {
                node.nodeValue = node.nodeValue.replace(new RegExp(sourceDomain.replace('.', '\\.'), 'g'), targetDomain);
            }
            */
            // --- End Optional Text Replacement ---
        });
    }

    // Initial run on existing content when DOM is ready
    function runInitialScan() {
        // Use querySelectorAll directly on the document for the initial scan
        const initialLinks = document.querySelectorAll(sourceHrefSelector);
        initialLinks.forEach(link => {
             if (link.href.includes(sourceDomain)) {
                link.href = link.href.replace(sourceDomain, targetDomain);
             }
        });

        // If enabling text replacement, uncomment the initial text scan:
        /*
        replaceInElements(document.body); // Run text replacement on the whole body once initially
        */
    }

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

    // Optimized MutationObserver: Process only added nodes
    const observer = new MutationObserver((mutationsList) => {
        for (const mutation of mutationsList) {
            if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                 // Process only the nodes that were actually added
                 replaceInElements(mutation.addedNodes);
            }
            // Note: CharacterData mutations are ignored here for performance.
            // If text replacement within existing nodes is crucial, you'd need
            // to handle mutation.type === 'characterData' carefully,
            // potentially checking mutation.target and its parent elements.
        }
    });

    // Observe the body once it exists
    const observerConfig = { childList: true, subtree: true };
    if (document.body) {
        observer.observe(document.body, observerConfig);
    } else {
        // If body doesn't exist yet (e.g., @run-at document-start), wait for it.
        new MutationObserver((_, obs) => {
            if (document.body) {
                observer.observe(document.body, observerConfig);
                // Also run the initial scan now that the body exists, in case DOMContentLoaded fired early
                runInitialScan();
                obs.disconnect(); // Stop observing the document element
            }
        }).observe(document.documentElement, { childList: true });
    }


    // --- Intercept network requests (Optimized with checks) ---

    const originalOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(method, url) {
        if (url && typeof url === 'string' && url.includes(sourceDomain)) {
            arguments[1] = url.replace(sourceDomain, targetDomain);
        }
        return originalOpen.apply(this, arguments);
    };

    const originalFetch = window.fetch;
    window.fetch = function(input, init) {
        let url = input instanceof Request ? input.url : input;
        let request = input instanceof Request ? input : null;

        if (typeof url === 'string' && url.includes(sourceDomain)) {
            const newUrl = url.replace(sourceDomain, targetDomain);
            if (request) {
                 // Clone Request object with modified URL
                 const newRequestInit = {};
                 // Manually list properties to clone for broader compatibility
                 ['method', 'headers', 'body', 'mode', 'credentials', 'cache', 'redirect', 'referrer', 'integrity', 'keepalive', 'signal'].forEach(prop => {
                     if (request[prop] !== undefined) {
                        newRequestInit[prop] = request[prop];
                     }
                 });
                 arguments[0] = new Request(newUrl, newRequestInit);
            } else {
                arguments[0] = newUrl;
            }
        }
        return originalFetch.apply(this, arguments);
    };

    // --- Cleanup ---
    window.addEventListener('unload', () => {
        if (observer) {
            observer.disconnect();
        }
    });

})();