From 577a9a93df34e51a4708e102b723269dfdd5cd36 Mon Sep 17 00:00:00 2001 From: EP Date: Wed, 20 Aug 2025 18:51:06 -0700 Subject: [PATCH] Transforms: add Base85 (Z85), Base91, and Quoted-Printable; register in Encoding category --- js/app.js | 2 +- js/transforms.js | 129 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) diff --git a/js/app.js b/js/app.js index ec1c7a8..bb03a1e 100644 --- a/js/app.js +++ b/js/app.js @@ -14,7 +14,7 @@ window.app = new Vue({ activeTransform: null, // Transform categories for styling transformCategories: { - encoding: ['Base64', 'Base64 URL', 'Base32', 'Base45', 'Base58', 'Base62', 'Binary', 'Hexadecimal', 'ASCII85', 'URL Encode', 'HTML Entities'], + encoding: ['Base64', 'Base64 URL', 'Base32', 'Base45', 'Base58', 'Base62', 'Base85 (Z85)', 'Base91', 'Binary', 'Hexadecimal', 'ASCII85', 'URL Encode', 'HTML Entities', 'Quoted-Printable'], 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', 'Ubbi Dubbi', 'Rövarspråket', 'NATO Phonetic', 'camelCase', 'snake_case', 'kebab-case', 'Squash Whitespace'], diff --git a/js/transforms.js b/js/transforms.js index 95e0a4a..f4acffc 100644 --- a/js/transforms.js +++ b/js/transforms.js @@ -1308,6 +1308,135 @@ const transforms = { } }, + // Base85 (Z85 variant) + base85_z85: { + name: 'Base85 (Z85)', + alphabet: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#', + 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