diff --git a/css/style.css b/css/style.css
index 65453b3..d6360bd 100644
--- a/css/style.css
+++ b/css/style.css
@@ -23,12 +23,15 @@
--format-color: #ffb74d; /* Orange for formatting */
--unicode-color: #42a5f5; /* Blue for unicode transformations */
--special-color: #66bb6a; /* Green for special transformations */
+ scroll-behavior: smooth;
+ scroll-padding-top: 20px;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
+ scroll-behavior: smooth;
}
body {
@@ -38,6 +41,10 @@ body {
line-height: 1.6;
padding: 20px;
min-height: 100vh;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ font-feature-settings: "liga" 1, "calt" 1; /* Enable ligatures */
}
.container {
@@ -591,6 +598,12 @@ textarea {
max-height: none !important;
overflow: visible !important;
max-width: 100% !important;
+ font-family: 'Segoe UI Emoji', 'Segoe UI Symbol', 'Apple Color Emoji',
+ 'Noto Color Emoji', 'Android Emoji', 'EmojiOne Color',
+ 'Twemoji Mozilla', sans-serif !important;
+ /* Improve emoji rendering */
+ text-rendering: optimizeLegibility !important;
+ -webkit-font-smoothing: antialiased !important;
}
.emoji-category-tabs {
@@ -870,8 +883,16 @@ button:hover {
border-left-style: solid;
background-color: var(--button-bg);
transition: all 0.2s ease;
- cursor: default;
+ cursor: pointer;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ user-select: none;
+}
+
+.legend-item.active-category {
+ background-color: var(--accent-color);
+ color: var(--main-bg-color);
+ transform: translateY(-1px);
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.legend-item:hover {
diff --git a/js/app.js b/js/app.js
index 5ba8436..82203d9 100644
--- a/js/app.js
+++ b/js/app.js
@@ -16,8 +16,8 @@ window.app = new Vue({
transformCategories: {
encoding: ['Base64', 'Base32', 'Binary', 'Hexadecimal', 'ASCII85', 'URL Encode', 'HTML Entities'],
cipher: ['Caesar Cipher', 'ROT13', 'ROT47', 'Morse Code'],
- visual: ['Rainbow Text', 'Strikethrough', 'Underline', 'Reverse Text'],
- format: ['Pig Latin', 'Leetspeak', 'NATO Phonetic'],
+ visual: ['Rainbow Text', 'Strikethrough', 'Underline', 'Reverse Text', 'Wingdings Style'],
+ format: ['Pig Latin', 'Leetspeak', 'NATO Phonetic', 'Greek Letters'],
unicode: ['Invisible Text', 'Upside Down', 'Full Width', 'Small Caps', 'Bubble', 'Braille'],
special: ['Medieval', 'Cursive', 'Monospace', 'Double-Struck', 'Elder Futhark', 'Mirror Text', 'Zalgo']
},
@@ -119,7 +119,18 @@ window.app = new Vue({
// Update active transform and apply it
this.activeTransform = transform;
- this.transformOutput = transform.func(this.transformInput);
+
+ // Handle text with proper Unicode segmentation
+ const segments = window.emojiLibrary.splitEmojis(this.transformInput);
+ const transformedSegments = segments.map(segment => {
+ // Skip transformation for emojis and complex Unicode characters
+ if (segment.length > 1 || /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}]/u.test(segment)) {
+ return segment;
+ }
+ return transform.func(segment);
+ });
+
+ this.transformOutput = window.emojiLibrary.joinEmojis(transformedSegments);
// Set flag to mark this as a transform-initiated copy
this.isTransformCopy = true;
@@ -154,8 +165,17 @@ window.app = new Vue({
autoTransform() {
// Only proceed if we're in the transforms tab and have an active transform
if (this.transformInput && this.activeTransform && this.activeTab === 'transforms') {
- // Update the output without copying
- this.transformOutput = this.activeTransform.func(this.transformInput);
+ // Handle text with proper Unicode segmentation
+ const segments = window.emojiLibrary.splitEmojis(this.transformInput);
+ const transformedSegments = segments.map(segment => {
+ // Skip transformation for emojis and complex Unicode characters
+ if (segment.length > 1 || /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}]/u.test(segment)) {
+ return segment;
+ }
+ return this.activeTransform.func(segment);
+ });
+
+ this.transformOutput = window.emojiLibrary.joinEmojis(transformedSegments);
}
},
@@ -171,8 +191,17 @@ window.app = new Vue({
}
try {
- // Use the transform's reverse function to decode the input
- const decodedText = transform.reverse(this.transformInput);
+ // Handle text with proper Unicode segmentation
+ const segments = window.emojiLibrary.splitEmojis(this.transformInput);
+ const decodedSegments = segments.map(segment => {
+ // Skip decoding for emojis and complex Unicode characters
+ if (segment.length > 1 || /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}]/u.test(segment)) {
+ return segment;
+ }
+ return transform.reverse(segment);
+ });
+
+ const decodedText = window.emojiLibrary.joinEmojis(decodedSegments);
if (decodedText !== this.transformInput) {
// Update the input with the decoded text
@@ -461,35 +490,71 @@ window.app = new Vue({
try {
// Use Clipboard API
if (navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(text)
- .then(() => {
- // Only show notification for transform copies
- if (this.isTransformCopy) {
- this.showCopiedPopup();
-
- // Temporarily disable keyboard events
- this.ignoreKeyboardEvents = true;
- clearTimeout(this.keyboardEventsTimeout);
- this.keyboardEventsTimeout = setTimeout(() => {
- this.ignoreKeyboardEvents = false;
- }, 1000);
- }
-
- // Reset transform flag
- this.isTransformCopy = false;
-
- // Focus back on input
- const inputBox = document.querySelector('#transform-input');
- if (inputBox) {
- inputBox.focus();
- const len = inputBox.value.length;
- inputBox.setSelectionRange(len, len);
- }
- })
- .catch(err => {
- console.warn('Clipboard API failed:', err);
- this.forceFallbackCopy(text);
- });
+ // For emojis and complex characters, use a more robust approach
+ const processedText = typeof text === 'string' ? text : String(text);
+
+ // Try to use the newer clipboard API methods if available
+ if (navigator.clipboard.write && processedText.match(/[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}]/u)) {
+ const blob = new Blob([processedText], { type: 'text/plain;charset=utf-8' });
+ const clipboardItem = new ClipboardItem({ 'text/plain': blob });
+ navigator.clipboard.write([clipboardItem])
+ .then(() => {
+ if (this.isTransformCopy) {
+ this.showCopiedPopup();
+ this.ignoreKeyboardEvents = true;
+ clearTimeout(this.keyboardEventsTimeout);
+ this.keyboardEventsTimeout = setTimeout(() => {
+ this.ignoreKeyboardEvents = false;
+ }, 1000);
+ }
+ this.isTransformCopy = false;
+ const inputBox = document.querySelector('#transform-input');
+ if (inputBox) {
+ inputBox.focus();
+ const len = inputBox.value.length;
+ inputBox.setSelectionRange(len, len);
+ }
+ })
+ .catch(err => {
+ console.warn('Advanced Clipboard API failed:', err);
+ // Fall back to basic writeText
+ navigator.clipboard.writeText(processedText)
+ .then(() => {
+ if (this.isTransformCopy) {
+ this.showCopiedPopup();
+ }
+ this.isTransformCopy = false;
+ const inputBox = document.querySelector('#transform-input');
+ if (inputBox) {
+ inputBox.focus();
+ const len = inputBox.value.length;
+ inputBox.setSelectionRange(len, len);
+ }
+ })
+ .catch(err => {
+ console.warn('Basic Clipboard API failed:', err);
+ this.forceFallbackCopy(processedText);
+ });
+ });
+ } else {
+ navigator.clipboard.writeText(processedText)
+ .then(() => {
+ if (this.isTransformCopy) {
+ this.showCopiedPopup();
+ }
+ this.isTransformCopy = false;
+ const inputBox = document.querySelector('#transform-input');
+ if (inputBox) {
+ inputBox.focus();
+ const len = inputBox.value.length;
+ inputBox.setSelectionRange(len, len);
+ }
+ })
+ .catch(err => {
+ console.warn('Basic Clipboard API failed:', err);
+ this.forceFallbackCopy(processedText);
+ });
+ }
} else {
this.forceFallbackCopy(text);
}
@@ -514,9 +579,17 @@ window.app = new Vue({
// Create temporary textarea for copying
const textarea = document.createElement('textarea');
textarea.value = text;
+
+ // Ensure proper emoji rendering
+ textarea.style.fontFamily = "'Segoe UI Emoji', 'Apple Color Emoji', sans-serif";
+ textarea.style.fontSize = '16px';
+
+ // Position offscreen but with proper dimensions
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
textarea.style.top = '0';
+ textarea.style.width = '100px';
+ textarea.style.height = '100px';
document.body.appendChild(textarea);
// Focus and select the text
@@ -666,11 +739,25 @@ window.app = new Vue({
universalDecode(input) {
if (!input) return '';
- // Try all decoders in order
+ // If we're in the steganography tab, only try emoji decoding
+ if (this.activeTab === 'steganography') {
+ if (/[\u{1F300}-\u{1F6FF}\u{2600}-\u{26FF}]/u.test(input)) {
+ console.log('In emoji tab: attempting emoji decode only...');
+ const decoded = window.steganography.decodeEmoji(input);
+ if (decoded) {
+ console.log('Successfully decoded emoji:', decoded);
+ return { text: decoded, method: 'Emoji Steganography' };
+ } else {
+ console.log('No hidden message found in emoji');
+ return null;
+ }
+ }
+ return null;
+ }
+ // For other tabs, try all decoders in order
// 1. Try steganography decoders
// - Check for emoji steganography first
- // The emoji encoding uses variation selectors which are hard to see
if (/[\u{1F300}-\u{1F6FF}\u{2600}-\u{26FF}]/u.test(input)) {
console.log('Detected emoji, attempting to decode...');
const decoded = window.steganography.decodeEmoji(input);
@@ -1074,38 +1161,41 @@ window.app = new Vue({
},
selectEmoji(emoji) {
- this.selectedEmoji = emoji;
+ // Directly copy the emoji to clipboard
+ this.forceCopyToClipboard(emoji);
+ this.showNotification(` Emoji copied!`, 'success');
+ this.addToCopyHistory('Emoji', emoji);
- // Create a temporary carrier for this emoji
- const tempCarrier = {
- name: `${emoji} Carrier`,
- emoji: emoji,
- encode: (text) => this.steganography.encode(text, emoji),
- decode: (text) => this.steganography.decode(text),
- preview: (text) => `${emoji}${text}${emoji}`
- };
-
- // Use this emoji as carrier
- this.selectedCarrier = tempCarrier;
- this.activeSteg = 'emoji';
-
- // Encode the message with this emoji
- if (this.emojiMessage) {
- this.autoEncode();
+ // Also set up carrier if we're in steganography mode
+ if (this.activeTab === 'steganography') {
+ this.selectedEmoji = emoji;
- // Wait for encoding to complete, then copy to clipboard
- this.$nextTick(() => {
- if (this.encodedMessage) {
- // Force clipboard copy regardless of event source
- this.forceCopyToClipboard(this.encodedMessage);
- this.showNotification(` Copied hidden message with ${emoji}`, 'success');
-
- // Add to copy history
- this.addToCopyHistory(`Emoji: ${emoji}`, this.encodedMessage);
- }
- });
- } else {
- this.showNotification(`Select an emoji and enter a message first`, 'info');
+ // Create a temporary carrier for this emoji
+ const tempCarrier = {
+ name: `${emoji} Carrier`,
+ emoji: emoji,
+ encode: (text) => this.steganography.encode(text, emoji),
+ decode: (text) => this.steganography.decode(text),
+ preview: (text) => `${emoji}${text}${emoji}`
+ };
+
+ // Use this emoji as carrier
+ this.selectedCarrier = tempCarrier;
+ this.activeSteg = 'emoji';
+
+ // Encode the message with this emoji if we have one
+ if (this.emojiMessage) {
+ this.autoEncode();
+
+ // Wait for encoding to complete, then copy to clipboard
+ this.$nextTick(() => {
+ if (this.encodedMessage) {
+ this.forceCopyToClipboard(this.encodedMessage);
+ this.showNotification(` Hidden message copied with ${emoji}`, 'success');
+ this.addToCopyHistory(`Hidden Message with ${emoji}`, this.encodedMessage);
+ }
+ });
+ }
}
},
diff --git a/js/emojiLibrary.js b/js/emojiLibrary.js
index cd5e152..d37f615 100644
--- a/js/emojiLibrary.js
+++ b/js/emojiLibrary.js
@@ -3,6 +3,25 @@
// 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('');
+};
+
// Additional emojis for expanded library
window.emojiLibrary.ADDITIONAL_EMOJIS = [
// Animals & Nature
diff --git a/js/transforms.js b/js/transforms.js
index 11e94d4..797701c 100644
--- a/js/transforms.js
+++ b/js/transforms.js
@@ -456,20 +456,32 @@ const transforms = {
strikethrough: {
name: 'Strikethrough',
func: function(text) {
- return [...text].map(c => c + '̶').join('');
+ // Use proper Unicode combining characters for strikethrough
+ const segments = window.emojiLibrary.splitEmojis(text);
+ return segments.map(c => c + '\u0336').join('');
},
preview: function(text) {
return this.func(text);
+ },
+ reverse: function(text) {
+ // Remove combining strikethrough characters
+ return text.replace(/\u0336/g, '');
}
},
underline: {
name: 'Underline',
func: function(text) {
- return [...text].map(c => c + '̲').join('');
+ // Use proper Unicode combining characters for underline
+ const segments = window.emojiLibrary.splitEmojis(text);
+ return segments.map(c => c + '\u0332').join('');
},
preview: function(text) {
return this.func(text);
+ },
+ reverse: function(text) {
+ // Remove combining underline characters
+ return text.replace(/\u0332/g, '');
}
},
@@ -792,6 +804,54 @@ const transforms = {
}
},
+ greek: {
+ name: 'Greek Letters',
+ map: {
+ 'A': 'Α', 'B': 'Β', 'G': 'Γ', 'D': 'Δ', 'E': 'Ε',
+ 'Z': 'Ζ', 'H': 'Η', 'TH': 'Θ', 'I': 'Ι', 'K': 'Κ',
+ 'L': 'Λ', 'M': 'Μ', 'N': 'Ν', 'X': 'Ξ', 'O': 'Ο',
+ 'P': 'Π', 'R': 'Ρ', 'S': 'Σ', 'T': 'Τ', 'Y': 'Υ',
+ 'F': 'Φ', 'CH': 'Χ', 'PS': 'Ψ', 'W': 'Ω',
+ 'a': 'α', 'b': 'β', 'g': 'γ', 'd': 'δ', 'e': 'ε',
+ 'z': 'ζ', 'h': 'η', 'th': 'θ', 'i': 'ι', 'k': 'κ',
+ 'l': 'λ', 'm': 'μ', 'n': 'ν', 'x': 'ξ', 'o': 'ο',
+ 'p': 'π', 'r': 'ρ', 's': 'σ', 't': 'τ', 'y': 'υ',
+ 'f': 'φ', 'ch': 'χ', 'ps': 'ψ', 'w': 'ω'
+ },
+ func: function(text) {
+ return text.replace(/ch|th|ps|[a-zA-Z]/g, match => this.map[match] || match);
+ },
+ preview: function(text) {
+ return this.func(text);
+ },
+ reverse: function(text) {
+ const reverseMap = {};
+ Object.entries(this.map).forEach(([k, v]) => reverseMap[v] = k);
+ return text.replace(/[Α-Ωα-ω]/g, match => reverseMap[match] || match);
+ }
+ },
+
+ wingdings: {
+ name: 'Wingdings Style',
+ 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': '💤', '1': '☝️', '2': '✌️', '3': '🤟', '4': '4️⃣',
+ '5': '5️⃣', '6': '6️⃣', '7': '7️⃣', '8': '8️⃣', '9': '9️⃣',
+ '0': '0️⃣', '!': '❗', '?': '❓', '.': '•', ',': '،',
+ ' ': ' '
+ },
+ func: function(text) {
+ return text.toLowerCase().split('').map(char => this.map[char] || char).join('');
+ },
+ preview: function(text) {
+ return this.func(text);
+ }
+ },
+
base32: {
name: 'Base32',
alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567',
diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json
new file mode 100644
index 0000000..6dc7dc3
--- /dev/null
+++ b/node_modules/.package-lock.json
@@ -0,0 +1,6 @@
+{
+ "name": "P4RS3LT0NGV3",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {}
+}