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.

  1. // ==UserScript==
  2. // @name Google Sheets Diff alerts mods & Google Search Link Cleaner
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.8
  5. // @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.
  6. // @author luascfl
  7. // @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
  8. // @match https://docs.google.com/spreadsheets/d/*/notify/show*
  9. // @match https://docs.google.com/spreadsheets/u/*/d/*/revisions/show*
  10. // @match https://cse.google.com/*
  11. // @match https://www.google.com/search*
  12. // @home https://github.com/luascfl/gsheets-diff-alerts-mods
  13. // @supportURL https://github.com/luascfl/gsheets-diff-alerts-mods/issues
  14. // @license MIT
  15. // @grant none
  16. // @run-at document-start
  17. // ==/UserScript==
  18. (function() {
  19. 'use strict';
  20. const INTERVALO = 100; // Interval in milliseconds
  21.  
  22. // --- Helper Functions ---
  23.  
  24. // Function to check if the current page is a Google Sheets diff/revision page
  25. function isSheetsPage() {
  26. const href = window.location.href;
  27. return href.includes('/spreadsheets/d/') && (href.includes('/notify/show') || href.includes('/revisions/show'));
  28. }
  29.  
  30. // Function to remove row headers (only on Sheets pages)
  31. function removerElementos() {
  32. if (isSheetsPage()) {
  33. document.querySelectorAll('.row-header-wrapper').forEach(el => {
  34. el.remove();
  35. });
  36. }
  37. }
  38.  
  39. // Function to modify links (redirects and srsltid) and decode text
  40. function modificarLinksETextos() {
  41. // Process all links on the page for redirects and srsltid
  42. document.querySelectorAll('a').forEach(link => {
  43. processarLink(link);
  44. });
  45.  
  46. // Only decode %3D within table bodies (relevant for Sheets/CSE)
  47. if (isSheetsPage() || window.location.href.includes('cse.google.com')) {
  48. document.querySelectorAll('tbody a').forEach(link => {
  49. decodificarEncodingNoLink(link); // Decode %3D in table links
  50. });
  51. decodificarTextosEmTbody(); // Decode %3D in table text
  52. }
  53. }
  54.  
  55. // Function to process each individual link - MODIFIED
  56. function processarLink(link) {
  57. let currentHref = link.getAttribute('href'); // Get current href attribute value
  58. let currentDataHref = link.getAttribute('data-href'); // Get current data-href attribute value
  59. let hrefChanged = false;
  60. let dataHrefChanged = false;
  61.  
  62. // 1. Handle Google Redirects (google.com/url?q=...)
  63. if (currentHref && currentHref.includes('google.com/url?')) {
  64. try {
  65. const urlObj = new URL(currentHref);
  66. const params = urlObj.searchParams;
  67. if (params.has('q')) {
  68. currentHref = params.get('q'); // Update currentHref with the real URL
  69. hrefChanged = true;
  70. }
  71. } catch (e) {
  72. console.warn('Erro ao processar URL de redirecionamento (href):', link.href, e);
  73. }
  74. }
  75. // Also check data-href for redirects
  76. if (currentDataHref && currentDataHref.includes('google.com/url?')) {
  77. try {
  78. const dataUrlObj = new URL(currentDataHref);
  79. const dataParams = dataUrlObj.searchParams;
  80. if (dataParams.has('q')) {
  81. currentDataHref = dataParams.get('q'); // Update currentDataHref
  82. dataHrefChanged = true;
  83. }
  84. } catch (e) {
  85. console.warn('Erro ao processar URL de redirecionamento (data-href):', link.getAttribute('data-href'), e);
  86. }
  87. }
  88.  
  89. // 2. Remove srsltid parameter from the potentially updated href
  90. if (currentHref && (currentHref.includes('?srsltid=') || currentHref.includes('&srsltid='))) {
  91. try {
  92. const urlObj = new URL(currentHref);
  93. if (urlObj.searchParams.has('srsltid')) {
  94. urlObj.searchParams.delete('srsltid');
  95. currentHref = urlObj.toString();
  96. // If the URL ends with '?' after removal, remove it too
  97. if (currentHref.endsWith('?')) {
  98. currentHref = currentHref.slice(0, -1);
  99. }
  100. hrefChanged = true;
  101. }
  102. } catch (e) {
  103. console.warn('Erro ao remover srsltid (href):', currentHref, e);
  104. // Attempt simple string replacement as fallback (less robust)
  105. const paramIndex = currentHref.indexOf('srsltid=');
  106. if (paramIndex > 0) {
  107. const charBefore = currentHref[paramIndex - 1];
  108. if (charBefore === '?' || charBefore === '&') {
  109. // Find the end of the parameter (next '&' or end of string)
  110. const nextAmp = currentHref.indexOf('&', paramIndex);
  111. if (nextAmp !== -1) {
  112. currentHref = currentHref.substring(0, paramIndex -1) + currentHref.substring(nextAmp); // Remove '&srsltid=...'
  113. } else {
  114. currentHref = currentHref.substring(0, paramIndex -1); // Remove '?srsltid=...' or '&srsltid=...' at the end
  115. }
  116. hrefChanged = true;
  117. }
  118. }
  119. }
  120. }
  121. // Also remove srsltid from the potentially updated data-href
  122. if (currentDataHref && (currentDataHref.includes('?srsltid=') || currentDataHref.includes('&srsltid='))) {
  123. try {
  124. const dataUrlObj = new URL(currentDataHref);
  125. if (dataUrlObj.searchParams.has('srsltid')) {
  126. dataUrlObj.searchParams.delete('srsltid');
  127. currentDataHref = dataUrlObj.toString();
  128. // If the URL ends with '?' after removal, remove it too
  129. if (currentDataHref.endsWith('?')) {
  130. currentDataHref = currentDataHref.slice(0, -1);
  131. }
  132. dataHrefChanged = true;
  133. }
  134. } catch (e) {
  135. console.warn('Erro ao remover srsltid (data-href):', currentDataHref, e);
  136. // Add fallback string replacement for data-href if needed, similar to href
  137. }
  138. }
  139.  
  140.  
  141. // 3. Apply changes if any occurred
  142. if (hrefChanged) {
  143. link.href = currentHref; // Set the final href property
  144. }
  145. if (dataHrefChanged) {
  146. link.setAttribute('data-href', currentDataHref); // Set the final data-href attribute
  147. }
  148. }
  149.  
  150. // Function specifically for decoding %3D to = in links within tbody
  151. function decodificarEncodingNoLink(link) {
  152. // Decode %3D to = in href (both property and attribute)
  153. let hrefChanged = false;
  154. let currentHref = link.getAttribute('href');
  155. if (currentHref && currentHref.includes('%3D')) {
  156. currentHref = currentHref.replaceAll('%3D', '=');
  157. hrefChanged = true;
  158. }
  159. if (hrefChanged) {
  160. link.setAttribute('href', currentHref);
  161. // Also update the property in case the attribute update doesn't reflect immediately
  162. link.href = currentHref;
  163. }
  164.  
  165.  
  166. // Decode %3D to = in data-href if it exists
  167. if (link.hasAttribute('data-href')) {
  168. const dataHref = link.getAttribute('data-href');
  169. if (dataHref.includes('%3D')) {
  170. link.setAttribute('data-href', dataHref.replaceAll('%3D', '='));
  171. }
  172. }
  173.  
  174. // Decode %3D to = in the link's text content if needed
  175. if (link.textContent.includes('%3D')) {
  176. link.textContent = link.textContent.replaceAll('%3D', '=');
  177. }
  178.  
  179. // Check if visible text is correct but href is not (re-check after potential changes)
  180. if (link.textContent.includes('=') && !link.textContent.includes('%3D')) {
  181. let hrefAttr = link.getAttribute('href'); // Get potentially updated href
  182. if (hrefAttr && hrefAttr.includes('%3D')) {
  183. const paramsInText = link.textContent.match(/[?&][^?&=]+=[^?&=]+/g);
  184. if (paramsInText) {
  185. let hrefAtual = hrefAttr;
  186. paramsInText.forEach(param => {
  187. const [paramName, paramValue] = param.substring(1).split('=');
  188. // Look for the encoded version in the href
  189. const encodedParam = `${paramName}%3D${encodeURIComponent(paramValue)}`; // More robust encoding check might be needed
  190. const encodedParamSimple = `${paramName}%3D${paramValue}`; // Simpler check
  191.  
  192. if (hrefAtual.includes(encodedParamSimple)) {
  193. hrefAtual = hrefAtual.replaceAll(encodedParamSimple, `${paramName}=${paramValue}`);
  194. } else if (hrefAtual.includes(encodedParam)) {
  195. hrefAtual = hrefAtual.replaceAll(encodedParam, `${paramName}=${paramValue}`);
  196. }
  197. });
  198. if (hrefAtual !== hrefAttr) {
  199. link.setAttribute('href', hrefAtual);
  200. link.href = hrefAtual; // Update property too
  201. }
  202. }
  203. }
  204. }
  205. }
  206.  
  207.  
  208. // Function to decode %3D in all text elements within tbody
  209. function decodificarTextosEmTbody() {
  210. document.querySelectorAll('tbody').forEach(tbody => {
  211. iterarESusbstituirTextoEmElemento(tbody);
  212. });
  213. }
  214.  
  215. // Recursive function to iterate over all child nodes and replace text
  216. function iterarESusbstituirTextoEmElemento(elemento) {
  217. Array.from(elemento.childNodes).forEach(node => {
  218. if (node.nodeType === Node.TEXT_NODE) {
  219. if (node.textContent.includes('%3D')) {
  220. node.textContent = node.textContent.replaceAll('%3D', '=');
  221. }
  222. } else if (node.nodeType === Node.ELEMENT_NODE && node.nodeName !== 'A') { // Don't re-process links here
  223. // Check common attributes
  224. ['value', 'title', 'alt', 'placeholder', 'data-text'].forEach(attr => {
  225. let attrValue = null;
  226. if (attr === 'value' && node.value && typeof node.value === 'string') {
  227. attrValue = node.value;
  228. if (attrValue.includes('%3D')) {
  229. node.value = attrValue.replaceAll('%3D', '=');
  230. }
  231. } else if (node.hasAttribute(attr)) {
  232. attrValue = node.getAttribute(attr);
  233. if (attrValue.includes('%3D')) {
  234. node.setAttribute(attr, attrValue.replaceAll('%3D', '='));
  235. }
  236. }
  237. });
  238. // Recurse into children
  239. iterarESusbstituirTextoEmElemento(node);
  240. }
  241. });
  242. }
  243.  
  244. // Function that combines all functionalities
  245. function processarPagina() {
  246. removerElementos(); // Runs conditionally inside the function
  247. modificarLinksETextos(); // Processes links/text based on page type inside
  248. }
  249.  
  250. // --- Execution Logic ---
  251.  
  252. // Configuration of the observer to detect DOM changes
  253. let observer;
  254. // Use a simple debounce to avoid excessive processing with the observer
  255. let timeoutId;
  256. const debouncedProcessarPagina = () => {
  257. clearTimeout(timeoutId);
  258. timeoutId = setTimeout(processarPagina, 50); // 50ms delay for debounce
  259. };
  260.  
  261. const callback = (mutationsList, observerInstance) => {
  262. // Check if any relevant mutation occurred (node addition or attribute changes)
  263. // This avoids reprocessing the entire page for trivial changes
  264. let relevantChange = false;
  265. for (const mutation of mutationsList) {
  266. if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
  267. // Check if added nodes contain links or if we are on sheets page where structure might change
  268. let addedLinks = false;
  269. mutation.addedNodes.forEach(node => {
  270. if (node.nodeType === Node.ELEMENT_NODE) {
  271. if (node.matches('a') || node.querySelector('a')) {
  272. addedLinks = true;
  273. }
  274. // On sheets page, any table change could be relevant
  275. if (isSheetsPage() && (node.matches('tbody, tr, td') || node.querySelector('tbody, tr, td'))) {
  276. addedLinks = true; // Treat table changes as relevant for Sheets
  277. }
  278. }
  279. });
  280. if(addedLinks){
  281. relevantChange = true;
  282. break;
  283. }
  284. }
  285. if (mutation.type === 'attributes' && (mutation.attributeName === 'href' || mutation.attributeName === 'data-href')) {
  286. relevantChange = true;
  287. break;
  288. }
  289. }
  290. if (relevantChange) {
  291. debouncedProcessarPagina();
  292. }
  293. };
  294.  
  295. // Execute immediately and maintain interval (Interval may be less necessary with Observer)
  296. (function loop() {
  297. processarPagina(); // Execute once
  298. // The loop can be removed or have a longer interval if the Observer is reliable
  299. setTimeout(loop, INTERVALO * 10); // Increased interval, as the observer should catch most changes
  300. })();
  301.  
  302. // Ensure execution after initial full load
  303. window.addEventListener('load', () => {
  304. processarPagina();
  305. // Start the observer after the initial load and first processing
  306. if (!observer) {
  307. observer = new MutationObserver(callback); // Use the callback defined above
  308. observer.observe(document.documentElement || document.body, {
  309. childList: true,
  310. subtree: true,
  311. attributes: true, // Observe attribute changes too (important for href/data-href)
  312. attributeFilter: ['href', 'data-href'] // Focus on relevant attributes
  313. });
  314. }
  315. });
  316.  
  317. // DOMNodeInserted listener is legacy and can cause performance issues.
  318. // The MutationObserver above is the modern and more efficient way.
  319. // Removed the old listener.
  320.  
  321. })();