Google Sheets Diff alerts mods & Google Search Link Cleaner

Remove .row-header-wrapper elements on Sheets diffs, modify Google redirect links (including CSE & Search), remove srsltid parameter from links, and decode %3D to = in table text.

// ==UserScript==
// @name            Google Sheets Diff alerts mods & Google Search Link Cleaner
// @namespace       http://tampermonkey.net/
// @version         3.8
// @description     Remove .row-header-wrapper elements on Sheets diffs, modify Google redirect links (including CSE & Search), remove srsltid parameter from links, and decode %3D to = in table text.
// @author          luascfl
// @icon            https://e7.pngegg.com/pngimages/660/350/png-clipart-green-and-white-sheet-icon-google-docs-google-sheets-spreadsheet-g-suite-google-angle-rectangle-thumbnail.png
// @match           https://docs.google.com/spreadsheets/d/*/notify/show*
// @match           https://docs.google.com/spreadsheets/u/*/d/*/revisions/show*
// @match           https://cse.google.com/*
// @match           https://www.google.com/search*
// @home            https://github.com/luascfl/gsheets-diff-alerts-mods
// @supportURL      https://github.com/luascfl/gsheets-diff-alerts-mods/issues
// @license         MIT
// @grant           none
// @run-at          document-start
// ==/UserScript==
(function() {
    'use strict';
    const INTERVALO = 100; // Interval in milliseconds

    // --- Helper Functions ---

    // Function to check if the current page is a Google Sheets diff/revision page
    function isSheetsPage() {
        const href = window.location.href;
        return href.includes('/spreadsheets/d/') && (href.includes('/notify/show') || href.includes('/revisions/show'));
    }

    // Function to remove row headers (only on Sheets pages)
    function removerElementos() {
        if (isSheetsPage()) {
            document.querySelectorAll('.row-header-wrapper').forEach(el => {
                el.remove();
            });
        }
    }

    // Function to modify links (redirects and srsltid) and decode text
    function modificarLinksETextos() {
        // Process all links on the page for redirects and srsltid
        document.querySelectorAll('a').forEach(link => {
            processarLink(link);
        });

        // Only decode %3D within table bodies (relevant for Sheets/CSE)
        if (isSheetsPage() || window.location.href.includes('cse.google.com')) {
            document.querySelectorAll('tbody a').forEach(link => {
                decodificarEncodingNoLink(link); // Decode %3D in table links
            });
            decodificarTextosEmTbody(); // Decode %3D in table text
        }
    }

    // Function to process each individual link - MODIFIED
    function processarLink(link) {
        let currentHref = link.getAttribute('href'); // Get current href attribute value
        let currentDataHref = link.getAttribute('data-href'); // Get current data-href attribute value
        let hrefChanged = false;
        let dataHrefChanged = false;

        // 1. Handle Google Redirects (google.com/url?q=...)
        if (currentHref && currentHref.includes('google.com/url?')) {
            try {
                const urlObj = new URL(currentHref);
                const params = urlObj.searchParams;
                if (params.has('q')) {
                    currentHref = params.get('q'); // Update currentHref with the real URL
                    hrefChanged = true;
                }
            } catch (e) {
                console.warn('Erro ao processar URL de redirecionamento (href):', link.href, e);
            }
        }
        // Also check data-href for redirects
        if (currentDataHref && currentDataHref.includes('google.com/url?')) {
             try {
                const dataUrlObj = new URL(currentDataHref);
                const dataParams = dataUrlObj.searchParams;
                if (dataParams.has('q')) {
                    currentDataHref = dataParams.get('q'); // Update currentDataHref
                    dataHrefChanged = true;
                }
            } catch (e) {
                console.warn('Erro ao processar URL de redirecionamento (data-href):', link.getAttribute('data-href'), e);
            }
        }

        // 2. Remove srsltid parameter from the potentially updated href
        if (currentHref && (currentHref.includes('?srsltid=') || currentHref.includes('&srsltid='))) {
            try {
                const urlObj = new URL(currentHref);
                if (urlObj.searchParams.has('srsltid')) {
                    urlObj.searchParams.delete('srsltid');
                    currentHref = urlObj.toString();
                    // If the URL ends with '?' after removal, remove it too
                    if (currentHref.endsWith('?')) {
                        currentHref = currentHref.slice(0, -1);
                    }
                    hrefChanged = true;
                }
            } catch (e) {
                console.warn('Erro ao remover srsltid (href):', currentHref, e);
                // Attempt simple string replacement as fallback (less robust)
                const paramIndex = currentHref.indexOf('srsltid=');
                if (paramIndex > 0) {
                    const charBefore = currentHref[paramIndex - 1];
                    if (charBefore === '?' || charBefore === '&') {
                         // Find the end of the parameter (next '&' or end of string)
                         const nextAmp = currentHref.indexOf('&', paramIndex);
                         if (nextAmp !== -1) {
                             currentHref = currentHref.substring(0, paramIndex -1) + currentHref.substring(nextAmp); // Remove '&srsltid=...'
                         } else {
                             currentHref = currentHref.substring(0, paramIndex -1); // Remove '?srsltid=...' or '&srsltid=...' at the end
                         }
                         hrefChanged = true;
                    }
                }
            }
        }
         // Also remove srsltid from the potentially updated data-href
        if (currentDataHref && (currentDataHref.includes('?srsltid=') || currentDataHref.includes('&srsltid='))) {
             try {
                const dataUrlObj = new URL(currentDataHref);
                 if (dataUrlObj.searchParams.has('srsltid')) {
                    dataUrlObj.searchParams.delete('srsltid');
                    currentDataHref = dataUrlObj.toString();
                    // If the URL ends with '?' after removal, remove it too
                    if (currentDataHref.endsWith('?')) {
                        currentDataHref = currentDataHref.slice(0, -1);
                    }
                    dataHrefChanged = true;
                }
            } catch (e) {
                console.warn('Erro ao remover srsltid (data-href):', currentDataHref, e);
                // Add fallback string replacement for data-href if needed, similar to href
            }
        }


        // 3. Apply changes if any occurred
        if (hrefChanged) {
            link.href = currentHref; // Set the final href property
        }
        if (dataHrefChanged) {
            link.setAttribute('data-href', currentDataHref); // Set the final data-href attribute
        }
    }

    // Function specifically for decoding %3D to = in links within tbody
    function decodificarEncodingNoLink(link) {
        // Decode %3D to = in href (both property and attribute)
        let hrefChanged = false;
        let currentHref = link.getAttribute('href');
        if (currentHref && currentHref.includes('%3D')) {
            currentHref = currentHref.replaceAll('%3D', '=');
            hrefChanged = true;
        }
        if (hrefChanged) {
             link.setAttribute('href', currentHref);
             // Also update the property in case the attribute update doesn't reflect immediately
             link.href = currentHref;
        }


        // Decode %3D to = in data-href if it exists
        if (link.hasAttribute('data-href')) {
            const dataHref = link.getAttribute('data-href');
            if (dataHref.includes('%3D')) {
                link.setAttribute('data-href', dataHref.replaceAll('%3D', '='));
            }
        }

        // Decode %3D to = in the link's text content if needed
        if (link.textContent.includes('%3D')) {
            link.textContent = link.textContent.replaceAll('%3D', '=');
        }

        // Check if visible text is correct but href is not (re-check after potential changes)
        if (link.textContent.includes('=') && !link.textContent.includes('%3D')) {
            let hrefAttr = link.getAttribute('href'); // Get potentially updated href
            if (hrefAttr && hrefAttr.includes('%3D')) {
                 const paramsInText = link.textContent.match(/[?&][^?&=]+=[^?&=]+/g);
                 if (paramsInText) {
                     let hrefAtual = hrefAttr;
                     paramsInText.forEach(param => {
                         const [paramName, paramValue] = param.substring(1).split('=');
                         // Look for the encoded version in the href
                         const encodedParam = `${paramName}%3D${encodeURIComponent(paramValue)}`; // More robust encoding check might be needed
                         const encodedParamSimple = `${paramName}%3D${paramValue}`; // Simpler check

                         if (hrefAtual.includes(encodedParamSimple)) {
                            hrefAtual = hrefAtual.replaceAll(encodedParamSimple, `${paramName}=${paramValue}`);
                         } else if (hrefAtual.includes(encodedParam)) {
                              hrefAtual = hrefAtual.replaceAll(encodedParam, `${paramName}=${paramValue}`);
                         }
                     });
                     if (hrefAtual !== hrefAttr) {
                         link.setAttribute('href', hrefAtual);
                         link.href = hrefAtual; // Update property too
                     }
                 }
            }
        }
    }


    // Function to decode %3D in all text elements within tbody
    function decodificarTextosEmTbody() {
        document.querySelectorAll('tbody').forEach(tbody => {
            iterarESusbstituirTextoEmElemento(tbody);
        });
    }

    // Recursive function to iterate over all child nodes and replace text
    function iterarESusbstituirTextoEmElemento(elemento) {
        Array.from(elemento.childNodes).forEach(node => {
            if (node.nodeType === Node.TEXT_NODE) {
                if (node.textContent.includes('%3D')) {
                    node.textContent = node.textContent.replaceAll('%3D', '=');
                }
            } else if (node.nodeType === Node.ELEMENT_NODE && node.nodeName !== 'A') { // Don't re-process links here
                // Check common attributes
                 ['value', 'title', 'alt', 'placeholder', 'data-text'].forEach(attr => {
                    let attrValue = null;
                    if (attr === 'value' && node.value && typeof node.value === 'string') {
                         attrValue = node.value;
                         if (attrValue.includes('%3D')) {
                             node.value = attrValue.replaceAll('%3D', '=');
                         }
                    } else if (node.hasAttribute(attr)) {
                        attrValue = node.getAttribute(attr);
                        if (attrValue.includes('%3D')) {
                            node.setAttribute(attr, attrValue.replaceAll('%3D', '='));
                        }
                    }
                });
                // Recurse into children
                iterarESusbstituirTextoEmElemento(node);
            }
        });
    }

    // Function that combines all functionalities
    function processarPagina() {
        removerElementos(); // Runs conditionally inside the function
        modificarLinksETextos(); // Processes links/text based on page type inside
    }

    // --- Execution Logic ---

    // Configuration of the observer to detect DOM changes
    let observer;
    // Use a simple debounce to avoid excessive processing with the observer
    let timeoutId;
    const debouncedProcessarPagina = () => {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(processarPagina, 50); // 50ms delay for debounce
    };

    const callback = (mutationsList, observerInstance) => {
        // Check if any relevant mutation occurred (node addition or attribute changes)
        // This avoids reprocessing the entire page for trivial changes
        let relevantChange = false;
        for (const mutation of mutationsList) {
            if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                 // Check if added nodes contain links or if we are on sheets page where structure might change
                 let addedLinks = false;
                 mutation.addedNodes.forEach(node => {
                     if (node.nodeType === Node.ELEMENT_NODE) {
                         if (node.matches('a') || node.querySelector('a')) {
                             addedLinks = true;
                         }
                         // On sheets page, any table change could be relevant
                         if (isSheetsPage() && (node.matches('tbody, tr, td') || node.querySelector('tbody, tr, td'))) {
                             addedLinks = true; // Treat table changes as relevant for Sheets
                         }
                     }
                 });
                 if(addedLinks){
                    relevantChange = true;
                    break;
                 }
            }
            if (mutation.type === 'attributes' && (mutation.attributeName === 'href' || mutation.attributeName === 'data-href')) {
                relevantChange = true;
                break;
            }
        }
        if (relevantChange) {
           debouncedProcessarPagina();
        }
    };

    // Execute immediately and maintain interval (Interval may be less necessary with Observer)
    (function loop() {
        processarPagina(); // Execute once
        // The loop can be removed or have a longer interval if the Observer is reliable
        setTimeout(loop, INTERVALO * 10); // Increased interval, as the observer should catch most changes
    })();

    // Ensure execution after initial full load
     window.addEventListener('load', () => {
         processarPagina();
         // Start the observer after the initial load and first processing
         if (!observer) {
             observer = new MutationObserver(callback); // Use the callback defined above
             observer.observe(document.documentElement || document.body, {
                 childList: true,
                 subtree: true,
                 attributes: true, // Observe attribute changes too (important for href/data-href)
                 attributeFilter: ['href', 'data-href'] // Focus on relevant attributes
             });
         }
     });

    // DOMNodeInserted listener is legacy and can cause performance issues.
    // The MutationObserver above is the modern and more efficient way.
    // Removed the old listener.

})();