refactor: migrate to modular tool-based architecture

- Implement tool registry system with individual tool modules
- Reorganize transformers into categorized source modules
- Remove emojiLibrary.js, consolidate into EmojiUtils and emojiData
- Fix mobile close button and tooltip functionality
- Add build system for transforms and emoji data
- Migrate from Python backend to pure JavaScript
- Add comprehensive documentation and testing
- Improve code organization and maintainability
- Ignore generated files (transforms-bundle.js, emojiData.js)
This commit is contained in:
Dustin Farley
2025-12-02 19:02:18 -08:00
parent 105084437a
commit dc10a90851
146 changed files with 12712 additions and 8171 deletions
+111
View File
@@ -0,0 +1,111 @@
// ascii85 transform
import BaseTransformer from '../BaseTransformer.js';
export default new BaseTransformer({
name: 'ASCII85',
priority: 290,
// Detector: ASCII85 has distinctive <~ ~> wrapper
detector: function(text) {
return text.startsWith('<~') && text.endsWith('~>');
},
func: function(text) {
// Simple ASCII85 encoding implementation
// Use TextEncoder to properly handle multi-byte UTF-8 characters
const bytes = new TextEncoder().encode(text);
let result = '<~';
let buffer = 0;
let bufferLength = 0;
for (let i = 0; i < bytes.length; i++) {
buffer = (buffer << 8) | bytes[i];
bufferLength += 8;
if (bufferLength >= 32) {
let value = buffer >>> (bufferLength - 32);
buffer &= (1 << (bufferLength - 32)) - 1;
bufferLength -= 32;
if (value === 0) {
result += 'z';
} else {
for (let j = 4; j >= 0; j--) {
const digit = (value / Math.pow(85, j)) % 85;
result += String.fromCharCode(digit + 33);
}
}
}
}
// Handle remaining bits
if (bufferLength > 0) {
buffer <<= (32 - bufferLength);
let value = buffer;
const bytes = Math.ceil(bufferLength / 8);
for (let j = 4; j >= (4 - bytes); j--) {
const digit = (value / Math.pow(85, j)) % 85;
result += String.fromCharCode(digit + 33);
}
}
return result + '~>';
},
preview: function(text) {
if (!text) return '[ascii85]';
const full = this.func(text);
return full.substring(0, 16) + (full.length > 16 ? '...' : '');
},
reverse: function(text) {
// Check if it's a valid ASCII85 string
if (!text.startsWith('<~') || !text.endsWith('~>')) {
return text;
}
// Remove delimiters and whitespace
text = text.substring(2, text.length - 2).replace(/\s+/g, '');
const bytes = [];
let i = 0;
while (i < text.length) {
// Handle 'z' special case (represents 4 zero bytes)
if (text[i] === 'z') {
bytes.push(0, 0, 0, 0);
i++;
continue;
}
// Process a group of 5 characters
if (i < text.length) {
let value = 0;
const groupSize = Math.min(5, text.length - i);
// Convert the group to a 32-bit value
for (let j = 0; j < groupSize; j++) {
value = value * 85 + (text.charCodeAt(i + j) - 33);
}
// Pad with 'u' (84) if needed for partial groups
for (let j = groupSize; j < 5; j++) {
value = value * 85 + 84;
}
// Extract bytes from the value
// groupSize chars encodes (groupSize - 1) bytes
const bytesToWrite = groupSize - 1;
for (let j = 0; j < bytesToWrite; j++) {
bytes.push((value >>> ((3 - j) * 8)) & 0xFF);
}
i += groupSize;
} else {
break;
}
}
// Use TextDecoder to properly handle UTF-8 multi-byte characters
return new TextDecoder().decode(new Uint8Array(bytes));
}
});
+85
View File
@@ -0,0 +1,85 @@
// base32 transform
import BaseTransformer from '../BaseTransformer.js';
export default new BaseTransformer({
name: 'Base32',
priority: 280,
// Detector: Only Base32 characters (A-Z, 2-7, =)
detector: function(text) {
const cleaned = text.trim().replace(/\s/g, '');
return cleaned.length >= 8 && /^[A-Z2-7=]+$/.test(cleaned);
},
alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567',
func: function(text) {
if (!text) return '';
// Convert text to bytes
const bytes = new TextEncoder().encode(text);
let result = '';
let bits = 0;
let value = 0;
for (let i = 0; i < bytes.length; i++) {
value = (value << 8) | bytes[i];
bits += 8;
while (bits >= 5) {
bits -= 5;
result += this.alphabet[(value >> bits) & 0x1F];
}
}
// Handle remaining bits
if (bits > 0) {
result += this.alphabet[(value << (5 - bits)) & 0x1F];
}
// Add padding
while (result.length % 8 !== 0) {
result += '=';
}
return result;
},
preview: function(text) {
if (!text) return '[base32]';
const full = this.func(text);
return full.substring(0, 16) + (full.length > 16 ? '...' : '');
},
reverse: function(text) {
if (!text) return '';
// Remove padding and whitespace
text = text.replace(/\s+/g, '').replace(/=+$/, '');
if (text.length === 0) return '';
// Create reverse map
const revMap = {};
for (let i = 0; i < this.alphabet.length; i++) {
revMap[this.alphabet[i]] = i;
}
const bytes = [];
let bits = 0;
let value = 0;
for (let i = 0; i < text.length; i++) {
const char = text[i].toUpperCase();
if (revMap[char] === undefined) continue; // Skip invalid characters
value = (value << 5) | revMap[char];
bits += 5;
while (bits >= 8) {
bits -= 8;
bytes.push((value >> bits) & 0xFF);
}
}
// Use TextDecoder to properly handle UTF-8 multi-byte characters
return new TextDecoder().decode(new Uint8Array(bytes));
}
});
+45
View File
@@ -0,0 +1,45 @@
// base45 transform
import BaseTransformer from '../BaseTransformer.js';
export default new BaseTransformer({
name: 'Base45',
priority: 290,
alphabet: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:',
func: function(text) {
const bytes = new TextEncoder().encode(text);
const chars = [];
for (let i=0;i<bytes.length;i+=2) {
if (i+1 < bytes.length) {
const x = 256*bytes[i] + bytes[i+1];
const e = x % 45; const d = Math.floor(x/45) % 45; const c = Math.floor(x/45/45);
chars.push(this.alphabet[e], this.alphabet[d], this.alphabet[c]);
} else {
const x = bytes[i];
const e = x % 45; const d = Math.floor(x/45);
chars.push(this.alphabet[e], this.alphabet[d]);
}
}
return chars.join('');
},
preview: function(text) {
if (!text) return 'QED8W';
return this.func(text.slice(0,3));
},
reverse: function(text) {
const index = {}; for (let i=0;i<this.alphabet.length;i++) index[this.alphabet[i]] = i;
const codes = [...text].map(c => index[c]).filter(v => v !== undefined);
const out = [];
for (let i=0;i<codes.length;i+=3) {
if (i+2 < codes.length) {
const x = codes[i] + codes[i+1]*45 + codes[i+2]*45*45;
out.push(x >> 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));
}
});
+61
View File
@@ -0,0 +1,61 @@
// base58 transform
import BaseTransformer from '../BaseTransformer.js';
export default new BaseTransformer({
name: 'Base58',
priority: 275,
// Detector: Only Base58 characters (excludes 0, O, I, l)
detector: function(text) {
const cleaned = text.trim().replace(/\s/g, '');
return cleaned.length >= 4 && /^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/.test(cleaned);
},
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]';
const full = this.func(text);
return full.substring(0, 12) + (full.length > 12 ? '...' : '');
},
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));
}
});
+44
View File
@@ -0,0 +1,44 @@
// base62 transform
import BaseTransformer from '../BaseTransformer.js';
export default new BaseTransformer({
name: 'Base62',
priority: 290,
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));
}
});
+51
View File
@@ -0,0 +1,51 @@
// base64 transform
import BaseTransformer from '../BaseTransformer.js';
export default new BaseTransformer({
name: 'Base64',
priority: 270,
// Detector: Only Base64 characters (A-Z, a-z, 0-9, +, /, =)
detector: function(text) {
const cleaned = text.trim().replace(/\s/g, '');
return cleaned.length >= 4 && /^[A-Za-z0-9+\/=]+$/.test(cleaned);
},
func: function(text) {
try {
// Properly encode UTF-8 text (including emojis) to Base64
const encoder = new TextEncoder();
const bytes = encoder.encode(text);
let binaryString = '';
for (let i = 0; i < bytes.length; i++) {
binaryString += String.fromCharCode(bytes[i]);
}
return btoa(binaryString);
} catch (e) {
return '[Invalid input]';
}
},
preview: function(text) {
if (!text) return '[base64]';
try {
const full = this.func(text);
return full.substring(0, 12) + (full.length > 12 ? '...' : '');
} catch (e) {
return '[Invalid input]';
}
},
reverse: function(text) {
try {
// Properly decode Base64 to UTF-8 text (including emojis)
const binaryString = atob(text);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
const decoder = new TextDecoder('utf-8');
return decoder.decode(bytes);
} catch (e) {
return text;
}
}
});
+53
View File
@@ -0,0 +1,53 @@
// base64url transform
import BaseTransformer from '../BaseTransformer.js';
export default new BaseTransformer({
name: 'Base64 URL',
priority: 270,
// Detector: Only Base64 URL characters (A-Z, a-z, 0-9, -, _, =)
detector: function(text) {
const cleaned = text.trim().replace(/\s/g, '');
return cleaned.length >= 4 && /^[A-Za-z0-9\-_=]+$/.test(cleaned);
},
func: function(text) {
if (!text) return '';
try {
// Properly encode UTF-8 text (including emojis) to Base64 URL
const encoder = new TextEncoder();
const bytes = encoder.encode(text);
let binaryString = '';
for (let i = 0; i < bytes.length; i++) {
binaryString += String.fromCharCode(bytes[i]);
}
const std = btoa(binaryString);
return std.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/,'');
} catch (e) {
return '[Invalid input]';
}
},
preview: function(text) {
if (!text) return '[b64url]';
const full = this.func(text);
return full.substring(0, 12) + (full.length > 12 ? '...' : '');
},
reverse: function(text) {
if (!text) return '';
let std = text.replace(/-/g, '+').replace(/_/g, '/');
// pad
while (std.length % 4 !== 0) std += '=';
try {
// Properly decode Base64 URL to UTF-8 text (including emojis)
const binaryString = atob(std);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
const decoder = new TextDecoder('utf-8');
return decoder.decode(bytes);
} catch (e) {
return text;
}
}
});
+43
View File
@@ -0,0 +1,43 @@
// binary transform
import BaseTransformer from '../BaseTransformer.js';
export default new BaseTransformer({
name: 'Binary',
priority: 300,
// Detector: Only 0s, 1s, and spaces
detector: function(text) {
const cleaned = text.trim();
const noSpaces = cleaned.replace(/\s/g, '');
return noSpaces.length >= 8 && /^[01\s]+$/.test(cleaned);
},
func: function(text) {
// Use TextEncoder to properly handle UTF-8 (including emoji)
const encoder = new TextEncoder();
const bytes = encoder.encode(text);
return Array.from(bytes).map(b => b.toString(2).padStart(8, '0')).join(' ');
},
preview: function(text) {
if (!text) return '[binary]';
const full = this.func(text);
return full.substring(0, 24) + (full.length > 24 ? '...' : '');
},
reverse: function(text) {
// Remove spaces and ensure we have valid binary
const binText = text.replace(/\s+/g, '');
const bytes = [];
// Process 8 bits at a time
for (let i = 0; i < binText.length; i += 8) {
const byte = binText.substr(i, 8);
if (byte.length === 8) {
bytes.push(parseInt(byte, 2));
}
}
// Use TextDecoder to properly decode UTF-8
const decoder = new TextDecoder('utf-8');
return decoder.decode(new Uint8Array(bytes));
}
});
+40
View File
@@ -0,0 +1,40 @@
// hex transform
import BaseTransformer from '../BaseTransformer.js';
export default new BaseTransformer({
name: 'Hexadecimal',
priority: 290,
// Detector: Only hex characters (0-9, A-F)
detector: function(text) {
const cleaned = text.trim().replace(/\s/g, '');
return cleaned.length >= 4 && /^[0-9A-Fa-f]+$/.test(cleaned);
},
func: function(text) {
// Use TextEncoder to properly handle UTF-8 (including emoji)
const encoder = new TextEncoder();
const bytes = encoder.encode(text);
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join(' ');
},
preview: function(text) {
if (!text) return '[hex]';
const full = this.func(text);
return full.substring(0, 20) + (full.length > 20 ? '...' : '');
},
reverse: function(text) {
const hexText = text.replace(/\s+/g, '');
const bytes = [];
for (let i = 0; i < hexText.length; i += 2) {
const byte = hexText.substr(i, 2);
if (byte.length === 2) {
bytes.push(parseInt(byte, 16));
}
}
// Use TextDecoder to properly decode UTF-8
const decoder = new TextDecoder('utf-8');
return decoder.decode(new Uint8Array(bytes));
}
});
+32
View File
@@ -0,0 +1,32 @@
// html transform
import BaseTransformer from '../BaseTransformer.js';
export default new BaseTransformer({
name: 'HTML Entities',
priority: 40,
// Detector: Look for &...; pattern (HTML entities)
detector: function(text) {
return text.includes('&') && text.includes(';') && /&[a-zA-Z0-9#]+;/.test(text);
},
func: function(text) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
},
preview: function(text) {
return this.func(text);
},
reverse: function(text) {
return text
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, '\'');
}
});
@@ -0,0 +1,39 @@
// invisible-text transform
import BaseTransformer from '../BaseTransformer.js';
export default new BaseTransformer({
name: 'Invisible Text',
priority: 100, // High confidence - uses exclusive Unicode Private Use Area (U+E0000-U+E00FF)
func: function(text) {
if (!text) return '';
const bytes = new TextEncoder().encode(text);
return Array.from(bytes)
.map(byte => String.fromCodePoint(0xE0000 + byte))
.join('');
},
preview: function(text) {
return '[invisible]';
},
reverse: function(text) {
if (!text) return '';
const matches = [...text.matchAll(/[\u{E0000}-\u{E00FF}]/gu)];
if (!matches.length) return '';
// Convert invisible characters back to bytes
const bytes = new Uint8Array(
matches.map(match => match[0].codePointAt(0) - 0xE0000)
);
// Use TextDecoder to properly handle UTF-8 encoded bytes (including emoji)
return new TextDecoder().decode(bytes);
},
// Detector: Check for at least one invisible Unicode character
detector: function(text) {
// Invisible text uses Unicode Private Use Area (U+E0000-U+E00FF for full byte range)
const invisibleMatches = text.match(/[\u{E0000}-\u{E00FF}]/gu);
// Return true if at least one invisible character is found
return invisibleMatches && invisibleMatches.length > 0;
}
});
+31
View File
@@ -0,0 +1,31 @@
// url transform
import BaseTransformer from '../BaseTransformer.js';
export default new BaseTransformer({
name: 'URL Encode',
priority: 40,
// Detector: Look for %XX pattern (URL encoding)
detector: function(text) {
return text.includes('%') && /%[0-9A-Fa-f]{2}/.test(text);
},
func: function(text) {
try {
return encodeURIComponent(text);
} catch (e) {
// Catch malformed Unicode or unpaired surrogates
return '[Invalid input]';
}
},
preview: function(text) {
return this.func(text);
},
reverse: function(text) {
try {
return decodeURIComponent(text);
} catch (e) {
return text;
}
}
});