mirror of
https://github.com/elder-plinius/P4RS3LT0NGV3.git
synced 2026-05-25 17:37:52 +02:00
refactor: migrate to modular tool-based architecture
- Implement tool registry system with individual tool modules - Reorganize transformers into categorized source modules - Remove emojiLibrary.js, consolidate into EmojiUtils and emojiData - Fix mobile close button and tooltip functionality - Add build system for transforms and emoji data - Migrate from Python backend to pure JavaScript - Add comprehensive documentation and testing - Improve code organization and maintainability - Ignore generated files (transforms-bundle.js, emojiData.js)
This commit is contained in:
@@ -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://<username>.github.io/<repository-name>/`
|
||||
|
||||
### 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
|
||||
|
||||
@@ -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
|
||||
+67
@@ -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/
|
||||
+412
@@ -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 <repo-url>
|
||||
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 `
|
||||
<!-- HTML template for this tool's tab -->
|
||||
<div class="my-feature-layout">
|
||||
<textarea v-model="myInput"></textarea>
|
||||
<div>{{ myOutput }}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
<script src="js/myUtility.js"></script>
|
||||
```
|
||||
|
||||
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! 🎉
|
||||
|
||||
@@ -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! 🎯
|
||||
-262
@@ -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! 🐉✨
|
||||
@@ -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.
|
||||
|
||||
@@ -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`
|
||||
@@ -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: <codepoints> ; <status> # <emoji> <version> <name>
|
||||
*/
|
||||
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`);
|
||||
}
|
||||
@@ -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(', ')}`);
|
||||
|
||||
Executable
+135
@@ -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}`);
|
||||
@@ -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 =>
|
||||
` <script src="js/tools/${file}"></script>`
|
||||
).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 = '<script src="js/tools/Tool.js"></script>';
|
||||
const registryMarker = '<script src="js/core/toolRegistry.js"></script>';
|
||||
|
||||
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`);
|
||||
|
||||
@@ -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 = '<div id="tool-content-container">';
|
||||
const endMarker = '</div>\n\n </div>\n\n <!-- Copy History Panel -->';
|
||||
|
||||
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 = `
|
||||
<!-- Tool templates injected from templates/ directory -->
|
||||
${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`);
|
||||
|
||||
+1642
-245
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
<div v-if="activeTab === 'mytool'" class="tab-content">
|
||||
<div class="transform-layout">
|
||||
<textarea v-model="myInput" @input="processInput"></textarea>
|
||||
<div v-if="myOutput">{{ myOutput }}</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 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)
|
||||
@@ -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 `
|
||||
<div class="static-content">
|
||||
<h1>Hello World</h1>
|
||||
<button onclick="doSomething()">Click</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
❌ **Doesn't Work in `getTabContentHTML()`:**
|
||||
```javascript
|
||||
getTabContentHTML() {
|
||||
return `
|
||||
<div v-if="activeTab === 'mytool'">
|
||||
<input v-model="myData" />
|
||||
<p>{{ myData }}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
## 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
|
||||
<!-- My Tool Tab -->
|
||||
<div v-if="activeTab === 'mytool'" class="tab-content">
|
||||
<div class="transform-layout">
|
||||
<input v-model="myInput" placeholder="Enter text..." />
|
||||
<button @click="processData">Process</button>
|
||||
<div>{{ myOutput }}</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Step 3: Register Tool
|
||||
|
||||
```javascript
|
||||
// js/tools/index.js
|
||||
if (typeof MyTool !== 'undefined') {
|
||||
window.toolRegistry.register(new MyTool());
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Add Script Tag
|
||||
|
||||
```html
|
||||
<!-- index.html -->
|
||||
<script src="js/tools/MyTool.js"></script>
|
||||
```
|
||||
|
||||
## 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 `<component :is="currentTool">`
|
||||
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
|
||||
|
||||
@@ -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
|
||||
<div class="section-header-card">
|
||||
<div class="section-header-card-title">
|
||||
<i class="fas fa-book"></i>
|
||||
<h3>Gibberish Dictionary</h3>
|
||||
</div>
|
||||
<p class="section-header-card-description">
|
||||
Translate text into random gibberish and corresponding dictionary.
|
||||
</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Simple Title with Icon
|
||||
|
||||
Use for inline titles with optional subtitles.
|
||||
|
||||
**Responsive:** Wraps naturally
|
||||
|
||||
```html
|
||||
<div class="title-with-icon">
|
||||
<i class="fas fa-magic"></i>
|
||||
<h3>Universal Decoder</h3>
|
||||
<small>Prioritizing Base64</small>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 Info Boxes
|
||||
|
||||
Use for tips, warnings, success messages, or disclaimers.
|
||||
|
||||
**Variants:** `.info-box-warning`, `.info-box-success`, `.info-box-danger`
|
||||
|
||||
```html
|
||||
<!-- Default (info) -->
|
||||
<div class="info-box">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<span>Copy this text and share it. The transformation can be reversed.</span>
|
||||
</div>
|
||||
|
||||
<!-- Warning -->
|
||||
<div class="info-box info-box-warning">
|
||||
<i class="fas fa-triangle-exclamation"></i>
|
||||
<span>DISCLAIMER: Use for testing only. Do not deploy to production.</span>
|
||||
</div>
|
||||
|
||||
<!-- Success -->
|
||||
<div class="info-box info-box-success">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span>Settings applied successfully!</span>
|
||||
</div>
|
||||
|
||||
<!-- Danger -->
|
||||
<div class="info-box info-box-danger">
|
||||
<i class="fas fa-radiation"></i>
|
||||
<span>Danger zone: This will freeze your browser!</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🃏 Card Container
|
||||
|
||||
Use for grouped content sections.
|
||||
|
||||
**Responsive:** Full width, proper padding adjustments
|
||||
|
||||
```html
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4>Card Title</h4>
|
||||
<button class="btn btn-secondary">Action</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Your main content goes here...</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<small>Optional footer information</small>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎛️ Button Groups
|
||||
|
||||
Use for multiple action buttons that should stay together.
|
||||
|
||||
**Responsive:** Stacks vertically on very small screens (< 400px)
|
||||
|
||||
```html
|
||||
<div class="button-group">
|
||||
<button class="btn btn-primary">
|
||||
<i class="fas fa-hammer"></i> Generate
|
||||
</button>
|
||||
<button class="btn">
|
||||
<i class="fas fa-copy"></i> Copy All
|
||||
</button>
|
||||
<button class="btn btn-secondary">
|
||||
<i class="fas fa-download"></i> Download
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔘 Button Variants
|
||||
|
||||
Standard button classes:
|
||||
|
||||
- `.btn` - Base button (default gray)
|
||||
- `.btn-primary` - Primary action (blue accent)
|
||||
- `.btn-secondary` - Secondary action (transparent with border)
|
||||
|
||||
```html
|
||||
<button class="btn">Default Button</button>
|
||||
<button class="btn btn-primary">Primary Action</button>
|
||||
<button class="btn btn-secondary">Cancel</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 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
|
||||
<label>
|
||||
Input Label
|
||||
<input type="text" placeholder="Automatically styled!">
|
||||
<small>Optional helper text</small>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Select Dropdown (custom styled with arrow)
|
||||
<select>
|
||||
<option>Option 1</option>
|
||||
<option>Option 2 with longer text</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Text Area
|
||||
<textarea placeholder="Also styled automatically!"></textarea>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Number Input
|
||||
<input type="number" min="0" max="100" value="50">
|
||||
</label>
|
||||
```
|
||||
|
||||
**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
|
||||
<div class="options-grid">
|
||||
<label>
|
||||
First Name
|
||||
<input type="text" placeholder="John">
|
||||
</label>
|
||||
<label>
|
||||
Last Name
|
||||
<input type="text" placeholder="Doe">
|
||||
</label>
|
||||
<label>
|
||||
Email
|
||||
<input type="email" placeholder="john@example.com">
|
||||
</label>
|
||||
</div>
|
||||
```
|
||||
|
||||
**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
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<text x="50" y="70" font-size="60" text-anchor="middle">🐉</text>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 141 B |
-1256
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,233 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Parseltongue 2.0 - LLM Payload Crafter</title>
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="css/notification.css">
|
||||
<!-- Vue.js (Production Build) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"></script>
|
||||
<!-- Font Awesome for icons -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="container">
|
||||
<header>
|
||||
<div class="logo">
|
||||
<h1>🐉️︎︎︎︎︎︎︎️︎︎︎️︎︎︎️︎️️︎️️︎︎︎︎︎︎︎️︎︎︎️︎︎︎️︎️️︎️️︎︎︎︎︎︎︎️︎︎︎️︎︎︎️︎️️︎️️︎︎︎︎︎︎︎️︎︎︎️︎︎︎️︎️️︎️︎︎️️️️︎︎︎️️️️️︎︎︎️︎︎︎️️️︎️︎︎️️️️︎️︎︎︎️︎︎︎️︎︎️️︎️︎️︎︎️️️️︎️︎︎︎️︎︎︎️︎︎︎️︎️︎︎️️️︎️︎︎️︎︎︎️︎️︎️︎︎️️️︎︎️︎︎︎︎︎️︎️︎︎︎︎️︎︎️︎︎️️︎︎︎️︎︎︎️︎️︎️︎︎︎️︎︎︎︎️︎️️️︎︎︎️︎️️️︎︎︎️︎️️️︎︎️︎︎️️︎︎︎️︎︎️️️️︎️︎️︎️️︎︎️︎︎︎️︎️︎︎️️️︎︎️︎️︎️︎︎︎︎︎︎️︎︎️️︎︎︎️︎︎️︎︎️︎️︎︎️️️︎︎️︎️️︎︎️︎️️️️️︎︎︎︎️️️️️︎︎︎️︎️️︎️️︎︎︎︎︎︎︎️︎︎︎️︎︎︎️︎️️︎️️︎︎︎︎︎︎︎️︎︎︎️︎︎︎️︎️️︎️️︎︎︎︎︎︎︎️︎︎︎️︎︎︎️︎️️︎️️︎︎︎︎︎︎︎️︎︎︎️︎︎︎️︎️️︎️️︎︎︎︎︎︎︎️︎︎︎️︎︎︎️︎️️︎️️︎︎︎︎︎︎︎️︎︎︎️︎ P4RS3LT0NGV3</h1>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button
|
||||
@click="toggleCopyHistory"
|
||||
class="history-button"
|
||||
title="Show copy history"
|
||||
aria-label="Show copy history"
|
||||
>
|
||||
<i class="fas fa-history"></i>
|
||||
</button>
|
||||
<button
|
||||
@click="toggleTheme"
|
||||
@keyup.d="toggleTheme"
|
||||
class="theme-button"
|
||||
title="Toggle dark mode (D)"
|
||||
aria-label="Toggle dark mode"
|
||||
>
|
||||
<i class="fas" :class="isDarkTheme ? 'fa-moon' : 'fa-sun'"></i>
|
||||
</button>
|
||||
<a
|
||||
href="https://github.com/elder-plinius/P4RS3LT0NGV3"
|
||||
target="_blank"
|
||||
class="github-button"
|
||||
title="View source on GitHub"
|
||||
aria-label="View source code on GitHub"
|
||||
>
|
||||
<i class="fab fa-github"></i>
|
||||
</a>
|
||||
<button
|
||||
@click="toggleUnicodePanel"
|
||||
class="history-button"
|
||||
title="Advanced Settings"
|
||||
aria-label="Advanced Settings"
|
||||
>
|
||||
<i class="fas fa-sliders-h"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="tabs">
|
||||
<div class="tab-buttons">
|
||||
<!-- Dynamically generated tab buttons from tool registry -->
|
||||
<button
|
||||
v-for="tool in registeredTools"
|
||||
:key="tool.id"
|
||||
:class="{ active: activeTab === tool.id }"
|
||||
@click="switchToTab(tool.id)"
|
||||
:title="tool.title"
|
||||
>
|
||||
<i :class="'fas ' + tool.icon"></i> {{ tool.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="tool-content-container">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Copy History Panel -->
|
||||
<div class="copy-history-panel" :class="{ 'active': showCopyHistory }">
|
||||
<div class="copy-history-header">
|
||||
<h3><i class="fas fa-history"></i> Copy History</h3>
|
||||
<div class="header-actions">
|
||||
<button
|
||||
v-if="copyHistory.length > 0"
|
||||
@click.stop="clearCopyHistory"
|
||||
class="clear-history-button"
|
||||
title="Clear all history"
|
||||
>
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<button class="close-button" @click="toggleCopyHistory" title="Close history">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy-history-content">
|
||||
<div v-if="copyHistory.length === 0" class="no-history">
|
||||
<p>No copy history yet. Use the app features to auto-copy content.</p>
|
||||
</div>
|
||||
<div v-else class="history-items">
|
||||
<div v-for="(item, index) in copyHistory" :key="item.id || index" class="history-item">
|
||||
<div class="history-item-header">
|
||||
<span class="history-source">{{ item.source }}</span>
|
||||
<span class="history-time">{{ formatHistoryTime(item.timestamp) }}</span>
|
||||
</div>
|
||||
<div class="history-content">
|
||||
{{ item.content }}
|
||||
</div>
|
||||
<div class="history-actions">
|
||||
<button class="copy-again-button" @click="copyToClipboard(item.content)" title="Copy again">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
<button class="remove-history-button" @click.stop="removeFromCopyHistory(item.id)" title="Remove from history">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End of .tabs div -->
|
||||
|
||||
<!-- Advanced Settings Panel (inside app so Vue bindings work) -->
|
||||
<div id="unicode-options-panel" class="unicode-options-panel">
|
||||
<div class="unicode-panel-header">
|
||||
<h3><i class="fas fa-sliders-h"></i> Advanced Settings</h3>
|
||||
<button class="close-button" title="Close Advanced Settings"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="unicode-panel-content options-grid steg-adv-panel">
|
||||
<label>
|
||||
Initial Presentation
|
||||
<select class="steg-initial-presentation">
|
||||
<option value="emoji">Emoji (VS16)</option>
|
||||
<option value="text">Text (VS15)</option>
|
||||
<option value="none">None</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Bit-0 Selector
|
||||
<select class="steg-vs-zero">
|
||||
<option value="\ufe0e">VS15 (\ufe0e)</option>
|
||||
<option value="\ufe0f">VS16 (\ufe0f)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Bit-1 Selector
|
||||
<select class="steg-vs-one">
|
||||
<option value="\ufe0f">VS16 (\ufe0f)</option>
|
||||
<option value="\ufe0e">VS15 (\ufe0e)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Inter-bit Zero-Width
|
||||
<select class="steg-inter-zw">
|
||||
<option value="">None</option>
|
||||
<option value="\u200C">ZWNJ (\u200C)</option>
|
||||
<option value="\u200D">ZWJ (\u200D)</option>
|
||||
<option value="\u200B">ZWSP (\u200B)</option>
|
||||
<option value="\ufeff">BOM (\ufeff)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Inter-bit Every N bits
|
||||
<input class="steg-inter-every" type="number" min="1" max="8" value="1" />
|
||||
</label>
|
||||
<label>
|
||||
Bit Order
|
||||
<select class="steg-bit-order">
|
||||
<option value="msb">MSB First</option>
|
||||
<option value="lsb">LSB First</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Trailing Zero-Width
|
||||
<select class="steg-trailing-zw">
|
||||
<option value="\u200B">ZWSP (\u200B)</option>
|
||||
<option value="\u200C">ZWNJ (\u200C)</option>
|
||||
<option value="\u200D">ZWJ (\u200D)</option>
|
||||
<option value="\ufeff">BOM (\ufeff)</option>
|
||||
<option value="">None</option>
|
||||
</select>
|
||||
</label>
|
||||
<div style="display:flex; align-items:center; gap:10px;">
|
||||
<button class="apply-steg-options" :class="{ applied: unicodeApplyFlash }" :disabled="unicodeApplyBusy" @click="applyUnicodeOptions" title="These options affect Unicode-based steganography encoding/decoding.">Apply</button>
|
||||
<small v-if="unicodeApplyFlash" class="apply-status">Applied</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- End of #app div -->
|
||||
|
||||
<!-- Load JavaScript files after Vue template -->
|
||||
<!-- Data files (generated/static data) -->
|
||||
<script src="js/data/emojiData.js"></script>
|
||||
<script src="js/data/emojiCompatibility.js"></script>
|
||||
|
||||
<!-- Generated bundles -->
|
||||
<script src="js/bundles/transforms-bundle.js"></script>
|
||||
|
||||
<!-- Load Configuration and Utilities (before modules that depend on them) -->
|
||||
<script src="js/config/constants.js"></script>
|
||||
<script src="js/utils/escapeParser.js"></script>
|
||||
<script src="js/utils/focus.js"></script>
|
||||
<script src="js/utils/notifications.js"></script>
|
||||
<script src="js/utils/history.js"></script>
|
||||
<script src="js/utils/clipboard.js"></script>
|
||||
<script src="js/utils/theme.js"></script>
|
||||
<script src="js/utils/emoji.js"></script>
|
||||
|
||||
<!-- Core modules (feature libraries) -->
|
||||
<script src="js/core/steganography.js"></script>
|
||||
<script src="js/core/decoder.js"></script>
|
||||
|
||||
<!-- Load Tool System -->
|
||||
<script src="js/tools/Tool.js"></script>
|
||||
<script src="js/tools/DecodeTool.js"></script>
|
||||
<script src="js/tools/EmojiTool.js"></script>
|
||||
<script src="js/tools/GibberishTool.js"></script>
|
||||
<script src="js/tools/MutationTool.js"></script>
|
||||
<script src="js/tools/SplitterTool.js"></script>
|
||||
<script src="js/tools/TokenadeTool.js"></script>
|
||||
<script src="js/tools/TokenizerTool.js"></script>
|
||||
<script src="js/tools/TransformTool.js"></script>
|
||||
<script src="js/core/toolRegistry.js"></script>
|
||||
|
||||
<script src="js/app.js"></script>
|
||||
|
||||
<!-- UI Initialization is handled in app.js mounted() lifecycle hook -->
|
||||
</body>
|
||||
</html>
|
||||
<!-- redeploy trigger: semaphore + tokenizer updates -->
|
||||
@@ -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)
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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<binary.length;i++) {
|
||||
const bit = binary[i];
|
||||
result += bit === '0' ? vs0 : vs1;
|
||||
if (__stegOptions__.interBitZW && i < binary.length-1 && ((i+1) % Math.max(1, __stegOptions__.interBitEvery)) === 0) {
|
||||
result += __stegOptions__.interBitZW;
|
||||
}
|
||||
}
|
||||
|
||||
if (__stegOptions__.trailingZW) {
|
||||
result += __stegOptions__.trailingZW;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function decodeEmoji(text) {
|
||||
if (!text) return '';
|
||||
|
||||
const emojiMatch = findEmojiMatch(text);
|
||||
if (!emojiMatch) return '';
|
||||
|
||||
const emojiChar = emojiMatch[1];
|
||||
const emojiIndex = emojiMatch.index;
|
||||
|
||||
const fromEmoji = text.substring(emojiIndex);
|
||||
const emojiCharEscaped = emojiChar.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const pattern = new RegExp(`^${emojiCharEscaped}([\ufe0e\ufe0f\u200B\u200C\u200D\ufeff]+)`, 'u');
|
||||
const emojiData = fromEmoji.match(pattern);
|
||||
|
||||
if (!emojiData || !emojiData[1]) return '';
|
||||
|
||||
const rawSeq = emojiData[1];
|
||||
const matches = [...rawSeq.matchAll(/[\ufe0e\ufe0f]/g)];
|
||||
if (matches.length === 0) return '';
|
||||
|
||||
const skip = (__stegOptions__.initialPresentation === 'none') ? 0 : 1;
|
||||
if (matches.length <= skip) return '';
|
||||
|
||||
const zeroSel = __stegOptions__.bitZeroVS || '\ufe0e';
|
||||
const oneSel = __stegOptions__.bitOneVS || '\ufe0f';
|
||||
let binary = matches.slice(skip).map(m => 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
|
||||
};
|
||||
@@ -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<Tool>}
|
||||
*/
|
||||
getAll() {
|
||||
return this.toolsArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all enabled tools
|
||||
* @returns {Array<Tool>}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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<string>} allEmojis - Full list of emojis to test
|
||||
* @param {Function} progressCallback - Optional callback (tested, total, compatible)
|
||||
* @returns {Promise<Array<string>>} - 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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 = '<h3><i class="fas fa-icons"></i> Choose an Emoji</h3><p class="emoji-subtitle"><i class="fas fa-magic"></i> Click any emoji to copy your hidden message</p>';
|
||||
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);
|
||||
};
|
||||
@@ -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<binary.length;i++) {
|
||||
const bit = binary[i];
|
||||
result += bit === '0' ? vs0 : vs1;
|
||||
if (__stegOptions__.interBitZW && i < binary.length-1 && ((i+1) % Math.max(1, __stegOptions__.interBitEvery)) === 0) {
|
||||
result += __stegOptions__.interBitZW;
|
||||
}
|
||||
}
|
||||
|
||||
// Optional trailing zero-width character
|
||||
if (__stegOptions__.trailingZW) {
|
||||
try { result += eval(`'${__stegOptions__.trailingZW}'`); } catch (_) { result += '\u200B'; }
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const carriers = [
|
||||
{
|
||||
emoji: '🐍',
|
||||
name: 'SNAKE',
|
||||
desc: 'Classic Snake',
|
||||
preview: function(text) {
|
||||
// Show actual encoded result for preview
|
||||
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);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Emoji encoding/decoding
|
||||
function encodeEmoji(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<binary.length;i++) {
|
||||
const bit = binary[i];
|
||||
result += bit === '0' ? vs0 : vs1;
|
||||
if (__stegOptions__.interBitZW && i < binary.length-1 && ((i+1) % Math.max(1, __stegOptions__.interBitEvery)) === 0) {
|
||||
result += __stegOptions__.interBitZW;
|
||||
}
|
||||
}
|
||||
|
||||
// Optional trailing zero-width character (helps with rendering in many browsers)
|
||||
if (__stegOptions__.trailingZW) {
|
||||
try { result += eval(`'${__stegOptions__.trailingZW}'`); } catch (_) { result += '\u200B'; }
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function decodeEmoji(text) {
|
||||
if (!text) return '';
|
||||
|
||||
// Find the first emoji character (looking for common emoji Unicode ranges)
|
||||
const emojiMatch = text.match(/^([\u{1F300}-\u{1F6FF}\u{2600}-\u{26FF}\u{1F1E6}-\u{1F1FF}])/u);
|
||||
if (!emojiMatch) return '';
|
||||
|
||||
// Extract variation selectors - remove any zero-width spaces first
|
||||
text = text.replace(/\u200B/g, '');
|
||||
|
||||
// Only extract the emoji and its variation selectors, ignoring other content
|
||||
// This prevents random characters from being included in the decoded result
|
||||
const emojiChar = emojiMatch[1];
|
||||
// Allow zero-width chars interleaved, but capture only variation selectors
|
||||
const pattern = new RegExp(`^${emojiChar}([\ufe0e\ufe0f\u200B\u200C\u200D\ufeff]+)`, 'u');
|
||||
const emojiData = text.match(pattern);
|
||||
|
||||
if (!emojiData || !emojiData[1]) return '';
|
||||
|
||||
// Extract variation selectors only
|
||||
const rawSeq = emojiData[1];
|
||||
const matches = [...rawSeq.matchAll(/[\ufe0e\ufe0f]/g)];
|
||||
if (matches.length === 0) return '';
|
||||
// Decide if the first selector is presentation
|
||||
const skip = (__stegOptions__.initialPresentation === 'none') ? 0 : 1;
|
||||
if (matches.length <= skip) return '';
|
||||
const zeroSel = __stegOptions__.bitZeroVS || '\ufe0e';
|
||||
const oneSel = __stegOptions__.bitOneVS || '\ufe0f';
|
||||
let binary = matches.slice(skip).map(m => 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
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<seedStr.length;i++) {
|
||||
h = Math.imul(h ^ seedStr.charCodeAt(i), 3432918353);
|
||||
h = (h << 13) | (h >>> 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;
|
||||
}
|
||||
|
||||
@@ -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<seedStr.length;i++) {
|
||||
h = Math.imul(h ^ seedStr.charCodeAt(i), 3432918353);
|
||||
h = (h << 13) | (h >>> 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<Math.max(1,Math.min(500,Number(this.fuzzerCount)||1)); i++) {
|
||||
let s = src;
|
||||
if (this.fuzzUseRandomMix) {
|
||||
try { s = window.transforms.randomizer.func(s, { minTransforms:2, maxTransforms:4 }); } catch(_) {}
|
||||
}
|
||||
if (this.fuzzZeroWidth) s = this.injectZeroWidth(s, rnd);
|
||||
if (this.fuzzUnicodeNoise) s = this.injectUnicodeNoise(s, rnd);
|
||||
if (this.fuzzWhitespace) s = this.whitespaceChaos(s, rnd);
|
||||
if (this.fuzzCasing) s = this.casingChaos(s, rnd);
|
||||
if (this.fuzzZalgo) { try { s = window.transforms.zalgo.func(s); } catch(_) {} }
|
||||
if (this.fuzzEncodeShuffle) s = this.encodeShuffle(s, rnd);
|
||||
out.push(s);
|
||||
}
|
||||
this.fuzzerOutputs = out;
|
||||
},
|
||||
copyAllFuzz: function() { this.copyToClipboard(this.fuzzerOutputs.join('\n')); },
|
||||
downloadFuzz: function() {
|
||||
const lines = this.fuzzerOutputs.map((s, 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<count;i++) {
|
||||
let token = base;
|
||||
if (combining) {
|
||||
const m = marks[i % marks.length];
|
||||
token += m;
|
||||
}
|
||||
if (addZW) {
|
||||
const z = zw[i % zw.length];
|
||||
token += z;
|
||||
}
|
||||
out += token;
|
||||
}
|
||||
this.textPayload = out;
|
||||
this.showNotification('Text payload generated', 'success', 'fas fa-bomb');
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = TokenadeTool;
|
||||
} else {
|
||||
window.TokenadeTool = TokenadeTool;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Tokenizer Tool - Tokenizer visualization tool
|
||||
*/
|
||||
class TokenizerTool extends Tool {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'tokenizer',
|
||||
name: 'Tokenizer',
|
||||
icon: 'fa-layer-group',
|
||||
title: 'Tokenizer visualization',
|
||||
order: 6
|
||||
});
|
||||
}
|
||||
|
||||
getVueData() {
|
||||
return {
|
||||
tokenizerInput: '',
|
||||
tokenizerEngine: 'byte',
|
||||
tokenizerTokens: [],
|
||||
tokenizerCharCount: 0,
|
||||
tokenizerWordCount: 0
|
||||
};
|
||||
}
|
||||
|
||||
getVueMethods() {
|
||||
return {
|
||||
runTokenizer: async function() {
|
||||
const text = this.tokenizerInput || '';
|
||||
const engine = this.tokenizerEngine;
|
||||
const tokens = [];
|
||||
if (!text) { this.tokenizerTokens = []; this.tokenizerCharCount = 0; this.tokenizerWordCount = 0; return; }
|
||||
if (engine === 'byte') {
|
||||
const encoder = new TextEncoder();
|
||||
const bytes = encoder.encode(text);
|
||||
for (let i=0;i<bytes.length;i++) {
|
||||
tokens.push({ id: bytes[i], text: `0x${bytes[i].toString(16).padStart(2,'0')}` });
|
||||
}
|
||||
} else if (engine === 'word') {
|
||||
const parts = text.split(/(\s+|[\.,!?:;()\[\]{}])/);
|
||||
for (const p of parts) { if (p) tokens.push({ text: p }); }
|
||||
} else if (['cl100k','o200k','p50k','r50k'].includes(engine)) {
|
||||
try {
|
||||
if (!window.gptTok) {
|
||||
window.gptTok = await import('https://cdn.jsdelivr.net/npm/gpt-tokenizer@2/+esm');
|
||||
}
|
||||
const map = { cl100k: 'cl100k_base', o200k: 'o200k_base', p50k: 'p50k_base', r50k: 'r50k_base' };
|
||||
const enc = map[engine];
|
||||
const ids = window.gptTok.encode(text, enc);
|
||||
for (const id of ids) {
|
||||
const piece = window.gptTok.decode([id], enc);
|
||||
tokens.push({ id, text: piece });
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load/use gpt-tokenizer; falling back to bytes', e);
|
||||
this.tokenizerEngine = 'byte';
|
||||
return this.runTokenizer();
|
||||
}
|
||||
} else {
|
||||
const encoder = new TextEncoder();
|
||||
const bytes = encoder.encode(text);
|
||||
for (let i=0;i<bytes.length;i++) tokens.push({ id: bytes[i], text: `0x${bytes[i].toString(16).padStart(2,'0')}` });
|
||||
}
|
||||
this.tokenizerTokens = tokens;
|
||||
this.tokenizerCharCount = Array.from(text).length;
|
||||
const wordMatches = text.trim().match(/[^\s]+/g) || [];
|
||||
this.tokenizerWordCount = wordMatches.length;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getVueWatchers() {
|
||||
return {
|
||||
tokenizerInput() {
|
||||
this.runTokenizer();
|
||||
},
|
||||
tokenizerEngine() {
|
||||
this.runTokenizer();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
onActivate(vueInstance) {
|
||||
vueInstance.$nextTick(() => vueInstance.runTokenizer());
|
||||
}
|
||||
}
|
||||
|
||||
// Export
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = TokenizerTool;
|
||||
} else {
|
||||
window.TokenizerTool = TokenizerTool;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 `
|
||||
<button
|
||||
:class="{ active: activeTab === '${this.id}' }"
|
||||
@click="switchToTab('${this.id}')"
|
||||
title="${this.title}"
|
||||
>
|
||||
<i class="fas ${this.icon}"></i> ${this.name}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
-2481
File diff suppressed because it is too large
Load Diff
@@ -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<boolean>} - 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "P4RS3LT0NGV3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
+25
-1
@@ -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"
|
||||
}
|
||||
|
||||
-498
@@ -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("""
|
||||
<style>
|
||||
/* Dark theme overrides */
|
||||
.stApp {
|
||||
background-color: #0E1117;
|
||||
color: #00FF41;
|
||||
}
|
||||
|
||||
/* Matrix-style headers */
|
||||
h1, h2, h3, h4 {
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #00FF41 !important;
|
||||
}
|
||||
|
||||
/* Custom button styling */
|
||||
.stButton > button {
|
||||
background-color: #1E1E1E;
|
||||
color: #00FF41;
|
||||
border: 1px solid #00FF41;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.stButton > button:hover {
|
||||
background-color: #00FF41;
|
||||
color: #1E1E1E;
|
||||
}
|
||||
|
||||
/* Small emoji buttons */
|
||||
.emoji-button > button {
|
||||
padding: 5px 10px;
|
||||
min-width: 60px;
|
||||
height: 60px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
/* Transform buttons */
|
||||
.transform-button > button {
|
||||
padding: 5px 10px;
|
||||
margin: 2px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Text area styling */
|
||||
.stTextArea textarea {
|
||||
background-color: #1E1E1E;
|
||||
color: #00FF41;
|
||||
border: 1px solid #00FF41;
|
||||
}
|
||||
|
||||
/* Code block styling */
|
||||
.stCodeBlock {
|
||||
background-color: #1E1E1E !important;
|
||||
}
|
||||
|
||||
/* Section dividers */
|
||||
.section-divider {
|
||||
border-top: 1px solid #00FF41;
|
||||
margin: 20px 0;
|
||||
}
|
||||
</style>
|
||||
""", 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("""<h3 style='color: #00FF41;'>LLM Payload Crafter</h3>""", unsafe_allow_html=True)
|
||||
|
||||
# Create tabs for steganography and decoder
|
||||
tab1, tab2 = st.tabs(["🔐 Steganography", "🔍 Universal Decoder"])
|
||||
|
||||
# Steganography Tab
|
||||
with tab1:
|
||||
st.markdown("""<h4>✨ Emoji Steganography</h4>""", 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("""<p style='color: #00FF41;'>Click an emoji to encode and copy to clipboard:</p>""", 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("<div class='emoji-button'></div>", 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("<div class='section-divider'></div>", unsafe_allow_html=True)
|
||||
|
||||
# Omni-Encoder Section
|
||||
st.markdown("""<h4>🔍 Omni-Encoder</h4>""", unsafe_allow_html=True)
|
||||
st.markdown("""<p style='color: #00FF41;'>Click to transform and copy to clipboard:</p>""", unsafe_allow_html=True)
|
||||
|
||||
# Create a grid of transformation options - 4 columns, 4 rows
|
||||
st.markdown("<div style='display: grid; grid-template-columns: repeat(4, 1fr); gap: 5px;'></div>", 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": "FW"},
|
||||
{"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": "vapor"},
|
||||
{"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("<div class='transform-button'></div>", 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("""<h4>👻 Invisible Text</h4>""", 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("""<h4>🖼️ Image Steganography</h4>""", 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)
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
};
|
||||
```
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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<s.length;i++) {
|
||||
const v = map[s[i]] || 0;
|
||||
const n = map[s[i+1]] || 0;
|
||||
total += v < n ? -v : v;
|
||||
}
|
||||
return String(total);
|
||||
}).join('');
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
// alternating-case transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
|
||||
name: 'Alternating Case',
|
||||
priority: 150, // Higher priority to detect before Base64
|
||||
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: 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;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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, ' ');
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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 ? '...' : '');
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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 ? '...' : '');
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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, ' ');
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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 ? '...' : '');
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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('');
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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('');
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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<len;i++) {
|
||||
pattern.push(rail);
|
||||
rail += dir;
|
||||
if (rail === 0 || rail === this.rails-1) dir *= -1;
|
||||
}
|
||||
const counts = Array(this.rails).fill(0);
|
||||
for (const r of pattern) counts[r]++;
|
||||
const railsArr = [];
|
||||
let idx = 0;
|
||||
for (let r=0;r<this.rails;r++) {
|
||||
railsArr[r] = chars.slice(idx, idx+counts[r]);
|
||||
idx += counts[r];
|
||||
}
|
||||
const positions = Array(this.rails).fill(0);
|
||||
let out = '';
|
||||
for (const r of pattern) {
|
||||
out += railsArr[r][positions[r]++];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
// rot13 transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'ROT13',
|
||||
priority: 60,
|
||||
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 '[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;
|
||||
}
|
||||
});
|
||||
@@ -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); }
|
||||
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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<text.length;i++) {
|
||||
const c = text[i];
|
||||
const code = c.charCodeAt(0);
|
||||
const k = key[j % key.length].toUpperCase().charCodeAt(0) - 65;
|
||||
if (code >= 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<text.length;i++) {
|
||||
const c = text[i];
|
||||
const code = c.charCodeAt(0);
|
||||
const k = key[j % key.length].toUpperCase().charCodeAt(0) - 65;
|
||||
if (code >= 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;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
// base45 transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
|
||||
name: 'Base45',
|
||||
priority: 290,
|
||||
alphabet: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:',
|
||||
func: function(text) {
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
const chars = [];
|
||||
for (let i=0;i<bytes.length;i+=2) {
|
||||
if (i+1 < bytes.length) {
|
||||
const x = 256*bytes[i] + bytes[i+1];
|
||||
const e = x % 45; const d = Math.floor(x/45) % 45; const c = Math.floor(x/45/45);
|
||||
chars.push(this.alphabet[e], this.alphabet[d], this.alphabet[c]);
|
||||
} else {
|
||||
const x = bytes[i];
|
||||
const e = x % 45; const d = Math.floor(x/45);
|
||||
chars.push(this.alphabet[e], this.alphabet[d]);
|
||||
}
|
||||
}
|
||||
return chars.join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return 'QED8W';
|
||||
return this.func(text.slice(0,3));
|
||||
},
|
||||
reverse: function(text) {
|
||||
const index = {}; for (let i=0;i<this.alphabet.length;i++) index[this.alphabet[i]] = i;
|
||||
const codes = [...text].map(c => index[c]).filter(v => v !== undefined);
|
||||
const out = [];
|
||||
for (let i=0;i<codes.length;i+=3) {
|
||||
if (i+2 < codes.length) {
|
||||
const x = codes[i] + codes[i+1]*45 + codes[i+2]*45*45;
|
||||
out.push(x >> 8, x & 0xFF);
|
||||
} else if (i+1 < codes.length) {
|
||||
const x = codes[i] + codes[i+1]*45;
|
||||
out.push(x & 0xFF);
|
||||
}
|
||||
}
|
||||
return new TextDecoder().decode(Uint8Array.from(out));
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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, '"')
|
||||
.replace(/'/g, ''');
|
||||
},
|
||||
preview: function(text) {
|
||||
return this.func(text);
|
||||
},
|
||||
reverse: function(text) {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, '\'');
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
// invisible-text transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
|
||||
name: 'Invisible Text',
|
||||
priority: 100, // High confidence - uses exclusive Unicode Private Use Area (U+E0000-U+E00FF)
|
||||
func: function(text) {
|
||||
if (!text) return '';
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
return Array.from(bytes)
|
||||
.map(byte => String.fromCodePoint(0xE0000 + byte))
|
||||
.join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
return '[invisible]';
|
||||
},
|
||||
reverse: function(text) {
|
||||
if (!text) return '';
|
||||
const matches = [...text.matchAll(/[\u{E0000}-\u{E00FF}]/gu)];
|
||||
if (!matches.length) return '';
|
||||
|
||||
// Convert invisible characters back to bytes
|
||||
const bytes = new Uint8Array(
|
||||
matches.map(match => match[0].codePointAt(0) - 0xE0000)
|
||||
);
|
||||
|
||||
// Use TextDecoder to properly handle UTF-8 encoded bytes (including emoji)
|
||||
return new TextDecoder().decode(bytes);
|
||||
},
|
||||
// Detector: Check for at least one invisible Unicode character
|
||||
detector: function(text) {
|
||||
// Invisible text uses Unicode Private Use Area (U+E0000-U+E00FF for full byte range)
|
||||
const invisibleMatches = text.match(/[\u{E0000}-\u{E00FF}]/gu);
|
||||
// Return true if at least one invisible character is found
|
||||
return invisibleMatches && invisibleMatches.length > 0;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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('');
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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(' ');
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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<row.length;i++) {
|
||||
const from = row[i], to = row[(i+1)%row.length];
|
||||
map[from] = to;
|
||||
map[from.toUpperCase()] = to.toUpperCase();
|
||||
}
|
||||
}
|
||||
this._map = map; return map;
|
||||
},
|
||||
func: function(text) {
|
||||
const m = this.buildMap();
|
||||
return [...text].map(c => 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('');
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
@@ -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 || [];
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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('');
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user