mirror of
https://github.com/elder-plinius/P4RS3LT0NGV3.git
synced 2026-06-06 06:53:56 +02:00
UI: Add global Advanced Unicode panel + header toggle; remove duplicate from decoder; wire Apply to steganography options; minor UI polish
This commit is contained in:
@@ -14,6 +14,8 @@ A powerful web-based text transformation and steganography tool that can encode/
|
||||
#### **Encoding & Decoding**
|
||||
- **Base64** - Standard base64 encoding/decoding
|
||||
- **Base32** - RFC 4648 compliant base32 encoding/decoding
|
||||
- **Base58** - Bitcoin alphabet encoding/decoding
|
||||
- **Base62** - 0-9A-Za-z compact encoding/decoding
|
||||
- **Binary** - Convert text to/from binary representation
|
||||
- **Hexadecimal** - Convert text to/from hex format
|
||||
- **ASCII85** - Advanced ASCII85 encoding/decoding
|
||||
@@ -26,6 +28,8 @@ A powerful web-based text transformation and steganography tool that can encode/
|
||||
- **ROT47** - Extended rotation cipher for ASCII 33-126
|
||||
- **Morse Code** - International Morse code with proper spacing
|
||||
- **NATO Phonetic** - NATO phonetic alphabet
|
||||
- **Vigenère Cipher** - Polyalphabetic cipher (default key "KEY")
|
||||
- **Rail Fence (3 Rails)** - Zig-zag transposition cipher
|
||||
|
||||
#### **Visual Transformations**
|
||||
- **Upside Down** - Flip text upside down using Unicode characters
|
||||
@@ -43,6 +47,11 @@ A powerful web-based text transformation and steganography tool that can encode/
|
||||
- **Double-Struck** - Mathematical double-struck characters
|
||||
- **Greek Letters** - Greek alphabet characters
|
||||
- **Wingdings** - Symbol font characters
|
||||
- **Fraktur** - Mathematical Fraktur alphabet
|
||||
- **Cyrillic Stylized** - Latin letters mapped to similar Cyrillic glyphs
|
||||
- **Katakana** - Romaji to Katakana (approximate, reversible)
|
||||
- **Hiragana** - Romaji to Hiragana (approximate, reversible)
|
||||
- **Roman Numerals** - Numbers to Roman numerals (reversible)
|
||||
|
||||
#### **Fantasy Languages** 🧙♂️
|
||||
- **Quenya (Tolkien Elvish)** - High Elvish language from Lord of the Rings
|
||||
@@ -128,6 +137,7 @@ streamlit run parsel_app.py
|
||||
|
||||
### **New Features**
|
||||
- 🆕 **50+ New Languages**: 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
|
||||
- 🆕 **Improved Decoder**: Better detection and fallback mechanisms
|
||||
|
||||
@@ -1848,3 +1848,96 @@ button:hover {
|
||||
30% { box-shadow: 0 0 18px rgba(156,39,176,0.6); }
|
||||
100% { box-shadow: 0 0 0 rgba(156,39,176,0); }
|
||||
}
|
||||
|
||||
/* Steganography split layout */
|
||||
.steg-split-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 280px 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.steg-advanced-sidebar {
|
||||
position: sticky;
|
||||
top: 8px;
|
||||
height: max-content;
|
||||
background: var(--main-bg-color);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.steg-adv-panel label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.steg-adv-panel select, .steg-adv-panel input[type=number] {
|
||||
background: var(--input-bg);
|
||||
color: var(--text-color);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 4px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.steg-note {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.steg-split-layout { grid-template-columns: 1fr; }
|
||||
.steg-advanced-sidebar { position: relative; top: 0; }
|
||||
}
|
||||
|
||||
/* Global Unicode options panel (subtle, like copy history) */
|
||||
.unicode-options-panel {
|
||||
position: fixed;
|
||||
right: -360px;
|
||||
top: 0;
|
||||
width: 340px;
|
||||
height: 100vh;
|
||||
background: var(--secondary-bg);
|
||||
border-left: 1px solid var(--input-border);
|
||||
z-index: 200;
|
||||
box-shadow: -5px 0 15px rgba(0,0,0,0.3);
|
||||
transition: right 0.3s ease-in-out;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.unicode-options-panel.active { right: 0; }
|
||||
|
||||
.unicode-panel-header {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--input-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.unicode-panel-header h3 { margin: 0; color: var(--accent-color); font-size: 1rem; }
|
||||
|
||||
.unicode-panel-content { padding: 12px; overflow-y: auto; }
|
||||
|
||||
/* Fluid UX: reduce scroll fatigue during repeated actions */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.transform-layout {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Keep the transform input visible while scrolling */
|
||||
.transform-layout .input-section {
|
||||
position: sticky;
|
||||
top: 8px;
|
||||
z-index: 20;
|
||||
background: var(--secondary-bg);
|
||||
border: 1px solid var(--input-border);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
+103
@@ -44,6 +44,14 @@
|
||||
>
|
||||
<i class="fab fa-github"></i>
|
||||
</a>
|
||||
<button
|
||||
@click="toggleUnicodePanel"
|
||||
class="history-button"
|
||||
title="Advanced Unicode options"
|
||||
aria-label="Advanced Unicode options"
|
||||
>
|
||||
<i class="fas fa-sliders-h"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -141,6 +149,7 @@
|
||||
</p>
|
||||
<p v-else>Paste any encoded text to try all decoding methods at once</p>
|
||||
</div>
|
||||
|
||||
<div class="input-container">
|
||||
<textarea
|
||||
id="universal-decode-input-steg"
|
||||
@@ -590,6 +599,71 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Global Advanced Unicode / Steg Options Panel -->
|
||||
<div id="unicode-options-panel" class="unicode-options-panel">
|
||||
<div class="unicode-panel-header">
|
||||
<h3><i class="fas fa-sliders-h"></i> Advanced Unicode Encoding</h3>
|
||||
<button class="close-button" @click="toggleUnicodePanel" title="Close options"><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>
|
||||
<button class="apply-steg-options">Apply</button>
|
||||
<small class="steg-note">These options affect Unicode-based steganography encoding/decoding.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/transforms.js"></script>
|
||||
<script src="js/steganography.js"></script>
|
||||
<script src="js/emojiLibrary.js"></script>
|
||||
@@ -628,6 +702,35 @@
|
||||
}, true);
|
||||
})();
|
||||
|
||||
// Wire up advanced steg options to steganography engine
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
document.querySelectorAll('.apply-steg-options').forEach(btn => {
|
||||
btn.addEventListener('click', function(e){
|
||||
e.preventDefault();
|
||||
const panel = btn.closest('.steg-adv-panel');
|
||||
if (!panel) return;
|
||||
const initSel = panel.querySelector('.steg-initial-presentation')?.value || 'emoji';
|
||||
const vs0 = panel.querySelector('.steg-vs-zero')?.value || '\\ufe0e';
|
||||
const vs1 = panel.querySelector('.steg-vs-one')?.value || '\\ufe0f';
|
||||
const inter = panel.querySelector('.steg-inter-zw')?.value || null;
|
||||
const trailing = panel.querySelector('.steg-trailing-zw')?.value || null;
|
||||
const every = parseInt(panel.querySelector('.steg-inter-every')?.value || '1', 10);
|
||||
const order = panel.querySelector('.steg-bit-order')?.value || 'msb';
|
||||
if (window.steganography && window.steganography.setStegOptions) {
|
||||
window.steganography.setStegOptions({
|
||||
initialPresentation: initSel,
|
||||
bitZeroVS: vs0,
|
||||
bitOneVS: vs1,
|
||||
interBitZW: inter,
|
||||
interBitEvery: isNaN(every) ? 1 : Math.max(1, Math.min(8, every)),
|
||||
trailingZW: trailing,
|
||||
bitOrder: order
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Function to initialize emoji grid with retries
|
||||
function initEmojiGrid(retryCount) {
|
||||
retryCount = retryCount || 0;
|
||||
|
||||
@@ -14,11 +14,11 @@ window.app = new Vue({
|
||||
activeTransform: null,
|
||||
// Transform categories for styling
|
||||
transformCategories: {
|
||||
encoding: ['Base64', 'Base32', 'Binary', 'Hexadecimal', 'ASCII85', 'URL Encode', 'HTML Entities'],
|
||||
cipher: ['Caesar Cipher', 'ROT13', 'ROT47', 'Morse Code', 'Atbash Cipher', 'ROT5'],
|
||||
encoding: ['Base64', 'Base64 URL', 'Base32', 'Base58', 'Base62', 'Binary', 'Hexadecimal', 'ASCII85', 'URL Encode', 'HTML Entities'],
|
||||
cipher: ['Caesar Cipher', 'ROT13', 'ROT47', 'Morse Code', 'Atbash Cipher', 'ROT5', 'Vigenère Cipher', 'Rail Fence (3 Rails)'],
|
||||
visual: ['Rainbow Text', 'Strikethrough', 'Underline', 'Reverse Text', 'Alternating Case', 'Reverse Words', 'Random Case', 'Title Case', 'Sentence Case', 'Emoji Speak'],
|
||||
format: ['Pig Latin', 'Leetspeak', 'NATO Phonetic', 'camelCase', 'snake_case', 'kebab-case'],
|
||||
unicode: ['Invisible Text', 'Upside Down', 'Full Width', 'Small Caps', 'Bubble', 'Braille', 'Greek Letters', 'Wingdings', 'Superscript', 'Subscript', 'Regional Indicator Letters', 'Fraktur', 'Cyrillic Stylized', 'Katakana', 'Hiragana'],
|
||||
unicode: ['Invisible Text', 'Upside Down', 'Full Width', 'Small Caps', 'Bubble', 'Braille', 'Greek Letters', 'Wingdings', 'Superscript', 'Subscript', 'Regional Indicator Letters', 'Fraktur', 'Cyrillic Stylized', 'Katakana', 'Hiragana', 'Roman Numerals'],
|
||||
special: ['Medieval', 'Cursive', 'Monospace', 'Double-Struck', 'Elder Futhark', 'Mirror Text', 'Zalgo'],
|
||||
fantasy: ['Quenya (Tolkien Elvish)', 'Tengwar Script', 'Klingon', 'Aurebesh (Star Wars)', 'Dovahzul (Dragon)'],
|
||||
ancient: ['Hieroglyphics', 'Ogham (Celtic)', 'Semaphore Flags'],
|
||||
@@ -56,9 +56,18 @@ window.app = new Vue({
|
||||
// History of copied content
|
||||
copyHistory: [],
|
||||
maxHistoryItems: 10,
|
||||
showCopyHistory: false
|
||||
showCopyHistory: false,
|
||||
showUnicodePanel: false
|
||||
},
|
||||
methods: {
|
||||
toggleUnicodePanel() {
|
||||
this.showUnicodePanel = !this.showUnicodePanel;
|
||||
const panel = document.getElementById('unicode-options-panel');
|
||||
if (panel) {
|
||||
if (this.showUnicodePanel) panel.classList.add('active');
|
||||
else panel.classList.remove('active');
|
||||
}
|
||||
},
|
||||
// Focus an element without causing the page to scroll
|
||||
focusWithoutScroll(el) {
|
||||
if (!el) return;
|
||||
@@ -1030,6 +1039,34 @@ window.app = new Vue({
|
||||
}
|
||||
}
|
||||
|
||||
// - Base58
|
||||
if (/^[1-9A-HJ-NP-Za-km-z]+$/.test(input.trim())) {
|
||||
try {
|
||||
if (window.transforms.base58 && window.transforms.base58.reverse) {
|
||||
const result = window.transforms.base58.reverse(input.trim());
|
||||
if (result && /[\x20-\x7E]{3,}/.test(result)) {
|
||||
return { text: result, method: 'Base58' };
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Base58 decode error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// - Base62
|
||||
if (/^[0-9A-Za-z]+$/.test(input.trim())) {
|
||||
try {
|
||||
if (window.transforms.base62 && window.transforms.base62.reverse) {
|
||||
const result = window.transforms.base62.reverse(input.trim());
|
||||
if (result && /[\x20-\x7E]{3,}/.test(result)) {
|
||||
return { text: result, method: 'Base62' };
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Base62 decode error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// - Upside Down text
|
||||
if (window.transforms.upside_down && window.transforms.upside_down.reverse) {
|
||||
try {
|
||||
|
||||
+64
-27
@@ -1,4 +1,19 @@
|
||||
// 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;
|
||||
@@ -9,20 +24,28 @@ function encodeForPreview(emoji, text) {
|
||||
.join('');
|
||||
|
||||
// Use variation selectors to encode binary
|
||||
const vs15 = '\ufe0e'; // text variation selector (0)
|
||||
const vs16 = '\ufe0f'; // emoji variation selector (1)
|
||||
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 + vs16; // Add emoji presentation selector first
|
||||
let result = emoji;
|
||||
if (__stegOptions__.initialPresentation === 'emoji') result += '\ufe0f';
|
||||
else if (__stegOptions__.initialPresentation === 'text') result += '\ufe0e';
|
||||
|
||||
// Add variation selectors based on binary representation
|
||||
for (const bit of binary) {
|
||||
result += bit === '0' ? vs15 : vs16;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure there's a zero-width space after the encoded content
|
||||
result += '\u200B';
|
||||
// Optional trailing zero-width character
|
||||
if (__stegOptions__.trailingZW) {
|
||||
try { result += eval(`'${__stegOptions__.trailingZW}'`); } catch (_) { result += '\u200B'; }
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -73,21 +96,28 @@ function encodeEmoji(emoji, text) {
|
||||
.join('');
|
||||
|
||||
// Use variation selectors to encode binary
|
||||
const vs15 = '\ufe0e'; // text variation selector (0)
|
||||
const vs16 = '\ufe0f'; // emoji variation selector (1)
|
||||
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 + vs16; // Add emoji presentation selector first
|
||||
let result = emoji;
|
||||
if (__stegOptions__.initialPresentation === 'emoji') result += '\ufe0f';
|
||||
else if (__stegOptions__.initialPresentation === 'text') result += '\ufe0e';
|
||||
|
||||
// Add variation selectors based on binary representation
|
||||
for (const bit of binary) {
|
||||
result += bit === '0' ? vs15 : vs16;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure there's a zero-width space after the encoded content
|
||||
// This helps with browser rendering
|
||||
result += '\u200B';
|
||||
// Optional trailing zero-width character (helps with rendering in many browsers)
|
||||
if (__stegOptions__.trailingZW) {
|
||||
try { result += eval(`'${__stegOptions__.trailingZW}'`); } catch (_) { result += '\u200B'; }
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -105,27 +135,33 @@ function decodeEmoji(text) {
|
||||
// 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];
|
||||
const pattern = new RegExp(`^${emojiChar}([\ufe0e\ufe0f]+)`, 'u');
|
||||
// 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 '';
|
||||
|
||||
// Get only the variation selectors that follow the emoji directly
|
||||
const varSelectors = emojiData[1];
|
||||
// Skip the first variation selector as it's used for presentation
|
||||
const matches = [...varSelectors.matchAll(/[\ufe0e\ufe0f]/g)];
|
||||
if (matches.length <= 1) return ''; // Need at least one bit after the presentation selector
|
||||
|
||||
// Convert variation selectors to binary, skipping the first one (presentation selector)
|
||||
const binary = matches.slice(1).map(m => m[0] === '\ufe0e' ? '0' : '1').join('');
|
||||
// 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
|
||||
// Convert binary to text (respect bitOrder)
|
||||
let decoded = '';
|
||||
for (let i = 0; i < validBinaryLength; i += 8) {
|
||||
const byte = binary.slice(i, 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
|
||||
@@ -191,5 +227,6 @@ window.steganography = {
|
||||
encodeEmoji,
|
||||
decodeEmoji,
|
||||
encodeInvisible,
|
||||
decodeInvisible
|
||||
decodeInvisible,
|
||||
setStegOptions
|
||||
};
|
||||
|
||||
+345
-1
@@ -263,6 +263,26 @@ const transforms = {
|
||||
}
|
||||
},
|
||||
|
||||
base64url: {
|
||||
name: 'Base64 URL',
|
||||
func: function(text) {
|
||||
if (!text) return '';
|
||||
const std = btoa(text);
|
||||
return std.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/,'');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[b64url]';
|
||||
return this.func(text.slice(0,3)) + '...';
|
||||
},
|
||||
reverse: function(text) {
|
||||
if (!text) return '';
|
||||
let std = text.replace(/-/g, '+').replace(/_/g, '/');
|
||||
// pad
|
||||
while (std.length % 4 !== 0) std += '=';
|
||||
try { return atob(std); } catch (e) { return text; }
|
||||
}
|
||||
},
|
||||
|
||||
hex: {
|
||||
name: 'Hexadecimal',
|
||||
func: function(text) {
|
||||
@@ -1229,6 +1249,329 @@ const transforms = {
|
||||
}
|
||||
},
|
||||
|
||||
// Base58 (Bitcoin alphabet)
|
||||
base58: {
|
||||
name: 'Base58',
|
||||
alphabet: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz',
|
||||
func: function(text) {
|
||||
if (!text) return '';
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
// Count leading zeros
|
||||
let zeros = 0;
|
||||
for (let b of bytes) { if (b === 0) zeros++; else break; }
|
||||
// Convert to BigInt
|
||||
let n = 0n;
|
||||
for (let b of bytes) { n = (n << 8n) + BigInt(b); }
|
||||
// Encode
|
||||
let out = '';
|
||||
while (n > 0n) {
|
||||
const rem = n % 58n;
|
||||
n = n / 58n;
|
||||
out = this.alphabet[Number(rem)] + out;
|
||||
}
|
||||
// Add leading zeros as '1'
|
||||
for (let i = 0; i < zeros; i++) out = '1' + out;
|
||||
return out || '1';
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[base58]';
|
||||
return this.func(text.slice(0, 3)) + '...';
|
||||
},
|
||||
reverse: function(text) {
|
||||
if (!text) return '';
|
||||
// Count leading '1's
|
||||
let zeros = 0;
|
||||
for (let c of text) { if (c === '1') zeros++; else break; }
|
||||
// Convert to BigInt
|
||||
let n = 0n;
|
||||
for (let c of text) {
|
||||
const i = this.alphabet.indexOf(c);
|
||||
if (i < 0) continue;
|
||||
n = n * 58n + BigInt(i);
|
||||
}
|
||||
// Convert BigInt to bytes
|
||||
const bytes = [];
|
||||
while (n > 0n) {
|
||||
bytes.unshift(Number(n % 256n));
|
||||
n = n / 256n;
|
||||
}
|
||||
for (let i = 0; i < zeros; i++) bytes.unshift(0);
|
||||
return new TextDecoder().decode(Uint8Array.from(bytes));
|
||||
}
|
||||
},
|
||||
|
||||
// Base62 (0-9A-Za-z)
|
||||
base62: {
|
||||
name: 'Base62',
|
||||
alphabet: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
|
||||
func: function(text) {
|
||||
if (!text) return '';
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
let n = 0n;
|
||||
for (let b of bytes) { n = (n << 8n) + BigInt(b); }
|
||||
if (n === 0n) return '0';
|
||||
let out = '';
|
||||
while (n > 0n) {
|
||||
const rem = n % 62n;
|
||||
n = n / 62n;
|
||||
out = this.alphabet[Number(rem)] + out;
|
||||
}
|
||||
return out;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[base62]';
|
||||
return this.func(text.slice(0, 3)) + '...';
|
||||
},
|
||||
reverse: function(text) {
|
||||
if (!text) return '';
|
||||
let n = 0n;
|
||||
for (let c of text) {
|
||||
const i = this.alphabet.indexOf(c);
|
||||
if (i < 0) continue;
|
||||
n = n * 62n + BigInt(i);
|
||||
}
|
||||
const bytes = [];
|
||||
while (n > 0n) {
|
||||
bytes.unshift(Number(n % 256n));
|
||||
n = n / 256n;
|
||||
}
|
||||
if (bytes.length === 0) bytes.push(0);
|
||||
return new TextDecoder().decode(Uint8Array.from(bytes));
|
||||
}
|
||||
},
|
||||
|
||||
// Roman Numerals (1..3999)
|
||||
roman_numerals: {
|
||||
name: 'Roman Numerals',
|
||||
numerals: [
|
||||
['M',1000],['CM',900],['D',500],['CD',400],
|
||||
['C',100],['XC',90],['L',50],['XL',40],
|
||||
['X',10],['IX',9],['V',5],['IV',4],['I',1]
|
||||
],
|
||||
func: function(text) {
|
||||
return text.replace(/\b\d+\b/g, m => {
|
||||
let num = parseInt(m,10);
|
||||
if (num <= 0 || num > 3999 || isNaN(num)) return m;
|
||||
let out = '';
|
||||
for (const [sym,val] of this.numerals) {
|
||||
while (num >= val) { out += sym; num -= val; }
|
||||
}
|
||||
return out;
|
||||
});
|
||||
},
|
||||
preview: function(text) {
|
||||
return this.func(text || '2024');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Greedy parse roman numerals to digits
|
||||
const map = {I:1,V:5,X:10,L:50,C:100,D:500,M:1000};
|
||||
const tokenize = s => s.match(/[IVXLCDM]+|[^IVXLCDM]+/gi) || [s];
|
||||
return tokenize(text).map(tok => {
|
||||
if (!/^[IVXLCDM]+$/i.test(tok)) return tok;
|
||||
const s = tok.toUpperCase();
|
||||
let total = 0;
|
||||
for (let i=0;i<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('');
|
||||
}
|
||||
},
|
||||
|
||||
// Vigenère Cipher (default key: KEY)
|
||||
vigenere: {
|
||||
name: 'Vigenère Cipher',
|
||||
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;
|
||||
}
|
||||
},
|
||||
|
||||
// Rail Fence Cipher (3 rails)
|
||||
rail_fence: {
|
||||
name: 'Rail Fence (3 Rails)',
|
||||
rails: 3,
|
||||
func: function(text) {
|
||||
const rails = Array.from({length: this.rails}, () => []);
|
||||
let rail = 0, dir = 1;
|
||||
for (const ch of text) {
|
||||
rails[rail].push(ch);
|
||||
rail += dir;
|
||||
if (rail === 0 || rail === this.rails-1) dir *= -1;
|
||||
}
|
||||
return rails.flat().join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[rail]';
|
||||
return this.func(text.slice(0,12)) + (text.length>12?'...':'');
|
||||
},
|
||||
reverse: function(text) {
|
||||
const len = text.length;
|
||||
const pattern = [];
|
||||
let rail = 0, dir = 1;
|
||||
for (let i=0;i<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] = text.slice(idx, idx+counts[r]).split('');
|
||||
idx += counts[r];
|
||||
}
|
||||
const positions = Array(this.rails).fill(0);
|
||||
let out = '';
|
||||
for (const r of pattern) {
|
||||
out += railsArr[r][positions[r]++];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
},
|
||||
|
||||
// ROT18 (ROT13 letters + ROT5 digits)
|
||||
rot18: {
|
||||
name: 'ROT18',
|
||||
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); }
|
||||
},
|
||||
|
||||
// A1Z26 (letters to 1-26, separated by hyphens)
|
||||
a1z26: {
|
||||
name: 'A1Z26',
|
||||
func: function(text) {
|
||||
return text.replace(/[A-Za-z]/g, c => {
|
||||
const n = (c.toUpperCase().charCodeAt(0) - 64);
|
||||
return String(n) + '-';
|
||||
}).replace(/-+(?!\d)/g,'-').replace(/-+$/,'');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[1-26]';
|
||||
return this.func(text.slice(0, 5)) + '...';
|
||||
},
|
||||
reverse: function(text) {
|
||||
return text.split(/([^0-9]+)/).map(tok => {
|
||||
if (!/^\d+$/.test(tok)) return tok;
|
||||
const n = parseInt(tok,10);
|
||||
if (n>=1 && n<=26) return String.fromCharCode(64+n).toLowerCase();
|
||||
return tok;
|
||||
}).join('');
|
||||
}
|
||||
},
|
||||
|
||||
// Affine Cipher (a=5, b=8)
|
||||
affine: {
|
||||
name: 'Affine Cipher (a=5,b=8)',
|
||||
a: 5, b: 8, m: 26, invA: 21, // 5*21 ≡ 1 (mod 26)
|
||||
func: function(text) {
|
||||
const {a,b,m} = this;
|
||||
return [...text].map(c => {
|
||||
const code = c.charCodeAt(0);
|
||||
if (code>=65 && code<=90) return String.fromCharCode(65 + ((a*(code-65)+b)%m));
|
||||
if (code>=97 && code<=122) return String.fromCharCode(97 + ((a*(code-97)+b)%m));
|
||||
return c;
|
||||
}).join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[affine]';
|
||||
return this.func(text.slice(0,8)) + (text.length>8?'...':'');
|
||||
},
|
||||
reverse: function(text) {
|
||||
const {invA,b,m} = this;
|
||||
return [...text].map(c => {
|
||||
const code = c.charCodeAt(0);
|
||||
if (code>=65 && code<=90) return String.fromCharCode(65 + ((invA*((code-65 - b + m)%m))%m));
|
||||
if (code>=97 && code<=122) return String.fromCharCode(97 + ((invA*((code-97 - b + m)%m))%m));
|
||||
return c;
|
||||
}).join('');
|
||||
}
|
||||
},
|
||||
|
||||
// QWERTY Right-Shift (maps to next key on same row)
|
||||
qwerty_shift: {
|
||||
name: 'QWERTY Right Shift',
|
||||
rows: [
|
||||
'qwertyuiop',
|
||||
'asdfghjkl',
|
||||
'zxcvbnm'
|
||||
],
|
||||
buildMap: function() {
|
||||
if (this._map) return this._map;
|
||||
const map = {};
|
||||
for (const row of this.rows) {
|
||||
for (let i=0;i<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('');
|
||||
}
|
||||
},
|
||||
|
||||
// Case/formatting transforms
|
||||
title_case: {
|
||||
name: 'Title Case',
|
||||
@@ -1711,7 +2054,8 @@ const transforms = {
|
||||
'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'
|
||||
'regional_indicator', 'fraktur', 'cyrillic_stylized', 'katakana', 'hiragana', 'emoji_speak',
|
||||
'base58', 'base62', 'roman_numerals', 'vigenere', 'rail_fence', 'base64url'
|
||||
];
|
||||
return suitable.filter(name => window.transforms[name]);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user