// ==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.
})();