Skip to content

Instantly share code, notes, and snippets.

@Vocaned
Last active September 17, 2025 17:28
Show Gist options
  • Select an option

  • Save Vocaned/e7dcfdc834ee52ae668e6bdd2b19794c to your computer and use it in GitHub Desktop.

Select an option

Save Vocaned/e7dcfdc834ee52ae668e6bdd2b19794c to your computer and use it in GitHub Desktop.
// ==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