Universal Text Reversal bypass

Bypass filters by reversing text structure. Modes: (Full) character reversal, (Smart) character-pair swap, or (Word Swap) logical word reordering. Visually, the text looks normal.

Fra 11.10.2025. Se den seneste versjonen.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Universal Text Reversal bypass
// @namespace    github.com/annaroblox
// @version      2.1
// @license      MIT
// @description  Bypass filters by reversing text structure. Modes: (Full) character reversal, (Smart) character-pair swap, or (Word Swap) logical word reordering. Visually, the text looks normal.
// @author       AnnaRoblox
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_setClipboard
// ==/UserScript==

(function () {
    'use strict';

    /* ---------- CONFIG ---------- */
    const STORAGE_KEY = 'utr_mode';
    const RLO = "\u202E";  // RIGHT-TO-LEFT OVERRIDE
    const PDF = "\u202C";  // POP DIRECTIONAL FORMATTING
    const RLI = "\u2067";  // RIGHT-TO-LEFT ISOLATE
    const LRI = "\u2066";  // LEFT-TO-RIGHT ISOLATE
    const PDI = "\u2069";  // POP DIRECTIONAL ISOLATE

    let isDebugMode = false;
    let mode        = localStorage.getItem(STORAGE_KEY) || 'smart';
    let menuCommandIds = []; // Array to store menu command IDs for unregistering

    /* ---------- CORE TEXT TRANSFORM ---------- */

    const reverseLines = text =>
        text.split('\n')
            .map(l => RLO + [...l].reverse().join('') + PDF)
            .join('\n');

    /**
     * wordIsolateSwap function
     * How it works:
     * This method swaps adjacent pairs of words but preserves the visual appearance.
     * e.g., "hello how are you" becomes "how hello you are" (stored) but displays as "hello how are you"
     * 1. Adjacent word pairs are swapped: (hello,how) -> (how,hello), (are,you) -> (you,are)
     * 2. Each swapped pair is wrapped in RLI...PDI, creating a Right-to-Left context that reverses them back.
     * 3. Each individual word is wrapped in LRI...PDI to force Left-to-Right rendering.
     * 4. The browser renders each RTL pair, swapping them back to appear normal.
     */
    const wordIsolateSwap = text =>
      text
        .split('\n')
        .map(line => {
          const words = line.match(/\S+/g) || [];
          if (words.length === 0) return line;

          // Swap adjacent pairs of words
          const swappedWords = [];
          for (let i = 0; i < words.length; i += 2) {
            if (i + 1 < words.length) {
              // Swap the pair
              swappedWords.push(words[i + 1]);
              swappedWords.push(words[i]);
            } else {
              // Odd word out, keep it in place
              swappedWords.push(words[i]);
            }
          }

          // Wrap each pair in RLI...PDI to reverse it back for display
          let result = '';
          for (let i = 0; i < swappedWords.length; i += 2) {
            if (i + 1 < swappedWords.length) {
              // Wrap the swapped pair to reverse it for display
              result += RLI + LRI + swappedWords[i] + PDI + ' ' + LRI + swappedWords[i + 1] + PDI + PDI;
              // Add space between pairs
              if (i + 2 < swappedWords.length) result += ' ';
            } else {
              // Odd word out, just wrap it normally
              result += LRI + swappedWords[i] + PDI;
            }
          }

          return result;
        })
        .join('\n');


    /**
     * smartReverse function
     * How it works: Swaps pairs of characters within each word.
     */
    const smartReverse = text =>
      text
        .split('\n')
        .map(line => {
          const tokens = line.match(/(\s+|\S+)/g) || [];
          const transformedTokens = tokens.map(tk => {
            if (/^\s+$/.test(tk)) return tk;
            const chars = [...tk];
            let processedWord = '';
            for (let i = 0; i < chars.length; i += 2) {
              if (i + 1 < chars.length) {
                const swappedPair = chars[i + 1] + chars[i];
                processedWord += RLI + RLO + swappedPair + PDI; // RLO is needed here for visual reversal
              } else {
                processedWord += chars[i];
              }
            }
            return processedWord;
          });
          return transformedTokens.join('');
        })
        .join('\n');

    // Dispatcher function to select the correct transformation
    const transform = txt => {
        switch (mode) {
            case 'full':
                return reverseLines(txt);
            case 'wordswap':
                return wordIsolateSwap(txt);
            case 'smart':
            default:
                return smartReverse(txt);
        }
    };

    /* ---------- UI / MENU ---------- */
    function buildMenu() {
        menuCommandIds.forEach(id => GM_unregisterMenuCommand(id));
        menuCommandIds = [];

        menuCommandIds.push(
            GM_registerMenuCommand('Process selected text', processSelection)
        );

        menuCommandIds.push(
            GM_registerMenuCommand(
                `Mode: ${mode.toUpperCase()} (click to toggle)`,
                () => {
                    const modes = ['full', 'smart', 'wordswap'];
                    const currentIndex = modes.indexOf(mode);
                    mode = modes[(currentIndex + 1) % modes.length]; // Cycle through the modes
                    localStorage.setItem(STORAGE_KEY, mode);
                    buildMenu();
                },
                'm'
            )
        );

        menuCommandIds.push(
            GM_registerMenuCommand(
                `DEBUG: ${isDebugMode ? 'ON' : 'OFF'} (click to toggle)`,
                () => { isDebugMode = !isDebugMode; buildMenu(); },
                'd'
            )
        );
    }

    /* ---------- CLIPBOARD & INPUT HANDLING (NO CHANGES BELOW) ---------- */

    function copyToClipboard(textToCopy) {
        if (typeof GM_setClipboard !== 'undefined') {
            GM_setClipboard(textToCopy);
        } else if (navigator.clipboard) {
            navigator.clipboard.writeText(textToCopy).catch(console.error);
        } else {
            const ta = document.createElement('textarea');
            ta.value = textToCopy;
            ta.style.position = 'fixed'; ta.style.left = '-9999px';
            document.body.appendChild(ta);
            ta.select();
            try { document.execCommand('copy'); } catch (e) {}
            document.body.removeChild(ta);
        }
    }

    function isEditableElement(el) {
        if (!el || !el.nodeType || el.nodeType !== 1) return false;
        const tag = el.tagName?.toLowerCase();
        if (tag === 'input' || tag === 'textarea') return true;
        if (el.contentEditable === 'true' || el.isContentEditable || el.designMode === 'on') return true;
        const role = el.getAttribute('role');
        if (role === 'textbox' || role === 'searchbox') return true;
        return false;
    }

    function findEditableInShadowDOM(root) {
        if (!root) return null;
        if (root.activeElement && isEditableElement(root.activeElement)) return root.activeElement;
        if (root.activeElement?.shadowRoot) {
            const found = findEditableInShadowDOM(root.activeElement.shadowRoot);
            if (found) return found;
        }
        const selectors = ['input:not([type="hidden"])', 'textarea', '[contenteditable="true"]', '[role="textbox"]', '[role="searchbox"]'];
        for (const selector of selectors) {
            const el = root.querySelector(selector);
            if (el && isEditableElement(el)) return el;
        }
        return null;
    }

    function locateRealInput(node) {
        let cur = node;
        while (cur) {
            if (cur.nodeType === 1 && isEditableElement(cur)) {
                const tag = cur.tagName?.toLowerCase();
                return { element: cur, type: (tag === 'input' || tag === 'textarea') ? tag : 'contenteditable' };
            }
            if (cur.shadowRoot) {
                const shadowEditable = findEditableInShadowDOM(cur.shadowRoot);
                if (shadowEditable) {
                    const tag = shadowEditable.tagName?.toLowerCase();
                    return { element: shadowEditable, type: (tag === 'input' || tag === 'textarea') ? tag : 'contenteditable' };
                }
            }
            cur = cur.parentNode || cur.host;
            if (!cur && node.getRootNode) {
                const root = node.getRootNode();
                if (root?.host) cur = root.host;
            }
        }
        let active = document.activeElement;
        while (active) {
            if (isEditableElement(active)) {
                const tag = active.tagName?.toLowerCase();
                return { element: active, type: (tag === 'input' || tag === 'textarea') ? tag : 'contenteditable' };
            }
            if (active.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
            else if (active.shadowRoot) {
                const shadowEditable = findEditableInShadowDOM(active.shadowRoot);
                if (shadowEditable) {
                    const tag = shadowEditable.tagName?.toLowerCase();
                    return { element: shadowEditable, type: (tag === 'input' || tag === 'textarea') ? tag : 'contenteditable' };
                }
                break;
            } else break;
        }
        try {
            for (const frame of document.querySelectorAll('iframe')) {
                try {
                    const frameDoc = frame.contentDocument || frame.contentWindow?.document;
                    if (frameDoc?.activeElement && isEditableElement(frameDoc.activeElement)) {
                        const tag = frameDoc.activeElement.tagName?.toLowerCase();
                        return { element: frameDoc.activeElement, type: (tag === 'input' || tag === 'textarea') ? tag : 'contenteditable' };
                    }
                } catch (e) { /* Cross-origin */ }
            }
        } catch (e) {}
        return null;
    }

    function replaceTextInInput(el, type, reversed, start, end) {
        if (type === 'input' || type === 'textarea') {
            const original = el.value;
            const replacement = original.slice(0, start) + reversed + original.slice(end);
            const scrollTop = el.scrollTop;
            el.value = replacement;
            el.setSelectionRange(start, start + reversed.length);
            el.scrollTop = scrollTop;
            el.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
            el.dispatchEvent(new Event('change', { bubbles: true }));
        } else {
            const sel = window.getSelection();
            if (document.queryCommandSupported('insertText')) {
                try {
                    if (sel.rangeCount > 0) {
                        const range = sel.getRangeAt(0);
                        if (start !== end) range.deleteContents();
                    }
                    document.execCommand('insertText', false, reversed);
                    el.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
                    return;
                } catch (e) {}
            }
            if (sel.rangeCount > 0) {
                const range = sel.getRangeAt(0);
                range.deleteContents();
                const textNode = document.createTextNode(reversed);
                range.insertNode(textNode);
                range.setStartAfter(textNode);
                range.setEndAfter(textNode);
                sel.removeAllRanges();
                sel.addRange(range);
                el.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
            }
        }
    }

    function processSelection() {
        const sel = window.getSelection();
        const inputInfo = locateRealInput(sel.focusNode || document.activeElement);

        if (!inputInfo) {
            const selected = sel.toString();
            if (selected) {
                const reversed = transform(selected);
                if (isDebugMode) copyToClipboard(reversed);
                try { document.execCommand('insertText', false, reversed); }
                catch (e) { copyToClipboard(reversed); }
            }
            return;
        }

        const { element: el, type } = inputInfo;
        let original, start, end;

        if (type === 'input' || type === 'textarea') {
            original = el.value;
            start = el.selectionStart ?? 0;
            end   = el.selectionEnd ?? 0;
        } else {
            original = el.textContent || '';
            const range = sel.rangeCount ? sel.getRangeAt(0) : null;
            if (!range) { start = end = 0; }
            else {
                const pre = range.cloneRange();
                pre.selectNodeContents(el);
                pre.setEnd(range.startContainer, range.startOffset);
                start = pre.toString().length;
                end   = start + range.toString().length;
            }
        }

        const chunk = (start === end) ? original : original.slice(start, end);
        if (!chunk) return;

        const reversed = transform(chunk);
        if (isDebugMode) copyToClipboard(reversed);
        replaceTextInInput(el, type, reversed, start, end);
    }

    document.addEventListener('keydown', e => {
        if (e.altKey && e.key.toLowerCase() === 'r') {
            e.preventDefault();
            processSelection();
        }
    }, true);

    buildMenu();
})();