3/5/2025, 7:16:43 PM
// ==UserScript==
// @name lib:textjack
// @version 9
// @match *://*/*
// @run-at document-start
// @author rssaromeo
// @license AGPLv3
// @include *
// @grant none
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAMAAABiM0N1AAAAAXNSR0IB2cksfwAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAHJQTFRFAAAAEIijAo2yAI60BYyuF4WaFIifAY6zBI2wB4usGIaZEYigIoiZCIyrE4igG4iYD4mjEomhFoedCoqpDIqnDomlBYyvE4efEYmiDYqlA42xBoytD4mkCYqqGYSUFYidC4qoC4upAo6yCoupDYqmCYur4zowOQAAACZ0Uk5TAO////9vr////1+/D/+/L+/Pf/////+f3///////H4////////+5G91rAAACgUlEQVR4nM2Y22KjIBCGidg1264liZqDadK03X3/V2wNKHMC7MpF/xthHD5mgERAqZhWhfYqH6K+Qf2qNNf625hCoFj9/gblMUi5q5jLkXLCKudgyiRm0FMK82cWJp1fLbV5VmvJbCIc0GCYaFqqlDJgADdBjncqAXYobm1xh72aFMflbysteFfdy2Yi1XGOm5HGBzQ1dq7TzEoxjeNTjQZb7VA3e1c7+ImgasAgQ9+xusNVNZIo5xmOMgihIS2PbCQIiHEUdTvhxCcS/kPomfFI2zHy2PkWmA6aNatIJpKFJyekyy02xh5Y3DI9T4aOT6VhIUrsNTFp1pf79Z4SIIVDegl6IJO6cHiL/GimIZDhgTu/BlYWCQzHMl0zBWT/T3KAhtxOuUB9FtBrpsz0RV4xsjHmW+UCaffcSy/5viMGer0/6HdFNMZBq/vjJL38H9Dqx4Fuy0Em12DbZy+9pGtiDijbglwAehyj11n0tRD3WUBm+lwulE/8h4BuA+iWAQQnteg2Xm63WQLTpnMnpjdge0Mgu/GRPsV4xdjQ94Lfi624fabhDkfUqIKNrM64Q837v8yL0prasepCgrtvw1sJpoqanGEX7b5mQboNW8eawXaWXTMfMGxub472hzWzHSn6Sg2G9+6TAyRruE71s+zAzjWaknoyJCQzwxrghH2k5FDT4eqWunuNxyN9QCGcxVod5oADbYnIUkDTGZEf1xDJnSFteQ3KdsT8zYDMQXcHxsevcLH1TrsABzkNPyA/L7b0jg704viMMlpQI96WsHknCt/3YH0kOEo9zcGkwrFK39ck72rmoehmKqo2RKlilzSy/nJKEV45CT38myJp456fezktHjN5aeMAAAAASUVORK5CYII=
// @description 3/5/2025, 7:16:43 PM
// @namespace https://greasyfork.org/users/1184528
// ==/UserScript==
Object.assign(window, console)
const a = loadlib("allfuncs")
var textJackList = []
const observedShadowRoots = new WeakSet()
const lastValues = new WeakMap()
function replaceText(text) {
let oldtext = text
for (let i = 0; i < textJackList.length; i++) {
text = textJackList[i](text)
if (text !== oldtext) {
return replaceText(text)
}
}
return text
}
/**
* Fast node scanner targeting text nodes directly
*/
function jackNode(node) {
if (node.nodeType === 3) { // Text Node
// FIX: Skip text replacement if the text node belongs to an input, textarea, or contenteditable element
const parent = node.parentElement
if (parent) {
const tag = parent.tagName
if (tag === 'INPUT' || tag === 'TEXTAREA' || parent.isContentEditable) {
return
}
}
const currentVal = node.nodeValue
if (!currentVal || !currentVal.trim()) return
if (currentVal !== lastValues.get(node)) {
const newVal = replaceText(currentVal)
lastValues.set(node, newVal)
node.nodeValue = newVal
}
}
}
/**
* Creates/appends a MutationObserver for a given root (body or shadow root)
*/
function observeRoot(root) {
const observer = new MutationObserver((mutations) => {
for (let i = 0; i < mutations.length; i++) {
const mutation = mutations[i]
if (mutation.type === "childList") {
mutation.addedNodes.forEach((n) => {
if (n.nodeType === 1) { // Element Node
jackRecursive(n)
} else if (n.nodeType === 3) { // Text Node
jackNode(n)
}
})
} else if (mutation.type === "characterData") {
jackNode(mutation.target)
}
}
})
observer.observe(root, {
childList: true,
subtree: true,
characterData: true,
})
}
/**
* Ultra-fast native TreeWalker replacement for filterExistingText
*/
function jackRecursive(root) {
// FIX: Added a filter logic to the TreeWalker to skip form inputs completely
const walker = document.createTreeWalker(
root,
NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
{
acceptNode: function(node) {
if (node.nodeType === 1) {
const tag = node.tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA' || node.isContentEditable) {
return NodeFilter.FILTER_REJECT; // Reject element and all its children
}
}
return NodeFilter.FILTER_ACCEPT;
}
}
)
let currentNode = walker.currentNode
while (currentNode) {
if (currentNode.nodeType === 3) {
jackNode(currentNode)
} else if (currentNode.nodeType === 1 && currentNode.shadowRoot) {
if (!observedShadowRoots.has(currentNode.shadowRoot)) {
observedShadowRoots.add(currentNode.shadowRoot)
jackRecursive(currentNode.shadowRoot)
observeRoot(currentNode.shadowRoot)
}
}
currentNode = walker.nextNode()
}
}
// Intercept future dynamic Shadow DOM generation instantly
const originalAttachShadow = Element.prototype.attachShadow
Element.prototype.attachShadow = function (init) {
const shadowRoot = originalAttachShadow.call(this, init)
setTimeout(() => {
if (!observedShadowRoots.has(shadowRoot)) {
observedShadowRoots.add(shadowRoot)
jackRecursive(shadowRoot)
observeRoot(shadowRoot)
}
}, 0)
return shadowRoot
}
// Initialize environment
function runInitialization() {
jackRecursive(document.body)
observeRoot(document.body)
}
let waitingForBody = !document.body
if (!waitingForBody) {
runInitialization()
}
loadlib("libloader").savelib("textjack", function newTextJack(cb) {
textJackList.push(cb)
if (document.body) {
runInitialization()
} else {
waitingForBody = true
}
})
a.bodyload().then(() => {
if (waitingForBody) {
runInitialization()
}
})