lots of transform updates, additions, etc. updated docs. standardized transform dropdown. added qr/bar code tool.

This commit is contained in:
ph1r3754r73r
2026-06-13 13:42:37 -07:00
parent a401bf5b58
commit b90397f0c0
137 changed files with 7636 additions and 449 deletions
+9 -5
View File
@@ -57,13 +57,14 @@ P4RS3LT0NGV3/
│ └── transformers/ # Transformer sources → bundled to dist/js/bundles/
│ ├── BaseTransformer.js
│ ├── index.js # Generated by npm run build:index (gitignored)
│ ├── ancient/
│ ├── case/
│ ├── cipher/
│ ├── concealment/
│ ├── encoding/
│ ├── fantasy/
│ ├── format/
│ ├── signwriting/
│ ├── special/
│ ├── symbol/
│ ├── technical/
│ ├── unicode/
│ └── visual/
@@ -89,7 +90,8 @@ P4RS3LT0NGV3/
│ ├── fetch-glitch-data.js
│ ├── inject-tool-scripts.js # Discovers tools; updates index.template.html + toolRegistry
│ ├── inject-tool-templates.js # Builds dist/index.html from index.template.html + templates/
── readme-transform-section.js # Maintainer helper for README transform list
── build-code-vendor.js # Bundles QR/barcode libs → js/vendor/
│ └── build-alphabet-transforms.js # Regenerates symbol/*.js from data/alphabets/
├── tests/
│ ├── test_universal.js
│ └── test_steganography_options.js
@@ -172,7 +174,6 @@ Transformers are the core text transformation logic. See `src/transformers/READM
export default new BaseTransformer({
name: 'My Cipher',
priority: 60, // See priority guide in transformers/README.md
category: 'ciphers',
func: function(text) {
// Encoding logic
return encoded;
@@ -203,7 +204,10 @@ Transformers are the core text transformation logic. See `src/transformers/READM
- Add test cases to `tests/test_universal.js`
- Run `npm test` to verify
**Important:** Transformers are automatically discovered and bundled. No manual registration needed!
6. Update documentation:
- Add a bullet to the matching category under **Text Transformations** in the root `README.md`
**Important:** Transformers are automatically discovered and bundled. Category is set from the parent folder at build time — no manual registration needed.
### 2. Adding a New Tool (New Tab/Feature)
+107 -46
View File
@@ -1,6 +1,6 @@
# 🐍 P4RS3LT0NGV3 - Universal Text Translator
A powerful web-based text transformation and steganography tool with **159** built-in text transforms spanning encodings, classical and modern ciphers, Unicode styles, formatting, and niche alphabets. Think of it as a universal translator for ALL alphabets and writing systems!
A powerful web-based text transformation and steganography tool with **223** built-in text transforms spanning encodings, classical and modern ciphers, Unicode styles, formatting, and niche alphabets. Think of it as a universal translator for ALL alphabets and writing systems!
The app is a **static site**: run **`npm run build`** (after `npm install`), then open **`dist/index.html`** in your browser—no local server required. **Alternatively**, you can run it as a local app over HTTP with **`npm start`** or **`npx serve dist -l 8080`** (see [Getting Started](#getting-started) below). Core transforms, decoder, and steganography work **without** calling the cloud; features that use [OpenRouter](https://openrouter.ai/) need **network access** and an API key (see below).
@@ -15,22 +15,22 @@ The app is a **static site**: run **`npm run build`** (after `npm install`), the
Categories match the Transform tab and the folders under `src/transformers/` (each transformers `name` as shown in the UI). Short descriptions explain what each transform does.
#### **Ancient**
- **Elder Futhark** - Germanic Elder Futhark runes
- **Hieroglyphics** - Egyptian hieroglyph-style mapping
- **Ogham (Celtic)** - Celtic Ogham tree alphabet
- **Roman Numerals** - Arabic numerals ↔ Roman numerals
#### **Case**
- **Alternating Case** - Alternate uppercase and lowercase per letter (first letter upper or lower)
- **camelCase** - lowerCamelCase for identifiers
- **Capitalize Words** - Capitalize the first letter of each word
- **kebab-case** - kebab-case for slugs and identifiers
- **Lowercase All** - Lowercase entire text
- **Random Case** - Random casing per character
- **Sentence Case** - Capitalize the first letter of each sentence
- **snake_case** - snake_case for identifiers
- **Title Case** - Capitalize each word
- **Toggle Case** - Swap case of each letter
- **Uppercase All** - Uppercase entire text
#### **Cipher**
- **A1Z26** - A=1 … Z=26 letter numbering
- **Acéré Cipher** - Solfege / duration encoding for musical steganography
- **ADFGX Cipher** - WWI ADFGVX-style polybius + column transposition
- **N7AX Cipher** - ADFGX-style polybius with N7AX coordinates + column transposition
- **Affine Cipher** - Affine substitution (ax + b mod 26)
@@ -40,16 +40,21 @@ Categories match the Transform tab and the folders under `src/transformers/` (ea
- **Beaufort Cipher** - Beaufort key-table polyalphabetic cipher
- **Bifid Cipher** - Polybius square + row/column interleaving
- **Caesar Cipher** - Classic alphabet shift (configurable)
- **Codons (Genetic Code)** - Letters AZ as DNA triplets
- **Columnar Transposition** - Columnar transposition with a keyword
- **Double Transposition** - Two keyed columnar transpositions
- **Four-Square Cipher** - Four 5×5 squares; digraph substitution
- **Fractionated Morse** - Morse with fractionation pattern
- **Gronsfeld Cipher** - Vigenère family with numeric key
- **Hill Cipher** - Matrix-based multi-letter substitution
- **Homophonic Cipher** - Multiple ciphertext symbols per plaintext letter
- **Multiplicative Cipher** - Multiply by key mod 26
- **Nihilist Cipher** - Keyed Polybius + additive encryption
- **Pigpen Cipher** - Masonic / pigpen grid symbols
- **Playfair Cipher** - Digraph cipher on a 5×5 square
- **Polybius Square** - Letter ↔ grid coordinates
- **Porta Cipher** - Porta table polyalphabetic cipher
- **QWERTY Right Shift** - Map keys to the key to the right on QWERTY
- **Route Cipher** - Read ciphertext along a grid route
- **Rail Fence** - Zig-zag rail-fence transposition
- **ROT128** - UTF-16 code unit rotation by 128
- **ROT13** - Rotate Latin letters by 13 places
@@ -58,11 +63,24 @@ Categories match the Transform tab and the folders under `src/transformers/` (ea
- **ROT5** - Rotate digits 09 by 5
- **ROT8000** - Plane-0 Unicode BMP rotation cipher
- **Scytale Cipher** - Wrap-around strip (scytale) transposition
- **Tap Code** - Polybius / tap / prison code
- **Trifid Cipher** - Three Polybius cubes + trifid grouping
- **Trithemius Cipher** - Progressive Caesar (tabula recta)
- **Two-Square Cipher** - Digraph cipher with two Playfair squares
- **Vernam Cipher** - One-time pad XOR (mod 26)
- **Vigenère Cipher** - Polyalphabetic cipher with repeating keyword
- **XOR Cipher** - XOR with a repeating key
#### **Concealment**
- **Acrostic** - First letter of each line or word spells a message
- **Cardan Grille** - Hide text through a rotating grille
- **Homoglyph Generator** - Latin letters to Cyrillic homoglyphs
- **Invisible Text** - Unicode Tags / invisible carrier encoding
- **Null Cipher** - Fixed letter position in each cover word
- **Trevanion Cipher** - Letters N positions after punctuation marks
- **Whitespace Steganography** - Hide bits in whitespace patterns
- **Zero-Width Steganography** - Hide data with zero-width characters
#### **Encoding**
- **ASCII85** - Ascii85 / Adobe-style base-85 encoding
- **Base122** - Binary → 122 printable ASCII characters
@@ -74,46 +92,39 @@ Categories match the Transform tab and the folders under `src/transformers/` (ea
- **Base64** - Standard Base64
- **Base64 URL** - Base64url (URL-safe alphabet)
- **Base91** - basE91 / Ascii91 encoding
- **Bibi-binary Code** - UTF-8 bytes via Bibi-binary syllables
- **Baudot Code (ITA2)** - Five-bit telegraph / ITA2
- **Binary Coded Decimal** - Decimal digits as BCD nibbles
- **Binary** - Text bytes ↔ binary strings
- **Bitwise NOT** - UTF-8 bytes NOT'd per byte; encode output is hex (decode pastes hex back to text)
- **Brainfuck** - Text ↔ Brainfuck program
- **Decabit Code** - Ten-bit patterns for decimal digits
- **EBCDIC** - EBCDIC byte encoding
- **Emoji Encoding** - Payload encoded with emoji
- **Base256Emoji** - Multiformats multibase encoding (1 byte → 1 emoji)
- **Gray Code** - Binary Gray code
- **Hexadecimal** - Hex encode/decode bytes
- **HTML Entities** - HTML entity escape / unescape
- **Invisible Text** - Unicode Tags / invisible carrier encoding
- **Manchester Code** - Manchester line coding
- **Metaphone** - Metaphone phonetic encoding
- **Quoted-Printable** - MIME quoted-printable
- **Shadoks Numeral System** - UTF-8 bytes as Shadoks base-4 words
- **Unicode Code Points** - Characters ↔ U+XXXX code points
- **URL Encode** - application/x-www-form-urlencoded
- **Uuencoding** - Classic uuencode / uudecode
- **YEnc** - yEnc line-oriented binary encoding
- **Z85** - ZeroMQ Z85 encoding
#### **Fantasy**
- **Aurebesh (Star Wars)** - Galactic Basic Aurebesh alphabet
- **Dovahzul (Dragon)** - Skyrim Dovahzul transliteration
- **Klingon** - Klingon transliteration
- **Quenya (Tolkien Elvish)** - Tolkien Quenya mapping
- **Tengwar Script** - Elvish Tengwar script
#### **Format**
- **Bitwise NOT** - UTF-8 bytes NOT'd per byte; encode output is hex (decode pastes hex back to text)
- **Boustrophedon** - Serpentine / alternating line direction
- **Capitalize Words** - Capitalize the first letter of each word
- **Group Letters** - Insert separators between letters
- **Indent** - Add leading spaces to each line (configurable width)
- **Javanais** - French “javanais” vowel-insertion game
- **Latin Gibberish** - Latin-flavored pseudo-text
- **Leetspeak** - 1337-style character substitutions
- **Letters Only** - Keep letters; strip other characters
- **Letters & Numbers Only** - Alphanumeric only
- **Line Numbers** - Prefix lines with numbers (start and column width configurable)
- **Louchebem** - French argot (loucherbem-style)
- **Lowercase All** - Lowercase entire text
- **Leading Zeros** - Pad numbers with leading zeros
- **List Deduplicate** - Remove duplicate lines from a list
- **Mirror Digits** - Mirror digits 09 visually
- **Numbers Only** - Digits only
- **Pig Latin** - English Pig Latin
- **QWERTY Right Shift** - Map keys to the key to the right on QWERTY
- **Remove Accents** - Strip diacritics / combining marks
- **Remove Consonants** - Remove consonant letters
- **Remove Duplicates** - Remove duplicate lines
@@ -128,35 +139,83 @@ Categories match the Transform tab and the folders under `src/transformers/` (ea
- **Reverse Text** - Reverse character order
- **Shuffle Characters** - Shuffle characters (random order)
- **Shuffle Words** - Shuffle word order
- **Shuffled Letters** - Randomize letter order within each word
- **Spaces Remover** - Remove space characters
- **Text Justify** - Pad each line to a fixed width (left, right, or center); not word-spacing justify
- **Uppercase All** - Uppercase entire text
- **Toggle Case** - Swap case of each letter
- **Whitespace Steganography** - Hide bits in whitespace patterns
- **Typoglycemia** - Scrambled middle letters (readable chaos)
- **Word Letter Add** - Insert a letter at a fixed position in each word
- **Word Letter Change** - Replace a letter at a fixed position in each word
- **Word Letter Remove** - Remove a letter at a fixed position in each word
- **Word Wrap** - Break long lines at spaces so each line fits a maximum width
- **Zero-Width Steganography** - Hide data with zero-width characters
#### **SignWriting**
- **ASL SignWriting** - American Sign Language fingerspelling (ISWA)
- **LIBRAS SignWriting** - Brazilian Sign Language fingerspelling
- **JSL SignWriting** - Japanese Sign Language (hiragana input)
- **IPA Lip-reading** - IPA symbols in SignWriting
- **Morse Blink** - Morse as SignWriting blink marks
- **Tactile SignWriting** - Tactile SignWriting notation
#### **Special**
- **Random Mix** - Pick random transforms and chain them
#### **Technical**
- **A1Z26** - A=1 … Z=26 letter numbering
- **Braille** - Unicode Braille patterns
- **Brainfuck** - Text ↔ Brainfuck program
- **ICAO Spelling Alphabet** - ICAO radiotelephony spelling
- **ITU Spelling Alphabet** - ITU phonetic / spelling alphabet
- **DTMF Tones** - Dual-tone multi-frequency telephone codes
- **Maritime Signal Flags** - International maritime signal flags
- **Morse Code** - International Morse code
- **Navajo Code** - WWII Navajo word code for AZ
- **NATO Phonetic** - NATO phonetic alphabet
- **Semaphore Flags** - Flag semaphore arm positions
- **Tap Code** - Polybius / tap / prison code
- **T9 (Predictive Text)** - Phone T9 multi-tap encoding
- **Phone Keypad** - Digits from phone keypad groups
#### **Symbol**
- **Alchemical Symbols** - Alchemical symbol alphabet
- **Aurebesh (Star Wars)** - Galactic Basic Aurebesh alphabet
- **Babylonian Numerals** - A1Z26 as cuneiform numerals
- **Braille** - Unicode Braille patterns
- **Celestial Alphabet** - Celestial / angelic script
- **Chemical Symbols** - Chemical element symbols
- **Daedric (Elder Scrolls)** - Daedric alphabet
- **Dancing Men (Sherlock Holmes)** - Dancing figure cipher
- **Dominos in Digits** - Digits as domino tile notation
- **Dovahzul (Dragon)** - Skyrim Dovahzul transliteration
- **Egyptian Numerals** - A1Z26 as hieroglyph numerals
- **Elder Futhark** - Germanic Elder Futhark runes
- **Enochian** - Enochian angelic alphabet
- **Eye of Horus (Wedjat)** - Wedjat fraction hieroglyphs
- **Friderici Cipher (Windows)** - 1685 window-pane cipher
- **Greek Letters** - Greek letter replacements
- **Hieroglyphics** - Egyptian hieroglyph-style mapping
- **Hiragana** - Rough Romaji → Hiragana
- **Klingon** - Klingon transliteration
- **Katakana** - Rough Romaji → Katakana
- **Malachim** - Malachim angelic alphabet
- **Mary Stuart Cipher** - Mary Queen of Scots cipher
- **Mayan Numerals** - A1Z26 as Mayan numeral glyphs
- **Moon Alphabet** - Moon type for blind readers
- **Ogham (Celtic)** - Celtic Ogham tree alphabet
- **Passing the River** - Golden Dawn Passing the River script
- **Periodic Table Cipher** - Letters AZ as element symbols
- **Pigpen Cipher** - Masonic / pigpen grid symbols
- **Quenya (Tolkien Elvish)** - Tolkien Quenya mapping
- **Roman Numerals** - Arabic numerals ↔ Roman numerals
- **Rosicrucian** - Rosicrucian cipher alphabet
- **Seven-Segment Display** - Digits as 7-segment ASCII art
- **Standard Galactic (Minecraft)** - Enchanting Table / SGA alphabet
- **Templars Cipher** - Templar pigpen variant
- **Tengwar Script** - Elvish Tengwar script
- **Theban Alphabet** - Witches' Theban script
- **Wingdings** - Wingdings-style symbol mapping
- **Younger Futhark** - Medieval Younger Futhark runes
#### **Unicode**
- **Bold Italic** - Mathematical sans-serif bold italic
- **Bold** - Mathematical bold
- **Bubble** - Circled / “bubble” letters
- **Chemical Symbols** - Chemical element symbols
- **Circled** - Circled Unicode letters
- **Bubble** - Circled letters (upper and lower case) and circled digits 09
- **Circled** - Circled uppercase letters and digits (no lowercase)
- **Cursive** - Mathematical script / cursive
- **Cyrillic Stylized** - Latin → Cyrillic lookalike letters
- **Dashed Underline** - Combining dashed underline
@@ -164,10 +223,7 @@ Categories match the Transform tab and the folders under `src/transformers/` (ea
- **Double-Struck** - Mathematical double-struck
- **Fraktur** - Mathematical Fraktur / Gothic
- **Full Width** - Fullwidth Latin (and related) forms
- **Greek Letters** - Greek letter replacements
- **Hiragana** - Rough Romaji → Hiragana
- **Italic** - Mathematical italic
- **Katakana** - Rough Romaji → Katakana
- **Mathematical Notation** - Mathematical alphanumeric symbols
- **Medieval** - Medieval Unicode letterforms
- **Mirror Text** - Leftright mirrored characters
@@ -186,12 +242,16 @@ Categories match the Transform tab and the folders under `src/transformers/` (ea
- **Vaporwave** - Fullwidth + aesthetic spacing
- **Wavy Underline** - Wavy underline combining marks
- **Wide Spacing** - Insert wide spaces between characters
- **Wingdings** - Wingdings-style symbol mapping
- **Zalgo** - Stacked combining marks (“glitch” text)
#### **Visual**
- **Disemvowel** - Remove vowels (speech game)
- **Emoji Speak** - Emoji-heavy “speak” transform
- **Javanais** - French “javanais” vowel-insertion game
- **Latin Gibberish** - Latin-flavored pseudo-text
- **Leetspeak** - 1337-style character substitutions
- **Louchebem** - French argot (loucherbem-style)
- **Pig Latin** - English Pig Latin
- **Rövarspråket** - Swedish consonant-doubling game
- **Ubbi Dubbi** - Insert “ub” before vowel sounds
@@ -201,7 +261,7 @@ Tabs appear in **UI order** below. **OpenRouter** (optional or required per tool
### 🔤 **Transform**
- **159 Transforms**: Encodings, ciphers, Unicode styles, formats, and more (full catalog above).
- **223 Transforms**: Encodings, ciphers, Unicode styles, formats, and more (full catalog above).
- **Categories**: Grouped sections you can **reorder**; quick-jump legend; **randomizer** last.
- **Favorites & last used**: Pin transforms and recall recent picks.
- **Per-transform options**: Gear icon where a transform exposes settings.
@@ -382,7 +442,7 @@ npm run preview # npm run build, then serve dist/
| [build/README.md](build/README.md) | What each `build:*` script does |
| [templates/README.md](templates/README.md) | Editing tool HTML templates |
**Keeping the transform list in this README in sync:** when you add or rename a transformer, add a one-line description to `DESCRIPTIONS` in `build/readme-transform-section.js`, run `node build/readme-transform-section.js`, and replace the **Text Transformations** section here (details in [src/transformers/README.md](src/transformers/README.md)).
**Keeping the transform list in this README in sync:** when you add, rename, or move a transformer, update the **Text Transformations** section below manually — one bullet per transform under the correct category heading (`case`, `cipher`, `concealment`, `encoding`, `format`, `signwriting`, `special`, `symbol`, `technical`, `unicode`, `visual`). Use the transforms UI `name` and a short description. Category comes from the folder under `src/transformers/` (see [src/transformers/README.md](src/transformers/README.md)).
## 🛠️ **Technical Details**
@@ -391,7 +451,7 @@ npm run preview # npm run build, then serve dist/
- **Tool System**: Modular tool registry with build-time template injection
- **Encoding**: UTF-8 with proper Unicode handling
- **Steganography**: Variation selectors and Tags Unicode block
- **Transforms**: Individual transformer modules live under `src/transformers/` (159; the bundle is generated by `npm run build:transforms`)
- **Transforms**: Individual transformer modules live under `src/transformers/` (223; the bundle is generated by `npm run build:transforms`)
- **Build Process**:
- `npm run build` writes the runnable app under `dist/` (ignored by git in most setups)
- Transformers are bundled from `src/transformers/` to `dist/js/bundles/transforms-bundle.js`
@@ -421,7 +481,7 @@ npm run preview # npm run build, then serve dist/
- 🆕 **AI Translation**: Translate to 20+ languages (including dead/exotic) via OpenRouter using TranslateGemma prompt format
- 🆕 **PromptCraft Tool**: AI-powered prompt mutation with 9 strategies and 48+ models
- 🆕 **OpenRouter Integration**: Unified API key management for all AI-powered features
- 🆕 **159 Transformations**: Full catalog of encodings, ciphers, Unicode styles, fantasy and ancient scripts, and technical codes (see README transform list)
- 🆕 **223 Transformations**: Full catalog of encodings, ciphers, Unicode styles, symbol alphabets, SignWriting, and technical codes (see README transform list)
- 🆕 **More Encodings/Ciphers**: Base58, Base62, Vigenère, Rail Fence, Roman Numerals
- 🆕 **Category Organization**: Better organized transform categories
- 🆕 **Enhanced Styling**: New color schemes for each category
@@ -432,7 +492,7 @@ npm run preview # npm run build, then serve dist/
### **Creative Writing**
- Create unique text styles for stories
- Encode secret messages in plain sight
- Generate fantasy language text
- Generate symbolic or script-style text
### **Education**
- Learn about different writing systems
@@ -477,6 +537,7 @@ This project is open source. See LICENSE file for details.
- **Star Wars** creators for Aurebesh
- **Bethesda** for Dovahzul language
- **Unicode Consortium** for character standards
- **[RaidedCluster](https://github.com/RaidedCluster)** — SignWriting transforms
---
+24 -7
View File
@@ -55,8 +55,24 @@ Injects tool templates from `templates/` into `dist/index.html`
npm run build:templates
```
### `build-alphabet-transforms.js`
Regenerates hand-maintained symbol alphabet files from `data/alphabets/*.json` into `src/transformers/symbol/`.
```bash
npm run build:alphabets
```
Runs automatically before `build-transforms.js` (`npm run build:transforms`).
### `build-code-vendor.js`
Bundles QR and barcode libraries into `js/vendor/` for the Codes tool (no CDN).
```bash
npm run build:codes-vendor
```
### `build-index.js`
Generates transformer index
Generates `src/transformers/index.js` (Node/test import index).
```bash
npm run build:index
@@ -66,12 +82,13 @@ npm run build:index
```bash
npm run build # Runs all scripts in order:
# 1. build:copy - Copy static files to dist/
# 2. build:index - Generate transformer index
# 3. build:transforms - Bundle transformers to dist/js/bundles/
# 4. build:emoji - Generate emoji data to dist/js/data/
# 5. build:tools - Inject tool scripts
# 6. build:templates - Inject templates to dist/index.html
# 1. build:tools - Inject tool scripts into index.template.html + toolRegistry
# 2. build:codes-vendor - Bundle qrcode, JsBarcode, ZXing → js/vendor/
# 3. build:copy - Copy static files to dist/
# 4. build:index - Generate src/transformers/index.js
# 5. build:transforms - build:alphabets + bundle transformers → dist/js/bundles/
# 6. build:emoji - Generate emoji data to dist/js/data/
# 7. build:templates - Inject templates → dist/index.html
```
## Output Structure
+178
View File
@@ -0,0 +1,178 @@
#!/usr/bin/env node
/**
* Generate symbol-alphabet transforms from data/alphabets/*.json
* Output: src/transformers/symbol/<slug>.js (regenerated each build)
*/
const fs = require('fs');
const path = require('path');
const root = path.join(__dirname, '..');
const dataDir = path.join(root, 'data', 'alphabets');
const outDir = path.join(root, 'src', 'transformers', 'symbol');
const GENERATED_HEADER = '// @generated from data/alphabets — do not edit by hand\n';
function slugToExportName(slug) {
return slug.replace(/-/g, '_');
}
function escapeJsString(s) {
return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
}
function escapeRegexCharClass(ch) {
return ch.replace(/\\/g, '\\\\').replace(/\]/g, '\\]');
}
function buildDetectorBlock(entry) {
if (entry.detectorHint) {
return `,
detector: function(text) {
return new RegExp('${escapeJsString(entry.detectorHint)}', 'u').test(text);
}`;
}
const chars = [...new Set(Object.values(entry.map))];
if (chars.length === 0) return '';
const classContent = chars.map(ch => escapeRegexCharClass(ch)).join('');
return `,
detector: function(text) {
return /[${classContent}]/u.test(text);
}`;
}
function buildMapObject(map) {
const lines = [];
for (const [key, value] of Object.entries(map)) {
lines.push(` '${escapeJsString(key)}': '${escapeJsString(value)}'`);
}
return lines.join(',\n');
}
function generateTransformFile(entry, slug) {
const name = entry.name || slug;
const priority = entry.priority != null ? entry.priority : 100;
const description = entry.description ? `\n description: '${escapeJsString(entry.description)}',` : '';
const mapBlock = buildMapObject(entry.map);
return `${GENERATED_HEADER}import BaseTransformer from '../BaseTransformer.js';
export default new BaseTransformer({
name: '${escapeJsString(name)}',
priority: ${priority},
category: 'symbol',${description}
map: {
${mapBlock}
},
func: function(text) {
return [...text].map(c => this.map[c] || this.map[c.toUpperCase()] || c).join('');
},
preview: function(text) {
if (!text) return '[${escapeJsString(slug)}]';
return this.func(text.slice(0, 6)) + (text.length > 6 ? '…' : '');
}${buildDetectorBlock(entry)}
});
`;
}
function validateEntry(entry, file) {
if (!entry.name || !entry.map || typeof entry.map !== 'object') {
throw new Error(`${file}: requires "name" and "map" object`);
}
const keys = Object.keys(entry.map);
if (keys.length < 26) {
console.warn(`⚠️ ${file}: map has only ${keys.length} entries`);
}
}
function loadUnicodeMaps() {
const unicodeDir = path.join(root, 'src', 'transformers', 'unicode');
const maps = [];
if (!fs.existsSync(unicodeDir)) return maps;
for (const file of fs.readdirSync(unicodeDir)) {
if (!file.endsWith('.js')) continue;
const content = fs.readFileSync(path.join(unicodeDir, file), 'utf8');
const nameMatch = content.match(/name:\s*'([^']+)'/);
const map = {};
for (const match of content.matchAll(/'([^']+)':\s*'([^']+)'/g)) {
map[match[1]] = match[2];
}
if (Object.keys(map).length >= 20) {
maps.push({ name: nameMatch ? nameMatch[1] : file, map });
}
}
return maps;
}
function overlapScore(entryMap, existingMap) {
let matched = 0;
let total = 0;
for (const [key, value] of Object.entries(entryMap)) {
total++;
const variants = [key, key.toLowerCase(), key.toUpperCase()];
for (const variant of variants) {
if (existingMap[variant] === value) {
matched++;
break;
}
}
}
return { matched, total };
}
function checkOverlap(entry, file, unicodeMaps) {
for (const existing of unicodeMaps) {
const { matched, total } = overlapScore(entry.map, existing.map);
if (total >= 20 && matched / total >= 0.9) {
throw new Error(
`${file}: ${matched}/${total} glyphs match existing Unicode transform "${existing.name}" — use that instead or pick distinct symbols`
);
}
if (total >= 20 && matched / total >= 0.75) {
console.warn(`⚠️ ${file}: ${matched}/${total} glyphs overlap "${existing.name}"`);
}
}
}
function main() {
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
console.log('Created data/alphabets/ (add JSON files to generate symbol transforms)');
return;
}
fs.mkdirSync(outDir, { recursive: true });
// Remove previously generated symbol transforms
for (const file of fs.readdirSync(outDir)) {
if (!file.endsWith('.js')) continue;
const full = path.join(outDir, file);
const head = fs.readFileSync(full, 'utf8').slice(0, 80);
if (head.includes('@generated')) {
fs.unlinkSync(full);
}
}
const jsonFiles = fs.readdirSync(dataDir).filter(f => f.endsWith('.json')).sort();
if (!jsonFiles.length) {
console.log('No alphabet JSON files in data/alphabets/');
return;
}
let count = 0;
const unicodeMaps = loadUnicodeMaps();
for (const file of jsonFiles) {
const slug = file.replace(/\.json$/, '');
const raw = fs.readFileSync(path.join(dataDir, file), 'utf8');
const entry = JSON.parse(raw);
validateEntry(entry, file);
checkOverlap(entry, file, unicodeMaps);
const outPath = path.join(outDir, `${slug}.js`);
fs.writeFileSync(outPath, generateTransformFile(entry, slug), 'utf8');
console.log(`✅ Alphabet: ${slug} → symbol/${slug}.js`);
count++;
}
console.log(`\n✨ Generated ${count} symbol alphabet transform(s)`);
}
main();
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env node
/**
* Bundle QR/barcode vendor libraries for the Codes tool (local copies, no CDN).
*/
const fs = require('fs');
const path = require('path');
const esbuild = require('esbuild');
const projectRoot = path.join(__dirname, '..');
const vendorDir = path.join(projectRoot, 'js/vendor');
if (!fs.existsSync(vendorDir)) {
fs.mkdirSync(vendorDir, { recursive: true });
}
const bundles = [
{
entry: path.join(projectRoot, 'node_modules/qrcode/lib/browser.js'),
outfile: path.join(vendorDir, 'qrcode.min.js'),
globalName: 'QRCode'
},
{
entry: path.join(projectRoot, 'node_modules/jsbarcode/bin/JsBarcode.js'),
outfile: path.join(vendorDir, 'JsBarcode.min.js'),
globalName: 'JsBarcode'
}
];
console.log('📦 Building Codes tool vendor bundles...\n');
bundles.forEach(function(spec) {
esbuild.buildSync({
entryPoints: [spec.entry],
bundle: true,
format: 'iife',
globalName: spec.globalName,
outfile: spec.outfile,
minify: true,
platform: 'browser'
});
const sizeKb = (fs.statSync(spec.outfile).size / 1024).toFixed(1);
console.log('✅ ' + path.basename(spec.outfile) + ' (' + sizeKb + 'KB)');
});
const zxingSrc = path.join(projectRoot, 'node_modules/@zxing/library/umd/index.min.js');
const zxingDest = path.join(vendorDir, 'zxing.min.js');
fs.copyFileSync(zxingSrc, zxingDest);
console.log('✅ zxing.min.js (' + (fs.statSync(zxingDest).size / 1024).toFixed(1) + 'KB)');
console.log('\n✨ Codes vendor bundles ready\n');
+8 -1
View File
@@ -101,8 +101,15 @@ for (const [name, filePath] of Object.entries(transforms)) {
.replace(/export default\s*/g, '') // Remove export statement
.trim();
if (/^(const|let|var|function)\s/m.test(cleanContent)) {
console.error(`${name}: top-level declarations break the bundle — wrap in export default (function(){ ... return new BaseTransformer(...); })();`);
process.exit(1);
}
output += `// ${name} (from ${filePath})\n`;
if (/^new BaseTransformer\s*\(/.test(cleanContent)) {
if (/^\(function\s*\(\)/.test(cleanContent)) {
output += `transforms['${name}'] = ${cleanContent}\n\n`;
} else if (/^new BaseTransformer\s*\(/.test(cleanContent)) {
output += `transforms['${name}'] = ${cleanContent}\n\n`;
} else {
output += `transforms['${name}'] = ${cleanContent}\n\n`;
+3 -1
View File
@@ -22,7 +22,8 @@ const templateFiles = [
'bijection.html',
'splitter.html',
'gibberish.html',
'spellingalphabet.html'
'spellingalphabet.html',
'codes.html'
];
@@ -53,6 +54,7 @@ if (!fs.existsSync(templatePath)) {
}
let indexContent = fs.readFileSync(templatePath, 'utf8');
indexContent = indexContent.replace(/\r\n/g, '\n');
// Find the tool-content-container
const startMarker = '<div id="tool-content-container">';
+368 -157
View File
@@ -1,3 +1,6 @@
/* SignWriting font (Google Fonts) — ISWA 2010 glyphs (U+1D800U+1DAFF) */
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SignWriting&display=swap');
.danger-modal-backdrop { display: none; }
.danger-modal-backdrop.danger-active { display: none; }
.danger-modal { display: none; }
@@ -27,44 +30,51 @@
--sidebar-width: 420px;
/* Transform category colors */
--encoding-color: #7e57c2; /* Purple for encoding/decoding */
/* Transform category colors — one distinct hue per category */
--case-color: #7986cb; /* Periwinkle — case transforms */
--case-color-rgb: 121, 134, 203;
--cipher-color: #00897b; /* Teal — classical ciphers */
--cipher-color-rgb: 0, 137, 123;
--concealment-color: #7cb342; /* Olive — steganography / hidden text */
--concealment-color-rgb: 124, 179, 66;
--encoding-color: #7e57c2; /* Purple — byte & data encodings */
--encoding-color-rgb: 126, 87, 194;
--cipher-color: #26a69a; /* Teal for ciphers */
--cipher-color-rgb: 38, 166, 154;
--visual-color: #ef5350; /* Red for visual transformations */
--visual-color-rgb: 239, 83, 80;
--format-color: #ffb74d; /* Orange for formatting */
--format-color-rgb: 255, 183, 77;
--unicode-color: #42a5f5; /* Blue for unicode transformations */
--format-color: #fb8c00; /* Orange — text formatting */
--format-color-rgb: 251, 140, 0;
--signwriting-color: #8d6e63; /* Brown — SignWriting notation */
--signwriting-color-rgb: 141, 110, 99;
--special-color: #43a047; /* Green — misc / randomizer-adjacent */
--special-color-rgb: 67, 160, 71;
--symbol-color: #d4af37; /* Gold — scripts & symbol alphabets */
--symbol-color-rgb: 212, 175, 55;
--technical-color: #00acc1; /* Cyan — Morse, NATO, phone codes */
--technical-color-rgb: 0, 172, 193;
--unicode-color: #42a5f5; /* Blue — Unicode letter styles */
--unicode-color-rgb: 66, 165, 245;
--special-color: #66bb6a; /* Green for special transformations */
--special-color-rgb: 102, 187, 106;
--fantasy-color: #ff6b9d; /* Pink for fantasy languages */
--fantasy-color-rgb: 255, 107, 157;
--ancient-color: #d4af37; /* Gold for ancient scripts */
--ancient-color-rgb: 212, 175, 55;
--technical-color: #00bcd4; /* Cyan for technical codes */
--technical-color-rgb: 0, 188, 212;
--randomizer-color: #9c27b0; /* Purple for randomizer */
--randomizer-color-rgb: 156, 39, 176;
--case-color: #9575cd; /* Indigo for case transformations */
--case-color-rgb: 149, 117, 205;
--custom-spelling-color: #ff7043; /* Deep orange for user spelling alphabets */
--visual-color: #ef5350; /* Coral — visual / spoken effects */
--visual-color-rgb: 239, 83, 80;
--randomizer-color: #ec407a; /* Pink — random mix */
--randomizer-color-rgb: 236, 64, 122;
--custom-spelling-color: #ff7043; /* Deep orange — user spelling alphabets */
--custom-spelling-color-rgb: 255, 112, 67;
--translate-color: #b388ff; /* Violet — AI translation (inline tool) */
--translate-color-rgb: 179, 136, 255;
/* Active state gradient end colors */
--case-active-end: #9fa8da;
--cipher-active-end: #26a69a;
--concealment-active-end: #aed581;
--encoding-active-end: #9575cd;
--cipher-active-end: #4db6ac;
--visual-active-end: #e57373;
--format-active-end: #ffcc80;
--unicode-active-end: #64b5f6;
--special-active-end: #81c784;
--fantasy-active-end: #ff8aad;
--ancient-active-end: #e6c84c;
--format-active-end: #ffb74d;
--signwriting-active-end: #a1887f;
--special-active-end: #66bb6a;
--symbol-active-end: #e6c84c;
--technical-active-end: #26c6da;
--randomizer-active-end: #ab47bc;
--case-active-end: #b39ddb;
--unicode-active-end: #64b5f6;
--visual-active-end: #e57373;
--randomizer-active-end: #f06292;
--custom-spelling-active-end: #ff8a65;
--translate-active-end: #d1b3ff;
/* Spacing scale */
--spacing-xs: 4px;
@@ -381,13 +391,18 @@ body {
min-height: 46px;
}
/* Transform picker dropdown (Splitter, Decoder, etc.) */
select.transform-select {
width: auto;
max-width: min(100%, 320px);
min-width: min(200px, 100%);
}
/* Decoder: method select beside label (override full-width select defaults) */
select.decoder-method-select {
margin-left: 15px;
padding: 5px 10px;
border-radius: 6px;
width: auto;
max-width: min(100%, 320px);
}
/* Tokenade text payload block label */
@@ -1468,48 +1483,52 @@ body.dark-theme .mobile-tool-dropdown {
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.legend-item.transform-category-encoding {
border-left-color: var(--encoding-color);
.legend-item.transform-category-case {
border-left-color: var(--case-color);
}
.legend-item.transform-category-cipher {
border-left-color: var(--cipher-color);
}
.legend-item.transform-category-visual {
border-left-color: var(--visual-color);
.legend-item.transform-category-concealment {
border-left-color: var(--concealment-color);
}
.legend-item.transform-category-encoding {
border-left-color: var(--encoding-color);
}
.legend-item.transform-category-format {
border-left-color: var(--format-color);
}
.legend-item.transform-category-unicode {
border-left-color: var(--unicode-color);
.legend-item.transform-category-signwriting {
border-left-color: var(--signwriting-color);
}
.legend-item.transform-category-special {
border-left-color: var(--special-color);
}
.legend-item.transform-category-fantasy {
border-left-color: var(--fantasy-color);
}
.legend-item.transform-category-ancient {
border-left-color: var(--ancient-color);
.legend-item.transform-category-symbol {
border-left-color: var(--symbol-color);
}
.legend-item.transform-category-technical {
border-left-color: var(--technical-color);
}
.legend-item.transform-category-randomizer {
border-left-color: var(--randomizer-color);
.legend-item.transform-category-unicode {
border-left-color: var(--unicode-color);
}
.legend-item.transform-category-case {
border-left-color: var(--case-color);
.legend-item.transform-category-visual {
border-left-color: var(--visual-color);
}
.legend-item.transform-category-randomizer {
border-left-color: var(--randomizer-color);
}
.legend-item.transform-category-custom_spelling {
@@ -1638,48 +1657,52 @@ body.dark-theme .mobile-tool-dropdown {
position: relative;
}
.category-title.transform-category-encoding {
border-left-color: var(--encoding-color);
.category-title.transform-category-case {
border-left-color: var(--case-color);
}
.category-title.transform-category-cipher {
border-left-color: var(--cipher-color);
}
.category-title.transform-category-visual {
border-left-color: var(--visual-color);
.category-title.transform-category-concealment {
border-left-color: var(--concealment-color);
}
.category-title.transform-category-encoding {
border-left-color: var(--encoding-color);
}
.category-title.transform-category-format {
border-left-color: var(--format-color);
}
.category-title.transform-category-unicode {
border-left-color: var(--unicode-color);
.category-title.transform-category-signwriting {
border-left-color: var(--signwriting-color);
}
.category-title.transform-category-special {
border-left-color: var(--special-color);
}
.category-title.transform-category-fantasy {
border-left-color: var(--fantasy-color);
}
.category-title.transform-category-ancient {
border-left-color: var(--ancient-color);
.category-title.transform-category-symbol {
border-left-color: var(--symbol-color);
}
.category-title.transform-category-technical {
border-left-color: var(--technical-color);
}
.category-title.transform-category-randomizer {
border-left-color: var(--randomizer-color);
.category-title.transform-category-unicode {
border-left-color: var(--unicode-color);
}
.category-title.transform-category-case {
border-left-color: var(--case-color);
.category-title.transform-category-visual {
border-left-color: var(--visual-color);
}
.category-title.transform-category-randomizer {
border-left-color: var(--randomizer-color);
}
.category-title.transform-category-custom_spelling {
@@ -1687,7 +1710,7 @@ body.dark-theme .mobile-tool-dropdown {
}
.category-title.transform-category-translate {
border-left-color: var(--ancient-color);
border-left-color: var(--translate-color);
text-transform: none;
margin-bottom: 10px;
padding-bottom: 6px;
@@ -1785,13 +1808,13 @@ button.translate-custom-label-btn {
button.translate-custom-label-btn:hover {
opacity: 1;
background: rgba(var(--ancient-color-rgb), 0.08);
background: rgba(var(--translate-color-rgb), 0.08);
border-color: var(--input-border);
}
button.translate-custom-label-btn:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(var(--ancient-color-rgb), 0.35);
box-shadow: 0 0 0 2px rgba(var(--translate-color-rgb), 0.35);
}
.translate-custom-toggle-end {
@@ -1809,8 +1832,8 @@ button.translate-custom-label-btn:focus {
button.translate-custom-label-btn:hover .translate-custom-toggle-end {
background: var(--button-hover-bg);
border-color: var(--ancient-color);
color: var(--ancient-color);
border-color: var(--translate-color);
color: var(--translate-color);
}
.translate-lang-grid {
@@ -1838,12 +1861,12 @@ button.translate-custom-label-btn:hover .translate-custom-toggle-end {
cursor: pointer;
transition: all 0.2s ease;
box-sizing: border-box;
background: linear-gradient(to right, rgba(var(--ancient-color-rgb), 0.06), var(--button-bg));
background: linear-gradient(to right, rgba(var(--translate-color-rgb), 0.06), var(--button-bg));
}
.translate-lang-btn:hover:not(:disabled) {
background: linear-gradient(to right, rgba(var(--ancient-color-rgb), 0.14), var(--button-hover-bg));
box-shadow: 0 2px 8px rgba(var(--ancient-color-rgb), 0.18);
background: linear-gradient(to right, rgba(var(--translate-color-rgb), 0.14), var(--button-hover-bg));
box-shadow: 0 2px 8px rgba(var(--translate-color-rgb), 0.18);
}
.translate-lang-btn:disabled {
@@ -1852,7 +1875,7 @@ button.translate-custom-label-btn:hover .translate-custom-toggle-end {
}
.translate-lang-exotic {
background: linear-gradient(to right, rgba(var(--ancient-color-rgb), 0.09), var(--button-bg));
background: linear-gradient(to right, rgba(var(--translate-color-rgb), 0.09), var(--button-bg));
}
.translate-lang-custom {
@@ -1867,7 +1890,7 @@ button.translate-custom-label-btn:hover .translate-custom-toggle-end {
}
.translate-custom-flag {
color: var(--ancient-color);
color: var(--translate-color);
font-size: 1.1rem;
}
@@ -1928,7 +1951,7 @@ button.translate-custom-label-btn:hover .translate-custom-toggle-end {
.translate-add-btn:hover:not(:disabled) {
background: var(--button-hover-bg);
border-color: var(--ancient-color);
border-color: var(--translate-color);
}
.translate-empty-custom {
@@ -2571,55 +2594,72 @@ button.endsequence-item:hover .endsequence-copy-affordance {
transition: all 0.2s ease;
}
.transform-category-encoding .transform-preview {
color: rgba(var(--encoding-color-rgb), 0.9);
.transform-category-case .transform-preview {
color: rgba(var(--case-color-rgb), 0.9);
}
.transform-category-cipher .transform-preview {
color: rgba(var(--cipher-color-rgb), 0.9);
}
.transform-category-visual .transform-preview {
color: rgba(var(--visual-color-rgb), 0.9);
.transform-category-concealment .transform-preview {
color: rgba(var(--concealment-color-rgb), 0.9);
}
.transform-category-encoding .transform-preview {
color: rgba(var(--encoding-color-rgb), 0.9);
}
.transform-category-format .transform-preview {
color: rgba(var(--format-color-rgb), 0.9);
}
.transform-category-unicode .transform-preview {
color: rgba(var(--unicode-color-rgb), 0.9);
.transform-category-signwriting .transform-preview {
color: rgba(var(--signwriting-color-rgb), 0.9);
font-family: 'Noto Sans SignWriting', 'Fira Code', monospace;
line-height: 2;
letter-spacing: 0.05em;
}
.transform-category-special .transform-preview {
color: rgba(var(--special-color-rgb), 0.9);
}
.transform-category-fantasy .transform-preview {
color: rgba(var(--fantasy-color-rgb), 0.9);
}
.transform-category-ancient .transform-preview {
color: rgba(var(--ancient-color-rgb), 0.9);
.transform-category-symbol .transform-preview {
color: rgba(var(--symbol-color-rgb), 0.9);
}
.transform-category-technical .transform-preview {
color: rgba(var(--technical-color-rgb), 0.9);
}
.transform-category-randomizer .transform-preview {
color: #e1baff;
text-shadow: 0 0 8px rgba(225,186,255,.35);
.transform-category-unicode .transform-preview {
color: rgba(var(--unicode-color-rgb), 0.9);
}
.transform-category-case .transform-preview {
color: rgba(var(--case-color-rgb), 0.9);
.transform-category-visual .transform-preview {
color: rgba(var(--visual-color-rgb), 0.9);
}
.transform-category-randomizer .transform-preview {
color: rgba(var(--randomizer-color-rgb), 0.95);
text-shadow: 0 0 8px rgba(var(--randomizer-color-rgb), 0.35);
}
.transform-category-custom_spelling .transform-preview {
color: rgba(var(--custom-spelling-color-rgb), 0.9);
}
.transform-category-translate .transform-preview {
color: rgba(var(--translate-color-rgb), 0.9);
}
.output-container.signwriting-output textarea {
font-family: 'Noto Sans SignWriting', 'Fira Code', 'Courier New', monospace;
line-height: 2;
letter-spacing: 0.1em;
}
.transform-button:hover .transform-preview {
background-color: rgba(0, 0, 0, 0.18);
opacity: 1;
@@ -2656,13 +2696,13 @@ button.endsequence-item:hover .endsequence-copy-affordance {
transition: all 0.2s ease;
}
.transform-category-encoding {
background: linear-gradient(to right, rgba(var(--encoding-color-rgb), 0.05), var(--button-bg));
.transform-category-case {
background: linear-gradient(to right, rgba(var(--case-color-rgb), 0.05), var(--button-bg));
}
.transform-category-encoding:hover {
background: linear-gradient(to right, rgba(var(--encoding-color-rgb), 0.15), var(--button-hover-bg));
box-shadow: 0 3px 10px rgba(var(--encoding-color-rgb), 0.2);
.transform-category-case:hover {
background: linear-gradient(to right, rgba(var(--case-color-rgb), 0.15), var(--button-hover-bg));
box-shadow: 0 3px 10px rgba(var(--case-color-rgb), 0.2);
}
.transform-category-cipher {
@@ -2674,13 +2714,22 @@ button.endsequence-item:hover .endsequence-copy-affordance {
box-shadow: 0 3px 10px rgba(var(--cipher-color-rgb), 0.2);
}
.transform-category-visual {
background: linear-gradient(to right, rgba(var(--visual-color-rgb), 0.05), var(--button-bg));
.transform-category-concealment {
background: linear-gradient(to right, rgba(var(--concealment-color-rgb), 0.05), var(--button-bg));
}
.transform-category-visual:hover {
background: linear-gradient(to right, rgba(var(--visual-color-rgb), 0.15), var(--button-hover-bg));
box-shadow: 0 3px 10px rgba(var(--visual-color-rgb), 0.2);
.transform-category-concealment:hover {
background: linear-gradient(to right, rgba(var(--concealment-color-rgb), 0.15), var(--button-hover-bg));
box-shadow: 0 3px 10px rgba(var(--concealment-color-rgb), 0.2);
}
.transform-category-encoding {
background: linear-gradient(to right, rgba(var(--encoding-color-rgb), 0.05), var(--button-bg));
}
.transform-category-encoding:hover {
background: linear-gradient(to right, rgba(var(--encoding-color-rgb), 0.15), var(--button-hover-bg));
box-shadow: 0 3px 10px rgba(var(--encoding-color-rgb), 0.2);
}
.transform-category-format {
@@ -2692,13 +2741,13 @@ button.endsequence-item:hover .endsequence-copy-affordance {
box-shadow: 0 3px 10px rgba(var(--format-color-rgb), 0.2);
}
.transform-category-unicode {
background: linear-gradient(to right, rgba(var(--unicode-color-rgb), 0.05), var(--button-bg));
.transform-category-signwriting {
background: linear-gradient(to right, rgba(var(--signwriting-color-rgb), 0.05), var(--button-bg));
}
.transform-category-unicode:hover {
background: linear-gradient(to right, rgba(var(--unicode-color-rgb), 0.15), var(--button-hover-bg));
box-shadow: 0 3px 10px rgba(var(--unicode-color-rgb), 0.2);
.transform-category-signwriting:hover {
background: linear-gradient(to right, rgba(var(--signwriting-color-rgb), 0.15), var(--button-hover-bg));
box-shadow: 0 3px 10px rgba(var(--signwriting-color-rgb), 0.2);
}
.transform-category-special {
@@ -2710,22 +2759,13 @@ button.endsequence-item:hover .endsequence-copy-affordance {
box-shadow: 0 3px 10px rgba(var(--special-color-rgb), 0.2);
}
.transform-category-fantasy {
background: linear-gradient(to right, rgba(var(--fantasy-color-rgb), 0.05), var(--button-bg));
.transform-category-symbol {
background: linear-gradient(to right, rgba(var(--symbol-color-rgb), 0.05), var(--button-bg));
}
.transform-category-fantasy:hover {
background: linear-gradient(to right, rgba(var(--fantasy-color-rgb), 0.15), var(--button-hover-bg));
box-shadow: 0 3px 10px rgba(var(--fantasy-color-rgb), 0.2);
}
.transform-category-ancient {
background: linear-gradient(to right, rgba(var(--ancient-color-rgb), 0.05), var(--button-bg));
}
.transform-category-ancient:hover {
background: linear-gradient(to right, rgba(var(--ancient-color-rgb), 0.15), var(--button-hover-bg));
box-shadow: 0 3px 10px rgba(var(--ancient-color-rgb), 0.2);
.transform-category-symbol:hover {
background: linear-gradient(to right, rgba(var(--symbol-color-rgb), 0.15), var(--button-hover-bg));
box-shadow: 0 3px 10px rgba(var(--symbol-color-rgb), 0.2);
}
.transform-category-technical {
@@ -2737,6 +2777,24 @@ button.endsequence-item:hover .endsequence-copy-affordance {
box-shadow: 0 3px 10px rgba(var(--technical-color-rgb), 0.2);
}
.transform-category-unicode {
background: linear-gradient(to right, rgba(var(--unicode-color-rgb), 0.05), var(--button-bg));
}
.transform-category-unicode:hover {
background: linear-gradient(to right, rgba(var(--unicode-color-rgb), 0.15), var(--button-hover-bg));
box-shadow: 0 3px 10px rgba(var(--unicode-color-rgb), 0.2);
}
.transform-category-visual {
background: linear-gradient(to right, rgba(var(--visual-color-rgb), 0.05), var(--button-bg));
}
.transform-category-visual:hover {
background: linear-gradient(to right, rgba(var(--visual-color-rgb), 0.15), var(--button-hover-bg));
box-shadow: 0 3px 10px rgba(var(--visual-color-rgb), 0.2);
}
.transform-category-randomizer {
background: linear-gradient(to right, rgba(var(--randomizer-color-rgb), 0.05), var(--button-bg));
}
@@ -2746,15 +2804,6 @@ button.endsequence-item:hover .endsequence-copy-affordance {
box-shadow: 0 3px 10px rgba(var(--randomizer-color-rgb), 0.2);
}
.transform-category-case {
background: linear-gradient(to right, rgba(var(--case-color-rgb), 0.05), var(--button-bg));
}
.transform-category-case:hover {
background: linear-gradient(to right, rgba(var(--case-color-rgb), 0.15), var(--button-hover-bg));
box-shadow: 0 3px 10px rgba(var(--case-color-rgb), 0.2);
}
.transform-category-custom_spelling {
background: linear-gradient(to right, rgba(var(--custom-spelling-color-rgb), 0.05), var(--button-bg));
}
@@ -2764,6 +2813,15 @@ button.endsequence-item:hover .endsequence-copy-affordance {
box-shadow: 0 3px 10px rgba(var(--custom-spelling-color-rgb), 0.2);
}
.transform-category-translate {
background: linear-gradient(to right, rgba(var(--translate-color-rgb), 0.05), var(--button-bg));
}
.transform-category-translate:hover {
background: linear-gradient(to right, rgba(var(--translate-color-rgb), 0.15), var(--button-hover-bg));
box-shadow: 0 3px 10px rgba(var(--translate-color-rgb), 0.2);
}
.transform-button:before {
content: '';
position: absolute;
@@ -2803,9 +2861,9 @@ button.endsequence-item:hover .endsequence-copy-affordance {
}
/* Category-specific active states */
.transform-category-encoding.active {
background: linear-gradient(to right, var(--encoding-color), var(--encoding-active-end));
box-shadow: 0 3px 12px rgba(var(--encoding-color-rgb), 0.4);
.transform-category-case.active {
background: linear-gradient(to right, var(--case-color), var(--case-active-end));
box-shadow: 0 3px 12px rgba(var(--case-color-rgb), 0.4);
}
.transform-category-cipher.active {
@@ -2813,9 +2871,14 @@ button.endsequence-item:hover .endsequence-copy-affordance {
box-shadow: 0 3px 12px rgba(var(--cipher-color-rgb), 0.4);
}
.transform-category-visual.active {
background: linear-gradient(to right, var(--visual-color), var(--visual-active-end));
box-shadow: 0 3px 12px rgba(var(--visual-color-rgb), 0.4);
.transform-category-concealment.active {
background: linear-gradient(to right, var(--concealment-color), var(--concealment-active-end));
box-shadow: 0 3px 12px rgba(var(--concealment-color-rgb), 0.4);
}
.transform-category-encoding.active {
background: linear-gradient(to right, var(--encoding-color), var(--encoding-active-end));
box-shadow: 0 3px 12px rgba(var(--encoding-color-rgb), 0.4);
}
.transform-category-format.active {
@@ -2823,9 +2886,9 @@ button.endsequence-item:hover .endsequence-copy-affordance {
box-shadow: 0 3px 12px rgba(var(--format-color-rgb), 0.4);
}
.transform-category-unicode.active {
background: linear-gradient(to right, var(--unicode-color), var(--unicode-active-end));
box-shadow: 0 3px 12px rgba(var(--unicode-color-rgb), 0.4);
.transform-category-signwriting.active {
background: linear-gradient(to right, var(--signwriting-color), var(--signwriting-active-end));
box-shadow: 0 3px 12px rgba(var(--signwriting-color-rgb), 0.4);
}
.transform-category-special.active {
@@ -2833,14 +2896,9 @@ button.endsequence-item:hover .endsequence-copy-affordance {
box-shadow: 0 3px 12px rgba(var(--special-color-rgb), 0.4);
}
.transform-category-fantasy.active {
background: linear-gradient(to right, var(--fantasy-color), var(--fantasy-active-end));
box-shadow: 0 3px 12px rgba(var(--fantasy-color-rgb), 0.4);
}
.transform-category-ancient.active {
background: linear-gradient(to right, var(--ancient-color), var(--ancient-active-end));
box-shadow: 0 3px 12px rgba(var(--ancient-color-rgb), 0.4);
.transform-category-symbol.active {
background: linear-gradient(to right, var(--symbol-color), var(--symbol-active-end));
box-shadow: 0 3px 12px rgba(var(--symbol-color-rgb), 0.4);
}
.transform-category-technical.active {
@@ -2848,16 +2906,26 @@ button.endsequence-item:hover .endsequence-copy-affordance {
box-shadow: 0 3px 12px rgba(var(--technical-color-rgb), 0.4);
}
.transform-category-translate.active {
background: linear-gradient(to right, var(--translate-color), var(--translate-active-end));
box-shadow: 0 3px 12px rgba(var(--translate-color-rgb), 0.4);
}
.transform-category-unicode.active {
background: linear-gradient(to right, var(--unicode-color), var(--unicode-active-end));
box-shadow: 0 3px 12px rgba(var(--unicode-color-rgb), 0.4);
}
.transform-category-visual.active {
background: linear-gradient(to right, var(--visual-color), var(--visual-active-end));
box-shadow: 0 3px 12px rgba(var(--visual-color-rgb), 0.4);
}
.transform-category-randomizer.active {
background: linear-gradient(to right, var(--randomizer-color), var(--randomizer-active-end));
box-shadow: 0 3px 12px rgba(var(--randomizer-color-rgb), 0.4);
}
.transform-category-case.active {
background: linear-gradient(to right, var(--case-color), var(--case-active-end));
box-shadow: 0 3px 12px rgba(var(--case-color-rgb), 0.4);
}
.transform-category-custom_spelling.active {
background: linear-gradient(to right, var(--custom-spelling-color), var(--custom-spelling-active-end));
box-shadow: 0 3px 12px rgba(var(--custom-spelling-color-rgb), 0.4);
@@ -4768,9 +4836,9 @@ html {
position: relative;
}
/* Transform chain select - only unique override for larger screens */
.transform-chain-item select {
min-width: min(200px, 100%); /* Never exceed container width */
/* Transform chain select inherits .transform-select sizing */
.transform-chain-item .transform-select {
min-width: min(200px, 100%);
}
@@ -4952,7 +5020,7 @@ html {
text-align: left;
}
.transform-chain-item select {
.transform-chain-item .transform-select {
min-width: 100%; /* Full width on smaller screens */
}
@@ -5275,7 +5343,7 @@ html {
}
/* Transform chain items stack vertically */
.transform-chain-item select {
.transform-chain-item .transform-select {
min-width: 100%;
width: 100%;
}
@@ -5408,3 +5476,146 @@ html {
background: rgba(var(--custom-spelling-color-rgb), 0.08);
border: 1px solid rgba(var(--custom-spelling-color-rgb), 0.25);
}
/* Codes tool (QR + barcodes) */
.codes-layout .codes-mode-tabs {
display: flex;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-md);
}
.codes-mode-btn {
flex: 1;
max-width: 220px;
padding: 10px 16px;
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--bg-secondary);
color: var(--text-color);
cursor: pointer;
font-size: 0.95rem;
transition: border-color 0.15s, background 0.15s;
}
.codes-mode-btn:hover {
border-color: var(--accent-color);
}
.codes-mode-btn.active {
border-color: var(--accent-color);
background: rgba(var(--accent-color-rgb, 99, 102, 241), 0.12);
font-weight: 600;
}
.codes-options-grid {
margin-bottom: var(--spacing-md);
}
.codes-checkbox-label {
display: flex;
align-items: center;
gap: 8px;
align-self: end;
}
.codes-error {
margin: var(--spacing-sm) 0;
padding: var(--spacing-sm) var(--spacing-md);
border-radius: 8px;
background: rgba(220, 53, 69, 0.12);
border: 1px solid rgba(220, 53, 69, 0.35);
color: #dc3545;
}
.codes-output-card {
margin-top: var(--spacing-md);
padding: var(--spacing-md);
border: 1px solid var(--border-color);
border-radius: 12px;
background: var(--bg-secondary);
}
.codes-preview-wrap {
display: flex;
justify-content: center;
align-items: center;
padding: var(--spacing-md);
background: #fff;
border-radius: 8px;
margin-bottom: var(--spacing-md);
}
.codes-preview-image {
max-width: 100%;
height: auto;
}
.codes-upload-zone {
margin-bottom: var(--spacing-md);
}
.codes-upload-label {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--spacing-sm);
padding: var(--spacing-lg);
border: 2px dashed var(--border-color);
border-radius: 12px;
cursor: pointer;
text-align: center;
transition: border-color 0.15s, background 0.15s;
}
.codes-upload-label:hover {
border-color: var(--accent-color);
background: rgba(var(--accent-color-rgb, 99, 102, 241), 0.06);
}
.codes-upload-label input[type="file"] {
display: none;
}
.codes-upload-label i {
font-size: 2rem;
color: var(--accent-color);
}
.codes-hint {
margin-top: var(--spacing-sm);
color: var(--text-muted);
font-size: 0.9rem;
}
.codes-loading {
padding: var(--spacing-md);
color: var(--text-muted);
}
.codes-decode-preview {
margin: var(--spacing-md) 0;
text-align: center;
}
.codes-decode-preview img {
max-width: 100%;
max-height: 280px;
border-radius: 8px;
border: 1px solid var(--border-color);
}
.codes-decode-result {
margin-top: var(--spacing-md);
padding: var(--spacing-md);
border: 1px solid var(--border-color);
border-radius: 12px;
background: var(--bg-secondary);
}
.codes-decode-meta {
margin-bottom: var(--spacing-sm);
color: var(--text-muted);
font-size: 0.9rem;
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "Alchemical Symbols",
"priority": 100,
"description": "Classical alchemical symbol alphabet",
"detectorHint": "[🜂🜃🜄🜅🜆🜇🜈🜉]",
"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": "🜛"
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "Celestial Alphabet",
"priority": 100,
"description": "Agrippa's celestial / angelic symbol alphabet",
"detectorHint": "[☉☽☿♀♂♃♄♅♆♇☊☋]",
"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": "⛣"
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "Daedric Alphabet",
"priority": 100,
"description": "Elder Scrolls inspired Daedric-style symbols",
"detectorHint": "[ᚠᚡᚢᚣᚤᚥᚦᚧᚨᚩ]",
"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": "ᚹ"
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "Dancing Men Cipher",
"priority": 100,
"description": "Sherlock Holmes stick-figure cipher (Unicode approximations)",
"detectorHint": "[┣┫┳┻├┤┬┴╋╞╡╤╧]",
"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": "▾"
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "Enochian Alphabet",
"priority": 100,
"description": "Enochian angelic script (Unicode approximations)",
"detectorHint": "[ᛂᛃᛄᛅᛆᛇᛈᛉᛊᛋ]",
"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": "ᛛ"
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "Malachim Alphabet",
"priority": 100,
"description": "Malachim / angel script symbol substitution",
"detectorHint": "[✁✂✃✄✆✇✈✉✊✋✌✍✎✏]",
"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": "✛"
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "Mary Stuart Cipher",
"priority": 100,
"description": "Mary Queen of Scots nomenclator-style symbols",
"detectorHint": "[❶❷❸❹❺❻❼❽❾]",
"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": "➏"
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "Moon Alphabet",
"priority": 100,
"description": "Moon phase / lunar symbol alphabet",
"detectorHint": "[☾☽☊☋⚸⚹⚺⚻]",
"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": "⛰"
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "Passing the River Alphabet",
"priority": 100,
"description": "Golden Dawn Passing the River tarot script",
"detectorHint": "[♠♣♥♦♤♧♡♢]",
"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": "∙"
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "Rosicrucian Cipher",
"priority": 100,
"description": "Rosicrucian / Golden Dawn symbol alphabet",
"detectorHint": "[⛤⛥⛦⛧⛨⛩⛪⛫]",
"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": "⛽"
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "Templars Cipher",
"priority": 100,
"description": "Templar / pigpen variant with dot markers",
"detectorHint": "[◧◨◩◪◫◬◭◮◯]",
"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": "⬒"
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "Theban Alphabet",
"priority": 100,
"description": "Honoric / Theban witch alphabet (symbol substitution)",
"detectorHint": "[∀∁∂∃∄∅∆∇∈∉∊∋∌∍∎∏∐∑−∓∔∕∖∗∘∙√∛∜∝]",
"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": "∙"
}
}
+7
View File
@@ -430,11 +430,13 @@
<script src="js/core/transformOptions.js"></script>
<script src="js/core/decoder.js"></script>
<script src="js/core/lexemeAnalysis.js"></script>
<script src="js/core/transformSelect.js"></script>
<!-- Load Tool System -->
<script src="js/tools/Tool.js"></script>
<script src="js/tools/AntiClassifierTool.js"></script>
<script src="js/tools/BijectionTool.js"></script>
<script src="js/tools/CodesTool.js"></script>
<script src="js/tools/DecodeTool.js"></script>
<script src="js/tools/EmojiTool.js"></script>
<script src="js/tools/GibberishTool.js"></script>
@@ -447,6 +449,11 @@
<script src="js/tools/TransformTool.js"></script>
<script src="js/tools/TranslateTool.js"></script>
<script src="js/core/toolRegistry.js"></script>
<!-- Codes tool vendor libraries (bundled locally at build time) -->
<script src="js/vendor/qrcode.min.js"></script>
<script src="js/vendor/JsBarcode.min.js"></script>
<script src="js/vendor/zxing.min.js"></script>
<script src="js/app.js"></script>
+3
View File
@@ -181,6 +181,9 @@ if (typeof AntiClassifierTool !== 'undefined') {
if (typeof BijectionTool !== 'undefined') {
window.toolRegistry.register(new BijectionTool());
}
if (typeof CodesTool !== 'undefined') {
window.toolRegistry.register(new CodesTool());
}
if (typeof DecodeTool !== 'undefined') {
window.toolRegistry.register(new DecodeTool());
}
+123
View File
@@ -0,0 +1,123 @@
/**
* Shared transform picker dropdown (category optgroups + favorites).
* Used by Splitter, Universal Decoder, and any tool that picks a transform.
*/
(function(global) {
'use strict';
function transformSelectFilter(root, decodableOnly) {
const list = (root && root.transforms) ? root.transforms : [];
if (!decodableOnly) return list;
return list.filter(function(t) {
return t && typeof t.reverse === 'function' && t.name !== 'Random Mix';
});
}
function transformSelectFavorites(root, decodableOnly) {
const favorites = (root && root.favorites) ? root.favorites : [];
const pool = transformSelectFilter(root, decodableOnly);
return favorites
.filter(function(f) { return typeof f === 'string'; })
.map(function(name) { return pool.find(function(t) { return t.name === name; }); })
.filter(Boolean);
}
function transformSelectByCategory(root, category, decodableOnly) {
const pool = transformSelectFilter(root, decodableOnly);
const list = pool.filter(function(t) { return t.category === category; });
const favorites = (root && root.favorites) ? root.favorites : [];
if (!favorites.length) return list;
return list.filter(function(t) {
return !favorites.some(function(f) { return typeof f === 'string' && f === t.name; });
});
}
function transformSelectCategoryOrder(root) {
if (root && Array.isArray(root.categories) && root.categories.length) {
return root.categories;
}
const pool = transformSelectFilter(root, false);
const set = new Set();
pool.forEach(function(t) {
if (t.category) set.add(t.category);
});
const cats = Array.from(set).filter(function(c) { return c !== 'randomizer'; }).sort();
if (set.has('randomizer')) cats.push('randomizer');
return cats;
}
function transformSelectCategoryLabel(category) {
if (!category) return '';
return category.charAt(0).toUpperCase() + category.slice(1);
}
function registerTransformSelectComponent(Vue) {
Vue.component('transform-select', {
props: {
value: { type: String, default: '' },
decodableOnly: { type: Boolean, default: false },
includeAuto: { type: Boolean, default: false },
showEmpty: { type: Boolean, default: false },
emptyLabel: { type: String, default: 'None' },
selectClass: { type: [String, Object, Array], default: 'transform-select' }
},
computed: {
categoryOrder: function() {
return transformSelectCategoryOrder(this.$root);
},
favoriteTransforms: function() {
return transformSelectFavorites(this.$root, this.decodableOnly);
},
visibleCategories: function() {
var self = this;
return this.categoryOrder.filter(function(category) {
return self.transformsForCategory(category).length > 0;
});
}
},
methods: {
categoryDisplayName: function(category) {
return transformSelectCategoryLabel(category);
},
transformsForCategory: function(category) {
return transformSelectByCategory(this.$root, category, this.decodableOnly);
},
onChange: function(event) {
this.$emit('input', event.target.value);
this.$emit('change', event);
}
},
template: '\
<select \
:value="value" \
@change="onChange" \
:class="selectClass"> \
<option v-if="showEmpty" value="">{{ emptyLabel }}</option> \
<option v-if="includeAuto" value="auto">&#128269; Auto-detect</option> \
<optgroup v-if="favoriteTransforms.length > 0" label="&#11088; Favorites"> \
<option v-for="t in favoriteTransforms" :key="\'f-\' + t.name" :value="t.name">{{ t.name }}</option> \
</optgroup> \
<optgroup \
v-for="category in visibleCategories" \
:key="category" \
:label="categoryDisplayName(category)"> \
<option \
v-for="t in transformsForCategory(category)" \
:key="t.name" \
:value="t.name">{{ t.name }}</option> \
</optgroup> \
</select>'
});
}
global.transformSelectFilter = transformSelectFilter;
global.transformSelectFavorites = transformSelectFavorites;
global.transformSelectByCategory = transformSelectByCategory;
global.transformSelectCategoryOrder = transformSelectCategoryOrder;
global.transformSelectCategoryLabel = transformSelectCategoryLabel;
global.registerTransformSelectComponent = registerTransformSelectComponent;
if (typeof Vue !== 'undefined') {
registerTransformSelectComponent(Vue);
}
})(typeof window !== 'undefined' ? window : global);
+262
View File
@@ -0,0 +1,262 @@
/**
* Codes Tool — generate and decode QR codes and barcodes.
*/
class CodesTool extends Tool {
constructor() {
super({
id: 'codes',
name: 'Codes',
icon: 'fa-qrcode',
title: 'QR codes and barcodes',
order: 9
});
}
getVueData() {
return {
codesMode: 'generate',
codesFormat: 'qr',
codesInput: '',
codesQrSize: 256,
codesQrMargin: 2,
codesQrEcl: 'M',
codesBarcodeHeight: 80,
codesBarcodeWidth: 2,
codesBarcodeDisplayValue: true,
codesOutputUrl: '',
codesOutputSvg: '',
codesError: '',
codesDecodeResult: '',
codesDecodeFormat: '',
codesDecodeError: '',
codesDecodeLoading: false,
codesDecodePreview: ''
};
}
getVueMethods() {
return {
codesFormatLabel: function(format) {
var labels = {
qr: 'QR Code',
code128: 'Code 128',
ean13: 'EAN-13',
code39: 'Code 39'
};
return labels[format] || format;
},
codesValidateGenerateInput: function() {
var text = String(this.codesInput || '').trim();
if (!text) {
return 'Enter text or data to encode.';
}
if (this.codesFormat === 'ean13') {
var digits = text.replace(/\D/g, '');
if (digits.length !== 12 && digits.length !== 13) {
return 'EAN-13 requires 12 or 13 digits (check digit is calculated automatically for 12).';
}
}
if (this.codesFormat === 'code39' && !/^[0-9A-Z\s\-\.\$\/\+\%]+$/.test(text.toUpperCase())) {
return 'Code 39 supports AZ, 09, space, and - . $ / + %.';
}
return '';
},
codesClearOutput: function() {
this.codesOutputUrl = '';
this.codesOutputSvg = '';
this.codesError = '';
},
codesGenerate: function() {
var validationError = this.codesValidateGenerateInput();
if (validationError) {
this.codesError = validationError;
this.codesClearOutput();
return;
}
this.codesError = '';
this.codesOutputUrl = '';
this.codesOutputSvg = '';
var text = String(this.codesInput || '').trim();
if (this.codesFormat === 'ean13') {
text = text.replace(/\D/g, '');
}
if (this.codesFormat === 'code39') {
text = text.toUpperCase();
}
var self = this;
if (this.codesFormat === 'qr') {
if (typeof window.QRCode === 'undefined' || typeof window.QRCode.toDataURL !== 'function') {
this.codesError = 'QR library not loaded. Rebuild the app (npm run build).';
return;
}
window.QRCode.toDataURL(text, {
width: Math.max(128, Math.min(1024, Number(this.codesQrSize) || 256)),
margin: Math.max(0, Math.min(20, Number(this.codesQrMargin) || 2)),
errorCorrectionLevel: this.codesQrEcl || 'M'
}).then(function(url) {
self.codesOutputUrl = url;
if (typeof self.showNotification === 'function') {
self.showNotification('QR code generated', 'success', 'fas fa-qrcode');
}
}).catch(function(err) {
self.codesError = (err && err.message) || 'Failed to generate QR code.';
});
return;
}
if (typeof window.JsBarcode === 'undefined') {
this.codesError = 'Barcode library not loaded. Rebuild the app (npm run build).';
return;
}
try {
var formatMap = {
code128: 'CODE128',
ean13: 'EAN13',
code39: 'CODE39'
};
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
window.JsBarcode(svg, text, {
format: formatMap[this.codesFormat] || 'CODE128',
displayValue: !!this.codesBarcodeDisplayValue,
height: Math.max(40, Math.min(200, Number(this.codesBarcodeHeight) || 80)),
width: Math.max(1, Math.min(4, Number(this.codesBarcodeWidth) || 2)),
margin: 10
});
var svgMarkup = new XMLSerializer().serializeToString(svg);
this.codesOutputSvg = svgMarkup;
this.codesOutputUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgMarkup);
if (typeof this.showNotification === 'function') {
this.showNotification(this.codesFormatLabel(this.codesFormat) + ' generated', 'success', 'fas fa-barcode');
}
} catch (err) {
this.codesError = (err && err.message) || 'Failed to generate barcode.';
}
},
codesDownload: function() {
if (!this.codesOutputUrl) {
return;
}
var ext = this.codesFormat === 'qr' ? 'png' : 'svg';
var link = document.createElement('a');
link.href = this.codesOutputUrl;
link.download = 'code-' + this.codesFormat + '.' + ext;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
if (typeof this.showNotification === 'function') {
this.showNotification('Download started', 'success', 'fas fa-download');
}
},
codesCopyOutput: function() {
if (!this.codesInput.trim()) {
return;
}
if (typeof this.copyToClipboard === 'function') {
this.copyToClipboard(this.codesInput);
} else if (navigator.clipboard) {
navigator.clipboard.writeText(this.codesInput);
}
},
codesResetDecode: function() {
this.codesDecodeResult = '';
this.codesDecodeFormat = '';
this.codesDecodeError = '';
this.codesDecodePreview = '';
this.codesDecodeLoading = false;
},
codesHandleFileUpload: function(event) {
var file = event && event.target && event.target.files && event.target.files[0];
if (!file) {
return;
}
this.codesDecodeFromFile(file);
event.target.value = '';
},
codesDecodeFromFile: function(file) {
var self = this;
if (!file || !file.type.match(/^image\//)) {
this.codesDecodeError = 'Choose a PNG, JPEG, GIF, or WebP image.';
return;
}
if (typeof window.ZXing === 'undefined' || !window.ZXing.BrowserMultiFormatReader) {
this.codesDecodeError = 'Scanner library not loaded. Rebuild the app (npm run build).';
return;
}
this.codesDecodeLoading = true;
this.codesDecodeError = '';
this.codesDecodeResult = '';
this.codesDecodeFormat = '';
var reader = new FileReader();
reader.onload = function(loadEvent) {
self.codesDecodePreview = loadEvent.target.result;
self.codesScanImage(loadEvent.target.result);
};
reader.onerror = function() {
self.codesDecodeLoading = false;
self.codesDecodeError = 'Could not read that file.';
};
reader.readAsDataURL(file);
},
codesScanImage: function(dataUrl) {
var self = this;
var codeReader = new window.ZXing.BrowserMultiFormatReader();
codeReader.decodeFromImageUrl(dataUrl).then(function(result) {
self.codesDecodeResult = result.getText();
self.codesDecodeFormat = result.getBarcodeFormat
? String(result.getBarcodeFormat())
: 'unknown';
self.codesDecodeError = '';
if (typeof self.showNotification === 'function') {
self.showNotification('Code decoded', 'success', 'fas fa-search');
}
}).catch(function() {
self.codesDecodeResult = '';
self.codesDecodeFormat = '';
self.codesDecodeError = 'No QR code or barcode found in that image.';
}).finally(function() {
self.codesDecodeLoading = false;
codeReader.reset();
});
},
codesUseDecodedText: function() {
if (!this.codesDecodeResult) {
return;
}
this.codesMode = 'generate';
this.codesInput = this.codesDecodeResult;
if (typeof this.showNotification === 'function') {
this.showNotification('Copied decoded text to Generate tab', 'success', 'fas fa-arrow-right');
}
}
};
}
getVueWatchers() {
return {
codesMode: function() {
this.codesError = '';
},
codesFormat: function() {
this.codesClearOutput();
}
};
}
onActivate(vueInstance) {
vueInstance.codesError = '';
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = CodesTool;
} else {
window.CodesTool = CodesTool;
}
+3 -1
View File
@@ -135,7 +135,9 @@ class DecodeTool extends Tool {
var self = this;
return {
getAllTransformsWithReverse: function() {
return this.transforms.filter(t => t && typeof t.reverse === 'function');
return (typeof transformSelectFilter === 'function')
? transformSelectFilter(this, true)
: this.transforms.filter(t => t && typeof t.reverse === 'function');
},
runUniversalDecode: function() {
const input = this.decoderInput;
+1 -70
View File
@@ -13,9 +13,6 @@ class SplitterTool extends Tool {
}
getVueData() {
// Load category order (same as TransformTool)
const categoryOrder = this.getCategoryOrder();
return {
// Message Splitter Tab
splitterInput: '',
@@ -35,78 +32,12 @@ class SplitterTool extends Tool {
splitterTokenizer: 'cl100k', // tokenizer for token-based mode
splitterTokenCount: 3, // token count per chunk for token-based mode
splitterPreserveEmptyLines: false, // preserve empty lines for line/sentence modes
splitMessages: [],
categoryOrder: categoryOrder
splitMessages: []
};
}
getCategoryOrder() {
// Get all categories from transforms
if (!window.transforms) return [];
const categorySet = new Set();
Object.values(window.transforms).forEach(transform => {
if (transform.category) {
categorySet.add(transform.category);
}
});
const allCategories = Array.from(categorySet);
const savedOrder = this.loadCategoryOrder();
return this.mergeCategoryOrder(allCategories, savedOrder);
}
loadCategoryOrder() {
try {
const saved = localStorage.getItem('transformCategoryOrder');
if (saved) {
return JSON.parse(saved);
}
} catch (e) {
console.warn('Failed to load category order:', e);
}
return null;
}
mergeCategoryOrder(allCategories, savedOrder) {
// Always ensure randomizer is last
const categoriesWithoutRandomizer = allCategories.filter(c => c !== 'randomizer');
if (!savedOrder || savedOrder.length === 0) {
// Default: alphabetical, randomizer last
const sorted = categoriesWithoutRandomizer.sort((a, b) => a.localeCompare(b));
return [...sorted, 'randomizer'];
}
// Use saved order, but filter out categories that no longer exist and remove duplicates
const validSavedOrder = savedOrder
.filter(cat => allCategories.includes(cat))
.filter((cat, index, arr) => arr.indexOf(cat) === index); // Remove duplicates
// Find new categories not in saved order
const newCategories = categoriesWithoutRandomizer.filter(cat => !validSavedOrder.includes(cat));
// Build final order: saved order (filtered, deduplicated) + new categories (alphabetically) + randomizer
const finalOrder = [...validSavedOrder];
if (newCategories.length > 0) {
finalOrder.push(...newCategories.sort((a, b) => a.localeCompare(b)));
}
// Ensure randomizer is always last and remove any duplicates
const finalWithoutRandomizer = finalOrder.filter(c => c !== 'randomizer');
const uniqueFinal = finalWithoutRandomizer.filter((cat, index, arr) => arr.indexOf(cat) === index);
return [...uniqueFinal, 'randomizer'];
}
getVueMethods() {
return {
/**
* Get display name for category (capitalized)
*/
getCategoryDisplayName: function(category) {
return category.charAt(0).toUpperCase() + category.slice(1);
},
/**
* Set encapsulation start and end strings
* @param {string} start - The start string
+1
View File
File diff suppressed because one or more lines are too long
+8
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+878
View File
@@ -9,9 +9,455 @@
"version": "1.0.0",
"license": "MIT",
"devDependencies": {
"@zxing/library": "^0.23.0",
"esbuild": "^0.28.0",
"jsbarcode": "^3.12.3",
"qrcode": "^1.5.4",
"serve": "^14.2.6"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@zeit/schemas": {
"version": "2.36.0",
"resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz",
@@ -19,6 +465,30 @@
"dev": true,
"license": "MIT"
},
"node_modules/@zxing/library": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.23.0.tgz",
"integrity": "sha512-6fkkoFwP8CHxl6ugnPsj74PLJgX2iRv5zczGAyt5OBzQgxFhuhF0NCEc4t4OvSr8xAv2MRLlI0Iu9ZGDZQ2urA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"ts-custom-error": "^3.3.1"
},
"engines": {
"node": ">= 24.0.0"
},
"optionalDependencies": {
"@zxing/text-encoding": "~0.9.0"
}
},
"node_modules/@zxing/text-encoding": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
"integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==",
"dev": true,
"license": "(Unlicense OR Apache-2.0)",
"optional": true
},
"node_modules/ajv": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
@@ -302,6 +772,94 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/cliui/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/cliui/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/cliui/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -396,6 +954,16 @@
"ms": "2.0.0"
}
},
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
@@ -406,6 +974,13 @@
"node": ">=4.0.0"
}
},
"node_modules/dijkstrajs": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
"dev": true,
"license": "MIT"
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -420,6 +995,48 @@
"dev": true,
"license": "MIT"
},
"node_modules/esbuild": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.0",
"@esbuild/android-arm": "0.28.0",
"@esbuild/android-arm64": "0.28.0",
"@esbuild/android-x64": "0.28.0",
"@esbuild/darwin-arm64": "0.28.0",
"@esbuild/darwin-x64": "0.28.0",
"@esbuild/freebsd-arm64": "0.28.0",
"@esbuild/freebsd-x64": "0.28.0",
"@esbuild/linux-arm": "0.28.0",
"@esbuild/linux-arm64": "0.28.0",
"@esbuild/linux-ia32": "0.28.0",
"@esbuild/linux-loong64": "0.28.0",
"@esbuild/linux-mips64el": "0.28.0",
"@esbuild/linux-ppc64": "0.28.0",
"@esbuild/linux-riscv64": "0.28.0",
"@esbuild/linux-s390x": "0.28.0",
"@esbuild/linux-x64": "0.28.0",
"@esbuild/netbsd-arm64": "0.28.0",
"@esbuild/netbsd-x64": "0.28.0",
"@esbuild/openbsd-arm64": "0.28.0",
"@esbuild/openbsd-x64": "0.28.0",
"@esbuild/openharmony-arm64": "0.28.0",
"@esbuild/sunos-x64": "0.28.0",
"@esbuild/win32-arm64": "0.28.0",
"@esbuild/win32-ia32": "0.28.0",
"@esbuild/win32-x64": "0.28.0"
}
},
"node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -468,6 +1085,30 @@
],
"license": "BSD-3-Clause"
},
"node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@@ -580,6 +1221,13 @@
"dev": true,
"license": "ISC"
},
"node_modules/jsbarcode": {
"version": "3.12.3",
"resolved": "https://registry.npmjs.org/jsbarcode/-/jsbarcode-3.12.3.tgz",
"integrity": "sha512-CuHU9hC6dPsHF5oVFMo8NW76uQVjH4L22CsP4hW+dNnGywJHC/B0ThA1CTDVLnxKLrrpYdicBLnd2xsgTfRnvg==",
"dev": true,
"license": "MIT"
},
"node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@@ -587,6 +1235,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -716,6 +1377,55 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-is-inside": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
@@ -740,6 +1450,34 @@
"dev": true,
"license": "MIT"
},
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/qrcode": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"dijkstrajs": "^1.0.1",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
},
"bin": {
"qrcode": "bin/qrcode"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/range-parser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
@@ -790,6 +1528,16 @@
"node": ">=0.10.0"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@@ -800,6 +1548,13 @@
"node": ">=0.10.0"
}
},
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"dev": true,
"license": "ISC"
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -873,6 +1628,13 @@
"node": ">= 0.8"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"dev": true,
"license": "ISC"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -970,6 +1732,16 @@
"node": ">=8"
}
},
"node_modules/ts-custom-error": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz",
"integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/type-fest": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
@@ -1020,6 +1792,13 @@
"node": ">= 8"
}
},
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"dev": true,
"license": "ISC"
},
"node_modules/widest-line": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz",
@@ -1053,6 +1832,105 @@
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"dev": true,
"license": "ISC"
},
"node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/yargs-parser/node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/yargs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/yargs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
}
}
}
+8 -2
View File
@@ -3,13 +3,15 @@
"version": "1.0.0",
"description": "Universal Text Encoder/Decoder & Steganography Tool",
"scripts": {
"build:codes-vendor": "node build/build-code-vendor.js",
"build:copy": "node build/copy-static.js",
"build:index": "node build/build-index.js",
"build:tools": "node build/inject-tool-scripts.js",
"build:templates": "node build/inject-tool-templates.js",
"build:emoji": "node build/build-emoji-data.js",
"build:transforms": "node build/build-transforms.js",
"build": "npm run build:tools && npm run build:copy && npm run build:index && npm run build:transforms && npm run build:emoji && npm run build:templates",
"build:alphabets": "node build/build-alphabet-transforms.js",
"build:transforms": "npm run build:alphabets && node build/build-transforms.js",
"build": "npm run build:tools && npm run build:codes-vendor && npm run build:copy && npm run build:index && npm run build:transforms && npm run build:emoji && npm run build:templates",
"start": "serve dist -l 8080",
"preview": "npm run build && serve dist -l 8080",
"test": "node tests/test_universal.js",
@@ -33,6 +35,10 @@
"author": "",
"license": "MIT",
"devDependencies": {
"@zxing/library": "^0.23.0",
"esbuild": "^0.28.0",
"jsbarcode": "^3.12.3",
"qrcode": "^1.5.4",
"serve": "^14.2.6"
}
}
+18 -17
View File
@@ -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 transforms `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 transforms `name` and a one-line description.
## Testing
+208
View File
@@ -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 AZ, reversible)'
},
{
value: 'an-septet',
label: '♪ AG, ● HN (AN only)'
},
{
value: 'eighth-then-half',
label: '♪ / ● / 𝅝 / ♩ septets (same as full)'
},
{
value: 'half-then-eighth',
label: 'Inverted septet durations'
},
{
value: 'first-half-eighth',
label: '♪ AM, ● NZ (lossy)'
},
{
value: 'solfège-only',
label: 'Solfège names only (lossy)'
}
]
},
{
id: 'includeDigits',
label: 'Encode digits 09 (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;
}
});
})();
+162
View File
@@ -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);
}
});
+90
View File
@@ -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) + '...';
}
});
+64
View File
@@ -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;
}
});
})();
+54
View File
@@ -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) + '...';
}
});
})();
+58
View File
@@ -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) + '...';
}
});
+94
View File
@@ -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) + '...';
}
});
})();
+163
View File
@@ -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) + '...';
}
});
})();
+48
View File
@@ -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) + '...';
}
});
+49
View File
@@ -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) + '...';
}
});
+122
View File
@@ -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;
});
}
});
})();
+34
View File
@@ -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;
}
});
})();
+154
View File
@@ -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;
}
});
})();
+133
View File
@@ -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;
}
});
})();
+74
View File
@@ -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));
}
});
})();
+86
View File
@@ -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)));
}
});
})();
+110 -76
View File
@@ -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);
}
});
})();
+214
View File
@@ -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);
}
});
})();
+77
View File
@@ -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);
}
});
})();
+71
View File
@@ -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);
}
});
+66
View File
@@ -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;
}
});
})();
+67
View File
@@ -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+0300U+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));
}
});
+1 -1
View File
@@ -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',
+47
View File
@@ -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 (159)
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);
}
});
})();
+47
View File
@@ -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);
}
});
+47
View File
@@ -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);
}
});
+47
View File
@@ -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 (126)
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);
}
});
})();
+47
View File
@@ -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);
}
});
+51
View File
@@ -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);
}
});
})();

Some files were not shown because too many files have changed in this diff Show More