mirror of
https://github.com/elder-plinius/P4RS3LT0NGV3.git
synced 2026-08-28 13:50:49 +02:00
Sync upstream and refine Translate/PromptCraft/Glitch UI
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
// ADFGX cipher transform (WWI German cipher)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'ADFGX Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key: 'KEYWORD', // Default transposition key
|
||||
// ADFGX uses a 5x5 Polybius square with letters A, D, F, G, X as coordinates
|
||||
// Standard square (I and J share position)
|
||||
square: [
|
||||
['A', 'B', 'C', 'D', 'E'],
|
||||
['F', 'G', 'H', 'I', 'K'],
|
||||
['L', 'M', 'N', 'O', 'P'],
|
||||
['Q', 'R', 'S', 'T', 'U'],
|
||||
['V', 'W', 'X', 'Y', 'Z']
|
||||
],
|
||||
// Coordinate labels
|
||||
coords: ['A', 'D', 'F', 'G', 'X'],
|
||||
func: function(text) {
|
||||
const transKey = (this.key || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (transKey.length === 0) return text;
|
||||
|
||||
const cleaned = text.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (cleaned.length === 0) return text;
|
||||
|
||||
// Step 1: Convert to ADFGX coordinates (two letters per character)
|
||||
let adfgxText = '';
|
||||
for (const char of cleaned) {
|
||||
let found = false;
|
||||
for (let row = 0; row < 5; row++) {
|
||||
for (let col = 0; col < 5; col++) {
|
||||
if (this.square[row][col] === char || (char === 'J' && this.square[row][col] === 'I')) {
|
||||
adfgxText += this.coords[row] + this.coords[col];
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Columnar transposition using the key
|
||||
const keyLength = transKey.length;
|
||||
const numCols = keyLength;
|
||||
const numRows = Math.ceil(adfgxText.length / numCols);
|
||||
|
||||
// Create grid
|
||||
const grid = [];
|
||||
let textIdx = 0;
|
||||
for (let row = 0; row < numRows; row++) {
|
||||
grid[row] = [];
|
||||
for (let col = 0; col < numCols; col++) {
|
||||
grid[row][col] = textIdx < adfgxText.length ? adfgxText[textIdx++] : '';
|
||||
}
|
||||
}
|
||||
|
||||
// Sort columns by key
|
||||
const keyOrder = [];
|
||||
for (let i = 0; i < transKey.length; i++) {
|
||||
keyOrder.push({ char: transKey[i], index: i });
|
||||
}
|
||||
keyOrder.sort((a, b) => {
|
||||
if (a.char < b.char) return -1;
|
||||
if (a.char > b.char) return 1;
|
||||
return a.index - b.index;
|
||||
});
|
||||
|
||||
// Read columns in sorted order
|
||||
let result = '';
|
||||
for (const keyItem of keyOrder) {
|
||||
const col = keyItem.index;
|
||||
for (let row = 0; row < numRows; row++) {
|
||||
if (grid[row][col]) {
|
||||
result += grid[row][col];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const transKey = (this.key || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (transKey.length === 0) return text;
|
||||
|
||||
// Only process ADFGX characters
|
||||
const cleaned = text.toUpperCase().replace(/[^ADFGX]/g, '');
|
||||
if (cleaned.length === 0) return text;
|
||||
if (cleaned.length % 2 !== 0) return text; // Must be even length
|
||||
|
||||
// Step 1: Reverse columnar transposition
|
||||
const keyLength = transKey.length;
|
||||
const numCols = keyLength;
|
||||
const numRows = Math.ceil(cleaned.length / numCols);
|
||||
|
||||
// Determine column order (same as encoding)
|
||||
const keyOrder = [];
|
||||
for (let i = 0; i < transKey.length; i++) {
|
||||
keyOrder.push({ char: transKey[i], index: i });
|
||||
}
|
||||
keyOrder.sort((a, b) => {
|
||||
if (a.char < b.char) return -1;
|
||||
if (a.char > b.char) return 1;
|
||||
return a.index - b.index;
|
||||
});
|
||||
|
||||
// Calculate how many characters each original column has
|
||||
// When writing row by row, column i gets chars at positions: i, i+numCols, i+2*numCols, ...
|
||||
// So column i has: Math.ceil((totalLength - i) / numCols) characters
|
||||
|
||||
// Fill grid: write into columns in sorted order
|
||||
const grid = [];
|
||||
for (let row = 0; row < numRows; row++) {
|
||||
grid[row] = new Array(numCols);
|
||||
}
|
||||
|
||||
let textIdx = 0;
|
||||
for (const keyItem of keyOrder) {
|
||||
const col = keyItem.index;
|
||||
// This column originally had this many characters when written row by row
|
||||
const colLength = Math.ceil((cleaned.length - col) / numCols);
|
||||
|
||||
// Write characters into this column, filling top to bottom
|
||||
for (let row = 0; row < colLength && textIdx < cleaned.length; row++) {
|
||||
grid[row][col] = cleaned[textIdx++];
|
||||
}
|
||||
}
|
||||
|
||||
// Read row by row to get ADFGX text
|
||||
let adfgxText = '';
|
||||
for (let row = 0; row < numRows; row++) {
|
||||
for (let col = 0; col < numCols; col++) {
|
||||
if (grid[row] && grid[row][col]) {
|
||||
adfgxText += grid[row][col];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Convert ADFGX coordinates back to letters
|
||||
let result = '';
|
||||
for (let i = 0; i < adfgxText.length; i += 2) {
|
||||
if (i + 1 < adfgxText.length) {
|
||||
const rowChar = adfgxText[i];
|
||||
const colChar = adfgxText[i + 1];
|
||||
const row = this.coords.indexOf(rowChar);
|
||||
const col = this.coords.indexOf(colChar);
|
||||
|
||||
if (row >= 0 && row < 5 && col >= 0 && col < 5) {
|
||||
result += this.square[row][col];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[adfgx]';
|
||||
const result = this.func(text.slice(0, 5));
|
||||
return result.substring(0, 12) + (result.length > 12 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// ADFGX produces only A, D, F, G, X characters
|
||||
const cleaned = text.replace(/[\s]/g, '').toUpperCase();
|
||||
if (cleaned.length < 10) return false;
|
||||
if (!/^[ADFGX]+$/.test(cleaned)) return false;
|
||||
// Must be even length (pairs of coordinates)
|
||||
return cleaned.length % 2 === 0;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// autokey cipher transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Autokey Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key: 'KEY', // Initial key
|
||||
func: function(text) {
|
||||
const key = (this.key || 'KEY').toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (key.length === 0) return text;
|
||||
|
||||
let result = '';
|
||||
let keyIndex = 0;
|
||||
const fullKey = key + text.toUpperCase().replace(/[^A-Z]/g, ''); // Key + plaintext
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i];
|
||||
const code = c.charCodeAt(0);
|
||||
|
||||
if (code >= 65 && code <= 90) { // Uppercase
|
||||
const k = fullKey[keyIndex % fullKey.length].charCodeAt(0) - 65;
|
||||
result += String.fromCharCode(65 + ((code - 65 + k) % 26));
|
||||
keyIndex++;
|
||||
} else if (code >= 97 && code <= 122) { // Lowercase
|
||||
const k = fullKey[keyIndex % fullKey.length].charCodeAt(0) - 65;
|
||||
result += String.fromCharCode(97 + ((code - 97 + k) % 26));
|
||||
keyIndex++;
|
||||
} else {
|
||||
result += c;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const key = (this.key || 'KEY').toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (key.length === 0) return text;
|
||||
|
||||
let result = '';
|
||||
let keyIndex = 0;
|
||||
let decodedSoFar = '';
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i];
|
||||
const code = c.charCodeAt(0);
|
||||
|
||||
if (code >= 65 && code <= 90) { // Uppercase
|
||||
// Use key for first part, then decoded text
|
||||
const keyChar = keyIndex < key.length
|
||||
? key[keyIndex]
|
||||
: decodedSoFar[keyIndex - key.length];
|
||||
const k = keyChar.charCodeAt(0) - 65;
|
||||
const decoded = String.fromCharCode(65 + ((code - 65 - k + 26) % 26));
|
||||
result += decoded;
|
||||
decodedSoFar += decoded;
|
||||
keyIndex++;
|
||||
} else if (code >= 97 && code <= 122) { // Lowercase
|
||||
const keyChar = keyIndex < key.length
|
||||
? key[keyIndex]
|
||||
: decodedSoFar[keyIndex - key.length];
|
||||
const k = keyChar.charCodeAt(0) - 65;
|
||||
const decoded = String.fromCharCode(97 + ((code - 97 - k + 26) % 26));
|
||||
result += decoded;
|
||||
decodedSoFar += decoded;
|
||||
keyIndex++;
|
||||
} else {
|
||||
result += c;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[autokey]';
|
||||
return this.func(text.slice(0, 8)) + (text.length > 8 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// Autokey produces ciphertext that looks like scrambled letters
|
||||
const cleaned = text.replace(/[^A-Za-z]/g, '');
|
||||
if (cleaned.length < 10) return false;
|
||||
|
||||
// Should be mostly letters with some pattern
|
||||
const letterRatio = cleaned.length / text.length;
|
||||
return letterRatio > 0.7;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// beaufort cipher transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Beaufort Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key: 'KEY', // Default key
|
||||
func: function(text) {
|
||||
const key = (this.key || 'KEY').toUpperCase();
|
||||
const keyLength = key.length;
|
||||
let keyIndex = 0;
|
||||
|
||||
return [...text].map(c => {
|
||||
const code = c.charCodeAt(0);
|
||||
if (code >= 65 && code <= 90) { // Uppercase
|
||||
const keyChar = key[keyIndex % keyLength].charCodeAt(0) - 65;
|
||||
const plainChar = code - 65;
|
||||
// Beaufort: cipher = (key - plain) mod 26
|
||||
const result = String.fromCharCode(((keyChar - plainChar + 26) % 26) + 65);
|
||||
keyIndex++;
|
||||
return result;
|
||||
} else if (code >= 97 && code <= 122) { // Lowercase
|
||||
const keyChar = key[keyIndex % keyLength].charCodeAt(0) - 65;
|
||||
const plainChar = code - 97;
|
||||
// Beaufort: cipher = (key - plain) mod 26
|
||||
const result = String.fromCharCode(((keyChar - plainChar + 26) % 26) + 97);
|
||||
keyIndex++;
|
||||
return result;
|
||||
} else {
|
||||
return c;
|
||||
}
|
||||
}).join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Beaufort cipher is self-reciprocal (same function for encode/decode)
|
||||
return this.func(text);
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[beaufort]';
|
||||
const result = this.func(text.slice(0, 8));
|
||||
return result.substring(0, 10) + (result.length > 10 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
const cleaned = text.replace(/[\s.,!?;:'"()\-&0-9]/g, '');
|
||||
if (cleaned.length < 5) return false;
|
||||
const letterCount = (cleaned.match(/[a-zA-Z]/g) || []).length;
|
||||
return letterCount / cleaned.length > 0.7;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
// bifid cipher transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Bifid Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
period: 5, // Period for fractionation (default 5)
|
||||
// Standard Polybius square (5x5, I and J share same cell)
|
||||
square: [
|
||||
['A', 'B', 'C', 'D', 'E'],
|
||||
['F', 'G', 'H', 'I', 'K'],
|
||||
['L', 'M', 'N', 'O', 'P'],
|
||||
['Q', 'R', 'S', 'T', 'U'],
|
||||
['V', 'W', 'X', 'Y', 'Z']
|
||||
],
|
||||
func: function(text) {
|
||||
const period = parseInt(this.period) || 5;
|
||||
const cleaned = text.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (cleaned.length === 0) return text;
|
||||
|
||||
// Step 1: Convert to Polybius coordinates
|
||||
const coords = [];
|
||||
for (const char of cleaned) {
|
||||
let found = false;
|
||||
for (let row = 0; row < 5; row++) {
|
||||
for (let col = 0; col < 5; col++) {
|
||||
if (this.square[row][col] === char || (char === 'J' && this.square[row][col] === 'I')) {
|
||||
coords.push({ row: row + 1, col: col + 1 });
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Write coordinates in rows, then columns
|
||||
const rowSeq = coords.map(c => c.row).join('');
|
||||
const colSeq = coords.map(c => c.col).join('');
|
||||
|
||||
// Step 3: Group by period and read pairs
|
||||
let result = '';
|
||||
for (let i = 0; i < rowSeq.length; i += period) {
|
||||
const rowChunk = rowSeq.substring(i, i + period);
|
||||
const colChunk = colSeq.substring(i, i + period);
|
||||
|
||||
for (let j = 0; j < rowChunk.length; j++) {
|
||||
const row = parseInt(rowChunk[j]) - 1;
|
||||
const col = parseInt(colChunk[j]) - 1;
|
||||
if (row >= 0 && row < 5 && col >= 0 && col < 5) {
|
||||
result += this.square[row][col];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const period = parseInt(this.period) || 5;
|
||||
const cleaned = text.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (cleaned.length === 0) return text;
|
||||
|
||||
// Step 1: Convert letters to coordinates
|
||||
const coords = [];
|
||||
for (const char of cleaned) {
|
||||
let found = false;
|
||||
for (let row = 0; row < 5; row++) {
|
||||
for (let col = 0; col < 5; col++) {
|
||||
if (this.square[row][col] === char || (char === 'J' && this.square[row][col] === 'I')) {
|
||||
coords.push({ row: row + 1, col: col + 1 });
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Group by period, extract row and column sequences
|
||||
let rowSeq = '';
|
||||
let colSeq = '';
|
||||
|
||||
for (let i = 0; i < coords.length; i += period) {
|
||||
const chunk = coords.slice(i, i + period);
|
||||
const chunkRowSeq = chunk.map(c => c.row).join('');
|
||||
const chunkColSeq = chunk.map(c => c.col).join('');
|
||||
rowSeq += chunkRowSeq;
|
||||
colSeq += chunkColSeq;
|
||||
}
|
||||
|
||||
// Step 3: Pair up coordinates and convert back to letters
|
||||
let result = '';
|
||||
for (let i = 0; i < rowSeq.length && i < colSeq.length; i++) {
|
||||
const row = parseInt(rowSeq[i]) - 1;
|
||||
const col = parseInt(colSeq[i]) - 1;
|
||||
if (row >= 0 && row < 5 && col >= 0 && col < 5) {
|
||||
result += this.square[row][col];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[bifid]';
|
||||
const result = this.func(text.slice(0, 5));
|
||||
return result.substring(0, 10) + (result.length > 10 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// Bifid produces scrambled text (all uppercase letters, no digits)
|
||||
const cleaned = text.replace(/[\s]/g, '').toUpperCase();
|
||||
if (cleaned.length < 10) return false;
|
||||
if (!/^[A-Z]+$/.test(cleaned)) return false;
|
||||
|
||||
// Check if it looks scrambled (not readable English)
|
||||
const commonWords = ['THE', 'AND', 'FOR', 'ARE'];
|
||||
const hasCommonWords = commonWords.some(word => cleaned.includes(word));
|
||||
if (hasCommonWords && cleaned.length < 20) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
// columnar transposition cipher transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Columnar Transposition',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key: 'KEY', // Default key
|
||||
func: function(text) {
|
||||
const key = (this.key || 'KEY').toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (key.length === 0) return text;
|
||||
|
||||
// Remove spaces and convert to uppercase for processing
|
||||
const cleaned = text.replace(/\s/g, '').toUpperCase();
|
||||
const keyLength = key.length;
|
||||
const numRows = Math.ceil(cleaned.length / keyLength);
|
||||
|
||||
// Create key order (sorted positions)
|
||||
const keyOrder = key.split('')
|
||||
.map((char, idx) => ({ char, idx }))
|
||||
.sort((a, b) => a.char.localeCompare(b.char))
|
||||
.map((item, newIdx) => ({ originalIdx: item.idx, newIdx }));
|
||||
|
||||
// Fill grid
|
||||
const grid = [];
|
||||
for (let i = 0; i < numRows; i++) {
|
||||
grid[i] = [];
|
||||
for (let j = 0; j < keyLength; j++) {
|
||||
const idx = i * keyLength + j;
|
||||
grid[i][j] = idx < cleaned.length ? cleaned[idx] : 'X';
|
||||
}
|
||||
}
|
||||
|
||||
// Read columns in key order
|
||||
const result = [];
|
||||
keyOrder.forEach(({ originalIdx }) => {
|
||||
for (let i = 0; i < numRows; i++) {
|
||||
result.push(grid[i][originalIdx]);
|
||||
}
|
||||
});
|
||||
|
||||
return result.join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
const key = (this.key || 'KEY').toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (key.length === 0) return text;
|
||||
|
||||
const keyLength = key.length;
|
||||
const numRows = Math.ceil(text.length / keyLength);
|
||||
|
||||
// Create key order
|
||||
const keyOrder = key.split('')
|
||||
.map((char, idx) => ({ char, idx }))
|
||||
.sort((a, b) => a.char.localeCompare(b.char))
|
||||
.map((item, newIdx) => ({ originalIdx: item.idx, newIdx, sortedIdx: newIdx }));
|
||||
|
||||
// Reconstruct grid by reading columns in key order
|
||||
const grid = [];
|
||||
for (let i = 0; i < numRows; i++) {
|
||||
grid[i] = new Array(keyLength);
|
||||
}
|
||||
|
||||
let textIdx = 0;
|
||||
keyOrder.forEach(({ originalIdx }) => {
|
||||
for (let i = 0; i < numRows && textIdx < text.length; i++) {
|
||||
grid[i][originalIdx] = text[textIdx++];
|
||||
}
|
||||
});
|
||||
|
||||
// Read grid row by row
|
||||
const result = [];
|
||||
for (let i = 0; i < numRows; i++) {
|
||||
for (let j = 0; j < keyLength; j++) {
|
||||
if (grid[i][j]) {
|
||||
result.push(grid[i][j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.join('').replace(/X+$/, ''); // Remove padding X's
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[columnar]';
|
||||
const result = this.func(text.slice(0, 10));
|
||||
return result.substring(0, 12) + (result.length > 12 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// Columnar transposition produces text that:
|
||||
// 1. Is all uppercase (after removing spaces)
|
||||
// 2. Has no spaces (or spaces removed)
|
||||
// 3. Has a length that suggests it was transposed (not too short)
|
||||
// 4. Doesn't look like readable English (columnar transposition scrambles text)
|
||||
const cleaned = text.replace(/[\s]/g, '').toUpperCase();
|
||||
|
||||
// Too short to be meaningful
|
||||
if (cleaned.length < 10) return false;
|
||||
|
||||
// Must be mostly letters (allow punctuation anywhere, but primarily letters)
|
||||
// Remove punctuation for the main check
|
||||
const lettersOnly = cleaned.replace(/[^A-Z]/g, '');
|
||||
if (lettersOnly.length < 10) return false; // Need at least 10 letters
|
||||
if (lettersOnly.length < cleaned.length * 0.8) return false; // At least 80% letters
|
||||
|
||||
// Columnar transposition scrambles text, so it shouldn't look like readable English
|
||||
// Check for common English word patterns that would indicate readable text
|
||||
// But be careful - columnar-transposition might preserve some word fragments
|
||||
const strongEnglishPatterns = [
|
||||
/THE[A-Z]{3,}[A-Z]{3,}/, // THE followed by two words (like "THEQUICKBROWN")
|
||||
/[A-Z]{3,}AND[A-Z]{3,}/, // Word AND word (both 3+ letters)
|
||||
/[A-Z]{3,}FOR[A-Z]{3,}/, // Word FOR word (both 3+ letters)
|
||||
/HELLOWORLD/, // HELLO WORLD together
|
||||
/THEQUICK/, // THE QUICK together
|
||||
/QUICKBROWN/, // QUICK BROWN together
|
||||
];
|
||||
|
||||
// If strong English patterns match, it's probably readable English, not scrambled
|
||||
for (const pattern of strongEnglishPatterns) {
|
||||
if (pattern.test(cleaned)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for sequential letter patterns (like ABC, XYZ) which are unlikely in columnar-transposition
|
||||
if (/ABCD|BCDE|CDEF|DEFG|EFGH|FGHI|GHIJ|HIJK|IJKL|JKLM|KLMN|LMNO|MNOP|NOPQ|OPQR|PQRS|QRST|RSTU|STUV|TUVW|UVWX|VWXY|WXYZ/.test(cleaned)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check letter frequency - columnar transposition should have roughly normal letter distribution
|
||||
const letterFreq = {};
|
||||
for (const char of lettersOnly) {
|
||||
letterFreq[char] = (letterFreq[char] || 0) + 1;
|
||||
}
|
||||
const uniqueLetters = Object.keys(letterFreq).length;
|
||||
// If we have very few unique letters, it's probably not columnar-transposition
|
||||
if (uniqueLetters < 5) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
// four-square cipher transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Four-Square Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key1: 'EXAMPLE', // Top-left square key
|
||||
key2: 'KEYWORD', // Bottom-right square key
|
||||
// Standard alphabet for top-right and bottom-left squares
|
||||
standardAlphabet: 'ABCDEFGHIKLMNOPQRSTUVWXYZ',
|
||||
// Create keyed squares
|
||||
createKeyedSquare: function(key) {
|
||||
const used = new Set();
|
||||
const square = [];
|
||||
let keyIdx = 0;
|
||||
let alphaIdx = 0;
|
||||
|
||||
// Fill with key letters first
|
||||
for (let i = 0; i < 5; i++) {
|
||||
square[i] = [];
|
||||
for (let j = 0; j < 5; j++) {
|
||||
while (keyIdx < key.length && used.has(key[keyIdx])) {
|
||||
keyIdx++;
|
||||
}
|
||||
if (keyIdx < key.length) {
|
||||
square[i][j] = key[keyIdx];
|
||||
used.add(key[keyIdx]);
|
||||
keyIdx++;
|
||||
} else {
|
||||
// Fill with remaining alphabet
|
||||
while (alphaIdx < this.standardAlphabet.length && used.has(this.standardAlphabet[alphaIdx])) {
|
||||
alphaIdx++;
|
||||
}
|
||||
if (alphaIdx < this.standardAlphabet.length) {
|
||||
square[i][j] = this.standardAlphabet[alphaIdx];
|
||||
used.add(this.standardAlphabet[alphaIdx]);
|
||||
alphaIdx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return square;
|
||||
},
|
||||
func: function(text) {
|
||||
const key1 = (this.key1 || 'EXAMPLE').toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
|
||||
const key2 = (this.key2 || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
|
||||
|
||||
if (key1.length === 0 || key2.length === 0) return text;
|
||||
|
||||
let cleaned = text.toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
|
||||
if (cleaned.length === 0) return text;
|
||||
if (cleaned.length % 2 !== 0) {
|
||||
// Pad with X if odd length
|
||||
cleaned += 'X';
|
||||
}
|
||||
|
||||
// Create the four squares
|
||||
const topLeft = this.createKeyedSquare(key1);
|
||||
const topRight = this.createKeyedSquare(this.standardAlphabet);
|
||||
const bottomLeft = this.createKeyedSquare(this.standardAlphabet);
|
||||
const bottomRight = this.createKeyedSquare(key2);
|
||||
|
||||
let result = '';
|
||||
|
||||
// Process pairs of letters
|
||||
for (let i = 0; i < cleaned.length; i += 2) {
|
||||
const char1 = cleaned[i];
|
||||
const char2 = cleaned[i + 1];
|
||||
|
||||
// Find char1 in top-left, char2 in bottom-right
|
||||
let row1 = -1, col1 = -1;
|
||||
let row2 = -1, col2 = -1;
|
||||
|
||||
for (let r = 0; r < 5; r++) {
|
||||
for (let c = 0; c < 5; c++) {
|
||||
if (topLeft[r][c] === char1) {
|
||||
row1 = r;
|
||||
col1 = c;
|
||||
}
|
||||
if (bottomRight[r][c] === char2) {
|
||||
row2 = r;
|
||||
col2 = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (row1 >= 0 && col1 >= 0 && row2 >= 0 && col2 >= 0) {
|
||||
// Use row1, col2 from top-right and row2, col1 from bottom-left
|
||||
result += topRight[row1][col2] + bottomLeft[row2][col1];
|
||||
} else {
|
||||
result += char1 + char2;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const key1 = (this.key1 || 'EXAMPLE').toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
|
||||
const key2 = (this.key2 || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
|
||||
|
||||
if (key1.length === 0 || key2.length === 0) return text;
|
||||
|
||||
let cleaned = text.toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
|
||||
if (cleaned.length === 0) return text;
|
||||
if (cleaned.length % 2 !== 0) return text;
|
||||
|
||||
// Create the four squares
|
||||
const topLeft = this.createKeyedSquare(key1);
|
||||
const topRight = this.createKeyedSquare(this.standardAlphabet);
|
||||
const bottomLeft = this.createKeyedSquare(this.standardAlphabet);
|
||||
const bottomRight = this.createKeyedSquare(key2);
|
||||
|
||||
let result = '';
|
||||
|
||||
// Process pairs of letters
|
||||
for (let i = 0; i < cleaned.length; i += 2) {
|
||||
const char1 = cleaned[i];
|
||||
const char2 = cleaned[i + 1];
|
||||
|
||||
// Find char1 in top-right, char2 in bottom-left
|
||||
let row1 = -1, col1 = -1;
|
||||
let row2 = -1, col2 = -1;
|
||||
|
||||
for (let r = 0; r < 5; r++) {
|
||||
for (let c = 0; c < 5; c++) {
|
||||
if (topRight[r][c] === char1) {
|
||||
row1 = r;
|
||||
col1 = c;
|
||||
}
|
||||
if (bottomLeft[r][c] === char2) {
|
||||
row2 = r;
|
||||
col2 = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (row1 >= 0 && col1 >= 0 && row2 >= 0 && col2 >= 0) {
|
||||
// Use row1, col2 from top-left and row2, col1 from bottom-right
|
||||
result += topLeft[row1][col2] + bottomRight[row2][col1];
|
||||
} else {
|
||||
result += char1 + char2;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[four-square]';
|
||||
return this.func(text.slice(0, 4)) + (text.length > 4 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// Four-Square produces scrambled text (all uppercase letters, no digits)
|
||||
const cleaned = text.replace(/[\s]/g, '').toUpperCase();
|
||||
if (cleaned.length < 10) return false;
|
||||
if (!/^[A-Z]+$/.test(cleaned)) return false;
|
||||
if (cleaned.length % 2 !== 0) return false; // Must be even length
|
||||
|
||||
// Check if it looks scrambled (not readable English)
|
||||
const commonWords = ['THE', 'AND', 'FOR', 'ARE'];
|
||||
const hasCommonWords = commonWords.some(word => cleaned.includes(word));
|
||||
if (hasCommonWords && cleaned.length < 20) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// gronsfeld cipher transform (Vigenère with numeric key)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Gronsfeld Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key: '12345', // Default numeric key
|
||||
func: function(text) {
|
||||
const key = (this.key || '12345').replace(/[^0-9]/g, '');
|
||||
if (key.length === 0) return text;
|
||||
|
||||
let result = '';
|
||||
let keyIndex = 0;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i];
|
||||
const code = c.charCodeAt(0);
|
||||
const shift = parseInt(key[keyIndex % key.length]);
|
||||
|
||||
if (code >= 65 && code <= 90) { // Uppercase
|
||||
result += String.fromCharCode(65 + ((code - 65 + shift) % 26));
|
||||
keyIndex++;
|
||||
} else if (code >= 97 && code <= 122) { // Lowercase
|
||||
result += String.fromCharCode(97 + ((code - 97 + shift) % 26));
|
||||
keyIndex++;
|
||||
} else {
|
||||
result += c;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const key = (this.key || '12345').replace(/[^0-9]/g, '');
|
||||
if (key.length === 0) return text;
|
||||
|
||||
let result = '';
|
||||
let keyIndex = 0;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i];
|
||||
const code = c.charCodeAt(0);
|
||||
const shift = parseInt(key[keyIndex % key.length]);
|
||||
|
||||
if (code >= 65 && code <= 90) { // Uppercase
|
||||
result += String.fromCharCode(65 + ((code - 65 - shift + 26) % 26));
|
||||
keyIndex++;
|
||||
} else if (code >= 97 && code <= 122) { // Lowercase
|
||||
result += String.fromCharCode(97 + ((code - 97 - shift + 26) % 26));
|
||||
keyIndex++;
|
||||
} else {
|
||||
result += c;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[gronsfeld]';
|
||||
return this.func(text.slice(0, 8)) + (text.length > 8 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// Gronsfeld produces ciphertext that looks like scrambled letters
|
||||
const cleaned = text.replace(/[^A-Za-z]/g, '');
|
||||
if (cleaned.length < 10) return false;
|
||||
|
||||
// Should be mostly letters with some pattern
|
||||
const letterRatio = cleaned.length / text.length;
|
||||
return letterRatio > 0.7;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// hill cipher transform (matrix-based cipher)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Hill Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
// Default 2x2 key matrix (must be invertible mod 26)
|
||||
key: [[3, 3], [2, 5]], // Default key matrix
|
||||
func: function(text) {
|
||||
const key = this.key || [[3, 3], [2, 5]];
|
||||
const matrixSize = key.length;
|
||||
|
||||
// Prepare text: remove non-letters, pad with X if needed
|
||||
let prepared = text.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
while (prepared.length % matrixSize !== 0) {
|
||||
prepared += 'X';
|
||||
}
|
||||
|
||||
let result = '';
|
||||
|
||||
// Process in blocks of matrixSize
|
||||
for (let i = 0; i < prepared.length; i += matrixSize) {
|
||||
const block = prepared.slice(i, i + matrixSize);
|
||||
const blockNums = block.split('').map(c => c.charCodeAt(0) - 65);
|
||||
|
||||
// Multiply key matrix by block vector
|
||||
const resultNums = [];
|
||||
for (let row = 0; row < matrixSize; row++) {
|
||||
let sum = 0;
|
||||
for (let col = 0; col < matrixSize; col++) {
|
||||
sum += key[row][col] * blockNums[col];
|
||||
}
|
||||
resultNums.push(sum % 26);
|
||||
}
|
||||
|
||||
// Convert back to letters
|
||||
result += resultNums.map(n => String.fromCharCode(n + 65)).join('');
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const key = this.key || [[3, 3], [2, 5]];
|
||||
const matrixSize = key.length;
|
||||
|
||||
// Calculate inverse matrix mod 26
|
||||
const invKey = this.getInverseMatrix(key);
|
||||
if (!invKey) {
|
||||
console.warn('Hill cipher key matrix is not invertible');
|
||||
return text;
|
||||
}
|
||||
|
||||
let prepared = text.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (prepared.length % matrixSize !== 0) {
|
||||
prepared += 'X'.repeat(matrixSize - (prepared.length % matrixSize));
|
||||
}
|
||||
|
||||
let result = '';
|
||||
|
||||
for (let i = 0; i < prepared.length; i += matrixSize) {
|
||||
const block = prepared.slice(i, i + matrixSize);
|
||||
const blockNums = block.split('').map(c => c.charCodeAt(0) - 65);
|
||||
|
||||
const resultNums = [];
|
||||
for (let row = 0; row < matrixSize; row++) {
|
||||
let sum = 0;
|
||||
for (let col = 0; col < matrixSize; col++) {
|
||||
sum += invKey[row][col] * blockNums[col];
|
||||
}
|
||||
resultNums.push((sum % 26 + 26) % 26);
|
||||
}
|
||||
|
||||
result += resultNums.map(n => String.fromCharCode(n + 65)).join('');
|
||||
}
|
||||
|
||||
// Remove padding X's
|
||||
return result.replace(/X+$/, '');
|
||||
},
|
||||
getInverseMatrix: function(matrix) {
|
||||
// For 2x2 matrix: inverse = (1/det) * [[d, -b], [-c, a]]
|
||||
// where det = ad - bc
|
||||
if (matrix.length !== 2 || matrix[0].length !== 2) {
|
||||
return null; // Only support 2x2 for now
|
||||
}
|
||||
|
||||
const a = matrix[0][0];
|
||||
const b = matrix[0][1];
|
||||
const c = matrix[1][0];
|
||||
const d = matrix[1][1];
|
||||
|
||||
const det = (a * d - b * c) % 26;
|
||||
if (det === 0 || this.gcd(det, 26) !== 1) {
|
||||
return null; // Matrix not invertible mod 26
|
||||
}
|
||||
|
||||
// Find modular inverse of det mod 26
|
||||
const detInv = this.modInverse(det, 26);
|
||||
|
||||
return [
|
||||
[(d * detInv) % 26, (-b * detInv + 26 * 26) % 26],
|
||||
[(-c * detInv + 26 * 26) % 26, (a * detInv) % 26]
|
||||
];
|
||||
},
|
||||
gcd: function(a, b) {
|
||||
while (b !== 0) {
|
||||
[a, b] = [b, a % b];
|
||||
}
|
||||
return a;
|
||||
},
|
||||
modInverse: function(a, m) {
|
||||
// Extended Euclidean algorithm
|
||||
let [oldR, r] = [a, m];
|
||||
let [oldS, s] = [1, 0];
|
||||
|
||||
while (r !== 0) {
|
||||
const quotient = Math.floor(oldR / r);
|
||||
[oldR, r] = [r, oldR - quotient * r];
|
||||
[oldS, s] = [s, oldS - quotient * s];
|
||||
}
|
||||
|
||||
return (oldS % m + m) % m;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[hill]';
|
||||
const result = this.func(text.slice(0, 4));
|
||||
return result.substring(0, 8) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
const cleaned = text.replace(/[\s]/g, '').toUpperCase();
|
||||
return cleaned.length >= 4 && cleaned.length % 2 === 0 && /^[A-Z]+$/.test(cleaned);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// homophonic cipher transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Homophonic Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
// Simple homophonic substitution - each letter maps to multiple symbols
|
||||
map: {
|
||||
'A': ['1', '2', '3'], 'B': ['4', '5'], 'C': ['6', '7', '8'],
|
||||
'D': ['9', '10'], 'E': ['11', '12', '13', '14', '15'], 'F': ['16', '17'],
|
||||
'G': ['18', '19'], 'H': ['20', '21', '22'], 'I': ['23', '24', '25', '26'],
|
||||
'J': ['27'], 'K': ['28'], 'L': ['29', '30', '31'], 'M': ['32', '33'],
|
||||
'N': ['34', '35', '36'], 'O': ['37', '38', '39', '40'], 'P': ['41', '42'],
|
||||
'Q': ['43'], 'R': ['44', '45', '46'], 'S': ['47', '48', '49', '50'],
|
||||
'T': ['51', '52', '53', '54', '55'], 'U': ['56', '57'], 'V': ['58'],
|
||||
'W': ['59', '60'], 'X': ['61'], 'Y': ['62', '63'], 'Z': ['64']
|
||||
},
|
||||
func: function(text) {
|
||||
let result = '';
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i].toUpperCase();
|
||||
if (this.map[c]) {
|
||||
// Randomly select one of the homophones
|
||||
const options = this.map[c];
|
||||
result += options[Math.floor(Math.random() * options.length)];
|
||||
// Add space after number (but not if next char is already a space)
|
||||
if (i < text.length - 1 && text[i + 1] !== ' ') {
|
||||
result += ' ';
|
||||
}
|
||||
} else if (c === ' ') {
|
||||
// Preserve spaces - add as double space to distinguish from number separators
|
||||
result += ' ';
|
||||
} else {
|
||||
// Non-letter characters (keep as-is, no space after)
|
||||
result += text[i];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Build reverse map
|
||||
if (!this._reverseMap) {
|
||||
this._reverseMap = {};
|
||||
for (const [letter, numbers] of Object.entries(this.map)) {
|
||||
numbers.forEach(num => {
|
||||
this._reverseMap[num] = letter;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Numbers are separated by single spaces, double spaces are original spaces
|
||||
let result = '';
|
||||
// Split on double spaces first to preserve original spaces
|
||||
const sections = text.split(/\s{2,}/);
|
||||
|
||||
for (let s = 0; s < sections.length; s++) {
|
||||
const section = sections[s];
|
||||
// Split on spaces, but also handle punctuation
|
||||
const tokens = section.split(/(\s+)/);
|
||||
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const token = tokens[i];
|
||||
if (/^\s+$/.test(token)) {
|
||||
// Whitespace - skip (single spaces between numbers)
|
||||
continue;
|
||||
} else if (/^\d+$/.test(token)) {
|
||||
// Pure number - decode it
|
||||
result += this._reverseMap[token] || token;
|
||||
} else {
|
||||
// Contains non-digits - extract numbers and decode, preserve rest
|
||||
// Match numbers that are space-separated
|
||||
const parts = token.split(/(\d+)/);
|
||||
for (let j = 0; j < parts.length; j++) {
|
||||
const part = parts[j];
|
||||
if (/^\d+$/.test(part)) {
|
||||
result += this._reverseMap[part] || part;
|
||||
} else {
|
||||
result += part;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add space between sections (original spaces)
|
||||
if (s < sections.length - 1) {
|
||||
result += ' ';
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[homophonic]';
|
||||
const result = this.func(text.slice(0, 3));
|
||||
return result.substring(0, 15) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check if text is space-separated numbers (homophonic cipher output)
|
||||
const parts = text.trim().split(/\s+/);
|
||||
return parts.length >= 3 && parts.every(p => /^\d+$/.test(p));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// nihilist cipher transform (Polybius square with numeric key)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Nihilist Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key: '12345', // Default numeric key
|
||||
// Standard Polybius square (5x5, I and J share same cell)
|
||||
square: [
|
||||
['A', 'B', 'C', 'D', 'E'],
|
||||
['F', 'G', 'H', 'I', 'K'],
|
||||
['L', 'M', 'N', 'O', 'P'],
|
||||
['Q', 'R', 'S', 'T', 'U'],
|
||||
['V', 'W', 'X', 'Y', 'Z']
|
||||
],
|
||||
func: function(text) {
|
||||
const key = (this.key || '12345').replace(/[^0-9]/g, '');
|
||||
if (key.length === 0) return text;
|
||||
|
||||
const cleaned = text.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (cleaned.length === 0) return text;
|
||||
|
||||
// Step 1: Convert text to Polybius coordinates
|
||||
const coords = [];
|
||||
for (const char of cleaned) {
|
||||
let found = false;
|
||||
for (let row = 0; row < 5; row++) {
|
||||
for (let col = 0; col < 5; col++) {
|
||||
if (this.square[row][col] === char || (char === 'J' && this.square[row][col] === 'I')) {
|
||||
coords.push((row + 1) * 10 + (col + 1)); // Two-digit number
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Add key values to coordinates (repeating key)
|
||||
let result = '';
|
||||
for (let i = 0; i < coords.length; i++) {
|
||||
const keyDigit = parseInt(key[i % key.length]);
|
||||
const sum = coords[i] + keyDigit;
|
||||
result += sum.toString().padStart(2, '0') + ' ';
|
||||
}
|
||||
|
||||
return result.trim();
|
||||
},
|
||||
reverse: function(text) {
|
||||
const key = (this.key || '12345').replace(/[^0-9]/g, '');
|
||||
if (key.length === 0) return text;
|
||||
|
||||
// Extract two-digit numbers
|
||||
const numbers = text.match(/\d{2}/g) || [];
|
||||
if (numbers.length === 0) return text;
|
||||
|
||||
// Step 1: Subtract key values from numbers
|
||||
const coords = [];
|
||||
for (let i = 0; i < numbers.length; i++) {
|
||||
const num = parseInt(numbers[i]);
|
||||
const keyDigit = parseInt(key[i % key.length]);
|
||||
const coord = num - keyDigit;
|
||||
if (coord >= 11 && coord <= 55) {
|
||||
coords.push(coord);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Convert coordinates back to letters
|
||||
let result = '';
|
||||
for (const coord of coords) {
|
||||
const row = Math.floor(coord / 10) - 1;
|
||||
const col = (coord % 10) - 1;
|
||||
|
||||
if (row >= 0 && row < 5 && col >= 0 && col < 5) {
|
||||
result += this.square[row][col];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[nihilist]';
|
||||
const result = this.func(text.slice(0, 5));
|
||||
return result.substring(0, 15) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
// Nihilist produces pairs of digits (typically 11-99 range after key addition)
|
||||
const digitPairs = text.match(/\d{2}/g) || [];
|
||||
if (digitPairs.length < 3) return false;
|
||||
|
||||
// Check if pairs are in reasonable range (after key addition, could be 11-99)
|
||||
const validPairs = digitPairs.filter(pair => {
|
||||
const num = parseInt(pair);
|
||||
return num >= 11 && num <= 99;
|
||||
});
|
||||
|
||||
// At least 70% should be valid Nihilist pairs
|
||||
return validPairs.length / digitPairs.length >= 0.7;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// pigpen cipher transform (also known as Masonic or Freemason's cipher)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Pigpen Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
// Pigpen cipher uses geometric symbols arranged in grids
|
||||
// Standard Pigpen cipher mapping based on dCode.fr implementation (Original variant)
|
||||
// Reference: https://www.dcode.fr/pigpen-cipher
|
||||
// Grid 1 (A-I): L-shapes and U-shapes in 3x3 grid positions
|
||||
// Grid 2 (J-R): Same shapes as A-I but with dots
|
||||
// Grid 3 (S-Z): Caret/X shapes (some with dots)
|
||||
map: {
|
||||
'A': 'ᒧ', 'B': '⊔', 'C': 'ᒪ',
|
||||
'D': '⊐', 'E': '☐', 'F': '⊏',
|
||||
'G': 'ᒣ', 'H': '⊓', 'I': 'ᒥ',
|
||||
'J': '⟓', 'K': '⨃', 'L': 'ᒷ',
|
||||
'M': '⪾', 'N': '🝕', 'O': '⪽',
|
||||
'P': 'ᒬ', 'Q': '⩀', 'R': '⟔',
|
||||
'S': 'ᐯ', 'T': 'ᐳ', 'U': 'ᐸ',
|
||||
'V': 'ᐱ', 'W': '⟇', 'X': 'ᑀ',
|
||||
'Y': 'ᑅ', 'Z': '⟑'
|
||||
},
|
||||
func: function(text) {
|
||||
return [...text].map(c => {
|
||||
const upper = c.toUpperCase();
|
||||
if (this.map[upper]) {
|
||||
// Preserve case: if original was lowercase, return lowercase symbol
|
||||
// (though symbols don't have case, we'll just use the symbol)
|
||||
return this.map[upper];
|
||||
}
|
||||
return c;
|
||||
}).join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[pigpen]';
|
||||
return this.func(text.slice(0, 5));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check if text contains Pigpen symbols (dCode.fr Unicode characters)
|
||||
const pigpenSymbols = /[ᒧ⊔ᒪ⊐☐⊏ᒣ⊓ᒥ⟓⨃ᒷ⪾🝕⪽ᒬ⩀⟔ᐯᐳᐸᐱ⟇ᑀᑅ⟑]/;
|
||||
return pigpenSymbols.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
// playfair cipher transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Playfair Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key: 'KEYWORD', // Default key
|
||||
func: function(text) {
|
||||
const key = (this.key || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (key.length === 0) return text;
|
||||
|
||||
// Create Playfair square
|
||||
const alphabet = 'ABCDEFGHIKLMNOPQRSTUVWXYZ'; // J is combined with I
|
||||
const keyChars = [...new Set(key.split(''))];
|
||||
const remaining = alphabet.split('').filter(c => !keyChars.includes(c));
|
||||
const square = [...keyChars, ...remaining];
|
||||
|
||||
// Helper to find position in square
|
||||
const findPos = (char) => {
|
||||
const idx = square.indexOf(char === 'J' ? 'I' : char);
|
||||
return { row: Math.floor(idx / 5), col: idx % 5 };
|
||||
};
|
||||
|
||||
// Helper to get char from position
|
||||
const getChar = (row, col) => square[row * 5 + col];
|
||||
|
||||
// Prepare text: remove non-letters, replace J with I, add X between double letters
|
||||
let prepared = text.toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
|
||||
if (prepared.length % 2 !== 0) prepared += 'X';
|
||||
|
||||
// Process pairs
|
||||
let result = '';
|
||||
for (let i = 0; i < prepared.length; i += 2) {
|
||||
const a = prepared[i];
|
||||
const b = prepared[i + 1];
|
||||
const posA = findPos(a);
|
||||
const posB = findPos(b);
|
||||
|
||||
if (posA.row === posB.row) {
|
||||
// Same row: shift right
|
||||
result += getChar(posA.row, (posA.col + 1) % 5);
|
||||
result += getChar(posB.row, (posB.col + 1) % 5);
|
||||
} else if (posA.col === posB.col) {
|
||||
// Same column: shift down
|
||||
result += getChar((posA.row + 1) % 5, posA.col);
|
||||
result += getChar((posB.row + 1) % 5, posB.col);
|
||||
} else {
|
||||
// Rectangle: swap columns
|
||||
result += getChar(posA.row, posB.col);
|
||||
result += getChar(posB.row, posA.col);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const key = (this.key || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (key.length === 0) return text;
|
||||
|
||||
const alphabet = 'ABCDEFGHIKLMNOPQRSTUVWXYZ';
|
||||
const keyChars = [...new Set(key.split(''))];
|
||||
const remaining = alphabet.split('').filter(c => !keyChars.includes(c));
|
||||
const square = [...keyChars, ...remaining];
|
||||
|
||||
const findPos = (char) => {
|
||||
const idx = square.indexOf(char === 'J' ? 'I' : char);
|
||||
return { row: Math.floor(idx / 5), col: idx % 5 };
|
||||
};
|
||||
|
||||
const getChar = (row, col) => square[row * 5 + col];
|
||||
|
||||
let prepared = text.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (prepared.length % 2 !== 0) prepared += 'X';
|
||||
|
||||
let result = '';
|
||||
for (let i = 0; i < prepared.length; i += 2) {
|
||||
const a = prepared[i];
|
||||
const b = prepared[i + 1];
|
||||
const posA = findPos(a);
|
||||
const posB = findPos(b);
|
||||
|
||||
if (posA.row === posB.row) {
|
||||
// Same row: shift left
|
||||
result += getChar(posA.row, (posA.col + 4) % 5);
|
||||
result += getChar(posB.row, (posB.col + 4) % 5);
|
||||
} else if (posA.col === posB.col) {
|
||||
// Same column: shift up
|
||||
result += getChar((posA.row + 4) % 5, posA.col);
|
||||
result += getChar((posB.row + 4) % 5, posB.col);
|
||||
} else {
|
||||
// Rectangle: swap columns (same as encode)
|
||||
result += getChar(posA.row, posB.col);
|
||||
result += getChar(posB.row, posA.col);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[playfair]';
|
||||
const result = this.func(text.slice(0, 8));
|
||||
return result.substring(0, 10) + (result.length > 10 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
const cleaned = text.replace(/[\s]/g, '').toUpperCase();
|
||||
return cleaned.length >= 4 && cleaned.length % 2 === 0 && /^[A-Z]+$/.test(cleaned);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// polybius square cipher transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Polybius Square',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
// Standard Polybius square (5x5, I and J share same cell)
|
||||
square: [
|
||||
['A', 'B', 'C', 'D', 'E'],
|
||||
['F', 'G', 'H', 'I', 'K'], // I and J share position
|
||||
['L', 'M', 'N', 'O', 'P'],
|
||||
['Q', 'R', 'S', 'T', 'U'],
|
||||
['V', 'W', 'X', 'Y', 'Z']
|
||||
],
|
||||
func: function(text) {
|
||||
const cleaned = text.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (cleaned.length === 0) return text;
|
||||
|
||||
let result = '';
|
||||
for (const char of cleaned) {
|
||||
let found = false;
|
||||
for (let row = 0; row < 5; row++) {
|
||||
for (let col = 0; col < 5; col++) {
|
||||
if (this.square[row][col] === char || (char === 'J' && this.square[row][col] === 'I')) {
|
||||
result += String(row + 1) + String(col + 1);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) break;
|
||||
}
|
||||
if (!found) {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Extract number pairs sequentially
|
||||
let result = '';
|
||||
let i = 0;
|
||||
|
||||
while (i < text.length) {
|
||||
// Look for two consecutive digits
|
||||
if (i + 1 < text.length && /\d/.test(text[i]) && /\d/.test(text[i + 1])) {
|
||||
const row = parseInt(text[i]) - 1;
|
||||
const col = parseInt(text[i + 1]) - 1;
|
||||
|
||||
if (row >= 0 && row < 5 && col >= 0 && col < 5) {
|
||||
result += this.square[row][col];
|
||||
i += 2;
|
||||
} else {
|
||||
result += text[i];
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
result += text[i];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[polybius]';
|
||||
const result = this.func(text.slice(0, 5));
|
||||
return result.substring(0, 10) + (result.length > 10 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// Polybius square produces pairs of digits (11-55)
|
||||
const digitPairs = text.match(/\d{2}/g) || [];
|
||||
if (digitPairs.length < 3) return false;
|
||||
|
||||
// Check if pairs are valid (1-5 for each digit)
|
||||
const validPairs = digitPairs.filter(pair => {
|
||||
const d1 = parseInt(pair[0]);
|
||||
const d2 = parseInt(pair[1]);
|
||||
return d1 >= 1 && d1 <= 5 && d2 >= 1 && d2 <= 5;
|
||||
});
|
||||
|
||||
// At least 70% should be valid Polybius pairs
|
||||
return validPairs.length / digitPairs.length >= 0.7;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
// porta cipher transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Porta Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key: 'KEY', // Default key
|
||||
func: function(text) {
|
||||
const key = (this.key || 'KEY').toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (key.length === 0) return text;
|
||||
|
||||
// Porta uses 13 reciprocal alphabets, each pair (A/B, C/D, etc.) shares a tableau
|
||||
// Each tableau is reciprocal: if A->B in encode, then B->A in decode (same operation)
|
||||
const tableaus = {
|
||||
'A': 'NOPQRSTUVWXYZABCDEFGHIJKLM', 'B': 'NOPQRSTUVWXYZABCDEFGHIJKLM',
|
||||
'C': 'OPQRSTUVWXYZABCDEFGHIJKLMN', 'D': 'OPQRSTUVWXYZABCDEFGHIJKLMN',
|
||||
'E': 'PQRSTUVWXYZABCDEFGHIJKLMNO', 'F': 'PQRSTUVWXYZABCDEFGHIJKLMNO',
|
||||
'G': 'QRSTUVWXYZABCDEFGHIJKLMNOP', 'H': 'QRSTUVWXYZABCDEFGHIJKLMNOP',
|
||||
'I': 'RSTUVWXYZABCDEFGHIJKLMNOPQ', 'J': 'RSTUVWXYZABCDEFGHIJKLMNOPQ',
|
||||
'K': 'STUVWXYZABCDEFGHIJKLMNOPQR', 'L': 'STUVWXYZABCDEFGHIJKLMNOPQR',
|
||||
'M': 'TUVWXYZABCDEFGHIJKLMNOPQRS', 'N': 'TUVWXYZABCDEFGHIJKLMNOPQRS',
|
||||
'O': 'UVWXYZABCDEFGHIJKLMNOPQRST', 'P': 'UVWXYZABCDEFGHIJKLMNOPQRST',
|
||||
'Q': 'VWXYZABCDEFGHIJKLMNOPQRSTU', 'R': 'VWXYZABCDEFGHIJKLMNOPQRSTU',
|
||||
'S': 'WXYZABCDEFGHIJKLMNOPQRSTUV', 'T': 'WXYZABCDEFGHIJKLMNOPQRSTUV',
|
||||
'U': 'XYZABCDEFGHIJKLMNOPQRSTUVW', 'V': 'XYZABCDEFGHIJKLMNOPQRSTUVW',
|
||||
'W': 'YZABCDEFGHIJKLMNOPQRSTUVWX', 'X': 'YZABCDEFGHIJKLMNOPQRSTUVWX',
|
||||
'Y': 'ZABCDEFGHIJKLMNOPQRSTUVWXY', 'Z': 'ZABCDEFGHIJKLMNOPQRSTUVWXY'
|
||||
};
|
||||
|
||||
let result = '';
|
||||
let keyIndex = 0;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i];
|
||||
const code = c.charCodeAt(0);
|
||||
|
||||
if (code >= 65 && code <= 90) { // Uppercase
|
||||
const keyChar = key[keyIndex % key.length];
|
||||
const tableau = tableaus[keyChar];
|
||||
const plainPos = code - 65;
|
||||
// Porta: cipher = tableau[plain]
|
||||
result += tableau[plainPos];
|
||||
keyIndex++;
|
||||
} else if (code >= 97 && code <= 122) { // Lowercase
|
||||
const keyChar = key[keyIndex % key.length];
|
||||
const tableau = tableaus[keyChar];
|
||||
const plainPos = code - 97;
|
||||
// Porta: cipher = tableau[plain] (lowercase)
|
||||
result += tableau[plainPos].toLowerCase();
|
||||
keyIndex++;
|
||||
} else {
|
||||
result += c;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Porta is self-reciprocal - use reverse lookup in tableau
|
||||
const key = (this.key || 'KEY').toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (key.length === 0) return text;
|
||||
|
||||
const tableaus = {
|
||||
'A': 'NOPQRSTUVWXYZABCDEFGHIJKLM', 'B': 'NOPQRSTUVWXYZABCDEFGHIJKLM',
|
||||
'C': 'OPQRSTUVWXYZABCDEFGHIJKLMN', 'D': 'OPQRSTUVWXYZABCDEFGHIJKLMN',
|
||||
'E': 'PQRSTUVWXYZABCDEFGHIJKLMNO', 'F': 'PQRSTUVWXYZABCDEFGHIJKLMNO',
|
||||
'G': 'QRSTUVWXYZABCDEFGHIJKLMNOP', 'H': 'QRSTUVWXYZABCDEFGHIJKLMNOP',
|
||||
'I': 'RSTUVWXYZABCDEFGHIJKLMNOPQ', 'J': 'RSTUVWXYZABCDEFGHIJKLMNOPQ',
|
||||
'K': 'STUVWXYZABCDEFGHIJKLMNOPQR', 'L': 'STUVWXYZABCDEFGHIJKLMNOPQR',
|
||||
'M': 'TUVWXYZABCDEFGHIJKLMNOPQRS', 'N': 'TUVWXYZABCDEFGHIJKLMNOPQRS',
|
||||
'O': 'UVWXYZABCDEFGHIJKLMNOPQRST', 'P': 'UVWXYZABCDEFGHIJKLMNOPQRST',
|
||||
'Q': 'VWXYZABCDEFGHIJKLMNOPQRSTU', 'R': 'VWXYZABCDEFGHIJKLMNOPQRSTU',
|
||||
'S': 'WXYZABCDEFGHIJKLMNOPQRSTUV', 'T': 'WXYZABCDEFGHIJKLMNOPQRSTUV',
|
||||
'U': 'XYZABCDEFGHIJKLMNOPQRSTUVW', 'V': 'XYZABCDEFGHIJKLMNOPQRSTUVW',
|
||||
'W': 'YZABCDEFGHIJKLMNOPQRSTUVWX', 'X': 'YZABCDEFGHIJKLMNOPQRSTUVWX',
|
||||
'Y': 'ZABCDEFGHIJKLMNOPQRSTUVWXY', 'Z': 'ZABCDEFGHIJKLMNOPQRSTUVWXY'
|
||||
};
|
||||
|
||||
let result = '';
|
||||
let keyIndex = 0;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i];
|
||||
const code = c.charCodeAt(0);
|
||||
|
||||
if (code >= 65 && code <= 90) { // Uppercase
|
||||
const keyChar = key[keyIndex % key.length];
|
||||
const tableau = tableaus[keyChar];
|
||||
// Find position of ciphertext char in tableau - that's the plaintext position
|
||||
const cipherChar = String.fromCharCode(code);
|
||||
const plainPos = tableau.indexOf(cipherChar);
|
||||
if (plainPos >= 0) {
|
||||
result += String.fromCharCode(plainPos + 65);
|
||||
} else {
|
||||
result += c;
|
||||
}
|
||||
keyIndex++;
|
||||
} else if (code >= 97 && code <= 122) { // Lowercase
|
||||
const keyChar = key[keyIndex % key.length];
|
||||
const tableau = tableaus[keyChar];
|
||||
const cipherChar = String.fromCharCode(code - 32); // Convert to uppercase for lookup
|
||||
const plainPos = tableau.indexOf(cipherChar);
|
||||
if (plainPos >= 0) {
|
||||
result += String.fromCharCode(plainPos + 97);
|
||||
} else {
|
||||
result += c;
|
||||
}
|
||||
keyIndex++;
|
||||
} else {
|
||||
result += c;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[porta]';
|
||||
const result = this.func(text.slice(0, 8));
|
||||
return result.substring(0, 10) + (result.length > 10 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
const cleaned = text.replace(/[\s.,!?;:'"()\-&0-9]/g, '');
|
||||
if (cleaned.length < 5) return false;
|
||||
const letterCount = (cleaned.match(/[a-zA-Z]/g) || []).length;
|
||||
return letterCount / cleaned.length > 0.7;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// ROT128 cipher transform (Extended ASCII rotation)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'ROT128',
|
||||
priority: 50,
|
||||
category: 'cipher',
|
||||
func: function(text) {
|
||||
// ROT128 rotates through Extended ASCII range 0-255
|
||||
const shift = 128;
|
||||
let result = '';
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const code = text.charCodeAt(i);
|
||||
// Rotate within 0-255 range
|
||||
if (code >= 0 && code <= 255) {
|
||||
const rotated = (code + shift) % 256;
|
||||
result += String.fromCharCode(rotated);
|
||||
} else {
|
||||
result += text[i]; // Keep characters outside range as-is
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
// ROT128 is self-reciprocal (rotating by 128 twice = full rotation)
|
||||
return this.func(text);
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[rot128]';
|
||||
return this.func(text.slice(0, 8)) + (text.length > 8 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// ROT128 produces extended ASCII characters (128-255)
|
||||
const hasExtendedAscii = /[\x80-\xFF]/.test(text);
|
||||
return hasExtendedAscii && text.length >= 5;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// ROT8000 cipher transform (Unicode rotation)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
// Build valid character codes once (excludes control chars and surrogates)
|
||||
function buildValidCodes() {
|
||||
const validCodes = [];
|
||||
for (let i = 0; i <= 0xFFFF; i++) {
|
||||
// Skip control characters (0x0000-0x001F)
|
||||
if (i >= 0x0000 && i <= 0x001F) continue;
|
||||
// Skip DEL and some controls (0x007F-0x00A0)
|
||||
if (i >= 0x007F && i <= 0x00A0) continue;
|
||||
// Skip surrogate pairs (0xD800-0xDFFF)
|
||||
if (i >= 0xD800 && i <= 0xDFFF) continue;
|
||||
validCodes.push(i);
|
||||
}
|
||||
return validCodes;
|
||||
}
|
||||
|
||||
// Cache the valid codes and mappings
|
||||
let cachedValidCodes = null;
|
||||
let cachedCodeToIndex = null;
|
||||
let cachedShift = null;
|
||||
|
||||
function getRot8000Data() {
|
||||
if (!cachedValidCodes) {
|
||||
cachedValidCodes = buildValidCodes();
|
||||
// ROT8000 uses a shift that makes it self-reciprocal (applying twice returns original)
|
||||
// The shift should be approximately 0x8000 (32768), which is half the BMP
|
||||
// For self-reciprocity: (index + shift + shift) % validCount == index
|
||||
// This means: (2 * shift) % validCount == 0, so shift = validCount / 2
|
||||
cachedShift = Math.floor(cachedValidCodes.length / 2);
|
||||
cachedCodeToIndex = new Map();
|
||||
cachedValidCodes.forEach((code, index) => {
|
||||
cachedCodeToIndex.set(code, index);
|
||||
});
|
||||
}
|
||||
return { validCodes: cachedValidCodes, codeToIndex: cachedCodeToIndex, shift: cachedShift };
|
||||
}
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'ROT8000',
|
||||
priority: 50,
|
||||
category: 'cipher',
|
||||
func: function(text) {
|
||||
// ROT8000 rotates Unicode BMP characters (0x0000-0xFFFF)
|
||||
// Excludes control characters: U+0000-U+001F, U+007F-U+00A0, U+D800-U+DFFF
|
||||
// Shift is half the valid range for self-reciprocity
|
||||
|
||||
const { validCodes, codeToIndex, shift } = getRot8000Data();
|
||||
const validCount = validCodes.length;
|
||||
|
||||
let result = '';
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const code = text.charCodeAt(i);
|
||||
|
||||
// Check if character is in valid range
|
||||
if (codeToIndex.has(code)) {
|
||||
const index = codeToIndex.get(code);
|
||||
const rotatedIndex = (index + shift) % validCount;
|
||||
const rotatedCode = validCodes[rotatedIndex];
|
||||
result += String.fromCharCode(rotatedCode);
|
||||
} else {
|
||||
// Keep invalid characters as-is (spaces, emojis, etc.)
|
||||
result += text[i];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
// ROT8000 is self-reciprocal (rotating by 0x8000 twice = full rotation)
|
||||
return this.func(text);
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[rot8000]';
|
||||
return this.func(text.slice(0, 8)) + (text.length > 8 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// ROT8000 produces characters in various Unicode ranges
|
||||
// Check for non-ASCII characters that aren't typical text
|
||||
const hasNonAscii = /[^\x00-\x7F]/.test(text);
|
||||
const hasControlChars = /[\x00-\x1F]/.test(text);
|
||||
return hasNonAscii && text.length >= 5;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// scytale cipher transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Scytale Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key: 5, // Default number of columns (wrapping width)
|
||||
func: function(text) {
|
||||
const key = parseInt(this.key) || 5;
|
||||
if (key < 2) return text;
|
||||
|
||||
// Remove spaces for encoding
|
||||
const cleaned = text.replace(/\s/g, '').toUpperCase();
|
||||
if (cleaned.length === 0) return text;
|
||||
|
||||
// Calculate number of rows needed
|
||||
const numRows = Math.ceil(cleaned.length / key);
|
||||
|
||||
// Fill grid row by row
|
||||
const grid = [];
|
||||
for (let i = 0; i < numRows; i++) {
|
||||
grid[i] = [];
|
||||
for (let j = 0; j < key; j++) {
|
||||
const idx = i * key + j;
|
||||
grid[i][j] = idx < cleaned.length ? cleaned[idx] : '';
|
||||
}
|
||||
}
|
||||
|
||||
// Read column by column
|
||||
let result = '';
|
||||
for (let j = 0; j < key; j++) {
|
||||
for (let i = 0; i < numRows; i++) {
|
||||
if (grid[i][j]) {
|
||||
result += grid[i][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const key = parseInt(this.key) || 5;
|
||||
if (key < 2) return text;
|
||||
|
||||
const cleaned = text.replace(/\s/g, '').toUpperCase();
|
||||
if (cleaned.length === 0) return text;
|
||||
|
||||
// Calculate number of rows
|
||||
const numRows = Math.ceil(cleaned.length / key);
|
||||
|
||||
// Fill grid column by column (reverse of encoding)
|
||||
const grid = [];
|
||||
for (let i = 0; i < numRows; i++) {
|
||||
grid[i] = new Array(key);
|
||||
}
|
||||
|
||||
let textIdx = 0;
|
||||
for (let j = 0; j < key; j++) {
|
||||
for (let i = 0; i < numRows && textIdx < cleaned.length; i++) {
|
||||
grid[i][j] = cleaned[textIdx++];
|
||||
}
|
||||
}
|
||||
|
||||
// Read row by row
|
||||
let result = '';
|
||||
for (let i = 0; i < numRows; i++) {
|
||||
for (let j = 0; j < key; j++) {
|
||||
if (grid[i][j]) {
|
||||
result += grid[i][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[scytale]';
|
||||
const result = this.func(text.slice(0, 10));
|
||||
return result.substring(0, 12) + (result.length > 12 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// Scytale produces scrambled text - similar to columnar transposition
|
||||
const cleaned = text.replace(/[\s]/g, '').toUpperCase();
|
||||
if (cleaned.length < 10) return false;
|
||||
if (!/^[A-Z]+$/.test(cleaned)) return false;
|
||||
|
||||
// Check if it looks scrambled (not readable English)
|
||||
const commonWords = ['THE', 'AND', 'FOR', 'ARE', 'BUT', 'NOT', 'YOU'];
|
||||
const hasCommonWords = commonWords.some(word => cleaned.includes(word));
|
||||
if (hasCommonWords && cleaned.length < 30) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
// trifid cipher transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Trifid Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
period: 5, // Period for fractionation (default 5)
|
||||
// Trifid uses a 3x3x3 cube (27 positions for A-Z and space/punctuation)
|
||||
// Structure: 3 layers, each with 3 rows and 3 columns
|
||||
cube: [
|
||||
// Layer 0
|
||||
[['A', 'B', 'C'], ['D', 'E', 'F'], ['G', 'H', 'I']],
|
||||
// Layer 1
|
||||
[['J', 'K', 'L'], ['M', 'N', 'O'], ['P', 'Q', 'R']],
|
||||
// Layer 2
|
||||
[['S', 'T', 'U'], ['V', 'W', 'X'], ['Y', 'Z', ' ']]
|
||||
],
|
||||
func: function(text) {
|
||||
const period = parseInt(this.period) || 5;
|
||||
const cleaned = text.toUpperCase().replace(/[^A-Z ]/g, '');
|
||||
if (cleaned.length === 0) return text;
|
||||
|
||||
// Step 1: Convert to Trifid coordinates (layer, row, col) - all 1-indexed
|
||||
const coords = [];
|
||||
for (const char of cleaned) {
|
||||
let found = false;
|
||||
for (let layer = 0; layer < 3; layer++) {
|
||||
for (let row = 0; row < 3; row++) {
|
||||
for (let col = 0; col < 3; col++) {
|
||||
if (this.cube[layer] && this.cube[layer][row] && this.cube[layer][row][col] === char) {
|
||||
coords.push({ layer: layer + 1, row: row + 1, col: col + 1 });
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) break;
|
||||
}
|
||||
if (found) break;
|
||||
}
|
||||
if (!found && char === ' ') {
|
||||
coords.push({ layer: 3, row: 3, col: 3 }); // Space at layer 3, row 3, col 3
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Write coordinates in sequence, then group by period
|
||||
const layerSeq = coords.map(c => c.layer).join('');
|
||||
const rowSeq = coords.map(c => c.row).join('');
|
||||
const colSeq = coords.map(c => c.col).join('');
|
||||
|
||||
// Step 3: Group by period and read triplets
|
||||
let result = '';
|
||||
for (let i = 0; i < layerSeq.length; i += period) {
|
||||
const layerChunk = layerSeq.substring(i, i + period);
|
||||
const rowChunk = rowSeq.substring(i, i + period);
|
||||
const colChunk = colSeq.substring(i, i + period);
|
||||
|
||||
for (let j = 0; j < layerChunk.length; j++) {
|
||||
const layer = parseInt(layerChunk[j]) - 1;
|
||||
const row = parseInt(rowChunk[j]) - 1;
|
||||
const col = parseInt(colChunk[j]) - 1;
|
||||
|
||||
if (layer >= 0 && layer < 3 && row >= 0 && row < 3 && col >= 0 && col < 3) {
|
||||
if (this.cube[layer] && this.cube[layer][row] && this.cube[layer][row][col]) {
|
||||
result += this.cube[layer][row][col];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const period = parseInt(this.period) || 5;
|
||||
const cleaned = text.toUpperCase().replace(/[^A-Z ]/g, '');
|
||||
if (cleaned.length === 0) return text;
|
||||
|
||||
// Step 1: Convert letters to coordinates
|
||||
const coords = [];
|
||||
for (const char of cleaned) {
|
||||
let found = false;
|
||||
for (let layer = 0; layer < 3; layer++) {
|
||||
for (let row = 0; row < 3; row++) {
|
||||
for (let col = 0; col < 3; col++) {
|
||||
if (this.cube[layer] && this.cube[layer][row] && this.cube[layer][row][col] === char) {
|
||||
coords.push({ layer: layer + 1, row: row + 1, col: col + 1 });
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) break;
|
||||
}
|
||||
if (found) break;
|
||||
}
|
||||
if (!found && char === ' ') {
|
||||
coords.push({ layer: 3, row: 3, col: 3 });
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Group by period, extract sequences
|
||||
let layerSeq = '';
|
||||
let rowSeq = '';
|
||||
let colSeq = '';
|
||||
|
||||
for (let i = 0; i < coords.length; i += period) {
|
||||
const chunk = coords.slice(i, i + period);
|
||||
const chunkLayerSeq = chunk.map(c => c.layer).join('');
|
||||
const chunkRowSeq = chunk.map(c => c.row).join('');
|
||||
const chunkColSeq = chunk.map(c => c.col).join('');
|
||||
layerSeq += chunkLayerSeq;
|
||||
rowSeq += chunkRowSeq;
|
||||
colSeq += chunkColSeq;
|
||||
}
|
||||
|
||||
// Step 3: Pair up coordinates and convert back to letters
|
||||
let result = '';
|
||||
for (let i = 0; i < layerSeq.length && i < rowSeq.length && i < colSeq.length; i++) {
|
||||
const layer = parseInt(layerSeq[i]) - 1;
|
||||
const row = parseInt(rowSeq[i]) - 1;
|
||||
const col = parseInt(colSeq[i]) - 1;
|
||||
|
||||
if (layer >= 0 && layer < 3 && row >= 0 && row < 3 && col >= 0 && col < 3) {
|
||||
if (this.cube[layer] && this.cube[layer][row] && this.cube[layer][row][col]) {
|
||||
result += this.cube[layer][row][col];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[trifid]';
|
||||
const result = this.func(text.slice(0, 5));
|
||||
return result.substring(0, 10) + (result.length > 10 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// Trifid produces scrambled text (all uppercase letters, no digits)
|
||||
const cleaned = text.replace(/[\s]/g, '').toUpperCase();
|
||||
if (cleaned.length < 10) return false;
|
||||
if (!/^[A-Z]+$/.test(cleaned)) return false;
|
||||
|
||||
// Check if it looks scrambled (not readable English)
|
||||
const commonWords = ['THE', 'AND', 'FOR', 'ARE'];
|
||||
const hasCommonWords = commonWords.some(word => cleaned.includes(word));
|
||||
if (hasCommonWords && cleaned.length < 20) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
// two-square cipher transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Two-Square Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key1: 'EXAMPLE', // Top square key
|
||||
key2: 'KEYWORD', // Bottom square key
|
||||
// Standard alphabet for reference
|
||||
standardAlphabet: 'ABCDEFGHIKLMNOPQRSTUVWXYZ',
|
||||
// Create keyed square
|
||||
createKeyedSquare: function(key) {
|
||||
const used = new Set();
|
||||
const square = [];
|
||||
let keyIdx = 0;
|
||||
let alphaIdx = 0;
|
||||
|
||||
// Fill with key letters first
|
||||
for (let i = 0; i < 5; i++) {
|
||||
square[i] = [];
|
||||
for (let j = 0; j < 5; j++) {
|
||||
while (keyIdx < key.length && used.has(key[keyIdx])) {
|
||||
keyIdx++;
|
||||
}
|
||||
if (keyIdx < key.length) {
|
||||
square[i][j] = key[keyIdx];
|
||||
used.add(key[keyIdx]);
|
||||
keyIdx++;
|
||||
} else {
|
||||
// Fill with remaining alphabet
|
||||
while (alphaIdx < this.standardAlphabet.length && used.has(this.standardAlphabet[alphaIdx])) {
|
||||
alphaIdx++;
|
||||
}
|
||||
if (alphaIdx < this.standardAlphabet.length) {
|
||||
square[i][j] = this.standardAlphabet[alphaIdx];
|
||||
used.add(this.standardAlphabet[alphaIdx]);
|
||||
alphaIdx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return square;
|
||||
},
|
||||
func: function(text) {
|
||||
const key1 = (this.key1 || 'EXAMPLE').toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
|
||||
const key2 = (this.key2 || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
|
||||
|
||||
if (key1.length === 0 || key2.length === 0) return text;
|
||||
|
||||
let cleaned = text.toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
|
||||
if (cleaned.length === 0) return text;
|
||||
if (cleaned.length % 2 !== 0) {
|
||||
// Pad with X if odd length
|
||||
cleaned += 'X';
|
||||
}
|
||||
|
||||
// Create the two squares
|
||||
const topSquare = this.createKeyedSquare(key1);
|
||||
const bottomSquare = this.createKeyedSquare(key2);
|
||||
|
||||
let result = '';
|
||||
|
||||
// Process pairs of letters
|
||||
for (let i = 0; i < cleaned.length; i += 2) {
|
||||
const char1 = cleaned[i];
|
||||
const char2 = cleaned[i + 1];
|
||||
|
||||
// Find char1 in top square, char2 in bottom square
|
||||
let row1 = -1, col1 = -1;
|
||||
let row2 = -1, col2 = -1;
|
||||
|
||||
for (let r = 0; r < 5; r++) {
|
||||
for (let c = 0; c < 5; c++) {
|
||||
if (topSquare[r][c] === char1) {
|
||||
row1 = r;
|
||||
col1 = c;
|
||||
}
|
||||
if (bottomSquare[r][c] === char2) {
|
||||
row2 = r;
|
||||
col2 = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (row1 >= 0 && col1 >= 0 && row2 >= 0 && col2 >= 0) {
|
||||
// Use row1, col2 from top square and row2, col1 from bottom square
|
||||
result += topSquare[row1][col2] + bottomSquare[row2][col1];
|
||||
} else {
|
||||
result += char1 + char2;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const key1 = (this.key1 || 'EXAMPLE').toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
|
||||
const key2 = (this.key2 || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
|
||||
|
||||
if (key1.length === 0 || key2.length === 0) return text;
|
||||
|
||||
let cleaned = text.toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
|
||||
if (cleaned.length === 0) return text;
|
||||
if (cleaned.length % 2 !== 0) return text;
|
||||
|
||||
// Create the two squares
|
||||
const topSquare = this.createKeyedSquare(key1);
|
||||
const bottomSquare = this.createKeyedSquare(key2);
|
||||
|
||||
let result = '';
|
||||
|
||||
// Process pairs of letters
|
||||
for (let i = 0; i < cleaned.length; i += 2) {
|
||||
const char1 = cleaned[i];
|
||||
const char2 = cleaned[i + 1];
|
||||
|
||||
// Find char1 in top square, char2 in bottom square
|
||||
let row1 = -1, col1 = -1;
|
||||
let row2 = -1, col2 = -1;
|
||||
|
||||
for (let r = 0; r < 5; r++) {
|
||||
for (let c = 0; c < 5; c++) {
|
||||
if (topSquare[r][c] === char1) {
|
||||
row1 = r;
|
||||
col1 = c;
|
||||
}
|
||||
if (bottomSquare[r][c] === char2) {
|
||||
row2 = r;
|
||||
col2 = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (row1 >= 0 && col1 >= 0 && row2 >= 0 && col2 >= 0) {
|
||||
// Reverse: use row1, col2 from top square and row2, col1 from bottom square
|
||||
// But we need to find the original positions
|
||||
// Actually, the reverse is the same as forward for Two-Square
|
||||
result += topSquare[row1][col2] + bottomSquare[row2][col1];
|
||||
} else {
|
||||
result += char1 + char2;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[two-square]';
|
||||
return this.func(text.slice(0, 4)) + (text.length > 4 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// Two-Square produces scrambled text (all uppercase letters, no digits)
|
||||
const cleaned = text.replace(/[\s]/g, '').toUpperCase();
|
||||
if (cleaned.length < 10) return false;
|
||||
if (!/^[A-Z]+$/.test(cleaned)) return false;
|
||||
if (cleaned.length % 2 !== 0) return false; // Must be even length
|
||||
|
||||
// Check if it looks scrambled (not readable English)
|
||||
const commonWords = ['THE', 'AND', 'FOR', 'ARE'];
|
||||
const hasCommonWords = commonWords.some(word => cleaned.includes(word));
|
||||
if (hasCommonWords && cleaned.length < 20) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// XOR cipher transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'XOR Cipher',
|
||||
priority: 70,
|
||||
category: 'cipher',
|
||||
key: 'KEY', // Default key
|
||||
func: function(text) {
|
||||
const key = this.key || 'KEY';
|
||||
const keyBytes = new TextEncoder().encode(key);
|
||||
const textBytes = new TextEncoder().encode(text);
|
||||
const result = new Uint8Array(textBytes.length);
|
||||
|
||||
for (let i = 0; i < textBytes.length; i++) {
|
||||
result[i] = textBytes[i] ^ keyBytes[i % keyBytes.length];
|
||||
}
|
||||
|
||||
// Convert to hex string
|
||||
return Array.from(result)
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// XOR is self-reciprocal, but we need to convert from hex first
|
||||
try {
|
||||
const hexBytes = text.match(/.{1,2}/g) || [];
|
||||
const bytes = new Uint8Array(hexBytes.map(h => parseInt(h, 16)));
|
||||
const key = this.key || 'KEY';
|
||||
const keyBytes = new TextEncoder().encode(key);
|
||||
const result = new Uint8Array(bytes.length);
|
||||
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
result[i] = bytes[i] ^ keyBytes[i % keyBytes.length];
|
||||
}
|
||||
|
||||
return new TextDecoder().decode(result);
|
||||
} catch (e) {
|
||||
return text;
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[xor]';
|
||||
const result = this.func(text.slice(0, 4));
|
||||
return result.substring(0, 12) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check if text is hex-encoded (XOR cipher output)
|
||||
const cleaned = text.trim().replace(/\s/g, '');
|
||||
return cleaned.length >= 4 &&
|
||||
cleaned.length % 2 === 0 &&
|
||||
/^[0-9a-fA-F]+$/.test(cleaned);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
// base122 encoding (more efficient than Base64)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Base122',
|
||||
priority: 250,
|
||||
category: 'encoding',
|
||||
func: function(text) {
|
||||
// Base122 uses UTF-8 bytes and encodes them more efficiently
|
||||
// It uses 7-bit ASCII (0-127) plus some safe 2-byte UTF-8 sequences
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
let result = '';
|
||||
|
||||
let i = 0;
|
||||
while (i < bytes.length) {
|
||||
const byte = bytes[i];
|
||||
|
||||
if (byte < 128) {
|
||||
// Single byte ASCII
|
||||
result += String.fromCharCode(byte);
|
||||
i++;
|
||||
} else if (i + 1 < bytes.length) {
|
||||
// Try to encode as 2-byte sequence
|
||||
const b1 = byte;
|
||||
const b2 = bytes[i + 1];
|
||||
|
||||
// Check if it's a valid 2-byte UTF-8 sequence
|
||||
if ((b1 & 0xE0) === 0xC0 && (b2 & 0xC0) === 0x80) {
|
||||
result += String.fromCharCode(b1, b2);
|
||||
i += 2;
|
||||
} else {
|
||||
// Fallback: encode as escaped sequence
|
||||
result += String.fromCharCode(0xC2, 0x80 + (byte - 128));
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
// Last byte, encode as escaped
|
||||
result += String.fromCharCode(0xC2, 0x80 + (byte - 128));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const bytes = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < text.length) {
|
||||
const code = text.charCodeAt(i);
|
||||
|
||||
if (code < 128) {
|
||||
bytes.push(code);
|
||||
i++;
|
||||
} else if (i + 1 < text.length) {
|
||||
// Check for 2-byte sequence
|
||||
const b1 = code;
|
||||
const b2 = text.charCodeAt(i + 1);
|
||||
|
||||
if ((b1 & 0xE0) === 0xC0 && (b2 & 0xC0) === 0x80) {
|
||||
// Extract original byte from escaped sequence
|
||||
if (b1 === 0xC2 && b2 >= 0x80 && b2 < 0xC0) {
|
||||
bytes.push(b2 - 0x80);
|
||||
} else {
|
||||
bytes.push(b1, b2);
|
||||
}
|
||||
i += 2;
|
||||
} else {
|
||||
bytes.push(code);
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
bytes.push(code);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return new TextDecoder().decode(new Uint8Array(bytes));
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[base122]';
|
||||
const result = this.func(text.slice(0, 10));
|
||||
return result.substring(0, 15) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
// Base122 produces text that's mostly ASCII with some UTF-8 sequences
|
||||
// Hard to detect reliably, but check for mix of ASCII and UTF-8
|
||||
const hasAscii = /[\x00-\x7F]/.test(text);
|
||||
const hasUtf8 = /[\xC0-\xFF]/.test(text);
|
||||
return hasAscii && text.length >= 8;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// base36 encoding transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Base36',
|
||||
priority: 270,
|
||||
category: 'encoding',
|
||||
alphabet: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
||||
func: function(text) {
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
let num = 0n;
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
num = num * 256n + BigInt(bytes[i]);
|
||||
}
|
||||
|
||||
if (num === 0n) return '0';
|
||||
|
||||
let result = '';
|
||||
const base = 36n;
|
||||
while (num > 0n) {
|
||||
result = this.alphabet[Number(num % base)] + result;
|
||||
num = num / base;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
try {
|
||||
let num = 0n;
|
||||
const base = 36n;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text[i].toUpperCase();
|
||||
const idx = this.alphabet.indexOf(char);
|
||||
if (idx === -1) return text;
|
||||
num = num * base + BigInt(idx);
|
||||
}
|
||||
|
||||
// Convert back to bytes
|
||||
const bytes = [];
|
||||
while (num > 0n) {
|
||||
bytes.unshift(Number(num % 256n));
|
||||
num = num / 256n;
|
||||
}
|
||||
|
||||
return new TextDecoder().decode(new Uint8Array(bytes));
|
||||
} catch (e) {
|
||||
return text;
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[base36]';
|
||||
const result = this.func(text.slice(0, 4));
|
||||
return result.substring(0, 12) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
const cleaned = text.trim().replace(/\s/g, '').toUpperCase();
|
||||
return cleaned.length >= 4 && /^[0-9A-Z]+$/.test(cleaned);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// base91 encoding transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Base91',
|
||||
priority: 270,
|
||||
category: 'encoding',
|
||||
alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&()*+,./:;<=>?@[]^_`{|}~"',
|
||||
func: function(text) {
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
let num = 0n;
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
num = num * 256n + BigInt(bytes[i]);
|
||||
}
|
||||
|
||||
if (num === 0n) return this.alphabet[0];
|
||||
|
||||
let result = '';
|
||||
const base = 91n;
|
||||
while (num > 0n) {
|
||||
result = this.alphabet[Number(num % base)] + result;
|
||||
num = num / base;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
try {
|
||||
let num = 0n;
|
||||
const base = 91n;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const idx = this.alphabet.indexOf(text[i]);
|
||||
if (idx === -1) return text;
|
||||
num = num * base + BigInt(idx);
|
||||
}
|
||||
|
||||
// Convert back to bytes
|
||||
const bytes = [];
|
||||
while (num > 0n) {
|
||||
bytes.unshift(Number(num % 256n));
|
||||
num = num / 256n;
|
||||
}
|
||||
|
||||
return new TextDecoder().decode(new Uint8Array(bytes));
|
||||
} catch (e) {
|
||||
return text;
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[base91]';
|
||||
const result = this.func(text.slice(0, 4));
|
||||
return result.substring(0, 12) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
const cleaned = text.trim().replace(/\s/g, '');
|
||||
return cleaned.length >= 4 && /^[A-Za-z0-9!#$%&()*+,./:;<=>?@[\]^_`{|}~"]+$/.test(cleaned);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
// baudot code / ITA2 encoding (teletype code)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Baudot Code (ITA2)',
|
||||
priority: 250,
|
||||
category: 'encoding',
|
||||
// Baudot/ITA2 5-bit code (letters and figures shift)
|
||||
letters: {
|
||||
0b00000: ' ', // NULL/blank
|
||||
0b00010: 'E',
|
||||
0b00011: '\n', // Line feed
|
||||
0b00100: 'A',
|
||||
0b00101: ' ',
|
||||
0b00110: 'S',
|
||||
0b00111: 'I',
|
||||
0b01000: 'U',
|
||||
0b01001: '\r', // Carriage return
|
||||
0b01010: 'D',
|
||||
0b01011: 'R',
|
||||
0b01100: 'J',
|
||||
0b01101: 'N',
|
||||
0b01110: 'F',
|
||||
0b01111: 'C',
|
||||
0b10000: 'K',
|
||||
0b10001: 'T',
|
||||
0b10010: 'Z',
|
||||
0b10011: 'L',
|
||||
0b10100: 'W',
|
||||
0b10101: 'H',
|
||||
0b10110: 'Y',
|
||||
0b10111: 'P',
|
||||
0b11000: 'Q',
|
||||
0b11001: 'O',
|
||||
0b11010: 'B',
|
||||
0b11011: 'G',
|
||||
0b11100: 'Figures', // Shift to figures
|
||||
0b11101: 'M',
|
||||
0b11110: 'X',
|
||||
0b11111: 'V',
|
||||
},
|
||||
figures: {
|
||||
0b00000: ' ',
|
||||
0b00010: '3',
|
||||
0b00011: '\n',
|
||||
0b00100: '-',
|
||||
0b00101: ' ',
|
||||
0b00110: '\'',
|
||||
0b00111: '8',
|
||||
0b01000: '7',
|
||||
0b01001: '\r',
|
||||
0b01010: '\u0005', // ENQ
|
||||
0b01011: '4',
|
||||
0b01100: '\'', // Bell
|
||||
0b01101: ',',
|
||||
0b01110: '!',
|
||||
0b01111: ':',
|
||||
0b10000: '(',
|
||||
0b10001: '5',
|
||||
0b10010: '+',
|
||||
0b10011: ')',
|
||||
0b10100: '2',
|
||||
0b10101: '$',
|
||||
0b10110: '6',
|
||||
0b10111: '0',
|
||||
0b11000: '1',
|
||||
0b11001: '9',
|
||||
0b11010: '?',
|
||||
0b11011: '&',
|
||||
0b11100: 'Letters', // Shift to letters
|
||||
0b11101: '.',
|
||||
0b11110: '/',
|
||||
0b11111: '=',
|
||||
},
|
||||
func: function(text) {
|
||||
// Create reverse maps
|
||||
const lettersToCode = {};
|
||||
const figuresToCode = {};
|
||||
for (const [code, char] of Object.entries(this.letters)) {
|
||||
if (char !== 'Figures' && char !== 'Letters') {
|
||||
lettersToCode[char] = parseInt(code);
|
||||
}
|
||||
}
|
||||
for (const [code, char] of Object.entries(this.figures)) {
|
||||
if (char !== 'Figures' && char !== 'Letters') {
|
||||
figuresToCode[char] = parseInt(code);
|
||||
}
|
||||
}
|
||||
|
||||
let result = '';
|
||||
let inFigures = false;
|
||||
|
||||
for (const char of text.toUpperCase()) {
|
||||
// Check if we need to shift
|
||||
const isFigure = /[0-9\-'():!$?&.\/+=]/.test(char);
|
||||
|
||||
if (isFigure && !inFigures) {
|
||||
result += String.fromCharCode(0b11100); // Figures shift
|
||||
inFigures = true;
|
||||
} else if (!isFigure && inFigures) {
|
||||
result += String.fromCharCode(0b11111); // Letters shift (approximate)
|
||||
inFigures = false;
|
||||
}
|
||||
|
||||
// Encode character
|
||||
const code = inFigures ? figuresToCode[char] : lettersToCode[char];
|
||||
if (code !== undefined) {
|
||||
result += String.fromCharCode(code);
|
||||
} else {
|
||||
result += char; // Keep unmapped
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
let result = '';
|
||||
let inFigures = false;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const code = text.charCodeAt(i) & 0x1F; // 5 bits
|
||||
|
||||
if (code === 0b11100) {
|
||||
inFigures = true;
|
||||
continue;
|
||||
} else if (code === 0b11111) {
|
||||
inFigures = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
const map = inFigures ? this.figures : this.letters;
|
||||
const char = map[code];
|
||||
if (char && char !== 'Figures' && char !== 'Letters') {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[baudot]';
|
||||
return this.func(text.slice(0, 5));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Baudot uses 5-bit codes (0-31)
|
||||
// Check for characters in the 5-bit range
|
||||
const has5Bit = /[\x00-\x1F]/.test(text);
|
||||
return has5Bit && text.length >= 5;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// binary coded decimal (BCD) transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Binary Coded Decimal',
|
||||
priority: 300,
|
||||
category: 'encoding',
|
||||
func: function(text) {
|
||||
return [...text].map(c => {
|
||||
const code = c.charCodeAt(0);
|
||||
// Convert each digit of the char code to BCD
|
||||
return code.toString().split('').map(d => {
|
||||
const digit = parseInt(d);
|
||||
return digit.toString(2).padStart(4, '0');
|
||||
}).join(' ');
|
||||
}).join(' ');
|
||||
},
|
||||
reverse: function(text) {
|
||||
try {
|
||||
const bcdGroups = text.trim().split(/\s+/);
|
||||
const chars = [];
|
||||
let currentCode = '';
|
||||
|
||||
for (let i = 0; i < bcdGroups.length; i++) {
|
||||
if (bcdGroups[i].length === 4 && /^[01]+$/.test(bcdGroups[i])) {
|
||||
currentCode += parseInt(bcdGroups[i], 2).toString();
|
||||
if (currentCode.length >= 3) {
|
||||
const code = parseInt(currentCode);
|
||||
if (code >= 0 && code <= 65535) {
|
||||
chars.push(String.fromCharCode(code));
|
||||
currentCode = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return chars.join('');
|
||||
} catch (e) {
|
||||
return text;
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[bcd]';
|
||||
return this.func(text.slice(0, 2));
|
||||
},
|
||||
detector: function(text) {
|
||||
const cleaned = text.trim().replace(/\s/g, '');
|
||||
return cleaned.length >= 4 && /^[01]+$/.test(cleaned) && cleaned.length % 4 === 0;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
// EBCDIC encoding (IBM character encoding)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'EBCDIC',
|
||||
priority: 250,
|
||||
category: 'encoding',
|
||||
// EBCDIC to ASCII mapping (simplified - full EBCDIC has many variants)
|
||||
ebcdicToAscii: {
|
||||
0x40: 0x20, // Space
|
||||
0x4A: 0x21, // !
|
||||
0x4B: 0x22, // "
|
||||
0x4C: 0x23, // #
|
||||
0x4D: 0x24, // $
|
||||
0x4E: 0x25, // %
|
||||
0x4F: 0x26, // &
|
||||
0x50: 0x27, // '
|
||||
0x5A: 0x28, // (
|
||||
0x5B: 0x29, // )
|
||||
0x5C: 0x2A, // *
|
||||
0x5D: 0x2B, // +
|
||||
0x5E: 0x2C, // ,
|
||||
0x5F: 0x2D, // -
|
||||
0x60: 0x2E, // .
|
||||
0x61: 0x2F, // /
|
||||
0xF0: 0x30, // 0
|
||||
0xF1: 0x31, // 1
|
||||
0xF2: 0x32, // 2
|
||||
0xF3: 0x33, // 3
|
||||
0xF4: 0x34, // 4
|
||||
0xF5: 0x35, // 5
|
||||
0xF6: 0x36, // 6
|
||||
0xF7: 0x37, // 7
|
||||
0xF8: 0x38, // 8
|
||||
0xF9: 0x39, // 9
|
||||
0x7A: 0x3A, // :
|
||||
0x7B: 0x3B, // ;
|
||||
0x7C: 0x3C, // <
|
||||
0x7D: 0x3D, // =
|
||||
0x7E: 0x3E, // >
|
||||
0x7F: 0x3F, // ?
|
||||
0x81: 0x41, // A
|
||||
0x82: 0x42, // B
|
||||
0x83: 0x43, // C
|
||||
0x84: 0x44, // D
|
||||
0x85: 0x45, // E
|
||||
0x86: 0x46, // F
|
||||
0x87: 0x47, // G
|
||||
0x88: 0x48, // H
|
||||
0x89: 0x49, // I
|
||||
0x91: 0x4A, // J
|
||||
0x92: 0x4B, // K
|
||||
0x93: 0x4C, // L
|
||||
0x94: 0x4D, // M
|
||||
0x95: 0x4E, // N
|
||||
0x96: 0x4F, // O
|
||||
0x97: 0x50, // P
|
||||
0x98: 0x51, // Q
|
||||
0x99: 0x52, // R
|
||||
0xA2: 0x53, // S
|
||||
0xA3: 0x54, // T
|
||||
0xA4: 0x55, // U
|
||||
0xA5: 0x56, // V
|
||||
0xA6: 0x57, // W
|
||||
0xA7: 0x58, // X
|
||||
0xA8: 0x59, // Y
|
||||
0xA9: 0x5A, // Z
|
||||
},
|
||||
func: function(text) {
|
||||
// Convert ASCII to EBCDIC
|
||||
const asciiToEbcdic = {};
|
||||
for (const [ebcdic, ascii] of Object.entries(this.ebcdicToAscii)) {
|
||||
asciiToEbcdic[ascii] = parseInt(ebcdic);
|
||||
}
|
||||
|
||||
let result = '';
|
||||
for (const char of text) {
|
||||
const code = char.charCodeAt(0);
|
||||
// Convert lowercase letters to uppercase before encoding (EBCDIC is uppercase-only)
|
||||
if (code >= 0x61 && code <= 0x7A) { // a-z
|
||||
const upperCode = code - 0x20; // Convert to A-Z
|
||||
if (asciiToEbcdic[upperCode] !== undefined) {
|
||||
result += String.fromCharCode(asciiToEbcdic[upperCode]);
|
||||
} else {
|
||||
result += char; // Keep unmapped characters
|
||||
}
|
||||
} else if (asciiToEbcdic[code] !== undefined) {
|
||||
result += String.fromCharCode(asciiToEbcdic[code]);
|
||||
} else {
|
||||
result += char; // Keep unmapped characters
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
let result = '';
|
||||
for (const char of text) {
|
||||
const code = char.charCodeAt(0);
|
||||
if (this.ebcdicToAscii[code] !== undefined) {
|
||||
result += String.fromCharCode(this.ebcdicToAscii[code]);
|
||||
} else {
|
||||
result += char; // Keep unmapped characters
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[ebcdic]';
|
||||
return this.func(text.slice(0, 8)) + (text.length > 8 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
if (!text || text.length < 2) return false;
|
||||
|
||||
// EBCDIC uses specific byte ranges for letters and numbers
|
||||
// Letters: 0x81-0xA9 (A-Z)
|
||||
// Numbers: 0xF0-0xF9 (0-9)
|
||||
// Punctuation: 0x40-0x7F range
|
||||
|
||||
// Check for EBCDIC-specific character codes (letters and numbers)
|
||||
const hasEbcdicLetters = /[\x81-\x89\x91-\x99\xA2-\xA9]/.test(text); // A-Z in EBCDIC
|
||||
const hasEbcdicNumbers = /[\xF0-\xF9]/.test(text); // 0-9 in EBCDIC
|
||||
|
||||
// Must have at least some EBCDIC-specific characters
|
||||
if (!hasEbcdicLetters && !hasEbcdicNumbers) return false;
|
||||
|
||||
// Reject if text is already readable ASCII (common English words)
|
||||
// This prevents false positives on plain text
|
||||
const commonWords = /\b(the|and|for|are|but|not|you|all|can|her|was|one|our|out|day|get|has|him|his|how|man|new|now|old|see|two|way|who|boy|did|its|let|put|say|she|too|use)\b/i;
|
||||
if (commonWords.test(text)) return false;
|
||||
|
||||
// Check if decoding produces text that looks like it was encoded
|
||||
// EBCDIC-encoded text, when decoded, should have readable ASCII
|
||||
// If the input is already readable ASCII, it's not EBCDIC
|
||||
const readableAscii = /^[\x20-\x7E\s]*$/.test(text);
|
||||
if (readableAscii && !hasEbcdicLetters && !hasEbcdicNumbers) {
|
||||
// If it's all readable ASCII and has no EBCDIC-specific codes, reject
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify that at least some characters are in EBCDIC-specific ranges
|
||||
// For short strings, require at least 1 EBCDIC character
|
||||
// For longer strings, require at least 10% to be EBCDIC-specific
|
||||
const ebcdicChars = (text.match(/[\x81-\x89\x91-\x99\xA2-\xA9\xF0-\xF9]/g) || []).length;
|
||||
if (ebcdicChars === 0) return false;
|
||||
|
||||
// For short strings (<= 20 chars), just need at least 1 EBCDIC char
|
||||
if (text.length <= 20) {
|
||||
return ebcdicChars >= 1;
|
||||
}
|
||||
|
||||
// For longer strings, require at least 10% to be EBCDIC-specific
|
||||
const ebcdicRatio = ebcdicChars / text.length;
|
||||
return ebcdicRatio >= 0.1; // At least 10% must be EBCDIC-specific
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// emoji encoding transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Emoji Encoding',
|
||||
priority: 250,
|
||||
category: 'encoding',
|
||||
// Map bytes to emoji (using common emojis)
|
||||
emojiMap: [
|
||||
'😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃',
|
||||
'😉', '😊', '😇', '🥰', '😍', '🤩', '😘', '😗', '😚', '😙',
|
||||
'😋', '😛', '😜', '🤪', '😝', '🤑', '🤗', '🤭', '🤫', '🤔',
|
||||
'🤐', '🤨', '😐', '😑', '😶', '😏', '😒', '🙄', '😬', '🤥',
|
||||
'😌', '😔', '😪', '🤤', '😴', '😷', '🤒', '🤕', '🤢', '🤮',
|
||||
'🤧', '🥵', '🥶', '😶🌫️', '😵', '😵💫', '🤯', '🤠', '🥳', '😎',
|
||||
'🤓', '🧐', '😕', '😟', '🙁', '😮', '😯', '😲', '😳', '🥺',
|
||||
'😦', '😧', '😨', '😰', '😥', '😢', '😭', '😱', '😖', '😣',
|
||||
'😞', '😓', '😩', '😫', '🥱', '😤', '😡', '😠', '🤬', '😈',
|
||||
'👿', '💀', '☠️', '💩', '🤡', '👹', '👺', '👻', '👽', '👾',
|
||||
'🤖', '😺', '😸', '😹', '😻', '😼', '😽', '🙀', '😿', '😾',
|
||||
'🙈', '🙉', '🙊', '💋', '💌', '💘', '💝', '💖', '💗', '💓',
|
||||
'💞', '💕', '💟', '❣️', '💔', '❤️', '🧡', '💛', '💚', '💙',
|
||||
'💜', '🖤', '🤍', '🤎', '💯', '💢', '💥', '💫', '💦', '💨',
|
||||
'🕳️', '💣', '💬', '👁️🗨️', '🗨️', '🗯️', '💭', '💤', '👋', '🤚',
|
||||
'🖐️', '✋', '🖖', '👌', '🤌', '🤏', '✌️', '🤞', '🤟', '🤘',
|
||||
'🤙', '👈', '👉', '👆', '🖕', '👇', '☝️', '👍', '👎', '✊',
|
||||
'👊', '🤛', '🤜', '👏', '🙌', '👐', '🤲', '🤝', '🙏', '✍️',
|
||||
'💪', '🦾', '🦿', '🦵', '🦶', '👂', '🦻', '👃', '🧠', '🫀',
|
||||
'🫁', '🦷', '🦴', '👀', '👁️', '👅', '👄', '💋', '🩸', '👶',
|
||||
'🧒', '👦', '👧', '🧑', '👱', '👨', '🧔', '👨🦰', '👨🦱', '👨🦳',
|
||||
'👨🦲', '👩', '👩🦰', '👩🦱', '👩🦳', '👩🦲', '🧓', '👴', '👵', '🙍',
|
||||
'🙎', '🙅', '🙆', '💁', '🙋', '🧏', '🤦', '🤦♂️', '🤦♀️', '🤷',
|
||||
'🤷♂️', '🤷♀️', '🙇', '🙇♂️', '🙇♀️', '🤦', '🤦♂️', '🤦♀️', '🤷', '🤷♂️'
|
||||
],
|
||||
func: function(text) {
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
let result = '';
|
||||
|
||||
for (const byte of bytes) {
|
||||
result += this.emojiMap[byte % this.emojiMap.length] + ' ';
|
||||
}
|
||||
|
||||
return result.trim();
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Create reverse map
|
||||
const reverseMap = {};
|
||||
for (let i = 0; i < this.emojiMap.length; i++) {
|
||||
reverseMap[this.emojiMap[i]] = i;
|
||||
}
|
||||
|
||||
// Extract emojis (match any emoji, not just specific range)
|
||||
const emojis = text.match(/[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/gu) || [];
|
||||
const bytes = [];
|
||||
|
||||
for (const emoji of emojis) {
|
||||
if (reverseMap[emoji] !== undefined) {
|
||||
bytes.push(reverseMap[emoji]);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return new TextDecoder().decode(new Uint8Array(bytes));
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[emoji-encoding]';
|
||||
return this.func(text.slice(0, 3));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for emoji patterns (broader range)
|
||||
const emojiPattern = /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/gu;
|
||||
const matches = text.match(emojiPattern) || [];
|
||||
return matches.length >= 3;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// gray code transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Gray Code',
|
||||
priority: 300,
|
||||
category: 'encoding',
|
||||
func: function(text) {
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
const binary = Array.from(bytes)
|
||||
.map(b => b.toString(2).padStart(8, '0'))
|
||||
.join('');
|
||||
|
||||
// Convert to Gray code
|
||||
let gray = binary[0];
|
||||
for (let i = 1; i < binary.length; i++) {
|
||||
gray += (parseInt(binary[i - 1]) ^ parseInt(binary[i])).toString();
|
||||
}
|
||||
|
||||
return gray;
|
||||
},
|
||||
reverse: function(text) {
|
||||
try {
|
||||
// Convert from Gray code to binary
|
||||
if (!/^[01]+$/.test(text)) return text;
|
||||
|
||||
let binary = text[0];
|
||||
for (let i = 1; i < text.length; i++) {
|
||||
binary += (parseInt(binary[i - 1]) ^ parseInt(text[i])).toString();
|
||||
}
|
||||
|
||||
// Convert binary to bytes
|
||||
const bytes = [];
|
||||
for (let i = 0; i < binary.length; i += 8) {
|
||||
const byte = binary.slice(i, i + 8);
|
||||
if (byte.length === 8) {
|
||||
bytes.push(parseInt(byte, 2));
|
||||
}
|
||||
}
|
||||
|
||||
return new TextDecoder().decode(new Uint8Array(bytes));
|
||||
} catch (e) {
|
||||
return text;
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[gray]';
|
||||
const result = this.func(text.slice(0, 2));
|
||||
return result.substring(0, 16) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
const cleaned = text.trim().replace(/\s/g, '');
|
||||
return cleaned.length >= 8 && /^[01]+$/.test(cleaned);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// quoted-printable encoding transform (RFC 2045)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Quoted-Printable',
|
||||
priority: 70,
|
||||
category: 'encoding',
|
||||
func: function(text) {
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
let result = '';
|
||||
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
const byte = bytes[i];
|
||||
// Printable ASCII (33-126) except = (61) can be used as-is
|
||||
// Space (32) can be used, but often encoded as =20
|
||||
// = (61) must be encoded as =3D
|
||||
if (byte >= 33 && byte <= 60 || byte >= 63 && byte <= 126) {
|
||||
result += String.fromCharCode(byte);
|
||||
} else if (byte === 32) {
|
||||
// Space can be space or =20
|
||||
result += ' ';
|
||||
} else {
|
||||
// Encode as =XX
|
||||
result += '=' + byte.toString(16).toUpperCase().padStart(2, '0');
|
||||
}
|
||||
}
|
||||
|
||||
// Soft line breaks: lines should not exceed 76 chars (excluding CRLF)
|
||||
// For simplicity, we'll add = at end of long lines
|
||||
const lines = [];
|
||||
let currentLine = '';
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
if (currentLine.length >= 75) {
|
||||
lines.push(currentLine + '=');
|
||||
currentLine = result[i];
|
||||
} else {
|
||||
currentLine += result[i];
|
||||
}
|
||||
}
|
||||
if (currentLine) lines.push(currentLine);
|
||||
|
||||
return lines.join('\r\n');
|
||||
},
|
||||
reverse: function(text) {
|
||||
try {
|
||||
// Remove soft line breaks (= at end of line)
|
||||
let cleaned = text.replace(/=\r?\n/g, '').replace(/=\r/g, '');
|
||||
let result = '';
|
||||
|
||||
for (let i = 0; i < cleaned.length; i++) {
|
||||
if (cleaned[i] === '=' && i + 2 < cleaned.length) {
|
||||
const hex = cleaned.substring(i + 1, i + 3);
|
||||
const byte = parseInt(hex, 16);
|
||||
if (!isNaN(byte)) {
|
||||
result += String.fromCharCode(byte);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result += cleaned[i];
|
||||
}
|
||||
|
||||
return new TextDecoder().decode(new TextEncoder().encode(result));
|
||||
} catch (e) {
|
||||
return text;
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[qp]';
|
||||
const result = this.func(text.slice(0, 10));
|
||||
return result.substring(0, 20).replace(/\r?\n/g, ' ') + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for quoted-printable patterns (=XX hex codes)
|
||||
return /=([0-9A-F]{2})/i.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// unicode code points encoding transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Unicode Code Points',
|
||||
priority: 250,
|
||||
category: 'encoding',
|
||||
func: function(text) {
|
||||
// Encode text as Unicode code points (U+XXXX format)
|
||||
let result = '';
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const code = text.charCodeAt(i);
|
||||
result += 'U+' + code.toString(16).toUpperCase().padStart(4, '0') + ' ';
|
||||
}
|
||||
return result.trim();
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Extract U+XXXX patterns and convert back to characters
|
||||
const matches = text.match(/U\+([0-9A-Fa-f]{4,6})/g) || [];
|
||||
let result = '';
|
||||
for (const match of matches) {
|
||||
const code = parseInt(match.substring(2), 16);
|
||||
if (code >= 0 && code <= 0x10FFFF) {
|
||||
result += String.fromCharCode(code);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[unicode-points]';
|
||||
const result = this.func(text.slice(0, 3));
|
||||
return result.substring(0, 20) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for U+XXXX pattern
|
||||
const pattern = /U\+[0-9A-Fa-f]{4,6}/;
|
||||
return pattern.test(text) && text.match(/U\+[0-9A-Fa-f]{4,6}/g).length >= 2;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// uuencoding transform (Unix-to-Unix encoding)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Uuencoding',
|
||||
priority: 250,
|
||||
category: 'encoding',
|
||||
func: function(text) {
|
||||
// Uuencoding encodes 3 bytes into 4 characters
|
||||
// Each character represents 6 bits (0-63)
|
||||
const uuChars = ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_';
|
||||
|
||||
let result = '';
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
|
||||
for (let i = 0; i < bytes.length; i += 3) {
|
||||
const b1 = bytes[i] || 0;
|
||||
const b2 = bytes[i + 1] || 0;
|
||||
const b3 = bytes[i + 2] || 0;
|
||||
|
||||
// Combine 3 bytes (24 bits) into 4 6-bit values
|
||||
const val1 = (b1 >> 2) & 0x3F;
|
||||
const val2 = ((b1 << 4) | (b2 >> 4)) & 0x3F;
|
||||
const val3 = ((b2 << 2) | (b3 >> 6)) & 0x3F;
|
||||
const val4 = b3 & 0x3F;
|
||||
|
||||
result += uuChars[val1] + uuChars[val2] + uuChars[val3] + uuChars[val4];
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const uuChars = ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_';
|
||||
|
||||
const bytes = [];
|
||||
const totalChunks = Math.floor(text.length / 4);
|
||||
|
||||
for (let i = 0; i < totalChunks; i++) {
|
||||
const chunk = text.substring(i * 4, (i + 1) * 4);
|
||||
if (chunk.length < 4) break;
|
||||
|
||||
const val1 = uuChars.indexOf(chunk[0]);
|
||||
const val2 = uuChars.indexOf(chunk[1]);
|
||||
const val3 = uuChars.indexOf(chunk[2]);
|
||||
const val4 = uuChars.indexOf(chunk[3]);
|
||||
|
||||
if (val1 === -1 || val2 === -1 || val3 === -1 || val4 === -1) continue;
|
||||
|
||||
// Reconstruct 3 bytes from 4 6-bit values
|
||||
const b1 = (val1 << 2) | (val2 >> 4);
|
||||
const b2 = ((val2 << 4) | (val3 >> 2)) & 0xFF;
|
||||
const b3 = ((val3 << 6) | val4) & 0xFF;
|
||||
|
||||
bytes.push(b1);
|
||||
bytes.push(b2);
|
||||
bytes.push(b3);
|
||||
}
|
||||
|
||||
// Remove trailing null bytes (padding)
|
||||
while (bytes.length > 0 && bytes[bytes.length - 1] === 0) {
|
||||
bytes.pop();
|
||||
}
|
||||
|
||||
try {
|
||||
return new TextDecoder().decode(new Uint8Array(bytes));
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[uuencoding]';
|
||||
const result = this.func(text.slice(0, 3));
|
||||
return result.substring(0, 8) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
// Uuencoding uses specific character set: space through underscore (ASCII 32-95)
|
||||
const uuPattern = /^[ !"#$%&'()*+,\-./0-9:;<=>?@A-Z[\\\]^_]+$/;
|
||||
return text.length >= 8 && uuPattern.test(text) && text.length % 4 === 0;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// yenc encoding transform (Usenet binary encoding)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'YEnc',
|
||||
priority: 250,
|
||||
category: 'encoding',
|
||||
func: function(text) {
|
||||
// YEnc encodes bytes by adding 42 (0x2A) and escaping special characters
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
let result = '';
|
||||
|
||||
for (const byte of bytes) {
|
||||
let encoded = (byte + 42) % 256;
|
||||
|
||||
// Escape special characters: NULL (0), LF (10), CR (13), = (61)
|
||||
if (encoded === 0 || encoded === 10 || encoded === 13 || encoded === 61) {
|
||||
result += '=' + String.fromCharCode((encoded + 64) % 256);
|
||||
} else {
|
||||
result += String.fromCharCode(encoded);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const bytes = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < text.length) {
|
||||
if (text[i] === '=' && i + 1 < text.length) {
|
||||
// Escaped character
|
||||
const escaped = text.charCodeAt(i + 1);
|
||||
const decoded = (escaped - 64) % 256;
|
||||
bytes.push((decoded - 42 + 256) % 256);
|
||||
i += 2;
|
||||
} else {
|
||||
// Normal character
|
||||
const encoded = text.charCodeAt(i);
|
||||
bytes.push((encoded - 42 + 256) % 256);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return new TextDecoder().decode(new Uint8Array(bytes));
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[yenc]';
|
||||
const result = this.func(text.slice(0, 3));
|
||||
return result.substring(0, 8) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
// YEnc produces binary-like data, hard to detect reliably
|
||||
// Check for escape sequences (= followed by character)
|
||||
const escapePattern = /=[\x00-\xFF]/;
|
||||
return escapePattern.test(text) && text.length >= 8;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
// z85 encoding (ZeroMQ Base85)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Z85',
|
||||
priority: 250,
|
||||
category: 'encoding',
|
||||
// Z85 uses a different character set than standard Base85
|
||||
charset: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#',
|
||||
func: function(text) {
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
let result = '';
|
||||
const originalLength = bytes.length;
|
||||
|
||||
// Z85 encodes 4 bytes into 5 characters
|
||||
for (let i = 0; i < bytes.length; i += 4) {
|
||||
const chunk = Array.from(bytes.slice(i, i + 4));
|
||||
const chunkLength = chunk.length;
|
||||
while (chunk.length < 4) chunk.push(0);
|
||||
|
||||
// Convert 4 bytes to 32-bit integer
|
||||
let value = 0;
|
||||
for (let j = 0; j < 4; j++) {
|
||||
value = (value << 8) + chunk[j];
|
||||
}
|
||||
|
||||
// Convert to base 85 (5 digits)
|
||||
const z85Chars = [];
|
||||
for (let j = 0; j < 5; j++) {
|
||||
z85Chars.unshift(this.charset[value % 85]);
|
||||
value = Math.floor(value / 85);
|
||||
}
|
||||
|
||||
result += z85Chars.join('');
|
||||
}
|
||||
|
||||
// Store original length for decoding
|
||||
this._z85OriginalLength = originalLength;
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const bytes = [];
|
||||
|
||||
// Z85 decodes 5 characters into 4 bytes
|
||||
for (let i = 0; i < text.length; i += 5) {
|
||||
const chunk = text.substring(i, i + 5);
|
||||
if (chunk.length < 5) break;
|
||||
|
||||
// Convert 5 base-85 digits to 32-bit integer
|
||||
let value = 0;
|
||||
for (const char of chunk) {
|
||||
const idx = this.charset.indexOf(char);
|
||||
if (idx === -1) return ''; // Invalid character
|
||||
value = value * 85 + idx;
|
||||
}
|
||||
|
||||
// Extract 4 bytes
|
||||
bytes.push((value >> 24) & 0xFF);
|
||||
bytes.push((value >> 16) & 0xFF);
|
||||
bytes.push((value >> 8) & 0xFF);
|
||||
bytes.push(value & 0xFF);
|
||||
}
|
||||
|
||||
// Trim to original length if we stored it
|
||||
if (this._z85OriginalLength !== undefined) {
|
||||
bytes.length = Math.min(bytes.length, this._z85OriginalLength);
|
||||
} else {
|
||||
// Remove trailing null bytes (padding)
|
||||
while (bytes.length > 0 && bytes[bytes.length - 1] === 0) {
|
||||
bytes.pop();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return new TextDecoder().decode(new Uint8Array(bytes));
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[z85]';
|
||||
const result = this.func(text.slice(0, 4));
|
||||
return result.substring(0, 10) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
// Z85 uses specific character set
|
||||
const z85Pattern = /^[0-9a-zA-Z.\-:+=^!\/\*\?&<>()\[\]{}@%$#]+$/;
|
||||
return text.length >= 5 && z85Pattern.test(text) && text.length % 5 === 0;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,21 +5,59 @@ export default new BaseTransformer({
|
||||
name: 'Dovahzul (Dragon)',
|
||||
priority: 285,
|
||||
// Detector: Look for characteristic Dovahzul patterns (vowel expansions)
|
||||
// Dovahzul encoding expands vowels: a->ah, e->eh, i->ii, q->kw, x->ks
|
||||
// We need to detect when text actually looks like Dovahzul, not just contains these patterns
|
||||
detector: function(text) {
|
||||
if (!/[a-z]/i.test(text)) return false;
|
||||
|
||||
const dovahzulPatterns = ['ah', 'eh', 'ii', 'kw', 'ks'];
|
||||
let patternCount = 0;
|
||||
const lowerInput = text.toLowerCase();
|
||||
const textLength = text.length;
|
||||
|
||||
for (const pattern of dovahzulPatterns) {
|
||||
// Check for Dovahzul-specific patterns that are less common in regular English
|
||||
// 'kw' (from 'q') and 'ks' (from 'x') are strong indicators
|
||||
const strongPatterns = ['kw', 'ks'];
|
||||
let strongCount = 0;
|
||||
for (const pattern of strongPatterns) {
|
||||
const matches = lowerInput.match(new RegExp(pattern, 'g'));
|
||||
if (matches) patternCount += matches.length;
|
||||
if (matches) strongCount += matches.length;
|
||||
}
|
||||
|
||||
// For short inputs, require at least 1 pattern, for longer require 2+
|
||||
const minPatterns = text.length < 30 ? 1 : 2;
|
||||
return patternCount >= minPatterns;
|
||||
// Check for vowel expansions: 'ah', 'eh', 'ii'
|
||||
// These can appear anywhere in Dovahzul-encoded text
|
||||
const vowelExpansions = ['ah', 'eh', 'ii'];
|
||||
let expansionCount = 0;
|
||||
|
||||
for (const pattern of vowelExpansions) {
|
||||
const matches = lowerInput.match(new RegExp(pattern, 'g'));
|
||||
if (matches) expansionCount += matches.length;
|
||||
}
|
||||
|
||||
// Calculate pattern density
|
||||
const totalPatterns = strongCount + expansionCount;
|
||||
const patternDensity = totalPatterns / Math.max(textLength / 10, 1);
|
||||
|
||||
// Strong patterns (kw/ks) are very rare in English - even 1 is a strong indicator
|
||||
if (strongCount >= 1) return true;
|
||||
|
||||
// For vowel expansions, we need to be more careful to avoid false positives
|
||||
// Check if the patterns appear in positions that suggest Dovahzul encoding
|
||||
// rather than natural English words
|
||||
|
||||
// Common English words that contain these patterns (false positives to avoid):
|
||||
const falsePositiveWords = ['what', 'that', 'when', 'where', 'which', 'while', 'this', 'with', 'think', 'thank', 'the', 'then', 'there', 'their', 'they'];
|
||||
const words = lowerInput.split(/\s+/);
|
||||
const hasFalsePositives = words.some(word => falsePositiveWords.some(fp => word.includes(fp)));
|
||||
|
||||
// If we have false positive words and low pattern count, it's probably English
|
||||
if (hasFalsePositives && expansionCount < 3) return false;
|
||||
|
||||
// Require sufficient pattern density to indicate Dovahzul encoding
|
||||
// For short text: need at least 1 pattern with density > 0.3
|
||||
// For longer text: need at least 2 patterns with density > 0.2
|
||||
const minPatterns = textLength < 30 ? 1 : 2;
|
||||
const minDensity = textLength < 30 ? 0.3 : 0.2;
|
||||
|
||||
return expansionCount >= minPatterns && patternDensity >= minDensity;
|
||||
},
|
||||
|
||||
map: {
|
||||
|
||||
@@ -50,9 +50,72 @@ export default new BaseTransformer({
|
||||
detector: function(text) {
|
||||
// Klingon has characteristic patterns like 'ch', 'gh', 'Q' (capital Q for q sound)
|
||||
// Also uses capital letters in specific ways (D, H, I, Q, S)
|
||||
const patterns = text.match(/ch|gh|CH|GH/g);
|
||||
const capitalPattern = /[DHIQS]/.test(text) && /[a-z]/.test(text); // Mix of specific capitals with lowercase
|
||||
return (patterns && patterns.length >= 1) || capitalPattern;
|
||||
const patterns = text.match(/ch|gh|CH|GH/gi);
|
||||
const patternCount = patterns ? patterns.length : 0;
|
||||
|
||||
// Check for Klingon-specific capital letter usage
|
||||
// Klingon uses capitals D, H, I, Q, S in specific contexts
|
||||
// But we need to avoid false positives from regular English
|
||||
const klingonCapitals = text.match(/[DHIQS]/g);
|
||||
const lowercaseLetters = text.match(/[a-z]/g);
|
||||
const hasQ = /Q/.test(text);
|
||||
|
||||
// Strong indicators: 'ch' or 'gh' patterns
|
||||
if (patternCount >= 1) {
|
||||
const lowerText = text.toLowerCase();
|
||||
const commonEnglishPatterns = /(which|much|such|each|teach|reach|beach|church|chance|change|charm|chart|chase|cheap|check|cheek|cheer|cheese|chest|chick|chief|child|chill|china|chips|choke|choose|chop|chord|chore|chose|chuck|chunk|churn)/;
|
||||
const isCommonEnglish = commonEnglishPatterns.test(lowerText);
|
||||
|
||||
// If we have multiple patterns, check if they're all in common English words
|
||||
if (patternCount >= 2) {
|
||||
// If all patterns are in common English words, it's probably not Klingon
|
||||
// Count how many patterns are NOT in common English contexts
|
||||
const nonEnglishPatterns = patterns.filter(p => {
|
||||
const patternLower = p.toLowerCase();
|
||||
// Check if this pattern appears outside common English words
|
||||
const patternIndex = lowerText.indexOf(patternLower);
|
||||
if (patternIndex === -1) return false;
|
||||
// Extract surrounding context
|
||||
const start = Math.max(0, patternIndex - 5);
|
||||
const end = Math.min(lowerText.length, patternIndex + patternLower.length + 5);
|
||||
const context = lowerText.substring(start, end);
|
||||
return !commonEnglishPatterns.test(context);
|
||||
});
|
||||
// If we have patterns outside common English words, it's likely Klingon
|
||||
if (nonEnglishPatterns.length > 0) return true;
|
||||
// Even if all patterns are in common words, Q or multiple capitals suggest Klingon
|
||||
if (hasQ || (klingonCapitals && klingonCapitals.length >= 2)) return true;
|
||||
// Otherwise, it's probably just English
|
||||
return false;
|
||||
}
|
||||
|
||||
// Single pattern
|
||||
// Single pattern with Q (strong Klingon indicator)
|
||||
if (hasQ) return true;
|
||||
// Single pattern with multiple Klingon capitals (indicates encoding)
|
||||
if (klingonCapitals && klingonCapitals.length >= 2) return true;
|
||||
// Single pattern is acceptable - 'ch' and 'gh' are less common in English
|
||||
// But avoid if it's clearly English (e.g., "ch" in "which", "much")
|
||||
if (!isCommonEnglish) return true;
|
||||
// Even if it's common English, if we have Q or multiple capitals, it might be Klingon
|
||||
if (hasQ || (klingonCapitals && klingonCapitals.length >= 2)) return true;
|
||||
}
|
||||
|
||||
// Capital pattern: need multiple Klingon capitals mixed with lowercase
|
||||
// This indicates Klingon encoding, not just English with one capital letter
|
||||
if (klingonCapitals && lowercaseLetters && klingonCapitals.length >= 2) {
|
||||
// Check if capitals appear in middle/end of words (Klingon style)
|
||||
// This is different from English where capitals are usually at word start
|
||||
const midWordCapitals = text.match(/[a-z][DHIQS][a-z]/g);
|
||||
if (midWordCapitals && midWordCapitals.length >= 1) return true;
|
||||
// Or if Q is present (strong indicator)
|
||||
if (hasQ) return true;
|
||||
}
|
||||
|
||||
// Single Q with lowercase is a strong indicator
|
||||
if (hasQ && lowercaseLetters && lowercaseLetters.length >= 2) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
// bitwise NOT transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Bitwise NOT',
|
||||
priority: 100,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
// Invert all bits in each byte
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
const result = new Uint8Array(bytes.length);
|
||||
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
result[i] = ~bytes[i] & 0xFF; // NOT operation, mask to 8 bits
|
||||
}
|
||||
|
||||
try {
|
||||
return new TextDecoder().decode(result);
|
||||
} catch (e) {
|
||||
// If decoding fails, return as hex
|
||||
return Array.from(result).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Bitwise NOT is self-reciprocal (NOT NOT = original)
|
||||
return this.func(text);
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[bitwise-not]';
|
||||
return this.func(text.slice(0, 5));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Bitwise NOT produces scrambled text, hard to detect
|
||||
// Check for non-printable characters or unusual patterns
|
||||
const hasNonPrintable = /[\x00-\x1F\x7F-\x9F]/.test(text);
|
||||
return hasNonPrintable && text.length >= 5;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// boustrophedon writing transform (alternating direction)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Boustrophedon',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
const lines = text.split(/\r?\n/);
|
||||
return lines.map((line, index) => {
|
||||
// Alternate direction: even lines left-to-right, odd lines right-to-left
|
||||
if (index % 2 === 0) {
|
||||
return line;
|
||||
} else {
|
||||
return line.split('').reverse().join('');
|
||||
}
|
||||
}).join('\n');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Same function - boustrophedon is self-reciprocal
|
||||
return this.func(text);
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[boustrophedon]';
|
||||
const lines = text.split(/\r?\n/);
|
||||
if (lines.length === 0) return '';
|
||||
return this.func(lines.slice(0, 2).join('\n'));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Hard to detect - would need line analysis
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// capitalize words transform (first letter of each word uppercase)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Capitalize Words',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.replace(/\b\w/g, c => c.toUpperCase());
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - original case is lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[Capitalized]';
|
||||
return this.func(text.slice(0, 15));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Check if words start with uppercase (Title Case pattern)
|
||||
const words = text.split(/\s+/).filter(w => /[a-zA-Z]/.test(w));
|
||||
if (words.length < 2) return false;
|
||||
return words.every(w => /^[A-Z]/.test(w) || !/[a-zA-Z]/.test(w));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// indent transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Indent',
|
||||
priority: 100,
|
||||
category: 'format',
|
||||
spaces: 4, // Default indent spaces
|
||||
func: function(text) {
|
||||
const spaces = parseInt(this.spaces) || 4;
|
||||
const indent = ' '.repeat(spaces);
|
||||
|
||||
return text.split('\n').map(line => indent + line).join('\n');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Remove leading spaces from each line
|
||||
return text.split('\n').map(line => line.replace(/^\s+/, '')).join('\n');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[indent]';
|
||||
return this.func(text.slice(0, 20));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check if all lines start with same amount of whitespace
|
||||
const lines = text.split('\n').filter(l => l.trim());
|
||||
if (lines.length < 2) return false;
|
||||
|
||||
const leadingSpaces = lines.map(line => line.match(/^\s*/)[0].length);
|
||||
const allSame = leadingSpaces.every(count => count === leadingSpaces[0]);
|
||||
|
||||
return allSame && leadingSpaces[0] > 0;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// javanais transform (French slang insertion)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Javanais',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
// Insert "av" before each vowel (a, e, i, o, u, y) that follows a consonant
|
||||
const vowels = /[aeiouyAEIOUY]/;
|
||||
const consonants = /[bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ]/;
|
||||
|
||||
let result = '';
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text[i];
|
||||
const prevChar = i > 0 ? text[i - 1] : '';
|
||||
|
||||
if (vowels.test(char) && (i === 0 || consonants.test(prevChar))) {
|
||||
result += 'av' + char;
|
||||
} else {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Remove "av" before vowels that follow consonants
|
||||
return text.replace(/av([aeiouyAEIOUY])/g, '$1');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[javanais]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for "av" pattern before vowels
|
||||
return /av[aeiouyAEIOUY]/i.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// latin gibberish transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Latin Gibberish',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
// Insert "us" or "um" after consonants before vowels (simplified Latin-sounding)
|
||||
const vowels = /[aeiouyAEIOUY]/;
|
||||
const consonants = /[bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ]/;
|
||||
|
||||
let result = '';
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text[i];
|
||||
const nextChar = i + 1 < text.length ? text[i + 1] : '';
|
||||
|
||||
if (consonants.test(char) && vowels.test(nextChar)) {
|
||||
// Insert "us" after consonant before vowel
|
||||
result += char + 'us';
|
||||
} else {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Remove "us" after consonants
|
||||
return text.replace(/([bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ])us([aeiouyAEIOUY])/gi, '$1$2');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[latin]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for "us" pattern after consonants
|
||||
return /[bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ]us[aeiouyAEIOUY]/i.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// letters extraction transform (extract only letters)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Letters Only',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.replace(/[^a-zA-Z]/g, '');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - non-letters are lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[letters]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// letters and numbers only transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Letters & Numbers Only',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.replace(/[^a-zA-Z0-9]/g, '');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - other characters are lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[alphanum]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Check if text is only alphanumeric
|
||||
return /^[a-zA-Z0-9]+$/.test(text.trim()) && text.length >= 5;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// line numbering transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Line Numbers',
|
||||
priority: 100,
|
||||
category: 'format',
|
||||
start: 1, // Starting line number
|
||||
func: function(text) {
|
||||
const start = parseInt(this.start) || 1;
|
||||
const lines = text.split('\n');
|
||||
let result = '';
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const lineNum = start + i;
|
||||
result += lineNum.toString().padStart(4, ' ') + ': ' + lines[i] + '\n';
|
||||
}
|
||||
|
||||
return result.trimEnd();
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Remove line numbers (format: " 1: text" or "1: text")
|
||||
return text.split('\n').map(line => {
|
||||
return line.replace(/^\s*\d+\s*:\s*/, '');
|
||||
}).join('\n');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[line-numbers]';
|
||||
return this.func(text.slice(0, 30));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for line number pattern at start of lines
|
||||
const lines = text.split('\n');
|
||||
if (lines.length < 2) return false;
|
||||
|
||||
const hasLineNumbers = lines.filter(line => /^\s*\d+\s*:/.test(line)).length;
|
||||
return hasLineNumbers / lines.length > 0.7;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// louchebem transform (French slang)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Louchebem',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
// Move first consonant(s) to end and add "l" + "em" (or "oc", "ic", "uche", etc.)
|
||||
const words = text.split(/(\s+|[.,!?;:])/);
|
||||
|
||||
return words.map(word => {
|
||||
if (!/^[a-zA-Z]+$/.test(word)) return word;
|
||||
|
||||
const match = word.match(/^([bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ]+)([aeiouyAEIOUY].*)$/);
|
||||
if (match) {
|
||||
const [, consonants, rest] = match;
|
||||
return 'l' + rest + consonants + 'em';
|
||||
}
|
||||
return word;
|
||||
}).join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Reverse louchebem: remove "l" prefix and "em" suffix, move consonants back
|
||||
const words = text.split(/(\s+|[.,!?;:])/);
|
||||
|
||||
return words.map(word => {
|
||||
if (!/^l[a-zA-Z]+em$/i.test(word)) return word;
|
||||
|
||||
const body = word.slice(1, -2); // Remove 'l' and 'em'
|
||||
const match = body.match(/^([aeiouyAEIOUY].*?)([bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ]+)$/);
|
||||
if (match) {
|
||||
const [, rest, consonants] = match;
|
||||
return consonants + rest;
|
||||
}
|
||||
return word;
|
||||
}).join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[louchebem]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for "l" prefix and "em" suffix pattern
|
||||
return /\bl[a-z]+em\b/i.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// lowercase all transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Lowercase All',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.toLowerCase();
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - original case is lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[lowercase]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Check if all letters are lowercase
|
||||
const letters = text.replace(/[^a-zA-Z]/g, '');
|
||||
return letters.length > 0 && letters === letters.toLowerCase() && /[A-Z]/.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// mirror digits transform (mirror only numbers)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Mirror Digits',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.replace(/\d+/g, match => {
|
||||
return match.split('').reverse().join('');
|
||||
});
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Mirror digits is its own inverse
|
||||
return this.func(text);
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[mirror-digits]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check if text has numbers that might be mirrored
|
||||
return /\d/.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// numbers only transform (extract only numbers)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Numbers Only',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.replace(/[^0-9]/g, '');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - non-numbers are lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[numbers]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// If text is only digits, might be extracted
|
||||
return /^\d+$/.test(text.trim()) && text.length >= 3;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// remove accents/diacritics transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Remove Accents',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
map: {
|
||||
'à': 'a', 'á': 'a', 'â': 'a', 'ã': 'a', 'ä': 'a', 'å': 'a',
|
||||
'è': 'e', 'é': 'e', 'ê': 'e', 'ë': 'e',
|
||||
'ì': 'i', 'í': 'i', 'î': 'i', 'ï': 'i',
|
||||
'ò': 'o', 'ó': 'o', 'ô': 'o', 'õ': 'o', 'ö': 'o',
|
||||
'ù': 'u', 'ú': 'u', 'û': 'u', 'ü': 'u',
|
||||
'ý': 'y', 'ÿ': 'y',
|
||||
'À': 'A', 'Á': 'A', 'Â': 'A', 'Ã': 'A', 'Ä': 'A', 'Å': 'A',
|
||||
'È': 'E', 'É': 'E', 'Ê': 'E', 'Ë': 'E',
|
||||
'Ì': 'I', 'Í': 'I', 'Î': 'I', 'Ï': 'I',
|
||||
'Ò': 'O', 'Ó': 'O', 'Ô': 'O', 'Õ': 'O', 'Ö': 'O',
|
||||
'Ù': 'U', 'Ú': 'U', 'Û': 'U', 'Ü': 'U',
|
||||
'Ý': 'Y', 'Ÿ': 'Y',
|
||||
'ç': 'c', 'Ç': 'C',
|
||||
'ñ': 'n', 'Ñ': 'N',
|
||||
'ß': 'ss', 'ẞ': 'SS'
|
||||
},
|
||||
func: function(text) {
|
||||
return [...text].map(c => {
|
||||
// Check map first
|
||||
if (this.map[c]) return this.map[c];
|
||||
// Use Unicode normalization to remove combining diacritics
|
||||
return c.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
||||
}).join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - accents are lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[no-accents]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Check if text has no accented characters
|
||||
const normalized = text.normalize('NFD');
|
||||
return !/[\u0300-\u036f]/.test(normalized) && /[àáâãäåèéêëìíîïòóôõöùúûüýÿçñßÀÁÂÃÄÅÈÉÊËÌÍÎÏÒÓÔÕÖÙÚÛÜÝŸÇÑẞ]/.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// remove consonants transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Remove Consonants',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.replace(/[bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ]/g, '');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - consonants are lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[vowels-only]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// If text has only vowels/spaces/punctuation, might have consonants removed
|
||||
const cleaned = text.replace(/[\s.,!?;:'"()\-&0-9]/g, '');
|
||||
return cleaned.length > 0 && !/[bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ]/i.test(cleaned);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// remove duplicate characters transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Remove Duplicates',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
const seen = new Set();
|
||||
return [...text].filter(c => {
|
||||
if (seen.has(c)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(c);
|
||||
return true;
|
||||
}).join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - duplicates are lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[no-dupes]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Check if text has no duplicate characters
|
||||
const chars = [...text];
|
||||
const unique = new Set(chars);
|
||||
return chars.length === unique.size && text.length >= 5;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// remove extra spaces transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Remove Extra Spaces',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.replace(/[ \t]+/g, ' ').trim();
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - original spacing is lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[no-extra-spaces]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Check if text has multiple consecutive spaces
|
||||
return / +/.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// remove html tags transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Remove HTML Tags',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.replace(/<[^>]*>/g, '');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - HTML tags are lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[no-html]';
|
||||
return this.func(text.slice(0, 15));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Check if text contains HTML tags
|
||||
return /<[^>]+>/.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// remove newlines transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Remove Newlines',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.replace(/[\r\n]+/g, ' ');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - newline positions are lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[no-newlines]';
|
||||
return this.func(text.slice(0, 20));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Check if text should have newlines (has long lines)
|
||||
return !/[\r\n]/.test(text) && text.length > 50 && text.split(/\s+/).some(w => w.length > 20);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// remove numbers transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Remove Numbers',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.replace(/[0-9]/g, '');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - numbers are lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[no-numbers]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Hard to detect - would need context
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// remove punctuation transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Remove Punctuation',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.replace(/[.,!?;:'"()\-_\[\]{}@#$%^&*+=|\\\/<>~`]/g, '');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - punctuation is lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[no-punct]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Hard to detect - would need to check if text should have punctuation
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// remove tabs transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Remove Tabs',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.replace(/\t/g, ' ');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - tab positions are lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[no-tabs]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Hard to detect
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// remove zero width characters transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Remove Zero Width',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
// Remove zero-width spaces, joiners, non-joiners, and other invisible characters
|
||||
return text.replace(/[\u200B-\u200D\uFEFF\u2060]/g, '');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - zero-width characters are lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[no-zw]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Check if text contains zero-width characters
|
||||
return /[\u200B-\u200D\uFEFF\u2060]/.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// shuffle characters transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Shuffle Characters',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
// Fisher-Yates shuffle
|
||||
const chars = [...text];
|
||||
for (let i = chars.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[chars[i], chars[j]] = [chars[j], chars[i]];
|
||||
}
|
||||
return chars.join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - order is randomized
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[shuffled]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Cannot detect - random order
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// shuffle words transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Shuffle Words',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
// Split by whitespace, shuffle, rejoin
|
||||
const words = text.split(/(\s+)/);
|
||||
const wordOnly = words.filter((_, i) => i % 2 === 0);
|
||||
const spaces = words.filter((_, i) => i % 2 === 1);
|
||||
|
||||
// Fisher-Yates shuffle words
|
||||
for (let i = wordOnly.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[wordOnly[i], wordOnly[j]] = [wordOnly[j], wordOnly[i]];
|
||||
}
|
||||
|
||||
// Recombine
|
||||
let result = '';
|
||||
for (let i = 0; i < wordOnly.length; i++) {
|
||||
result += wordOnly[i];
|
||||
if (i < spaces.length) result += spaces[i];
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - order is randomized
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[shuffled-words]';
|
||||
return this.func(text.slice(0, 20));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Cannot detect - random order
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// spaces remover transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Spaces Remover',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.replace(/\s+/g, '');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - spaces are lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[no-spaces]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false
|
||||
});
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// text justification transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Text Justify',
|
||||
priority: 100,
|
||||
category: 'format',
|
||||
width: 80, // Default width
|
||||
align: 'left', // left, right, center
|
||||
func: function(text) {
|
||||
const width = parseInt(this.width) || 80;
|
||||
const align = this.align || 'left';
|
||||
|
||||
const lines = text.split('\n');
|
||||
let result = '';
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) {
|
||||
result += '\n';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trimmed.length >= width) {
|
||||
result += trimmed + '\n';
|
||||
continue;
|
||||
}
|
||||
|
||||
let justified = '';
|
||||
if (align === 'left') {
|
||||
justified = trimmed.padEnd(width);
|
||||
} else if (align === 'right') {
|
||||
justified = trimmed.padStart(width);
|
||||
} else if (align === 'center') {
|
||||
const padding = Math.floor((width - trimmed.length) / 2);
|
||||
justified = ' '.repeat(padding) + trimmed + ' '.repeat(width - trimmed.length - padding);
|
||||
} else {
|
||||
justified = trimmed;
|
||||
}
|
||||
|
||||
result += justified + '\n';
|
||||
}
|
||||
|
||||
return result.trimEnd();
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Remove padding spaces
|
||||
return text.split('\n').map(line => line.trim()).join('\n');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[text-justify]';
|
||||
return this.func(text.slice(0, 20));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for consistent line lengths with padding
|
||||
const lines = text.split('\n');
|
||||
if (lines.length < 2) return false;
|
||||
|
||||
const lengths = lines.map(l => l.length);
|
||||
const allSameLength = lengths.every(len => len === lengths[0]);
|
||||
const hasLeadingTrailingSpaces = lines.some(line => /^\s+|\s+$/.test(line));
|
||||
|
||||
return allSameLength && hasLeadingTrailingSpaces && lengths[0] > 40;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// uppercase all transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Uppercase All',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.toUpperCase();
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - original case is lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[UPPERCASE]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Check if all letters are uppercase
|
||||
const letters = text.replace(/[^a-zA-Z]/g, '');
|
||||
return letters.length > 0 && letters === letters.toUpperCase() && /[a-z]/.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// uppercase lowercase toggle transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Toggle Case',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return [...text].map(c => {
|
||||
if (c >= 'A' && c <= 'Z') {
|
||||
return c.toLowerCase();
|
||||
} else if (c >= 'a' && c <= 'z') {
|
||||
return c.toUpperCase();
|
||||
}
|
||||
return c;
|
||||
}).join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Toggle case is its own inverse
|
||||
return this.func(text);
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[toggle]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Hard to detect - would need pattern analysis
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// whitespace steganography transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Whitespace Steganography',
|
||||
priority: 100,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
// Encode text in whitespace patterns (space = 0, tab = 1)
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
let result = '';
|
||||
|
||||
for (const byte of bytes) {
|
||||
// Encode each byte as 8 characters (space or tab)
|
||||
for (let i = 7; i >= 0; i--) {
|
||||
const bit = (byte >> i) & 1;
|
||||
result += bit === 1 ? '\t' : ' ';
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Decode whitespace patterns back to text
|
||||
const bytes = [];
|
||||
let bitBuffer = '';
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text[i];
|
||||
if (char === ' ' || char === '\t') {
|
||||
bitBuffer += char === '\t' ? '1' : '0';
|
||||
|
||||
// Every 8 bits, convert to a byte
|
||||
if (bitBuffer.length === 8) {
|
||||
bytes.push(parseInt(bitBuffer, 2));
|
||||
bitBuffer = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return new TextDecoder().decode(new Uint8Array(bytes));
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[whitespace-stego]';
|
||||
return this.func(text.slice(0, 1)) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check if text is mostly spaces and tabs in groups of 8
|
||||
const cleaned = text.replace(/[^\s\t]/g, '');
|
||||
if (cleaned.length < 8) return false;
|
||||
// Should be mostly whitespace
|
||||
return cleaned.length / text.length > 0.8;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// word wrap transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Word Wrap',
|
||||
priority: 100,
|
||||
category: 'format',
|
||||
width: 80, // Default wrap width
|
||||
func: function(text) {
|
||||
const width = parseInt(this.width) || 80;
|
||||
if (width < 1) return text;
|
||||
|
||||
const lines = text.split('\n');
|
||||
let result = '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.length <= width) {
|
||||
result += line + '\n';
|
||||
continue;
|
||||
}
|
||||
|
||||
// Word wrap: break at word boundaries
|
||||
let currentLine = '';
|
||||
const words = line.split(/(\s+)/);
|
||||
|
||||
for (const word of words) {
|
||||
if ((currentLine + word).length <= width) {
|
||||
currentLine += word;
|
||||
} else {
|
||||
if (currentLine) {
|
||||
result += currentLine.trim() + '\n';
|
||||
}
|
||||
currentLine = word;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentLine) {
|
||||
result += currentLine.trim() + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
return result.trimEnd();
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Remove line breaks (simple approach - may not be perfect)
|
||||
return text.replace(/\n/g, ' ');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[word-wrap]';
|
||||
return this.func(text.slice(0, 50));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check if text has consistent line lengths
|
||||
const lines = text.split('\n');
|
||||
if (lines.length < 2) return false;
|
||||
|
||||
const lengths = lines.map(l => l.length);
|
||||
const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
|
||||
const variance = lengths.reduce((sum, len) => sum + Math.pow(len - avgLength, 2), 0) / lengths.length;
|
||||
|
||||
// Low variance suggests word wrapping
|
||||
return variance < 100 && avgLength > 40 && avgLength < 120;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// zero-width steganography transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Zero-Width Steganography',
|
||||
priority: 100,
|
||||
category: 'format',
|
||||
// Zero-width characters: ZWSP (U+200B), ZWNJ (U+200C), ZWJ (U+200D), ZWNBSP (U+FEFF)
|
||||
zeroWidthChars: ['\u200B', '\u200C', '\u200D', '\uFEFF'],
|
||||
func: function(text) {
|
||||
// Encode text using zero-width characters
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
let result = '';
|
||||
|
||||
for (const byte of bytes) {
|
||||
// Encode each byte as 4 zero-width characters (2 bits per char)
|
||||
for (let i = 6; i >= 0; i -= 2) {
|
||||
const bits = (byte >> i) & 3; // Get 2 bits
|
||||
result += this.zeroWidthChars[bits];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Decode zero-width characters back to text
|
||||
const bytes = [];
|
||||
let bitBuffer = [];
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text[i];
|
||||
const index = this.zeroWidthChars.indexOf(char);
|
||||
if (index >= 0) {
|
||||
bitBuffer.push(index);
|
||||
|
||||
// Every 4 characters, convert to a byte
|
||||
if (bitBuffer.length === 4) {
|
||||
const byte = (bitBuffer[0] << 6) | (bitBuffer[1] << 4) | (bitBuffer[2] << 2) | bitBuffer[3];
|
||||
bytes.push(byte);
|
||||
bitBuffer = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return new TextDecoder().decode(new Uint8Array(bytes));
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[zerowidth-stego]';
|
||||
return '[zero-width encoded]';
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for zero-width characters
|
||||
return /[\u200B\u200C\u200D\uFEFF]/.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// ICAO spelling alphabet transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'ICAO Spelling Alphabet',
|
||||
priority: 200,
|
||||
category: 'technical',
|
||||
alphabet: {
|
||||
'A': 'ALFA', 'B': 'BRAVO', 'C': 'CHARLIE', 'D': 'DELTA', 'E': 'ECHO',
|
||||
'F': 'FOXTROT', 'G': 'GOLF', 'H': 'HOTEL', 'I': 'INDIA', 'J': 'JULIETT',
|
||||
'K': 'KILO', 'L': 'LIMA', 'M': 'MIKE', 'N': 'NOVEMBER', 'O': 'OSCAR',
|
||||
'P': 'PAPA', 'Q': 'QUEBEC', 'R': 'ROMEO', 'S': 'SIERRA', 'T': 'TANGO',
|
||||
'U': 'UNIFORM', 'V': 'VICTOR', 'W': 'WHISKEY', 'X': 'XRAY', 'Y': 'YANKEE',
|
||||
'Z': 'ZULU'
|
||||
},
|
||||
func: function(text) {
|
||||
const cleaned = text.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (cleaned.length === 0) return text;
|
||||
|
||||
let result = '';
|
||||
for (const char of cleaned) {
|
||||
if (this.alphabet[char]) {
|
||||
result += this.alphabet[char] + ' ';
|
||||
} else {
|
||||
result += char + ' ';
|
||||
}
|
||||
}
|
||||
return result.trim();
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Create reverse map
|
||||
const reverseMap = {};
|
||||
for (const [letter, word] of Object.entries(this.alphabet)) {
|
||||
reverseMap[word.toUpperCase()] = letter;
|
||||
}
|
||||
|
||||
// Split by spaces and convert back
|
||||
const words = text.toUpperCase().split(/\s+/);
|
||||
let result = '';
|
||||
for (const word of words) {
|
||||
if (reverseMap[word]) {
|
||||
result += reverseMap[word];
|
||||
} else if (word.length === 1 && /[A-Z]/.test(word)) {
|
||||
result += word;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[icao]';
|
||||
return this.func(text.slice(0, 3));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for ICAO words
|
||||
const icaoWords = ['ALFA', 'BRAVO', 'CHARLIE', 'DELTA', 'ECHO', 'FOXTROT', 'GOLF', 'HOTEL', 'INDIA', 'JULIETT', 'KILO', 'LIMA', 'MIKE', 'NOVEMBER', 'OSCAR', 'PAPA', 'QUEBEC', 'ROMEO', 'SIERRA', 'TANGO', 'UNIFORM', 'VICTOR', 'WHISKEY', 'XRAY', 'YANKEE', 'ZULU'];
|
||||
const upper = text.toUpperCase();
|
||||
const matches = icaoWords.filter(word => upper.includes(word));
|
||||
return matches.length >= 2;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// ITU spelling alphabet transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'ITU Spelling Alphabet',
|
||||
priority: 200,
|
||||
category: 'technical',
|
||||
alphabet: {
|
||||
'A': 'AMSTERDAM', 'B': 'BALTIMORE', 'C': 'CASABLANCA', 'D': 'DANMARK', 'E': 'EDISON',
|
||||
'F': 'FLORIDA', 'G': 'GALLIPOLI', 'H': 'HAVANA', 'I': 'ITALIA', 'J': 'JERUSALEM',
|
||||
'K': 'KILOGRAMME', 'L': 'LIVERPOOL', 'M': 'MADRID', 'N': 'NEAPOLIS', 'O': 'OSLO',
|
||||
'P': 'PARIS', 'Q': 'QUEBEC', 'R': 'ROMA', 'S': 'SANTIAGO', 'T': 'TRIPOLI',
|
||||
'U': 'UPPSALA', 'V': 'VALENCIA', 'W': 'WASHINGTON', 'X': 'XANTIPPE', 'Y': 'YOKOHAMA',
|
||||
'Z': 'ZURICH'
|
||||
},
|
||||
func: function(text) {
|
||||
const cleaned = text.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (cleaned.length === 0) return text;
|
||||
|
||||
let result = '';
|
||||
for (const char of cleaned) {
|
||||
if (this.alphabet[char]) {
|
||||
result += this.alphabet[char] + ' ';
|
||||
} else {
|
||||
result += char + ' ';
|
||||
}
|
||||
}
|
||||
return result.trim();
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Create reverse map
|
||||
const reverseMap = {};
|
||||
for (const [letter, word] of Object.entries(this.alphabet)) {
|
||||
reverseMap[word.toUpperCase()] = letter;
|
||||
}
|
||||
|
||||
// Split by spaces and convert back
|
||||
const words = text.toUpperCase().split(/\s+/);
|
||||
let result = '';
|
||||
for (const word of words) {
|
||||
if (reverseMap[word]) {
|
||||
result += reverseMap[word];
|
||||
} else if (word.length === 1 && /[A-Z]/.test(word)) {
|
||||
result += word;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[itu]';
|
||||
return this.func(text.slice(0, 3));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for ITU words
|
||||
const ituWords = ['AMSTERDAM', 'BALTIMORE', 'CASABLANCA', 'DANMARK', 'EDISON', 'FLORIDA', 'GALLIPOLI', 'HAVANA', 'ITALIA', 'JERUSALEM', 'KILOGRAMME', 'LIVERPOOL', 'MADRID', 'NEAPOLIS', 'OSLO', 'PARIS', 'QUEBEC', 'ROMA', 'SANTIAGO', 'TRIPOLI', 'UPPSALA', 'VALENCIA', 'WASHINGTON', 'XANTIPPE', 'YOKOHAMA', 'ZURICH'];
|
||||
const upper = text.toUpperCase();
|
||||
const matches = ituWords.filter(word => upper.includes(word));
|
||||
return matches.length >= 2;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// maritime signal flags transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Maritime Signal Flags',
|
||||
priority: 200,
|
||||
category: 'technical',
|
||||
// International maritime signal flags (NATO phonetic with flag emojis)
|
||||
flags: {
|
||||
'A': '🚩', 'B': '🚩', 'C': '🚩', 'D': '🚩', 'E': '🚩',
|
||||
'F': '🚩', 'G': '🚩', 'H': '🚩', 'I': '🚩', 'J': '🚩',
|
||||
'K': '🚩', 'L': '🚩', 'M': '🚩', 'N': '🚩', 'O': '🚩',
|
||||
'P': '🚩', 'Q': '🚩', 'R': '🚩', 'S': '🚩', 'T': '🚩',
|
||||
'U': '🚩', 'V': '🚩', 'W': '🚩', 'X': '🚩', 'Y': '🚩',
|
||||
'Z': '🚩'
|
||||
},
|
||||
// Using flag emojis - actual maritime flags would need proper Unicode
|
||||
// For now, using regional indicator symbols which represent flags
|
||||
flagMap: {
|
||||
'A': '🇦', 'B': '🇧', 'C': '🇨', 'D': '🇩', 'E': '🇪',
|
||||
'F': '🇫', 'G': '🇬', 'H': '🇭', 'I': '🇮', 'J': '🇯',
|
||||
'K': '🇰', 'L': '🇱', 'M': '🇲', 'N': '🇳', 'O': '🇴',
|
||||
'P': '🇵', 'Q': '🇶', 'R': '🇷', 'S': '🇸', 'T': '🇹',
|
||||
'U': '🇺', 'V': '🇻', 'W': '🇼', 'X': '🇽', 'Y': '🇾',
|
||||
'Z': '🇿'
|
||||
},
|
||||
func: function(text) {
|
||||
const cleaned = text.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (cleaned.length === 0) return text;
|
||||
|
||||
let result = '';
|
||||
for (const char of cleaned) {
|
||||
if (this.flagMap[char]) {
|
||||
result += this.flagMap[char] + ' ';
|
||||
} else {
|
||||
result += char + ' ';
|
||||
}
|
||||
}
|
||||
return result.trim();
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Reverse map from flag emoji to letter
|
||||
const reverseMap = {};
|
||||
for (const [letter, flag] of Object.entries(this.flagMap)) {
|
||||
reverseMap[flag] = letter;
|
||||
}
|
||||
|
||||
let result = '';
|
||||
// Match flag emojis (regional indicators - match each one individually)
|
||||
const flagChars = Object.values(this.flagMap);
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
// Check for 2-char regional indicator sequences
|
||||
if (i + 1 < text.length) {
|
||||
const pair = text.substring(i, i + 2);
|
||||
if (reverseMap[pair]) {
|
||||
result += reverseMap[pair];
|
||||
i++; // Skip next char
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Check single char
|
||||
const char = text[i];
|
||||
if (reverseMap[char]) {
|
||||
result += reverseMap[char];
|
||||
} else if (/[A-Z]/.test(char)) {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[maritime-flags]';
|
||||
return this.func(text.slice(0, 3));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for regional indicator flag emojis (check for any flag in the map)
|
||||
const flagChars = Object.values(this.flagMap);
|
||||
return flagChars.some(flag => text.includes(flag));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// bold italic text transform (Mathematical Bold Italic)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Bold Italic',
|
||||
priority: 85,
|
||||
category: 'unicode',
|
||||
map: {
|
||||
'A': '𝑨', 'B': '𝑩', 'C': '𝑪', 'D': '𝑫', 'E': '𝑬', 'F': '𝑭', 'G': '𝑮', 'H': '𝑯',
|
||||
'I': '𝑰', 'J': '𝑱', 'K': '𝑲', 'L': '𝑳', 'M': '𝑴', 'N': '𝑵', 'O': '𝑶', 'P': '𝑷',
|
||||
'Q': '𝑸', 'R': '𝑹', 'S': '𝑺', 'T': '𝑻', 'U': '𝑼', 'V': '𝑽', 'W': '𝑾', 'X': '𝑿',
|
||||
'Y': '𝒀', 'Z': '𝒁',
|
||||
'a': '𝒂', 'b': '𝒃', 'c': '𝒄', 'd': '𝒅', 'e': '𝒆', 'f': '𝒇', 'g': '𝒈', 'h': '𝒉',
|
||||
'i': '𝒊', 'j': '𝒋', 'k': '𝒌', 'l': '𝒍', 'm': '𝒎', 'n': '𝒏', 'o': '𝒐', 'p': '𝒑',
|
||||
'q': '𝒒', 'r': '𝒓', 's': '𝒔', 't': '𝒕', 'u': '𝒖', 'v': '𝒗', 'w': '𝒘', 'x': '𝒙',
|
||||
'y': '𝒚', 'z': '𝒛'
|
||||
},
|
||||
func: function(text) {
|
||||
return [...text].map(c => this.map[c] || c).join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[bold-italic]';
|
||||
return this.func(text.slice(0, 8));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for Mathematical Bold Italic characters (U+1D468-U+1D49C)
|
||||
return /[\u{1D468}-\u{1D49C}]/u.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// bold text transform (Mathematical Bold)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Bold',
|
||||
priority: 85,
|
||||
category: 'unicode',
|
||||
map: {
|
||||
'A': '𝐀', 'B': '𝐁', 'C': '𝐂', 'D': '𝐃', 'E': '𝐄', 'F': '𝐅', 'G': '𝐆', 'H': '𝐇',
|
||||
'I': '𝐈', 'J': '𝐉', 'K': '𝐊', 'L': '𝐋', 'M': '𝐌', 'N': '𝐍', 'O': '𝐎', 'P': '𝐏',
|
||||
'Q': '𝐐', 'R': '𝐑', 'S': '𝐒', 'T': '𝐓', 'U': '𝐔', 'V': '𝐕', 'W': '𝐖', 'X': '𝐗',
|
||||
'Y': '𝐘', 'Z': '𝐙',
|
||||
'a': '𝐚', 'b': '𝐛', 'c': '𝐜', 'd': '𝐝', 'e': '𝐞', 'f': '𝐟', 'g': '𝐠', 'h': '𝐡',
|
||||
'i': '𝐢', 'j': '𝐣', 'k': '𝐤', 'l': '𝐥', 'm': '𝐦', 'n': '𝐧', 'o': '𝐨', 'p': '𝐩',
|
||||
'q': '𝐪', 'r': '𝐫', 's': '𝐬', 't': '𝐭', 'u': '𝐮', 'v': '𝐯', 'w': '𝐰', 'x': '𝐱',
|
||||
'y': '𝐲', 'z': '𝐳',
|
||||
'0': '𝟎', '1': '𝟏', '2': '𝟐', '3': '𝟑', '4': '𝟒', '5': '𝟓', '6': '𝟔', '7': '𝟕',
|
||||
'8': '𝟖', '9': '𝟗'
|
||||
},
|
||||
func: function(text) {
|
||||
return [...text].map(c => this.map[c] || c).join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[bold]';
|
||||
return this.func(text.slice(0, 8));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for Mathematical Bold characters (U+1D400-U+1D7FF)
|
||||
return /[\u{1D400}-\u{1D7FF}]/u.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// circled unicode transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Circled',
|
||||
priority: 150,
|
||||
category: 'unicode',
|
||||
map: {
|
||||
'A': 'Ⓐ', 'B': 'Ⓑ', 'C': 'Ⓒ', 'D': 'Ⓓ', 'E': 'Ⓔ',
|
||||
'F': 'Ⓕ', 'G': 'Ⓖ', 'H': 'Ⓗ', 'I': 'Ⓘ', 'J': 'Ⓙ',
|
||||
'K': 'Ⓚ', 'L': 'Ⓛ', 'M': 'Ⓜ', 'N': 'Ⓝ', 'O': 'Ⓞ',
|
||||
'P': 'Ⓟ', 'Q': 'Ⓠ', 'R': 'Ⓡ', 'S': 'Ⓢ', 'T': 'Ⓣ',
|
||||
'U': 'Ⓤ', 'V': 'Ⓥ', 'W': 'Ⓦ', 'X': 'Ⓧ', 'Y': 'Ⓨ',
|
||||
'Z': 'Ⓩ',
|
||||
'0': '⓪', '1': '①', '2': '②', '3': '③', '4': '④',
|
||||
'5': '⑤', '6': '⑥', '7': '⑦', '8': '⑧', '9': '⑨'
|
||||
},
|
||||
func: function(text) {
|
||||
let result = '';
|
||||
for (const char of text) {
|
||||
const upper = char.toUpperCase();
|
||||
if (this.map[upper]) {
|
||||
result += this.map[upper];
|
||||
} else if (this.map[char]) {
|
||||
result += this.map[char];
|
||||
} else {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const reverseMap = {};
|
||||
for (const [key, value] of Object.entries(this.map)) {
|
||||
reverseMap[value] = key;
|
||||
}
|
||||
|
||||
let result = '';
|
||||
for (const char of text) {
|
||||
if (reverseMap[char]) {
|
||||
result += reverseMap[char];
|
||||
} else {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[circled]';
|
||||
return this.func(text.slice(0, 5));
|
||||
},
|
||||
detector: function(text) {
|
||||
const circledChars = Object.values(this.map);
|
||||
return circledChars.some(char => text.includes(char));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// dashed underline transform (using combining characters)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Dashed Underline',
|
||||
priority: 100,
|
||||
category: 'unicode',
|
||||
func: function(text) {
|
||||
// Add dashed underline combining character (U+0320) after each character
|
||||
return [...text].map(c => c + '\u0320').join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Remove combining dashed below character
|
||||
return text.replace(/\u0320/g, '');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[dashed-underline]';
|
||||
return this.func(text.slice(0, 8));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for dashed below combining character
|
||||
return /\u0320/.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// dotted underline transform (using combining characters)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Dotted Underline',
|
||||
priority: 100,
|
||||
category: 'unicode',
|
||||
func: function(text) {
|
||||
// Add dotted underline combining character (U+0324) after each character
|
||||
return [...text].map(c => c + '\u0324').join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Remove combining dotted below character
|
||||
return text.replace(/\u0324/g, '');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[dotted-underline]';
|
||||
return this.func(text.slice(0, 8));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for dotted below combining character
|
||||
return /\u0324/.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// italic text transform (Mathematical Italic)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Italic',
|
||||
priority: 85,
|
||||
category: 'unicode',
|
||||
map: {
|
||||
'A': '𝐴', 'B': '𝐵', 'C': '𝐶', 'D': '𝐷', 'E': '𝐸', 'F': '𝐹', 'G': '𝐺', 'H': '𝐻',
|
||||
'I': '𝐼', 'J': '𝐽', 'K': '𝐾', 'L': '𝐿', 'M': '𝑀', 'N': '𝑁', 'O': '𝑂', 'P': '𝑃',
|
||||
'Q': '𝑄', 'R': '𝑅', 'S': '𝑆', 'T': '𝑇', 'U': '𝑈', 'V': '𝑉', 'W': '𝑊', 'X': '𝑋',
|
||||
'Y': '𝑌', 'Z': '𝑍',
|
||||
'a': '𝑎', 'b': '𝑏', 'c': '𝑐', 'd': '𝑑', 'e': '𝑒', 'f': '𝑓', 'g': '𝑔', 'h': 'ℎ',
|
||||
'i': '𝑖', 'j': '𝑗', 'k': '𝑘', 'l': '𝑙', 'm': '𝑚', 'n': '𝑛', 'o': '𝑜', 'p': '𝑝',
|
||||
'q': '𝑞', 'r': '𝑟', 's': '𝑠', 't': '𝑡', 'u': '𝑢', 'v': '𝑣', 'w': '𝑤', 'x': '𝑥',
|
||||
'y': '𝑦', 'z': '𝑧'
|
||||
},
|
||||
func: function(text) {
|
||||
return [...text].map(c => this.map[c] || c).join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[italic]';
|
||||
return this.func(text.slice(0, 8));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for Mathematical Italic characters (U+1D434-U+1D468)
|
||||
return /[\u{1D434}-\u{1D468}]/u.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// negative squared unicode transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Negative Squared',
|
||||
priority: 150,
|
||||
category: 'unicode',
|
||||
map: {
|
||||
'A': '🅰', 'B': '🅱', 'C': '🅲', 'D': '🅳', 'E': '🅴',
|
||||
'F': '🅵', 'G': '🅶', 'H': '🅷', 'I': '🅸', 'J': '🅹',
|
||||
'K': '🅺', 'L': '🅻', 'M': '🅼', 'N': '🅽', 'O': '🅾',
|
||||
'P': '🅿', 'Q': '🆀', 'R': '🆁', 'S': '🆂', 'T': '🆃',
|
||||
'U': '🆄', 'V': '🆅', 'W': '🆆', 'X': '🆇', 'Y': '🆈',
|
||||
'Z': '🆉'
|
||||
},
|
||||
func: function(text) {
|
||||
let result = '';
|
||||
for (const char of text) {
|
||||
const upper = char.toUpperCase();
|
||||
if (this.map[upper]) {
|
||||
result += this.map[upper];
|
||||
} else {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const reverseMap = {};
|
||||
for (const [key, value] of Object.entries(this.map)) {
|
||||
reverseMap[value] = key;
|
||||
}
|
||||
|
||||
let result = '';
|
||||
for (const char of text) {
|
||||
if (reverseMap[char]) {
|
||||
result += reverseMap[char];
|
||||
} else {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[negative-squared]';
|
||||
return this.func(text.slice(0, 5));
|
||||
},
|
||||
detector: function(text) {
|
||||
const negSquaredChars = Object.values(this.map);
|
||||
return negSquaredChars.some(char => text.includes(char));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// overline transform (using combining characters)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Overline',
|
||||
priority: 100,
|
||||
category: 'unicode',
|
||||
func: function(text) {
|
||||
// Add overline combining character (U+0305) after each character
|
||||
return [...text].map(c => c + '\u0305').join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Remove combining overline character
|
||||
return text.replace(/\u0305/g, '');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[overline]';
|
||||
return this.func(text.slice(0, 8));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for combining overline character
|
||||
return /\u0305/.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// parenthesized unicode transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Parenthesized',
|
||||
priority: 150,
|
||||
category: 'unicode',
|
||||
map: {
|
||||
'A': '⒜', 'B': '⒝', 'C': '⒞', 'D': '⒟', 'E': '⒠',
|
||||
'F': '⒡', 'G': '⒢', 'H': '⒣', 'I': '⒤', 'J': '⒥',
|
||||
'K': '⒦', 'L': '⒧', 'M': '⒨', 'N': '⒩', 'O': '⒪',
|
||||
'P': '⒫', 'Q': '⒬', 'R': '⒭', 'S': '⒮', 'T': '⒯',
|
||||
'U': '⒰', 'V': '⒱', 'W': '⒲', 'X': '⒳', 'Y': '⒴',
|
||||
'Z': '⒵',
|
||||
'1': '⑴', '2': '⑵', '3': '⑶', '4': '⑷', '5': '⑸',
|
||||
'6': '⑹', '7': '⑺', '8': '⑻', '9': '⑼', '0': '⑽'
|
||||
},
|
||||
func: function(text) {
|
||||
let result = '';
|
||||
for (const char of text) {
|
||||
const upper = char.toUpperCase();
|
||||
if (this.map[upper]) {
|
||||
result += this.map[upper];
|
||||
} else if (this.map[char]) {
|
||||
result += this.map[char];
|
||||
} else {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const reverseMap = {};
|
||||
for (const [key, value] of Object.entries(this.map)) {
|
||||
reverseMap[value] = key;
|
||||
}
|
||||
|
||||
let result = '';
|
||||
for (const char of text) {
|
||||
if (reverseMap[char]) {
|
||||
result += reverseMap[char];
|
||||
} else {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[parenthesized]';
|
||||
return this.func(text.slice(0, 5));
|
||||
},
|
||||
detector: function(text) {
|
||||
const parenthesizedChars = Object.values(this.map);
|
||||
return parenthesizedChars.some(char => text.includes(char));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// squared unicode transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Squared',
|
||||
priority: 150,
|
||||
category: 'unicode',
|
||||
map: {
|
||||
'A': '🄰', 'B': '🄱', 'C': '🄲', 'D': '🄳', 'E': '🄴',
|
||||
'F': '🄵', 'G': '🄶', 'H': '🄷', 'I': '🄸', 'J': '🄹',
|
||||
'K': '🄺', 'L': '🄻', 'M': '🄼', 'N': '🄽', 'O': '🄾',
|
||||
'P': '🄿', 'Q': '🅀', 'R': '🅁', 'S': '🅂', 'T': '🅃',
|
||||
'U': '🅄', 'V': '🅅', 'W': '🅆', 'X': '🅇', 'Y': '🅈',
|
||||
'Z': '🅉',
|
||||
'0': '⓪', '1': '①', '2': '②', '3': '③', '4': '④',
|
||||
'5': '⑤', '6': '⑥', '7': '⑦', '8': '⑧', '9': '⑨'
|
||||
},
|
||||
func: function(text) {
|
||||
let result = '';
|
||||
for (const char of text) {
|
||||
const upper = char.toUpperCase();
|
||||
if (this.map[upper]) {
|
||||
result += this.map[upper];
|
||||
} else if (this.map[char]) {
|
||||
result += this.map[char];
|
||||
} else {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const reverseMap = {};
|
||||
for (const [key, value] of Object.entries(this.map)) {
|
||||
reverseMap[value] = key;
|
||||
}
|
||||
|
||||
let result = '';
|
||||
for (const char of text) {
|
||||
if (reverseMap[char]) {
|
||||
result += reverseMap[char];
|
||||
} else {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[squared]';
|
||||
return this.func(text.slice(0, 5));
|
||||
},
|
||||
detector: function(text) {
|
||||
const squaredChars = Object.values(this.map);
|
||||
return squaredChars.some(char => text.includes(char));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -17,6 +17,10 @@ export default new BaseTransformer({
|
||||
reverse: function(text) {
|
||||
// Remove combining strikethrough characters
|
||||
return text.replace(/\u0336/g, '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for combining strikethrough character (U+0336)
|
||||
return /\u0336/.test(text);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -17,6 +17,10 @@ export default new BaseTransformer({
|
||||
reverse: function(text) {
|
||||
// Remove combining underline characters
|
||||
return text.replace(/\u0332/g, '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for combining underline character (U+0332)
|
||||
return /\u0332/.test(text);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
// wavy underline transform (using combining characters)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Wavy Underline',
|
||||
priority: 100,
|
||||
category: 'unicode',
|
||||
func: function(text) {
|
||||
// Add wavy underline combining character (U+0330) after each character
|
||||
return [...text].map(c => c + '\u0330').join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Remove combining wavy below character
|
||||
return text.replace(/\u0330/g, '');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[wavy-underline]';
|
||||
return this.func(text.slice(0, 8));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for wavy below combining character
|
||||
return /\u0330/.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// wide spacing transform (adds spaces between characters)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Wide Spacing',
|
||||
priority: 100,
|
||||
category: 'unicode',
|
||||
func: function(text) {
|
||||
// Add space between each character
|
||||
return [...text].join(' ');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Remove all spaces
|
||||
return text.replace(/\s+/g, '');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[wide-spacing]';
|
||||
return this.func(text.slice(0, 8));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check if text has spaces between most characters
|
||||
// At least 50% of characters should be followed by a space
|
||||
if (text.length < 3) return false;
|
||||
const spaceCount = (text.match(/\s/g) || []).length;
|
||||
const charCount = text.replace(/\s/g, '').length;
|
||||
return charCount > 0 && spaceCount / (charCount + spaceCount) > 0.3;
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user