mirror of
https://github.com/elder-plinius/P4RS3LT0NGV3.git
synced 2026-08-28 22:00:48 +02:00
lots of transform updates, additions, etc. updated docs. standardized transform dropdown. added qr/bar code tool.
This commit is contained in:
+18
-17
@@ -4,17 +4,18 @@ Transformers are instantiated using `BaseTransformer` class. Category is automat
|
||||
|
||||
## Directory Structure
|
||||
|
||||
Categories (auto-assigned from directory name):
|
||||
- `encoding/` - Base64, Hex, Binary, URL, HTML, etc.
|
||||
- `cipher/` - ROT13, Caesar, Vigenère, Atbash, etc.
|
||||
- `unicode/` - Cursive, Medieval, Monospace, Bubble, etc.
|
||||
- `case/` - Snake case, Kebab case, Title case, etc.
|
||||
- `technical/` - Morse, Braille, NATO, Brainfuck, etc.
|
||||
- `fantasy/` - Elder Futhark, Tengwar, Klingon, Aurebesh, etc.
|
||||
- `ancient/` - Hieroglyphics, Ogham, Roman Numerals, etc.
|
||||
- `format/` - Leetspeak, Pig Latin, Reverse, etc.
|
||||
- `visual/` - Emoji speak, Rovarspraket, etc.
|
||||
- `special/` - Randomizer, etc.
|
||||
Categories (auto-assigned from directory name by `npm run build:transforms`):
|
||||
- `case/` — Case and capitalization transforms
|
||||
- `cipher/` — Classical ciphers, transposition, polyalphabetic, A1Z26, tap code, codons, etc.
|
||||
- `concealment/` — Steganography and hidden-message schemes (null cipher, acrostic, zero-width, etc.)
|
||||
- `encoding/` — Byte and data encodings (Base64, hex, binary, line codes, Brainfuck, etc.)
|
||||
- `format/` — Text cleanup and layout utilities
|
||||
- `signwriting/` — SignWriting fingerspelling and related notation
|
||||
- `special/` — Randomizer and other misc tools
|
||||
- `symbol/` — Substitution alphabets, runes, scripts, pigpen, braille, numeral systems, etc.
|
||||
- `technical/` — Morse, NATO, phone keypad, spelling alphabets, semaphore, etc.
|
||||
- `unicode/` — Unicode presentation styles (bold, bubble, zalgo, etc.)
|
||||
- `visual/` — Spoken-language games and wordplay (Pig Latin, leetspeak, etc.)
|
||||
|
||||
## Creating a Transformer
|
||||
|
||||
@@ -129,12 +130,12 @@ Higher priority = more specific pattern (used for decoder result ordering):
|
||||
|
||||
## After Adding
|
||||
|
||||
1. Place file in appropriate category directory
|
||||
2. Run `npm run build:transforms`
|
||||
3. Test in webapp
|
||||
4. Add `detector` function if format has distinctive patterns
|
||||
5. Optionally add test cases to `tests/test_universal.js`
|
||||
6. Add a one-line description for the transform’s `name` in `DESCRIPTIONS` inside `build/readme-transform-section.js`, then run `node build/readme-transform-section.js` and merge the printed block into the **Text Transformations** section of the root `README.md` (the script exits with an error if a transform is missing from `DESCRIPTIONS`)
|
||||
1. Place the file in the appropriate category directory (folder name = UI category).
|
||||
2. Run `npm run build:transforms` (or `npm run build`).
|
||||
3. Test in the webapp (`npm start` → http://localhost:8080).
|
||||
4. Add a `detector` function if the format has distinctive patterns (helps the universal decoder).
|
||||
5. Optionally add known limitations to `limitations` in `tests/test_universal.js`.
|
||||
6. Update the root **README.md**: add or edit a bullet under **Text Transformations** in the matching category section, using the transform’s `name` and a one-line description.
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
// Acéré cipher — French solfège (A = Ré) with optional note-duration variants
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
const SOLFEGE = ['Ré', 'Mi', 'Fa', 'Sol', 'La', 'Si', 'Do'];
|
||||
const SOLFEGE_ASCII = ['re', 'mi', 'fa', 'sol', 'la', 'si', 'do'];
|
||||
const EIGHTH = '♪';
|
||||
const HALF = '●';
|
||||
const WHOLE = '𝅝';
|
||||
const QUARTER = '♩';
|
||||
|
||||
/** Eighth then half; whole notes used for digits / extended tiers */
|
||||
const BLOCK_DURATIONS = {
|
||||
'full-alphabet': [EIGHTH, HALF, WHOLE, QUARTER],
|
||||
'eighth-then-half': [EIGHTH, HALF, WHOLE, QUARTER],
|
||||
'half-then-eighth': [HALF, EIGHTH, QUARTER, WHOLE],
|
||||
'first-half-eighth': null,
|
||||
'solfège-only': null,
|
||||
'an-septet': null
|
||||
};
|
||||
|
||||
function solfegeToPitch(token) {
|
||||
const key = token.toLowerCase().replace(/é/g, 'e');
|
||||
return SOLFEGE_ASCII.indexOf(key);
|
||||
}
|
||||
|
||||
function durationForLetterIndex(index, durationMode) {
|
||||
if (durationMode === 'solfège-only') return '';
|
||||
if (durationMode === 'first-half-eighth') {
|
||||
return index < 13 ? EIGHTH : HALF;
|
||||
}
|
||||
if (durationMode === 'an-septet') {
|
||||
return index < 7 ? EIGHTH : HALF;
|
||||
}
|
||||
const block = Math.floor(index / 7);
|
||||
const table = BLOCK_DURATIONS[durationMode] || BLOCK_DURATIONS['full-alphabet'];
|
||||
return table[block] || QUARTER;
|
||||
}
|
||||
|
||||
function durationForDigit(digit, durationMode) {
|
||||
if (durationMode === 'solfège-only') return '';
|
||||
return WHOLE;
|
||||
}
|
||||
|
||||
function encodeLetter(ch, options) {
|
||||
const upper = ch.toUpperCase();
|
||||
if (/[A-Z]/.test(upper)) {
|
||||
const index = upper.charCodeAt(0) - 65;
|
||||
const duration = durationForLetterIndex(index, options.durationMode);
|
||||
return duration + SOLFEGE[index % 7];
|
||||
}
|
||||
if (options.includeDigits && /[0-9]/.test(ch)) {
|
||||
const digit = parseInt(ch, 10);
|
||||
return durationForDigit(digit, options.durationMode) + SOLFEGE[digit % 7];
|
||||
}
|
||||
return ch;
|
||||
}
|
||||
|
||||
function matchingLetterIndices(pitch, duration, durationMode) {
|
||||
const out = [];
|
||||
for (let i = 0; i < 26; i++) {
|
||||
if (i % 7 !== pitch) continue;
|
||||
if (durationMode === 'solfège-only') {
|
||||
out.push(i);
|
||||
continue;
|
||||
}
|
||||
const expected = durationForLetterIndex(i, durationMode);
|
||||
if (!duration || duration === expected) out.push(i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseToken(raw) {
|
||||
const chars = [...raw.trim()];
|
||||
if (!chars.length) return { duration: '', namePart: '' };
|
||||
const first = chars[0];
|
||||
if (first === EIGHTH || first === HALF || first === WHOLE || first === QUARTER) {
|
||||
return { duration: first, namePart: chars.slice(1).join('') };
|
||||
}
|
||||
return { duration: '', namePart: raw.trim() };
|
||||
}
|
||||
|
||||
function decodeToken(raw, options) {
|
||||
const t = raw.trim();
|
||||
if (!t) return '';
|
||||
|
||||
const { duration, namePart } = parseToken(t);
|
||||
|
||||
const pitch = solfegeToPitch(namePart);
|
||||
if (pitch < 0) return t;
|
||||
|
||||
if (duration === WHOLE && options.includeDigits && !/[A-Za-z]/.test(t)) {
|
||||
for (let d = 0; d <= 9; d++) {
|
||||
if (d % 7 === pitch) return String(d);
|
||||
}
|
||||
}
|
||||
|
||||
const candidates = matchingLetterIndices(pitch, duration, options.durationMode);
|
||||
if (candidates.length === 0) return t;
|
||||
return String.fromCharCode(65 + candidates[0]);
|
||||
}
|
||||
|
||||
function splitTokens(text, separator) {
|
||||
if (separator === 'none') {
|
||||
return text.match(/[♪●𝅝♩]?(?:Ré|Re|Mi|Fa|Sol|La|Si|Do)/gi) || [];
|
||||
}
|
||||
if (separator === 'comma') {
|
||||
return text.split(/\s*,\s*/).filter(Boolean);
|
||||
}
|
||||
return text.trim().split(/\s+/).filter(Boolean);
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Acéré Cipher',
|
||||
priority: 88,
|
||||
category: 'music',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'durationMode',
|
||||
label: 'Note duration variant',
|
||||
type: 'select',
|
||||
default: 'full-alphabet',
|
||||
options: [
|
||||
{
|
||||
value: 'full-alphabet',
|
||||
label: '♪ / ● / 𝅝 / ♩ by septet (full A–Z, reversible)'
|
||||
},
|
||||
{
|
||||
value: 'an-septet',
|
||||
label: '♪ A–G, ● H–N (A–N only)'
|
||||
},
|
||||
{
|
||||
value: 'eighth-then-half',
|
||||
label: '♪ / ● / 𝅝 / ♩ septets (same as full)'
|
||||
},
|
||||
{
|
||||
value: 'half-then-eighth',
|
||||
label: 'Inverted septet durations'
|
||||
},
|
||||
{
|
||||
value: 'first-half-eighth',
|
||||
label: '♪ A–M, ● N–Z (lossy)'
|
||||
},
|
||||
{
|
||||
value: 'solfège-only',
|
||||
label: 'Solfège names only (lossy)'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'includeDigits',
|
||||
label: 'Encode digits 0–9 (0 = Ré, 𝅝 whole note)',
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
{
|
||||
id: 'lettersOnly',
|
||||
label: 'Letters (and digits) only on encode',
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
{
|
||||
id: 'separator',
|
||||
label: 'Separator between notes',
|
||||
type: 'select',
|
||||
default: 'space',
|
||||
options: [
|
||||
{ value: 'space', label: 'Space' },
|
||||
{ value: 'comma', label: 'Comma' },
|
||||
{ value: 'none', label: 'None' }
|
||||
]
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const durationMode = options.durationMode || 'full-alphabet';
|
||||
const opts = {
|
||||
durationMode,
|
||||
includeDigits: !!options.includeDigits
|
||||
};
|
||||
const sep = options.separator === 'comma' ? ', '
|
||||
: (options.separator === 'none' ? '' : ' ');
|
||||
const chars = options.lettersOnly !== false
|
||||
? [...text].filter(c => /[A-Za-z0-9]/.test(c) && (opts.includeDigits || /[A-Za-z]/.test(c)))
|
||||
: [...text];
|
||||
const parts = chars.map(c => encodeLetter(c, opts));
|
||||
return options.separator === 'none' ? parts.join('') : parts.join(sep);
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
options = options || {};
|
||||
const opts = {
|
||||
durationMode: options.durationMode || 'full-alphabet',
|
||||
includeDigits: !!options.includeDigits
|
||||
};
|
||||
const tokens = splitTokens(text, options.separator || 'space');
|
||||
return tokens.map(t => decodeToken(t, opts)).join('');
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[acéré]';
|
||||
return this.func(text.slice(0, 8), options) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
const solfegeHits = (text.match(/\b(ré|re|mi|fa|sol|la|si|do)\b/gi) || []).length;
|
||||
const symbolPairs = (text.match(/[♪●𝅝♩](?:Ré|Re|Mi|Fa|Sol|La|Si|Do)/g) || []).length;
|
||||
return solfegeHits >= 2 || symbolPairs >= 2;
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,162 @@
|
||||
// ADFGVX cipher (WWI extension of ADFGX with digits)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'ADFGVX Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key: 'KEYWORD',
|
||||
coords: ['A', 'D', 'F', 'G', 'V', 'X'],
|
||||
square: [
|
||||
['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']
|
||||
],
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'polybiusKey',
|
||||
label: 'Polybius square keyword',
|
||||
type: 'text',
|
||||
default: ''
|
||||
},
|
||||
{
|
||||
id: 'key',
|
||||
label: 'Transposition keyword',
|
||||
type: 'text',
|
||||
default: 'KEYWORD'
|
||||
}
|
||||
],
|
||||
_buildSquare: function(polybiusKey) {
|
||||
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
const key = String(polybiusKey || '').toUpperCase().replace(/[^A-Z0-9]/g, '').replace(/J/g, 'I');
|
||||
const seen = new Set();
|
||||
let cells = '';
|
||||
for (let i = 0; i < key.length; i++) {
|
||||
const c = key[i];
|
||||
if (!seen.has(c)) {
|
||||
seen.add(c);
|
||||
cells += c;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < alphabet.length; i++) {
|
||||
const c = alphabet[i];
|
||||
if (!seen.has(c)) {
|
||||
seen.add(c);
|
||||
cells += c;
|
||||
}
|
||||
}
|
||||
cells = cells.padEnd(36, 'X').slice(0, 36);
|
||||
const square = [];
|
||||
for (let row = 0; row < 6; row++) {
|
||||
square.push(cells.slice(row * 6, row * 6 + 6).split(''));
|
||||
}
|
||||
return square;
|
||||
},
|
||||
_squareFor: function(options) {
|
||||
options = options || {};
|
||||
const polybiusKey = String(options.polybiusKey || '').trim();
|
||||
return polybiusKey ? this._buildSquare(polybiusKey) : this.square;
|
||||
},
|
||||
_transKey: function(options) {
|
||||
const k = options && options.key !== undefined && options.key !== null
|
||||
? String(options.key)
|
||||
: null;
|
||||
return (k || this.key || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '');
|
||||
},
|
||||
_charToPair: function(char, square) {
|
||||
const c = char === 'J' ? 'I' : char;
|
||||
for (let row = 0; row < 6; row++) {
|
||||
for (let col = 0; col < 6; col++) {
|
||||
if (square[row][col] === c) {
|
||||
return this.coords[row] + this.coords[col];
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
},
|
||||
_pairToChar: function(pair, square) {
|
||||
if (pair.length !== 2) return '';
|
||||
const row = this.coords.indexOf(pair[0]);
|
||||
const col = this.coords.indexOf(pair[1]);
|
||||
if (row < 0 || col < 0) return '';
|
||||
return square[row][col];
|
||||
},
|
||||
_columnarEncode: function(text, transKey) {
|
||||
const numCols = transKey.length;
|
||||
const numRows = Math.ceil(text.length / numCols);
|
||||
const grid = [];
|
||||
let idx = 0;
|
||||
for (let row = 0; row < numRows; row++) {
|
||||
grid[row] = [];
|
||||
for (let col = 0; col < numCols; col++) {
|
||||
grid[row][col] = idx < text.length ? text[idx++] : '';
|
||||
}
|
||||
}
|
||||
const keyOrder = transKey.split('').map((char, index) => ({ char, index }))
|
||||
.sort((a, b) => (a.char === b.char ? a.index - b.index : a.char.localeCompare(b.char)));
|
||||
let result = '';
|
||||
for (const item of keyOrder) {
|
||||
for (let row = 0; row < numRows; row++) {
|
||||
if (grid[row][item.index]) result += grid[row][item.index];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
_columnarDecode: function(text, transKey) {
|
||||
const numCols = transKey.length;
|
||||
const numRows = Math.ceil(text.length / numCols);
|
||||
const keyOrder = transKey.split('').map((char, index) => ({ char, index }))
|
||||
.sort((a, b) => (a.char === b.char ? a.index - b.index : a.char.localeCompare(b.char)));
|
||||
const grid = Array.from({ length: numRows }, () => new Array(numCols));
|
||||
let textIdx = 0;
|
||||
for (const item of keyOrder) {
|
||||
const colLen = Math.ceil((text.length - item.index) / numCols);
|
||||
for (let row = 0; row < colLen && textIdx < text.length; row++) {
|
||||
grid[row][item.index] = text[textIdx++];
|
||||
}
|
||||
}
|
||||
let out = '';
|
||||
for (let row = 0; row < numRows; row++) {
|
||||
for (let col = 0; col < numCols; col++) {
|
||||
if (grid[row][col]) out += grid[row][col];
|
||||
}
|
||||
}
|
||||
return out;
|
||||
},
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const transKey = this._transKey(options);
|
||||
if (!transKey.length) return text;
|
||||
const square = this._squareFor(options);
|
||||
const cleaned = text.toUpperCase().replace(/[^A-Z0-9]/g, '').replace(/J/g, 'I');
|
||||
if (!cleaned.length) return text;
|
||||
let pairs = '';
|
||||
for (const char of cleaned) pairs += this._charToPair(char, square);
|
||||
return this._columnarEncode(pairs, transKey);
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
options = options || {};
|
||||
const transKey = this._transKey(options);
|
||||
if (!transKey.length) return text;
|
||||
const square = this._squareFor(options);
|
||||
const cleaned = text.toUpperCase().replace(/[^ADFGVX]/g, '');
|
||||
if (!cleaned.length || cleaned.length % 2 !== 0) return text;
|
||||
const pairs = this._columnarDecode(cleaned, transKey);
|
||||
let result = '';
|
||||
for (let i = 0; i < pairs.length; i += 2) {
|
||||
result += this._pairToChar(pairs.slice(i, i + 2), square);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[adfgvx]';
|
||||
return this.func(text.slice(0, 4), options).slice(0, 12) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
const cleaned = text.replace(/\s/g, '').toUpperCase();
|
||||
return cleaned.length >= 10 && cleaned.length % 2 === 0 && /^[ADFGVX]+$/.test(cleaned);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
// AMSCO cipher (alternating 1/2 column heights + keyword transposition)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'AMSCO Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key: 'KEY',
|
||||
configurableOptions: [
|
||||
{ id: 'key', label: 'Keyword', type: 'text', default: 'KEY' }
|
||||
],
|
||||
_key: function(options) {
|
||||
const k = options && options.key != null ? String(options.key) : null;
|
||||
return (k || this.key || 'KEY').toUpperCase().replace(/[^A-Z]/g, '');
|
||||
},
|
||||
_colCapacity: function(colIndex) {
|
||||
return colIndex % 2 === 0 ? 1 : 2;
|
||||
},
|
||||
_sortedColumns: function(key) {
|
||||
return key.split('').map((char, index) => ({ char, index }))
|
||||
.sort((a, b) => (a.char === b.char ? a.index - b.index : a.char.localeCompare(b.char)));
|
||||
},
|
||||
_columnLengths: function(textLen, numCols) {
|
||||
const lens = new Array(numCols).fill(0);
|
||||
let idx = 0;
|
||||
while (idx < textLen) {
|
||||
for (let col = 0; col < numCols && idx < textLen; col++) {
|
||||
const take = Math.min(this._colCapacity(col), textLen - idx);
|
||||
lens[col] += take;
|
||||
idx += take;
|
||||
}
|
||||
}
|
||||
return lens;
|
||||
},
|
||||
func: function(text, options) {
|
||||
const key = this._key(options);
|
||||
if (!key.length) return text;
|
||||
const cleaned = text.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (!cleaned.length) return text;
|
||||
|
||||
const numCols = key.length;
|
||||
const columns = Array.from({ length: numCols }, () => '');
|
||||
let idx = 0;
|
||||
while (idx < cleaned.length) {
|
||||
for (let col = 0; col < numCols && idx < cleaned.length; col++) {
|
||||
const take = Math.min(this._colCapacity(col), cleaned.length - idx);
|
||||
columns[col] += cleaned.slice(idx, idx + take);
|
||||
idx += take;
|
||||
}
|
||||
}
|
||||
|
||||
let out = '';
|
||||
for (const item of this._sortedColumns(key)) out += columns[item.index];
|
||||
return out;
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
const key = this._key(options);
|
||||
if (!key.length) return text;
|
||||
const cleaned = text.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (!cleaned.length) return text;
|
||||
|
||||
const numCols = key.length;
|
||||
const lens = this._columnLengths(cleaned.length, numCols);
|
||||
const columns = Array.from({ length: numCols }, () => '');
|
||||
let idx = 0;
|
||||
for (const item of this._sortedColumns(key)) {
|
||||
const len = lens[item.index];
|
||||
columns[item.index] = cleaned.slice(idx, idx + len);
|
||||
idx += len;
|
||||
}
|
||||
|
||||
const colOffsets = new Array(numCols).fill(0);
|
||||
let out = '';
|
||||
let remaining = cleaned.length;
|
||||
while (remaining > 0) {
|
||||
for (let col = 0; col < numCols && remaining > 0; col++) {
|
||||
const take = Math.min(this._colCapacity(col), columns[col].length - colOffsets[col], remaining);
|
||||
if (take <= 0) continue;
|
||||
out += columns[col].slice(colOffsets[col], colOffsets[col] + take);
|
||||
colOffsets[col] += take;
|
||||
remaining -= take;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[amsco]';
|
||||
return this.func(text.slice(0, 8), options).slice(0, 12) + '...';
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
// Book cipher (word indices in reference text)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
const DEFAULT_BOOK = 'the quick brown fox jumps over the lazy dog hello world while pack my box with five dozen liquor jugs';
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Book Cipher',
|
||||
priority: 55,
|
||||
category: 'cipher',
|
||||
configurableOptions: [
|
||||
{ id: 'book', label: 'Reference text (book)', type: 'text', default: DEFAULT_BOOK },
|
||||
{
|
||||
id: 'separator',
|
||||
label: 'Index separator',
|
||||
type: 'select',
|
||||
default: ' ',
|
||||
options: [
|
||||
{ value: ' ', label: 'Space' },
|
||||
{ value: '-', label: 'Dash' },
|
||||
{ value: '.', label: 'Dot' }
|
||||
]
|
||||
}
|
||||
],
|
||||
_bookWords: function(options) {
|
||||
const book = String(options && options.book != null ? options.book : DEFAULT_BOOK).toLowerCase();
|
||||
return book.match(/[a-z0-9']+/g) || [];
|
||||
},
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const words = this._bookWords(options);
|
||||
const sep = options.separator != null ? String(options.separator) : ' ';
|
||||
if (!words.length) return text;
|
||||
const inputWords = text.match(/[a-zA-Z0-9']+/g) || [];
|
||||
const indices = inputWords.map(w => {
|
||||
const target = w.toLowerCase();
|
||||
const idx = words.indexOf(target);
|
||||
return idx >= 0 ? String(idx + 1) : '?';
|
||||
});
|
||||
return indices.join(sep);
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
options = options || {};
|
||||
const words = this._bookWords(options);
|
||||
if (!words.length) return text;
|
||||
const sep = options.separator != null ? String(options.separator) : ' ';
|
||||
const esc = sep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const parts = text.trim().split(new RegExp(esc.length ? esc : '\\s+'));
|
||||
return parts.map(p => {
|
||||
const n = parseInt(p, 10);
|
||||
if (!Number.isFinite(n) || n < 1 || n > words.length) return p;
|
||||
return words[n - 1];
|
||||
}).join(' ');
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[book]';
|
||||
return this.func(text.slice(0, 20), options).slice(0, 20) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
const cleaned = text.trim();
|
||||
return /^[\d\s.\-]+$/.test(cleaned) && cleaned.replace(/\D/g, '').length >= 2;
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,54 @@
|
||||
// Genetic codons — letters encoded as DNA triplets (RNA codon table, simplified)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
const MAP = {
|
||||
'A': 'GCT', 'B': 'TGT', 'C': 'TGT', 'D': 'GAT', 'E': 'GAA', 'F': 'TTT',
|
||||
'G': 'GGT', 'H': 'CAT', 'I': 'ATT', 'J': 'TAT', 'K': 'AAA', 'L': 'TTA',
|
||||
'M': 'ATG', 'N': 'AAT', 'O': 'TAT', 'P': 'CCT', 'Q': 'CAA', 'R': 'CGT',
|
||||
'S': 'TCT', 'T': 'ACT', 'U': 'TGT', 'V': 'GTT', 'W': 'TGG', 'X': 'TAG',
|
||||
'Y': 'TAT', 'Z': 'TGT'
|
||||
};
|
||||
const REV = {};
|
||||
for (const [k, v] of Object.entries(MAP)) {
|
||||
if (!REV[v]) REV[v] = k;
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Codons (Genetic Code)',
|
||||
priority: 84,
|
||||
category: 'encoding',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'separator',
|
||||
label: 'Separator between codons',
|
||||
type: 'select',
|
||||
default: 'space',
|
||||
options: [
|
||||
{ value: 'space', label: 'Space' },
|
||||
{ value: 'none', label: 'None' }
|
||||
]
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const sep = options.separator === 'none' ? '' : ' ';
|
||||
return [...text.toUpperCase()].filter(c => /[A-Z]/.test(c)).map(c => MAP[c] || c).join(sep);
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
options = options || {};
|
||||
const tokens = options.separator === 'none'
|
||||
? text.match(/[ACGT]{3}/gi) || []
|
||||
: text.trim().split(/\s+/);
|
||||
return tokens.map(t => REV[t.toUpperCase()] || '').join('');
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[codon]';
|
||||
return this.func(text.slice(0, 6), options);
|
||||
},
|
||||
detector: function(text) {
|
||||
const tokens = text.trim().split(/\s+/);
|
||||
return tokens.length >= 2 && tokens.every(t => /^[ACGT]{3}$/i.test(t));
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,89 @@
|
||||
// Double columnar transposition cipher
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
function columnarEncode(text, key, pad) {
|
||||
pad = pad || 'X';
|
||||
const numCols = key.length;
|
||||
const numRows = Math.ceil(text.length / numCols);
|
||||
const grid = [];
|
||||
let idx = 0;
|
||||
for (let row = 0; row < numRows; row++) {
|
||||
grid[row] = [];
|
||||
for (let col = 0; col < numCols; col++) {
|
||||
grid[row][col] = idx < text.length ? text[idx++] : pad;
|
||||
}
|
||||
}
|
||||
const order = key.split('').map((char, index) => ({ char, index }))
|
||||
.sort((a, b) => (a.char === b.char ? a.index - b.index : a.char.localeCompare(b.char)));
|
||||
let out = '';
|
||||
for (const item of order) {
|
||||
for (let row = 0; row < numRows; row++) out += grid[row][item.index];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function columnarDecode(text, key, pad) {
|
||||
pad = pad || 'X';
|
||||
const numCols = key.length;
|
||||
const numRows = Math.ceil(text.length / numCols);
|
||||
const order = key.split('').map((char, index) => ({ char, index }))
|
||||
.sort((a, b) => (a.char === b.char ? a.index - b.index : a.char.localeCompare(b.char)));
|
||||
const grid = Array.from({ length: numRows }, () => new Array(numCols));
|
||||
let textIdx = 0;
|
||||
for (const item of order) {
|
||||
for (let row = 0; row < numRows && textIdx < text.length; row++) {
|
||||
grid[row][item.index] = text[textIdx++];
|
||||
}
|
||||
}
|
||||
let out = '';
|
||||
for (let row = 0; row < numRows; row++) {
|
||||
for (let col = 0; col < numCols; col++) {
|
||||
if (grid[row][col]) out += grid[row][col];
|
||||
}
|
||||
}
|
||||
return out.replace(new RegExp((pad || 'X').replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '+$'), '');
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Double Transposition',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key1: 'FIRST',
|
||||
key2: 'SECOND',
|
||||
configurableOptions: [
|
||||
{ id: 'key1', label: 'First keyword', type: 'text', default: 'FIRST' },
|
||||
{ id: 'key2', label: 'Second keyword', type: 'text', default: 'SECOND' },
|
||||
{
|
||||
id: 'padChar',
|
||||
label: 'Padding character',
|
||||
type: 'text',
|
||||
default: 'X'
|
||||
}
|
||||
],
|
||||
_keys: function(options) {
|
||||
options = options || {};
|
||||
const k1 = String(options.key1 != null ? options.key1 : this.key1).toUpperCase().replace(/[^A-Z]/g, '');
|
||||
const k2 = String(options.key2 != null ? options.key2 : this.key2).toUpperCase().replace(/[^A-Z]/g, '');
|
||||
const padChar = String(options.padChar != null ? options.padChar : 'X').charAt(0) || 'X';
|
||||
return { k1: k1 || 'FIRST', k2: k2 || 'SECOND', padChar };
|
||||
},
|
||||
func: function(text, options) {
|
||||
const { k1, k2, padChar } = this._keys(options);
|
||||
const cleaned = text.replace(/\s/g, '').toUpperCase();
|
||||
if (!cleaned.length) return text;
|
||||
return columnarEncode(columnarEncode(cleaned, k1, padChar), k2, padChar);
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
const { k1, k2, padChar } = this._keys(options);
|
||||
const cleaned = text.replace(/\s/g, '').toUpperCase();
|
||||
if (!cleaned.length) return text;
|
||||
const esc = padChar.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
return columnarDecode(columnarDecode(cleaned, k2, padChar), k1, padChar).replace(new RegExp(esc + '+$'), '');
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[double-trans]';
|
||||
return this.func(text.slice(0, 8), options).slice(0, 12) + '...';
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,134 @@
|
||||
// Fractionated Morse cipher (Morse pairs mapped via Polybius-style square)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
const MORSE = {
|
||||
'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': '--..'
|
||||
};
|
||||
const REV_MORSE = Object.fromEntries(Object.entries(MORSE).map(([k, v]) => [v, k]));
|
||||
const 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']
|
||||
];
|
||||
const SYM = ['.', '-'];
|
||||
|
||||
function morseStream(text) {
|
||||
return text.toUpperCase().replace(/[^A-Z]/g, '').split('')
|
||||
.map(c => MORSE[c] || '')
|
||||
.join('');
|
||||
}
|
||||
|
||||
function streamToText(stream) {
|
||||
const chars = [];
|
||||
let buf = '';
|
||||
for (const s of stream) {
|
||||
if (s === '.' || s === '-') buf += s;
|
||||
else if (buf) {
|
||||
chars.push(buf);
|
||||
buf = '';
|
||||
}
|
||||
}
|
||||
if (buf) chars.push(buf);
|
||||
let out = '';
|
||||
for (const code of chars) out += REV_MORSE[code] || '';
|
||||
return out;
|
||||
}
|
||||
|
||||
function pairToChar(a, b) {
|
||||
const row = SYM.indexOf(a);
|
||||
const col = SYM.indexOf(b);
|
||||
if (row < 0 || col < 0) return '';
|
||||
return SQUARE[row][col];
|
||||
}
|
||||
|
||||
function charToPair(ch) {
|
||||
const c = ch === 'J' ? 'I' : ch;
|
||||
for (let r = 0; r < 5; r++) {
|
||||
for (let col = 0; col < 5; col++) {
|
||||
if (SQUARE[r][col] === c) return SYM[r] + SYM[col];
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Fractionated Morse',
|
||||
priority: 65,
|
||||
category: 'technical',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'letterSeparator',
|
||||
label: 'Separator between letters',
|
||||
type: 'text',
|
||||
default: ''
|
||||
},
|
||||
{
|
||||
id: 'wordSeparator',
|
||||
label: 'Separator between words',
|
||||
type: 'select',
|
||||
default: ' ',
|
||||
options: [
|
||||
{ value: ' ', label: 'Space' },
|
||||
{ value: ' / ', label: 'Slash (Morse style)' },
|
||||
{ value: ' | ', label: 'Pipe' },
|
||||
{ value: '', label: 'None' }
|
||||
]
|
||||
}
|
||||
],
|
||||
_opts: function(options) {
|
||||
options = options || {};
|
||||
return {
|
||||
letterSeparator: options.letterSeparator != null ? String(options.letterSeparator) : '',
|
||||
wordSeparator: options.wordSeparator != null ? String(options.wordSeparator) : ' '
|
||||
};
|
||||
},
|
||||
func: function(text, options) {
|
||||
const { letterSeparator, wordSeparator } = this._opts(options);
|
||||
const words = text.toUpperCase().match(/[A-Z]+/g) || [];
|
||||
if (!words.length) {
|
||||
return text;
|
||||
}
|
||||
return words.map(function(word) {
|
||||
const stream = morseStream(word);
|
||||
let out = '';
|
||||
for (let i = 0; i < stream.length; i += 2) {
|
||||
if (i + 1 < stream.length) {
|
||||
if (out && letterSeparator) {
|
||||
out += letterSeparator;
|
||||
}
|
||||
out += pairToChar(stream[i], stream[i + 1]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}).join(wordSeparator);
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
const { letterSeparator, wordSeparator } = this._opts(options);
|
||||
let chunks = [text];
|
||||
if (wordSeparator) {
|
||||
chunks = text.split(wordSeparator);
|
||||
}
|
||||
return chunks.map(function(chunk) {
|
||||
const cleaned = letterSeparator
|
||||
? chunk.split(letterSeparator).join('')
|
||||
: chunk;
|
||||
let pairs = '';
|
||||
for (const ch of cleaned.toUpperCase().replace(/[^A-Z]/g, '')) {
|
||||
pairs += charToPair(ch);
|
||||
}
|
||||
return streamToText(pairs);
|
||||
}).join(' ');
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[frac-morse]';
|
||||
return this.func(text.slice(0, 6), options) + '...';
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,58 @@
|
||||
// Keyword shift cipher (Caesar shift from keyword letters)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Keyword Shift Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key: 'KEY',
|
||||
configurableOptions: [
|
||||
{ id: 'key', label: 'Keyword', type: 'text', default: 'KEY' }
|
||||
],
|
||||
_keyStr: function(options) {
|
||||
const k = options && options.key != null ? String(options.key) : null;
|
||||
return (k || this.key || 'KEY').toUpperCase().replace(/[^A-Z]/g, '');
|
||||
},
|
||||
_shiftChar: function(c, shift, decode) {
|
||||
const code = c.charCodeAt(0);
|
||||
const s = ((shift % 26) + 26) % 26;
|
||||
const delta = decode ? (26 - s) % 26 : s;
|
||||
if (code >= 65 && code <= 90) return String.fromCharCode(((code - 65 + delta) % 26) + 65);
|
||||
if (code >= 97 && code <= 122) return String.fromCharCode(((code - 97 + delta) % 26) + 97);
|
||||
return c;
|
||||
},
|
||||
func: function(text, options) {
|
||||
const key = this._keyStr(options);
|
||||
if (!key.length) return text;
|
||||
let out = '';
|
||||
let j = 0;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i];
|
||||
if (/[a-zA-Z]/.test(c)) {
|
||||
const shift = key[j % key.length].charCodeAt(0) - 65;
|
||||
out += this._shiftChar(c, shift, false);
|
||||
j++;
|
||||
} else out += c;
|
||||
}
|
||||
return out;
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
const key = this._keyStr(options);
|
||||
if (!key.length) return text;
|
||||
let out = '';
|
||||
let j = 0;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i];
|
||||
if (/[a-zA-Z]/.test(c)) {
|
||||
const shift = key[j % key.length].charCodeAt(0) - 65;
|
||||
out += this._shiftChar(c, shift, true);
|
||||
j++;
|
||||
} else out += c;
|
||||
}
|
||||
return out;
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[keyword-shift]';
|
||||
return this.func(text.slice(0, 8), options) + '...';
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
// Monoalphabetic substitution cipher (custom 26-letter alphabet)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
const PLAIN = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Monoalphabetic Substitution',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'keyword',
|
||||
label: 'Keyword (builds cipher alphabet when set)',
|
||||
type: 'text',
|
||||
default: ''
|
||||
},
|
||||
{
|
||||
id: 'alphabet',
|
||||
label: 'Cipher alphabet (26 unique letters A-Z)',
|
||||
type: 'text',
|
||||
default: 'QWERTYUIOPASDFGHJKLZXCVBNM'
|
||||
}
|
||||
],
|
||||
_keywordAlpha: function(keyword) {
|
||||
const kw = String(keyword || '').toUpperCase().replace(/[^A-Z]/g, '');
|
||||
const seen = new Set();
|
||||
let alpha = '';
|
||||
for (let i = 0; i < kw.length; i++) {
|
||||
const c = kw[i];
|
||||
if (!seen.has(c)) {
|
||||
seen.add(c);
|
||||
alpha += c;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < PLAIN.length; i++) {
|
||||
const c = PLAIN[i];
|
||||
if (!seen.has(c)) {
|
||||
seen.add(c);
|
||||
alpha += c;
|
||||
}
|
||||
}
|
||||
return alpha.slice(0, 26);
|
||||
},
|
||||
_cipherAlpha: function(options) {
|
||||
const keyword = String(options && options.keyword != null ? options.keyword : '').trim();
|
||||
if (keyword) {
|
||||
return this._keywordAlpha(keyword);
|
||||
}
|
||||
const raw = String(options && options.alphabet != null ? options.alphabet : 'QWERTYUIOPASDFGHJKLZXCVBNM')
|
||||
.toUpperCase()
|
||||
.replace(/[^A-Z]/g, '');
|
||||
const seen = new Set();
|
||||
let alpha = '';
|
||||
for (const c of raw) {
|
||||
if (!seen.has(c)) {
|
||||
seen.add(c);
|
||||
alpha += c;
|
||||
}
|
||||
}
|
||||
for (const c of PLAIN) {
|
||||
if (!seen.has(c)) {
|
||||
seen.add(c);
|
||||
alpha += c;
|
||||
}
|
||||
}
|
||||
return alpha.slice(0, 26);
|
||||
},
|
||||
_maps: function(options) {
|
||||
const cipher = this._cipherAlpha(options);
|
||||
const enc = {};
|
||||
const dec = {};
|
||||
for (let i = 0; i < 26; i++) {
|
||||
enc[PLAIN[i]] = cipher[i];
|
||||
enc[PLAIN[i].toLowerCase()] = cipher[i].toLowerCase();
|
||||
dec[cipher[i]] = PLAIN[i];
|
||||
dec[cipher[i].toLowerCase()] = PLAIN[i].toLowerCase();
|
||||
}
|
||||
return { enc, dec };
|
||||
},
|
||||
func: function(text, options) {
|
||||
const { enc } = this._maps(options || {});
|
||||
return [...text].map(c => enc[c] || c).join('');
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
const { dec } = this._maps(options || {});
|
||||
return [...text].map(c => dec[c] || c).join('');
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[mono]';
|
||||
return this.func(text.slice(0, 8), options) + '...';
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,50 @@
|
||||
// Multiplicative cipher (mod 26; key must be coprime with 26)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
const COPRIMES = [1, 3, 5, 7, 9, 11, 15, 17, 19, 21, 23, 25];
|
||||
const INVERSES = { 1: 1, 3: 9, 5: 21, 7: 15, 9: 3, 11: 19, 15: 7, 17: 23, 19: 11, 21: 5, 23: 17, 25: 25 };
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Multiplicative Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
multiplier: 5,
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'multiplier',
|
||||
label: 'Multiplier (coprime with 26)',
|
||||
type: 'select',
|
||||
default: '5',
|
||||
options: COPRIMES.map(n => ({ value: String(n), label: String(n) }))
|
||||
}
|
||||
],
|
||||
_mult: function(options) {
|
||||
const m = Number(options && options.multiplier != null ? options.multiplier : this.multiplier);
|
||||
return COPRIMES.includes(m) ? m : 5;
|
||||
},
|
||||
func: function(text, options) {
|
||||
const mult = this._mult(options || {});
|
||||
return [...text].map(c => {
|
||||
const code = c.charCodeAt(0);
|
||||
if (code >= 65 && code <= 90) return String.fromCharCode(((code - 65) * mult % 26) + 65);
|
||||
if (code >= 97 && code <= 122) return String.fromCharCode(((code - 97) * mult % 26) + 97);
|
||||
return c;
|
||||
}).join('');
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
const mult = this._mult(options || {});
|
||||
const inv = INVERSES[mult] || 21;
|
||||
return [...text].map(c => {
|
||||
const code = c.charCodeAt(0);
|
||||
if (code >= 65 && code <= 90) return String.fromCharCode(((code - 65) * inv % 26) + 65);
|
||||
if (code >= 97 && code <= 122) return String.fromCharCode(((code - 97) * inv % 26) + 97);
|
||||
return c;
|
||||
}).join('');
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[mult]';
|
||||
return this.func(text.slice(0, 6), options) + '...';
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,163 @@
|
||||
// Route / path cipher (grid + reading route)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
function buildGrid(text, cols, pad) {
|
||||
const cleaned = text.replace(/\s/g, '').toUpperCase();
|
||||
const rows = Math.ceil(cleaned.length / cols);
|
||||
const grid = [];
|
||||
let idx = 0;
|
||||
for (let r = 0; r < rows; r++) {
|
||||
grid[r] = [];
|
||||
for (let c = 0; c < cols; c++) {
|
||||
grid[r][c] = idx < cleaned.length ? cleaned[idx++] : pad;
|
||||
}
|
||||
}
|
||||
return { grid, rows, cols, len: cleaned.length };
|
||||
}
|
||||
|
||||
function readSpiral(grid, rows, cols) {
|
||||
const out = [];
|
||||
let top = 0;
|
||||
let bottom = rows - 1;
|
||||
let left = 0;
|
||||
let right = cols - 1;
|
||||
while (top <= bottom && left <= right) {
|
||||
for (let c = left; c <= right; c++) out.push(grid[top][c]);
|
||||
top++;
|
||||
for (let r = top; r <= bottom; r++) out.push(grid[r][right]);
|
||||
right--;
|
||||
if (top <= bottom) {
|
||||
for (let c = right; c >= left; c--) out.push(grid[bottom][c]);
|
||||
bottom--;
|
||||
}
|
||||
if (left <= right) {
|
||||
for (let r = bottom; r >= top; r--) out.push(grid[r][left]);
|
||||
left++;
|
||||
}
|
||||
}
|
||||
return out.join('');
|
||||
}
|
||||
|
||||
function stripPad(text, pad) {
|
||||
const esc = pad.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
return text.replace(new RegExp(esc + '+$'), '');
|
||||
}
|
||||
|
||||
function writeSpiral(text, rows, cols, pad) {
|
||||
const grid = Array.from({ length: rows }, () => new Array(cols).fill(pad));
|
||||
let top = 0;
|
||||
let bottom = rows - 1;
|
||||
let left = 0;
|
||||
let right = cols - 1;
|
||||
let idx = 0;
|
||||
while (top <= bottom && left <= right && idx < text.length) {
|
||||
for (let c = left; c <= right && idx < text.length; c++) grid[top][c] = text[idx++];
|
||||
top++;
|
||||
for (let r = top; r <= bottom && idx < text.length; r++) grid[r][right] = text[idx++];
|
||||
right--;
|
||||
if (top <= bottom) {
|
||||
for (let c = right; c >= left && idx < text.length; c--) grid[bottom][c] = text[idx++];
|
||||
bottom--;
|
||||
}
|
||||
if (left <= right) {
|
||||
for (let r = bottom; r >= top && idx < text.length; r--) grid[r][left] = text[idx++];
|
||||
left++;
|
||||
}
|
||||
}
|
||||
return grid;
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Route Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
configurableOptions: [
|
||||
{ id: 'cols', label: 'Grid columns', type: 'number', default: 5, min: 2, max: 20, step: 1 },
|
||||
{
|
||||
id: 'route',
|
||||
label: 'Reading route',
|
||||
type: 'select',
|
||||
default: 'spiral',
|
||||
options: [
|
||||
{ value: 'spiral', label: 'Spiral (clockwise)' },
|
||||
{ value: 'rows', label: 'Row by row' },
|
||||
{ value: 'cols', label: 'Column by column' },
|
||||
{ value: 'snake', label: 'Snake rows' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'padChar',
|
||||
label: 'Padding character',
|
||||
type: 'text',
|
||||
default: 'X'
|
||||
}
|
||||
],
|
||||
_opts: function(options) {
|
||||
options = options || {};
|
||||
const cols = Math.max(2, Number(options.cols != null ? options.cols : 5) || 5);
|
||||
const route = options.route || 'spiral';
|
||||
const padChar = String(options.padChar != null ? options.padChar : 'X').charAt(0) || 'X';
|
||||
return { cols, route, padChar };
|
||||
},
|
||||
_readRoute: function(grid, rows, cols, route) {
|
||||
if (route === 'rows') return grid.flat().join('');
|
||||
if (route === 'cols') {
|
||||
let out = '';
|
||||
for (let c = 0; c < cols; c++) {
|
||||
for (let r = 0; r < rows; r++) out += grid[r][c];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
if (route === 'snake') {
|
||||
return grid.map((row, i) => (i % 2 ? [...row].reverse() : row).join('')).join('');
|
||||
}
|
||||
return readSpiral(grid, rows, cols);
|
||||
},
|
||||
func: function(text, options) {
|
||||
const { cols, route, padChar } = this._opts(options);
|
||||
const { grid, rows } = buildGrid(text, cols, padChar);
|
||||
const raw = this._readRoute(grid, rows, cols, route);
|
||||
return stripPad(raw, padChar);
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
const { cols, route, padChar } = this._opts(options);
|
||||
const cleaned = text.replace(/\s/g, '').toUpperCase();
|
||||
if (!cleaned.length) return text;
|
||||
const rows = Math.ceil(cleaned.length / cols);
|
||||
if (route === 'spiral') {
|
||||
const grid = writeSpiral(cleaned, rows, cols, padChar);
|
||||
let out = '';
|
||||
for (let r = 0; r < rows; r++) {
|
||||
for (let c = 0; c < cols; c++) out += grid[r][c];
|
||||
}
|
||||
return stripPad(out, padChar);
|
||||
}
|
||||
if (route === 'rows') return cleaned;
|
||||
if (route === 'cols') {
|
||||
const grid = Array.from({ length: rows }, () => new Array(cols).fill(padChar));
|
||||
let idx = 0;
|
||||
for (let c = 0; c < cols; c++) {
|
||||
for (let r = 0; r < rows && idx < cleaned.length; r++) grid[r][c] = cleaned[idx++];
|
||||
}
|
||||
return stripPad(grid.flat().join(''), padChar);
|
||||
}
|
||||
if (route === 'snake') {
|
||||
const grid = Array.from({ length: rows }, () => new Array(cols).fill(padChar));
|
||||
let idx = 0;
|
||||
for (let r = 0; r < rows; r++) {
|
||||
const order = r % 2 ? [...Array(cols).keys()].reverse() : [...Array(cols).keys()];
|
||||
for (const c of order) {
|
||||
if (idx < cleaned.length) grid[r][c] = cleaned[idx++];
|
||||
}
|
||||
}
|
||||
return stripPad(grid.flat().join(''), padChar);
|
||||
}
|
||||
return cleaned;
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[route]';
|
||||
return this.func(text.slice(0, 10), options).slice(0, 12) + '...';
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,48 @@
|
||||
// Trithemius cipher (progressive Caesar / tabula recta rows)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Trithemius Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
configurableOptions: [
|
||||
{ id: 'startShift', label: 'Starting shift', type: 'number', default: 0, min: 0, max: 25, step: 1 },
|
||||
{ id: 'step', label: 'Shift increment per letter', type: 'number', default: 1, min: 1, max: 25, step: 1 }
|
||||
],
|
||||
_opts: function(options) {
|
||||
options = options || {};
|
||||
return {
|
||||
start: Number(options.startShift != null ? options.startShift : 0) || 0,
|
||||
step: Number(options.step != null ? options.step : 1) || 1
|
||||
};
|
||||
},
|
||||
_transform: function(text, options, decode) {
|
||||
const { start, step } = this._opts(options);
|
||||
let out = '';
|
||||
let letterIdx = 0;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i];
|
||||
const code = c.charCodeAt(0);
|
||||
const shift = (start + letterIdx * step) % 26;
|
||||
const delta = decode ? (26 - shift) % 26 : shift;
|
||||
if (code >= 65 && code <= 90) {
|
||||
out += String.fromCharCode(((code - 65 + delta) % 26) + 65);
|
||||
letterIdx++;
|
||||
} else if (code >= 97 && code <= 122) {
|
||||
out += String.fromCharCode(((code - 97 + delta) % 26) + 97);
|
||||
letterIdx++;
|
||||
} else out += c;
|
||||
}
|
||||
return out;
|
||||
},
|
||||
func: function(text, options) {
|
||||
return this._transform(text, options, false);
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
return this._transform(text, options, true);
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[trithemius]';
|
||||
return this.func(text.slice(0, 8), options) + '...';
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
// Vernam cipher (one-time pad style additive mod 26 with key)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Vernam Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key: 'SECRET',
|
||||
configurableOptions: [
|
||||
{ id: 'key', label: 'Key (pad)', type: 'text', default: 'SECRET' }
|
||||
],
|
||||
_keyStr: function(options) {
|
||||
const k = options && options.key != null ? String(options.key) : null;
|
||||
return (k || this.key || 'SECRET').toUpperCase().replace(/[^A-Z]/g, '');
|
||||
},
|
||||
_apply: function(text, options, decode) {
|
||||
const key = this._keyStr(options);
|
||||
if (!key.length) return text;
|
||||
let out = '';
|
||||
let j = 0;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i];
|
||||
const code = c.charCodeAt(0);
|
||||
const k = key[j % key.length].charCodeAt(0) - 65;
|
||||
if (code >= 65 && code <= 90) {
|
||||
out += String.fromCharCode(decode
|
||||
? ((code - 65 + 26 - k) % 26) + 65
|
||||
: ((code - 65 + k) % 26) + 65);
|
||||
j++;
|
||||
} else if (code >= 97 && code <= 122) {
|
||||
out += String.fromCharCode(decode
|
||||
? ((code - 97 + 26 - k) % 26) + 97
|
||||
: ((code - 97 + k) % 26) + 97);
|
||||
j++;
|
||||
} else out += c;
|
||||
}
|
||||
return out;
|
||||
},
|
||||
func: function(text, options) {
|
||||
return this._apply(text, options, false);
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
return this._apply(text, options, true);
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[vernam]';
|
||||
return this.func(text.slice(0, 8), options) + '...';
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
// Acrostic — read or build messages from initial letters
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
const TEMPLATES = [
|
||||
'{letter}idden words carry meaning beyond the surface.',
|
||||
'{letter}ach line begins with the secret letter.',
|
||||
'{letter}ften the first letter tells the story.',
|
||||
'{letter}essages can hide in plain sight.',
|
||||
'{letter}eaders who look closely will find clues.'
|
||||
];
|
||||
|
||||
function extractAcrostic(text, mode, includeSpecial) {
|
||||
if (mode === 'words') {
|
||||
if (includeSpecial) {
|
||||
const words = text.match(/\S+/g) || [];
|
||||
return words.map(function(word) {
|
||||
return word.charAt(0) || '';
|
||||
}).join('');
|
||||
}
|
||||
const words = text.match(/[a-zA-Z0-9']+/g) || [];
|
||||
return words.map(function(word) {
|
||||
return word.replace(/[^a-zA-Z0-9]/g, '')[0] || '';
|
||||
}).join('');
|
||||
}
|
||||
return text.split(/\r?\n/).map(function(line) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) {
|
||||
return '';
|
||||
}
|
||||
if (includeSpecial) {
|
||||
return trimmed.charAt(0);
|
||||
}
|
||||
const match = trimmed.match(/[a-zA-Z0-9]/);
|
||||
return match ? match[0] : '';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function normalizeSecret(text, includeSpecial) {
|
||||
if (includeSpecial) {
|
||||
return text.replace(/\s+/g, '');
|
||||
}
|
||||
return text.replace(/[^a-zA-Z0-9]/g, '');
|
||||
}
|
||||
|
||||
function formatAcrosticLine(letter, template) {
|
||||
const line = template.replace(/\{letter\}/g, letter);
|
||||
if (/[a-zA-Z]/.test(letter)) {
|
||||
return line.charAt(0).toUpperCase() + line.slice(1);
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Acrostic',
|
||||
priority: 46,
|
||||
category: 'concealment',
|
||||
description: 'Extract hidden messages from the first letter of each line or word, or build acrostic cover text.',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'mode',
|
||||
label: 'Read letters from',
|
||||
type: 'select',
|
||||
default: 'lines',
|
||||
options: [
|
||||
{ value: 'lines', label: 'First letter of each line' },
|
||||
{ value: 'words', label: 'First letter of each word' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'includeSpecial',
|
||||
label: 'Include special characters',
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
{
|
||||
id: 'lineTemplate',
|
||||
label: 'Custom line template ({letter} placeholder)',
|
||||
type: 'text',
|
||||
default: ''
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const mode = options.mode === 'words' ? 'words' : 'lines';
|
||||
const includeSpecial = !!options.includeSpecial;
|
||||
const customTemplate = String(options.lineTemplate || '').trim();
|
||||
const secret = normalizeSecret(text, includeSpecial);
|
||||
if (!secret) {
|
||||
return text;
|
||||
}
|
||||
if (mode === 'words') {
|
||||
return secret.split('').join(' ');
|
||||
}
|
||||
return secret.split('').map(function(letter, index) {
|
||||
const template = customTemplate || TEMPLATES[index % TEMPLATES.length];
|
||||
return formatAcrosticLine(letter, template);
|
||||
}).join('\n');
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
options = options || {};
|
||||
const mode = options.mode === 'words' ? 'words' : 'lines';
|
||||
const includeSpecial = !!options.includeSpecial;
|
||||
return extractAcrostic(text, mode, includeSpecial);
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) {
|
||||
return '[acrostic]';
|
||||
}
|
||||
return this.func(text.slice(0, 5), options);
|
||||
},
|
||||
detector: function(text) {
|
||||
if (/\n/.test(text)) {
|
||||
const lines = text.split(/\r?\n/).filter(function(line) {
|
||||
return line.trim().length > 0;
|
||||
});
|
||||
return lines.length >= 2;
|
||||
}
|
||||
return (text.match(/[a-zA-Z0-9']+/g) || []).length >= 3;
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,208 @@
|
||||
// Cardan grille — write/read through a rotating hole template
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
const DEFAULT_GRILLE = '#.#.\n.#.#\n#.#.\n.#.#';
|
||||
const DEFAULT_FILLER = 'abcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
function parseGrille(pattern, size) {
|
||||
const rows = String(pattern || DEFAULT_GRILLE)
|
||||
.trim()
|
||||
.split(/\r?\n/)
|
||||
.map(function(row) { return row.trim(); })
|
||||
.filter(function(row) { return row.length > 0; });
|
||||
|
||||
const holes = [];
|
||||
for (let r = 0; r < rows.length && r < size; r++) {
|
||||
for (let c = 0; c < rows[r].length && c < size; c++) {
|
||||
const ch = rows[r][c];
|
||||
if (ch === '#' || ch === 'X' || ch === 'x' || ch === '1') {
|
||||
holes.push([r, c]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return holes.sort(function(a, b) {
|
||||
return a[0] - b[0] || a[1] - b[1];
|
||||
});
|
||||
}
|
||||
|
||||
function rotateCoord(r, c, size) {
|
||||
return [c, size - 1 - r];
|
||||
}
|
||||
|
||||
function rotateHoles(holes, size) {
|
||||
return holes.map(function(pair) {
|
||||
return rotateCoord(pair[0], pair[1], size);
|
||||
}).sort(function(a, b) {
|
||||
return a[0] - b[0] || a[1] - b[1];
|
||||
});
|
||||
}
|
||||
|
||||
function makeGrid(size, filler) {
|
||||
const grid = [];
|
||||
for (let r = 0; r < size; r++) {
|
||||
grid.push(new Array(size).fill(''));
|
||||
}
|
||||
return grid;
|
||||
}
|
||||
|
||||
function flattenGrid(grid) {
|
||||
return grid.map(function(row) { return row.join(''); }).join('\n');
|
||||
}
|
||||
|
||||
function parseGrid(text, size) {
|
||||
const cleaned = text.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
|
||||
const grid = makeGrid(size);
|
||||
let index = 0;
|
||||
for (let r = 0; r < size; r++) {
|
||||
for (let c = 0; c < size; c++) {
|
||||
grid[r][c] = cleaned[index] || 'x';
|
||||
index++;
|
||||
}
|
||||
}
|
||||
return grid;
|
||||
}
|
||||
|
||||
function holeSets(baseHoles, size, rotations) {
|
||||
const sets = [baseHoles];
|
||||
let current = baseHoles;
|
||||
for (let i = 1; i < rotations; i++) {
|
||||
current = rotateHoles(current, size);
|
||||
sets.push(current);
|
||||
}
|
||||
return sets;
|
||||
}
|
||||
|
||||
function nextFillerChar(filler, index) {
|
||||
return filler[index % filler.length] || 'x';
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Cardan Grille',
|
||||
priority: 44,
|
||||
category: 'concealment',
|
||||
description: 'Hide text using a Cardan grille template with optional 90° rotations.',
|
||||
inputKind: 'textarea',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'gridSize',
|
||||
label: 'Grid size',
|
||||
type: 'number',
|
||||
default: 4,
|
||||
min: 3,
|
||||
max: 8,
|
||||
step: 1
|
||||
},
|
||||
{
|
||||
id: 'rotations',
|
||||
label: 'Grille rotations',
|
||||
type: 'select',
|
||||
default: '4',
|
||||
options: [
|
||||
{ value: '1', label: '1 (single pass)' },
|
||||
{ value: '4', label: '4 (classic Cardan)' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'grille',
|
||||
label: 'Grille pattern (# = hole, . = cover)',
|
||||
type: 'text',
|
||||
default: DEFAULT_GRILLE
|
||||
},
|
||||
{
|
||||
id: 'filler',
|
||||
label: 'Decoy letters for empty cells',
|
||||
type: 'text',
|
||||
default: DEFAULT_FILLER
|
||||
},
|
||||
{
|
||||
id: 'outputFormat',
|
||||
label: 'Output layout',
|
||||
type: 'select',
|
||||
default: 'grid',
|
||||
options: [
|
||||
{ value: 'grid', label: 'Grid with newlines' },
|
||||
{ value: 'flat', label: 'Continuous string' }
|
||||
]
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const size = Math.max(3, Math.min(8, parseInt(options.gridSize, 10) || 4));
|
||||
const rotations = Math.max(1, Math.min(4, parseInt(options.rotations, 10) || 4));
|
||||
const filler = String(options.filler || DEFAULT_FILLER).replace(/[^a-zA-Z0-9]/g, '').toLowerCase() || DEFAULT_FILLER;
|
||||
const message = text.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
|
||||
if (!message) {
|
||||
return text;
|
||||
}
|
||||
|
||||
const baseHoles = parseGrille(options.grille || DEFAULT_GRILLE, size);
|
||||
const sets = holeSets(baseHoles, size, rotations);
|
||||
const grid = makeGrid(size);
|
||||
let msgIndex = 0;
|
||||
let fillerIndex = 0;
|
||||
|
||||
sets.forEach(function(holes) {
|
||||
holes.forEach(function(pair) {
|
||||
const r = pair[0];
|
||||
const c = pair[1];
|
||||
if (!grid[r][c]) {
|
||||
grid[r][c] = message[msgIndex] || nextFillerChar(filler, fillerIndex++);
|
||||
if (message[msgIndex]) {
|
||||
msgIndex++;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
for (let r = 0; r < size; r++) {
|
||||
for (let c = 0; c < size; c++) {
|
||||
if (!grid[r][c]) {
|
||||
grid[r][c] = nextFillerChar(filler, fillerIndex++);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (options.outputFormat === 'flat') {
|
||||
return grid.map(function(row) { return row.join(''); }).join('');
|
||||
}
|
||||
return flattenGrid(grid);
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
options = options || {};
|
||||
const size = Math.max(3, Math.min(8, parseInt(options.gridSize, 10) || 4));
|
||||
const rotations = Math.max(1, Math.min(4, parseInt(options.rotations, 10) || 4));
|
||||
const baseHoles = parseGrille(options.grille || DEFAULT_GRILLE, size);
|
||||
const sets = holeSets(baseHoles, size, rotations);
|
||||
const grid = parseGrid(text, size);
|
||||
let result = '';
|
||||
|
||||
sets.forEach(function(holes) {
|
||||
holes.forEach(function(pair) {
|
||||
result += grid[pair[0]][pair[1]] || '';
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) {
|
||||
return '[cardan]';
|
||||
}
|
||||
return this.func(text.slice(0, 8), options);
|
||||
},
|
||||
detector: function(text) {
|
||||
const lines = text.trim().split(/\r?\n/);
|
||||
if (lines.length < 3) {
|
||||
return false;
|
||||
}
|
||||
const widths = lines.map(function(line) {
|
||||
return line.replace(/\s/g, '').length;
|
||||
});
|
||||
const first = widths[0];
|
||||
return first >= 3 && widths.every(function(width) {
|
||||
return width === first;
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,34 @@
|
||||
// Homoglyph substitution — visually confusable Unicode characters
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
const TO = {
|
||||
'A': '\u0410', 'B': '\u0412', 'C': '\u0421', 'E': '\u0415', 'H': '\u041D',
|
||||
'I': '\u0406', 'J': '\u0408', 'K': '\u041A', 'M': '\u041C', 'N': '\u041D',
|
||||
'O': '\u041E', 'P': '\u0420', 'S': '\u0405', 'T': '\u0422', 'X': '\u0425',
|
||||
'Y': '\u0423', 'a': '\u0430', 'c': '\u0441', 'e': '\u0435', 'i': '\u0456',
|
||||
'j': '\u0458', 'o': '\u043E', 'p': '\u0440', 's': '\u0455', 'x': '\u0445',
|
||||
'y': '\u0443'
|
||||
};
|
||||
const REV = {};
|
||||
for (const [k, v] of Object.entries(TO)) REV[v] = k;
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Homoglyph Generator',
|
||||
priority: 70,
|
||||
category: 'unicode',
|
||||
func: function(text) {
|
||||
return [...text].map(c => TO[c] || c).join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
return [...text].map(c => REV[c] || c).join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[homoglyph]';
|
||||
return this.func(text.slice(0, 12));
|
||||
},
|
||||
detector: function(text) {
|
||||
return /[\u0400-\u04FF]/.test(text) && /[A-Za-z]/.test(text) === false;
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,154 @@
|
||||
// Null cipher — hide/extract letters at a fixed position in each word
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
function syntheticWord(letter, position) {
|
||||
const L = letter.toLowerCase();
|
||||
const pads = ['a', 'e', 'i', 'o', 'u', 'r', 's', 't', 'n', 'l'];
|
||||
let word = '';
|
||||
const len = Math.max(position + 1, 3);
|
||||
for (let i = 0; i < len; i++) {
|
||||
if (i === position - 1) {
|
||||
word += L;
|
||||
} else {
|
||||
word += pads[i % pads.length];
|
||||
}
|
||||
}
|
||||
return word;
|
||||
}
|
||||
|
||||
function pickCoverWord(letter, position, index, coverWords) {
|
||||
const target = letter.toLowerCase();
|
||||
const pos = position - 1;
|
||||
if (coverWords && coverWords.length) {
|
||||
for (let i = 0; i < coverWords.length; i++) {
|
||||
const word = coverWords[(index + i) % coverWords.length];
|
||||
const clean = word.replace(/[^a-zA-Z0-9]/g, '');
|
||||
if (clean[pos] && clean[pos].toLowerCase() === target) {
|
||||
return word;
|
||||
}
|
||||
}
|
||||
}
|
||||
return syntheticWord(letter, position);
|
||||
}
|
||||
|
||||
function normalizeSecret(text, includeSpecial) {
|
||||
if (includeSpecial) {
|
||||
return text.replace(/\s+/g, '');
|
||||
}
|
||||
return text.replace(/[^a-zA-Z0-9]/g, '');
|
||||
}
|
||||
|
||||
function extractNull(text, options) {
|
||||
options = options || {};
|
||||
const scope = options.scope === 'text' ? 'text' : 'words';
|
||||
const includeSpecial = !!options.includeSpecial;
|
||||
const position = Math.max(1, Math.min(10, parseInt(options.position, 10) || 2));
|
||||
|
||||
if (scope === 'text') {
|
||||
const source = includeSpecial ? text.replace(/\s/g, '') : text.replace(/[^a-zA-Z0-9]/g, '');
|
||||
let out = '';
|
||||
for (let i = position - 1; i < source.length; i += position) {
|
||||
out += source[i];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const pos = position - 1;
|
||||
if (includeSpecial) {
|
||||
const words = text.match(/\S+/g) || [];
|
||||
return words.map(function(word) {
|
||||
return word.charAt(pos) || '';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
const words = text.match(/[a-zA-Z0-9']+/g) || [];
|
||||
return words.map(function(word) {
|
||||
const clean = word.replace(/[^a-zA-Z0-9]/g, '');
|
||||
if (pos >= 0 && pos < clean.length) {
|
||||
return clean[pos];
|
||||
}
|
||||
return '';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Null Cipher',
|
||||
priority: 45,
|
||||
category: 'concealment',
|
||||
description: 'Conceal a message in cover text by fixing one letter position in each word, or extract hidden letters.',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'scope',
|
||||
label: 'Extraction scope',
|
||||
type: 'select',
|
||||
default: 'words',
|
||||
options: [
|
||||
{ value: 'words', label: 'Nth letter of each word' },
|
||||
{ value: 'text', label: 'Every Nth letter of full text' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'position',
|
||||
label: 'Letter position / interval (1 = first letter or every letter)',
|
||||
type: 'number',
|
||||
default: 2,
|
||||
min: 1,
|
||||
max: 10,
|
||||
step: 1
|
||||
},
|
||||
{
|
||||
id: 'includeSpecial',
|
||||
label: 'Include special characters',
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
{
|
||||
id: 'coverText',
|
||||
label: 'Cover word pool (optional, space-separated)',
|
||||
type: 'text',
|
||||
default: ''
|
||||
},
|
||||
{
|
||||
id: 'wordSeparator',
|
||||
label: 'Word separator',
|
||||
type: 'select',
|
||||
default: ' ',
|
||||
options: [
|
||||
{ value: ' ', label: 'Space' },
|
||||
{ value: '\n', label: 'New line' },
|
||||
{ value: ', ', label: 'Comma' }
|
||||
]
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const position = Math.max(1, Math.min(10, parseInt(options.position, 10) || 2));
|
||||
const sep = options.wordSeparator != null ? String(options.wordSeparator) : ' ';
|
||||
const includeSpecial = !!options.includeSpecial;
|
||||
const secret = normalizeSecret(text, includeSpecial);
|
||||
if (!secret) {
|
||||
return text;
|
||||
}
|
||||
const coverWords = String(options.coverText || '')
|
||||
.match(/[a-zA-Z0-9']+/g) || [];
|
||||
const words = secret.split('').map(function(ch, index) {
|
||||
return pickCoverWord(ch, position, index, coverWords);
|
||||
});
|
||||
return words.join(sep);
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
return extractNull(text, options || {});
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) {
|
||||
return '[null-cipher]';
|
||||
}
|
||||
return this.func(text.slice(0, 8), options) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
const words = text.match(/[a-zA-Z0-9']+/g) || [];
|
||||
return words.length >= 3 && words.length <= 500;
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,133 @@
|
||||
// Trevanion cipher — extract letters N positions after punctuation (steganography)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
const DEFAULT_TRIGGERS = '.,!?;:\u2014';
|
||||
const DEFAULT_FILLER = 'the quick brown fox jumps over the lazy dog while a warm wind blew across the open field';
|
||||
|
||||
function isTrigger(ch, triggers) {
|
||||
return triggers.includes(ch);
|
||||
}
|
||||
|
||||
function extractLetter(text, start, offset, lettersOnly) {
|
||||
let count = 0;
|
||||
for (let i = start; i < text.length; i++) {
|
||||
const c = text[i];
|
||||
if (lettersOnly) {
|
||||
if (/[A-Za-z]/.test(c)) count++;
|
||||
} else if (!/\s/.test(c)) {
|
||||
count++;
|
||||
}
|
||||
if (count === offset) return c;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function postTriggerSnippet(letter, offset) {
|
||||
const pads = 'etaoinshrdlucmfwypvbgkqjxz';
|
||||
let out = '';
|
||||
for (let i = 1; i <= offset; i++) {
|
||||
if (i === offset) {
|
||||
out += letter;
|
||||
} else {
|
||||
out += pads[(letter.charCodeAt(0) + i * 3) % pads.length];
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function buildCover(secret, options) {
|
||||
const offset = Math.max(1, Number(options.offset) || 3);
|
||||
const triggers = options.triggers || DEFAULT_TRIGGERS;
|
||||
const trigger = triggers.charAt(0) || ',';
|
||||
const coverWords = String(options.fillerWords || DEFAULT_FILLER)
|
||||
.match(/[a-zA-Z']+/g) || DEFAULT_FILLER.split(/\s+/);
|
||||
let out = '';
|
||||
for (let si = 0; si < secret.length; si++) {
|
||||
if (si > 0) out += ' ';
|
||||
out += coverWords[si % coverWords.length];
|
||||
out += trigger;
|
||||
out += postTriggerSnippet(secret[si], offset);
|
||||
}
|
||||
return out.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Trevanion Cipher',
|
||||
priority: 75,
|
||||
category: 'concealment',
|
||||
description: 'Hide letters N positions after punctuation marks, or extract them from cover text.',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'offset',
|
||||
label: 'Letters after each mark (N)',
|
||||
type: 'number',
|
||||
default: 3,
|
||||
min: 1,
|
||||
max: 20
|
||||
},
|
||||
{
|
||||
id: 'lettersOnly',
|
||||
label: 'Count letters only (ignore spaces/punct)',
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
{
|
||||
id: 'triggers',
|
||||
label: 'Trigger punctuation characters',
|
||||
type: 'text',
|
||||
default: '.,!?;:'
|
||||
},
|
||||
{
|
||||
id: 'mode',
|
||||
label: 'Encode mode',
|
||||
type: 'select',
|
||||
default: 'embed',
|
||||
options: [
|
||||
{ value: 'embed', label: 'Build cover text around secret' },
|
||||
{ value: 'decode', label: 'Extract only (input is cover text)' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'fillerWords',
|
||||
label: 'Cover word pool (space-separated)',
|
||||
type: 'text',
|
||||
default: ''
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
if (options.mode === 'decode') return text;
|
||||
const secret = text.replace(/\s/g, '');
|
||||
if (!secret) return text;
|
||||
return buildCover(secret, options);
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
options = options || {};
|
||||
const offset = Number(options.offset) || 3;
|
||||
const triggers = options.triggers || DEFAULT_TRIGGERS;
|
||||
const lettersOnly = options.lettersOnly !== false;
|
||||
let out = '';
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
if (isTrigger(text[i], triggers)) {
|
||||
const ch = extractLetter(text, i + 1, offset, lettersOnly);
|
||||
if (ch) out += ch;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[trevanion]';
|
||||
const dec = this.reverse(text, options);
|
||||
return dec ? dec.slice(0, 16) + '...' : this.func(text.slice(0, 8), options).slice(0, 24) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
const triggers = DEFAULT_TRIGGERS;
|
||||
let hits = 0;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
if (isTrigger(text[i], triggers)) hits++;
|
||||
}
|
||||
return hits >= 3;
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,74 @@
|
||||
// Bibi-binary (Boby Lapointe) — hex syllables for text via UTF-8 bytes
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
const HEX_TO_BIBI = {
|
||||
'0': 'HO', '1': 'HA', '2': 'HE', '3': 'HI', '4': 'BO', '5': 'BA',
|
||||
'6': 'BE', '7': 'BI', '8': 'KO', '9': 'KA', 'a': 'KE', 'b': 'KI',
|
||||
'c': 'DO', 'd': 'DA', 'e': 'DE', 'f': 'DI'
|
||||
};
|
||||
const BIBI_TO_HEX = {};
|
||||
for (const [h, b] of Object.entries(HEX_TO_BIBI)) BIBI_TO_HEX[b.toUpperCase()] = h;
|
||||
|
||||
function parseBibiToken(t) {
|
||||
return BIBI_TO_HEX[t.toUpperCase()] || null;
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Bibi-binary Code',
|
||||
priority: 85,
|
||||
category: 'encoding',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'separator',
|
||||
label: 'Separator between syllable pairs',
|
||||
type: 'select',
|
||||
default: 'space',
|
||||
options: [
|
||||
{ value: 'space', label: 'Space' },
|
||||
{ value: 'none', label: 'None' }
|
||||
]
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const sep = options.separator === 'none' ? '' : ' ';
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
const hex = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
const pairs = [];
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
const pair = hex.slice(i, i + 2);
|
||||
pairs.push(HEX_TO_BIBI[pair[0]] + HEX_TO_BIBI[pair[1]]);
|
||||
}
|
||||
return pairs.join(sep);
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
options = options || {};
|
||||
const tokens = options.separator === 'none'
|
||||
? text.match(/[A-Za-z]{4}/g) || []
|
||||
: text.trim().split(/\s+/);
|
||||
let hex = '';
|
||||
for (const tok of tokens) {
|
||||
if (tok.length !== 4) continue;
|
||||
const h1 = parseBibiToken(tok.slice(0, 2));
|
||||
const h2 = parseBibiToken(tok.slice(2, 4));
|
||||
if (h1 && h2) hex += h1 + h2;
|
||||
}
|
||||
if (!hex || hex.length % 2 !== 0) return text;
|
||||
const bytes = new Uint8Array(hex.match(/.{2}/g).map(h => parseInt(h, 16)));
|
||||
try {
|
||||
return new TextDecoder().decode(bytes);
|
||||
} catch (e) {
|
||||
return text;
|
||||
}
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[bibi]';
|
||||
return this.func(text.slice(0, 3), options);
|
||||
},
|
||||
detector: function(text) {
|
||||
const tokens = text.trim().split(/\s+/);
|
||||
return tokens.length >= 2 && tokens.every(t => /^[A-Za-z]{4}$/.test(t));
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,86 @@
|
||||
// Decabit code (10-pulse ripple control encoding, ASCII 0-126)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
// Patterns use + (positive pulse) and - (negative pulse); index = code value
|
||||
const PATTERNS = [
|
||||
'--+-+++-+-', '+--+++--+-', '+--++-+-+-', '+--+-++-+-', '----+++-++',
|
||||
'++--+++---', '++--++--+-', '++--+-+-+-', '++---++-+-', '---++++-+-',
|
||||
'+-+-+++---', '+-+-+-+-+-', '+-+--++-+-', '+---++-++-', '+---++--++',
|
||||
'--+++-++--', '---++-+++-', '+---+-++-+', '+--++--+-+', '+--++-+--+',
|
||||
'+-+++--+--', '+--+++-+--', '++--+-++--', '-+-++-++--', '+--++--++-',
|
||||
'+-+++-+---', '++-+--++--', '+-+-+-++--', '+--+-+++--', '+--+--++-+',
|
||||
'+-++-++---', '+-++-+-+--', '+-+-++-+--', '+---++++--', '+-+--+-++-',
|
||||
'+++--++---', '+++--+-+--', '+++---++--', '++---+++--', '--+-++++--',
|
||||
'++--++-+--', '-+-+-+-++-', '++----+++-', '+----+-+++', '++---+-+-+',
|
||||
'++-+-+-+--', '++-+-+--+-', '+++----++-', '++--+--++-', '+--+-+-++-',
|
||||
'++++----+-', '++-++---+-', '+-+++---+-', '-++++---+-', '+-+-+---++',
|
||||
'+++-++----', '+++-+-+---', '+-+-+--++-', '-++-+--++-', '+++-+----',
|
||||
'-+++-++---', '-+-+-++-+-', '++---++--+', '++-+--+--+', '++-+++----',
|
||||
'++++--+---', '+--++++---', '-+-++++---', '++-+--+-+-', '-++---+++-',
|
||||
'+---+-+++-', '--+-+-+++-', '+----++++-', '--+--++++-', '+++---+-+-',
|
||||
'+-++---++-', '+--+--+++-', '--++--+++-', '-+-+---+-++', '-+++--+-+-',
|
||||
'-+-++-+-+-', '-+++---++-', '-+-++--++-', '-+---++++-', '-++++--+--',
|
||||
'-++-++-+--', '--++++-+--', '--++-+++--', '--++-+-++-', '-++++----',
|
||||
'--++++--+-', '--++-++-+-', '+-++----++', '-+-+++--+-', '-++-+-+-+-',
|
||||
'-+--++-++-', '---+++-++-', '-+--+-+++-', '+---+++-+-', '-+--+++-+-',
|
||||
'+-+-++--+-', '+--++-++--', '++-++--+--', '+-++--++--', '+-+--+++--',
|
||||
'-++--+++--', '++---+-++-', '++-+---++-', '+++-+---+-', '+++-+--+--',
|
||||
'++-+-++---', '++-++-+---', '+-+---+++-', '+-++--+-+-', '-+-+--+++-',
|
||||
'-+++-+-+--', '+-++-+--+-', '-++-+++---', '+++--+--+-', '+++++-----',
|
||||
'-+++++----', '--+++++---', '---+++++--', '----+++++-', '++++++++++'
|
||||
];
|
||||
const TO_CODE = {};
|
||||
const TO_CHAR = {};
|
||||
for (let i = 0; i < PATTERNS.length; i++) {
|
||||
TO_CODE[i] = PATTERNS[i];
|
||||
TO_CHAR[PATTERNS[i]] = String.fromCharCode(i);
|
||||
}
|
||||
|
||||
function normalizePattern(raw) {
|
||||
return raw.replace(/[+]/g, '+').replace(/[-−–—]/g, '-').replace(/\s/g, '');
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Decabit Code',
|
||||
priority: 90,
|
||||
category: 'electronics',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'groupSeparator',
|
||||
label: 'Separator between pulse groups',
|
||||
type: 'select',
|
||||
default: 'space',
|
||||
options: [
|
||||
{ value: 'space', label: 'Space' },
|
||||
{ value: 'newline', label: 'New line' },
|
||||
{ value: 'none', label: 'None' }
|
||||
]
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const sep = options.groupSeparator === 'newline' ? '\n'
|
||||
: (options.groupSeparator === 'none' ? '' : ' ');
|
||||
return [...text].map(c => TO_CODE[c.charCodeAt(0)] || TO_CODE[63]).join(sep);
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
options = options || {};
|
||||
const splitRe = options.groupSeparator === 'newline' ? /\n+/
|
||||
: (options.groupSeparator === 'none' ? /(?=[+-]{10})/ : /\s+/);
|
||||
const groups = text.trim().split(splitRe).filter(Boolean);
|
||||
return groups.map(g => {
|
||||
const pat = normalizePattern(g);
|
||||
return TO_CHAR[pat] || '';
|
||||
}).join('');
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[decabit]';
|
||||
return this.func(text.slice(0, 3), options) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
const groups = text.trim().split(/\s+/);
|
||||
return groups.length >= 2 && groups.every(g => /^[+-]{10}$/.test(normalizePattern(g)));
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -1,79 +1,113 @@
|
||||
// emoji encoding transform
|
||||
// Base256Emoji — multiformats/multibase (1 byte ↔ 1 emoji, lossless)
|
||||
// https://github.com/multiformats/multibase/blob/master/rfcs/Base256Emoji.md
|
||||
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;
|
||||
}
|
||||
});
|
||||
export default (function() {
|
||||
// Official 256-rune table (go-multibase / base256emoji crate)
|
||||
const ALPHABET = '🚀🪐☄🛰🌌🌑🌒🌓🌔🌕🌖🌗🌘🌍🌏🌎🐉☀💻🖥💾💿😂❤😍🤣😊🙏💕😭😘👍😅👏😁🔥🥰💔💖💙😢🤔😆🙄💪😉☺👌🤗💜😔😎😇🌹🤦🎉💞✌✨🤷😱😌🌸🙌😋💗💚😏💛🙂💓🤩😄😀🖤😃💯🙈👇🎶😒🤭❣😜💋👀😪😑💥🙋😞😩😡🤪👊🥳😥🤤👉💃😳✋😚😝😴🌟😬🙃🍀🌷😻😓⭐✅🥺🌈😈🤘💦✔😣🏃💐☹🎊💘😠☝😕🌺🎂🌻😐🖕💝🙊😹🗣💫💀👑🎵🤞😛🔴😤🌼😫⚽🤙☕🏆🤫👈😮🙆🍻🍃🐶💁😲🌿🧡🎁⚡🌞🎈❌✊👋😰🤨😶🤝🚶💰🍓💢🤟🙁🚨💨🤬✈🎀🍺🤓😙💟🌱😖👶🥴▶➡❓💎💸⬇😨🌚🦋😷🕺⚠🙅😟😵👎🤲🤠🤧📌🔵💅🧐🐾🍒😗🤑🌊🤯🐷☎💧😯💆👆🎤🙇🍑❄🌴💣🐸💌📍🥀🤢👅💡💩👐📸👻🤐🤮🎼🥵🚩🍎🍊👼💍📣🥂';
|
||||
const TABLE = [...ALPHABET];
|
||||
const MULTIBASE_PREFIX = TABLE[0]; // U+1F680 🚀 — multibase code for this encoding
|
||||
|
||||
if (TABLE.length !== 256) {
|
||||
throw new Error('Base256Emoji alphabet must contain exactly 256 codepoints');
|
||||
}
|
||||
|
||||
const REVERSE = new Map(TABLE.map(function(emoji, index) {
|
||||
return [emoji, index];
|
||||
}));
|
||||
|
||||
function encodeBytes(bytes, multibase) {
|
||||
const emojis = Array.from(bytes, function(byte) {
|
||||
return TABLE[byte];
|
||||
});
|
||||
if (multibase) {
|
||||
return MULTIBASE_PREFIX + emojis.join('');
|
||||
}
|
||||
return emojis.join('');
|
||||
}
|
||||
|
||||
function decodeToBytes(text, multibase) {
|
||||
let runes = [...String(text).replace(/\s/g, '')];
|
||||
if (multibase && runes[0] === MULTIBASE_PREFIX) {
|
||||
runes = runes.slice(1);
|
||||
}
|
||||
if (runes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const bytes = new Uint8Array(runes.length);
|
||||
for (let i = 0; i < runes.length; i++) {
|
||||
const value = REVERSE.get(runes[i]);
|
||||
if (value === undefined) {
|
||||
return null;
|
||||
}
|
||||
bytes[i] = value;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Base256Emoji',
|
||||
priority: 265,
|
||||
category: 'encoding',
|
||||
description: 'Multiformats Base256Emoji encoding (multibase): each byte maps to one emoji from the standard 256-codepoint alphabet.',
|
||||
configurableOptions: [
|
||||
{
|
||||
key: 'multibase',
|
||||
label: 'Multibase prefix',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
help: 'Prepend the 🚀 multibase code on encode; strip it on decode (interoperable with IPFS/multibase tools).'
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
const multibase = !!(options && options.multibase);
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
return encodeBytes(bytes, multibase);
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
const multibase = !!(options && options.multibase);
|
||||
let bytes = decodeToBytes(text, multibase);
|
||||
if (!bytes && !multibase) {
|
||||
bytes = decodeToBytes(text, true);
|
||||
}
|
||||
if (!bytes) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) {
|
||||
return '[base256emoji]';
|
||||
}
|
||||
return this.func(text.slice(0, 4), options);
|
||||
},
|
||||
detector: function(text) {
|
||||
const runes = [...String(text).replace(/\s/g, '')];
|
||||
if (runes.length < 3) {
|
||||
return false;
|
||||
}
|
||||
let start = 0;
|
||||
if (runes[0] === MULTIBASE_PREFIX && runes.length > 3) {
|
||||
start = 1;
|
||||
}
|
||||
let hits = 0;
|
||||
for (let i = start; i < runes.length; i++) {
|
||||
if (REVERSE.has(runes[i])) {
|
||||
hits++;
|
||||
}
|
||||
}
|
||||
const checked = runes.length - start;
|
||||
return checked >= 3 && hits / checked >= 0.85;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// Manchester line code (IEEE 802.3: 0=10, 1=01)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
function toBits(text) {
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
return Array.from(bytes).map(b => b.toString(2).padStart(8, '0')).join('');
|
||||
}
|
||||
|
||||
function fromBits(bits) {
|
||||
const bytes = [];
|
||||
for (let i = 0; i + 8 <= bits.length; i += 8) {
|
||||
bytes.push(parseInt(bits.slice(i, i + 8), 2));
|
||||
}
|
||||
try {
|
||||
return new TextDecoder().decode(new Uint8Array(bytes));
|
||||
} catch (e) {
|
||||
return bits;
|
||||
}
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Manchester Code',
|
||||
priority: 95,
|
||||
category: 'technical',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'spacing',
|
||||
label: 'Space between bit pairs',
|
||||
type: 'boolean',
|
||||
default: true
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const pairs = [...toBits(text)].map(b => (b === '0' ? '10' : '01'));
|
||||
return options.spacing ? pairs.join(' ') : pairs.join('');
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
options = options || {};
|
||||
const cleaned = text.replace(/\s/g, '');
|
||||
if (!/^[01]+$/.test(cleaned) || cleaned.length % 2 !== 0) return text;
|
||||
let bits = '';
|
||||
for (let i = 0; i < cleaned.length; i += 2) {
|
||||
const pair = cleaned.slice(i, i + 2);
|
||||
if (pair === '10') bits += '0';
|
||||
else if (pair === '01') bits += '1';
|
||||
else return text;
|
||||
}
|
||||
return fromBits(bits);
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[manchester]';
|
||||
const out = this.func(text.slice(0, 2), options);
|
||||
return out.length > 24 ? out.slice(0, 24) + '...' : out;
|
||||
},
|
||||
detector: function(text) {
|
||||
const cleaned = text.replace(/\s/g, '');
|
||||
return cleaned.length >= 16 && /^[01]+$/.test(cleaned) && cleaned.length % 2 === 0
|
||||
&& /^((10|01)+)$/.test(cleaned);
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,214 @@
|
||||
// Metaphone phonetic encoding
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
function metaphoneWord(word) {
|
||||
let w = String(word).toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (!w) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const first = w.charAt(0);
|
||||
if (/^[AEIOU]/.test(w)) {
|
||||
w = w.replace(/^([AEIOU])/, 'A');
|
||||
} else if (w.startsWith('KN') || w.startsWith('GN') || w.startsWith('PN') || w.startsWith('WR') || w.startsWith('AE')) {
|
||||
w = w.slice(1);
|
||||
}
|
||||
|
||||
let out = first === 'A' && /^[AEIOU]/.test(String(word).toUpperCase()) ? 'A' : first;
|
||||
let i = 1;
|
||||
|
||||
while (i < w.length && out.length < 4) {
|
||||
const ch = w.charAt(i);
|
||||
const next = w.charAt(i + 1);
|
||||
const prev = w.charAt(i - 1);
|
||||
|
||||
if ('AEIOU'.indexOf(ch) >= 0) {
|
||||
if (i === 0 || prev === 'A') {
|
||||
out += 'A';
|
||||
}
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === 'B') {
|
||||
out += 'P';
|
||||
i += next === 'B' ? 2 : 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === 'C') {
|
||||
if (next === 'I' || next === 'E' || next === 'Y') {
|
||||
out += 'S';
|
||||
} else {
|
||||
out += 'K';
|
||||
}
|
||||
i += next === 'H' && (next === 'I' || next === 'E' || next === 'Y') ? 2 : 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === 'D') {
|
||||
out += next === 'G' && 'EIY'.indexOf(w.charAt(i + 2)) >= 0 ? 'J' : 'T';
|
||||
i += next === 'G' ? 3 : 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === 'F') {
|
||||
out += 'F';
|
||||
i += next === 'F' ? 2 : 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === 'G') {
|
||||
if (next === 'H' && 'EIY'.indexOf(w.charAt(i + 2)) < 0) {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (next === 'N' && w.charAt(i + 2) === 'E' && w.charAt(i + 3) === 'D') {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
out += 'K';
|
||||
i += next === 'G' ? 2 : 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === 'H') {
|
||||
if ('AEIOU'.indexOf(prev) >= 0 && 'AEIOU'.indexOf(next) >= 0) {
|
||||
out += 'H';
|
||||
}
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (ch === 'J') {
|
||||
out += 'J';
|
||||
i += next === 'J' ? 2 : 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === 'K') {
|
||||
out += 'K';
|
||||
i += next === 'K' ? 2 : 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === 'L') {
|
||||
out += 'L';
|
||||
i += next === 'L' ? 2 : 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === 'M') {
|
||||
out += 'M';
|
||||
i += next === 'M' ? 2 : 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === 'N') {
|
||||
out += 'N';
|
||||
i += next === 'N' ? 2 : 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === 'P') {
|
||||
out += next === 'H' ? 'F' : 'P';
|
||||
i += next === 'H' ? 2 : next === 'P' ? 2 : 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === 'Q') {
|
||||
out += 'K';
|
||||
i += next === 'Q' ? 2 : 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === 'R') {
|
||||
out += 'R';
|
||||
i += next === 'R' ? 2 : 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === 'S') {
|
||||
out += next === 'H' ? 'X' : 'S';
|
||||
i += next === 'H' ? 2 : next === 'S' ? 2 : 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === 'T') {
|
||||
if (next === 'I' && 'AO'.indexOf(w.charAt(i + 2)) >= 0) {
|
||||
out += 'X';
|
||||
} else if (next === 'H') {
|
||||
out += '0';
|
||||
} else if (next === 'C' && w.charAt(i + 2) === 'H') {
|
||||
out += 'X';
|
||||
} else {
|
||||
out += 'T';
|
||||
}
|
||||
i += next === 'H' || (next === 'C' && w.charAt(i + 2) === 'H') ? 2 : next === 'T' ? 2 : 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === 'V') {
|
||||
out += 'F';
|
||||
i += next === 'V' ? 2 : 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === 'W') {
|
||||
if ('AEIOU'.indexOf(next) >= 0) {
|
||||
out += 'W';
|
||||
}
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (ch === 'X') {
|
||||
out += 'KS';
|
||||
i += next === 'X' ? 2 : 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === 'Y') {
|
||||
if ('AEIOU'.indexOf(next) >= 0) {
|
||||
out += 'Y';
|
||||
}
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (ch === 'Z') {
|
||||
out += 'S';
|
||||
i += next === 'Z' ? 2 : 1;
|
||||
continue;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
return out.slice(0, 4);
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Metaphone',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
canDecode: false,
|
||||
description: 'Phonetic encoding for English words (Metaphone algorithm).',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'separator',
|
||||
label: 'Word separator',
|
||||
type: 'select',
|
||||
default: ' ',
|
||||
options: [
|
||||
{ value: ' ', label: 'Space' },
|
||||
{ value: '-', label: 'Dash' },
|
||||
{ value: ',', label: 'Comma' },
|
||||
{ value: '', label: 'None' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'uppercase',
|
||||
label: 'Uppercase output',
|
||||
type: 'boolean',
|
||||
default: true
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const separator = options.separator != null ? String(options.separator) : ' ';
|
||||
const uppercase = options.uppercase !== false;
|
||||
const encoded = text.split(/\s+/).map(function(word) {
|
||||
return metaphoneWord(word);
|
||||
}).filter(Boolean);
|
||||
let out = encoded.join(separator);
|
||||
return uppercase ? out.toUpperCase() : out.toLowerCase();
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) {
|
||||
return '[metaphone]';
|
||||
}
|
||||
return this.func(text.slice(0, 30), options);
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,77 @@
|
||||
// Shadoks numeral system — GA BU ZO MEU (base 4) for text via UTF-8 bytes
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
const DIGITS = ['GA', 'BU', 'ZO', 'MEU'];
|
||||
const REV = { GA: 0, BU: 1, ZO: 2, MEU: 3 };
|
||||
|
||||
function toShadoks(n) {
|
||||
if (n === 0) return 'GA';
|
||||
let x = n;
|
||||
let out = '';
|
||||
while (x > 0) {
|
||||
out = DIGITS[x % 4] + (out ? ' ' + out : '');
|
||||
x = Math.floor(x / 4);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function fromShadoks(tokens) {
|
||||
let n = 0;
|
||||
for (const t of tokens) {
|
||||
const v = REV[t.toUpperCase()];
|
||||
if (v === undefined) return null;
|
||||
n = n * 4 + v;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Shadoks Numeral System',
|
||||
priority: 82,
|
||||
category: 'encoding',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'separator',
|
||||
label: 'Separator between bytes',
|
||||
type: 'select',
|
||||
default: 'pipe',
|
||||
options: [
|
||||
{ value: 'pipe', label: 'Pipe (|)' },
|
||||
{ value: 'space', label: 'Space' }
|
||||
]
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const sep = options.separator === 'space' ? ' ' : ' | ';
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
return Array.from(bytes).map(b => toShadoks(b)).join(sep);
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
options = options || {};
|
||||
const groups = options.separator === 'space'
|
||||
? text.trim().split(/\s{2,}|\|/)
|
||||
: text.split('|');
|
||||
const bytes = [];
|
||||
for (const g of groups) {
|
||||
const tokens = g.trim().split(/\s+/).filter(Boolean);
|
||||
const n = fromShadoks(tokens);
|
||||
if (n === null || n > 255) return text;
|
||||
bytes.push(n);
|
||||
}
|
||||
try {
|
||||
return new TextDecoder().decode(new Uint8Array(bytes));
|
||||
} catch (e) {
|
||||
return text;
|
||||
}
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[shadok]';
|
||||
return this.func(text.slice(0, 2), options);
|
||||
},
|
||||
detector: function(text) {
|
||||
return /\b(GA|BU|ZO|MEU)(\s+(GA|BU|ZO|MEU))+\b/i.test(text);
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,71 @@
|
||||
// Split text into fixed-size letter groups
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Group Letters',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
description: 'Split letters into fixed-size groups or rejoin grouped text.',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'groupSize',
|
||||
label: 'Letters per group',
|
||||
type: 'number',
|
||||
default: 5,
|
||||
min: 1,
|
||||
max: 32,
|
||||
step: 1
|
||||
},
|
||||
{
|
||||
id: 'separator',
|
||||
label: 'Group separator',
|
||||
type: 'select',
|
||||
default: ' ',
|
||||
options: [
|
||||
{ value: ' ', label: 'Space' },
|
||||
{ value: '-', label: 'Dash' },
|
||||
{ value: '.', label: 'Dot' },
|
||||
{ value: '\n', label: 'New line' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'lettersOnly',
|
||||
label: 'Letters only (ignore spaces/punctuation)',
|
||||
type: 'boolean',
|
||||
default: true
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const size = Math.max(1, Math.min(32, parseInt(options.groupSize, 10) || 5));
|
||||
const sep = options.separator != null ? String(options.separator) : ' ';
|
||||
const lettersOnly = options.lettersOnly !== false;
|
||||
const source = lettersOnly ? text.replace(/[^a-zA-Z]/g, '') : text.replace(/\s+/g, '');
|
||||
if (!source) {
|
||||
return text;
|
||||
}
|
||||
const groups = [];
|
||||
for (let i = 0; i < source.length; i += size) {
|
||||
groups.push(source.slice(i, i + size));
|
||||
}
|
||||
return groups.join(sep);
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
options = options || {};
|
||||
const sep = options.separator != null ? String(options.separator) : ' ';
|
||||
if (sep === ' ') {
|
||||
return text.replace(/\s+/g, '');
|
||||
}
|
||||
const esc = sep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
return text.split(new RegExp(esc.length ? esc : '\\s+')).join('');
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) {
|
||||
return '[group-letters]';
|
||||
}
|
||||
return this.func(text.slice(0, 20), options);
|
||||
},
|
||||
detector: function(text) {
|
||||
return /\b[a-zA-Z]{1,8}(?:[ \-.][a-zA-Z]{1,8}){2,}\b/.test(text);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
// Pad or strip leading zeros on numeric tokens
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Leading Zeros',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
description: 'Pad numbers with leading zeros or strip them from numeric tokens.',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'mode',
|
||||
label: 'Mode',
|
||||
type: 'select',
|
||||
default: 'pad',
|
||||
options: [
|
||||
{ value: 'pad', label: 'Pad to width' },
|
||||
{ value: 'strip', label: 'Strip leading zeros' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'width',
|
||||
label: 'Pad width',
|
||||
type: 'number',
|
||||
default: 4,
|
||||
min: 1,
|
||||
max: 32,
|
||||
step: 1
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const mode = options.mode === 'strip' ? 'strip' : 'pad';
|
||||
const width = Math.max(1, Math.min(32, parseInt(options.width, 10) || 4));
|
||||
|
||||
return text.replace(/\d+/g, function(num) {
|
||||
if (mode === 'strip') {
|
||||
const stripped = num.replace(/^0+(?=\d)/, '');
|
||||
return stripped === '' ? '0' : stripped;
|
||||
}
|
||||
return num.length >= width ? num : num.padStart(width, '0');
|
||||
});
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
options = options || {};
|
||||
const mode = options.mode === 'strip' ? 'strip' : 'pad';
|
||||
if (mode === 'pad') {
|
||||
return text.replace(/\d+/g, function(num) {
|
||||
const stripped = num.replace(/^0+(?=\d)/, '');
|
||||
return stripped === '' ? '0' : stripped;
|
||||
});
|
||||
}
|
||||
const width = Math.max(1, Math.min(32, parseInt(options.width, 10) || 4));
|
||||
return text.replace(/\d+/g, function(num) {
|
||||
return num.length >= width ? num : num.padStart(width, '0');
|
||||
});
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) {
|
||||
return '[leading-zeros]';
|
||||
}
|
||||
return this.func(text.slice(0, 40), options);
|
||||
},
|
||||
detector: function(text) {
|
||||
return /\b0+\d+\b/.test(text) || /\b\d{1,3}\b/.test(text);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
// Remove duplicate lines from a list
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'List Deduplicate',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
canDecode: false,
|
||||
description: 'Remove duplicate lines while preserving first occurrence order.',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'ignoreCase',
|
||||
label: 'Ignore case when comparing',
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
{
|
||||
id: 'trimLines',
|
||||
label: 'Trim whitespace on each line',
|
||||
type: 'boolean',
|
||||
default: true
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const ignoreCase = !!options.ignoreCase;
|
||||
const trimLines = options.trimLines !== false;
|
||||
const lines = text.split(/\r?\n/);
|
||||
const seen = {};
|
||||
const out = [];
|
||||
|
||||
lines.forEach(function(line) {
|
||||
let value = trimLines ? line.trim() : line;
|
||||
const key = ignoreCase ? value.toLowerCase() : value;
|
||||
if (seen[key]) {
|
||||
return;
|
||||
}
|
||||
seen[key] = true;
|
||||
out.push(value);
|
||||
});
|
||||
|
||||
return out.join('\n');
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) {
|
||||
return '[dedupe]';
|
||||
}
|
||||
return this.func(text, options).split('\n').slice(0, 3).join('\n') + '...';
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
// Shuffle letters within each word (seeded for reversible decode)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
function seededRandom(seed) {
|
||||
let s = seed >>> 0;
|
||||
return function() {
|
||||
s = (s * 1664525 + 1013904223) >>> 0;
|
||||
return s / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
function shuffleWord(word, seed) {
|
||||
const chars = [...word];
|
||||
const rand = seededRandom(seed);
|
||||
for (let i = chars.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(rand() * (i + 1));
|
||||
[chars[i], chars[j]] = [chars[j], chars[i]];
|
||||
}
|
||||
return chars.join('');
|
||||
}
|
||||
|
||||
function unshuffleWord(word, seed) {
|
||||
const n = word.length;
|
||||
const rand = seededRandom(seed);
|
||||
const swaps = [];
|
||||
for (let i = n - 1; i > 0; i--) {
|
||||
swaps.push([i, Math.floor(rand() * (i + 1))]);
|
||||
}
|
||||
const chars = [...word];
|
||||
for (let s = swaps.length - 1; s >= 0; s--) {
|
||||
const [i, j] = swaps[s];
|
||||
[chars[i], chars[j]] = [chars[j], chars[i]];
|
||||
}
|
||||
return chars.join('');
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Shuffled Letters',
|
||||
priority: 58,
|
||||
category: 'format',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'seed',
|
||||
label: 'Shuffle seed (same seed to decode)',
|
||||
type: 'number',
|
||||
default: 42,
|
||||
min: 0,
|
||||
max: 999999
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const seed = Number(options.seed);
|
||||
const base = Number.isFinite(seed) ? seed : 42;
|
||||
let wi = 0;
|
||||
return text.replace(/\S+/g, w => shuffleWord(w, base + wi++));
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
options = options || {};
|
||||
const seed = Number(options.seed);
|
||||
const base = Number.isFinite(seed) ? seed : 42;
|
||||
let wi = 0;
|
||||
return text.replace(/\S+/g, w => unshuffleWord(w, base + wi++));
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[shuffle]';
|
||||
return this.func(text.slice(0, 20), options);
|
||||
},
|
||||
detector: function(text) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,67 @@
|
||||
// Typoglycemia — scramble middle letters while keeping first/last
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
function hashString(str) {
|
||||
let hash = 2166136261;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash ^= str.charCodeAt(i);
|
||||
hash = Math.imul(hash, 16777619);
|
||||
}
|
||||
return hash >>> 0;
|
||||
}
|
||||
|
||||
function seededShuffle(arr, seed) {
|
||||
const copy = arr.slice();
|
||||
for (let i = copy.length - 1; i > 0; i--) {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
const j = seed % (i + 1);
|
||||
const tmp = copy[i];
|
||||
copy[i] = copy[j];
|
||||
copy[j] = tmp;
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
function scrambleWord(word, minLength) {
|
||||
if (word.length < minLength) {
|
||||
return word;
|
||||
}
|
||||
const first = word.charAt(0);
|
||||
const last = word.charAt(word.length - 1);
|
||||
const middle = word.slice(1, -1).split('');
|
||||
return first + seededShuffle(middle, hashString(word)).join('') + last;
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Typoglycemia',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
canDecode: false,
|
||||
description: 'Reorder inner letters of each word — readable but scrambled (deterministic shuffle).',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'minLength',
|
||||
label: 'Minimum word length to scramble',
|
||||
type: 'number',
|
||||
default: 4,
|
||||
min: 3,
|
||||
max: 12,
|
||||
step: 1
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const minLength = Math.max(3, Math.min(12, parseInt(options.minLength, 10) || 4));
|
||||
return text.replace(/[a-zA-Z]+/g, function(word) {
|
||||
return scrambleWord(word, minLength);
|
||||
});
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) {
|
||||
return '[typoglycemia]';
|
||||
}
|
||||
return this.func(text.slice(0, 40), options);
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,72 @@
|
||||
// Insert a letter into each word at a fixed position
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
function mutateWord(word, insertChar, position) {
|
||||
if (!word || !insertChar) return word;
|
||||
const ch = insertChar.charAt(0);
|
||||
if (position === 'start' || position === 'prefix') return ch + word;
|
||||
if (position === 'end' || position === 'suffix') return word + ch;
|
||||
const idx = parseInt(position, 10);
|
||||
if (Number.isFinite(idx) && idx >= 0 && idx <= word.length) {
|
||||
return word.slice(0, idx) + ch + word.slice(idx);
|
||||
}
|
||||
return word + ch;
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Word Letter Add',
|
||||
priority: 60,
|
||||
category: 'format',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'insertChar',
|
||||
label: 'Letter to insert',
|
||||
type: 'text',
|
||||
default: 'x'
|
||||
},
|
||||
{
|
||||
id: 'position',
|
||||
label: 'Position in each word',
|
||||
type: 'select',
|
||||
default: 'end',
|
||||
options: [
|
||||
{ value: 'start', label: 'Start (prefix)' },
|
||||
{ value: 'end', label: 'End (suffix)' },
|
||||
{ value: '1', label: 'After 1st character' },
|
||||
{ value: '2', label: 'After 2nd character' }
|
||||
]
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const insertChar = (options.insertChar || 'x').charAt(0);
|
||||
const position = options.position || 'end';
|
||||
return text.replace(/\S+/g, w => mutateWord(w, insertChar, position));
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
options = options || {};
|
||||
const insertChar = (options.insertChar || 'x').charAt(0);
|
||||
const position = options.position || 'end';
|
||||
const esc = insertChar.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
if (position === 'start' || position === 'prefix') {
|
||||
return text.replace(new RegExp('^' + esc + '(\\S+)|(\\s|^)' + esc + '(\\S+)', 'g'), '$1$2$3');
|
||||
}
|
||||
if (position === 'end' || position === 'suffix') {
|
||||
return text.replace(new RegExp('(\\S+)' + esc + '(?=\\s|$)', 'g'), '$1');
|
||||
}
|
||||
const idx = parseInt(position, 10);
|
||||
if (Number.isFinite(idx)) {
|
||||
return text.replace(new RegExp('(\\S{' + idx + '})' + esc + '(\\S*)', 'g'), '$1$2');
|
||||
}
|
||||
return text.replace(new RegExp(esc + '$'), '');
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[+letter]';
|
||||
return this.func(text.slice(0, 24), options);
|
||||
},
|
||||
detector: function(text) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,59 @@
|
||||
// Replace a letter at a fixed index in each word
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
function changeAt(word, index, replacement) {
|
||||
if (!word || !replacement) return word;
|
||||
let i = index;
|
||||
if (i < 0) i = word.length + i;
|
||||
if (i < 0 || i >= word.length) return word;
|
||||
return word.slice(0, i) + replacement.charAt(0) + word.slice(i + 1);
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Word Letter Change',
|
||||
priority: 60,
|
||||
category: 'format',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'index',
|
||||
label: 'Character index to replace',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
min: -20,
|
||||
max: 20
|
||||
},
|
||||
{
|
||||
id: 'replaceChar',
|
||||
label: 'Replacement letter',
|
||||
type: 'text',
|
||||
default: 'x'
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const index = Number.isFinite(Number(options.index)) ? Number(options.index) : 0;
|
||||
const rep = (options.replaceChar || 'x').charAt(0);
|
||||
return text.replace(/\S+/g, w => changeAt(w, index, rep));
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
options = options || {};
|
||||
const index = Number.isFinite(Number(options.index)) ? Number(options.index) : 0;
|
||||
const rep = (options.replaceChar || 'x').charAt(0);
|
||||
const esc = rep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
return text.replace(/\S+/g, w => {
|
||||
let i = index;
|
||||
if (i < 0) i = w.length + i;
|
||||
if (i < 0 || i >= w.length) return w;
|
||||
const re = new RegExp('^(.{' + i + '})' + esc + '(.*)$');
|
||||
const m = w.match(re);
|
||||
return m ? m[1] + '?' + m[2] : w;
|
||||
});
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[~letter]';
|
||||
return this.func(text.slice(0, 24), options);
|
||||
},
|
||||
canDecode: false
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,49 @@
|
||||
// Remove a letter at a fixed index from each word
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
function removeAt(word, index) {
|
||||
if (!word) return word;
|
||||
let i = index;
|
||||
if (i < 0) i = word.length + i;
|
||||
if (i < 0 || i >= word.length) return word;
|
||||
return word.slice(0, i) + word.slice(i + 1);
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Word Letter Remove',
|
||||
priority: 60,
|
||||
category: 'format',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'index',
|
||||
label: 'Character index to remove (0 = first, -1 = last)',
|
||||
type: 'number',
|
||||
default: -1,
|
||||
min: -20,
|
||||
max: 20
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const index = Number.isFinite(Number(options.index)) ? Number(options.index) : -1;
|
||||
return text.replace(/\S+/g, w => removeAt(w, index));
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
options = options || {};
|
||||
const index = Number.isFinite(Number(options.index)) ? Number(options.index) : -1;
|
||||
const placeholder = '\u0001';
|
||||
return text.replace(/\S+/g, w => {
|
||||
if (index === 0 || index <= -w.length) return placeholder + w;
|
||||
if (index === -1 || index === w.length - 1) return w + placeholder;
|
||||
const i = index < 0 ? w.length + index : index;
|
||||
return w.slice(0, i) + placeholder + w.slice(i);
|
||||
}).replace(/\u0001/g, '?');
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[-letter]';
|
||||
return this.func(text.slice(0, 24), options);
|
||||
},
|
||||
canDecode: false
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,99 @@
|
||||
// ASL SignWriting transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'ASL SignWriting',
|
||||
priority: 0,
|
||||
canDecode: false,
|
||||
description: 'American Sign Language fingerspelling in SignWriting (ISWA 2010). Horizontal or vertical layout.',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'layout',
|
||||
label: 'Layout',
|
||||
type: 'select',
|
||||
default: 'horizontal',
|
||||
options: [
|
||||
{ label: 'Horizontal', value: 'horizontal' },
|
||||
{ label: 'Vertical', value: 'vertical' }
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
THIN: '\u2004',
|
||||
NBSP: '\u00A0',
|
||||
|
||||
aslMap: {
|
||||
'A': '𝣷𝪜', 'B': '𝡇𝪜', 'C': '𝡭𝪜', 'D': '𝠁𝪜', 'E': '𝡊𝪜',
|
||||
'F': '𝣎𝪜', 'G': '𝣰', 'H': '𝠕𝪢', 'I': '𝢒𝪜',
|
||||
'J': '𝦢𝪬\n𝢒𝪜', 'K': '𝡀𝪜', 'L': '𝣜𝪜', 'M': '𝢍𝪜',
|
||||
'N': '𝠙𝪜', 'O': '𝡶𝪜', 'P': '𝡀𝪜𝪡', 'Q': '𝣱𝪜𝪡',
|
||||
'R': '𝠚𝪜', 'S': '𝤃𝪜', 'T': '𝣻𝪜', 'U': '𝠕𝪜',
|
||||
'V': '𝠎𝪜', 'W': '𝢇𝪜', 'X': '𝠆𝪜', 'Y': '𝢚𝪜',
|
||||
'Z': '\u2004𝥅𝪪\n𝠀𝪜',
|
||||
'.': '𝪈𝪢', ',': '𝪇𝪢', ':': '𝪊𝪢', ';': '𝪉𝪢',
|
||||
'(': '𝪋𝪢', ')': '𝪋𝪦', '?': '𝦟𝪝𝪬\n𝠀𝪜',
|
||||
'0': '𝡶𝪜', '1': '𝠀𝪜', '2': '𝠎𝪜', '3': '𝠞𝪜',
|
||||
'4': '𝡄𝪜', '5': '𝡌𝪜', '6': '𝢇𝪜', '7': '𝢥𝪜',
|
||||
'8': '𝢻𝪜', '9': '𝣎𝪜'
|
||||
},
|
||||
|
||||
func: function(text, options) {
|
||||
var layout = (options && options.layout) || 'horizontal';
|
||||
text = text.toUpperCase();
|
||||
|
||||
if (layout === 'vertical') {
|
||||
var words = text.split(/\s+/);
|
||||
var wordBlocks = [];
|
||||
for (var w = 0; w < words.length; w++) {
|
||||
var chars = [];
|
||||
for (var c = 0; c < words[w].length; c++) {
|
||||
var val = this.aslMap[words[w][c]];
|
||||
if (val !== undefined) chars.push(val);
|
||||
}
|
||||
wordBlocks.push(chars.join('\n\n'));
|
||||
}
|
||||
return wordBlocks.join('\n\n\n');
|
||||
}
|
||||
|
||||
// Horizontal layout
|
||||
var SPACE_TOKEN = this.NBSP + this.NBSP;
|
||||
var signs = [];
|
||||
for (var i = 0; i < text.length; i++) {
|
||||
var ch = text[i];
|
||||
if (ch === ' ') {
|
||||
signs.push([SPACE_TOKEN]);
|
||||
} else {
|
||||
var val = this.aslMap[ch];
|
||||
if (val !== undefined) {
|
||||
signs.push(val.split('\n'));
|
||||
} else {
|
||||
signs.push([ch]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var maxH = 1;
|
||||
for (var s = 0; s < signs.length; s++) {
|
||||
if (signs[s].length > maxH) maxH = signs[s].length;
|
||||
}
|
||||
|
||||
var lanes = [];
|
||||
for (var r = 0; r < maxH; r++) lanes.push('');
|
||||
|
||||
for (var s = 0; s < signs.length; s++) {
|
||||
var padCount = maxH - signs[s].length;
|
||||
var padded = [];
|
||||
for (var p = 0; p < padCount; p++) padded.push(this.NBSP);
|
||||
for (var p = 0; p < signs[s].length; p++) padded.push(signs[s][p]);
|
||||
for (var r = 0; r < maxH; r++) {
|
||||
lanes[r] += padded[r];
|
||||
}
|
||||
}
|
||||
return lanes.join('\n');
|
||||
},
|
||||
|
||||
preview: function(text) {
|
||||
if (!text) return '[ASL SignWriting]';
|
||||
return this.func(text.slice(0, 5));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
// IPA Lip-Reading SignWriting transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'IPA Lip-Reading',
|
||||
priority: 0,
|
||||
canDecode: false,
|
||||
description: 'Converts IPA phonetic text to SignWriting lip-reading mouth shapes (ISWA 2010 head/face symbols).',
|
||||
|
||||
HEAD: '𝧿', EYE: '𝨔',
|
||||
|
||||
ipaToSw: {
|
||||
'p': '𝩓𝨵', 'b': '𝩓𝨮', 'm': '𝩓𝨳',
|
||||
'ɓ': '𝩓𝨮', 'ʙ': '𝩓𝨮', 'ɸ': '𝩓𝨵𝪛', 'β': '𝩓𝨮',
|
||||
'f': '𝩥𝨵', 'v': '𝩥𝨮', 'ʋ': '𝩥𝨮', 'ɱ': '𝩥𝨳',
|
||||
'θ': '𝩛𝨵', 'ð': '𝩛𝨮',
|
||||
't': '𝩜𝨵', 'd': '𝩜𝨮', 'n': '𝩜𝨳',
|
||||
's': '𝩀𝨵𝪛', 'z': '𝩀𝨮', 'l': '𝩜𝪤', 'r': '𝩟', 'ɾ': '𝩟', 'ɹ': '𝩟', 'ɻ': '𝩟',
|
||||
'ʃ': '𝩍𝨵𝪛', 'ʒ': '𝩍𝨮',
|
||||
'c': '𝩀𝨵', 'ɟ': '𝩀𝨮', 'ɲ': '𝩀𝨳', 'ç': '𝩀𝨵𝪛', 'ʝ': '𝩀𝨮', 'j': '𝩀', 'ʎ': '𝩀',
|
||||
'k': '𝩄𝨵', 'g': '𝩄𝨮', 'ŋ': '𝩄𝨳', 'x': '𝩄𝨵𝪛', 'ɣ': '𝩄𝨮', 'w': '𝩆', 'ʍ': '𝩆',
|
||||
'q': '𝩉𝨵', 'ɢ': '𝩉𝨮', 'ɴ': '𝩉𝨳', 'χ': '𝩉𝨵𝪛', 'ʁ': '𝩉𝨮', 'ʀ': '𝩉𝨮',
|
||||
'ʕ': '𝩌𝨮', 'ħ': '𝩌𝨮', 'ʔ': '𝩡', 'h': '𝩄𝨵𝪛', 'ɦ': '𝩄𝨵𝪛',
|
||||
'i': '𝩀', 'y': '𝩆', 'ɨ': '𝩀', 'ʉ': '𝩆', 'ɯ': '𝩐', 'u': '𝩆',
|
||||
'ɪ': '𝩊', 'ʏ': '𝩇', 'ʊ': '𝩇',
|
||||
'e': '𝩊', 'ø': '𝩇', 'ɘ': '𝩊', 'ɵ': '𝩇', 'ɤ': '𝩊', 'o': '𝩇', 'ə': '𝩊', 'ɚ': '𝩊',
|
||||
'ɛ': '𝩈', 'œ': '𝩈', 'ɜ': '𝩈', 'ɞ': '𝩈', 'ʌ': '𝩉', 'ɔ': '𝩉',
|
||||
'a': '𝩌', 'ɶ': '𝩌', 'ä': '𝩌', 'ɑ': '𝩌', 'ɒ': '𝩌', 'æ': '𝩌', 'ɐ': '𝩌',
|
||||
'ʘ': '𝩓𝨶', 'ǀ': '𝩣𝨶', 'ǃ': '𝩡𝨶', 'ǂ': '𝩡𝨶', 'ǁ': '𝩡𝨶'
|
||||
},
|
||||
|
||||
diphthongs: {
|
||||
'aɪ': ['𝩌', '𝩀'], 'aʊ': ['𝩌', '𝩆'], 'eɪ': ['𝩊', '𝩀'],
|
||||
'oʊ': ['𝩇', '𝩆'], 'ɔɪ': ['𝩉', '𝩀'], 'əʊ': ['𝩊', '𝩆'],
|
||||
'ɪə': ['𝩊', '𝩊'], 'eə': ['𝩊', '𝩌'], 'ʊə': ['𝩇', '𝩊']
|
||||
},
|
||||
|
||||
skipChars: { 'ˈ': 1, 'ˌ': 1, 'ː': 1, ' ': 1, '\t': 1, '\n': 1 },
|
||||
|
||||
func: function(text) {
|
||||
var result = [], i = 0, HEAD = this.HEAD, EYE = this.EYE;
|
||||
while (i < text.length) {
|
||||
if (this.skipChars[text[i]]) { i++; continue; }
|
||||
// Check diphthongs (2-char)
|
||||
if (i + 1 < text.length) {
|
||||
var di = text[i] + text[i + 1];
|
||||
if (this.diphthongs[di]) {
|
||||
var shapes = this.diphthongs[di];
|
||||
for (var s = 0; s < shapes.length; s++) {
|
||||
result.push(HEAD + EYE + shapes[s]);
|
||||
if (s < shapes.length - 1) result.push('\u2192');
|
||||
}
|
||||
i += 2; continue;
|
||||
}
|
||||
}
|
||||
var ch = text[i]; i++;
|
||||
if (this.ipaToSw[ch]) {
|
||||
result.push(HEAD + EYE + this.ipaToSw[ch]);
|
||||
} else {
|
||||
result.push(HEAD + EYE + '𝨻');
|
||||
}
|
||||
}
|
||||
return result.join(' ');
|
||||
},
|
||||
|
||||
preview: function(text) {
|
||||
if (!text) return '[IPA Lip-Reading]';
|
||||
return this.func(text.slice(0, 8));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
// JSL SignWriting transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'JSL SignWriting',
|
||||
priority: 0,
|
||||
canDecode: false,
|
||||
description: 'Japanese Sign Language fingerspelling in SignWriting (ISWA 2010). Hiragana input.',
|
||||
configurableOptions: [
|
||||
{ id: 'layout', label: 'Layout', type: 'select', default: 'horizontal',
|
||||
options: [{ label: 'Horizontal', value: 'horizontal' }, { label: 'Vertical', value: 'vertical' }] }
|
||||
],
|
||||
NBSP: '\u00A0',
|
||||
SEP: '\u2001',
|
||||
jslMap: {
|
||||
'あ': ['𝣷𝪜'], 'ぁ': ['𝥥𝪤', '𝣷𝪜'], 'い': ['𝢒𝪜'], 'ぃ': ['𝥥𝪤', '𝢒𝪜'],
|
||||
'う': ['𝠕𝪜'], 'ぅ': ['𝥥𝪤', '𝠕𝪜'], 'え': ['𝡦𝪜'], 'ぇ': ['𝥥𝪤', '𝡦𝪜'],
|
||||
'お': ['𝡶𝪛'], 'ぉ': ['𝥥𝪤', '𝡶𝪛'], 'か': ['𝡀𝪜'], 'ゕ': ['𝥥𝪤', '𝡀𝪜'],
|
||||
'き': ['𝣮𝪜𝪦'], 'く': ['𝡝𝪢'], 'け': ['𝡇𝪜'], 'ゖ': ['𝥥𝪤', '𝡇𝪜'],
|
||||
'こ': ['𝢀𝪛'], 'さ': ['𝤃𝪜'], 'し': ['𝠞𝪢'], 'す': ['𝠞𝪤'], 'せ': ['𝣆𝪜'], 'そ': ['𝠀𝪞'],
|
||||
'た': ['𝣷𝪞'], 'ち': ['𝢖𝪜'], 'つ': ['𝢳𝪜'], 'っ': ['𝥥𝪤', '𝢳𝪜'],
|
||||
'て': ['𝡌𝪜'], 'と': ['𝠕'], 'な': ['𝠎𝪤'], 'に': ['𝠎𝪢'], 'ぬ': ['𝠆𝪛'], 'ね': ['𝡌𝪤'],
|
||||
'の': ['𝤪𝪣', '𝠀𝪛'], 'は': ['𝠕𝪞'], 'ひ': ['𝠀𝪜'], 'ふ': ['𝣰𝪟𝪡'], 'へ': ['𝢚𝪟'], 'ほ': ['𝢀𝪝'],
|
||||
'ま': ['𝢌𝪤'], 'み': ['𝢌𝪢'], 'む': ['𝣜𝪢'], 'め': ['𝣎𝪜'], 'も': ['𝤪𝪤', '𝤜𝣴𝪝𝪦'],
|
||||
'や': ['𝢚𝪜'], 'ゃ': ['𝥥𝪤', '𝢚𝪜'], 'ゆ': ['𝢌'], 'ゅ': ['𝥥𝪤', '𝢌'],
|
||||
'よ': ['𝡄𝪢'], 'ょ': ['𝥥𝪤', '𝡄𝪢'], 'ら': ['𝠚𝪜'], 'り': ['𝦢𝪬', '𝠎𝪞'], 'る': ['𝠞𝪜'],
|
||||
'れ': ['𝣜𝪜'], 'ろ': ['𝠐𝪛'], 'わ': ['𝢆𝪜'], 'ゎ': ['𝥥𝪤', '𝢆𝪜'],
|
||||
'ゐ': ['𝥥𝪤', '𝢒𝪜'], 'ゑ': ['𝥥𝪤', '𝡦𝪜'], 'を': ['𝥥𝪤', '𝡶𝪛'],
|
||||
'ん': ['𝦢𝪤', '𝠀𝪞'],
|
||||
'が': ['𝡀𝪜𝥥𝪦'], 'ぎ': ['𝣮𝪜𝪦𝥥𝪦'], 'ぐ': ['𝡝𝪢𝥥𝪦'], 'げ': ['𝡇𝪜𝥥𝪦'], 'ご': ['𝢀𝪛𝥥𝪦'],
|
||||
'ざ': ['𝤃𝪜𝥥𝪦'], 'じ': ['𝠞𝪢𝥥𝪦'], 'ず': ['𝠞𝪤𝥥𝪦'], 'ぜ': ['𝣆𝪜𝥥𝪦'], 'ぞ': ['𝠀𝪞𝥥𝪦'],
|
||||
'だ': ['𝣷𝪞𝥥𝪦'], 'ぢ': ['𝢖𝪜𝥥𝪦'], 'づ': ['𝢳𝪜𝥥𝪦'], 'で': ['𝡌𝪜𝥥𝪦'], 'ど': ['𝠕𝥥𝪦'],
|
||||
'ば': ['𝠕𝪞𝥥𝪦'], 'び': ['𝠀𝪜𝥥𝪦'], 'ぶ': ['𝣰𝪟𝪡𝥥𝪦'], 'べ': ['𝢚𝪟𝥥𝪦'], 'ぼ': ['𝢀𝪝𝥥𝪦'],
|
||||
'ぱ': ['𝤪', '𝠕𝪞'], 'ぴ': ['𝤪', '𝠀𝪜'], 'ぷ': ['𝤪', '𝣰𝪟𝪡'], 'ぺ': ['𝤪', '𝢚𝪟'], 'ぽ': ['𝤪', '𝢀𝪝'],
|
||||
'ー': ['𝥥𝪤'],
|
||||
'0': ['𝠊𝪛'], '1': ['𝠀𝪜'], '2': ['𝠎𝪜'], '3': ['𝢆𝪜'], '4': ['𝡄𝪜'],
|
||||
'5': ['𝣷𝪜'], '6': ['𝣜𝪢'], '7': ['𝠞𝪢'], '8': ['𝢎𝪢'], '9': ['𝡝𝪢'],
|
||||
' ': ['\u00A0']
|
||||
},
|
||||
jslDigraph: {
|
||||
'きゃ': ['𝣮𝪜𝪦', '𝥥𝪤', '𝢚𝪜'], 'きゅ': ['𝣮𝪜𝪦', '𝥥𝪤', '𝢌'], 'きょ': ['𝣮𝪜𝪦', '𝥥𝪤', '𝡄𝪢'],
|
||||
'しゃ': ['𝠞𝪢', '𝥥𝪤', '𝢚𝪜'], 'しゅ': ['𝠞𝪢', '𝥥𝪤', '𝢌'], 'しょ': ['𝠞𝪢', '𝥥𝪤', '𝡄𝪢'],
|
||||
'ちゃ': ['𝢖𝪜', '𝥥𝪤', '𝢚𝪜'], 'ちゅ': ['𝢖𝪜', '𝥥𝪤', '𝢌'], 'ちょ': ['𝢖𝪜', '𝥥𝪤', '𝡄𝪢'],
|
||||
'にゃ': ['𝠎𝪢', '𝥥𝪤', '𝢚𝪜'], 'にゅ': ['𝠎𝪢', '𝥥𝪤', '𝢌'], 'にょ': ['𝠎𝪢', '𝥥𝪤', '𝡄𝪢'],
|
||||
'ひゃ': ['𝠀𝪜', '𝥥𝪤', '𝢚𝪜'], 'ひゅ': ['𝠀𝪜', '𝥥𝪤', '𝢌'], 'ひょ': ['𝠀𝪜', '𝥥𝪤', '𝡄𝪢'],
|
||||
'みゃ': ['𝢌𝪢', '𝥥𝪤', '𝢚𝪜'], 'みゅ': ['𝢌𝪢', '𝥥𝪤', '𝢌'], 'みょ': ['𝢌𝪢', '𝥥𝪤', '𝡄𝪢'],
|
||||
'りゃ': ['𝦢𝪬', '𝠎𝪞', '𝥥𝪤', '𝢚𝪜'], 'りゅ': ['𝦢𝪬', '𝠎𝪞', '𝥥𝪤', '𝢌'],
|
||||
'りょ': ['𝦢𝪬', '𝠎𝪞', '𝥥𝪤', '𝡄𝪢'],
|
||||
'ぎゃ': ['𝣮𝪜𝪦𝥥𝪦', '𝥥𝪤', '𝢚𝪜'], 'ぎゅ': ['𝣮𝪜𝪦𝥥𝪦', '𝥥𝪤', '𝢌'],
|
||||
'ぎょ': ['𝣮𝪜𝪦𝥥𝪦', '𝥥𝪤', '𝡄𝪢'],
|
||||
'じゃ': ['𝠞𝪢𝥥𝪦', '𝥥𝪤', '𝢚𝪜'], 'じゅ': ['𝠞𝪢𝥥𝪦', '𝥥𝪤', '𝢌'],
|
||||
'じょ': ['𝠞𝪢𝥥𝪦', '𝥥𝪤', '𝡄𝪢'],
|
||||
'ぴゃ': ['𝤪', '𝠀𝪜', '𝥥𝪤', '𝢚𝪜'], 'ぴゅ': ['𝤪', '𝠀𝪜', '𝥥𝪤', '𝢌'],
|
||||
'ぴょ': ['𝤪', '𝠀𝪜', '𝥥𝪤', '𝡄𝪢']
|
||||
},
|
||||
tokenize: function(text) {
|
||||
var out = [], i = 0;
|
||||
while (i < text.length) {
|
||||
if (i + 1 < text.length && this.jslDigraph[text[i] + text[i + 1]]) {
|
||||
out.push(this.jslDigraph[text[i] + text[i + 1]]); i += 2;
|
||||
} else {
|
||||
out.push(this.jslMap[text[i]] || [text[i]]); i++;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
},
|
||||
func: function(text, options) {
|
||||
var layout = (options && options.layout) || 'horizontal';
|
||||
if (layout === 'vertical') {
|
||||
var words = text.split(/\s+/), wb = [];
|
||||
for (var w = 0; w < words.length; w++) {
|
||||
var tok = this.tokenize(words[w]), cb = [];
|
||||
for (var t = 0; t < tok.length; t++) cb.push(tok[t].join('\n'));
|
||||
wb.push(cb.join('\n\n'));
|
||||
}
|
||||
return wb.join('\n\n\n');
|
||||
}
|
||||
var tokens = this.tokenize(text), maxH = 1;
|
||||
for (var t = 0; t < tokens.length; t++) if (tokens[t].length > maxH) maxH = tokens[t].length;
|
||||
var lanes = [];
|
||||
for (var r = 0; r < maxH; r++) lanes.push([]);
|
||||
for (var t = 0; t < tokens.length; t++) {
|
||||
var pad = maxH - tokens[t].length, padded = [];
|
||||
for (var p = 0; p < pad; p++) padded.push('');
|
||||
for (var p = 0; p < tokens[t].length; p++) padded.push(tokens[t][p]);
|
||||
for (var r = 0; r < maxH; r++) lanes[r].push(padded[r] || this.NBSP);
|
||||
}
|
||||
return lanes.map(function(l) { return l.join('\u2001'); }).join('\n');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[JSL SignWriting]';
|
||||
return this.func(text.slice(0, 5));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
// LIBRAS SignWriting transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'LIBRAS SignWriting',
|
||||
priority: 0,
|
||||
canDecode: false,
|
||||
description: 'Brazilian Sign Language (Libras) fingerspelling in SignWriting (ISWA 2010). Horizontal or vertical layout.',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'layout',
|
||||
label: 'Layout',
|
||||
type: 'select',
|
||||
default: 'horizontal',
|
||||
options: [
|
||||
{ label: 'Horizontal', value: 'horizontal' },
|
||||
{ label: 'Vertical', value: 'vertical' }
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
NBSP: '\u00A0',
|
||||
|
||||
librasMap: {
|
||||
'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': ['\u2004𝥅𝪪', '𝠀𝪜'],
|
||||
'0': ['𝡶𝪜'], '1': ['𝠀'], '2': ['𝠎'], '3': ['𝢆'],
|
||||
'4': ['𝡄'], '5': ['𝠐𝪨'], '6': ['𝡴𝪝𝪮'],
|
||||
'7': ['𝢃𝪛'], '8': ['𝧢𝪝', '𝤃𝪛'], '9': ['𝡵𝪟𝪢']
|
||||
},
|
||||
|
||||
/**
|
||||
* Strip diacritics from text (e.g. Ç → C, É → E)
|
||||
*/
|
||||
stripDiacritics: function(text) {
|
||||
// NFD decomposes, then remove combining marks
|
||||
var decomposed = text.normalize('NFD');
|
||||
var out = '';
|
||||
for (var i = 0; i < decomposed.length; i++) {
|
||||
// Combining Diacritical Marks block: U+0300–U+036F
|
||||
var cp = decomposed.charCodeAt(i);
|
||||
if (cp < 0x0300 || cp > 0x036F) {
|
||||
out += decomposed[i];
|
||||
}
|
||||
}
|
||||
return out;
|
||||
},
|
||||
|
||||
func: function(text, options) {
|
||||
var layout = (options && options.layout) || 'horizontal';
|
||||
text = this.stripDiacritics(text).toUpperCase();
|
||||
|
||||
if (layout === 'vertical') {
|
||||
var words = text.split(/\s+/);
|
||||
var wordBlocks = [];
|
||||
for (var w = 0; w < words.length; w++) {
|
||||
var chars = [];
|
||||
for (var c = 0; c < words[w].length; c++) {
|
||||
var sign = this.librasMap[words[w][c]];
|
||||
if (sign) chars.push(sign.join('\n'));
|
||||
}
|
||||
wordBlocks.push(chars.join('\n\n'));
|
||||
}
|
||||
return wordBlocks.join('\n\n\n');
|
||||
}
|
||||
|
||||
// Horizontal layout
|
||||
var SPACE_TOKEN = this.NBSP + this.NBSP;
|
||||
var signs = [];
|
||||
for (var i = 0; i < text.length; i++) {
|
||||
var ch = text[i];
|
||||
if (ch === ' ') {
|
||||
signs.push([SPACE_TOKEN]);
|
||||
} else {
|
||||
var sign = this.librasMap[ch];
|
||||
if (sign) {
|
||||
signs.push(sign.slice());
|
||||
} else {
|
||||
signs.push([ch]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var maxH = 1;
|
||||
for (var s = 0; s < signs.length; s++) {
|
||||
if (signs[s].length > maxH) maxH = signs[s].length;
|
||||
}
|
||||
|
||||
var lanes = [];
|
||||
for (var r = 0; r < maxH; r++) lanes.push('');
|
||||
|
||||
for (var s = 0; s < signs.length; s++) {
|
||||
var padCount = maxH - signs[s].length;
|
||||
var padded = [];
|
||||
for (var p = 0; p < padCount; p++) padded.push(this.NBSP);
|
||||
for (var p = 0; p < signs[s].length; p++) padded.push(signs[s][p]);
|
||||
for (var r = 0; r < maxH; r++) {
|
||||
lanes[r] += padded[r];
|
||||
}
|
||||
}
|
||||
return lanes.join('\n');
|
||||
},
|
||||
|
||||
preview: function(text) {
|
||||
if (!text) return '[LIBRAS SignWriting]';
|
||||
return this.func(text.slice(0, 5));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
// Morse Blink SignWriting transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Morse Blink',
|
||||
priority: 0,
|
||||
canDecode: false,
|
||||
description: 'Encodes text as Morse code using SignWriting eye blink symbols (dot = brief close, dash = tight press).',
|
||||
|
||||
DOT: '𝧿𝨕', // Eye closes briefly
|
||||
DASH: '𝧿𝨖', // Eye pressed tightly
|
||||
GAP: '𝧿𝨚', // Eye open (delimiter)
|
||||
|
||||
morseMap: {
|
||||
'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) {
|
||||
var upper = text.toUpperCase();
|
||||
var lines = [];
|
||||
for (var i = 0; i < upper.length; i++) {
|
||||
var ch = upper[i];
|
||||
var code = this.morseMap[ch];
|
||||
if (!code) continue;
|
||||
if (code === '/') {
|
||||
lines.push('');
|
||||
continue;
|
||||
}
|
||||
var symbols = [];
|
||||
for (var j = 0; j < code.length; j++) {
|
||||
symbols.push(code[j] === '.' ? this.DOT : this.DASH);
|
||||
}
|
||||
lines.push(symbols.join(' ') + ' ' + this.GAP);
|
||||
}
|
||||
return lines.join('\n');
|
||||
},
|
||||
|
||||
preview: function(text) {
|
||||
if (!text) return '[Morse Blink]';
|
||||
return this.func(text.slice(0, 3));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
// Deafblind Tactile SignWriting transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Tactile SignWriting',
|
||||
priority: 0,
|
||||
canDecode: false,
|
||||
description: 'Deafblind tactile fingerspelling approximation in SignWriting (ISWA 2010). Two-hand layers per letter.',
|
||||
|
||||
tactileMap: {
|
||||
'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) {
|
||||
var upper = text.toUpperCase();
|
||||
var blocks = [];
|
||||
for (var i = 0; i < upper.length; i++) {
|
||||
var ch = upper[i];
|
||||
if (ch === ' ') {
|
||||
blocks.push('\u00A0\n\u00A0');
|
||||
continue;
|
||||
}
|
||||
var sign = this.tactileMap[ch];
|
||||
if (sign) {
|
||||
blocks.push(sign[0] + '\n' + sign[1]);
|
||||
}
|
||||
}
|
||||
return blocks.join('\n\n');
|
||||
},
|
||||
|
||||
preview: function(text) {
|
||||
if (!text) return '[Tactile SignWriting]';
|
||||
return this.func(text.slice(0, 3));
|
||||
}
|
||||
});
|
||||
@@ -10,7 +10,7 @@ export default new BaseTransformer({
|
||||
const suitable = [
|
||||
'base64', 'binary', 'hex', 'morse', 'rot13', 'caesar', 'atbash', 'rot5',
|
||||
'upside_down', 'bubble', 'small_caps', 'fullwidth', 'leetspeak', 'superscript', 'subscript',
|
||||
'quenya', 'tengwar', 'klingon', 'dovahzul', 'elder_futhark',
|
||||
'quenya', 'tengwar', 'klingon', 'dovahzul', 'standard_galactic', 'elder_futhark',
|
||||
'hieroglyphics', 'ogham', 'mathematical', 'cursive', 'medieval',
|
||||
'monospace', 'greek', 'braille', 'alternating_case', 'reverse_words',
|
||||
'title_case', 'sentence_case', 'camel_case', 'snake_case', 'kebab_case', 'random_case',
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// @generated from data/alphabets — do not edit by hand
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Alchemical Symbols',
|
||||
priority: 100,
|
||||
category: 'symbol',
|
||||
description: 'Classical alchemical symbol alphabet',
|
||||
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 => this.map[c] || this.map[c.toUpperCase()] || c).join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[alchemical]';
|
||||
return this.func(text.slice(0, 6)) + (text.length > 6 ? '…' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
return new RegExp('[🜂🜃🜄🜅🜆🜇🜈🜉]', 'u').test(text);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
// Babylonian numerals — A1Z26 letters to cuneiform sexagesimal digits (1–59)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
const UNITS = ['', '\u{12415}', '\u{12416}', '\u{12417}', '\u{12418}', '\u{12419}',
|
||||
'\u{1241A}', '\u{1241B}', '\u{1241C}', '\u{1241D}'];
|
||||
const TENS = ['', '\u{1248B}', '\u{12499}', '\u{1240D}', '\u{1240F}', '\u{12410}'];
|
||||
|
||||
function letterValue(ch) {
|
||||
const code = ch.toUpperCase().charCodeAt(0);
|
||||
if (code >= 65 && code <= 90) return code - 64;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function babylonianGlyph(n) {
|
||||
if (n < 1 || n > 59) return '';
|
||||
const tens = Math.floor(n / 10);
|
||||
const units = n % 10;
|
||||
return (TENS[tens] || '') + (UNITS[units] || '');
|
||||
}
|
||||
|
||||
const GLYPHS = [];
|
||||
for (let n = 1; n <= 26; n++) {
|
||||
GLYPHS.push({ glyph: babylonianGlyph(n), letter: String.fromCharCode(64 + n) });
|
||||
}
|
||||
GLYPHS.sort(function(a, b) { return b.glyph.length - a.glyph.length; });
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Babylonian Numerals',
|
||||
priority: 100,
|
||||
category: 'symbol',
|
||||
description: 'A1Z26 (A=1 … Z=26) as Babylonian cuneiform numerals (Unicode sexagesimal signs)',
|
||||
func: function(text) {
|
||||
return [...text].map(function(ch) {
|
||||
const n = letterValue(ch);
|
||||
return n ? babylonianGlyph(n) : ch;
|
||||
}).join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
let out = '';
|
||||
let i = 0;
|
||||
while (i < text.length) {
|
||||
let hit = null;
|
||||
for (let g = 0; g < GLYPHS.length; g++) {
|
||||
const entry = GLYPHS[g];
|
||||
if (text.startsWith(entry.glyph, i)) {
|
||||
hit = entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hit) {
|
||||
out += hit.letter;
|
||||
i += hit.glyph.length;
|
||||
} else {
|
||||
const cp = text.codePointAt(i);
|
||||
out += String.fromCodePoint(cp);
|
||||
i += cp > 0xFFFF ? 2 : 1;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[babylonian-numerals]';
|
||||
return this.func(text.slice(0, 6)) + (text.length > 6 ? '…' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
return /[\u{12415}-\u{1241D}\u{1248B}\u{12499}\u{1240D}-\u{12410}]/u.test(text);
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,47 @@
|
||||
// @generated from data/alphabets — do not edit by hand
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Celestial Alphabet',
|
||||
priority: 100,
|
||||
category: 'symbol',
|
||||
description: 'Agrippa\'s celestial / angelic symbol alphabet',
|
||||
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 => this.map[c] || this.map[c.toUpperCase()] || c).join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[celestial]';
|
||||
return this.func(text.slice(0, 6)) + (text.length > 6 ? '…' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
return new RegExp('[☉☽☿♀♂♃♄♅♆♇☊☋]', 'u').test(text);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
// @generated from data/alphabets — do not edit by hand
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Daedric Alphabet',
|
||||
priority: 100,
|
||||
category: 'symbol',
|
||||
description: 'Elder Scrolls inspired Daedric-style symbols',
|
||||
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 => this.map[c] || this.map[c.toUpperCase()] || c).join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[daedric]';
|
||||
return this.func(text.slice(0, 6)) + (text.length > 6 ? '…' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
return new RegExp('[ᚠᚡᚢᚣᚤᚥᚦᚧᚨᚩ]', 'u').test(text);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
// @generated from data/alphabets — do not edit by hand
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Dancing Men Cipher',
|
||||
priority: 100,
|
||||
category: 'symbol',
|
||||
description: 'Sherlock Holmes stick-figure cipher (Unicode approximations)',
|
||||
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 => this.map[c] || this.map[c.toUpperCase()] || c).join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[dancing-men]';
|
||||
return this.func(text.slice(0, 6)) + (text.length > 6 ? '…' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
return new RegExp('[┣┫┳┻├┤┬┴╋╞╡╤╧]', 'u').test(text);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
// Dominos in digits — map 0-9 to domino tile notation
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
// Unicode domino tiles U+1F019 (🀙) = 0-0, through 6-6 grid; use simplified [a|b] for 0-9
|
||||
const MAP = {
|
||||
'0': '[0|0]', '1': '[0|1]', '2': '[1|1]', '3': '[1|2]', '4': '[2|2]',
|
||||
'5': '[2|3]', '6': '[3|3]', '7': '[3|4]', '8': '[4|4]', '9': '[4|5]'
|
||||
};
|
||||
const REV = {};
|
||||
for (const [d, v] of Object.entries(MAP)) REV[v] = d;
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Dominos in Digits',
|
||||
priority: 78,
|
||||
category: 'cipher',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'separator',
|
||||
label: 'Separator between dominoes',
|
||||
type: 'select',
|
||||
default: 'space',
|
||||
options: [
|
||||
{ value: 'space', label: 'Space' },
|
||||
{ value: 'none', label: 'None' }
|
||||
]
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const sep = options.separator === 'none' ? '' : ' ';
|
||||
const digits = text.replace(/\D/g, '');
|
||||
if (!digits) return text;
|
||||
return [...digits].map(d => MAP[d] || d).join(sep);
|
||||
},
|
||||
reverse: function(text) {
|
||||
const tokens = text.match(/\[\d\|\d\]/g) || [];
|
||||
if (!tokens.length) return text;
|
||||
return tokens.map(t => REV[t] || '').join('');
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[domino]';
|
||||
const digits = text.replace(/\D/g, '').slice(0, 5);
|
||||
return digits ? this.func(digits, options) : '[digits only]';
|
||||
},
|
||||
detector: function(text) {
|
||||
return (text.match(/\[\d\|\d\]/g) || []).length >= 2;
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,85 @@
|
||||
// Egyptian hieroglyph numerals — A1Z26 letters to additive hieroglyph counts (1–26)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
const ONE = '\u{133E4}';
|
||||
const TEN = '\u{13386}';
|
||||
const SEP = '\u2063';
|
||||
|
||||
function letterValue(ch) {
|
||||
const code = ch.toUpperCase().charCodeAt(0);
|
||||
if (code >= 65 && code <= 90) return code - 64;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function egyptianGlyph(n) {
|
||||
if (n < 1 || n > 99) return '';
|
||||
const tens = Math.floor(n / 10);
|
||||
const units = n % 10;
|
||||
return TEN.repeat(tens) + ONE.repeat(units);
|
||||
}
|
||||
|
||||
function parseEgyptian(text) {
|
||||
let n = 0;
|
||||
for (const ch of text) {
|
||||
if (ch === ONE) n += 1;
|
||||
else if (ch === TEN) n += 10;
|
||||
else break;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Egyptian Numerals',
|
||||
priority: 100,
|
||||
category: 'symbol',
|
||||
description: 'A1Z26 (A=1 … Z=26) as Egyptian hieroglyph stroke (1) and hobble (10) numerals',
|
||||
func: function(text) {
|
||||
let out = '';
|
||||
let prevGlyph = false;
|
||||
for (const ch of text) {
|
||||
const n = letterValue(ch);
|
||||
if (n) {
|
||||
if (prevGlyph) out += SEP;
|
||||
out += egyptianGlyph(n);
|
||||
prevGlyph = true;
|
||||
} else {
|
||||
out += ch;
|
||||
prevGlyph = false;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
},
|
||||
reverse: function(text) {
|
||||
return text.split(SEP).map(function(token) {
|
||||
if (!token) return '';
|
||||
let run = '';
|
||||
let out = '';
|
||||
for (const ch of token) {
|
||||
if (ch === ONE || ch === TEN) {
|
||||
run += ch;
|
||||
} else {
|
||||
if (run) {
|
||||
const n = parseEgyptian(run);
|
||||
out += (n >= 1 && n <= 26) ? String.fromCharCode(64 + n) : run;
|
||||
run = '';
|
||||
}
|
||||
out += ch;
|
||||
}
|
||||
}
|
||||
if (run) {
|
||||
const n = parseEgyptian(run);
|
||||
out += (n >= 1 && n <= 26) ? String.fromCharCode(64 + n) : run;
|
||||
}
|
||||
return out;
|
||||
}).join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[egyptian-numerals]';
|
||||
return this.func(text.slice(0, 4)) + (text.length > 4 ? '…' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
return /[\u{133E4}\u{13386}]/u.test(text);
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,47 @@
|
||||
// @generated from data/alphabets — do not edit by hand
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Enochian Alphabet',
|
||||
priority: 100,
|
||||
category: 'symbol',
|
||||
description: 'Enochian angelic script (Unicode approximations)',
|
||||
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 => this.map[c] || this.map[c.toUpperCase()] || c).join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[enochian]';
|
||||
return this.func(text.slice(0, 6)) + (text.length > 6 ? '…' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
return new RegExp('[ᛂᛃᛄᛅᛆᛇᛈᛉᛊᛋ]', 'u').test(text);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
// Eye of Horus (Wedjat) — A1Z26 mapped to the seven Wedjat fraction hieroglyphs
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
const PARTS = [
|
||||
'\u{13080}',
|
||||
'\u{13081}',
|
||||
'\u{13082}',
|
||||
'\u{13083}',
|
||||
'\u{13084}',
|
||||
'\u{13085}',
|
||||
'\u{13086}'
|
||||
];
|
||||
|
||||
function letterValue(ch) {
|
||||
const code = ch.toUpperCase().charCodeAt(0);
|
||||
if (code >= 65 && code <= 90) return code - 64;
|
||||
return 0;
|
||||
}
|
||||
|
||||
const REV = {};
|
||||
for (let n = 1; n <= 26; n++) {
|
||||
const glyph = PARTS[(n - 1) % PARTS.length];
|
||||
if (!REV[glyph]) REV[glyph] = String.fromCharCode(64 + n);
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Eye of Horus (Wedjat)',
|
||||
priority: 100,
|
||||
category: 'symbol',
|
||||
description: 'A1Z26 cycles through the seven Eye of Horus (Wedjat) fraction hieroglyphs',
|
||||
func: function(text) {
|
||||
return [...text].map(function(ch) {
|
||||
const n = letterValue(ch);
|
||||
return n ? PARTS[(n - 1) % PARTS.length] : ch;
|
||||
}).join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
return [...text].map(function(ch) {
|
||||
return REV[ch] || ch;
|
||||
}).join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[eye-of-horus]';
|
||||
return this.func(text.slice(0, 6)) + (text.length > 6 ? '…' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
return /[\u{13080}-\u{13086}]/u.test(text);
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,80 @@
|
||||
// Friderici window cipher — 4-pane window symbols per letter (1685 Fensterchiffre)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
// Pane order TL TR BL BR — B=black ▓, W=white □, D=dotted ◉
|
||||
// 24-letter Latin (J→I, V→U). Pane map from Friderici Cryptographia (1685).
|
||||
const MAP = {
|
||||
'A': 'WWBW', 'B': 'BBWW', 'C': 'BWWB', 'D': 'WBBB', 'E': 'WBWW', 'F': 'BBBW',
|
||||
'G': 'WWBB', 'H': 'WWWB', 'I': 'WBBW', 'K': 'BBWB', 'L': 'BWDB', 'M': 'BWBW',
|
||||
'N': 'BWBB', 'O': 'BWWW', 'P': 'WBWB', 'Q': 'DWWB', 'R': 'BBBB', 'S': 'BWWD',
|
||||
'T': 'BBBD', 'U': 'WBBD', 'W': 'BBDB', 'X': 'WWWW', 'Y': 'BBWD', 'Z': 'BBDW'
|
||||
};
|
||||
const REV = {};
|
||||
for (const [k, v] of Object.entries(MAP)) REV[v] = k;
|
||||
|
||||
function normalizeLatin24(text) {
|
||||
return text.toUpperCase().replace(/J/g, 'I').replace(/V/g, 'U');
|
||||
}
|
||||
|
||||
function paneChar(code) {
|
||||
if (code === 'B') return '\u2593';
|
||||
if (code === 'D') return '\u25C9';
|
||||
return '\u25A1';
|
||||
}
|
||||
|
||||
function encodeWindow(code) {
|
||||
return [...code].map(paneChar).join('');
|
||||
}
|
||||
|
||||
function decodeWindow(symbol) {
|
||||
let code = '';
|
||||
for (const ch of symbol) {
|
||||
if (ch === '\u2593') code += 'B';
|
||||
else if (ch === '\u25C9') code += 'D';
|
||||
else code += 'W';
|
||||
}
|
||||
return code.length === 4 ? REV[code] || '' : '';
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Friderici Cipher (Windows)',
|
||||
priority: 95,
|
||||
category: 'symbol',
|
||||
description: 'Friderici Fensterchiffre (1685): 4-pane windows (▓ □ ◉). J→I, V→U. Pane map from Cryptographia key.',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'separator',
|
||||
label: 'Separator between windows',
|
||||
type: 'select',
|
||||
default: 'space',
|
||||
options: [
|
||||
{ value: 'space', label: 'Space' },
|
||||
{ value: 'none', label: 'None' }
|
||||
]
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const sep = options.separator === 'none' ? '' : ' ';
|
||||
return [...normalizeLatin24(text)].map(function(c) {
|
||||
const code = MAP[c];
|
||||
return code ? encodeWindow(code) : c;
|
||||
}).join(sep);
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
options = options || {};
|
||||
const parts = options.separator === 'none'
|
||||
? text.match(/[\u2593\u25C9\u25A1]{4}/g) || []
|
||||
: text.trim().split(/\s+/);
|
||||
return parts.map(decodeWindow).join('');
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[friderici]';
|
||||
return this.func(text.slice(0, 4), options);
|
||||
},
|
||||
detector: function(text) {
|
||||
return /([\u2593\u25C9\u25A1]{4}\s*){2,}/.test(text);
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,47 @@
|
||||
// @generated from data/alphabets — do not edit by hand
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Malachim Alphabet',
|
||||
priority: 100,
|
||||
category: 'symbol',
|
||||
description: 'Malachim / angel script symbol substitution',
|
||||
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 => this.map[c] || this.map[c.toUpperCase()] || c).join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[malachim]';
|
||||
return this.func(text.slice(0, 6)) + (text.length > 6 ? '…' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
return new RegExp('[✁✂✃✄✆✇✈✉✊✋✌✍✎✏]', 'u').test(text);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
// @generated from data/alphabets — do not edit by hand
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Mary Stuart Cipher',
|
||||
priority: 100,
|
||||
category: 'symbol',
|
||||
description: 'Mary Queen of Scots nomenclator-style symbols',
|
||||
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 => this.map[c] || this.map[c.toUpperCase()] || c).join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[mary-stuart]';
|
||||
return this.func(text.slice(0, 6)) + (text.length > 6 ? '…' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
return new RegExp('[❶❷❸❹❺❻❼❽❾]', 'u').test(text);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
// Mayan numerals — A1Z26 letters to Unicode Mayan numeral glyphs (positional for 20+)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
const ZERO = '\u{124E0}';
|
||||
const SEP = '\u2063';
|
||||
|
||||
function letterValue(ch) {
|
||||
const code = ch.toUpperCase().charCodeAt(0);
|
||||
if (code >= 65 && code <= 90) return code - 64;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function mayanDigit(n) {
|
||||
if (n < 0 || n > 19) return '';
|
||||
if (n === 0) return ZERO;
|
||||
return String.fromCodePoint(0x124E0 + n);
|
||||
}
|
||||
|
||||
function mayanGlyph(n) {
|
||||
if (n < 1 || n > 26) return '';
|
||||
if (n <= 19) return mayanDigit(n);
|
||||
const hi = Math.floor(n / 20);
|
||||
const lo = n % 20;
|
||||
return mayanDigit(hi) + mayanDigit(lo);
|
||||
}
|
||||
|
||||
function codePointLen(text, index) {
|
||||
const cp = text.codePointAt(index);
|
||||
return cp > 0xFFFF ? 2 : 1;
|
||||
}
|
||||
|
||||
function decodeMayanToken(token) {
|
||||
let out = '';
|
||||
let i = 0;
|
||||
while (i < token.length) {
|
||||
const cp = token.codePointAt(i);
|
||||
if (cp >= 0x124E0 && cp <= 0x124F3) {
|
||||
const len = codePointLen(token, i);
|
||||
const cp2 = token.codePointAt(i + len);
|
||||
if (cp2 >= 0x124E0 && cp2 <= 0x124F3) {
|
||||
const pair = (cp - 0x124E0) * 20 + (cp2 - 0x124E0);
|
||||
if (pair >= 20 && pair <= 26) {
|
||||
out += String.fromCharCode(64 + pair);
|
||||
i += len + codePointLen(token, i + len);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const single = cp - 0x124E0;
|
||||
if (single >= 1 && single <= 19) {
|
||||
out += String.fromCharCode(64 + single);
|
||||
}
|
||||
i += len;
|
||||
} else {
|
||||
out += String.fromCodePoint(cp);
|
||||
i += cp > 0xFFFF ? 2 : 1;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Mayan Numerals',
|
||||
priority: 100,
|
||||
category: 'symbol',
|
||||
description: 'A1Z26 (A=1 … Z=26) as Unicode Mayan numerals (0–19 glyphs; 20+ uses positional pairs)',
|
||||
func: function(text) {
|
||||
let out = '';
|
||||
let prevMayan = false;
|
||||
for (const ch of text) {
|
||||
const n = letterValue(ch);
|
||||
if (n) {
|
||||
if (prevMayan) out += SEP;
|
||||
out += mayanGlyph(n);
|
||||
prevMayan = true;
|
||||
} else {
|
||||
out += ch;
|
||||
prevMayan = false;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
},
|
||||
reverse: function(text) {
|
||||
return text.split(SEP).map(decodeMayanToken).join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[mayan-numerals]';
|
||||
return this.func(text.slice(0, 6)) + (text.length > 6 ? '…' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
return /[\u{124E0}-\u{124F3}]/u.test(text);
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,47 @@
|
||||
// @generated from data/alphabets — do not edit by hand
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Moon Alphabet',
|
||||
priority: 100,
|
||||
category: 'symbol',
|
||||
description: 'Moon phase / lunar symbol alphabet',
|
||||
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 => this.map[c] || this.map[c.toUpperCase()] || c).join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[moon-alphabet]';
|
||||
return this.func(text.slice(0, 6)) + (text.length > 6 ? '…' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
return new RegExp('[☾☽☊☋⚸⚹⚺⚻]', 'u').test(text);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
// @generated from data/alphabets — do not edit by hand
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Passing the River Alphabet',
|
||||
priority: 100,
|
||||
category: 'symbol',
|
||||
description: 'Golden Dawn Passing the River tarot script',
|
||||
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 => this.map[c] || this.map[c.toUpperCase()] || c).join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[passing-the-river]';
|
||||
return this.func(text.slice(0, 6)) + (text.length > 6 ? '…' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
return new RegExp('[♠♣♥♦♤♧♡♢]', 'u').test(text);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
// Periodic table cipher — letters to element symbols
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
const MAP = {
|
||||
'A': 'Ag', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'Es', 'F': 'F', 'G': 'Ge',
|
||||
'H': 'H', 'I': 'I', 'J': 'Jr', 'K': 'K', 'L': 'Li', 'M': 'Mg', 'N': 'N',
|
||||
'O': 'O', 'P': 'P', 'Q': 'Qu', 'R': 'Ra', 'S': 'S', 'T': 'Ti', 'U': 'U',
|
||||
'V': 'V', 'W': 'W', 'X': 'Xe', 'Y': 'Y', 'Z': 'Zn'
|
||||
};
|
||||
const REV = {};
|
||||
for (const [k, v] of Object.entries(MAP)) REV[v] = k;
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Periodic Table Cipher',
|
||||
priority: 86,
|
||||
category: 'cipher',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'separator',
|
||||
label: 'Separator between symbols',
|
||||
type: 'select',
|
||||
default: 'space',
|
||||
options: [
|
||||
{ value: 'space', label: 'Space' },
|
||||
{ value: 'none', label: 'None' }
|
||||
]
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const sep = options.separator === 'none' ? '' : ' ';
|
||||
return [...text.toUpperCase()].filter(c => /[A-Z]/.test(c)).map(c => MAP[c] || c).join(sep);
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
options = options || {};
|
||||
const tokens = options.separator === 'none'
|
||||
? text.match(/[A-Z][a-z]?/g) || []
|
||||
: text.trim().split(/\s+/);
|
||||
return tokens.map(t => REV[t] || '').join('');
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[periodic]';
|
||||
return this.func(text.slice(0, 8), options);
|
||||
},
|
||||
detector: function(text) {
|
||||
const tokens = text.trim().split(/\s+/);
|
||||
return tokens.length >= 2 && tokens.every(t => REV[t] !== undefined);
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -6,8 +6,7 @@ export default new BaseTransformer({
|
||||
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
|
||||
// Standard Pigpen cipher mapping (original variant, Unicode symbol set)
|
||||
// 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)
|
||||
@@ -38,7 +37,7 @@ export default new BaseTransformer({
|
||||
return this.func(text.slice(0, 5));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check if text contains Pigpen symbols (dCode.fr Unicode characters)
|
||||
// Check if text contains Pigpen symbols
|
||||
const pigpenSymbols = /[ᒧ⊔ᒪ⊐☐⊏ᒣ⊓ᒥ⟓⨃ᒷ⪾🝕⪽ᒬ⩀⟔ᐯᐳᐸᐱ⟇ᑀᑅ⟑]/;
|
||||
return pigpenSymbols.test(text);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// @generated from data/alphabets — do not edit by hand
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Rosicrucian Cipher',
|
||||
priority: 100,
|
||||
category: 'symbol',
|
||||
description: 'Rosicrucian / Golden Dawn symbol alphabet',
|
||||
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 => this.map[c] || this.map[c.toUpperCase()] || c).join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[rosicrucian]';
|
||||
return this.func(text.slice(0, 6)) + (text.length > 6 ? '…' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
return new RegExp('[⛤⛥⛦⛧⛨⛩⛪⛫]', 'u').test(text);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
// 7-segment display encoding (segment masks a-g)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
// Segment order: abcdefg (1 = lit)
|
||||
const MASK = {
|
||||
'0': '1111110', '1': '0110000', '2': '1101101', '3': '1111001', '4': '0110011',
|
||||
'5': '1011011', '6': '1011111', '7': '1110000', '8': '1111111', '9': '1111011',
|
||||
'A': '1110111', 'B': '0011111', 'C': '1001110', 'D': '0111101', 'E': '1001111',
|
||||
'F': '1000111', '-': '0000001', ' ': '0000000'
|
||||
};
|
||||
const REV = {};
|
||||
for (const [ch, mask] of Object.entries(MASK)) {
|
||||
if (!REV[mask]) REV[mask] = ch;
|
||||
}
|
||||
|
||||
function toMask(ch) {
|
||||
const u = ch.toUpperCase();
|
||||
return MASK[u] || MASK[ch] || null;
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: '7-Segment Display',
|
||||
priority: 85,
|
||||
category: 'electronics',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'separator',
|
||||
label: 'Separator between characters',
|
||||
type: 'select',
|
||||
default: 'space',
|
||||
options: [
|
||||
{ value: 'space', label: 'Space' },
|
||||
{ value: 'pipe', label: 'Pipe (|)' },
|
||||
{ value: 'none', label: 'None' }
|
||||
]
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const sep = options.separator === 'pipe' ? '|' : (options.separator === 'none' ? '' : ' ');
|
||||
return [...text].map(c => toMask(c) || '0000000').join(sep);
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
options = options || {};
|
||||
const parts = options.separator === 'pipe'
|
||||
? text.split('|')
|
||||
: text.trim().split(/\s+/);
|
||||
return parts.map(p => {
|
||||
const mask = p.replace(/[^01]/g, '');
|
||||
if (mask.length !== 7) return '';
|
||||
return REV[mask] || '?';
|
||||
}).join('');
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[7seg]';
|
||||
return this.func(text.slice(0, 4), options) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
const tokens = text.trim().split(/[\s|]+/);
|
||||
return tokens.length >= 2 && tokens.every(t => /^[01]{7}$/.test(t));
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,46 @@
|
||||
// Standard Galactic Alphabet (Commander Keen / Minecraft Enchanting Table)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Minecraft Enchanting Table',
|
||||
priority: 100,
|
||||
category: 'fantasy',
|
||||
description: 'Standard Galactic Alphabet (Commander Keen / Minecraft enchanting table)',
|
||||
map: {
|
||||
'A': 'ᔑ', 'B': 'ʖ', 'C': 'ᓵ', 'D': '↸', 'E': 'ᒷ', 'F': '⎓', 'G': '⊣',
|
||||
'H': '⍑', 'I': '╎', 'J': '⋮', 'K': 'ꖌ', 'L': 'ꖎ', 'M': 'ᒲ', 'N': 'リ',
|
||||
'O': '𝙹', 'P': '!¡', 'Q': 'ᑑ', 'R': '∷', 'S': 'ᓭ', 'T': 'ℸ\u0323', 'U': '⚍',
|
||||
'V': '⍊', 'W': '∴', 'X': '\u0307/', 'Y': '||', 'Z': '⨅'
|
||||
},
|
||||
func: function(text) {
|
||||
return [...text].map(c => {
|
||||
const upperC = c.toUpperCase();
|
||||
return this.map[upperC] || c;
|
||||
}).join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
const revMap = new Map();
|
||||
for (const [letter, sgaChar] of Object.entries(this.map)) {
|
||||
if (sgaChar && sgaChar !== letter) {
|
||||
revMap.set(sgaChar, letter);
|
||||
}
|
||||
}
|
||||
let result = text;
|
||||
result = result.replace(/!¡/g, 'P');
|
||||
result = result.replace(/ℸ\u0323/g, 'T');
|
||||
result = result.replace(/\u0307\//g, 'X');
|
||||
result = result.replace(/\|\|/g, 'Y');
|
||||
let decoded = '';
|
||||
for (const ch of result) {
|
||||
decoded += revMap.has(ch) ? revMap.get(ch) : ch;
|
||||
}
|
||||
return decoded;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[sga]';
|
||||
return this.func(text.slice(0, 8));
|
||||
},
|
||||
detector: function(text) {
|
||||
return /[ᔑʖᓵ↸ᒷ⎓⊣⍑╎⋮ꖌꖎᒲリ𝙹ᑑ∷ᓭ⚍⍊∴⨅]/.test(text);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
// @generated from data/alphabets — do not edit by hand
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Templars Cipher',
|
||||
priority: 100,
|
||||
category: 'symbol',
|
||||
description: 'Templar / pigpen variant with dot markers',
|
||||
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 => this.map[c] || this.map[c.toUpperCase()] || c).join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[templars]';
|
||||
return this.func(text.slice(0, 6)) + (text.length > 6 ? '…' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
return new RegExp('[◧◨◩◪◫◬◭◮◯]', 'u').test(text);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
// @generated from data/alphabets — do not edit by hand
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Theban Alphabet',
|
||||
priority: 100,
|
||||
category: 'symbol',
|
||||
description: 'Honoric / Theban witch alphabet (symbol substitution)',
|
||||
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 => this.map[c] || this.map[c.toUpperCase()] || c).join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[theban]';
|
||||
return this.func(text.slice(0, 6)) + (text.length > 6 ? '…' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
return new RegExp('[∀∁∂∃∄∅∆∇∈∉∊∋∌∍∎∏∐∑−∓∔∕∖∗∘∙√∛∜∝]', 'u').test(text);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
// Younger Futhark — 16-rune medieval alphabet (J→I, V→U; lossy Latin round-trip)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
const MAP = {
|
||||
A: '\u16BC', B: '\u16D2', C: '\u16B9', D: '\u16CF', E: '\u16C1',
|
||||
F: '\u16A0', G: '\u16B9', H: '\u16BC', I: '\u16C1', J: '\u16C1',
|
||||
K: '\u16B9', L: '\u16D2', M: '\u16D2', N: '\u16BE', O: '\u16A2',
|
||||
P: '\u16D2', Q: '\u16B9', R: '\u16B1', S: '\u16C7', T: '\u16CF',
|
||||
U: '\u16A6', V: '\u16A0', W: '\u16A6', X: '\u16C7', Y: '\u16E6',
|
||||
Z: '\u16C7'
|
||||
};
|
||||
|
||||
const REV = {
|
||||
'\u16A0': 'F', '\u16A2': 'O', '\u16A6': 'U', '\u16B1': 'R', '\u16B9': 'K',
|
||||
'\u16BC': 'H', '\u16BE': 'N', '\u16C1': 'I', '\u16C7': 'S', '\u16CF': 'T',
|
||||
'\u16D2': 'L', '\u16E6': 'Y'
|
||||
};
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Younger Futhark',
|
||||
priority: 100,
|
||||
category: 'symbol',
|
||||
description: 'Younger Futhark runes for Latin letters (16 runes; J→I, V→U; lossy reverse)',
|
||||
func: function(text) {
|
||||
return [...text.toUpperCase()].map(function(ch) {
|
||||
return MAP[ch] || ch;
|
||||
}).join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
return [...text].map(function(ch) {
|
||||
return REV[ch] || ch;
|
||||
}).join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[younger-futhark]';
|
||||
return this.func(text.slice(0, 6)) + (text.length > 6 ? '…' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
return /[\u16A0-\u16FF]/u.test(text);
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,66 @@
|
||||
// DTMF (Dual-Tone Multi-Frequency) telephone tone codes
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
const DTMF = {
|
||||
'1': '697+1209', '2': '697+1336', '3': '697+1477',
|
||||
'4': '770+1209', '5': '770+1336', '6': '770+1477',
|
||||
'7': '852+1209', '8': '852+1336', '9': '852+1477',
|
||||
'*': '941+1209', '0': '941+1336', '#': '941+1477'
|
||||
};
|
||||
const REV = Object.fromEntries(Object.entries(DTMF).map(([k, v]) => [v, k]));
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'DTMF Code',
|
||||
priority: 70,
|
||||
category: 'technical',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'format',
|
||||
label: 'Output format',
|
||||
type: 'select',
|
||||
default: 'freq',
|
||||
options: [
|
||||
{ value: 'freq', label: '697+1209 (Hz pairs)' },
|
||||
{ value: 'compact', label: '697-1209' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'spacing',
|
||||
label: 'Space between tone codes',
|
||||
type: 'boolean',
|
||||
default: true
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const compact = options.format === 'compact';
|
||||
const spaced = options.spacing !== false;
|
||||
const sep = compact ? '-' : '+';
|
||||
const parts = [...text].map(function(c) {
|
||||
const code = DTMF[c];
|
||||
if (!code) {
|
||||
return c;
|
||||
}
|
||||
return compact ? code.replace('+', sep) : code;
|
||||
});
|
||||
return spaced ? parts.join(' ') : parts.join('');
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
options = options || {};
|
||||
const spaced = options.spacing !== false;
|
||||
const tokens = spaced ? text.trim().split(/\s+/) : text.split('');
|
||||
return tokens.map(function(tok) {
|
||||
const norm = tok.replace(/-/g, '+');
|
||||
return REV[norm] || tok;
|
||||
}).join('');
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[dtmf]';
|
||||
return this.func(text.slice(0, 4), options) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
return /\d{3}[+\-]\d{4}/.test(text);
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,62 @@
|
||||
// Navajo code — WWII code talker alphabet (letter → Navajo word)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
const ALPHA = {
|
||||
'A': 'WOL-LA-CHEE', 'B': 'SHUSH', 'C': 'MOASI', 'D': 'BE', 'E': 'DZEH',
|
||||
'F': 'MA-E', 'G': 'KLIZZIE', 'H': 'LIN', 'I': 'TKIN', 'J': 'TKELE-CHO-G',
|
||||
'K': 'KLIZZIE-YAZZIE', 'L': 'DIBEH-YAZZIE', 'M': 'NA-AS-TSO-SI', 'N': 'NESH-CHEE',
|
||||
'O': 'NE-AHS-JAH', 'P': 'BI-SO-DIH', 'Q': 'CA-YEILTH', 'R': 'GAH', 'S': 'DIBEH',
|
||||
'T': 'THAN-ZIE', 'U': 'NO-DA-IH', 'V': 'A-KEH-DI-GLINI', 'W': 'GLOE-IH',
|
||||
'X': 'AL-NA-AS-DZOH', 'Y': 'TSAH-AS-ZIH', 'Z': 'BESH-DO-TLIZ'
|
||||
};
|
||||
const REV = {};
|
||||
for (const [k, v] of Object.entries(ALPHA)) {
|
||||
REV[v.toUpperCase()] = k;
|
||||
}
|
||||
|
||||
function normalizeToken(t) {
|
||||
return t.toUpperCase().replace(/\s/g, '-');
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Navajo Code',
|
||||
priority: 88,
|
||||
category: 'cipher',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'separator',
|
||||
label: 'Separator between words',
|
||||
type: 'select',
|
||||
default: 'space',
|
||||
options: [
|
||||
{ value: 'space', label: 'Space' },
|
||||
{ value: 'newline', label: 'New line' }
|
||||
]
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const sep = options.separator === 'newline' ? '\n' : ' ';
|
||||
return [...text].filter(c => /[A-Za-z]/.test(c)).map(c => {
|
||||
return ALPHA[c.toUpperCase()] || c.toUpperCase();
|
||||
}).join(sep);
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
options = options || {};
|
||||
const splitRe = options.separator === 'newline' ? /\n+/ : /\s+/;
|
||||
return text.trim().split(splitRe).filter(Boolean).map(t => {
|
||||
return REV[normalizeToken(t)] || '';
|
||||
}).join('');
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[navajo]';
|
||||
return this.func(text.slice(0, 6), options);
|
||||
},
|
||||
detector: function(text) {
|
||||
const upper = text.toUpperCase();
|
||||
const hits = (upper.match(/WOL-LA-CHEE|DZEH|KLIZZIE|NE-AHS-JAH|DIBEH/g) || []).length;
|
||||
return hits >= 2;
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,51 @@
|
||||
// Phone keypad cipher (letters to dial digits, one press each)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
const KEYPAD = {
|
||||
'A': '2', 'B': '2', 'C': '2',
|
||||
'D': '3', 'E': '3', 'F': '3',
|
||||
'G': '4', 'H': '4', 'I': '4',
|
||||
'J': '5', 'K': '5', 'L': '5',
|
||||
'M': '6', 'N': '6', 'O': '6',
|
||||
'P': '7', 'Q': '7', 'R': '7', 'S': '7',
|
||||
'T': '8', 'U': '8', 'V': '8',
|
||||
'W': '9', 'X': '9', 'Y': '9', 'Z': '9',
|
||||
'0': '0', '1': '1'
|
||||
};
|
||||
const REV = {};
|
||||
for (const [letter, digit] of Object.entries(KEYPAD)) {
|
||||
if (!REV[digit]) REV[digit] = letter;
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Phone Keypad Cipher',
|
||||
priority: 70,
|
||||
category: 'technical',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'spacing',
|
||||
label: 'Space between digits',
|
||||
type: 'boolean',
|
||||
default: true
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const digits = [...text.toUpperCase()].map(c => KEYPAD[c] || (/[0-9]/.test(c) ? c : '')).filter(Boolean);
|
||||
return options.spacing ? digits.join(' ') : digits.join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
const tokens = text.trim().split(/\s+/).join('').split('');
|
||||
return tokens.map(d => REV[d] || d).join('');
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[keypad]';
|
||||
return this.func(text.slice(0, 6), options) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
const cleaned = text.replace(/\s/g, '');
|
||||
return cleaned.length >= 4 && /^[0-9]+$/.test(cleaned);
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,67 @@
|
||||
// T9 / multi-tap cellphone encoding
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default (function() {
|
||||
const KEYPAD = {
|
||||
'2': 'ABC', '3': 'DEF', '4': 'GHI', '5': 'JKL',
|
||||
'6': 'MNO', '7': 'PQRS', '8': 'TUV', '9': 'WXYZ'
|
||||
};
|
||||
|
||||
function letterToT9(ch) {
|
||||
const u = ch.toUpperCase();
|
||||
if (u < 'A' || u > 'Z') return ch;
|
||||
for (const [digit, letters] of Object.entries(KEYPAD)) {
|
||||
const idx = letters.indexOf(u);
|
||||
if (idx >= 0) return digit.repeat(idx + 1);
|
||||
}
|
||||
return ch;
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'T9 Multi-tap',
|
||||
priority: 70,
|
||||
category: 'technical',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'spacing',
|
||||
label: 'Space between letters',
|
||||
type: 'boolean',
|
||||
default: true
|
||||
}
|
||||
],
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const parts = [...text].map(c => letterToT9(c));
|
||||
return options.spacing ? parts.join(' ') : parts.join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
const cleaned = text.replace(/\s/g, '');
|
||||
let out = '';
|
||||
let i = 0;
|
||||
while (i < cleaned.length) {
|
||||
const d = cleaned[i];
|
||||
if (!KEYPAD[d]) {
|
||||
out += d;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
let count = 0;
|
||||
while (i < cleaned.length && cleaned[i] === d) {
|
||||
count++;
|
||||
i++;
|
||||
}
|
||||
const letters = KEYPAD[d];
|
||||
out += letters[(count - 1) % letters.length];
|
||||
}
|
||||
return out;
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[t9]';
|
||||
return this.func(text.slice(0, 5), options) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
const cleaned = text.replace(/\s/g, '');
|
||||
return cleaned.length >= 4 && /^[2-9]+$/.test(cleaned);
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -1,25 +1,35 @@
|
||||
// bubble transform
|
||||
// bubble transform — circled letters (incl. lowercase) and circled digits
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
|
||||
name: 'Bubble',
|
||||
name: 'Bubble',
|
||||
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('');
|
||||
},
|
||||
// Detector: Check for bubble (enclosed alphanumerics) characters
|
||||
detector: function(text) {
|
||||
// Enclosed alphanumerics (U+24B6-U+24EA for circled letters)
|
||||
return /[ⓐ-ⓩⒶ-Ⓩ]/.test(text);
|
||||
'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('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
const rev = {};
|
||||
for (const [key, value] of Object.entries(this.map)) {
|
||||
rev[value] = key;
|
||||
}
|
||||
|
||||
});
|
||||
return [...text].map(c => rev[c] || c).join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[bubble]';
|
||||
return this.func(text.slice(0, 5));
|
||||
},
|
||||
detector: function(text) {
|
||||
return /[\u2460-\u2469\u24B6-\u24EA]/u.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -17,6 +17,18 @@ export default new BaseTransformer({
|
||||
'(': ')', ')': '(', '[': ']', ']': '[', '{': '}', '}': '{', '<': '>', '>': '<',
|
||||
'&': '⅋', '_': '‾'
|
||||
},
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'mode',
|
||||
label: 'Orientation',
|
||||
type: 'select',
|
||||
default: 'rotate180',
|
||||
options: [
|
||||
{ value: 'rotate180', label: '180° rotation (flip + reverse)' },
|
||||
{ value: 'flipVertical', label: 'Vertical flip (flip only)' }
|
||||
]
|
||||
}
|
||||
],
|
||||
// Create reverse map for decoding
|
||||
reverseMap: function() {
|
||||
const revMap = {};
|
||||
@@ -25,16 +37,26 @@ export default new BaseTransformer({
|
||||
}
|
||||
return revMap;
|
||||
},
|
||||
func: function(text) {
|
||||
return [...text].map(c => this.map[c] || c).reverse().join('');
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const flipped = [...text].map(c => this.map[c] || c);
|
||||
if (options.mode === 'flipVertical') {
|
||||
return flipped.join('');
|
||||
}
|
||||
return flipped.reverse().join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
preview: function(text, options) {
|
||||
if (!text) return '[upside down]';
|
||||
return this.func(text.slice(0, 8));
|
||||
return this.func(text.slice(0, 8), options);
|
||||
},
|
||||
reverse: function(text) {
|
||||
reverse: function(text, options) {
|
||||
options = options || {};
|
||||
const revMap = this.reverseMap();
|
||||
return [...text].map(c => revMap[c] || c).reverse().join('');
|
||||
const restored = [...text].map(c => revMap[c] || c);
|
||||
if (options.mode === 'flipVertical') {
|
||||
return restored.join('');
|
||||
}
|
||||
return restored.reverse().join('');
|
||||
}
|
||||
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user