Universal Text Reversal bypass

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

// ==UserScript==
// @name         Universal Text Reversal bypass
// @namespace    github.com/annaroblox
// @version      2.3
// @license      MIT
// @description  Bypass filters by reversing text structure. Modes: (Full) character reversal, (Smart) character-pair swap, (Word Swap) logical word reordering, or (Smart-Swap) combined word & character swap. 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-swap';
    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');

   /**
 * smartSwap function - combines word swapping with character pair swapping
 * Structure: RLI + [LRI + char-swapped-word + PDI] + SPACE + [LRI + char-swapped-word + PDI] + PDI
 * - Character pairs within each word are swapped using RLI+RLO+chars+PDI
 * - Each complete word is wrapped in LRI...PDI
 * - Adjacent word pairs are physically swapped in storage
 * - The outer RLI...PDI reverses the word order for correct display
 */
const smartSwap = text =>
  text
    .split('\n')
    .map(line => {
      const words = line.match(/\S+/g) || [];
      if (words.length === 0) return line;

      // Apply character pair swapping to each word
      const processedWords = words.map(word => {
        const chars = [...word];
        let charSwapped = '';
        for (let i = 0; i < chars.length; i += 2) {
          if (i + 1 < chars.length) {
            // Swap the pair: chars[i+1] + chars[i]
            const swappedPair = chars[i + 1] + chars[i];
            charSwapped += RLI + RLO + swappedPair + PDI;
          } else {
            charSwapped += chars[i];
          }
        }
        // Wrap entire word in LRI...PDI
        return LRI + charSwapped + PDI;
      });

      // Swap adjacent pairs of words
      const swappedWords = [];
      for (let i = 0; i < processedWords.length; i += 2) {
        if (i + 1 < processedWords.length) {
          swappedWords.push(processedWords[i + 1]);
          swappedWords.push(processedWords[i]);
        } else {
          swappedWords.push(processedWords[i]);
        }
      }

      // Wrap all pairs in RLI...PDI to reverse word order for display
      let result = '';
      for (let i = 0; i < swappedWords.length; i += 2) {
        if (i + 1 < swappedWords.length) {
          // Wrap the swapped pair in RLI...PDI
          result += RLI + swappedWords[i] + ' ' + swappedWords[i + 1] + PDI;
          if (i + 2 < swappedWords.length) result += ' ';
        } else {
          // Odd word out
          result += swappedWords[i];
        }
      }

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

    /**
     * Selective word swap - swaps any specified word pairs
     * For each pair:
     * - If one word is the last word: first word output normally, then RLI block with rest reversed
     * - Otherwise: reverse the entire range between them
     */
    const selectiveWordSwap = (text, indicesToSwap) => {
      return text
        .split('\n')
        .map(line => {
          const words = line.match(/\S+/g) || [];
          if (words.length === 0) return line;

          // Create swap pairs (ensure min, max order)
          const swapPairs = [];
          for (let i = 0; i < indicesToSwap.length; i += 2) {
            if (i + 1 < indicesToSwap.length) {
              const idx1 = indicesToSwap[i];
              const idx2 = indicesToSwap[i + 1];
              swapPairs.push([Math.min(idx1, idx2), Math.max(idx1, idx2)]);
            }
          }

          // Build result
          let result = '';
          const processed = new Set();

          for (let i = 0; i < words.length; i++) {
            if (processed.has(i)) continue;

            // Check if this index starts a swap pair
            const pair = swapPairs.find(p => p[0] === i);

            if (pair) {
              const [startIdx, endIdx] = pair;

              // Special case: if swapping with the last word
              if (endIdx === words.length - 1) {
                // Output the first swapped word normally
                result += LRI + words[startIdx] + PDI;
                processed.add(startIdx);

                // Add space before RLI block if there are words in between
                if (startIdx + 1 <= endIdx) result += ' ';

                // Start RLI block
                result += RLI;

                // Add words from startIdx+1 to endIdx in REVERSE order
                for (let j = endIdx; j >= startIdx + 1; j--) {
                  result += LRI + words[j] + PDI;
                  if (j > startIdx + 1) result += ' ';
                  processed.add(j);
                }

                // End RLI block
                result += PDI;
              } else {
                // Normal case: reverse the entire range
                // Start RLI block
                result += RLI;

                // Add words in REVERSE order (from end to start)
                for (let j = endIdx; j >= startIdx; j--) {
                  result += LRI + words[j] + PDI;
                  if (j > startIdx) result += ' ';
                  processed.add(j);
                }

                // End RLI block
                result += PDI;
              }

              // Add space after the block if not at the end
              if (endIdx + 1 < words.length) result += ' ';
            } else {
              // Not part of a swap, check if already processed
              if (!processed.has(i)) {
                result += LRI + words[i] + PDI;
                processed.add(i);
                // Add space after the word if not at the end
                if (i + 1 < words.length) result += ' ';
              }
            }
          }

          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;
              } 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-swap':
                return smartSwap(txt);
            case 'smart':
            default:
                return smartReverse(txt);
        }
    };

    /* ---------- WORD SELECTION UI ---------- */

    function createWordSelectionModal(text) {
        // Create modal overlay
        const overlay = document.createElement('div');
        overlay.id = 'utr-word-selector-overlay';
        overlay.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.7);
            z-index: 999999;
            display: flex;
            align-items: center;
            justify-content: center;
            font-family: Arial, sans-serif;
        `;

        // Create modal container
        const modal = document.createElement('div');
        modal.style.cssText = `
            background: white;
            border-radius: 8px;
            padding: 20px;
            max-width: 600px;
            max-height: 80vh;
            overflow-y: auto;
            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
        `;

        // Title
        const title = document.createElement('h3');
        title.textContent = 'Select Words to Swap';
        title.style.cssText = 'margin: 0 0 15px 0; color: #333;';
        modal.appendChild(title);

        // Instructions
        const instructions = document.createElement('p');
        instructions.textContent = 'Click words in pairs to swap them. When swapping with the last word, middle words stay in place. Otherwise, the entire range is reversed.';
        instructions.style.cssText = 'margin: 0 0 15px 0; color: #666; font-size: 14px;';
        modal.appendChild(instructions);

        // Word container
        const wordContainer = document.createElement('div');
        wordContainer.style.cssText = `
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
            margin-bottom: 20px;
            padding: 15px;
            background: #f5f5f5;
            border-radius: 4px;
            min-height: 60px;
        `;

        const words = text.match(/\S+/g) || [];
        const selectedIndices = [];
        const wordElements = [];

        words.forEach((word, index) => {
            const wordSpan = document.createElement('span');
            wordSpan.textContent = word;
            wordSpan.dataset.index = index;
            wordSpan.style.cssText = `
                padding: 6px 12px;
                background: white;
                border: 2px solid #ddd;
                border-radius: 4px;
                cursor: pointer;
                transition: all 0.2s;
                user-select: none;
            `;

            wordSpan.addEventListener('click', () => {
                const idx = parseInt(wordSpan.dataset.index);
                const selectedIdx = selectedIndices.indexOf(idx);

                if (selectedIdx > -1) {
                    // Deselect
                    selectedIndices.splice(selectedIdx, 1);
                    wordSpan.style.background = 'white';
                    wordSpan.style.borderColor = '#ddd';
                    wordSpan.style.color = 'black';
                } else {
                    // Select
                    selectedIndices.push(idx);
                    const pairIndex = selectedIndices.length - 1;
                    const color = pairIndex % 2 === 0 ? '#4CAF50' : '#2196F3';
                    wordSpan.style.background = color;
                    wordSpan.style.borderColor = color;
                    wordSpan.style.color = 'white';
                }
            });

            wordElements.push(wordSpan);
            wordContainer.appendChild(wordSpan);
        });

        modal.appendChild(wordContainer);

        // Button container
        const buttonContainer = document.createElement('div');
        buttonContainer.style.cssText = 'display: flex; gap: 10px; justify-content: flex-end;';

        // Apply button
        const applyBtn = document.createElement('button');
        applyBtn.textContent = 'Apply Swap';
        applyBtn.style.cssText = `
            padding: 10px 20px;
            background: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            font-weight: bold;
        `;
        applyBtn.addEventListener('click', () => {
            if (selectedIndices.length >= 2) {
                const transformed = selectiveWordSwap(text, selectedIndices);
                applyTransformedText(transformed);
            }
            document.body.removeChild(overlay);
        });

        // Cancel button
        const cancelBtn = document.createElement('button');
        cancelBtn.textContent = 'Cancel';
        cancelBtn.style.cssText = `
            padding: 10px 20px;
            background: #f44336;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            font-weight: bold;
        `;
        cancelBtn.addEventListener('click', () => {
            document.body.removeChild(overlay);
        });

        buttonContainer.appendChild(cancelBtn);
        buttonContainer.appendChild(applyBtn);
        modal.appendChild(buttonContainer);

        overlay.appendChild(modal);
        document.body.appendChild(overlay);

        // Close on overlay click
        overlay.addEventListener('click', (e) => {
            if (e.target === overlay) {
                document.body.removeChild(overlay);
            }
        });

        // Close on Escape key
        const escapeHandler = (e) => {
            if (e.key === 'Escape') {
                document.body.removeChild(overlay);
                document.removeEventListener('keydown', escapeHandler);
            }
        };
        document.addEventListener('keydown', escapeHandler);
    }

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

        if (!inputInfo) {
            copyToClipboard(transformed);
            try { document.execCommand('insertText', false, transformed); }
            catch (e) { copyToClipboard(transformed); }
            return;
        }

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

        if (type === 'input' || type === 'textarea') {
            start = el.selectionStart ?? 0;
            end = el.selectionEnd ?? 0;
        } else {
            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;
            }
        }

        replaceTextInInput(el, type, transformed, start, end);
    }

    /* ---------- 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', 'smart-swap'];
                    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 ---------- */

    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);
    }

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

        let selectedText = '';

        if (!inputInfo) {
            selectedText = sel.toString();
        } else {
            const { element: el, type } = inputInfo;
            if (type === 'input' || type === 'textarea') {
                const start = el.selectionStart ?? 0;
                const end = el.selectionEnd ?? 0;
                selectedText = el.value.slice(start, end);
            } else {
                const range = sel.rangeCount ? sel.getRangeAt(0) : null;
                if (range) {
                    selectedText = range.toString();
                }
            }
        }

        if (selectedText && selectedText.trim()) {
            createWordSelectionModal(selectedText);
        }
    }

    // Alt + R for normal processing
    document.addEventListener('keydown', e => {
        if (e.altKey && !e.ctrlKey && e.key.toLowerCase() === 'r') {
            e.preventDefault();
            processSelection();
        }
    }, true);

    // Ctrl + Alt + R for word selector
    document.addEventListener('keydown', e => {
        if (e.ctrlKey && e.altKey && e.key.toLowerCase() === 'r') {
            e.preventDefault();
            openWordSelectorForSelection();
        }
    }, true);

    buildMenu();
})();