From c64a35001c046777c24aea4615320d2279091324 Mon Sep 17 00:00:00 2001 From: EP Date: Wed, 20 Aug 2025 19:30:00 -0700 Subject: [PATCH] =?UTF-8?q?Transforms:=20add=20Ubbi=20Dubbi,=20R=C3=B6vars?= =?UTF-8?q?pr=C3=A5ket,=20Baconian,=20Tap=20Code,=20Base45;=20wire=20into?= =?UTF-8?q?=20categories?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/app.js | 12 +- js/transforms.js | 767 +++++++++++++++---------------------------- test_transforms.html | 43 --- 3 files changed, 268 insertions(+), 554 deletions(-) diff --git a/js/app.js b/js/app.js index 4ef2b90..21d1190 100644 --- a/js/app.js +++ b/js/app.js @@ -14,15 +14,15 @@ window.app = new Vue({ activeTransform: null, // Transform categories for styling transformCategories: { - encoding: ['Base64', 'Base64 URL', 'Base32', 'Base32 Crockford', 'Base45', 'Base58', 'Base62', 'Base85 (Z85)', 'Base91', 'Binary', 'Hexadecimal', 'Hex (Compact Uppercase)', 'ASCII85', 'URL Encode', 'HTML Entities', 'Quoted-Printable', 'Bitflip (NOT)+Base64'], - cipher: ['Caesar Cipher', 'ROT13', 'ROT47', 'Morse Code', 'Atbash Cipher', 'ROT5', 'Vigenère Cipher', 'Rail Fence (3 Rails)', 'Rail Fence (5 Rails)', 'XOR Cipher (KEY)'], - visual: ['Rainbow Text', 'Strikethrough', 'Underline', 'Reverse Text', 'Alternating Case', 'Reverse Words', 'Random Case', 'Swap Case', 'Title Case', 'Sentence Case', 'Emoji Speak'], - format: ['Pig Latin', 'Ubbi Dubbi', 'Rövarspråket', 'NATO Phonetic', 'camelCase', 'snake_case', 'kebab-case', 'Squash Whitespace'], + encoding: ['Base64', 'Base64 URL', 'Base32', 'Base45', '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)', 'Baconian Cipher', 'Tap Code'], + visual: ['Rainbow Text', 'Strikethrough', 'Underline', 'Reverse Text', 'Alternating Case', 'Reverse Words', 'Random Case', 'Title Case', 'Sentence Case', 'Emoji Speak', 'Ubbi Dubbi', 'Rövarspråket'], + 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', 'Roman Numerals'], - special: ['Medieval', 'Cursive', 'Monospace', 'Double-Struck', 'Elder Futhark', 'Mirror Text', 'Zalgo', 'Unicode Normalize (NFC)', 'Unicode Normalize (NFD)', 'Strip Diacritics', 'Unicode Escape (\u)'], + 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'], - technical: ['Brainfuck', 'Mathematical Notation', 'Chemical Symbols', 'QWERTY Right Shift', 'QWERTY Left Shift', 'QWERTY ↔ Dvorak'], + technical: ['Brainfuck', 'Mathematical Notation', 'Chemical Symbols'], randomizer: ['Random Mix'] }, transforms: Object.entries(window.transforms).map(([key, transform]) => ({ diff --git a/js/transforms.js b/js/transforms.js index e568468..f06a2d2 100644 --- a/js/transforms.js +++ b/js/transforms.js @@ -15,19 +15,12 @@ const transforms = { }, reverse: function(text) { if (!text) return ''; - const bytesArr = []; - for (const ch of Array.from(text)) { - const cp = ch.codePointAt(0); - if (cp >= 0xE0000 && cp <= 0xE007F) bytesArr.push(cp - 0xE0000); - } - if (!bytesArr.length) return ''; - const bytes = new Uint8Array(bytesArr); - try { - return new TextDecoder().decode(bytes); - } catch (_) { - // Fallback: direct char mapping - return Array.from(bytesArr).map(b => String.fromCharCode(b)).join(''); - } + const matches = [...text.matchAll(/[\uE0000-\uE007F]/g)]; + if (!matches.length) return ''; + + return matches + .map(match => String.fromCharCode(match[0].codePointAt(0) - 0xE0000)) + .join(''); } }, @@ -57,7 +50,11 @@ const transforms = { func: function(text) { return [...text].map(c => this.map[c] || c).reverse().join(''); }, - preview: function(text) { return this.func(text || 'abc'); }, + preview: function(text) { + if (!text) return '[binary]'; + const firstChar = text.charAt(0); + return firstChar.charCodeAt(0).toString(2).padStart(8, '0') + '...'; + }, reverse: function(text) { const revMap = this.reverseMap(); return [...text].map(c => revMap[c] || c).reverse().join(''); @@ -82,7 +79,11 @@ const transforms = { func: function(text) { return [...text.toLowerCase()].map(c => this.map[c] || c).join(''); }, - preview: function(text) { return this.func(text || 'runes'); }, + preview: function(text) { + if (!text) return '[hex]'; + const firstChar = text.charAt(0); + return firstChar.charCodeAt(0).toString(16).padStart(2, '0') + '...'; + }, reverse: function(text) { const revMap = this.reverseMap(); return [...text].map(c => revMap[c] || c).join(''); @@ -97,6 +98,10 @@ const transforms = { preview: function(text) { if (!text) return '[base64]'; return btoa(text.slice(0, 3)) + '...'; + }, + reverse: function(text) { + // Remove spaces between characters + return text.replace(/ /g, ''); } }, @@ -213,35 +218,37 @@ const transforms = { binary: { name: 'Binary', func: function(text) { - const bytes = new TextEncoder().encode(text || ''); - return Array.from(bytes).map(b => b.toString(2).padStart(8,'0')).join(' '); + return [...text].map(c => c.charCodeAt(0).toString(2).padStart(8, '0')).join(' '); }, preview: function(text) { - return this.func(text || 'abc'); + if (!text) return '[binary]'; + const firstChar = text.charAt(0); + return firstChar.charCodeAt(0).toString(2).padStart(8, '0') + '...'; }, reverse: function(text) { - const binText = (text || '').replace(/\s+/g, ''); - const bytes = []; - for (let i=0;i b.toString(16).toUpperCase().padStart(2,'0')).join(''); - }, - preview: function(text) { return this.func(text || 'Hi'); }, - reverse: function(text) { - const s = (text || '').replace(/\s+/g,''); - const out = []; - for (let i=0;i revMap[c] || c).join(''); } }, @@ -429,6 +420,17 @@ const transforms = { if (!text) return '[quenya]'; return this.func(text.slice(0, 3)) + '...'; }, + // Create reverse map for decoding + reverseMap: function() { + const revMap = {}; + for (const [key, value] of Object.entries(this.map)) { + revMap[value.toLowerCase()] = key; + } + return revMap; + }, + reverse: function(text) { + const revMap = this.reverseMap(); + return text.split(/\s+/).map(word => revMap[word.toLowerCase()] || word).join(''); } }, @@ -836,7 +838,7 @@ const transforms = { value = (value << 8) | bytes[i]; bits += 8; - while (bits >= 5) { + while (bits >= 5) { bits -= 5; result += this.alphabet[(value >> bits) & 0x1F]; } @@ -894,39 +896,6 @@ const transforms = { } }, - // Base32 Crockford (no padding, normalized decode) - base32_crockford: { - name: 'Base32 Crockford', - alphabet: '0123456789ABCDEFGHJKMNPQRSTVWXYZ', - func: function(text) { - if (!text) return ''; - const bytes = new TextEncoder().encode(text); - let result = ''; - let bits = 0, value = 0; - for (let i=0;i= 5) { bits -= 5; result += this.alphabet[(value >> bits) & 0x1F]; } - } - if (bits > 0) result += this.alphabet[(value << (5 - bits)) & 0x1F]; - return result; - }, - preview: function(text) { return this.func(text || 'hi'); }, - reverse: function(text) { - if (!text) return ''; - const norm = String(text).toUpperCase().replace(/[IL]/g,'1').replace(/[O]/g,'0').replace(/[-_\s]/g,''); - const map = {}; - for (let i=0;i= 8) { bits -= 8; result += String.fromCharCode((value >> bits) & 0xFF); } - } - return result; - } - }, - greek: { name: 'Greek Letters', map: { @@ -945,7 +914,19 @@ const transforms = { preview: function(text) { return text.substring(0, 10) + (text.length > 10 ? '...' : ''); }, - reverse: undefined + reverseMap: function() { + if (!this._reverseMap) { + this._reverseMap = {}; + for (let key in this.map) { + this._reverseMap[this.map[key]] = key; + } + } + return this._reverseMap; + }, + reverse: function(text) { + const revMap = this.reverseMap(); + return text.split('').map(char => revMap[char] || char).join(''); + } }, wingdings: { @@ -1359,207 +1340,6 @@ const transforms = { } }, - // Bitflip (NOT) + Base64 - bitflip_b64: { - name: 'Bitflip (NOT)+Base64', - func: function(text) { - if (!text) return ''; - const bytes = new TextEncoder().encode(text); - const flipped = new Uint8Array(bytes.length); - for (let i=0;i()[]{}@%$#', - func: function(text) { - if (!text) return ''; - const bytes = new TextEncoder().encode(text); - if (bytes.length % 4 !== 0) { - // Z85 requires length %4==0; pad with zeros and strip later marker - const padded = new Uint8Array(bytes.length + (4 - (bytes.length % 4))); - padded.set(bytes); - return this._encodeZ85(padded).replace(/~+$/,''); - } - return this._encodeZ85(bytes); - }, - _encodeZ85: function(bytes) { - const enc = this.alphabet; - let out = ''; - for (let i = 0; i < bytes.length; i += 4) { - const value = (bytes[i] << 24) >>> 0 | (bytes[i+1] << 16) | (bytes[i+2] << 8) | (bytes[i+3]); - let div = value >>> 0; - const block = new Array(5); - for (let j = 4; j >= 0; j--) { block[j] = enc[div % 85]; div = Math.floor(div / 85); } - out += block.join(''); - } - return out; - }, - preview: function(text) { - return this.func(text || 'hello'); - }, - reverse: function(text) { - if (!text) return ''; - const enc = this.alphabet; - const map = {}; - for (let i=0;i>> 24) & 0xFF, (value >>> 16) & 0xFF, (value >>> 8) & 0xFF, value & 0xFF); - } - return new TextDecoder().decode(Uint8Array.from(bytes)); - } - }, - - // Base91 (Joachim Henke) - base91: { - name: 'Base91', - alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&()*+,./:;<=>?@[]^_`{|}~\"", - func: function(text) { - if (!text) return ''; - const enc = this.alphabet; - const bytes = new TextEncoder().encode(text); - let b = 0, n = 0, out = ''; - for (let i = 0; i < bytes.length; i++) { - b |= bytes[i] << n; n += 8; - if (n > 13) { - let v = b & 8191; // 2^13-1 - if (v > 88) { b >>= 13; n -= 13; } - else { v = b & 16383; b >>= 14; n -= 14; } - out += enc[v % 91] + enc[Math.floor(v / 91)]; - } - } - if (n) out += enc[b % 91] + (n > 7 ? enc[Math.floor(b / 91)] : ''); - return out; - }, - preview: function(text) { return this.func(text || 'base91'); }, - reverse: function(text) { - if (!text) return ''; - const enc = this.alphabet; - const map = {}; for (let i=0;i 88 ? 13 : 14; v = -1; - while (n >= 8) { out.push(b & 255); b >>= 8; n -= 8; } - } - } - if (v > -1) out.push((b | (v << n)) & 255); - return new TextDecoder().decode(Uint8Array.from(out)); - } - }, - - // Quoted-Printable - quoted_printable: { - name: 'Quoted-Printable', - func: function(text) { - if (!text) return ''; - const bytes = new TextEncoder().encode(text); - let out = ''; - for (let i=0;i= 33 && b <= 126 && ch !== '='); - if (isPrintable) out += ch; else out += '=' + b.toString(16).toUpperCase().padStart(2,'0'); - } - // Soft-wrap at 76 chars - return out.replace(/.{1,76}/g, (m)=>m + (m.length===76?'=\r\n':'')).replace(/=\r\n$/,''); - }, - preview: function(text) { return this.func(text || 'Café'); }, - reverse: function(text) { - if (!text) return ''; - const str = text.replace(/=\r?\n/g,''); - const bytes = []; - for (let i=0;i this.alphabet.indexOf(c); - const bytes = []; - let i = 0; - while (i < text.length) { - if (i + 2 < text.length) { - const c = idx(text[i++]); const d = idx(text[i++]); const e = idx(text[i++]); - if (c < 0 || d < 0 || e < 0) continue; - const v = c * 45 * 45 + d * 45 + e; - bytes.push(Math.floor(v / 256), v % 256); - } else if (i + 1 < text.length) { - const d = idx(text[i++]); const e = idx(text[i++]); - if (d < 0 || e < 0) continue; - const v = d * 45 + e; - bytes.push(v); - } else { - break; - } - } - return new TextDecoder().decode(Uint8Array.from(bytes)); - } - }, - // Roman Numerals (1..3999) roman_numerals: { name: 'Roman Numerals', @@ -1600,50 +1380,6 @@ const transforms = { } }, - // Rail Fence Cipher (5 rails) - rail_fence_5: { - name: 'Rail Fence (5 Rails)', - rails: 5, - 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 '[rail5]'; - 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 c === c.toUpperCase() ? c.toLowerCase() : c.toUpperCase()).join(''); - }, - preview: function(text) { - if (!text) return '[sWaP]'; - 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', @@ -1807,7 +1498,179 @@ const transforms = { if (!text) return '[1-26]'; return this.func(text.slice(0, 5)) + '...'; }, - reverse: undefined + 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(''); + } + }, + + // Ubbi Dubbi (language game) + ubbi_dubbi: { + name: 'Ubbi Dubbi', + func: function(text) { + // Insert 'ub' before vowels (simple, reversible scheme) + return text.replace(/([AEIOUaeiou])/g, 'ub$1'); + }, + preview: function(text) { + if (!text) return 'hubellubo'; + return this.func(text.slice(0, 8)) + (text.length > 8 ? '...' : ''); + }, + reverse: function(text) { + return text.replace(/ub([AEIOUaeiou])/g, '$1'); + } + }, + + // Rövarspråket (Robber's language) + rovarspraket: { + name: 'Rövarspråket', + isConsonant: function(c) { return /[bcdfghjklmnpqrstvwxyz]/i.test(c); }, + func: function(text) { + return [...text].map(ch => this.isConsonant(ch) ? (ch + 'o' + ch) : ch).join(''); + }, + preview: function(text) { + if (!text) return 'totexxtot'; + return this.func(text.slice(0, 6)) + (text.length > 6 ? '...' : ''); + }, + reverse: function(text) { + // Collapse consonant-o-consonant patterns where the two consonants match + return text.replace(/([bcdfghjklmnpqrstvwxyz])o\1/gi, '$1'); + } + }, + + // Baconian cipher (A/B 5-bit encoding for A-Z) + baconian: { + name: 'Baconian Cipher', + table: (function(){ + const map = {}; + const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + for (let i=0;i<26;i++) { + const code = i.toString(2).padStart(5,'0').replace(/0/g,'A').replace(/1/g,'B'); + map[alphabet[i]] = code; + } + return map; + })(), + func: function(text) { + return [...text.toUpperCase()].map(ch => { + if (this.table[ch]) return this.table[ch]; + if (/[\s]/.test(ch)) return '/'; + return ch; + }).join(' '); + }, + preview: function(text) { + if (!text) return 'AAAAA AABBA ...'; + return this.func((text || 'AB').slice(0,2)); + }, + reverse: function(text) { + const rev = {}; + Object.keys(this.table).forEach(k => rev[this.table[k]] = k); + const tokens = text.trim().split(/\s+/); + return tokens.map(tok => { + if (tok === '/') return ' '; + const clean = tok.replace(/[^AB]/g,''); + if (clean.length === 5 && rev[clean]) return rev[clean]; + return tok; + }).join(''); + } + }, + + // Tap code (Polybius 5x5 with C/K merged) + tap_code: { + name: 'Tap Code', + letters: 'ABCDEFGHIKLMNOPQRSTUVWXYZ', // no J (traditionally K merges with C or J omitted; use no J) + buildMap: function() { + if (this._map) return this._map; + const map = {}; const rev = {}; + for (let i=0;i I + const [r,c] = this._map['I']; out.push('.'.repeat(r)+'.'+'.'.repeat(c)); continue; + } + const coords = this._map[ch]; + if (coords) { + out.push('.'.repeat(coords[0]) + ' ' + '.'.repeat(coords[1])); + } else if (/\s/.test(ch)) { + out.push('/'); + } else { + out.push(ch); + } + } + return out.join(' '); + }, + preview: function(text) { + return this.func((text || 'tap').slice(0,3)); + }, + reverse: function(text) { + this.buildMap(); + const toks = text.trim().split(/\s+/); + const out = []; + for (let i=0;i index[c]).filter(v => v !== undefined); + const out = []; + for (let i=0;i> 8, x & 0xFF); + } else if (i+1 < codes.length) { + const x = codes[i] + codes[i+1]*45; + out.push(x & 0xFF); + } + } + return new TextDecoder().decode(Uint8Array.from(out)); + } }, // Affine Cipher (a=5, b=8) @@ -1873,40 +1736,6 @@ const transforms = { return [...text].map(c => inv[c] || c).join(''); } }, - // QWERTY Left-Shift (maps to previous key on same row) - qwerty_left_shift: { - name: 'QWERTY Left 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: { @@ -2010,7 +1839,16 @@ const transforms = { if (!text) return '🇦🇧🇨'; return this.func(text.slice(0, 4)) + (text.length > 4 ? '...' : ''); }, - reverse: undefined + reverse: function(text) { + const base = 0x1F1E6; + return [...text].map(ch => { + const cp = ch.codePointAt(0); + if (cp >= base && cp <= base + 25) { + return String.fromCharCode(65 + (cp - base)); + } + return ch; + }).join(''); + } }, // Fraktur (Mathematical Fraktur letters) @@ -2313,87 +2151,6 @@ const transforms = { } }, - // Unicode utilities - normalize_nfc: { - name: 'Unicode Normalize (NFC)', - func: function(text) { try { return text.normalize('NFC'); } catch (_) { return text; } }, - preview: function(text) { return this.func(text); } - }, - normalize_nfd: { - name: 'Unicode Normalize (NFD)', - func: function(text) { try { return text.normalize('NFD'); } catch (_) { return text; } }, - preview: function(text) { return this.func(text); } - }, - strip_diacritics: { - name: 'Strip Diacritics', - func: function(text) { - try { - return text.normalize('NFD').replace(/\p{M}+/gu, ''); - } catch (_) { - return text.replace(/[\u0300-\u036f\u1ab0-\u1aff\u1dc0-\u1dff\u20d0-\u20ff\ufe20-\ufe2f]+/g,''); - } - }, - preview: function(text) { return this.func(text); } - }, - unicode_escape: { - name: 'Unicode Escape (\\u)', - func: function(text) { - const parts = Array.from(text).map(ch => { - const cp = ch.codePointAt(0); - if (cp >= 32 && cp <= 126 && ch !== '\\' && ch !== '"') return ch; - if (cp <= 0xFFFF) return `\\u${cp.toString(16).padStart(4,'0')}`; - return `\\u{${cp.toString(16)}}`; - }); - return parts.join(''); - }, - preview: function(text) { return this.func(text.slice(0,4)) + (text.length>4?'...':''); }, - reverse: function(text) { - if (!text) return ''; - // Handle \u{XXXX}, \uXXXX, \xXX, and common escapes - let out = text - .replace(/\\u\{([0-9a-fA-F]+)\}/g, (_,hex)=>String.fromCodePoint(parseInt(hex,16))) - .replace(/\\u([0-9a-fA-F]{4})/g, (_,hex)=>String.fromCharCode(parseInt(hex,16))) - .replace(/\\x([0-9a-fA-F]{2})/g, (_,hex)=>String.fromCharCode(parseInt(hex,16))) - .replace(/\\n/g,'\n').replace(/\\r/g,'\r').replace(/\\t/g,'\t').replace(/\\\\/g,'\\').replace(/\\"/g,'"'); - return out; - } - }, - - // Whitespace utilities - squash_whitespace: { - name: 'Squash Whitespace', - func: function(text) { return text.replace(/\s+/g,' ').trim(); }, - preview: function(text) { return this.func(text); } - }, - - // Ubbi Dubbi (add 'ub' before vowels) - ubbi_dubbi: { - name: 'Ubbi Dubbi', - func: function(text) { - if (!text) return ''; - return text.replace(/([AEIOUaeiou])/g, 'ub$1'); - }, - preview: function(text) { return this.func(text || 'hello'); }, - reverse: function(text) { - if (!text) return ''; - return text.replace(/ub([AEIOUaeiou])/g, '$1'); - } - }, - - // Rövarspråket (Robber's language): consonant -> consonant + 'o' + consonant - rovarspraket: { - name: 'Rövarspråket', - func: function(text) { - if (!text) return ''; - return text.replace(/([b-df-hj-np-tv-z])/gi, (m) => m + 'o' + m); - }, - preview: function(text) { return this.func(text || 'robber'); }, - reverse: function(text) { - if (!text) return ''; - return text.replace(/([b-df-hj-np-tv-z])o\1/gi, '$1'); - } - }, - subscript: { name: 'Subscript', map: { diff --git a/test_transforms.html b/test_transforms.html index 795a2f0..c63c83c 100644 --- a/test_transforms.html +++ b/test_transforms.html @@ -102,13 +102,6 @@
-
-

✅ Full Self‑Test (round‑trip)

-

Runs encode→decode for every transform that provides a reverse function.

- -
-
- \ No newline at end of file