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 github.com/annaroblox
// @version 1.7
// @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';
const RLO = "\u202E"; // RIGHT-TO-LEFT OVERRIDE
const PDF = "\u202C"; // POP DIRECTIONAL FORMATTING
const RLI = "\u2067"; // RIGHT-TO-LEFT ISOLATE
const PDI = "\u2069"; // POP DIRECTIONAL ISOLATE
const LRO = "\u202D"; // LEFT-TO-RIGHT OVERRIDE
let isDebugMode = false;
let mode = localStorage.getItem(STORAGE_KEY) || 'smart';
/* ---------- CORE TEXT TRANSFORM ---------- */
const reverseLines = text =>
text.split('\n')
.map(l => RLO + [...l].reverse().join('') + PDF) // Added PDF for better compatibility
.join('\n');
/**
* smartreverse function
* How it works:
* 1. Tokenize the line into words and whitespace.
* 2. For each word:
* a. Break it into pairs of characters and swap them (e.g., "test" -> "et", "ts").
* b. Wrap each swapped pair in RLI + RLO + ... + PDI.
* - RLI/PDI (Isolates) prevent the directional change from affecting other parts of the text.
* - RLO (Override) visually reverses the already swapped pair back to the correct order (e.g., "et" looks like "te").
* c. Append any leftover odd character without modification.
* 3. Whitespace tokens are left unchanged.
* 4. Join all the processed tokens back together.
*/
const smartReverse = text =>
text
.split('\n')
.map(line => {
const tokens = line.match(/(\s+|\S+)/g) || [];
const transformedTokens = tokens.map(tk => {
// If the token is just whitespace, return it as is.
if (/^\s+$/.test(tk)) {
return tk;
}
// It's a word, so process it.
const chars = [...tk];
let processedWord = '';
for (let i = 0; i < chars.length; i += 2) {
if (i + 1 < chars.length) {
// A pair exists. Swap the characters.
const swappedPair = chars[i + 1] + chars[i];
// Wrap the pair in control characters to isolate and reverse it visually.
processedWord += RLI + RLO + swappedPair + PDI;
} else {
// An odd character at the end of the word. Append it directly.
processedWord += chars[i];
}
}
return processedWord;
});
return transformedTokens.join('');
})
.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(); // Rebuild menu to show new state
},
'm'
);
GM_registerMenuCommand(
`DEBUG: ${isDebugMode ? 'ON' : 'OFF'} (click to toggle)`,
() => { isDebugMode = !isDebugMode; buildMenu(); },
'd'
);
}
/* ---------- 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);
}
}
/* ---------- IMPROVED INPUT DETECTION ---------- */
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') return true;
if (el.isContentEditable) return true;
if (el.designMode === 'on') return true;
// Check ARIA attributes
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"]):not([type="checkbox"]):not([type="radio"])',
'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;
// Traverse up including shadow DOM boundaries
while (cur) {
if (cur.nodeType === 1 && isEditableElement(cur)) {
const tag = cur.tagName?.toLowerCase();
if (tag === 'input' || tag === 'textarea') {
return { element: cur, type: tag };
}
return { element: cur, type: 'contenteditable' };
}
if (cur.shadowRoot) {
const shadowEditable = findEditableInShadowDOM(cur.shadowRoot);
if (shadowEditable) {
const tag = shadowEditable.tagName?.toLowerCase();
if (tag === 'input' || tag === 'textarea') {
return { element: shadowEditable, type: tag };
}
return { element: shadowEditable, type: 'contenteditable' };
}
}
cur = cur.parentNode || cur.host;
// Handle shadow root boundaries
if (!cur && node.getRootNode) {
const root = node.getRootNode();
if (root?.host) cur = root.host;
}
}
// Check document.activeElement chain
let active = document.activeElement;
while (active) {
if (isEditableElement(active)) {
const tag = active.tagName?.toLowerCase();
if (tag === 'input' || tag === 'textarea') {
return { element: active, type: tag };
}
return { element: active, type: '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();
if (tag === 'input' || tag === 'textarea') {
return { element: shadowEditable, type: tag };
}
return { element: shadowEditable, type: 'contenteditable' };
}
break;
} else {
break;
}
}
// Check iframes
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();
if (tag === 'input' || tag === 'textarea') {
return { element: frameDoc.activeElement, type: tag };
}
return { element: frameDoc.activeElement, type: 'contenteditable' };
}
} catch (e) { /* Cross-origin */ }
}
} catch (e) {}
return null;
}
/* ---------- IMPROVED TEXT REPLACEMENT ---------- */
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;
// Trigger events for React/Vue/Angular
el.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
} else {
const sel = window.getSelection();
// Try execCommand first (better undo support)
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) {}
}
// Fallback: manual insertion
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 || el.innerText || '';
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; // Do nothing if there's no text to process
const reversed = transform(chunk);
if (isDebugMode) copyToClipboard(reversed);
replaceTextInInput(el, type, reversed, start, end);
}
/* ---------- KEYBOARD SHORTCUT ---------- */
document.addEventListener('keydown', e => {
if (e.altKey && e.key.toLowerCase() === 'r') {
e.preventDefault();
processSelection();
}
}, true); // Capture phase
buildMenu();
})();