diff --git a/js/app.js b/js/app.js index a2c05c2..be8e140 100644 --- a/js/app.js +++ b/js/app.js @@ -14,12 +14,12 @@ window.app = new Vue({ activeTransform: null, // Transform categories for styling transformCategories: { - 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'], + 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)', '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', 'Leetspeak', 'NATO Phonetic', 'camelCase', 'snake_case', 'kebab-case', 'Squash Whitespace'], 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'], + special: ['Medieval', 'Cursive', 'Monospace', 'Double-Struck', 'Elder Futhark', 'Mirror Text', 'Zalgo', 'Unicode Normalize (NFC)', 'Unicode Normalize (NFD)', 'Strip Diacritics', 'Unicode Escape (\u)'], fantasy: ['Quenya (Tolkien Elvish)', 'Tengwar Script', 'Klingon', 'Aurebesh (Star Wars)', 'Dovahzul (Dragon)'], ancient: ['Hieroglyphics', 'Ogham (Celtic)', 'Semaphore Flags'], technical: ['Brainfuck', 'Mathematical Notation', 'Chemical Symbols'], diff --git a/js/transforms.js b/js/transforms.js index e63a31a..70d6f3f 100644 --- a/js/transforms.js +++ b/js/transforms.js @@ -1340,6 +1340,57 @@ const transforms = { } }, + // Base45 (RFC 9285, used in QR payloads) + base45: { + name: 'Base45', + alphabet: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:', + func: function(text) { + if (!text) return ''; + const bytes = new TextEncoder().encode(text); + let out = ''; + for (let i = 0; i < bytes.length; i += 2) { + if (i + 1 < bytes.length) { + const x = bytes[i]; + const y = bytes[i + 1]; + const v = x * 256 + y; + const e = v % 45; const d = Math.floor(v / 45) % 45; const c = Math.floor(v / (45 * 45)); + out += this.alphabet[c] + this.alphabet[d] + this.alphabet[e]; + } else { + const x = bytes[i]; + const d = Math.floor(x / 45); const e = x % 45; + out += this.alphabet[d] + this.alphabet[e]; + } + } + return out; + }, + preview: function(text) { + if (!text) return '[base45]'; + return this.func(text.slice(0,3)) + '...'; + }, + reverse: function(text) { + if (!text) return ''; + const idx = c => 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', @@ -1380,6 +1431,50 @@ 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', @@ -1986,6 +2126,59 @@ 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); } + }, + subscript: { name: 'Subscript', map: {