// ==UserScript== // @name URL Modifier for Search Engines // @namespace http://tampermonkey.net/ // @version 1.9 // @description Modify URLs in search results of search engines // @author Domenic // @match *://www.google.com/search?*q=* // @match *://searx.tiekoetter.com/search* // @match *://search.disroot.org/search* // @match *://www.startpage.com/search* // @match *://www.startpage.com/sp/search* // @match *://search.brave.com/search* // @match *://duckduckgo.com // @match *://duckduckgo.com/?*q=* // @match *://metager.org/meta/meta.ger3* // @match *://metager.de/meta/meta.ger3* // @match *://www.mojeek.com/search?q=* // @grant none // @run-at document-end // @license GPL-2.0-only // @downloadURL none // ==/UserScript== (function() { 'use strict'; // Define URL modification rules with precompiled regex const urlModificationRules = [ { matchRegex: new RegExp(/^https?:\/\/www\.reddit\.com(.*)/), replaceWith: 'https://old.reddit.com$1' }, { matchRegex: new RegExp(/^https?:\/\/twitter\.com\/([A-Za-z_][\w]+)(\/status\/(\d+))?.*/), replaceWith: 'https://nitter.net/$1$2' }, { matchRegex: new RegExp(/^https?:\/\/(?:www\.)?youtube\.com\/(@[\w-]+|watch\?v=[\w-]+|playlist\?list=[\w-]+)/), replaceWith: 'https://yewtu.be/$1' // replaceWith: 'https://piped.video/$1' }, { matchRegex: new RegExp(/^https?:\/\/stackoverflow\.com(\/questions\/\d+\/.*)/), replaceWith: 'https://code.whatever.social$1' }, { matchRegex: new RegExp(/^https?:\/\/(?:en\.?m?|simple)\.wikipedia\.org\/wiki\/(?!Special:Search)(.*)/), replaceWith: 'https://www.wikiwand.com/en/$1' }, { matchRegex: new RegExp(/^https?:\/\/zh\.?m?\.wikipedia\.org\/(?:zh-hans|wiki)\/(.*)/), replaceWith: 'https://www.wikiwand.com/zh-hans/$1' }, { matchRegex: new RegExp(/https?:\/\/([a-z]+)\.?m?\.wikipedia\.org\/(?:[a-z]+|wiki)\/(.*)/), replaceWith: 'https://www.wikiwand.com/$1/$2' }, { matchRegex: new RegExp(/^https?:\/\/((?:(?:\w+\.)?medium|towardsdatascience)\.com\/.*)/), replaceWith: 'https://freedium.cfd/https://$1' }, { matchRegex: new RegExp(/^https?:\/\/imgur\.com\/(a\/)?((?!gallery)\w+)/), replaceWith: 'https://rimgo.totaldarkness.net/a/$1$2' }, { matchRegex: new RegExp(/^https?:\/\/www\.npr\.org\/(?:\d{4}\/\d{2}\/\d{2}|sections)\/(?:[A-Za-z-]+\/\d{4}\/\d{2}\/\d{2}\/)?(\d+)\/.*/), replaceWith: 'https://text.npr.org/$1' }, { matchRegex: new RegExp(/^https?:\/\/(?:(?:.*)arxiv\.org\/pdf|arxiv-export-lb\.library\.cornell\.edu\/(?:pdf|abs))\/(\d{4}\.\d{4,5}(v\d)?)(?:.*)/), replaceWith: 'https://arxiv.org/abs/$1' }, { matchRegex: new RegExp(/^https?:\/\/(ieeexplore\.ieee\.org\/document\/\d+)\//), replaceWith: 'https://$1' }, { matchRegex: new RegExp(/^https?:\/\/github\.ink\/(.*)/), replaceWith: 'https://github.com/$1' } // Add more rules here as needed ]; // Define enhanced selector rules for each search engine const selectorRules = { 'google': [ { selector: 'div.yuRUbf div span a', childSelector: 'div.byrV5b cite', updateChildText: true, useTopLevelDomain: true, // Flag for using top-level domain containProtocol: true, displayMethod: 1 } // ... [Other rules for Google] ], 'searx': [ { selector: 'article a.url_wrapper', childSelector: '.url_i1', updateChildText: true, useTopLevelDomain: true, containProtocol: true, displayMethod: 1 }, { selector: 'h3 a' } ], 'startpage': [ { selector: 'a.w-gl__result-url.result-link', updateText: true, displayMethod: 2 }, { selector: 'a.w-gl__result-title.result-link' } ], 'brave': [ { selector: 'a.h.svelte-1dihpoi', childSelector: 'cite.snippet-url.svelte-1ygzem6 span', updateChildText: true, containProtocol: false, displayMethod: 1 } ], 'duckduckgo': [ { selector: 'a.eVNpHGjtxRBq_gLOfGDr.LQNqh2U1kzYxREs65IJu' }, { selector: 'a.Rn_JXVtoPVAFyGkcaXyK', childSelector: 'span', updateChildText: true, containProtocol: true, displayMethod: 1 } ], 'metager': [ { selector: 'h2.result-title a' }, { selector: 'div.result-subheadline a', updateText: true, containProtocol: false, displayMethod: 3 } ], 'mojeek': [ { selector: 'li a.ob', childSelector: 'span.url', updateChildText: true, useTopLevelDomain: true, containProtocol: true, displayMethod: 1 } // ... [Other rules for Mojeek] ] // Additional search engines can be defined here... }; // User-defined list of search engine instance URLs const searchEngines = { 'google': { hosts: ['www.google.com'], // search results container // you can ignore this parameter if you don't want to set it, just delete it // defult value is 'body' resultContainerSelectors: ['div.GyAeWb#rcnt'] }, 'searx': { hosts: [ 'searx.tiekoetter.com', 'search.disroot.org' ], resultContainerSelectors: [ 'main#main_results' // 'maindiv#main_results div#urls' // 'div#sidebar div#infoboxes' ] }, 'startpage': { hosts: ['www.startpage.com'], resultContainerSelectors: [ 'div.show-results' // 'div.sidebar-results' ] }, 'brave': { hosts: ['search.brave.com'], resultContainerSelectors: [ 'main.main-column' // 'aside.sidebar' ] }, 'duckduckgo': { hosts: ['duckduckgo.com'], resultContainerSelectors: [ 'section[data-testid="mainline"][data-area="mainline"]' // 'section[data-testid="sidebar"][data-area="sidebar"]' ] }, 'metager': { hosts: [ 'metager.org', 'metager.de' ], resultContainerSelectors: ['div#results'] }, 'mojeek': { hosts: ['mojeek.com'] } // ... more search engines }; // Function to modify URLs and optionally text const modifyUrls = (engine) => { try { const selectors = selectorRules[engine]; if (selectors) { selectors.forEach(rule => { const elements = document.querySelectorAll(rule.selector); if (elements.length > 0) { elements.forEach(element => { urlModificationRules.forEach(urlRule => { if (element.href && urlRule.matchRegex.test(element.href)) { const newHref = element.href.replace(urlRule.matchRegex, urlRule.replaceWith); element.href = newHref; updateTextContent(element, rule, newHref, engine); } }); }); } }); } } catch (error) { console.error("URL Modifier Script Error: ", error); } }; // Function to update text content const updateTextContent = (element, rule, newUrl, engine) => { if (rule.updateText || (rule.updateChildText && rule.childSelector)) { // Special handling for DuckDuckGo and Brave if (engine === 'duckduckgo' || engine === 'brave') { updateDoubleElementContent(element, rule, newUrl); } else { // General handling for other search engines const targetElement = rule.childSelector ? element.querySelector(rule.childSelector) : element; updateSingleElementText(targetElement, rule, newUrl); } } }; // Function to update text for multi elements (i.e. DuckDuckGo, Brave) const updateDoubleElementContent = (element, rule, newUrl) => { // Remove the "https://" protocol if containProtocol is false newUrl = rule.containProtocol ? newUrl : removeProtocol(newUrl); let formattedUrl = formatMethod1(newUrl, 70); // Assume max length 70 for splitting let urlParts = formattedUrl.split(' › '); // Correctly select the first and second elements let spans = element.querySelectorAll(rule.childSelector); if (spans && (spans.length >= 2 || spans.length <= 1)) { spans[0].textContent = urlParts[0]; // Update the first part spans[1].textContent = ' › ' + urlParts.slice(1).join(' › '); // Update the second part } else { console.error("Script: Expected structure not found for Double Element URL update!"); } }; // Function to update text for a single element const updateSingleElementText = (targetElement, rule, newUrl) => { if (targetElement) { let formattedUrl = ''; switch (rule.displayMethod) { case 1: formattedUrl = formatMethod1(newUrl, rule.maxLength); break; case 2: formattedUrl = newUrl; // Full URL with protocol break; case 3: formattedUrl = decodeURIComponent(removeProtocol(newUrl)); // Full URL without protocol break; } targetElement.textContent = formattedUrl; } }; // Function for Method 1 (Breadcrumb Style URLs), leaving 'https://' intact const formatMethod1 = (url, maxLength) => { // Split the URL while keeping 'https://' intact let parts = url.replace('https://', 'https›').split('/'); parts[0] = parts[0].replace('https›', 'https://'); // Restore 'https://' // Join the URL parts with ' › ' and check if it exceeds maxLength let joinedUrl = parts.join(' › '); if (joinedUrl.length > maxLength) { // Apply truncation based on maxLength let truncatedUrl = joinedUrl.slice(0, maxLength - 3); // Reserve space for '...' truncatedUrl += '...'; joinedUrl = truncatedUrl; } // Decode the URL to convert encoded characters to their original form return decodeURIComponent(joinedUrl); }; const removeProtocol = (url) => { return url.replace(/^https?:\/\//, ''); }; // Improved function to determine the search engine const getSearchEngineInfo = () => { try { const host = window.location.host; for (const engine in searchEngines) { if (searchEngines[engine].hosts.some(instanceHost => host.includes(instanceHost))) { const selectors = searchEngines[engine].resultContainerSelectors || ['body']; // Default to 'body' if not specified return { engine, selectors: selectors }; } } } catch (error) { console.error("Error determining search engine: ", error); } }; const observeToExecute = (engine, selector) => { const resultContainers = document.querySelectorAll(selector); if (resultContainers) { resultContainers.forEach(resultContainer => { modifyUrls(engine.engine); // Observe changes in each result container const observer = new MutationObserver(() => modifyUrls(engine)); observer.observe(resultContainer, { childList: true, subtree: true }); }); } }; // Run the script for the current search engine try { const engineInfo = getSearchEngineInfo(); if (engineInfo) { engineInfo.selectors.forEach(containerSelector => { observeToExecute(engineInfo.engine, containerSelector); }); } } catch (error) { console.error("Error executing URL Modifier Script: ", error); } })();