Universal Text Reversal bypass

Reverse every selected line (Full) or (Smart). Alt+R or Tampermonkey menu. Debug mode copies result to clipboard for doing it manually if a certain site doesn't work (string looks normal but bypasses some filters)

// ==UserScript==
// @name         Universal Text Reversal bypass
// @namespace    http://tampermonkey.net/
// @version      1.3
// @license      MIT
// @description  Reverse every selected line (Full) or (Smart).  Alt+R or Tampermonkey menu.  Debug mode copies result to clipboard for doing it manually if a certain site doesn't work (string looks normal but bypasses some filters)
// @author       AnnaRoblox
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @grant        GM_setClipboard
// ==/UserScript==

(function () {
    'use strict';

    /* ---------- CONFIG ---------- */
    const STORAGE_KEY = 'utr_mode';          // localStorage key
    const RTL_MAGIC = "\u202E";              // RIGHT-TO-LEFT OVERRIDE
    const PDF       = "\u202C";              // POP DIRECTIONAL FORMATTING
    const RLO       = "\u202E";              // Right-TO-RIGHT OVERRIDE

    let isDebugMode = false;                  // debug: copy result to clipboard
    let mode        = localStorage.getItem(STORAGE_KEY) || 'smart'; // 'full' | 'smart'

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

    // FULL mode – reverse every line
    const reverseLines = text =>
        text.split('\n')
            .map(l => RTL_MAGIC + [...l].reverse().join(''))
            .join('\n');
// SMART pattern – cycles through:
// RTL → reverse next 2 → PDF → next 1 → RLO → reverse next 2 → PDF → next 1 → …
const smartReverse = text =>
  text
    .split('\n')
    .map(line => {
      /* ---------- 1. split into runs of non-white / white ---------- */
      const tokens = line.match(/(\s+|\S+)/g) || [];
      let out = '';

      /* ---------- 2. state that survives across words -------------- */
      let buf = '';
      let dir = 'ltr';            // direction we are *about* to emit

      const push = (segment, newDir) => {
        if (newDir === 'rtl') {
          out += RTL_MAGIC + [...segment].reverse().join('') + PDF;
        } else {
          out += segment;
        }
        dir = newDir;
        buf = '';
      };

      /* ---------- 3. process every token --------------------------- */
      for (const tk of tokens) {
        if (/^\s+$/.test(tk)) {          // whitespace – flush first, then inject
          if (buf) push(buf, dir);       // flush leftover chars
          out += tk;
          continue;
        }

        /* ----- non-white: feed chars into the global cycle --------- */
        for (const ch of tk) {
          buf += ch;
          if (dir === 'ltr' && buf.length === 2) push(buf, 'rtl');
          else if (dir === 'rtl' && buf.length === 1) push(buf, 'ltr');
        }
      }

      /* ---------- 4. flush anything still in buffer ---------------- */
      if (buf) {
        if (dir === 'rtl') {
          out += RTL_MAGIC + [...buf].reverse().join('') + PDF;
        } else {
          out += buf;
        }
      }
      return out;
    })
    .join('\n');

    const transform = txt => mode === 'full' ? reverseLines(txt) : smartReverse(txt);

    /* ---------- UI / MENU ---------- */
    function buildMenu() {
        GM_registerMenuCommand('Reverse selected lines', processSelection);
        GM_registerMenuCommand(
            `Mode: ${mode.toUpperCase()} (click to toggle)`,
            () => {
                mode = mode === 'full' ? 'smart' : 'full';
                localStorage.setItem(STORAGE_KEY, mode);
                buildMenu();                // refresh label
            }
        );
        GM_registerMenuCommand(
            `DEBUG: ${isDebugMode ? 'ON' : 'OFF'} (click to toggle)`,
            () => { isDebugMode = !isDebugMode; buildMenu(); }
        );
    }

    /* ---------- CLIPBOARD HELPERS ---------- */
    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);
        }
    }

    /* ---------- SELECTION / INPUT LOGIC  ---------- */
    function locateRealInput(node) {
        let cur = node;
        while (cur && cur !== document.documentElement) {
            if (cur.nodeType !== 1) { cur = cur.parentNode; continue; }
            if (cur.tagName === 'INPUT' || cur.tagName === 'TEXTAREA')
                return { element: cur, type: cur.tagName.toLowerCase() };
            if (cur.contentEditable === 'true')
                return { element: cur, type: 'contenteditable' };
            if (cur.shadowRoot) {
                const sr = cur.shadowRoot;
                const active = sr.activeElement || sr.querySelector('input, textarea, [contenteditable="true"]');
                if (active) return locateRealInput(active);
            }
            cur = cur.parentNode || cur.host;
        }
        return null;
    }

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

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

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

        if (type === 'input' || type === 'textarea') {
            original = el.value;
            start = el.selectionStart;
            end   = el.selectionEnd;
        } 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);
        const reversed   = transform(chunk);
        const replacement =
            start === end
                ? reversed
                : original.slice(0, start) + reversed + original.slice(end);

        if (isDebugMode) copyToClipboard(reversed);

        if (type === 'input' || type === 'textarea') {
            el.value = replacement;
            el.setSelectionRange(start, start + reversed.length);
        } else {
            el.textContent = replacement;
            const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null);
            let node, offset = 0, startNode;
            while (node = walker.nextNode()) {
                const len = node.textContent.length;
                if (!startNode && offset + len >= start) {
                    startNode = node;
                    const r = new Range();
                    r.setStart(startNode, start - offset);
                    r.setEnd(startNode, start - offset + reversed.length);
                    sel.removeAllRanges(); sel.addRange(r);
                    break;
                }
                offset += len;
            }
        }
    }

    /* ---------- KEYBOARD SHORTCUT ---------- */
    document.addEventListener('keydown', e => {
        if (e.altKey && e.key.toLowerCase() === 'r') {
            e.preventDefault();
            processSelection();
        }
    });

    buildMenu();
})();