// ==UserScript== // @name ExternalLinks2NewTab // @namespace http://tampermonkey.net/ // @version 1.2.1 // @description Opens external links in a new tab with wildcard domain support (*google.com/search*, 4get.*, searx.* etc.) // @author ChatGPT & Dituar // @match *://*/* // @grant none // ==/UserScript== (function () { 'use strict'; // --- SETTINGS --- // ✔️ Work ONLY on these domains and paths (if empty INCLUDE_DOMAINS = []; works everywhere except EXCLUDE_DOMAINS) const INCLUDE_DOMAINS = [ '*google.com/search*', '4get.*', // For all 4get search engine instances https://4get.ca/instances 'searx.*', 'search.*', // For many SearXNG search engine instances https://searx.space/ ]; // ❌ Exclude these domains (has a higher priority than INCLUDE_DOMAINS) const EXCLUDE_DOMAINS = [ // '*google.com/search*', // '4get.bloat.cat', ]; // --- FUNCTIONS --- const currentHost = window.location.hostname; const currentPath = window.location.pathname + window.location.search; function isIncluded() { // First check exclusions — they always have priority if (EXCLUDE_DOMAINS.some(pattern => matchPattern(pattern, currentHost, currentPath))) { return false; } // Then check inclusions (if any) if (INCLUDE_DOMAINS.length > 0) { return INCLUDE_DOMAINS.some(pattern => matchPattern(pattern, currentHost, currentPath)); } // If no inclusions — allow all sites (if not excluded above) return true; } function wildcardToRegex(str) { return '^' + str .replace(/\./g, '\\.') // escape dots .replace(/\*/g, '.*') // * => .* + '$'; } function matchPattern(pattern, host, path) { const [domainPart, ...pathParts] = pattern.split('/'); const pathPattern = pathParts.length > 0 ? '/' + pathParts.join('/') : '*'; const domainRegex = new RegExp(wildcardToRegex(domainPart)); const pathRegex = new RegExp(wildcardToRegex(pathPattern)); return domainRegex.test(host) && pathRegex.test(path); } function isExternal(link) { try { const url = new URL(link.href); return url.hostname !== location.hostname; } catch { return false; } } function processLink(link) { if (link.tagName !== 'A') return; if (link.target !== '_blank' && isExternal(link)) { link.target = '_blank'; link.rel = 'noopener noreferrer'; } } function processAllLinks(root = document) { const links = root.querySelectorAll('a[href]'); links.forEach(processLink); } // --- DOM Mutation Observer --- const observer = new MutationObserver(mutations => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === 1) { if (node.matches('a[href]')) { processLink(node); } else { processAllLinks(node); } } } } }); if (isIncluded()) { processAllLinks(); observer.observe(document.body, { childList: true, subtree: true }); } })();