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:
Dustin Farley
2025-12-02 19:02:18 -08:00
parent 105084437a
commit dc10a90851
146 changed files with 12712 additions and 8171 deletions
+51
View File
@@ -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
+57
View File
@@ -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
View File
@@ -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
View File
@@ -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! 🎉
-191
View File
@@ -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
View File
@@ -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! 🐉✨
+55 -58
View File
@@ -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.
+66
View File
@@ -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`
+491
View File
@@ -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`);
}
+71
View File
@@ -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(', ')}`);
+135
View File
@@ -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}`);
+92
View File
@@ -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`);
+85
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+80
View File
@@ -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)
+181
View File
@@ -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
+256
View File
@@ -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
+4
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+233
View File
@@ -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 -->
+39
View File
@@ -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)
+265 -2256
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -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
};
+101
View File
@@ -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 };
}
+270
View File
@@ -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
};
+209
View File
@@ -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;
}
+208
View File
@@ -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;
}
};
-232
View File
@@ -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);
};
-232
View File
@@ -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
};
+96
View File
@@ -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;
}
+398
View File
@@ -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;
}
+236
View File
@@ -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;
}
+119
View File
@@ -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;
}
+267
View File
@@ -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;
}
+245
View File
@@ -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;
}
+95
View File
@@ -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;
}
+97
View File
@@ -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;
}
+193
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+51
View File
@@ -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;
}
}
};
+33
View File
@@ -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;
}
};
+40
View File
@@ -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;
}
};
+29
View File
@@ -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();
}
};
+60
View File
@@ -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';
}
};
+37
View File
@@ -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');
}
};
+37
View File
@@ -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
View File
@@ -1,6 +0,0 @@
{
"name": "P4RS3LT0NGV3",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
+25 -1
View File
@@ -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
View File
@@ -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)
+153
View File
@@ -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;
+151
View File
@@ -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 }
}
};
```
+39
View File
@@ -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);
}
});
+32
View File
@@ -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);
}
});
+32
View File
@@ -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('');
}
});
+51
View File
@@ -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;
}
});
+20
View File
@@ -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);
}
});
+37
View File
@@ -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, ' ');
}
});
+16
View File
@@ -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 ? '...' : '');
}
});
+18
View File
@@ -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 ? '...' : '');
}
});
+29
View File
@@ -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, ' ');
}
});
+16
View File
@@ -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 ? '...' : '');
}
});
+32
View File
@@ -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('');
}
});
+34
View File
@@ -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);
}
});
+40
View File
@@ -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('');
}
});
+45
View File
@@ -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;
}
});
+50
View File
@@ -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;
}
});
+37
View File
@@ -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;
}
});
+27
View File
@@ -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); }
});
+27
View File
@@ -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);
}
});
+26
View File
@@ -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);
}
});
+42
View File
@@ -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;
}
});
+111
View File
@@ -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));
}
});
+85
View File
@@ -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));
}
});
+45
View File
@@ -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));
}
});
+61
View File
@@ -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));
}
});
+44
View File
@@ -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));
}
});
+51
View File
@@ -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;
}
}
});
+53
View File
@@ -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;
}
}
});
+43
View File
@@ -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));
}
});
+40
View File
@@ -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));
}
});
+32
View File
@@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
},
preview: function(text) {
return this.func(text);
},
reverse: function(text) {
return text
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/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;
}
});
+31
View File
@@ -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;
}
}
});
+38
View File
@@ -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;
}
});
+56
View File
@@ -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;
}
});
+58
View File
@@ -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;
}
});
+36
View File
@@ -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;
}
});
+32
View File
@@ -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);
}
});
+32
View File
@@ -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('');
}
});
+143
View File
@@ -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(' ');
}
});
+40
View File
@@ -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('');
}
});
+23
View File
@@ -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);
}
});
+18
View File
@@ -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
}
});
+151
View File
@@ -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();
+146
View File
@@ -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 || [];
}
});
+53
View File
@@ -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('');
}
});
+56
View File
@@ -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;
}
});
+88
View File
@@ -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