From dc10a90851115416975b667598d6ffd72ffbd296 Mon Sep 17 00:00:00 2001 From: Dustin Farley Date: Tue, 2 Dec 2025 19:02:18 -0800 Subject: [PATCH] refactor: migrate to modular tool-based architecture - Implement tool registry system with individual tool modules - Reorganize transformers into categorized source modules - Remove emojiLibrary.js, consolidate into EmojiUtils and emojiData - Fix mobile close button and tooltip functionality - Add build system for transforms and emoji data - Migrate from Python backend to pure JavaScript - Add comprehensive documentation and testing - Improve code organization and maintainability - Ignore generated files (transforms-bundle.js, emojiData.js) --- .github/workflows/README.md | 51 + .github/workflows/deploy.yml | 57 + .gitignore | 67 + CONTRIBUTING.md | 412 +++ DECODER_IMPROVEMENTS.md | 191 -- IMPROVEMENTS.md | 262 -- README.md | 113 +- build/README.md | 66 + build/build-emoji-data.js | 491 ++++ build/build-index.js | 71 + build/build-transforms.js | 135 + build/inject-tool-scripts.js | 92 + build/inject-tool-templates.js | 85 + css/style.css | 1887 ++++++++++-- docs/TOOL-SYSTEM.md | 80 + docs/TOOL_ARCHITECTURE.md | 181 ++ docs/UI-COMPONENTS.md | 256 ++ favicon.svg | 4 + index.html | 1256 -------- index.template.html | 233 ++ js/README.md | 39 + js/app.js | 2521 ++--------------- js/config/constants.js | 21 + js/core/decoder.js | 101 + js/core/steganography.js | 270 ++ js/core/toolRegistry.js | 209 ++ js/data/emojiCompatibility.js | 208 ++ js/emojiLibrary.js | 232 -- js/steganography.js | 232 -- js/tools/DecodeTool.js | 96 + js/tools/EmojiTool.js | 398 +++ js/tools/GibberishTool.js | 236 ++ js/tools/MutationTool.js | 119 + js/tools/SplitterTool.js | 267 ++ js/tools/TokenadeTool.js | 245 ++ js/tools/TokenizerTool.js | 95 + js/tools/Tool.js | 97 + js/tools/TransformTool.js | 193 ++ js/transforms.js | 2481 ---------------- js/utils/clipboard.js | 51 + js/utils/emoji.js | 33 + js/utils/escapeParser.js | 40 + js/utils/focus.js | 29 + js/utils/history.js | 60 + js/utils/notifications.js | 37 + js/utils/theme.js | 37 + node_modules/.package-lock.json | 6 - package.json | 26 +- parsel_app.py | 498 ---- {js => src}/emojiWordMap.js | 0 src/transformers/BaseTransformer.js | 153 + src/transformers/README.md | 151 + src/transformers/ancient/elder-futhark.js | 39 + src/transformers/ancient/hieroglyphics.js | 32 + src/transformers/ancient/ogham.js | 32 + src/transformers/ancient/roman-numerals.js | 44 + src/transformers/case/alternating-case.js | 51 + src/transformers/case/camel-case.js | 20 + src/transformers/case/kebab-case.js | 37 + src/transformers/case/random-case.js | 16 + src/transformers/case/sentence-case.js | 18 + src/transformers/case/snake-case.js | 29 + src/transformers/case/title-case.js | 16 + src/transformers/cipher/affine.js | 32 + src/transformers/cipher/atbash.js | 34 + src/transformers/cipher/baconian.js | 40 + src/transformers/cipher/caesar.js | 45 + src/transformers/cipher/rail-fence.js | 50 + src/transformers/cipher/rot13.js | 37 + src/transformers/cipher/rot18.js | 27 + src/transformers/cipher/rot47.js | 27 + src/transformers/cipher/rot5.js | 26 + src/transformers/cipher/vigenere.js | 42 + src/transformers/encoding/ascii85.js | 111 + src/transformers/encoding/base32.js | 85 + src/transformers/encoding/base45.js | 45 + src/transformers/encoding/base58.js | 61 + src/transformers/encoding/base62.js | 44 + src/transformers/encoding/base64.js | 51 + src/transformers/encoding/base64url.js | 53 + src/transformers/encoding/binary.js | 43 + src/transformers/encoding/hex.js | 40 + src/transformers/encoding/html.js | 32 + src/transformers/encoding/invisible-text.js | 39 + src/transformers/encoding/url.js | 31 + src/transformers/fantasy/aurebesh.js | 38 + src/transformers/fantasy/dovahzul.js | 56 + src/transformers/fantasy/klingon.js | 58 + src/transformers/fantasy/quenya.js | 36 + src/transformers/fantasy/tengwar.js | 32 + src/transformers/format/leetspeak.js | 32 + src/transformers/format/pigLatin.js | 143 + src/transformers/format/qwerty-shift.js | 40 + src/transformers/format/reverse-words.js | 23 + src/transformers/format/reverse.js | 18 + src/transformers/loader-node.js | 151 + src/transformers/special/randomizer.js | 146 + src/transformers/technical/a1z26.js | 53 + src/transformers/technical/braille.js | 56 + src/transformers/technical/brainfuck.js | 88 + src/transformers/technical/morse.js | 61 + src/transformers/technical/nato.js | 44 + src/transformers/technical/semaphore.js | 54 + src/transformers/technical/tap-code.js | 68 + src/transformers/unicode/bubble.js | 25 + src/transformers/unicode/chemical.js | 71 + src/transformers/unicode/cursive.js | 23 + src/transformers/unicode/cyrillic-stylized.js | 25 + src/transformers/unicode/doubleStruck.js | 24 + src/transformers/unicode/fraktur.js | 48 + src/transformers/unicode/fullwidth.js | 39 + src/transformers/unicode/greek.js | 44 + src/transformers/unicode/hiragana.js | 70 + src/transformers/unicode/katakana.js | 69 + src/transformers/unicode/mathematical.js | 32 + src/transformers/unicode/medieval.js | 23 + src/transformers/unicode/mirror.js | 19 + src/transformers/unicode/monospace.js | 24 + .../unicode/regional-indicator.js | 34 + src/transformers/unicode/small-caps.js | 22 + src/transformers/unicode/strikethrough.js | 22 + src/transformers/unicode/subscript.js | 25 + src/transformers/unicode/superscript.js | 26 + src/transformers/unicode/underline.js | 22 + src/transformers/unicode/upside-down.js | 40 + src/transformers/unicode/vaporwave.js | 21 + src/transformers/unicode/wingdings.js | 45 + src/transformers/unicode/zalgo.js | 39 + src/transformers/visual/disemvowel.js | 16 + src/transformers/visual/emoji-speak.js | 80 + src/transformers/visual/rovarspraket.js | 21 + src/transformers/visual/ubbi-dubbi.js | 20 + templates/README.md | 34 + templates/decoder.html | 65 + templates/fuzzer.html | 48 + templates/gibberish.html | 205 ++ templates/splitter.html | 149 + templates/steganography.html | 62 + templates/tokenade.html | 152 + templates/tokenizer.html | 48 + templates/transforms.html | 119 + test_transforms.html | 258 -- tests/README.md | 42 + tests/test_steganography_options.js | 221 ++ tests/test_universal.js | 619 ++++ text_transforms.py | 195 -- 146 files changed, 12712 insertions(+), 8171 deletions(-) create mode 100644 .github/workflows/README.md create mode 100644 .github/workflows/deploy.yml create mode 100644 .gitignore create mode 100644 CONTRIBUTING.md delete mode 100644 DECODER_IMPROVEMENTS.md delete mode 100644 IMPROVEMENTS.md create mode 100644 build/README.md create mode 100644 build/build-emoji-data.js create mode 100644 build/build-index.js create mode 100755 build/build-transforms.js create mode 100644 build/inject-tool-scripts.js create mode 100644 build/inject-tool-templates.js create mode 100644 docs/TOOL-SYSTEM.md create mode 100644 docs/TOOL_ARCHITECTURE.md create mode 100644 docs/UI-COMPONENTS.md create mode 100644 favicon.svg delete mode 100644 index.html create mode 100644 index.template.html create mode 100644 js/README.md create mode 100644 js/config/constants.js create mode 100644 js/core/decoder.js create mode 100644 js/core/steganography.js create mode 100644 js/core/toolRegistry.js create mode 100644 js/data/emojiCompatibility.js delete mode 100644 js/emojiLibrary.js delete mode 100644 js/steganography.js create mode 100644 js/tools/DecodeTool.js create mode 100644 js/tools/EmojiTool.js create mode 100644 js/tools/GibberishTool.js create mode 100644 js/tools/MutationTool.js create mode 100644 js/tools/SplitterTool.js create mode 100644 js/tools/TokenadeTool.js create mode 100644 js/tools/TokenizerTool.js create mode 100644 js/tools/Tool.js create mode 100644 js/tools/TransformTool.js delete mode 100644 js/transforms.js create mode 100644 js/utils/clipboard.js create mode 100644 js/utils/emoji.js create mode 100644 js/utils/escapeParser.js create mode 100644 js/utils/focus.js create mode 100644 js/utils/history.js create mode 100644 js/utils/notifications.js create mode 100644 js/utils/theme.js delete mode 100644 node_modules/.package-lock.json delete mode 100644 parsel_app.py rename {js => src}/emojiWordMap.js (100%) create mode 100644 src/transformers/BaseTransformer.js create mode 100644 src/transformers/README.md create mode 100644 src/transformers/ancient/elder-futhark.js create mode 100644 src/transformers/ancient/hieroglyphics.js create mode 100644 src/transformers/ancient/ogham.js create mode 100644 src/transformers/ancient/roman-numerals.js create mode 100644 src/transformers/case/alternating-case.js create mode 100644 src/transformers/case/camel-case.js create mode 100644 src/transformers/case/kebab-case.js create mode 100644 src/transformers/case/random-case.js create mode 100644 src/transformers/case/sentence-case.js create mode 100644 src/transformers/case/snake-case.js create mode 100644 src/transformers/case/title-case.js create mode 100644 src/transformers/cipher/affine.js create mode 100644 src/transformers/cipher/atbash.js create mode 100644 src/transformers/cipher/baconian.js create mode 100644 src/transformers/cipher/caesar.js create mode 100644 src/transformers/cipher/rail-fence.js create mode 100644 src/transformers/cipher/rot13.js create mode 100644 src/transformers/cipher/rot18.js create mode 100644 src/transformers/cipher/rot47.js create mode 100644 src/transformers/cipher/rot5.js create mode 100644 src/transformers/cipher/vigenere.js create mode 100644 src/transformers/encoding/ascii85.js create mode 100644 src/transformers/encoding/base32.js create mode 100644 src/transformers/encoding/base45.js create mode 100644 src/transformers/encoding/base58.js create mode 100644 src/transformers/encoding/base62.js create mode 100644 src/transformers/encoding/base64.js create mode 100644 src/transformers/encoding/base64url.js create mode 100644 src/transformers/encoding/binary.js create mode 100644 src/transformers/encoding/hex.js create mode 100644 src/transformers/encoding/html.js create mode 100644 src/transformers/encoding/invisible-text.js create mode 100644 src/transformers/encoding/url.js create mode 100644 src/transformers/fantasy/aurebesh.js create mode 100644 src/transformers/fantasy/dovahzul.js create mode 100644 src/transformers/fantasy/klingon.js create mode 100644 src/transformers/fantasy/quenya.js create mode 100644 src/transformers/fantasy/tengwar.js create mode 100644 src/transformers/format/leetspeak.js create mode 100644 src/transformers/format/pigLatin.js create mode 100644 src/transformers/format/qwerty-shift.js create mode 100644 src/transformers/format/reverse-words.js create mode 100644 src/transformers/format/reverse.js create mode 100644 src/transformers/loader-node.js create mode 100644 src/transformers/special/randomizer.js create mode 100644 src/transformers/technical/a1z26.js create mode 100644 src/transformers/technical/braille.js create mode 100644 src/transformers/technical/brainfuck.js create mode 100644 src/transformers/technical/morse.js create mode 100644 src/transformers/technical/nato.js create mode 100644 src/transformers/technical/semaphore.js create mode 100644 src/transformers/technical/tap-code.js create mode 100644 src/transformers/unicode/bubble.js create mode 100644 src/transformers/unicode/chemical.js create mode 100644 src/transformers/unicode/cursive.js create mode 100644 src/transformers/unicode/cyrillic-stylized.js create mode 100644 src/transformers/unicode/doubleStruck.js create mode 100644 src/transformers/unicode/fraktur.js create mode 100644 src/transformers/unicode/fullwidth.js create mode 100644 src/transformers/unicode/greek.js create mode 100644 src/transformers/unicode/hiragana.js create mode 100644 src/transformers/unicode/katakana.js create mode 100644 src/transformers/unicode/mathematical.js create mode 100644 src/transformers/unicode/medieval.js create mode 100644 src/transformers/unicode/mirror.js create mode 100644 src/transformers/unicode/monospace.js create mode 100644 src/transformers/unicode/regional-indicator.js create mode 100644 src/transformers/unicode/small-caps.js create mode 100644 src/transformers/unicode/strikethrough.js create mode 100644 src/transformers/unicode/subscript.js create mode 100644 src/transformers/unicode/superscript.js create mode 100644 src/transformers/unicode/underline.js create mode 100644 src/transformers/unicode/upside-down.js create mode 100644 src/transformers/unicode/vaporwave.js create mode 100644 src/transformers/unicode/wingdings.js create mode 100644 src/transformers/unicode/zalgo.js create mode 100644 src/transformers/visual/disemvowel.js create mode 100644 src/transformers/visual/emoji-speak.js create mode 100644 src/transformers/visual/rovarspraket.js create mode 100644 src/transformers/visual/ubbi-dubbi.js create mode 100644 templates/README.md create mode 100644 templates/decoder.html create mode 100644 templates/fuzzer.html create mode 100644 templates/gibberish.html create mode 100644 templates/splitter.html create mode 100644 templates/steganography.html create mode 100644 templates/tokenade.html create mode 100644 templates/tokenizer.html create mode 100644 templates/transforms.html delete mode 100644 test_transforms.html create mode 100644 tests/README.md create mode 100644 tests/test_steganography_options.js create mode 100644 tests/test_universal.js delete mode 100644 text_transforms.py diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..d55c8b7 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,51 @@ +# GitHub Actions Workflows + +## `deploy.yml` - Automated GitHub Pages Deployment + +This workflow automatically builds and deploys the project to GitHub Pages whenever changes are pushed to the `main` or `master` branch. + +### What it does: + +1. **Build Stage:** + - Checks out the repository + - Sets up Node.js (v18) + - Installs dependencies with `npm ci` + - Runs `npm run build` which: + - Builds transformer index (`build-index.js`) + - Bundles all transformers (`build-transforms.js`) + - Generates emoji data (`build-emoji-data.js`) + - Injects tool scripts (`inject-tool-scripts.js`) + - Injects tool templates into `index.html` (`inject-tool-templates.js`) + - Uploads the entire project as a Pages artifact + +2. **Deploy Stage:** + - Deploys the artifact to GitHub Pages + - Makes the site available at your GitHub Pages URL + +### Manual Deployment + +You can also trigger a deployment manually from the GitHub Actions tab by selecting "Build and Deploy to GitHub Pages" and clicking "Run workflow". + +### Required GitHub Settings + +For this workflow to function, ensure GitHub Pages is configured in your repository settings: + +1. Go to **Settings** โ†’ **Pages** +2. Under **Build and deployment**: + - Source: **GitHub Actions** +3. Save the settings + +The site will be available at: `https://.github.io//` + +### Troubleshooting + +- **Build fails**: Check the Actions tab for error logs +- **Missing templates**: Ensure all templates exist in `templates/` directory +- **Test locally first**: Run `npm run build:templates` before pushing to catch errors early +- **Verify build output**: Check that `index.html` contains injected templates after build + +### Workflow Triggers + +- **Push**: Automatically runs on push to `main` or `master` +- **Workflow Dispatch**: Can be manually triggered from the Actions tab + diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..4b467fe --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,57 @@ +name: Build and Deploy to GitHub Pages + +on: + push: + branches: [ main, master ] + # Allow manual trigger from Actions tab + workflow_dispatch: + +# Sets permissions for GitHub Pages deployment +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Install dependencies + run: npm ci + + - name: Build project + run: | + echo "Running full build..." + npm run build + echo "Build complete!" + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: '.' + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..13ebf2c --- /dev/null +++ b/.gitignore @@ -0,0 +1,67 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Build outputs +dist/ +.cache/ +*.log + +# Generated files +index.html +src/transformers/index.js +js/bundles/transforms-bundle.js +js/data/emojiData.js + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE & Editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# OS files +Thumbs.db +desktop.ini +.DS_Store +.AppleDouble +.LSOverride +._* + +# Temporary files +*.tmp +*.temp +.tmp/ +.temp/ + +# Coverage & Testing +coverage/ +.nyc_output/ +*.lcov + +# Package manager locks (optional - uncomment if you want to ignore) +# package-lock.json +# yarn.lock +# pnpm-lock.yaml + +# Debug files +npm-debug.log +yarn-debug.log +lerna-debug.log + +# Misc +*.bak +*.backup +*.orig +.sass-cache/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ef516d0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,412 @@ +# Contributing to P4RS3LT0NGV3 + +Thank you for your interest in contributing! This guide will help you understand the project structure and how to add new features. + +## ๐Ÿ“ Project Structure + +``` +P4RS3LT0NGV3/ +โ”œโ”€โ”€ js/ +โ”‚ โ”œโ”€โ”€ app.js # Main Vue.js application entry point +โ”‚ โ”œโ”€โ”€ core/ # Core feature modules (shared libraries) +โ”‚ โ”‚ โ”œโ”€โ”€ decoder.js # Universal decoder +โ”‚ โ”‚ โ”œโ”€โ”€ steganography.js # Steganography encoding/decoding +โ”‚ โ”‚ โ””โ”€โ”€ toolRegistry.js # Tool registry system +โ”‚ โ”œโ”€โ”€ bundles/ # Build-generated files (auto-created) +โ”‚ โ”‚ โ””โ”€โ”€ transforms-bundle.js # Generated bundle from src/transformers/ +โ”‚ โ”œโ”€โ”€ config/ # Configuration constants +โ”‚ โ”‚ โ””โ”€โ”€ constants.js +โ”‚ โ”œโ”€โ”€ data/ # Generated or static data files (auto-created) +โ”‚ โ”‚ โ”œโ”€โ”€ emojiData.js # Generated from Unicode emoji data +โ”‚ โ”‚ โ””โ”€โ”€ emojiCompatibility.js +โ”‚ โ”œโ”€โ”€ utils/ # Utility functions +โ”‚ โ”‚ โ”œโ”€โ”€ clipboard.js +โ”‚ โ”‚ โ”œโ”€โ”€ emoji.js +โ”‚ โ”‚ โ”œโ”€โ”€ escapeParser.js +โ”‚ โ”‚ โ”œโ”€โ”€ focus.js +โ”‚ โ”‚ โ”œโ”€โ”€ history.js +โ”‚ โ”‚ โ”œโ”€โ”€ notifications.js +โ”‚ โ”‚ โ””โ”€โ”€ theme.js +โ”‚ โ””โ”€โ”€ tools/ # Tool implementations (Vue integration) +โ”‚ โ”œโ”€โ”€ Tool.js # Base class +โ”‚ โ”œโ”€โ”€ TransformTool.js +โ”‚ โ”œโ”€โ”€ DecodeTool.js +โ”‚ โ”œโ”€โ”€ EmojiTool.js +โ”‚ โ”œโ”€โ”€ TokenadeTool.js +โ”‚ โ”œโ”€โ”€ MutationTool.js +โ”‚ โ”œโ”€โ”€ TokenizerTool.js +โ”‚ โ”œโ”€โ”€ SplitterTool.js +โ”‚ โ””โ”€โ”€ GibberishTool.js +โ”œโ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ emojiWordMap.js # Emoji keyword mappings (merged into emojiData.js) +โ”‚ โ””โ”€โ”€ transformers/ # Transformer modules (source - bundled at build time) +โ”‚ โ”œโ”€โ”€ BaseTransformer.js +โ”‚ โ”œโ”€โ”€ ancient/ # Elder Futhark, Hieroglyphics, Ogham, Roman Numerals +โ”‚ โ”œโ”€โ”€ case/ # Camel, Kebab, Snake, Title, etc. +โ”‚ โ”œโ”€โ”€ cipher/ # Caesar, ROT13, Vigenรจre, Atbash, etc. +โ”‚ โ”œโ”€โ”€ encoding/ # Base64, Hex, Binary, URL, etc. +โ”‚ โ”œโ”€โ”€ fantasy/ # Quenya, Tengwar, Klingon, Aurebesh, Dovahzul +โ”‚ โ”œโ”€โ”€ format/ # Leetspeak, Pig Latin, Reverse, etc. +โ”‚ โ”œโ”€โ”€ special/ # Randomizer +โ”‚ โ”œโ”€โ”€ technical/ # Morse, NATO, Braille, Brainfuck, etc. +โ”‚ โ”œโ”€โ”€ unicode/ # Upside-down, Fullwidth, Bubble, etc. +โ”‚ โ””โ”€โ”€ visual/ # Disemvowel, Rovarspraket, Ubbi-dubbi, etc. +โ”œโ”€โ”€ templates/ # HTML templates for tools (injected at build time) +โ”‚ โ”œโ”€โ”€ decoder.html +โ”‚ โ”œโ”€โ”€ steganography.html +โ”‚ โ”œโ”€โ”€ transforms.html +โ”‚ โ”œโ”€โ”€ tokenade.html +โ”‚ โ”œโ”€โ”€ fuzzer.html +โ”‚ โ”œโ”€โ”€ tokenizer.html +โ”‚ โ”œโ”€โ”€ splitter.html +โ”‚ โ””โ”€โ”€ gibberish.html +โ”œโ”€โ”€ build/ # Build scripts +โ”‚ โ”œโ”€โ”€ build-index.js # Generates transformer index +โ”‚ โ”œโ”€โ”€ build-transforms.js # Bundles transformers into js/bundles/ +โ”‚ โ”œโ”€โ”€ build-emoji-data.js # Generates emojiData.js from Unicode data +โ”‚ โ”œโ”€โ”€ inject-tool-scripts.js # Auto-discovers and registers tools +โ”‚ โ””โ”€โ”€ inject-tool-templates.js # Injects templates into index.html +โ”œโ”€โ”€ tests/ # Test suites +โ”‚ โ”œโ”€โ”€ test_universal.js +โ”‚ โ””โ”€โ”€ test_steganography_options.js +โ”œโ”€โ”€ css/ # Stylesheets +โ”‚ โ”œโ”€โ”€ style.css +โ”‚ โ””โ”€โ”€ notification.css +โ”œโ”€โ”€ index.template.html # Base HTML template (templates injected here) +โ”œโ”€โ”€ index.html # Generated file (created by build process) +โ””โ”€โ”€ docs/ # Documentation +``` + +## ๐ŸŽฏ Key Concepts + +### Core vs Tools + +- **`js/core/`** - Shared business logic and infrastructure + - These are **NOT** tool-specific + - Examples: + - `decoder.js` (used by DecodeTool + app.js) + - `steganography.js` (used by EmojiTool + decoder.js) + - `emojiLibrary.js` (used by EmojiTool) + - `toolRegistry.js` (ToolRegistry class - infrastructure for the tool system) + +- **Source files** (`src/`) - Source files used by build process + - `emojiWordMap.js` - Emoji keyword mappings (merged into emojiData.js during build) + - `transformers/` - Transformer modules (bundled into transforms-bundle.js) + +- **Generated files** (`js/bundles/`) + - `transforms-bundle.js` - Generated bundle from `src/transformers/` (created by `npm run build:transforms`) + +- **`js/tools/`** - Vue.js integration layer for UI features + - Each tool represents a tab/feature in the UI + - Tools use core modules and utilities for functionality + - Example: `DecodeTool.js` uses `window.universalDecode` from `core/decoder.js` + +### Transformers vs Tools + +- **Transformers** (`src/transformers/`) - Text transformation logic (encoding/decoding) +- **Tools** (`js/tools/`) - UI features/tabs (Transform tab, Decoder tab, Emoji tab) + +## ๐Ÿš€ Getting Started + +### Prerequisites + +- Node.js (for running tests and builds) +- Modern web browser (for testing) + +### Setup + +```bash +# Clone the repository +git clone +cd P4RS3LT0NGV3 + +# Install dependencies (if any) +npm install + +# Build transformers bundle +npm run build + +# Run tests +npm test +``` + +## โœจ Adding New Features + +### 1. Adding a New Transformer + +Transformers are the core text transformation logic. See `src/transformers/README.md` for detailed instructions. + +**Quick Start:** + +1. Create a new file in the appropriate category directory: + ```bash + src/transformers/ciphers/my-cipher.js + ``` + +2. Use the `BaseTransformer` class: + ```javascript + import BaseTransformer from '../BaseTransformer.js'; + + 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; + }, + reverse: function(text) { + // Decoding logic + return decoded; + }, + detector: function(text) { + // Optional: pattern detection for universal decoder + return /pattern/.test(text); + } + }); + ``` + +3. Rebuild the bundle: + ```bash + npm run build + ``` + +4. Test it: + - Open `index.html` in a browser + - Your transformer will appear in the Transform tab automatically + - Test encoding/decoding + - Test with the Universal Decoder + +5. Add tests (optional but recommended): + - Add test cases to `tests/test_universal.js` + - Run `npm test` to verify + +**Important:** Transformers are automatically discovered and bundled. No manual registration needed! + +### 2. Adding a New Tool (New Tab/Feature) + +Tools represent UI features/tabs. Examples: Transform tab, Decoder tab, Emoji tab. + +**Steps:** + +1. Create a new tool class in `js/tools/`: + ```javascript + // js/tools/MyNewTool.js + class MyNewTool extends Tool { + constructor() { + super({ + id: 'myfeature', // Unique ID (used for tab switching) + name: 'My Feature', // Display name + icon: 'fa-star', // Font Awesome icon class + title: 'My Feature (M)', // Tooltip with keyboard shortcut + order: 5 // Display order (lower = earlier) + }); + } + + getVueData() { + return { + // Vue data properties for this tool + myInput: '', + myOutput: '' + }; + } + + getVueMethods() { + return { + // Vue methods for this tool + doSomething: function() { + // Your logic here + } + }; + } + + getTabContentHTML() { + return ` + +
+ +
{{ myOutput }}
+
+ `; + } + } + ``` + +2. Run the build script to auto-register your tool: + ```bash + npm run build:tools + ``` + + This will: + - Auto-discover your new tool file + - Add script tag to `index.template.html` + - Generate registration code in `toolRegistry.js` + +3. If you created a template file, build templates: + ```bash + npm run build:templates + ``` + +4. Test it: + - Open `index.html` + - Your new tab should appear automatically + - Test all functionality + +**See `js/tools/Tool.js` for the base class API and `js/tools/TransformTool.js` for a complete example.** + +### 3. Adding a New Utility Function + +Utilities are shared helper functions used across the app. Currently, utility functions are typically added directly to the modules that need them or as part of core modules. + +**If you need to create a new utility module:** + +1. Create a new file in `js/` (root level) or `js/core/`: + ```javascript + // js/myUtility.js + window.MyUtility = { + doSomething: function(param) { + // Your utility function + return result; + } + }; + ``` + +2. Add script tag to `index.html` (before `app.js`): + ```html + + ``` + +3. Use it in your code: + ```javascript + window.MyUtility.doSomething(value); + ``` + +**Guidelines:** +- Keep utilities pure (no side effects when possible) +- Use `window` namespace for browser compatibility +- Document with JSDoc comments +- Consider adding to existing modules if functionality is related + +**Note:** The `js/utils/` directory contains utility functions: clipboard, escapeParser, focus, history, notifications, and theme. The `js/config/` directory contains configuration constants. + +## ๐Ÿงช Testing + +### Running Tests + +```bash +# Run all tests +npm test + +# Run specific test suite +npm run test:universal # Universal decoder tests +npm run test:steg # Steganography options tests +``` + +### Writing Tests + +- **Transformer tests**: Add to `tests/test_universal.js` + - Tests are automatically discovered + - Add limitations/expected behavior to the `limitations` object if needed + +- **Steganography tests**: Add to `tests/test_steganography_options.js` + - Tests encoding/decoding round-trips with various option combinations + +- **New test files**: Create in `tests/` directory + - Use `path.resolve(__dirname, '..')` to get project root + - Use `path.join(projectRoot, '...')` for file paths + +## ๐Ÿ“ Code Style + +### JavaScript + +- Use ES6+ features (arrow functions, const/let, template literals) +- Use meaningful variable names +- Add JSDoc comments for public functions +- Follow existing code style in the file you're editing + +### File Organization + +- **Core modules** (`js/core/`) - Shared business logic (e.g., `decoder.js`) +- **Root-level modules** (`js/`) - Feature libraries (e.g., `steganography.js`, `emojiLibrary.js`) +- **Tools** (`js/tools/`) - Vue.js UI integration layer +- **Templates** (`templates/`) - HTML templates for tools (injected at build time) +- **Transformers** (`src/transformers/`) - Text transformation logic +- **Bundles** (`js/bundles/`) - Build-generated files + +### Naming Conventions + +- **Files**: `camelCase.js` for utilities/tools, `kebab-case.js` for transformers +- **Classes**: `PascalCase` (e.g., `DecodeTool`) +- **Functions**: `camelCase` (e.g., `runUniversalDecode`) +- **Constants**: `UPPER_SNAKE_CASE` (e.g., `MAX_HISTORY_ITEMS`) + +## ๐Ÿ”ง Build Process + +### Building Templates + +```bash +npm run build:templates +``` + +This: +1. Reads all `.html` files from `templates/` directory +2. Injects them into `index.html` at the `#tool-content-container` marker +3. Creates a single static HTML file for fast loading + +**When to run:** +- After editing any template in `templates/` +- Before committing template changes + +### Build Script Details + +- **Directory Creation**: Build scripts automatically create output directories (`js/bundles/`, `js/data/`) if they don't exist +- **Full Build**: Run `npm run build` to execute all build steps in order +- **Individual Builds**: Each build script can be run independently + +**Note:** Transformers are loaded from `js/bundles/transforms-bundle.js` which may be pre-built or generated separately. + +## ๐Ÿ› Debugging + +### Common Issues + +1. **Template changes not showing**: Run `npm run build:templates` to inject templates into `index.html` +2. **Tool not showing**: Check that: + - Tool is registered in `js/core/toolRegistry.js` + - Script tag is in `index.html` before `app.js` + - Template file exists in `templates/` directory +3. **Tests failing**: Check file paths use `path.join(projectRoot, '...')` + +### Browser Console + +- Open browser DevTools (F12) +- Check console for errors +- Use `window.transforms` to see all transformers +- Use `window.steganography` to access steganography functions +- Use `window.emojiLibrary` to access emoji functions + +## ๐Ÿ“š Documentation + +- **Project README**: `README.md` - Overview and user guide +- **Templates**: `templates/README.md` - How to edit tool templates +- **Build Process**: `build/README.md` - Build script documentation +- **Tool System**: `docs/TOOL-SYSTEM.md` - Tool architecture details +- **Code Review**: `docs/CODE-REVIEW.md` - Architecture and code review guidelines + +## โœ… Checklist Before Submitting + +- [ ] Code follows existing style +- [ ] Tests pass (`npm test`) +- [ ] Templates built (`npm run build:templates`) if template files were edited +- [ ] Tested in browser (open `index.html`) +- [ ] No console errors +- [ ] Documentation updated (if needed) +- [ ] JSDoc comments added (for new functions) + +## ๐Ÿค Questions? + +- Check existing code for examples +- Review `docs/CODE_REVIEW.md` for architecture details +- Look at similar features to understand patterns + +Thank you for contributing! ๐ŸŽ‰ + diff --git a/DECODER_IMPROVEMENTS.md b/DECODER_IMPROVEMENTS.md deleted file mode 100644 index 6c441b1..0000000 --- a/DECODER_IMPROVEMENTS.md +++ /dev/null @@ -1,191 +0,0 @@ -# ๐Ÿ” Universal Decoder - Comprehensive Improvements - -## ๐Ÿ“‹ **Overview** - -The Universal Decoder in P4RS3LT0NGV3 has been significantly enhanced to support all the new fantasy, ancient, and technical languages we added. It now provides **intelligent pattern detection**, **priority matching**, and **comprehensive fallback methods** for decoding virtually any supported format. - ---- - -## ๐Ÿš€ **New Decoder Capabilities** - -### **๐Ÿง™โ€โ™‚๏ธ Fantasy Languages Support** -- **Quenya (Tolkien Elvish)**: Phonetic transformations with reverse mapping -- **Tengwar Script**: Unicode rune detection and decoding -- **Klingon**: Star Trek language with phonetic enhancements -- **Aurebesh (Star Wars)**: Word-based galactic alphabet -- **Dovahzul (Dragon)**: Skyrim dragon language with reverse functions - -### **๐Ÿ›๏ธ Ancient Scripts Support** -- **Hieroglyphics**: Egyptian symbol detection and decoding -- **Ogham (Celtic)**: Celtic tree alphabet support -- **Elder Futhark**: Germanic rune system -- **Semaphore Flags**: Flag signaling detection - -### **โš™๏ธ Technical Codes Support** -- **Brainfuck**: Esoteric programming language detection -- **Mathematical Notation**: Unicode mathematical symbols -- **Chemical Symbols**: Periodic table element abbreviations - ---- - -## ๐Ÿ”ง **Enhanced Detection Methods** - -### **1. Smart Pattern Recognition** -The decoder now uses **advanced regex patterns** to identify specific transform types: - -```javascript -// Fantasy language patterns -if (/[แšชแ›’แ›ฒแ›žแ›–แš แšทแšบแ›แ›ƒแ›šแ›—แšพแ›Ÿแ›ˆแ›ฉแšฑแ›‹แ›แšขแ›ฉแ›‰]/.test(input)) { - // Detects Tengwar and Elder Futhark runes -} - -// Hieroglyphic patterns -if (/[๐“ƒญ๐“ƒฎ๐“ƒฏ๐“ƒฐ๐“ƒฑ๐“ƒฒ๐“ƒณ๐“ƒด๐“ƒต๐“ƒถ๐“ƒท๐“ƒธ๐“ƒน๐“ƒบ๐“ƒป๐“ƒผ]/.test(input)) { - // Detects Egyptian hieroglyphics -} - -// Mathematical notation patterns -if (/[๐’ถ๐’ท๐’ธ๐’น๐‘’๐’ป๐‘”๐’ฝ๐’พ๐’ฟ๐“€๐“๐“‚๐“ƒ๐‘œ๐“…๐“†๐“‡๐“ˆ๐“‰๐“Š๐“‹๐“Œ๐“๐“Ž๐“]/.test(input)) { - // Detects mathematical script characters -} -``` - -### **2. Priority Matching System** -- **Active Transform Priority**: Uses currently selected transform first -- **Pattern Priority**: Recognizes specific character patterns for immediate identification -- **Fallback Methods**: Tries all available decoders if primary methods fail - -### **3. Reverse Function Mapping** -All transforms with reverse functions are automatically supported: - -```javascript -// Generic reverse function testing -for (const name in window.transforms) { - const transform = window.transforms[name]; - if (transform.reverse) { - try { - const result = transform.reverse(input); - if (result !== input && /[a-zA-Z0-9\s]{3,}/.test(result)) { - return { text: result, method: transform.name }; - } - } catch (e) { - console.error(`Error decoding with ${name}:`, e); - } - } -} -``` - ---- - -## ๐ŸŽฏ **Decoder Workflow** - -### **Step 1: Steganography Detection** -1. **Emoji Steganography**: Detects hidden messages in emojis -2. **Invisible Text**: Finds text encoded in Unicode Tags block - -### **Step 2: Active Transform Priority** -1. **Current Selection**: Uses the transform currently selected in the UI -2. **Priority Match**: Returns results with `priorityMatch: true` flag - -### **Step 3: Smart Pattern Detection** -1. **Rune Detection**: Identifies Tengwar, Elder Futhark, Ogham -2. **Symbol Detection**: Finds hieroglyphics, mathematical notation -3. **Language Detection**: Recognizes fantasy and ancient scripts - -### **Step 4: Comprehensive Fallback** -1. **Built-in Reverses**: Tests all transforms with reverse functions -2. **Pattern Matching**: Uses character-based detection for map-based transforms -3. **Format Validation**: Ensures decoded results are readable text - ---- - -## ๐Ÿงช **Testing & Validation** - -### **Test Page Features** -- **Individual Transform Testing**: Test each transform separately -- **Reverse Function Testing**: Validate encoding/decoding cycles -- **Universal Decoder Testing**: Test the complete decoder system -- **Real-time Results**: Instant feedback on decode success - -### **Test Cases Included** -```javascript -// Base64 test -testDecoder('SGVsbG8gV29ybGQh') // "Hello World!" - -// Tengwar test -testDecoder('แšชแ›–แ›šแ›šแšฉ แšนแšฉแšฑแ›šแ›ž') // "Hello World" - -// Hieroglyphics test -testDecoder('๐“ƒด๐“ƒฑ๐“ƒธ๐“ƒน๐“ƒบ') // "Hello" - -// Mathematical test -testDecoder('๐’œ๐’ท๐’ธ๐’น๐‘’') // "Abcde" -``` - ---- - -## ๐Ÿ“Š **Performance Improvements** - -### **Detection Speed** -- **Pattern Recognition**: < 1ms for character-based detection -- **Reverse Functions**: < 5ms for most transforms -- **Fallback Methods**: < 10ms for comprehensive decoding - -### **Memory Efficiency** -- **Lazy Loading**: Only loads transform data when needed -- **Efficient Mapping**: Uses optimized reverse map creation -- **Garbage Collection**: Proper cleanup of temporary objects - ---- - -## ๐Ÿ”ฎ **Future Enhancements** - -### **Advanced Detection** -- **Machine Learning**: Train models to recognize complex patterns -- **Fuzzy Matching**: Handle corrupted or partial encoded text -- **Context Awareness**: Use surrounding text to improve detection - -### **Performance Optimization** -- **Web Workers**: Background processing for large texts -- **Caching**: Store frequently used decode results -- **Parallel Processing**: Decode multiple formats simultaneously - ---- - -## ๐Ÿ“ˆ **Success Metrics** - -### **Coverage** -- โœ… **100% New Transforms**: All 11 new languages supported -- โœ… **100% Reverse Functions**: Every reversible transform works -- โœ… **100% Pattern Detection**: Advanced character recognition -- โœ… **100% Fallback Support**: Comprehensive decoding methods - -### **Accuracy** -- **False Positives**: < 1% for pattern detection -- **Decode Success**: > 99% for valid encoded text -- **Performance**: < 16ms average decode time - ---- - -## ๐ŸŽ‰ **Result** - -The Universal Decoder is now a **comprehensive, intelligent decoding system** that can: - -1. **Automatically Detect** the encoding method used -2. **Prioritize** the most likely decode method -3. **Fallback** to alternative methods if needed -4. **Support** all 50+ transforms in the system -5. **Provide** real-time feedback and results - -This makes P4RS3LT0NGV3 a true **Universal Text Translator** that can not only encode text in countless formats but also intelligently decode any of those formats back to readable text! ๐Ÿ‰โœจ - ---- - -## ๐Ÿงช **How to Test** - -1. **Open `test_transforms.html`** in your browser -2. **Use the Universal Decoder section** to test various encoded texts -3. **Try different transform combinations** to see the decoder in action -4. **Verify reverse functions** work correctly for all transforms - -The decoder will now handle everything from Tolkien Elvish to Egyptian hieroglyphics with ease! ๐ŸŽฏ \ No newline at end of file diff --git a/IMPROVEMENTS.md b/IMPROVEMENTS.md deleted file mode 100644 index 8e35864..0000000 --- a/IMPROVEMENTS.md +++ /dev/null @@ -1,262 +0,0 @@ -# ๐Ÿš€ P4RS3LT0NGV3 - Major Improvements & New Features - -## ๐Ÿ“‹ **Summary of Changes** - -This document details all the improvements, fixes, and new features added to transform P4RS3LT0NGV3 from a basic text transformation tool into a comprehensive **Universal Text Translator** with over 50 different languages, scripts, and encoding systems. - ---- - -## ๐Ÿ”ง **Critical Fixes Applied** - -### **1. Duplicate Transform Issue** -- **Problem**: The `invisible_text` transform was duplicated in `transforms.js` (lines 20-40) -- **Solution**: Removed the duplicate, keeping only one properly implemented version -- **Impact**: Eliminates confusion and potential conflicts - -### **2. Base32 Implementation** -- **Problem**: Original Base32 had encoding/decoding issues and poor error handling -- **Solution**: - - Fixed byte handling using `TextEncoder().encode()` for proper UTF-8 support - - Improved padding handling and validation - - Enhanced reverse function with better error handling -- **Impact**: Now provides RFC 4648 compliant Base32 encoding/decoding - -### **3. Unicode Support Improvements** -- **Problem**: Some transforms didn't handle complex Unicode characters properly -- **Solution**: Enhanced text processing to respect Unicode boundaries and emoji characters -- **Impact**: Better support for international text and emojis - ---- - -## ๐Ÿ†• **New Languages & Scripts Added** - -### **๐Ÿง™โ€โ™‚๏ธ Fantasy Languages (5 new)** -1. **Quenya (Tolkien Elvish)** - - High Elvish language from Lord of the Rings - - Phonetic transformations with proper vowel handling - - Full reverse function for decoding - -2. **Tengwar Script** - - Elvish writing system characters - - Unicode rune mappings - - Bidirectional transformation - -3. **Klingon** - - Star Trek Klingon language - - Phonetic transformations (ch, gh, etc.) - - Proper case handling - -4. **Aurebesh (Star Wars)** - - Galactic Basic alphabet from Star Wars - - Full word transformations (Aurek, Besh, Cresh, etc.) - - Space-separated output format - -5. **Dovahzul (Dragon)** - - Dragon language from Skyrim - - Phonetic enhancements (ah, eh, ii, etc.) - - Maintains original pronunciation - -### **๐Ÿ›๏ธ Ancient Scripts (3 new)** -1. **Hieroglyphics** - - Egyptian hieroglyphic symbols - - Unicode block U+13000-U+1342F - - Visual representation of ancient writing - -2. **Ogham (Celtic)** - - Celtic tree alphabet - - Unicode block U+1680-U+169F - - Historical Irish writing system - -3. **Semaphore Flags** - - Flag signaling system - - Visual flag representations - - Communication method - -### **โš™๏ธ Technical Codes (3 new)** -1. **Brainfuck** - - Esoteric programming language - - Complex code generation - - Programming challenge format - -2. **Mathematical Notation** - - Mathematical script characters - - Unicode mathematical symbols - - Scientific notation support - -3. **Chemical Symbols** - - Chemical element abbreviations - - Periodic table symbols - - Scientific notation - ---- - -## ๐ŸŽจ **Enhanced User Interface** - -### **New Category System** -- **Fantasy**: Pink theme (#ff6b9d) for fictional languages -- **Ancient**: Gold theme (#d4af37) for historical scripts -- **Technical**: Cyan theme (#00bcd4) for programming/scientific codes - -### **Improved Organization** -- **8 Main Categories** instead of 6 -- **Logical Grouping** of related transforms -- **Visual Distinction** with unique color schemes -- **Better Navigation** with category legend - -### **Enhanced Styling** -- **Gradient Backgrounds** for each category -- **Hover Effects** with category-specific colors -- **Active States** with enhanced visual feedback -- **Consistent Theming** across all new categories - ---- - -## ๐Ÿ” **Universal Decoder Improvements** - -### **Enhanced Detection** -- **Priority Matching**: Uses active transform first -- **Fallback Methods**: Tries all available decoders -- **Pattern Recognition**: Better detection of encoded formats -- **Error Handling**: Graceful fallbacks for invalid input - -### **New Decoder Support** -- **Fantasy Languages**: All new fantasy transforms supported -- **Ancient Scripts**: Hieroglyphics, Ogham, etc. -- **Technical Codes**: Brainfuck, mathematical notation -- **Improved Unicode**: Better handling of complex characters - ---- - -## ๐Ÿ“ **File Structure Updates** - -### **Modified Files** -- `js/transforms.js` - Added 11 new transforms, fixed Base32 -- `js/app.js` - Updated categories and transform organization -- `index.html` - Added new category sections and UI elements -- `css/style.css` - Added new category styles and color schemes -- `README.md` - Complete rewrite with comprehensive documentation - -### **New Files** -- `test_transforms.html` - Testing page for all transforms -- `IMPROVEMENTS.md` - This detailed improvements document - ---- - -## ๐Ÿงช **Testing & Validation** - -### **Test Page Created** -- **Comprehensive Testing**: All 50+ transforms testable -- **Category Grouping**: Organized by transform type -- **Reverse Function Testing**: Validates encoding/decoding -- **Error Handling**: Shows detailed error messages -- **Real-time Results**: Instant feedback on transform quality - -### **Validation Results** -- โœ… **Base32**: Fixed and working correctly -- โœ… **New Transforms**: All 11 new transforms functional -- โœ… **Reverse Functions**: Bidirectional where applicable -- โœ… **Unicode Support**: Handles complex characters properly -- โœ… **Category System**: All new categories properly styled - ---- - -## ๐Ÿ“Š **Performance Improvements** - -### **Code Optimization** -- **Eliminated Duplicates**: Removed redundant transform definitions -- **Improved Functions**: Better error handling and edge cases -- **Memory Efficiency**: Optimized for large text processing -- **Rendering**: Enhanced Vue.js component organization - -### **User Experience** -- **Faster Loading**: Optimized transform initialization -- **Smoother Interactions**: Better event handling -- **Responsive Design**: Improved mobile experience -- **Accessibility**: Better screen reader support - ---- - -## ๐ŸŒŸ **Use Cases & Applications** - -### **Creative Writing** -- **Fantasy Stories**: Generate text in fictional languages -- **Secret Messages**: Hide information in plain sight -- **Unique Styles**: Create distinctive text appearances - -### **Education** -- **Language Learning**: Explore different writing systems -- **Cryptography**: Study encoding and decoding methods -- **Cultural Studies**: Learn about ancient scripts - -### **Entertainment** -- **Gaming**: Create character names and messages -- **Social Media**: Add unique flair to posts -- **Puzzles**: Create encoded challenges - -### **Professional** -- **Data Encoding**: Convert text to various formats -- **Testing**: Validate encoding/decoding systems -- **Documentation**: Create multilingual content - ---- - -## ๐Ÿ”ฎ **Future Enhancement Ideas** - -### **Additional Languages** -- **Constructed Languages**: Esperanto, Ithkuil, etc. -- **Regional Scripts**: More Asian, African, American scripts -- **Modern Codes**: QR codes, barcodes, etc. - -### **Advanced Features** -- **Batch Processing**: Transform multiple texts at once -- **Custom Transforms**: User-defined transformation rules -- **API Integration**: REST API for programmatic access -- **Mobile App**: Native mobile application - -### **Performance** -- **Web Workers**: Background processing for large texts -- **Caching**: Store frequently used transforms -- **Lazy Loading**: Load transforms on demand - ---- - -## ๐Ÿ“ˆ **Impact Summary** - -### **Before Improvements** -- **~25 Transforms**: Basic encoding and visual effects -- **6 Categories**: Limited organization -- **Basic UI**: Simple button layout -- **Some Bugs**: Base32 issues, duplicate transforms - -### **After Improvements** -- **~50+ Transforms**: Comprehensive language coverage -- **8 Categories**: Well-organized system -- **Enhanced UI**: Professional appearance with themes -- **Bug-Free**: All critical issues resolved -- **Universal Translator**: True to the project name - ---- - -## ๐ŸŽฏ **Success Metrics** - -- โœ… **100% Bug Fixes**: All identified issues resolved -- โœ… **100% New Features**: All planned features implemented -- โœ… **100% Testing**: Comprehensive test coverage -- โœ… **100% Documentation**: Complete README and guides -- โœ… **100% Styling**: Professional appearance achieved - ---- - -## ๐Ÿ™ **Acknowledgments** - -This project now truly lives up to its name as a **Universal Text Translator** thanks to: - -- **J.R.R. Tolkien** for inspiring fantasy languages -- **Star Trek/Star Wars** creators for sci-fi languages -- **Bethesda** for the Dovahzul language -- **Unicode Consortium** for character standards -- **Open Source Community** for development tools - ---- - -**P4RS3LT0NGV3** is now a comprehensive, professional-grade text transformation tool that can handle virtually any writing system, real or fictional! ๐Ÿ‰โœจ \ No newline at end of file diff --git a/README.md b/README.md index ff60d68..0f0aa3f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ๐Ÿ P4RS3LT0NGV3 - Universal Text Translator -A powerful web-based text transformation and steganography tool that can encode/decode text in over 50 different languages, scripts, and formats. Think of it as a universal translator for ALL alphabets and writing systems! +A powerful web-based text transformation and steganography tool that can encode/decode text in 79+ different languages, scripts, and formats. Think of it as a universal translator for ALL alphabets and writing systems! ## โœจ Features @@ -77,7 +77,6 @@ A powerful web-based text transformation and steganography tool that can encode/ - **Vaporwave** - Aesthetic spacing - **Zalgo** - Glitch text with combining marks - **Mirror Text** - Reversed text -- **Rainbow Text** - Colorful text effects ### ๐Ÿ” **Universal Decoder** - **Smart Detection**: Automatically detects and decodes any supported format @@ -85,6 +84,16 @@ A powerful web-based text transformation and steganography tool that can encode/ - **Fallback Methods**: Tries all available decoders if primary fails - **Real-time Processing**: Instant decoding as you type +### ๐Ÿ› ๏ธ **Available Tools** +- **Universal Decoder**: Auto-detect and decode any supported format +- **Text Transforms**: 79+ encoding, cipher, and transformation options +- **Steganography**: Emoji and invisible text steganography +- **Tokenade Generator**: High-density token payload builder +- **Mutation Lab (Fuzzer)**: Generate diverse text mutations +- **Tokenizer Visualization**: Visualize tokenization for various engines +- **Message Splitter**: Split text into multiple copyable chunks +- **Gibberish Generator**: Create gibberish dictionaries and character removal variants + ### ๐Ÿ“ฑ **User Experience** - **Dark/Light Theme**: Toggle between themes - **Copy History**: Track all copied content with timestamps @@ -95,26 +104,50 @@ A powerful web-based text transformation and steganography tool that can encode/ ## ๐Ÿš€ **Getting Started** -### **Web Version** -1. Open `index.html` in any modern web browser -2. Type text in the input field -3. Choose a transformation from the categorized buttons -4. Click any transform button to apply and auto-copy -5. Use the Universal Decoder to decode any encoded text +### **Quick Start (Built Version)** +1. Run the build process (see Development Setup below) +2. Open `index.html` in any modern web browser +3. Type text in the input field +4. Choose a transformation from the categorized buttons +5. Click any transform button to apply and auto-copy +6. Use the Universal Decoder to decode any encoded text -### **Python Version** +### **Development Setup** ```bash -pip install streamlit pillow pyperclip -streamlit run parsel_app.py +# Install dependencies +npm install + +# Build all assets (required before use): +# - Builds transform bundle from source files +# - Generates emoji data +# - Injects tool templates into index.html +npm run build + +# Or build individual components: +npm run build:transforms # Bundle all transformers +npm run build:emoji # Generate emoji data +npm run build:templates # Inject tool HTML templates +npm run build:index # Generate transformer index + +# Run tests +npm test # Run universal decoder tests +npm run test:universal # Same as above +npm run test:steg # Test steganography options ``` + ## ๐Ÿ› ๏ธ **Technical Details** ### **Architecture** -- **Frontend**: Vue.js 2.6 with modern CSS -- **Backend**: Streamlit Python app (alternative) +- **Frontend**: Vue.js 2.6 with modern CSS (staying on Vue 2) +- **Tool System**: Modular tool registry with build-time template injection - **Encoding**: UTF-8 with proper Unicode handling - **Steganography**: Variation selectors and Tags Unicode block +- **Build Process**: + - Transformers are bundled from `src/transformers/` into `js/bundles/transforms-bundle.js` + - Tool templates are injected from `templates/` into `index.html` + - Emoji data is generated from Unicode specifications + - All build steps are required before the app can run ### **Browser Support** - Chrome/Edge 80+ @@ -136,7 +169,7 @@ streamlit run parsel_app.py - โœ… **Reverse Functions**: Added missing reverse functions for many transforms ### **New Features** -- ๐Ÿ†• **50+ New Languages**: Added fantasy, ancient, and technical scripts +- ๐Ÿ†• **79+ Transformations**: Added fantasy, ancient, and technical scripts - ๐Ÿ†• **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 @@ -166,57 +199,21 @@ streamlit run parsel_app.py ## ๐Ÿค **Contributing** -This project welcomes contributions! Areas for improvement: +This project welcomes contributions! See **[CONTRIBUTING.md](CONTRIBUTING.md)** for detailed guidelines. +**Quick Start:** +- **Adding a transformer?** See `src/transformers/` directory structure +- **Adding a new tool/feature?** See `CONTRIBUTING.md` โ†’ "Adding a New Tool" +- **Adding utilities?** See `CONTRIBUTING.md` โ†’ "Adding a New Utility Function" +- **Editing tool templates?** See `templates/README.md` + +**Areas for improvement:** - **New Languages**: Add more fictional or historical scripts - **Better Decoding**: Improve universal decoder accuracy - **Performance**: Optimize for very long texts - **Mobile**: Enhance mobile experience - **Accessibility**: Improve screen reader support -### ๐Ÿงฉ How to add a new transform - -1) Define the transform in `js/transforms.js` inside the `transforms` object: - -```js -new_transform_key: { - name: 'Human Friendly Name', - // Optional: map for character โ†” character transforms - map: { /* 'a': 'ฮฑ', ... */ }, - // Required: encoding function - func: function(text) { /* return transformed */ }, - // Optional but recommended: short, readable preview - preview: function(text) { return this.func((text||'').slice(0, 3)) + '...'; }, - // Optional: reverse/decoder (enables universal decoder to use it directly) - reverse: function(text) { /* return decoded */ } -} -``` - -2) Add it to a category in `js/app.js` under `transformCategories` so it shows in the UI, e.g.: - -```js -transformCategories: { - cipher: ['Caesar Cipher', 'ROT13', 'Your New Transform'] -} -``` - -3) If your transform uses a custom script or style (not simple ASCII substitutions), ensure the universal decoder can detect it. Add pattern detection or reverse mapping in `universalDecode` in `js/app.js`: - -```js -// Example: add to a check list -const customChecks = [{ name: 'Your New Transform', transform: 'your_key' }]; -// build reverse map and try decoding if the input contains your characters -``` - -4) If you want it considered by the Randomizer, add its key to `getRandomizableTransforms()` in `js/transforms.js`. - -5) Test it in `test_transforms.html`. Add a button and a simple test harness calling `testTransform('your_key')`. - -Tips: -- Keep `preview()` short to avoid UI overflow. -- Prefer providing `reverse()` so the universal decoder can decode it directly. -- Unicode-heavy styles should provide a reverse map for accurate decoding. - ## ๐Ÿ“„ **License** This project is open source. See LICENSE file for details. diff --git a/build/README.md b/build/README.md new file mode 100644 index 0000000..660578d --- /dev/null +++ b/build/README.md @@ -0,0 +1,66 @@ +# Build Scripts + +## Scripts + +### `build-transforms.js` +Bundles all transformers from `src/transformers/` into `js/bundles/transforms-bundle.js` + +- Automatically creates the `js/bundles/` directory if it doesn't exist +- Discovers all transformers from category directories +- Generates a single bundled file for browser use + +```bash +npm run build:transforms +``` + +### `build-emoji-data.js` +Fetches Unicode emoji data and generates `js/data/emojiData.js` + +- Automatically creates the `js/data/` directory if it doesn't exist +- Uses cached data if available (7-day cache) +- Merges keywords from `src/emojiWordMap.js` + +```bash +npm run build:emoji +``` + +### `inject-tool-scripts.js` +Auto-discovers tools in `js/tools/` and: +- Generates script tags in `index.template.html` +- Generates auto-registration code in `js/core/toolRegistry.js` + +```bash +npm run build:tools +``` + +### `inject-tool-templates.js` +Injects tool templates from `templates/` into `index.html` + +```bash +npm run build:templates +``` + +### `build-index.js` +Generates transformer index + +```bash +npm run build:index +``` + +## Build Pipeline + +```bash +npm run build # Runs all scripts in order: +# 1. build:index +# 2. build:transforms +# 3. build:emoji +# 4. build:tools +# 5. build:templates +``` + +## Development Workflow + +- **Edit transformers** โ†’ `npm run build:transforms` +- **Add new tool** โ†’ `npm run build:tools` +- **Edit templates** โ†’ `npm run build:templates` +- **Full rebuild** โ†’ `npm run build` diff --git a/build/build-emoji-data.js b/build/build-emoji-data.js new file mode 100644 index 0000000..bb5b14f --- /dev/null +++ b/build/build-emoji-data.js @@ -0,0 +1,491 @@ +#!/usr/bin/env node + +/** + * Build Emoji Data from Official Unicode Source + * Fetches emoji-test.txt from Unicode.org and generates emojiData.js + */ + +const https = require('https'); +const fs = require('fs'); +const path = require('path'); + +// Unicode emoji test file (always uses latest version - compatibility testing handles older devices) +// URL automatically redirects to newest Unicode emoji release +const EMOJI_DATA_URL = 'https://www.unicode.org/Public/emoji/latest/emoji-test.txt'; +const CACHE_DIR = path.join(__dirname, '..', '.cache'); +const CACHE_FILE = path.join(CACHE_DIR, 'emoji-test.txt'); +const CACHE_MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds + +// Check for --force flag to bypass cache +const FORCE_DOWNLOAD = process.argv.includes('--force') || process.argv.includes('-f'); + +const startTime = Date.now(); + +/** + * Check if cached file exists and is recent enough + */ +function shouldUseCache() { + if (FORCE_DOWNLOAD) { + console.log('๐Ÿ”„ Force download requested, bypassing cache...'); + return false; + } + + if (!fs.existsSync(CACHE_FILE)) { + return false; + } + + const stats = fs.statSync(CACHE_FILE); + const age = Date.now() - stats.mtimeMs; + + if (age > CACHE_MAX_AGE) { + console.log(`โฐ Cache is ${Math.floor(age / (24 * 60 * 60 * 1000))} days old, will refresh...`); + return false; + } + + return true; +} + +/** + * Download emoji data from Unicode.org + */ +function downloadEmojiData(callback) { + console.log('๐Ÿ“ฅ Downloading emoji data from Unicode.org...'); + console.log(` Source: ${EMOJI_DATA_URL}`); + + https.get(EMOJI_DATA_URL, (response) => { + let data = ''; + let downloadedBytes = 0; + const totalBytes = parseInt(response.headers['content-length'] || '0', 10); + + response.on('data', (chunk) => { + data += chunk; + downloadedBytes += chunk.length; + + // Show progress if we know the total size + if (totalBytes > 0) { + const percent = ((downloadedBytes / totalBytes) * 100).toFixed(1); + process.stdout.write(`\r Progress: ${percent}% (${(downloadedBytes / 1024).toFixed(0)} KB)`); + } + }); + + response.on('end', () => { + const downloadTime = ((Date.now() - startTime) / 1000).toFixed(2); + console.log(`\nโœ… Downloaded ${(data.length / 1024).toFixed(2)} KB in ${downloadTime}s`); + + // Save to cache + if (!fs.existsSync(CACHE_DIR)) { + fs.mkdirSync(CACHE_DIR, { recursive: true }); + } + fs.writeFileSync(CACHE_FILE, data, 'utf8'); + console.log(`๐Ÿ’พ Cached to ${CACHE_FILE}`); + + callback(data, downloadTime); + }); + }).on('error', (err) => { + console.error('โŒ Error fetching emoji data:', err.message); + process.exit(1); + }); +} + +/** + * Load emoji data from cache or download if needed + */ +function loadEmojiData() { + if (shouldUseCache()) { + console.log('๐Ÿ“‚ Using cached emoji data...'); + const stats = fs.statSync(CACHE_FILE); + const age = Math.floor((Date.now() - stats.mtimeMs) / (60 * 60 * 1000)); + console.log(` Cache age: ${age} hours`); + + const data = fs.readFileSync(CACHE_FILE, 'utf8'); + const loadTime = ((Date.now() - startTime) / 1000).toFixed(2); + console.log(`โœ… Loaded ${(data.length / 1024).toFixed(2)} KB from cache in ${loadTime}s`); + + processEmojiData(data, '0.00'); + } else { + downloadEmojiData((data, downloadTime) => { + processEmojiData(data, downloadTime); + }); + } +} + +/** + * Process emoji data (parse and generate) + */ +function processEmojiData(data, downloadTime) { + // Parse the emoji data + console.log('๐Ÿ”จ Parsing emoji data...'); + const parseStart = Date.now(); + const emojiData = parseEmojiTestFile(data); + const parseTime = ((Date.now() - parseStart) / 1000).toFixed(2); + console.log(`โœ… Parsed ${Object.keys(emojiData).length} emojis in ${parseTime}s`); + + // Generate JavaScript file + console.log('๐Ÿ“ Generating emojiData.js...'); + const genStart = Date.now(); + generateEmojiDataFile(emojiData); + const genTime = ((Date.now() - genStart) / 1000).toFixed(2); + + const totalTime = ((Date.now() - startTime) / 1000).toFixed(2); + console.log(`\nโฑ๏ธ Total time: ${totalTime}s (download: ${downloadTime}s, parse: ${parseTime}s, generate: ${genTime}s)`); +} + +// Start loading emoji data +loadEmojiData(); + +/** + * Check if an emoji has complex modifiers (skin tones, ZWJ sequences, etc.) + * Currently disabled - we want to use the full Unicode 15.1 set + */ +function hasComplexModifiers(emoji, name) { + // Mark all emojis as simple (no filtering) + return false; +} + +/** + * Parse the emoji-test.txt file format + * Format: ; # + */ +function parseEmojiTestFile(content) { + const lines = content.split('\n'); + const emojis = {}; + let currentGroup = ''; + let currentSubgroup = ''; + + for (const line of lines) { + // Parse group headers + if (line.startsWith('# group:')) { + currentGroup = line.replace('# group:', '').trim(); + continue; + } + + // Parse subgroup headers + if (line.startsWith('# subgroup:')) { + currentSubgroup = line.replace('# subgroup:', '').trim(); + continue; + } + + // Skip comments and empty lines + if (line.startsWith('#') || !line.trim() || !line.includes(';')) { + continue; + } + + // Parse emoji line + // Format: 1F600 ; fully-qualified # ๐Ÿ˜€ E1.0 grinning face + // Or: 1F64D 1F3FD 200D 2642 FE0F ; fully-qualified # ๐Ÿ™๐Ÿฝโ€โ™‚๏ธ E2.0 man frowning: medium skin tone + + // Extract codepoints from the left side (more reliable than character representation) + const codepointMatch = line.match(/^([0-9A-Fa-f\s]+)\s*;\s*(fully-qualified|minimally-qualified|unqualified)/); + let emoji = null; + + if (codepointMatch) { + // Reconstruct emoji from codepoints to avoid corruption issues + const codepoints = codepointMatch[1].trim().split(/\s+/) + .map(cp => parseInt(cp, 16)) + .filter(cp => !isNaN(cp)); + + if (codepoints.length > 0) { + // Convert codepoints to emoji string + emoji = String.fromCodePoint(...codepoints); + } + } + + // Fallback: extract from character representation if codepoint parsing fails + if (!emoji) { + const parts = line.split('#'); + if (parts.length < 2) continue; + + const emojiPart = parts[1].trim(); + const match = emojiPart.match(/^(.+?)\s+E\d+\.\d+\s+(.+)$/); + + if (match) { + emoji = match[1].trim(); + } else { + continue; + } + } + + // Extract name from the line + const nameMatch = line.match(/#\s+.+?\s+E\d+\.\d+\s+(.+)$/); + const name = nameMatch ? nameMatch[1].trim() : ''; + + if (emoji && name) { + + // Only include fully-qualified emojis + if (line.includes('fully-qualified')) { + // Filter out overly complex sequences for better UX + const isSimple = !hasComplexModifiers(emoji, name); + + emojis[emoji] = { + official: name, + group: currentGroup, + subgroup: currentSubgroup, + keywords: generateKeywords(name, currentGroup, currentSubgroup), + isSimple: isSimple + }; + } + } + } + + return emojis; +} + +/** + * Generate keywords from the official emoji name + */ +function generateKeywords(name, group, subgroup) { + const keywords = new Set(); + + // Add words from the official name + const nameWords = name.toLowerCase() + .replace(/[()]/g, '') + .split(/[\s-]+/) + .filter(word => word.length > 2 && !['with', 'and', 'the'].includes(word)); + + nameWords.forEach(word => keywords.add(word)); + + // Add group/subgroup as keywords + if (group) { + const groupWords = group.toLowerCase().split(/[\s&-]+/); + groupWords.forEach(word => { + if (word.length > 3) keywords.add(word); + }); + } + + // Special keyword mappings for common words + const keywordMap = { + 'grinning': ['smile', 'happy', 'grin'], + 'tears of joy': ['laugh', 'lol', 'funny'], + 'heart': ['love', 'like'], + 'thumbs up': ['good', 'yes', 'approve', 'like'], + 'thumbs down': ['bad', 'no', 'disapprove'], + 'waving': ['hello', 'hi', 'bye', 'wave'], + 'clapping': ['applause', 'clap', 'praise'], + 'folded': ['pray', 'thanks', 'please'], + 'fire': ['hot', 'lit', 'flame'], + 'crying': ['sad', 'tear', 'cry'], + 'skull': ['dead', 'death'], + 'poop': ['shit', 'crap', 'poo'], + 'hundred': ['100', 'perfect'], + 'collision': ['boom', 'bang', 'explosion'], + 'dog': ['puppy', 'pet'], + 'cat': ['kitty', 'pet'], + 'sun': ['sunny', 'day'], + 'moon': ['night'], + 'star': ['favorite'], + 'rainbow': ['pride', 'colorful'] + }; + + // Add mapped keywords + for (const [trigger, extras] of Object.entries(keywordMap)) { + if (name.toLowerCase().includes(trigger)) { + extras.forEach(k => keywords.add(k)); + } + } + + return Array.from(keywords); +} + +/** + * Map Unicode groups to category IDs (using official Unicode categories) + * Split "People & Body" into subcategories for better organization + */ +function mapGroupToCategory(group, subgroup) { + const groupMap = { + 'Smileys & Emotion': 'smileys_emotion', + 'Animals & Nature': 'animals_nature', + 'Food & Drink': 'food_drink', + 'Travel & Places': 'travel_places', + 'Activities': 'activities', + 'Objects': 'objects', + 'Symbols': 'symbols', + 'Flags': 'flags' + }; + + // Special handling for People & Body - split into subcategories + if (group === 'People & Body') { + // Hands and gestures + if (subgroup.startsWith('hand-') || subgroup === 'hands' || subgroup === 'hand-prop') { + return 'people_hands'; + } + // Body parts + if (subgroup === 'body-parts') { + return 'people_body_parts'; + } + // People (person-*, person, family) + if (subgroup.startsWith('person-') || subgroup === 'person' || subgroup === 'family' || subgroup === 'person-symbol') { + return 'people_persons'; + } + // Default to people_body if subgroup doesn't match + return 'people_body'; + } + + return groupMap[group] || 'symbols'; +} + +/** + * Load keyword mappings from emojiWordMap.js + */ +function loadEmojiWordMap() { + const wordMapPath = path.join(__dirname, '..', 'src', 'emojiWordMap.js'); + + if (!fs.existsSync(wordMapPath)) { + console.log('โš ๏ธ emojiWordMap.js not found, skipping keyword merge'); + return {}; + } + + try { + const code = fs.readFileSync(wordMapPath, 'utf8'); + + // Use vm to safely execute the file and extract emojiKeywords + const vm = require('vm'); + const sandbox = { + window: {}, + console: console // Allow console in case the file uses it + }; + vm.createContext(sandbox); + + // Execute the entire file in the sandbox + vm.runInContext(code, sandbox); + + const keywordMap = sandbox.window.emojiKeywords || {}; + console.log(`๐Ÿ“š Loaded ${Object.keys(keywordMap).length} keyword mappings from emojiWordMap.js`); + + return keywordMap; + } catch (error) { + console.log(`โš ๏ธ Error loading emojiWordMap.js: ${error.message}, skipping keyword merge`); + return {}; + } +} + +/** + * Merge keywords from wordMap into emojiData keywords + */ +function mergeKeywords(baseKeywords, wordMapKeywords) { + const merged = new Set(baseKeywords); + + // Add all keywords from wordMap + if (Array.isArray(wordMapKeywords)) { + wordMapKeywords.forEach(kw => merged.add(kw.toLowerCase())); + } + + return Array.from(merged).sort(); +} + +/** + * Generate the emojiData.js file + */ +function generateEmojiDataFile(emojiData) { + const outputPath = path.join(__dirname, '..', 'js', 'data', 'emojiData.js'); + + // Ensure data directory exists + const dataDir = path.dirname(outputPath); + if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }); + } + + // Load keyword mappings from emojiWordMap.js + console.log('๐Ÿ“š Loading keyword mappings from emojiWordMap.js...'); + const wordMap = loadEmojiWordMap(); + + let output = `// Unified Emoji Data for P4RS3LT0NGV3 +// Generated from Unicode Official Emoji Data (latest version with compatibility testing) +// Keywords merged from emojiWordMap.js for enhanced searchability +// Source: ${EMOJI_DATA_URL} +// Generated: ${new Date().toISOString()} + +window.emojiData = { +`; + + let mergedCount = 0; + + // Add each emoji + for (const [emoji, data] of Object.entries(emojiData)) { + const category = mapGroupToCategory(data.group, data.subgroup); + + // Merge keywords from wordMap if available + let finalKeywords = data.keywords; + if (wordMap[emoji]) { + finalKeywords = mergeKeywords(data.keywords, wordMap[emoji]); + mergedCount++; + } + + const keywordsStr = JSON.stringify(finalKeywords); + const isSimple = data.isSimple ? 'true' : 'false'; + + output += ` '${emoji}': { official: '${data.official.replace(/'/g, "\\'")}', keywords: ${keywordsStr}, category: '${category}', isSimple: ${isSimple} },\n`; + } + + if (mergedCount > 0) { + console.log(`โœ… Merged keywords for ${mergedCount} emojis from emojiWordMap.js`); + } + + output += `}; + +// Helper to get all emojis by category (optionally filter to simple emojis only) +window.emojiData.getByCategory = function(categoryId, simpleOnly = false) { + let emojis = categoryId === 'all' + ? Object.keys(window.emojiData).filter(key => typeof window.emojiData[key] === 'object') + : Object.entries(window.emojiData) + .filter(([emoji, data]) => typeof data === 'object' && data.category === categoryId) + .map(([emoji]) => emoji); + + // Filter to simple emojis if requested (better for UI display) + if (simpleOnly) { + emojis = emojis.filter(emoji => window.emojiData[emoji]?.isSimple); + } + + return emojis; +}; + +// Helper to search emojis by keyword +window.emojiData.searchByKeyword = function(keyword) { + const lowerKeyword = keyword.toLowerCase(); + return Object.entries(window.emojiData) + .filter(([emoji, data]) => + typeof data === 'object' && ( + data.official.toLowerCase().includes(lowerKeyword) || + data.keywords.some(kw => kw.toLowerCase().includes(lowerKeyword)) + ) + ) + .map(([emoji]) => emoji); +}; + +// Helper to get emoji by keyword (for encoding) +window.emojiData.getEmojiForWord = function(word) { + const lowerWord = word.toLowerCase(); + const matches = Object.entries(window.emojiData) + .filter(([emoji, data]) => + typeof data === 'object' && data.keywords.includes(lowerWord) + ) + .map(([emoji]) => emoji); + + // Return random match if multiple found + return matches.length > 0 ? matches[Math.floor(Math.random() * matches.length)] : null; +}; + +// Categories for UI (official Unicode 15.1 categories, with People & Body split) +window.emojiData.categories = [ + { id: 'all', name: 'All Emojis', icon: '๐Ÿ”' }, + { id: 'smileys_emotion', name: 'Smileys & Emotion', icon: '๐Ÿ˜€' }, + { id: 'people_hands', name: 'Hands & Gestures', icon: '๐Ÿ‘‹' }, + { id: 'people_persons', name: 'People', icon: '๐Ÿ‘ค' }, + { id: 'people_body_parts', name: 'Body Parts', icon: '๐Ÿฆต' }, + { id: 'animals_nature', name: 'Animals & Nature', icon: '๐Ÿถ' }, + { id: 'food_drink', name: 'Food & Drink', icon: '๐Ÿ•' }, + { id: 'travel_places', name: 'Travel & Places', icon: 'โœˆ๏ธ' }, + { id: 'activities', name: 'Activities', icon: 'โšฝ' }, + { id: 'objects', name: 'Objects', icon: '๐Ÿ’ก' }, + { id: 'symbols', name: 'Symbols', icon: 'โค๏ธ' }, + { id: 'flags', name: 'Flags', icon: '๐Ÿ' } +]; +`; + + // Write the file + fs.writeFileSync(outputPath, output, 'utf8'); + + const emojiCount = Object.keys(emojiData).length; + const fileSize = (output.length / 1024).toFixed(2); + + console.log(`โœ… Generated ${emojiCount} emojis โ†’ ${fileSize} KB`); +} diff --git a/build/build-index.js b/build/build-index.js new file mode 100644 index 0000000..f916f66 --- /dev/null +++ b/build/build-index.js @@ -0,0 +1,71 @@ +#!/usr/bin/env node + +/** + * Build Script for Transformers Index + * Dynamically generates index.js with all transformer imports + */ + +const fs = require('fs'); +const path = require('path'); + +const transformersDir = path.join(__dirname, '..', 'src', 'transformers'); +const outputPath = path.join(transformersDir, 'index.js'); + +// Files to skip +const skipFiles = ['BaseTransformer.js', 'index.js', 'loader.js', 'loader-node.js', 'README.md']; + +// Get all category directories +const categoryDirs = fs.readdirSync(transformersDir, { withFileTypes: true }) + .filter(dirent => dirent.isDirectory()) + .map(dirent => dirent.name) + .sort(); + +let imports = []; +let transformNames = []; + +// Discover transforms from each category directory +for (const categoryDir of categoryDirs) { + const categoryPath = path.join(transformersDir, categoryDir); + const files = fs.readdirSync(categoryPath) + .filter(file => file.endsWith('.js') && !skipFiles.includes(file)) + .sort(); + + for (const file of files) { + // Convert filename to transform name (kebab-case to snake_case) + const transformName = file.replace('.js', '').replace(/-/g, '_'); + const filePath = `./${categoryDir}/${file}`; + + imports.push(`import ${transformName} from '${filePath}';`); + transformNames.push(transformName); + } +} + +// Generate the index.js content +const output = `// Transformers Index - Auto-generated +// This file is automatically generated by build/build-index.js +// Do not edit manually - run 'npm run build:index' to regenerate + +${imports.join('\n')} + +// Combine all transforms +const transforms = { +${transformNames.map(name => ` ${name}`).join(',\n')} +}; + +// Export for both ES6 modules and browser global +export default transforms; + +// Also expose as window.transforms for backward compatibility +if (typeof window !== 'undefined') { + window.transforms = transforms; + window.encoders = transforms; // alias +} +`; + +// Write the file +fs.writeFileSync(outputPath, output, 'utf8'); + +console.log(`โœจ Generated: ${outputPath}`); +console.log(`๐Ÿ“ฆ Total transforms: ${transformNames.length}`); +console.log(`๐Ÿ“ Categories: ${categoryDirs.join(', ')}`); + diff --git a/build/build-transforms.js b/build/build-transforms.js new file mode 100755 index 0000000..f1d4c5a --- /dev/null +++ b/build/build-transforms.js @@ -0,0 +1,135 @@ +#!/usr/bin/env node + +/** + * Build Script for Transforms + * Dynamically discovers and bundles all transformers from the directory structure + */ + +const fs = require('fs'); +const path = require('path'); + +// First, read the BaseTransformer class +const baseTransformerPath = path.join(__dirname, '..', 'src', 'transformers', 'BaseTransformer.js'); +const baseTransformerContent = fs.readFileSync(baseTransformerPath, 'utf8') + .replace(/export\s+(default\s+)?/g, ''); // Remove export/export default statements + +// Discover all transformers dynamically +const transformersDir = path.join(__dirname, '..', 'src', 'transformers'); +const transforms = {}; + +// Files to skip +const skipFiles = ['BaseTransformer.js', 'index.js', 'loader.js', 'loader-node.js', 'README.md']; + +// Get all category directories +const categoryDirs = fs.readdirSync(transformersDir, { withFileTypes: true }) + .filter(dirent => dirent.isDirectory()) + .map(dirent => dirent.name) + .sort(); + +// Discover transforms from each category directory +for (const categoryDir of categoryDirs) { + const categoryPath = path.join(transformersDir, categoryDir); + const files = fs.readdirSync(categoryPath) + .filter(file => file.endsWith('.js') && !skipFiles.includes(file)); + + for (const file of files) { + // Convert filename to transform name (kebab-case to snake_case) + // e.g., "upside-down.js" -> "upside_down", "base64.js" -> "base64" + const transformName = file.replace('.js', '').replace(/-/g, '_'); + const filePath = `${categoryDir}/${file}`; + transforms[transformName] = filePath; + } +} + +// Start building the output +let output = `/** + * P4RS3LT0NGV3 Transforms - Bundled for Browser + * Auto-generated from modular source files + * Build date: ${new Date().toISOString()} + * Total transforms: ${Object.keys(transforms).length} + */ + +(function() { +'use strict'; + +// BaseTransformer class +${baseTransformerContent} + +const transforms = {}; + +`; + +// Load and bundle each transform +for (const [name, filePath] of Object.entries(transforms)) { + const fullPath = path.join(transformersDir, filePath); + + try { + let content = fs.readFileSync(fullPath, 'utf8'); + + // Extract category from directory path + const categoryDir = path.dirname(filePath); + let category = categoryDir === '.' ? 'special' : categoryDir; + + // Special case: randomizer.js should have category 'randomizer' for UI handling + if (name === 'randomizer') { + category = 'randomizer'; + } + + // Automatically set/update category based on directory + // Pattern: category: '...' or category: "..." + const categoryPattern = /category\s*:\s*['"]([^'"]+)['"]/; + + if (categoryPattern.test(content)) { + // Replace existing category + content = content.replace(categoryPattern, `category: '${category}'`); + } else { + // Inject category after name or priority property + // Look for the object opening and find a good place to inject + const injectPattern = /(name\s*:\s*['"][^'"]+['"],?\s*)/; + if (injectPattern.test(content)) { + content = content.replace(injectPattern, `$1 category: '${category}',\n `); + } else { + // Fallback: inject after the opening brace of BaseTransformer + content = content.replace(/(new BaseTransformer\(\{)/, `$1\n category: '${category}',`); + } + } + + // Extract the object definition (remove comments, import, and export statements) + const cleanContent = content + .replace(/^\/\/.*$/gm, '') // Remove single-line comments + .replace(/import\s+.*?from\s+['"].*?['"]\s*;?\s*/g, '') // Remove import statements + .replace(/export default\s*/g, '') // Remove export statement + .trim(); + + output += `// ${name} (from ${filePath})\n`; + output += `transforms['${name}'] = ${cleanContent}\n\n`; + + console.log(`โœ… Bundled: ${name} (category: ${category})`); + } catch (error) { + console.error(`โŒ Error bundling ${name}:`, error.message); + } +} + +// Close the IIFE and expose to window +output += ` +// Expose to window +window.transforms = transforms; +window.encoders = transforms; // Alias for compatibility + +})(); +`; + +// Write the bundled file +const outputPath = path.join(__dirname, '..', 'js', 'bundles', 'transforms-bundle.js'); +const outputDir = path.dirname(outputPath); + +// Ensure the directory exists +if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); +} + +fs.writeFileSync(outputPath, output, 'utf8'); + +console.log(`\nโœจ Bundle created: ${outputPath}`); +console.log(`๐Ÿ“ฆ Size: ${(output.length / 1024).toFixed(2)} KB`); +console.log(`๐Ÿ”ข Total transforms: ${Object.keys(transforms).length}`); diff --git a/build/inject-tool-scripts.js b/build/inject-tool-scripts.js new file mode 100644 index 0000000..f97a244 --- /dev/null +++ b/build/inject-tool-scripts.js @@ -0,0 +1,92 @@ +#!/usr/bin/env node +/** + * Auto-discover and inject tool script tags into index.template.html + * Also generates auto-registration code for toolRegistry.js + */ + +const fs = require('fs'); +const path = require('path'); + +console.log('๐Ÿ”ง Auto-discovering tools and generating script tags...\n'); + +const toolsDir = path.join(__dirname, '../js/tools'); +const templatePath = path.join(__dirname, '../index.template.html'); +const registryPath = path.join(__dirname, '../js/core/toolRegistry.js'); + +// Discover all tool files (excluding Tool.js base class) +const toolFiles = fs.readdirSync(toolsDir) + .filter(file => file.endsWith('Tool.js') && file !== 'Tool.js') + .sort(); // Sort for consistent ordering + +console.log(`๐Ÿ“ฆ Found ${toolFiles.length} tools:`); +toolFiles.forEach(file => console.log(` - ${file}`)); + +// Generate script tags +const scriptTags = toolFiles.map(file => + ` ` +).join('\n'); + +// Read index.template.html +let templateContent = fs.readFileSync(templatePath, 'utf8'); + +// Find the tool scripts section (between Tool.js and toolRegistry.js) +const toolJsMarker = ''; +const registryMarker = ''; + +const toolJsIndex = templateContent.indexOf(toolJsMarker); +const registryIndex = templateContent.indexOf(registryMarker); + +if (toolJsIndex === -1 || registryIndex === -1) { + console.error('\nโŒ Could not find tool script markers in index.template.html'); + process.exit(1); +} + +// Extract the section between Tool.js and toolRegistry.js +const before = templateContent.substring(0, toolJsIndex + toolJsMarker.length); +const after = templateContent.substring(registryIndex); + +// Replace with dynamic script tags +const newTemplateContent = before + '\n' + scriptTags + '\n' + after; + +// Write updated template +fs.writeFileSync(templatePath, newTemplateContent, 'utf8'); +console.log('\nโœ… Updated index.template.html with dynamic tool script tags'); + +// Generate auto-registration code +const registrationCode = toolFiles.map(file => { + // Extract class name from filename (e.g., "TransformTool.js" -> "TransformTool") + const className = file.replace('.js', ''); + return `if (typeof ${className} !== 'undefined') { + window.toolRegistry.register(new ${className}()); +}`; +}).join('\n'); + +// Read toolRegistry.js +let registryContent = fs.readFileSync(registryPath, 'utf8'); + +// Find and replace the manual registration section +const autoRegisterStart = '// Auto-register tools if they\'re available'; +const autoRegisterEnd = '// Export for module systems'; + +const startIndex = registryContent.indexOf(autoRegisterStart); +const endIndex = registryContent.indexOf(autoRegisterEnd); + +if (startIndex === -1 || endIndex === -1) { + console.error('\nโŒ Could not find auto-registration section in toolRegistry.js'); + process.exit(1); +} + +// Replace with dynamic registration +const beforeRegistry = registryContent.substring(0, startIndex); +const afterRegistry = registryContent.substring(endIndex); + +const newRegistryContent = beforeRegistry + + autoRegisterStart + '\n' + + registrationCode + '\n\n' + + afterRegistry; + +// Write updated registry +fs.writeFileSync(registryPath, newRegistryContent, 'utf8'); +console.log('โœ… Updated toolRegistry.js with dynamic tool registration'); +console.log(`\nโœจ ${toolFiles.length} tools auto-discovered and registered!\n`); + diff --git a/build/inject-tool-templates.js b/build/inject-tool-templates.js new file mode 100644 index 0000000..4c2b1de --- /dev/null +++ b/build/inject-tool-templates.js @@ -0,0 +1,85 @@ +#!/usr/bin/env node +/** + * Inject tool templates into index.html + * Reads HTML from separate template files and injects them into the main template + */ + +const fs = require('fs'); +const path = require('path'); + +console.log('๐Ÿ“ Injecting tool templates into index.html...\n'); + +// Template files in order +const templateFiles = [ + 'decoder.html', + 'steganography.html', + 'transforms.html', + 'tokenade.html', + 'fuzzer.html', + 'tokenizer.html', + 'splitter.html', + 'gibberish.html' +]; + +const templatesDir = path.join(__dirname, '../templates'); +let allToolHTML = ''; + +// Read each template file +templateFiles.forEach(templateFile => { + const templatePath = path.join(templatesDir, templateFile); + + if (!fs.existsSync(templatePath)) { + console.log(`โš ๏ธ Warning: ${templateFile} not found`); + return; + } + + const html = fs.readFileSync(templatePath, 'utf8'); + console.log(`โœ… Loaded: ${templateFile} (${(html.length / 1024).toFixed(2)}KB)`); + allToolHTML += html + '\n\n'; +}); + +// Read index.template.html (base template) +const templatePath = path.join(__dirname, '../index.template.html'); +const indexPath = path.join(__dirname, '../index.html'); + +if (!fs.existsSync(templatePath)) { + console.error('\nโŒ index.template.html not found!'); + process.exit(1); +} + +let indexContent = fs.readFileSync(templatePath, 'utf8'); + +// Find the tool-content-container +const startMarker = '
'; +const endMarker = '
\n\n \n\n '; + +const startIndex = indexContent.indexOf(startMarker); +const endIndex = indexContent.indexOf(endMarker); + +if (startIndex === -1 || endIndex === -1) { + console.error('\nโŒ Could not find tool content container markers'); + process.exit(1); +} + +// Build the replacement content +const before = indexContent.substring(0, startIndex + startMarker.length); +const after = indexContent.substring(endIndex); + +const replacement = ` + +${allToolHTML} `; + +const newContent = before + replacement + after; + +// Calculate size changes +const oldSize = indexContent.length; +const newSize = newContent.length; +const sizeDiff = newSize - oldSize; + +// Write back +fs.writeFileSync(indexPath, newContent, 'utf8'); + +console.log('\nโœจ Tool templates injected into index.html'); +console.log(`๐Ÿ“ฆ index.html: ${(newSize / 1024).toFixed(2)}KB ${sizeDiff > 0 ? '+' : ''}${(sizeDiff / 1024).toFixed(2)}KB`); +console.log(`๐Ÿ”ง ${templateFiles.length} templates injected\n`); + diff --git a/css/style.css b/css/style.css index 9c56f9b..2b19c25 100644 --- a/css/style.css +++ b/css/style.css @@ -2,9 +2,9 @@ .danger-modal-backdrop.danger-active { display: none; } .danger-modal { display: none; } @keyframes danger-pop { to { transform: scale(1); } } -.danger-actions { display:flex; gap:8px; justify-content:flex-end; padding: 12px 16px; border-top:1px solid #1b5e20; } -.danger-cancel { background: var(--button-bg); border:1px solid var(--input-border); padding:8px 12px; border-radius:6px; cursor:pointer; } -.danger-proceed { background:#1b5e20; border:1px solid #2e7d32; color:#69f0ae; padding:8px 12px; border-radius:6px; cursor:pointer; box-shadow: 0 0 12px rgba(105,240,174,.35) inset, 0 0 16px rgba(105,240,174,.2); } +.danger-actions { display:flex; gap:var(--spacing-sm); justify-content:flex-end; padding: var(--spacing-sm) var(--spacing-md); border-top:1px solid var(--danger-color); } +.danger-cancel { background: var(--button-bg); border:1px solid var(--input-border); padding:var(--spacing-sm) var(--spacing-sm); border-radius:6px; cursor:pointer; } +.danger-proceed { background:var(--danger-color); border:1px solid var(--danger-border); color:var(--danger-text); padding:var(--spacing-sm) var(--spacing-sm); border-radius:6px; cursor:pointer; box-shadow: 0 0 12px rgba(var(--danger-text-rgb),.35) inset, 0 0 16px rgba(var(--danger-text-rgb),.2); } /* Modern dark theme styling */ :root { --main-bg-color: #1a1a1a; @@ -25,15 +25,57 @@ /* Transform category colors */ --encoding-color: #7e57c2; /* Purple for encoding/decoding */ + --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 */ + --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; + + /* Active state gradient end colors */ + --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; + --technical-active-end: #26c6da; + --randomizer-active-end: #ab47bc; + --case-active-end: #b39ddb; + + /* Spacing scale */ + --spacing-xs: 4px; + --spacing-sm: 8px; + --spacing-md: 16px; + --spacing-lg: 24px; + --spacing-xl: 32px; + + /* Danger/error colors */ + --danger-color: #1b5e20; + --danger-border: #2e7d32; + --danger-text: #69f0ae; + --danger-text-rgb: 105, 240, 174; + + /* Logo glitch color */ + --glitch-color: #00ff00; + --glitch-color-rgb: 0, 255, 0; } * { @@ -55,13 +97,426 @@ body { font-feature-settings: "liga" 1, "calt" 1; /* Enable ligatures */ } +/* ======================================== + STANDARD UI COMPONENT TEMPLATES + + Reusable components for consistent design + across the entire application. + ======================================== */ + +/* Section Header with Icon and Description + Usage: +
+
+ +

Title Here

+
+

+ Description text that explains what this section does. +

+
+*/ +.section-header-card { + display: flex; + align-items: flex-start; + gap: 16px; + padding: 12px 16px; + background: var(--secondary-bg); + border: 1px solid var(--input-border); + border-radius: 8px; + margin-bottom: 16px; +} + +.section-header-card-title { + display: flex; + align-items: center; + gap: 10px; + min-width: fit-content; + flex-shrink: 0; +} + +.section-header-card-title i { + font-size: 1.3rem; + color: var(--accent-color); +} + +.section-header-card-title h3, +.section-header-card-title h4 { + margin: 0; + color: var(--accent-color); + font-size: 1.1rem; + white-space: nowrap; +} + +.section-header-card-description { + margin: 0; + color: var(--text-muted); + font-size: 0.9rem; + line-height: 1.5; + padding-top: 2px; +} + +/* Responsive stacking for section header cards */ + +/* Icon + Title Inline (simpler variant) + Usage: +
+ +

Title

+ optional subtitle +
+*/ +.title-with-icon { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + margin-bottom: 12px; +} + +.title-with-icon i { + color: var(--accent-color); + font-size: 1.2rem; +} + +.title-with-icon h3, +.title-with-icon h4 { + margin: 0; + color: var(--accent-color); +} + +.title-with-icon small { + font-size: 0.75em; + font-weight: normal; + color: var(--text-muted); + background-color: rgba(100, 181, 246, 0.1); + padding: 2px 8px; + border-radius: 4px; +} + +/* Info Box - For disclaimers, tips, warnings + Usage: +
+ + Your message here +
+ + Add class modifiers: .info-box-warning, .info-box-success, .info-box-danger +*/ +.info-box { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 10px 12px; + border: 1px solid var(--input-border); + background: rgba(100, 181, 246, 0.1); + border-left: 3px solid var(--accent-color); + border-radius: 6px; + margin: 12px 0; + font-size: 0.9rem; + line-height: 1.4; +} + +.info-box i { + color: var(--accent-color); + margin-top: 2px; + flex-shrink: 0; +} + +.info-box-warning { + background: rgba(255, 183, 77, 0.1); + border-left-color: var(--format-color); +} + +.info-box-warning i { + color: var(--format-color); +} + +.info-box-success { + background: rgba(102, 187, 106, 0.1); + border-left-color: var(--special-color); +} + +.info-box-success i { + color: var(--special-color); +} + +.info-box-danger { + background: rgba(239, 83, 80, 0.1); + border-left-color: var(--visual-color); +} + +.info-box-danger i { + color: var(--visual-color); +} + +/* Card Container - Standard content card + Usage: +
+
+

Card Title

+ +
+
+ Content goes here +
+ +
+*/ +.card { + background: var(--main-bg-color); + border: 1px solid var(--input-border); + border-radius: 8px; + overflow: hidden; + margin-bottom: 16px; +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid var(--input-border); + background: var(--secondary-bg); +} + +.card-header h3, +.card-header h4 { + margin: 0; + color: var(--accent-color); + font-size: 1rem; +} + +.card-body { + padding: 16px; +} + +.card-footer { + padding: 12px 16px; + border-top: 1px solid var(--input-border); + background: var(--secondary-bg); +} + +/* Button Group - Standard action buttons + Usage: +
+ + +
+*/ +.button-group { + display: flex; + gap: 10px; + align-items: center; + flex-wrap: wrap; +} + +.button-group .btn { + flex: 0 1 auto; +} + + +/* Button variants */ +.btn-secondary { + background: transparent; + border: 1px solid var(--input-border); +} + +.btn-secondary:hover { + background: var(--button-bg); +} + +.btn-primary { + background: var(--accent-color); + color: var(--main-bg-color); + border-color: var(--accent-color); +} + +.btn-primary:hover { + background: var(--accent-hover); + border-color: var(--accent-hover); +} + +/* ======================================== + STANDARD FORM INPUT STYLES + + These base styles apply to ALL form inputs + across the entire application. Only add + specific overrides when truly needed. + + Benefits: + - Consistent styling everywhere + - Easy to maintain and test + - No need to chase down individual inputs + - Responsive by default + ======================================== */ + +/* Standard text input, number input, and select styles */ +input[type="text"], +input[type="number"], +input[type="email"], +input[type="password"], +select { + width: 100%; + max-width: 100%; + min-width: 0; /* Allow shrinking below content width */ + padding: 8px 10px; + background: var(--input-bg); + color: var(--text-color); + border: 1px solid var(--input-border); + border-radius: 4px; + font-family: inherit; + font-size: 0.9rem; + line-height: 1.4; + transition: all 0.2s ease; + box-sizing: border-box; +} + +/* Select-specific styles for better overflow handling */ +select { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + appearance: none; /* Remove default arrow to control width better */ + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23e0e0e0' d='M6 9L1 4h10z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 8px center; + padding-right: 28px; /* Space for custom arrow */ +} + +/* Option elements inherit text color */ +select option { + background: var(--input-bg); + color: var(--text-color); + padding: 4px 8px; +} + +/* Safety net: Ensure ALL form controls respect container width */ +input, select, textarea, button { + max-width: 100%; + box-sizing: border-box; +} + +/* Focus states for all inputs */ +input[type="text"]:focus, +input[type="number"]:focus, +input[type="email"]:focus, +input[type="password"]:focus, +select:focus { + outline: none; + border-color: var(--accent-color); + box-shadow: 0 0 0 2px rgba(100, 181, 246, 0.2); +} + +/* Disabled state */ +input[type="text"]:disabled, +input[type="number"]:disabled, +input[type="email"]:disabled, +input[type="password"]:disabled, +select:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* Standard textarea styles */ +textarea { + width: 100%; + max-width: 100%; + padding: 12px; + background-color: var(--input-bg); + color: var(--text-color); + border: 1px solid var(--input-border); + border-radius: 4px; + font-family: 'Fira Code', 'Courier New', monospace; + resize: vertical; + min-height: 100px; + line-height: 1.4; + font-size: 14px; + transition: all 0.2s ease; + box-sizing: border-box; +} + +textarea::placeholder { + color: rgba(224, 224, 224, 0.5); +} + +textarea:focus { + outline: none; + border-color: var(--accent-color); + box-shadow: var(--focus-shadow); +} + +/* Standard button styles */ +button, +.btn { + padding: 8px 16px; + background: var(--button-bg); + color: var(--text-color); + border: 1px solid var(--input-border); + border-radius: 4px; + font-family: inherit; + font-size: 0.9rem; + cursor: pointer; + transition: all 0.2s ease; + box-sizing: border-box; + white-space: nowrap; +} + +button:hover, +.btn:hover { + background: var(--button-hover-bg); + border-color: var(--accent-color); +} + +button:active, +.btn:active { + transform: translateY(1px); +} + +button:focus-visible, +.btn:focus-visible { + outline: none; + border-color: var(--accent-color); + box-shadow: var(--focus-shadow); +} + +button:disabled, +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* Standard label styles */ +label { + display: flex; + flex-direction: column; + gap: 6px; + width: 100%; + color: var(--text-color); + font-size: 0.9rem; + font-weight: 500; + box-sizing: border-box; + word-wrap: break-word; +} + +label small { + color: var(--text-muted); + font-size: 0.8rem; + font-weight: normal; + margin-top: -2px; +} + .container { max-width: 900px; + width: 100%; margin: 0 auto; padding: 24px; background-color: var(--secondary-bg); border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + box-sizing: border-box; } header { @@ -82,11 +537,11 @@ header { font-weight: bold; letter-spacing: 2px; text-transform: uppercase; - color: #00ff00; - text-shadow: 0 0 5px #00ff00, 0 0 10px rgba(0, 255, 0, 0.8); - background: linear-gradient(90deg, transparent 0%, rgba(0, 255, 0, 0.2) 50%, transparent 100%); - padding: 8px; - border: 1px solid rgba(0, 255, 0, 0.3); + color: var(--glitch-color); + text-shadow: 0 0 5px var(--glitch-color), 0 0 10px rgba(var(--glitch-color-rgb), 0.8); + background: linear-gradient(90deg, transparent 0%, rgba(var(--glitch-color-rgb), 0.2) 50%, transparent 100%); + padding: var(--spacing-sm); + border: 1px solid rgba(var(--glitch-color-rgb), 0.3); position: relative; overflow: hidden; animation: glitch 3s infinite; @@ -186,6 +641,7 @@ h1, h2, h3, h4, h5 { .tab-buttons { display: flex; + flex-wrap: wrap; gap: 8px; margin-bottom: 24px; } @@ -258,6 +714,31 @@ h1, h2, h3, h4, h5 { margin-bottom: 15px; } +.section-title { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 8px; +} + +.section-title h3 { + display: flex; + align-items: center; + gap: 8px; + color: var(--accent-color); + margin: 0; +} + +.section-title small { + font-size: 0.85em; + font-weight: normal; + color: var(--text-muted); + background-color: rgba(100, 181, 246, 0.1); + padding: 4px 10px; + border-radius: 4px; +} + .section-header h3 { display: flex; align-items: center; @@ -341,28 +822,11 @@ h1, h2, h3, h4, h5 { background-color: rgba(var(--accent-color-rgb), 0.2); } -textarea { - width: 100%; - padding: 12px; - background-color: var(--input-bg); - color: var(--text-color); - border: 1px solid var(--input-border); - border-radius: 4px; - font-family: 'Fira Code', 'Courier New', monospace; - resize: vertical; - min-height: 100px; - line-height: 1.4; - font-size: 14px; - transition: all 0.2s ease; -} - -/* Special styling for encoded message textarea */ +/* Special styling for encoded message textarea - only overrides */ .output-section textarea { background-color: var(--secondary-bg); color: var(--accent-color); border: 1px solid rgba(100, 181, 246, 0.3); - border-radius: 6px; - font-family: 'Fira Code', monospace; letter-spacing: 0.5px; line-height: 1.5; box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2); @@ -452,21 +916,9 @@ textarea { width: 220px; } +/* Emoji search input - only unique override for icon padding */ .emoji-search input { - width: 100%; - padding: 8px 12px 8px 32px; - background-color: var(--input-bg); - border: 1px solid var(--input-border); - border-radius: 4px; - color: var(--text-color); - font-size: 0.9rem; - transition: all 0.2s ease; -} - -.emoji-search input:focus { - border-color: var(--accent-color); - box-shadow: 0 0 0 2px rgba(var(--accent-color-rgb), 0.2); - outline: none; + padding-left: 32px; /* Space for search icon */ } .emoji-search i { @@ -593,28 +1045,32 @@ textarea { opacity: 0.9; } +/* Emoji grid with increased specificity to avoid !important */ +.emoji-library .emoji-grid, +#emoji-grid-container .emoji-grid, +.emoji-grid-container .emoji-grid, .emoji-grid { - display: grid !important; - grid-template-columns: repeat(auto-fill, minmax(42px, 1fr)) !important; - grid-auto-rows: 42px !important; - gap: 6px !important; - padding: 12px !important; - border-radius: 6px !important; - border: 1px solid var(--input-border) !important; - background-color: var(--secondary-bg) !important; - box-shadow: none !important; - transition: all 0.2s ease !important; - margin-bottom: 10px !important; - width: 100% !important; - max-height: none !important; - overflow: visible !important; - max-width: 100% !important; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(42px, 1fr)); + grid-auto-rows: 42px; + gap: var(--spacing-xs); + padding: var(--spacing-sm); + border-radius: 6px; + border: 1px solid var(--input-border); + background-color: var(--secondary-bg); + box-shadow: none; + transition: all 0.2s ease; + margin-bottom: 10px; + width: 100%; + max-height: none; + overflow: visible; + max-width: 100%; font-family: 'Segoe UI Emoji', 'Segoe UI Symbol', 'Apple Color Emoji', 'Noto Color Emoji', 'Android Emoji', 'EmojiOne Color', - 'Twemoji Mozilla', sans-serif !important; + 'Twemoji Mozilla', sans-serif; /* Improve emoji rendering */ - text-rendering: optimizeLegibility !important; - -webkit-font-smoothing: antialiased !important; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; } .emoji-category-tabs { @@ -638,8 +1094,9 @@ textarea { align-items: center; justify-content: center; text-align: center; - min-width: 120px; + min-width: max-content; /* Fit content but allow wrapping */ white-space: nowrap; + flex-shrink: 1; /* Allow shrinking if needed */ } .emoji-category-tab.active, @@ -648,22 +1105,25 @@ textarea { color: white; } +/* Emoji grid container with increased specificity */ +.emoji-library #emoji-grid-container, +.emoji-library .emoji-grid-container, #emoji-grid-container, .emoji-grid-container { - width: 100% !important; - display: flex !important; - flex-direction: column !important; - align-items: stretch !important; - background-color: transparent !important; - padding: 0 !important; - margin: 0 !important; - border: none !important; - box-shadow: none !important; - overflow: visible !important; - min-height: 0 !important; - max-width: 100% !important; - border-bottom: none !important; - border-top: none !important; + width: 100%; + display: flex; + flex-direction: column; + align-items: stretch; + background-color: transparent; + padding: 0; + margin: 0; + border: none; + box-shadow: none; + overflow: visible; + min-height: 0; + max-width: 100%; + border-bottom: none; + border-top: none; } .emoji-button { @@ -776,25 +1236,7 @@ textarea { border-color: var(--accent-color); } -textarea::placeholder { - color: rgba(224, 224, 224, 0.5); -} - -textarea:focus { - outline: none; - border-color: var(--accent-color); - box-shadow: var(--focus-shadow); -} - -button:focus-visible { - outline: none; - border-color: var(--accent-color); - box-shadow: var(--focus-shadow); -} - -button:active { - transform: translateY(1px); -} +/* Button focus and active states handled in standard styles above */ /* Invisible Text Button */ .invisible-button { @@ -833,23 +1275,7 @@ button:active { border-style: solid; } -button { - background-color: var(--button-bg); - color: var(--text-color); - border: 1px solid var(--button-border); - border-radius: 4px; - padding: 8px 16px; - cursor: pointer; - font-family: 'Courier New', monospace; - transition: all 0.3s; -} - -/* Copy button styling moved to a single definition below */ - -button:hover { - background-color: var(--button-hover-bg); - color: var(--button-hover-text); -} +/* Base button styles defined in standard form styles above */ /* Emoji grid */ @@ -901,6 +1327,7 @@ button:hover { transition: all 0.2s ease; cursor: pointer; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + text-transform: capitalize; } .legend-item:hover { @@ -916,23 +1343,50 @@ button:hover { box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); } -/* Legend item colors for new categories */ -.legend-item[data-target="category-fantasy"] { +.legend-item.transform-category-encoding { + border-left-color: var(--encoding-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-format { + border-left-color: var(--format-color); +} + +.legend-item.transform-category-unicode { + border-left-color: var(--unicode-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[data-target="category-ancient"] { +.legend-item.transform-category-ancient { border-left-color: var(--ancient-color); } -.legend-item[data-target="category-technical"] { +.legend-item.transform-category-technical { border-left-color: var(--technical-color); } -.legend-item[data-target="category-randomizer"] { +.legend-item.transform-category-randomizer { border-left-color: var(--randomizer-color); } +.legend-item.transform-category-case { + border-left-color: var(--case-color); +} + .transform-section:focus-within { border-color: var(--accent-color); box-shadow: var(--focus-shadow); @@ -967,6 +1421,7 @@ button:hover { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); transition: all 0.3s ease; position: relative; + border-left: none; } .highlight-section { @@ -988,17 +1443,64 @@ button:hover { font-size: 1.1rem; font-weight: 500; padding-left: 8px; + text-transform: capitalize; + border-left: 4px solid; +} + +.category-title.transform-category-encoding { + border-left-color: var(--encoding-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-format { + border-left-color: var(--format-color); +} + +.category-title.transform-category-unicode { + border-left-color: var(--unicode-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-technical { + border-left-color: var(--technical-color); +} + +.category-title.transform-category-randomizer { + border-left-color: var(--randomizer-color); +} + +.category-title.transform-category-case { + border-left-color: var(--case-color); } .transform-buttons { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + display: flex; + flex-wrap: wrap; gap: 8px; margin-bottom: 16px; - width: 100%; /* Ensure grid doesn't overflow container */ + width: 100%; /* Ensure flex doesn't overflow container */ box-sizing: border-box; } + #category-randomizer .transform-buttons { display: flex; } @@ -1093,7 +1595,7 @@ button:hover { /* Copy History Panel */ .copy-history-panel { position: fixed; - right: -400px; /* Start offscreen */ + right: -100%; top: 0; width: 380px; height: 100vh; @@ -1127,6 +1629,13 @@ button:hover { color: var(--accent-color); } +.copy-history-header .header-actions { + display: flex; + gap: 10px; + align-items: center; +} + +.copy-history-header .clear-history-button, .copy-history-header .close-button { background: none; border: none; @@ -1134,12 +1643,18 @@ button:hover { cursor: pointer; font-size: 1.2rem; padding: 5px; + transition: color 0.2s; } +.copy-history-header .clear-history-button:hover, .copy-history-header .close-button:hover { color: var(--accent-color); } +.copy-history-header .clear-history-button:hover { + color: #ff6b6b; +} + .copy-history-content { flex: 1; overflow-y: auto; @@ -1206,10 +1721,12 @@ button:hover { .history-actions { display: flex; justify-content: flex-end; + gap: 8px; margin-top: 8px; } -.copy-again-button { +.copy-again-button, +.remove-history-button { background-color: var(--button-bg); color: var(--text-color); border: 1px solid var(--input-border); @@ -1225,6 +1742,12 @@ button:hover { color: var(--accent-color); } +.remove-history-button:hover { + background-color: #ff6b6b; + border-color: #ff6b6b; + color: white; +} + .history-button { background: none; color: var(--text-color); @@ -1268,39 +1791,39 @@ button:hover { } .transform-category-encoding .transform-preview { - color: rgba(126, 87, 194, 0.9); + color: rgba(var(--encoding-color-rgb), 0.9); } .transform-category-cipher .transform-preview { - color: rgba(38, 166, 154, 0.9); + color: rgba(var(--cipher-color-rgb), 0.9); } .transform-category-visual .transform-preview { - color: rgba(239, 83, 80, 0.9); + color: rgba(var(--visual-color-rgb), 0.9); } .transform-category-format .transform-preview { - color: rgba(255, 183, 77, 0.9); + color: rgba(var(--format-color-rgb), 0.9); } .transform-category-unicode .transform-preview { - color: rgba(66, 165, 245, 0.9); + color: rgba(var(--unicode-color-rgb), 0.9); } .transform-category-special .transform-preview { - color: rgba(102, 187, 106, 0.9); + color: rgba(var(--special-color-rgb), 0.9); } .transform-category-fantasy .transform-preview { - color: rgba(255, 107, 157, 0.9); + color: rgba(var(--fantasy-color-rgb), 0.9); } .transform-category-ancient .transform-preview { - color: rgba(212, 175, 55, 0.9); + color: rgba(var(--ancient-color-rgb), 0.9); } .transform-category-technical .transform-preview { - color: rgba(0, 188, 212, 0.9); + color: rgba(var(--technical-color-rgb), 0.9); } .transform-category-randomizer .transform-preview { @@ -1308,6 +1831,10 @@ button:hover { text-shadow: 0 0 8px rgba(225,186,255,.35); } +.transform-category-case .transform-preview { + color: rgba(var(--case-color-rgb), 0.9); +} + .transform-button:hover .transform-preview { background-color: rgba(0, 0, 0, 0.18); opacity: 1; @@ -1339,115 +1866,108 @@ button:hover { min-height: 70px; } -/* Transform category styling */ +[class*="transform-category-"] { + background: linear-gradient(to right, transparent, var(--button-bg)); + transition: all 0.2s ease; +} + .transform-category-encoding { - border-left: 4px solid var(--encoding-color); - background: linear-gradient(to right, rgba(126, 87, 194, 0.05), var(--button-bg)); + 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(126, 87, 194, 0.15), var(--button-hover-bg)); - border-color: var(--encoding-color); - box-shadow: 0 3px 10px rgba(126, 87, 194, 0.2); + 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-cipher { - border-left: 4px solid var(--cipher-color); - background: linear-gradient(to right, rgba(38, 166, 154, 0.05), var(--button-bg)); + background: linear-gradient(to right, rgba(var(--cipher-color-rgb), 0.05), var(--button-bg)); } .transform-category-cipher:hover { - background: linear-gradient(to right, rgba(38, 166, 154, 0.15), var(--button-hover-bg)); - border-color: var(--cipher-color); - box-shadow: 0 3px 10px rgba(38, 166, 154, 0.2); + background: linear-gradient(to right, rgba(var(--cipher-color-rgb), 0.15), var(--button-hover-bg)); + box-shadow: 0 3px 10px rgba(var(--cipher-color-rgb), 0.2); } .transform-category-visual { - border-left: 4px solid var(--visual-color); - background: linear-gradient(to right, rgba(239, 83, 80, 0.05), var(--button-bg)); + 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(239, 83, 80, 0.15), var(--button-hover-bg)); - border-color: var(--visual-color); - box-shadow: 0 3px 10px rgba(239, 83, 80, 0.2); + 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-format { - border-left: 4px solid var(--format-color); - background: linear-gradient(to right, rgba(255, 183, 77, 0.05), var(--button-bg)); + background: linear-gradient(to right, rgba(var(--format-color-rgb), 0.05), var(--button-bg)); } .transform-category-format:hover { - background: linear-gradient(to right, rgba(255, 183, 77, 0.15), var(--button-hover-bg)); - border-color: var(--format-color); - box-shadow: 0 3px 10px rgba(255, 183, 77, 0.2); + background: linear-gradient(to right, rgba(var(--format-color-rgb), 0.15), var(--button-hover-bg)); + box-shadow: 0 3px 10px rgba(var(--format-color-rgb), 0.2); } .transform-category-unicode { - border-left: 4px solid var(--unicode-color); - background: linear-gradient(to right, rgba(66, 165, 245, 0.05), var(--button-bg)); + 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(66, 165, 245, 0.15), var(--button-hover-bg)); - border-color: var(--unicode-color); - box-shadow: 0 3px 10px rgba(66, 165, 245, 0.2); + 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-special { - border-left: 4px solid var(--special-color); - background: linear-gradient(to right, rgba(102, 187, 106, 0.05), var(--button-bg)); + background: linear-gradient(to right, rgba(var(--special-color-rgb), 0.05), var(--button-bg)); } .transform-category-special:hover { - background: linear-gradient(to right, rgba(102, 187, 106, 0.15), var(--button-hover-bg)); - border-color: var(--special-color); - box-shadow: 0 3px 10px rgba(102, 187, 106, 0.2); + background: linear-gradient(to right, rgba(var(--special-color-rgb), 0.15), var(--button-hover-bg)); + box-shadow: 0 3px 10px rgba(var(--special-color-rgb), 0.2); } .transform-category-fantasy { - border-left: 4px solid var(--fantasy-color); - background: linear-gradient(to right, rgba(255, 107, 157, 0.05), var(--button-bg)); + background: linear-gradient(to right, rgba(var(--fantasy-color-rgb), 0.05), var(--button-bg)); } .transform-category-fantasy:hover { - background: linear-gradient(to right, rgba(255, 107, 157, 0.15), var(--button-hover-bg)); - border-color: var(--fantasy-color); - box-shadow: 0 3px 10px rgba(255, 107, 157, 0.2); + 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 { - border-left: 4px solid var(--ancient-color); - background: linear-gradient(to right, rgba(212, 175, 55, 0.05), var(--button-bg)); + 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(212, 175, 55, 0.15), var(--button-hover-bg)); - border-color: var(--ancient-color); - box-shadow: 0 3px 10px rgba(212, 175, 55, 0.2); + 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-technical { - border-left: 4px solid var(--technical-color); - background: linear-gradient(to right, rgba(0, 188, 212, 0.05), var(--button-bg)); + background: linear-gradient(to right, rgba(var(--technical-color-rgb), 0.05), var(--button-bg)); } .transform-category-technical:hover { - background: linear-gradient(to right, rgba(0, 188, 212, 0.15), var(--button-hover-bg)); - border-color: var(--technical-color); - box-shadow: 0 3px 10px rgba(0, 188, 212, 0.2); + background: linear-gradient(to right, rgba(var(--technical-color-rgb), 0.15), var(--button-hover-bg)); + box-shadow: 0 3px 10px rgba(var(--technical-color-rgb), 0.2); } .transform-category-randomizer { - border-left: 4px solid var(--randomizer-color); - background: linear-gradient(to right, rgba(156, 39, 176, 0.05), var(--button-bg)); + background: linear-gradient(to right, rgba(var(--randomizer-color-rgb), 0.05), var(--button-bg)); } .transform-category-randomizer:hover { - background: linear-gradient(to right, rgba(156, 39, 176, 0.15), var(--button-hover-bg)); - border-color: var(--randomizer-color); - box-shadow: 0 3px 10px rgba(156, 39, 176, 0.2); + background: linear-gradient(to right, rgba(var(--randomizer-color-rgb), 0.15), var(--button-hover-bg)); + 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-button:before { @@ -1490,63 +2010,58 @@ button:hover { /* Category-specific active states */ .transform-category-encoding.active { - background: linear-gradient(to right, var(--encoding-color), #9575cd); - border-color: var(--encoding-color); - box-shadow: 0 3px 12px rgba(126, 87, 194, 0.4); + 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-cipher.active { - background: linear-gradient(to right, var(--cipher-color), #4db6ac); - border-color: var(--cipher-color); - box-shadow: 0 3px 12px rgba(38, 166, 154, 0.4); + background: linear-gradient(to right, var(--cipher-color), var(--cipher-active-end)); + box-shadow: 0 3px 12px rgba(var(--cipher-color-rgb), 0.4); } .transform-category-visual.active { - background: linear-gradient(to right, var(--visual-color), #e57373); - border-color: var(--visual-color); - box-shadow: 0 3px 12px rgba(239, 83, 80, 0.4); + 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-format.active { - background: linear-gradient(to right, var(--format-color), #ffcc80); - border-color: var(--format-color); - box-shadow: 0 3px 12px rgba(255, 183, 77, 0.4); + background: linear-gradient(to right, var(--format-color), var(--format-active-end)); + box-shadow: 0 3px 12px rgba(var(--format-color-rgb), 0.4); } .transform-category-unicode.active { - background: linear-gradient(to right, var(--unicode-color), #64b5f6); - border-color: var(--unicode-color); - box-shadow: 0 3px 12px rgba(66, 165, 245, 0.4); + 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-special.active { - background: linear-gradient(to right, var(--special-color), #81c784); - border-color: var(--special-color); - box-shadow: 0 3px 12px rgba(102, 187, 106, 0.4); + background: linear-gradient(to right, var(--special-color), var(--special-active-end)); + box-shadow: 0 3px 12px rgba(var(--special-color-rgb), 0.4); } .transform-category-fantasy.active { - background: linear-gradient(to right, var(--fantasy-color), #ff8aad); - border-color: var(--fantasy-color); - box-shadow: 0 3px 12px rgba(255, 107, 157, 0.4); + 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), #e6c84c); - border-color: var(--ancient-color); - box-shadow: 0 3px 12px rgba(212, 175, 55, 0.4); + 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-technical.active { - background: linear-gradient(to right, var(--technical-color), #26c6da); - border-color: var(--technical-color); - box-shadow: 0 3px 12px rgba(0, 188, 212, 0.4); + background: linear-gradient(to right, var(--technical-color), var(--technical-active-end)); + box-shadow: 0 3px 12px rgba(var(--technical-color-rgb), 0.4); } .transform-category-randomizer.active { - background: linear-gradient(to right, var(--randomizer-color), #ab47bc); - border-color: var(--randomizer-color); - box-shadow: 0 3px 12px rgba(156, 39, 176, 0.4); + 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); } /* Add a subtle indicator for clickable buttons */ @@ -1750,25 +2265,7 @@ button:hover { } /* Responsive adjustments */ -@media (max-width: 768px) { - .emoji-grid { - grid-template-columns: repeat(auto-fill, minmax(50px, 1fr)); - } - - .emoji-button { - min-width: 50px; - height: 50px; - } - - .tab-buttons { - flex-direction: column; - } - - .tab-buttons button { - width: 100%; - text-align: left; - } -} + /* Special styling for randomizer section */ .randomizer-special { @@ -1827,13 +2324,13 @@ button:hover { } .randomizer-button { - background: linear-gradient(135deg, #7e57c2, #ab47bc) !important; - color: #f8f8ff !important; + background: linear-gradient(135deg, var(--encoding-color), var(--randomizer-active-end)); + color: #f8f8ff; font-weight: 800; position: relative; overflow: hidden; - border: 2px solid rgba(200, 170, 255, 0.35) !important; - padding: 18px 28px !important; + border: 2px solid rgba(200, 170, 255, 0.35); + padding: var(--spacing-md) var(--spacing-lg); letter-spacing: .3px; justify-content: center; text-align: center; @@ -1925,12 +2422,10 @@ button:hover { margin-bottom: 8px; } -.steg-adv-panel select, .steg-adv-panel input[type=number] { - background: var(--input-bg); - color: var(--text-color); - border: 1px solid var(--input-border); - border-radius: 4px; - padding: 6px 8px; +/* Steg panel inputs - only unique overrides */ +.steg-adv-panel select, +.steg-adv-panel input[type=number] { + padding: 6px 8px; /* Slightly smaller padding */ } .steg-note { @@ -1940,10 +2435,6 @@ button:hover { font-size: 0.8rem; } -@media (max-width: 900px) { - .steg-split-layout { grid-template-columns: 1fr; } - .steg-advanced-sidebar { position: relative; top: 0; } -} /* Global Unicode options panel (subtle, like copy history) */ .unicode-options-panel { @@ -1969,12 +2460,45 @@ button:hover { display: flex; align-items: center; justify-content: space-between; + position: relative; + z-index: 300; + pointer-events: auto; + touch-action: manipulation; } .unicode-panel-header h3 { margin: 0; color: var(--accent-color); font-size: 1rem; } .unicode-panel-content { padding: 12px; overflow-y: auto; } -.unicode-panel-header .close-button { background: transparent; border: none; color: var(--text-color); cursor: pointer; padding: 4px; } +.unicode-panel-header .close-button { + background: transparent; + border: none; + color: var(--text-color); + cursor: pointer; + padding: 8px; + min-width: 44px; + min-height: 44px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; + transition: all 0.2s ease; + border-radius: 4px; + pointer-events: auto; + touch-action: manipulation; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0.1); + user-select: none; + z-index: 301; + position: relative; +} + +.unicode-panel-header .close-button:hover { + background: var(--button-hover-bg); + color: var(--accent-color); +} + +.unicode-panel-header .close-button:active { + transform: scale(0.95); +} .apply-status { color:#69f0ae; opacity:.9; } .apply-steg-options.applied { border-color:#2e7d32; box-shadow: 0 0 12px rgba(105,240,174,.35) inset, 0 0 16px rgba(105,240,174,.2); } @@ -2057,7 +2581,16 @@ html { color: var(--main-bg-color); border-color: var(--accent-color); } -.switch { display:flex; gap:6px; align-items:center; padding: 6px 8px; background: var(--main-bg-color); border:1px solid var(--input-border); border-radius:6px; } +.switch { + display:flex; + gap:8px; + align-items:center; + padding: 6px 8px 6px 8px; + background: var(--main-bg-color); + border:1px solid var(--input-border); + border-radius:6px; + margin-left: 6px; +} .switch span { opacity:.85; } .switch.neon { border-color:#1b5e20; box-shadow:0 0 0 1px rgba(76,175,80,.15) inset, 0 0 10px rgba(76,175,80,.15); } .switch.neon input[type="checkbox"] { @@ -2117,11 +2650,16 @@ html { border-radius: 8px; padding: 10px; margin-bottom: 8px; + grid-column: 1 / -1; /* Span full width of the grid */ + width: 100%; } .carrier-quick-grid .emoji-grid { border: none !important; background: transparent !important; - padding: 6px !important; + padding: 6px !important; + grid-template-columns: repeat(auto-fill, minmax(32px, 1fr)) !important; + grid-auto-rows: 32px !important; + width: 100% !important; } .tokenade-carrier-options { margin-top: 8px; } .carrier-quick-grid .emoji-button { @@ -2160,3 +2698,862 @@ html { .mutation-actions .action-button.copy:hover { color: #bbdefb; box-shadow: 0 0 0 1px rgba(144,202,249,.2) inset, 0 0 14px rgba(144,202,249,.18); } .mutation-actions .action-button.download { border-color: #2e7d32; color: #69f0ae; } .mutation-actions .action-button.download:hover { color: #b9f6ca; box-shadow: 0 0 0 1px rgba(105,240,174,.2) inset, 0 0 14px rgba(105,240,174,.18); } + +/* Message Splitter Styles */ +.encapsulation-section { + margin-top: 16px; + padding: 16px; + background: var(--main-bg-color); + border: 1px solid var(--input-border); + border-radius: 8px; +} + +.encapsulation-section .section-header { + margin-bottom: 16px; +} + +.encapsulation-section .section-header h4 { + margin-bottom: 4px; + margin-top: 0; + color: var(--accent-color); + font-size: 1rem; + display: flex; + align-items: center; + gap: 8px; +} + +.encapsulation-section .section-header p { + margin: 0; + color: var(--text-muted); + font-size: 0.9em; +} + +.encapsulation-presets { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin-top: 12px; +} + +.encapsulation-presets small { + color: var(--text-muted); + font-weight: 500; + margin-right: 4px; +} + +.preset-btn { + padding: 6px 12px; + background: var(--button-bg); + border: 1px solid var(--input-border); + border-radius: 6px; + color: var(--text-color); + cursor: pointer; + transition: all 0.2s ease; + font-family: 'Fira Code', monospace; + font-size: 0.9rem; +} + +.preset-btn:hover { + background: var(--button-hover-bg); + border-color: var(--accent-color); + transform: translateY(-1px); +} + +.preset-btn:active { + transform: translateY(0); +} + +.split-messages-container { + margin-top: 16px; +} + +.split-messages-container .output-heading { + margin-bottom: 12px; +} + +.split-messages-container .output-heading h4 { + display: flex; + align-items: center; + gap: 8px; + color: var(--accent-color); + margin-bottom: 0; +} + +.split-messages-container .output-heading h4 small { + font-size: 0.7em; + font-weight: normal; + color: var(--text-muted); + background-color: rgba(100, 181, 246, 0.1); + padding: 2px 8px; + border-radius: 4px; + margin-left: 8px; +} + +.split-messages-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 12px; +} + +.split-message-card { + background: var(--input-bg); + border: 1px solid var(--input-border); + border-radius: 8px; + padding: 12px; + transition: all 0.2s ease; + position: relative; +} + +.split-message-card:hover { + border-color: var(--accent-color); + box-shadow: 0 2px 8px rgba(100, 181, 246, 0.1); + transform: translateY(-2px); +} + +.split-message-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + padding-bottom: 8px; + border-bottom: 1px solid var(--input-border); +} + +.message-number { + font-size: 0.85rem; + font-weight: 600; + color: var(--text-muted); + font-family: 'Fira Code', monospace; +} + +.copy-button-small { + padding: 4px 8px; + background: var(--button-bg); + border: 1px solid var(--input-border); + border-radius: 4px; + color: var(--accent-color); + cursor: pointer; + transition: all 0.2s ease; + font-size: 0.9rem; +} + +.copy-button-small:hover { + background: var(--accent-color); + color: var(--main-bg-color); + transform: scale(1.05); +} + +.copy-button-small i { + font-size: 0.85rem; +} + +.split-message-content { + font-family: 'Fira Code', monospace; + font-size: 0.95rem; + color: var(--text-color); + word-break: break-all; + line-height: 1.5; + min-height: 40px; + padding: 8px; + background: rgba(0, 0, 0, 0.2); + border-radius: 4px; +} + +/* Ensure consistent spacing with other tabs */ +.tab-content .transform-layout .transform-section { + margin-bottom: 16px; +} + +.tab-content .transform-layout .transform-section:last-child { + margin-bottom: 0; +} + +/* Options grid styling (used across multiple tabs) */ +.options-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 12px; + margin: 16px 0; +} + +/* Options grid label overrides */ +.options-grid label.switch { + flex-direction: row; + gap: 8px; +} + +.label-with-tooltip { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.tooltip-icon { + color: var(--text-muted); + font-size: 0.85rem; + cursor: pointer; + opacity: 0.7; + transition: opacity 0.2s ease, color 0.2s ease; + position: relative; + display: inline-block; + margin-left: 4px; + z-index: 999; +} + +.tooltip-icon:hover { + opacity: 1; + color: var(--accent-color); + z-index: 999; +} + +.tooltip-icon:active { + transform: scale(0.95); +} + +/* Custom click-based tooltip */ +.custom-tooltip { + position: fixed; + padding: 8px 12px; + background: rgba(20, 20, 25, 0.98); + color: #fff; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 0.8rem; + line-height: 1.4; + border-radius: 6px; + z-index: 10000; + opacity: 0; + visibility: hidden; + pointer-events: none; + max-width: 280px; + min-width: 200px; + text-align: left; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(100, 181, 246, 0.2); + transition: opacity 0.2s ease, visibility 0.2s ease; + word-wrap: break-word; + white-space: normal; +} + +.custom-tooltip::before { + content: ''; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + border: 6px solid transparent; + border-top-color: rgba(20, 20, 25, 0.98); + filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.2)); +} + +.custom-tooltip.active { + opacity: 1; + visibility: visible; + pointer-events: auto; +} + +/* Position tooltip icons properly */ +.tooltip-icon[data-tooltip] { + position: relative; +} + +/* Options grid inputs inherit from standard form styles */ + +.options-grid .switch { + margin-bottom: 0; + margin-left: 6px; + margin-top: auto; + padding: 6px 8px; + flex-direction: row; + align-items: center; + gap: 8px; + height: fit-content; + min-height: 38px; + max-height: 38px; + align-self: flex-end; +} + +/* Transform chain styling */ +.transform-chain { + display: flex; + flex-direction: column; + gap: 12px; +} + +.transform-chain-inline { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.transform-chain-item { + display: flex; + align-items: center; + gap: 8px; + 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-arrow { + color: var(--accent-color); + font-size: 1rem; + opacity: 0.7; + margin: 0 4px; +} + +.remove-transform-inline-btn { + padding: 4px 6px; + background: transparent; + border: none; + border-radius: 4px; + color: var(--text-muted); + cursor: pointer; + transition: all 0.2s ease; + font-size: 0.85rem; + opacity: 0.6; +} + +.remove-transform-inline-btn:hover { + background: rgba(211, 47, 47, 0.1); + color: #d32f2f; + opacity: 1; +} + +.add-transform-inline-btn { + padding: 8px 12px; + background: var(--button-bg); + border: 1px solid var(--input-border); + border-radius: 4px; + color: var(--accent-color); + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + min-width: 40px; + height: 38px; +} + +.add-transform-inline-btn:hover { + background: var(--button-hover-bg); + border-color: var(--accent-color); + transform: translateY(-1px); +} + +.add-transform-inline-btn i { + font-size: 0.9rem; +} + +/* Splitter-specific action button styling */ +.splitter-actions { + display: flex; + gap: 10px; + align-items: center; + flex-wrap: wrap; + margin: 16px 0 0 0; +} + +.splitter-copy-option { + margin-left: 0; +} + + +/* Universal Decoder - Alternatives Section */ +.alternatives-section { + margin-top: 1rem; + padding: 1rem; + background: var(--input-bg); + border: 1px solid var(--border-color); + border-radius: 8px; +} + +.alternatives-header { + font-weight: 600; + margin-bottom: 0.75rem; + color: var(--text-color); + display: flex; + align-items: center; + gap: 0.5rem; +} + +.alternatives-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.alternative-item { + padding: 0.75rem; + background: var(--bg-color); + border: 1px solid var(--border-color); + border-radius: 6px; +} + +.alternative-method { + font-weight: 600; + color: var(--accent-color); + margin-bottom: 0.25rem; + font-size: 0.9rem; +} + +.alternative-preview { + color: var(--text-secondary); + font-size: 0.85rem; + font-family: 'Monaco', 'Menlo', 'Consolas', monospace; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.input-header, .output-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.input-header label, .output-header label { + font-weight: 500; + color: var(--text-color); +} +/* ======================================== + MEDIA QUERIES + All responsive styles consolidated at end of file + ======================================== */ + +/* Desktop: 5 columns (only applies above 768px) */ +@media (min-width: 769px) { + .transform-buttons .transform-button-group { + flex: 0 0 calc((100% - 32px) / 5); /* 5 columns: account for 4 gaps of 8px */ + min-width: 0; + } +} + +/* Tablets and below (768px) */ +@media (max-width: 768px) { + .section-header-card { + flex-direction: column; + gap: 10px; + } + + .section-header-card-title { + width: 100%; + } + + .section-header-card-description { + padding-top: 0; + } + + .emoji-grid { + grid-template-columns: repeat(auto-fill, minmax(50px, 1fr)); + } + + .emoji-button { + min-width: 50px; + height: 50px; + } + + .tab-buttons { + flex-direction: column; + } + + .tab-buttons button { + width: 100%; + text-align: left; + } + + .section-title { + flex-direction: column; + align-items: flex-start; + gap: var(--spacing-sm); + } + + .section-title h3 { + width: 100%; + } + + .section-title small { + width: 100%; + text-align: left; + } + + .transform-chain-item select { + min-width: 100%; /* Full width on smaller screens */ + } + + .split-messages-grid { + grid-template-columns: 1fr; + } + + .encapsulation-presets { + gap: var(--spacing-xs); + } + + .preset-btn { + padding: 5px 10px; + font-size: 0.85rem; + } + + .options-grid { + grid-template-columns: 1fr; + } + + .transform-chain-inline { + flex-direction: column; + align-items: flex-start; + } + + .transform-arrow { + transform: rotate(90deg); + margin: 4px 0; + } + + .splitter-actions { + flex-direction: column; + align-items: stretch; + } + + .splitter-actions button { + width: 100%; + } + + /* Transform buttons - 2 columns on tablets and below */ + .transform-buttons { + gap: 8px; + } + + .transform-buttons .transform-button-group { + flex: 0 0 calc(50% - 4px) !important; /* 2 columns: simpler calculation */ + min-width: 0; + max-width: calc(50% - 4px); + } +} + +/* Medium screens (900px) */ +@media (max-width: 900px) { + .steg-split-layout { + grid-template-columns: 1fr; + } + .steg-advanced-sidebar { + position: relative; + top: 0; + } +} + +@media (max-width: 576px) { + .button-group { + flex-direction: column; + align-items: stretch; + } + + .button-group .btn { + width: 100%; + } + + body { + padding: 0; + } + + .container { + padding: var(--spacing-sm); + border-radius: 4px; + } + + header { + flex-direction: column; + gap: var(--spacing-sm); + padding: var(--spacing-sm); + margin-bottom: 20px; + } + + .logo h1 { + font-size: 1.2rem; + padding: var(--spacing-xs); + } + + .actions { + width: 100%; + justify-content: center; + } + + /* Tab buttons remain stacked but with smaller padding */ + .tab-buttons button { + padding: var(--spacing-sm) var(--spacing-sm); + font-size: 13px; + } + + /* Transform buttons flex more compact - 2 columns on mobile */ + .transform-buttons { + gap: var(--spacing-xs); + } + + .transform-buttons .transform-button-group { + flex: 0 0 calc(50% - 2px) !important; /* 2 columns: simpler calculation */ + min-width: 0; + max-width: calc(50% - 2px); + } + + /* Options grid single column on very small screens */ + .options-grid { + grid-template-columns: 1fr; + gap: 10px; + margin: var(--spacing-sm) 0; + } + + /* Hacker controls single column */ + .hacker-controls { + grid-template-columns: 1fr; + gap: var(--spacing-sm); + } + + /* Unicode options panel full width */ + .unicode-options-panel { + width: 100%; + right: -100%; + z-index: 300; /* Ensure it's above other content on mobile */ + } + + .unicode-options-panel.active { + right: 0; + } + + .unicode-panel-header .close-button { + min-width: 48px; + min-height: 48px; + padding: 12px; + font-size: 1.4rem; + z-index: 302; + position: relative; + pointer-events: auto; + touch-action: manipulation; + background: var(--button-bg); + border: 1px solid var(--input-border); + -webkit-tap-highlight-color: rgba(0, 0, 0, 0.1); + } + + .unicode-panel-header .close-button:active { + background: var(--button-active-bg); + transform: scale(0.9); + } + + /* Emoji grid more compact */ + .emoji-library .emoji-grid, + #emoji-grid-container .emoji-grid, + .emoji-grid-container .emoji-grid, + .emoji-grid { + grid-template-columns: repeat(auto-fill, minmax(38px, 1fr)); + grid-auto-rows: 38px; + gap: var(--spacing-xs); + padding: var(--spacing-sm); + } + + .emoji-button { + font-size: 1.3rem; + } + + /* Emoji category tabs smaller and flexible */ + .emoji-category-tab { + padding: 5px var(--spacing-sm); + font-size: 0.75rem; + min-width: auto; /* Remove fixed width */ + flex: 1 1 auto; /* Allow flexible sizing */ + } + + /* Output heading smaller */ + .output-heading h4 { + font-size: 0.9rem; + } + + .output-heading h4 small { + font-size: 0.7rem; + padding: 2px var(--spacing-xs); + } + + /* Tokenizer tiles more compact */ + .token-tiles { + gap: var(--spacing-xs); + } + + .token-chip { + padding: var(--spacing-xs) var(--spacing-xs); + font-size: 0.85rem; + } + + /* Mutation actions stack */ + .mutation-actions { + flex-direction: column; + align-items: stretch; + } + + .mutation-actions button { + width: 100%; + } + + /* Splitter actions stack */ + .splitter-actions { + flex-direction: column; + align-items: stretch; + gap: var(--spacing-sm); + } + + .splitter-actions button, + .splitter-actions .switch { + width: 100%; + } + + /* Transform button preview text */ + .transform-preview { + font-size: 0.7rem; + max-width: 100%; + } + + /* Category section padding */ + .transform-category-section { + padding: var(--spacing-sm); + } + + .category-title { + font-size: 1rem; + padding-left: var(--spacing-xs); + } + + /* Tokenade carrier options single column */ + .tokenade-carrier-options { + grid-template-columns: 1fr; + } + + /* Carrier quick grid emojis smaller */ + .carrier-quick-grid .emoji-grid { + grid-template-columns: repeat(auto-fill, minmax(28px, 1fr)); + grid-auto-rows: 28px; + gap: 3px; + } + + .carrier-quick-grid .emoji-button { + min-width: 28px; + height: 28px; + font-size: 1rem; + } + + /* Additional mobile-specific rules */ + /* Smaller inputs and selects on very small screens */ + input[type="number"], + input[type="text"], + input[type="email"], + input[type="password"], + select { + padding: var(--spacing-xs) var(--spacing-sm); + font-size: 14px; + } + + /* Compact section headers */ + .section-header h3 { + font-size: 1rem; + } + + .section-header h3 small { + font-size: 0.65em; + padding: 2px var(--spacing-xs); + } + + /* Smaller textareas */ + textarea { + padding: 10px; + font-size: 13px; + min-height: 80px; + } + + /* Encapsulation presets wrap better */ + .encapsulation-presets { + gap: var(--spacing-xs); + } + + .preset-btn { + padding: 5px var(--spacing-sm); + font-size: 0.8rem; + } + + /* Transform section padding */ + .transform-section { + padding: var(--spacing-sm); + } + + .input-section, + .output-section, + .decode-section { + padding: var(--spacing-sm); + } + + /* Token bomb controls more compact */ + .token-bomb-section { + padding: var(--spacing-sm); + } + + .tokenade-presets .transform-button { + padding: var(--spacing-xs) var(--spacing-xs); + font-size: 0.75rem; + } + + /* Smaller segmented buttons */ + .segmented button { + padding: 5px var(--spacing-sm); + font-size: 0.85rem; + } + + /* Legend items wrap better */ + .transform-category-legend { + gap: var(--spacing-xs); + } + + .legend-item { + font-size: 0.75rem; + padding: 3px var(--spacing-xs); + } + + /* Copy history panel full width */ + .copy-history-panel { + width: 100%; + right: -100%; + } + + .copy-history-panel.active { + right: 0; + } + + /* Transform chain items stack vertically */ + .transform-chain-item select { + min-width: 100%; + width: 100%; + } + + /* Split messages grid single column */ + .split-messages-grid { + grid-template-columns: 1fr; + gap: 10px; + } + + /* Switch controls full width */ + .switch { + width: 100%; + justify-content: space-between; + } + + /* Randomizer section adjustments */ + .randomizer-special { + padding: var(--spacing-md); + } + + .randomizer-info { + padding: var(--spacing-sm); + } + + .randomizer-info ul { + margin-left: var(--spacing-md); + } +} + diff --git a/docs/TOOL-SYSTEM.md b/docs/TOOL-SYSTEM.md new file mode 100644 index 0000000..f270476 --- /dev/null +++ b/docs/TOOL-SYSTEM.md @@ -0,0 +1,80 @@ +# Tool System - Build-Time Template Injection + +## Architecture + +- **Templates**: Separate `.html` files in `templates/` directory +- **Build Process**: Injected into `index.html` at build time +- **Result**: Single static HTML file (fast loading, no HTTP requests) + +## File Structure + +``` +โ”œโ”€โ”€ index.template.html # Base shell +โ”œโ”€โ”€ index.html # Generated (templates injected) +โ”œโ”€โ”€ templates/ # Edit HTML here +โ”‚ โ”œโ”€โ”€ decoder.html +โ”‚ โ”œโ”€โ”€ steganography.html +โ”‚ โ””โ”€โ”€ ... +โ”œโ”€โ”€ js/tools/ # Tool classes (logic) +โ”‚ โ”œโ”€โ”€ Tool.js # Base class +โ”‚ โ””โ”€โ”€ *Tool.js # Auto-discovered +โ””โ”€โ”€ build/ + โ””โ”€โ”€ inject-tool-templates.js +``` + +## Creating a New Tool + +### 1. Create Tool Class + +`js/tools/MyTool.js`: +```javascript +class MyTool extends Tool { + constructor() { + super({ + id: 'mytool', + name: 'My Tool', + icon: 'fa-star', + title: 'Description', + order: 10 + }); + } + + getVueData() { + return { myInput: '', myOutput: '' }; + } + + getVueMethods() { + return { + processInput() { + this.myOutput = this.myInput.toUpperCase(); + } + }; + } +} +``` + +### 2. Create Template + +`templates/mytool.html`: +```html +
+
+ +
{{ myOutput }}
+
+
+``` + +### 3. Build + +```bash +npm run build:tools # Auto-discovers and registers tool +npm run build:templates # Injects template into index.html +``` + +## How It Works + +1. **Development**: Edit templates in `templates/*.html` +2. **Build**: `inject-tool-templates.js` reads templates and injects into `index.template.html` +3. **Output**: Complete `index.html` with all templates embedded +4. **Browser**: Vue compiles templates at page load (already in DOM) diff --git a/docs/TOOL_ARCHITECTURE.md b/docs/TOOL_ARCHITECTURE.md new file mode 100644 index 0000000..6bb5c35 --- /dev/null +++ b/docs/TOOL_ARCHITECTURE.md @@ -0,0 +1,181 @@ +# Tool Architecture + +## Overview + +The Tool system provides a way to organize features into modular, self-contained units. Each tool has: +- Vue data properties +- Vue methods +- Tab button configuration +- Tab content (template) + +## Important Limitation: Vue Template Compilation + +**Critical**: Tab content that uses Vue directives (`v-if`, `v-for`, `v-model`, `{{ }}`) **MUST** be defined in `index.html`, not in the Tool's `getTabContentHTML()` method. + +### Why? + +Vue's `v-html` directive (used for dynamic content insertion) has a fundamental limitation: +- It inserts **raw HTML only** +- It does **NOT** compile Vue templates +- Vue directives and interpolations are treated as literal text + +This is by design for security and performance reasons. + +### What Works vs What Doesn't + +โœ… **Works in `getTabContentHTML()`:** +```javascript +getTabContentHTML() { + return ` +
+

Hello World

+ +
+ `; +} +``` + +โŒ **Doesn't Work in `getTabContentHTML()`:** +```javascript +getTabContentHTML() { + return ` +
+ +

{{ myData }}

+
+ `; +} +``` + +## Architecture Pattern + +### For Simple Tools (Static HTML) + +1. Define content in Tool's `getTabContentHTML()` +2. Use plain HTML with inline event handlers +3. No Vue directives needed + +Example: A simple documentation viewer + +### For Complex Tools (Vue Templates) + +1. Define content in `index.html` +2. Use full Vue template syntax +3. Tool provides only data and methods +4. `getTabContentHTML()` returns empty string + +Example: Transform Tool, Decoder Tool, Emoji Tool + +## Current Implementation + +### Tools with Index.html Templates +- โœ… Transform Tool - Complex category system +- โœ… Decoder Tool - Dynamic alternatives list +- โœ… Emoji Tool - Interactive emoji grid +- โœ… Tokenade Tool - Complex nested options +- โœ… Mutation Tool - Multiple fuzzing options +- โœ… Tokenizer Tool - Dynamic token display + +### Tools with Dynamic Content +- โœ… Splitter Tool - Self-contained in SplitterTool.js + +## Adding a New Tool + +### Step 1: Create Tool Class + +```javascript +// js/tools/MyTool.js +class MyTool extends Tool { + constructor() { + super({ + id: 'mytool', + name: 'My Tool', + icon: 'fa-star', + title: 'My awesome tool', + order: 10 + }); + } + + getVueData() { + return { + myInput: '', + myOutput: '' + }; + } + + getVueMethods() { + return { + processData: function() { + this.myOutput = this.myInput.toUpperCase(); + } + }; + } + + getTabContentHTML() { + // If you need Vue directives, return empty and use index.html + return ''; + } +} +``` + +### Step 2: Add Content to index.html + +```html + +
+
+ + +
{{ myOutput }}
+
+
+``` + +### Step 3: Register Tool + +```javascript +// js/tools/index.js +if (typeof MyTool !== 'undefined') { + window.toolRegistry.register(new MyTool()); +} +``` + +### Step 4: Add Script Tag + +```html + + +``` + +## Future Improvements + +To enable fully dynamic tools with Vue templates, we would need to: + +1. **Use Vue Components** - Convert each tool to a proper Vue component +2. **Dynamic Component Loading** - Use `` +3. **Component Registration** - Register components instead of raw HTML + +This would require a significant refactor but would provide: +- Fully modular tools +- No index.html modifications for new tools +- Better encapsulation +- Proper Vue template compilation + +## Summary + +**Current Pattern:** +- Tool provides: data, methods, lifecycle hooks +- Index.html provides: template (for Vue directives) +- Tool registry: merges data/methods, handles activation + +**This works because:** +- Vue compiles templates in index.html at app initialization +- Data and methods are merged into the Vue instance +- Templates can reference the merged data/methods + +**Keep in mind:** +- v-html is not a replacement for Vue components +- Complex interactive UIs need proper Vue templates +- Static content can be fully dynamic +- The current hybrid approach is a practical compromise + diff --git a/docs/UI-COMPONENTS.md b/docs/UI-COMPONENTS.md new file mode 100644 index 0000000..97682b7 --- /dev/null +++ b/docs/UI-COMPONENTS.md @@ -0,0 +1,256 @@ +# UI Component Templates + +This document outlines the standard reusable UI components available in the project. Use these to maintain consistency across the application. + +--- + +## ๐Ÿ“ฆ Section Header with Description + +Use when you need a section title with an icon and descriptive text. + +**Responsive:** Stacks vertically on mobile (< 768px) + +```html +
+
+ +

Gibberish Dictionary

+
+

+ Translate text into random gibberish and corresponding dictionary. +

+
+``` + +--- + +## ๐ŸŽฏ Simple Title with Icon + +Use for inline titles with optional subtitles. + +**Responsive:** Wraps naturally + +```html +
+ +

Universal Decoder

+ Prioritizing Base64 +
+``` + +--- + +## ๐Ÿ’ก Info Boxes + +Use for tips, warnings, success messages, or disclaimers. + +**Variants:** `.info-box-warning`, `.info-box-success`, `.info-box-danger` + +```html + +
+ + Copy this text and share it. The transformation can be reversed. +
+ + +
+ + DISCLAIMER: Use for testing only. Do not deploy to production. +
+ + +
+ + Settings applied successfully! +
+ + +
+ + Danger zone: This will freeze your browser! +
+``` + +--- + +## ๐Ÿƒ Card Container + +Use for grouped content sections. + +**Responsive:** Full width, proper padding adjustments + +```html +
+
+

Card Title

+ +
+
+

Your main content goes here...

+
+ +
+``` + +--- + +## ๐ŸŽ›๏ธ Button Groups + +Use for multiple action buttons that should stay together. + +**Responsive:** Stacks vertically on very small screens (< 400px) + +```html +
+ + + +
+``` + +--- + +## ๐Ÿ”˜ Button Variants + +Standard button classes: + +- `.btn` - Base button (default gray) +- `.btn-primary` - Primary action (blue accent) +- `.btn-secondary` - Secondary action (transparent with border) + +```html + + + +``` + +--- + +## ๐Ÿ“ Form Inputs + +All standard HTML inputs are automatically styled and fully responsive. No extra classes needed! + +**Features:** +- โœ… Responsive width (never overflows container) +- โœ… Consistent styling across all inputs +- โœ… Custom styled select dropdowns +- โœ… Proper focus states +- โœ… Text overflow handling (ellipsis) + +```html + + + + + + + +``` + +**Responsive Behavior:** +- Desktop: Standard padding (8px 10px) +- Mobile (< 400px): Reduced padding (6px 8px) and smaller font + +--- + +## ๐ŸŽจ Grid Layouts + +Use `.options-grid` for form layouts: + +```html +
+ + + +
+``` + +**Responsive:** Automatically switches to single column on small screens + +--- + +## โœจ Best Practices + +1. **Always use these standard components** instead of creating custom styles +2. **Only add overrides when absolutely necessary** - document why +3. **Test responsiveness** at 400px, 768px, and 900px breakpoints +4. **Use semantic HTML** - proper heading levels, labels, etc. +5. **Include icons from Font Awesome** for visual consistency +6. **Add ARIA labels** for accessibility when needed + +--- + +## ๐Ÿšซ Anti-Patterns (Don't Do This) + +โŒ Creating inline styles +โŒ Duplicating component markup with slight variations +โŒ Adding `!important` to override standard styles +โŒ Using fixed widths that break responsiveness +โŒ Nesting cards more than 2 levels deep +โŒ Skipping semantic HTML elements + +--- + +## ๐Ÿ“ Breakpoints + +- **Mobile:** < 400px - Everything stacks, full width +- **Tablet:** 400px - 768px - Moderate stacking +- **Desktop:** > 768px - Full layout with sidebars + +--- + +## ๐ŸŽฏ Quick Reference + +| Component | Class | Responsive | +|-----------|-------|------------| +| Section Header | `.section-header-card` | Stacks < 768px | +| Title + Icon | `.title-with-icon` | Wraps | +| Info Box | `.info-box` | Full width | +| Card | `.card` | Full width | +| Button Group | `.button-group` | Stacks < 400px | +| Options Grid | `.options-grid` | Single col < 400px | + +--- + +## ๐Ÿ“ž Need a New Component? + +If you find yourself copying the same markup pattern 3+ times: +1. Document the pattern +2. Add it to `style.css` with clear comments +3. Update this documentation +4. Refactor existing code to use it + diff --git a/favicon.svg b/favicon.svg new file mode 100644 index 0000000..5ebcd38 --- /dev/null +++ b/favicon.svg @@ -0,0 +1,4 @@ + + ๐Ÿ‰ + + diff --git a/index.html b/index.html deleted file mode 100644 index b948858..0000000 --- a/index.html +++ /dev/null @@ -1,1256 +0,0 @@ - - - - - - Parseltongue 2.0 - LLM Payload Crafter - - - - - - - - -
-
- -
- - - - - - -
-
- -
-
- - - - - - -
- - -
-
-
- -
- - -
- - -
- -
- - - - - - - - - - - -
-
- -
- - - -
-
-

- - Encoded Message - using {{ selectedCarrier.name }} - using Invisible Text -

-
-
- - -
-
- Copy this text and share it. Only people who know how to decode it will be able to read your message. -
-
- - - -
-
-

- Universal Decoder - (Prioritizing {{ activeTransform.name }}) -

-

- Paste encoded text to decode with {{ activeTransform.name }} or try other methods -

-

Paste any encoded text to try all decoding methods at once

-
- -
- -
-
- Decoded using: {{ universalDecodeResult.method }} - (Priority Match) -
-
{{ universalDecodeResult.text }}
-
- - -
-
-
-
-
-
- - -
-
- -
- - -
- - -
-
-

Gibberish DictionaryTranslate text into random gibberish and corresponding dictionary.

-
-
- - - -
-
- -
-
-
- Gibberized Output: -
- - -
- Gibberish Dictionary: -
- - -
-
-
-
- - -
-
-

Gibberish by Removal Gibberish via character removal

-
- - -
- - -
- - -
-
- - - - - - -
-
- - -
-
-
-
- #{{ i+1 }} - - -
-
-
-
- - -
-
- - -
-
- -
-
-
-
- - -
-
-
-
-
-
-
- - -
-
-
-
-

Tokenizer Visualization {{ tokenizerEngine }}

-

Paste text to see how different tokenizers segment it.

-
-
- -
- -
- -
-
-

- Tokens - {{ tokenizerTokens.length }} total ยท {{ tokenizerWordCount }} words ยท {{ tokenizerCharCount }} chars -

-
-
-
- {{ i }} - {{ tok.text }} - #{{ tok.id }} -
-
-
- Tokens will appear here. -
-
-
-
- - -
-
-
-
-

- ๐Ÿ’ฅ Tokenade Generator - Craft dense token payloads with emojis and zero-width characters -

-
-
- - DISCLAIMER: Tokenade payloads can severely degrade model performance and crash UIs. Use for testing only. Do not deploy to production or target systems without explicit permission. -
-
- - - Danger zone: Estimated length {{ estimateTokenadeLength().toLocaleString() }} chars exceeds the safe threshold ({{ dangerThresholdTokens.toLocaleString() }}). - Generating this will very likely freeze/crash your browser or downstream tools. Proceed only if you fully understand the risks. - -
-
- - - - - -
-
- - - - - - - -
-
Separator
-
- - - - -
-
-
-
- - Estimated length: {{ estimateTokenadeLength().toLocaleString() }} chars - -
-
-
- Quick picks -
- -
-
- - - -
-
- - -
- - Length: {{ tokenBombOutput.length.toLocaleString() }} chars ยท Tip: Increasing depth/breadth grows size multiplicatively. - -
-
-
- -
-
- - -
-

Text Payload Generator

-
- - - - -
-
- - -
-
- -
-
-
-
- - -
-
-
-
-

Mutation Lab mutate text into diverse payloads

-
-
- - - -
-
- - - - - - - -
-
- - - -
-
-
-
- #{{ i+1 }} - - -
-
-
-
-
-
- -
-
-
- -
- -
-
-
Categories:
-
-
Encoding
-
Ciphers
-
Visual
-
Formatting
-
Unicode
-
Special
-
Fantasy
-
Ancient
-
Technical
-
๐ŸŽฒ Randomizer
-
-
-
- -
-

Encoding

-
-
- - -
-
-
- - -
-

Ciphers

-
-
- - -
-
-
- - -
-

Visual

-
-
- - -
-
-
- - -
-

Formatting

-
-
- - -
-
-
- - -
-

Unicode

-
-
- - -
-
-
- - -
-

Special

-
-
- - -
-
-
- - -
-

Fantasy

-
-
- - -
-
-
- - -
-

Ancient

-
-
- - -
-
-
- - -
-

Technical

-
-
- - -
-
-
- - -
-

๐ŸŽฒ Randomizer - Code Switching Magic!

-

- ๐ŸŒŸ Apply different transforms to each word in your sentence! Creates a polyglot mix of encodings. -

- -
-
- -
-
-
-
๐ŸŽฎ How it works:
-
    -
  • ๐Ÿ”€ Each word gets a random transform
  • -
  • ๐ŸŽฏ Mixes fantasy, ancient, and modern encodings
  • -
  • ๐Ÿ“ Preserves punctuation and spacing
  • -
  • ๐Ÿ” Check console for transform mapping details
  • -
-

Example: "Hello World!" โ†’ "SGVsbG8= แšนแšฉแšฑแ›šแ›ž!"

-
-
-
-
- -
-
-

- - Transformed Message - ({{ activeTransform.name }}) -

-
-
- - -
-
- Copy this text and share it. The transformation can be reversed using the Universal Decoder below. -
-
- - -
-
-

- Universal Decoder - (Prioritizing {{ activeTransform.name }}) -

-

- Paste encoded text to decode with {{ activeTransform.name }} or try other methods -

-

Paste any encoded text to try all decoding methods at once

-
-
- -
-
- Decoded using: {{ universalDecodeResult.method }} - (Priority Match) -
-
{{ universalDecodeResult.text }}
-
- - -
-
-
-
-
-
- - - -
-
-

Copy History

- -
-
-
-

No copy history yet. Use the app features to auto-copy content.

-
-
-
-
- {{ item.source }} - {{ item.timestamp }} -
-
- {{ item.content }} -
-
- -
-
-
-
-
-
- - - -
-
-

Advanced Settings

- -
-
- - - - - - - -
- - Applied -
- These options affect Unicode-based steganography encoding/decoding. -
-
- -
- - - - - - - - - - - - - - diff --git a/index.template.html b/index.template.html new file mode 100644 index 0000000..e96494c --- /dev/null +++ b/index.template.html @@ -0,0 +1,233 @@ + + + + + + Parseltongue 2.0 - LLM Payload Crafter + + + + + + + + + +
+
+ +
+ + + + + + +
+
+ +
+
+ + +
+ +
+
+ +
+ + +
+
+

Copy History

+
+ + +
+
+
+
+

No copy history yet. Use the app features to auto-copy content.

+
+
+
+
+ {{ item.source }} + {{ formatHistoryTime(item.timestamp) }} +
+
+ {{ item.content }} +
+
+ + +
+
+
+
+
+
+ + + +
+
+

Advanced Settings

+ +
+
+ + + + + + + +
+ + Applied +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/README.md b/js/README.md new file mode 100644 index 0000000..77424ab --- /dev/null +++ b/js/README.md @@ -0,0 +1,39 @@ +# JavaScript Directory Structure + +## Core Modules (`js/core/`) + +- `decoder.js` - Universal decoder for automatic encoding detection +- `steganography.js` - Emoji and invisible text steganography +- `emojiLibrary.js` - Emoji search, filtering, and library functions +- `toolRegistry.js` - Tool registration and Vue data/method merging + +## Utilities (`js/utils/`) + +- `clipboard.js` - `ClipboardUtils.copy()` - Clipboard API wrapper +- `focus.js` - `FocusUtils.focusWithoutScroll()`, `clearFocusAndSelection()` +- `history.js` - `HistoryUtils` - Copy history management +- `notifications.js` - `NotificationUtils` - Toast notifications +- `theme.js` - `ThemeUtils` - Dark/light theme management +- `escapeParser.js` - Escape sequence parsing + +## Tools (`js/tools/`) + +Tool classes extending `Tool` base class. Auto-discovered by `build/inject-tool-scripts.js`. + +## Data (`js/data/`) + +- `emojiData.js` - Generated emoji data (build output) +- `emojiCompatibility.js` - Emoji compatibility mappings + +## Bundles (`js/bundles/`) + +- `transforms-bundle.js` - Bundled transformer modules (build output) + +## Load Order + +1. Data files (emojiData, emojiCompatibility) +2. Generated bundles (transforms-bundle) +3. Utilities (escapeParser, focus, notifications, history, clipboard, theme) +4. Core modules (steganography, decoder, emojiLibrary) +5. Tool system (Tool.js, *Tool.js files, toolRegistry) +6. Main app (app.js) diff --git a/js/app.js b/js/app.js index 1a1ed49..4f69d7c 100644 --- a/js/app.js +++ b/js/app.js @@ -1,148 +1,67 @@ -// Initialize Vue app +const baseData = { + isDarkTheme: true, + activeTab: 'transforms', + registeredTools: [], + universalDecodeInput: '', + universalDecodeResult: null, + isPasteOperation: false, + lastCopyTime: 0, + ignoreKeyboardEvents: false, + isTransformCopy: false, + keyboardEventsTimeout: null, + showDecoder: true, + tbCarrierManual: '', + copyHistory: [], + maxHistoryItems: window.CONFIG.MAX_HISTORY_ITEMS, + showCopyHistory: false, + showUnicodePanel: false, + unicodeApplyBusy: false, + unicodeApplyFlash: false, + unicodePanelToggleLock: false, + unicodeApplyFlashTimeout: null, + showDangerModal: false, + dangerThresholdTokens: window.CONFIG.DANGER_THRESHOLD_TOKENS +}; + +const toolData = (window.toolRegistry && typeof window.toolRegistry.mergeVueData === 'function') + ? window.toolRegistry.mergeVueData() + : {}; +const mergedData = Object.assign({}, baseData, toolData); + +const toolMethods = (window.toolRegistry && typeof window.toolRegistry.mergeVueMethods === 'function') + ? window.toolRegistry.mergeVueMethods() + : {}; + window.app = new Vue({ el: '#app', - data: { - // Theme - isDarkTheme: true, - - // Tab Management - activeTab: 'transforms', - - // Transform Tab - transformInput: '', - transformOutput: '', - activeTransform: null, - // Transform categories for styling - transformCategories: { - encoding: ['Base64', 'Base64 URL', 'Base32', 'Base45', 'Base58', 'Base62', 'Binary', 'Hexadecimal', 'ASCII85', 'URL Encode', 'HTML Entities'], - cipher: ['Caesar Cipher', 'ROT13', 'ROT47', 'ROT18', 'ROT5', 'Morse Code', 'Atbash Cipher', 'Vigenรจre Cipher', 'Affine Cipher (a=5,b=8)', 'Rail Fence (3 Rails)', 'Baconian Cipher', 'Tap Code', 'A1Z26', 'QWERTY Right Shift'], - visual: ['Rainbow Text', 'Strikethrough', 'Underline', 'Reverse Text', 'Alternating Case', 'Reverse Words', 'Random Case', 'Title Case', 'Sentence Case', 'Emoji Speak', 'Ubbi Dubbi', 'Rรถvarsprรฅket', 'Vaporwave', 'Disemvowel'], - format: ['Pig Latin', 'Leetspeak', 'NATO Phonetic', 'camelCase', 'snake_case', 'kebab-case'], - unicode: ['Invisible Text', 'Upside Down', 'Full Width', 'Small Caps', 'Bubble', 'Braille', 'Greek Letters', 'Wingdings', 'Superscript', 'Subscript', 'Regional Indicator Letters', 'Fraktur', 'Cyrillic Stylized', 'Katakana', 'Hiragana', 'Roman Numerals'], - special: ['Medieval', 'Cursive', 'Monospace', 'Double-Struck', 'Elder Futhark', 'Mirror Text', 'Zalgo'], - fantasy: ['Quenya (Tolkien Elvish)', 'Tengwar Script', 'Klingon', 'Aurebesh (Star Wars)', 'Dovahzul (Dragon)'], - ancient: ['Hieroglyphics', 'Ogham (Celtic)', 'Semaphore Flags'], - technical: ['Brainfuck', 'Mathematical Notation', 'Chemical Symbols'], - randomizer: ['Random Mix'] - }, - // Be resilient if transforms.js fails to load - transforms: Object.entries(window.transforms || {}).map(([key, transform]) => ({ - name: transform.name, - func: transform.func.bind(transform), - preview: transform.preview.bind(transform) - })), - - // Steganography Tab - emojiMessage: '', - encodedMessage: '', - decodeInput: '', - decodedMessage: '', - selectedCarrier: null, - - // Universal Decoder - works on both tabs - universalDecodeInput: '', - universalDecodeResult: null, - isPasteOperation: false, // Flag to track paste operations - lastCopyTime: 0, // Timestamp of last copy operation for debounce - ignoreKeyboardEvents: false, // Flag to prevent keyboard events from triggering copies - isTransformCopy: false, // Flag to mark transform-initiated copy operations - keyboardEventsTimeout: null, // Timeout for resetting keyboard event flag - activeSteg: null, - carriers: window.steganography.carriers, - showDecoder: true, - // Emoji Library - filteredEmojis: [...window.emojiLibrary.EMOJI_LIST], - selectedEmoji: null, - carrierEmojiList: [...window.emojiLibrary.EMOJI_LIST], - quickCarrierEmojis: ['๐Ÿ','๐Ÿ‰','๐Ÿฒ','๐Ÿ”ฅ','๐Ÿ’ฅ','๐Ÿ—ฟ','โš“','โญ','โœจ','๐Ÿš€','๐Ÿ’€','๐Ÿชจ','๐Ÿƒ','๐Ÿชถ','๐Ÿ”ฎ','๐Ÿข','๐ŸŠ','๐ŸฆŽ'], - tbCarrierManual: '', - // Token Bomb Generator - tbDepth: 3, - tbBreadth: 4, - tbRepeats: 5, - tbSeparator: 'zwnj', - tbIncludeVS: true, - tbIncludeNoise: true, - tbRandomizeEmojis: true, // forced on; no UI control - tbAutoCopy: true, - tbSingleCarrier: true, - tbCarrier: '', - tbPayloadEmojis: [], - tokenBombOutput: '', - // Text Payload Generator - tpBase: '', - tpRepeat: 100, - tpCombining: true, - tpZW: false, - textPayload: '', - // Tokenizer tab - tokenizerInput: '', - tokenizerEngine: 'byte', - tokenizerTokens: [], - tokenizerCharCount: 0, - tokenizerWordCount: 0, - - // Fuzzer - fuzzerInput: '', - fuzzerCount: 20, - fuzzerSeed: '', - fuzzUseRandomMix: true, - fuzzZeroWidth: true, - fuzzUnicodeNoise: true, - fuzzZalgo: false, - fuzzWhitespace: true, - fuzzCasing: true, - fuzzEncodeShuffle: false, - fuzzerOutputs: [], - - // Gibberish Dictionary - gibberishInput: '', - gibberishOutput: '', - gibberishSeed: '', - gibberishDictionary: '', - gibberishChars: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', - gibberishMode: 'random', - - // Removal mode properties - removalSubMode: 'random', - removalInput: '', - removalVariations: 10, - removalMinLetters: 1, - removalMaxLetters: 3, - removalSeed: '', - removalOutputs: [], - - removalSpecificInput: '', - removalCharsToRemove: '', - removalSpecificOutput: '', - - // History of copied content - copyHistory: [], - maxHistoryItems: 10, - showCopyHistory: false, - showUnicodePanel: false, - unicodeApplyBusy: false, - unicodeApplyFlash: false, - - // Danger zone controls - showDangerModal: false, - dangerThresholdTokens: 25_000_000, - - // Copy operation tracking (moved from methods) - lastCopyTime: 0 - }, - methods: { - toggleUnicodePanel() { + data: mergedData, + methods: Object.assign({}, toolMethods || {}, { + toggleUnicodePanel(event) { + if (this.unicodePanelToggleLock) return; + this.unicodePanelToggleLock = true; + this.showUnicodePanel = !this.showUnicodePanel; const panel = document.getElementById('unicode-options-panel'); if (panel) { if (this.showUnicodePanel) panel.classList.add('active'); else panel.classList.remove('active'); } + + setTimeout(() => { + this.unicodePanelToggleLock = false; + }, 300); }, applyUnicodeOptions() { if (this.unicodeApplyBusy) return; + + if (this.unicodeApplyFlashTimeout) { + clearTimeout(this.unicodeApplyFlashTimeout); + this.unicodeApplyFlashTimeout = null; + } + + this.unicodeApplyFlash = false; this.unicodeApplyBusy = true; + try { const initSel = document.querySelector('.steg-initial-presentation'); const vs0Sel = document.querySelector('.steg-vs-zero'); @@ -152,10 +71,7 @@ window.app = new Vue({ const orderSel = document.querySelector('.steg-bit-order'); const trailSel = document.querySelector('.steg-trailing-zw'); - const parseEsc = (s)=>{ - if (!s) return s; - try { return eval(`'${s}'`); } catch(_) { return s; } - }; + const parseEsc = (s) => window.EscapeParser.parseEscapeSequence(s); if (window.steganography && window.steganography.setStegOptions) { window.steganography.setStegOptions({ @@ -168,29 +84,26 @@ window.app = new Vue({ trailingZW: parseEsc(trailSel && trailSel.value) || '' }); this.unicodeApplyFlash = true; - this.showNotification(' Advanced settings applied', 'success'); - setTimeout(()=>{ this.unicodeApplyFlash = false; }, 1200); + this.showNotification('Advanced settings applied', 'success', 'fas fa-sliders-h'); + this.unicodeApplyFlashTimeout = setTimeout(() => { + this.unicodeApplyFlash = false; + this.unicodeApplyFlashTimeout = null; + }, 1200); } else { - this.showNotification(' Engine missing setStegOptions()', 'warning'); + this.showNotification('Engine missing setStegOptions()', 'warning', 'fas fa-exclamation-triangle'); } } catch (e) { console.error('Apply Unicode options error', e); - this.showNotification(' Failed to apply settings', 'error'); - } finally { this.unicodeApplyBusy = false; } - }, - // Focus an element without causing the page to scroll - focusWithoutScroll(el) { - if (!el) return; - const x = window.scrollX, y = window.scrollY; - try { - el.focus({ preventScroll: true }); - } catch (e) { - el.focus(); - window.scrollTo(x, y); + this.showNotification('Failed to apply settings', 'error', 'fas fa-exclamation-triangle'); + this.unicodeApplyFlash = false; + } finally { + this.unicodeApplyBusy = false; } }, + focusWithoutScroll(el) { + window.FocusUtils.focusWithoutScroll(el); + }, - // Trigger randomizer chaos animation regardless of input triggerRandomizerChaos() { try { const section = document.getElementById('category-randomizer'); @@ -211,62 +124,27 @@ window.app = new Vue({ setTimeout(()=>section && section.classList.remove('shake-once','randomizer-glow'), 600); } catch(_) {} }, - // Switch between tabs with proper initialization switchToTab(tabName) { - this.activeTab = tabName; - console.log('Switched to tab:', tabName); + if (this.activeTab && window.toolRegistry) { + window.toolRegistry.deactivateTool(this.activeTab, this); + } - // Reset universal decoder input when switching tabs + this.activeTab = tabName; this.universalDecodeInput = ''; this.universalDecodeResult = null; - // Initialize emoji grid when switching to steganography tab - if (tabName === 'steganography') { - this.$nextTick(() => { - console.log('Tab switch: Initializing emoji grid'); - const emojiGridContainer = document.getElementById('emoji-grid-container'); - if (emojiGridContainer) { - console.log('Found emoji grid container after tab switch'); - // Make sure the container is visible - emojiGridContainer.setAttribute('style', 'display: block !important; visibility: visible !important; min-height: 300px; padding: 10px;'); - // Render the emoji grid - this.renderEmojiGrid(); - } else { - console.log('Emoji grid container not found after tab switch'); - } - }); - } - - // Initialize category navigation when switching to transforms tab - if (tabName === 'transforms') { - this.$nextTick(() => { - this.initializeCategoryNavigation(); - }); - } - if (tabName === 'tokenizer') { - this.$nextTick(() => this.runTokenizer()); + if (window.toolRegistry) { + window.toolRegistry.activateTool(tabName, this); } }, - // Get transforms grouped by category - getTransformsByCategory(category) { - return this.transforms.filter(transform => - this.transformCategories[category].includes(transform.name) - ); - }, - - // Theme Toggle toggleTheme() { - this.isDarkTheme = !this.isDarkTheme; - document.body.classList.toggle('light-theme'); + this.isDarkTheme = window.ThemeUtils.toggleTheme(this.isDarkTheme); }, - // Copy History Toggle toggleCopyHistory() { this.showCopyHistory = !this.showCopyHistory; - console.log('Copy history toggled:', this.showCopyHistory); - // If showing history panel, focus the first copy-again button if available if (this.showCopyHistory && this.copyHistory.length > 0) { this.$nextTick(() => { const firstCopyButton = document.querySelector('.copy-again-button'); @@ -277,2081 +155,219 @@ window.app = new Vue({ } }, - // Transform Methods - applyTransform(transform, event) { - // Prevent default button behavior and scrolling - event && event.preventDefault(); - event && event.stopPropagation(); - - // Always trigger chaos animation for Random Mix, even with empty input - if (transform && transform.name === 'Random Mix') { - this.triggerRandomizerChaos(); - } - - if (this.transformInput) { - // Update active transform and apply it - this.activeTransform = transform; - - if (transform.name === 'Random Mix') { - this.transformOutput = window.transforms.randomizer.func(this.transformInput); - // Show transform mapping info - const transformInfo = window.transforms.randomizer.getLastTransformInfo(); - if (transformInfo.length > 0) { - const transformsList = transformInfo.map(t => t.transformName).join(', '); - this.showNotification(` Mixed with: ${transformsList}`, 'success'); - console.log('Transform mapping:', transformInfo); - } - } else { - // Apply transform to full text - let the transform handle segmentation if needed - this.transformOutput = transform.func(this.transformInput); - } - - // Set flag to mark this as a transform-initiated copy - this.isTransformCopy = true; - - // Force copy the transform output to clipboard - this.forceCopyToClipboard(this.transformOutput); - - // Add to copy history - this.addToCopyHistory(`Transform: ${transform.name}`, this.transformOutput); - - // Enhanced notification for transform and copy (if not randomizer - it has its own notification) - if (transform.name !== 'Random Mix') { - this.showNotification(` ${transform.name} applied and copied!`, 'success'); - } - - // Remove active state from transform buttons - document.querySelectorAll('.transform-button').forEach(button => { - button.classList.remove('active'); - }); - - // Keep focus on input and move cursor to end - const inputBox = document.querySelector('#transform-input'); - if (inputBox) { - this.focusWithoutScroll(inputBox); - const len = inputBox.value.length; - try { inputBox.setSelectionRange(len, len); } catch (_) {} - } - - // Reset flags immediately - this.isTransformCopy = false; - this.ignoreKeyboardEvents = false; - } - }, - autoTransform() { - // Only proceed if we're in the transforms tab and have an active transform - if (this.transformInput && this.activeTransform && this.activeTab === 'transforms') { - // Handle text with proper Unicode segmentation - const segments = window.emojiLibrary.splitEmojis(this.transformInput); - const transformedSegments = segments.map(segment => { - // Skip transformation for emojis and complex Unicode characters - if (segment.length > 1 || /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}]/u.test(segment)) { - return segment; - } - return this.activeTransform.func(segment); - }); - - this.transformOutput = window.emojiLibrary.joinEmojis(transformedSegments); - } - }, - - // Check if a transform has a reverse function - transformHasReverse(transform) { - return transform && typeof transform.reverse === 'function'; - }, - - // Decode text using the specific transform's reverse function - decodeWithTransform(transform) { - if (!this.transformInput || !transform || !this.transformHasReverse(transform)) { - return; - } - - try { - // Handle text with proper Unicode segmentation - const segments = window.emojiLibrary.splitEmojis(this.transformInput); - const decodedSegments = segments.map(segment => { - // Skip decoding for emojis and complex Unicode characters - if (segment.length > 1 || /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}]/u.test(segment)) { - return segment; - } - return transform.reverse(segment); - }); - - const decodedText = window.emojiLibrary.joinEmojis(decodedSegments); - - if (decodedText !== this.transformInput) { - // Update the input with the decoded text - this.transformInput = decodedText; - - // Show a notification - this.showNotification(` Decoded using ${transform.name}`, 'success'); - - // Add to copy history - this.addToCopyHistory(`Decoded (${transform.name})`, decodedText); - } else { - this.showNotification(` Could not decode with ${transform.name}`, 'warning'); - } - } catch (error) { - console.error(`Error decoding with ${transform.name}:`, error); - this.showNotification(` Error decoding with ${transform.name}`, 'error'); - } - }, - - // Steganography Methods - selectCarrier(carrier) { - // Toggle carrier selection if clicking the same one again - if (this.selectedCarrier === carrier) { - this.selectedCarrier = null; - this.encodedMessage = ''; - } else { - this.selectedCarrier = carrier; - this.activeSteg = 'emoji'; - this.autoEncode(); - } - }, - setStegMode(mode) { - // For invisible text, make it a direct action (not a toggle) - if (mode === 'invisible') { - // Set the mode temporarily to generate the encoded message - this.activeSteg = mode; - // Clear any carrier selection - this.selectedCarrier = null; - // Generate the encoded message - this.autoEncode(); - - // Auto-copy the encoded message - if (this.encodedMessage) { - this.$nextTick(() => { - this.forceCopyToClipboard(this.encodedMessage); - this.showNotification(' Invisible text created and copied!', 'success'); - this.addToCopyHistory('Invisible Text', this.encodedMessage); - }); - } - } else { - // For other modes (like emoji), keep the toggle behavior - if (this.activeSteg === mode) { - this.activeSteg = null; - this.encodedMessage = ''; - } else { - this.activeSteg = mode; - this.autoEncode(); - } - } - }, - autoEncode() { - // Only proceed if we're in the steganography tab - if (!this.emojiMessage || this.activeTab !== 'steganography') { - this.encodedMessage = ''; - return; - } - - if (this.activeSteg === 'invisible') { - this.encodedMessage = window.steganography.encodeInvisible(this.emojiMessage); - // Auto-copy will be handled in setStegMode method - } else if (this.selectedCarrier) { - this.encodedMessage = window.steganography.encodeEmoji( - this.selectedCarrier.emoji, - this.emojiMessage - ); - // Auto-copy for emoji carrier is handled in selectEmoji method - } - }, - autoDecode() { - if (!this.decodeInput) { - this.decodedMessage = ''; - return; - } - - // Use the universal decoder - const result = this.universalDecode(this.decodeInput); - - if (result) { - this.decodedMessage = `Decoded (${result.method}): ${result.text}`; - - // Auto-copy decoded message to clipboard - this.$nextTick(() => { - // Only copy the actual decoded text, not the formatted message - const decodedText = result.text; - - if (decodedText) { - // Force clipboard copy regardless of event source - this.forceCopyToClipboard(decodedText); - this.showNotification(` Decoded message copied!`, 'success'); - - // Add to copy history - this.addToCopyHistory(`Decoded (${result.method})`, decodedText); - } - }); - } else { - this.decodedMessage = 'No encoded message detected'; - } - }, - previewInvisible(text) { - return '[invisible]'; - }, - - // Add to copy history functionality addToCopyHistory(source, content) { - // Create history item with timestamp - const historyItem = { - source: source, - content: content, - timestamp: new Date().toLocaleTimeString(), - date: new Date().toLocaleDateString() - }; - - // Add to beginning of array (most recent first) - this.copyHistory.unshift(historyItem); - - // Limit history to maxHistoryItems - if (this.copyHistory.length > this.maxHistoryItems) { - this.copyHistory.pop(); - } - - // Log history item for debugging - console.log('Added to copy history:', historyItem); + window.HistoryUtils.addToHistory( + this.copyHistory, + this.maxHistoryItems, + source, + content + ); }, - // Utility Methods - async copyToClipboard(text) { - if (!text) return; - - // Check clipboard lock - don't proceed if locked - if (this.clipboardLocked) { - console.log('Copy operation prevented by clipboard lock'); - return; - } - - // Prevent rapid successive copy operations (debounce) - const now = Date.now(); - if (now - this.lastCopyTime < 500) { - console.log('Copy operation debounced'); - return; - } - this.lastCopyTime = now; - - // Set clipboard lock immediately - this.clipboardLocked = true; - console.log('Setting clipboard lock during regular copy'); - - // Always try to copy, regardless of event source - try { - await navigator.clipboard.writeText(text); - - // Show a success notification - this.showNotification(' Copied!', 'success'); - - // Add to history - determine source from active tab or context - const source = this.activeTab === 'transforms' ? 'Transform' : 'Steganography'; - this.addToCopyHistory(source, text); - - // Aggressively clear focus and selections - if (document.activeElement && document.activeElement.blur) { - document.activeElement.blur(); - } - - // Clear any text selection - if (window.getSelection) { - window.getSelection().removeAllRanges(); - } - - // Focus body to avoid any specific interactive elements - document.body.focus(); - - // Release clipboard lock after a longer delay - setTimeout(() => { - this.clipboardLocked = false; - console.log('Clipboard lock released after regular copy'); - }, 500); - } catch (err) { - console.warn('Clipboard access not available:', err); - - // Try fallback method for copying (textarea method) - this.fallbackCopy(text); - } + clearCopyHistory() { + window.HistoryUtils.clearHistory(this.copyHistory); + this.showNotification('History cleared', 'success', 'fas fa-check'); }, - fallbackCopy(text) { - try { - // Check if keyboard events should be ignored - if (this.ignoreKeyboardEvents && !this.isTransformCopy) { - console.log('Ignoring fallback copy due to keyboard event flag'); - return; - } - - // Reset the transform flag if it was set - if (this.isTransformCopy) { - this.isTransformCopy = false; - } - - // Debounce check - const now = Date.now(); - if (now - this.lastCopyTime < 300) { - console.log('Fallback copy operation debounced'); - return; - } - this.lastCopyTime = now; - - // Create temporary textarea - const textarea = document.createElement('textarea'); - textarea.value = text; - textarea.style.position = 'fixed'; // Avoid scrolling to bottom - textarea.style.left = '-9999px'; // Move offscreen - textarea.style.top = '0'; - document.body.appendChild(textarea); - textarea.select(); - - // Try the copy command - const successful = document.execCommand('copy'); - - // Show appropriate notification - if (successful) { - this.showNotification(' Copied!', 'success'); - - // Add to history with context - let source = this.activeTab === 'transforms' ? 'Transform' : 'Steganography'; - if (this.activeTab === 'transforms' && this.activeTransform) { - source = `Transform: ${this.activeTransform.name}`; - } else if (this.activeTab === 'steganography') { - if (this.activeSteg === 'invisible') { - source = 'Invisible Text'; - } else if (this.selectedEmoji) { - source = `Emoji: ${this.selectedEmoji}`; - } + removeFromCopyHistory(id) { + window.HistoryUtils.removeFromHistory(this.copyHistory, id); + this.showNotification('Removed from history', 'success', 'fas fa-check'); + }, + + formatHistoryTime(timestamp) { + if (!timestamp) return ''; + const date = new Date(timestamp); + return date.toLocaleString(); + }, + + async copyToClipboard(text, skipHistory = false) { + if (!text || !window.ClipboardUtils) return; + + // Check if content already exists in history + const alreadyInHistory = this.copyHistory.some(item => item.content === text); + + const source = window.HistoryUtils.getHistorySource(this.activeTab, { + activeTransform: this.activeTransform, + activeSteg: this.activeSteg, + selectedEmoji: this.selectedEmoji + }); + + const success = await window.ClipboardUtils.copy(text, { + onSuccess: () => { + // Only add to history if not skipping and not already in history + if (!skipHistory && !alreadyInHistory) { + this.addToCopyHistory(source, text); } - this.addToCopyHistory(source, text); - } else { - this.showNotification(' Copy not supported', 'error'); + window.FocusUtils.clearFocusAndSelection(); } - - // Clean up - document.body.removeChild(textarea); - - // Aggressively clear focus and selection - if (document.activeElement && document.activeElement.blur) { - document.activeElement.blur(); - } - - // Clear any text selection - if (window.getSelection) { - window.getSelection().removeAllRanges(); - } - - // Focus on body element - document.body.focus(); - } catch (err) { - console.warn('Fallback copy method failed:', err); - this.showNotification(' Copy not supported', 'error'); - } + }); + + return success; }, - // Force copy to clipboard regardless of event context forceCopyToClipboard(text) { - if (!text) return; + if (!text || !window.ClipboardUtils) return; - // Skip copy operations during paste if (this.isPasteOperation) { this.isPasteOperation = false; return; } - // Block keyboard-triggered copies unless it's a transform if (!this.isTransformCopy && this.ignoreKeyboardEvents) { return; } - try { - // Use Clipboard API - if (navigator.clipboard && navigator.clipboard.writeText) { - // For emojis and complex characters, use a more robust approach - const processedText = typeof text === 'string' ? text : String(text); - - // Try to use the newer clipboard API methods if available - if (navigator.clipboard.write && processedText.match(/[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}]/u)) { - const blob = new Blob([processedText], { type: 'text/plain;charset=utf-8' }); - const clipboardItem = new ClipboardItem({ 'text/plain': blob }); - navigator.clipboard.write([clipboardItem]) - .then(() => { - if (this.isTransformCopy) { - this.showCopiedPopup(); - this.ignoreKeyboardEvents = true; - clearTimeout(this.keyboardEventsTimeout); - this.keyboardEventsTimeout = setTimeout(() => { - this.ignoreKeyboardEvents = false; - }, 1000); - } - this.isTransformCopy = false; - const inputBox = document.querySelector('#transform-input'); - if (inputBox) { - inputBox.focus(); - const len = inputBox.value.length; - inputBox.setSelectionRange(len, len); - } - }) - .catch(err => { - console.warn('Advanced Clipboard API failed:', err); - // Fall back to basic writeText - navigator.clipboard.writeText(processedText) - .then(() => { - if (this.isTransformCopy) { - this.showCopiedPopup(); - } - this.isTransformCopy = false; - const inputBox = document.querySelector('#transform-input'); - if (inputBox) { - inputBox.focus(); - const len = inputBox.value.length; - inputBox.setSelectionRange(len, len); - } - }) - .catch(err => { - console.warn('Basic Clipboard API failed:', err); - this.forceFallbackCopy(processedText); - }); - }); - } else { - navigator.clipboard.writeText(processedText) - .then(() => { - if (this.isTransformCopy) { - this.showCopiedPopup(); - } - this.isTransformCopy = false; - const inputBox = document.querySelector('#transform-input'); - if (inputBox) { - inputBox.focus(); - const len = inputBox.value.length; - inputBox.setSelectionRange(len, len); - } - }) - .catch(err => { - console.warn('Basic Clipboard API failed:', err); - this.forceFallbackCopy(processedText); - }); - } - } else { - this.forceFallbackCopy(text); - } - } catch (error) { - console.error('Force copy failed:', error); - this.forceFallbackCopy(text); - } - }, - - // Fallback copy method that doesn't rely on user-initiated events - forceFallbackCopy(text) { - try { - // If clipboard is locked, don't proceed - if (this.clipboardLocked) { - console.log('Fallback copy prevented by clipboard lock'); - return; - } - - // Set clipboard lock immediately - this.clipboardLocked = true; - - // Create temporary textarea for copying - const textarea = document.createElement('textarea'); - textarea.value = text; - - // Ensure proper emoji rendering - textarea.style.fontFamily = "'Segoe UI Emoji', 'Apple Color Emoji', sans-serif"; - textarea.style.fontSize = '16px'; - - // Position offscreen but with proper dimensions - textarea.style.position = 'fixed'; - textarea.style.left = '-9999px'; - textarea.style.top = '0'; - textarea.style.width = '100px'; - textarea.style.height = '100px'; - document.body.appendChild(textarea); - - // Focus and select the text - textarea.focus(); - textarea.select(); - - try { - document.execCommand('copy'); - console.log('Force fallback copy successful'); - } catch (err) { - console.error('Force fallback copy command failed:', err); - } - - // Remove the temporary element - document.body.removeChild(textarea); - - // Keep focus on input - const inputBox = document.querySelector('#transform-input'); - if (inputBox) { - inputBox.focus(); - const len = inputBox.value.length; - inputBox.setSelectionRange(len, len); - } - - // Reset flags immediately - this.clipboardLocked = false; - this.isTransformCopy = false; - this.ignoreKeyboardEvents = false; - console.log('Clipboard lock released after fallback copy'); - } catch (err) { - console.error('Force fallback copy method failed:', err); - this.clipboardLocked = false; // Make sure we don't leave it locked in case of error - } - }, - - // Notification system - showNotification(message, type = 'success') { - // Create notification element - const notification = document.createElement('div'); - notification.className = `copy-notification ${type}`; - notification.innerHTML = message; - document.body.appendChild(notification); + const source = window.HistoryUtils.getHistorySource(this.activeTab, { + activeTransform: this.activeTransform, + activeSteg: this.activeSteg, + selectedEmoji: this.selectedEmoji + }); - // Remove after animation - setTimeout(() => { - notification.classList.add('fade-out'); - setTimeout(() => { - if (notification.parentNode) { - document.body.removeChild(notification); + window.ClipboardUtils.copy(text, { + onSuccess: () => { + this.addToCopyHistory(source, text); + if (this.isTransformCopy) { + this.showCopiedPopup(); + this.ignoreKeyboardEvents = true; + clearTimeout(this.keyboardEventsTimeout); + this.keyboardEventsTimeout = setTimeout(() => { + this.ignoreKeyboardEvents = false; + }, window.CONFIG.KEYBOARD_EVENTS_TIMEOUT_MS); } - }, 300); - }, 1000); + this.isTransformCopy = false; + const inputBox = document.querySelector('#transform-input'); + if (inputBox) { + window.FocusUtils.focusWithoutScroll(inputBox); + const len = inputBox.value.length; + try { inputBox.setSelectionRange(len, len); } catch (_) {} + } + } + }); + }, + + showNotification(message, type = 'success', iconClass = null) { + window.NotificationUtils.showNotification(message, type, iconClass); }, - // Special prominent copy notification showCopiedPopup() { - // Create a more visible popup just for copy operations - const popup = document.createElement('div'); - popup.className = 'copy-popup'; - popup.innerHTML = ' Copied to clipboard!'; - - // Add to body - document.body.appendChild(popup); - - // Force it to be visible and centered - popup.style.position = 'fixed'; - popup.style.top = '50%'; - popup.style.left = '50%'; - popup.style.transform = 'translate(-50%, -50%)'; - popup.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'; - popup.style.color = 'white'; - popup.style.padding = '15px 25px'; - popup.style.borderRadius = '5px'; - popup.style.fontSize = '18px'; - popup.style.fontWeight = 'bold'; - popup.style.zIndex = '10000'; - popup.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.5)'; - popup.style.textAlign = 'center'; - - // Add fade-in animation - popup.style.opacity = '0'; - popup.style.transition = 'opacity 0.3s ease-in-out'; - - // Force reflow to make animation work - void popup.offsetWidth; - - // Fade in - popup.style.opacity = '1'; - - // Remove after a short delay - setTimeout(() => { - popup.style.opacity = '0'; - setTimeout(() => { - if (popup.parentNode) { - document.body.removeChild(popup); - } - }, 300); - }, 1500); + window.NotificationUtils.showCopiedPopup(); }, - // Run the universal decoder when input changes - runUniversalDecode() { - console.log('Running universal decoder with input:', this.universalDecodeInput); - - // Clear result if input is empty - if (!this.universalDecodeInput) { - this.universalDecodeResult = null; - return; - } - - // Try to decode using the currently selected transform first, if any - if (this.activeTransform && this.transformHasReverse(this.activeTransform)) { - try { - console.log(`Trying to decode with currently selected transform: ${this.activeTransform.name}`); - const decodedText = this.activeTransform.reverse(this.universalDecodeInput); - - // If the decoded text is different from the input and looks like readable text - if (decodedText !== this.universalDecodeInput && /[a-zA-Z0-9\s]{3,}/.test(decodedText)) { - this.universalDecodeResult = { - text: decodedText, - method: this.activeTransform.name - }; - console.log(`Successfully decoded with ${this.activeTransform.name}`); - return; - } - } catch (e) { - console.error(`Error decoding with selected transform ${this.activeTransform.name}:`, e); - } - } - - // If the selected transform didn't work or there isn't one selected, - // fall back to trying all available methods - const result = this.universalDecode(this.universalDecodeInput); - - // Update the result - this.universalDecodeResult = result; - - // Log the result - if (result) { - console.log(`Universal decoder found a match: ${result.method}`); - } else { - console.log('Universal decoder could not decode the input'); - } - }, - - // Universal Decoder - tries all decoding methods - universalDecode(input) { - if (!input) return ''; - - // Try all decoders in order - - // 1. Try steganography decoders - // - Check for emoji steganography first - // The emoji encoding uses variation selectors which are hard to see - if (/[\u{1F300}-\u{1F6FF}\u{2600}-\u{26FF}]/u.test(input)) { - console.log('Detected emoji, attempting to decode...'); - const decoded = window.steganography.decodeEmoji(input); - if (decoded) { - console.log('Successfully decoded emoji:', decoded); - return { text: decoded, method: 'Emoji Steganography' }; - } else { - console.log('Emoji detected but no hidden message found'); - } - } - - // - Invisible text (only check if the input actually contains invisible characters) - if (/[\uE0000-\uE007F]/.test(input)) { - let decoded = window.steganography.decodeInvisible(input); - if (decoded && decoded.length > 0) { - return { text: decoded, method: 'Invisible Text' }; - } - } - - // 2. Try transform reversals - // Try to decode using active transform first - if (this.activeTab === 'transforms' && this.activeTransform) { - try { - const transformKey = Object.keys(window.transforms).find( - key => window.transforms[key].name === this.activeTransform.name - ); - - if (transformKey && window.transforms[transformKey].reverse) { - const result = window.transforms[transformKey].reverse(input); - if (result && result !== input) { - return { - text: result, - method: this.activeTransform.name, - priorityMatch: true - }; - } - } - } catch (e) { - console.error('Error decoding with active transform:', e); - } - } - - // 3. Smart pattern detection for new transforms - // Check for specific patterns that indicate certain transform types - - // - Check for fantasy language patterns - if (/[แšชแ›’แ›ฒแ›žแ›–แš แšทแšบแ›แ›ƒแ›šแ›—แšพแ›Ÿแ›ˆแ›ฉแšฑแ›‹แ›แšขแ›ฉแ›‰]/.test(input)) { - // This looks like Tengwar or Elder Futhark runes - try { - if (window.transforms.tengwar && window.transforms.tengwar.reverse) { - const result = window.transforms.tengwar.reverse(input); - if (result !== input && /[a-zA-Z0-9]/.test(result)) { - return { text: result, method: 'Tengwar Script', priorityMatch: true }; - } - } - if (window.transforms.elder_futhark && window.transforms.elder_futhark.reverse) { - const result = window.transforms.elder_futhark.reverse(input); - if (result !== input && /[a-zA-Z0-9]/.test(result)) { - return { text: result, method: 'Elder Futhark', priorityMatch: true }; - } - } - } catch (e) { - console.error('Rune decode error:', e); - } - } - - // - Check for hieroglyphic patterns - if (/[๐“ƒญ๐“ƒฎ๐“ƒฏ๐“ƒฐ๐“ƒฑ๐“ƒฒ๐“ƒณ๐“ƒด๐“ƒต๐“ƒถ๐“ƒท๐“ƒธ๐“ƒน๐“ƒบ๐“ƒป๐“ƒผ]/.test(input)) { - try { - if (window.transforms.hieroglyphics && window.transforms.hieroglyphics.reverse) { - const result = window.transforms.hieroglyphics.reverse(input); - if (result !== input && /[a-zA-Z0-9]/.test(result)) { - return { text: result, method: 'Hieroglyphics', priorityMatch: true }; - } - } - } catch (e) { - console.error('Hieroglyphics decode error:', e); - } - } - - // - Check for Ogham patterns - if (/[แšแšแš‰แš‡แš“แšƒแšŒแš†แš”แšˆแšŠแš‚แš‹แš…แš‘แššแšแš„]/.test(input)) { - try { - if (window.transforms.ogham && window.transforms.ogham.reverse) { - const result = window.transforms.ogham.reverse(input); - if (result !== input && /[a-zA-Z0-9]/.test(result)) { - return { text: result, method: 'Ogham (Celtic)', priorityMatch: true }; - } - } - } catch (e) { - console.error('Ogham decode error:', e); - } - } - - // - Check for mathematical notation patterns - if (/[๐’ถ๐’ท๐’ธ๐’น๐‘’๐’ป๐‘”๐’ฝ๐’พ๐’ฟ๐“€๐“๐“‚๐“ƒ๐‘œ๐“…๐“†๐“‡๐“ˆ๐“‰๐“Š๐“‹๐“Œ๐“๐“Ž๐“๐’œโ„ฌ๐’ž๐’Ÿโ„ฐโ„ฑ๐’ขโ„‹โ„๐’ฅ๐’ฆโ„’โ„ณ๐’ฉ๐’ช๐’ซ๐’ฌโ„›๐’ฎ๐’ฏ๐’ฐ๐’ฑ๐’ฒ๐’ณ๐’ด๐’ต]/.test(input)) { - try { - if (window.transforms.mathematical && window.transforms.mathematical.reverse) { - const result = window.transforms.mathematical.reverse(input); - if (result !== input && /[a-zA-Z0-9]/.test(result)) { - return { text: result, method: 'Mathematical Notation', priorityMatch: true }; - } - } - } catch (e) { - console.error('Mathematical notation decode error:', e); - } - } - - // - Check for chemical symbol patterns - if (/^(Ac|B|C|D|Es|F|Ge|H|I|J|K|L|Mn|N|O|P|Q|R|S|Ti|U|V|W|Xe|Y|Zn|AC|ES|GE|MN|TI|XE)\s*$/.test(input.trim())) { - try { - if (window.transforms.chemical && window.transforms.chemical.reverse) { - const result = window.transforms.chemical.reverse(input); - if (result !== input && /[a-zA-Z0-9]/.test(result)) { - return { text: result, method: 'Chemical Symbols', priorityMatch: true }; - } - } - } catch (e) { - console.error('Chemical symbols decode error:', e); - } - } - - // - Binary (improved with more patterns) - if (/^[01\s]+$/.test(input.trim())) { - try { - // Use binary transform's reverse function if available - if (window.transforms.binary && window.transforms.binary.reverse) { - const result = window.transforms.binary.reverse(input); - if (result && /[\x20-\x7E]{3,}/.test(result)) { // Make sure it's readable ASCII - return { text: result, method: 'Binary' }; - } - } - - // Try different binary formats (with and without spaces) - const variations = [ - input.trim(), // Original input - input.replace(/\s+/g, ''), // No spaces - input.replace(/([01]{8})/g, '$1 ') // Force 8-bit spacing - ]; - - for (const binVariation of variations) { - // Fallback implementation - const binText = binVariation.replace(/\s+/g, ''); - let result = ''; - - // Try standard 8-bit ASCII - for (let i = 0; i < binText.length; i += 8) { - const byte = binText.substr(i, 8); - if (byte.length === 8) { - result += String.fromCharCode(parseInt(byte, 2)); - } - } - - if (result && /[\x20-\x7E]{3,}/.test(result)) { // Make sure it's readable ASCII - return { text: result, method: 'Binary' }; - } - } - } catch (e) { - console.error('Binary decode error:', e); - } - } - - // - Morse code - if (/^[.\-\s\/]+$/.test(input.trim())) { - try { - // Use morse transform's reverse function if available - if (window.transforms.morse && window.transforms.morse.reverse) { - const result = window.transforms.morse.reverse(input); - if (result !== input && /[a-zA-Z0-9]/.test(result)) { - return { text: result, method: 'Morse Code' }; - } - } - } catch (e) { - console.error('Morse decode error:', e); - } - } - - // - Braille - const braillePattern = /[โ €-โฃฟ]/; - if (braillePattern.test(input)) { - try { - // Count how many braille characters are in the input - const brailleMatches = [...input.matchAll(/[โ €-โฃฟ]/g)]; - // Only proceed if there are enough braille characters (to avoid false positives) - if (brailleMatches.length > 2) { - // Create a reverse mapping for braille - const brailleReverseMap = {}; - if (window.transforms.braille && window.transforms.braille.map) { - for (const [key, value] of Object.entries(window.transforms.braille.map)) { - brailleReverseMap[value] = key; - } - - // Decode the braille - let result = ''; - for (const char of input) { - result += brailleReverseMap[char] || char; - } - - if (result !== input && /[a-zA-Z0-9]/.test(result)) { - return { text: result, method: 'Braille' }; - } - } - } - } catch (e) { - console.error('Braille decode error:', e); - } - } - - // - Base64 - if (/^[A-Za-z0-9+/=]+$/.test(input.trim())) { - try { - // Attempt to decode as base64 - const result = atob(input.trim()); - // Check if result is readable text - if (/[\x20-\x7E]{3,}/.test(result)) { // At least 3 readable ASCII chars - return { text: result, method: 'Base64' }; - } - } catch (e) { - // Not valid base64, continue to next decoder - console.error('Base64 decode error:', e); - } - } - - // - Base58 - if (/^[1-9A-HJ-NP-Za-km-z]+$/.test(input.trim())) { - try { - if (window.transforms.base58 && window.transforms.base58.reverse) { - const result = window.transforms.base58.reverse(input.trim()); - if (result && /[\x20-\x7E]{3,}/.test(result)) { - return { text: result, method: 'Base58' }; - } - } - } catch (e) { - console.error('Base58 decode error:', e); - } - } - - // - Base62 - if (/^[0-9A-Za-z]+$/.test(input.trim())) { - try { - if (window.transforms.base62 && window.transforms.base62.reverse) { - const result = window.transforms.base62.reverse(input.trim()); - if (result && /[\x20-\x7E]{3,}/.test(result)) { - return { text: result, method: 'Base62' }; - } - } - } catch (e) { - console.error('Base62 decode error:', e); - } - } - - // - Upside Down text - if (window.transforms.upside_down && window.transforms.upside_down.reverse) { - try { - const result = window.transforms.upside_down.reverse(input); - // Check if the result is significantly different - if (result !== input && result.length > 3 && /[a-zA-Z0-9\s]{3,}/.test(result)) { - return { text: result, method: 'Upside Down' }; - } - } catch (e) { - console.error('Upside Down decode error:', e); - } - } - - // - Small Caps (create reverse mapping since there's no built-in decoder) - if (window.transforms.small_caps && window.transforms.small_caps.map) { - try { - // Create reverse mapping - const smallCapsReverseMap = {}; - for (const [key, value] of Object.entries(window.transforms.small_caps.map)) { - smallCapsReverseMap[value] = key; - } - - // Check if input contains small caps characters - const smallCapsChars = Object.values(window.transforms.small_caps.map); - const hasSmallCaps = smallCapsChars.some(char => input.includes(char)); - - if (hasSmallCaps) { - // Decode text - let result = ''; - for (const char of input) { - result += smallCapsReverseMap[char] || char; - } - - if (result !== input && /[a-zA-Z]/.test(result)) { - return { text: result, method: 'Small Caps' }; - } - } - } catch (e) { - console.error('Small Caps decode error:', e); - } - } - - // - Bubble text (create reverse mapping) - if (window.transforms.bubble && window.transforms.bubble.map) { - try { - // Create reverse mapping - const bubbleReverseMap = {}; - for (const [key, value] of Object.entries(window.transforms.bubble.map)) { - bubbleReverseMap[value] = key; - } - - // Check if input contains bubble characters - const bubbleChars = Object.values(window.transforms.bubble.map); - const hasBubbleChars = bubbleChars.some(char => input.includes(char)); - - if (hasBubbleChars) { - // Decode text - let result = ''; - for (const char of input) { - result += bubbleReverseMap[char] || char; - } - - if (result !== input && /[a-zA-Z]/.test(result)) { - return { text: result, method: 'Bubble' }; - } - } - } catch (e) { - console.error('Bubble decode error:', e); - } - } - - // Check for specific new transforms before trying the generic approach - - // - Hexadecimal - if (/^[0-9A-Fa-f\s]+$/.test(input.trim())) { - try { - if (window.transforms.hex && window.transforms.hex.reverse) { - const result = window.transforms.hex.reverse(input); - if (result && /[\x20-\x7E]{3,}/.test(result)) { - return { text: result, method: 'Hexadecimal' }; - } - } - } catch (e) { - console.error('Hex decode error:', e); - } - } - - // - URL Encoded - if (/%[0-9A-Fa-f]{2}/.test(input)) { - try { - if (window.transforms.url && window.transforms.url.reverse) { - const result = window.transforms.url.reverse(input); - if (result !== input && /[\x20-\x7E]{3,}/.test(result)) { - return { text: result, method: 'URL Encoded' }; - } - } else { - // Fallback implementation - try { - const result = decodeURIComponent(input); - if (result !== input && /[\x20-\x7E]{3,}/.test(result)) { - return { text: result, method: 'URL Encoded' }; - } - } catch (e) { - console.error('URL decode fallback error:', e); - } - } - } catch (e) { - console.error('URL decode error:', e); - } - } - - // - HTML Entities - if (/&[#a-zA-Z0-9]+;/.test(input)) { - try { - if (window.transforms.html && window.transforms.html.reverse) { - const result = window.transforms.html.reverse(input); - if (result !== input && /[\x20-\x7E]{3,}/.test(result)) { - return { text: result, method: 'HTML Entities' }; - } - } - } catch (e) { - console.error('HTML entities decode error:', e); - } - } - - // - ROT13/Caesar Cipher (check if decoding produces more common English words) - if (/^[a-zA-Z\s.,!?]+$/.test(input)) { - try { - // Try ROT13 first as it's more common - if (window.transforms.rot13 && window.transforms.rot13.reverse) { - const result = window.transforms.rot13.reverse(input); - if (result !== input) { - return { text: result, method: 'ROT13' }; - } - } - - // Then try Caesar cipher - if (window.transforms.caesar && window.transforms.caesar.reverse) { - const result = window.transforms.caesar.reverse(input); - if (result !== input) { - return { text: result, method: 'Caesar Cipher' }; - } - } - } catch (e) { - console.error('Cipher decode error:', e); - } - } - - // - Base32 - if (/^[A-Z2-7=]+$/.test(input.trim())) { - try { - if (window.transforms.base32 && window.transforms.base32.reverse) { - const result = window.transforms.base32.reverse(input); - if (result && /[\x20-\x7E]{3,}/.test(result)) { - return { text: result, method: 'Base32' }; - } - } - } catch (e) { - console.error('Base32 decode error:', e); - } - } - - // - ASCII85 - if (/^<~.*~>$/.test(input.trim())) { - try { - if (window.transforms.ascii85 && window.transforms.ascii85.reverse) { - const result = window.transforms.ascii85.reverse(input); - if (result && /[\x20-\x7E]{3,}/.test(result)) { - return { text: result, method: 'ASCII85' }; - } - } - } catch (e) { - console.error('ASCII85 decode error:', e); - } - } - - // - Check for Zalgo text (text with combining marks) - const combiningMarksRegex = /[\u0300-\u036f\u1ab0-\u1aff\u1dc0-\u1dff\u20d0-\u20ff\ufe20-\ufe2f]/; - if (combiningMarksRegex.test(input)) { - try { - // Count the number of combining marks to ensure it's actually Zalgo text - // and not just text with a few accents - const matches = input.match(combiningMarksRegex) || []; - if (matches.length > 3) { // Threshold to distinguish Zalgo from normal accented text - // Fallback implementation to remove combining marks - const result = input.replace(/[\u0300-\u036f\u1ab0-\u1aff\u1dc0-\u1dff\u20d0-\u20ff\ufe20-\ufe2f]/g, ''); - if (result !== input && result.length > 0) { - return { text: result, method: 'Zalgo' }; - } - } - } catch (e) { - console.error('Zalgo decode error:', e); - } - } - - // - Check for various Unicode text styles (medieval, cursive, monospace, double-struck) - const unicodeStyleChecks = [ - { name: 'Medieval', transform: 'medieval' }, - { name: 'Cursive', transform: 'cursive' }, - { name: 'Monospace', transform: 'monospace' }, - { name: 'Double-Struck', transform: 'doubleStruck' } - ]; - - for (const style of unicodeStyleChecks) { - if (window.transforms[style.transform] && window.transforms[style.transform].map) { - try { - // Create reverse mapping - const reverseMap = {}; - for (const [key, value] of Object.entries(window.transforms[style.transform].map)) { - reverseMap[value] = key; - } - - // Check if input contains characters from this style - const styleChars = Object.values(window.transforms[style.transform].map); - const hasStyleChars = styleChars.some(char => input.includes(char)); - - if (hasStyleChars) { - // Decode text - let result = ''; - for (const char of input) { - result += reverseMap[char] || char; - } - - if (result !== input && /[a-zA-Z0-9]/.test(result)) { - return { text: result, method: style.name }; - } - } - } catch (e) { - console.error(`${style.name} decode error:`, e); - } - } - } - - // - Check for Fantasy Languages - const fantasyLanguageChecks = [ - { name: 'Quenya (Tolkien Elvish)', transform: 'quenya' }, - { name: 'Tengwar Script', transform: 'tengwar' }, - { name: 'Klingon', transform: 'klingon' }, - { name: 'Dovahzul (Dragon)', transform: 'dovahzul' } - ]; - - for (const language of fantasyLanguageChecks) { - if (window.transforms[language.transform] && window.transforms[language.transform].map) { - try { - // Create reverse mapping - const reverseMap = {}; - for (const [key, value] of Object.entries(window.transforms[language.transform].map)) { - reverseMap[value] = key; - } - - // Check if input contains characters from this language - const languageChars = Object.values(window.transforms[language.transform].map); - const hasLanguageChars = languageChars.some(char => input.includes(char)); - - if (hasLanguageChars) { - // Decode text - let result = ''; - for (const char of input) { - result += reverseMap[char] || char; - } - - if (result !== input && /[a-zA-Z0-9]/.test(result)) { - return { text: result, method: language.name }; - } - } - } catch (e) { - console.error(`${language.name} decode error:`, e); - } - } - } - - // - Check for Aurebesh (Star Wars) - special case due to word-based mapping - if (window.transforms.aurebesh && window.transforms.aurebesh.map) { - try { - // Check if input contains Aurebesh words - const aurebeshWords = Object.values(window.transforms.aurebesh.map); - const hasAurebeshWords = aurebeshWords.some(word => - input.toLowerCase().includes(word.toLowerCase()) - ); - - if (hasAurebeshWords) { - const result = window.transforms.aurebesh.reverse(input); - if (result !== input && /[a-zA-Z0-9]/.test(result)) { - return { text: result, method: 'Aurebesh (Star Wars)' }; - } - } - } catch (e) { - console.error('Aurebesh decode error:', e); - } - } - - // - Check for Ancient Scripts - const ancientScriptChecks = [ - { name: 'Hieroglyphics', transform: 'hieroglyphics' }, - { name: 'Ogham (Celtic)', transform: 'ogham' }, - { name: 'Elder Futhark', transform: 'elder_futhark' } - ]; - - for (const script of ancientScriptChecks) { - if (window.transforms[script.transform] && window.transforms[script.transform].map) { - try { - // Create reverse mapping - const reverseMap = {}; - for (const [key, value] of Object.entries(window.transforms[script.transform].map)) { - reverseMap[value] = key; - } - - // Check if input contains characters from this script - const scriptChars = Object.values(window.transforms[script.transform].map); - const hasScriptChars = scriptChars.some(char => input.includes(char)); - - if (hasScriptChars) { - // Decode text - let result = ''; - for (const char of input) { - result += reverseMap[char] || char; - } - - if (result !== input && /[a-zA-Z0-9]/.test(result)) { - return { text: result, method: script.name }; - } - } - } catch (e) { - console.error(`${script.name} decode error:`, e); - } - } - } - - // - Check for Technical Codes - const technicalCodeChecks = [ - { name: 'Mathematical Notation', transform: 'mathematical' }, - { name: 'Chemical Symbols', transform: 'chemical' } - ]; - - for (const code of technicalCodeChecks) { - if (window.transforms[code.transform] && window.transforms[code.transform].map) { - try { - // Create reverse mapping - const reverseMap = {}; - for (const [key, value] of Object.entries(window.transforms[code.transform].map)) { - reverseMap[value] = key; - } - - // Check if input contains characters from this code - const codeChars = Object.values(window.transforms[code.transform].map); - const hasCodeChars = codeChars.some(char => input.includes(char)); - - if (hasCodeChars) { - // Decode text - let result = ''; - for (const char of input) { - result += reverseMap[char] || char; - } - - if (result !== input && /[a-zA-Z0-9]/.test(result)) { - return { text: result, method: code.name }; - } - } - } catch (e) { - console.error(`${code.name} decode error:`, e); - } - } - } - - // - Check for Brainfuck (special case - look for brainfuck patterns) - if (window.transforms.brainfuck) { - try { - // Brainfuck typically contains lots of +, -, <, >, [, ], ., and , - const brainfuckPattern = /^[+\-<>\[\].,\s]+$/; - if (brainfuckPattern.test(input.trim()) && input.length > 20) { - // This looks like brainfuck code, but we can't easily reverse it - // Just indicate that it was detected - return { text: '[Brainfuck code detected - cannot decode]', method: 'Brainfuck' }; - } - } catch (e) { - console.error('Brainfuck detection error:', e); - } - } - - // - Check for Semaphore Flags (special case - look for flag emojis) - if (window.transforms.semaphore) { - try { - // Look for flag-like characters or emojis - const flagPattern = /[๐Ÿ”„๐Ÿšฉ๐Ÿ๐Ÿด๐Ÿณ๏ธ]/; - if (flagPattern.test(input)) { - return { text: '[Semaphore flags detected]', method: 'Semaphore Flags' }; - } - } catch (e) { - console.error('Semaphore detection error:', e); - } - } - - // - Try reverse each transform that has a built-in reverse function - for (const name in window.transforms) { - const transform = window.transforms[name]; - if (transform.reverse) { - try { - const result = transform.reverse(input); - // Only return if the result is different and contains readable characters - if (result !== input && /[a-zA-Z0-9\s]{3,}/.test(result)) { - return { text: result, method: transform.name }; - } - } catch (e) { - console.error(`Error decoding with ${name}:`, e); - } - } - } - - // 4. Mixed/Randomized text decoding (token-wise decoding) - // Split on whitespace and common punctuation, keep separators - const tokens = input.split(/(\s+|[\.,!?:;()\[\]{}])/); - if (tokens.length > 1) { - const decodedTokens = tokens.map(tok => { - // Skip separators - if (!tok || /^(\s+|[\.,!?:;()\[\]{}])$/.test(tok)) return tok; - - // Try specific pattern checks first for token - const quick = this.universalDecode(tok); - if (quick && quick.text) return quick.text; - - // Fallback: try all reverses for token - for (const name in window.transforms) { - const transform = window.transforms[name]; - if (transform.reverse) { - try { - const r = transform.reverse(tok); - if (r && r !== tok && /[a-zA-Z0-9\s]{1,}/.test(r)) return r; - } catch (_) {} - } - } - return tok; - }); - const joined = decodedTokens.join(''); - if (joined !== input && /[a-zA-Z0-9\s]{3,}/.test(joined)) { - return { text: joined, method: 'Mixed (token-wise)' }; - } - } - - return null; - }, - - // Emoji Library Methods - filterEmojis() { - // Always show all emojis - search functionality removed - this.filteredEmojis = [...window.emojiLibrary.EMOJI_LIST]; - this.renderEmojiGrid(); - }, - - selectEmoji(emoji) { - // Directly copy the emoji to clipboard - ensure it's a string - const emojiStr = String(emoji); - - // Special handling for emoji characters - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(emojiStr) - .then(() => { - this.showNotification(` Emoji copied!`, 'success'); - this.addToCopyHistory('Emoji', emojiStr); - }) - .catch(err => { - console.warn('Emoji clipboard API failed:', err); - // Fallback to our custom method - this.forceCopyToClipboard(emojiStr); - this.showNotification(` Emoji copied!`, 'success'); - this.addToCopyHistory('Emoji', emojiStr); - }); - } else { - // Use our custom method if Clipboard API not available - this.forceCopyToClipboard(emojiStr); - this.showNotification(` Emoji copied!`, 'success'); - this.addToCopyHistory('Emoji', emojiStr); - } - - // Also set up carrier if we're in steganography mode - if (this.activeTab === 'steganography') { - this.selectedEmoji = emoji; - - // Create a temporary carrier for this emoji - const tempCarrier = { - name: `${emoji} Carrier`, - emoji: emoji, - encode: (text) => this.steganography.encode(text, emoji), - decode: (text) => this.steganography.decode(text), - preview: (text) => `${emoji}${text}${emoji}` - }; - - // Use this emoji as carrier - this.selectedCarrier = tempCarrier; - this.activeSteg = 'emoji'; - - // Encode the message with this emoji if we have one - if (this.emojiMessage) { - this.autoEncode(); - - // Wait for encoding to complete, then copy to clipboard - this.$nextTick(() => { - if (this.encodedMessage) { - const encodedStr = String(this.encodedMessage); - - // Use native clipboard API first for better emoji support - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(encodedStr) - .then(() => { - this.showNotification(` Hidden message copied with ${emoji}`, 'success'); - this.addToCopyHistory(`Hidden Message with ${emoji}`, encodedStr); - }) - .catch(err => { - console.warn('Encoded emoji clipboard API failed:', err); - // Fall back to our custom method - this.forceCopyToClipboard(encodedStr); - this.showNotification(` Hidden message copied with ${emoji}`, 'success'); - this.addToCopyHistory(`Hidden Message with ${emoji}`, encodedStr); - }); - } else { - // Use our custom method if Clipboard API not available - this.forceCopyToClipboard(encodedStr); - this.showNotification(` Hidden message copied with ${emoji}`, 'success'); - this.addToCopyHistory(`Hidden Message with ${emoji}`, encodedStr); - } - } - }); - } - } - }, - - renderEmojiGrid() { - console.log('renderEmojiGrid called with', this.filteredEmojis.length, 'emojis'); - - // Make sure container exists - const container = document.getElementById('emoji-grid-container'); - if (!container) { - console.error('emoji-grid-container not found!'); - return; - } - - // Force container to be completely visible - container.style.cssText = 'display: block !important; visibility: visible !important; min-height: 300px;'; - - // Make sure parent containers are visible too - const emojiLibrary = document.querySelector('.emoji-library'); - if (emojiLibrary) { - emojiLibrary.style.cssText = 'display: block !important; visibility: visible !important;'; - } - - // Clear any existing content to avoid duplication - container.innerHTML = ''; - - // Render the emoji grid - window.emojiLibrary.renderEmojiGrid('emoji-grid-container', this.selectEmoji.bind(this), this.filteredEmojis); - - // Message about copying has been removed as requested - - // Log success - console.log('Emoji grid rendered successfully'); - }, - - // Initialize category navigation for transform sections - initializeCategoryNavigation() { - this.$nextTick(() => { - console.log('Initializing category navigation'); - const legendItems = document.querySelectorAll('.transform-category-legend .legend-item'); - - // First, remove any existing event listeners to prevent duplicates - legendItems.forEach(item => { - const newItem = item.cloneNode(true); - item.parentNode.replaceChild(newItem, item); - }); - - // Now add event listeners to the fresh elements - document.querySelectorAll('.transform-category-legend .legend-item').forEach(item => { - item.addEventListener('click', () => { - const targetId = item.getAttribute('data-target'); - if (targetId) { - const targetElement = document.getElementById(targetId); - if (targetElement) { - // Add active class to the clicked legend item - document.querySelectorAll('.transform-category-legend .legend-item').forEach(li => { - li.classList.remove('active-category'); - }); - item.classList.add('active-category'); - - // Get height of .input-section so we can offset the scroll - const inputSection = document.querySelector('.input-section'); - const inputSectionHeight = inputSection.offsetHeight; - - // Calculate the target scroll position with offset - const elementPosition = targetElement.getBoundingClientRect().top + window.pageYOffset; - const offsetPosition = elementPosition - inputSectionHeight - 10; // Extra 10px padding - - // Scroll to the calculated position - window.scrollTo({ - top: offsetPosition, - behavior: 'smooth' - }); - - // Highlight the section briefly to draw attention - targetElement.classList.add('highlight-section'); - setTimeout(() => { - targetElement.classList.remove('highlight-section'); - }, 1000); - } - } - }); - }); - }); - } - , - // -------- Fuzzer -------- - seededRandomFactory(seedStr) { - if (!seedStr) return Math.random; - let h = 1779033703 ^ seedStr.length; - for (let i=0;i>> 19); - } - return function() { - h ^= h >>> 16; h = Math.imul(h, 2246822507); h ^= h >>> 13; h = Math.imul(h, 3266489909); h ^= h >>> 16; - return (h >>> 0) / 4294967296; - }; - }, - pick(arr, rnd) { return arr[Math.floor(rnd()*arr.length)]; }, - injectZeroWidth(text, rnd) { - const zw = ['\u200B','\u200C','\u200D','\u2060']; - return [...text].map(ch => (rnd()<0.2 ? ch+this.pick(zw,rnd) : ch)).join(''); - }, - injectUnicodeNoise(text, rnd) { - const marks = ['\u0301','\u0300','\u0302','\u0303','\u0308','\u0307','\u0304']; - return [...text].map(ch => (rnd()<0.15 ? ch+this.pick(marks,rnd) : ch)).join(''); - }, - whitespaceChaos(text, rnd) { - return text.replace(/\s/g, (m)=> (rnd()<0.5? m : (rnd()<0.5?'\t':'\u00A0'))); - }, - casingChaos(text, rnd) { - return [...text].map(c => /[a-z]/i.test(c)? (rnd()<0.5? c.toUpperCase():c.toLowerCase()) : c).join(''); - }, - // Replace encodeShuffle with homoglyph confusables injection - encodeShuffle(text, rnd) { - const map = { - 'A':'ฮ‘','B':'ฮ’','C':'ฯน','E':'ฮ•','H':'ฮ—','I':'ฮ™','K':'ฮš','M':'ฮœ','N':'ฮ','O':'ฮŸ','P':'ฮก','T':'ฮค','X':'ฮง','Y':'ฮฅ', - 'a':'ะฐ','c':'ั','e':'ะต','i':'ั–','j':'ั˜','o':'ะพ','p':'ั€','s':'ั•','x':'ั…','y':'ัƒ' - }; - return [...text].map(ch => { - if (map[ch] && rnd() < 0.25) return map[ch]; - return ch; - }).join(''); - }, - generateFuzzCases() { - const src = String(this.fuzzerInput || ''); - if (!src) { this.fuzzerOutputs = []; return; } - const rnd = this.seededRandomFactory(String(this.fuzzerSeed||'')); - const out = []; - for (let i=0;i `#${i+1}\t${s}`).join('\n'); - const header = `# Parseltongue Fuzzer Output\n# count=${this.fuzzerOutputs.length}\n# seed=${this.fuzzerSeed || ''}\n# strategies=${[ - this.fuzzUseRandomMix?'randomMix':null, - this.fuzzZeroWidth?'zeroWidth':null, - this.fuzzUnicodeNoise?'unicodeNoise':null, - this.fuzzWhitespace?'whitespace':null, - this.fuzzCasing?'casing':null, - this.fuzzZalgo?'zalgo':null, - this.fuzzEncodeShuffle?'encodeShuffle':null - ].filter(Boolean).join(',')}\n`; - const blob = new Blob([header + lines + '\n'], { type: 'text/plain;charset=utf-8' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); a.href = url; a.download = 'fuzz_cases.txt'; a.click(); - setTimeout(()=>URL.revokeObjectURL(url), 200); - }, - // Quick estimate of token count for Tokenade - estimateTokenadeTokens() { - // Roughly approximate tokens by estimated character length - // This intentionally errs on the conservative side for warning purposes - const approx = this.estimateTokenadeLength(); - return Math.max(0, approx); - }, - - // Confirm danger threshold before generating - checkTokenadeDangerThenGenerate() { - const estTokens = this.estimateTokenadeTokens(); - if (estTokens > this.dangerThresholdTokens) { - this.showDangerModal = true; - return; - } - this.generateTokenBomb(); - }, - - // Modal acknowledge handler - proceedDangerAction() { - // Close modal and return focus to Generate button for accessibility - this.showDangerModal = false; - this.$nextTick(() => { - try { - const btn = document.querySelector('.token-bomb-actions .transform-button'); - btn && btn.focus(); - } catch (_) {} - }); - }, - - // Token Bomb Generator Logic - generateTokenBomb() { - const depth = Math.max(1, Math.min(8, Number(this.tbDepth) || 1)); - const breadth = Math.max(1, Math.min(10, Number(this.tbBreadth) || 1)); - const repeats = Math.max(1, Math.min(50, Number(this.tbRepeats) || 1)); - const sep = this.tbSeparator === 'zwj' ? '\u200D' : this.tbSeparator === 'zwnj' ? '\u200C' : this.tbSeparator === 'zwsp' ? '\u200B' : ''; - const includeVS = !!this.tbIncludeVS; - const includeNoise = !!this.tbIncludeNoise; - const randomize = !!this.tbRandomizeEmojis; - - const emojiList = this.filteredEmojis && this.filteredEmojis.length ? this.filteredEmojis : window.emojiLibrary.EMOJI_LIST; - - function pickEmojis(count) { - const out = []; - for (let i = 0; i < count; i++) { - const idx = randomize ? Math.floor(Math.random() * emojiList.length) : (i % emojiList.length); - out.push(String(emojiList[idx])); - } - return out; - } - - function addVS(str) { - if (!includeVS) return str; - // Alternate VS16/VS15 to maximize tokenization churn - const vs16 = '\uFE0F'; - const vs15 = '\uFE0E'; - let out = ''; - for (let i = 0; i < str.length; i++) { - const ch = str[i]; - out += ch + (i % 2 === 0 ? vs16 : vs15); - } - return out; - } - - function noise() { - if (!includeNoise) return ''; - const parts = ['\u200B','\u200C','\u200D','\u2060','\u2062','\u2063']; - let s = ''; - const n = 1 + Math.floor(Math.random() * 3); - for (let i = 0; i < n; i++) s += parts[Math.floor(Math.random() * parts.length)]; - return s; - } - - function buildLevel(level) { - if (level === 0) { - const base = pickEmojis(breadth).join(''); - return addVS(base); - } - const items = []; - for (let i = 0; i < breadth; i++) { - const inner = buildLevel(level - 1); - items.push(inner + noise()); - } - return items.join(sep); - } - - if (this.tbSingleCarrier) { - const manual = (this.tbCarrierManual || '').trim(); - const carrier = manual || (this.tbCarrier && String(this.tbCarrier)) || (this.selectedEmoji ? String(this.selectedEmoji) : '๐Ÿ’ฅ'); - function countUnits(level) { - if (level === 0) return breadth; - return breadth * countUnits(level - 1); - } - const unitsPerBlock = countUnits(depth - 1); - const totalUnits = Math.max(1, repeats * unitsPerBlock); - - let payload = []; - payload = pickEmojis(totalUnits); - - function toTagSeqForEmojiChar(ch) { - const cp = ch.codePointAt(0); - const hex = cp.toString(16); - let seq = ''; - for (const d of hex) { - if (d >= '0' && d <= '9') { - const base = 0xE0030 + (d.charCodeAt(0) - '0'.charCodeAt(0)); - seq += String.fromCodePoint(base); - } else { - const base = 0xE0061 + (d.charCodeAt(0) - 'a'.charCodeAt(0)); - seq += String.fromCodePoint(base); - } - } - seq += String.fromCodePoint(0xE007F); - return seq; - } - - const vs16 = includeVS ? '\uFE0F' : ''; - let out = carrier + vs16; - for (let i = 0; i < payload.length; i++) { - out += sep + toTagSeqForEmojiChar(payload[i]) + noise(); - } - this.tokenBombOutput = out; - } else { - let block = buildLevel(depth - 1); - // Repeat the block to increase token length - const blocks = []; - for (let i = 0; i < repeats; i++) { - blocks.push(block + noise()); - } - this.tokenBombOutput = blocks.join(sep); - } - - // Provide a quick visual confirmation - this.showNotification(' Tokenade generated', 'success'); - - if (this.tbAutoCopy && this.tokenBombOutput) { - this.$nextTick(() => this.copyToClipboard(this.tokenBombOutput)); - } - }, - - applyTokenadePreset(preset) { - if (preset === 'feather') { - this.tbDepth = 1; this.tbBreadth = 3; this.tbRepeats = 2; this.tbSeparator = 'zwnj'; - this.tbIncludeVS = false; this.tbIncludeNoise = false; this.tbRandomizeEmojis = true; - } else if (preset === 'light') { - this.tbDepth = 2; this.tbBreadth = 3; this.tbRepeats = 3; this.tbSeparator = 'zwnj'; - this.tbIncludeVS = false; this.tbIncludeNoise = true; this.tbRandomizeEmojis = true; - } else if (preset === 'middle') { - this.tbDepth = 3; this.tbBreadth = 4; this.tbRepeats = 6; this.tbSeparator = 'zwnj'; - this.tbIncludeVS = true; this.tbIncludeNoise = true; this.tbRandomizeEmojis = true; - } else if (preset === 'heavy') { - this.tbDepth = 4; this.tbBreadth = 6; this.tbRepeats = 12; this.tbSeparator = 'zwnj'; - this.tbIncludeVS = true; this.tbIncludeNoise = true; this.tbRandomizeEmojis = true; - } else if (preset === 'super') { - this.tbDepth = 5; this.tbBreadth = 8; this.tbRepeats = 18; this.tbSeparator = 'zwnj'; - this.tbIncludeVS = true; this.tbIncludeNoise = true; this.tbRandomizeEmojis = true; - } - this.showNotification(' Preset applied', 'success'); - }, - - // Live estimator for pre-generation length - estimateTokenadeLength() { - const depth = Math.max(1, Math.min(8, Number(this.tbDepth) || 1)); - const breadth = Math.max(1, Math.min(10, Number(this.tbBreadth) || 1)); - const repeats = Math.max(1, Math.min(50, Number(this.tbRepeats) || 1)); - const sepLen = this.tbSeparator === 'none' ? 0 : 1; - const vsPerEmoji = this.tbIncludeVS ? 1 : 0; - const noiseAvg = this.tbIncludeNoise ? 2 : 0; - - function lenLevel(level) { - if (level === 0) { - return breadth * (1 + vsPerEmoji); - } - const inner = lenLevel(level - 1); - return breadth * (inner + noiseAvg) + Math.max(0, breadth - 1) * sepLen; - } - - if (this.tbSingleCarrier) { - function countUnits(level) { return level === 0 ? breadth : breadth * countUnits(level - 1); } - const unitsPerBlock = countUnits(depth - 1); - const totalUnits = Math.max(1, repeats * unitsPerBlock); - const avgDigits = 5; // avg hex digits in tag sequence - const perUnit = avgDigits + 1 + sepLen + (this.tbIncludeNoise ? 2 : 0); // tags+term + sep + noise - const carrierLen = 1 + (this.tbIncludeVS ? 1 : 0); - return carrierLen + totalUnits * perUnit; - } else { - const blockLen = lenLevel(depth - 1); - return repeats * (blockLen + noiseAvg) + Math.max(0, repeats - 1) * sepLen; - } - }, - - setCarrierFromSelected() { - if (this.selectedEmoji) this.tbCarrier = String(this.selectedEmoji); - }, - clearTokenadePayload() { this.tbPayloadEmojis = []; }, - removeTokenadePayloadAt(idx) { this.tbPayloadEmojis.splice(idx, 1); }, - onCarrierInput() { - const q = (this.tbCarrier || '').trim(); - if (!q) { - this.carrierEmojiList = [...window.emojiLibrary.EMOJI_LIST]; - return; - } - // Filter emoji list by simple name guess or by including the character; if q contains an emoji, keep it - const list = window.emojiLibrary.EMOJI_LIST; - const byChar = list.filter(e => e.includes(q)); - // Also support colon-like query (e.g., ':heart') by rough keywords - const keywords = { - heart: ['โค๏ธ','๐Ÿ’›','๐Ÿ’š','๐Ÿ’™','๐Ÿ’œ','๐Ÿ’–','๐Ÿ’˜','๐Ÿ’','๐Ÿ’—'], - star: ['โญ','๐ŸŒŸ'], - fire: ['๐Ÿ”ฅ'], - bomb: ['๐Ÿ’ฃ'], - snake: ['๐Ÿ'], - dragon: ['๐Ÿ‰','๐Ÿฒ'], - skull: ['๐Ÿ’€'], - sparkles: ['โœจ'], - moon: ['๐ŸŒ‘','๐ŸŒ’','๐ŸŒ“','๐ŸŒ”','๐ŸŒ•','๐ŸŒ–','๐ŸŒ—','๐ŸŒ˜','๐ŸŒ™'] - }; - let byKey = []; - const qk = q.replace(/[:_\s]/g,'').toLowerCase(); - Object.keys(keywords).forEach(k => { - if (k.indexOf(qk) !== -1) byKey = byKey.concat(keywords[k]); - }); - const merged = Array.from(new Set([...byChar, ...byKey])); - this.carrierEmojiList = merged.length ? merged : [...window.emojiLibrary.EMOJI_LIST]; - }, - handleTokenadeDrop(e) { - e.preventDefault(); - const text = e.dataTransfer && (e.dataTransfer.getData('text/plain') || e.dataTransfer.getData('text')); - if (!text) return; - const parts = window.emojiLibrary.splitEmojis(text); - const onlyEmojis = parts.filter(p => /\p{Extended_Pictographic}/u.test(p)); - this.tbPayloadEmojis.push(...onlyEmojis); - }, - handleTokenadePaste(e) { - const text = (e.clipboardData && e.clipboardData.getData('text')) || ''; - if (!text) return; - const parts = window.emojiLibrary.splitEmojis(text); - const onlyEmojis = parts.filter(p => /\p{Extended_Pictographic}/u.test(p)); - this.tbPayloadEmojis.push(...onlyEmojis); - } - , - // Tokenizer visualization - async runTokenizer() { - const text = this.tokenizerInput || ''; - const engine = this.tokenizerEngine; - const tokens = []; - if (!text) { this.tokenizerTokens = []; this.tokenizerCharCount = 0; this.tokenizerWordCount = 0; return; } - if (engine === 'byte') { - // Split into UTF-8 bytes, display hex and glyphs - const encoder = new TextEncoder(); - const bytes = encoder.encode(text); - for (let i=0;i Text payload generated', 'success'); - }, - - // Gibberish Logic - seededRandom(seed) { - const x = Math.sin(seed) * 10000; - return x - Math.floor(x); - }, - - sentenceToGibberish() { - function generateGibberish(word, seed) { - const length = Math.max(4, word.length); - let gibberish = ""; - const chars = this.gibberishChars; - - for (let i = 0; i < length; i++) { - const randomValue = this.seededRandom(seed + i * 0.1); - gibberish += chars[Math.floor(randomValue * chars.length)]; - } - return gibberish; - } - const src = String(this.gibberishInput || ''); - if (!src) { this.gibberishOutput = ''; return; } - - const words = this.gibberishInput.match(/\b\w+\b/g) || []; - const dictionary = {}; - let gibberishSentence = ""; - let wordIndex = 0; - - words.forEach((word) => { - const lowerWord = word.toLowerCase(); - const seed = - this.gibberishSeed === "" - ? Math.random() * 100 - : Number(this.gibberishSeed); - - if (!dictionary[lowerWord]) { - const wordSeed = seed + wordIndex * 100; - dictionary[lowerWord] = generateGibberish.call(this, word, wordSeed); - wordIndex++; - } - }); - - let charIndex = 0; - for (let i = 0; i < this.gibberishInput.length; i++) { - const char = this.gibberishInput[i]; - - if (/\w/.test(char)) { - let j = i; - while ( - j < this.gibberishInput.length && - /\w/.test(this.gibberishInput[j]) - ) { - j++; - } - - const word = this.gibberishInput.substring(i, j).toLowerCase(); - gibberishSentence += dictionary[word]; - i = j - 1; - } else { - gibberishSentence += char; - } - } - - const dictionaryString = Object.entries(dictionary) - .map(([plain, gib]) => `"${plain}": "${gib}"`) - .join(", "); - - this.gibberishOutput = gibberishSentence; - this.gibberishDictionary = '{' + dictionaryString + '}'; - }, - - - generateRandomRemovals() { - if (!this.removalInput.trim()) { - this.showNotification('Please enter text to process', 'error'); - return; - } - - const seed = this.removalSeed ? String(this.removalSeed) : String(Date.now()); - let rng = this.seededRandomFactory(seed); - - this.removalOutputs = []; - const words = this.removalInput.split(/\s+/); - - for (let v = 0; v < this.removalVariations; v++) { - const modifiedWords = words.map(word => { - // Skip very short words or non-alphabetic - if (word.length <= 1 || !/[a-zA-Z]/.test(word)) { - return word; - } - - // Determine how many letters to remove for this word - const minRemove = Math.max(0, this.removalMinLetters); - const maxRemove = Math.min(word.length - 1, this.removalMaxLetters); - const numToRemove = minRemove + Math.floor(rng() * (maxRemove - minRemove + 1)); - - if (numToRemove === 0) { - return word; - } - - // Get letter positions - const letters = word.split('').map((c, i) => ({ char: c, index: i })) - .filter(item => /[a-zA-Z]/.test(item.char)); - - // Randomly select positions to remove - const toRemoveIndices = new Set(); - const maxAttempts = numToRemove * 3; - let attempts = 0; - - while (toRemoveIndices.size < Math.min(numToRemove, letters.length) && attempts < maxAttempts) { - const randIdx = Math.floor(rng() * letters.length); - toRemoveIndices.add(letters[randIdx].index); - attempts++; - } - - // Build result by skipping removed indices - return word.split('').filter((_, i) => !toRemoveIndices.has(i)).join(''); - }); - - this.removalOutputs.push(modifiedWords.join(' ')); - } - - this.showNotification(`Generated ${this.removalOutputs.length} variations`, 'success'); - }, - - - generateSpecificRemoval() { - if (!this.removalSpecificInput.trim()) { - this.showNotification('Please enter text to process', 'error'); - return; - } - - if (!this.removalCharsToRemove) { - this.showNotification('Please specify characters to remove', 'error'); - return; - } - - const charsToRemove = new Set(this.removalCharsToRemove.split('')); - this.removalSpecificOutput = this.removalSpecificInput - .split('') - .filter(char => !charsToRemove.has(char)) - .join(''); - - this.showNotification('Characters removed', 'success'); - }, - - // Copy all removal outputs - copyAllRemovals() { - const allText = this.removalOutputs.join('\n'); - this.copyToClipboard(allText); - }, - - // Set up paste event handlers for all textareas setupPasteHandlers() { - // Get all textareas in the app const textareas = document.querySelectorAll('textarea'); - - // Add paste event listener to each textarea textareas.forEach(textarea => { textarea.addEventListener('paste', (e) => { - // Mark this as an explicit paste event this.isPasteOperation = true; - - // Reset the flag after a short delay setTimeout(() => { this.isPasteOperation = false; - }, 100); + }, window.CONFIG.PASTE_FLAG_RESET_DELAY_MS); }); }); } - }, - // Initialize theme and components + }), mounted() { - console.log('Vue app mounted'); - // Apply theme + if (window.ThemeUtils && window.ThemeUtils.initializeTheme) { + this.isDarkTheme = window.ThemeUtils.initializeTheme(); if (this.isDarkTheme) { + document.body.classList.add('dark-theme'); + } else { + document.body.classList.add('light-theme'); + } + } else if (this.isDarkTheme) { document.body.classList.add('dark-theme'); } - // Initialize category navigation - this.initializeCategoryNavigation(); + if (window.toolRegistry && typeof window.toolRegistry.mergeVueLifecycle === 'function') { + const lifecycleHooks = window.toolRegistry.mergeVueLifecycle(); + if (lifecycleHooks && lifecycleHooks.mounted) { + lifecycleHooks.mounted.call(this); + } + } + + if (window.toolRegistry && typeof window.toolRegistry.getAll === 'function') { + this.registeredTools = window.toolRegistry.getAll(); + } - // Initialize emoji grid with all emojis shown by default this.$nextTick(() => { - console.log('nextTick: Initializing emoji grid'); - // Make sure filtered emojis is populated - this.filteredEmojis = [...window.emojiLibrary.EMOJI_LIST]; + const closeButton = document.querySelector('#unicode-options-panel .close-button'); + if (closeButton) { + const handleClose = (e) => { + e.preventDefault(); + e.stopPropagation(); + this.toggleUnicodePanel(e); + }; + + closeButton.addEventListener('click', handleClose, { passive: false }); + closeButton.addEventListener('touchend', handleClose, { passive: false }); + } + }); + + document.addEventListener('click', (e) => { + if (e.target.closest('.custom-tooltip')) { + return; + } - // Define a function to properly initialize the emoji grid + const tooltipIcon = e.target.closest('.tooltip-icon'); + + if (tooltipIcon) { + e.preventDefault(); + e.stopPropagation(); + + const tooltipText = tooltipIcon.getAttribute('data-tooltip'); + if (!tooltipText) return; + + const existingTooltip = document.querySelector('.custom-tooltip.active'); + if (existingTooltip && existingTooltip.textContent === tooltipText) { + existingTooltip.classList.remove('active'); + setTimeout(() => { + if (!existingTooltip.classList.contains('active')) { + existingTooltip.remove(); + } + }, 200); + return; + } + + document.querySelectorAll('.custom-tooltip.active').forEach(tooltip => { + tooltip.classList.remove('active'); + setTimeout(() => { + if (!tooltip.classList.contains('active')) { + tooltip.remove(); + } + }, 200); + }); + + setTimeout(() => { + const tooltip = document.createElement('div'); + tooltip.className = 'custom-tooltip active'; + tooltip.textContent = tooltipText; + + document.body.appendChild(tooltip); + + const rect = tooltipIcon.getBoundingClientRect(); + tooltip.style.left = (rect.left + rect.width / 2) + 'px'; + tooltip.style.top = (rect.top - tooltip.offsetHeight - 8) + 'px'; + tooltip.style.transform = 'translateX(-50%)'; + }, 10); + + return; + } + + if (e.target.closest('#unicode-options-panel')) { + return; + } + + document.querySelectorAll('.custom-tooltip.active').forEach(tooltip => { + tooltip.classList.remove('active'); + setTimeout(() => { + if (!tooltip.classList.contains('active')) { + tooltip.remove(); + } + }, 200); + }); + }); + + this.$nextTick(() => { const initializeEmojiGrid = () => { - // Only try to initialize when steganography tab is active if (this.activeTab !== 'steganography') { return; } @@ -2359,55 +375,48 @@ window.app = new Vue({ const emojiGridContainer = document.getElementById('emoji-grid-container'); if (emojiGridContainer) { - console.log('Found emoji-grid-container, rendering grid'); - - // Set inline styles to ensure visibility emojiGridContainer.setAttribute('style', 'display: block !important; visibility: visible !important; min-height: 300px; padding: 10px;'); - // Also make sure the parent container is visible const emojiLibrary = document.querySelector('.emoji-library'); if (emojiLibrary) { emojiLibrary.setAttribute('style', 'display: block !important; visibility: visible !important; margin-top: 20px; overflow: visible;'); } - // Now render the grid this.renderEmojiGrid(); - console.log('Emoji grid rendering complete in mounted()'); - - // Stop retrying once we've successfully found and rendered the grid clearInterval(emojiGridInitializer); - } else { - console.log('emoji-grid-container not found, will retry when steganography tab is active'); } }; - // Use an interval instead of recursive setTimeout for more reliable initialization - // This will try every 500ms until it succeeds or the page is navigated away from - const emojiGridInitializer = setInterval(initializeEmojiGrid, 500); - - // Set up paste event handlers for all textareas to prevent unwanted clipboard notifications + const emojiGridInitializer = setInterval(initializeEmojiGrid, window.CONFIG.EMOJI_GRID_INIT_INTERVAL_MS); + this._emojiGridInitializer = emojiGridInitializer; this.setupPasteHandlers(); }); }, - // No keyboard shortcuts - they were removed as requested created() { - // Initialize any required functionality - // But no keyboard shortcuts/hotkeys for now - }, - - // Watch for input events and ensure proper focus handling - watch: { - // Watch transform input to update transforms - transformInput() { - // Only auto-transform if we have an active transform - if (this.activeTransform && this.activeTab === 'transforms') { - this.transformOutput = this.activeTransform.func(this.transformInput); + if (window.toolRegistry && typeof window.toolRegistry.mergeVueLifecycle === 'function') { + const lifecycleHooks = window.toolRegistry.mergeVueLifecycle(); + if (lifecycleHooks && lifecycleHooks.created) { + lifecycleHooks.created.call(this); } } - // Note: Removed watchers for emojiMessage and decodeInput that were - // unnecessarily re-rendering the emoji grid on every keystroke. - // The emoji grid is now only rendered when switching tabs or categories, - // which prevents losing the selected emoji state while typing. - } + }, + + beforeDestroy() { + if (this._emojiGridInitializer) { + clearInterval(this._emojiGridInitializer); + this._emojiGridInitializer = null; + } + + if (window.toolRegistry && typeof window.toolRegistry.mergeVueLifecycle === 'function') { + const lifecycleHooks = window.toolRegistry.mergeVueLifecycle(); + if (lifecycleHooks && lifecycleHooks.beforeDestroy) { + lifecycleHooks.beforeDestroy.call(this); + } + } + }, + + watch: (window.toolRegistry && typeof window.toolRegistry.mergeVueWatchers === 'function') + ? window.toolRegistry.mergeVueWatchers() + : {} }); diff --git a/js/config/constants.js b/js/config/constants.js new file mode 100644 index 0000000..e7b930a --- /dev/null +++ b/js/config/constants.js @@ -0,0 +1,21 @@ +/** + * Application Configuration Constants + */ +window.CONFIG = { + // History configuration + MAX_HISTORY_ITEMS: 50, + + // Danger threshold for tokenade + DANGER_THRESHOLD_TOKENS: 10000, + + // Clipboard operation timing + CLIPBOARD_DEBOUNCE_MS: 100, + CLIPBOARD_LOCK_TIMEOUT_MS: 500, + CLIPBOARD_FALLBACK_DEBOUNCE_MS: 150, + KEYBOARD_EVENTS_TIMEOUT_MS: 1000, + PASTE_FLAG_RESET_DELAY_MS: 200, + + // Emoji grid initialization + EMOJI_GRID_INIT_INTERVAL_MS: 500 +}; + diff --git a/js/core/decoder.js b/js/core/decoder.js new file mode 100644 index 0000000..1b6c943 --- /dev/null +++ b/js/core/decoder.js @@ -0,0 +1,101 @@ +function universalDecode(input, context = {}) { + if (!input) return null; + + const allDecodings = []; + const { activeTab, activeTransform } = context; + + function addDecoding(text, method, priority = 20) { + if (text && text !== input && text.length > 0) { + const exists = allDecodings.some(d => d.text === text); + if (!exists) { + allDecodings.push({ text, method, priority }); + } + } + } + + let foundHighPriorityMatch = false; + for (const [transformKey, transform] of Object.entries(window.transforms)) { + if (transform.detector && transform.reverse) { + try { + if (transform.detector(input)) { + const result = transform.reverse(input); + if (result && result !== input && result.length > 0) { + const hasContent = result.replace(/[\x00-\x1F\x7F-\x9F\s]/g, '').length > 0; + if (hasContent) { + const detectorPriority = transform.priority || 285; + addDecoding(result, transform.name, detectorPriority); + if (detectorPriority >= 280) { + foundHighPriorityMatch = true; + } + } + } + } + } catch (e) { + console.debug('Error in transform detector:', e); + } + } + } + + if (foundHighPriorityMatch || allDecodings.some(d => d.priority >= 280)) { + const exclusiveMatches = allDecodings.filter(d => d.priority >= 280); + if (exclusiveMatches.length > 0) { + exclusiveMatches.sort((a, b) => b.priority - a.priority); + return { + text: exclusiveMatches[0].text, + method: exclusiveMatches[0].method, + alternatives: exclusiveMatches.slice(1).map(d => ({ text: d.text, method: d.method })) + }; + } + } + + if (window.steganography && window.steganography.hasEmojiInText && window.steganography.hasEmojiInText(input)) { + try { + const decoded = window.steganography.decodeEmoji(input); + if (decoded) { + addDecoding(decoded, 'Emoji Steganography', 100); + } + } catch (e) { + console.debug('Error decoding emoji steganography:', e); + } + } + + if (activeTab === 'transforms' && activeTransform) { + try { + const transformKey = Object.keys(window.transforms).find( + key => window.transforms[key].name === activeTransform.name + ); + + if (transformKey && window.transforms[transformKey].reverse) { + const result = window.transforms[transformKey].reverse(input); + if (result && result !== input) { + addDecoding(result, activeTransform.name, 150); + } + } + } catch (e) { + console.error('Error decoding with active transform:', e); + } + } + + for (const name in window.transforms) { + const transform = window.transforms[name]; + if (transform.reverse && !transform.detector) { + try { + const result = transform.reverse(input); + if (result !== input && /[a-zA-Z0-9\s]{3,}/.test(result)) { + addDecoding(result, transform.name, 10); + } + } catch (e) { + console.error(`Error decoding with ${name}:`, e); + } + } + } + + allDecodings.sort((a, b) => b.priority - a.priority); + + if (allDecodings.length === 0) return null; + + const primary = allDecodings[0]; + const alternatives = allDecodings.slice(1).map(({ text, method }) => ({ text, method })); + + return { text: primary.text, method: primary.method, alternatives }; +} \ No newline at end of file diff --git a/js/core/steganography.js b/js/core/steganography.js new file mode 100644 index 0000000..ba8c682 --- /dev/null +++ b/js/core/steganography.js @@ -0,0 +1,270 @@ +const __STEG_DEFAULTS__ = { + bitZeroVS: '\ufe0e', + bitOneVS: '\ufe0f', + initialPresentation: 'emoji', + trailingZW: '\u200B', + interBitZW: null, + interBitEvery: 1, + bitOrder: 'msb' +}; +let __stegOptions__ = Object.assign({}, __STEG_DEFAULTS__); +function setStegOptions(opts) { + if (!opts) return; + __stegOptions__ = Object.assign({}, __stegOptions__, opts); +} + +function encodeForPreview(emoji, text) { + return encodeEmoji(emoji, text); +} + +function hasEmojiInText(text) { + if (!text) return false; + if (window.emojiData && typeof window.emojiData === 'object') { + const emojiKeys = Object.keys(window.emojiData).filter(key => { + const value = window.emojiData[key]; + return typeof value === 'object' && value !== null && 'official' in value; + }); + if (emojiKeys.some(emoji => text.includes(emoji))) return true; + } + return /[\u{1F300}-\u{1F9FF}\u{1FA00}-\u{1FAFF}\u{2600}-\u{27BF}\u{1F1E6}-\u{1F1FF}\u{2300}-\u{23FF}\u{2B50}\u{1F004}]/u.test(text); +} + +function findEmojiMatch(text) { + if (!text) return null; + + if (window.emojiData && typeof window.emojiData === 'object') { + const emojiKeys = Object.keys(window.emojiData).filter(key => { + const value = window.emojiData[key]; + return typeof value === 'object' && value !== null && 'official' in value; + }); + + if (emojiKeys.length > 0) { + emojiKeys.sort((a, b) => b.length - a.length); + const escapedEmojis = emojiKeys.map(emoji => + emoji.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ); + const emojiRegex = new RegExp(`(${escapedEmojis.join('|')})`, 'u'); + const match = text.match(emojiRegex); + if (match) return match; + } + } + + const flagEmojiRegex = /([\u{1F1E6}-\u{1F1FF}][\u{1F1E6}-\u{1F1FF}])/u; + const singleEmojiRegex = /([\u{1F300}-\u{1F9FF}\u{1FA00}-\u{1FAFF}\u{2600}-\u{27BF}\u{2300}-\u{23FF}\u{2B50}\u{1F004}])/u; + + return text.match(flagEmojiRegex) || text.match(singleEmojiRegex); +} + +const carriers = [ + { + emoji: '๐Ÿ', + name: 'SNAKE', + desc: 'Classic Snake', + preview: function(text) { + return encodeForPreview(this.emoji, text); + } + }, + { + emoji: '๐Ÿ‰', + name: 'DRAGON', + desc: 'Mystical Dragon', + preview: function(text) { + return encodeForPreview(this.emoji, text); + } + }, + { + emoji: '๐ŸฆŽ', + name: 'LIZARD', + desc: 'Sneaky Lizard', + preview: function(text) { + return encodeForPreview(this.emoji, text); + } + }, + { + emoji: '๐ŸŠ', + name: 'CROCODILE', + desc: 'Dangerous Croc', + preview: function(text) { + return encodeForPreview(this.emoji, text); + } + } +]; + +function encodeEmoji(emoji, text) { + if (!text) return emoji; + + let binary = ''; + try { + const encoder = new TextEncoder(); + const bytes = encoder.encode(text); + const bitOrder = __stegOptions__.bitOrder || 'msb'; + binary = Array.from(bytes) + .map(byte => { + let byteStr = byte.toString(2).padStart(8, '0'); + if (bitOrder === 'lsb') { + byteStr = byteStr.split('').reverse().join(''); + } + return byteStr; + }) + .join(''); + } catch (e) { + const bitOrder = __stegOptions__.bitOrder || 'msb'; + binary = Array.from(text) + .map(c => { + const codePoint = c.codePointAt(0); + let bytes = []; + if (codePoint <= 0x7F) { + bytes.push(codePoint); + } else if (codePoint <= 0x7FF) { + bytes.push(0xC0 | (codePoint >> 6)); + bytes.push(0x80 | (codePoint & 0x3F)); + } else if (codePoint <= 0xFFFF) { + bytes.push(0xE0 | (codePoint >> 12)); + bytes.push(0x80 | ((codePoint >> 6) & 0x3F)); + bytes.push(0x80 | (codePoint & 0x3F)); + } else { + bytes.push(0xF0 | (codePoint >> 18)); + bytes.push(0x80 | ((codePoint >> 12) & 0x3F)); + bytes.push(0x80 | ((codePoint >> 6) & 0x3F)); + bytes.push(0x80 | (codePoint & 0x3F)); + } + return bytes.map(byte => { + let byteStr = byte.toString(2).padStart(8, '0'); + if (bitOrder === 'lsb') { + byteStr = byteStr.split('').reverse().join(''); + } + return byteStr; + }).join(''); + }) + .join(''); + } + + const vs0 = __stegOptions__.bitZeroVS || '\ufe0e'; + const vs1 = __stegOptions__.bitOneVS || '\ufe0f'; + + let result = emoji; + if (__stegOptions__.initialPresentation === 'emoji') result += '\ufe0f'; + else if (__stegOptions__.initialPresentation === 'text') result += '\ufe0e'; + + for (let i=0;i m[0] === zeroSel ? '0' : (m[0] === oneSel ? '1' : '')).join(''); + + const validBinaryLength = Math.floor(binary.length / 8) * 8; + const bytes = []; + for (let i = 0; i < validBinaryLength; i += 8) { + let byte = binary.slice(i, i + 8); + if (__stegOptions__.bitOrder === 'lsb') { + byte = byte.split('').reverse().join(''); + } + if (byte.length === 8) { + const byteValue = parseInt(byte, 2); + bytes.push(byteValue); + } + } + + try { + const decoder = new TextDecoder('utf-8', { fatal: false }); + const uint8Array = new Uint8Array(bytes); + return decoder.decode(uint8Array); + } catch (e) { + let decoded = ''; + for (const byteValue of bytes) { + if (byteValue >= 0 && byteValue <= 255) { + decoded += String.fromCharCode(byteValue); + } + } + try { + return decodeURIComponent(escape(decoded)); + } catch (e2) { + return decoded; + } + } +} + +function encodeInvisible(text) { + if (!text) return ''; + + const bytes = new TextEncoder().encode(text); + return Array.from(bytes) + .map(byte => String.fromCodePoint(0xE0000 + byte)) + .join(''); +} + +function decodeInvisible(text) { + if (!text) return ''; + + const matches = [...text.matchAll(/[\uE0000-\uE007F]/g)]; + if (!matches.length) return ''; + + const bytes = new Uint8Array(matches.length); + for (let i = 0; i < matches.length; i++) { + bytes[i] = matches[i][0].codePointAt(0) - 0xE0000; + } + + try { + const decoder = new TextDecoder('utf-8', {fatal: false}); + let decoded = decoder.decode(bytes); + decoded = decoded.replace(/@+(?=[a-zA-Z0-9])/g, ''); + decoded = decoded.replace(/([a-zA-Z0-9])@+/g, '$1'); + decoded = decoded.replace(/@+/g, ''); + return decoded; + } catch (e) { + console.error('Error decoding invisible text:', e); + let result = ''; + for (let i = 0; i < bytes.length; i++) { + if (bytes[i] >= 32 && bytes[i] <= 126) { + result += String.fromCharCode(bytes[i]); + } + } + return result; + } +} + +window.steganography = { + carriers, + encodeEmoji, + decodeEmoji, + encodeInvisible, + decodeInvisible, + setStegOptions, + hasEmojiInText +}; diff --git a/js/core/toolRegistry.js b/js/core/toolRegistry.js new file mode 100644 index 0000000..3c7c617 --- /dev/null +++ b/js/core/toolRegistry.js @@ -0,0 +1,209 @@ +/** + * Tool Registry and Loader + * Manages all available tools and provides dynamic loading + */ + +// Import all tools (they should be loaded before this file) +// Tools will be registered here + +class ToolRegistry { + constructor() { + this.tools = new Map(); + this.toolsArray = []; + } + + /** + * Register a tool + * @param {Tool} tool - Tool instance to register + */ + register(tool) { + if (!(tool instanceof Tool)) { + console.error('Tool must be an instance of Tool class'); + return; + } + + if (!tool.enabled) { + return; + } + + this.tools.set(tool.id, tool); + this.toolsArray.push(tool); + + // Sort by order + this.toolsArray.sort((a, b) => a.order - b.order); + } + + /** + * Get a tool by ID + * @param {string} id - Tool ID + * @returns {Tool|null} + */ + get(id) { + return this.tools.get(id) || null; + } + + /** + * Get all registered tools + * @returns {Array} + */ + getAll() { + return this.toolsArray; + } + + /** + * Get all enabled tools + * @returns {Array} + */ + getEnabled() { + return this.toolsArray.filter(tool => tool.enabled); + } + + /** + * Merge Vue data from all tools + * @returns {Object} + */ + mergeVueData() { + const merged = {}; + this.toolsArray.forEach(tool => { + const toolData = tool.getVueData(); + Object.assign(merged, toolData); + }); + return merged; + } + + /** + * Merge Vue methods from all tools + * @returns {Object} + */ + mergeVueMethods() { + const merged = {}; + this.toolsArray.forEach(tool => { + const toolMethods = tool.getVueMethods(); + Object.assign(merged, toolMethods); + }); + return merged; + } + + /** + * Merge Vue watchers from all tools + * @returns {Object} + */ + mergeVueWatchers() { + const merged = {}; + this.toolsArray.forEach(tool => { + const toolWatchers = tool.getVueWatchers(); + Object.assign(merged, toolWatchers); + }); + return merged; + } + + /** + * Merge Vue lifecycle hooks from all tools + * @returns {Object} + */ + mergeVueLifecycle() { + const merged = {}; + this.toolsArray.forEach(tool => { + const toolLifecycle = tool.getVueLifecycle(); + Object.keys(toolLifecycle).forEach(hook => { + if (!merged[hook]) { + merged[hook] = []; + } + merged[hook].push(toolLifecycle[hook]); + }); + }); + + // Convert arrays to functions that call all hooks + const result = {}; + Object.keys(merged).forEach(hook => { + result[hook] = function() { + const args = arguments; + merged[hook].forEach(fn => { + if (typeof fn === 'function') { + fn.apply(this, args); + } + }); + }; + }); + + return result; + } + + /** + * Generate HTML for all tab buttons + * @returns {String} + */ + generateTabButtonsHTML() { + return this.toolsArray.map(tool => tool.getTabButtonHTML()).join('\n'); + } + + /** + * Generate HTML for all tab content + * @returns {String} + */ + generateTabContentHTML() { + return this.toolsArray.map(tool => tool.getTabContentHTML()).join('\n'); + } + + /** + * Handle tool activation + * @param {string} toolId - Tool ID + * @param {Vue} vueInstance - Vue instance + */ + activateTool(toolId, vueInstance) { + const tool = this.get(toolId); + if (tool && typeof tool.onActivate === 'function') { + tool.onActivate(vueInstance); + } + } + + /** + * Handle tool deactivation + * @param {string} toolId - Tool ID + * @param {Vue} vueInstance - Vue instance + */ + deactivateTool(toolId, vueInstance) { + const tool = this.get(toolId); + if (tool && typeof tool.onDeactivate === 'function') { + tool.onDeactivate(vueInstance); + } + } +} + +// Create global registry instance +window.ToolRegistry = ToolRegistry; +window.toolRegistry = new ToolRegistry(); + +// Auto-register tools if they're available +if (typeof DecodeTool !== 'undefined') { + window.toolRegistry.register(new DecodeTool()); +} +if (typeof EmojiTool !== 'undefined') { + window.toolRegistry.register(new EmojiTool()); +} +if (typeof GibberishTool !== 'undefined') { + window.toolRegistry.register(new GibberishTool()); +} +if (typeof MutationTool !== 'undefined') { + window.toolRegistry.register(new MutationTool()); +} +if (typeof SplitterTool !== 'undefined') { + window.toolRegistry.register(new SplitterTool()); +} +if (typeof TokenadeTool !== 'undefined') { + window.toolRegistry.register(new TokenadeTool()); +} +if (typeof TokenizerTool !== 'undefined') { + window.toolRegistry.register(new TokenizerTool()); +} +if (typeof TransformTool !== 'undefined') { + window.toolRegistry.register(new TransformTool()); +} + +// Export for module systems +if (typeof module !== 'undefined' && module.exports) { + module.exports = ToolRegistry; +} + + + diff --git a/js/data/emojiCompatibility.js b/js/data/emojiCompatibility.js new file mode 100644 index 0000000..2a7e9ef --- /dev/null +++ b/js/data/emojiCompatibility.js @@ -0,0 +1,208 @@ +/** + * Emoji Compatibility Checker + * Tests which emoji features the user's browser/device supports + */ + +window.emojiCompatibility = { + // Cache key for localStorage + CACHE_KEY: 'emojiTestResults_v2_simple', // Simple pixel detection only + CACHE_EXPIRY_DAYS: 30, + + // In-memory cache for emoji test results + _emojiTestCache: null, + + /** + * Load emoji test cache from localStorage + */ + loadCache: function() { + if (this._emojiTestCache) return this._emojiTestCache; + + try { + const cached = localStorage.getItem(this.CACHE_KEY); + if (!cached) return null; + + const data = JSON.parse(cached); + + // Check if cache is expired + const now = Date.now(); + const age = now - data.timestamp; + const maxAge = this.CACHE_EXPIRY_DAYS * 24 * 60 * 60 * 1000; + + if (age > maxAge) { + localStorage.removeItem(this.CACHE_KEY); + return null; + } + + this._emojiTestCache = data.results; + return this._emojiTestCache; + } catch (e) { + return null; + } + }, + + /** + * Save emoji test results to localStorage + * (Called after testing all emojis) + */ + saveCache: function() { + if (!this._emojiTestCache) return; + + try { + const data = { + timestamp: Date.now(), + results: this._emojiTestCache + }; + localStorage.setItem(this.CACHE_KEY, JSON.stringify(data)); + } catch (e) { + console.warn('โš ๏ธ Could not save emoji test cache:', e); + } + }, + + /** + * Clear the emoji test cache (useful for debugging or forcing refresh) + */ + clearCache: function() { + localStorage.removeItem(this.CACHE_KEY); + this._emojiTestCache = null; + }, + + /** + * Test if a specific emoji actually renders in the browser + * Uses canvas pixel detection - the definitive test for visual rendering + */ + testEmojiRenders: function(emoji) { + // Load cache if not already loaded + if (!this._emojiTestCache) { + this._emojiTestCache = this.loadCache() || {}; + } + + // Check cache first + if (emoji in this._emojiTestCache) { + return this._emojiTestCache[emoji]; + } + + // Cache canvas for performance + if (!this._testCanvas) { + this._testCanvas = document.createElement('canvas'); + this._testCanvas.width = 64; + this._testCanvas.height = 64; + // Set willReadFrequently for better performance with multiple getImageData calls + this._testCtx = this._testCanvas.getContext('2d', { willReadFrequently: true }); + } + + const ctx = this._testCtx; + // Use emoji font to ensure missing emojis render as boxes + ctx.font = '48px "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", "EmojiOne Color", "Android Emoji", sans-serif'; + ctx.textBaseline = 'top'; + ctx.textAlign = 'left'; + + // Width test - catches multi-character fallbacks like "???" + const emojiWidth = ctx.measureText(emoji).width; + const referenceWidth = ctx.measureText('๐Ÿ˜Š').width; + + // If emoji is much wider than a single emoji, it's likely broken into multiple chars + if (emojiWidth > referenceWidth * 1.8) { + this._emojiTestCache[emoji] = false; + return false; + } + + // Pixel detection - does the emoji actually render visually? + ctx.clearRect(0, 0, 64, 64); + ctx.fillStyle = 'black'; + ctx.fillText(emoji, 8, 8); + + const imageData = ctx.getImageData(0, 0, 64, 64).data; + + // Check if any pixels were drawn (alpha channel > 0) + let hasPixels = false; + for (let i = 0; i < imageData.length; i += 4) { + if (imageData[i + 3] > 0) { + hasPixels = true; + break; + } + } + + // Cache and return result + this._emojiTestCache[emoji] = hasPixels; + return hasPixels; + }, + + /** + * Check if a specific emoji should be shown in the UI picker + * based on browser compatibility + */ + shouldShowInPicker: function(emoji, data) { + // Simple check: Does it actually render? + // This single test catches all broken emojis regardless of type + return this.testEmojiRenders(emoji); + }, + + /** + * Get compatible emojis from a list (batch testing with progress callback) + * @param {Array} allEmojis - Full list of emojis to test + * @param {Function} progressCallback - Optional callback (tested, total, compatible) + * @returns {Promise>} - Array of compatible emojis + */ + getCompatibleEmojis: async function(allEmojis, progressCallback) { + // Load cache first + this.loadCache(); + + const compatible = []; + let tested = 0; + const total = allEmojis.length; + + // Test emojis in batches to avoid blocking + const batchSize = 50; + + function testBatch() { + return new Promise((resolve) => { + const end = Math.min(tested + batchSize, total); + + for (let i = tested; i < end; i++) { + const emoji = allEmojis[i]; + if (this.shouldShowInPicker(emoji)) { + compatible.push(emoji); + } + tested++; + } + + // Report progress + if (progressCallback) { + progressCallback(tested, total, compatible.length); + } + + // Continue or finish + if (tested < total) { + requestAnimationFrame(() => { + setTimeout(() => resolve(testBatch.call(this)), 10); + }); + } else { + // Save cache when done + this.saveCache(); + resolve(); + } + }); + } + + await testBatch.call(this); + return compatible; + }, + + /** + * Get compatibility stats + */ + getStats: function() { + const cache = this.loadCache(); + if (cache) { + const compatible = Object.values(cache).filter(v => v === true).length; + const total = Object.keys(cache).length; + return { + compatible: compatible, + total: total, + percentage: total > 0 ? ((compatible / total) * 100).toFixed(1) : 0 + }; + } + return null; + } +}; + diff --git a/js/emojiLibrary.js b/js/emojiLibrary.js deleted file mode 100644 index 157aa62..0000000 --- a/js/emojiLibrary.js +++ /dev/null @@ -1,232 +0,0 @@ -// Emoji Library for P4RS3LT0NGV3 - -// Create namespace for emoji library -window.emojiLibrary = {}; - -// Polyfill for Intl.Segmenter if not available -if (!Intl.Segmenter) { - console.warn('Intl.Segmenter not available, falling back to basic character splitting'); -} - -// Helper function to properly split text into grapheme clusters (emojis) -window.emojiLibrary.splitEmojis = function(text) { - if (Intl.Segmenter) { - const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' }); - return Array.from(segmenter.segment(text), ({ segment }) => segment); - } - return Array.from(text); -}; - -// Helper function to properly join emojis -window.emojiLibrary.joinEmojis = function(emojis) { - return emojis.join(''); -}; - -// Define emoji categories with specific emojis for each category -window.emojiLibrary.EMOJIS = { - nature: ["๐ŸŒˆ", "๐ŸŒž", "๐ŸŒ‘", "๐ŸŒ’", "๐ŸŒ“", "๐ŸŒ”", "๐ŸŒ•", "๐ŸŒ–", "๐ŸŒ—", "๐ŸŒ˜", "๐ŸฆŠ", "๐Ÿฆ", "๐Ÿฏ", "๐Ÿฎ", "๐Ÿท", "๐Ÿธ", "๐Ÿต", "๐Ÿ”", "๐Ÿง", "๐Ÿฆ", "๐Ÿค", "๐Ÿฆ†", "๐Ÿฆ…", "๐Ÿฆ‰", "๐Ÿฆ‡", "๐Ÿบ", "๐Ÿ—", "๐Ÿด", "๐Ÿฆ„", "๐Ÿ", "๐Ÿ›", "๐Ÿฆ‹", "๐ŸŒ", "๐Ÿž", "๐Ÿœ", "๐Ÿ•ท๏ธ", "๐Ÿฆ‚", "๐ŸฆŸ", "๐Ÿฆ ", "๐Ÿชฑ"], - mystical: ["๐Ÿง™", "๐Ÿง™โ€โ™‚๏ธ", "๐Ÿง™โ€โ™€๏ธ", "๐Ÿงš", "๐Ÿงšโ€โ™‚๏ธ", "๐Ÿงšโ€โ™€๏ธ", "๐Ÿง›", "๐Ÿง›โ€โ™‚๏ธ", "๐Ÿง›โ€โ™€๏ธ", "๐Ÿงœ", "๐Ÿงœโ€โ™‚๏ธ", "๐Ÿงœโ€โ™€๏ธ", "๐Ÿ‘น", "๐Ÿ‘บ", "๐Ÿ‘ป", "๐Ÿ‘ฝ", "๐Ÿ‘พ", "๐Ÿฒ", "๐Ÿ”ฎ", "๐Ÿ", "๐Ÿ‰", "๐Ÿฆ„", "โš—๏ธ", "๐Ÿ”ฏ", "๐Ÿ”ฑ", "โšœ๏ธ", "โœจ", "๐ŸŒ ", "๐ŸŒ‹", "๐Ÿ’Ž", "๐Ÿฉธ"], - faces_people: ["๐Ÿ˜€", "๐Ÿ˜", "๐Ÿ˜‚", "๐Ÿคฃ", "๐Ÿ˜ƒ", "๐Ÿ˜„", "๐Ÿ˜…", "๐Ÿ˜†", "๐Ÿ˜‰", "๐Ÿ˜Š", "๐Ÿ˜‹", "๐Ÿ˜Ž", "๐Ÿ˜", "๐Ÿ˜˜", "๐Ÿฅฐ", "๐Ÿ˜—", "๐Ÿ˜™", "๐Ÿ˜š", "๐Ÿ™‚", "๐Ÿค—", "๐Ÿคฉ", "๐Ÿค”", "๐Ÿคจ", "๐Ÿ˜", "๐Ÿ˜‘", "๐Ÿ˜ถ", "๐Ÿ™„", "๐Ÿ˜", "๐Ÿ˜ฃ", "๐Ÿ˜ฅ", "๐Ÿ˜ฎ", "๐Ÿค", "๐Ÿ˜ฏ", "๐Ÿ˜ช", "๐Ÿ˜ซ", "๐Ÿ˜ด", "๐Ÿ˜Œ", "๐Ÿ˜›", "๐Ÿ˜œ", "๐Ÿ˜", "๐Ÿคค", "๐Ÿ˜’", "๐Ÿ˜“", "๐Ÿ˜”", "๐Ÿ˜•", "๐Ÿ™ƒ", "๐Ÿค‘", "๐Ÿ˜ฒ", "๐Ÿ™", "๐Ÿ˜–", "๐Ÿ˜ž", "๐Ÿ˜Ÿ", "๐Ÿ˜ค", "๐Ÿ˜ข", "๐Ÿ˜ญ", "๐Ÿ˜ง", "๐Ÿ˜จ", "๐Ÿ˜ฉ", "๐Ÿคฏ", "๐Ÿ˜ฑ", "๐Ÿ˜ณ", "๐Ÿฅต", "๐Ÿฅถ", "๐Ÿ˜ก", "๐Ÿ˜ ", "๐Ÿคฌ", "๐Ÿ˜ท", "๐Ÿค’", "๐Ÿค•", "๐Ÿคข", "๐Ÿคฎ", "๐Ÿคง", "๐Ÿ˜‡", "๐Ÿฅณ", "๐Ÿฅด", "๐Ÿฅบ", "๐Ÿง", "๐Ÿฅฑ", "๐Ÿง "], - - gestures: ["๐Ÿ‘", "๐Ÿ‘Ž", "๐Ÿ‘Œ", "โœŒ๏ธ", "๐Ÿคž", "๐ŸคŸ", "๐Ÿค˜", "๐Ÿค™", "๐Ÿ‘ˆ", "๐Ÿ‘‰", "๐Ÿ‘†", "๐Ÿ‘‡", "๐Ÿ–•", "โ˜๏ธ", "โœ‹", "๐Ÿคš", "๐Ÿ–๏ธ", "๐Ÿ––", "๐Ÿ‘‹", "๐Ÿค", "๐Ÿ‘", "๐Ÿ™Œ", "๐Ÿ‘", "๐Ÿค", "๐Ÿ™"], - - animals_nature: ["๐Ÿ‡", "๐ŸฆŠ", "๐Ÿฆ", "๐Ÿฏ", "๐Ÿฎ", "๐Ÿท", "๐Ÿธ", "๐Ÿต", "๐Ÿ”", "๐Ÿง", "๐Ÿฆ", "๐Ÿค", "๐Ÿฆ†", "๐Ÿฆ…", "๐Ÿฆ‰", "๐Ÿฆ‡", "๐Ÿบ", "๐Ÿ—", "๐Ÿด", "๐Ÿ", "๐Ÿ›", "๐Ÿฆ‹", "๐ŸŒ", "๐Ÿž", "๐Ÿœ", "๐Ÿ•ท๏ธ", "๐Ÿฆ‚", "๐Ÿ", "๐Ÿฆจ", "๐Ÿฆฉ", "๐Ÿฆซ", "๐Ÿฆฌ", "๐Ÿปโ€โ„๏ธ", "๐Ÿผ", "๐Ÿจ", "๐Ÿ•", "๐Ÿถ", "๐Ÿฉ", "๐Ÿˆ", "๐Ÿฑ"], - - activities_sports: ["โšฝ", "๐Ÿ€", "๐Ÿˆ", "๐Ÿ", "๐Ÿ‰", "๐ŸŽพ", "๐ŸŽณ", "๐Ÿ‘", "๐Ÿ’", "๐Ÿ“", "๐Ÿธ", "๐ŸฅŠ", "๐Ÿฅ‹", "๐Ÿฅ…", "๐Ÿคพ", "๐ŸŽฟ", "๐Ÿ„", "๐Ÿ‚", "๐ŸŠ", "๐Ÿ‹๏ธ", "๐Ÿคผ", "๐Ÿคธ", "๐Ÿคบ", "๐Ÿคฝ", "๐Ÿคน", "๐ŸŽฏ", "๐ŸŽฑ", "๐ŸŽฝ", "๐Ÿšด", "๐Ÿšต"], - - technology_objects: ["๐Ÿ’ป", "โŒจ๏ธ", "๐Ÿ–ฅ๏ธ", "๐Ÿ–ฑ๏ธ", "๐Ÿ–จ๏ธ", "๐Ÿ“ฑ", "โ˜Ž๏ธ", "๐Ÿ“ž", "๐Ÿ“Ÿ", "๐Ÿ“ ", "๐Ÿ“บ", "๐Ÿ“ป", "๐ŸŽ™๏ธ", "๐ŸŽš๏ธ", "๐ŸŽ›๏ธ", "๐Ÿงญ", "๐Ÿ“ก", "๐Ÿ”‹", "๐Ÿ”Œ", "๐Ÿ’ก", "๐Ÿ›ข๏ธ", "๐Ÿ’ธ", "๐Ÿ’ต", "๐Ÿ’ณ", "๐Ÿ”‘", "๐Ÿ”“", "๐Ÿ”’"], - - mystical_fantasy: ["๐Ÿง™", "๐Ÿงš", "๐Ÿง›", "๐Ÿงœ", "๐Ÿ‘น", "๐Ÿ‘บ", "๐Ÿ‘ป", "๐Ÿ‘ฝ", "๐Ÿ‘พ", "๐Ÿ”ฎ", "๐Ÿช„", "๐Ÿ‰", "๐Ÿฒ", "๐Ÿฆ„"], - - nature_weather: ["๐ŸŒˆ", "๐ŸŒž", "๐ŸŒ™", "โญ", "๐ŸŒŸ", "โšก", "โ„๏ธ", "๐Ÿ”ฅ", "๐Ÿ’ง", "๐ŸŒŠ", "๐ŸŒช๏ธ", "๐ŸŒ‹"], - - symbols: ["โค๏ธ", "๐Ÿ’›", "๐Ÿ’š", "๐Ÿ’™", "๐Ÿ’œ", "๐Ÿ’”", "๐Ÿ’•", "๐Ÿ’ž", "๐Ÿ’“", "๐Ÿ’—", "๐Ÿ’–", "๐Ÿ’˜", "๐Ÿ’", "๐Ÿ’Ÿ", "๐Ÿ’ข", "๐Ÿ’ฃ", "๐Ÿ’ฅ", "๐Ÿ’ฆ", "๐Ÿ’จ", "๐Ÿ’ฉ", "๐Ÿ’ซ", "๐Ÿ’ฌ", "๐Ÿ’ ", "๐Ÿ’ฎ"], - - flags: ["๐Ÿ", "๐Ÿšฉ", "๐ŸŽŒ", "๐Ÿด", "๐Ÿณ๏ธ", "๐Ÿณ๏ธโ€๐ŸŒˆ", "๐Ÿณ๏ธโ€โšง๏ธ", "๐Ÿดโ€โ˜ ๏ธ", "๐Ÿ‡บ๐Ÿ‡ธ", "๐Ÿ‡จ๐Ÿ‡ฆ", "๐Ÿ‡ฌ๐Ÿ‡ง", "๐Ÿ‡ฉ๐Ÿ‡ช", "๐Ÿ‡ซ๐Ÿ‡ท", "๐Ÿ‡ฎ๐Ÿ‡น", "๐Ÿ‡ฏ๐Ÿ‡ต", "๐Ÿ‡ฐ๐Ÿ‡ท", "๐Ÿ‡ท๐Ÿ‡บ", "๐Ÿ‡จ๐Ÿ‡ณ", "๐Ÿ‡ฎ๐Ÿ‡ณ", "๐Ÿ‡ง๐Ÿ‡ท", "๐Ÿ‡ฆ๐Ÿ‡บ", "๐Ÿ‡ช๐Ÿ‡ธ", "๐Ÿ‡ณ๐Ÿ‡ฑ", "๐Ÿ‡ธ๐Ÿ‡ช"] -}; - -// Define standard emoji categories -window.emojiLibrary.CATEGORIES = [ - { id: 'all', name: 'All Emojis', icon: '๐Ÿ”' }, - { id: 'faces_people', name: 'Faces & People', icon: '๐Ÿ˜€' }, - { id: 'gestures', name: 'Gestures', icon: '๐Ÿ‘' }, - { id: 'animals_nature', name: 'Animals & Nature', icon: '๐ŸฆŠ' }, - { id: 'activities_sports', name: 'Activities & Sports', icon: 'โšฝ' }, - { id: 'technology_objects', name: 'Tech & Objects', icon: '๐Ÿ’ป' }, - { id: 'mystical_fantasy', name: 'Mystical & Fantasy', icon: '๐Ÿง™' }, - { id: 'nature_weather', name: 'Nature & Weather', icon: '๐ŸŒˆ' }, - { id: 'symbols', name: 'Symbols', icon: 'โค๏ธ' }, - { id: 'flags', name: 'Flags', icon: '๐Ÿ' } -]; - -// Auto-generate EMOJI_LIST from the categorized EMOJIS object -// This ensures a single source of truth for all emojis -window.emojiLibrary.EMOJI_LIST = (() => { - const allEmojis = []; - // Combine all emojis from all categories - Object.values(window.emojiLibrary.EMOJIS).forEach(categoryEmojis => { - allEmojis.push(...categoryEmojis); - }); - // Remove duplicates using Set and return as array - return Array.from(new Set(allEmojis)); -})(); - -// Function to render emoji grid with categories -window.emojiLibrary.renderEmojiGrid = function(containerId, onEmojiSelect, filteredList) { - console.log('Rendering emoji grid to:', containerId); - - // Get container by ID - const container = document.getElementById(containerId); - if (!container) { - console.error('Container not found:', containerId); - return; - } - - // Clear container - container.innerHTML = ''; - - // Add header with instruction message - const emojiHeader = document.createElement('div'); - emojiHeader.className = 'emoji-header'; - emojiHeader.innerHTML = '

Choose an Emoji

Click any emoji to copy your hidden message

'; - container.appendChild(emojiHeader); - - // Create category tabs - const categoryTabs = document.createElement('div'); - categoryTabs.className = 'emoji-category-tabs'; - - // Add category tabs - window.emojiLibrary.CATEGORIES.forEach(category => { - const tab = document.createElement('button'); - tab.className = 'emoji-category-tab'; - if (category.id === 'all') { - tab.classList.add('active'); - } - tab.setAttribute('data-category', category.id); - tab.innerHTML = `${category.icon} ${category.name}`; - categoryTabs.appendChild(tab); - }); - - container.appendChild(categoryTabs); - - // Create emoji grid with enforced styling - const gridContainer = document.createElement('div'); - gridContainer.className = 'emoji-grid'; - - // Get the active category - let activeCategory = 'all'; - const activeCategoryTab = container.querySelector('.emoji-category-tab.active'); - if (activeCategoryTab) { - activeCategory = activeCategoryTab.getAttribute('data-category'); - } - - // Determine which emojis to show based on category and filter - let emojisToShow = []; - - if (filteredList && filteredList.length > 0) { - // If we have a filtered list (from search), use that - emojisToShow = filteredList; - } else if (activeCategory === 'all') { - // For 'all' category, combine all emojis from the categories and deduplicate - Object.values(window.emojiLibrary.EMOJIS).forEach(categoryEmojis => { - emojisToShow = [...emojisToShow, ...categoryEmojis]; - }); - // Remove duplicates using Set - emojisToShow = Array.from(new Set(emojisToShow)); - } else if (window.emojiLibrary.EMOJIS[activeCategory]) { - // For specific category, use emojis from that category - emojisToShow = window.emojiLibrary.EMOJIS[activeCategory]; - } - - console.log(`Adding ${emojisToShow.length} emojis to grid for category: ${activeCategory}`); - - // Add emojis to grid with enforced styling - emojisToShow.forEach(emoji => { - const emojiButton = document.createElement('button'); - emojiButton.className = 'emoji-button'; - emojiButton.textContent = emoji; // Use textContent for better emoji handling - emojiButton.title = 'Click to encode with this emoji'; - - emojiButton.addEventListener('click', () => { - if (typeof onEmojiSelect === 'function') { - onEmojiSelect(emoji); - // Add visual feedback when clicked - emojiButton.style.backgroundColor = '#e6f7ff'; - setTimeout(() => { - emojiButton.style.backgroundColor = ''; - }, 300); - } - }); - - gridContainer.appendChild(emojiButton); - }); - - container.appendChild(gridContainer); - console.log('Emoji grid rendering complete'); - - // Add event listeners to category tabs - const categoryTabButtons = container.querySelectorAll('.emoji-category-tab'); - categoryTabButtons.forEach(tab => { - tab.addEventListener('click', () => { - // Update active tab - categoryTabButtons.forEach(t => t.classList.remove('active')); - tab.classList.add('active'); - - // Re-render the emoji grid with the selected category - const selectedCategory = tab.getAttribute('data-category'); - console.log('Category selected:', selectedCategory); - - // Determine which emojis to show - let emojisToShow = []; - if (selectedCategory === 'all') { - // For 'all' category, combine all emojis from the categories and deduplicate - Object.values(window.emojiLibrary.EMOJIS).forEach(categoryEmojis => { - emojisToShow = [...emojisToShow, ...categoryEmojis]; - }); - // Remove duplicates using Set - emojisToShow = Array.from(new Set(emojisToShow)); - } else if (window.emojiLibrary.EMOJIS[selectedCategory]) { - // For specific category, use emojis from that category - emojisToShow = window.emojiLibrary.EMOJIS[selectedCategory]; - } - - console.log(`Updating grid with ${emojisToShow.length} emojis for category: ${selectedCategory}`); - - // Clear only the grid and rebuild it - gridContainer.innerHTML = ''; - - // Add emojis to grid - emojisToShow.forEach(emoji => { - const emojiButton = document.createElement('button'); - emojiButton.className = 'emoji-button'; - emojiButton.textContent = emoji; - emojiButton.title = 'Click to encode with this emoji'; - - emojiButton.addEventListener('click', () => { - if (typeof onEmojiSelect === 'function') { - onEmojiSelect(emoji); - // Add visual feedback when clicked - emojiButton.style.backgroundColor = '#e6f7ff'; - setTimeout(() => { - emojiButton.style.backgroundColor = ''; - }, 300); - } - }); - - gridContainer.appendChild(emojiButton); - }); - - // Update the count display - const countDisplay = container.querySelector('.emoji-count'); - if (countDisplay) { - countDisplay.textContent = `${emojisToShow.length} emojis available`; - } - }); - }); - - // Debug info - add count display - const countDisplay = document.createElement('div'); - countDisplay.className = 'emoji-count'; - countDisplay.textContent = `${emojisToShow.length} emojis available`; - container.appendChild(countDisplay); -}; diff --git a/js/steganography.js b/js/steganography.js deleted file mode 100644 index da7e296..0000000 --- a/js/steganography.js +++ /dev/null @@ -1,232 +0,0 @@ -// Steganography carriers -// Global adjustable options for selectors/zero-width usage -const __STEG_DEFAULTS__ = { - bitZeroVS: '\ufe0e', // VS15 as 0 - bitOneVS: '\ufe0f', // VS16 as 1 - initialPresentation: 'emoji', // 'emoji' -> VS16, 'text' -> VS15, 'none' - trailingZW: '\u200B', // e.g., ZWSP; set to null to disable - interBitZW: null, // e.g., '\u200C' ZWNJ, '\u200D' ZWJ; null disables - interBitEvery: 1, // insert interBitZW every N bits (1 = after each bit) - bitOrder: 'msb' // 'msb' or 'lsb' within each byte -}; -let __stegOptions__ = Object.assign({}, __STEG_DEFAULTS__); -function setStegOptions(opts) { - if (!opts) return; - __stegOptions__ = Object.assign({}, __stegOptions__, opts); -} -// First define encoding function for preview usage -function encodeForPreview(emoji, text) { - if (!text) return emoji; - - // Convert text to binary string - const binary = Array.from(text) - .map(c => c.charCodeAt(0).toString(2).padStart(8, '0')) - .join(''); - - // Use variation selectors to encode binary - const vs0 = __stegOptions__.bitZeroVS || '\ufe0e'; - const vs1 = __stegOptions__.bitOneVS || '\ufe0f'; - - // Start with the emoji character - // Ensure the emoji has a presentation selector first to standardize it - let result = emoji; - if (__stegOptions__.initialPresentation === 'emoji') result += '\ufe0f'; - else if (__stegOptions__.initialPresentation === 'text') result += '\ufe0e'; - - // Add variation selectors based on binary representation - for (let i=0;i c.charCodeAt(0).toString(2).padStart(8, '0')) - .join(''); - - // Use variation selectors to encode binary - const vs0 = __stegOptions__.bitZeroVS || '\ufe0e'; - const vs1 = __stegOptions__.bitOneVS || '\ufe0f'; - - // Start with the emoji character - // Ensure the emoji has a presentation selector first to standardize it - let result = emoji; - if (__stegOptions__.initialPresentation === 'emoji') result += '\ufe0f'; - else if (__stegOptions__.initialPresentation === 'text') result += '\ufe0e'; - - // Add variation selectors based on binary representation - for (let i=0;i m[0] === zeroSel ? '0' : (m[0] === oneSel ? '1' : '')).join(''); - - // Make sure we have complete bytes (multiples of 8 bits) - const validBinaryLength = Math.floor(binary.length / 8) * 8; - - // Convert binary to text (respect bitOrder) - let decoded = ''; - for (let i = 0; i < validBinaryLength; i += 8) { - let byte = binary.slice(i, i + 8); - if (__stegOptions__.bitOrder === 'lsb') { - byte = byte.split('').reverse().join(''); - } - if (byte.length === 8) { - const charCode = parseInt(byte, 2); - // Only include printable ASCII characters - if (charCode >= 32 && charCode <= 126) { - decoded += String.fromCharCode(charCode); - } - } - } - - return decoded; -} - -// Invisible text encoding/decoding -function encodeInvisible(text) { - if (!text) return ''; - - const bytes = new TextEncoder().encode(text); - return Array.from(bytes) - .map(byte => String.fromCodePoint(0xE0000 + byte)) - .join(''); -} - -function decodeInvisible(text) { - if (!text) return ''; - - // Extract valid invisible characters - const matches = [...text.matchAll(/[\uE0000-\uE007F]/g)]; - if (!matches.length) return ''; - - // Create byte array from code points - const bytes = new Uint8Array(matches.length); - for (let i = 0; i < matches.length; i++) { - bytes[i] = matches[i][0].codePointAt(0) - 0xE0000; - } - - try { - // Attempt to properly decode the bytes - const decoder = new TextDecoder('utf-8', {fatal: false}); - let decoded = decoder.decode(bytes); - - // Apply multiple cleaning patterns to eliminate '@' characters - decoded = decoded.replace(/@+(?=[a-zA-Z0-9])/g, ''); // Remove @ before alphanumeric - decoded = decoded.replace(/([a-zA-Z0-9])@+/g, '$1'); // Remove @ after alphanumeric - decoded = decoded.replace(/@+/g, ''); // Remove any remaining @ - - return decoded; - } catch (e) { - console.error('Error decoding invisible text:', e); - // Fallback approach: character by character reassembly - let result = ''; - for (let i = 0; i < bytes.length; i++) { - if (bytes[i] >= 32 && bytes[i] <= 126) { // ASCII printable range - result += String.fromCharCode(bytes[i]); - } - } - return result; - } -} - -// Export for use in app.js -window.steganography = { - carriers, - encodeEmoji, - decodeEmoji, - encodeInvisible, - decodeInvisible, - setStegOptions -}; diff --git a/js/tools/DecodeTool.js b/js/tools/DecodeTool.js new file mode 100644 index 0000000..4e1013b --- /dev/null +++ b/js/tools/DecodeTool.js @@ -0,0 +1,96 @@ +/** + * Decode Tool - Universal decoder tool + */ +class DecodeTool extends Tool { + constructor() { + super({ + id: 'decoder', + name: 'Decoder', + icon: 'fa-key', + title: 'Universal Decoder (D)', + order: 2 + }); + } + + getVueData() { + return { + decoderInput: '', + decoderOutput: '', + decoderResult: null, + selectedDecoder: 'auto' + }; + } + + getVueMethods() { + return { + getAllTransformsWithReverse: function() { + return this.transforms.filter(t => t && typeof t.reverse === 'function'); + }, + runUniversalDecode: function() { + const input = this.decoderInput; + + if (!input) { + this.decoderOutput = ''; + this.decoderResult = null; + return; + } + + let result = null; + + if (this.selectedDecoder !== 'auto') { + const selectedTransform = this.transforms.find(t => t.name === this.selectedDecoder); + if (selectedTransform && selectedTransform.reverse) { + try { + const decoded = selectedTransform.reverse(input); + if (decoded && decoded !== input) { + result = { + text: decoded, + method: selectedTransform.name, + alternatives: [] + }; + } + } catch (e) { + console.error(`Error using manual decoder ${this.selectedDecoder}:`, e); + } + } + } else { + result = window.universalDecode(input, { + activeTab: this.activeTab, + activeTransform: this.activeTransform + }); + } + + this.decoderResult = result; + this.decoderOutput = result ? result.text : ''; + }, + useAlternative: function(alternative) { + if (alternative && alternative.text) { + this.decoderOutput = alternative.text; + this.decoderResult = { + method: alternative.method, + text: alternative.text, + alternatives: this.decoderResult.alternatives.filter(a => a.method !== alternative.method) + }; + } + } + }; + } + + getVueWatchers() { + return { + decoderInput() { + this.runUniversalDecode(); + } + }; + } +} + +// Export +if (typeof module !== 'undefined' && module.exports) { + module.exports = DecodeTool; +} else { + window.DecodeTool = DecodeTool; +} + + + diff --git a/js/tools/EmojiTool.js b/js/tools/EmojiTool.js new file mode 100644 index 0000000..57b4cbd --- /dev/null +++ b/js/tools/EmojiTool.js @@ -0,0 +1,398 @@ +/** + * Emoji Tool - Steganography/Emoji encoding tool + */ +class EmojiTool extends Tool { + constructor() { + super({ + id: 'steganography', + name: 'Emoji', + icon: 'fa-smile', + title: 'Hide text in emojis (H)', + order: 3 + }); + } + + getVueData() { + const allEmojis = window.EmojiUtils ? window.EmojiUtils.getAllEmojis() : []; + return { + emojiMessage: '', + encodedMessage: '', + decodeInput: '', + decodedMessage: '', + selectedCarrier: null, + activeSteg: null, + carriers: window.steganography.carriers, + filteredEmojis: [...allEmojis], + selectedEmoji: null, + carrierEmojiList: [...allEmojis], + compatibleEmojis: [], + quickCarrierEmojis: ['๐Ÿ','๐Ÿ‰','๐Ÿฒ','๐Ÿ”ฅ','๐Ÿ’ฅ','๐Ÿ—ฟ','โš“','โญ','โœจ','๐Ÿš€','๐Ÿ’€','๐Ÿชจ','๐Ÿƒ','๐Ÿชถ','๐Ÿ”ฎ','๐Ÿข','๐ŸŠ','๐ŸฆŽ'] + }; + } + + getVueMethods() { + const self = this; + return { + async initializeEmojiList() { + if (!window.EmojiUtils) { + console.warn('EmojiUtils not available'); + return; + } + + this.showNotification('Checking emoji compatibility...', 'info', 'fas fa-spinner fa-spin'); + + const progressCallback = (tested, total, compatible) => { + if (tested % 500 === 0 || tested === total) { + const percent = ((tested / total) * 100).toFixed(0); + console.log(`Emoji compatibility: ${percent}% (${compatible} compatible so far)`); + } + }; + + const compatible = await window.EmojiUtils.getCompatibleEmojis(progressCallback); + this.compatibleEmojis = compatible; + this.filteredEmojis = [...compatible]; + this.carrierEmojiList = [...compatible]; + this.emojiListInitialized = true; + + this.showNotification(`${compatible.length} compatible emojis loaded`, 'success', 'fas fa-check'); + + if (this.activeTab === 'steganography') { + this.$nextTick(() => { + this.renderEmojiGrid(); + }); + } + }, + selectCarrier: function(carrier) { + if (this.selectedCarrier === carrier) { + this.selectedCarrier = null; + this.encodedMessage = ''; + } else { + this.selectedCarrier = carrier; + this.activeSteg = 'emoji'; + this.autoEncode(); + } + }, + setStegMode: function(mode) { + if (mode === 'invisible') { + this.activeSteg = mode; + this.selectedCarrier = null; + this.autoEncode(); + + if (this.encodedMessage) { + this.$nextTick(() => { + this.forceCopyToClipboard(this.encodedMessage); + this.showNotification('Invisible text created and copied!', 'success', 'fas fa-check'); + }); + } + } else { + if (this.activeSteg === mode) { + this.activeSteg = null; + this.encodedMessage = ''; + } else { + this.activeSteg = mode; + this.autoEncode(); + } + } + }, + autoEncode: function() { + if (!this.emojiMessage || this.activeTab !== 'steganography') { + this.encodedMessage = ''; + return; + } + + if (this.activeSteg === 'invisible') { + this.encodedMessage = window.steganography.encodeInvisible(this.emojiMessage); + } else if (this.selectedCarrier) { + this.encodedMessage = window.steganography.encodeEmoji( + this.selectedCarrier.emoji, + this.emojiMessage + ); + } + }, + selectEmoji: function(emoji) { + const emojiStr = String(emoji); + + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(emojiStr) + .then(() => { + this.showNotification('Emoji copied!', 'success', 'fas fa-check'); + this.addToCopyHistory('Emoji', emojiStr); + }) + .catch(err => { + console.warn('Emoji clipboard API failed:', err); + this.forceCopyToClipboard(emojiStr); + this.showNotification('Emoji copied!', 'success', 'fas fa-check'); + }); + } else { + this.forceCopyToClipboard(emojiStr); + this.showNotification('Emoji copied!', 'success', 'fas fa-check'); + } + + if (this.activeTab === 'steganography') { + this.selectedEmoji = emoji; + + const tempCarrier = { + name: `${emoji} Carrier`, + emoji: emoji, + encode: (text) => this.steganography.encode(text, emoji), + decode: (text) => this.steganography.decode(text), + preview: (text) => `${emoji}${text}${emoji}` + }; + + this.selectedCarrier = tempCarrier; + this.activeSteg = 'emoji'; + + if (this.emojiMessage) { + this.autoEncode(); + + this.$nextTick(() => { + if (this.encodedMessage) { + const encodedStr = String(this.encodedMessage); + + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(encodedStr) + .then(() => { + this.showNotification(`Hidden message copied with ${emoji}`, 'success', 'fas fa-check'); + this.addToCopyHistory(`Hidden Message with ${emoji}`, encodedStr); + }) + .catch(err => { + console.warn('Encoded emoji clipboard API failed:', err); + this.forceCopyToClipboard(encodedStr); + this.showNotification(`Hidden message copied with ${emoji}`, 'success', 'fas fa-check'); + }); + } else { + this.forceCopyToClipboard(encodedStr); + this.showNotification(`Hidden message copied with ${emoji}`, 'success', 'fas fa-check'); + } + } + }); + } + } + }, + renderEmojiGrid: function() { + const container = document.getElementById('emoji-grid-container'); + if (!container) { + console.error('emoji-grid-container not found!'); + return; + } + + container.style.cssText = 'display: block !important; visibility: visible !important; min-height: 300px;'; + + const emojiLibrary = document.querySelector('.emoji-library'); + if (emojiLibrary) { + emojiLibrary.style.cssText = 'display: block !important; visibility: visible !important;'; + } + + while (container.firstChild) { + container.removeChild(container.firstChild); + } + + this._renderEmojiGridInternal('emoji-grid-container', this.selectEmoji.bind(this), this.filteredEmojis); + }, + _renderEmojiGridInternal: function(containerId, onEmojiSelect, filteredList) { + const container = document.getElementById(containerId); + if (!container) { + console.error('Container not found:', containerId); + return; + } + + const categories = window.emojiData && window.emojiData.categories ? window.emojiData.categories : []; + + const emojiHeader = document.createElement('div'); + emojiHeader.className = 'emoji-header'; + + const headerTitle = document.createElement('h3'); + const icon = document.createElement('i'); + icon.className = 'fas fa-icons'; + headerTitle.appendChild(icon); + headerTitle.appendChild(document.createTextNode(' Choose an Emoji')); + + const subtitle = document.createElement('p'); + subtitle.className = 'emoji-subtitle'; + const magicIcon = document.createElement('i'); + magicIcon.className = 'fas fa-magic'; + subtitle.appendChild(magicIcon); + subtitle.appendChild(document.createTextNode(' Click any emoji to copy your hidden message')); + + emojiHeader.appendChild(headerTitle); + emojiHeader.appendChild(subtitle); + container.appendChild(emojiHeader); + + const categoryTabs = document.createElement('div'); + categoryTabs.className = 'emoji-category-tabs'; + + categories.forEach(category => { + const tab = document.createElement('button'); + tab.className = 'emoji-category-tab'; + if (category.id === 'all') { + tab.classList.add('active'); + } + tab.setAttribute('data-category', category.id); + tab.textContent = `${category.icon} ${category.name}`; + categoryTabs.appendChild(tab); + }); + + container.appendChild(categoryTabs); + + const gridContainer = document.createElement('div'); + gridContainer.className = 'emoji-grid'; + + let activeCategory = 'all'; + const activeCategoryTab = container.querySelector('.emoji-category-tab.active'); + if (activeCategoryTab) { + activeCategory = activeCategoryTab.getAttribute('data-category'); + } + + let emojisToShow = []; + if (filteredList && filteredList.length > 0) { + emojisToShow = filteredList; + } else if (window.emojiData && typeof window.emojiData.getByCategory === 'function') { + emojisToShow = window.emojiData.getByCategory(activeCategory, false); + } + + const emojisToRender = emojisToShow.filter(emoji => { + if (this.compatibleEmojis && this.compatibleEmojis.length > 0) { + return this.compatibleEmojis.includes(emoji); + } + if (window.emojiCompatibility && typeof window.emojiCompatibility.shouldShowInPicker === 'function') { + return window.emojiCompatibility.shouldShowInPicker(emoji); + } + return true; + }); + + emojisToRender.forEach(emoji => { + const emojiButton = document.createElement('button'); + emojiButton.className = 'emoji-button'; + emojiButton.textContent = emoji; + emojiButton.title = 'Click to encode with this emoji'; + + emojiButton.addEventListener('click', () => { + if (typeof onEmojiSelect === 'function') { + onEmojiSelect(emoji); + emojiButton.style.backgroundColor = '#e6f7ff'; + setTimeout(() => { + emojiButton.style.backgroundColor = ''; + }, 300); + } + }); + + gridContainer.appendChild(emojiButton); + }); + + container.appendChild(gridContainer); + + const categoryTabButtons = container.querySelectorAll('.emoji-category-tab'); + categoryTabButtons.forEach(tab => { + tab.addEventListener('click', () => { + categoryTabButtons.forEach(t => t.classList.remove('active')); + tab.classList.add('active'); + + const selectedCategory = tab.getAttribute('data-category'); + let emojisToShow = []; + if (window.emojiData && typeof window.emojiData.getByCategory === 'function') { + emojisToShow = window.emojiData.getByCategory(selectedCategory, false); + } + + while (gridContainer.firstChild) { + gridContainer.removeChild(gridContainer.firstChild); + } + + const emojisToRender = emojisToShow.filter(emoji => { + if (this.compatibleEmojis && this.compatibleEmojis.length > 0) { + return this.compatibleEmojis.includes(emoji); + } + if (window.emojiCompatibility && typeof window.emojiCompatibility.shouldShowInPicker === 'function') { + return window.emojiCompatibility.shouldShowInPicker(emoji); + } + return true; + }); + + emojisToRender.forEach(emoji => { + const emojiButton = document.createElement('button'); + emojiButton.className = 'emoji-button'; + emojiButton.textContent = emoji; + emojiButton.title = 'Click to encode with this emoji'; + + emojiButton.addEventListener('click', () => { + if (typeof onEmojiSelect === 'function') { + onEmojiSelect(emoji); + emojiButton.style.backgroundColor = '#e6f7ff'; + setTimeout(() => { + emojiButton.style.backgroundColor = ''; + }, 300); + } + }); + + gridContainer.appendChild(emojiButton); + }); + + const countDisplay = container.querySelector('.emoji-count'); + if (countDisplay) { + countDisplay.textContent = `${emojisToShow.length} emojis available`; + } + }); + }); + + const countDisplay = document.createElement('div'); + countDisplay.className = 'emoji-count'; + countDisplay.textContent = `${emojisToShow.length} emojis available`; + container.appendChild(countDisplay); + }, + filterEmojis: function() { + const allEmojis = window.EmojiUtils ? window.EmojiUtils.getAllEmojis() : []; + this.filteredEmojis = this.compatibleEmojis.length > 0 ? [...this.compatibleEmojis] : [...allEmojis]; + this.renderEmojiGrid(); + } + }; + } + + getVueLifecycle() { + return { + mounted() { + this.initializeEmojiList(); + + this.$nextTick(() => { + const allEmojis = window.EmojiUtils ? window.EmojiUtils.getAllEmojis() : []; + this.filteredEmojis = this.compatibleEmojis.length > 0 ? [...this.compatibleEmojis] : [...allEmojis]; + + const initializeEmojiGrid = () => { + if (this.activeTab !== 'steganography') { + return; + } + + const emojiGridContainer = document.getElementById('emoji-grid-container'); + if (emojiGridContainer) { + emojiGridContainer.setAttribute('style', 'display: block !important; visibility: visible !important; min-height: 300px; padding: 10px;'); + + const emojiLibrary = document.querySelector('.emoji-library'); + if (emojiLibrary) { + emojiLibrary.setAttribute('style', 'display: block !important; visibility: visible !important; margin-top: 20px; overflow: visible;'); + } + + this.renderEmojiGrid(); + clearInterval(emojiGridInitializer); + } + }; + + const emojiGridInitializer = setInterval(initializeEmojiGrid, 500); + }); + } + }; + } + + onActivate(vueInstance) { + vueInstance.$nextTick(() => { + const emojiGridContainer = document.getElementById('emoji-grid-container'); + if (emojiGridContainer) { + emojiGridContainer.setAttribute('style', 'display: block !important; visibility: visible !important; min-height: 300px; padding: 10px;'); + vueInstance.renderEmojiGrid(); + } + }); + } +} + +if (typeof module !== 'undefined' && module.exports) { + module.exports = EmojiTool; +} else { + window.EmojiTool = EmojiTool; +} diff --git a/js/tools/GibberishTool.js b/js/tools/GibberishTool.js new file mode 100644 index 0000000..f7205a4 --- /dev/null +++ b/js/tools/GibberishTool.js @@ -0,0 +1,236 @@ +/** + * Gibberish Tool - Generate gibberish dictionary and random/specific character removal + */ +class GibberishTool extends Tool { + constructor() { + super({ + id: 'gibberish', + name: 'Gibberish', + icon: 'fa-comments', + title: 'Gibberish Generator', + order: 8 + }); + } + + getVueData() { + return { + // Gibberish Dictionary + gibberishInput: '', + gibberishOutput: '', + gibberishSeed: '', + gibberishDictionary: '', + gibberishChars: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', + gibberishMode: 'random', + + // Removal mode properties + removalSubMode: 'random', + removalInput: '', + removalVariations: 10, + removalMinLetters: 1, + removalMaxLetters: 3, + removalSeed: '', + removalOutputs: [], + + removalSpecificInput: '', + removalCharsToRemove: '', + removalSpecificOutput: '' + }; + } + + getVueMethods() { + return { + // Gibberish Logic - Seeded random number generator + seededRandom(seed) { + const x = Math.sin(seed) * 10000; + return x - Math.floor(x); + }, + + /** + * Generate gibberish from input sentence while maintaining structure + * Creates a consistent dictionary mapping for words + */ + sentenceToGibberish() { + function generateGibberish(word, seed) { + const length = Math.max(4, word.length); + let gibberish = ""; + const chars = this.gibberishChars; + + for (let i = 0; i < length; i++) { + const randomValue = this.seededRandom(seed + i * 0.1); + gibberish += chars[Math.floor(randomValue * chars.length)]; + } + return gibberish; + } + + const src = String(this.gibberishInput || ''); + if (!src) { + this.gibberishOutput = ''; + return; + } + + const words = this.gibberishInput.match(/\b\w+\b/g) || []; + const dictionary = {}; + let gibberishSentence = ""; + let wordIndex = 0; + + words.forEach((word) => { + const lowerWord = word.toLowerCase(); + const seed = + this.gibberishSeed === "" + ? Math.random() * 100 + : Number(this.gibberishSeed); + + if (!dictionary[lowerWord]) { + const wordSeed = seed + wordIndex * 100; + dictionary[lowerWord] = generateGibberish.call(this, word, wordSeed); + wordIndex++; + } + }); + + let charIndex = 0; + for (let i = 0; i < this.gibberishInput.length; i++) { + const char = this.gibberishInput[i]; + + if (/\w/.test(char)) { + let j = i; + while ( + j < this.gibberishInput.length && + /\w/.test(this.gibberishInput[j]) + ) { + j++; + } + + const word = this.gibberishInput.substring(i, j).toLowerCase(); + gibberishSentence += dictionary[word]; + i = j - 1; + } else { + gibberishSentence += char; + } + } + + const dictionaryString = Object.entries(dictionary) + .map(([plain, gib]) => `"${plain}": "${gib}"`) + .join(", "); + + this.gibberishOutput = gibberishSentence; + this.gibberishDictionary = '{' + dictionaryString + '}'; + }, + + /** + * Factory for creating seeded random number generators + * @param {string} seedStr - Seed string for RNG + * @returns {Function} Random number generator function + */ + seededRandomFactory(seedStr) { + if (!seedStr) return Math.random; + let h = 1779033703 ^ seedStr.length; + for (let i=0;i>> 19); + } + return function() { + h = Math.imul(h ^ (h >>> 16), 2246822507); + h = Math.imul(h ^ (h >>> 13), 3266489909); + return ((h ^= h >>> 16) >>> 0) / 4294967296; + }; + }, + + /** + * Generate random character removals from input text + * Creates multiple variations with different random removals + */ + generateRandomRemovals() { + if (!this.removalInput.trim()) { + this.showNotification('Please enter text to process', 'error'); + return; + } + + const seed = this.removalSeed ? String(this.removalSeed) : String(Date.now()); + let rng = this.seededRandomFactory(seed); + + this.removalOutputs = []; + const words = this.removalInput.split(/\s+/); + + for (let v = 0; v < this.removalVariations; v++) { + const modifiedWords = words.map(word => { + // Skip very short words or non-alphabetic + if (word.length <= 1 || !/[a-zA-Z]/.test(word)) { + return word; + } + + // Determine how many letters to remove for this word + const minRemove = Math.max(0, this.removalMinLetters); + const maxRemove = Math.min(word.length - 1, this.removalMaxLetters); + const numToRemove = minRemove + Math.floor(rng() * (maxRemove - minRemove + 1)); + + if (numToRemove === 0) { + return word; + } + + // Get letter positions + const letters = word.split('').map((c, i) => ({ char: c, index: i })) + .filter(item => /[a-zA-Z]/.test(item.char)); + + // Randomly select positions to remove + const toRemoveIndices = new Set(); + const maxAttempts = numToRemove * 3; + let attempts = 0; + + while (toRemoveIndices.size < Math.min(numToRemove, letters.length) && attempts < maxAttempts) { + const randIdx = Math.floor(rng() * letters.length); + toRemoveIndices.add(letters[randIdx].index); + attempts++; + } + + // Build result by skipping removed indices + return word.split('').filter((_, i) => !toRemoveIndices.has(i)).join(''); + }); + + this.removalOutputs.push(modifiedWords.join(' ')); + } + + this.showNotification(`Generated ${this.removalOutputs.length} variations`, 'success'); + }, + + /** + * Remove specific characters from input text + */ + generateSpecificRemoval() { + if (!this.removalSpecificInput.trim()) { + this.showNotification('Please enter text to process', 'error'); + return; + } + + if (!this.removalCharsToRemove) { + this.showNotification('Please specify characters to remove', 'error'); + return; + } + + const charsToRemove = new Set(this.removalCharsToRemove.split('')); + this.removalSpecificOutput = this.removalSpecificInput + .split('') + .filter(char => !charsToRemove.has(char)) + .join(''); + + this.showNotification('Characters removed', 'success'); + }, + + /** + * Copy all removal outputs to clipboard (one per line) + */ + copyAllRemovals() { + if (this.removalOutputs.length === 0) return; + const allOutputs = this.removalOutputs.join('\n'); + this.copyToClipboard(allOutputs); + } + }; + } +} + +// Export +if (typeof module !== 'undefined' && module.exports) { + module.exports = GibberishTool; +} else { + window.GibberishTool = GibberishTool; +} + diff --git a/js/tools/MutationTool.js b/js/tools/MutationTool.js new file mode 100644 index 0000000..85a30b0 --- /dev/null +++ b/js/tools/MutationTool.js @@ -0,0 +1,119 @@ +/** + * Mutation Tool - Fuzzer/Mutation Lab tool + */ +class MutationTool extends Tool { + constructor() { + super({ + id: 'fuzzer', + name: 'Mutation Lab', + icon: 'fa-bug', + title: 'Generate many mutated payloads for testing', + order: 5 + }); + } + + getVueData() { + return { + fuzzerInput: '', + fuzzerCount: 20, + fuzzerSeed: '', + fuzzUseRandomMix: true, + fuzzZeroWidth: true, + fuzzUnicodeNoise: true, + fuzzZalgo: false, + fuzzWhitespace: true, + fuzzCasing: true, + fuzzEncodeShuffle: false, + fuzzerOutputs: [] + }; + } + + getVueMethods() { + return { + seededRandomFactory: function(seedStr) { + if (!seedStr) return Math.random; + let h = 1779033703 ^ seedStr.length; + for (let i=0;i>> 19); + } + return function() { + h ^= h >>> 16; h = Math.imul(h, 2246822507); h ^= h >>> 13; h = Math.imul(h, 3266489909); h ^= h >>> 16; + return (h >>> 0) / 4294967296; + }; + }, + pick: function(arr, rnd) { return arr[Math.floor(rnd()*arr.length)]; }, + injectZeroWidth: function(text, rnd) { + const zw = ['\u200B','\u200C','\u200D','\u2060']; + return [...text].map(ch => (rnd()<0.2 ? ch+this.pick(zw,rnd) : ch)).join(''); + }, + injectUnicodeNoise: function(text, rnd) { + const marks = ['\u0301','\u0300','\u0302','\u0303','\u0308','\u0307','\u0304']; + return [...text].map(ch => (rnd()<0.15 ? ch+this.pick(marks,rnd) : ch)).join(''); + }, + whitespaceChaos: function(text, rnd) { + return text.replace(/\s/g, (m)=> (rnd()<0.5? m : (rnd()<0.5?'\t':'\u00A0'))); + }, + casingChaos: function(text, rnd) { + return [...text].map(c => /[a-z]/i.test(c)? (rnd()<0.5? c.toUpperCase():c.toLowerCase()) : c).join(''); + }, + encodeShuffle: function(text, rnd) { + const map = { + 'A':'ฮ‘','B':'ฮ’','C':'ฯน','E':'ฮ•','H':'ฮ—','I':'ฮ™','K':'ฮš','M':'ฮœ','N':'ฮ','O':'ฮŸ','P':'ฮก','T':'ฮค','X':'ฮง','Y':'ฮฅ', + 'a':'ะฐ','c':'ั','e':'ะต','i':'ั–','j':'ั˜','o':'ะพ','p':'ั€','s':'ั•','x':'ั…','y':'ัƒ' + }; + return [...text].map(ch => { + if (map[ch] && rnd() < 0.25) return map[ch]; + return ch; + }).join(''); + }, + generateFuzzCases: function() { + const src = String(this.fuzzerInput || ''); + if (!src) { this.fuzzerOutputs = []; return; } + const rnd = this.seededRandomFactory(String(this.fuzzerSeed||'')); + const out = []; + for (let i=0;i `#${i+1}\t${s}`).join('\n'); + const header = `# Parseltongue Fuzzer Output\n# count=${this.fuzzerOutputs.length}\n# seed=${this.fuzzerSeed || ''}\n# strategies=${[ + this.fuzzUseRandomMix?'randomMix':null, + this.fuzzZeroWidth?'zeroWidth':null, + this.fuzzUnicodeNoise?'unicodeNoise':null, + this.fuzzWhitespace?'whitespace':null, + this.fuzzCasing?'casing':null, + this.fuzzZalgo?'zalgo':null, + this.fuzzEncodeShuffle?'encodeShuffle':null + ].filter(Boolean).join(',')}\n`; + const blob = new Blob([header + lines + '\n'], { type: 'text/plain;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); a.href = url; a.download = 'fuzz_cases.txt'; a.click(); + setTimeout(()=>URL.revokeObjectURL(url), 200); + } + }; + } +} + +// Export +if (typeof module !== 'undefined' && module.exports) { + module.exports = MutationTool; +} else { + window.MutationTool = MutationTool; +} + + + diff --git a/js/tools/SplitterTool.js b/js/tools/SplitterTool.js new file mode 100644 index 0000000..97f4484 --- /dev/null +++ b/js/tools/SplitterTool.js @@ -0,0 +1,267 @@ +/** + * Splitter Tool - Split text into multiple copyable messages + */ +class SplitterTool extends Tool { + constructor() { + super({ + id: 'splitter', + name: 'Splitter', + icon: 'fa-grip-lines', + title: 'Split text into multiple copyable messages', + order: 7 + }); + } + + getVueData() { + return { + // Message Splitter Tab + splitterInput: '', + splitterMode: 'word', // 'chunk' or 'word' - default to word + splitterChunkSize: 6, + splitterWordSplitSide: 'left', // 'left' or 'right' for even-length words + splitterWordSkip: 0, // number of words to skip between splits + splitterMinWordLength: 2, // minimum word length to consider for splitting (skip shorter words) + splitterSplitFirstWord: true, // whether to split the first word (true) or keep it whole (false) + splitterCopyAsSingleLine: false, // copy as single line (true) or multiline (false) + splitterTransforms: [''], // array of transform names to apply in sequence (start with one empty slot) + splitterStartWrap: '', + splitterEndWrap: '', + splitMessages: [] + }; + } + + getVueMethods() { + return { + /** + * Set encapsulation start and end strings + * @param {string} start - The start string + * @param {string} end - The end string + */ + setEncapsulation(start, end) { + this.splitterStartWrap = start; + this.splitterEndWrap = end; + }, + + /** + * Handle transform change - auto-add next dropdown or collapse consecutive Nones + * @param {number} index - The index of the transformation that changed + */ + handleTransformChange(index) { + const value = this.splitterTransforms[index]; + + if (value && value !== '') { + // Transform was selected - add next dropdown if it doesn't exist + if (index === this.splitterTransforms.length - 1) { + this.splitterTransforms.push(''); + } + } else { + // Transform was set to None + // Check if previous dropdown is also None - if so, remove current one and collapse from previous position + if (index > 0) { + const prev = this.splitterTransforms[index - 1]; + if (!prev || prev === '') { + // Collapse: remove this dropdown + this.splitterTransforms.splice(index, 1); + } + } else if (index === 0 && this.splitterTransforms.length === 1) { + // Only one dropdown and it's set to None - keep it as the starting dropdown + // Do nothing + } else if (index === 0 && this.splitterTransforms.length > 1) { + // First dropdown set to None, check if next is also None + const next = this.splitterTransforms[1]; + if (!next || next === '') { + // Remove the first one + this.splitterTransforms.splice(0, 1); + } + } + } + + // Ensure there's always at least one dropdown + if (this.splitterTransforms.length === 0) { + this.splitterTransforms = ['']; + } + + // Force Vue to update + this.$forceUpdate(); + }, + + /** + * Generate split messages from input text + * Supports two modes: character chunks or split words in half + */ + generateSplitMessages() { + // Clear previous output at the start + this.splitMessages = []; + + const input = this.splitterInput; + if (!input) { + return; + } + + let chunks = []; + + if (this.splitterMode === 'chunk') { + // Character chunk mode + const chunkSize = Math.max(1, Math.min(500, this.splitterChunkSize || 6)); + for (let i = 0; i < input.length; i += chunkSize) { + chunks.push(input.slice(i, i + chunkSize)); + } + } else if (this.splitterMode === 'word') { + // Word split mode - creates messages with pattern: secondHalf + wholeWords + firstHalf + // IMPORTANT: ALL words must be included in output, never filtered out + const words = input.match(/\S+/g) || []; + if (words.length === 0) return; + + const skipCount = Math.max(0, Math.min(20, this.splitterWordSkip || 0)); + const minLength = Math.max(1, this.splitterMinWordLength || 2); + + // Process all words - only split words that meet minimum length + // Short words are kept whole but still included in the pattern + let wordsToProcess = words; + let prependToFirst = []; + + // Handle "Split First Word" option + if (!this.splitterSplitFirstWord && words.length > 0) { + prependToFirst = [words[0]]; + wordsToProcess = words.slice(1); + } + + // Build word processing array - track which words can be split vs kept whole + const wordData = wordsToProcess.map((word, idx) => { + const canSplit = word.length >= minLength && word.length > 1; + return { + word: word, + canSplit: canSplit, + index: idx + }; + }); + + // Determine which words to split (only words that can be split) + const splittableWords = wordData.filter(w => w.canSplit); + if (splittableWords.length === 0) { + // No words can be split, output everything as one message + chunks.push([...prependToFirst, ...wordsToProcess].join(' ')); + return; + } + + // Determine split pattern based on splittable words only + const splitIndexes = new Set(); + for (let i = 0; i < splittableWords.length; i++) { + if ((i % (skipCount + 1)) === 0) { + splitIndexes.add(splittableWords[i].index); + } + } + + // Process all words and build split structure + const processedWords = wordData.map((wd, idx) => { + if (splitIndexes.has(idx) && wd.canSplit) { + // Split this word + let splitPos; + if (wd.word.length % 2 === 0) { + splitPos = wd.word.length / 2; + } else { + splitPos = this.splitterWordSplitSide === 'left' + ? Math.ceil(wd.word.length / 2) + : Math.floor(wd.word.length / 2); + } + return { + firstHalf: wd.word.slice(0, splitPos), + secondHalf: wd.word.slice(splitPos), + split: true + }; + } + // Keep whole (either too short or skipped) + return { whole: wd.word, split: false }; + }); + + // Build output messages + let currentMessage = [...prependToFirst]; + let messageStarted = false; + + for (let i = 0; i < processedWords.length; i++) { + const item = processedWords[i]; + + if (item.split) { + if (!messageStarted) { + // First split word - add first half to current message + currentMessage.push(item.firstHalf); + chunks.push(currentMessage.join(' ')); + currentMessage = [item.secondHalf]; + messageStarted = true; + } else { + // Add first half to current message, then start new message with second half + currentMessage.push(item.firstHalf); + chunks.push(currentMessage.join(' ')); + currentMessage = [item.secondHalf]; + } + } else { + // Whole word - add to current message (ALL words included) + currentMessage.push(item.whole); + } + } + + // Add any remaining message + if (currentMessage.length > 0) { + chunks.push(currentMessage.join(' ')); + } + } + + // Apply transformations in sequence (chaining) + let processedChunks = chunks; + if (this.splitterTransforms && this.splitterTransforms.length > 0) { + // Filter out empty transforms + const activeTransforms = this.splitterTransforms.filter(t => t && t !== ''); + + if (activeTransforms.length > 0) { + // Apply each transformation in sequence + for (const transformName of activeTransforms) { + const selectedTransform = this.transforms.find(t => t.name === transformName); + if (selectedTransform && selectedTransform.func) { + processedChunks = processedChunks.map(chunk => { + try { + return selectedTransform.func(chunk); + } catch (e) { + console.error('Transform error:', e); + return chunk; + } + }); + } + } + } + } + + // Apply encapsulation + const start = this.splitterStartWrap || ''; + const end = this.splitterEndWrap || ''; + this.splitMessages = processedChunks.map(chunk => `${start}${chunk}${end}`); + }, + + /** + * Copy all split messages to clipboard + * Single line: merges messages into one continuous string (keeps encapsulation/transformations) + * Multiline: copies messages separated by newlines + */ + copyAllSplitMessages() { + if (this.splitMessages.length === 0) return; + + if (this.splitterCopyAsSingleLine) { + // Merge all messages back together, keeping encapsulation and transformations + // Just join without newlines - all encapsulation/transformations are already in splitMessages + const merged = this.splitMessages.join(''); + this.copyToClipboard(merged); + } else { + // Copy all messages separated by newlines + const allMessages = this.splitMessages.join('\n'); + this.copyToClipboard(allMessages); + } + } + }; + } +} + +// Export +if (typeof module !== 'undefined' && module.exports) { + module.exports = SplitterTool; +} else { + window.SplitterTool = SplitterTool; +} diff --git a/js/tools/TokenadeTool.js b/js/tools/TokenadeTool.js new file mode 100644 index 0000000..e52101b --- /dev/null +++ b/js/tools/TokenadeTool.js @@ -0,0 +1,245 @@ +/** + * Tokenade Tool - Token bomb generator tool + * Note: This is a complex tool, so we'll include the key methods + */ +class TokenadeTool extends Tool { + constructor() { + super({ + id: 'tokenade', + name: 'Tokenade', + icon: 'fa-bomb', + title: 'Tokenade Generator', + order: 4 + }); + } + + getVueData() { + return { + tbDepth: 3, + tbBreadth: 4, + tbRepeats: 5, + tbSeparator: 'zwnj', + tbIncludeVS: true, + tbIncludeNoise: true, + tbRandomizeEmojis: true, + tbAutoCopy: true, + tbSingleCarrier: true, + tbCarrier: '', + tbPayloadEmojis: [], + tokenBombOutput: '', + tpBase: '', + tpRepeat: 100, + tpCombining: true, + tpZW: false, + textPayload: '', + dangerThresholdTokens: 25_000_000, + quickCarrierEmojis: ['๐Ÿ','๐Ÿ‰','๐Ÿฒ','๐Ÿ”ฅ','๐Ÿ’ฅ','๐Ÿ—ฟ','โš“','โญ','โœจ','๐Ÿš€','๐Ÿ’€','๐Ÿชจ','๐Ÿƒ','๐Ÿชถ','๐Ÿ”ฎ','๐Ÿข','๐ŸŠ','๐ŸฆŽ'], + tbCarrierManual: '', + carrierEmojiList: [...(window.EmojiUtils ? window.EmojiUtils.getAllEmojis() : [])] + }; + } + + getVueMethods() { + return { + generateTokenBomb: function() { + const depth = Math.max(1, Math.min(8, Number(this.tbDepth) || 1)); + const breadth = Math.max(1, Math.min(10, Number(this.tbBreadth) || 1)); + const repeats = Math.max(1, Math.min(50, Number(this.tbRepeats) || 1)); + const sep = this.tbSeparator === 'zwj' ? '\u200D' : this.tbSeparator === 'zwnj' ? '\u200C' : this.tbSeparator === 'zwsp' ? '\u200B' : ''; + const includeVS = !!this.tbIncludeVS; + const includeNoise = !!this.tbIncludeNoise; + const randomize = !!this.tbRandomizeEmojis; + + const emojiList = (this.carrierEmojiList && this.carrierEmojiList.length) ? this.carrierEmojiList : + (window.EmojiUtils ? window.EmojiUtils.getAllEmojis() : this.quickCarrierEmojis); + + function pickEmojis(count) { + const out = []; + for (let i = 0; i < count; i++) { + const idx = randomize ? Math.floor(Math.random() * emojiList.length) : (i % emojiList.length); + out.push(String(emojiList[idx])); + } + return out; + } + + function addVS(str) { + if (!includeVS) return str; + // Alternate VS16/VS15 to maximize tokenization churn + const vs16 = '\uFE0F'; + const vs15 = '\uFE0E'; + let out = ''; + for (let i = 0; i < str.length; i++) { + const ch = str[i]; + out += ch + (i % 2 === 0 ? vs16 : vs15); + } + return out; + } + + function noise() { + if (!includeNoise) return ''; + const parts = ['\u200B','\u200C','\u200D','\u2060','\u2062','\u2063']; + let s = ''; + const n = 1 + Math.floor(Math.random() * 3); + for (let i = 0; i < n; i++) s += parts[Math.floor(Math.random() * parts.length)]; + return s; + } + + function buildLevel(level) { + if (level === 0) { + const base = pickEmojis(breadth).join(''); + return addVS(base); + } + const items = []; + for (let i = 0; i < breadth; i++) { + const inner = buildLevel(level - 1); + items.push(inner + noise()); + } + return items.join(sep); + } + + if (this.tbSingleCarrier) { + const manual = (this.tbCarrierManual || '').trim(); + const carrier = manual || (this.tbCarrier && String(this.tbCarrier)) || (this.selectedEmoji ? String(this.selectedEmoji) : '๐Ÿ’ฅ'); + function countUnits(level) { + if (level === 0) return breadth; + return breadth * countUnits(level - 1); + } + const unitsPerBlock = countUnits(depth - 1); + const totalUnits = Math.max(1, repeats * unitsPerBlock); + + let payload = []; + payload = pickEmojis(totalUnits); + + function toTagSeqForEmojiChar(ch) { + const cp = ch.codePointAt(0); + const hex = cp.toString(16); + let seq = ''; + for (const d of hex) { + if (d >= '0' && d <= '9') { + const base = 0xE0030 + (d.charCodeAt(0) - '0'.charCodeAt(0)); + seq += String.fromCodePoint(base); + } else { + const base = 0xE0061 + (d.charCodeAt(0) - 'a'.charCodeAt(0)); + seq += String.fromCodePoint(base); + } + } + seq += String.fromCodePoint(0xE007F); + return seq; + } + + const vs16 = includeVS ? '\uFE0F' : ''; + let out = carrier + vs16; + for (let i = 0; i < payload.length; i++) { + out += sep + toTagSeqForEmojiChar(payload[i]) + noise(); + } + this.tokenBombOutput = out; + } else { + let block = buildLevel(depth - 1); + // Repeat the block to increase token length + const blocks = []; + for (let i = 0; i < repeats; i++) { + blocks.push(block + noise()); + } + this.tokenBombOutput = blocks.join(sep); + } + + // Auto-copy if enabled + if (this.tbAutoCopy && this.tokenBombOutput) { + this.$nextTick(() => { + this.forceCopyToClipboard(this.tokenBombOutput); + this.showNotification('Tokenade generated and copied!', 'success', 'fas fa-bomb'); + }); + } else { + this.showNotification('Tokenade generated!', 'success', 'fas fa-bomb'); + } + }, + applyTokenadePreset: function(preset) { + if (preset === 'feather') { + this.tbDepth = 1; this.tbBreadth = 3; this.tbRepeats = 2; this.tbSeparator = 'zwnj'; + this.tbIncludeVS = false; this.tbIncludeNoise = false; this.tbRandomizeEmojis = true; + } else if (preset === 'light') { + this.tbDepth = 2; this.tbBreadth = 3; this.tbRepeats = 3; this.tbSeparator = 'zwnj'; + this.tbIncludeVS = false; this.tbIncludeNoise = true; this.tbRandomizeEmojis = true; + } else if (preset === 'middle') { + this.tbDepth = 3; this.tbBreadth = 4; this.tbRepeats = 6; this.tbSeparator = 'zwnj'; + this.tbIncludeVS = true; this.tbIncludeNoise = true; this.tbRandomizeEmojis = true; + } else if (preset === 'heavy') { + this.tbDepth = 4; this.tbBreadth = 6; this.tbRepeats = 12; this.tbSeparator = 'zwnj'; + this.tbIncludeVS = true; this.tbIncludeNoise = true; this.tbRandomizeEmojis = true; + } else if (preset === 'super') { + this.tbDepth = 5; this.tbBreadth = 8; this.tbRepeats = 18; this.tbSeparator = 'zwnj'; + this.tbIncludeVS = true; this.tbIncludeNoise = true; this.tbRandomizeEmojis = true; + } + this.showNotification('Preset applied', 'success', 'fas fa-sliders-h'); + }, + estimateTokenadeLength: function() { + const depth = Math.max(1, Math.min(8, Number(this.tbDepth) || 1)); + const breadth = Math.max(1, Math.min(10, Number(this.tbBreadth) || 1)); + const repeats = Math.max(1, Math.min(50, Number(this.tbRepeats) || 1)); + const sepLen = this.tbSeparator === 'none' ? 0 : 1; + const vsPerEmoji = this.tbIncludeVS ? 1 : 0; + const noiseAvg = this.tbIncludeNoise ? 2 : 0; + + function lenLevel(level) { + if (level === 0) { + return breadth * (1 + vsPerEmoji); + } + const inner = lenLevel(level - 1); + return breadth * (inner + noiseAvg) + Math.max(0, breadth - 1) * sepLen; + } + + if (this.tbSingleCarrier) { + function countUnits(level) { return level === 0 ? breadth : breadth * countUnits(level - 1); } + const unitsPerBlock = countUnits(depth - 1); + const totalUnits = Math.max(1, repeats * unitsPerBlock); + const avgDigits = 5; + const perUnit = avgDigits + 1 + sepLen + (this.tbIncludeNoise ? 2 : 0); + const carrierLen = 1 + (this.tbIncludeVS ? 1 : 0); + return carrierLen + totalUnits * perUnit; + } else { + const blockLen = lenLevel(depth - 1); + return repeats * (blockLen + noiseAvg) + Math.max(0, repeats - 1) * sepLen; + } + }, + estimateTokenadeTokens: function() { + return Math.max(0, this.estimateTokenadeLength()); + }, + setCarrierFromSelected: function() { + if (this.selectedEmoji) this.tbCarrier = String(this.selectedEmoji); + }, + generateTextPayload: function() { + const base = String(this.tpBase || 'A'); + const count = Math.max(1, Math.min(10000, Number(this.tpRepeat) || 1)); + const combining = this.tpCombining; + const addZW = this.tpZW; + const marks = ['\u0301','\u0300','\u0302','\u0303','\u0308','\u0307','\u0304']; + const zw = ['\u200B','\u200C','\u200D','\u2060']; + let out = ''; + for (let i=0;i vueInstance.runTokenizer()); + } +} + +// Export +if (typeof module !== 'undefined' && module.exports) { + module.exports = TokenizerTool; +} else { + window.TokenizerTool = TokenizerTool; +} + + + diff --git a/js/tools/Tool.js b/js/tools/Tool.js new file mode 100644 index 0000000..5c27acc --- /dev/null +++ b/js/tools/Tool.js @@ -0,0 +1,97 @@ +/** + * Base Tool Class + * All tools should inherit from this class and implement required methods + */ +class Tool { + constructor(config) { + // Required properties + this.id = config.id; // Unique identifier (e.g., 'transforms', 'decoder') + this.name = config.name; // Display name (e.g., 'Transform', 'Decoder') + this.icon = config.icon || 'fa-circle'; // Font Awesome icon class + this.title = config.title || this.name; // Tooltip/title text + + // Optional properties + this.order = config.order || 999; // Order in tab bar (lower = earlier) + this.enabled = config.enabled !== false; // Whether tool is enabled + } + + /** + * Get Vue data properties needed for this tool + * Should return an object that will be merged into Vue's data + * @returns {Object} + */ + getVueData() { + return {}; + } + + /** + * Get Vue methods needed for this tool + * Should return an object with method definitions + * @returns {Object} + */ + getVueMethods() { + return {}; + } + + /** + * Get Vue watchers needed for this tool + * Should return an object with watcher definitions + * @returns {Object} + */ + getVueWatchers() { + return {}; + } + + /** + * Get Vue lifecycle hooks + * Should return an object with lifecycle methods (mounted, created, etc.) + * @returns {Object} + */ + getVueLifecycle() { + return {}; + } + + /** + * Get HTML template for the tab button + * @returns {String} HTML string for the tab button + */ + getTabButtonHTML() { + return ` + + `; + } + + /** + * Initialize tool-specific functionality + * Called when the tool's tab is activated + * @param {Vue} vueInstance - The Vue app instance + */ + onActivate(vueInstance) { + // Override in subclasses + } + + /** + * Cleanup tool-specific functionality + * Called when switching away from this tool's tab + * @param {Vue} vueInstance - The Vue app instance + */ + onDeactivate(vueInstance) { + // Override in subclasses + } +} + +// Export for use in other files +if (typeof module !== 'undefined' && module.exports) { + module.exports = Tool; +} else { + window.Tool = Tool; +} + + + diff --git a/js/tools/TransformTool.js b/js/tools/TransformTool.js new file mode 100644 index 0000000..f8965eb --- /dev/null +++ b/js/tools/TransformTool.js @@ -0,0 +1,193 @@ +/** + * Transform Tool - Text transformation tool + */ +class TransformTool extends Tool { + constructor() { + super({ + id: 'transforms', + name: 'Transform', + icon: 'fa-font', + title: 'Transform text (T)', + order: 1 + }); + } + + getVueData() { + const transforms = (window.transforms && Object.keys(window.transforms).length > 0) + ? Object.entries(window.transforms).map(([key, transform]) => ({ + name: transform.name, + func: transform.func.bind(transform), + preview: transform.preview.bind(transform), + reverse: transform.reverse ? transform.reverse.bind(transform) : null, + category: transform.category || 'special' + })) + : []; + + const categorySet = new Set(); + transforms.forEach(transform => { + if (transform.category) { + categorySet.add(transform.category); + } + }); + + // Sort categories, but always put randomizer last + const sortedCategories = Array.from(categorySet).sort((a, b) => { + if (a === 'randomizer') return 1; + if (b === 'randomizer') return -1; + return a.localeCompare(b); + }); + + return { + transformInput: '', + transformOutput: '', + activeTransform: null, + transforms: transforms, + categories: sortedCategories + }; + } + + getVueMethods() { + return { + getDisplayCategory: function(transformName) { + // Find transform by name and return its category property + const transform = this.transforms.find(t => t.name === transformName); + return transform ? transform.category : 'special'; + }, + getTransformsByCategory: function(category) { + return this.transforms.filter(transform => transform.category === category); + }, + isSpecialCategory: function(category) { + return category === 'randomizer'; + }, + applyTransform: function(transform, event) { + event && event.preventDefault(); + event && event.stopPropagation(); + + if (transform && transform.name === 'Random Mix') { + this.triggerRandomizerChaos(); + } + + if (this.transformInput) { + this.activeTransform = transform; + + if (transform.name === 'Random Mix') { + this.transformOutput = window.transforms.randomizer.func(this.transformInput); + const transformInfo = window.transforms.randomizer.getLastTransformInfo(); + if (transformInfo.length > 0) { + const transformsList = transformInfo.map(t => t.transformName).join(', '); + this.showNotification(`Mixed with: ${transformsList}`, 'success', 'fas fa-random'); + } + } else { + this.transformOutput = transform.func(this.transformInput); + } + + this.isTransformCopy = true; + this.forceCopyToClipboard(this.transformOutput); + + if (transform.name !== 'Random Mix') { + this.showNotification(`${transform.name} applied and copied!`, 'success', 'fas fa-check'); + } + + document.querySelectorAll('.transform-button').forEach(button => { + button.classList.remove('active'); + }); + + const inputBox = document.querySelector('#transform-input'); + if (inputBox) { + this.focusWithoutScroll(inputBox); + const len = inputBox.value.length; + try { inputBox.setSelectionRange(len, len); } catch (_) {} + } + + this.isTransformCopy = false; + this.ignoreKeyboardEvents = false; + } + }, + autoTransform: function() { + if (this.transformInput && this.activeTransform && this.activeTab === 'transforms') { + const segments = window.EmojiUtils.splitEmojis(this.transformInput); + const transformedSegments = segments.map(segment => { + if (segment.length > 1 || /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}]/u.test(segment)) { + return segment; + } + return this.activeTransform.func(segment); + }); + this.transformOutput = window.EmojiUtils.joinEmojis(transformedSegments); + } + }, + initializeCategoryNavigation: function() { + this.$nextTick(() => { + const legendItems = document.querySelectorAll('.transform-category-legend .legend-item'); + legendItems.forEach(item => { + const newItem = item.cloneNode(true); + item.parentNode.replaceChild(newItem, item); + }); + + document.querySelectorAll('.transform-category-legend .legend-item').forEach(item => { + item.addEventListener('click', () => { + const targetId = item.getAttribute('data-target'); + if (targetId) { + const targetElement = document.getElementById(targetId); + if (targetElement) { + document.querySelectorAll('.transform-category-legend .legend-item').forEach(li => { + li.classList.remove('active-category'); + }); + item.classList.add('active-category'); + + const inputSection = document.querySelector('.input-section'); + const inputSectionHeight = inputSection.offsetHeight; + const elementPosition = targetElement.getBoundingClientRect().top + window.pageYOffset; + const offsetPosition = elementPosition - inputSectionHeight - 10; + + window.scrollTo({ + top: offsetPosition, + behavior: 'smooth' + }); + + targetElement.classList.add('highlight-section'); + setTimeout(() => { + targetElement.classList.remove('highlight-section'); + }, 1000); + } + } + }); + }); + }); + } + }; + } + + getVueWatchers() { + return { + transformInput() { + if (this.activeTransform && this.activeTab === 'transforms') { + this.transformOutput = this.activeTransform.func(this.transformInput); + } + } + }; + } + + getVueLifecycle() { + return { + mounted() { + this.initializeCategoryNavigation(); + } + }; + } + + onActivate(vueInstance) { + vueInstance.$nextTick(() => { + vueInstance.initializeCategoryNavigation(); + }); + } +} + +// Export +if (typeof module !== 'undefined' && module.exports) { + module.exports = TransformTool; +} else { + window.TransformTool = TransformTool; +} + + + diff --git a/js/transforms.js b/js/transforms.js deleted file mode 100644 index 8a1952b..0000000 --- a/js/transforms.js +++ /dev/null @@ -1,2481 +0,0 @@ -// Text transformation functions -const transforms = { - // Invisible Text transform - invisible_text: { - name: 'Invisible Text', - func: function(text) { - if (!text) return ''; - const bytes = new TextEncoder().encode(text); - return Array.from(bytes) - .map(byte => String.fromCodePoint(0xE0000 + byte)) - .join(''); - }, - preview: function(text) { - return '[invisible]'; - }, - reverse: function(text) { - if (!text) return ''; - const matches = [...text.matchAll(/[\uE0000-\uE007F]/g)]; - if (!matches.length) return ''; - - return matches - .map(match => String.fromCharCode(match[0].codePointAt(0) - 0xE0000)) - .join(''); - } - }, - - // Basic transforms - upside_down: { - name: 'Upside Down', - map: { - 'a': 'ษ', 'b': 'q', 'c': 'ษ”', 'd': 'p', 'e': 'ว', 'f': 'ษŸ', 'g': 'ฦƒ', 'h': 'ษฅ', 'i': 'แด‰', - 'j': 'ษพ', 'k': 'สž', 'l': 'l', 'm': 'ษฏ', 'n': 'u', 'o': 'o', 'p': 'd', 'q': 'b', 'r': 'ษน', - 's': 's', 't': 'ส‡', 'u': 'n', 'v': 'สŒ', 'w': 'ส', 'x': 'x', 'y': 'สŽ', 'z': 'z', - 'A': 'โˆ€', 'B': 'B', 'C': 'ฦ†', 'D': 'D', 'E': 'ฦŽ', 'F': 'โ„ฒ', 'G': 'ืค', 'H': 'H', 'I': 'I', - 'J': 'ลฟ', 'K': 'K', 'L': 'หฅ', 'M': 'W', 'N': 'N', 'O': 'O', 'P': 'ิ€', 'Q': 'Q', 'R': 'R', - 'S': 'S', 'T': 'โ”ด', 'U': 'โˆฉ', 'V': 'ฮ›', 'W': 'M', 'X': 'X', 'Y': 'โ…„', 'Z': 'Z', - '0': '0', '1': 'ฦ–', '2': 'แ„…', '3': 'ฦ', '4': 'ใ„ฃ', '5': 'ฯ›', '6': '9', '7': 'ใ„ฅ', - '8': '8', '9': '6', '.': 'ห™', ',': "'", '?': 'ยฟ', '!': 'ยก', '"': ',,', "'": ',', - '(': ')', ')': '(', '[': ']', ']': '[', '{': '}', '}': '{', '<': '>', '>': '<', - '&': 'โ…‹', '_': 'โ€พ' - }, - // Create reverse map for decoding - reverseMap: function() { - const revMap = {}; - for (const [key, value] of Object.entries(this.map)) { - revMap[value] = key; - } - return revMap; - }, - func: function(text) { - return [...text].map(c => this.map[c] || c).reverse().join(''); - }, - preview: function(text) { - if (!text) return '[upside down]'; - return this.func(text.slice(0, 8)); - }, - reverse: function(text) { - const revMap = this.reverseMap(); - return [...text].map(c => revMap[c] || c).reverse().join(''); - } - }, - - elder_futhark: { - name: 'Elder Futhark', - 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': 'แ›‰' - }, - // Create reverse map for decoding - reverseMap: function() { - const revMap = {}; - for (const [key, value] of Object.entries(this.map)) { - revMap[value] = key; - } - return revMap; - }, - func: function(text) { - return [...text.toLowerCase()].map(c => this.map[c] || c).join(''); - }, - preview: function(text) { - if (!text) return '[runes]'; - return this.func(text.slice(0, 5)); - }, - reverse: function(text) { - const revMap = this.reverseMap(); - return [...text].map(c => revMap[c] || c).join(''); - } - }, - - vaporwave: { - name: 'Vaporwave', - func: function(text) { - return [...text].join(' '); - }, - preview: function(text) { - if (!text) return '[vaporwave]'; - return [...text.slice(0, 3)].join(' ') + '...'; - }, - reverse: function(text) { - // Remove spaces between characters - return text.replace(/ /g, ''); - } - }, - - zalgo: { - name: 'Zalgo', - marks: [ - '\u0300', '\u0301', '\u0302', '\u0303', '\u0304', '\u0305', '\u0306', '\u0307', '\u0308', - '\u0309', '\u030A', '\u030B', '\u030C', '\u030D', '\u030E', '\u030F', '\u0310', '\u0311', - '\u0312', '\u0313', '\u0314', '\u0315', '\u031A', '\u031B', '\u033D', '\u033E', '\u033F' - ], - func: function(text) { - return [...text].map(c => { - let result = c; - for (let i = 0; i < Math.floor(Math.random() * 3) + 1; i++) { - result += this.marks[Math.floor(Math.random() * this.marks.length)]; - } - return result; - }).join(''); - }, - preview: function(text) { - return this.func(text); - } - }, - - small_caps: { - name: 'Small Caps', - map: { - 'a': 'แด€', 'b': 'ส™', 'c': 'แด„', 'd': 'แด…', 'e': 'แด‡', 'f': '๊œฐ', 'g': 'ษข', 'h': 'สœ', 'i': 'ษช', - 'j': 'แดŠ', 'k': 'แด‹', 'l': 'สŸ', 'm': 'แด', 'n': 'ษด', 'o': 'แด', 'p': 'แด˜', 'q': 'วซ', 'r': 'ส€', - 's': 's', 't': 'แด›', 'u': 'แดœ', 'v': 'แด ', 'w': 'แดก', 'x': 'x', 'y': 'ส', 'z': 'แดข' - }, - func: function(text) { - return [...text.toLowerCase()].map(c => this.map[c] || c).join(''); - }, - preview: function(text) { - return this.func(text); - } - }, - - braille: { - name: 'Braille', - map: { - 'a': 'โ ', 'b': 'โ ƒ', 'c': 'โ ‰', 'd': 'โ ™', 'e': 'โ ‘', 'f': 'โ ‹', 'g': 'โ ›', 'h': 'โ “', 'i': 'โ Š', - 'j': 'โ š', 'k': 'โ …', 'l': 'โ ‡', 'm': 'โ ', 'n': 'โ ', 'o': 'โ •', 'p': 'โ ', 'q': 'โ Ÿ', 'r': 'โ —', - 's': 'โ Ž', 't': 'โ ž', 'u': 'โ ฅ', 'v': 'โ ง', 'w': 'โ บ', 'x': 'โ ญ', 'y': 'โ ฝ', 'z': 'โ ต', - '0': 'โ ผโ š', '1': 'โ ผโ ', '2': 'โ ผโ ƒ', '3': 'โ ผโ ‰', '4': 'โ ผโ ™', '5': 'โ ผโ ‘', - '6': 'โ ผโ ‹', '7': 'โ ผโ ›', '8': 'โ ผโ “', '9': 'โ ผโ Š' - }, - func: function(text) { - return [...text.toLowerCase()].map(c => this.map[c] || c).join(''); - }, - preview: function(text) { - return this.func(text); - } - }, - - bubble: { - name: 'Bubble', - map: { - 'a': 'โ“', 'b': 'โ“‘', 'c': 'โ“’', 'd': 'โ““', 'e': 'โ“”', 'f': 'โ“•', 'g': 'โ“–', 'h': 'โ“—', 'i': 'โ“˜', - 'j': 'โ“™', 'k': 'โ“š', 'l': 'โ“›', 'm': 'โ“œ', 'n': 'โ“', 'o': 'โ“ž', 'p': 'โ“Ÿ', 'q': 'โ“ ', 'r': 'โ“ก', - 's': 'โ“ข', 't': 'โ“ฃ', 'u': 'โ“ค', 'v': 'โ“ฅ', 'w': 'โ“ฆ', 'x': 'โ“ง', 'y': 'โ“จ', 'z': 'โ“ฉ', - 'A': 'โ’ถ', 'B': 'โ’ท', 'C': 'โ’ธ', 'D': 'โ’น', 'E': 'โ’บ', 'F': 'โ’ป', 'G': 'โ’ผ', 'H': 'โ’ฝ', 'I': 'โ’พ', - 'J': 'โ’ฟ', 'K': 'โ“€', 'L': 'โ“', 'M': 'โ“‚', 'N': 'โ“ƒ', 'O': 'โ“„', 'P': 'โ“…', 'Q': 'โ“†', 'R': 'โ“‡', - 'S': 'โ“ˆ', 'T': 'โ“‰', 'U': 'โ“Š', 'V': 'โ“‹', 'W': 'โ“Œ', 'X': 'โ“', 'Y': 'โ“Ž', 'Z': 'โ“' - }, - func: function(text) { - return [...text].map(c => this.map[c] || c).join(''); - }, - preview: function(text) { - return this.func(text); - } - }, - - morse: { - name: 'Morse Code', - map: { - 'a': '.-', 'b': '-...', 'c': '-.-.', 'd': '-..', 'e': '.', 'f': '..-.', - 'g': '--.', 'h': '....', 'i': '..', 'j': '.---', 'k': '-.-', 'l': '.-..', - 'm': '--', 'n': '-.', 'o': '---', 'p': '.--.', 'q': '--.-', 'r': '.-.', - 's': '...', 't': '-', 'u': '..-', 'v': '...-', 'w': '.--', 'x': '-..-', - 'y': '-.--', 'z': '--..', '0': '-----', '1': '.----', '2': '..---', - '3': '...--', '4': '....-', '5': '.....', '6': '-....', '7': '--...', - '8': '---..', '9': '----.' - }, - // Create reverse map for decoding - reverseMap: function() { - const revMap = {}; - for (const [key, value] of Object.entries(this.map)) { - revMap[value] = key; - } - return revMap; - }, - func: function(text, decode = false) { - if (decode) { - // Decode mode - const revMap = this.reverseMap(); - return text.split(/\s+/).map(c => revMap[c] || c).join(''); - } else { - // Encode mode - return [...text.toLowerCase()].map(c => this.map[c] || c).join(' '); - } - }, - preview: function(text) { - if (!text) return '[base32]'; - const result = this.func(text.slice(0, 2)); - return result + '...'; - }, - reverse: function(text) { - return this.func(text, true); - } - }, - - binary: { - name: 'Binary', - func: function(text) { - return [...text].map(c => c.charCodeAt(0).toString(2).padStart(8, '0')).join(' '); - }, - preview: function(text) { - if (!text) return '[binary]'; - const firstChar = text.charAt(0); - return firstChar.charCodeAt(0).toString(2).padStart(8, '0') + '...'; - }, - reverse: function(text) { - // Remove spaces and ensure we have valid binary - const binText = text.replace(/\s+/g, ''); - let result = ''; - - // Process 8 bits at a time - for (let i = 0; i < binText.length; i += 8) { - const byte = binText.substr(i, 8); - if (byte.length === 8) { - result += String.fromCharCode(parseInt(byte, 2)); - } - } - return result; - } - } - // Note: other transforms don't have reverse functions because they're not easily reversible - // The universal decoder will only try to reverse transforms that have a reverse function - , - - // Additional transforms - base64: { - name: 'Base64', - func: function(text) { - try { - return btoa(text); - } catch (e) { - return '[Invalid input]'; - } - }, - preview: function(text) { - if (!text) return '[base64]'; - try { - return btoa(text.slice(0, 3)) + '...'; - } catch (e) { - return '[Invalid input]'; - } - }, - reverse: function(text) { - try { - return atob(text); - } catch (e) { - return text; - } - } - }, - - base64url: { - name: 'Base64 URL', - func: function(text) { - if (!text) return ''; - try { - const std = btoa(text); - return std.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/,''); - } catch (e) { - return '[Invalid input]'; - } - }, - preview: function(text) { - if (!text) return '[b64url]'; - return this.func(text.slice(0,3)) + '...'; - }, - reverse: function(text) { - if (!text) return ''; - let std = text.replace(/-/g, '+').replace(/_/g, '/'); - // pad - while (std.length % 4 !== 0) std += '='; - try { return atob(std); } catch (e) { return text; } - } - }, - - hex: { - name: 'Hexadecimal', - func: function(text) { - return [...text].map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join(' '); - }, - preview: function(text) { - if (!text) return '[hex]'; - const firstChar = text.charAt(0); - return firstChar.charCodeAt(0).toString(16).padStart(2, '0') + '...'; - }, - reverse: function(text) { - const hexText = text.replace(/\s+/g, ''); - let result = ''; - - for (let i = 0; i < hexText.length; i += 2) { - const byte = hexText.substr(i, 2); - if (byte.length === 2) { - result += String.fromCharCode(parseInt(byte, 16)); - } - } - return result; - } - }, - - caesar: { - name: 'Caesar Cipher', - shift: 3, // Traditional Caesar shift is 3 - func: function(text) { - return [...text].map(c => { - const code = c.charCodeAt(0); - // Only shift letters, leave other characters unchanged - if (code >= 65 && code <= 90) { // Uppercase letters - return String.fromCharCode(((code - 65 + this.shift) % 26) + 65); - } else if (code >= 97 && code <= 122) { // Lowercase letters - return String.fromCharCode(((code - 97 + this.shift) % 26) + 97); - } else { - return c; - } - }).join(''); - }, - preview: function(text) { - if (!text) return '[cursive]'; - return this.func(text.slice(0, 3)) + '...'; - }, - reverse: function(text) { - // For decoding, shift in the opposite direction - const originalShift = this.shift; - this.shift = 26 - (this.shift % 26); // Reverse the shift - const result = this.func(text); - this.shift = originalShift; // Restore original shift - return result; - } - }, - - rot13: { - name: 'ROT13', - func: function(text) { - return [...text].map(c => { - const code = c.charCodeAt(0); - if (code >= 65 && code <= 90) { // Uppercase letters - return String.fromCharCode(((code - 65 + 13) % 26) + 65); - } else if (code >= 97 && code <= 122) { // Lowercase letters - return String.fromCharCode(((code - 97 + 13) % 26) + 97); - } else { - return c; - } - }).join(''); - }, - preview: function(text) { - if (!text) return '[monospace]'; - return this.func(text.slice(0, 3)) + '...'; - }, - reverse: function(text) { - // ROT13 is its own inverse - return this.func(text); - } - }, - - leetspeak: { - name: 'Leetspeak', - map: { - 'a': '4', 'e': '3', 'i': '1', 'o': '0', 's': '5', 't': '7', 'l': '1', - 'A': '4', 'E': '3', 'I': '1', 'O': '0', 'S': '5', 'T': '7', 'L': '1' - }, - func: function(text) { - return [...text].map(c => this.map[c] || c).join(''); - }, - preview: function(text) { - if (!text) return '[double-struck]'; - return this.func(text.slice(0, 3)) + '...'; - }, - // Create reverse map for decoding - reverseMap: function() { - const revMap = {}; - for (const [key, value] of Object.entries(this.map)) { - revMap[value] = key.toLowerCase(); - } - return revMap; - }, - reverse: function(text) { - const revMap = this.reverseMap(); - return [...text].map(c => revMap[c] || c).join(''); - } - }, - - mirror: { - name: 'Mirror Text', - func: function(text) { - return [...text].reverse().join(''); - }, - preview: function(text) { - if (!text) return '[math]'; - return this.func(text.slice(0, 3)) + '...'; - }, - reverse: function(text) { - return this.func(text); // Mirror is its own inverse - } - }, - - nato: { - name: 'NATO Phonetic', - map: { - 'a': 'Alpha', 'b': 'Bravo', 'c': 'Charlie', 'd': 'Delta', 'e': 'Echo', - 'f': 'Foxtrot', 'g': 'Golf', 'h': 'Hotel', 'i': 'India', 'j': 'Juliett', - 'k': 'Kilo', 'l': 'Lima', 'm': 'Mike', 'n': 'November', 'o': 'Oscar', - 'p': 'Papa', 'q': 'Quebec', 'r': 'Romeo', 's': 'Sierra', 't': 'Tango', - 'u': 'Uniform', 'v': 'Victor', 'w': 'Whiskey', 'x': 'X-ray', 'y': 'Yankee', 'z': 'Zulu', - '0': 'Zero', '1': 'One', '2': 'Two', '3': 'Three', '4': 'Four', - '5': 'Five', '6': 'Six', '7': 'Seven', '8': 'Eight', '9': 'Nine' - }, - func: function(text) { - return [...text.toLowerCase()].map(c => this.map[c] || c).join(' '); - }, - preview: function(text) { - if (!text) return '[quenya]'; - return this.func(text.slice(0, 3)) + '...'; - }, - // Create reverse map for decoding - reverseMap: function() { - const revMap = {}; - for (const [key, value] of Object.entries(this.map)) { - revMap[value.toLowerCase()] = key; - } - return revMap; - }, - reverse: function(text) { - const revMap = this.reverseMap(); - return text.split(/\s+/).map(word => revMap[word.toLowerCase()] || word).join(''); - } - }, - - fullwidth: { - name: 'Full Width', - func: function(text) { - return [...text].map(c => { - const code = c.charCodeAt(0); - // Convert ASCII to full-width equivalents - if (code >= 33 && code <= 126) { - return String.fromCharCode(code + 0xFEE0); - } else if (code === 32) { // Space - return 'ใ€€'; // Full-width space - } else { - return c; - } - }).join(''); - }, - preview: function(text) { - if (!text) return '[tengwar]'; - return this.func(text.slice(0, 3)) + '...'; - }, - reverse: function(text) { - return [...text].map(c => { - const code = c.charCodeAt(0); - // Convert full-width back to ASCII - if (code >= 0xFF01 && code <= 0xFF5E) { - return String.fromCharCode(code - 0xFEE0); - } else if (code === 0x3000) { // Full-width space - return ' '; // ASCII space - } else { - return c; - } - }).join(''); - } - }, - - strikethrough: { - name: 'Strikethrough', - func: function(text) { - // Use proper Unicode combining characters for strikethrough - const segments = window.emojiLibrary.splitEmojis(text); - return segments.map(c => c + '\u0336').join(''); - }, - preview: function(text) { - if (!text) return '[hieroglyphics]'; - return this.func(text.slice(0, 3)) + '...'; - }, - reverse: function(text) { - // Remove combining strikethrough characters - return text.replace(/\u0336/g, ''); - } - }, - - underline: { - name: 'Underline', - func: function(text) { - // Use proper Unicode combining characters for underline - const segments = window.emojiLibrary.splitEmojis(text); - return segments.map(c => c + '\u0332').join(''); - }, - preview: function(text) { - if (!text) return '[ogham]'; - return this.func(text.slice(0, 3)) + '...'; - }, - reverse: function(text) { - // Remove combining underline characters - return text.replace(/\u0332/g, ''); - } - }, - - medieval: { - name: 'Medieval', - map: { - 'a': '๐–†', 'b': '๐–‡', 'c': '๐–ˆ', 'd': '๐–‰', 'e': '๐–Š', 'f': '๐–‹', 'g': '๐–Œ', 'h': '๐–', 'i': '๐–Ž', - 'j': '๐–', 'k': '๐–', 'l': '๐–‘', 'm': '๐–’', 'n': '๐–“', 'o': '๐–”', 'p': '๐–•', 'q': '๐––', 'r': '๐–—', - 's': '๐–˜', 't': '๐–™', 'u': '๐–š', 'v': '๐–›', 'w': '๐–œ', 'x': '๐–', 'y': '๐–ž', 'z': '๐–Ÿ', - 'A': '๐•ฌ', 'B': '๐•ญ', 'C': '๐•ฎ', 'D': '๐•ฏ', 'E': '๐•ฐ', 'F': '๐•ฑ', 'G': '๐•ฒ', 'H': '๐•ณ', 'I': '๐•ด', - 'J': '๐•ต', 'K': '๐•ถ', 'L': '๐•ท', 'M': '๐•ธ', 'N': '๐•น', 'O': '๐•บ', 'P': '๐•ป', 'Q': '๐•ผ', 'R': '๐•ฝ', - 'S': '๐•พ', 'T': '๐•ฟ', 'U': '๐–€', 'V': '๐–', 'W': '๐–‚', 'X': '๐–ƒ', 'Y': '๐–„', 'Z': '๐–…' - }, - func: function(text) { - return [...text].map(c => this.map[c] || c).join(''); - }, - preview: function(text) { - return this.func(text); - } - }, - - cursive: { - name: 'Cursive', - map: { - 'a': '๐“ช', 'b': '๐“ซ', 'c': '๐“ฌ', 'd': '๐“ญ', 'e': '๐“ฎ', 'f': '๐“ฏ', 'g': '๐“ฐ', 'h': '๐“ฑ', 'i': '๐“ฒ', - 'j': '๐“ณ', 'k': '๐“ด', 'l': '๐“ต', 'm': '๐“ถ', 'n': '๐“ท', 'o': '๐“ธ', 'p': '๐“น', 'q': '๐“บ', 'r': '๐“ป', - 's': '๐“ผ', 't': '๐“ฝ', 'u': '๐“พ', 'v': '๐“ฟ', 'w': '๐”€', 'x': '๐”', 'y': '๐”‚', 'z': '๐”ƒ', - 'A': '๐“', 'B': '๐“‘', 'C': '๐“’', 'D': '๐““', 'E': '๐“”', 'F': '๐“•', 'G': '๐“–', 'H': '๐“—', 'I': '๐“˜', - 'J': '๐“™', 'K': '๐“š', 'L': '๐“›', 'M': '๐“œ', 'N': '๐“', 'O': '๐“ž', 'P': '๐“Ÿ', 'Q': '๐“ ', 'R': '๐“ก', - 'S': '๐“ข', 'T': '๐“ฃ', 'U': '๐“ค', 'V': '๐“ฅ', 'W': '๐“ฆ', 'X': '๐“ง', 'Y': '๐“จ', 'Z': '๐“ฉ' - }, - func: function(text) { - return [...text].map(c => this.map[c] || c).join(''); - }, - preview: function(text) { - return this.func(text); - } - }, - - monospace: { - name: 'Monospace', - map: { - 'a': '๐šŠ', 'b': '๐š‹', 'c': '๐šŒ', 'd': '๐š', 'e': '๐šŽ', 'f': '๐š', 'g': '๐š', 'h': '๐š‘', 'i': '๐š’', - 'j': '๐š“', 'k': '๐š”', 'l': '๐š•', 'm': '๐š–', 'n': '๐š—', 'o': '๐š˜', 'p': '๐š™', 'q': '๐šš', 'r': '๐š›', - 's': '๐šœ', 't': '๐š', 'u': '๐šž', 'v': '๐šŸ', 'w': '๐š ', 'x': '๐šก', 'y': '๐šข', 'z': '๐šฃ', - 'A': '๐™ฐ', 'B': '๐™ฑ', 'C': '๐™ฒ', 'D': '๐™ณ', 'E': '๐™ด', 'F': '๐™ต', 'G': '๐™ถ', 'H': '๐™ท', 'I': '๐™ธ', - 'J': '๐™น', 'K': '๐™บ', 'L': '๐™ป', 'M': '๐™ผ', 'N': '๐™ฝ', 'O': '๐™พ', 'P': '๐™ฟ', 'Q': '๐š€', 'R': '๐š', - 'S': '๐š‚', 'T': '๐šƒ', 'U': '๐š„', 'V': '๐š…', 'W': '๐š†', 'X': '๐š‡', 'Y': '๐šˆ', 'Z': '๐š‰', - '0': '๐Ÿถ', '1': '๐Ÿท', '2': '๐Ÿธ', '3': '๐Ÿน', '4': '๐Ÿบ', '5': '๐Ÿป', '6': '๐Ÿผ', '7': '๐Ÿฝ', '8': '๐Ÿพ', '9': '๐Ÿฟ' - }, - func: function(text) { - return [...text].map(c => this.map[c] || c).join(''); - }, - preview: function(text) { - return this.func(text); - } - }, - - doubleStruck: { - name: 'Double-Struck', - map: { - 'a': '๐•’', 'b': '๐•“', 'c': '๐•”', 'd': '๐••', 'e': '๐•–', 'f': '๐•—', 'g': '๐•˜', 'h': '๐•™', 'i': '๐•š', - 'j': '๐•›', 'k': '๐•œ', 'l': '๐•', 'm': '๐•ž', 'n': '๐•Ÿ', 'o': '๐• ', 'p': '๐•ก', 'q': '๐•ข', 'r': '๐•ฃ', - 's': '๐•ค', 't': '๐•ฅ', 'u': '๐•ฆ', 'v': '๐•ง', 'w': '๐•จ', 'x': '๐•ฉ', 'y': '๐•ช', 'z': '๐•ซ', - 'A': '๐”ธ', 'B': '๐”น', 'C': 'โ„‚', 'D': '๐”ป', 'E': '๐”ผ', 'F': '๐”ฝ', 'G': '๐”พ', 'H': 'โ„', 'I': '๐•€', - 'J': '๐•', 'K': '๐•‚', 'L': '๐•ƒ', 'M': '๐•„', 'N': 'โ„•', 'O': '๐•†', 'P': 'โ„™', 'Q': 'โ„š', 'R': 'โ„', - 'S': '๐•Š', 'T': '๐•‹', 'U': '๐•Œ', 'V': '๐•', 'W': '๐•Ž', 'X': '๐•', 'Y': '๐•', 'Z': 'โ„ค', - '0': '๐Ÿ˜', '1': '๐Ÿ™', '2': '๐Ÿš', '3': '๐Ÿ›', '4': '๐Ÿœ', '5': '๐Ÿ', '6': '๐Ÿž', '7': '๐ŸŸ', '8': '๐Ÿ ', '9': '๐Ÿก' - }, - func: function(text) { - return [...text].map(c => this.map[c] || c).join(''); - }, - preview: function(text) { - return this.func(text); - } - }, - - ascii85: { - name: 'ASCII85', - func: function(text) { - // Simple ASCII85 encoding implementation - let result = '<~'; - let buffer = 0; - let bufferLength = 0; - - for (let i = 0; i < text.length; i++) { - buffer = (buffer << 8) | text.charCodeAt(i); - bufferLength += 8; - - if (bufferLength >= 32) { - let value = buffer >>> (bufferLength - 32); - buffer &= (1 << (bufferLength - 32)) - 1; - bufferLength -= 32; - - if (value === 0) { - result += 'z'; - } else { - for (let j = 4; j >= 0; j--) { - const digit = (value / Math.pow(85, j)) % 85; - result += String.fromCharCode(digit + 33); - } - } - } - } - - // Handle remaining bits - if (bufferLength > 0) { - buffer <<= (32 - bufferLength); - let value = buffer; - const bytes = Math.ceil(bufferLength / 8); - - for (let j = 4; j >= (4 - bytes); j--) { - const digit = (value / Math.pow(85, j)) % 85; - result += String.fromCharCode(digit + 33); - } - } - - return result + '~>'; - }, - preview: function(text) { - if (!text) return '[runes]'; - return this.func(text.slice(0, 3)) + '...'; - }, - reverse: function(text) { - // Check if it's a valid ASCII85 string - if (!text.startsWith('<~') || !text.endsWith('~>')) { - return text; - } - - // Remove delimiters and whitespace - text = text.substring(2, text.length - 2).replace(/\s+/g, ''); - - let result = ''; - let i = 0; - - while (i < text.length) { - // Handle 'z' special case (represents 4 zero bytes) - if (text[i] === 'z') { - result += '\0\0\0\0'; - i++; - continue; - } - - // Process a group of 5 characters - if (i + 5 <= text.length || i + 1 <= text.length) { - let value = 0; - const limit = Math.min(i + 5, text.length); - - // Convert the group to a 32-bit value - for (let j = i; j < limit; j++) { - value = value * 85 + (text.charCodeAt(j) - 33); - } - - // Pad with 'u' (84) if needed - for (let j = limit; j < i + 5; j++) { - value = value * 85 + 84; - } - - // Extract bytes from the value - const bytesToWrite = limit - i - 1; - for (let j = 3; j >= 4 - bytesToWrite; j--) { - result += String.fromCharCode((value >>> (j * 8)) & 0xFF); - } - - i = limit; - } else { - break; - } - } - - return result; - } - }, - - reverse: { - name: 'Reverse Text', - func: function(text) { - return [...text].reverse().join(''); - }, - preview: function(text) { - return this.func(text); - }, - reverse: function(text) { - return this.func(text); // Reversing is its own inverse - } - }, - - url: { - name: 'URL Encode', - func: function(text) { - try { - return encodeURIComponent(text); - } catch (e) { - // Catch malformed Unicode or unpaired surrogates - return '[Invalid input]'; - } - }, - preview: function(text) { - return this.func(text); - }, - reverse: function(text) { - try { - return decodeURIComponent(text); - } catch (e) { - return text; - } - } - }, - - html: { - name: 'HTML Entities', - func: function(text) { - return text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - }, - preview: function(text) { - return this.func(text); - }, - reverse: function(text) { - return text - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, '\''); - } - }, - - pigLatin: { - name: 'Pig Latin', - func: function(text) { - return text.split(/\s+/).map(word => { - if (!word) return ''; - - // Check if the word starts with a vowel - if (/^[aeiou]/i.test(word)) { - return word + 'way'; - } - - // Handle consonant clusters at the beginning - const match = word.match(/^([^aeiou]+)(.*)/i); - if (match) { - return match[2] + match[1] + 'ay'; - } - - return word; - }).join(' '); - }, - preview: function(text) { - return this.func(text); - }, - reverse: function(text) { - return text.split(/\s+/).map(word => { - if (!word) return ''; - - // Check if the word ends with 'way' (vowel case) - if (word.endsWith('way')) { - return word.slice(0, -3); - } - - // Check if the word ends with 'ay' (consonant case) - if (word.endsWith('ay')) { - // Extract the part before 'ay' - const base = word.slice(0, -2); - - // Find the last consonant cluster - // In Pig Latin, the original first consonant cluster is moved to the end - // So we need to move it back to the beginning - for (let i = 1; i <= base.length; i++) { - const possibleCluster = base.slice(-i); - const possibleResult = possibleCluster + base.slice(0, -i); - - // If this looks like a valid word, return it - // This is a simple heuristic and might not work for all cases - if (/^[bcdfghjklmnpqrstvwxyz]/i.test(possibleResult)) { - return possibleResult; - } - } - } - - return word; - }).join(' '); - } - }, - - rainbow: { - name: 'Rainbow Text', - colors: ['#FF0000', '#FF7F00', '#FFFF00', '#00FF00', '#0000FF', '#4B0082', '#9400D3'], - func: function(text) { - // This is just a preview function that returns a description - // The actual rainbow effect is applied in the UI - return text; - }, - preview: function(text) { - return text; - } - }, - - rot47: { - name: 'ROT47', - func: function(text) { - return [...text].map(c => { - const code = c.charCodeAt(0); - // ROT47 operates on a character set from ASCII 33 to ASCII 126 - if (code >= 33 && code <= 126) { - return String.fromCharCode(33 + ((code - 33 + 14) % 94)); - } - return c; - }).join(''); - }, - preview: function(text) { - return this.func(text); - }, - reverse: function(text) { - return [...text].map(c => { - const code = c.charCodeAt(0); - if (code >= 33 && code <= 126) { - return String.fromCharCode(33 + ((code - 33 + 94 - 14) % 94)); - } - return c; - }).join(''); - } - }, - - base32: { - name: 'Base32', - alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', - func: function(text) { - if (!text) return ''; - - // Convert text to bytes - const bytes = new TextEncoder().encode(text); - let result = ''; - let bits = 0; - let value = 0; - - for (let i = 0; i < bytes.length; i++) { - value = (value << 8) | bytes[i]; - bits += 8; - - while (bits >= 5) { - bits -= 5; - result += this.alphabet[(value >> bits) & 0x1F]; - } - } - - // Handle remaining bits - if (bits > 0) { - result += this.alphabet[(value << (5 - bits)) & 0x1F]; - } - - // Add padding - while (result.length % 8 !== 0) { - result += '='; - } - - return result; - }, - preview: function(text) { - if (!text) return '[base32]'; - const result = this.func(text.slice(0, 2)); - return result + '...'; - }, - reverse: function(text) { - if (!text) return ''; - - // Remove padding and whitespace - text = text.replace(/\s+/g, '').replace(/=+$/, ''); - - if (text.length === 0) return ''; - - // Create reverse map - const revMap = {}; - for (let i = 0; i < this.alphabet.length; i++) { - revMap[this.alphabet[i]] = i; - } - - let result = ''; - let bits = 0; - let value = 0; - - for (let i = 0; i < text.length; i++) { - const char = text[i].toUpperCase(); - if (revMap[char] === undefined) continue; // Skip invalid characters - - value = (value << 5) | revMap[char]; - bits += 5; - - while (bits >= 8) { - bits -= 8; - result += String.fromCharCode((value >> bits) & 0xFF); - } - } - - return result; - } - }, - - greek: { - name: 'Greek Letters', - map: { - 'a': 'ฮฑ', 'b': 'ฮฒ', 'c': 'ฯ‡', 'd': 'ฮด', 'e': 'ฮต', 'f': 'ฯ†', 'g': 'ฮณ', 'h': 'ฮท', - 'i': 'ฮน', 'j': 'ฮพ', 'k': 'ฮบ', 'l': 'ฮป', 'm': 'ฮผ', 'n': 'ฮฝ', 'o': 'ฮฟ', 'p': 'ฯ€', - 'q': 'ฮธ', 'r': 'ฯ', 's': 'ฯƒ', 't': 'ฯ„', 'u': 'ฯ…', 'v': 'ฯ‚', 'w': 'ฯ‰', 'x': 'ฯ‡', - 'y': 'ฯˆ', 'z': 'ฮถ', - 'A': 'ฮ‘', 'B': 'ฮ’', 'C': 'ฮง', 'D': 'ฮ”', 'E': 'ฮ•', 'F': 'ฮฆ', 'G': 'ฮ“', 'H': 'ฮ—', - 'I': 'ฮ™', 'J': 'ฮž', 'K': 'ฮš', 'L': 'ฮ›', 'M': 'ฮœ', 'N': 'ฮ', 'O': 'ฮŸ', 'P': 'ฮ ', - 'Q': 'ฮ˜', 'R': 'ฮก', 'S': 'ฮฃ', 'T': 'ฮค', 'U': 'ฮฅ', 'V': 'ฯ‚', 'W': 'ฮฉ', 'X': 'ฮง', - 'Y': 'ฮจ', 'Z': 'ฮ–' - }, - func: function(text) { - return text.split('').map(char => this.map[char] || char).join(''); - }, - preview: function(text) { - if (!text) return '[greek]'; - return this.func(text.slice(0, 10)); - }, - reverseMap: function() { - if (!this._reverseMap) { - this._reverseMap = {}; - for (let key in this.map) { - this._reverseMap[this.map[key]] = key; - } - } - return this._reverseMap; - }, - reverse: function(text) { - const revMap = this.reverseMap(); - return text.split('').map(char => revMap[char] || char).join(''); - } - }, - - wingdings: { - name: 'Wingdings', - map: { - 'a': 'โ™‹', 'b': 'โ™Œ', 'c': 'โ™', 'd': 'โ™Ž', 'e': 'โ™', 'f': 'โ™', 'g': 'โ™‘', 'h': 'โ™’', - 'i': 'โ™“', 'j': 'โ›Ž', 'k': 'โ˜€', 'l': 'โ˜', 'm': 'โ˜‚', 'n': 'โ˜ƒ', 'o': 'โ˜„', 'p': 'โ˜…', - 'q': 'โ˜†', 'r': 'โ˜‡', 's': 'โ˜ˆ', 't': 'โ˜‰', 'u': 'โ˜Š', 'v': 'โ˜‹', 'w': 'โ˜Œ', 'x': 'โ˜', - 'y': 'โ˜Ž', 'z': 'โ˜', - 'A': 'โ™ ', 'B': 'โ™ก', 'C': 'โ™ข', 'D': 'โ™ฃ', 'E': 'โ™ค', 'F': 'โ™ฅ', 'G': 'โ™ฆ', 'H': 'โ™ง', - 'I': 'โ™จ', 'J': 'โ™ฉ', 'K': 'โ™ช', 'L': 'โ™ซ', 'M': 'โ™ฌ', 'N': 'โ™ญ', 'O': 'โ™ฎ', 'P': 'โ™ฏ', - 'Q': 'โœ', 'R': 'โœ‚', 'S': 'โœƒ', 'T': 'โœ„', 'U': 'โœ†', 'V': 'โœ‡', 'W': 'โœˆ', 'X': 'โœ‰', - 'Y': 'โœŒ', 'Z': 'โœ', - '0': 'โœ“', '1': 'โœ”', '2': 'โœ•', '3': 'โœ–', '4': 'โœ—', '5': 'โœ˜', '6': 'โœ™', '7': 'โœš', - '8': 'โœ›', '9': 'โœœ', - '.': 'โœ ', ',': 'โœก', '?': 'โœข', '!': 'โœฃ', '@': 'โœค', '#': 'โœฅ', '$': 'โœฆ', '%': 'โœง', - '^': 'โœฉ', '&': 'โœช', '*': 'โœซ', '(': 'โœฌ', ')': 'โœญ', '-': 'โœฎ', '_': 'โœฏ', '=': 'โœฐ', - '+': 'โœฑ', '[': 'โœฒ', ']': 'โœณ', '{': 'โœด', '}': 'โœต', '|': 'โœถ', '\\': 'โœท', ';': 'โœธ', - ':': 'โœน', '"': 'โœบ', '\'': 'โœป', '<': 'โœผ', '>': 'โœฝ', '/': 'โœพ', '~': 'โœฟ', '`': 'โ€' - }, - func: function(text) { - return text.split('').map(char => this.map[char] || char).join(''); - }, - preview: function(text) { - if (!text) return '[wingdings]'; - return this.func(text.slice(0, 10)); - }, - reverseMap: function() { - if (!this._reverseMap) { - this._reverseMap = {}; - for (let key in this.map) { - this._reverseMap[this.map[key]] = key; - } - } - return this._reverseMap; - }, - reverse: function(text) { - const revMap = this.reverseMap(); - return text.split('').map(char => revMap[char] || char).join(''); - } - }, - - // Fantasy and Fictional Languages - - quenya: { - name: 'Quenya (Tolkien Elvish)', - map: { - 'a': 'a', 'b': 'v', 'c': 'k', 'd': 'd', 'e': 'e', 'f': 'f', 'g': 'g', 'h': 'h', 'i': 'i', - 'j': 'y', 'k': 'k', 'l': 'l', 'm': 'm', 'n': 'n', 'o': 'o', 'p': 'p', 'q': 'kw', 'r': 'r', - 's': 's', 't': 't', 'u': 'u', 'v': 'v', 'w': 'w', 'x': 'ks', 'y': 'y', 'z': 'z', - 'A': 'A', 'B': 'V', 'C': 'K', 'D': 'D', 'E': 'E', 'F': 'F', 'G': 'G', 'H': 'H', 'I': 'I', - 'J': 'Y', 'K': 'K', 'L': 'L', 'M': 'M', 'N': 'N', 'O': 'O', 'P': 'P', 'Q': 'KW', 'R': 'R', - 'S': 'S', 'T': 'T', 'U': 'U', 'V': 'V', 'W': 'W', 'X': 'KS', 'Y': 'Y', 'Z': 'Z' - }, - func: function(text) { - return [...text.toLowerCase()].map(c => this.map[c] || c).join(''); - }, - preview: function(text) { - return this.func(text); - }, - reverse: function(text) { - // Create reverse map - const revMap = {}; - for (const [key, value] of Object.entries(this.map)) { - revMap[value] = key; - } - return [...text].map(c => revMap[c] || c).join(''); - } - }, - - tengwar: { - name: 'Tengwar Script', - map: { - 'a': 'แšช', 'b': 'แ›’', 'c': 'แ›ฃ', 'd': 'แ›ž', 'e': 'แ›–', 'f': 'แš ', 'g': 'แšท', 'h': 'แšบ', 'i': 'แ›', - 'j': 'แ›ƒ', 'k': 'แ›ฃ', 'l': 'แ›š', 'm': 'แ›—', 'n': 'แšพ', 'o': 'แšฉ', 'p': 'แ›ˆ', 'q': 'แ›ฉ', 'r': 'แšฑ', - 's': 'แ›‹', 't': 'แ›', 'u': 'แšข', 'v': 'แšก', 'w': 'แšน', 'x': 'แ›‰', 'y': 'แšฃ', 'z': 'แ›‰', - '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.toLowerCase()].map(c => this.map[c] || c).join(''); - }, - preview: function(text) { - return this.func(text); - }, - reverse: function(text) { - const revMap = {}; - for (const [key, value] of Object.entries(this.map)) { - revMap[value] = key; - } - return [...text].map(c => revMap[c] || c).join(''); - } - }, - - klingon: { - name: 'Klingon', - map: { - 'a': 'a', 'b': 'b', 'c': 'ch', 'd': 'D', 'e': 'e', 'f': 'f', 'g': 'gh', 'h': 'H', 'i': 'I', - 'j': 'j', 'k': 'q', 'l': 'l', 'm': 'm', 'n': 'n', 'o': 'o', 'p': 'p', 'q': 'Q', 'r': 'r', - 's': 'S', 't': 't', 'u': 'u', 'v': 'v', 'w': 'w', 'x': 'x', 'y': 'y', 'z': 'z', - 'A': 'A', 'B': 'B', 'C': 'CH', 'D': 'D', 'E': 'E', 'F': 'F', 'G': 'GH', 'H': 'H', 'I': 'I', - 'J': 'J', 'K': 'Q', 'L': 'L', 'M': 'M', 'N': 'N', 'O': 'O', 'P': 'P', 'Q': 'Q', 'R': 'R', - 'S': 'S', 'T': 'T', 'U': 'U', 'V': 'V', 'W': 'W', 'X': 'X', 'Y': 'Y', 'Z': 'Z' - }, - func: function(text) { - // Process character by character, preserving case - return [...text].map(c => this.map[c] || c).join(''); - }, - preview: function(text) { - if (!text) return '[klingon]'; - return this.func(text.slice(0, 8)); - }, - reverse: function(text) { - // Build reverse map with multi-character strings - const revMap = {}; - for (const [key, value] of Object.entries(this.map)) { - revMap[value] = key; - } - // Try to match multi-character sequences first, then single chars - let result = ''; - let i = 0; - while (i < text.length) { - // Try 2-character match first (for 'ch', 'gh', 'CH', 'GH') - const twoChar = text.substr(i, 2); - if (revMap[twoChar]) { - result += revMap[twoChar]; - i += 2; - } else if (revMap[text[i]]) { - result += revMap[text[i]]; - i++; - } else { - result += text[i]; - i++; - } - } - return result; - } - }, - - aurebesh: { - name: 'Aurebesh (Star Wars)', - map: { - 'a': 'Aurek', 'b': 'Besh', 'c': 'Cresh', 'd': 'Dorn', 'e': 'Esk', 'f': 'Forn', 'g': 'Grek', 'h': 'Herf', 'i': 'Isk', - 'j': 'Jenth', 'k': 'Krill', 'l': 'Leth', 'm': 'Mern', 'n': 'Nern', 'o': 'Osk', 'p': 'Peth', 'q': 'Qek', 'r': 'Resh', - 's': 'Senth', 't': 'Trill', 'u': 'Usk', 'v': 'Vev', 'w': 'Wesk', 'x': 'Xesh', 'y': 'Yirt', 'z': 'Zerek', - 'A': 'AUREK', 'B': 'BESH', 'C': 'CRESH', 'D': 'DORN', 'E': 'ESK', 'F': 'FORN', 'G': 'GREK', 'H': 'HERF', 'I': 'ISK', - 'J': 'JENTH', 'K': 'KRILL', 'L': 'LETH', 'M': 'MERN', 'N': 'NERN', 'O': 'OSK', 'P': 'PETH', 'Q': 'QEK', 'R': 'RESH', - 'S': 'SENTH', 'T': 'TRILL', 'U': 'USK', 'V': 'VEV', 'W': 'WESK', 'X': 'XESH', 'Y': 'YIRT', 'Z': 'ZEREK' - }, - func: function(text) { - return [...text.toLowerCase()].map(c => this.map[c] || c).join(' '); - }, - preview: function(text) { - return this.func(text); - }, - reverse: function(text) { - const revMap = {}; - for (const [key, value] of Object.entries(this.map)) { - revMap[value.toLowerCase()] = key; - } - return text.split(/\s+/).map(word => revMap[word.toLowerCase()] || word).join(''); - } - }, - - dovahzul: { - name: 'Dovahzul (Dragon)', - map: { - 'a': 'ah', 'b': 'b', 'c': 'k', 'd': 'd', 'e': 'eh', 'f': 'f', 'g': 'g', 'h': 'h', 'i': 'ii', - 'j': 'j', 'k': 'k', 'l': 'l', 'm': 'm', 'n': 'n', 'o': 'o', 'p': 'p', 'q': 'kw', 'r': 'r', - 's': 's', 't': 't', 'u': 'u', 'v': 'v', 'w': 'w', 'x': 'ks', 'y': 'y', 'z': 'z', - 'A': 'AH', 'B': 'B', 'C': 'K', 'D': 'D', 'E': 'EH', 'F': 'F', 'G': 'G', 'H': 'H', 'I': 'II', - 'J': 'J', 'K': 'K', 'L': 'L', 'M': 'M', 'N': 'N', 'O': 'O', 'P': 'P', 'Q': 'KW', 'R': 'R', - 'S': 'S', 'T': 'T', 'U': 'U', 'V': 'V', 'W': 'W', 'X': 'KS', 'Y': 'Y', 'Z': 'Z' - }, - func: function(text) { - return [...text.toLowerCase()].map(c => this.map[c] || c).join(''); - }, - preview: function(text) { - return this.func(text); - }, - reverse: function(text) { - const revMap = {}; - for (const [key, value] of Object.entries(this.map)) { - revMap[value] = key; - } - return [...text].map(c => revMap[c] || c).join(''); - } - }, - - hieroglyphics: { - name: 'Hieroglyphics', - map: { - 'a': '๐“ƒญ', 'b': '๐“ƒฎ', 'c': '๐“ƒฏ', 'd': '๐“ƒฐ', 'e': '๐“ƒฑ', 'f': '๐“ƒฒ', 'g': '๐“ƒณ', 'h': '๐“ƒด', 'i': '๐“ƒต', - 'j': '๐“ƒถ', 'k': '๐“ƒท', 'l': '๐“ƒธ', 'm': '๐“ƒน', 'n': '๐“ƒบ', 'o': '๐“ƒป', 'p': '๐“ƒผ', 'q': '๐“ƒฝ', 'r': '๐“ƒพ', - 's': '๐“ƒฟ', 't': '๐“„€', 'u': '๐“„', 'v': '๐“„‚', 'w': '๐“„ƒ', 'x': '๐“„„', 'y': '๐“„…', 'z': '๐“„†', - 'A': '๐“„‡', 'B': '๐“„ˆ', 'C': '๐“„‰', 'D': '๐“„Š', 'E': '๐“„‹', 'F': '๐“„Œ', 'G': '๐“„', 'H': '๐“„Ž', 'I': '๐“„', - 'J': '๐“„', 'K': '๐“„‘', 'L': '๐“„’', 'M': '๐“„“', 'N': '๐“„”', 'O': '๐“„•', 'P': '๐“„–', 'Q': '๐“„—', 'R': '๐“„˜', - 'S': '๐“„™', 'T': '๐“„š', 'U': '๐“„›', 'V': '๐“„œ', 'W': '๐“„', 'X': '๐“„ž', 'Y': '๐“„Ÿ', 'Z': '๐“„ ' - }, - func: function(text) { - return [...text.toLowerCase()].map(c => this.map[c] || c).join(''); - }, - preview: function(text) { - return this.func(text); - }, - reverse: function(text) { - const revMap = {}; - for (const [key, value] of Object.entries(this.map)) { - revMap[value] = key; - } - return [...text].map(c => revMap[c] || c).join(''); - } - }, - - ogham: { - name: 'Ogham (Celtic)', - map: { - 'a': 'แš', 'b': 'แš', 'c': 'แš‰', 'd': 'แš‡', 'e': 'แš“', 'f': 'แšƒ', 'g': 'แšŒ', 'h': 'แš†', 'i': 'แš”', - 'j': 'แšˆ', 'k': 'แšŠ', 'l': 'แš‚', 'm': 'แš‹', 'n': 'แš…', 'o': 'แš‘', 'p': 'แšš', 'q': 'แšŠ', 'r': 'แš', - 's': 'แš„', 't': 'แšˆ', 'u': 'แš’', 'v': 'แšƒ', 'w': 'แšƒ', 'x': 'แšŠ', 'y': 'แš”', 'z': 'แšŽ', - 'A': 'แš', 'B': 'แš', 'C': 'แš‰', 'D': 'แš‡', 'E': 'แš“', 'F': 'แšƒ', 'G': 'แšŒ', 'H': 'แš†', 'I': 'แš”', - 'J': 'แšˆ', 'K': 'แšŠ', 'L': 'แš‚', 'M': 'แš‹', 'N': 'แš…', 'O': 'แš‘', 'P': 'แšš', 'Q': 'แšŠ', 'R': 'แš', - 'S': 'แš„', 'T': 'แšˆ', 'U': 'แš’', 'V': 'แšƒ', 'W': 'แšƒ', 'X': 'แšŠ', 'Y': 'แš”', 'Z': 'แšŽ' - }, - func: function(text) { - return [...text.toLowerCase()].map(c => this.map[c] || c).join(''); - }, - preview: function(text) { - return this.func(text); - }, - reverse: function(text) { - const revMap = {}; - for (const [key, value] of Object.entries(this.map)) { - revMap[value] = key; - } - return [...text].map(c => revMap[c] || c).join(''); - } - }, - - semaphore: { - name: 'Semaphore Flags', - // Positions 1..8 around the clock: 1=โฌ†๏ธ 2=โ†—๏ธ 3=โžก๏ธ 4=โ†˜๏ธ 5=โฌ‡๏ธ 6=โ†™๏ธ 7=โฌ…๏ธ 8=โ†–๏ธ - arrows: ['','โฌ†๏ธ','โ†—๏ธ','โžก๏ธ','โ†˜๏ธ','โฌ‡๏ธ','โ†™๏ธ','โฌ…๏ธ','โ†–๏ธ'], - // Standard semaphore mapping (J is special: 2-1) - table: { - 'A':[1,2],'B':[1,3],'C':[1,4],'D':[1,5],'E':[1,6],'F':[1,7],'G':[1,8], - 'H':[2,3],'I':[2,4],'J':[2,1], - 'K':[2,5],'L':[2,6],'M':[2,7],'N':[2,8], - 'O':[3,4],'P':[3,5],'Q':[3,6],'R':[3,7],'S':[3,8], - 'T':[4,5],'U':[4,6],'V':[4,7],'W':[4,8], - 'X':[5,6],'Y':[5,7],'Z':[5,8] - }, - encodePair: function(pair) { return this.arrows[pair[0]] + this.arrows[pair[1]]; }, - buildReverse: function() { - if (this._rev) return this._rev; - const rev = {}; - for (const [k,v] of Object.entries(this.table)) { - rev[this.encodePair(v)] = k; - } - this._rev = rev; return rev; - }, - func: function(text) { - return [...text].map(ch => { - if (/\s/.test(ch)) return '/'; - const up = ch.toUpperCase(); - const pair = this.table[up]; - return pair ? this.encodePair(pair) : ch; - }).join(' '); - }, - preview: function(text) { - return this.func((text || 'flag').slice(0, 4)); - }, - reverse: function(text) { - const rev = this.buildReverse(); - const tokens = text.trim().split(/\s+/); - return tokens.map(tok => { - if (tok === '/') return ' '; - // Some platforms add variation selectors; normalize by direct match first - return rev[tok] || tok; - }).join(''); - } - }, - - brainfuck: { - name: 'Brainfuck', - 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.toLowerCase()].map(c => this.map[c] || c).join(''); - }, - preview: function(text) { - return '[brainfuck]'; - } - }, - - mathematical: { - name: 'Mathematical Notation', - map: { - 'a': '๐’ถ', 'b': '๐’ท', 'c': '๐’ธ', 'd': '๐’น', 'e': '๐‘’', 'f': '๐’ป', 'g': '๐‘”', 'h': '๐’ฝ', 'i': '๐’พ', - 'j': '๐’ฟ', 'k': '๐“€', 'l': '๐“', 'm': '๐“‚', 'n': '๐“ƒ', 'o': '๐‘œ', 'p': '๐“…', 'q': '๐“†', 'r': '๐“‡', - 's': '๐“ˆ', 't': '๐“‰', 'u': '๐“Š', 'v': '๐“‹', 'w': '๐“Œ', 'x': '๐“', 'y': '๐“Ž', 'z': '๐“', - 'A': '๐’œ', 'B': 'โ„ฌ', 'C': '๐’ž', 'D': '๐’Ÿ', 'E': 'โ„ฐ', 'F': 'โ„ฑ', 'G': '๐’ข', 'H': 'โ„‹', 'I': 'โ„', - 'J': '๐’ฅ', 'K': '๐’ฆ', 'L': 'โ„’', 'M': 'โ„ณ', 'N': '๐’ฉ', 'O': '๐’ช', 'P': '๐’ซ', 'Q': '๐’ฌ', 'R': 'โ„›', - 'S': '๐’ฎ', 'T': '๐’ฏ', 'U': '๐’ฐ', 'V': '๐’ฑ', 'W': '๐’ฒ', 'X': '๐’ณ', 'Y': '๐’ด', 'Z': '๐’ต' - }, - func: function(text) { - return [...text].map(c => this.map[c] || c).join(''); - }, - preview: function(text) { - return this.func(text); - }, - reverse: function(text) { - const revMap = {}; - for (const [key, value] of Object.entries(this.map)) { - revMap[value] = key; - } - return [...text].map(c => revMap[c] || c).join(''); - } - }, - - chemical: { - name: 'Chemical Symbols', - map: { - 'a': 'Ac', 'b': 'B', 'c': 'C', 'd': 'D', 'e': 'Es', 'f': 'F', 'g': 'Ge', 'h': 'H', 'i': 'I', - 'j': 'J', 'k': 'K', 'l': 'L', 'm': 'Mn', 'n': 'N', 'o': 'O', 'p': 'P', 'q': 'Q', 'r': 'R', - 's': 'S', 't': 'Ti', 'u': 'U', 'v': 'V', 'w': 'W', 'x': 'Xe', 'y': 'Y', 'z': 'Zn', - 'A': 'AC', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'ES', 'F': 'F', 'G': 'GE', 'H': 'H', 'I': 'I', - 'J': 'J', 'K': 'K', 'L': 'L', 'M': 'MN', 'N': 'N', 'O': 'O', 'P': 'P', 'Q': 'Q', 'R': 'R', - 'S': 'S', 'T': 'TI', 'U': 'U', 'V': 'V', 'W': 'W', 'X': 'XE', 'Y': 'Y', 'Z': 'ZN' - }, - func: function(text) { - return [...text.toLowerCase()].map(c => this.map[c] || c).join(''); - }, - preview: function(text) { - return this.func(text); - }, - reverse: function(text) { - const revMap = {}; - for (const [key, value] of Object.entries(this.map)) { - revMap[value] = key; - } - return [...text].map(c => revMap[c] || c).join(''); - } - }, - - // Base58 (Bitcoin alphabet) - base58: { - name: 'Base58', - alphabet: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz', - func: function(text) { - if (!text) return ''; - const bytes = new TextEncoder().encode(text); - // Count leading zeros - let zeros = 0; - for (let b of bytes) { if (b === 0) zeros++; else break; } - // Convert to BigInt - let n = 0n; - for (let b of bytes) { n = (n << 8n) + BigInt(b); } - // Encode - let out = ''; - while (n > 0n) { - const rem = n % 58n; - n = n / 58n; - out = this.alphabet[Number(rem)] + out; - } - // Add leading zeros as '1' - for (let i = 0; i < zeros; i++) out = '1' + out; - return out || '1'; - }, - preview: function(text) { - if (!text) return '[base58]'; - return this.func(text.slice(0, 3)) + '...'; - }, - reverse: function(text) { - if (!text) return ''; - // Count leading '1's - let zeros = 0; - for (let c of text) { if (c === '1') zeros++; else break; } - // Convert to BigInt - let n = 0n; - for (let c of text) { - const i = this.alphabet.indexOf(c); - if (i < 0) continue; - n = n * 58n + BigInt(i); - } - // Convert BigInt to bytes - const bytes = []; - while (n > 0n) { - bytes.unshift(Number(n % 256n)); - n = n / 256n; - } - for (let i = 0; i < zeros; i++) bytes.unshift(0); - return new TextDecoder().decode(Uint8Array.from(bytes)); - } - }, - - // Base62 (0-9A-Za-z) - base62: { - name: 'Base62', - alphabet: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', - func: function(text) { - if (!text) return ''; - const bytes = new TextEncoder().encode(text); - let n = 0n; - for (let b of bytes) { n = (n << 8n) + BigInt(b); } - if (n === 0n) return '0'; - let out = ''; - while (n > 0n) { - const rem = n % 62n; - n = n / 62n; - out = this.alphabet[Number(rem)] + out; - } - return out; - }, - preview: function(text) { - if (!text) return '[base62]'; - return this.func(text.slice(0, 3)) + '...'; - }, - reverse: function(text) { - if (!text) return ''; - let n = 0n; - for (let c of text) { - const i = this.alphabet.indexOf(c); - if (i < 0) continue; - n = n * 62n + BigInt(i); - } - const bytes = []; - while (n > 0n) { - bytes.unshift(Number(n % 256n)); - n = n / 256n; - } - if (bytes.length === 0) bytes.push(0); - return new TextDecoder().decode(Uint8Array.from(bytes)); - } - }, - - // Roman Numerals (1..3999) - roman_numerals: { - name: 'Roman Numerals', - numerals: [ - ['M',1000],['CM',900],['D',500],['CD',400], - ['C',100],['XC',90],['L',50],['XL',40], - ['X',10],['IX',9],['V',5],['IV',4],['I',1] - ], - func: function(text) { - return text.replace(/\b\d+\b/g, m => { - let num = parseInt(m,10); - if (num <= 0 || num > 3999 || isNaN(num)) return m; - let out = ''; - for (const [sym,val] of this.numerals) { - while (num >= val) { out += sym; num -= val; } - } - return out; - }); - }, - preview: function(text) { - return this.func(text || '2024'); - }, - reverse: function(text) { - // Greedy parse roman numerals to digits - const map = {I:1,V:5,X:10,L:50,C:100,D:500,M:1000}; - const tokenize = s => s.match(/[IVXLCDM]+|[^IVXLCDM]+/gi) || [s]; - return tokenize(text).map(tok => { - if (!/^[IVXLCDM]+$/i.test(tok)) return tok; - const s = tok.toUpperCase(); - let total = 0; - for (let i=0;i= 65 && code <= 90) { out += String.fromCharCode(65 + ((code-65 + k)%26)); j++; } - else if (code >= 97 && code <= 122) { out += String.fromCharCode(97 + ((code-97 + k)%26)); j++; } - else out += c; - } - return out; - }, - preview: function(text) { - if (!text) return '[Vigenรจre]'; - return this.func(text.slice(0,8)) + (text.length>8?'...':''); - }, - reverse: function(text) { - const key = this.key; - let out = ''; - let j = 0; - for (let i=0;i= 65 && code <= 90) { out += String.fromCharCode(65 + ((code-65 + 26 - (k%26))%26)); j++; } - else if (code >= 97 && code <= 122) { out += String.fromCharCode(97 + ((code-97 + 26 - (k%26))%26)); j++; } - else out += c; - } - return out; - } - }, - - // Rail Fence Cipher (3 rails) - rail_fence: { - name: 'Rail Fence (3 Rails)', - rails: 3, - func: function(text) { - const rails = Array.from({length: this.rails}, () => []); - let rail = 0, dir = 1; - for (const ch of text) { - rails[rail].push(ch); - rail += dir; - if (rail === 0 || rail === this.rails-1) dir *= -1; - } - return rails.flat().join(''); - }, - preview: function(text) { - if (!text) return '[rail]'; - return this.func(text.slice(0,12)) + (text.length>12?'...':''); - }, - reverse: function(text) { - const len = text.length; - const pattern = []; - let rail = 0, dir = 1; - for (let i=0;i { - const code = c.charCodeAt(0); - if (code >= 65 && code <= 90) return String.fromCharCode(65 + ((code-65 + 13)%26)); - if (code >= 97 && code <= 122) return String.fromCharCode(97 + ((code-97 + 13)%26)); - return c; - }; - const rot5 = c => { - if (c >= '0' && c <= '9') return String.fromCharCode(48 + (((c.charCodeAt(0)-48)+5)%10)); - return c; - }; - return [...text].map(c => rot5(rot13(c))).join(''); - }, - preview: function(text) { - if (!text) return '[rot18]'; - return this.func(text.slice(0, 8)) + (text.length>8?'...':''); - }, - reverse: function(text) { return this.func(text); } - }, - - // A1Z26 (letters to 1-26, separated by hyphens) - a1z26: { - name: 'A1Z26', - func: function(text) { - return text.replace(/[A-Za-z]/g, c => { - const n = (c.toUpperCase().charCodeAt(0) - 64); - return String(n) + '-'; - }).replace(/-+(?!\d)/g,'-').replace(/-+$/,''); - }, - preview: function(text) { - if (!text) return '[1-26]'; - return this.func(text.slice(0, 5)) + '...'; - }, - reverse: function(text) { - return text.split(/([^0-9]+)/).map(tok => { - if (!/^\d+$/.test(tok)) return tok; - const n = parseInt(tok,10); - if (n>=1 && n<=26) return String.fromCharCode(64+n).toLowerCase(); - return tok; - }).join(''); - } - }, - - // Ubbi Dubbi (language game) - ubbi_dubbi: { - name: 'Ubbi Dubbi', - func: function(text) { - // Insert 'ub' before vowels (simple, reversible scheme) - return text.replace(/([AEIOUaeiou])/g, 'ub$1'); - }, - preview: function(text) { - if (!text) return 'hubellubo'; - return this.func(text.slice(0, 8)) + (text.length > 8 ? '...' : ''); - }, - reverse: function(text) { - return text.replace(/ub([AEIOUaeiou])/g, '$1'); - } - }, - - // Rรถvarsprรฅket (Robber's language) - rovarspraket: { - name: 'Rรถvarsprรฅket', - isConsonant: function(c) { return /[bcdfghjklmnpqrstvwxyz]/i.test(c); }, - func: function(text) { - return [...text].map(ch => this.isConsonant(ch) ? (ch + 'o' + ch) : ch).join(''); - }, - preview: function(text) { - if (!text) return 'totexxtot'; - return this.func(text.slice(0, 6)) + (text.length > 6 ? '...' : ''); - }, - reverse: function(text) { - // Collapse consonant-o-consonant patterns where the two consonants match - return text.replace(/([bcdfghjklmnpqrstvwxyz])o\1/gi, '$1'); - } - }, - - // Baconian cipher (A/B 5-bit encoding for A-Z) - baconian: { - name: 'Baconian Cipher', - table: (function(){ - const map = {}; - const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - for (let i=0;i<26;i++) { - const code = i.toString(2).padStart(5,'0').replace(/0/g,'A').replace(/1/g,'B'); - map[alphabet[i]] = code; - } - return map; - })(), - func: function(text) { - return [...text.toUpperCase()].map(ch => { - if (this.table[ch]) return this.table[ch]; - if (/[\s]/.test(ch)) return '/'; - return ch; - }).join(' '); - }, - preview: function(text) { - if (!text) return 'AAAAA AABBA ...'; - return this.func((text || 'AB').slice(0,2)); - }, - reverse: function(text) { - const rev = {}; - Object.keys(this.table).forEach(k => rev[this.table[k]] = k); - const tokens = text.trim().split(/\s+/); - return tokens.map(tok => { - if (tok === '/') return ' '; - const clean = tok.replace(/[^AB]/g,''); - if (clean.length === 5 && rev[clean]) return rev[clean]; - return tok; - }).join(''); - } - }, - - // Tap code (Polybius 5x5 with C/K merged) - tap_code: { - name: 'Tap Code', - letters: 'ABCDEFGHIKLMNOPQRSTUVWXYZ', // no J (traditionally K merges with C or J omitted; use no J) - buildMap: function() { - if (this._map) return this._map; - const map = {}; const rev = {}; - for (let i=0;i I - const [r,c] = this._map['I']; out.push('.'.repeat(r)+'.'+'.'.repeat(c)); continue; - } - const coords = this._map[ch]; - if (coords) { - out.push('.'.repeat(coords[0]) + ' ' + '.'.repeat(coords[1])); - } else if (/\s/.test(ch)) { - out.push('/'); - } else { - out.push(ch); - } - } - return out.join(' '); - }, - preview: function(text) { - return this.func((text || 'tap').slice(0,3)); - }, - reverse: function(text) { - this.buildMap(); - const toks = text.trim().split(/\s+/); - const out = []; - for (let i=0;i index[c]).filter(v => v !== undefined); - const out = []; - for (let i=0;i> 8, x & 0xFF); - } else if (i+1 < codes.length) { - const x = codes[i] + codes[i+1]*45; - out.push(x & 0xFF); - } - } - return new TextDecoder().decode(Uint8Array.from(out)); - } - }, - - // Affine Cipher (a=5, b=8) - affine: { - name: 'Affine Cipher (a=5,b=8)', - a: 5, b: 8, m: 26, invA: 21, // 5*21 โ‰ก 1 (mod 26) - func: function(text) { - const {a,b,m} = this; - return [...text].map(c => { - const code = c.charCodeAt(0); - if (code>=65 && code<=90) return String.fromCharCode(65 + ((a*(code-65)+b)%m)); - if (code>=97 && code<=122) return String.fromCharCode(97 + ((a*(code-97)+b)%m)); - return c; - }).join(''); - }, - preview: function(text) { - if (!text) return '[affine]'; - return this.func(text.slice(0,8)) + (text.length>8?'...':''); - }, - reverse: function(text) { - const {invA,b,m} = this; - return [...text].map(c => { - const code = c.charCodeAt(0); - if (code>=65 && code<=90) return String.fromCharCode(65 + ((invA*((code-65 - b + m)%m))%m)); - if (code>=97 && code<=122) return String.fromCharCode(97 + ((invA*((code-97 - b + m)%m))%m)); - return c; - }).join(''); - } - }, - - // QWERTY Right-Shift (maps to next key on same row) - qwerty_shift: { - name: 'QWERTY Right Shift', - rows: [ - 'qwertyuiop', - 'asdfghjkl', - 'zxcvbnm' - ], - buildMap: function() { - if (this._map) return this._map; - const map = {}; - for (const row of this.rows) { - for (let i=0;i m[c] || c).join(''); - }, - preview: function(text) { - if (!text) return '[qwerty]'; - return this.func(text.slice(0,8)) + (text.length>8?'...':''); - }, - reverse: function(text) { - const m = this.buildMap(); - const inv = {}; - Object.keys(m).forEach(k => inv[m[k]] = k); - return [...text].map(c => inv[c] || c).join(''); - } - }, - - // Case/formatting transforms - title_case: { - name: 'Title Case', - func: function(text) { - return text.replace(/\w\S*/g, (w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()); - }, - preview: function(text) { - if (!text) return '[Title Case]'; - return this.func(text.slice(0, 12)) + (text.length > 12 ? '...' : ''); - } - }, - - sentence_case: { - name: 'Sentence Case', - func: function(text) { - if (!text) return ''; - const lower = text.toLowerCase(); - return lower.charAt(0).toUpperCase() + lower.slice(1); - }, - preview: function(text) { - if (!text) return '[Sentence]'; - return this.func(text.slice(0, 12)) + (text.length > 12 ? '...' : ''); - } - }, - - camel_case: { - name: 'camelCase', - func: function(text) { - const parts = text.split(/[^a-zA-Z0-9]+/).filter(Boolean); - if (parts.length === 0) return ''; - const first = parts[0].toLowerCase(); - const rest = parts.slice(1).map(p => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase()).join(''); - return first + rest; - }, - preview: function(text) { - if (!text) return '[camel]'; - return this.func(text); - } - }, - - snake_case: { - name: 'snake_case', - func: function(text) { - return text.trim().split(/[^a-zA-Z0-9]+/).filter(Boolean).map(s => s.toLowerCase()).join('_'); - }, - preview: function(text) { - if (!text) return '[snake]'; - return this.func(text); - } - }, - - kebab_case: { - name: 'kebab-case', - func: function(text) { - return text.trim().split(/[^a-zA-Z0-9]+/).filter(Boolean).map(s => s.toLowerCase()).join('-'); - }, - preview: function(text) { - if (!text) return '[kebab]'; - return this.func(text); - } - }, - - random_case: { - name: 'Random Case', - func: function(text) { - return [...text].map(c => /[a-z]/i.test(c) ? (Math.random() < 0.5 ? c.toLowerCase() : c.toUpperCase()) : c).join(''); - }, - preview: function(text) { - if (!text) return '[RaNdOm]'; - return this.func(text.slice(0, 8)) + (text.length > 8 ? '...' : ''); - } - }, - - disemvowel: { - name: 'Disemvowel', - func: function(text) { - return text.replace(/[aeiouAEIOU]/g, ''); - }, - preview: function(text) { - if (!text) return '[dsmvwl]'; - return this.func(text.slice(0, 12)) + (text.length > 12 ? '...' : ''); - } - }, - - // Emoji letters (Regional Indicator Letters) - regional_indicator: { - name: 'Regional Indicator Letters', - func: function(text) { - const base = 0x1F1E6; - return [...text].map(c => { - const up = c.toUpperCase(); - if (up >= 'A' && up <= 'Z') { - const code = base + (up.charCodeAt(0) - 65); - return String.fromCodePoint(code); - } - return c; - }).join(''); - }, - preview: function(text) { - if (!text) return '๐Ÿ‡ฆ๐Ÿ‡ง๐Ÿ‡จ'; - return this.func(text.slice(0, 4)) + (text.length > 4 ? '...' : ''); - }, - reverse: function(text) { - const base = 0x1F1E6; - return [...text].map(ch => { - const cp = ch.codePointAt(0); - if (cp >= base && cp <= base + 25) { - return String.fromCharCode(65 + (cp - base)); - } - return ch; - }).join(''); - } - }, - - // Fraktur (Mathematical Fraktur letters) - fraktur: { - name: 'Fraktur', - func: function(text) { - const capMap = { - 'A': 0x1D504, 'B': 0x1D505, 'C': 0x212D, 'D': 0x1D507, 'E': 0x1D508, 'F': 0x1D509, 'G': 0x1D50A, - 'H': 0x210C, 'I': 0x2111, 'J': 0x1D50D, 'K': 0x1D50E, 'L': 0x1D50F, 'M': 0x1D510, 'N': 0x1D511, - 'O': 0x1D512, 'P': 0x1D513, 'Q': 0x1D514, 'R': 0x211C, 'S': 0x1D516, 'T': 0x1D517, 'U': 0x1D518, - 'V': 0x1D519, 'W': 0x1D51A, 'X': 0x1D51B, 'Y': 0x1D51C, 'Z': 0x2128 - }; - const lowerBase = 0x1D51E; // 'a' - return [...text].map(c => { - const code = c.charCodeAt(0); - if (c >= 'A' && c <= 'Z') { - const fr = capMap[c]; - return fr ? String.fromCodePoint(fr) : c; - } - if (c >= 'a' && c <= 'z') { - return String.fromCodePoint(lowerBase + (code - 97)); - } - return c; - }).join(''); - }, - preview: function(text) { - if (!text) return '[fraktur]'; - return this.func(text.slice(0, 6)) + (text.length > 6 ? '...' : ''); - }, - reverse: function(text) { - const capMap = { - 0x1D504:'A',0x1D505:'B',0x212D:'C',0x1D507:'D',0x1D508:'E',0x1D509:'F',0x1D50A:'G', - 0x210C:'H',0x2111:'I',0x1D50D:'J',0x1D50E:'K',0x1D50F:'L',0x1D510:'M',0x1D511:'N', - 0x1D512:'O',0x1D513:'P',0x1D514:'Q',0x211C:'R',0x1D516:'S',0x1D517:'T',0x1D518:'U', - 0x1D519:'V',0x1D51A:'W',0x1D51B:'X',0x1D51C:'Y',0x2128:'Z' - }; - const lowerBase = 0x1D51E; - return Array.from(text).map(ch => { - const cp = ch.codePointAt(0); - if (cp in capMap) return capMap[cp]; - if (cp >= lowerBase && cp < lowerBase + 26) return String.fromCharCode(97 + (cp - lowerBase)); - return ch; - }).join(''); - } - }, - - // Cyrillic lookalike stylization - cyrillic_stylized: { - name: 'Cyrillic Stylized', - map: { - 'A':'ะ','B':'ะ’','C':'ะก','E':'ะ•','H':'ะ','K':'ะš','M':'ะœ','O':'ะž','P':'ะ ','T':'ะข','X':'ะฅ','Y':'ะฃ', - 'a':'ะฐ','e':'ะต','o':'ะพ','p':'ั€','c':'ั','y':'ัƒ','x':'ั…','k':'ะบ','h':'าป','m':'ะผ','t':'ั‚','b':'ะฌ' - }, - func: function(text) { - return [...text].map(c => this.map[c] || c).join(''); - }, - preview: function(text) { - if (!text) return '[cyrillic]'; - return this.func(text.slice(0, 8)) + (text.length > 8 ? '...' : ''); - }, - reverse: function(text) { - const rev = {}; - for (const [k,v] of Object.entries(this.map)) rev[v] = k; - return [...text].map(c => rev[c] || c).join(''); - } - }, - - // Simple romaji <-> Katakana converter (approximate) - katakana: { - name: 'Katakana', - table: [ - ['kyo','ใ‚ญใƒง'],['kyu','ใ‚ญใƒฅ'],['kya','ใ‚ญใƒฃ'], - ['sho','ใ‚ทใƒง'],['shu','ใ‚ทใƒฅ'],['sha','ใ‚ทใƒฃ'],['shi','ใ‚ท'], - ['cho','ใƒใƒง'],['chu','ใƒใƒฅ'],['cha','ใƒใƒฃ'],['chi','ใƒ'], - ['tsu','ใƒ„'],['fu','ใƒ•'], - ['ryo','ใƒชใƒง'],['ryu','ใƒชใƒฅ'],['rya','ใƒชใƒฃ'], - ['nyo','ใƒ‹ใƒง'],['nyu','ใƒ‹ใƒฅ'],['nya','ใƒ‹ใƒฃ'], - ['gya','ใ‚ฎใƒฃ'],['gyu','ใ‚ฎใƒฅ'],['gyo','ใ‚ฎใƒง'], - ['hya','ใƒ’ใƒฃ'],['hyu','ใƒ’ใƒฅ'],['hyo','ใƒ’ใƒง'], - ['mya','ใƒŸใƒฃ'],['myu','ใƒŸใƒฅ'],['myo','ใƒŸใƒง'], - ['pya','ใƒ”ใƒฃ'],['pyu','ใƒ”ใƒฅ'],['pyo','ใƒ”ใƒง'], - ['bya','ใƒ“ใƒฃ'],['byu','ใƒ“ใƒฅ'],['byo','ใƒ“ใƒง'], - ['ja','ใ‚ธใƒฃ'],['ju','ใ‚ธใƒฅ'],['jo','ใ‚ธใƒง'], - ['ka','ใ‚ซ'],['ki','ใ‚ญ'],['ku','ใ‚ฏ'],['ke','ใ‚ฑ'],['ko','ใ‚ณ'], - ['ga','ใ‚ฌ'],['gi','ใ‚ฎ'],['gu','ใ‚ฐ'],['ge','ใ‚ฒ'],['go','ใ‚ด'], - ['sa','ใ‚ต'],['su','ใ‚น'],['se','ใ‚ป'],['so','ใ‚ฝ'], - ['za','ใ‚ถ'],['zu','ใ‚บ'],['ze','ใ‚ผ'],['zo','ใ‚พ'], - ['ta','ใ‚ฟ'],['te','ใƒ†'],['to','ใƒˆ'], - ['da','ใƒ€'],['de','ใƒ‡'],['do','ใƒ‰'], - ['na','ใƒŠ'],['ni','ใƒ‹'],['nu','ใƒŒ'],['ne','ใƒ'],['no','ใƒŽ'], - ['ha','ใƒ'],['hi','ใƒ’'],['he','ใƒ˜'],['ho','ใƒ›'], - ['ba','ใƒ'],['bi','ใƒ“'],['bu','ใƒ–'],['be','ใƒ™'],['bo','ใƒœ'], - ['pa','ใƒ‘'],['pi','ใƒ”'],['pu','ใƒ—'],['pe','ใƒš'],['po','ใƒ'], - ['ma','ใƒž'],['mi','ใƒŸ'],['mu','ใƒ '],['me','ใƒก'],['mo','ใƒข'], - ['ra','ใƒฉ'],['ri','ใƒช'],['ru','ใƒซ'],['re','ใƒฌ'],['ro','ใƒญ'], - ['wa','ใƒฏ'],['wo','ใƒฒ'],['n','ใƒณ'], - ['a','ใ‚ข'],['i','ใ‚ค'],['u','ใ‚ฆ'],['e','ใ‚จ'],['o','ใ‚ช'] - ], - func: function(text) { - let i = 0, out = ''; - const lower = text.toLowerCase(); - const sorted = [...this.table].sort((a,b)=>b[0].length-a[0].length); - while (i < lower.length) { - let matched = false; - for (const [rom,kana] of sorted) { - if (lower.startsWith(rom, i)) { - out += kana; - i += rom.length; - matched = true; - break; - } - } - if (!matched) { - out += text[i]; - i += 1; - } - } - return out; - }, - preview: function(text) { - if (!text) return '[ใ‚ซใ‚ฟใ‚ซใƒŠ]'; - return this.func(text.slice(0, 6)) + (text.length > 6 ? '...' : ''); - }, - reverse: function(text) { - const rev = {}; - for (const [rom,kana] of this.table) rev[kana] = rom; - let out = ''; - for (const ch of text) out += (rev[ch] || ch); - return out; - } - }, - - // Romaji <-> Hiragana (approximate) - hiragana: { - name: 'Hiragana', - table: [ - ['kyo','ใใ‚‡'],['kyu','ใใ‚…'],['kya','ใใ‚ƒ'], - ['sho','ใ—ใ‚‡'],['shu','ใ—ใ‚…'],['sha','ใ—ใ‚ƒ'],['shi','ใ—'], - ['cho','ใกใ‚‡'],['chu','ใกใ‚…'],['cha','ใกใ‚ƒ'],['chi','ใก'], - ['tsu','ใค'],['fu','ใต'], - ['ryo','ใ‚Šใ‚‡'],['ryu','ใ‚Šใ‚…'],['rya','ใ‚Šใ‚ƒ'], - ['nyo','ใซใ‚‡'],['nyu','ใซใ‚…'],['nya','ใซใ‚ƒ'], - ['gya','ใŽใ‚ƒ'],['gyu','ใŽใ‚…'],['gyo','ใŽใ‚‡'], - ['hya','ใฒใ‚ƒ'],['hyu','ใฒใ‚…'],['hyo','ใฒใ‚‡'], - ['mya','ใฟใ‚ƒ'],['myu','ใฟใ‚…'],['myo','ใฟใ‚‡'], - ['pya','ใดใ‚ƒ'],['pyu','ใดใ‚…'],['pyo','ใดใ‚‡'], - ['bya','ใณใ‚ƒ'],['byu','ใณใ‚…'],['byo','ใณใ‚‡'], - ['ja','ใ˜ใ‚ƒ'],['ju','ใ˜ใ‚…'],['jo','ใ˜ใ‚‡'], - ['ka','ใ‹'],['ki','ใ'],['ku','ใ'],['ke','ใ‘'],['ko','ใ“'], - ['ga','ใŒ'],['gi','ใŽ'],['gu','ใ'],['ge','ใ’'],['go','ใ”'], - ['sa','ใ•'],['su','ใ™'],['se','ใ›'],['so','ใ'], - ['za','ใ–'],['zu','ใš'],['ze','ใœ'],['zo','ใž'], - ['ta','ใŸ'],['te','ใฆ'],['to','ใจ'], - ['da','ใ '],['de','ใง'],['do','ใฉ'], - ['na','ใช'],['ni','ใซ'],['nu','ใฌ'],['ne','ใญ'],['no','ใฎ'], - ['ha','ใฏ'],['hi','ใฒ'],['he','ใธ'],['ho','ใป'], - ['ba','ใฐ'],['bi','ใณ'],['bu','ใถ'],['be','ใน'],['bo','ใผ'], - ['pa','ใฑ'],['pi','ใด'],['pu','ใท'],['pe','ใบ'],['po','ใฝ'], - ['ma','ใพ'],['mi','ใฟ'],['mu','ใ‚€'],['me','ใ‚'],['mo','ใ‚‚'], - ['ra','ใ‚‰'],['ri','ใ‚Š'],['ru','ใ‚‹'],['re','ใ‚Œ'],['ro','ใ‚'], - ['wa','ใ‚'],['wo','ใ‚’'],['n','ใ‚“'], - ['a','ใ‚'],['i','ใ„'],['u','ใ†'],['e','ใˆ'],['o','ใŠ'] - ], - func: function(text) { - // reuse katakana logic with different table - let i = 0, out = ''; - const lower = text.toLowerCase(); - const sorted = [...this.table].sort((a,b)=>b[0].length-a[0].length); - while (i < lower.length) { - let matched = false; - for (const [rom,kana] of sorted) { - if (lower.startsWith(rom, i)) { - out += kana; - i += rom.length; - matched = true; - break; - } - } - if (!matched) { - out += text[i]; - i += 1; - } - } - return out; - }, - preview: function(text) { - if (!text) return '[ใฒใ‚‰ใŒใช]'; - return this.func(text.slice(0, 6)) + (text.length > 6 ? '...' : ''); - }, - reverse: function(text) { - const rev = {}; - for (const [rom,kana] of this.table) rev[kana] = rom; - let out = ''; - for (const ch of text) out += (rev[ch] || ch); - return out; - } - }, - - // Emoji Speak (word โ†’ emoji, digits โ†’ keycaps) - // Emoji keywords loaded from emojiWordMap.js - emoji_speak: { - name: 'Emoji Speak', - digitMap: {'0':'0๏ธโƒฃ','1':'1๏ธโƒฃ','2':'2๏ธโƒฃ','3':'3๏ธโƒฃ','4':'4๏ธโƒฃ','5':'5๏ธโƒฃ','6':'6๏ธโƒฃ','7':'7๏ธโƒฃ','8':'8๏ธโƒฃ','9':'9๏ธโƒฃ'}, - func: function(text) { - // Replace digits with keycap emojis - let out = [...text].map(c => this.digitMap[c] || c).join(''); - - // Replace words with emojis using keyword lookup - if (window.emojiKeywords) { - // Split into words while preserving spaces and punctuation - const words = out.match(/\b\w+\b/g); - if (words) { - // Process each unique word - const processed = new Set(); - for (const word of words) { - const lower = word.toLowerCase(); - if (processed.has(lower)) continue; - processed.add(lower); - - // Find all emojis that have this word as a keyword - const matchingEmojis = []; - for (const [emoji, keywords] of Object.entries(window.emojiKeywords)) { - if (keywords.includes(lower)) { - matchingEmojis.push(emoji); - } - } - - // If we found matches, replace with a random one - if (matchingEmojis.length > 0) { - const randomEmoji = matchingEmojis[Math.floor(Math.random() * matchingEmojis.length)]; - const re = new RegExp(`\\b${word}\\b`, 'gi'); - out = out.replace(re, randomEmoji); - } - } - } - - // Second pass: Replace single characters and symbols (?, !, <3, arrows, etc.) - // Build a map of all single-char/symbol keywords - const symbolMap = new Map(); - for (const [emoji, keywords] of Object.entries(window.emojiKeywords)) { - for (const keyword of keywords) { - // Only consider symbols (non-word characters or very short patterns) - // Exclude single digits since they're already handled by digitMap - if (keyword.length <= 3 && !/^\w+$/.test(keyword) && !/^\d$/.test(keyword)) { - if (!symbolMap.has(keyword)) { - symbolMap.set(keyword, []); - } - symbolMap.get(keyword).push(emoji); - } - } - } - - // Replace symbols (longest first to handle multi-char like <3 before <) - const sortedSymbols = Array.from(symbolMap.keys()).sort((a, b) => b.length - a.length); - for (const symbol of sortedSymbols) { - if (out.includes(symbol)) { - const matchingEmojis = symbolMap.get(symbol); - const randomEmoji = matchingEmojis[Math.floor(Math.random() * matchingEmojis.length)]; - // Escape special regex characters - const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - out = out.replace(new RegExp(escaped, 'g'), randomEmoji); - } - } - } - return out; - }, - preview: function(text) { - if (!text) return '1๏ธโƒฃ2๏ธโƒฃ3๏ธโƒฃ โœ…'; - return this.func(text.slice(0, 12)) + (text.length > 12 ? '...' : ''); - }, - reverse: function(text) { - let out = text; - // reverse digits - for (const [d, em] of Object.entries(this.digitMap)) { - const re = new RegExp(em.replace(/([.*+?^${}()|\[\]\\])/g, '\\$1'), 'g'); - out = out.replace(re, d); - } - // reverse words - for (const [word, emoji] of Object.entries(this.wordMap)) { - const re = new RegExp(emoji.replace(/([.*+?^${}()|\[\]\\])/g, '\\$1'), 'g'); - out = out.replace(re, word); - } - return out; - } - }, - - // Additional Ciphers - atbash: { - name: 'Atbash Cipher', - func: function(text) { - const a = 'a'.charCodeAt(0), z = 'z'.charCodeAt(0); - const A = 'A'.charCodeAt(0), Z = 'Z'.charCodeAt(0); - return [...text].map(c => { - const code = c.charCodeAt(0); - if (code >= A && code <= Z) return String.fromCharCode(Z - (code - A)); - if (code >= a && code <= z) return String.fromCharCode(z - (code - a)); - return c; - }).join(''); - }, - preview: function(text) { - if (!text) return '[atbash]'; - return this.func(text.slice(0, 6)) + (text.length > 6 ? '...' : ''); - }, - reverse: function(text) { - // Atbash is its own inverse - return this.func(text); - } - }, - - rot5: { - name: 'ROT5', - func: function(text) { - return [...text].map(c => { - if (c >= '0' && c <= '9') { - const n = c.charCodeAt(0) - 48; - return String.fromCharCode(48 + ((n + 5) % 10)); - } - return c; - }).join(''); - }, - preview: function(text) { - if (!text) return '[rot5]'; - return this.func(text.slice(0, 6)) + (text.length > 6 ? '...' : ''); - }, - reverse: function(text) { - // ROT5 is its own inverse - return this.func(text); - } - }, - - // Unicode scripts - superscript: { - name: 'Superscript', - map: { - '0':'โฐ','1':'ยน','2':'ยฒ','3':'ยณ','4':'โด','5':'โต','6':'โถ','7':'โท','8':'โธ','9':'โน', - 'a':'แตƒ','b':'แต‡','c':'แถœ','d':'แตˆ','e':'แต‰','f':'แถ ','g':'แต','h':'สฐ','i':'โฑ','j':'สฒ','k':'แต','l':'หก','m':'แต','n':'โฟ','o':'แต’','p':'แต–','q':'แต ','r':'สณ','s':'หข','t':'แต—','u':'แต˜','v':'แต›','w':'สท','x':'หฃ','y':'สธ','z':'แถป', - 'A':'แดฌ','B':'แดฎ','C':'แถœ','D':'แดฐ','E':'แดฑ','F':'แถ ','G':'แดณ','H':'แดด','I':'แดต','J':'แดถ','K':'แดท','L':'แดธ','M':'แดน','N':'แดบ','O':'แดผ','P':'แดพ','Q':'แต ','R':'แดฟ','S':'หข','T':'แต€','U':'แต','V':'โฑฝ','W':'แต‚','X':'หฃ','Y':'สธ','Z':'แถป' - }, - func: function(text) { - return [...text].map(c => this.map[c] || c).join(''); - }, - preview: function(text) { - if (!text) return '[super]'; - return this.func(text.slice(0, 4)) + (text.length > 4 ? '...' : ''); - }, - reverse: function(text) { - const revMap = {}; - for (const [k,v] of Object.entries(this.map)) revMap[v] = k; - return [...text].map(c => revMap[c] || c).join(''); - } - }, - - subscript: { - name: 'Subscript', - map: { - '0':'โ‚€','1':'โ‚','2':'โ‚‚','3':'โ‚ƒ','4':'โ‚„','5':'โ‚…','6':'โ‚†','7':'โ‚‡','8':'โ‚ˆ','9':'โ‚‰', - 'a':'โ‚','e':'โ‚‘','h':'โ‚•','i':'แตข','j':'โฑผ','k':'โ‚–','l':'โ‚—','m':'โ‚˜','n':'โ‚™','o':'โ‚’','p':'โ‚š','r':'แตฃ','s':'โ‚›','t':'โ‚œ','u':'แตค','v':'แตฅ','x':'โ‚“' - }, - func: function(text) { - return [...text].map(c => this.map[c] || c).join(''); - }, - preview: function(text) { - if (!text) return '[sub]'; - return this.func(text.slice(0, 4)) + (text.length > 4 ? '...' : ''); - }, - reverse: function(text) { - const revMap = {}; - for (const [k,v] of Object.entries(this.map)) revMap[v] = k; - return [...text].map(c => revMap[c] || c).join(''); - } - }, - - // Formatting fun - alternating_case: { - name: 'Alternating Case', - func: function(text) { - let upper = true; - return [...text].map(c => { - if (/[a-zA-Z]/.test(c)) { - const out = upper ? c.toUpperCase() : c.toLowerCase(); - upper = !upper; - return out; - } - return c; - }).join(''); - }, - preview: function(text) { - if (!text) return '[alt case]'; - return this.func(text.slice(0, 6)) + (text.length > 6 ? '...' : ''); - } - }, - - reverse_words: { - name: 'Reverse Words', - func: function(text) { - return text.split(/(\s+)/).reverse().join(''); - }, - preview: function(text) { - if (!text) return '[rev words]'; - // Take last 2-3 words and reverse them to show the effect - const words = text.split(/\s+/); - const lastWords = words.slice(-3).join(' '); - return this.func(lastWords) + '...'; - }, - reverse: function(text) { - // Reversing words twice restores - return this.func(text); - } - }, - - // Special Randomizer Functions - randomizer: { - name: 'Random Mix', - - // Get a list of transforms suitable for randomization - getRandomizableTransforms() { - 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', - 'hieroglyphics', 'ogham', 'mathematical', 'cursive', 'medieval', - 'monospace', 'greek', 'braille', 'alternating_case', 'reverse_words', - 'title_case', 'sentence_case', 'camel_case', 'snake_case', 'kebab_case', 'random_case', - 'regional_indicator', 'fraktur', 'cyrillic_stylized', 'katakana', 'hiragana', 'emoji_speak', - 'base58', 'base62', 'roman_numerals', 'vigenere', 'rail_fence', 'base64url' - ]; - return suitable.filter(name => window.transforms[name]); - }, - - // Apply random transforms to each word in a sentence - func: function(text, options = {}) { - if (!text) return ''; - - const { - preservePunctuation = true, - minTransforms = 2, - maxTransforms = 5, - allowRepeats = false - } = options; - - // Split text into words while preserving punctuation - const words = this.smartWordSplit(text); - const availableTransforms = this.getRandomizableTransforms(); - - if (availableTransforms.length === 0) return text; - - // Select random transforms to use - const numTransforms = Math.min( - Math.max(minTransforms, Math.floor(Math.random() * maxTransforms) + 1), - availableTransforms.length - ); - - const selectedTransforms = []; - const usedTransforms = new Set(); - - for (let i = 0; i < numTransforms; i++) { - let transform; - do { - transform = availableTransforms[Math.floor(Math.random() * availableTransforms.length)]; - } while (!allowRepeats && usedTransforms.has(transform) && usedTransforms.size < availableTransforms.length); - - selectedTransforms.push(transform); - usedTransforms.add(transform); - } - - // Apply random transforms to words - const transformedWords = words.map(wordObj => { - if (wordObj.isWord) { - const randomTransform = selectedTransforms[Math.floor(Math.random() * selectedTransforms.length)]; - const transform = window.transforms[randomTransform]; - - try { - const transformed = transform.func(wordObj.text); - return { - ...wordObj, - text: transformed, - transform: transform.name, - originalTransform: randomTransform - }; - } catch (e) { - console.error(`Error applying ${randomTransform} to "${wordObj.text}":`, e); - return wordObj; - } - } else { - return wordObj; // Keep punctuation/spaces as-is - } - }); - - // Reconstruct the text - const result = transformedWords.map(w => w.text).join(''); - - // Store transform mapping for decoding - this.lastTransformMap = transformedWords - .filter(w => w.isWord && w.originalTransform) - .map(w => ({ - original: w.text, - transform: w.originalTransform, - transformName: w.transform - })); - - return result; - }, - - // Smart word splitting that preserves punctuation - smartWordSplit: function(text) { - const words = []; - let currentWord = ''; - let isInWord = false; - - for (let i = 0; i < text.length; i++) { - const char = text[i]; - const isWordChar = /[a-zA-Z0-9]/.test(char); - - if (isWordChar) { - if (!isInWord && currentWord) { - // We were in punctuation/space, now starting a word - words.push({ text: currentWord, isWord: false }); - currentWord = ''; - } - currentWord += char; - isInWord = true; - } else { - if (isInWord && currentWord) { - // We were in a word, now in punctuation/space - words.push({ text: currentWord, isWord: true }); - currentWord = ''; - } - currentWord += char; - isInWord = false; - } - } - - // Add the last segment - if (currentWord) { - words.push({ text: currentWord, isWord: isInWord }); - } - - return words; - }, - - preview: function(text) { - return '[mixed transforms]'; - }, - - // Attempt to decode a mixed-transform sentence - reverse: function(text) { - if (!this.lastTransformMap) { - return '[Cannot decode - no transform map available]'; - } - - // This is a simplified reverse - in practice, mixed decoding is complex - // because we need to identify which transform was applied to which word - return '[Mixed decode - use Universal Decoder for individual words]'; - }, - - // Get info about the last randomization - getLastTransformInfo: function() { - return this.lastTransformMap || []; - } - } -}; - -// Export transforms for use in app.js -window.transforms = transforms; diff --git a/js/utils/clipboard.js b/js/utils/clipboard.js new file mode 100644 index 0000000..00d99b3 --- /dev/null +++ b/js/utils/clipboard.js @@ -0,0 +1,51 @@ +/** + * Clipboard Utility + * Provides unified clipboard copy functionality using Clipboard API + */ +window.ClipboardUtils = { + /** + * Copy text to clipboard using Clipboard API + * @param {string} text - Text to copy + * @param {Object} options - Options object + * @param {Function} options.onSuccess - Callback on success + * @param {Function} options.onError - Callback on error + * @param {boolean} options.suppressNotification - Don't show notification + * @returns {Promise} - Success status + */ + async copy(text, options = {}) { + if (!text) return false; + + const { + onSuccess, + onError, + suppressNotification = false + } = options; + + if (!navigator.clipboard || !navigator.clipboard.writeText) { + const errorMsg = 'Clipboard API not available'; + console.error(errorMsg); + if (!suppressNotification && window.NotificationUtils) { + window.NotificationUtils.showNotification('Clipboard not supported', 'error', 'fas fa-exclamation-triangle'); + } + if (onError) onError(new Error(errorMsg)); + return false; + } + + try { + await navigator.clipboard.writeText(text); + if (!suppressNotification && window.NotificationUtils) { + window.NotificationUtils.showNotification('Copied!', 'success', 'fas fa-check'); + } + if (onSuccess) onSuccess(); + return true; + } catch (err) { + console.error('Clipboard copy failed:', err); + if (!suppressNotification && window.NotificationUtils) { + window.NotificationUtils.showNotification('Copy failed', 'error', 'fas fa-exclamation-triangle'); + } + if (onError) onError(err); + return false; + } + } +}; + diff --git a/js/utils/emoji.js b/js/utils/emoji.js new file mode 100644 index 0000000..778df94 --- /dev/null +++ b/js/utils/emoji.js @@ -0,0 +1,33 @@ +window.EmojiUtils = { + splitEmojis(text) { + if (Intl.Segmenter) { + const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' }); + return Array.from(segmenter.segment(text), ({ segment }) => segment); + } + return Array.from(text); + }, + + joinEmojis(emojis) { + return emojis.join(''); + }, + + getAllEmojis() { + if (!window.emojiData || typeof window.emojiData !== 'object') { + return []; + } + return Object.keys(window.emojiData).filter(key => { + const value = window.emojiData[key]; + return typeof value === 'object' && value !== null && 'official' in value; + }); + }, + + async getCompatibleEmojis(progressCallback) { + const allEmojis = this.getAllEmojis(); + + if (window.emojiCompatibility && typeof window.emojiCompatibility.getCompatibleEmojis === 'function') { + return await window.emojiCompatibility.getCompatibleEmojis(allEmojis, progressCallback); + } + return allEmojis; + } +}; + diff --git a/js/utils/escapeParser.js b/js/utils/escapeParser.js new file mode 100644 index 0000000..c140927 --- /dev/null +++ b/js/utils/escapeParser.js @@ -0,0 +1,40 @@ +window.EscapeParser = { + parseEscapeSequence(str) { + if (!str || typeof str !== 'string') { + return str; + } + + const escapeMap = { + '\\u200B': '\u200B', // Zero Width Space + '\\u200C': '\u200C', // Zero Width Non-Joiner + '\\u200D': '\u200D', // Zero Width Joiner + '\\u2060': '\u2060', // Word Joiner + '\\uFE0E': '\uFE0E', // Variation Selector-15 + '\\uFE0F': '\uFE0F', // Variation Selector-16 + '\\n': '\n', + '\\r': '\r', + '\\t': '\t', + '\\0': '\0', + '\\\'': '\'', + '\\"': '"', + '\\\\': '\\' + }; + + if (escapeMap[str] !== undefined) { + return escapeMap[str]; + } + + const unicodeMatch = str.match(/^\\u([0-9A-Fa-f]{4})$/); + if (unicodeMatch) { + return String.fromCharCode(parseInt(unicodeMatch[1], 16)); + } + + const hexMatch = str.match(/^\\x([0-9A-Fa-f]{2})$/); + if (hexMatch) { + return String.fromCharCode(parseInt(hexMatch[1], 16)); + } + + return str; + } +}; + diff --git a/js/utils/focus.js b/js/utils/focus.js new file mode 100644 index 0000000..bafcb79 --- /dev/null +++ b/js/utils/focus.js @@ -0,0 +1,29 @@ +window.FocusUtils = { + focusWithoutScroll(element) { + if (!element) return; + + try { + const scrollX = window.pageXOffset || window.scrollX || 0; + const scrollY = window.pageYOffset || window.scrollY || 0; + element.focus(); + window.scrollTo(scrollX, scrollY); + } catch (e) { + try { + element.focus(); + } catch (err) { + console.warn('Failed to focus element:', err); + } + } + }, + + clearFocusAndSelection() { + if (document.activeElement && document.activeElement.blur) { + document.activeElement.blur(); + } + if (window.getSelection) { + window.getSelection().removeAllRanges(); + } + document.body.focus(); + } +}; + diff --git a/js/utils/history.js b/js/utils/history.js new file mode 100644 index 0000000..aca2831 --- /dev/null +++ b/js/utils/history.js @@ -0,0 +1,60 @@ +window.HistoryUtils = { + addToHistory(historyArray, maxItems, source, content) { + if (!historyArray || !Array.isArray(historyArray)) { + console.warn('HistoryUtils.addToHistory: historyArray is not an array'); + return; + } + + if (!content) { + return; + } + + const entry = { + source: source || 'Unknown', + content: content, + timestamp: new Date().toISOString(), + id: Date.now() + Math.random() + }; + + historyArray.unshift(entry); + + if (historyArray.length > maxItems) { + historyArray.splice(maxItems); + } + }, + + clearHistory(historyArray) { + if (historyArray && Array.isArray(historyArray)) { + // Use splice to remove all items (same approach as removeFromHistory) + historyArray.splice(0, historyArray.length); + } + }, + + removeFromHistory(historyArray, id) { + if (!historyArray || !Array.isArray(historyArray)) { + return; + } + + const index = historyArray.findIndex(item => item.id === id); + if (index !== -1) { + historyArray.splice(index, 1); + } + }, + + getHistorySource(activeTab, context = {}) { + if (activeTab === 'transforms' && context.activeTransform) { + return `Transform: ${context.activeTransform.name}`; + } else if (activeTab === 'steganography') { + if (context.activeSteg === 'invisible') { + return 'Invisible Text'; + } else if (context.selectedEmoji) { + return `Emoji: ${context.selectedEmoji}`; + } + return 'Steganography'; + } else if (activeTab === 'transforms') { + return 'Transform'; + } + return 'Unknown'; + } +}; + diff --git a/js/utils/notifications.js b/js/utils/notifications.js new file mode 100644 index 0000000..778a708 --- /dev/null +++ b/js/utils/notifications.js @@ -0,0 +1,37 @@ +window.NotificationUtils = { + showNotification(message, type = 'success', iconClass = null) { + const existing = document.querySelector('.copy-notification'); + if (existing) { + existing.remove(); + } + + const notification = document.createElement('div'); + notification.className = `copy-notification ${type || 'success'}`; + + if (iconClass) { + const icon = document.createElement('i'); + icon.className = iconClass; + notification.appendChild(icon); + } + + const text = document.createElement('span'); + text.textContent = message; + notification.appendChild(text); + + document.body.appendChild(notification); + + setTimeout(() => { + notification.classList.add('fade-out'); + setTimeout(() => { + if (notification.parentNode) { + notification.parentNode.removeChild(notification); + } + }, 300); + }, 3000); + }, + + showCopiedPopup() { + this.showNotification('Copied!', 'success', 'fas fa-check'); + } +}; + diff --git a/js/utils/theme.js b/js/utils/theme.js new file mode 100644 index 0000000..f52f256 --- /dev/null +++ b/js/utils/theme.js @@ -0,0 +1,37 @@ +window.ThemeUtils = { + toggleTheme(currentTheme) { + const newTheme = !currentTheme; + + if (newTheme) { + document.body.classList.add('dark-theme'); + document.body.classList.remove('light-theme'); + } else { + document.body.classList.add('light-theme'); + document.body.classList.remove('dark-theme'); + } + + try { + localStorage.setItem('theme', newTheme ? 'dark' : 'light'); + } catch (e) { + console.warn('Failed to save theme preference:', e); + } + + return newTheme; + }, + + initializeTheme() { + try { + const saved = localStorage.getItem('theme'); + if (saved === 'light') { + return false; + } else if (saved === 'dark') { + return true; + } + } catch (e) { + console.warn('Failed to load theme preference:', e); + } + + return true; + } +}; + diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json deleted file mode 100644 index 6dc7dc3..0000000 --- a/node_modules/.package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "P4RS3LT0NGV3", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} diff --git a/package.json b/package.json index 0967ef4..ae4476f 100644 --- a/package.json +++ b/package.json @@ -1 +1,25 @@ -{} +{ + "name": "p4rs3lt0ngv3", + "version": "1.0.0", + "description": "Universal Text Encoder/Decoder & Steganography Tool", + "scripts": { + "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:index && npm run build:transforms && npm run build:emoji && npm run build:tools && npm run build:templates", + "test": "node tests/test_universal.js", + "test:universal": "node tests/test_universal.js", + "test:steg": "node tests/test_steganography_options.js", + "test:all": "npm run test:universal && npm run test:steg", + "precommit": "npm run test:all" + }, + "repository": { + "type": "git", + "url": "." + }, + "keywords": ["encoder", "decoder", "steganography", "cipher"], + "author": "", + "license": "MIT" +} diff --git a/parsel_app.py b/parsel_app.py deleted file mode 100644 index 59a5f9d..0000000 --- a/parsel_app.py +++ /dev/null @@ -1,498 +0,0 @@ -import streamlit as st -import base64 -import pyperclip -from PIL import Image -import io -import zlib -import numpy as np -import re -import logging -import random -from typing import List, Dict, Optional -from string import ascii_lowercase - -# Import additional transformations -from text_transforms import ( - to_upside_down, to_elder_futhark, to_vaporwave, to_zalgo, - to_unicode_circled, to_small_caps, to_braille -) - -# Configure logging -logging.basicConfig( - level=logging.DEBUG, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger('parseltongue') - -# Set page config with dark theme and wide layout -st.set_page_config( - page_title="Parseltongue 2.0", - page_icon="๐Ÿ", - layout="wide", - initial_sidebar_state="collapsed" -) - -# Custom CSS for dark hacker theme -st.markdown(""" - -""", unsafe_allow_html=True) - -# Helper Functions -def text_to_leetspeak(text: str) -> str: - """Convert text to leetspeak""" - leet_dict = { - 'a': '4', 'e': '3', 'i': '1', 'l': '1', - 'o': '0', 's': '5', 't': '7', 'b': '8', - 'g': '9', 'z': '2' - } - return ''.join(leet_dict.get(c.lower(), c) for c in text) - -def text_to_pig_latin(text: str) -> str: - """Convert text to Pig Latin""" - words = text.split() - result = [] - for word in words: - if word[0].lower() in 'aeiou': - result.append(word + 'way') - else: - consonants = '' - i = 0 - while i < len(word) and word[i].lower() not in 'aeiou': - consonants += word[i] - i += 1 - result.append(word[i:] + consonants + 'ay') - return ' '.join(result) - -def rot13(text: str) -> str: - """Apply ROT13 encoding""" - result = '' - for char in text: - if char.isalpha(): - ascii_offset = ord('a') if char.islower() else ord('A') - rotated = (ord(char) - ascii_offset + 13) % 26 + ascii_offset - result += chr(rotated) - else: - result += char - return result - -def to_base64(text: str) -> str: - """Convert text to Base64""" - if not text: - return "" - return base64.b64encode(text.encode('utf-8')).decode('utf-8') - -def to_binary(text: str) -> str: - """Convert text to binary""" - if not text: - return "" - return ' '.join(format(ord(c), '08b') for c in text) - -def to_hex(text: str) -> str: - """Convert text to hexadecimal""" - if not text: - return "" - return ' '.join(format(ord(c), '02x') for c in text) - -def to_bubble_text(text: str) -> str: - """Convert text to bubble text""" - if not text: - return "" - # Map for bubble text (circled latin letters) - bubble_map = { - 'a': '\u24d0', 'b': '\u24d1', 'c': '\u24d2', 'd': '\u24d3', 'e': '\u24d4', 'f': '\u24d5', 'g': '\u24d6', 'h': '\u24d7', 'i': '\u24d8', - 'j': '\u24d9', 'k': '\u24da', 'l': '\u24db', 'm': '\u24dc', 'n': '\u24dd', 'o': '\u24de', 'p': '\u24df', 'q': '\u24e0', 'r': '\u24e1', - 's': '\u24e2', 't': '\u24e3', 'u': '\u24e4', 'v': '\u24e5', 'w': '\u24e6', 'x': '\u24e7', 'y': '\u24e8', 'z': '\u24e9', - 'A': '\u24b6', 'B': '\u24b7', 'C': '\u24b8', 'D': '\u24b9', 'E': '\u24ba', 'F': '\u24bb', 'G': '\u24bc', 'H': '\u24bd', 'I': '\u24be', - 'J': '\u24bf', 'K': '\u24c0', 'L': '\u24c1', 'M': '\u24c2', 'N': '\u24c3', 'O': '\u24c4', 'P': '\u24c5', 'Q': '\u24c6', 'R': '\u24c7', - 'S': '\u24c8', 'T': '\u24c9', 'U': '\u24ca', 'V': '\u24cb', 'W': '\u24cc', 'X': '\u24cd', 'Y': '\u24ce', 'Z': '\u24cf', - '0': '\u24ea', '1': '\u2460', '2': '\u2461', '3': '\u2462', '4': '\u2463', '5': '\u2464', '6': '\u2465', '7': '\u2466', '8': '\u2467', '9': '\u2468', - ' ': ' ' - } - return ''.join(bubble_map.get(c, c) for c in text) - -def to_fullwidth(text: str) -> str: - """Convert text to fullwidth characters""" - if not text: - return "" - # Map for fullwidth text - fullwidth_map = { - 'a': '\uff41', 'b': '\uff42', 'c': '\uff43', 'd': '\uff44', 'e': '\uff45', 'f': '\uff46', 'g': '\uff47', 'h': '\uff48', 'i': '\uff49', - 'j': '\uff4a', 'k': '\uff4b', 'l': '\uff4c', 'm': '\uff4d', 'n': '\uff4e', 'o': '\uff4f', 'p': '\uff50', 'q': '\uff51', 'r': '\uff52', - 's': '\uff53', 't': '\uff54', 'u': '\uff55', 'v': '\uff56', 'w': '\uff57', 'x': '\uff58', 'y': '\uff59', 'z': '\uff5a', - 'A': '\uff21', 'B': '\uff22', 'C': '\uff23', 'D': '\uff24', 'E': '\uff25', 'F': '\uff26', 'G': '\uff27', 'H': '\uff28', 'I': '\uff29', - 'J': '\uff2a', 'K': '\uff2b', 'L': '\uff2c', 'M': '\uff2d', 'N': '\uff2e', 'O': '\uff2f', 'P': '\uff30', 'Q': '\uff31', 'R': '\uff32', - 'S': '\uff33', 'T': '\uff34', 'U': '\uff35', 'V': '\uff36', 'W': '\uff37', 'X': '\uff38', 'Y': '\uff39', 'Z': '\uff3a', - '0': '\uff10', '1': '\uff11', '2': '\uff12', '3': '\uff13', '4': '\uff14', '5': '\uff15', '6': '\uff16', '7': '\uff17', '8': '\uff18', '9': '\uff19', - ' ': '\u3000', '!': '\uff01', '?': '\uff1f', '.': '\uff0e', ',': '\uff0c', ';': '\uff1b', ':': '\uff1a', '(': '\uff08', ')': '\uff09', - '[': '\uff3b', ']': '\uff3d', '{': '\uff5b', '}': '\uff5d', '<': '\uff1c', '>': '\uff1e', '\\': '\uff3c', '/': '\uff0f', '|': '\uff5c', - '`': '\uff40', '~': '\uff5e', '@': '\uff20', '#': '\uff03', '$': '\uff04', '%': '\uff05', '^': '\uff3e', '&': '\uff06', '*': '\uff0a', - '-': '\uff0d', '_': '\uff3f', '+': '\uff0b', '=': '\uff1d', '"': '\uff02', '\'': '\uff07' - } - return ''.join(fullwidth_map.get(c, c) for c in text) - -def to_morse(text: str) -> str: - """Convert text to Morse code""" - if not text: - return "" - # Morse code mapping - morse_map = { - 'a': '.-', 'b': '-...', 'c': '-.-.', 'd': '-..', 'e': '.', 'f': '..-.', 'g': '--.', 'h': '....', - 'i': '..', 'j': '.---', 'k': '-.-', 'l': '.-..', 'm': '--', 'n': '-.', 'o': '---', 'p': '.--.', - 'q': '--.-', 'r': '.-.', 's': '...', 't': '-', 'u': '..-', 'v': '...-', 'w': '.--', 'x': '-..-', - 'y': '-.--', 'z': '--..', '0': '-----', '1': '.----', '2': '..---', '3': '...--', '4': '....-', - '5': '.....', '6': '-....', '7': '--...', '8': '---..', '9': '----.', ' ': '/' - } - return ' '.join(morse_map.get(c.lower(), c) for c in text) - -def to_binary_ascii(text: str) -> str: - """Convert text to ASCII binary (with ASCII values)""" - if not text: - return "" - return ' '.join(str(ord(c)) for c in text) - -def to_reverse(text: str) -> str: - """Reverse the text""" - if not text: - return "" - return text[::-1] - -# Define carriers for steganography with descriptions -CARRIERS = [ - {'emoji': '๐Ÿ', 'name': 'SNAKE', 'desc': 'Classic Snake'}, - {'emoji': '๐Ÿ‰', 'name': 'DRAGON', 'desc': 'Mystical Dragon'}, - {'emoji': '๐Ÿง™', 'name': 'WIZARD', 'desc': 'Powerful Wizard'}, - {'emoji': '๐Ÿ”ฎ', 'name': 'CRYSTAL', 'desc': 'Magic Crystal Ball'}, - {'emoji': 'โšก', 'name': 'LIGHTNING', 'desc': 'Lightning Bolt'}, - {'emoji': '๐ŸŒŸ', 'name': 'STAR', 'desc': 'Shining Star'}, - {'emoji': '๐ŸŽญ', 'name': 'MASK', 'desc': 'Theater Mask'}, - {'emoji': '๐Ÿ—๏ธ', 'name': 'KEY', 'desc': 'Ancient Key'}, - {'emoji': '๐Ÿ“œ', 'name': 'SCROLL', 'desc': 'Magic Scroll'}, - {'emoji': '๐Ÿ”’', 'name': 'LOCK', 'desc': 'Secure Lock'} -] - -def to_variation_selector(byte: int) -> str: - """Convert a byte to a variation selector character""" - return chr(0xFE00 + byte) - -def from_variation_selector(code_point: int) -> int: - """Convert a variation selector character back to a byte""" - return code_point - 0xFE00 - -def encode_emoji(emoji: str, text: str) -> str: - """Encode text using variation selectors""" - if not text: - return emoji - - # Convert text to binary - binary = ''.join(format(ord(c), '08b') for c in text) - - # Use variation selectors to encode binary - vs15, vs16 = '\ufe0e', '\ufe0f' - encoded = emoji - - for bit in binary: - encoded += vs15 if bit == '0' else vs16 - - return encoded - -def decode_emoji(text: str) -> str: - """Decode text from variation selectors""" - if not text: - return "" - - # Extract variation selectors - vs_pattern = r'[\ufe0e\ufe0f]' - matches = re.findall(vs_pattern, text) - - if not matches: - return "" - - # Convert variation selectors to binary - binary = ''.join('0' if vs == '\ufe0e' else '1' for vs in matches) - - # Convert binary to text - decoded = "" - for i in range(0, len(binary), 8): - byte = binary[i:i+8] - if len(byte) == 8: - decoded += chr(int(byte, 2)) - - return decoded - -def encode_invisible(text: str) -> str: - """Encode text using Tags Unicode block (U+E0000 to U+E007F)""" - if not text: - return "" - - result = '' - for c in text: - # Add the character as a Tags character - result += chr(0xE0000 + ord(c) % 0x7F) - # Add a space from the Tags block - result += chr(0xE0020) # This is the space character in Tags block - - return result - -def decode_invisible(text: str) -> str: - """Decode text from Tags Unicode block (U+E0000 to U+E007F)""" - if not text: - return "" - - result = '' - # Filter valid Tags characters - chars = [c for c in text if 0xE0000 <= ord(c) <= 0xE007F] - - for c in chars: - if ord(c) != 0xE0020: # Skip the space character - # Convert back from Tags block - original = chr((ord(c) - 0xE0000) % 128) - result += original - - return result - -def encode_image_steganography(image: Image.Image, message: str, plane: str = 'red') -> Image.Image: - """Encode a message into an image using LSB steganography""" - # Convert the image to RGB if it's not already - img = image.convert('RGB') - - # Get the image data as a numpy array - data = np.array(img) - - # Convert message to binary - binary = ''.join(format(ord(c), '08b') for c in message) - binary += '00000000' # Add null terminator - - # Get the correct color plane - plane_idx = {'red': 0, 'green': 1, 'blue': 2}[plane] - - # Encode the message - idx = 0 - for i in range(data.shape[0]): - for j in range(data.shape[1]): - if idx < len(binary): - # Clear the LSB and set it to the message bit - data[i, j, plane_idx] = (data[i, j, plane_idx] & 0xFE) | int(binary[idx]) - idx += 1 - - # Create a new image from the modified data - encoded_image = Image.fromarray(data) - return encoded_image - -# Main App -st.title("๐Ÿ Parseltongue 2.0") -st.markdown("""

LLM Payload Crafter

""", unsafe_allow_html=True) - -# Create tabs for steganography and decoder -tab1, tab2 = st.tabs(["๐Ÿ” Steganography", "๐Ÿ” Universal Decoder"]) - -# Steganography Tab -with tab1: - st.markdown("""

โœจ Emoji Steganography

""", unsafe_allow_html=True) - - # Text input for message - message = st.text_area("Enter your message:", key="emoji_message", placeholder="Type your message here...") - - if message: - st.markdown("""

Click an emoji to encode and copy to clipboard:

""", unsafe_allow_html=True) - - # Create a grid of emojis with their descriptions - cols = st.columns(8) # More columns for smaller buttons - for i, carrier in enumerate(CARRIERS): - with cols[i % 8]: - # Create a smaller button with just the emoji - button = st.button(carrier['emoji'], key=f"emoji_{i}", help=carrier['desc']) - st.markdown("
", unsafe_allow_html=True) - if button: - try: - # Encode the message - encoded = encode_emoji(carrier['emoji'], message) - # Copy to clipboard - pyperclip.copy(encoded) - # Show success message with preview - st.success(f"โœ… Encoded with {carrier['name']} and copied to clipboard!") - except Exception as e: - st.error(f"Error encoding message: {str(e)}") - - # Add a divider - st.markdown("
", unsafe_allow_html=True) - - # Omni-Encoder Section - st.markdown("""

๐Ÿ” Omni-Encoder

""", unsafe_allow_html=True) - st.markdown("""

Click to transform and copy to clipboard:

""", unsafe_allow_html=True) - - # Create a grid of transformation options - 4 columns, 4 rows - st.markdown("
", unsafe_allow_html=True) - # Use 4 rows of 4 columns each - for row in range(4): - transform_cols = st.columns(4) - - # Define transformations - transformations = [ - {"name": "Base64", "func": to_base64, "icon": "๐Ÿ”ข"}, - {"name": "Binary", "func": to_binary, "icon": "01"}, - {"name": "Hex", "func": to_hex, "icon": "0x"}, - {"name": "ASCII", "func": to_binary_ascii, "icon": "๐Ÿ”ค"}, - {"name": "ROT13", "func": rot13, "icon": "๐Ÿ”“"}, - {"name": "Leetspeak", "func": text_to_leetspeak, "icon": "1337"}, - {"name": "Pig Latin", "func": text_to_pig_latin, "icon": "๐Ÿท"}, - {"name": "Morse", "func": to_morse, "icon": "โ€ขโˆ’โ€ข"}, - {"name": "Bubble", "func": to_bubble_text, "icon": "โ“‘โ“คโ“‘"}, - {"name": "Fullwidth", "func": to_fullwidth, "icon": "๏ผฆ๏ผท"}, - {"name": "Reversed", "func": to_reverse, "icon": "โŸฒ"}, - {"name": "Upside Down", "func": to_upside_down, "icon": "๐Ÿ™ƒ"}, - {"name": "Runes", "func": to_elder_futhark, "icon": "แš แšขแšฆแšจแšฑแšฒ"}, - {"name": "Vaporwave", "func": to_vaporwave, "icon": "๏ฝ–๏ฝ๏ฝ๏ฝ๏ฝ’"}, - {"name": "Zalgo", "func": to_zalgo, "icon": "Zฬทฬขฬงอaฬถฬขอlฬธฬจฬ›อgฬตฬขฬงฬ›oฬตฬกอ˜"}, - {"name": "Circled", "func": to_unicode_circled, "icon": "๐Ÿ…’๐Ÿ…˜๐Ÿ…ก"}, - {"name": "Small Caps", "func": to_small_caps, "icon": "แด€ส™แด„"}, - {"name": "Braille", "func": to_braille, "icon": "โ ƒโ —โ โ Šโ ‡โ ‡โ ‘"} - ] - - for i, transform in enumerate(transformations): - row_idx = i // 4 # Determine which row this transform belongs to - col_idx = i % 4 # Determine which column in the row - - # Only process transforms for the current row - if row_idx < 4: # We have 4 rows total - with transform_cols[col_idx]: - # Create a button for each transformation - button = st.button(f"{transform['icon']} {transform['name']}", key=f"transform_{i}") - st.markdown("
", unsafe_allow_html=True) - if button: - try: - # Transform the message - transformed = transform['func'](message) - # Copy to clipboard - pyperclip.copy(transformed) - # Show success message with preview - st.success(f"โœ… {transform['name']} encoded and copied to clipboard!\n\nPreview: {transformed[:50] + '...' if len(transformed) > 50 else transformed}") - except Exception as e: - st.error(f"Error transforming message: {str(e)}") - - # Invisible Text Section - st.markdown("""

๐Ÿ‘ป Invisible Text

""", unsafe_allow_html=True) - invisible_input = st.text_area("Enter text to make invisible", key="invisible_input") - if invisible_input: - invisible_output = encode_invisible(invisible_input) - st.text_area("Invisible text (copied to clipboard)", invisible_output, height=100) - if st.button("๐Ÿ“‹ Copy Invisible Text", key="copy_invisible"): - pyperclip.copy(invisible_output) - st.success("โœ… Copied to clipboard!") - - # Image Steganography Section - st.markdown("""

๐Ÿ–ผ๏ธ Image Steganography

""", unsafe_allow_html=True) - col1, col2 = st.columns(2) - with col1: - uploaded_file = st.file_uploader("Choose carrier image", type=['png', 'jpg', 'jpeg']) - if uploaded_file: - image = Image.open(uploaded_file) - st.image(image, caption="Carrier Image") - - with col2: - steg_message = st.text_area("Enter message to hide", key="steg_message") - color_plane = st.selectbox("Select color plane", ["red", "green", "blue"]) - - if uploaded_file and steg_message and st.button("๐Ÿ”’ Encode Message"): - try: - encoded_image = encode_image_steganography(image, steg_message, color_plane) - st.image(encoded_image, caption="Encoded Image") - - # Save the image to a bytes buffer - buf = io.BytesIO() - encoded_image.save(buf, format='PNG') - byte_im = buf.getvalue() - - st.download_button( - label="๐Ÿ’พ Download Encoded Image", - data=byte_im, - file_name="encoded_image.png", - mime="image/png" - ) - except Exception as e: - st.error(f"Error encoding message in image: {str(e)}") - -# Universal Decoder Tab -with tab2: - st.markdown("### Text Transformation Tools") - text_input = st.text_area("Enter text to transform", key="obfuscate_input") - - if text_input: - # Create columns for different transformations - col1, col2 = st.columns(2) - - with col1: - st.markdown("#### Basic Transformations") - st.text_area("Leetspeak:", text_to_leetspeak(text_input)) - st.text_area("Pig Latin:", text_to_pig_latin(text_input)) - st.text_area("ROT13:", rot13(text_input)) - - with col2: - st.markdown("#### Decodings") - # Try to decode emoji - emoji_decoded = decode_emoji(text_input) - if emoji_decoded: - st.text_area("Emoji Decoded:", emoji_decoded) - - # Try to decode invisible text - invisible_decoded = decode_invisible(text_input) - if invisible_decoded: - st.text_area("Invisible Text Decoded:", invisible_decoded) diff --git a/js/emojiWordMap.js b/src/emojiWordMap.js similarity index 100% rename from js/emojiWordMap.js rename to src/emojiWordMap.js diff --git a/src/transformers/BaseTransformer.js b/src/transformers/BaseTransformer.js new file mode 100644 index 0000000..e4aae81 --- /dev/null +++ b/src/transformers/BaseTransformer.js @@ -0,0 +1,153 @@ +/** + * Base Transformer Class + * + * Provides default implementations and structure for all text transformers. + * + * USAGE: + * + * 1. Simple character map transformer (auto-generates reverse): + * + * export default new BaseTransformer({ + * name: 'My Transform', + * priority: 85, + * map: { 'a': 'ฮฑ', 'b': 'ฮฒ', ... }, + * func: function(text) { + * return [...text].map(c => this.map[c] || c).join(''); + * } + * }); + * + * 2. Custom transformer with manual reverse: + * + * export default new BaseTransformer({ + * name: 'ROT13', + * priority: 60, + * func: function(text) { ... }, + * reverse: function(text) { ... } + * }); + * + * 3. Encoding-only transformer (no reverse): + * + * export default new BaseTransformer({ + * name: 'Random Mix', + * priority: 0, + * canDecode: false, + * func: function(text) { ... } + * }); + */ + +export class BaseTransformer { + /** + * Create a new transformer + * @param {Object} config - Transformer configuration + * @param {string} config.name - Display name (required) + * @param {Function} config.func - Encoding function (required) + * @param {number} [config.priority=85] - Decoder priority (1-310) + * @param {Object} [config.map] - Character mapping (if provided, auto-generates reverse) + * @param {Function} [config.reverse] - Custom decoder function + * @param {Function} [config.preview] - Preview function (defaults to func) + * @param {Function} [config.detector] - Custom detection function (text) => boolean + * @param {boolean} [config.canDecode=true] - Whether this transformer can decode + * @param {string} [config.category] - Category for organization + * @param {string} [config.description] - Help text + */ + constructor(config) { + if (!config.name || !config.func) { + throw new Error('Transformer requires at least "name" and "func"'); + } + + // Copy ALL config properties to instance first (for custom properties like alphabet, etc.) + Object.assign(this, config); + + // Override with properly bound functions + this.func = config.func.bind(this); + this.priority = config.priority ?? 85; // Default: Unicode transformations + this.canDecode = config.canDecode ?? true; + + // Preview function (defaults to func) + if (config.preview) { + this.preview = config.preview.bind(this); + } else { + this.preview = this.func; + } + + // Detector function (for universal decoder) + if (config.detector) { + this.detector = config.detector.bind(this); + } else { + this.detector = null; + } + + // Reverse/decode function + if (!this.canDecode) { + // Explicitly cannot decode + this.reverse = null; + } else if (config.reverse) { + // Custom reverse function provided + this.reverse = config.reverse.bind(this); + } else if (config.map) { + // Auto-generate reverse from character map + this.reverse = this._autoReverse.bind(this); + } else { + // No reverse available (but might be added later) + this.reverse = null; + } + } + + /** + * Auto-generated reverse function for character map transformers + * Builds a reverse map and decodes character-by-character + * @private + */ + _autoReverse(text) { + if (!this.map) return text; + + // Build reverse map (cached for performance) + if (!this._reverseMap) { + this._reverseMap = {}; + for (const [key, value] of Object.entries(this.map)) { + this._reverseMap[value] = key; + } + } + + return [...text].map(c => this._reverseMap[c] || c).join(''); + } + + /** + * Get transformer info as JSON + */ + toJSON() { + return { + name: this.name, + priority: this.priority, + canDecode: this.canDecode, + category: this.category, + description: this.description, + hasMap: !!this.map, + hasReverse: !!this.reverse + }; + } +} + +/** + * PRIORITY GUIDE: + * + * 310 = Semaphore Flags (only 8 specific arrow emojis) + * 300 = Exclusive character sets (Binary, Morse, Braille, Brainfuck, Tap Code) + * 290 = Hexadecimal + * 285 = Pattern-based (Pig Latin, Dovahzul) + * 280 = Base32 + * 270-275 = Base64/Base58 family + * 260 = A1Z26 + * 150 = Active transform (user context) + * 100 = High confidence (Emoji Steganography, unique Unicode ranges) + * 85 = Unicode transformations (default for fancy text) + * 70 = Common encodings (URL, HTML, ASCII85) + * 60 = Ciphers (ROT13, Caesar) + * 50 = Generic text transforms + * 20 = Low confidence generic + * 1 = Invisible text (last resort) + * 0 = Cannot decode / encode-only + */ + +export default BaseTransformer; + diff --git a/src/transformers/README.md b/src/transformers/README.md new file mode 100644 index 0000000..0eeb983 --- /dev/null +++ b/src/transformers/README.md @@ -0,0 +1,151 @@ +# Transformers + +Transformers are instantiated using `BaseTransformer` class. Category is automatically assigned from the directory name. + +## 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. + +## Creating a Transformer + +### Required Properties + +- `name` - Display name (string) +- `func` - Encoding function `(text) => string` +- `priority` - Decoder priority (number, 1-310) + +### Optional Properties + +- `reverse` - Decoding function `(text) => string` (auto-generated if `map` provided) +- `map` - Character mapping object (auto-generates `reverse`) +- `detector` - Detection function `(text) => boolean` (for universal decoder) +- `preview` - Preview function `(text) => string` (defaults to `func`) +- `canDecode` - Boolean (default: `true`) +- `description` - Help text (string) + +### Example: Character Map (Auto-generates reverse) + +```javascript +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + name: 'Cursive', + priority: 85, + map: { + 'a': '๐’ถ', 'b': '๐’ท', 'c': '๐’ธ', + // ... more mappings + }, + func: function(text) { + return [...text].map(c => this.map[c] || c).join(''); + } + // reverse is auto-generated from map! +}); +``` + +### Example: Custom Transformer + +```javascript +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + name: 'Base64', + priority: 270, + detector: function(text) { + const cleaned = text.trim().replace(/\s/g, ''); + return cleaned.length >= 4 && /^[A-Za-z0-9+\/=]+$/.test(cleaned); + }, + func: function(text) { + // Encoding logic + const encoder = new TextEncoder(); + const bytes = encoder.encode(text); + let binaryString = ''; + for (let i = 0; i < bytes.length; i++) { + binaryString += String.fromCharCode(bytes[i]); + } + return btoa(binaryString); + }, + reverse: function(text) { + // Decoding logic + const binaryString = atob(text); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + const decoder = new TextDecoder('utf-8'); + return decoder.decode(bytes); + }, + preview: function(text) { + if (!text) return '[base64]'; + const full = this.func(text); + return full.substring(0, 12) + (full.length > 12 ? '...' : ''); + } +}); +``` + +### Example: Encoding-Only (No Reverse) + +```javascript +export default new BaseTransformer({ + name: 'Random Mix', + priority: 0, + canDecode: false, + func: function(text) { + // Encoding logic only + return randomized; + } +}); +``` + +## Priority Guide + +Higher priority = more specific pattern (used for decoder result ordering): + +- **310**: Most exclusive (Semaphore Flags) +- **300**: Exclusive character sets (Binary, Morse, Braille, Brainfuck, Tap Code) +- **290**: Hexadecimal +- **285**: Pattern-based (Pig Latin, Dovahzul) +- **280**: Base32 +- **270-275**: Base encodings (Base64, Base58, Base45) +- **260**: A1Z26 +- **150**: Active transform (user context) +- **100**: High confidence (Fantasy scripts, unique Unicode ranges) +- **85**: Unicode transformations (default) +- **70**: Common encodings (URL, HTML, ASCII85) +- **60**: Ciphers (ROT13, Caesar) +- **50**: Generic text transforms +- **20**: Low confidence generic +- **1**: Invisible text (last resort) +- **0**: Cannot decode / encode-only + +## 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` + +## Testing + +All transformers with `reverse` are automatically tested by `tests/test_universal.js`. + +For transformers with known limitations (e.g., lowercases input), add to `limitations` object in `test_universal.js`: + +```javascript +const limitations = { + 'your_transform': { + issues: 'Description of changes', + normalize: { lowercase: true, stripEmoji: true } + } +}; +``` diff --git a/src/transformers/ancient/elder-futhark.js b/src/transformers/ancient/elder-futhark.js new file mode 100644 index 0000000..bbd5123 --- /dev/null +++ b/src/transformers/ancient/elder-futhark.js @@ -0,0 +1,39 @@ +// elder-futhark transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Elder Futhark', + priority: 100, + 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': 'แ›‰' + }, + // Create reverse map for decoding + reverseMap: function() { + const revMap = {}; + for (const [key, value] of Object.entries(this.map)) { + revMap[value] = key; + } + return revMap; + }, + func: function(text) { + return [...text.toLowerCase()].map(c => this.map[c] || c).join(''); + }, + preview: function(text) { + if (!text) return '[runes]'; + return this.func(text.slice(0, 5)); + }, + reverse: function(text) { + const revMap = this.reverseMap(); + return [...text].map(c => revMap[c] || c).join(''); + }, + // Detector: Check for Elder Futhark runes + detector: function(text) { + // Elder Futhark runes (U+16A0-U+16F8) + // Check for the unique runes used in this transform + return /[แšจแšณแšฒแ›Ÿแšคแ›’แ›žแ›–แš แšทแšบแ›แ›ƒแ›šแ›—แšพแ›ˆแ›ฉแšฑแ›‹แ›แšขแšกแšนแ›‰]/.test(text); + } + +}); \ No newline at end of file diff --git a/src/transformers/ancient/hieroglyphics.js b/src/transformers/ancient/hieroglyphics.js new file mode 100644 index 0000000..d99d931 --- /dev/null +++ b/src/transformers/ancient/hieroglyphics.js @@ -0,0 +1,32 @@ +// hieroglyphics transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Hieroglyphics', + priority: 70, + map: { + 'a': '๐“ƒญ', 'b': '๐“ƒฎ', 'c': '๐“ƒฏ', 'd': '๐“ƒฐ', 'e': '๐“ƒฑ', 'f': '๐“ƒฒ', 'g': '๐“ƒณ', 'h': '๐“ƒด', 'i': '๐“ƒต', + 'j': '๐“ƒถ', 'k': '๐“ƒท', 'l': '๐“ƒธ', 'm': '๐“ƒน', 'n': '๐“ƒบ', 'o': '๐“ƒป', 'p': '๐“ƒผ', 'q': '๐“ƒฝ', 'r': '๐“ƒพ', + 's': '๐“ƒฟ', 't': '๐“„€', 'u': '๐“„', 'v': '๐“„‚', 'w': '๐“„ƒ', 'x': '๐“„„', 'y': '๐“„…', 'z': '๐“„†', + 'A': '๐“„‡', 'B': '๐“„ˆ', 'C': '๐“„‰', 'D': '๐“„Š', 'E': '๐“„‹', 'F': '๐“„Œ', 'G': '๐“„', 'H': '๐“„Ž', 'I': '๐“„', + 'J': '๐“„', 'K': '๐“„‘', 'L': '๐“„’', 'M': '๐“„“', 'N': '๐“„”', 'O': '๐“„•', 'P': '๐“„–', 'Q': '๐“„—', 'R': '๐“„˜', + 'S': '๐“„™', 'T': '๐“„š', 'U': '๐“„›', 'V': '๐“„œ', 'W': '๐“„', 'X': '๐“„ž', 'Y': '๐“„Ÿ', 'Z': '๐“„ ' + }, + func: function(text) { + return [...text.toLowerCase()].map(c => this.map[c] || c).join(''); + }, + reverse: function(text) { + const revMap = {}; + for (const [key, value] of Object.entries(this.map)) { + revMap[value] = key; + } + return [...text].map(c => revMap[c] || c).join(''); + }, + // Detector: Check for Egyptian hieroglyphic characters + detector: function(text) { + // Egyptian hieroglyphs - check for presence of any hieroglyphic character + return /[\u{13000}-\u{1342F}]/u.test(text); + } + +}); \ No newline at end of file diff --git a/src/transformers/ancient/ogham.js b/src/transformers/ancient/ogham.js new file mode 100644 index 0000000..64782fd --- /dev/null +++ b/src/transformers/ancient/ogham.js @@ -0,0 +1,32 @@ +// ogham transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Ogham (Celtic)', + priority: 70, + map: { + 'a': 'แš', 'b': 'แš', 'c': 'แš‰', 'd': 'แš‡', 'e': 'แš“', 'f': 'แšƒ', 'g': 'แšŒ', 'h': 'แš†', 'i': 'แš”', + 'j': 'แšˆ', 'k': 'แšŠ', 'l': 'แš‚', 'm': 'แš‹', 'n': 'แš…', 'o': 'แš‘', 'p': 'แšš', 'q': 'แšŠ', 'r': 'แš', + 's': 'แš„', 't': 'แšˆ', 'u': 'แš’', 'v': 'แšƒ', 'w': 'แšƒ', 'x': 'แšŠ', 'y': 'แš”', 'z': 'แšŽ', + 'A': 'แš', 'B': 'แš', 'C': 'แš‰', 'D': 'แš‡', 'E': 'แš“', 'F': 'แšƒ', 'G': 'แšŒ', 'H': 'แš†', 'I': 'แš”', + 'J': 'แšˆ', 'K': 'แšŠ', 'L': 'แš‚', 'M': 'แš‹', 'N': 'แš…', 'O': 'แš‘', 'P': 'แšš', 'Q': 'แšŠ', 'R': 'แš', + 'S': 'แš„', 'T': 'แšˆ', 'U': 'แš’', 'V': 'แšƒ', 'W': 'แšƒ', 'X': 'แšŠ', 'Y': 'แš”', 'Z': 'แšŽ' + }, + func: function(text) { + return [...text.toLowerCase()].map(c => this.map[c] || c).join(''); + }, + reverse: function(text) { + const revMap = {}; + for (const [key, value] of Object.entries(this.map)) { + revMap[value] = key; + } + return [...text].map(c => revMap[c] || c).join(''); + }, + // Detector: Check for Ogham characters + detector: function(text) { + // Ogham alphabet (U+1680-U+169C) + return /[แšแšแš‰แš‡แš“แšƒแšŒแš†แš”แšˆแšŠแš‚แš‹แš…แš‘แššแšแš„แš’แšŽ]/.test(text); + } + +}); \ No newline at end of file diff --git a/src/transformers/ancient/roman-numerals.js b/src/transformers/ancient/roman-numerals.js new file mode 100644 index 0000000..cb1a4eb --- /dev/null +++ b/src/transformers/ancient/roman-numerals.js @@ -0,0 +1,44 @@ +// roman-numerals transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Roman Numerals', + priority: 70, + numerals: [ + ['M',1000],['CM',900],['D',500],['CD',400], + ['C',100],['XC',90],['L',50],['XL',40], + ['X',10],['IX',9],['V',5],['IV',4],['I',1] + ], + func: function(text) { + return text.replace(/\b\d+\b/g, m => { + let num = parseInt(m,10); + if (num <= 0 || num > 3999 || isNaN(num)) return m; + let out = ''; + for (const [sym,val] of this.numerals) { + while (num >= val) { out += sym; num -= val; } + } + return out; + }); + }, + preview: function(text) { + return this.func(text || '2024'); + }, + reverse: function(text) { + // Greedy parse roman numerals to digits + const map = {I:1,V:5,X:10,L:50,C:100,D:500,M:1000}; + const tokenize = s => s.match(/[IVXLCDM]+|[^IVXLCDM]+/gi) || [s]; + return tokenize(text).map(tok => { + if (!/^[IVXLCDM]+$/i.test(tok)) return tok; + const s = tok.toUpperCase(); + let total = 0; + for (let i=0;i { + if (/[a-zA-Z]/.test(c)) { + const out = upper ? c.toUpperCase() : c.toLowerCase(); + upper = !upper; + return out; + } + return c; + }).join(''); + }, + preview: function(text) { + if (!text) return '[alt case]'; + return this.func(text.slice(0, 6)) + (text.length > 6 ? '...' : ''); + }, + reverse: function(text) { + // Reverse by lowercasing (loses original case pattern) + return text.toLowerCase(); + }, + detector: function(text) { + const cleaned = text.trim(); + if (cleaned.length < 4) return false; + + // Check for alternating pattern in letters only + let lastWasUpper = null; + let alternations = 0; + let letterCount = 0; + + for (const char of cleaned) { + if (/[a-zA-Z]/.test(char)) { + const isUpper = char === char.toUpperCase(); + if (lastWasUpper !== null && isUpper !== lastWasUpper) { + alternations++; + } + lastWasUpper = isUpper; + letterCount++; + } + } + + // Must have at least 3 alternations and at least 70% alternation rate + return letterCount >= 4 && alternations >= 3 && alternations >= letterCount * 0.7; + } + +}); \ No newline at end of file diff --git a/src/transformers/case/camel-case.js b/src/transformers/case/camel-case.js new file mode 100644 index 0000000..b812b71 --- /dev/null +++ b/src/transformers/case/camel-case.js @@ -0,0 +1,20 @@ +// camel-case transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'camelCase', + priority: 275, + func: function(text) { + const parts = text.split(/[^a-zA-Z0-9]+/).filter(Boolean); + if (parts.length === 0) return ''; + const first = parts[0].toLowerCase(); + const rest = parts.slice(1).map(p => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase()).join(''); + return first + rest; + }, + preview: function(text) { + if (!text) return '[camel]'; + return this.func(text); + } + +}); \ No newline at end of file diff --git a/src/transformers/case/kebab-case.js b/src/transformers/case/kebab-case.js new file mode 100644 index 0000000..2078e15 --- /dev/null +++ b/src/transformers/case/kebab-case.js @@ -0,0 +1,37 @@ +// kebab-case transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'kebab-case', + priority: 280, + func: function(text) { + return text.trim().split(/[^a-zA-Z0-9]+/).filter(Boolean).map(s => s.toLowerCase()).join('-'); + }, + preview: function(text) { + if (!text) return '[kebab]'; + return this.func(text); + }, + // Detector: Look for lowercase alphanumeric words separated by hyphens + detector: function(text) { + const cleaned = text.trim(); + // Must have at least one hyphen and only lowercase letters, numbers, and hyphens + if (!/^[a-z0-9]+(-[a-z0-9]+)+$/.test(cleaned)) return false; + + // Exclude A1Z26 (all numbers 1-26) + const parts = cleaned.split('-'); + const allValidA1Z26 = parts.every(p => { + const num = parseInt(p, 10); + return !isNaN(num) && num >= 1 && num <= 26; + }); + if (allValidA1Z26 && parts.length > 1) return false; // Likely A1Z26 + + // Must contain at least some letters (not just numbers) + return /[a-z]/.test(cleaned); + }, + // Reverse: Replace hyphens with spaces + reverse: function(text) { + return text.replace(/-/g, ' '); + } + +}); \ No newline at end of file diff --git a/src/transformers/case/random-case.js b/src/transformers/case/random-case.js new file mode 100644 index 0000000..1cbabe6 --- /dev/null +++ b/src/transformers/case/random-case.js @@ -0,0 +1,16 @@ +// random-case transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Random Case', + priority: 40, + func: function(text) { + return [...text].map(c => /[a-z]/i.test(c) ? (Math.random() < 0.5 ? c.toLowerCase() : c.toUpperCase()) : c).join(''); + }, + preview: function(text) { + if (!text) return '[RaNdOm]'; + return this.func(text.slice(0, 8)) + (text.length > 8 ? '...' : ''); + } + +}); \ No newline at end of file diff --git a/src/transformers/case/sentence-case.js b/src/transformers/case/sentence-case.js new file mode 100644 index 0000000..63006db --- /dev/null +++ b/src/transformers/case/sentence-case.js @@ -0,0 +1,18 @@ +// sentence-case transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Sentence Case', + priority: 150, // Higher priority to detect before Base64 + func: function(text) { + if (!text) return ''; + const lower = text.toLowerCase(); + return lower.charAt(0).toUpperCase() + lower.slice(1); + }, + preview: function(text) { + if (!text) return '[Sentence]'; + return this.func(text.slice(0, 12)) + (text.length > 12 ? '...' : ''); + } + +}); \ No newline at end of file diff --git a/src/transformers/case/snake-case.js b/src/transformers/case/snake-case.js new file mode 100644 index 0000000..d93951e --- /dev/null +++ b/src/transformers/case/snake-case.js @@ -0,0 +1,29 @@ +// snake-case transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'snake_case', + priority: 280, + func: function(text) { + return text.trim().split(/[^a-zA-Z0-9]+/).filter(Boolean).map(s => s.toLowerCase()).join('_'); + }, + preview: function(text) { + if (!text) return '[snake]'; + return this.func(text); + }, + // Detector: Look for lowercase alphanumeric words separated by underscores + detector: function(text) { + const cleaned = text.trim(); + // Must have at least one underscore and only lowercase letters, numbers, and underscores + if (!/^[a-z0-9]+(_[a-z0-9]+)+$/.test(cleaned)) return false; + + // Must contain at least some letters (not just numbers) + return /[a-z]/.test(cleaned); + }, + // Reverse: Replace underscores with spaces + reverse: function(text) { + return text.replace(/_/g, ' '); + } + +}); \ No newline at end of file diff --git a/src/transformers/case/title-case.js b/src/transformers/case/title-case.js new file mode 100644 index 0000000..a7b3f60 --- /dev/null +++ b/src/transformers/case/title-case.js @@ -0,0 +1,16 @@ +// title-case transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Title Case', + priority: 150, // Higher priority to detect before Base64 + func: function(text) { + return text.replace(/\w\S*/g, (w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()); + }, + preview: function(text) { + if (!text) return '[Title Case]'; + return this.func(text.slice(0, 12)) + (text.length > 12 ? '...' : ''); + } + +}); \ No newline at end of file diff --git a/src/transformers/cipher/affine.js b/src/transformers/cipher/affine.js new file mode 100644 index 0000000..9aafec7 --- /dev/null +++ b/src/transformers/cipher/affine.js @@ -0,0 +1,32 @@ +// affine transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Affine Cipher (a=5,b=8)', + priority: 60, + a: 5, b: 8, m: 26, invA: 21, // 5*21 โ‰ก 1 (mod 26) + func: function(text) { + const {a,b,m} = this; + return [...text].map(c => { + const code = c.charCodeAt(0); + if (code>=65 && code<=90) return String.fromCharCode(65 + ((a*(code-65)+b)%m)); + if (code>=97 && code<=122) return String.fromCharCode(97 + ((a*(code-97)+b)%m)); + return c; + }).join(''); + }, + preview: function(text) { + if (!text) return '[affine]'; + return this.func(text.slice(0,8)) + (text.length>8?'...':''); + }, + reverse: function(text) { + const {invA,b,m} = this; + return [...text].map(c => { + const code = c.charCodeAt(0); + if (code>=65 && code<=90) return String.fromCharCode(65 + ((invA*((code-65 - b + m)%m))%m)); + if (code>=97 && code<=122) return String.fromCharCode(97 + ((invA*((code-97 - b + m)%m))%m)); + return c; + }).join(''); + } + +}); \ No newline at end of file diff --git a/src/transformers/cipher/atbash.js b/src/transformers/cipher/atbash.js new file mode 100644 index 0000000..d743c05 --- /dev/null +++ b/src/transformers/cipher/atbash.js @@ -0,0 +1,34 @@ +// atbash transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + name: 'Atbash Cipher', + priority: 60, + // Detector: Check if text is mostly letters (atbash is hard to detect specifically) + detector: function(text) { + // Remove punctuation, numbers, and common symbols for the ratio check + const cleaned = text.replace(/[\s.,!?;:'"()\-&0-9]/g, ''); + if (cleaned.length < 5) return false; + const letterCount = (cleaned.match(/[a-zA-Z]/g) || []).length; + // Must be mostly letters (at least 70%) + return letterCount / cleaned.length > 0.7; + }, + func: function(text) { + const a = 'a'.charCodeAt(0), z = 'z'.charCodeAt(0); + const A = 'A'.charCodeAt(0), Z = 'Z'.charCodeAt(0); + return [...text].map(c => { + const code = c.charCodeAt(0); + if (code >= A && code <= Z) return String.fromCharCode(Z - (code - A)); + if (code >= a && code <= z) return String.fromCharCode(z - (code - a)); + return c; + }).join(''); + }, + preview: function(text) { + if (!text) return '[atbash]'; + return this.func(text.slice(0, 6)) + (text.length > 6 ? '...' : ''); + }, + reverse: function(text) { + // Atbash is its own inverse + return this.func(text); + } +}); diff --git a/src/transformers/cipher/baconian.js b/src/transformers/cipher/baconian.js new file mode 100644 index 0000000..bdf6acf --- /dev/null +++ b/src/transformers/cipher/baconian.js @@ -0,0 +1,40 @@ +// baconian transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Baconian Cipher', + priority: 60, + table: (function(){ + const map = {}; + const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + for (let i=0;i<26;i++) { + const code = i.toString(2).padStart(5,'0').replace(/0/g,'A').replace(/1/g,'B'); + map[alphabet[i]] = code; + } + return map; + })(), + func: function(text) { + return [...text.toUpperCase()].map(ch => { + if (this.table[ch]) return this.table[ch]; + if (/[\s]/.test(ch)) return '/'; + return ch; + }).join(' '); + }, + preview: function(text) { + if (!text) return 'AAAAA AABBA ...'; + return this.func((text || 'AB').slice(0,2)); + }, + reverse: function(text) { + const rev = {}; + Object.keys(this.table).forEach(k => rev[this.table[k]] = k); + const tokens = text.trim().split(/\s+/); + return tokens.map(tok => { + if (tok === '/') return ' '; + const clean = tok.replace(/[^AB]/g,''); + if (clean.length === 5 && rev[clean]) return rev[clean]; + return tok; + }).join(''); + } + +}); \ No newline at end of file diff --git a/src/transformers/cipher/caesar.js b/src/transformers/cipher/caesar.js new file mode 100644 index 0000000..f39ab0a --- /dev/null +++ b/src/transformers/cipher/caesar.js @@ -0,0 +1,45 @@ +// caesar transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Caesar Cipher', + priority: 60, + shift: 3, // Traditional Caesar shift is 3 + func: function(text) { + return [...text].map(c => { + const code = c.charCodeAt(0); + // Only shift letters, leave other characters unchanged + if (code >= 65 && code <= 90) { // Uppercase letters + return String.fromCharCode(((code - 65 + this.shift) % 26) + 65); + } else if (code >= 97 && code <= 122) { // Lowercase letters + return String.fromCharCode(((code - 97 + this.shift) % 26) + 97); + } else { + return c; + } + }).join(''); + }, + preview: function(text) { + if (!text) return '[cursive]'; + return this.func(text.slice(0, 3)) + '...'; + }, + reverse: function(text) { + // For decoding, shift in the opposite direction + const originalShift = this.shift; + this.shift = 26 - (this.shift % 26); // Reverse the shift + const result = this.func(text); + this.shift = originalShift; // Restore original shift + return result; + }, + // Detector: Check if text is letters-only (potential Caesar cipher) + detector: function(text) { + // Caesar cipher only affects letters, so check if text contains mostly letters + // Remove punctuation, numbers, and common symbols for the ratio check + const cleaned = text.replace(/[\s.,!?;:'"()\-&0-9]/g, ''); + // Must be mostly letters (at least 70%) and have some length + if (cleaned.length < 5) return false; + const letterCount = (cleaned.match(/[a-zA-Z]/g) || []).length; + return letterCount / cleaned.length > 0.7; + } + +}); \ No newline at end of file diff --git a/src/transformers/cipher/rail-fence.js b/src/transformers/cipher/rail-fence.js new file mode 100644 index 0000000..3e1b552 --- /dev/null +++ b/src/transformers/cipher/rail-fence.js @@ -0,0 +1,50 @@ +// rail-fence transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Rail Fence (3 Rails)', + priority: 60, + rails: 3, + func: function(text) { + const rails = Array.from({length: this.rails}, () => []); + let rail = 0, dir = 1; + for (const ch of text) { + rails[rail].push(ch); + rail += dir; + if (rail === 0 || rail === this.rails-1) dir *= -1; + } + return rails.flat().join(''); + }, + preview: function(text) { + if (!text) return '[rail]'; + return this.func(text.slice(0,12)) + (text.length>12?'...':''); + }, + reverse: function(text) { + // Use Array.from to properly handle multi-byte UTF-8 characters + const chars = Array.from(text); + const len = chars.length; + const pattern = []; + let rail = 0, dir = 1; + for (let i=0;i { + const code = c.charCodeAt(0); + if (code >= 65 && code <= 90) { // Uppercase letters + return String.fromCharCode(((code - 65 + 13) % 26) + 65); + } else if (code >= 97 && code <= 122) { // Lowercase letters + return String.fromCharCode(((code - 97 + 13) % 26) + 97); + } else { + return c; + } + }).join(''); + }, + preview: function(text) { + if (!text) return '[rot13]'; + return this.func(text.slice(0, 3)) + '...'; + }, + reverse: function(text) { + // ROT13 is its own inverse + return this.func(text); + }, + // Detector: Check if text is letters-only (potential ROT13) + detector: function(text) { + // ROT13 only affects letters, so check if text contains mostly letters + // Remove punctuation, numbers, and common symbols for the ratio check + const cleaned = text.replace(/[\s.,!?;:'"()\-&0-9]/g, ''); + // Must be mostly letters (at least 70%) and have some length + if (cleaned.length < 5) return false; + const letterCount = (cleaned.match(/[a-zA-Z]/g) || []).length; + return letterCount / cleaned.length > 0.7; + } +}); diff --git a/src/transformers/cipher/rot18.js b/src/transformers/cipher/rot18.js new file mode 100644 index 0000000..e86203f --- /dev/null +++ b/src/transformers/cipher/rot18.js @@ -0,0 +1,27 @@ +// rot18 transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'ROT18', + priority: 60, + func: function(text) { + const rot13 = c => { + const code = c.charCodeAt(0); + if (code >= 65 && code <= 90) return String.fromCharCode(65 + ((code-65 + 13)%26)); + if (code >= 97 && code <= 122) return String.fromCharCode(97 + ((code-97 + 13)%26)); + return c; + }; + const rot5 = c => { + if (c >= '0' && c <= '9') return String.fromCharCode(48 + (((c.charCodeAt(0)-48)+5)%10)); + return c; + }; + return [...text].map(c => rot5(rot13(c))).join(''); + }, + preview: function(text) { + if (!text) return '[rot18]'; + return this.func(text.slice(0, 8)) + (text.length>8?'...':''); + }, + reverse: function(text) { return this.func(text); } + +}); \ No newline at end of file diff --git a/src/transformers/cipher/rot47.js b/src/transformers/cipher/rot47.js new file mode 100644 index 0000000..ecab8d5 --- /dev/null +++ b/src/transformers/cipher/rot47.js @@ -0,0 +1,27 @@ +// rot47 transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'ROT47', + priority: 60, + func: function(text) { + return [...text].map(c => { + const code = c.charCodeAt(0); + // ROT47 operates on ASCII 33-126 (94 chars), rotating by 47 (half of 94) + // This makes ROT47 self-inverse (encoding = decoding) + if (code >= 33 && code <= 126) { + return String.fromCharCode(33 + ((code - 33 + 47) % 94)); + } + return c; + }).join(''); + }, + preview: function(text) { + return this.func(text); + }, + reverse: function(text) { + // ROT47 is self-inverse, so reverse is the same as forward + return this.func(text); + } + +}); \ No newline at end of file diff --git a/src/transformers/cipher/rot5.js b/src/transformers/cipher/rot5.js new file mode 100644 index 0000000..5822d85 --- /dev/null +++ b/src/transformers/cipher/rot5.js @@ -0,0 +1,26 @@ +// rot5 transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'ROT5', + priority: 60, + func: function(text) { + return [...text].map(c => { + if (c >= '0' && c <= '9') { + const n = c.charCodeAt(0) - 48; + return String.fromCharCode(48 + ((n + 5) % 10)); + } + return c; + }).join(''); + }, + preview: function(text) { + if (!text) return '[rot5]'; + return this.func(text.slice(0, 6)) + (text.length > 6 ? '...' : ''); + }, + reverse: function(text) { + // ROT5 is its own inverse + return this.func(text); + } + +}); \ No newline at end of file diff --git a/src/transformers/cipher/vigenere.js b/src/transformers/cipher/vigenere.js new file mode 100644 index 0000000..75908c2 --- /dev/null +++ b/src/transformers/cipher/vigenere.js @@ -0,0 +1,42 @@ +// vigenere transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Vigenรจre Cipher', + priority: 60, + key: 'KEY', + func: function(text) { + const key = this.key; + let out = ''; + let j = 0; + for (let i=0;i= 65 && code <= 90) { out += String.fromCharCode(65 + ((code-65 + k)%26)); j++; } + else if (code >= 97 && code <= 122) { out += String.fromCharCode(97 + ((code-97 + k)%26)); j++; } + else out += c; + } + return out; + }, + preview: function(text) { + if (!text) return '[Vigenรจre]'; + return this.func(text.slice(0,8)) + (text.length>8?'...':''); + }, + reverse: function(text) { + const key = this.key; + let out = ''; + let j = 0; + for (let i=0;i= 65 && code <= 90) { out += String.fromCharCode(65 + ((code-65 + 26 - (k%26))%26)); j++; } + else if (code >= 97 && code <= 122) { out += String.fromCharCode(97 + ((code-97 + 26 - (k%26))%26)); j++; } + else out += c; + } + return out; + } + +}); \ No newline at end of file diff --git a/src/transformers/encoding/ascii85.js b/src/transformers/encoding/ascii85.js new file mode 100644 index 0000000..5ab2d2b --- /dev/null +++ b/src/transformers/encoding/ascii85.js @@ -0,0 +1,111 @@ +// ascii85 transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + name: 'ASCII85', + priority: 290, + // Detector: ASCII85 has distinctive <~ ~> wrapper + detector: function(text) { + return text.startsWith('<~') && text.endsWith('~>'); + }, + + func: function(text) { + // Simple ASCII85 encoding implementation + // Use TextEncoder to properly handle multi-byte UTF-8 characters + const bytes = new TextEncoder().encode(text); + let result = '<~'; + let buffer = 0; + let bufferLength = 0; + + for (let i = 0; i < bytes.length; i++) { + buffer = (buffer << 8) | bytes[i]; + bufferLength += 8; + + if (bufferLength >= 32) { + let value = buffer >>> (bufferLength - 32); + buffer &= (1 << (bufferLength - 32)) - 1; + bufferLength -= 32; + + if (value === 0) { + result += 'z'; + } else { + for (let j = 4; j >= 0; j--) { + const digit = (value / Math.pow(85, j)) % 85; + result += String.fromCharCode(digit + 33); + } + } + } + } + + // Handle remaining bits + if (bufferLength > 0) { + buffer <<= (32 - bufferLength); + let value = buffer; + const bytes = Math.ceil(bufferLength / 8); + + for (let j = 4; j >= (4 - bytes); j--) { + const digit = (value / Math.pow(85, j)) % 85; + result += String.fromCharCode(digit + 33); + } + } + + return result + '~>'; + }, + preview: function(text) { + if (!text) return '[ascii85]'; + const full = this.func(text); + return full.substring(0, 16) + (full.length > 16 ? '...' : ''); + }, + reverse: function(text) { + // Check if it's a valid ASCII85 string + if (!text.startsWith('<~') || !text.endsWith('~>')) { + return text; + } + + // Remove delimiters and whitespace + text = text.substring(2, text.length - 2).replace(/\s+/g, ''); + + const bytes = []; + let i = 0; + + while (i < text.length) { + // Handle 'z' special case (represents 4 zero bytes) + if (text[i] === 'z') { + bytes.push(0, 0, 0, 0); + i++; + continue; + } + + // Process a group of 5 characters + if (i < text.length) { + let value = 0; + const groupSize = Math.min(5, text.length - i); + + // Convert the group to a 32-bit value + for (let j = 0; j < groupSize; j++) { + value = value * 85 + (text.charCodeAt(i + j) - 33); + } + + // Pad with 'u' (84) if needed for partial groups + for (let j = groupSize; j < 5; j++) { + value = value * 85 + 84; + } + + // Extract bytes from the value + // groupSize chars encodes (groupSize - 1) bytes + const bytesToWrite = groupSize - 1; + for (let j = 0; j < bytesToWrite; j++) { + bytes.push((value >>> ((3 - j) * 8)) & 0xFF); + } + + i += groupSize; + } else { + break; + } + } + + // Use TextDecoder to properly handle UTF-8 multi-byte characters + return new TextDecoder().decode(new Uint8Array(bytes)); + } + +}); \ No newline at end of file diff --git a/src/transformers/encoding/base32.js b/src/transformers/encoding/base32.js new file mode 100644 index 0000000..e20b310 --- /dev/null +++ b/src/transformers/encoding/base32.js @@ -0,0 +1,85 @@ +// base32 transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + name: 'Base32', + priority: 280, + // Detector: Only Base32 characters (A-Z, 2-7, =) + detector: function(text) { + const cleaned = text.trim().replace(/\s/g, ''); + return cleaned.length >= 8 && /^[A-Z2-7=]+$/.test(cleaned); + }, + + alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', + func: function(text) { + if (!text) return ''; + + // Convert text to bytes + const bytes = new TextEncoder().encode(text); + let result = ''; + let bits = 0; + let value = 0; + + for (let i = 0; i < bytes.length; i++) { + value = (value << 8) | bytes[i]; + bits += 8; + + while (bits >= 5) { + bits -= 5; + result += this.alphabet[(value >> bits) & 0x1F]; + } + } + + // Handle remaining bits + if (bits > 0) { + result += this.alphabet[(value << (5 - bits)) & 0x1F]; + } + + // Add padding + while (result.length % 8 !== 0) { + result += '='; + } + + return result; + }, + preview: function(text) { + if (!text) return '[base32]'; + const full = this.func(text); + return full.substring(0, 16) + (full.length > 16 ? '...' : ''); + }, + reverse: function(text) { + if (!text) return ''; + + // Remove padding and whitespace + text = text.replace(/\s+/g, '').replace(/=+$/, ''); + + if (text.length === 0) return ''; + + // Create reverse map + const revMap = {}; + for (let i = 0; i < this.alphabet.length; i++) { + revMap[this.alphabet[i]] = i; + } + + const bytes = []; + let bits = 0; + let value = 0; + + for (let i = 0; i < text.length; i++) { + const char = text[i].toUpperCase(); + if (revMap[char] === undefined) continue; // Skip invalid characters + + value = (value << 5) | revMap[char]; + bits += 5; + + while (bits >= 8) { + bits -= 8; + bytes.push((value >> bits) & 0xFF); + } + } + + // Use TextDecoder to properly handle UTF-8 multi-byte characters + return new TextDecoder().decode(new Uint8Array(bytes)); + } + +}); \ No newline at end of file diff --git a/src/transformers/encoding/base45.js b/src/transformers/encoding/base45.js new file mode 100644 index 0000000..f97c36f --- /dev/null +++ b/src/transformers/encoding/base45.js @@ -0,0 +1,45 @@ +// base45 transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Base45', + priority: 290, + alphabet: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:', + func: function(text) { + const bytes = new TextEncoder().encode(text); + const chars = []; + for (let i=0;i index[c]).filter(v => v !== undefined); + const out = []; + for (let i=0;i> 8, x & 0xFF); + } else if (i+1 < codes.length) { + const x = codes[i] + codes[i+1]*45; + out.push(x & 0xFF); + } + } + return new TextDecoder().decode(Uint8Array.from(out)); + } + +}); \ No newline at end of file diff --git a/src/transformers/encoding/base58.js b/src/transformers/encoding/base58.js new file mode 100644 index 0000000..22535b6 --- /dev/null +++ b/src/transformers/encoding/base58.js @@ -0,0 +1,61 @@ +// base58 transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + name: 'Base58', + priority: 275, + // Detector: Only Base58 characters (excludes 0, O, I, l) + detector: function(text) { + const cleaned = text.trim().replace(/\s/g, ''); + return cleaned.length >= 4 && /^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/.test(cleaned); + }, + + alphabet: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz', + func: function(text) { + if (!text) return ''; + const bytes = new TextEncoder().encode(text); + // Count leading zeros + let zeros = 0; + for (let b of bytes) { if (b === 0) zeros++; else break; } + // Convert to BigInt + let n = 0n; + for (let b of bytes) { n = (n << 8n) + BigInt(b); } + // Encode + let out = ''; + while (n > 0n) { + const rem = n % 58n; + n = n / 58n; + out = this.alphabet[Number(rem)] + out; + } + // Add leading zeros as '1' + for (let i = 0; i < zeros; i++) out = '1' + out; + return out || '1'; + }, + preview: function(text) { + if (!text) return '[base58]'; + const full = this.func(text); + return full.substring(0, 12) + (full.length > 12 ? '...' : ''); + }, + reverse: function(text) { + if (!text) return ''; + // Count leading '1's + let zeros = 0; + for (let c of text) { if (c === '1') zeros++; else break; } + // Convert to BigInt + let n = 0n; + for (let c of text) { + const i = this.alphabet.indexOf(c); + if (i < 0) continue; + n = n * 58n + BigInt(i); + } + // Convert BigInt to bytes + const bytes = []; + while (n > 0n) { + bytes.unshift(Number(n % 256n)); + n = n / 256n; + } + for (let i = 0; i < zeros; i++) bytes.unshift(0); + return new TextDecoder().decode(Uint8Array.from(bytes)); + } + +}); \ No newline at end of file diff --git a/src/transformers/encoding/base62.js b/src/transformers/encoding/base62.js new file mode 100644 index 0000000..2be2d0d --- /dev/null +++ b/src/transformers/encoding/base62.js @@ -0,0 +1,44 @@ +// base62 transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Base62', + priority: 290, + alphabet: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', + func: function(text) { + if (!text) return ''; + const bytes = new TextEncoder().encode(text); + let n = 0n; + for (let b of bytes) { n = (n << 8n) + BigInt(b); } + if (n === 0n) return '0'; + let out = ''; + while (n > 0n) { + const rem = n % 62n; + n = n / 62n; + out = this.alphabet[Number(rem)] + out; + } + return out; + }, + preview: function(text) { + if (!text) return '[base62]'; + return this.func(text.slice(0, 3)) + '...'; + }, + reverse: function(text) { + if (!text) return ''; + let n = 0n; + for (let c of text) { + const i = this.alphabet.indexOf(c); + if (i < 0) continue; + n = n * 62n + BigInt(i); + } + const bytes = []; + while (n > 0n) { + bytes.unshift(Number(n % 256n)); + n = n / 256n; + } + if (bytes.length === 0) bytes.push(0); + return new TextDecoder().decode(Uint8Array.from(bytes)); + } + +}); \ No newline at end of file diff --git a/src/transformers/encoding/base64.js b/src/transformers/encoding/base64.js new file mode 100644 index 0000000..077c5bb --- /dev/null +++ b/src/transformers/encoding/base64.js @@ -0,0 +1,51 @@ +// base64 transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + name: 'Base64', + priority: 270, + // Detector: Only Base64 characters (A-Z, a-z, 0-9, +, /, =) + detector: function(text) { + const cleaned = text.trim().replace(/\s/g, ''); + return cleaned.length >= 4 && /^[A-Za-z0-9+\/=]+$/.test(cleaned); + }, + + func: function(text) { + try { + // Properly encode UTF-8 text (including emojis) to Base64 + const encoder = new TextEncoder(); + const bytes = encoder.encode(text); + let binaryString = ''; + for (let i = 0; i < bytes.length; i++) { + binaryString += String.fromCharCode(bytes[i]); + } + return btoa(binaryString); + } catch (e) { + return '[Invalid input]'; + } + }, + preview: function(text) { + if (!text) return '[base64]'; + try { + const full = this.func(text); + return full.substring(0, 12) + (full.length > 12 ? '...' : ''); + } catch (e) { + return '[Invalid input]'; + } + }, + reverse: function(text) { + try { + // Properly decode Base64 to UTF-8 text (including emojis) + const binaryString = atob(text); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + const decoder = new TextDecoder('utf-8'); + return decoder.decode(bytes); + } catch (e) { + return text; + } + } + +}); \ No newline at end of file diff --git a/src/transformers/encoding/base64url.js b/src/transformers/encoding/base64url.js new file mode 100644 index 0000000..b6cbea5 --- /dev/null +++ b/src/transformers/encoding/base64url.js @@ -0,0 +1,53 @@ +// base64url transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + name: 'Base64 URL', + priority: 270, + // Detector: Only Base64 URL characters (A-Z, a-z, 0-9, -, _, =) + detector: function(text) { + const cleaned = text.trim().replace(/\s/g, ''); + return cleaned.length >= 4 && /^[A-Za-z0-9\-_=]+$/.test(cleaned); + }, + + func: function(text) { + if (!text) return ''; + try { + // Properly encode UTF-8 text (including emojis) to Base64 URL + const encoder = new TextEncoder(); + const bytes = encoder.encode(text); + let binaryString = ''; + for (let i = 0; i < bytes.length; i++) { + binaryString += String.fromCharCode(bytes[i]); + } + const std = btoa(binaryString); + return std.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/,''); + } catch (e) { + return '[Invalid input]'; + } + }, + preview: function(text) { + if (!text) return '[b64url]'; + const full = this.func(text); + return full.substring(0, 12) + (full.length > 12 ? '...' : ''); + }, + reverse: function(text) { + if (!text) return ''; + let std = text.replace(/-/g, '+').replace(/_/g, '/'); + // pad + while (std.length % 4 !== 0) std += '='; + try { + // Properly decode Base64 URL to UTF-8 text (including emojis) + const binaryString = atob(std); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + const decoder = new TextDecoder('utf-8'); + return decoder.decode(bytes); + } catch (e) { + return text; + } + } + +}); \ No newline at end of file diff --git a/src/transformers/encoding/binary.js b/src/transformers/encoding/binary.js new file mode 100644 index 0000000..6063d23 --- /dev/null +++ b/src/transformers/encoding/binary.js @@ -0,0 +1,43 @@ +// binary transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + name: 'Binary', + priority: 300, + // Detector: Only 0s, 1s, and spaces + detector: function(text) { + const cleaned = text.trim(); + const noSpaces = cleaned.replace(/\s/g, ''); + return noSpaces.length >= 8 && /^[01\s]+$/.test(cleaned); + }, + + func: function(text) { + // Use TextEncoder to properly handle UTF-8 (including emoji) + const encoder = new TextEncoder(); + const bytes = encoder.encode(text); + return Array.from(bytes).map(b => b.toString(2).padStart(8, '0')).join(' '); + }, + preview: function(text) { + if (!text) return '[binary]'; + const full = this.func(text); + return full.substring(0, 24) + (full.length > 24 ? '...' : ''); + }, + reverse: function(text) { + // Remove spaces and ensure we have valid binary + const binText = text.replace(/\s+/g, ''); + const bytes = []; + + // Process 8 bits at a time + for (let i = 0; i < binText.length; i += 8) { + const byte = binText.substr(i, 8); + if (byte.length === 8) { + bytes.push(parseInt(byte, 2)); + } + } + + // Use TextDecoder to properly decode UTF-8 + const decoder = new TextDecoder('utf-8'); + return decoder.decode(new Uint8Array(bytes)); + } + +}); \ No newline at end of file diff --git a/src/transformers/encoding/hex.js b/src/transformers/encoding/hex.js new file mode 100644 index 0000000..dbd0f4d --- /dev/null +++ b/src/transformers/encoding/hex.js @@ -0,0 +1,40 @@ +// hex transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + name: 'Hexadecimal', + priority: 290, + // Detector: Only hex characters (0-9, A-F) + detector: function(text) { + const cleaned = text.trim().replace(/\s/g, ''); + return cleaned.length >= 4 && /^[0-9A-Fa-f]+$/.test(cleaned); + }, + + func: function(text) { + // Use TextEncoder to properly handle UTF-8 (including emoji) + const encoder = new TextEncoder(); + const bytes = encoder.encode(text); + return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join(' '); + }, + preview: function(text) { + if (!text) return '[hex]'; + const full = this.func(text); + return full.substring(0, 20) + (full.length > 20 ? '...' : ''); + }, + reverse: function(text) { + const hexText = text.replace(/\s+/g, ''); + const bytes = []; + + for (let i = 0; i < hexText.length; i += 2) { + const byte = hexText.substr(i, 2); + if (byte.length === 2) { + bytes.push(parseInt(byte, 16)); + } + } + + // Use TextDecoder to properly decode UTF-8 + const decoder = new TextDecoder('utf-8'); + return decoder.decode(new Uint8Array(bytes)); + } + +}); \ No newline at end of file diff --git a/src/transformers/encoding/html.js b/src/transformers/encoding/html.js new file mode 100644 index 0000000..35277e5 --- /dev/null +++ b/src/transformers/encoding/html.js @@ -0,0 +1,32 @@ +// html transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + name: 'HTML Entities', + priority: 40, + // Detector: Look for &...; pattern (HTML entities) + detector: function(text) { + return text.includes('&') && text.includes(';') && /&[a-zA-Z0-9#]+;/.test(text); + }, + + func: function(text) { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + }, + preview: function(text) { + return this.func(text); + }, + reverse: function(text) { + return text + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, '\''); + } + +}); \ No newline at end of file diff --git a/src/transformers/encoding/invisible-text.js b/src/transformers/encoding/invisible-text.js new file mode 100644 index 0000000..52faa38 --- /dev/null +++ b/src/transformers/encoding/invisible-text.js @@ -0,0 +1,39 @@ +// invisible-text transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Invisible Text', + priority: 100, // High confidence - uses exclusive Unicode Private Use Area (U+E0000-U+E00FF) + func: function(text) { + if (!text) return ''; + const bytes = new TextEncoder().encode(text); + return Array.from(bytes) + .map(byte => String.fromCodePoint(0xE0000 + byte)) + .join(''); + }, + preview: function(text) { + return '[invisible]'; + }, + reverse: function(text) { + if (!text) return ''; + const matches = [...text.matchAll(/[\u{E0000}-\u{E00FF}]/gu)]; + if (!matches.length) return ''; + + // Convert invisible characters back to bytes + const bytes = new Uint8Array( + matches.map(match => match[0].codePointAt(0) - 0xE0000) + ); + + // Use TextDecoder to properly handle UTF-8 encoded bytes (including emoji) + return new TextDecoder().decode(bytes); + }, + // Detector: Check for at least one invisible Unicode character + detector: function(text) { + // Invisible text uses Unicode Private Use Area (U+E0000-U+E00FF for full byte range) + const invisibleMatches = text.match(/[\u{E0000}-\u{E00FF}]/gu); + // Return true if at least one invisible character is found + return invisibleMatches && invisibleMatches.length > 0; + } + +}); \ No newline at end of file diff --git a/src/transformers/encoding/url.js b/src/transformers/encoding/url.js new file mode 100644 index 0000000..2d51150 --- /dev/null +++ b/src/transformers/encoding/url.js @@ -0,0 +1,31 @@ +// url transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + name: 'URL Encode', + priority: 40, + // Detector: Look for %XX pattern (URL encoding) + detector: function(text) { + return text.includes('%') && /%[0-9A-Fa-f]{2}/.test(text); + }, + + func: function(text) { + try { + return encodeURIComponent(text); + } catch (e) { + // Catch malformed Unicode or unpaired surrogates + return '[Invalid input]'; + } + }, + preview: function(text) { + return this.func(text); + }, + reverse: function(text) { + try { + return decodeURIComponent(text); + } catch (e) { + return text; + } + } + +}); \ No newline at end of file diff --git a/src/transformers/fantasy/aurebesh.js b/src/transformers/fantasy/aurebesh.js new file mode 100644 index 0000000..a01dd59 --- /dev/null +++ b/src/transformers/fantasy/aurebesh.js @@ -0,0 +1,38 @@ +// aurebesh transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Aurebesh (Star Wars)', + priority: 100, + map: { + 'a': 'Aurek', 'b': 'Besh', 'c': 'Cresh', 'd': 'Dorn', 'e': 'Esk', 'f': 'Forn', 'g': 'Grek', 'h': 'Herf', 'i': 'Isk', + 'j': 'Jenth', 'k': 'Krill', 'l': 'Leth', 'm': 'Mern', 'n': 'Nern', 'o': 'Osk', 'p': 'Peth', 'q': 'Qek', 'r': 'Resh', + 's': 'Senth', 't': 'Trill', 'u': 'Usk', 'v': 'Vev', 'w': 'Wesk', 'x': 'Xesh', 'y': 'Yirt', 'z': 'Zerek', + 'A': 'AUREK', 'B': 'BESH', 'C': 'CRESH', 'D': 'DORN', 'E': 'ESK', 'F': 'FORN', 'G': 'GREK', 'H': 'HERF', 'I': 'ISK', + 'J': 'JENTH', 'K': 'KRILL', 'L': 'LETH', 'M': 'MERN', 'N': 'NERN', 'O': 'OSK', 'P': 'PETH', 'Q': 'QEK', 'R': 'RESH', + 'S': 'SENTH', 'T': 'TRILL', 'U': 'USK', 'V': 'VEV', 'W': 'WESK', 'X': 'XESH', 'Y': 'YIRT', 'Z': 'ZEREK' + }, + func: function(text) { + return [...text.toLowerCase()].map(c => this.map[c] || c).join(' '); + }, + reverse: function(text) { + const revMap = {}; + for (const [key, value] of Object.entries(this.map)) { + revMap[value.toLowerCase()] = key; + } + return text.split(/\s+/).map(word => revMap[word.toLowerCase()] || word).join(''); + }, + // Detector: Check for Aurebesh words + detector: function(text) { + // Aurebesh uses specific word patterns like "Aurek", "Besh", "Cresh", etc. + const aurebeshWords = ['aurek', 'besh', 'cresh', 'dorn', 'esk', 'forn', 'grek', 'herf', 'isk', + 'jenth', 'krill', 'leth', 'mern', 'nern', 'osk', 'peth', 'qek', 'resh', + 'senth', 'trill', 'usk', 'vev', 'wesk', 'xesh', 'yirt', 'zerek']; + const lowerText = text.toLowerCase(); + // Check if at least 2 Aurebesh words are present + const matches = aurebeshWords.filter(word => lowerText.includes(word)); + return matches.length >= 2; + } + +}); \ No newline at end of file diff --git a/src/transformers/fantasy/dovahzul.js b/src/transformers/fantasy/dovahzul.js new file mode 100644 index 0000000..2d1f5e1 --- /dev/null +++ b/src/transformers/fantasy/dovahzul.js @@ -0,0 +1,56 @@ +// dovahzul transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + name: 'Dovahzul (Dragon)', + priority: 285, + // Detector: Look for characteristic Dovahzul patterns (vowel expansions) + detector: function(text) { + if (!/[a-z]/i.test(text)) return false; + + const dovahzulPatterns = ['ah', 'eh', 'ii', 'kw', 'ks']; + let patternCount = 0; + const lowerInput = text.toLowerCase(); + + for (const pattern of dovahzulPatterns) { + const matches = lowerInput.match(new RegExp(pattern, 'g')); + if (matches) patternCount += matches.length; + } + + // For short inputs, require at least 1 pattern, for longer require 2+ + const minPatterns = text.length < 30 ? 1 : 2; + return patternCount >= minPatterns; + }, + + map: { + 'a': 'ah', 'b': 'b', 'c': 'k', 'd': 'd', 'e': 'eh', 'f': 'f', 'g': 'g', 'h': 'h', 'i': 'ii', + 'j': 'j', 'k': 'k', 'l': 'l', 'm': 'm', 'n': 'n', 'o': 'o', 'p': 'p', 'q': 'kw', 'r': 'r', + 's': 's', 't': 't', 'u': 'u', 'v': 'v', 'w': 'w', 'x': 'ks', 'y': 'y', 'z': 'z', + 'A': 'AH', 'B': 'B', 'C': 'K', 'D': 'D', 'E': 'EH', 'F': 'F', 'G': 'G', 'H': 'H', 'I': 'II', + 'J': 'J', 'K': 'K', 'L': 'L', 'M': 'M', 'N': 'N', 'O': 'O', 'P': 'P', 'Q': 'KW', 'R': 'R', + 'S': 'S', 'T': 'T', 'U': 'U', 'V': 'V', 'W': 'W', 'X': 'KS', 'Y': 'Y', 'Z': 'Z' + }, + func: function(text) { + return [...text.toLowerCase()].map(c => this.map[c] || c).join(''); + }, + reverse: function(text) { + // Build reverse map from multi-character sequences to single chars + const revMap = {}; + for (const [key, value] of Object.entries(this.map)) { + revMap[value.toLowerCase()] = key.toLowerCase(); + } + + // Sort by length (longest first) to match multi-char sequences first + const patterns = Object.keys(revMap).sort((a, b) => b.length - a.length); + + let result = text.toLowerCase(); + // Replace multi-character patterns with their original characters + for (const pattern of patterns) { + const regex = new RegExp(pattern, 'g'); + result = result.replace(regex, revMap[pattern]); + } + + return result; + } + +}); \ No newline at end of file diff --git a/src/transformers/fantasy/klingon.js b/src/transformers/fantasy/klingon.js new file mode 100644 index 0000000..8acd25b --- /dev/null +++ b/src/transformers/fantasy/klingon.js @@ -0,0 +1,58 @@ +// klingon transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Klingon', + priority: 100, + map: { + 'a': 'a', 'b': 'b', 'c': 'ch', 'd': 'D', 'e': 'e', 'f': 'f', 'g': 'gh', 'h': 'H', 'i': 'I', + 'j': 'j', 'k': 'q', 'l': 'l', 'm': 'm', 'n': 'n', 'o': 'o', 'p': 'p', 'q': 'Q', 'r': 'r', + 's': 'S', 't': 't', 'u': 'u', 'v': 'v', 'w': 'w', 'x': 'x', 'y': 'y', 'z': 'z', + 'A': 'A', 'B': 'B', 'C': 'CH', 'D': 'D', 'E': 'E', 'F': 'F', 'G': 'GH', 'H': 'H', 'I': 'I', + 'J': 'J', 'K': 'Q', 'L': 'L', 'M': 'M', 'N': 'N', 'O': 'O', 'P': 'P', 'Q': 'Q', 'R': 'R', + 'S': 'S', 'T': 'T', 'U': 'U', 'V': 'V', 'W': 'W', 'X': 'X', 'Y': 'Y', 'Z': 'Z' + }, + func: function(text) { + // Process character by character, preserving case + return [...text].map(c => this.map[c] || c).join(''); + }, + preview: function(text) { + if (!text) return '[klingon]'; + return this.func(text.slice(0, 8)); + }, + reverse: function(text) { + // Build reverse map with multi-character strings + const revMap = {}; + for (const [key, value] of Object.entries(this.map)) { + revMap[value] = key; + } + // Try to match multi-character sequences first, then single chars + let result = ''; + let i = 0; + while (i < text.length) { + // Try 2-character match first (for 'ch', 'gh', 'CH', 'GH') + const twoChar = text.substr(i, 2); + if (revMap[twoChar]) { + result += revMap[twoChar]; + i += 2; + } else if (revMap[text[i]]) { + result += revMap[text[i]]; + i++; + } else { + result += text[i]; + i++; + } + } + return result; + }, + // Detector: Check for Klingon patterns + detector: function(text) { + // Klingon has characteristic patterns like 'ch', 'gh', 'Q' (capital Q for q sound) + // Also uses capital letters in specific ways (D, H, I, Q, S) + const patterns = text.match(/ch|gh|CH|GH/g); + const capitalPattern = /[DHIQS]/.test(text) && /[a-z]/.test(text); // Mix of specific capitals with lowercase + return (patterns && patterns.length >= 1) || capitalPattern; + } + +}); \ No newline at end of file diff --git a/src/transformers/fantasy/quenya.js b/src/transformers/fantasy/quenya.js new file mode 100644 index 0000000..1ff3082 --- /dev/null +++ b/src/transformers/fantasy/quenya.js @@ -0,0 +1,36 @@ +// quenya transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Quenya (Tolkien Elvish)', + priority: 100, + map: { + 'a': 'a', 'b': 'v', 'c': 'k', 'd': 'd', 'e': 'e', 'f': 'f', 'g': 'g', 'h': 'h', 'i': 'i', + 'j': 'y', 'k': 'k', 'l': 'l', 'm': 'm', 'n': 'n', 'o': 'o', 'p': 'p', 'q': 'kw', 'r': 'r', + 's': 's', 't': 't', 'u': 'u', 'v': 'v', 'w': 'w', 'x': 'ks', 'y': 'y', 'z': 'z', + 'A': 'A', 'B': 'V', 'C': 'K', 'D': 'D', 'E': 'E', 'F': 'F', 'G': 'G', 'H': 'H', 'I': 'I', + 'J': 'Y', 'K': 'K', 'L': 'L', 'M': 'M', 'N': 'N', 'O': 'O', 'P': 'P', 'Q': 'KW', 'R': 'R', + 'S': 'S', 'T': 'T', 'U': 'U', 'V': 'V', 'W': 'W', 'X': 'KS', 'Y': 'Y', 'Z': 'Z' + }, + func: function(text) { + return [...text.toLowerCase()].map(c => this.map[c] || c).join(''); + }, + reverse: function(text) { + // Create reverse map + const revMap = {}; + for (const [key, value] of Object.entries(this.map)) { + revMap[value] = key; + } + return [...text].map(c => revMap[c] || c).join(''); + }, + // Detector: Check for Quenya patterns + detector: function(text) { + // Quenya has characteristic patterns like 'kw' and 'ks', but since the encoding is mostly + // 1:1 (b->v, c->k, j->y, q->kw, x->ks), we look for multiple instances of these patterns + const patterns = text.match(/kw|ks/gi); + // If there are at least 1 multi-char pattern, it's likely Quenya + return patterns && patterns.length >= 1; + } + +}); \ No newline at end of file diff --git a/src/transformers/fantasy/tengwar.js b/src/transformers/fantasy/tengwar.js new file mode 100644 index 0000000..39f511d --- /dev/null +++ b/src/transformers/fantasy/tengwar.js @@ -0,0 +1,32 @@ +// tengwar transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Tengwar Script', + priority: 100, + map: { + 'a': 'แšช', 'b': 'แ›’', 'c': 'แ›ฃ', 'd': 'แ›ž', 'e': 'แ›–', 'f': 'แš ', 'g': 'แšท', 'h': 'แšบ', 'i': 'แ›', + 'j': 'แ›ƒ', 'k': 'แ›ฃ', 'l': 'แ›š', 'm': 'แ›—', 'n': 'แšพ', 'o': 'แšฉ', 'p': 'แ›ˆ', 'q': 'แ›ฉ', 'r': 'แšฑ', + 's': 'แ›‹', 't': 'แ›', 'u': 'แšข', 'v': 'แšก', 'w': 'แšน', 'x': 'แ›‰', 'y': 'แšฃ', 'z': 'แ›‰', + 'A': 'แšช', 'B': 'แ›’', 'C': 'แ›ฃ', 'D': 'แ›ž', 'E': 'แ›–', 'F': 'แš ', 'G': 'แšท', 'H': 'แšบ', 'I': 'แ›', + 'J': 'แ›ƒ', 'K': 'แ›ฃ', 'L': 'แ›š', 'M': 'แ›—', 'N': 'แšพ', 'O': 'แšฉ', 'P': 'แ›ˆ', 'Q': 'แ›ฉ', 'R': 'แšฑ', + 'S': 'แ›‹', 'T': 'แ›', 'U': 'แšข', 'V': 'แšก', 'W': 'แšน', 'X': 'แ›‰', 'Y': 'แšฃ', 'Z': 'แ›‰' + }, + func: function(text) { + return [...text.toLowerCase()].map(c => this.map[c] || c).join(''); + }, + reverse: function(text) { + const revMap = {}; + for (const [key, value] of Object.entries(this.map)) { + revMap[value] = key; + } + return [...text].map(c => revMap[c] || c).join(''); + }, + // Detector: Check for Tengwar Script characters + detector: function(text) { + // Tengwar has unique characters like แšช, แ›ฃ, แšฉ, แ›ฉ, แšฃ + return /[แšชแ›ฃแšฉแ›ฉแšฃแ›’แ›žแ›–แš แšทแšบแ›แ›ƒแ›šแ›—แšพแ›ˆแšฑแ›‹แ›แšขแšกแšนแ›‰]/.test(text); + } + +}); \ No newline at end of file diff --git a/src/transformers/format/leetspeak.js b/src/transformers/format/leetspeak.js new file mode 100644 index 0000000..69bfee6 --- /dev/null +++ b/src/transformers/format/leetspeak.js @@ -0,0 +1,32 @@ +// leetspeak transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Leetspeak', + priority: 40, + map: { + 'a': '4', 'e': '3', 'i': '1', 'o': '0', 's': '5', 't': '7', 'l': '1', + 'A': '4', 'E': '3', 'I': '1', 'O': '0', 'S': '5', 'T': '7', 'L': '1' + }, + func: function(text) { + return [...text].map(c => this.map[c] || c).join(''); + }, + preview: function(text) { + if (!text) return '[double-struck]'; + return this.func(text.slice(0, 3)) + '...'; + }, + // Create reverse map for decoding + reverseMap: function() { + const revMap = {}; + for (const [key, value] of Object.entries(this.map)) { + revMap[value] = key.toLowerCase(); + } + return revMap; + }, + reverse: function(text) { + const revMap = this.reverseMap(); + return [...text].map(c => revMap[c] || c).join(''); + } + +}); \ No newline at end of file diff --git a/src/transformers/format/pigLatin.js b/src/transformers/format/pigLatin.js new file mode 100644 index 0000000..83174a9 --- /dev/null +++ b/src/transformers/format/pigLatin.js @@ -0,0 +1,143 @@ +// pigLatin transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + name: 'Pig Latin', + priority: 285, + // Detector: Look for words ending in "ay" or "way" (Pig Latin pattern) + detector: function(text) { + if (!/[a-z]/i.test(text)) return false; + + const words = text.toLowerCase().split(/\s+/); + if (words.length < 2) return false; + + let ayEndingCount = 0; + for (const word of words) { + const cleanWord = word.replace(/[^a-z]/g, ''); + if (cleanWord.endsWith('ay') || cleanWord.endsWith('way')) { + ayEndingCount++; + } + } + + // If more than 50% of words end in "ay" or "way", it's likely Pig Latin + const ratio = ayEndingCount / words.length; + return ratio >= 0.5; + }, + + func: function(text) { + return text.split(/\s+/).map(word => { + if (!word) return ''; + + // Check if the word starts with a vowel + if (/^[aeiou]/i.test(word)) { + return word + 'way'; + } + + // Handle consonant clusters at the beginning + const match = word.match(/^([^aeiou]+)(.*)/i); + if (match) { + return match[2] + match[1] + 'ay'; + } + + return word; + }).join(' '); + }, + preview: function(text) { + return this.func(text); + }, + reverse: function(text) { + return text.split(/\s+/).map(word => { + if (!word) return ''; + + // Handle words ending in 'way' + // Ambiguity: could be vowel+"way" OR consonant-moved+"w"+"ay" + if (word.endsWith('way') && word.length > 3) { + const base = word.slice(0, -3); + + // Try both possibilities + const option1 = base; // Assume vowel-starting word + const option2 = 'w' + base; // Assume "w" was moved + + // Re-encode both and see which matches + const test1 = (/^[aeiou]/i.test(option1)) ? option1 + 'way' : null; + const test2 = option2.match(/^([^aeiou]+)(.*)/i); + const reencoded2 = test2 ? test2[2] + test2[1] + 'ay' : null; + + // If only one matches, use it + if (test1 === word && reencoded2 !== word) return option1; + if (reencoded2 === word && test1 !== word) return option2; + + // If both match (ambiguous), use heuristics: + // 1. Very short bases (1-2 chars) are likely complete words: "is", "a", "I" + if (test1 === word && reencoded2 === word && base.length <= 2) { + return option1; // base without "w" + } + // 2. Prefer "w" + base if base starts with vowel AND ends with consonant AND longer + // e.g., "world" (orld), "win" (in) but NOT "away" (away) + if (test1 === word && reencoded2 === word && + /^[aeiou]/i.test(base) && /[bcdfghjklmnpqrstvwxyz]$/i.test(base)) { + return option2; // w + base + } + + // Fallback + return /^[aeiou]/i.test(base) ? base : 'w' + base; + } + + // Handle words ending in 'ay' (but not 'way') + if (word.endsWith('ay') && !word.endsWith('way') && word.length > 2) { + const base = word.slice(0, -2); + + // If base contains non-letter characters, return as-is + if (!/^[a-z]+$/i.test(base)) { + return word; + } + + // Try different consonant cluster lengths and score them + const commonClusters = ['th', 'ch', 'sh', 'wh', 'ph', 'gh', 'ck', 'ng', 'qu', + 'str', 'spr', 'thr', 'chr', 'scr', 'squ', 'spl', 'shr']; + let bestOption = null; + let bestScore = -1; + + for (let i = 1; i < base.length; i++) { + const cluster = base.slice(-i); + const remaining = base.slice(0, -i); + + // Must be all consonants and remaining must start with vowel + if (remaining.length > 0 && + /^[bcdfghjklmnpqrstvwxyz]+$/i.test(cluster) && + /^[aeiou]/i.test(remaining)) { + + let score = 0; + + // Prefer common multi-consonant clusters (score 10) + if (commonClusters.includes(cluster.toLowerCase())) { + score = 10; + } + // Prefer 2-3 letter clusters over single letters (score 5) + else if (cluster.length >= 2 && cluster.length <= 3) { + score = 5; + } + // Single consonants get lower score (score 2) + else if (cluster.length === 1) { + score = 2; + } + // Very long clusters are unlikely (score 1) + else { + score = 1; + } + + if (score > bestScore) { + bestScore = score; + bestOption = cluster + remaining; + } + } + } + + if (bestOption) return bestOption; + } + + return word; + }).join(' '); + } + +}); \ No newline at end of file diff --git a/src/transformers/format/qwerty-shift.js b/src/transformers/format/qwerty-shift.js new file mode 100644 index 0000000..7638fc0 --- /dev/null +++ b/src/transformers/format/qwerty-shift.js @@ -0,0 +1,40 @@ +// qwerty-shift transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'QWERTY Right Shift', + priority: 40, + rows: [ + 'qwertyuiop', + 'asdfghjkl', + 'zxcvbnm' + ], + buildMap: function() { + if (this._map) return this._map; + const map = {}; + for (const row of this.rows) { + for (let i=0;i m[c] || c).join(''); + }, + preview: function(text) { + if (!text) return '[qwerty]'; + return this.func(text.slice(0,8)) + (text.length>8?'...':''); + }, + reverse: function(text) { + const m = this.buildMap(); + const inv = {}; + Object.keys(m).forEach(k => inv[m[k]] = k); + return [...text].map(c => inv[c] || c).join(''); + } + +}); \ No newline at end of file diff --git a/src/transformers/format/reverse-words.js b/src/transformers/format/reverse-words.js new file mode 100644 index 0000000..f34d6e5 --- /dev/null +++ b/src/transformers/format/reverse-words.js @@ -0,0 +1,23 @@ +// reverse-words transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Reverse Words', + priority: 40, + func: function(text) { + return text.split(/(\s+)/).reverse().join(''); + }, + preview: function(text) { + if (!text) return '[rev words]'; + // Take last 2-3 words and reverse them to show the effect + const words = text.split(/\s+/); + const lastWords = words.slice(-3).join(' '); + return this.func(lastWords) + '...'; + }, + reverse: function(text) { + // Reversing words twice restores + return this.func(text); + } + +}); \ No newline at end of file diff --git a/src/transformers/format/reverse.js b/src/transformers/format/reverse.js new file mode 100644 index 0000000..0ca9646 --- /dev/null +++ b/src/transformers/format/reverse.js @@ -0,0 +1,18 @@ +// reverse transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Reverse Text', + priority: 40, + func: function(text) { + return [...text].reverse().join(''); + }, + preview: function(text) { + return this.func(text); + }, + reverse: function(text) { + return this.func(text); // Reversing is its own inverse + } + +}); \ No newline at end of file diff --git a/src/transformers/loader-node.js b/src/transformers/loader-node.js new file mode 100644 index 0000000..2797781 --- /dev/null +++ b/src/transformers/loader-node.js @@ -0,0 +1,151 @@ +/** + * Node.js Loader for Transforms + * Dynamically discovers and loads all transform modules for Node.js/testing environment + */ + +const fs = require('fs'); +const path = require('path'); +const vm = require('vm'); + +// Load BaseTransformer class once +let BaseTransformerClass = null; +function loadBaseTransformer() { + if (BaseTransformerClass) return BaseTransformerClass; + + const baseTransformerPath = path.join(__dirname, 'BaseTransformer.js'); + const code = fs.readFileSync(baseTransformerPath, 'utf8'); + + const sandbox = { + exports: {}, + module: { exports: {} }, + console: console + }; + + // Remove all export keywords, then add module.exports at the end + const wrappedCode = code + .replace(/export\s+(default\s+)?/g, '') // Remove export default or export + + '\nmodule.exports = BaseTransformer;'; // Export the class + + vm.createContext(sandbox); + vm.runInContext(wrappedCode, sandbox); + + BaseTransformerClass = sandbox.module.exports; + return BaseTransformerClass; +} + +// Load emojiData from emojiData.js +function loadEmojiData() { + try { + const emojiDataPath = path.join(__dirname, '..', '..', 'js', 'emojiData.js'); + const code = fs.readFileSync(emojiDataPath, 'utf8'); + + // Create a temporary window object to capture emojiData + const tempWindow = { emojiData: {} }; + const sandbox = { + window: tempWindow, + console: console + }; + + vm.createContext(sandbox); + vm.runInContext(code, sandbox); + + return tempWindow.emojiData; + } catch (error) { + console.warn('โš ๏ธ Could not load emojiData:', error.message); + return {}; + } +} + +// Create a mock window object with necessary properties +const mockWindow = { + emojiLibrary: { + splitEmojis: function(text) { + // Simple emoji splitting - if Intl.Segmenter is available, use it + if (typeof Intl !== 'undefined' && Intl.Segmenter) { + const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' }); + return Array.from(segmenter.segment(text), ({ segment }) => segment); + } + // Fallback to Array.from for basic splitting + return Array.from(text); + } + }, + emojiData: loadEmojiData() +}; + +// Create sandbox for executing transform modules +function loadTransform(filePath) { + const code = fs.readFileSync(filePath, 'utf8'); + + // Create a sandbox to execute the module + const sandbox = { + exports: {}, + module: { exports: {} }, + console: console, + TextEncoder: TextEncoder, + TextDecoder: TextDecoder, + btoa: (str) => Buffer.from(str, 'binary').toString('base64'), + atob: (str) => Buffer.from(str, 'base64').toString('binary'), + String: String, + parseInt: parseInt, + Math: Math, + Object: Object, + Array: Array, + RegExp: RegExp, + Date: Date, + JSON: JSON, + Intl: Intl, + window: mockWindow, + BaseTransformer: loadBaseTransformer() // Add BaseTransformer to sandbox + }; + + // Convert ES6 export to CommonJS (multiline mode) and remove import statements + const wrappedCode = code + .replace(/import\s+.+from\s+['"'][^'"]+['"]\s*;?\s*\n?/g, '') // Remove imports + .replace(/export\s+default\s*/g, 'module.exports = ') // Handle with or without space + .replace(/export\s+{/g, 'module.exports = {'); + + vm.createContext(sandbox); + vm.runInContext(wrappedCode, sandbox); + + return sandbox.module.exports; +} + +// Load all transforms from all categories +function loadAllTransforms() { + const transforms = {}; + const baseDir = __dirname; + + // Files to skip + const skipFiles = ['BaseTransformer.js', 'index.js', 'loader-node.js', 'README.md']; + + // Dynamically discover all category directories + const categoryDirs = fs.readdirSync(baseDir, { withFileTypes: true }) + .filter(dirent => dirent.isDirectory()) + .map(dirent => dirent.name) + .sort(); + + for (const categoryDir of categoryDirs) { + const categoryPath = path.join(baseDir, categoryDir); + const files = fs.readdirSync(categoryPath) + .filter(file => file.endsWith('.js') && !skipFiles.includes(file)); + + for (const file of files) { + const filePath = path.join(categoryPath, file); + // Convert filename to transform name (kebab-case to snake_case) + // e.g., "upside-down.js" -> "upside_down", "base64.js" -> "base64" + const name = file.replace('.js', '').replace(/-/g, '_'); + + try { + transforms[name] = loadTransform(filePath); + } catch (error) { + console.error(`โŒ Error loading ${categoryDir}/${file}:`, error.message); + } + } + } + + return transforms; +} + +// Export for Node.js +module.exports = loadAllTransforms(); + diff --git a/src/transformers/special/randomizer.js b/src/transformers/special/randomizer.js new file mode 100644 index 0000000..b798f57 --- /dev/null +++ b/src/transformers/special/randomizer.js @@ -0,0 +1,146 @@ +// randomizer transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Random Mix', + priority: 20, + // Get a list of transforms suitable for randomization + getRandomizableTransforms() { + 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', + 'hieroglyphics', 'ogham', 'mathematical', 'cursive', 'medieval', + 'monospace', 'greek', 'braille', 'alternating_case', 'reverse_words', + 'title_case', 'sentence_case', 'camel_case', 'snake_case', 'kebab_case', 'random_case', + 'regional_indicator', 'fraktur', 'cyrillic_stylized', 'katakana', 'hiragana', 'emoji_speak', + 'base58', 'base62', 'roman_numerals', 'vigenere', 'rail_fence', 'base64url' + ]; + return suitable.filter(name => window.transforms[name]); + }, + + // Apply random transforms to each word in a sentence + func: function(text, options = {}) { + if (!text) return ''; + + const { + preservePunctuation = true, + minTransforms = 2, + maxTransforms = 5, + allowRepeats = false + } = options; + + // Split text into words while preserving punctuation + const words = this.smartWordSplit(text); + const availableTransforms = this.getRandomizableTransforms(); + + if (availableTransforms.length === 0) return text; + + // Select random transforms to use + const numTransforms = Math.min( + Math.max(minTransforms, Math.floor(Math.random() * maxTransforms) + 1), + availableTransforms.length + ); + + const selectedTransforms = []; + const usedTransforms = new Set(); + + for (let i = 0; i < numTransforms; i++) { + let transform; + do { + transform = availableTransforms[Math.floor(Math.random() * availableTransforms.length)]; + } while (!allowRepeats && usedTransforms.has(transform) && usedTransforms.size < availableTransforms.length); + + selectedTransforms.push(transform); + usedTransforms.add(transform); + } + + // Apply random transforms to words + const transformedWords = words.map(wordObj => { + if (wordObj.isWord) { + const randomTransform = selectedTransforms[Math.floor(Math.random() * selectedTransforms.length)]; + const transform = window.transforms[randomTransform]; + + try { + const transformed = transform.func(wordObj.text); + return { + ...wordObj, + text: transformed, + transform: transform.name, + originalTransform: randomTransform + }; + } catch (e) { + console.error(`Error applying ${randomTransform} to "${wordObj.text}":`, e); + return wordObj; + } + } else { + return wordObj; // Keep punctuation/spaces as-is + } + }); + + // Reconstruct the text + const result = transformedWords.map(w => w.text).join(''); + + // Store transform mapping for decoding + this.lastTransformMap = transformedWords + .filter(w => w.isWord && w.originalTransform) + .map(w => ({ + original: w.text, + transform: w.originalTransform, + transformName: w.transform + })); + + return result; + }, + + // Smart word splitting that preserves punctuation + smartWordSplit: function(text) { + const words = []; + let currentWord = ''; + let isInWord = false; + + for (let i = 0; i < text.length; i++) { + const char = text[i]; + const isWordChar = /[a-zA-Z0-9]/.test(char); + + if (isWordChar) { + if (!isInWord && currentWord) { + // We were in punctuation/space, now starting a word + words.push({ text: currentWord, isWord: false }); + currentWord = ''; + } + currentWord += char; + isInWord = true; + } else { + if (isInWord && currentWord) { + // We were in a word, now in punctuation/space + words.push({ text: currentWord, isWord: true }); + currentWord = ''; + } + currentWord += char; + isInWord = false; + } + } + + // Add the last segment + if (currentWord) { + words.push({ text: currentWord, isWord: isInWord }); + } + + return words; + }, + + preview: function(text) { + return '[mixed transforms]'; + }, + + // Note: No reverse function - this transform is non-reversible + // because different random transforms are applied to different words + + // Get info about the last randomization + getLastTransformInfo: function() { + return this.lastTransformMap || []; + } + +}); \ No newline at end of file diff --git a/src/transformers/technical/a1z26.js b/src/transformers/technical/a1z26.js new file mode 100644 index 0000000..23c3f2d --- /dev/null +++ b/src/transformers/technical/a1z26.js @@ -0,0 +1,53 @@ +// a1z26 transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + name: 'A1Z26', + priority: 275, + // Detector: Check for A1Z26 pattern (numbers 1-26 separated by hyphens, words by spaces) + detector: function(text) { + const cleaned = text.trim(); + if (cleaned.length < 3) return false; + + // Must contain only digits, hyphens, and spaces + if (!/^[0-9\-\s]+$/.test(cleaned)) return false; + + // Check if numbers are in valid A1Z26 range (1-26) + const numbers = cleaned.split(/[-\s]+/).filter(n => n.length > 0); + if (numbers.length === 0) return false; + + // At least 50% of numbers should be in 1-26 range (allows some flexibility) + const validCount = numbers.filter(n => { + const num = parseInt(n, 10); + return !isNaN(num) && num >= 1 && num <= 26; + }).length; + + return validCount / numbers.length >= 0.5; + }, + + func: function(text) { + // Encode letters as numbers with hyphens, strip everything else (standard A1Z26) + const letters = text.replace(/[^A-Za-z]/g, ''); + if (!letters) return ''; + return letters.split('').map(c => { + const n = (c.toUpperCase().charCodeAt(0) - 64); + return String(n); + }).join('-'); + }, + preview: function(text) { + if (!text) return '[1-26]'; + const full = this.func(text); + return full.substring(0, 20) + (full.length > 20 ? '...' : ''); + }, + reverse: function(text) { + // Decode numbers back to letters (standard A1Z26: strips spaces) + return text.split(/[-\s,.\|\/]+/).filter(tok => tok).map(tok => { + const n = parseInt(tok, 10); + if (n >= 1 && n <= 26) { + return String.fromCharCode(64 + n).toLowerCase(); + } + return ''; + }).join(''); + } + +}); \ No newline at end of file diff --git a/src/transformers/technical/braille.js b/src/transformers/technical/braille.js new file mode 100644 index 0000000..628cef4 --- /dev/null +++ b/src/transformers/technical/braille.js @@ -0,0 +1,56 @@ +// braille transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + name: 'Braille', + priority: 300, + // Detector: Must contain Braille characters (allows other chars too since braille doesn't encode everything) + detector: function(text) { + const cleaned = text.trim(); + // Must contain at least 2 braille characters + const brailleCount = (cleaned.match(/[โ €-โฃฟ]/g) || []).length; + return brailleCount >= 2; + }, + + map: { + 'a': 'โ ', 'b': 'โ ƒ', 'c': 'โ ‰', 'd': 'โ ™', 'e': 'โ ‘', 'f': 'โ ‹', 'g': 'โ ›', 'h': 'โ “', 'i': 'โ Š', + 'j': 'โ š', 'k': 'โ …', 'l': 'โ ‡', 'm': 'โ ', 'n': 'โ ', 'o': 'โ •', 'p': 'โ ', 'q': 'โ Ÿ', 'r': 'โ —', + 's': 'โ Ž', 't': 'โ ž', 'u': 'โ ฅ', 'v': 'โ ง', 'w': 'โ บ', 'x': 'โ ญ', 'y': 'โ ฝ', 'z': 'โ ต', + '0': 'โ ผโ š', '1': 'โ ผโ ', '2': 'โ ผโ ƒ', '3': 'โ ผโ ‰', '4': 'โ ผโ ™', '5': 'โ ผโ ‘', + '6': 'โ ผโ ‹', '7': 'โ ผโ ›', '8': 'โ ผโ “', '9': 'โ ผโ Š' + }, + func: function(text) { + return [...text.toLowerCase()].map(c => this.map[c] || c).join(''); + }, + reverse: function(text) { + // Build reverse map + const revMap = {}; + for (const [key, value] of Object.entries(this.map)) { + revMap[value] = key; + } + + // Decode character by character + // Handle multi-character sequences (numbers use โ ผ prefix) + let result = ''; + let i = 0; + while (i < text.length) { + // Check for number indicator (โ ผ) + if (text[i] === 'โ ผ' && i + 1 < text.length) { + const twoChar = text[i] + text[i + 1]; + if (revMap[twoChar]) { + result += revMap[twoChar]; + i += 2; + continue; + } + } + + // Single character lookup + const char = text[i]; + result += revMap[char] || char; + i++; + } + + return result; + } + +}); \ No newline at end of file diff --git a/src/transformers/technical/brainfuck.js b/src/transformers/technical/brainfuck.js new file mode 100644 index 0000000..70d4368 --- /dev/null +++ b/src/transformers/technical/brainfuck.js @@ -0,0 +1,88 @@ +// brainfuck transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + name: 'Brainfuck', + priority: 300, + // Detector: Only Brainfuck commands (8 characters) + detector: function(text) { + const cleaned = text.trim(); + return cleaned.length >= 10 && /^[><+\-.,\[\]\s]+$/.test(cleaned); + }, + + // Simple character to Brainfuck encoding + encode: function(char) { + const code = char.charCodeAt(0); + return '+'.repeat(code) + '.'; + }, + func: function(text) { + // Convert each character to Brainfuck + // Use >[-] to move to next cell and clear it (stay on the new cell) + return [...text].map(c => this.encode(c)).join('>[-]'); + }, + preview: function(text) { + return '[brainfuck]'; + }, + // Brainfuck interpreter for decoding + reverse: function(code) { + const cells = new Array(30000).fill(0); + let pointer = 0; + let output = ''; + let codePointer = 0; + let iterations = 0; + const maxIterations = 100000; // Prevent infinite loops + + while (codePointer < code.length && iterations < maxIterations) { + iterations++; + const instruction = code[codePointer]; + + switch (instruction) { + case '>': + pointer++; + if (pointer >= cells.length) pointer = 0; + break; + case '<': + pointer--; + if (pointer < 0) pointer = cells.length - 1; + break; + case '+': + cells[pointer] = (cells[pointer] + 1) % 256; + break; + case '-': + cells[pointer] = (cells[pointer] - 1 + 256) % 256; + break; + case '.': + output += String.fromCharCode(cells[pointer]); + break; + case '[': + if (cells[pointer] === 0) { + let depth = 1; + while (depth > 0) { + codePointer++; + if (code[codePointer] === '[') depth++; + if (code[codePointer] === ']') depth--; + } + } + break; + case ']': + if (cells[pointer] !== 0) { + let depth = 1; + while (depth > 0) { + codePointer--; + if (code[codePointer] === ']') depth++; + if (code[codePointer] === '[') depth--; + } + } + break; + case ',': + // Input not supported in web context + cells[pointer] = 0; + break; + } + codePointer++; + } + + return output || null; + } + +}); \ No newline at end of file diff --git a/src/transformers/technical/morse.js b/src/transformers/technical/morse.js new file mode 100644 index 0000000..2d594ee --- /dev/null +++ b/src/transformers/technical/morse.js @@ -0,0 +1,61 @@ +// morse transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + name: 'Morse Code', + priority: 300, + // Detector: Only dots, dashes, slashes, and spaces + detector: function(text) { + const cleaned = text.trim(); + return cleaned.length >= 5 && /^[\.\-\/\s]+$/.test(cleaned); + }, + + map: { + // Letters + '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': '--..', + // Numbers + '0': '-----', '1': '.----', '2': '..---', '3': '...--', '4': '....-', + '5': '.....', '6': '-....', '7': '--...', '8': '---..', '9': '----.', + // Punctuation + '.': '.-.-.-', ',': '--..--', '?': '..--..', "'": '.----.', '!': '-.-.--', + '/': '-..-.', '(': '-.--.', ')': '-.--.-', '&': '.-...', ':': '---...', + ';': '-.-.-.', '=': '-...-', '+': '.-.-.', '-': '-....-', '_': '..--.-', + '"': '.-..-.', '$': '...-..-', '@': '.--.-.' + }, + // Create reverse map for decoding + reverseMap: function() { + const revMap = {}; + for (const [key, value] of Object.entries(this.map)) { + revMap[value] = key; + } + return revMap; + }, + func: function(text, decode = false) { + if (decode) { + // Decode mode + const revMap = this.reverseMap(); + // Split by word separator (/ or multiple spaces) and then by character separator (single space) + return text.split(/\s*\/\s*|\s{3,}/).map(word => + word.split(/\s+/).map(code => revMap[code] || '').join('') + ).join(' '); + } else { + // Encode mode - handle word boundaries with / + return text.split(/\s+/).map(word => + [...word.toLowerCase()].map(c => this.map[c] || '').filter(x => x).join(' ') + ).join(' / '); + } + }, + preview: function(text) { + if (!text) return '[base32]'; + const result = this.func(text.slice(0, 2)); + return result + '...'; + }, + reverse: function(text) { + return this.func(text, true); + } + +}); \ No newline at end of file diff --git a/src/transformers/technical/nato.js b/src/transformers/technical/nato.js new file mode 100644 index 0000000..9b3adb5 --- /dev/null +++ b/src/transformers/technical/nato.js @@ -0,0 +1,44 @@ +// nato transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'NATO Phonetic', + priority: 300, + map: { + 'a': 'Alpha', 'b': 'Bravo', 'c': 'Charlie', 'd': 'Delta', 'e': 'Echo', + 'f': 'Foxtrot', 'g': 'Golf', 'h': 'Hotel', 'i': 'India', 'j': 'Juliett', + 'k': 'Kilo', 'l': 'Lima', 'm': 'Mike', 'n': 'November', 'o': 'Oscar', + 'p': 'Papa', 'q': 'Quebec', 'r': 'Romeo', 's': 'Sierra', 't': 'Tango', + 'u': 'Uniform', 'v': 'Victor', 'w': 'Whiskey', 'x': 'X-ray', 'y': 'Yankee', 'z': 'Zulu', + '0': 'Zero', '1': 'One', '2': 'Two', '3': 'Three', '4': 'Four', + '5': 'Five', '6': 'Six', '7': 'Seven', '8': 'Eight', '9': 'Nine' + }, + func: function(text) { + // Use | to mark word boundaries + return [...text.toLowerCase()].map(c => { + if (c === ' ') return '|'; + return this.map[c] || c; + }).join(' '); + }, + preview: function(text) { + if (!text) return '[quenya]'; + return this.func(text.slice(0, 3)) + '...'; + }, + // Create reverse map for decoding + reverseMap: function() { + const revMap = {}; + for (const [key, value] of Object.entries(this.map)) { + revMap[value.toLowerCase()] = key; + } + return revMap; + }, + reverse: function(text) { + const revMap = this.reverseMap(); + return text.split(/\s+/).map(word => { + if (word === '|') return ' '; + return revMap[word.toLowerCase()] || word; + }).join(''); + } + +}); \ No newline at end of file diff --git a/src/transformers/technical/semaphore.js b/src/transformers/technical/semaphore.js new file mode 100644 index 0000000..1d9b127 --- /dev/null +++ b/src/transformers/technical/semaphore.js @@ -0,0 +1,54 @@ +// semaphore transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + name: 'Semaphore Flags', + priority: 310, + // Detector: Only uses 8 specific arrow emojis (most exclusive character set) + detector: function(text) { + const cleaned = text.trim(); + return cleaned.length >= 2 && /^[โฌ†๏ธโ†—๏ธโžก๏ธโ†˜๏ธโฌ‡๏ธโ†™๏ธโฌ…๏ธโ†–๏ธโฌ†โ†—โžกโ†˜โฌ‡โ†™โฌ…โ†–\s\/]+$/u.test(cleaned); + }, + + // Positions 1..8 around the clock: 1=โฌ†๏ธ 2=โ†—๏ธ 3=โžก๏ธ 4=โ†˜๏ธ 5=โฌ‡๏ธ 6=โ†™๏ธ 7=โฌ…๏ธ 8=โ†–๏ธ + arrows: ['','โฌ†๏ธ','โ†—๏ธ','โžก๏ธ','โ†˜๏ธ','โฌ‡๏ธ','โ†™๏ธ','โฌ…๏ธ','โ†–๏ธ'], + // Standard semaphore mapping (J is special: 2-1) + table: { + 'A':[1,2],'B':[1,3],'C':[1,4],'D':[1,5],'E':[1,6],'F':[1,7],'G':[1,8], + 'H':[2,3],'I':[2,4],'J':[2,1], + 'K':[2,5],'L':[2,6],'M':[2,7],'N':[2,8], + 'O':[3,4],'P':[3,5],'Q':[3,6],'R':[3,7],'S':[3,8], + 'T':[4,5],'U':[4,6],'V':[4,7],'W':[4,8], + 'X':[5,6],'Y':[5,7],'Z':[5,8] + }, + encodePair: function(pair) { return this.arrows[pair[0]] + this.arrows[pair[1]]; }, + buildReverse: function() { + if (this._rev) return this._rev; + const rev = {}; + for (const [k,v] of Object.entries(this.table)) { + rev[this.encodePair(v)] = k; + } + this._rev = rev; return rev; + }, + func: function(text) { + return [...text].map(ch => { + if (/\s/.test(ch)) return '/'; + const up = ch.toUpperCase(); + const pair = this.table[up]; + return pair ? this.encodePair(pair) : ch; + }).join(' '); + }, + preview: function(text) { + return this.func((text || 'flag').slice(0, 4)); + }, + reverse: function(text) { + const rev = this.buildReverse(); + const tokens = text.trim().split(/\s+/); + return tokens.map(tok => { + if (tok === '/') return ' '; + // Some platforms add variation selectors; normalize by direct match first + return rev[tok] || tok; + }).join(''); + } + +}); \ No newline at end of file diff --git a/src/transformers/technical/tap-code.js b/src/transformers/technical/tap-code.js new file mode 100644 index 0000000..f65728c --- /dev/null +++ b/src/transformers/technical/tap-code.js @@ -0,0 +1,68 @@ +// tap-code transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + name: 'Tap Code', + priority: 300, + // Detector: Must contain mostly dots, spaces, and slashes (allow other chars like emojis/numbers) + detector: function(text) { + const cleaned = text.trim(); + if (cleaned.length < 3) return false; + // Count tap code characters (dots, spaces, slashes) + const tapChars = (cleaned.match(/[\.\s\/]/g) || []).length; + // Must be at least 70% tap code characters + return tapChars / cleaned.length > 0.7; + }, + + letters: 'ABCDEFGHIKLMNOPQRSTUVWXYZ', // no J (traditionally K merges with C or J omitted; use no J) + buildMap: function() { + if (this._map) return this._map; + const map = {}; const rev = {}; + for (let i=0;i I + const [r,c] = this._map['I']; out.push('.'.repeat(r)+'.'+'.'.repeat(c)); continue; + } + const coords = this._map[ch]; + if (coords) { + out.push('.'.repeat(coords[0]) + ' ' + '.'.repeat(coords[1])); + } else if (/\s/.test(ch)) { + out.push('/'); + } else { + out.push(ch); + } + } + return out.join(' '); + }, + preview: function(text) { + return this.func((text || 'tap').slice(0,3)); + }, + reverse: function(text) { + this.buildMap(); + const toks = text.trim().split(/\s+/); + const out = []; + for (let i=0;i this.map[c] || c).join(''); + }, + // Detector: Check for bubble (enclosed alphanumerics) characters + detector: function(text) { + // Enclosed alphanumerics (U+24B6-U+24EA for circled letters) + return /[โ“-โ“ฉโ’ถ-โ“]/.test(text); + } + +}); \ No newline at end of file diff --git a/src/transformers/unicode/chemical.js b/src/transformers/unicode/chemical.js new file mode 100644 index 0000000..18e20c4 --- /dev/null +++ b/src/transformers/unicode/chemical.js @@ -0,0 +1,71 @@ +// chemical transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Chemical Symbols', + priority: 70, + map: { + 'a': 'Ac', 'b': 'B', 'c': 'C', 'd': 'D', 'e': 'Es', 'f': 'F', 'g': 'Ge', 'h': 'H', 'i': 'I', + 'j': 'J', 'k': 'K', 'l': 'L', 'm': 'Mn', 'n': 'N', 'o': 'O', 'p': 'P', 'q': 'Q', 'r': 'R', + 's': 'S', 't': 'Ti', 'u': 'U', 'v': 'V', 'w': 'W', 'x': 'Xe', 'y': 'Y', 'z': 'Zn', + 'A': 'AC', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'ES', 'F': 'F', 'G': 'GE', 'H': 'H', 'I': 'I', + 'J': 'J', 'K': 'K', 'L': 'L', 'M': 'MN', 'N': 'N', 'O': 'O', 'P': 'P', 'Q': 'Q', 'R': 'R', + 'S': 'S', 'T': 'TI', 'U': 'U', 'V': 'V', 'W': 'W', 'X': 'XE', 'Y': 'Y', 'Z': 'ZN' + }, + func: function(text) { + return [...text.toLowerCase()].map(c => this.map[c] || c).join(''); + }, + reverse: function(text) { + // Build reverse map using only lowercase keys (since func() lowercases before encoding) + const revMap = {}; + for (const [key, value] of Object.entries(this.map)) { + // Only use lowercase letter mappings for reverse + if (key >= 'a' && key <= 'z') { + revMap[value] = key; + } + } + + // Parse the text, trying 2-character symbols first, then 1-character + let result = ''; + let i = 0; + while (i < text.length) { + // Try 2-character symbol first + if (i + 1 < text.length) { + const twoChar = text.substring(i, i + 2); + if (revMap[twoChar]) { + result += revMap[twoChar]; + i += 2; + continue; + } + } + + // Try 1-character symbol + const oneChar = text[i]; + if (revMap[oneChar]) { + result += revMap[oneChar]; + } else { + result += oneChar; // Keep non-mapped characters + } + i++; + } + return result; + }, + // Detector: Check for chemical element symbols pattern + detector: function(text) { + const cleaned = text.trim(); + if (cleaned.length < 3) return false; + + // Extract only the letter sequences (ignoring spaces, punctuation, emojis, etc.) + const letterParts = cleaned.match(/[A-Za-z]+/g); + if (!letterParts || letterParts.length === 0) return false; + + // Check if the letters follow chemical symbol patterns + const chemicalPattern = /^(Ac|B|C|D|Es|F|Ge|H|I|J|K|L|Mn|N|O|P|Q|R|S|Ti|U|V|W|Xe|Y|Zn|AC|ES|GE|MN|TI|XE|ZN)+$/; + + // At least 70% of letter parts should match the chemical pattern + const matchingParts = letterParts.filter(part => chemicalPattern.test(part)); + return matchingParts.length >= letterParts.length * 0.7; + } + +}); \ No newline at end of file diff --git a/src/transformers/unicode/cursive.js b/src/transformers/unicode/cursive.js new file mode 100644 index 0000000..0c1ff7e --- /dev/null +++ b/src/transformers/unicode/cursive.js @@ -0,0 +1,23 @@ +// cursive transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + name: 'Cursive', + priority: 85, + map: { + 'a': '๐“ช', 'b': '๐“ซ', 'c': '๐“ฌ', 'd': '๐“ญ', 'e': '๐“ฎ', 'f': '๐“ฏ', 'g': '๐“ฐ', 'h': '๐“ฑ', 'i': '๐“ฒ', + 'j': '๐“ณ', 'k': '๐“ด', 'l': '๐“ต', 'm': '๐“ถ', 'n': '๐“ท', 'o': '๐“ธ', 'p': '๐“น', 'q': '๐“บ', 'r': '๐“ป', + 's': '๐“ผ', 't': '๐“ฝ', 'u': '๐“พ', 'v': '๐“ฟ', 'w': '๐”€', 'x': '๐”', 'y': '๐”‚', 'z': '๐”ƒ', + 'A': '๐“', 'B': '๐“‘', 'C': '๐“’', 'D': '๐““', 'E': '๐“”', 'F': '๐“•', 'G': '๐“–', 'H': '๐“—', 'I': '๐“˜', + 'J': '๐“™', 'K': '๐“š', 'L': '๐“›', 'M': '๐“œ', 'N': '๐“', 'O': '๐“ž', 'P': '๐“Ÿ', 'Q': '๐“ ', 'R': '๐“ก', + 'S': '๐“ข', 'T': '๐“ฃ', 'U': '๐“ค', 'V': '๐“ฅ', 'W': '๐“ฆ', 'X': '๐“ง', 'Y': '๐“จ', 'Z': '๐“ฉ' + }, + func: function(text) { + return [...text].map(c => this.map[c] || c).join(''); + }, + // Detector: Check for cursive/bold cursive Unicode characters + detector: function(text) { + // Bold cursive mathematical characters (check for presence) + return /[๐“ช๐“ซ๐“ฌ๐“ญ๐“ฎ๐“ฏ๐“ฐ๐“ฑ๐“ฒ๐“ณ๐“ด๐“ต๐“ถ๐“ท๐“ธ๐“น๐“บ๐“ป๐“ผ๐“ฝ๐“พ๐“ฟ๐”€๐”๐”‚๐”ƒ๐“๐“‘๐“’๐““๐“”๐“•๐“–๐“—๐“˜๐“™๐“š๐“›๐“œ๐“๐“ž๐“Ÿ๐“ ๐“ก๐“ข๐“ฃ๐“ค๐“ฅ๐“ฆ๐“ง๐“จ๐“ฉ]/u.test(text); + } +}); diff --git a/src/transformers/unicode/cyrillic-stylized.js b/src/transformers/unicode/cyrillic-stylized.js new file mode 100644 index 0000000..d558eeb --- /dev/null +++ b/src/transformers/unicode/cyrillic-stylized.js @@ -0,0 +1,25 @@ +// cyrillic-stylized transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Cyrillic Stylized', + priority: 100, + map: { + 'A':'ะ','B':'ะ’','C':'ะก','E':'ะ•','H':'ะ','K':'ะš','M':'ะœ','O':'ะž','P':'ะ ','T':'ะข','X':'ะฅ','Y':'ะฃ', + 'a':'ะฐ','e':'ะต','o':'ะพ','p':'ั€','c':'ั','y':'ัƒ','x':'ั…','k':'ะบ','h':'าป','m':'ะผ','t':'ั‚','b':'ะฌ' + }, + func: function(text) { + return [...text].map(c => this.map[c] || c).join(''); + }, + preview: function(text) { + if (!text) return '[cyrillic]'; + return this.func(text.slice(0, 8)) + (text.length > 8 ? '...' : ''); + }, + reverse: function(text) { + const rev = {}; + for (const [k,v] of Object.entries(this.map)) rev[v] = k; + return [...text].map(c => rev[c] || c).join(''); + } + +}); \ No newline at end of file diff --git a/src/transformers/unicode/doubleStruck.js b/src/transformers/unicode/doubleStruck.js new file mode 100644 index 0000000..a87885d --- /dev/null +++ b/src/transformers/unicode/doubleStruck.js @@ -0,0 +1,24 @@ +// doubleStruck transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + name: 'Double-Struck', + priority: 85, + map: { + 'a': '๐•’', 'b': '๐•“', 'c': '๐•”', 'd': '๐••', 'e': '๐•–', 'f': '๐•—', 'g': '๐•˜', 'h': '๐•™', 'i': '๐•š', + 'j': '๐•›', 'k': '๐•œ', 'l': '๐•', 'm': '๐•ž', 'n': '๐•Ÿ', 'o': '๐• ', 'p': '๐•ก', 'q': '๐•ข', 'r': '๐•ฃ', + 's': '๐•ค', 't': '๐•ฅ', 'u': '๐•ฆ', 'v': '๐•ง', 'w': '๐•จ', 'x': '๐•ฉ', 'y': '๐•ช', 'z': '๐•ซ', + 'A': '๐”ธ', 'B': '๐”น', 'C': 'โ„‚', 'D': '๐”ป', 'E': '๐”ผ', 'F': '๐”ฝ', 'G': '๐”พ', 'H': 'โ„', 'I': '๐•€', + 'J': '๐•', 'K': '๐•‚', 'L': '๐•ƒ', 'M': '๐•„', 'N': 'โ„•', 'O': '๐•†', 'P': 'โ„™', 'Q': 'โ„š', 'R': 'โ„', + 'S': '๐•Š', 'T': '๐•‹', 'U': '๐•Œ', 'V': '๐•', 'W': '๐•Ž', 'X': '๐•', 'Y': '๐•', 'Z': 'โ„ค', + '0': '๐Ÿ˜', '1': '๐Ÿ™', '2': '๐Ÿš', '3': '๐Ÿ›', '4': '๐Ÿœ', '5': '๐Ÿ', '6': '๐Ÿž', '7': '๐ŸŸ', '8': '๐Ÿ ', '9': '๐Ÿก' + }, + func: function(text) { + return [...text].map(c => this.map[c] || c).join(''); + }, + // Detector: Check for double-struck Unicode characters + detector: function(text) { + // Double-struck (blackboard bold) characters + return /[๐•’๐•“๐•”๐••๐•–๐•—๐•˜๐•™๐•š๐•›๐•œ๐•๐•ž๐•Ÿ๐• ๐•ก๐•ข๐•ฃ๐•ค๐•ฅ๐•ฆ๐•ง๐•จ๐•ฉ๐•ช๐•ซ๐”ธ๐”น๐”ป๐”ผ๐”ฝ๐”พ๐•€๐•๐•‚๐•ƒ๐•„๐•†๐•Š๐•‹๐•Œ๐•๐•Ž๐•๐•โ„‚โ„โ„•โ„™โ„šโ„โ„ค๐Ÿ˜๐Ÿ™๐Ÿš๐Ÿ›๐Ÿœ๐Ÿ๐Ÿž๐ŸŸ๐Ÿ ๐Ÿก]/.test(text); + } +}); diff --git a/src/transformers/unicode/fraktur.js b/src/transformers/unicode/fraktur.js new file mode 100644 index 0000000..a25cd34 --- /dev/null +++ b/src/transformers/unicode/fraktur.js @@ -0,0 +1,48 @@ +// fraktur transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Fraktur', + priority: 85, + func: function(text) { + const capMap = { + 'A': 0x1D504, 'B': 0x1D505, 'C': 0x212D, 'D': 0x1D507, 'E': 0x1D508, 'F': 0x1D509, 'G': 0x1D50A, + 'H': 0x210C, 'I': 0x2111, 'J': 0x1D50D, 'K': 0x1D50E, 'L': 0x1D50F, 'M': 0x1D510, 'N': 0x1D511, + 'O': 0x1D512, 'P': 0x1D513, 'Q': 0x1D514, 'R': 0x211C, 'S': 0x1D516, 'T': 0x1D517, 'U': 0x1D518, + 'V': 0x1D519, 'W': 0x1D51A, 'X': 0x1D51B, 'Y': 0x1D51C, 'Z': 0x2128 + }; + const lowerBase = 0x1D51E; // 'a' + return [...text].map(c => { + const code = c.charCodeAt(0); + if (c >= 'A' && c <= 'Z') { + const fr = capMap[c]; + return fr ? String.fromCodePoint(fr) : c; + } + if (c >= 'a' && c <= 'z') { + return String.fromCodePoint(lowerBase + (code - 97)); + } + return c; + }).join(''); + }, + preview: function(text) { + if (!text) return '[fraktur]'; + return this.func(text.slice(0, 6)) + (text.length > 6 ? '...' : ''); + }, + reverse: function(text) { + const capMap = { + 0x1D504:'A',0x1D505:'B',0x212D:'C',0x1D507:'D',0x1D508:'E',0x1D509:'F',0x1D50A:'G', + 0x210C:'H',0x2111:'I',0x1D50D:'J',0x1D50E:'K',0x1D50F:'L',0x1D510:'M',0x1D511:'N', + 0x1D512:'O',0x1D513:'P',0x1D514:'Q',0x211C:'R',0x1D516:'S',0x1D517:'T',0x1D518:'U', + 0x1D519:'V',0x1D51A:'W',0x1D51B:'X',0x1D51C:'Y',0x2128:'Z' + }; + const lowerBase = 0x1D51E; + return Array.from(text).map(ch => { + const cp = ch.codePointAt(0); + if (cp in capMap) return capMap[cp]; + if (cp >= lowerBase && cp < lowerBase + 26) return String.fromCharCode(97 + (cp - lowerBase)); + return ch; + }).join(''); + } + +}); \ No newline at end of file diff --git a/src/transformers/unicode/fullwidth.js b/src/transformers/unicode/fullwidth.js new file mode 100644 index 0000000..6281be2 --- /dev/null +++ b/src/transformers/unicode/fullwidth.js @@ -0,0 +1,39 @@ +// fullwidth transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Full Width', + priority: 85, + func: function(text) { + return [...text].map(c => { + const code = c.charCodeAt(0); + // Convert ASCII to full-width equivalents + if (code >= 33 && code <= 126) { + return String.fromCharCode(code + 0xFEE0); + } else if (code === 32) { // Space + return 'ใ€€'; // Full-width space + } else { + return c; + } + }).join(''); + }, + preview: function(text) { + if (!text) return '[tengwar]'; + return this.func(text.slice(0, 3)) + '...'; + }, + reverse: function(text) { + return [...text].map(c => { + const code = c.charCodeAt(0); + // Convert full-width back to ASCII + if (code >= 0xFF01 && code <= 0xFF5E) { + return String.fromCharCode(code - 0xFEE0); + } else if (code === 0x3000) { // Full-width space + return ' '; // ASCII space + } else { + return c; + } + }).join(''); + } + +}); \ No newline at end of file diff --git a/src/transformers/unicode/greek.js b/src/transformers/unicode/greek.js new file mode 100644 index 0000000..a5bc9ec --- /dev/null +++ b/src/transformers/unicode/greek.js @@ -0,0 +1,44 @@ +// greek transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + name: 'Greek Letters', + priority: 100, + // Detector: Look for Greek alphabet characters + detector: function(text) { + // Check if text contains Greek letters (ฮฑ-ฯ‰, ฮ‘-ฮฉ range) + return /[ฮฑ-ฯ‰ฮ‘-ฮฉฯฯ‘ฮพ]/u.test(text); + }, + map: { + // Fixed ambiguous mappings: cโ†’ฮพ (was ฯ‡), vโ†’ฯ (was ฯ‚), xโ†’ฯ‡ stays + 'a': 'ฮฑ', 'b': 'ฮฒ', 'c': 'ฮพ', 'd': 'ฮด', 'e': 'ฮต', 'f': 'ฯ†', 'g': 'ฮณ', 'h': 'ฮท', + 'i': 'ฮน', 'j': 'ฯ‘', 'k': 'ฮบ', 'l': 'ฮป', 'm': 'ฮผ', 'n': 'ฮฝ', 'o': 'ฮฟ', 'p': 'ฯ€', + 'q': 'ฮธ', 'r': 'ฯ', 's': 'ฯƒ', 't': 'ฯ„', 'u': 'ฯ…', 'v': 'ฯ', 'w': 'ฯ‰', 'x': 'ฯ‡', + 'y': 'ฯˆ', 'z': 'ฮถ', + 'A': 'ฮ‘', 'B': 'ฮ’', 'C': 'ฮž', 'D': 'ฮ”', 'E': 'ฮ•', 'F': 'ฮฆ', 'G': 'ฮ“', 'H': 'ฮ—', + 'I': 'ฮ™', 'J': 'ฮ˜', 'K': 'ฮš', 'L': 'ฮ›', 'M': 'ฮœ', 'N': 'ฮ', 'O': 'ฮŸ', 'P': 'ฮ ', + 'Q': 'ฮ˜', 'R': 'ฮก', 'S': 'ฮฃ', 'T': 'ฮค', 'U': 'ฮฅ', 'V': 'ฯ‚', 'W': 'ฮฉ', 'X': 'ฮง', + 'Y': 'ฮจ', 'Z': 'ฮ–' + }, + func: function(text) { + return text.split('').map(char => this.map[char] || char).join(''); + }, + preview: function(text) { + if (!text) return '[greek]'; + return this.func(text.slice(0, 10)); + }, + reverseMap: function() { + if (!this._reverseMap) { + this._reverseMap = {}; + for (let key in this.map) { + this._reverseMap[this.map[key]] = key; + } + } + return this._reverseMap; + }, + reverse: function(text) { + const revMap = this.reverseMap(); + return text.split('').map(char => revMap[char] || char).join(''); + } + +}); \ No newline at end of file diff --git a/src/transformers/unicode/hiragana.js b/src/transformers/unicode/hiragana.js new file mode 100644 index 0000000..e2da59a --- /dev/null +++ b/src/transformers/unicode/hiragana.js @@ -0,0 +1,70 @@ +// hiragana transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Hiragana', + priority: 100, + table: [ + ['kyo','ใใ‚‡'],['kyu','ใใ‚…'],['kya','ใใ‚ƒ'], + ['sho','ใ—ใ‚‡'],['shu','ใ—ใ‚…'],['sha','ใ—ใ‚ƒ'],['shi','ใ—'], + ['cho','ใกใ‚‡'],['chu','ใกใ‚…'],['cha','ใกใ‚ƒ'],['chi','ใก'], + ['tsu','ใค'],['fu','ใต'], + ['ryo','ใ‚Šใ‚‡'],['ryu','ใ‚Šใ‚…'],['rya','ใ‚Šใ‚ƒ'], + ['nyo','ใซใ‚‡'],['nyu','ใซใ‚…'],['nya','ใซใ‚ƒ'], + ['gya','ใŽใ‚ƒ'],['gyu','ใŽใ‚…'],['gyo','ใŽใ‚‡'], + ['hya','ใฒใ‚ƒ'],['hyu','ใฒใ‚…'],['hyo','ใฒใ‚‡'], + ['mya','ใฟใ‚ƒ'],['myu','ใฟใ‚…'],['myo','ใฟใ‚‡'], + ['pya','ใดใ‚ƒ'],['pyu','ใดใ‚…'],['pyo','ใดใ‚‡'], + ['bya','ใณใ‚ƒ'],['byu','ใณใ‚…'],['byo','ใณใ‚‡'], + ['ja','ใ˜ใ‚ƒ'],['ju','ใ˜ใ‚…'],['jo','ใ˜ใ‚‡'], + ['ka','ใ‹'],['ki','ใ'],['ku','ใ'],['ke','ใ‘'],['ko','ใ“'], + ['ga','ใŒ'],['gi','ใŽ'],['gu','ใ'],['ge','ใ’'],['go','ใ”'], + ['sa','ใ•'],['su','ใ™'],['se','ใ›'],['so','ใ'], + ['za','ใ–'],['zu','ใš'],['ze','ใœ'],['zo','ใž'], + ['ta','ใŸ'],['te','ใฆ'],['to','ใจ'], + ['da','ใ '],['de','ใง'],['do','ใฉ'], + ['na','ใช'],['ni','ใซ'],['nu','ใฌ'],['ne','ใญ'],['no','ใฎ'], + ['ha','ใฏ'],['hi','ใฒ'],['he','ใธ'],['ho','ใป'], + ['ba','ใฐ'],['bi','ใณ'],['bu','ใถ'],['be','ใน'],['bo','ใผ'], + ['pa','ใฑ'],['pi','ใด'],['pu','ใท'],['pe','ใบ'],['po','ใฝ'], + ['ma','ใพ'],['mi','ใฟ'],['mu','ใ‚€'],['me','ใ‚'],['mo','ใ‚‚'], + ['ra','ใ‚‰'],['ri','ใ‚Š'],['ru','ใ‚‹'],['re','ใ‚Œ'],['ro','ใ‚'], + ['wa','ใ‚'],['wo','ใ‚’'],['n','ใ‚“'], + ['a','ใ‚'],['i','ใ„'],['u','ใ†'],['e','ใˆ'],['o','ใŠ'] + ], + func: function(text) { + // reuse katakana logic with different table + let i = 0, out = ''; + const lower = text.toLowerCase(); + const sorted = [...this.table].sort((a,b)=>b[0].length-a[0].length); + while (i < lower.length) { + let matched = false; + for (const [rom,kana] of sorted) { + if (lower.startsWith(rom, i)) { + out += kana; + i += rom.length; + matched = true; + break; + } + } + if (!matched) { + out += text[i]; + i += 1; + } + } + return out; + }, + preview: function(text) { + if (!text) return '[ใฒใ‚‰ใŒใช]'; + return this.func(text.slice(0, 6)) + (text.length > 6 ? '...' : ''); + }, + reverse: function(text) { + const rev = {}; + for (const [rom,kana] of this.table) rev[kana] = rom; + let out = ''; + for (const ch of text) out += (rev[ch] || ch); + return out; + } + +}); \ No newline at end of file diff --git a/src/transformers/unicode/katakana.js b/src/transformers/unicode/katakana.js new file mode 100644 index 0000000..c21e890 --- /dev/null +++ b/src/transformers/unicode/katakana.js @@ -0,0 +1,69 @@ +// katakana transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Katakana', + priority: 100, + table: [ + ['kyo','ใ‚ญใƒง'],['kyu','ใ‚ญใƒฅ'],['kya','ใ‚ญใƒฃ'], + ['sho','ใ‚ทใƒง'],['shu','ใ‚ทใƒฅ'],['sha','ใ‚ทใƒฃ'],['shi','ใ‚ท'], + ['cho','ใƒใƒง'],['chu','ใƒใƒฅ'],['cha','ใƒใƒฃ'],['chi','ใƒ'], + ['tsu','ใƒ„'],['fu','ใƒ•'], + ['ryo','ใƒชใƒง'],['ryu','ใƒชใƒฅ'],['rya','ใƒชใƒฃ'], + ['nyo','ใƒ‹ใƒง'],['nyu','ใƒ‹ใƒฅ'],['nya','ใƒ‹ใƒฃ'], + ['gya','ใ‚ฎใƒฃ'],['gyu','ใ‚ฎใƒฅ'],['gyo','ใ‚ฎใƒง'], + ['hya','ใƒ’ใƒฃ'],['hyu','ใƒ’ใƒฅ'],['hyo','ใƒ’ใƒง'], + ['mya','ใƒŸใƒฃ'],['myu','ใƒŸใƒฅ'],['myo','ใƒŸใƒง'], + ['pya','ใƒ”ใƒฃ'],['pyu','ใƒ”ใƒฅ'],['pyo','ใƒ”ใƒง'], + ['bya','ใƒ“ใƒฃ'],['byu','ใƒ“ใƒฅ'],['byo','ใƒ“ใƒง'], + ['ja','ใ‚ธใƒฃ'],['ju','ใ‚ธใƒฅ'],['jo','ใ‚ธใƒง'], + ['ka','ใ‚ซ'],['ki','ใ‚ญ'],['ku','ใ‚ฏ'],['ke','ใ‚ฑ'],['ko','ใ‚ณ'], + ['ga','ใ‚ฌ'],['gi','ใ‚ฎ'],['gu','ใ‚ฐ'],['ge','ใ‚ฒ'],['go','ใ‚ด'], + ['sa','ใ‚ต'],['su','ใ‚น'],['se','ใ‚ป'],['so','ใ‚ฝ'], + ['za','ใ‚ถ'],['zu','ใ‚บ'],['ze','ใ‚ผ'],['zo','ใ‚พ'], + ['ta','ใ‚ฟ'],['te','ใƒ†'],['to','ใƒˆ'], + ['da','ใƒ€'],['de','ใƒ‡'],['do','ใƒ‰'], + ['na','ใƒŠ'],['ni','ใƒ‹'],['nu','ใƒŒ'],['ne','ใƒ'],['no','ใƒŽ'], + ['ha','ใƒ'],['hi','ใƒ’'],['he','ใƒ˜'],['ho','ใƒ›'], + ['ba','ใƒ'],['bi','ใƒ“'],['bu','ใƒ–'],['be','ใƒ™'],['bo','ใƒœ'], + ['pa','ใƒ‘'],['pi','ใƒ”'],['pu','ใƒ—'],['pe','ใƒš'],['po','ใƒ'], + ['ma','ใƒž'],['mi','ใƒŸ'],['mu','ใƒ '],['me','ใƒก'],['mo','ใƒข'], + ['ra','ใƒฉ'],['ri','ใƒช'],['ru','ใƒซ'],['re','ใƒฌ'],['ro','ใƒญ'], + ['wa','ใƒฏ'],['wo','ใƒฒ'],['n','ใƒณ'], + ['a','ใ‚ข'],['i','ใ‚ค'],['u','ใ‚ฆ'],['e','ใ‚จ'],['o','ใ‚ช'] + ], + func: function(text) { + let i = 0, out = ''; + const lower = text.toLowerCase(); + const sorted = [...this.table].sort((a,b)=>b[0].length-a[0].length); + while (i < lower.length) { + let matched = false; + for (const [rom,kana] of sorted) { + if (lower.startsWith(rom, i)) { + out += kana; + i += rom.length; + matched = true; + break; + } + } + if (!matched) { + out += text[i]; + i += 1; + } + } + return out; + }, + preview: function(text) { + if (!text) return '[ใ‚ซใ‚ฟใ‚ซใƒŠ]'; + return this.func(text.slice(0, 6)) + (text.length > 6 ? '...' : ''); + }, + reverse: function(text) { + const rev = {}; + for (const [rom,kana] of this.table) rev[kana] = rom; + let out = ''; + for (const ch of text) out += (rev[ch] || ch); + return out; + } + +}); \ No newline at end of file diff --git a/src/transformers/unicode/mathematical.js b/src/transformers/unicode/mathematical.js new file mode 100644 index 0000000..d0dda09 --- /dev/null +++ b/src/transformers/unicode/mathematical.js @@ -0,0 +1,32 @@ +// mathematical transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Mathematical Notation', + priority: 85, + map: { + 'a': '๐’ถ', 'b': '๐’ท', 'c': '๐’ธ', 'd': '๐’น', 'e': '๐‘’', 'f': '๐’ป', 'g': '๐‘”', 'h': '๐’ฝ', 'i': '๐’พ', + 'j': '๐’ฟ', 'k': '๐“€', 'l': '๐“', 'm': '๐“‚', 'n': '๐“ƒ', 'o': '๐‘œ', 'p': '๐“…', 'q': '๐“†', 'r': '๐“‡', + 's': '๐“ˆ', 't': '๐“‰', 'u': '๐“Š', 'v': '๐“‹', 'w': '๐“Œ', 'x': '๐“', 'y': '๐“Ž', 'z': '๐“', + 'A': '๐’œ', 'B': 'โ„ฌ', 'C': '๐’ž', 'D': '๐’Ÿ', 'E': 'โ„ฐ', 'F': 'โ„ฑ', 'G': '๐’ข', 'H': 'โ„‹', 'I': 'โ„', + 'J': '๐’ฅ', 'K': '๐’ฆ', 'L': 'โ„’', 'M': 'โ„ณ', 'N': '๐’ฉ', 'O': '๐’ช', 'P': '๐’ซ', 'Q': '๐’ฌ', 'R': 'โ„›', + 'S': '๐’ฎ', 'T': '๐’ฏ', 'U': '๐’ฐ', 'V': '๐’ฑ', 'W': '๐’ฒ', 'X': '๐’ณ', 'Y': '๐’ด', 'Z': '๐’ต' + }, + func: function(text) { + return [...text].map(c => this.map[c] || c).join(''); + }, + reverse: function(text) { + const revMap = {}; + for (const [key, value] of Object.entries(this.map)) { + revMap[value] = key; + } + return [...text].map(c => revMap[c] || c).join(''); + }, + // Detector: Check for mathematical script characters + detector: function(text) { + // Mathematical script characters (similar to cursive but distinct) + return /[๐’ถ๐’ท๐’ธ๐’น๐‘’๐’ป๐‘”๐’ฝ๐’พ๐’ฟ๐“€๐“๐“‚๐“ƒ๐‘œ๐“…๐“†๐“‡๐“ˆ๐“‰๐“Š๐“‹๐“Œ๐“๐“Ž๐“๐’œโ„ฌ๐’ž๐’Ÿโ„ฐโ„ฑ๐’ขโ„‹โ„๐’ฅ๐’ฆโ„’โ„ณ๐’ฉ๐’ช๐’ซ๐’ฌโ„›๐’ฎ๐’ฏ๐’ฐ๐’ฑ๐’ฒ๐’ณ๐’ด๐’ต]/u.test(text); + } + +}); \ No newline at end of file diff --git a/src/transformers/unicode/medieval.js b/src/transformers/unicode/medieval.js new file mode 100644 index 0000000..0002049 --- /dev/null +++ b/src/transformers/unicode/medieval.js @@ -0,0 +1,23 @@ +// medieval transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + name: 'Medieval', + priority: 85, + map: { + 'a': '๐–†', 'b': '๐–‡', 'c': '๐–ˆ', 'd': '๐–‰', 'e': '๐–Š', 'f': '๐–‹', 'g': '๐–Œ', 'h': '๐–', 'i': '๐–Ž', + 'j': '๐–', 'k': '๐–', 'l': '๐–‘', 'm': '๐–’', 'n': '๐–“', 'o': '๐–”', 'p': '๐–•', 'q': '๐––', 'r': '๐–—', + 's': '๐–˜', 't': '๐–™', 'u': '๐–š', 'v': '๐–›', 'w': '๐–œ', 'x': '๐–', 'y': '๐–ž', 'z': '๐–Ÿ', + 'A': '๐•ฌ', 'B': '๐•ญ', 'C': '๐•ฎ', 'D': '๐•ฏ', 'E': '๐•ฐ', 'F': '๐•ฑ', 'G': '๐•ฒ', 'H': '๐•ณ', 'I': '๐•ด', + 'J': '๐•ต', 'K': '๐•ถ', 'L': '๐•ท', 'M': '๐•ธ', 'N': '๐•น', 'O': '๐•บ', 'P': '๐•ป', 'Q': '๐•ผ', 'R': '๐•ฝ', + 'S': '๐•พ', 'T': '๐•ฟ', 'U': '๐–€', 'V': '๐–', 'W': '๐–‚', 'X': '๐–ƒ', 'Y': '๐–„', 'Z': '๐–…' + }, + func: function(text) { + return [...text].map(c => this.map[c] || c).join(''); + }, + // Detector: Check for medieval Unicode characters + detector: function(text) { + // Medieval characters (Fraktur bold) + return /[๐–†๐–‡๐–ˆ๐–‰๐–Š๐–‹๐–Œ๐–๐–Ž๐–๐–๐–‘๐–’๐–“๐–”๐–•๐––๐–—๐–˜๐–™๐–š๐–›๐–œ๐–๐–ž๐–Ÿ๐•ฌ๐•ญ๐•ฎ๐•ฏ๐•ฐ๐•ฑ๐•ฒ๐•ณ๐•ด๐•ต๐•ถ๐•ท๐•ธ๐•น๐•บ๐•ป๐•ผ๐•ฝ๐•พ๐•ฟ๐–€๐–๐–‚๐–ƒ๐–„๐–…]/.test(text); + } +}); diff --git a/src/transformers/unicode/mirror.js b/src/transformers/unicode/mirror.js new file mode 100644 index 0000000..bd1c346 --- /dev/null +++ b/src/transformers/unicode/mirror.js @@ -0,0 +1,19 @@ +// mirror transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Mirror Text', + priority: 85, + func: function(text) { + return [...text].reverse().join(''); + }, + preview: function(text) { + if (!text) return '[math]'; + return this.func(text.slice(0, 3)) + '...'; + }, + reverse: function(text) { + return this.func(text); // Mirror is its own inverse + } + +}); \ No newline at end of file diff --git a/src/transformers/unicode/monospace.js b/src/transformers/unicode/monospace.js new file mode 100644 index 0000000..4301715 --- /dev/null +++ b/src/transformers/unicode/monospace.js @@ -0,0 +1,24 @@ +// monospace transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + name: 'Monospace', + priority: 85, + map: { + 'a': '๐šŠ', 'b': '๐š‹', 'c': '๐šŒ', 'd': '๐š', 'e': '๐šŽ', 'f': '๐š', 'g': '๐š', 'h': '๐š‘', 'i': '๐š’', + 'j': '๐š“', 'k': '๐š”', 'l': '๐š•', 'm': '๐š–', 'n': '๐š—', 'o': '๐š˜', 'p': '๐š™', 'q': '๐šš', 'r': '๐š›', + 's': '๐šœ', 't': '๐š', 'u': '๐šž', 'v': '๐šŸ', 'w': '๐š ', 'x': '๐šก', 'y': '๐šข', 'z': '๐šฃ', + 'A': '๐™ฐ', 'B': '๐™ฑ', 'C': '๐™ฒ', 'D': '๐™ณ', 'E': '๐™ด', 'F': '๐™ต', 'G': '๐™ถ', 'H': '๐™ท', 'I': '๐™ธ', + 'J': '๐™น', 'K': '๐™บ', 'L': '๐™ป', 'M': '๐™ผ', 'N': '๐™ฝ', 'O': '๐™พ', 'P': '๐™ฟ', 'Q': '๐š€', 'R': '๐š', + 'S': '๐š‚', 'T': '๐šƒ', 'U': '๐š„', 'V': '๐š…', 'W': '๐š†', 'X': '๐š‡', 'Y': '๐šˆ', 'Z': '๐š‰', + '0': '๐Ÿถ', '1': '๐Ÿท', '2': '๐Ÿธ', '3': '๐Ÿน', '4': '๐Ÿบ', '5': '๐Ÿป', '6': '๐Ÿผ', '7': '๐Ÿฝ', '8': '๐Ÿพ', '9': '๐Ÿฟ' + }, + func: function(text) { + return [...text].map(c => this.map[c] || c).join(''); + }, + // Detector: Check for monospace Unicode characters + detector: function(text) { + // Monospace characters + return /[๐šŠ๐š‹๐šŒ๐š๐šŽ๐š๐š๐š‘๐š’๐š“๐š”๐š•๐š–๐š—๐š˜๐š™๐šš๐š›๐šœ๐š๐šž๐šŸ๐š ๐šก๐šข๐šฃ๐™ฐ๐™ฑ๐™ฒ๐™ณ๐™ด๐™ต๐™ถ๐™ท๐™ธ๐™น๐™บ๐™ป๐™ผ๐™ฝ๐™พ๐™ฟ๐š€๐š๐š‚๐šƒ๐š„๐š…๐š†๐š‡๐šˆ๐š‰๐Ÿถ๐Ÿท๐Ÿธ๐Ÿน๐Ÿบ๐Ÿป๐Ÿผ๐Ÿฝ๐Ÿพ๐Ÿฟ]/.test(text); + } +}); diff --git a/src/transformers/unicode/regional-indicator.js b/src/transformers/unicode/regional-indicator.js new file mode 100644 index 0000000..929308c --- /dev/null +++ b/src/transformers/unicode/regional-indicator.js @@ -0,0 +1,34 @@ +// regional-indicator transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Regional Indicator Letters', + priority: 70, + func: function(text) { + const base = 0x1F1E6; + return [...text].map(c => { + const up = c.toUpperCase(); + if (up >= 'A' && up <= 'Z') { + const code = base + (up.charCodeAt(0) - 65); + return String.fromCodePoint(code); + } + return c; + }).join(''); + }, + preview: function(text) { + if (!text) return '๐Ÿ‡ฆ๐Ÿ‡ง๐Ÿ‡จ'; + return this.func(text.slice(0, 4)) + (text.length > 4 ? '...' : ''); + }, + reverse: function(text) { + const base = 0x1F1E6; + return [...text].map(ch => { + const cp = ch.codePointAt(0); + if (cp >= base && cp <= base + 25) { + return String.fromCharCode(65 + (cp - base)); + } + return ch; + }).join(''); + } + +}); \ No newline at end of file diff --git a/src/transformers/unicode/small-caps.js b/src/transformers/unicode/small-caps.js new file mode 100644 index 0000000..e67ebd0 --- /dev/null +++ b/src/transformers/unicode/small-caps.js @@ -0,0 +1,22 @@ +// small-caps transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Small Caps', + priority: 85, + map: { + 'a': 'แด€', 'b': 'ส™', 'c': 'แด„', 'd': 'แด…', 'e': 'แด‡', 'f': '๊œฐ', 'g': 'ษข', 'h': 'สœ', 'i': 'ษช', + 'j': 'แดŠ', 'k': 'แด‹', 'l': 'สŸ', 'm': 'แด', 'n': 'ษด', 'o': 'แด', 'p': 'แด˜', 'q': 'วซ', 'r': 'ส€', + 's': 's', 't': 'แด›', 'u': 'แดœ', 'v': 'แด ', 'w': 'แดก', 'x': 'x', 'y': 'ส', 'z': 'แดข' + }, + func: function(text) { + return [...text.toLowerCase()].map(c => this.map[c] || c).join(''); + }, + // Detector: Check for small caps Unicode characters + detector: function(text) { + // Small caps use various Unicode ranges (U+1D00-U+1D7F phonetic extensions, U+A730-U+A7FF Latin Extended-D) + return /[แด€ส™แด„แด…แด‡๊œฐษขสœษชแดŠแด‹สŸแดษดแดแด˜วซส€แด›แดœแด แดกสแดข]/.test(text); + } + +}); \ No newline at end of file diff --git a/src/transformers/unicode/strikethrough.js b/src/transformers/unicode/strikethrough.js new file mode 100644 index 0000000..0136b6a --- /dev/null +++ b/src/transformers/unicode/strikethrough.js @@ -0,0 +1,22 @@ +// strikethrough transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Strikethrough', + priority: 85, + func: function(text) { + // Use proper Unicode combining characters for strikethrough + const segments = window.EmojiUtils.splitEmojis(text); + return segments.map(c => c + '\u0336').join(''); + }, + preview: function(text) { + if (!text) return '[hieroglyphics]'; + return this.func(text.slice(0, 3)) + '...'; + }, + reverse: function(text) { + // Remove combining strikethrough characters + return text.replace(/\u0336/g, ''); + } + +}); \ No newline at end of file diff --git a/src/transformers/unicode/subscript.js b/src/transformers/unicode/subscript.js new file mode 100644 index 0000000..2999f99 --- /dev/null +++ b/src/transformers/unicode/subscript.js @@ -0,0 +1,25 @@ +// subscript transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Subscript', + priority: 85, + map: { + '0':'โ‚€','1':'โ‚','2':'โ‚‚','3':'โ‚ƒ','4':'โ‚„','5':'โ‚…','6':'โ‚†','7':'โ‚‡','8':'โ‚ˆ','9':'โ‚‰', + 'a':'โ‚','e':'โ‚‘','h':'โ‚•','i':'แตข','j':'โฑผ','k':'โ‚–','l':'โ‚—','m':'โ‚˜','n':'โ‚™','o':'โ‚’','p':'โ‚š','r':'แตฃ','s':'โ‚›','t':'โ‚œ','u':'แตค','v':'แตฅ','x':'โ‚“' + }, + func: function(text) { + return [...text].map(c => this.map[c] || c).join(''); + }, + preview: function(text) { + if (!text) return '[sub]'; + return this.func(text.slice(0, 4)) + (text.length > 4 ? '...' : ''); + }, + reverse: function(text) { + const revMap = {}; + for (const [k,v] of Object.entries(this.map)) revMap[v] = k; + return [...text].map(c => revMap[c] || c).join(''); + } + +}); \ No newline at end of file diff --git a/src/transformers/unicode/superscript.js b/src/transformers/unicode/superscript.js new file mode 100644 index 0000000..e1cd219 --- /dev/null +++ b/src/transformers/unicode/superscript.js @@ -0,0 +1,26 @@ +// superscript transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Superscript', + priority: 85, + map: { + '0':'โฐ','1':'ยน','2':'ยฒ','3':'ยณ','4':'โด','5':'โต','6':'โถ','7':'โท','8':'โธ','9':'โน', + 'a':'แตƒ','b':'แต‡','c':'แถœ','d':'แตˆ','e':'แต‰','f':'แถ ','g':'แต','h':'สฐ','i':'โฑ','j':'สฒ','k':'แต','l':'หก','m':'แต','n':'โฟ','o':'แต’','p':'แต–','q':'แต ','r':'สณ','s':'หข','t':'แต—','u':'แต˜','v':'แต›','w':'สท','x':'หฃ','y':'สธ','z':'แถป', + 'A':'แดฌ','B':'แดฎ','C':'แถœ','D':'แดฐ','E':'แดฑ','F':'แถ ','G':'แดณ','H':'แดด','I':'แดต','J':'แดถ','K':'แดท','L':'แดธ','M':'แดน','N':'แดบ','O':'แดผ','P':'แดพ','Q':'แต ','R':'แดฟ','S':'หข','T':'แต€','U':'แต','V':'โฑฝ','W':'แต‚','X':'หฃ','Y':'สธ','Z':'แถป' + }, + func: function(text) { + return [...text].map(c => this.map[c] || c).join(''); + }, + preview: function(text) { + if (!text) return '[super]'; + return this.func(text.slice(0, 4)) + (text.length > 4 ? '...' : ''); + }, + reverse: function(text) { + const revMap = {}; + for (const [k,v] of Object.entries(this.map)) revMap[v] = k; + return [...text].map(c => revMap[c] || c).join(''); + } + +}); \ No newline at end of file diff --git a/src/transformers/unicode/underline.js b/src/transformers/unicode/underline.js new file mode 100644 index 0000000..6019662 --- /dev/null +++ b/src/transformers/unicode/underline.js @@ -0,0 +1,22 @@ +// underline transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Underline', + priority: 85, + func: function(text) { + // Use proper Unicode combining characters for underline + const segments = window.EmojiUtils.splitEmojis(text); + return segments.map(c => c + '\u0332').join(''); + }, + preview: function(text) { + if (!text) return '[ogham]'; + return this.func(text.slice(0, 3)) + '...'; + }, + reverse: function(text) { + // Remove combining underline characters + return text.replace(/\u0332/g, ''); + } + +}); \ No newline at end of file diff --git a/src/transformers/unicode/upside-down.js b/src/transformers/unicode/upside-down.js new file mode 100644 index 0000000..719e8cd --- /dev/null +++ b/src/transformers/unicode/upside-down.js @@ -0,0 +1,40 @@ +// upside-down transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Upside Down', + priority: 85, + map: { + 'a': 'ษ', 'b': 'q', 'c': 'ษ”', 'd': 'p', 'e': 'ว', 'f': 'ษŸ', 'g': 'ฦƒ', 'h': 'ษฅ', 'i': 'แด‰', + 'j': 'ษพ', 'k': 'สž', 'l': 'l', 'm': 'ษฏ', 'n': 'u', 'o': 'o', 'p': 'd', 'q': 'b', 'r': 'ษน', + 's': 's', 't': 'ส‡', 'u': 'n', 'v': 'สŒ', 'w': 'ส', 'x': 'x', 'y': 'สŽ', 'z': 'z', + 'A': 'โˆ€', 'B': 'B', 'C': 'ฦ†', 'D': 'D', 'E': 'ฦŽ', 'F': 'โ„ฒ', 'G': 'ืค', 'H': 'H', 'I': 'I', + 'J': 'ลฟ', 'K': 'K', 'L': 'หฅ', 'M': 'W', 'N': 'N', 'O': 'O', 'P': 'ิ€', 'Q': 'Q', 'R': 'R', + 'S': 'S', 'T': 'โ”ด', 'U': 'โˆฉ', 'V': 'ฮ›', 'W': 'M', 'X': 'X', 'Y': 'โ…„', 'Z': 'Z', + '0': '0', '1': 'ฦ–', '2': 'แ„…', '3': 'ฦ', '4': 'ใ„ฃ', '5': 'ฯ›', '6': '9', '7': 'ใ„ฅ', + '8': '8', '9': '6', '.': 'ห™', ',': "'", '?': 'ยฟ', '!': 'ยก', '"': ',,', "'": ',', + '(': ')', ')': '(', '[': ']', ']': '[', '{': '}', '}': '{', '<': '>', '>': '<', + '&': 'โ…‹', '_': 'โ€พ' + }, + // Create reverse map for decoding + reverseMap: function() { + const revMap = {}; + for (const [key, value] of Object.entries(this.map)) { + revMap[value] = key; + } + return revMap; + }, + func: function(text) { + return [...text].map(c => this.map[c] || c).reverse().join(''); + }, + preview: function(text) { + if (!text) return '[upside down]'; + return this.func(text.slice(0, 8)); + }, + reverse: function(text) { + const revMap = this.reverseMap(); + return [...text].map(c => revMap[c] || c).reverse().join(''); + } + +}); \ No newline at end of file diff --git a/src/transformers/unicode/vaporwave.js b/src/transformers/unicode/vaporwave.js new file mode 100644 index 0000000..c0e9725 --- /dev/null +++ b/src/transformers/unicode/vaporwave.js @@ -0,0 +1,21 @@ +// vaporwave transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Vaporwave', + priority: 85, + func: function(text) { + return [...text].join(' '); + }, + preview: function(text) { + if (!text) return '[vaporwave]'; + return [...text.slice(0, 3)].join(' ') + '...'; + }, + reverse: function(text) { + // Remove single spaces between characters, but preserve word boundaries (double+ spaces) + // Replace double spaces with a marker, remove single spaces, restore markers + return text.replace(/ +/g, '\x00').replace(/ /g, '').replace(/\x00/g, ' '); + } + +}); \ No newline at end of file diff --git a/src/transformers/unicode/wingdings.js b/src/transformers/unicode/wingdings.js new file mode 100644 index 0000000..18ccbda --- /dev/null +++ b/src/transformers/unicode/wingdings.js @@ -0,0 +1,45 @@ +// wingdings transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Wingdings', + priority: 100, + map: { + 'a': 'โ™‹', 'b': 'โ™Œ', 'c': 'โ™', 'd': 'โ™Ž', 'e': 'โ™', 'f': 'โ™', 'g': 'โ™‘', 'h': 'โ™’', + 'i': 'โ™“', 'j': 'โ›Ž', 'k': 'โ˜€', 'l': 'โ˜', 'm': 'โ˜‚', 'n': 'โ˜ƒ', 'o': 'โ˜„', 'p': 'โ˜…', + 'q': 'โ˜†', 'r': 'โ˜‡', 's': 'โ˜ˆ', 't': 'โ˜‰', 'u': 'โ˜Š', 'v': 'โ˜‹', 'w': 'โ˜Œ', 'x': 'โ˜', + 'y': 'โ˜Ž', 'z': 'โ˜', + 'A': 'โ™ ', 'B': 'โ™ก', 'C': 'โ™ข', 'D': 'โ™ฃ', 'E': 'โ™ค', 'F': 'โ™ฅ', 'G': 'โ™ฆ', 'H': 'โ™ง', + 'I': 'โ™จ', 'J': 'โ™ฉ', 'K': 'โ™ช', 'L': 'โ™ซ', 'M': 'โ™ฌ', 'N': 'โ™ญ', 'O': 'โ™ฎ', 'P': 'โ™ฏ', + 'Q': 'โœ', 'R': 'โœ‚', 'S': 'โœƒ', 'T': 'โœ„', 'U': 'โœ†', 'V': 'โœ‡', 'W': 'โœˆ', 'X': 'โœ‰', + 'Y': 'โœŒ', 'Z': 'โœ', + '0': 'โœ“', '1': 'โœ”', '2': 'โœ•', '3': 'โœ–', '4': 'โœ—', '5': 'โœ˜', '6': 'โœ™', '7': 'โœš', + '8': 'โœ›', '9': 'โœœ', + '.': 'โœ ', ',': 'โœก', '?': 'โœข', '!': 'โœฃ', '@': 'โœค', '#': 'โœฅ', '$': 'โœฆ', '%': 'โœง', + '^': 'โœฉ', '&': 'โœช', '*': 'โœซ', '(': 'โœฌ', ')': 'โœญ', '-': 'โœฎ', '_': 'โœฏ', '=': 'โœฐ', + '+': 'โœฑ', '[': 'โœฒ', ']': 'โœณ', '{': 'โœด', '}': 'โœต', '|': 'โœถ', '\\': 'โœท', ';': 'โœธ', + ':': 'โœน', '"': 'โœบ', '\'': 'โœป', '<': 'โœผ', '>': 'โœฝ', '/': 'โœพ', '~': 'โœฟ', '`': 'โ€' + }, + func: function(text) { + return text.split('').map(char => this.map[char] || char).join(''); + }, + preview: function(text) { + if (!text) return '[wingdings]'; + return this.func(text.slice(0, 10)); + }, + reverseMap: function() { + if (!this._reverseMap) { + this._reverseMap = {}; + for (let key in this.map) { + this._reverseMap[this.map[key]] = key; + } + } + return this._reverseMap; + }, + reverse: function(text) { + const revMap = this.reverseMap(); + return text.split('').map(char => revMap[char] || char).join(''); + } + +}); \ No newline at end of file diff --git a/src/transformers/unicode/zalgo.js b/src/transformers/unicode/zalgo.js new file mode 100644 index 0000000..d36ac64 --- /dev/null +++ b/src/transformers/unicode/zalgo.js @@ -0,0 +1,39 @@ +// zalgo transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Zalgo', + priority: 85, + marks: [ + '\u0300', '\u0301', '\u0302', '\u0303', '\u0304', '\u0305', '\u0306', '\u0307', '\u0308', + '\u0309', '\u030A', '\u030B', '\u030C', '\u030D', '\u030E', '\u030F', '\u0310', '\u0311', + '\u0312', '\u0313', '\u0314', '\u0315', '\u031A', '\u031B', '\u033D', '\u033E', '\u033F' + ], + func: function(text) { + return [...text].map(c => { + let result = c; + for (let i = 0; i < Math.floor(Math.random() * 3) + 1; i++) { + result += this.marks[Math.floor(Math.random() * this.marks.length)]; + } + return result; + }).join(''); + }, + preview: function(text) { + return this.func(text); + }, + reverse: function(text) { + // Remove all combining diacritical marks (Unicode range 0300-036F) + // This includes the marks used by Zalgo and many others + return text.normalize('NFD').replace(/[\u0300-\u036F]/g, ''); + }, + // Detector: Check for Zalgo text (excessive combining marks) + detector: function(text) { + // Zalgo text has many combining diacritical marks + const combiningMarksRegex = /[\u0300-\u036f\u1ab0-\u1aff\u1dc0-\u1dff\u20d0-\u20ff\ufe20-\ufe2f]/g; + const matches = text.match(combiningMarksRegex) || []; + // Threshold: at least 4 combining marks to distinguish from normal accented text + return matches.length > 3; + } + +}); \ No newline at end of file diff --git a/src/transformers/visual/disemvowel.js b/src/transformers/visual/disemvowel.js new file mode 100644 index 0000000..d3a4e1f --- /dev/null +++ b/src/transformers/visual/disemvowel.js @@ -0,0 +1,16 @@ +// disemvowel transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Disemvowel', + priority: 40, + func: function(text) { + return text.replace(/[aeiouAEIOU]/g, ''); + }, + preview: function(text) { + if (!text) return '[dsmvwl]'; + return this.func(text.slice(0, 12)) + (text.length > 12 ? '...' : ''); + } + +}); \ No newline at end of file diff --git a/src/transformers/visual/emoji-speak.js b/src/transformers/visual/emoji-speak.js new file mode 100644 index 0000000..80aa78c --- /dev/null +++ b/src/transformers/visual/emoji-speak.js @@ -0,0 +1,80 @@ +// emoji-speak transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Emoji Speak', + priority: 70, + digitMap: {'0':'0๏ธโƒฃ','1':'1๏ธโƒฃ','2':'2๏ธโƒฃ','3':'3๏ธโƒฃ','4':'4๏ธโƒฃ','5':'5๏ธโƒฃ','6':'6๏ธโƒฃ','7':'7๏ธโƒฃ','8':'8๏ธโƒฃ','9':'9๏ธโƒฃ'}, + func: function(text) { + // Replace digits with keycap emojis + let out = [...text].map(c => this.digitMap[c] || c).join(''); + + // Replace words with emojis using keyword lookup + if (window.emojiData) { + // Split into words while preserving spaces and punctuation + const words = out.match(/\b\w+\b/g); + if (words) { + // Process each unique word + const processed = new Set(); + for (const word of words) { + const lower = word.toLowerCase(); + if (processed.has(lower)) continue; + processed.add(lower); + + // Find all emojis that have this word as a keyword + const matchingEmojis = []; + for (const [emoji, data] of Object.entries(window.emojiData)) { + if (typeof data === 'object' && data.keywords && data.keywords.includes(lower)) { + matchingEmojis.push(emoji); + } + } + + // If we found matches, replace with a random one + if (matchingEmojis.length > 0) { + const randomEmoji = matchingEmojis[Math.floor(Math.random() * matchingEmojis.length)]; + const re = new RegExp(`\\b${word}\\b`, 'gi'); + out = out.replace(re, randomEmoji); + } + } + } + + // Second pass: Replace single characters and symbols (?, !, <3, arrows, etc.) + // Build a map of all single-char/symbol keywords + const symbolMap = new Map(); + for (const [emoji, data] of Object.entries(window.emojiData)) { + if (typeof data === 'object' && data.keywords) { + for (const keyword of data.keywords) { + // Only consider symbols (non-word characters or very short patterns) + // Exclude single digits since they're already handled by digitMap + if (keyword.length <= 3 && !/^\w+$/.test(keyword) && !/^\d$/.test(keyword)) { + if (!symbolMap.has(keyword)) { + symbolMap.set(keyword, []); + } + symbolMap.get(keyword).push(emoji); + } + } + } + } + + // Replace symbols (longest first to handle multi-char like <3 before <) + const sortedSymbols = Array.from(symbolMap.keys()).sort((a, b) => b.length - a.length); + for (const symbol of sortedSymbols) { + if (out.includes(symbol)) { + const matchingEmojis = symbolMap.get(symbol); + const randomEmoji = matchingEmojis[Math.floor(Math.random() * matchingEmojis.length)]; + // Escape special regex characters + const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + out = out.replace(new RegExp(escaped, 'g'), randomEmoji); + } + } + } + return out; + }, + preview: function(text) { + if (!text) return '1๏ธโƒฃ2๏ธโƒฃ3๏ธโƒฃ โœ…'; + return this.func(text.slice(0, 12)) + (text.length > 12 ? '...' : ''); + } + // No reverse function - emoji speak is not meant to be decoded + +}); \ No newline at end of file diff --git a/src/transformers/visual/rovarspraket.js b/src/transformers/visual/rovarspraket.js new file mode 100644 index 0000000..c900a25 --- /dev/null +++ b/src/transformers/visual/rovarspraket.js @@ -0,0 +1,21 @@ +// rovarspraket transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Rรถvarsprรฅket', + priority: 40, + isConsonant: function(c) { return /[bcdfghjklmnpqrstvwxyz]/i.test(c); }, + func: function(text) { + return [...text].map(ch => this.isConsonant(ch) ? (ch + 'o' + ch) : ch).join(''); + }, + preview: function(text) { + if (!text) return 'totexxtot'; + return this.func(text.slice(0, 6)) + (text.length > 6 ? '...' : ''); + }, + reverse: function(text) { + // Collapse consonant-o-consonant patterns where the two consonants match + return text.replace(/([bcdfghjklmnpqrstvwxyz])o\1/gi, '$1'); + } + +}); \ No newline at end of file diff --git a/src/transformers/visual/ubbi-dubbi.js b/src/transformers/visual/ubbi-dubbi.js new file mode 100644 index 0000000..1083805 --- /dev/null +++ b/src/transformers/visual/ubbi-dubbi.js @@ -0,0 +1,20 @@ +// ubbi-dubbi transform +import BaseTransformer from '../BaseTransformer.js'; + +export default new BaseTransformer({ + + name: 'Ubbi Dubbi', + priority: 40, + func: function(text) { + // Insert 'ub' before vowels (simple, reversible scheme) + return text.replace(/([AEIOUaeiou])/g, 'ub$1'); + }, + preview: function(text) { + if (!text) return 'hubellubo'; + return this.func(text.slice(0, 8)) + (text.length > 8 ? '...' : ''); + }, + reverse: function(text) { + return text.replace(/ub([AEIOUaeiou])/g, '$1'); + } + +}); \ No newline at end of file diff --git a/templates/README.md b/templates/README.md new file mode 100644 index 0000000..53e7e78 --- /dev/null +++ b/templates/README.md @@ -0,0 +1,34 @@ +# Tool HTML Templates + +Templates are injected at build time into `index.html` via `npm run build:templates`. + +## Workflow + +1. Edit `.html` files in `templates/` +2. Run `npm run build:templates` +3. Refresh browser + +## Template Files + +- `decoder.html` - Universal Decoder +- `steganography.html` - Emoji Steganography +- `transforms.html` - Text Transformations +- `tokenade.html` - Tokenade Generator +- `fuzzer.html` - Mutation Lab +- `tokenizer.html` - Tokenizer Visualization +- `splitter.html` - Message Splitter +- `gibberish.html` - Gibberish Generator + +## Adding a New Tool Template + +1. Create `templates/yourtool.html` +2. Start with: `
` +3. Use Vue directives (`v-model`, `@click`, etc.) +4. Reference data/methods from your tool's `getVueData()` and `getVueMethods()` +5. Run `npm run build:templates` + +## Important + +- **Never edit `index.html` directly** - it's generated +- **Always rebuild after template changes** - `npm run build:templates` +- Templates are injected at the `#tool-content-container` marker diff --git a/templates/decoder.html b/templates/decoder.html new file mode 100644 index 0000000..c7e7dca --- /dev/null +++ b/templates/decoder.html @@ -0,0 +1,65 @@ +
+
+
+
+ + +
+ +
+ +
+
+ + +
+ + + +
+
+ {{ decoderResult.alternatives.length }} Alternative{{ decoderResult.alternatives.length > 1 ? 's' : '' }}: +
+
+
+
{{ alt.method }}
+
{{ alt.text.substring(0, 100) }}{{ alt.text.length > 100 ? '...' : '' }}
+
+
+
+
+
+
\ No newline at end of file diff --git a/templates/fuzzer.html b/templates/fuzzer.html new file mode 100644 index 0000000..dfba406 --- /dev/null +++ b/templates/fuzzer.html @@ -0,0 +1,48 @@ +
+
+
+
+

Mutation Lab mutate text into diverse payloads

+
+
+ +
+
+ + +
+
+ + + + + + + +
+
+ + + +
+
+
+
+ #{{ i+1 }} + + +
+
+
+
+
+
\ No newline at end of file diff --git a/templates/gibberish.html b/templates/gibberish.html new file mode 100644 index 0000000..5db3901 --- /dev/null +++ b/templates/gibberish.html @@ -0,0 +1,205 @@ +
+
+
+
+
+ +

Gibberish Generator

+
+

Generate gibberish text with consistent word mappings or create variations with random character removal.

+
+ + +
+ + +
+ + +
+
+
+

Input Text

+

Enter text to convert into gibberish. Structure and punctuation will be preserved.

+
+ +
+ +
+ +
+
+ +
+ +
+ +
+ +
+
+

Gibberish Output

+
+
+ + +
+ +
+

Dictionary

+
+
+ + +
+
+
+ + +
+ +
+ + +
+ + +
+
+
+

Input Text

+

Random letters will be removed from each word

+
+ +
+ +
+ + + + +
+ +
+ + +
+ +
+
+

+ Variations + {{ removalOutputs.length }} variation{{ removalOutputs.length !== 1 ? 's' : '' }} +

+
+
+
+
+ Variation {{ index + 1 }} + +
+
{{ output }}
+
+
+
+
+ + +
+
+
+

Input Text

+

Remove specific characters from your text

+
+ +
+ +
+ +
+ +
+ +
+ +
+
+

Result

+
+
+ + +
+
+
+
+
+
+
\ No newline at end of file diff --git a/templates/splitter.html b/templates/splitter.html new file mode 100644 index 0000000..a56d54c --- /dev/null +++ b/templates/splitter.html @@ -0,0 +1,149 @@ +
+
+
+
+
+ +

Message Splitter

+
+

Split text into multiple copyable chunks. Each message can be transformed and encapsulated individually.

+
+ + +
+ +
+ + +
+ + + + + + + + + + + + + +
+ + +
+
+

Transform

+

Apply transformations to each split message individually. Transformations are applied in sequence.

+
+
+
+
+ + +
+
+
+
+ + +
+
+

Encapsulation

+

Wrap each message with custom start and end strings

+
+
+ + +
+
+ Quick presets: + + + + + + + + +
+
+ + +
+ + + +
+ + +
+
+

+ Split Messages + {{ splitMessages.length }} message{{ splitMessages.length !== 1 ? 's' : '' }} +

+
+
+
+
+ Message {{ index + 1 }} + +
+
{{ msg }}
+
+
+
+
+
+
\ No newline at end of file diff --git a/templates/steganography.html b/templates/steganography.html new file mode 100644 index 0000000..ed210dd --- /dev/null +++ b/templates/steganography.html @@ -0,0 +1,62 @@ +
+
+
+ +
+ + +
+ + +
+ +
+ + + + + + + + + + + +
+
+ +
+ + + +
+
+

+ + Encoded Message + using {{ selectedCarrier.name }} + using Invisible Text +

+
+
+ + +
+
+ Copy this text and share it. Only people who know how to decode it will be able to read your message. +
+
+
+
\ No newline at end of file diff --git a/templates/tokenade.html b/templates/tokenade.html new file mode 100644 index 0000000..38022d7 --- /dev/null +++ b/templates/tokenade.html @@ -0,0 +1,152 @@ +
+
+
+
+
+

๐Ÿ’ฅ Tokenade Generator

+ Craft dense token payloads with emojis and zero-width characters +
+
+
+ + DISCLAIMER: Tokenade payloads can severely degrade model performance and crash UIs. Use for testing only. Do not deploy to production or target systems without explicit permission. +
+
+ + + Danger zone: Estimated length {{ estimateTokenadeLength().toLocaleString() }} chars exceeds the safe threshold ({{ dangerThresholdTokens.toLocaleString() }}). + Generating this will very likely freeze/crash your browser or downstream tools. Proceed only if you fully understand the risks. + +
+
+ + + + + +
+
+ + + + + + + +
+
Separator
+
+ + + + +
+
+
+
+ + Estimated length: {{ estimateTokenadeLength().toLocaleString() }} chars + +
+
+
+ Quick picks +
+ +
+
+ + + +
+
+ + +
+ + Length: {{ tokenBombOutput.length.toLocaleString() }} chars + +
+
+
+ +
+
+ + +
+

Text Payload Generator

+ +
+ + + +
+
+ + +
+
+ +
+
+
+
\ No newline at end of file diff --git a/templates/tokenizer.html b/templates/tokenizer.html new file mode 100644 index 0000000..c527f08 --- /dev/null +++ b/templates/tokenizer.html @@ -0,0 +1,48 @@ +
+
+
+
+

Tokenizer Visualization {{ tokenizerEngine }}

+

Paste text to see how different tokenizers segment it.

+
+
+ +
+ +
+ +
+
+

+ Tokens + {{ tokenizerTokens.length }} total ยท {{ tokenizerWordCount }} words ยท {{ tokenizerCharCount }} chars +

+
+
+
+ {{ i }} + {{ tok.text }} + #{{ tok.id }} +
+
+
+ Tokens will appear here. +
+
+
+
\ No newline at end of file diff --git a/templates/transforms.html b/templates/transforms.html new file mode 100644 index 0000000..e21a7f4 --- /dev/null +++ b/templates/transforms.html @@ -0,0 +1,119 @@ +
+
+
+ +
+ +
+
+
Categories:
+
+
+ {{ category }} +
+
+
+
+ +
+
+ +
+
+

+ + Transformed Message + ({{ activeTransform.name }}) +

+
+
+ + +
+
+ Copy this text and share it. Use the Decoder tab to reverse transformations. +
+
+
+
\ No newline at end of file diff --git a/test_transforms.html b/test_transforms.html deleted file mode 100644 index c63c83c..0000000 --- a/test_transforms.html +++ /dev/null @@ -1,258 +0,0 @@ - - - - - - Transform Test Page - - - -

๐Ÿงช Transform Test Page

-

Test all the new transforms to make sure they work correctly!

- -
-

Test Input

- -
- -
-

๐Ÿ”ง Encoding & Decoding

- - - - -
-
- -
-

๐Ÿง™โ€โ™‚๏ธ Fantasy Languages

- - - - - -
-
- -
-

๐Ÿ›๏ธ Ancient Scripts

- - - -
-
- -
-

โš™๏ธ Technical Codes

- - - -
-
- -
-

๐ŸŽจ Visual & Unicode

- - - - -
-
- -
-

๐Ÿ” Test Reverse Functions

- - - -
-
- -
-

๐ŸŽฒ Test Randomizer - Code Switching Magic!

-

๐ŸŒŸ Apply different transforms to each word! Each run creates a unique mix.

- - -
-
- - - - - \ No newline at end of file diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..fa47330 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,42 @@ +# Tests + +## Test Suites + +### `test_universal.js` +Tests universal decoder and all transformers: +- Encoding/decoding round-trips +- Universal decoder detection accuracy +- Edge cases and Unicode handling + +```bash +npm run test:universal +``` + +### `test_steganography_options.js` +Tests steganography with all advanced option combinations: + - Bit order (MSB/LSB) +- Variation selector mapping + - Initial presentation options +- Zero-width character options + +```bash +npm run test:steg +``` + +## Running Tests + +```bash +npm run test:all # Run all tests +npm test # Universal decoder tests +npm run test:steg # Steganography tests +``` + +## Test Structure + +Tests use Node.js `vm` module to create sandboxed environments that mirror the browser context. + +## Adding New Tests + +1. Place test files in `tests/` directory +2. Use `path.resolve(__dirname, '..')` for project root +3. Add corresponding npm script in `package.json` diff --git a/tests/test_steganography_options.js b/tests/test_steganography_options.js new file mode 100644 index 0000000..a4358a7 --- /dev/null +++ b/tests/test_steganography_options.js @@ -0,0 +1,221 @@ +#!/usr/bin/env node + +/** + * Test Suite for Steganography Options Round-Trip + * + * Tests encoding and decoding with various advanced option combinations + * to ensure they work correctly together. + */ + +const fs = require('fs'); +const path = require('path'); +const vm = require('vm'); + +// Get project root directory (parent of tests directory) +const projectRoot = path.resolve(__dirname, '..'); + +// Load steganography module +const stegCode = fs.readFileSync(path.join(projectRoot, 'js/core/steganography.js'), 'utf8'); +const escapeParserCode = fs.readFileSync(path.join(projectRoot, 'js/utils/escapeParser.js'), 'utf8'); + +// Create sandbox +const sandbox = { + window: { + EscapeParser: {} + }, + console: console, + TextEncoder: TextEncoder, + TextDecoder: TextDecoder +}; + +vm.createContext(sandbox); + +// Load escape parser first +vm.runInContext(escapeParserCode, sandbox); + +// Load steganography +vm.runInContext(stegCode, sandbox); + +const steganography = sandbox.window.steganography; + +// Test strings +const testStrings = [ + 'hello', + 'hello world', + 'Hello World!', + 'Test with emoji ๐Ÿ', + 'Special chars: !@#$%^&*()', + 'Unicode: ไฝ ๅฅฝ ๐ŸŒž', + 'Longer text: The quick brown fox jumps over the lazy dog.' +]; + +// Test option combinations +const testConfigs = [ + { + name: 'Default options', + options: {} + }, + { + name: 'LSB bit order', + options: { bitOrder: 'lsb' } + }, + { + name: 'Swapped VS (VS15=1, VS16=0)', + options: { bitZeroVS: '\ufe0f', bitOneVS: '\ufe0e' } + }, + { + name: 'No initial presentation', + options: { initialPresentation: 'none' } + }, + { + name: 'Text initial presentation', + options: { initialPresentation: 'text' } + }, + { + name: 'Inter-bit ZWNJ every bit', + options: { interBitZW: '\u200C', interBitEvery: 1 } + }, + { + name: 'Inter-bit ZWJ every 2 bits', + options: { interBitZW: '\u200D', interBitEvery: 2 } + }, + { + name: 'Inter-bit ZWSP every 4 bits', + options: { interBitZW: '\u200B', interBitEvery: 4 } + }, + { + name: 'Trailing ZWNJ', + options: { trailingZW: '\u200C' } + }, + { + name: 'Trailing ZWJ', + options: { trailingZW: '\u200D' } + }, + { + name: 'Trailing BOM', + options: { trailingZW: '\ufeff' } + }, + { + name: 'Complex: LSB + swapped VS + inter-bit', + options: { + bitOrder: 'lsb', + bitZeroVS: '\ufe0f', + bitOneVS: '\ufe0e', + interBitZW: '\u200C', + interBitEvery: 2 + } + }, + { + name: 'Complex: All options', + options: { + bitOrder: 'lsb', + bitZeroVS: '\ufe0f', + bitOneVS: '\ufe0e', + initialPresentation: 'text', + interBitZW: '\u200D', + interBitEvery: 3, + trailingZW: '\u200C' + } + } +]; + +console.log('๐Ÿงช Testing Steganography Options Round-Trip\n'); +console.log('='.repeat(80)); + +let totalTests = 0; +let passedTests = 0; +let failedTests = 0; +const failures = []; + +// Test each configuration +for (const config of testConfigs) { + console.log(`\n๐Ÿ“‹ Testing: ${config.name}`); + console.log(' Options:', JSON.stringify(config.options, null, 2)); + + // Reset to defaults first, then apply test options + // This ensures each test starts with a clean state + steganography.setStegOptions({ + bitZeroVS: '\ufe0e', + bitOneVS: '\ufe0f', + initialPresentation: 'emoji', + trailingZW: '\u200B', + interBitZW: null, + interBitEvery: 1, + bitOrder: 'msb' + }); + steganography.setStegOptions(config.options); + + // Test each string + for (const testString of testStrings) { + totalTests++; + + try { + // Encode with current options + const encoded = steganography.encodeEmoji('๐Ÿ', testString); + + // Decode with same options (should match) + const decoded = steganography.decodeEmoji(encoded); + + // Check if decoded matches original + if (decoded === testString) { + passedTests++; + process.stdout.write(' โœ…'); + } else { + failedTests++; + process.stdout.write(' โŒ'); + failures.push({ + config: config.name, + options: config.options, + original: testString, + encoded: encoded, + decoded: decoded, + encodedLength: encoded.length, + decodedLength: decoded.length + }); + } + } catch (error) { + failedTests++; + process.stdout.write(' ๐Ÿ’ฅ'); + failures.push({ + config: config.name, + options: config.options, + original: testString, + error: error.message, + stack: error.stack + }); + } + } + + console.log(''); // New line after each config +} + +// Summary +console.log('\n' + '='.repeat(80)); +console.log('\n๐Ÿ“Š Test Summary:\n'); +console.log(`Total tests: ${totalTests}`); +console.log(`โœ… Passed: ${passedTests}`); +console.log(`โŒ Failed: ${failedTests}`); +console.log(`Success rate: ${((passedTests / totalTests) * 100).toFixed(1)}%`); + +// Show failures +if (failures.length > 0) { + console.log('\nโŒ Failed Tests:\n'); + failures.forEach((failure, index) => { + console.log(`${index + 1}. Config: ${failure.config}`); + console.log(` Options: ${JSON.stringify(failure.options)}`); + if (failure.error) { + console.log(` Error: ${failure.error}`); + } else { + console.log(` Original: "${failure.original}"`); + console.log(` Encoded length: ${failure.encodedLength} chars`); + console.log(` Decoded: "${failure.decoded}"`); + console.log(` Decoded length: ${failure.decodedLength} chars`); + console.log(` Match: ${failure.original === failure.decoded ? 'YES' : 'NO'}`); + } + console.log(''); + }); +} + +// Exit with error code if tests failed +process.exit(failedTests > 0 ? 1 : 0); + diff --git a/tests/test_universal.js b/tests/test_universal.js new file mode 100644 index 0000000..39779a4 --- /dev/null +++ b/tests/test_universal.js @@ -0,0 +1,619 @@ +#!/usr/bin/env node + +/** + * Comprehensive Universal Test Suite + * + * For each transformer: + * 1. Encode simple string ("hello world") + * 2. Encode complex string ("Hello World. <3 ๐ŸŒž") + * 3. Pass encoded output to universal decoder + * 4. Verify decoder correctly identifies the encoding method + * 5. Verify decoded output matches original (accounting for limitations) + */ + +const fs = require('fs'); +const path = require('path'); +const vm = require('vm'); + +// Get project root directory (parent of tests directory) +const projectRoot = path.resolve(__dirname, '..'); + +// Load transforms +const transforms = require(path.join(projectRoot, 'src/transformers/loader-node.js')); + +// Load decoder +const decoderCode = fs.readFileSync(path.join(projectRoot, 'js/core/decoder.js'), 'utf8'); + +// Load emoji dependencies +const emojiLibraryCode = fs.readFileSync(path.join(projectRoot, 'js/core/emojiLibrary.js'), 'utf8'); +const emojiWordMapCode = fs.readFileSync(path.join(projectRoot, 'src/emojiWordMap.js'), 'utf8'); + +// Create mock window/steganography objects +const mockSteganography = { + decodeEmoji: (text) => null, + decodeInvisible: (text) => null +}; + +// Create sandbox for decoder +const sandbox = { + window: { + transforms: transforms, + steganography: mockSteganography, + emojiLibrary: {}, + emojiKeywords: {} + }, + console: console, + TextEncoder: TextEncoder, + TextDecoder: TextDecoder, + btoa: (str) => Buffer.from(str, 'binary').toString('base64'), + atob: (str) => Buffer.from(str, 'base64').toString('binary'), + Intl: Intl +}; + +vm.createContext(sandbox); + +// Load emoji library and keywords +vm.runInContext(emojiLibraryCode, sandbox); +vm.runInContext(emojiWordMapCode, sandbox); + +// Load decoder +vm.runInContext(decoderCode, sandbox); + +const universalDecode = sandbox.universalDecode; + +// Test strings +const testStrings = { + simple: 'hello world', + complex: 'Hello World. <3 ๐ŸŒž', + edgeCase: 'this is the vest sukkess we\'ve ever had! hello world. <#3 ๐Ÿ˜Š' +}; + +// Helper function to normalize strings based on transformer limitations +function normalizeForComparison(text, transformations = {}) { + let normalized = text; + + if (transformations.lowercase) { + normalized = normalized.toLowerCase(); + } + + if (transformations.uppercase) { + normalized = normalized.toUpperCase(); + } + + if (transformations.stripEmoji) { + normalized = normalized.replace(/[\u{1F300}-\u{1F9FF}\u{2600}-\u{27BF}\u{1F1E6}-\u{1F1FF}\u{2300}-\u{23FF}\u{2B50}\u{1F004}]/gu, ''); + } + + if (transformations.stripPunctuation) { + normalized = normalized.replace(/[.,!?;:'"]/g, ''); + } + + if (transformations.stripSpecialChars) { + normalized = normalized.replace(/[^a-zA-Z0-9\s]/g, ''); + } + + if (transformations.stripWhitespace) { + normalized = normalized.replace(/\s+/g, ''); + } + + if (transformations.stripNonLetters) { + normalized = normalized.replace(/[^a-zA-Z\s]/g, ''); + } + + if (transformations.collapseWhitespace) { + normalized = normalized.replace(/\s+/g, ' ').trim(); + } + + return normalized; +} + +// Transformers with known limitations +const limitations = { + // These transformers intentionally don't preserve everything + 'morse': { + issues: 'Lowercases, strips emoji, converts some punctuation to text', + acceptPartial: true, + normalize: { lowercase: true, stripEmoji: true, collapseWhitespace: true }, + emojiOnly: true // Only has limitations with emoji, works fine for simple text + }, + 'braille': { + issues: 'Lowercases, may not encode all special characters', + acceptPartial: true, + normalize: { lowercase: true, stripEmoji: true } + }, + 'nato': { + issues: 'Lowercases, may not encode symbols/emoji', + acceptPartial: true, + normalize: { lowercase: true, stripEmoji: true, stripSpecialChars: true, collapseWhitespace: true } + }, + 'rail_fence': { + issues: 'Rearranges characters, hard to detect uniquely', + acceptPartial: true + }, + 'quenya': { + issues: 'Fantasy language with vowel/consonant pairs', + acceptPartial: true + }, + 'hieroglyphics': { + issues: 'Lossy encoding, case sensitive', + acceptPartial: true + }, + 'a1z26': { + issues: 'Only encodes letters A-Z, strips everything else including spaces, lowercases', + normalize: { lowercase: true, stripNonLetters: true, stripWhitespace: true } + }, + 'semaphore': { + issues: 'Limited character set (uppercase letters only), uses emoji arrows', + normalize: { uppercase: true, stripNonLetters: true, stripWhitespace: true }, + acceptPartial: true + }, + 'tap_code': { + issues: 'Limited character set, lowercases, uses dots', + normalize: { lowercase: true, stripNonLetters: true, stripPunctuation: true }, + acceptPartial: true + }, + 'html': { + issues: 'Only encodes special characters, rest unchanged', + acceptPartial: true + }, + 'ubbi_dubbi': { + issues: 'May not preserve all special characters', + acceptPartial: true + }, + 'rovarspraket': { + issues: 'Swedish language game, may not preserve everything', + acceptPartial: true + }, + 'baconian': { + issues: 'Only encodes A-Z', + acceptPartial: true, + minMatch: 'hello' + }, + 'alternating_case': { + issues: 'Generic case formatting, hard to detect uniquely (looks like Base64)', + acceptPartial: true, + normalize: (t) => t.toLowerCase() + }, + + // === CIPHERS (Added during BaseTransformer conversion) === + 'atbash': { + issues: 'Simple substitution cipher, hard to detect uniquely', + acceptPartial: true + }, + 'caesar': { + issues: 'Simple substitution cipher, hard to detect uniquely', + acceptPartial: true + }, + 'affine': { + issues: 'Only encodes A-Z, preserves case', + acceptPartial: true, + caseInsensitive: true + }, + 'vigenere': { + issues: 'Only encodes A-Z, preserves case', + acceptPartial: true + }, + 'rot13': { + issues: 'Letter substitution, hard to detect uniquely', + acceptPartial: true + }, + 'rot18': { + issues: 'Letter and number substitution, hard to detect uniquely', + acceptPartial: true + }, + 'rot47': { + issues: 'ASCII rotation, hard to detect uniquely', + acceptPartial: true + }, + 'rot5': { + issues: 'Number rotation only, hard to detect uniquely', + acceptPartial: true + }, + + // === TEXT FORMATTING === + 'disemvowel': { + issues: 'Removes vowels, reverse is ambiguous', + acceptPartial: true + }, + 'leetspeak': { + issues: 'One-way transformation, reverse is ambiguous', + acceptPartial: true + }, + 'qwerty_shift': { + issues: 'May not encode all characters', + acceptPartial: true + }, + 'pigLatin': { + issues: 'Ambiguous rules for "way" endings', + acceptPartial: true + }, + 'kebab_case': { + issues: 'Lowercases, removes punctuation (splits on apostrophes), removes special characters', + normalize: { lowercase: true, stripPunctuation: true, stripSpecialChars: true, stripEmoji: true, collapseWhitespace: true }, + acceptPartial: true // Apostrophes within words cause ambiguity + }, + 'snake_case': { + issues: 'Lowercases, removes punctuation (splits on apostrophes), removes special characters', + normalize: { lowercase: true, stripPunctuation: true, stripSpecialChars: true, stripEmoji: true, collapseWhitespace: true }, + acceptPartial: true // Apostrophes within words cause ambiguity + }, + 'camel_case': { + issues: 'Lowercases, removes punctuation, loses word boundaries (especially for numbers)', + normalize: { lowercase: true, stripPunctuation: true, stripSpecialChars: true, stripEmoji: true, collapseWhitespace: true }, + acceptPartial: true // Word boundaries and apostrophes cause ambiguity + }, + + // === FANTASY SCRIPTS (Case-insensitive) === + 'tengwar': { + issues: 'Case-insensitive mapping, hard to distinguish from Elder Futhark', + acceptPartial: true, + caseInsensitive: true + }, + 'klingon': { + issues: 'Case-insensitive mapping, mixed case letters', + acceptPartial: true, + caseInsensitive: true + }, + 'aurebesh': { + issues: 'Uppercase only, removes whitespace', + normalize: { uppercase: true, stripWhitespace: true }, + acceptPartial: true + }, + 'dovahzul': { + issues: 'Case-insensitive mapping with vowel expansion', + caseInsensitive: true + }, + 'ogham': { + issues: 'Ancient script, uppercase only', + normalize: { uppercase: true }, + acceptPartial: true + }, + 'elder_futhark': { + issues: 'Runic alphabet, case-insensitive', + caseInsensitive: true + }, + 'small_caps': { + issues: 'Lowercases', + caseInsensitive: true + }, + 'hiragana': { + issues: 'Syllabic script, may not preserve everything', + acceptPartial: true + }, + 'katakana': { + issues: 'Syllabic script, may not preserve everything', + acceptPartial: true + }, + 'cyrillic_stylized': { + issues: 'Mixed script, partial character replacement', + acceptPartial: true + }, + + // === SYMBOLS === + 'chemical': { + issues: 'Lowercases all text (case-insensitive)', + caseInsensitive: true + }, + 'regional_indicator': { + issues: 'Only encodes A-Z as flag emojis', + acceptPartial: true + }, + 'emoji_speak': { + issues: 'Limited vocabulary, word-based, random selection means decoded words may differ from original (synonyms)', + acceptPartial: true // Decodes to synonyms, not exact original words + }, + 'roman_numerals': { + issues: 'Only converts numbers, may have limits', + acceptPartial: true + }, + + // === UNICODE STYLES === + 'zalgo': { + issues: 'Adds combining marks, may not decode perfectly', + acceptPartial: true + }, + 'mirror': { + issues: 'Reverses text, hard to distinguish from reverse transform', + acceptPartial: true + }, + 'reverse': { + issues: 'Reverses text, hard to distinguish from mirror transform', + acceptPartial: true + }, + 'reverse_words': { + issues: 'Reverses word order, may be confused with other transforms', + acceptPartial: true + }, + 'upside_down': { + issues: 'Uses Unicode lookalikes, may be confused with ciphers', + acceptPartial: true + }, + 'vaporwave': { + issues: 'Fullwidth + spaces, may be confused with other styles', + acceptPartial: true + }, + 'fraktur': { + issues: 'Gothic script with special chars, may confuse detector', + acceptPartial: true + }, + 'subscript': { + issues: 'Limited character set, some chars use special Unicode', + acceptPartial: true + }, + 'superscript': { + issues: 'Limited character set, some chars use special Unicode', + acceptPartial: true + }, + 'regional_indicator': { + issues: 'Flag emojis for letters, special Unicode handling', + acceptPartial: true + }, + + // === BASE ENCODINGS === + 'ascii85': { + issues: 'May have issues with certain emoji at end of string', + acceptPartial: true, + complexOnly: true + }, + 'base62': { + issues: 'Hard to distinguish from other base encodings', + acceptPartial: true + }, + 'base64url': { + issues: 'Very similar to Base64, hard to distinguish', + acceptPartial: true + }, + 'base45': { + issues: 'May be confused with other encodings', + acceptPartial: true + }, + + // === SPECIAL === + 'brainfuck': { + issues: 'Esoteric language, encoding is not bijective', + acceptPartial: true + }, + 'invisible_text': { + issues: 'Uses private use area, may have decoding issues', + acceptPartial: true + } +}; + +// Track results +let totalTests = 0; +let passedTests = 0; +let failedTests = 0; +let skippedTests = 0; + +const failures = []; + +console.log('๐Ÿงช Comprehensive Universal Encoder/Decoder Test Suite\n'); +console.log('=' .repeat(80)); +console.log('\nTest Strategy:'); +console.log('1. For each transformer, encode simple & complex strings'); +console.log('2. Pass encoded output to universal decoder'); +console.log('3. Verify decoder identifies correct method'); +console.log('4. Verify decoded output matches original (with known limitations)'); +console.log('\n' + '='.repeat(80)); + +// Discover all transforms +const transformNames = Object.keys(transforms).sort(); + +console.log(`\nFound ${transformNames.length} transformers to test\n`); +console.log('='.repeat(80)); + +for (const transformName of transformNames) { + const transform = transforms[transformName]; + + // Skip transforms without reverse function + if (!transform.reverse) { + console.log(`\nโญ๏ธ ${transformName}: Skipped (no reverse function)`); + skippedTests += 2; // Would have tested both simple and complex + continue; + } + + console.log(`\n๐Ÿ“ Testing: ${transform.name || transformName}`); + console.log('-'.repeat(80)); + + const limitation = limitations[transformName]; + if (limitation) { + console.log(`โš ๏ธ Known limitation: ${limitation.issues}`); + } + + // Test both strings + for (const [testType, testString] of Object.entries(testStrings)) { + totalTests++; + + // Skip simple test if complex-only limitation + if (testType === 'simple' && limitation?.complexOnly) { + console.log(` [${testType}] Skipped (complex-only limitation)`); + skippedTests++; + totalTests--; + continue; + } + + // For emoji-only limitations, only apply them to tests with emoji + const hasEmoji = /[\u{1F300}-\u{1F9FF}\u{2600}-\u{27BF}\u{1F1E6}-\u{1F1FF}]/u.test(testString); + const currentLimitation = (limitation?.emojiOnly && !hasEmoji) ? null : limitation; + + try { + // Step 1: Encode + const encoded = transform.func(testString); + + if (!encoded || encoded === testString) { + console.log(` [${testType}] โญ๏ธ No encoding produced`); + skippedTests++; + continue; + } + + // Step 2: Decode with universal decoder + const decoderResult = universalDecode(encoded); + + if (!decoderResult) { + // Some transformers are non-reversible and that's expected + if (currentLimitation?.nonReversible) { + console.log(` [${testType}] โš ๏ธ Non-reversible: Decoder returned null (expected)`); + passedTests++; + continue; + } + + console.log(` [${testType}] โŒ Decoder returned null`); + console.log(` Input: "${testString}"`); + console.log(` Encoded: "${encoded.substring(0, 60)}..."`); + failedTests++; + failures.push({ + transform: transformName, + testType, + issue: 'Decoder returned null', + input: testString, + encoded: encoded.substring(0, 100) + }); + continue; + } + + const { text: decoded, method: detectedMethod, alternatives = [] } = decoderResult; + + // Step 3: Check if correct decoding is in primary or alternatives + const expectedMethod = transform.name || transformName; + + // Build list of all possible decodings to check + const allDecodings = [ + { text: decoded, method: detectedMethod, isPrimary: true }, + ...alternatives.map(alt => ({ ...alt, isPrimary: false })) + ]; + + // Helper to check if method name matches (flexible matching) + const methodNameMatches = (detected, expected) => { + return detected === expected || + detected.toLowerCase() === expected.toLowerCase() || + detected.replace(/\s/g, '') === expected.replace(/\s/g, ''); + }; + + // Find the first decoding that matches our expected method + const correctDecoding = allDecodings.find(d => methodNameMatches(d.method, expectedMethod)); + + // If we didn't find it in the expected method, log it + if (!correctDecoding) { + const alternativeNames = allDecodings.map(d => d.method).join(', '); + console.log(` [${testType}] โš ๏ธ Method mismatch: expected "${expectedMethod}", got "${detectedMethod}"${alternatives.length > 0 ? ` (alternatives: ${alternatives.map(a => a.method).join(', ')})` : ''}`); + } + + // Use the correct decoding if found, otherwise fall back to primary + const decodingToCheck = correctDecoding || allDecodings[0]; + const actualDecoded = decodingToCheck.text; + const isFromAlternative = correctDecoding && !correctDecoding.isPrimary; + + // Step 4: Verify decoded content + let contentMatches = actualDecoded === testString; + let normalizedMatches = false; + let caseInsensitiveMatches = false; + + // Check if there's a normalization rule + if (!contentMatches && currentLimitation) { + // Apply normalization if specified + if (currentLimitation.normalize) { + const normalizedExpected = normalizeForComparison(testString, currentLimitation.normalize); + const normalizedDecoded = normalizeForComparison(actualDecoded, currentLimitation.normalize); + normalizedMatches = normalizedExpected === normalizedDecoded; + } + + // Check case-insensitive match + if (!normalizedMatches && currentLimitation.caseInsensitive) { + caseInsensitiveMatches = actualDecoded.toLowerCase() === testString.toLowerCase(); + } + } + + const altIndicator = isFromAlternative ? ' (from alternative)' : ''; + + if (contentMatches) { + console.log(` [${testType}] โœ… Perfect: "${testString}" โ†’ [encoded] โ†’ "${actualDecoded}"${altIndicator}`); + passedTests++; + } else if (normalizedMatches) { + console.log(` [${testType}] โœ… Match (with expected transformations): "${testString}" โ†’ "${actualDecoded}"${altIndicator}`); + passedTests++; + } else if (caseInsensitiveMatches) { + console.log(` [${testType}] โœ… Match (case-insensitive): "${testString}" โ†’ "${actualDecoded}"${altIndicator}`); + passedTests++; + } else if (currentLimitation?.acceptPartial) { + // For acceptPartial, we're lenient - just check that decoding returned something + // and it's not completely empty or broken + const hasReasonableContent = actualDecoded && actualDecoded.length > 0 && + actualDecoded.length >= Math.min(5, testString.length * 0.3) && + actualDecoded !== '[Invalid input]' && + actualDecoded !== 'undefined'; + + if (hasReasonableContent) { + console.log(` [${testType}] โš ๏ธ Partial: Expected limitations in decoded content${altIndicator}`); + console.log(` Original: "${testString}"`); + console.log(` Decoded: "${actualDecoded}"`); + passedTests++; + } else { + console.log(` [${testType}] โŒ Content mismatch (beyond expected limitations)`); + console.log(` Expected: "${testString}"`); + console.log(` Decoded: "${actualDecoded}"`); + failedTests++; + failures.push({ + transform: transformName, + testType, + issue: 'Content mismatch', + expected: testString, + decoded: actualDecoded + }); + } + } else { + console.log(` [${testType}] โŒ Content mismatch`); + console.log(` Expected: "${testString}"`); + console.log(` Decoded: "${actualDecoded}"`); + console.log(` Method detected: ${decodingToCheck.method}`); + failedTests++; + failures.push({ + transform: transformName, + testType, + issue: 'Content mismatch', + expected: testString, + decoded: actualDecoded, + method: decodingToCheck.method + }); + } + + } catch (error) { + console.log(` [${testType}] โŒ Error: ${error.message}`); + failedTests++; + failures.push({ + transform: transformName, + testType, + issue: `Error: ${error.message}`, + input: testString + }); + } + } +} + +// Summary +console.log('\n' + '='.repeat(80)); +console.log('\n๐Ÿ“Š Test Summary:\n'); +console.log(`โœ… Passed: ${passedTests} tests`); +console.log(`โŒ Failed: ${failedTests} tests`); +console.log(`โญ๏ธ Skipped: ${skippedTests} tests`); +console.log(`๐Ÿ“ Total: ${totalTests} tests\n`); + +if (failures.length > 0) { + console.log('Failed Tests Details:\n'); + failures.forEach((failure, index) => { + console.log(`${index + 1}. ${failure.transform} (${failure.testType}):`); + console.log(` ${failure.issue}`); + if (failure.expected) console.log(` Expected: "${failure.expected}"`); + if (failure.decoded) console.log(` Decoded: "${failure.decoded}"`); + if (failure.detectedMethod) console.log(` Detected as: ${failure.detectedMethod}`); + console.log(); + }); +} + +if (failedTests === 0) { + console.log('โœจ All tests passed!\n'); + process.exit(0); +} else { + console.log(`โŒ ${failedTests} test(s) failed!\n`); + process.exit(1); +} + diff --git a/text_transforms.py b/text_transforms.py deleted file mode 100644 index 8e907d5..0000000 --- a/text_transforms.py +++ /dev/null @@ -1,195 +0,0 @@ -def to_upside_down(text: str) -> str: - """Convert text to upside down characters""" - if not text: - return "" - # Map for upside down text - upside_down_map = { - 'a': 'ษ', 'b': 'q', 'c': 'ษ”', 'd': 'p', 'e': 'ว', 'f': 'ษŸ', 'g': 'ฦƒ', 'h': 'ษฅ', 'i': 'แด‰', - 'j': 'ษพ', 'k': 'สž', 'l': 'l', 'm': 'ษฏ', 'n': 'u', 'o': 'o', 'p': 'd', 'q': 'b', 'r': 'ษน', - 's': 's', 't': 'ส‡', 'u': 'n', 'v': 'สŒ', 'w': 'ส', 'x': 'x', 'y': 'สŽ', 'z': 'z', - 'A': 'โˆ€', 'B': 'B', 'C': 'ฦ†', 'D': 'D', 'E': 'ฦŽ', 'F': 'โ„ฒ', 'G': 'ืค', 'H': 'H', 'I': 'I', - 'J': 'ลฟ', 'K': 'K', 'L': 'หฅ', 'M': 'W', 'N': 'N', 'O': 'O', 'P': 'ิ€', 'Q': 'Q', 'R': 'R', - 'S': 'S', 'T': 'โ”ด', 'U': 'โˆฉ', 'V': 'ฮ›', 'W': 'M', 'X': 'X', 'Y': 'โ…„', 'Z': 'Z', - '0': '0', '1': 'ฦ–', '2': 'แ„…', '3': 'ฦ', '4': 'ใ„ฃ', '5': 'ฯ›', '6': '9', '7': 'ใ„ฅ', '8': '8', '9': '6', - '.': 'ห™', ',': "'", '?': 'ยฟ', '!': 'ยก', '"': ',,', "'": ',', '(': ')', ')': '(', '[': ']', ']': '[', - '{': '}', '}': '{', '<': '>', '>': '<', '&': 'โ…‹', '_': 'โ€พ', ' ': ' ' - } - return ''.join(upside_down_map.get(c, c) for c in text)[::-1] # Reverse for proper effect - -def to_elder_futhark(text: str) -> str: - """Convert text to Elder Futhark runes""" - if not text: - return "" - # Map for Elder Futhark runes - rune_map = { - 'a': 'แšจ', 'b': 'แ›’', 'c': 'แ›ฒ', 'd': 'แ›ž', 'e': 'แ›–', 'f': 'แš ', 'g': 'แšท', 'h': 'แšบ', 'i': 'แ›', - 'j': 'แ›ƒ', 'k': 'แ›ฒ', 'l': 'แ›š', 'm': 'แ›—', 'n': 'แšพ', 'o': 'แ›Ÿ', 'p': 'แ›ˆ', 'q': 'แ›ฒแ›ฉ', 'r': 'แšฑ', - 's': 'แ›‹', 't': 'แ›', 'u': 'แšข', 'v': 'แ›ฉ', 'w': 'แ›ฉ', 'x': 'แ›ฒแ›‹', 'y': 'แ›', 'z': 'แ›‰', - 'A': 'แšจ', 'B': 'แ›’', 'C': 'แ›ฒ', 'D': 'แ›ž', 'E': 'แ›–', 'F': 'แš ', 'G': 'แšท', 'H': 'แšบ', 'I': 'แ›', - 'J': 'แ›ƒ', 'K': 'แ›ฒ', 'L': 'แ›š', 'M': 'แ›—', 'N': 'แšพ', 'O': 'แ›Ÿ', 'P': 'แ›ˆ', 'Q': 'แ›ฒแ›ฉ', 'R': 'แšฑ', - 'S': 'แ›‹', 'T': 'แ›', 'U': 'แšข', 'V': 'แ›ฉ', 'W': 'แ›ฉ', 'X': 'แ›ฒแ›‹', 'Y': 'แ›', 'Z': 'แ›‰', - ' ': ' ' - } - return ''.join(rune_map.get(c, c) for c in text) - -def to_vaporwave(text: str) -> str: - """Convert text to vaporwave aesthetic (wide spaced)""" - if not text: - return "" - return ' '.join(c for c in text) - -import random - -def to_zalgo(text: str) -> str: - """Convert text to zalgo (glitchy) text""" - if not text: - return "" - # Zalgo diacritical marks - zalgo_marks = [ - '\u0300', '\u0301', '\u0302', '\u0303', '\u0304', '\u0305', '\u0306', '\u0307', '\u0308', - '\u0309', '\u030A', '\u030B', '\u030C', '\u030D', '\u030E', '\u030F', '\u0310', '\u0311', - '\u0312', '\u0313', '\u0314', '\u0315', '\u031A', '\u031B', '\u033D', '\u033E', '\u033F', - '\u0340', '\u0341', '\u0342', '\u0343', '\u0344', '\u0345', '\u0346', '\u0347', '\u0348', - '\u0349', '\u034A', '\u034B', '\u034C', '\u034D', '\u034E', '\u034F' - ] - - result = '' - for c in text: - result += c - # Add 1-3 random zalgo marks to each character - for _ in range(random.randint(1, 3)): - result += random.choice(zalgo_marks) - return result - -def to_unicode_circled(text: str) -> str: - """Convert text to unicode circled characters""" - if not text: - return "" - # Map for circled text - circled_map = { - 'a': '๐Ÿ…', 'b': '๐Ÿ…‘', 'c': '๐Ÿ…’', 'd': '๐Ÿ…“', 'e': '๐Ÿ…”', 'f': '๐Ÿ…•', 'g': '๐Ÿ…–', 'h': '๐Ÿ…—', 'i': '๐Ÿ…˜', - 'j': '๐Ÿ…™', 'k': '๐Ÿ…š', 'l': '๐Ÿ…›', 'm': '๐Ÿ…œ', 'n': '๐Ÿ…', 'o': '๐Ÿ…ž', 'p': '๐Ÿ…Ÿ', 'q': '๐Ÿ… ', 'r': '๐Ÿ…ก', - 's': '๐Ÿ…ข', 't': '๐Ÿ…ฃ', 'u': '๐Ÿ…ค', 'v': '๐Ÿ…ฅ', 'w': '๐Ÿ…ฆ', 'x': '๐Ÿ…ง', 'y': '๐Ÿ…จ', 'z': '๐Ÿ…ฉ', - 'A': '๐Ÿ…', 'B': '๐Ÿ…‘', 'C': '๐Ÿ…’', 'D': '๐Ÿ…“', 'E': '๐Ÿ…”', 'F': '๐Ÿ…•', 'G': '๐Ÿ…–', 'H': '๐Ÿ…—', 'I': '๐Ÿ…˜', - 'J': '๐Ÿ…™', 'K': '๐Ÿ…š', 'L': '๐Ÿ…›', 'M': '๐Ÿ…œ', 'N': '๐Ÿ…', 'O': '๐Ÿ…ž', 'P': '๐Ÿ…Ÿ', 'Q': '๐Ÿ… ', 'R': '๐Ÿ…ก', - 'S': '๐Ÿ…ข', 'T': '๐Ÿ…ฃ', 'U': '๐Ÿ…ค', 'V': '๐Ÿ…ฅ', 'W': '๐Ÿ…ฆ', 'X': '๐Ÿ…ง', 'Y': '๐Ÿ…จ', 'Z': '๐Ÿ…ฉ', - '0': 'โ“ช', '1': 'โ‘ ', '2': 'โ‘ก', '3': 'โ‘ข', '4': 'โ‘ฃ', '5': 'โ‘ค', '6': 'โ‘ฅ', '7': 'โ‘ฆ', '8': 'โ‘ง', '9': 'โ‘จ', - ' ': ' ' - } - return ''.join(circled_map.get(c, c) for c in text) - -def to_small_caps(text: str) -> str: - """Convert text to small caps""" - if not text: - return "" - # Map for small caps - small_caps_map = { - 'a': 'แด€', 'b': 'ส™', 'c': 'แด„', 'd': 'แด…', 'e': 'แด‡', 'f': '๊œฐ', 'g': 'ษข', 'h': 'สœ', 'i': 'ษช', - 'j': 'แดŠ', 'k': 'แด‹', 'l': 'สŸ', 'm': 'แด', 'n': 'ษด', 'o': 'แด', 'p': 'แด˜', 'q': 'วซ', 'r': 'ส€', - 's': 's', 't': 'แด›', 'u': 'แดœ', 'v': 'แด ', 'w': 'แดก', 'x': 'x', 'y': 'ส', 'z': 'แดข', - 'A': 'A', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'E', 'F': 'F', 'G': 'G', 'H': 'H', 'I': 'I', - 'J': 'J', 'K': 'K', 'L': 'L', 'M': 'M', 'N': 'N', 'O': 'O', 'P': 'P', 'Q': 'Q', 'R': 'R', - 'S': 'S', 'T': 'T', 'U': 'U', 'V': 'V', 'W': 'W', 'X': 'X', 'Y': 'Y', 'Z': 'Z', - ' ': ' ' - } - return ''.join(small_caps_map.get(c, c) for c in text) - -def to_braille(text: str) -> str: - """Convert text to braille""" - if not text: - return "" - # Map for braille - braille_map = { - 'a': 'โ ', 'b': 'โ ƒ', 'c': 'โ ‰', 'd': 'โ ™', 'e': 'โ ‘', 'f': 'โ ‹', 'g': 'โ ›', 'h': 'โ “', 'i': 'โ Š', - 'j': 'โ š', 'k': 'โ …', 'l': 'โ ‡', 'm': 'โ ', 'n': 'โ ', 'o': 'โ •', 'p': 'โ ', 'q': 'โ Ÿ', 'r': 'โ —', - 's': 'โ Ž', 't': 'โ ž', 'u': 'โ ฅ', 'v': 'โ ง', 'w': 'โ บ', 'x': 'โ ญ', 'y': 'โ ฝ', 'z': 'โ ต', - 'A': 'โ  โ ', 'B': 'โ  โ ƒ', 'C': 'โ  โ ‰', 'D': 'โ  โ ™', 'E': 'โ  โ ‘', 'F': 'โ  โ ‹', 'G': 'โ  โ ›', 'H': 'โ  โ “', 'I': 'โ  โ Š', - 'J': 'โ  โ š', 'K': 'โ  โ …', 'L': 'โ  โ ‡', 'M': 'โ  โ ', 'N': 'โ  โ ', 'O': 'โ  โ •', 'P': 'โ  โ ', 'Q': 'โ  โ Ÿ', 'R': 'โ  โ —', - 'S': 'โ  โ Ž', 'T': 'โ  โ ž', 'U': 'โ  โ ฅ', 'V': 'โ  โ ง', 'W': 'โ  โ บ', 'X': 'โ  โ ญ', 'Y': 'โ  โ ฝ', 'Z': 'โ  โ ต', - '0': 'โ ผโ š', '1': 'โ ผโ ', '2': 'โ ผโ ƒ', '3': 'โ ผโ ‰', '4': 'โ ผโ ™', '5': 'โ ผโ ‘', '6': 'โ ผโ ‹', '7': 'โ ผโ ›', '8': 'โ ผโ “', '9': 'โ ผโ Š', - ' ': ' ' - } - return ''.join(braille_map.get(c, c) for c in text) - -def to_bubble(text: str) -> str: - """Convert text to bubble letters""" - if not text: - return "" - bubble_map = { - 'a': 'โ“', 'b': 'โ“‘', 'c': 'โ“’', 'd': 'โ““', 'e': 'โ“”', 'f': 'โ“•', 'g': 'โ“–', 'h': 'โ“—', 'i': 'โ“˜', - 'j': 'โ“™', 'k': 'โ“š', 'l': 'โ“›', 'm': 'โ“œ', 'n': 'โ“', 'o': 'โ“ž', 'p': 'โ“Ÿ', 'q': 'โ“ ', 'r': 'โ“ก', - 's': 'โ“ข', 't': 'โ“ฃ', 'u': 'โ“ค', 'v': 'โ“ฅ', 'w': 'โ“ฆ', 'x': 'โ“ง', 'y': 'โ“จ', 'z': 'โ“ฉ', - 'A': 'โ’ถ', 'B': 'โ’ท', 'C': 'โ’ธ', 'D': 'โ’น', 'E': 'โ’บ', 'F': 'โ’ป', 'G': 'โ’ผ', 'H': 'โ’ฝ', 'I': 'โ’พ', - 'J': 'โ’ฟ', 'K': 'โ“€', 'L': 'โ“', 'M': 'โ“‚', 'N': 'โ“ƒ', 'O': 'โ“„', 'P': 'โ“…', 'Q': 'โ“†', 'R': 'โ“‡', - 'S': 'โ“ˆ', 'T': 'โ“‰', 'U': 'โ“Š', 'V': 'โ“‹', 'W': 'โ“Œ', 'X': 'โ“', 'Y': 'โ“Ž', 'Z': 'โ“', - ' ': ' ' - } - return ''.join(bubble_map.get(c, c) for c in text) - -def to_medieval(text: str) -> str: - """Convert text to medieval-style characters""" - if not text: - return "" - medieval_map = { - 'a': '๐”ž', 'b': '๐”Ÿ', 'c': '๐” ', 'd': '๐”ก', 'e': '๐”ข', 'f': '๐”ฃ', 'g': '๐”ค', 'h': '๐”ฅ', 'i': '๐”ฆ', - 'j': '๐”ง', 'k': '๐”จ', 'l': '๐”ฉ', 'm': '๐”ช', 'n': '๐”ซ', 'o': '๐”ฌ', 'p': '๐”ญ', 'q': '๐”ฎ', 'r': '๐”ฏ', - 's': '๐”ฐ', 't': '๐”ฑ', 'u': '๐”ฒ', 'v': '๐”ณ', 'w': '๐”ด', 'x': '๐”ต', 'y': '๐”ถ', 'z': '๐”ท', - 'A': '๐”„', 'B': '๐”…', 'C': 'โ„ญ', 'D': '๐”‡', 'E': '๐”ˆ', 'F': '๐”‰', 'G': '๐”Š', 'H': 'โ„Œ', 'I': 'โ„‘', - 'J': '๐”', 'K': '๐”Ž', 'L': '๐”', 'M': '๐”', 'N': '๐”‘', 'O': '๐”’', 'P': '๐”“', 'Q': '๐””', 'R': 'โ„œ', - 'S': '๐”–', 'T': '๐”—', 'U': '๐”˜', 'V': '๐”™', 'W': '๐”š', 'X': '๐”›', 'Y': '๐”œ', 'Z': 'โ„จ', - ' ': ' ' - } - return ''.join(medieval_map.get(c, c) for c in text) - -def to_morse(text: str) -> str: - """Convert text to Morse code""" - if not text: - return "" - morse_map = { - 'a': '.-', 'b': '-...', 'c': '-.-.', 'd': '-..', 'e': '.', 'f': '..-.', - 'g': '--.', 'h': '....', 'i': '..', 'j': '.---', 'k': '-.-', 'l': '.-..', - 'm': '--', 'n': '-.', 'o': '---', 'p': '.--.', 'q': '--.-', 'r': '.-.', - 's': '...', 't': '-', 'u': '..-', 'v': '...-', 'w': '.--', 'x': '-..-', - 'y': '-.--', 'z': '--..', '0': '-----', '1': '.----', '2': '..---', - '3': '...--', '4': '....-', '5': '.....', '6': '-....', '7': '--...', - '8': '---..', '9': '----.', ' ': ' ' - } - return ' '.join(morse_map.get(c.lower(), c) for c in text) - -def to_binary(text: str) -> str: - """Convert text to binary""" - if not text: - return "" - return ' '.join(format(ord(c), '08b') for c in text) - -def to_strikethrough(text: str) -> str: - """Convert text to strikethrough""" - if not text: - return "" - return ''.join(c + 'ฬถ' for c in text) - -def to_fullwidth(text: str) -> str: - """Convert text to fullwidth characters""" - if not text: - return "" - # Convert to fullwidth (shift ASCII range) - return ''.join(chr(ord(c) + 0xFEE0) if 0x21 <= ord(c) <= 0x7E else c for c in text) - -def to_mirror(text: str) -> str: - """Convert text to mirrored characters""" - if not text: - return "" - mirror_map = { - 'a': 'ษ’', 'b': 'd', 'c': 'ษ”', 'd': 'b', 'e': 'ษ˜', 'f': 'แŽธ', 'g': 'วซ', 'h': 'สœ', - 'i': 'i', 'j': 'ฤฏ', 'k': 'สž', 'l': '|', 'm': 'm', 'n': 'แดŽ', 'o': 'o', 'p': 'q', - 'q': 'p', 'r': 'ษฟ', 's': 'ฦจ', 't': 'ฦš', 'u': 'u', 'v': 'v', 'w': 'w', 'x': 'x', - 'y': 'y', 'z': 'z', ' ': ' ' - } - return ''.join(mirror_map.get(c.lower(), c) for c in text)[::-1] - -def to_wavey(text: str) -> str: - """Convert text to wavey style using combining characters""" - if not text: - return "" - wave_marks = ['ฬพ', 'อ‚', 'ฬฝ', 'อŒ'] - return ''.join(c + random.choice(wave_marks) for c in text)