Last active
September 17, 2025 17:28
-
-
Save Vocaned/e7dcfdc834ee52ae668e6bdd2b19794c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Regex text replacer | |
| // @version 1.0.2 | |
| // @description Replace any regex with any other text | |
| // @match http://*/* | |
| // @match https://*/* | |
| // @match file://*/* | |
| // @author voc | |
| // @namespace https://github.com/Vocaned | |
| // @grant none | |
| // @run-at document-end | |
| // ==/UserScript== | |
| // NOTE: The regex will always have /gi flags applied | |
| const DICTIONARY = [ | |
| [/\bfoot\b/, 'paw'], | |
| [/\bfeet\b/, 'paws'] | |
| ]; | |
| (() => { | |
| const patterns = Object.fromEntries(DICTIONARY.map(e => [e[0].source, e[1]])); | |
| const replacementRegex = new RegExp(Object.keys(patterns).map(p => `(${p})`).join('|'), 'gi'); | |
| const seenNodes = new WeakSet(); | |
| function replaceTextNode(node) { | |
| if (seenNodes.has(node)) return; | |
| let changed = false; | |
| const newText = node.nodeValue.replace(replacementRegex, (...matches) => { | |
| for (let i = 1; i < matches.length - 2; i++) { | |
| if (matches[i] !== undefined) { | |
| changed = true; | |
| const newStr = patterns[Object.keys(patterns)[i - 1]]; | |
| if (newStr === newStr.toLowerCase()) return matchCase(matches[i], newStr); | |
| return newStr; | |
| } | |
| } | |
| }); | |
| if (changed) { | |
| node.nodeValue = newText; | |
| seenNodes.add(node); | |
| } | |
| } | |
| const elementBlacklist = ['script', 'style', 'noscript', 'textarea', 'input', 'template', 'svg']; | |
| function replaceElementNode(node) { | |
| if (node === null) return; // FIXME: what causes a null node to get here on some sites when manually replacing <title>? | |
| const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, | |
| { | |
| acceptNode: n => { | |
| if (n.parentNode && elementBlacklist.includes(n.parentNode.nodeName)) { | |
| return NodeFilter.FILTER_REJECT; | |
| } | |
| return NodeFilter.FILTER_ACCEPT; | |
| } | |
| }); | |
| let curNode; | |
| while (curNode = walker.nextNode()) { | |
| replaceTextNode(curNode); | |
| } | |
| } | |
| function matchCase(text, replacement) { | |
| const isAllCaps = text.toUpperCase() === text; | |
| const isCapitalized = text.charAt(0).toUpperCase() === text.charAt(0) && text.slice(1).toLowerCase() === text.slice(1); | |
| const isAllLower = text.toLowerCase() === text; | |
| if (isAllCaps) { | |
| return replacement.toUpperCase(); | |
| } else if (isCapitalized) { | |
| return replacement.charAt(0).toUpperCase() + replacement.slice(1).toLowerCase(); | |
| } else if (isAllLower) { | |
| return replacement.toLowerCase(); | |
| } | |
| return replacement; | |
| } | |
| const observer = new MutationObserver(mutations => { | |
| for (const mutation of mutations) { | |
| for (const node of mutation.addedNodes) { | |
| if (node?.nodeType === Node.TEXT_NODE) replaceTextNode(node); | |
| else if (node?.nodeType == Node.ELEMENT_NODE) replaceElementNode(node); | |
| } | |
| } | |
| }); | |
| replaceElementNode(document.head.querySelector('title')); | |
| observer.observe(document.body, { | |
| childList: true, | |
| subtree: true | |
| }); | |
| const origAttachShadow = Element.prototype.attachShadow; | |
| Element.prototype.attachShadow = function(init) { | |
| const shadow = origAttachShadow.call(this, init); | |
| observer.observe(shadow, { | |
| childList: true, | |
| subtree: true | |
| }); | |
| return shadow; | |
| }; | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment