From c306e9d0fd8c97c1a074fea94c387ff29ac6287c Mon Sep 17 00:00:00 2001
From: EP
Date: Wed, 20 Aug 2025 16:26:34 -0700
Subject: [PATCH] UI: Add global Advanced Unicode panel + header toggle; remove
duplicate from decoder; wire Apply to steganography options; minor UI polish
---
README.md | 10 ++
css/style.css | 93 ++++++++++++
index.html | 103 +++++++++++++
js/app.js | 45 +++++-
js/steganography.js | 91 ++++++++----
js/transforms.js | 346 +++++++++++++++++++++++++++++++++++++++++++-
6 files changed, 656 insertions(+), 32 deletions(-)
diff --git a/README.md b/README.md
index 1cb4f7a..ff60d68 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,8 @@ A powerful web-based text transformation and steganography tool that can encode/
#### **Encoding & Decoding**
- **Base64** - Standard base64 encoding/decoding
- **Base32** - RFC 4648 compliant base32 encoding/decoding
+- **Base58** - Bitcoin alphabet encoding/decoding
+- **Base62** - 0-9A-Za-z compact encoding/decoding
- **Binary** - Convert text to/from binary representation
- **Hexadecimal** - Convert text to/from hex format
- **ASCII85** - Advanced ASCII85 encoding/decoding
@@ -26,6 +28,8 @@ A powerful web-based text transformation and steganography tool that can encode/
- **ROT47** - Extended rotation cipher for ASCII 33-126
- **Morse Code** - International Morse code with proper spacing
- **NATO Phonetic** - NATO phonetic alphabet
+- **Vigenère Cipher** - Polyalphabetic cipher (default key "KEY")
+- **Rail Fence (3 Rails)** - Zig-zag transposition cipher
#### **Visual Transformations**
- **Upside Down** - Flip text upside down using Unicode characters
@@ -43,6 +47,11 @@ A powerful web-based text transformation and steganography tool that can encode/
- **Double-Struck** - Mathematical double-struck characters
- **Greek Letters** - Greek alphabet characters
- **Wingdings** - Symbol font characters
+- **Fraktur** - Mathematical Fraktur alphabet
+- **Cyrillic Stylized** - Latin letters mapped to similar Cyrillic glyphs
+- **Katakana** - Romaji to Katakana (approximate, reversible)
+- **Hiragana** - Romaji to Hiragana (approximate, reversible)
+- **Roman Numerals** - Numbers to Roman numerals (reversible)
#### **Fantasy Languages** 🧙♂️
- **Quenya (Tolkien Elvish)** - High Elvish language from Lord of the Rings
@@ -128,6 +137,7 @@ streamlit run parsel_app.py
### **New Features**
- 🆕 **50+ New Languages**: Added fantasy, ancient, and technical scripts
+- 🆕 **More Encodings/Ciphers**: Base58, Base62, Vigenère, Rail Fence, Roman Numerals
- 🆕 **Category Organization**: Better organized transform categories
- 🆕 **Enhanced Styling**: New color schemes for each category
- 🆕 **Improved Decoder**: Better detection and fallback mechanisms
diff --git a/css/style.css b/css/style.css
index f099d67..00321d8 100644
--- a/css/style.css
+++ b/css/style.css
@@ -1848,3 +1848,96 @@ button:hover {
30% { box-shadow: 0 0 18px rgba(156,39,176,0.6); }
100% { box-shadow: 0 0 0 rgba(156,39,176,0); }
}
+
+/* Steganography split layout */
+.steg-split-layout {
+ display: grid;
+ grid-template-columns: 280px 1fr;
+ gap: 16px;
+}
+
+.steg-advanced-sidebar {
+ position: sticky;
+ top: 8px;
+ height: max-content;
+ background: var(--main-bg-color);
+ border: 1px solid var(--input-border);
+ border-radius: 8px;
+ padding: 12px;
+}
+
+.steg-adv-panel label {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ margin-bottom: 8px;
+}
+
+.steg-adv-panel select, .steg-adv-panel input[type=number] {
+ background: var(--input-bg);
+ color: var(--text-color);
+ border: 1px solid var(--input-border);
+ border-radius: 4px;
+ padding: 6px 8px;
+}
+
+.steg-note {
+ display: block;
+ margin-top: 8px;
+ color: var(--text-muted);
+ font-size: 0.8rem;
+}
+
+@media (max-width: 900px) {
+ .steg-split-layout { grid-template-columns: 1fr; }
+ .steg-advanced-sidebar { position: relative; top: 0; }
+}
+
+/* Global Unicode options panel (subtle, like copy history) */
+.unicode-options-panel {
+ position: fixed;
+ right: -360px;
+ top: 0;
+ width: 340px;
+ height: 100vh;
+ background: var(--secondary-bg);
+ border-left: 1px solid var(--input-border);
+ z-index: 200;
+ box-shadow: -5px 0 15px rgba(0,0,0,0.3);
+ transition: right 0.3s ease-in-out;
+ display: flex;
+ flex-direction: column;
+}
+
+.unicode-options-panel.active { right: 0; }
+
+.unicode-panel-header {
+ padding: 12px;
+ border-bottom: 1px solid var(--input-border);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.unicode-panel-header h3 { margin: 0; color: var(--accent-color); font-size: 1rem; }
+
+.unicode-panel-content { padding: 12px; overflow-y: auto; }
+
+/* Fluid UX: reduce scroll fatigue during repeated actions */
+html {
+ scroll-behavior: smooth;
+}
+
+.transform-layout {
+ position: relative;
+}
+
+/* Keep the transform input visible while scrolling */
+.transform-layout .input-section {
+ position: sticky;
+ top: 8px;
+ z-index: 20;
+ background: var(--secondary-bg);
+ border: 1px solid var(--input-border);
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
+}
diff --git a/index.html b/index.html
index 3171d2d..530c816 100644
--- a/index.html
+++ b/index.html
@@ -44,6 +44,14 @@
>
+
@@ -141,6 +149,7 @@
Paste any encoded text to try all decoding methods at once
+
+
+
+
+
+
+
+
+
+
+
+
+
+ These options affect Unicode-based steganography encoding/decoding.
+
+
+
@@ -628,6 +702,35 @@
}, true);
})();
+ // Wire up advanced steg options to steganography engine
+ document.addEventListener('DOMContentLoaded', function(){
+ document.querySelectorAll('.apply-steg-options').forEach(btn => {
+ btn.addEventListener('click', function(e){
+ e.preventDefault();
+ const panel = btn.closest('.steg-adv-panel');
+ if (!panel) return;
+ const initSel = panel.querySelector('.steg-initial-presentation')?.value || 'emoji';
+ const vs0 = panel.querySelector('.steg-vs-zero')?.value || '\\ufe0e';
+ const vs1 = panel.querySelector('.steg-vs-one')?.value || '\\ufe0f';
+ const inter = panel.querySelector('.steg-inter-zw')?.value || null;
+ const trailing = panel.querySelector('.steg-trailing-zw')?.value || null;
+ const every = parseInt(panel.querySelector('.steg-inter-every')?.value || '1', 10);
+ const order = panel.querySelector('.steg-bit-order')?.value || 'msb';
+ if (window.steganography && window.steganography.setStegOptions) {
+ window.steganography.setStegOptions({
+ initialPresentation: initSel,
+ bitZeroVS: vs0,
+ bitOneVS: vs1,
+ interBitZW: inter,
+ interBitEvery: isNaN(every) ? 1 : Math.max(1, Math.min(8, every)),
+ trailingZW: trailing,
+ bitOrder: order
+ });
+ }
+ });
+ });
+ });
+
// Function to initialize emoji grid with retries
function initEmojiGrid(retryCount) {
retryCount = retryCount || 0;
diff --git a/js/app.js b/js/app.js
index 63260cd..3d02894 100644
--- a/js/app.js
+++ b/js/app.js
@@ -14,11 +14,11 @@ window.app = new Vue({
activeTransform: null,
// Transform categories for styling
transformCategories: {
- encoding: ['Base64', 'Base32', 'Binary', 'Hexadecimal', 'ASCII85', 'URL Encode', 'HTML Entities'],
- cipher: ['Caesar Cipher', 'ROT13', 'ROT47', 'Morse Code', 'Atbash Cipher', 'ROT5'],
+ encoding: ['Base64', 'Base64 URL', 'Base32', 'Base58', 'Base62', 'Binary', 'Hexadecimal', 'ASCII85', 'URL Encode', 'HTML Entities'],
+ cipher: ['Caesar Cipher', 'ROT13', 'ROT47', 'Morse Code', 'Atbash Cipher', 'ROT5', 'Vigenère Cipher', 'Rail Fence (3 Rails)'],
visual: ['Rainbow Text', 'Strikethrough', 'Underline', 'Reverse Text', 'Alternating Case', 'Reverse Words', 'Random Case', 'Title Case', 'Sentence Case', 'Emoji Speak'],
format: ['Pig Latin', 'Leetspeak', 'NATO Phonetic', 'camelCase', 'snake_case', 'kebab-case'],
- unicode: ['Invisible Text', 'Upside Down', 'Full Width', 'Small Caps', 'Bubble', 'Braille', 'Greek Letters', 'Wingdings', 'Superscript', 'Subscript', 'Regional Indicator Letters', 'Fraktur', 'Cyrillic Stylized', 'Katakana', 'Hiragana'],
+ unicode: ['Invisible Text', 'Upside Down', 'Full Width', 'Small Caps', 'Bubble', 'Braille', 'Greek Letters', 'Wingdings', 'Superscript', 'Subscript', 'Regional Indicator Letters', 'Fraktur', 'Cyrillic Stylized', 'Katakana', 'Hiragana', 'Roman Numerals'],
special: ['Medieval', 'Cursive', 'Monospace', 'Double-Struck', 'Elder Futhark', 'Mirror Text', 'Zalgo'],
fantasy: ['Quenya (Tolkien Elvish)', 'Tengwar Script', 'Klingon', 'Aurebesh (Star Wars)', 'Dovahzul (Dragon)'],
ancient: ['Hieroglyphics', 'Ogham (Celtic)', 'Semaphore Flags'],
@@ -56,9 +56,18 @@ window.app = new Vue({
// History of copied content
copyHistory: [],
maxHistoryItems: 10,
- showCopyHistory: false
+ showCopyHistory: false,
+ showUnicodePanel: false
},
methods: {
+ toggleUnicodePanel() {
+ this.showUnicodePanel = !this.showUnicodePanel;
+ const panel = document.getElementById('unicode-options-panel');
+ if (panel) {
+ if (this.showUnicodePanel) panel.classList.add('active');
+ else panel.classList.remove('active');
+ }
+ },
// Focus an element without causing the page to scroll
focusWithoutScroll(el) {
if (!el) return;
@@ -1030,6 +1039,34 @@ window.app = new Vue({
}
}
+ // - Base58
+ if (/^[1-9A-HJ-NP-Za-km-z]+$/.test(input.trim())) {
+ try {
+ if (window.transforms.base58 && window.transforms.base58.reverse) {
+ const result = window.transforms.base58.reverse(input.trim());
+ if (result && /[\x20-\x7E]{3,}/.test(result)) {
+ return { text: result, method: 'Base58' };
+ }
+ }
+ } catch (e) {
+ console.error('Base58 decode error:', e);
+ }
+ }
+
+ // - Base62
+ if (/^[0-9A-Za-z]+$/.test(input.trim())) {
+ try {
+ if (window.transforms.base62 && window.transforms.base62.reverse) {
+ const result = window.transforms.base62.reverse(input.trim());
+ if (result && /[\x20-\x7E]{3,}/.test(result)) {
+ return { text: result, method: 'Base62' };
+ }
+ }
+ } catch (e) {
+ console.error('Base62 decode error:', e);
+ }
+ }
+
// - Upside Down text
if (window.transforms.upside_down && window.transforms.upside_down.reverse) {
try {
diff --git a/js/steganography.js b/js/steganography.js
index b57c1b5..da7e296 100644
--- a/js/steganography.js
+++ b/js/steganography.js
@@ -1,4 +1,19 @@
// Steganography carriers
+// Global adjustable options for selectors/zero-width usage
+const __STEG_DEFAULTS__ = {
+ bitZeroVS: '\ufe0e', // VS15 as 0
+ bitOneVS: '\ufe0f', // VS16 as 1
+ initialPresentation: 'emoji', // 'emoji' -> VS16, 'text' -> VS15, 'none'
+ trailingZW: '\u200B', // e.g., ZWSP; set to null to disable
+ interBitZW: null, // e.g., '\u200C' ZWNJ, '\u200D' ZWJ; null disables
+ interBitEvery: 1, // insert interBitZW every N bits (1 = after each bit)
+ bitOrder: 'msb' // 'msb' or 'lsb' within each byte
+};
+let __stegOptions__ = Object.assign({}, __STEG_DEFAULTS__);
+function setStegOptions(opts) {
+ if (!opts) return;
+ __stegOptions__ = Object.assign({}, __stegOptions__, opts);
+}
// First define encoding function for preview usage
function encodeForPreview(emoji, text) {
if (!text) return emoji;
@@ -9,20 +24,28 @@ function encodeForPreview(emoji, text) {
.join('');
// Use variation selectors to encode binary
- const vs15 = '\ufe0e'; // text variation selector (0)
- const vs16 = '\ufe0f'; // emoji variation selector (1)
+ const vs0 = __stegOptions__.bitZeroVS || '\ufe0e';
+ const vs1 = __stegOptions__.bitOneVS || '\ufe0f';
// Start with the emoji character
// Ensure the emoji has a presentation selector first to standardize it
- let result = emoji + vs16; // Add emoji presentation selector first
+ let result = emoji;
+ if (__stegOptions__.initialPresentation === 'emoji') result += '\ufe0f';
+ else if (__stegOptions__.initialPresentation === 'text') result += '\ufe0e';
// Add variation selectors based on binary representation
- for (const bit of binary) {
- result += bit === '0' ? vs15 : vs16;
+ for (let i=0;i m[0] === '\ufe0e' ? '0' : '1').join('');
+ // Extract variation selectors only
+ const rawSeq = emojiData[1];
+ const matches = [...rawSeq.matchAll(/[\ufe0e\ufe0f]/g)];
+ if (matches.length === 0) return '';
+ // Decide if the first selector is presentation
+ const skip = (__stegOptions__.initialPresentation === 'none') ? 0 : 1;
+ if (matches.length <= skip) return '';
+ const zeroSel = __stegOptions__.bitZeroVS || '\ufe0e';
+ const oneSel = __stegOptions__.bitOneVS || '\ufe0f';
+ let binary = matches.slice(skip).map(m => m[0] === zeroSel ? '0' : (m[0] === oneSel ? '1' : '')).join('');
// Make sure we have complete bytes (multiples of 8 bits)
const validBinaryLength = Math.floor(binary.length / 8) * 8;
- // Convert binary to text
+ // Convert binary to text (respect bitOrder)
let decoded = '';
for (let i = 0; i < validBinaryLength; i += 8) {
- const byte = binary.slice(i, i + 8);
+ let byte = binary.slice(i, i + 8);
+ if (__stegOptions__.bitOrder === 'lsb') {
+ byte = byte.split('').reverse().join('');
+ }
if (byte.length === 8) {
const charCode = parseInt(byte, 2);
// Only include printable ASCII characters
@@ -191,5 +227,6 @@ window.steganography = {
encodeEmoji,
decodeEmoji,
encodeInvisible,
- decodeInvisible
+ decodeInvisible,
+ setStegOptions
};
diff --git a/js/transforms.js b/js/transforms.js
index b3b7849..e63a31a 100644
--- a/js/transforms.js
+++ b/js/transforms.js
@@ -263,6 +263,26 @@ const transforms = {
}
},
+ base64url: {
+ name: 'Base64 URL',
+ func: function(text) {
+ if (!text) return '';
+ const std = btoa(text);
+ return std.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/,'');
+ },
+ preview: function(text) {
+ if (!text) return '[b64url]';
+ return this.func(text.slice(0,3)) + '...';
+ },
+ reverse: function(text) {
+ if (!text) return '';
+ let std = text.replace(/-/g, '+').replace(/_/g, '/');
+ // pad
+ while (std.length % 4 !== 0) std += '=';
+ try { return atob(std); } catch (e) { return text; }
+ }
+ },
+
hex: {
name: 'Hexadecimal',
func: function(text) {
@@ -1229,6 +1249,329 @@ const transforms = {
}
},
+ // Base58 (Bitcoin alphabet)
+ base58: {
+ name: 'Base58',
+ alphabet: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz',
+ func: function(text) {
+ if (!text) return '';
+ const bytes = new TextEncoder().encode(text);
+ // Count leading zeros
+ let zeros = 0;
+ for (let b of bytes) { if (b === 0) zeros++; else break; }
+ // Convert to BigInt
+ let n = 0n;
+ for (let b of bytes) { n = (n << 8n) + BigInt(b); }
+ // Encode
+ let out = '';
+ while (n > 0n) {
+ const rem = n % 58n;
+ n = n / 58n;
+ out = this.alphabet[Number(rem)] + out;
+ }
+ // Add leading zeros as '1'
+ for (let i = 0; i < zeros; i++) out = '1' + out;
+ return out || '1';
+ },
+ preview: function(text) {
+ if (!text) return '[base58]';
+ return this.func(text.slice(0, 3)) + '...';
+ },
+ reverse: function(text) {
+ if (!text) return '';
+ // Count leading '1's
+ let zeros = 0;
+ for (let c of text) { if (c === '1') zeros++; else break; }
+ // Convert to BigInt
+ let n = 0n;
+ for (let c of text) {
+ const i = this.alphabet.indexOf(c);
+ if (i < 0) continue;
+ n = n * 58n + BigInt(i);
+ }
+ // Convert BigInt to bytes
+ const bytes = [];
+ while (n > 0n) {
+ bytes.unshift(Number(n % 256n));
+ n = n / 256n;
+ }
+ for (let i = 0; i < zeros; i++) bytes.unshift(0);
+ return new TextDecoder().decode(Uint8Array.from(bytes));
+ }
+ },
+
+ // Base62 (0-9A-Za-z)
+ base62: {
+ name: 'Base62',
+ alphabet: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
+ func: function(text) {
+ if (!text) return '';
+ const bytes = new TextEncoder().encode(text);
+ let n = 0n;
+ for (let b of bytes) { n = (n << 8n) + BigInt(b); }
+ if (n === 0n) return '0';
+ let out = '';
+ while (n > 0n) {
+ const rem = n % 62n;
+ n = n / 62n;
+ out = this.alphabet[Number(rem)] + out;
+ }
+ return out;
+ },
+ preview: function(text) {
+ if (!text) return '[base62]';
+ return this.func(text.slice(0, 3)) + '...';
+ },
+ reverse: function(text) {
+ if (!text) return '';
+ let n = 0n;
+ for (let c of text) {
+ const i = this.alphabet.indexOf(c);
+ if (i < 0) continue;
+ n = n * 62n + BigInt(i);
+ }
+ const bytes = [];
+ while (n > 0n) {
+ bytes.unshift(Number(n % 256n));
+ n = n / 256n;
+ }
+ if (bytes.length === 0) bytes.push(0);
+ return new TextDecoder().decode(Uint8Array.from(bytes));
+ }
+ },
+
+ // Roman Numerals (1..3999)
+ roman_numerals: {
+ name: 'Roman Numerals',
+ numerals: [
+ ['M',1000],['CM',900],['D',500],['CD',400],
+ ['C',100],['XC',90],['L',50],['XL',40],
+ ['X',10],['IX',9],['V',5],['IV',4],['I',1]
+ ],
+ func: function(text) {
+ return text.replace(/\b\d+\b/g, m => {
+ let num = parseInt(m,10);
+ if (num <= 0 || num > 3999 || isNaN(num)) return m;
+ let out = '';
+ for (const [sym,val] of this.numerals) {
+ while (num >= val) { out += sym; num -= val; }
+ }
+ return out;
+ });
+ },
+ preview: function(text) {
+ return this.func(text || '2024');
+ },
+ reverse: function(text) {
+ // Greedy parse roman numerals to digits
+ const map = {I:1,V:5,X:10,L:50,C:100,D:500,M:1000};
+ const tokenize = s => s.match(/[IVXLCDM]+|[^IVXLCDM]+/gi) || [s];
+ return tokenize(text).map(tok => {
+ if (!/^[IVXLCDM]+$/i.test(tok)) return tok;
+ const s = tok.toUpperCase();
+ let total = 0;
+ for (let i=0;i= 65 && code <= 90) { out += String.fromCharCode(65 + ((code-65 + k)%26)); j++; }
+ else if (code >= 97 && code <= 122) { out += String.fromCharCode(97 + ((code-97 + k)%26)); j++; }
+ else out += c;
+ }
+ return out;
+ },
+ preview: function(text) {
+ if (!text) return '[Vigenère]';
+ return this.func(text.slice(0,8)) + (text.length>8?'...':'');
+ },
+ reverse: function(text) {
+ const key = this.key;
+ let out = '';
+ let j = 0;
+ for (let i=0;i= 65 && code <= 90) { out += String.fromCharCode(65 + ((code-65 + 26 - (k%26))%26)); j++; }
+ else if (code >= 97 && code <= 122) { out += String.fromCharCode(97 + ((code-97 + 26 - (k%26))%26)); j++; }
+ else out += c;
+ }
+ return out;
+ }
+ },
+
+ // Rail Fence Cipher (3 rails)
+ rail_fence: {
+ name: 'Rail Fence (3 Rails)',
+ rails: 3,
+ func: function(text) {
+ const rails = Array.from({length: this.rails}, () => []);
+ let rail = 0, dir = 1;
+ for (const ch of text) {
+ rails[rail].push(ch);
+ rail += dir;
+ if (rail === 0 || rail === this.rails-1) dir *= -1;
+ }
+ return rails.flat().join('');
+ },
+ preview: function(text) {
+ if (!text) return '[rail]';
+ return this.func(text.slice(0,12)) + (text.length>12?'...':'');
+ },
+ reverse: function(text) {
+ const len = text.length;
+ const pattern = [];
+ let rail = 0, dir = 1;
+ for (let i=0;i {
+ const code = c.charCodeAt(0);
+ if (code >= 65 && code <= 90) return String.fromCharCode(65 + ((code-65 + 13)%26));
+ if (code >= 97 && code <= 122) return String.fromCharCode(97 + ((code-97 + 13)%26));
+ return c;
+ };
+ const rot5 = c => {
+ if (c >= '0' && c <= '9') return String.fromCharCode(48 + (((c.charCodeAt(0)-48)+5)%10));
+ return c;
+ };
+ return [...text].map(c => rot5(rot13(c))).join('');
+ },
+ preview: function(text) {
+ if (!text) return '[rot18]';
+ return this.func(text.slice(0, 8)) + (text.length>8?'...':'');
+ },
+ reverse: function(text) { return this.func(text); }
+ },
+
+ // A1Z26 (letters to 1-26, separated by hyphens)
+ a1z26: {
+ name: 'A1Z26',
+ func: function(text) {
+ return text.replace(/[A-Za-z]/g, c => {
+ const n = (c.toUpperCase().charCodeAt(0) - 64);
+ return String(n) + '-';
+ }).replace(/-+(?!\d)/g,'-').replace(/-+$/,'');
+ },
+ preview: function(text) {
+ if (!text) return '[1-26]';
+ return this.func(text.slice(0, 5)) + '...';
+ },
+ reverse: function(text) {
+ return text.split(/([^0-9]+)/).map(tok => {
+ if (!/^\d+$/.test(tok)) return tok;
+ const n = parseInt(tok,10);
+ if (n>=1 && n<=26) return String.fromCharCode(64+n).toLowerCase();
+ return tok;
+ }).join('');
+ }
+ },
+
+ // Affine Cipher (a=5, b=8)
+ affine: {
+ name: 'Affine Cipher (a=5,b=8)',
+ a: 5, b: 8, m: 26, invA: 21, // 5*21 ≡ 1 (mod 26)
+ func: function(text) {
+ const {a,b,m} = this;
+ return [...text].map(c => {
+ const code = c.charCodeAt(0);
+ if (code>=65 && code<=90) return String.fromCharCode(65 + ((a*(code-65)+b)%m));
+ if (code>=97 && code<=122) return String.fromCharCode(97 + ((a*(code-97)+b)%m));
+ return c;
+ }).join('');
+ },
+ preview: function(text) {
+ if (!text) return '[affine]';
+ return this.func(text.slice(0,8)) + (text.length>8?'...':'');
+ },
+ reverse: function(text) {
+ const {invA,b,m} = this;
+ return [...text].map(c => {
+ const code = c.charCodeAt(0);
+ if (code>=65 && code<=90) return String.fromCharCode(65 + ((invA*((code-65 - b + m)%m))%m));
+ if (code>=97 && code<=122) return String.fromCharCode(97 + ((invA*((code-97 - b + m)%m))%m));
+ return c;
+ }).join('');
+ }
+ },
+
+ // QWERTY Right-Shift (maps to next key on same row)
+ qwerty_shift: {
+ name: 'QWERTY Right Shift',
+ rows: [
+ 'qwertyuiop',
+ 'asdfghjkl',
+ 'zxcvbnm'
+ ],
+ buildMap: function() {
+ if (this._map) return this._map;
+ const map = {};
+ for (const row of this.rows) {
+ for (let i=0;i m[c] || c).join('');
+ },
+ preview: function(text) {
+ if (!text) return '[qwerty]';
+ return this.func(text.slice(0,8)) + (text.length>8?'...':'');
+ },
+ reverse: function(text) {
+ const m = this.buildMap();
+ const inv = {};
+ Object.keys(m).forEach(k => inv[m[k]] = k);
+ return [...text].map(c => inv[c] || c).join('');
+ }
+ },
+
// Case/formatting transforms
title_case: {
name: 'Title Case',
@@ -1711,7 +2054,8 @@ const transforms = {
'hieroglyphics', 'ogham', 'mathematical', 'cursive', 'medieval',
'monospace', 'greek', 'braille', 'alternating_case', 'reverse_words',
'title_case', 'sentence_case', 'camel_case', 'snake_case', 'kebab_case', 'random_case',
- 'regional_indicator', 'fraktur', 'cyrillic_stylized', 'katakana', 'hiragana', 'emoji_speak'
+ 'regional_indicator', 'fraktur', 'cyrillic_stylized', 'katakana', 'hiragana', 'emoji_speak',
+ 'base58', 'base62', 'roman_numerals', 'vigenere', 'rail_fence', 'base64url'
];
return suitable.filter(name => window.transforms[name]);
},