Fix emoji encoding and UI issues: 1) Fixed emoji encoding to properly handle all emoji types including flags 2) Improved decoding logic 3) Removed duplicate black bars from UI 4) Increased emoji size and visibility

This commit is contained in:
EP
2025-03-11 16:23:13 -04:00
parent 6fe724b312
commit 25785d9981
7 changed files with 2321 additions and 230 deletions

View File

@@ -13,6 +13,14 @@
--input-border: #404040;
--section-divider: #404040;
--focus-shadow: 0 0 0 2px rgba(100, 181, 246, 0.4);
/* Transform category colors */
--encoding-color: #7e57c2; /* Purple for encoding/decoding */
--cipher-color: #26a69a; /* Teal for ciphers */
--visual-color: #ef5350; /* Red for visual transformations */
--format-color: #ffb74d; /* Orange for formatting */
--unicode-color: #42a5f5; /* Blue for unicode transformations */
--special-color: #66bb6a; /* Green for special transformations */
}
* {
@@ -195,21 +203,36 @@ textarea {
/* Emoji Library Styling */
.emoji-library {
margin: 16px 0;
margin: 12px 0;
background-color: var(--main-bg-color);
border-radius: 8px;
padding: 16px;
border-radius: 6px;
padding: 12px;
border: 1px solid var(--input-border);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
}
.emoji-library-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
margin-bottom: 12px;
border-bottom: 1px solid var(--input-border);
padding-bottom: 12px;
padding: 10px 12px;
background: var(--secondary-bg);
border-radius: 6px 6px 0 0;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
position: relative;
overflow: hidden;
}
.emoji-library-header:before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(to right, var(--special-color), var(--encoding-color), var(--cipher-color), var(--visual-color));
}
.emoji-library-title {
@@ -220,6 +243,18 @@ textarea {
gap: 4px;
}
.emoji-library-title h3 {
margin: 0;
color: var(--accent-color);
display: flex;
align-items: center;
gap: 8px;
}
.emoji-library-title h3 i {
color: var(--special-color);
}
.emoji-library-title i {
margin-right: 8px;
color: var(--accent-color);
@@ -228,8 +263,19 @@ textarea {
.emoji-library-subtitle {
font-size: 0.8rem;
font-weight: normal;
opacity: 0.7;
margin-top: 4px;
color: var(--text-color);
margin-top: 6px;
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
background-color: rgba(0, 0, 0, 0.1);
border-radius: 4px;
max-width: fit-content;
}
.emoji-library-subtitle i {
color: var(--encoding-color);
}
.emoji-search {
@@ -263,67 +309,183 @@ textarea {
opacity: 0.5;
}
.emoji-grid-wrapper {
border-radius: 6px;
.emoji-library {
display: block !important;
margin-top: 24px;
margin-bottom: 24px;
border-radius: 10px;
background-color: var(--secondary-bg);
padding: 8px;
margin-bottom: 12px;
padding: 0;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
position: relative;
overflow: hidden;
}
.emoji-library:before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(to right, var(--unicode-color), var(--special-color), var(--encoding-color));
z-index: 1;
}
.emoji-grid-wrapper {
display: flex;
flex-direction: column;
align-items: center;
border-radius: 0;
background-color: transparent;
padding: 0;
margin: 0;
border: none;
}
.emoji-grid-note {
background-color: rgba(var(--accent-color-rgb), 0.1);
background: linear-gradient(to right, rgba(102, 187, 106, 0.05), rgba(126, 87, 194, 0.05));
padding: 8px 12px;
border-radius: 4px;
margin-bottom: 8px;
font-size: 0.9rem;
font-size: 0.85rem;
color: var(--text-color);
display: flex;
align-items: center;
gap: 8px;
gap: 6px;
border-left: 2px solid var(--special-color);
box-shadow: none;
max-width: 450px;
width: 100%;
}
.emoji-grid-note i {
color: var(--accent-color);
color: var(--special-color);
font-size: 1rem;
opacity: 0.9;
}
.emoji-count {
font-size: 0.75rem;
color: var(--text-color);
text-align: center;
padding: 4px 8px;
margin-top: 8px;
border-radius: 12px;
background: linear-gradient(to right, rgba(66, 165, 245, 0.05), rgba(126, 87, 194, 0.05));
display: inline-block;
margin-left: auto;
margin-right: auto;
font-weight: 500;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
border: 1px solid rgba(66, 165, 245, 0.15);
opacity: 0.9;
}
.emoji-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(42px, 1fr));
gap: 10px;
max-height: 250px;
overflow-y: auto;
padding: 8px;
scrollbar-width: thin;
border-radius: 6px;
display: grid !important;
grid-template-columns: repeat(auto-fill, minmax(40px, 1fr)) !important;
grid-auto-rows: 40px !important;
gap: 4px !important;
padding: 10px !important;
border-radius: 4px !important;
border: 1px solid var(--input-border) !important;
background-color: var(--secondary-bg) !important;
box-shadow: none !important;
transition: all 0.2s ease !important;
margin-bottom: 8px !important;
width: 100% !important;
max-height: none !important;
overflow: visible !important;
max-width: 100% !important;
}
.emoji-category-tabs {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 8px;
width: 100%;
}
.emoji-category-tab {
padding: 5px 10px;
background: var(--button-bg);
border: 1px solid var(--input-border);
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: all 0.2s ease;
}
.emoji-category-tab.active,
.emoji-category-tab:hover {
background: var(--accent-color);
color: white;
}
#emoji-grid-container,
.emoji-grid-container {
width: 100% !important;
display: flex !important;
flex-direction: column !important;
align-items: stretch !important;
background-color: transparent !important;
padding: 0 !important;
margin: 0 !important;
border: none !important;
box-shadow: none !important;
overflow: visible !important;
min-height: 0 !important;
max-width: 100% !important;
}
.emoji-button {
display: flex;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
font-size: 1.25rem;
width: 100%;
height: 100%;
font-size: 1.3rem;
background-color: var(--button-bg);
border: 1px solid transparent;
border-radius: 6px;
border: 1px solid var(--input-border);
border-radius: 3px;
cursor: pointer;
transition: all 0.2s ease;
padding: 0;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
position: relative;
overflow: hidden;
}
.emoji-button:before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(to right, var(--special-color), var(--encoding-color));
opacity: 0;
transition: opacity 0.2s ease;
}
.emoji-button:hover {
background-color: var(--button-hover-bg);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border-color: var(--accent-color);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
}
.emoji-button:hover:before {
opacity: 1;
}
.emoji-button:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
background: var(--button-bg);
border-color: var(--accent-color);
transform: scale(0.98);
}
/* Emoji Library Footer */
@@ -479,6 +641,41 @@ button:hover {
border: 1px solid var(--input-border);
}
/* Transform category legend styling */
.transform-category-legend {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--input-border);
}
.legend-title {
font-size: 0.9rem;
font-weight: 500;
color: var(--text-color);
margin-right: 8px;
opacity: 0.7;
}
.legend-item {
font-size: 0.8rem;
padding: 4px 8px;
border-radius: 4px;
border-left-width: 4px;
border-left-style: solid;
background-color: var(--button-bg);
transition: all 0.2s ease;
cursor: default;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.legend-item:hover {
transform: translateY(-1px);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.transform-section:focus-within {
border-color: var(--accent-color);
box-shadow: var(--focus-shadow);
@@ -500,10 +697,33 @@ button:hover {
color: var(--accent-color);
}
.transform-categories {
display: flex;
flex-direction: column;
gap: 24px;
}
.transform-category-section {
background: var(--secondary-bg);
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.category-title {
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid var(--input-border);
font-size: 1.1rem;
font-weight: 500;
padding-left: 8px;
}
.transform-buttons {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 12px;
gap: 8px;
margin-bottom: 16px;
}
/* Carrier styling */
@@ -531,12 +751,18 @@ button:hover {
.transform-preview {
margin-top: auto;
width: 100%;
font-size: 0.7rem;
padding: 4px;
background-color: rgba(0, 0, 0, 0.1);
font-size: 0.65rem;
padding: 3px 4px;
background-color: rgba(0, 0, 0, 0.12);
border-radius: 3px;
white-space: normal;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.15;
max-width: 100%;
display: block;
border: 1px solid rgba(0, 0, 0, 0.06);
opacity: 0.85;
}
.preview-label {
@@ -554,16 +780,21 @@ button:hover {
/* Encoded preview styling */
.encoded-preview {
display: inline-block;
padding: 2px 4px;
background-color: rgba(0, 0, 0, 0.2);
border-radius: 3px;
font-family: monospace;
display: block;
padding: 6px 8px;
background-color: rgba(0, 0, 0, 0.25);
border-radius: 4px;
font-family: 'Fira Code', monospace;
word-break: break-all;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
letter-spacing: 0.5px; /* Better display for variation selectors */
white-space: nowrap;
letter-spacing: 0.5px;
margin-top: 4px;
color: var(--accent-color);
border: 1px solid rgba(0, 0, 0, 0.1);
font-size: 0.85rem;
}
.transform-button kbd {
@@ -572,33 +803,165 @@ button:hover {
right: 4px;
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
padding: 2px 4px;
font-size: 10px;
font-family: 'Fira Code', monospace;
pointer-events: none;
}
.transform-preview {
font-size: 11px;
opacity: 0.7;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.transform-button {
/* Copy History Panel */
.copy-history-panel {
position: fixed;
right: -400px; /* Start offscreen */
top: 0;
width: 380px;
height: 100vh;
background-color: var(--secondary-bg);
border-left: 1px solid var(--input-border);
z-index: 100;
box-shadow: -5px 0 15px rgba(0, 0, 0, 0.3);
transition: right 0.3s ease-in-out;
display: flex;
flex-direction: column;
gap: 4px;
padding: 10px;
text-align: left;
border-radius: 4px;
background: var(--button-bg);
padding: 0;
overflow: hidden;
}
.copy-history-panel.active {
right: 0;
}
.copy-history-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
border-bottom: 1px solid var(--input-border);
background-color: var(--button-bg);
}
.copy-history-header h3 {
font-size: 1.2rem;
margin: 0;
color: var(--accent-color);
}
.copy-history-header .close-button {
background: none;
border: none;
color: var(--text-color);
cursor: pointer;
font-size: 1.2rem;
padding: 5px;
}
.copy-history-header .close-button:hover {
color: var(--accent-color);
}
.copy-history-content {
flex: 1;
overflow-y: auto;
padding: 15px;
}
.no-history {
padding: 20px;
text-align: center;
color: var(--text-color);
opacity: 0.7;
}
.history-items {
display: flex;
flex-direction: column;
gap: 15px;
}
.history-item {
background-color: var(--input-bg);
border: 1px solid var(--input-border);
border-left: 3px solid var(--accent-color);
border-radius: 4px;
padding: 10px;
transition: all 0.2s ease;
}
.history-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.history-item-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 0.85rem;
}
.history-source {
font-weight: bold;
color: var(--accent-color);
}
.history-time {
color: var(--text-color);
opacity: 0.7;
}
.history-content {
background-color: var(--main-bg-color);
padding: 10px;
border-radius: 4px;
white-space: pre-wrap;
word-break: break-word;
max-height: 150px;
overflow-y: auto;
font-family: monospace;
font-size: 0.9rem;
color: var(--text-color);
}
.history-actions {
display: flex;
justify-content: flex-end;
margin-top: 8px;
}
.copy-again-button {
background-color: var(--button-bg);
color: var(--text-color);
border: 1px solid var(--input-border);
border-radius: 4px;
padding: 5px 10px;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s ease;
}
.copy-again-button:hover {
background-color: var(--button-hover-bg);
color: var(--accent-color);
}
.history-button {
background: none;
color: var(--text-color);
border: none;
border-radius: 50%;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
margin-right: 8px;
transition: all 0.2s ease;
}
.history-button:hover {
background-color: var(--button-hover-bg);
color: var(--accent-color);
}
.transform-name {
font-weight: 500;
color: var(--text-color);
@@ -606,26 +969,65 @@ button:hover {
.transform-preview {
font-size: 0.75rem;
opacity: 0.6;
font-family: 'Fira Code', monospace;
color: var(--accent-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 140px;
width: 100%;
max-width: 180px;
padding: 3px 6px;
border-radius: 3px;
background-color: rgba(0, 0, 0, 0.2);
margin-top: 4px;
transition: all 0.2s ease;
}
.transform-category-encoding .transform-preview {
color: rgba(126, 87, 194, 0.9);
}
.transform-category-cipher .transform-preview {
color: rgba(38, 166, 154, 0.9);
}
.transform-category-visual .transform-preview {
color: rgba(239, 83, 80, 0.9);
}
.transform-category-format .transform-preview {
color: rgba(255, 183, 77, 0.9);
}
.transform-category-unicode .transform-preview {
color: rgba(66, 165, 245, 0.9);
}
.transform-category-special .transform-preview {
color: rgba(102, 187, 106, 0.9);
}
.transform-button:hover .transform-preview {
background-color: rgba(0, 0, 0, 0.18);
opacity: 1;
}
.transform-button.active .transform-preview {
background-color: rgba(0, 0, 0, 0.3);
color: rgba(255, 255, 255, 0.9);
}
.transform-button {
padding: 12px;
padding: 8px;
height: auto;
border-radius: 6px;
font-size: 0.9rem;
border-radius: 4px;
font-size: 0.8rem;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
min-width: 120px;
min-width: 110px;
max-width: 100%;
font-weight: 500;
text-align: center;
border: 1px solid var(--input-border);
@@ -633,6 +1035,74 @@ button:hover {
color: var(--text-color);
transition: all 0.2s ease;
overflow: hidden;
min-height: 70px;
}
/* Transform category styling */
.transform-category-encoding {
border-left: 4px solid var(--encoding-color);
background: linear-gradient(to right, rgba(126, 87, 194, 0.05), var(--button-bg));
}
.transform-category-encoding:hover {
background: linear-gradient(to right, rgba(126, 87, 194, 0.15), var(--button-hover-bg));
border-color: var(--encoding-color);
box-shadow: 0 3px 10px rgba(126, 87, 194, 0.2);
}
.transform-category-cipher {
border-left: 4px solid var(--cipher-color);
background: linear-gradient(to right, rgba(38, 166, 154, 0.05), var(--button-bg));
}
.transform-category-cipher:hover {
background: linear-gradient(to right, rgba(38, 166, 154, 0.15), var(--button-hover-bg));
border-color: var(--cipher-color);
box-shadow: 0 3px 10px rgba(38, 166, 154, 0.2);
}
.transform-category-visual {
border-left: 4px solid var(--visual-color);
background: linear-gradient(to right, rgba(239, 83, 80, 0.05), var(--button-bg));
}
.transform-category-visual:hover {
background: linear-gradient(to right, rgba(239, 83, 80, 0.15), var(--button-hover-bg));
border-color: var(--visual-color);
box-shadow: 0 3px 10px rgba(239, 83, 80, 0.2);
}
.transform-category-format {
border-left: 4px solid var(--format-color);
background: linear-gradient(to right, rgba(255, 183, 77, 0.05), var(--button-bg));
}
.transform-category-format:hover {
background: linear-gradient(to right, rgba(255, 183, 77, 0.15), var(--button-hover-bg));
border-color: var(--format-color);
box-shadow: 0 3px 10px rgba(255, 183, 77, 0.2);
}
.transform-category-unicode {
border-left: 4px solid var(--unicode-color);
background: linear-gradient(to right, rgba(66, 165, 245, 0.05), var(--button-bg));
}
.transform-category-unicode:hover {
background: linear-gradient(to right, rgba(66, 165, 245, 0.15), var(--button-hover-bg));
border-color: var(--unicode-color);
box-shadow: 0 3px 10px rgba(66, 165, 245, 0.2);
}
.transform-category-special {
border-left: 4px solid var(--special-color);
background: linear-gradient(to right, rgba(102, 187, 106, 0.05), var(--button-bg));
}
.transform-category-special:hover {
background: linear-gradient(to right, rgba(102, 187, 106, 0.15), var(--button-hover-bg));
border-color: var(--special-color);
box-shadow: 0 3px 10px rgba(102, 187, 106, 0.2);
}
.transform-button:before {
@@ -652,7 +1122,7 @@ button:hover {
background: var(--button-hover-bg);
border-color: var(--accent-color);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}
.transform-button:hover:before {
@@ -664,7 +1134,8 @@ button:hover {
background: var(--button-active-bg);
color: var(--main-bg-color);
border-color: var(--button-active-bg);
box-shadow: 0 2px 12px rgba(100, 181, 246, 0.3);
box-shadow: 0 2px 8px rgba(100, 181, 246, 0.25);
transform: translateY(-1px);
}
.transform-button.active:before {
@@ -672,6 +1143,43 @@ button:hover {
opacity: 1;
}
/* Category-specific active states */
.transform-category-encoding.active {
background: linear-gradient(to right, var(--encoding-color), #9575cd);
border-color: var(--encoding-color);
box-shadow: 0 3px 12px rgba(126, 87, 194, 0.4);
}
.transform-category-cipher.active {
background: linear-gradient(to right, var(--cipher-color), #4db6ac);
border-color: var(--cipher-color);
box-shadow: 0 3px 12px rgba(38, 166, 154, 0.4);
}
.transform-category-visual.active {
background: linear-gradient(to right, var(--visual-color), #e57373);
border-color: var(--visual-color);
box-shadow: 0 3px 12px rgba(239, 83, 80, 0.4);
}
.transform-category-format.active {
background: linear-gradient(to right, var(--format-color), #ffcc80);
border-color: var(--format-color);
box-shadow: 0 3px 12px rgba(255, 183, 77, 0.4);
}
.transform-category-unicode.active {
background: linear-gradient(to right, var(--unicode-color), #64b5f6);
border-color: var(--unicode-color);
box-shadow: 0 3px 12px rgba(66, 165, 245, 0.4);
}
.transform-category-special.active {
background: linear-gradient(to right, var(--special-color), #81c784);
border-color: var(--special-color);
box-shadow: 0 3px 12px rgba(102, 187, 106, 0.4);
}
/* Add a subtle indicator for clickable buttons */
.transform-button:after {
content: '';
@@ -691,6 +1199,22 @@ button:hover {
width: 30px;
}
/* Auto-copy icon styling */
.auto-copy-icon {
position: absolute;
right: 8px;
bottom: 8px;
font-size: 0.8rem;
opacity: 0.5;
transition: all 0.2s ease;
}
.transform-button:hover .auto-copy-icon {
opacity: 1;
transform: scale(1.2);
color: var(--accent-color);
}
/* Output section */
.output-section {
display: flex;

View File

@@ -18,6 +18,14 @@
<h1>🐍 Parseltongue</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"
@@ -51,9 +59,9 @@
<button
:class="{ active: activeTab === 'steganography' }"
@click="activeTab = 'steganography'"
title="Hide text (H)"
title="Hide text in emojis (H)"
>
<i class="fas fa-eye-slash"></i> Hide
<i class="fas fa-smile"></i> Emoji
</button>
</div>
@@ -70,59 +78,32 @@
</div>
<div class="transform-section">
<div class="transform-buttons">
<button
v-for="carrier in carriers"
:key="carrier.name"
class="transform-button"
:class="{ active: selectedCarrier === carrier }"
@click="selectCarrier(carrier)"
:title="carrier.desc"
>
<div class="carrier-content">
<span class="carrier-emoji">{{ carrier.emoji }}</span>
<span class="carrier-name">{{ carrier.name }}</span>
</div>
<small class="transform-preview" v-if="emojiMessage">
<span class="preview-label">Preview:</span>
<span class="encoded-preview">{{ carrier.preview ? carrier.preview(emojiMessage.slice(0, 10)) : '' }}</span>
<span class="preview-ellipsis" v-if="emojiMessage.length > 10">...</span>
</small>
</button>
</div>
<!-- Big Invisible Text Button -->
<button
class="transform-button invisible-button"
:class="{ active: activeSteg === 'invisible' }"
@click="setStegMode('invisible')"
title="Make text invisible using zero-width characters"
>
<div class="carrier-content">
<span class="carrier-emoji"><i class="fas fa-eye-slash"></i></span>
<span class="carrier-name">Invisible Text Mode</span>
</div>
<small class="transform-preview" v-if="emojiMessage">
<span class="preview-label">Preview:</span>
<span class="encoded-preview">{{ previewInvisible(emojiMessage.slice(0, 10)) }}</span>
<span class="preview-ellipsis" v-if="emojiMessage.length > 10">...</span>
</small>
</button>
</div>
<!-- Emoji Library Section -->
<div class="emoji-library">
<div class="emoji-library-header">
<div class="emoji-library-title">
<i class="fas fa-icons"></i> Quick Emoji Picker
<span class="emoji-library-subtitle">Click any emoji to insert it at cursor position</span>
</div>
<div class="emoji-search">
<i class="fas fa-search"></i>
<input type="text" placeholder="Search emojis..." v-model="emojiSearch" @input="filterEmojis">
<h3><i class="fas fa-icons"></i> Choose an Emoji</h3>
</div>
</div>
<!-- Emoji grid container - this MUST have id="emoji-grid-container" -->
<div id="emoji-grid-container" class="emoji-grid-container">
<!-- Dynamic content will be inserted here by JavaScript -->
<div class="emoji-grid">
<!-- Common emojis as fallback -->
<button class="emoji-button" onclick="app.selectEmoji('😀')">😀</button>
<button class="emoji-button" onclick="app.selectEmoji('😂')">😂</button>
<button class="emoji-button" onclick="app.selectEmoji('🥰')">🥰</button>
<button class="emoji-button" onclick="app.selectEmoji('😎')">😎</button>
<button class="emoji-button" onclick="app.selectEmoji('🤔')">🤔</button>
<button class="emoji-button" onclick="app.selectEmoji('👍')">👍</button>
<button class="emoji-button" onclick="app.selectEmoji('🎉')">🎉</button>
<button class="emoji-button" onclick="app.selectEmoji('🔥')">🔥</button>
<button class="emoji-button" onclick="app.selectEmoji('🚀')">🚀</button>
<button class="emoji-button" onclick="app.selectEmoji('🐍')">🐍</button>
</div>
</div>
<div id="emoji-grid-container" class="emoji-grid-wrapper"></div>
<div class="emoji-library-footer" v-if="selectedEmoji">
<div class="selected-emoji-info">
<span class="selected-emoji">{{ selectedEmoji }}</span>
@@ -151,17 +132,24 @@
</div>
</div>
<div class="decode-section" v-if="showDecoder">
<!-- Universal Decoder Section for Steganography Tab -->
<div class="decode-section">
<div class="section-header">
<h3><i class="fas fa-magic"></i> Universal Decoder</h3>
<p>Paste any encoded text to try all decoding methods at once</p>
</div>
<div class="input-container">
<textarea
id="decode-input"
v-model="decodeInput"
placeholder="Paste encoded text to decode..."
@input="autoDecode"
id="universal-decode-input-steg"
v-model="universalDecodeInput"
placeholder="Paste encoded text to decode automatically..."
@input="runUniversalDecode"
></textarea>
<div class="decoded-message" v-if="decodedMessage">
{{ decodedMessage }}
<button class="copy-button" @click="copyToClipboard(decodedMessage.substring(decodedMessage.indexOf(': ') + 2))" title="Copy decoded text">
<div class="decoded-message" v-if="universalDecodeResult">
<div class="decode-method">Decoded using: <strong>{{ universalDecodeResult.method }}</strong></div>
<div class="decode-result">{{ universalDecodeResult.text }}</div>
<button class="copy-button" @click="copyToClipboard(universalDecodeResult.text)" title="Copy decoded text">
<i class="fas fa-copy"></i>
</button>
</div>
@@ -183,22 +171,189 @@
</div>
<div class="transform-section">
<div class="transform-buttons">
<button
v-for="(transform, index) in transforms"
:key="transform.name"
@click="applyTransform(transform)"
class="transform-button"
:class="{ active: activeTransform === transform }"
:title="transform.name + (index < 9 ? ' (' + (index + 1) + ')' : '')"
:data-shortcut="index < 9 ? index + 1 : ''"
>
{{ transform.name }}
<small class="transform-preview" v-if="transformInput">
{{ transform.preview(transformInput.slice(0, 10)) }}
</small>
<kbd v-if="index < 9">{{ index + 1 }}</kbd>
</button>
<div class="transform-category-legend">
<div class="legend-title">Categories:</div>
<div class="legend-item transform-category-encoding" data-target="category-encoding">Encoding</div>
<div class="legend-item transform-category-cipher" data-target="category-cipher">Ciphers</div>
<div class="legend-item transform-category-visual" data-target="category-visual">Visual</div>
<div class="legend-item transform-category-format" data-target="category-format">Formatting</div>
<div class="legend-item transform-category-unicode" data-target="category-unicode">Unicode</div>
<div class="legend-item transform-category-special" data-target="category-special">Special</div>
</div>
<div class="transform-categories">
<!-- Encoding Category -->
<div id="category-encoding" class="transform-category-section">
<h4 class="category-title transform-category-encoding">Encoding</h4>
<div class="transform-buttons">
<div v-for="transform in getTransformsByCategory('encoding')" :key="transform.name" class="transform-button-group">
<button
@click="applyTransform(transform)"
class="transform-button transform-category-encoding"
:class="{ active: activeTransform === transform }"
:title="'Click to transform and copy: ' + transform.name"
>
{{ transform.name }}
<small class="transform-preview" v-if="transformInput">
{{ transform.preview(transformInput.slice(0, 10)) }}
</small>
<i class="fas fa-copy auto-copy-icon"></i>
</button>
<button
v-if="transformHasReverse(transform)"
@click="decodeWithTransform(transform)"
class="decode-button transform-category-encoding"
:title="'Click to decode using: ' + transform.name"
>
<i class="fas fa-undo"></i>
</button>
</div>
</div>
</div>
<!-- Cipher Category -->
<div id="category-cipher" class="transform-category-section">
<h4 class="category-title transform-category-cipher">Ciphers</h4>
<div class="transform-buttons">
<div v-for="transform in getTransformsByCategory('cipher')" :key="transform.name" class="transform-button-group">
<button
@click="applyTransform(transform)"
class="transform-button transform-category-cipher"
:class="{ active: activeTransform === transform }"
:title="'Click to transform and copy: ' + transform.name"
>
{{ transform.name }}
<small class="transform-preview" v-if="transformInput">
{{ transform.preview(transformInput.slice(0, 10)) }}
</small>
<i class="fas fa-copy auto-copy-icon"></i>
</button>
<button
v-if="transformHasReverse(transform)"
@click="decodeWithTransform(transform)"
class="decode-button transform-category-cipher"
:title="'Click to decode using: ' + transform.name"
>
<i class="fas fa-undo"></i>
</button>
</div>
</div>
</div>
<!-- Visual Category -->
<div id="category-visual" class="transform-category-section">
<h4 class="category-title transform-category-visual">Visual</h4>
<div class="transform-buttons">
<div v-for="transform in getTransformsByCategory('visual')" :key="transform.name" class="transform-button-group">
<button
@click="applyTransform(transform)"
class="transform-button transform-category-visual"
:class="{ active: activeTransform === transform }"
:title="'Click to transform and copy: ' + transform.name"
>
{{ transform.name }}
<small class="transform-preview" v-if="transformInput">
{{ transform.preview(transformInput.slice(0, 10)) }}
</small>
<i class="fas fa-copy auto-copy-icon"></i>
</button>
<button
v-if="transformHasReverse(transform)"
@click="decodeWithTransform(transform)"
class="decode-button transform-category-visual"
:title="'Click to decode using: ' + transform.name"
>
<i class="fas fa-undo"></i>
</button>
</div>
</div>
</div>
<!-- Format Category -->
<div id="category-format" class="transform-category-section">
<h4 class="category-title transform-category-format">Formatting</h4>
<div class="transform-buttons">
<div v-for="transform in getTransformsByCategory('format')" :key="transform.name" class="transform-button-group">
<button
@click="applyTransform(transform)"
class="transform-button transform-category-format"
:class="{ active: activeTransform === transform }"
:title="'Click to transform and copy: ' + transform.name"
>
{{ transform.name }}
<small class="transform-preview" v-if="transformInput">
{{ transform.preview(transformInput.slice(0, 10)) }}
</small>
<i class="fas fa-copy auto-copy-icon"></i>
</button>
<button
v-if="transformHasReverse(transform)"
@click="decodeWithTransform(transform)"
class="decode-button transform-category-format"
:title="'Click to decode using: ' + transform.name"
>
<i class="fas fa-undo"></i>
</button>
</div>
</div>
</div>
<!-- Unicode Category -->
<div id="category-unicode" class="transform-category-section">
<h4 class="category-title transform-category-unicode">Unicode</h4>
<div class="transform-buttons">
<div v-for="transform in getTransformsByCategory('unicode')" :key="transform.name" class="transform-button-group">
<button
@click="applyTransform(transform)"
class="transform-button transform-category-unicode"
:class="{ active: activeTransform === transform }"
:title="'Click to transform and copy: ' + transform.name"
>
{{ transform.name }}
<small class="transform-preview" v-if="transformInput">
{{ transform.preview(transformInput.slice(0, 10)) }}
</small>
<i class="fas fa-copy auto-copy-icon"></i>
</button>
<button
v-if="transformHasReverse(transform)"
@click="decodeWithTransform(transform)"
class="decode-button transform-category-unicode"
:title="'Click to decode using: ' + transform.name"
>
<i class="fas fa-undo"></i>
</button>
</div>
</div>
</div>
<!-- Special Category -->
<div id="category-special" class="transform-category-section">
<h4 class="category-title transform-category-special">Special</h4>
<div class="transform-buttons">
<div v-for="transform in getTransformsByCategory('special')" :key="transform.name" class="transform-button-group">
<button
@click="applyTransform(transform)"
class="transform-button transform-category-special"
:class="{ active: activeTransform === transform }"
:title="'Click to transform and copy: ' + transform.name"
>
{{ transform.name }}
<small class="transform-preview" v-if="transformInput">
{{ transform.preview(transformInput.slice(0, 10)) }}
</small>
<i class="fas fa-copy auto-copy-icon"></i>
</button>
<button
v-if="transformHasReverse(transform)"
@click="decodeWithTransform(transform)"
class="decode-button transform-category-special"
:title="'Click to decode using: ' + transform.name"
>
<i class="fas fa-undo"></i>
</button>
</div>
</div>
</div>
</div>
</div>
@@ -214,10 +369,63 @@
</button>
</div>
</div>
<!-- Universal Decoder Section for Transforms Tab -->
<div class="decode-section">
<div class="section-header">
<h3><i class="fas fa-magic"></i> Universal Decoder</h3>
<p>Paste any encoded text to try all decoding methods at once</p>
</div>
<div class="input-container">
<textarea
id="universal-decode-input-transforms"
v-model="universalDecodeInput"
placeholder="Paste encoded text to decode automatically..."
@input="runUniversalDecode"
></textarea>
<div class="decoded-message" v-if="universalDecodeResult">
<div class="decode-method">Decoded using: <strong>{{ universalDecodeResult.method }}</strong></div>
<div class="decode-result">{{ universalDecodeResult.text }}</div>
<button class="copy-button" @click="copyToClipboard(universalDecodeResult.text)" title="Copy decoded text">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
</div>
</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>
<button class="close-button" @click="toggleCopyHistory" title="Close history">
<i class="fas fa-times"></i>
</button>
</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="index" class="history-item">
<div class="history-item-header">
<span class="history-source">{{ item.source }}</span>
<span class="history-time">{{ 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> Copy Again
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -225,5 +433,75 @@
<script src="js/steganography.js"></script>
<script src="js/emojiLibrary.js"></script>
<script src="js/app.js"></script>
<!-- Force emoji grid rendering -->
<script>
// Function to initialize emoji grid with retries
function initEmojiGrid(retryCount) {
retryCount = retryCount || 0;
const maxRetries = 5;
console.log('Initializing emoji grid, attempt:', retryCount + 1);
// Get the emoji library element
const emojiLibrary = document.querySelector('.emoji-library');
// Access the emoji grid container without forcing styles
const emojiGridContainer = document.getElementById('emoji-grid-container');
if (emojiGridContainer) {
console.log('Found emoji grid container, initializing...');
// Only set content, let CSS handle display
emojiGridContainer.innerHTML = '<div class="loading-emojis">Loading emoji grid...</div>';
// Manually render emoji grid
if (window.emojiLibrary && window.emojiLibrary.renderEmojiGrid) {
try {
window.emojiLibrary.renderEmojiGrid('emoji-grid-container', function(emoji) {
// Simulate emoji selection by calling the Vue method if possible
const app = document.getElementById('app').__vue__;
if (app && app.selectEmoji) {
app.selectEmoji(emoji);
}
});
console.log('Emoji grid successfully rendered');
} catch (error) {
console.error('Error rendering emoji grid:', error);
}
} else {
console.warn('Emoji library not yet available, will retry');
if (retryCount < maxRetries) {
setTimeout(() => initEmojiGrid(retryCount + 1), 500);
}
}
} else {
console.warn('Emoji grid container not found, will retry');
if (retryCount < maxRetries) {
setTimeout(() => initEmojiGrid(retryCount + 1), 500);
}
}
}
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
// First attempt after a short delay to ensure Vue has initialized
setTimeout(initEmojiGrid, 500);
// Also initialize when switching to the steganography tab
const app = document.getElementById('app');
if (app && app.__vue__) {
const vue = app.__vue__;
// Watch for tab changes
const originalWatch = vue.$watch;
if (originalWatch) {
vue.$watch('activeTab', function(newTab) {
if (newTab === 'steganography') {
console.log('Switched to steganography tab, initializing emoji grid');
setTimeout(initEmojiGrid, 100);
}
});
}
}
});
</script>
</body>
</html>

286
js/app.js
View File

@@ -12,6 +12,15 @@ window.app = new Vue({
transformInput: '',
transformOutput: '',
activeTransform: null,
// Transform categories for styling
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'],
unicode: ['Invisible Text', 'Upside Down', 'Full Width', 'Small Caps', 'Bubble', 'Braille'],
special: ['Medieval', 'Cursive', 'Monospace', 'Double-Struck', 'Elder Futhark', 'Mirror Text', 'Zalgo']
},
transforms: Object.entries(window.transforms).map(([key, transform]) => ({
name: transform.name,
func: transform.func.bind(transform),
@@ -42,6 +51,13 @@ window.app = new Vue({
showCopyHistory: false
},
methods: {
// Get transforms grouped by category
getTransformsByCategory(category) {
return this.transforms.filter(transform =>
this.transformCategories[category].includes(transform.name)
);
},
// Theme Toggle
toggleTheme() {
this.isDarkTheme = !this.isDarkTheme;
@@ -92,6 +108,39 @@ window.app = new Vue({
this.addToCopyHistory(`Transform: ${this.activeTransform.name}`, this.transformOutput);
}
},
// Check if a transform has a reverse function
transformHasReverse(transform) {
return transform && typeof transform.reverse === 'function';
},
// Decode text using the specific transform's reverse function
decodeWithTransform(transform) {
if (!this.transformInput || !transform || !this.transformHasReverse(transform)) {
return;
}
try {
// Use the transform's reverse function to decode the input
const decodedText = transform.reverse(this.transformInput);
if (decodedText !== this.transformInput) {
// Update the input with the decoded text
this.transformInput = decodedText;
// Show a notification
this.showNotification(`<i class="fas fa-check"></i> Decoded using ${transform.name}`, 'success');
// Add to copy history
this.addToCopyHistory(`Decoded (${transform.name})`, decodedText);
} else {
this.showNotification(`<i class="fas fa-exclamation-triangle"></i> Could not decode with ${transform.name}`, 'warning');
}
} catch (error) {
console.error(`Error decoding with ${transform.name}:`, error);
this.showNotification(`<i class="fas fa-exclamation-triangle"></i> Error decoding with ${transform.name}`, 'error');
}
},
// Steganography Methods
selectCarrier(carrier) {
@@ -449,10 +498,12 @@ window.app = new Vue({
}
}
// - Invisible text
let decoded = window.steganography.decodeInvisible(input);
if (decoded) {
return { text: decoded, method: 'Invisible Text' };
// - Invisible text (only check if the input actually contains invisible characters)
if (/[\uE0000-\uE007F]/.test(input)) {
let decoded = window.steganography.decodeInvisible(input);
if (decoded && decoded.length > 0) {
return { text: decoded, method: 'Invisible Text' };
}
}
// 2. Try transform reversals
@@ -503,21 +554,26 @@ window.app = new Vue({
const braillePattern = /[-⣿]/;
if (braillePattern.test(input)) {
try {
// Create a reverse mapping for braille
const brailleReverseMap = {};
if (window.transforms.braille && window.transforms.braille.map) {
for (const [key, value] of Object.entries(window.transforms.braille.map)) {
brailleReverseMap[value] = key;
}
// Decode the braille
let result = '';
for (const char of input) {
result += brailleReverseMap[char] || char;
}
if (result !== input && /[a-zA-Z0-9]/.test(result)) {
return { text: result, method: 'Braille' };
// Count how many braille characters are in the input
const brailleMatches = [...input.matchAll(/[-⣿]/g)];
// Only proceed if there are enough braille characters (to avoid false positives)
if (brailleMatches.length > 2) {
// Create a reverse mapping for braille
const brailleReverseMap = {};
if (window.transforms.braille && window.transforms.braille.map) {
for (const [key, value] of Object.entries(window.transforms.braille.map)) {
brailleReverseMap[value] = key;
}
// Decode the braille
let result = '';
for (const char of input) {
result += brailleReverseMap[char] || char;
}
if (result !== input && /[a-zA-Z0-9]/.test(result)) {
return { text: result, method: 'Braille' };
}
}
}
} catch (e) {
@@ -611,6 +667,168 @@ window.app = new Vue({
}
}
// Check for specific new transforms before trying the generic approach
// - Hexadecimal
if (/^[0-9A-Fa-f\s]+$/.test(input.trim())) {
try {
if (window.transforms.hex && window.transforms.hex.reverse) {
const result = window.transforms.hex.reverse(input);
if (result && /[\x20-\x7E]{3,}/.test(result)) {
return { text: result, method: 'Hexadecimal' };
}
}
} catch (e) {
console.error('Hex decode error:', e);
}
}
// - URL Encoded
if (/%[0-9A-Fa-f]{2}/.test(input)) {
try {
if (window.transforms.url && window.transforms.url.reverse) {
const result = window.transforms.url.reverse(input);
if (result !== input && /[\x20-\x7E]{3,}/.test(result)) {
return { text: result, method: 'URL Encoded' };
}
} else {
// Fallback implementation
try {
const result = decodeURIComponent(input);
if (result !== input && /[\x20-\x7E]{3,}/.test(result)) {
return { text: result, method: 'URL Encoded' };
}
} catch (e) {
console.error('URL decode fallback error:', e);
}
}
} catch (e) {
console.error('URL decode error:', e);
}
}
// - HTML Entities
if (/&[#a-zA-Z0-9]+;/.test(input)) {
try {
if (window.transforms.html && window.transforms.html.reverse) {
const result = window.transforms.html.reverse(input);
if (result !== input && /[\x20-\x7E]{3,}/.test(result)) {
return { text: result, method: 'HTML Entities' };
}
}
} catch (e) {
console.error('HTML entities decode error:', e);
}
}
// - ROT13/Caesar Cipher (check if decoding produces more common English words)
if (/^[a-zA-Z\s.,!?]+$/.test(input)) {
try {
// Try ROT13 first as it's more common
if (window.transforms.rot13 && window.transforms.rot13.reverse) {
const result = window.transforms.rot13.reverse(input);
if (result !== input) {
return { text: result, method: 'ROT13' };
}
}
// Then try Caesar cipher
if (window.transforms.caesar && window.transforms.caesar.reverse) {
const result = window.transforms.caesar.reverse(input);
if (result !== input) {
return { text: result, method: 'Caesar Cipher' };
}
}
} catch (e) {
console.error('Cipher decode error:', e);
}
}
// - Base32
if (/^[A-Z2-7=]+$/.test(input.trim())) {
try {
if (window.transforms.base32 && window.transforms.base32.reverse) {
const result = window.transforms.base32.reverse(input);
if (result && /[\x20-\x7E]{3,}/.test(result)) {
return { text: result, method: 'Base32' };
}
}
} catch (e) {
console.error('Base32 decode error:', e);
}
}
// - ASCII85
if (/^<~.*~>$/.test(input.trim())) {
try {
if (window.transforms.ascii85 && window.transforms.ascii85.reverse) {
const result = window.transforms.ascii85.reverse(input);
if (result && /[\x20-\x7E]{3,}/.test(result)) {
return { text: result, method: 'ASCII85' };
}
}
} catch (e) {
console.error('ASCII85 decode error:', e);
}
}
// - Check for Zalgo text (text with combining marks)
const combiningMarksRegex = /[\u0300-\u036f\u1ab0-\u1aff\u1dc0-\u1dff\u20d0-\u20ff\ufe20-\ufe2f]/;
if (combiningMarksRegex.test(input)) {
try {
// Count the number of combining marks to ensure it's actually Zalgo text
// and not just text with a few accents
const matches = input.match(combiningMarksRegex) || [];
if (matches.length > 3) { // Threshold to distinguish Zalgo from normal accented text
// Fallback implementation to remove combining marks
const result = input.replace(/[\u0300-\u036f\u1ab0-\u1aff\u1dc0-\u1dff\u20d0-\u20ff\ufe20-\ufe2f]/g, '');
if (result !== input && result.length > 0) {
return { text: result, method: 'Zalgo' };
}
}
} catch (e) {
console.error('Zalgo decode error:', e);
}
}
// - Check for various Unicode text styles (medieval, cursive, monospace, double-struck)
const unicodeStyleChecks = [
{ name: 'Medieval', transform: 'medieval' },
{ name: 'Cursive', transform: 'cursive' },
{ name: 'Monospace', transform: 'monospace' },
{ name: 'Double-Struck', transform: 'doubleStruck' }
];
for (const style of unicodeStyleChecks) {
if (window.transforms[style.transform] && window.transforms[style.transform].map) {
try {
// Create reverse mapping
const reverseMap = {};
for (const [key, value] of Object.entries(window.transforms[style.transform].map)) {
reverseMap[value] = key;
}
// Check if input contains characters from this style
const styleChars = Object.values(window.transforms[style.transform].map);
const hasStyleChars = styleChars.some(char => input.includes(char));
if (hasStyleChars) {
// Decode text
let result = '';
for (const char of input) {
result += reverseMap[char] || char;
}
if (result !== input && /[a-zA-Z0-9]/.test(result)) {
return { text: result, method: style.name };
}
}
} catch (e) {
console.error(`${style.name} decode error:`, e);
}
}
}
// - Try reverse each transform that has a built-in reverse function
for (const name in window.transforms) {
const transform = window.transforms[name];
@@ -698,11 +916,7 @@ window.app = new Vue({
// Render the emoji grid
window.emojiLibrary.renderEmojiGrid('emoji-grid-container', this.selectEmoji.bind(this), this.filteredEmojis);
// Add a bold message about copying
const copyNote = document.createElement('div');
copyNote.style.cssText = 'text-align: center; margin-top: 10px; font-weight: bold; padding: 5px; background-color: #f0f0f0; border-radius: 4px;';
copyNote.innerHTML = '<i class="fas fa-info-circle"></i> Clicking an emoji will automatically copy your hidden message';
container.appendChild(copyNote);
// Message about copying has been removed as requested
// Log success
console.log('Emoji grid rendered successfully');
@@ -716,6 +930,30 @@ window.app = new Vue({
document.body.classList.add('dark-theme');
}
// Add smooth scrolling for category navigation
this.$nextTick(() => {
const legendItems = document.querySelectorAll('.transform-category-legend .legend-item');
legendItems.forEach(item => {
item.addEventListener('click', () => {
const targetId = item.getAttribute('data-target');
if (targetId) {
const targetElement = document.getElementById(targetId);
if (targetElement) {
// Add active class to the clicked legend item
legendItems.forEach(li => li.classList.remove('active-category'));
item.classList.add('active-category');
// Scroll to the target element with smooth behavior
targetElement.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
}
});
});
});
// Initialize emoji grid with all emojis shown by default
this.$nextTick(() => {
console.log('nextTick: Initializing emoji grid');

View File

@@ -3,6 +3,33 @@
// Create namespace for emoji library
window.emojiLibrary = {};
// Additional emojis for expanded library
window.emojiLibrary.ADDITIONAL_EMOJIS = [
// Animals & Nature
"🦊", "🦁", "🐯", "🐮", "🐷", "🐸", "🐵", "🐔", "🐧", "🐦", "🐤", "🦆", "🦅", "🦉", "🦇", "🐺", "🐗", "🐴", "🦄", "🐝", "🐛", "🦋", "🐌", "🐞", "🐜", "🕷️", "🦂", "🦟", "🦠", "🦨", "🦩", "🦫", "🦬", "🐻‍❄️", "🐼", "🐨", "🐕", "🐶", "🐩", "🐈", "🐱",
// Food & Drink
"🍏", "🍎", "🍐", "🍊", "🍋", "🍌", "🍉", "🍇", "🍓", "🍈", "🍒", "🍑", "🥭", "🍍", "🥥", "🥝", "🍅", "🍆", "🥑", "🥦", "🥬", "🥒", "🌶️", "🌽", "🥕", "🧄", "🧅", "🥔", "🍠", "🥐", "🍔", "🍕", "🍖", "🍗", "🍤", "🍣", "🍱", "🍜", "🍲", "🍥",
// Travel & Places
"🚗", "🚕", "🚙", "🚌", "🚎", "🚒", "🚑", "🚚", "🚛", "🚜", "🚲", "🚐", "🚟", "🚡", "🚀", "🛸", "🛥️", "🏎️", "🏍️", "🚤", "🚢", "🚁", "🚂", "🚆", "🚈", "🌎", "🌏", "🌍", "🏔️", "🏕️",
// Activities & Sports
"⚽", "🏀", "🏈", "🏐", "🏉", "🎾", "🎳", "🏑", "🏒", "🏓", "🏸", "🥊", "🥋", "🥅", "🤾", "🎿", "🏄", "🏂", "🏊", "🏋️", "🤼", "🤸", "🤺", "🤽", "🤹", "🎯", "🎱", "🎽", "🚴", "🚵",
// Tech & Objects
"💻", "⌨️", "🖥️", "🖱️", "🖨️", "📱", "☎️", "📞", "📟", "📠", "📺", "📻", "🎙️", "🎚️", "🎛️", "🧭", "⏱️", "⏲️", "⏰", "🕰️", "📡", "🔋", "🔌", "💡", "🏮", "🪔", "🧯", "🛢️", "💸", "💵", "💳", "💴", "💶", "💷", "💰", "💱", "💲", "💼", "💽", "💾", "💿",
// Symbols
"❤️", "💛", "💚", "💙", "💜", "💔", "💕", "💞", "💓", "💗", "💖", "💘", "💝", "💟", "💤", "💢", "💣", "💥", "💦", "💨", "💩", "💫", "💬", "🔥", "💠", "👾", "👻", "💀", "👽", "👿",
// Mystical & Fantasy
"🧙", "🧙‍♂️", "🧙‍♀️", "🧚", "🧚‍♂️", "🧚‍♀️", "🧛", "🧛‍♂️", "🧛‍♀️", "🧜", "🧜‍♂️", "🧜‍♀️", "👹", "👺", "👻", "👽", "👾", "🐲", "🔮", "🐍", "🐉", "🦄", "👸", "🥷", "👰", "🧔", "⚗️", "🔯", "🔱", "⚜️", "✨", "🌠", "🌋", "💎", "💐", "🍄", "🌺", "🌹", "🐭", "🐚", "🐊", "🐢", "🐇", "🐰", "🔥", "💥", "🌀", "🌈", "🌪️",
// Flags
"🏁", "🚩", "🎌", "🏴", "🏳️", "🏳️‍🌈", "🏳️‍⚧️", "🏴‍☠️", "🇺🇸", "🇨🇦", "🇬🇧", "🇩🇪", "🇫🇷", "🇮🇹", "🇯🇵", "🇰🇷", "🇷🇺", "🇨🇳", "🇮🇳", "🇦🇺", "🇧🇷", "🇪🇸", "🇳🇱", "🇵🇹", "🇸🇪", "🇦🇷", "🇦🇺", "🇦🇹", "🇧🇪", "🇧🇴"
];
// Make emoji list globally available
window.emojiLibrary.EMOJI_LIST = [
// Faces and People
@@ -274,9 +301,147 @@ window.emojiLibrary.EMOJI_LIST = [
"🚭", // No Smoking (hacker symbol)
"🚯", // No Littering (hacker symbol)
"🚱", // Non-Potable Water (hacker symbol)
// Additional Smileys & Emotion
"😊", // Smiling Face with Smiling Eyes
"😇", // Smiling Face with Halo
"🙂", // Slightly Smiling Face
"🙃", // Upside-Down Face
"😉", // Winking Face
"😌", // Relieved Face
"😍", // Smiling Face with Heart-Eyes
"🥰", // Smiling Face with Hearts
"😘", // Face Blowing a Kiss
"😗", // Kissing Face
"😙", // Kissing Face with Smiling Eyes
"😚", // Kissing Face with Closed Eyes
"😋", // Face Savoring Food
"😛", // Face with Tongue
"😝", // Squinting Face with Tongue
"😜", // Winking Face with Tongue
"🤪", // Zany Face
// Additional People & Body
"🧑‍🚀", // Astronaut
"👨‍🚀", // Man Astronaut
"👩‍🚀", // Woman Astronaut
"🧑‍🔬", // Scientist
"👨‍🔬", // Man Scientist
"👩‍🔬", // Woman Scientist
"🧑‍⚕️", // Health Worker
"👨‍⚕️", // Man Health Worker
"👩‍⚕️", // Woman Health Worker
"🧑‍🔧", // Mechanic
"👨‍🔧", // Man Mechanic
"👩‍🔧", // Woman Mechanic
"🧑‍🚒", // Firefighter
"👨‍🚒", // Man Firefighter
"👩‍🚒", // Woman Firefighter
// Additional Animals & Nature
"🦒", // Giraffe
"🦓", // Zebra
"🦬", // Bison
"🦙", // Llama
"🦘", // Kangaroo
"🦥", // Sloth
"🦦", // Otter
"🦡", // Badger
"🦔", // Hedgehog
"🦝", // Raccoon
"🐿️", // Chipmunk
"🦫", // Beaver
"🦎", // Lizard
"🐊", // Crocodile
"🐢", // Turtle
"🦕", // Sauropod
"🦖", // T-Rex
"🐋", // Whale
"🐬", // Dolphin
"🦭", // Seal
// Additional Food & Drink
"🥞", // Pancakes
"🧇", // Waffle
"🧀", // Cheese Wedge
"🍖", // Meat on Bone
"🍗", // Poultry Leg
"🥩", // Cut of Meat
"🥓", // Bacon
"🍔", // Hamburger
"🍟", // French Fries
"🍕", // Pizza
"🌭", // Hot Dog
"🥪", // Sandwich
"🌮", // Taco
"🌯", // Burrito
"🥙", // Stuffed Flatbread
"🧆", // Falafel
"🥚", // Egg
"🍳", // Cooking
"🥘", // Shallow Pan of Food
"🍲", // Pot of Food
// Additional Travel & Places
"🏙️", // Cityscape
"🌆", // Cityscape at Dusk
"🌇", // Sunset
"🌃", // Night with Stars
"🌉", // Bridge at Night
"🏞️", // National Park
"🏜️", // Desert
"🏝️", // Desert Island
"🏖️", // Beach with Umbrella
"⛰️", // Mountain
"🏔️", // Snow-Capped Mountain
"🌋", // Volcano
"🗻", // Mount Fuji
"🏠", // House
"🏡", // House with Garden
"🏢", // Office Building
"🏣", // Japanese Post Office
"🏤", // Post Office
"🏥", // Hospital
"🏦", // Bank
// Additional Flags
"🇺🇸", // United States
"🇬🇧", // United Kingdom
"🇨🇦", // Canada
"🇯🇵", // Japan
"🇩🇪", // Germany
"🇫🇷", // France
"🇮🇹", // Italy
"🇪🇸", // Spain
"🇷🇺", // Russia
"🇨🇳", // China
"🇮🇳", // India
"🇧🇷", // Brazil
"🇦🇺", // Australia
"🇲🇽", // Mexico
"🇰🇷", // South Korea
"🇿🇦", // South Africa
"🇸🇪", // Sweden
"🇳🇴", // Norway
"🇳🇿", // New Zealand
"🇮🇪", // Ireland
];
// Function to render emoji grid
// Define standard emoji categories using the Unicode CLDR categorization
window.emojiLibrary.CATEGORIES = [
{ id: 'all', name: 'All Emojis', icon: '🔍' },
{ id: 'smileys', name: 'Smileys & Emotion', icon: '😀' },
{ id: 'people', name: 'People & Body', icon: '👋' },
{ id: 'animals', name: 'Animals & Nature', icon: '🐵' },
{ id: 'food', name: 'Food & Drink', icon: '🍎' },
{ id: 'travel', name: 'Travel & Places', icon: '🚗' },
{ id: 'activities', name: 'Activities', icon: '⚽' },
{ id: 'objects', name: 'Objects', icon: '💡' },
{ id: 'symbols', name: 'Symbols', icon: '🔣' },
{ id: 'flags', name: 'Flags', icon: '🏁' }
];
// Function to render emoji grid with categories
window.emojiLibrary.renderEmojiGrid = function(containerId, onEmojiSelect, filteredList) {
console.log('Rendering emoji grid to:', containerId);
@@ -290,36 +455,39 @@ window.emojiLibrary.renderEmojiGrid = function(containerId, onEmojiSelect, filte
// Clear container
container.innerHTML = '';
// Create grid note
const gridNote = document.createElement('div');
gridNote.className = 'emoji-grid-note';
gridNote.innerHTML = '<i class="fas fa-magic"></i> Click any emoji to automatically copy your hidden message';
container.appendChild(gridNote);
// 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';
// Force grid styling
gridContainer.style.display = 'grid';
gridContainer.style.gridTemplateColumns = 'repeat(auto-fill, minmax(50px, 1fr))';
gridContainer.style.gap = '8px';
gridContainer.style.padding = '15px';
gridContainer.style.maxHeight = '300px';
gridContainer.style.overflowY = 'auto';
gridContainer.style.border = '1px solid #ccc';
gridContainer.style.borderRadius = '4px';
gridContainer.style.margin = '10px 0';
// Combine all emojis for a larger selection
const allEmojis = [...window.emojiLibrary.EMOJI_LIST, ...window.emojiLibrary.ADDITIONAL_EMOJIS];
// Add a message showing we're displaying all emojis
const fullLibraryNote = document.createElement('div');
fullLibraryNote.className = 'emoji-grid-note';
fullLibraryNote.innerHTML = '<i class="fas fa-magic"></i> Click an emoji to automatically copy your hidden message';
fullLibraryNote.style.padding = '10px';
fullLibraryNote.style.marginBottom = '10px';
fullLibraryNote.style.backgroundColor = 'rgba(0,0,0,0.05)';
fullLibraryNote.style.borderRadius = '4px';
fullLibraryNote.style.textAlign = 'center';
container.appendChild(fullLibraryNote);
// Always use full emoji list - search removed
// Use the provided filtered list if available, otherwise default to full list
// This ensures we always show ALL emojis regardless of input state
const emojisToShow = filteredList && filteredList.length > 0 ? filteredList : window.emojiLibrary.EMOJI_LIST;
const emojisToShow = filteredList && filteredList.length > 0 ? filteredList : allEmojis;
console.log(`Adding ${emojisToShow.length} emojis to grid`);
// Add emojis to grid with enforced styling
@@ -329,32 +497,13 @@ window.emojiLibrary.renderEmojiGrid = function(containerId, onEmojiSelect, filte
emojiButton.textContent = emoji; // Use textContent for better emoji handling
emojiButton.title = 'Click to encode with this emoji';
// Force button styling
emojiButton.style.fontSize = '24px';
emojiButton.style.padding = '8px';
emojiButton.style.border = '1px solid #ddd';
emojiButton.style.borderRadius = '8px';
emojiButton.style.cursor = 'pointer';
emojiButton.style.backgroundColor = '#fff';
emojiButton.style.transition = 'transform 0.1s';
// Add hover effect
emojiButton.onmouseover = function() {
this.style.transform = 'scale(1.1)';
this.style.boxShadow = '0 0 5px rgba(0,0,0,0.2)';
};
emojiButton.onmouseout = function() {
this.style.transform = 'scale(1)';
this.style.boxShadow = 'none';
};
emojiButton.addEventListener('click', () => {
if (typeof onEmojiSelect === 'function') {
onEmojiSelect(emoji);
// Add visual feedback when clicked
emojiButton.style.backgroundColor = '#e6f7ff';
setTimeout(() => {
emojiButton.style.backgroundColor = '#fff';
emojiButton.style.backgroundColor = '';
}, 300);
}
});
@@ -365,9 +514,199 @@ window.emojiLibrary.renderEmojiGrid = function(containerId, onEmojiSelect, filte
container.appendChild(gridContainer);
console.log('Emoji grid rendering complete');
// Force container to be visible
container.style.display = 'block !important';
container.style.visibility = 'visible !important';
// Helper function to categorize emojis using standard Unicode ranges
function categorizeEmoji(emoji) {
// Get the code point of the emoji
const code = emoji.codePointAt(0);
// Smileys & Emotion (faces, emotions, hearts)
if ((code >= 0x1F600 && code <= 0x1F64F) || // Emoticons
(code >= 0x1F910 && code <= 0x1F92F) || // Face-hand
(code >= 0x1F970 && code <= 0x1F97A) || // Faces
(code >= 0x1F9D0 && code <= 0x1F9DF) || // Faces
(code >= 0x2763 && code <= 0x2764) || // Hearts
(code >= 0x1F48B && code <= 0x1F49F) || // Hearts and love
(code >= 0x1F493 && code <= 0x1F49F) || // Hearts
emoji === '😀' || emoji === '😃' || emoji === '😄' || emoji === '😁' || emoji === '😆' ||
emoji === '😅' || emoji === '😂' || emoji === '🤣' || emoji === '☺️' || emoji === '😊') {
return 'smileys';
}
// People & Body (people, hands, body parts)
if ((code >= 0x1F466 && code <= 0x1F487) || // People
(code >= 0x1F9D1 && code <= 0x1F9DD) || // People
(code >= 0x1F468 && code <= 0x1F469) || // Man/Woman
(code >= 0x1F46E && code <= 0x1F9CF) || // People roles
(code >= 0x1F44B && code <= 0x1F450) || // Hands
(code >= 0x1F918 && code <= 0x1F91F) || // Hand symbols
(code >= 0x1F926 && code <= 0x1F937) || // People gestures
emoji.includes('👨') || emoji.includes('👩') || emoji.includes('🧑') ||
emoji.includes('👶') || emoji.includes('👦') || emoji.includes('👧') ||
emoji.includes('🧒') || emoji.includes('👴') || emoji.includes('👵') ||
emoji.includes('🧓') || emoji.includes('👮') || emoji.includes('👷')) {
return 'people';
}
// Animals & Nature (animals, plants, weather)
if ((code >= 0x1F400 && code <= 0x1F43F) || // Animals
(code >= 0x1F980 && code <= 0x1F9AF) || // Animals
(code >= 0x1F330 && code <= 0x1F33F) || // Plants
(code >= 0x1F340 && code <= 0x1F37F) || // More plants
(code >= 0x1F300 && code <= 0x1F32C) || // Weather
emoji === '🐵' || emoji === '🐒' || emoji === '🦍' || emoji === '🦧' ||
emoji === '🐶' || emoji === '🐕' || emoji === '🦮' || emoji === '🐩' ||
emoji === '🐺' || emoji === '🦊' || emoji === '🦝' || emoji === '🐱' ||
emoji === '🌱' || emoji === '🌲' || emoji === '🌳' || emoji === '🌴' ||
emoji === '🌵' || emoji === '🌷' || emoji === '🌸' || emoji === '🌹') {
return 'animals';
}
// Food & Drink
if ((code >= 0x1F32D && code <= 0x1F37F) || // Food items
(code >= 0x1F95F && code <= 0x1F9AA) || // More food
(code >= 0x1F950 && code <= 0x1F96F) || // More food
emoji === '🍇' || emoji === '🍈' || emoji === '🍉' || emoji === '🍊' ||
emoji === '🍋' || emoji === '🍌' || emoji === '🍍' || emoji === '🥭' ||
emoji === '🍎' || emoji === '🍏' || emoji === '🍐' || emoji === '🍑' ||
emoji === '🍒' || emoji === '🍓' || emoji === '🥝' || emoji === '🍅' ||
emoji === '🥥' || emoji === '🥑' || emoji === '🍆' || emoji === '🥔') {
return 'food';
}
// Travel & Places (transportation, buildings, maps)
if ((code >= 0x1F680 && code <= 0x1F6FF) || // Transport
(code >= 0x1F30D && code <= 0x1F32C) || // Earth/Weather
(code >= 0x1F3D7 && code <= 0x1F3DB) || // Buildings
(code >= 0x1F3E0 && code <= 0x1F3F0) || // Buildings
(code >= 0x26E9 && code <= 0x26F5) || // Buildings/Places
emoji === '🚗' || emoji === '🚕' || emoji === '🚙' || emoji === '🚌' ||
emoji === '🚎' || emoji === '🏎️' || emoji === '🚓' || emoji === '🚑' ||
emoji === '🚒' || emoji === '🚐' || emoji === '🛻' || emoji === '🚚' ||
emoji === '🚛' || emoji === '🚜' || emoji === '🛵' || emoji === '🏍️' ||
emoji === '🛺' || emoji === '🚲' || emoji === '🛴' || emoji === '🚏') {
return 'travel';
}
// Activities (sports, music, arts, hobbies)
if ((code >= 0x1F380 && code <= 0x1F3A0) || // Events
(code >= 0x1F3A3 && code <= 0x1F3BE) || // Sports
(code >= 0x1F3BF && code <= 0x1F3C9) || // Sports
(code >= 0x1F3CF && code <= 0x1F3D6) || // Sports
(code >= 0x1F3F8 && code <= 0x1F3FF) || // Activities
(code >= 0x1F93A && code <= 0x1F94F) || // Sports
emoji === '⚽' || emoji === '⚾' || emoji === '🏀' || emoji === '🏐' ||
emoji === '🏈' || emoji === '🏉' || emoji === '🎾' || emoji === '🥏' ||
emoji === '🎳' || emoji === '🏏' || emoji === '🏑' || emoji === '🏒' ||
emoji === '🥍' || emoji === '🏓' || emoji === '🏸' || emoji === '🥊') {
return 'activities';
}
// Objects (household, office, tools)
if ((code >= 0x1F4A1 && code <= 0x1F4CC) || // Office
(code >= 0x1F4D0 && code <= 0x1F4F7) || // Office/Tools
(code >= 0x1F4FF && code <= 0x1F53D) || // Various objects
(code >= 0x1F56F && code <= 0x1F5A4) || // Objects
(code >= 0x1F5D1 && code <= 0x1F5FF) || // Office objects
(code >= 0x1F6D1 && code <= 0x1F6DF) || // Misc objects
emoji === '⌚' || emoji === '📱' || emoji === '📲' || emoji === '💻' ||
emoji === '⌨️' || emoji === '🖥️' || emoji === '🖨️' || emoji === '🖱️' ||
emoji === '🖲️' || emoji === '🕹️' || emoji === '🗜️' || emoji === '💽' ||
emoji === '💾' || emoji === '💿' || emoji === '📀' || emoji === '📼') {
return 'objects';
}
// Symbols (punctuation, alphanum, geometric, etc)
if ((code >= 0x1F300 && code <= 0x1F320) || // Various symbols
(code >= 0x1F170 && code <= 0x1F251) || // Enclosed characters
(code >= 0x1F523 && code <= 0x1F5FF) || // Symbols
(code >= 0x2600 && code <= 0x26FF) || // Misc symbols
(code >= 0x2700 && code <= 0x27BF) || // Dingbats
(code >= 0x1F5FB && code <= 0x1F64F) || // Symbols
(code >= 0x1F680 && code <= 0x1F6FF) || // Transport symbols
emoji === '💯' || emoji === '📛' || emoji === '🔰' || emoji === '⭕' ||
emoji === '✅' || emoji === '☑️' || emoji === '✔️' || emoji === '❌' ||
emoji === '❎' || emoji === '➰' || emoji === '➿' || emoji === '〽️' ||
emoji === '✳️' || emoji === '✴️' || emoji === '❇️' || emoji === '©️') {
return 'symbols';
}
// Flags (country flags, flag symbols)
if ((code >= 0x1F1E6 && code <= 0x1F1FF) || // Regional indicators for flags
emoji === '🏁' || emoji === '🚩' || emoji === '🎌' || emoji === '🏴' ||
emoji.includes('🏳️') || // Flag variants
emoji.includes('🏴') || // Flag variants
// Check for country flags (pairs of regional indicators)
(emoji.length >= 2 &&
emoji.codePointAt(0) >= 0x1F1E6 && emoji.codePointAt(0) <= 0x1F1FF &&
emoji.codePointAt(2) >= 0x1F1E6 && emoji.codePointAt(2) <= 0x1F1FF)) {
return 'flags';
}
// Default to 'all' if we can't categorize
return 'all';
}
// Add event listeners to category tabs with actual filtering
document.querySelectorAll('.emoji-category-tab').forEach(tab => {
tab.addEventListener('click', function() {
// Remove active class from all tabs
document.querySelectorAll('.emoji-category-tab').forEach(t => {
t.classList.remove('active');
});
// Add active class to clicked tab
this.classList.add('active');
const selectedCategory = this.getAttribute('data-category');
console.log('Selected category:', selectedCategory);
// Get all emoji buttons
const allEmojis = [...window.emojiLibrary.EMOJI_LIST, ...window.emojiLibrary.ADDITIONAL_EMOJIS];
// Filter emojis based on selected category
let filteredEmojis = allEmojis;
if (selectedCategory !== 'all') {
filteredEmojis = allEmojis.filter(emoji => {
const category = categorizeEmoji(emoji);
console.log(`Emoji: ${emoji}, Category: ${category}`);
return category === selectedCategory;
});
}
// Clear and rebuild the grid with filtered emojis
const gridContainer = container.querySelector('.emoji-grid');
if (gridContainer) {
// Clear existing emojis
gridContainer.innerHTML = '';
// Add filtered emojis
filteredEmojis.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 = `${filteredEmojis.length} emojis available`;
}
}
});
});
// Debug info - add count display
const countDisplay = document.createElement('div');

View File

@@ -13,7 +13,8 @@ function encodeForPreview(emoji, text) {
const vs16 = '\ufe0f'; // emoji variation selector (1)
// Start with the emoji character
let result = emoji;
// Ensure the emoji has a presentation selector first to standardize it
let result = emoji + vs16; // Add emoji presentation selector first
// Add variation selectors based on binary representation
for (const bit of binary) {
@@ -76,7 +77,8 @@ function encodeEmoji(emoji, text) {
const vs16 = '\ufe0f'; // emoji variation selector (1)
// Start with the emoji character
let result = emoji;
// Ensure the emoji has a presentation selector first to standardize it
let result = emoji + vs16; // Add emoji presentation selector first
// Add variation selectors based on binary representation
for (const bit of binary) {
@@ -94,23 +96,42 @@ 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);
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, '');
const matches = [...text.matchAll(/[\ufe0e\ufe0f]/g)];
if (!matches.length) return '';
// Convert variation selectors to binary
const binary = matches.map(m => m[0] === '\ufe0e' ? '0' : '1').join('');
// 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');
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('');
// Make sure we have complete bytes (multiples of 8 bits)
const validBinaryLength = Math.floor(binary.length / 8) * 8;
// Convert binary to text
let decoded = '';
for (let i = 0; i < binary.length; i += 8) {
for (let i = 0; i < validBinaryLength; i += 8) {
const byte = binary.slice(i, i + 8);
if (byte.length === 8) {
decoded += String.fromCharCode(parseInt(byte, 2));
const charCode = parseInt(byte, 2);
// Only include printable ASCII characters
if (charCode >= 32 && charCode <= 126) {
decoded += String.fromCharCode(charCode);
}
}
}
@@ -130,14 +151,38 @@ function encodeInvisible(text) {
function decodeInvisible(text) {
if (!text) return '';
// Extract valid invisible characters
const matches = [...text.matchAll(/[\uE0000-\uE007F]/g)];
if (!matches.length) return '';
const bytes = new Uint8Array(
matches.map(m => m[0].codePointAt(0) - 0xE0000)
);
// 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;
}
return new TextDecoder().decode(bytes);
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

View File

@@ -1,5 +1,52 @@
// Text transformation functions
const transforms = {
// Invisible Text transform
invisible_text: {
name: 'Invisible Text',
func: function(text) {
if (!text) return '';
const bytes = new TextEncoder().encode(text);
return Array.from(bytes)
.map(byte => String.fromCodePoint(0xE0000 + byte))
.join('');
},
preview: function(text) {
return '[invisible]';
},
reverse: function(text) {
if (!text) return '';
const matches = [...text.matchAll(/[\uE0000-\uE007F]/g)];
if (!matches.length) return '';
return matches
.map(match => String.fromCharCode(match[0].codePointAt(0) - 0xE0000))
.join('');
}
},
// Invisible Text transform
invisible_text: {
name: 'Invisible Text',
func: function(text) {
if (!text) return '';
const bytes = new TextEncoder().encode(text);
return Array.from(bytes)
.map(byte => String.fromCodePoint(0xE0000 + byte))
.join('');
},
preview: function(text) {
return '[invisible]';
},
reverse: function(text) {
if (!text) return '';
const matches = [...text.matchAll(/[\uE0000-\uE007F]/g)];
if (!matches.length) return '';
return matches
.map(match => String.fromCharCode(match[0].codePointAt(0) - 0xE0000))
.join('');
}
},
// Basic transforms
upside_down: {
name: 'Upside Down',
@@ -42,11 +89,23 @@ const transforms = {
'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) {
return this.func(text);
},
reverse: function(text) {
const revMap = this.reverseMap();
return [...text].map(c => revMap[c] || c).join('');
}
},
@@ -57,6 +116,10 @@ const transforms = {
},
preview: function(text) {
return this.func(text);
},
reverse: function(text) {
// Remove spaces between characters
return text.replace(/ /g, '');
}
},
@@ -193,6 +256,610 @@ const transforms = {
}
// Note: other transforms don't have reverse functions because they're not easily reversible
// The universal decoder will only try to reverse transforms that have a reverse function
,
// Additional transforms
base64: {
name: 'Base64',
func: function(text) {
return btoa(text);
},
preview: function(text) {
return this.func(text);
},
reverse: function(text) {
try {
return atob(text);
} catch (e) {
return text;
}
}
},
hex: {
name: 'Hexadecimal',
func: function(text) {
return [...text].map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join(' ');
},
preview: function(text) {
return this.func(text);
},
reverse: function(text) {
const hexText = text.replace(/\s+/g, '');
let result = '';
for (let i = 0; i < hexText.length; i += 2) {
const byte = hexText.substr(i, 2);
if (byte.length === 2) {
result += String.fromCharCode(parseInt(byte, 16));
}
}
return result;
}
},
caesar: {
name: 'Caesar Cipher',
shift: 3, // Traditional Caesar shift is 3
func: function(text) {
return [...text].map(c => {
const code = c.charCodeAt(0);
// Only shift letters, leave other characters unchanged
if (code >= 65 && code <= 90) { // Uppercase letters
return String.fromCharCode(((code - 65 + this.shift) % 26) + 65);
} else if (code >= 97 && code <= 122) { // Lowercase letters
return String.fromCharCode(((code - 97 + this.shift) % 26) + 97);
} else {
return c;
}
}).join('');
},
preview: function(text) {
return this.func(text);
},
reverse: function(text) {
// For decoding, shift in the opposite direction
const originalShift = this.shift;
this.shift = 26 - (this.shift % 26); // Reverse the shift
const result = this.func(text);
this.shift = originalShift; // Restore original shift
return result;
}
},
rot13: {
name: 'ROT13',
func: function(text) {
return [...text].map(c => {
const code = c.charCodeAt(0);
if (code >= 65 && code <= 90) { // Uppercase letters
return String.fromCharCode(((code - 65 + 13) % 26) + 65);
} else if (code >= 97 && code <= 122) { // Lowercase letters
return String.fromCharCode(((code - 97 + 13) % 26) + 97);
} else {
return c;
}
}).join('');
},
preview: function(text) {
return this.func(text);
},
reverse: function(text) {
// ROT13 is its own inverse
return this.func(text);
}
},
leetspeak: {
name: 'Leetspeak',
map: {
'a': '4', 'e': '3', 'i': '1', 'o': '0', 's': '5', 't': '7', 'l': '1',
'A': '4', 'E': '3', 'I': '1', 'O': '0', 'S': '5', 'T': '7', 'L': '1'
},
func: function(text) {
return [...text].map(c => this.map[c] || c).join('');
},
preview: function(text) {
return this.func(text);
},
// Create reverse map for decoding
reverseMap: function() {
const revMap = {};
for (const [key, value] of Object.entries(this.map)) {
revMap[value] = key.toLowerCase();
}
return revMap;
},
reverse: function(text) {
const revMap = this.reverseMap();
return [...text].map(c => revMap[c] || c).join('');
}
},
mirror: {
name: 'Mirror Text',
func: function(text) {
return [...text].reverse().join('');
},
preview: function(text) {
return this.func(text);
},
reverse: function(text) {
return this.func(text); // Mirror is its own inverse
}
},
nato: {
name: 'NATO Phonetic',
map: {
'a': 'Alpha', 'b': 'Bravo', 'c': 'Charlie', 'd': 'Delta', 'e': 'Echo',
'f': 'Foxtrot', 'g': 'Golf', 'h': 'Hotel', 'i': 'India', 'j': 'Juliett',
'k': 'Kilo', 'l': 'Lima', 'm': 'Mike', 'n': 'November', 'o': 'Oscar',
'p': 'Papa', 'q': 'Quebec', 'r': 'Romeo', 's': 'Sierra', 't': 'Tango',
'u': 'Uniform', 'v': 'Victor', 'w': 'Whiskey', 'x': 'X-ray', 'y': 'Yankee', 'z': 'Zulu',
'0': 'Zero', '1': 'One', '2': 'Two', '3': 'Three', '4': 'Four',
'5': 'Five', '6': 'Six', '7': 'Seven', '8': 'Eight', '9': 'Nine'
},
func: function(text) {
return [...text.toLowerCase()].map(c => this.map[c] || c).join(' ');
},
preview: function(text) {
return this.func(text);
},
// Create reverse map for decoding
reverseMap: function() {
const revMap = {};
for (const [key, value] of Object.entries(this.map)) {
revMap[value.toLowerCase()] = key;
}
return revMap;
},
reverse: function(text) {
const revMap = this.reverseMap();
return text.split(/\s+/).map(word => revMap[word.toLowerCase()] || word).join('');
}
},
fullwidth: {
name: 'Full Width',
func: function(text) {
return [...text].map(c => {
const code = c.charCodeAt(0);
// Convert ASCII to full-width equivalents
if (code >= 33 && code <= 126) {
return String.fromCharCode(code + 0xFEE0);
} else if (code === 32) { // Space
return ' '; // Full-width space
} else {
return c;
}
}).join('');
},
preview: function(text) {
return this.func(text);
},
reverse: function(text) {
return [...text].map(c => {
const code = c.charCodeAt(0);
// Convert full-width back to ASCII
if (code >= 0xFF01 && code <= 0xFF5E) {
return String.fromCharCode(code - 0xFEE0);
} else if (code === 0x3000) { // Full-width space
return ' '; // ASCII space
} else {
return c;
}
}).join('');
}
},
strikethrough: {
name: 'Strikethrough',
func: function(text) {
return [...text].map(c => c + '̶').join('');
},
preview: function(text) {
return this.func(text);
}
},
underline: {
name: 'Underline',
func: function(text) {
return [...text].map(c => c + '̲').join('');
},
preview: function(text) {
return this.func(text);
}
},
medieval: {
name: 'Medieval',
map: {
'a': '𝖆', 'b': '𝖇', 'c': '𝖈', 'd': '𝖉', 'e': '𝖊', 'f': '𝖋', 'g': '𝖌', 'h': '𝖍', 'i': '𝖎',
'j': '𝖏', 'k': '𝖐', 'l': '𝖑', 'm': '𝖒', 'n': '𝖓', 'o': '𝖔', 'p': '𝖕', 'q': '𝖖', 'r': '𝖗',
's': '𝖘', 't': '𝖙', 'u': '𝖚', 'v': '𝖛', 'w': '𝖜', 'x': '𝖝', 'y': '𝖞', 'z': '𝖟',
'A': '𝕬', 'B': '𝕭', 'C': '𝕮', 'D': '𝕯', 'E': '𝕰', 'F': '𝕱', 'G': '𝕲', 'H': '𝕳', 'I': '𝕴',
'J': '𝕵', 'K': '𝕶', 'L': '𝕷', 'M': '𝕸', 'N': '𝕹', 'O': '𝕺', 'P': '𝕻', 'Q': '𝕼', 'R': '𝕽',
'S': '𝕾', 'T': '𝕿', 'U': '𝖀', 'V': '𝖁', 'W': '𝖂', 'X': '𝖃', 'Y': '𝖄', 'Z': '𝖅'
},
func: function(text) {
return [...text].map(c => this.map[c] || c).join('');
},
preview: function(text) {
return this.func(text);
}
},
cursive: {
name: 'Cursive',
map: {
'a': '𝓪', 'b': '𝓫', 'c': '𝓬', 'd': '𝓭', 'e': '𝓮', 'f': '𝓯', 'g': '𝓰', 'h': '𝓱', 'i': '𝓲',
'j': '𝓳', 'k': '𝓴', 'l': '𝓵', 'm': '𝓶', 'n': '𝓷', 'o': '𝓸', 'p': '𝓹', 'q': '𝓺', 'r': '𝓻',
's': '𝓼', 't': '𝓽', 'u': '𝓾', 'v': '𝓿', 'w': '𝔀', 'x': '𝔁', 'y': '𝔂', 'z': '𝔃',
'A': '𝓐', 'B': '𝓑', 'C': '𝓒', 'D': '𝓓', 'E': '𝓔', 'F': '𝓕', 'G': '𝓖', 'H': '𝓗', 'I': '𝓘',
'J': '𝓙', 'K': '𝓚', 'L': '𝓛', 'M': '𝓜', 'N': '𝓝', 'O': '𝓞', 'P': '𝓟', 'Q': '𝓠', 'R': '𝓡',
'S': '𝓢', 'T': '𝓣', 'U': '𝓤', 'V': '𝓥', 'W': '𝓦', 'X': '𝓧', 'Y': '𝓨', 'Z': '𝓩'
},
func: function(text) {
return [...text].map(c => this.map[c] || c).join('');
},
preview: function(text) {
return this.func(text);
}
},
monospace: {
name: 'Monospace',
map: {
'a': '𝚊', 'b': '𝚋', 'c': '𝚌', 'd': '𝚍', 'e': '𝚎', 'f': '𝚏', 'g': '𝚐', 'h': '𝚑', 'i': '𝚒',
'j': '𝚓', 'k': '𝚔', 'l': '𝚕', 'm': '𝚖', 'n': '𝚗', 'o': '𝚘', 'p': '𝚙', 'q': '𝚚', 'r': '𝚛',
's': '𝚜', 't': '𝚝', 'u': '𝚞', 'v': '𝚟', 'w': '𝚠', 'x': '𝚡', 'y': '𝚢', 'z': '𝚣',
'A': '𝙰', 'B': '𝙱', 'C': '𝙲', 'D': '𝙳', 'E': '𝙴', 'F': '𝙵', 'G': '𝙶', 'H': '𝙷', 'I': '𝙸',
'J': '𝙹', 'K': '𝙺', 'L': '𝙻', 'M': '𝙼', 'N': '𝙽', 'O': '𝙾', 'P': '𝙿', 'Q': '𝚀', 'R': '𝚁',
'S': '𝚂', 'T': '𝚃', 'U': '𝚄', 'V': '𝚅', 'W': '𝚆', 'X': '𝚇', 'Y': '𝚈', 'Z': '𝚉',
'0': '𝟶', '1': '𝟷', '2': '𝟸', '3': '𝟹', '4': '𝟺', '5': '𝟻', '6': '𝟼', '7': '𝟽', '8': '𝟾', '9': '𝟿'
},
func: function(text) {
return [...text].map(c => this.map[c] || c).join('');
},
preview: function(text) {
return this.func(text);
}
},
doubleStruck: {
name: 'Double-Struck',
map: {
'a': '𝕒', 'b': '𝕓', 'c': '𝕔', 'd': '𝕕', 'e': '𝕖', 'f': '𝕗', 'g': '𝕘', 'h': '𝕙', 'i': '𝕚',
'j': '𝕛', 'k': '𝕜', 'l': '𝕝', 'm': '𝕞', 'n': '𝕟', 'o': '𝕠', 'p': '𝕡', 'q': '𝕢', 'r': '𝕣',
's': '𝕤', 't': '𝕥', 'u': '𝕦', 'v': '𝕧', 'w': '𝕨', 'x': '𝕩', 'y': '𝕪', 'z': '𝕫',
'A': '𝔸', 'B': '𝔹', 'C': '', 'D': '𝔻', 'E': '𝔼', 'F': '𝔽', 'G': '𝔾', 'H': '', 'I': '𝕀',
'J': '𝕁', 'K': '𝕂', 'L': '𝕃', 'M': '𝕄', 'N': '', 'O': '𝕆', 'P': '', 'Q': '', 'R': '',
'S': '𝕊', 'T': '𝕋', 'U': '𝕌', 'V': '𝕍', 'W': '𝕎', 'X': '𝕏', 'Y': '𝕐', 'Z': '',
'0': '𝟘', '1': '𝟙', '2': '𝟚', '3': '𝟛', '4': '𝟜', '5': '𝟝', '6': '𝟞', '7': '𝟟', '8': '𝟠', '9': '𝟡'
},
func: function(text) {
return [...text].map(c => this.map[c] || c).join('');
},
preview: function(text) {
return this.func(text);
}
},
ascii85: {
name: 'ASCII85',
func: function(text) {
// Simple ASCII85 encoding implementation
let result = '<~';
let buffer = 0;
let bufferLength = 0;
for (let i = 0; i < text.length; i++) {
buffer = (buffer << 8) | text.charCodeAt(i);
bufferLength += 8;
if (bufferLength >= 32) {
let value = buffer >>> (bufferLength - 32);
buffer &= (1 << (bufferLength - 32)) - 1;
bufferLength -= 32;
if (value === 0) {
result += 'z';
} else {
for (let j = 4; j >= 0; j--) {
const digit = (value / Math.pow(85, j)) % 85;
result += String.fromCharCode(digit + 33);
}
}
}
}
// Handle remaining bits
if (bufferLength > 0) {
buffer <<= (32 - bufferLength);
let value = buffer;
const bytes = Math.ceil(bufferLength / 8);
for (let j = 4; j >= (4 - bytes); j--) {
const digit = (value / Math.pow(85, j)) % 85;
result += String.fromCharCode(digit + 33);
}
}
return result + '~>';
},
preview: function(text) {
return this.func(text);
},
reverse: function(text) {
// Check if it's a valid ASCII85 string
if (!text.startsWith('<~') || !text.endsWith('~>')) {
return text;
}
// Remove delimiters and whitespace
text = text.substring(2, text.length - 2).replace(/\s+/g, '');
let result = '';
let i = 0;
while (i < text.length) {
// Handle 'z' special case (represents 4 zero bytes)
if (text[i] === 'z') {
result += '\0\0\0\0';
i++;
continue;
}
// Process a group of 5 characters
if (i + 5 <= text.length || i + 1 <= text.length) {
let value = 0;
const limit = Math.min(i + 5, text.length);
// Convert the group to a 32-bit value
for (let j = i; j < limit; j++) {
value = value * 85 + (text.charCodeAt(j) - 33);
}
// Pad with 'u' (84) if needed
for (let j = limit; j < i + 5; j++) {
value = value * 85 + 84;
}
// Extract bytes from the value
const bytesToWrite = limit - i - 1;
for (let j = 3; j >= 4 - bytesToWrite; j--) {
result += String.fromCharCode((value >>> (j * 8)) & 0xFF);
}
i = limit;
} else {
break;
}
}
return result;
}
},
reverse: {
name: 'Reverse Text',
func: function(text) {
return [...text].reverse().join('');
},
preview: function(text) {
return this.func(text);
},
reverse: function(text) {
return this.func(text); // Reversing is its own inverse
}
},
url: {
name: 'URL Encode',
func: function(text) {
return encodeURIComponent(text);
},
preview: function(text) {
return this.func(text);
},
reverse: function(text) {
try {
return decodeURIComponent(text);
} catch (e) {
return text;
}
}
},
html: {
name: 'HTML Entities',
func: function(text) {
return text
.replace(/&/g, '&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, '\'');
}
},
pigLatin: {
name: 'Pig Latin',
func: function(text) {
return text.split(/\s+/).map(word => {
if (!word) return '';
// Check if the word starts with a vowel
if (/^[aeiou]/i.test(word)) {
return word + 'way';
}
// Handle consonant clusters at the beginning
const match = word.match(/^([^aeiou]+)(.*)/i);
if (match) {
return match[2] + match[1] + 'ay';
}
return word;
}).join(' ');
},
preview: function(text) {
return this.func(text);
},
reverse: function(text) {
return text.split(/\s+/).map(word => {
if (!word) return '';
// Check if the word ends with 'way' (vowel case)
if (word.endsWith('way')) {
return word.slice(0, -3);
}
// Check if the word ends with 'ay' (consonant case)
if (word.endsWith('ay')) {
// Extract the part before 'ay'
const base = word.slice(0, -2);
// Find the last consonant cluster
// In Pig Latin, the original first consonant cluster is moved to the end
// So we need to move it back to the beginning
for (let i = 1; i <= base.length; i++) {
const possibleCluster = base.slice(-i);
const possibleResult = possibleCluster + base.slice(0, -i);
// If this looks like a valid word, return it
// This is a simple heuristic and might not work for all cases
if (/^[bcdfghjklmnpqrstvwxyz]/i.test(possibleResult)) {
return possibleResult;
}
}
}
return word;
}).join(' ');
}
},
rainbow: {
name: 'Rainbow Text',
colors: ['#FF0000', '#FF7F00', '#FFFF00', '#00FF00', '#0000FF', '#4B0082', '#9400D3'],
func: function(text) {
// This is just a preview function that returns a description
// The actual rainbow effect is applied in the UI
return text;
},
preview: function(text) {
return text;
}
},
rot47: {
name: 'ROT47',
func: function(text) {
return [...text].map(c => {
const code = c.charCodeAt(0);
// ROT47 operates on a character set from ASCII 33 to ASCII 126
if (code >= 33 && code <= 126) {
return String.fromCharCode(33 + ((code - 33 + 14) % 94));
}
return c;
}).join('');
},
preview: function(text) {
return this.func(text);
},
reverse: function(text) {
return [...text].map(c => {
const code = c.charCodeAt(0);
if (code >= 33 && code <= 126) {
return String.fromCharCode(33 + ((code - 33 + 94 - 14) % 94));
}
return c;
}).join('');
}
},
base32: {
name: 'Base32',
alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567',
func: function(text) {
let result = '';
let bits = 0;
let value = 0;
for (let i = 0; i < text.length; i++) {
value = (value << 8) | text.charCodeAt(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) {
return this.func(text);
},
// Create reverse map for decoding
reverseMap: function() {
const revMap = {};
for (let i = 0; i < this.alphabet.length; i++) {
revMap[this.alphabet[i]] = i;
}
return revMap;
},
reverse: function(text) {
// Remove padding and whitespace
text = text.replace(/\s+/g, '').replace(/=+$/, '');
if (text.length === 0) return '';
const revMap = this.reverseMap();
let result = '';
let bits = 0;
let value = 0;
for (let i = 0; i < text.length; i++) {
const char = text[i].toUpperCase();
if (revMap[char] === undefined) continue; // Skip invalid characters
value = (value << 5) | revMap[char];
bits += 5;
while (bits >= 8) {
bits -= 8;
result += String.fromCharCode((value >> bits) & 0xFF);
}
}
return result;
}
}
};
// Export transforms for use in app.js

View File

@@ -12,7 +12,7 @@ from typing import List, Dict, Optional
from string import ascii_lowercase
# Import additional transformations
from more_transforms_fixed import (
from text_transforms import (
to_upside_down, to_elder_futhark, to_vaporwave, to_zalgo,
to_unicode_circled, to_small_caps, to_braille
)