mirror of
https://github.com/elder-plinius/P4RS3LT0NGV3.git
synced 2026-07-25 13:30:49 +02:00
Sync upstream and refine Translate/PromptCraft/Glitch UI
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Script to fetch and format glitch token data from the repository
|
||||
* This script reads the JSON from stdin and writes it as a JavaScript file
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Read JSON from stdin
|
||||
let jsonData = '';
|
||||
process.stdin.setEncoding('utf8');
|
||||
|
||||
process.stdin.on('data', (chunk) => {
|
||||
jsonData += chunk;
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
try {
|
||||
const json = JSON.parse(jsonData);
|
||||
|
||||
const output = `/**
|
||||
* Glitch Tokens Data
|
||||
* Contains glitch token data structure (LLM vocabulary anomalies)
|
||||
*
|
||||
* Source: https://github.com/elder-plinius/L1B3RT4S
|
||||
* Format: AGGREGLITCH structure
|
||||
*
|
||||
* This file contains the complete glitch token data from the AGGREGLITCH repository.
|
||||
* Last updated: ${json._metadata.last_updated}
|
||||
* Total tokens cataloged: ${json._metadata.total_tokens_cataloged}
|
||||
*/
|
||||
|
||||
window.glitchTokensData = ${JSON.stringify(json, null, 4)};
|
||||
`;
|
||||
|
||||
const outputPath = path.join(__dirname, '..', 'js', 'data', 'glitchTokens.js');
|
||||
fs.writeFileSync(outputPath, output, 'utf8');
|
||||
|
||||
console.log(`✅ Glitch token data written successfully!`);
|
||||
console.log(` File: ${outputPath}`);
|
||||
console.log(` Total tokens: ${json._metadata.total_tokens_cataloged}`);
|
||||
console.log(` Last updated: ${json._metadata.last_updated}`);
|
||||
} catch (error) {
|
||||
console.error('Error processing JSON:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
+846
@@ -1551,6 +1551,257 @@ h1, h2, h3, h4, h5 {
|
||||
border-left-color: var(--case-color);
|
||||
}
|
||||
|
||||
.category-title.transform-category-translate {
|
||||
border-left-color: var(--ancient-color);
|
||||
text-transform: none;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.translate-inline-section {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.translate-inline-section .translate-powered-by {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 400;
|
||||
opacity: 0.8;
|
||||
margin-left: 6px;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.translate-inline-section .pc-error {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.pc-error {
|
||||
color: #f07178;
|
||||
font-size: 0.85rem;
|
||||
padding: 8px 10px;
|
||||
background: rgba(240, 113, 120, 0.08);
|
||||
border: 1px solid rgba(240, 113, 120, 0.35);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.translate-loading {
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 8px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.translate-model-picker {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.translate-model-select {
|
||||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
font-size: 0.8rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--input-border);
|
||||
background: var(--input-bg);
|
||||
color: var(--text-color);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.translate-subsection {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.translate-subsection:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.translate-subsection-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-color);
|
||||
opacity: 0.88;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
button.translate-custom-label-btn {
|
||||
width: 100%;
|
||||
margin: 0 0 6px 0;
|
||||
padding: 6px 8px;
|
||||
font: inherit;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
text-align: left;
|
||||
color: var(--text-color);
|
||||
opacity: 0.88;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
box-sizing: border-box;
|
||||
transition: background 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
button.translate-custom-label-btn:hover {
|
||||
opacity: 1;
|
||||
background: rgba(var(--ancient-color-rgb), 0.08);
|
||||
border-color: var(--input-border);
|
||||
}
|
||||
|
||||
button.translate-custom-label-btn:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(var(--ancient-color-rgb), 0.35);
|
||||
}
|
||||
|
||||
.translate-custom-toggle-end {
|
||||
margin-left: auto;
|
||||
padding: 2px 8px;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--input-border);
|
||||
background: var(--button-bg);
|
||||
color: var(--text-color);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
button.translate-custom-label-btn:hover .translate-custom-toggle-end {
|
||||
background: var(--button-hover-bg);
|
||||
border-color: var(--ancient-color);
|
||||
color: var(--ancient-color);
|
||||
}
|
||||
|
||||
.translate-lang-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.translate-lang-btn {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 8px 6px 20px;
|
||||
min-height: 58px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--input-border);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(to right, rgba(var(--ancient-color-rgb), 0.06), var(--button-bg));
|
||||
}
|
||||
|
||||
.translate-lang-btn:hover:not(:disabled) {
|
||||
background: linear-gradient(to right, rgba(var(--ancient-color-rgb), 0.14), var(--button-hover-bg));
|
||||
box-shadow: 0 2px 8px rgba(var(--ancient-color-rgb), 0.18);
|
||||
}
|
||||
|
||||
.translate-lang-btn:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.translate-lang-exotic {
|
||||
background: linear-gradient(to right, rgba(var(--ancient-color-rgb), 0.09), var(--button-bg));
|
||||
}
|
||||
|
||||
.translate-lang-custom {
|
||||
position: relative;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.translate-lang-favorite.favorite-icon {
|
||||
right: 6px;
|
||||
bottom: 5px;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.translate-custom-flag {
|
||||
color: var(--ancient-color);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.translate-flag {
|
||||
font-size: 1.05rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.translate-name {
|
||||
line-height: 1.2;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.translate-remove {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 4px;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
opacity: 0.55;
|
||||
cursor: pointer;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.translate-remove:hover {
|
||||
opacity: 1;
|
||||
color: #f07178;
|
||||
}
|
||||
|
||||
.translate-add-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.translate-add-form input {
|
||||
flex: 1;
|
||||
min-width: 160px;
|
||||
padding: 6px 10px;
|
||||
font-size: 0.8rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--input-border);
|
||||
background: var(--input-bg);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.translate-add-btn {
|
||||
padding: 6px 12px;
|
||||
font-size: 0.8rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--input-border);
|
||||
background: var(--button-bg);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.translate-add-btn:hover:not(:disabled) {
|
||||
background: var(--button-hover-bg);
|
||||
border-color: var(--ancient-color);
|
||||
}
|
||||
|
||||
.translate-empty-custom {
|
||||
margin-top: 2px;
|
||||
opacity: 0.72;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.transform-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -1725,6 +1976,273 @@ h1, h2, h3, h4, h5 {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
/* Glitch Token Panel */
|
||||
.glitch-token-panel {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 420px;
|
||||
max-width: 90vw;
|
||||
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);
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease-in-out;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.glitch-token-panel.active {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.glitch-token-panel {
|
||||
width: 94vw;
|
||||
max-width: 94vw;
|
||||
}
|
||||
}
|
||||
|
||||
.glitch-token-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid var(--input-border);
|
||||
background-color: var(--button-bg);
|
||||
}
|
||||
|
||||
.glitch-token-header h3 {
|
||||
font-size: 1.2rem;
|
||||
margin: 0;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.glitch-token-header .header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.glitch-token-header .close-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
padding: 5px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.glitch-token-header .close-button:hover {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.glitch-token-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.glitch-token-filters {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid var(--input-border);
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.filter-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-group select,
|
||||
.filter-group input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
background-color: var(--input-bg);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 4px;
|
||||
color: var(--text-color);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.filter-group select:focus,
|
||||
.filter-group input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.glitch-token-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.no-tokens {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: var(--text-color);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.token-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.token-card {
|
||||
background-color: var(--input-bg);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.token-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.token-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.token-text {
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 1rem;
|
||||
color: var(--text-color);
|
||||
word-break: break-all;
|
||||
flex: 1;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.copy-token-button {
|
||||
background: var(--button-bg);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 4px;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
padding: 6px 10px;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.copy-token-button:hover {
|
||||
background-color: var(--accent-color);
|
||||
color: var(--button-bg);
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.token-card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.token-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.badge-unspeakable {
|
||||
background-color: rgba(255, 107, 107, 0.2);
|
||||
color: #ff6b6b;
|
||||
border: 1px solid #ff6b6b;
|
||||
}
|
||||
|
||||
.badge-polysemantic {
|
||||
background-color: rgba(255, 193, 7, 0.2);
|
||||
color: #ffc107;
|
||||
border: 1px solid #ffc107;
|
||||
}
|
||||
|
||||
.badge-glitched_spelling {
|
||||
background-color: rgba(156, 39, 176, 0.2);
|
||||
color: #9c27b0;
|
||||
border: 1px solid #9c27b0;
|
||||
}
|
||||
|
||||
.badge-context_corruptor {
|
||||
background-color: rgba(244, 67, 54, 0.2);
|
||||
color: #f44336;
|
||||
border: 1px solid #f44336;
|
||||
}
|
||||
|
||||
.badge-loop_inducer {
|
||||
background-color: rgba(255, 0, 0, 0.3);
|
||||
color: #ff0000;
|
||||
border: 1px solid #ff0000;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.badge-identity_disruptor {
|
||||
background-color: rgba(255, 152, 0, 0.2);
|
||||
color: #ff9800;
|
||||
border: 1px solid #ff9800;
|
||||
}
|
||||
|
||||
.badge-fragment {
|
||||
background-color: rgba(158, 158, 158, 0.2);
|
||||
color: #9e9e9e;
|
||||
border: 1px solid #9e9e9e;
|
||||
}
|
||||
|
||||
.badge-unreachable {
|
||||
background-color: rgba(96, 125, 139, 0.2);
|
||||
color: #607d8b;
|
||||
border: 1px solid #607d8b;
|
||||
}
|
||||
|
||||
.token-id,
|
||||
.token-origin,
|
||||
.token-output,
|
||||
.token-note {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.token-id {
|
||||
font-family: 'Fira Code', monospace;
|
||||
}
|
||||
|
||||
.token-output {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token-note {
|
||||
margin-top: 4px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid var(--input-border);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.no-history {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
@@ -2874,6 +3392,232 @@ html {
|
||||
.mutation-actions .action-button.download { border-color: #2e7d32; color: #69f0ae; }
|
||||
.mutation-actions .action-button.download:hover { color: #b9f6ca; box-shadow: 0 0 0 1px rgba(105,240,174,.2) inset, 0 0 14px rgba(105,240,174,.18); }
|
||||
|
||||
/* PromptCraft — align with tab / transform chip styling */
|
||||
.promptcraft-section.transform-section {
|
||||
background: var(--secondary-bg);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.promptcraft-section .section-header h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 4px;
|
||||
border-left: 4px solid var(--accent-color);
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.promptcraft-section .section-header h3 small {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 400;
|
||||
color: var(--text-muted);
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--input-border);
|
||||
background: var(--main-bg-color);
|
||||
}
|
||||
|
||||
.promptcraft-section .input-section {
|
||||
position: static;
|
||||
top: auto;
|
||||
z-index: auto;
|
||||
background: var(--main-bg-color);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.pc-controls {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.pc-strategies {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.pc-label {
|
||||
display: block;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.pc-strategy-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pc-strategy-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
min-width: 96px;
|
||||
flex: 1 1 calc(33.333% - 8px);
|
||||
max-width: calc(33.333% - 6px);
|
||||
padding: 10px 8px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
color: var(--text-color);
|
||||
background-color: var(--button-bg);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.pc-strategy-btn i {
|
||||
font-size: 1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.pc-strategy-btn:hover:not(.active) {
|
||||
background-color: var(--button-hover-bg);
|
||||
border-color: var(--accent-color);
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.pc-strategy-btn:hover:not(.active) i {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.pc-strategy-btn.active {
|
||||
background-color: var(--accent-color);
|
||||
color: var(--main-bg-color);
|
||||
border-color: var(--accent-color);
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 8px rgba(var(--accent-color-rgb), 0.35);
|
||||
}
|
||||
|
||||
.pc-strategy-btn.active i {
|
||||
color: var(--main-bg-color);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.pc-strategy-btn:focus {
|
||||
outline: none;
|
||||
box-shadow: var(--focus-shadow);
|
||||
}
|
||||
|
||||
.pc-strategy-btn.active:focus {
|
||||
box-shadow: 0 0 0 2px rgba(var(--accent-color-rgb), 0.5), 0 2px 8px rgba(var(--accent-color-rgb), 0.35);
|
||||
}
|
||||
|
||||
.pc-custom-instruction {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.pc-custom-instruction textarea {
|
||||
width: 100%;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.pc-options.options-grid {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.promptcraft-section .pc-generate-btn {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--accent-color), #42a5f5);
|
||||
color: var(--main-bg-color);
|
||||
border: 1px solid var(--accent-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.promptcraft-section .pc-generate-btn:hover:not(:disabled) {
|
||||
filter: brightness(1.08);
|
||||
box-shadow: 0 4px 12px rgba(var(--accent-color-rgb), 0.35);
|
||||
}
|
||||
|
||||
.promptcraft-section .pc-generate-btn:disabled {
|
||||
opacity: 0.65;
|
||||
cursor: wait;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.pc-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.pc-result-card {
|
||||
background: var(--main-bg-color);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.pc-result-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--input-border);
|
||||
}
|
||||
|
||||
.pc-result-num {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--accent-color);
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.pc-result-text {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.pc-empty-state {
|
||||
text-align: center;
|
||||
padding: 24px 16px;
|
||||
color: var(--text-muted);
|
||||
border: 1px dashed var(--input-border);
|
||||
border-radius: 8px;
|
||||
margin-top: 12px;
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.pc-empty-state i {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 8px;
|
||||
opacity: 0.5;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.pc-strategy-btn {
|
||||
flex: 1 1 calc(50% - 4px);
|
||||
max-width: calc(50% - 4px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.pc-strategy-btn {
|
||||
flex: 1 1 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Message Splitter Styles */
|
||||
.encapsulation-section {
|
||||
margin-top: 16px;
|
||||
@@ -2908,6 +3652,91 @@ html {
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* JSON/XML Fields Section */
|
||||
.json-fields-section,
|
||||
.xml-attributes-section {
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
background: var(--main-bg-color);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.json-fields-section .section-header,
|
||||
.xml-attributes-section .section-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.json-fields-section .section-header h4,
|
||||
.xml-attributes-section .section-header h4 {
|
||||
margin-bottom: 4px;
|
||||
margin-top: 0;
|
||||
color: var(--accent-color);
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.json-fields-section .section-header p,
|
||||
.xml-attributes-section .section-header p {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.field-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.field-row input {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
background-color: var(--input-bg);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 4px;
|
||||
color: var(--text-color);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.field-row input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.remove-field-button,
|
||||
.add-field-button {
|
||||
background-color: var(--button-bg);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 4px;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.remove-field-button:hover {
|
||||
background-color: #ff6b6b;
|
||||
border-color: #ff6b6b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.add-field-button:hover {
|
||||
background-color: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
color: var(--button-bg);
|
||||
}
|
||||
|
||||
.add-field-button {
|
||||
margin-top: 8px;
|
||||
}
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
@@ -3307,6 +4136,11 @@ html {
|
||||
flex: 0 0 calc((100% - 32px) / 5); /* 5 columns: account for 4 gaps of 8px */
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.translate-lang-grid-inline .translate-lang-btn {
|
||||
flex: 0 0 calc((100% - 32px) / 5);
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablets and below (768px) */
|
||||
@@ -3407,6 +4241,12 @@ html {
|
||||
min-width: 0;
|
||||
max-width: calc(50% - 4px);
|
||||
}
|
||||
|
||||
.translate-lang-grid-inline .translate-lang-btn {
|
||||
flex: 0 0 calc(50% - 4px) !important;
|
||||
min-width: 0;
|
||||
max-width: calc(50% - 4px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Medium screens (900px) */
|
||||
@@ -3472,6 +4312,12 @@ html {
|
||||
min-width: 0;
|
||||
max-width: calc(50% - 2px);
|
||||
}
|
||||
|
||||
.translate-lang-grid-inline .translate-lang-btn {
|
||||
flex: 0 0 calc(50% - 2px) !important;
|
||||
min-width: 0;
|
||||
max-width: calc(50% - 2px);
|
||||
}
|
||||
|
||||
/* Options grid single column on very small screens */
|
||||
.options-grid {
|
||||
|
||||
@@ -53,6 +53,14 @@
|
||||
>
|
||||
<i class="fas fa-sliders-h"></i>
|
||||
</button>
|
||||
<button
|
||||
@click="toggleGlitchTokenPanel"
|
||||
class="history-button"
|
||||
title="Glitch Tokens"
|
||||
aria-label="Glitch Tokens"
|
||||
>
|
||||
<i class="fas fa-bug"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -120,6 +128,104 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Glitch Tokens Panel -->
|
||||
<div class="glitch-token-panel" :class="{ 'active': showGlitchTokenPanel }">
|
||||
<div class="glitch-token-header">
|
||||
<h3><i class="fas fa-bug"></i> Glitch Tokens</h3>
|
||||
<div class="header-actions">
|
||||
<button class="close-button" @click="toggleGlitchTokenPanel" title="Close glitch tokens">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="glitch-token-content">
|
||||
<!-- Filter Section -->
|
||||
<div class="glitch-token-filters">
|
||||
<div class="filter-group">
|
||||
<label>Behavior:</label>
|
||||
<select v-model="glitchTokenBehavior" @change="filterGlitchTokens">
|
||||
<option value="">All Behaviors</option>
|
||||
<option value="UNSPEAKABLE">UNSPEAKABLE</option>
|
||||
<option value="POLYSEMANTIC">POLYSEMANTIC</option>
|
||||
<option value="GLITCHED_SPELLING">GLITCHED_SPELLING</option>
|
||||
<option value="CONTEXT_CORRUPTOR">CONTEXT_CORRUPTOR</option>
|
||||
<option value="LOOP_INDUCER">LOOP_INDUCER</option>
|
||||
<option value="IDENTITY_DISRUPTOR">IDENTITY_DISRUPTOR</option>
|
||||
<option value="FRAGMENT">FRAGMENT</option>
|
||||
<option value="CONTROL_CHARACTER">CONTROL_CHARACTER</option>
|
||||
<option value="SPECIAL_TOKEN">SPECIAL_TOKEN</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>Search:</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="glitchTokenSearch"
|
||||
@input="filterGlitchTokens"
|
||||
placeholder="Search tokens..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tokens List -->
|
||||
<div class="glitch-token-list">
|
||||
<div v-if="filteredGlitchTokens.length === 0" class="no-tokens">
|
||||
<p v-if="!glitchTokensLoaded">Loading glitch tokens...</p>
|
||||
<div v-else class="empty-state">
|
||||
<p><strong>No glitch tokens available.</strong></p>
|
||||
<p>Glitch token data is not bundled by default. To use this feature:</p>
|
||||
<ol style="text-align: left; margin: 10px 0; padding-left: 20px;">
|
||||
<li>Obtain glitch token data (e.g., from AGGREGLITCH library)</li>
|
||||
<li>Open browser console and run:<br/>
|
||||
<code style="background: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 3px;">window.setGlitchTokensData(yourData)</code>
|
||||
</li>
|
||||
<li>Refresh the panel to see tokens</li>
|
||||
</ol>
|
||||
<p style="font-size: 0.9em; color: #666; margin-top: 10px;">
|
||||
The data structure should match the AGGREGLITCH format with <code>glitch_tokens</code> containing categorized token arrays.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="token-cards">
|
||||
<div
|
||||
v-for="(token, index) in filteredGlitchTokens"
|
||||
:key="index"
|
||||
class="token-card"
|
||||
:class="'behavior-' + (token.behavior || '').toLowerCase()"
|
||||
>
|
||||
<div class="token-card-header">
|
||||
<span class="token-text">{{ token.token || 'N/A' }}</span>
|
||||
<button
|
||||
class="copy-token-button"
|
||||
@click="copyGlitchToken(token.token)"
|
||||
title="Copy token"
|
||||
>
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="token-card-body">
|
||||
<div class="token-badge" :class="'badge-' + (token.behavior || '').toLowerCase()">
|
||||
{{ token.behavior || 'Unknown' }}
|
||||
</div>
|
||||
<div v-if="token.token_id" class="token-id">
|
||||
ID: {{ token.token_id }}
|
||||
</div>
|
||||
<div v-if="token.origin" class="token-origin">
|
||||
Origin: {{ token.origin }}
|
||||
</div>
|
||||
<div v-if="token.observed_output" class="token-output">
|
||||
Observed: {{ token.observed_output }}
|
||||
</div>
|
||||
<div v-if="token.note" class="token-note">
|
||||
{{ token.note }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Settings Panel (inside app so Vue bindings work) -->
|
||||
<div id="unicode-options-panel" class="unicode-options-panel">
|
||||
<div class="unicode-panel-header">
|
||||
@@ -230,9 +336,13 @@
|
||||
<!-- Generated bundles -->
|
||||
<script src="js/bundles/transforms-bundle.js"></script>
|
||||
|
||||
<!-- Glitch Tokens Data -->
|
||||
<script src="js/data/glitchTokens.js"></script>
|
||||
|
||||
<!-- Load Configuration and Utilities (before modules that depend on them) -->
|
||||
<script src="js/config/constants.js"></script>
|
||||
<script src="js/utils/escapeParser.js"></script>
|
||||
<script src="js/utils/glitchTokens.js"></script>
|
||||
<script src="js/utils/focus.js"></script>
|
||||
<script src="js/utils/notifications.js"></script>
|
||||
<script src="js/utils/history.js"></script>
|
||||
|
||||
@@ -21,6 +21,12 @@ const baseData = {
|
||||
unicodeApplyFlashTimeout: null,
|
||||
showDangerModal: false,
|
||||
dangerThresholdTokens: window.CONFIG.DANGER_THRESHOLD_TOKENS,
|
||||
showGlitchTokenPanel: false,
|
||||
glitchTokensLoaded: false,
|
||||
glitchTokenBehavior: '',
|
||||
glitchTokenSearch: '',
|
||||
filteredGlitchTokens: [],
|
||||
allGlitchTokens: [],
|
||||
openrouterApiKey: localStorage.getItem('openrouter-api-key') || '',
|
||||
showApiKey: false,
|
||||
apiKeySaved: false
|
||||
@@ -157,6 +163,68 @@ window.app = new Vue({
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
toggleGlitchTokenPanel(event) {
|
||||
this.showGlitchTokenPanel = !this.showGlitchTokenPanel;
|
||||
|
||||
// Load tokens if not already loaded
|
||||
if (this.showGlitchTokenPanel && !this.glitchTokensLoaded) {
|
||||
this.loadGlitchTokens();
|
||||
}
|
||||
},
|
||||
|
||||
async loadGlitchTokens() {
|
||||
if (this.glitchTokensLoaded) return;
|
||||
|
||||
try {
|
||||
if (window.loadGlitchTokens) {
|
||||
await window.loadGlitchTokens();
|
||||
}
|
||||
|
||||
if (window.getAllGlitchTokens) {
|
||||
this.allGlitchTokens = window.getAllGlitchTokens();
|
||||
this.filteredGlitchTokens = this.allGlitchTokens;
|
||||
this.glitchTokensLoaded = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading glitch tokens:', error);
|
||||
this.showNotification('Failed to load glitch tokens', 'error', 'fas fa-exclamation-triangle');
|
||||
}
|
||||
},
|
||||
|
||||
filterGlitchTokens() {
|
||||
let filtered = this.allGlitchTokens;
|
||||
|
||||
// Filter by behavior
|
||||
if (this.glitchTokenBehavior) {
|
||||
filtered = filtered.filter(token => token.behavior === this.glitchTokenBehavior);
|
||||
}
|
||||
|
||||
// Filter by search
|
||||
if (this.glitchTokenSearch) {
|
||||
const searchLower = this.glitchTokenSearch.toLowerCase();
|
||||
filtered = filtered.filter(token => {
|
||||
const tokenText = (token.token || '').toLowerCase();
|
||||
const origin = (token.origin || '').toLowerCase();
|
||||
const observedOutput = (token.observed_output || '').toLowerCase();
|
||||
const tokenId = String(token.token_id || '');
|
||||
|
||||
return tokenText.includes(searchLower) ||
|
||||
origin.includes(searchLower) ||
|
||||
observedOutput.includes(searchLower) ||
|
||||
tokenId.includes(searchLower);
|
||||
});
|
||||
}
|
||||
|
||||
this.filteredGlitchTokens = filtered;
|
||||
},
|
||||
|
||||
copyGlitchToken(tokenText) {
|
||||
if (!tokenText) return;
|
||||
|
||||
this.copyToClipboard(tokenText);
|
||||
this.showNotification('Glitch token copied!', 'success', 'fas fa-copy');
|
||||
},
|
||||
|
||||
addToCopyHistory(source, content) {
|
||||
window.HistoryUtils.addToHistory(
|
||||
|
||||
+2
-11
@@ -36,17 +36,8 @@ function universalDecode(input, context = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
if (foundHighPriorityMatch || allDecodings.some(d => d.priority >= 280)) {
|
||||
const exclusiveMatches = allDecodings.filter(d => d.priority >= 280);
|
||||
if (exclusiveMatches.length > 0) {
|
||||
exclusiveMatches.sort((a, b) => b.priority - a.priority);
|
||||
return {
|
||||
text: exclusiveMatches[0].text,
|
||||
method: exclusiveMatches[0].method,
|
||||
alternatives: exclusiveMatches.slice(1).map(d => ({ text: d.text, method: d.method }))
|
||||
};
|
||||
}
|
||||
}
|
||||
// Continue processing to collect all decodings, even if high-priority matches are found
|
||||
// This ensures alternatives are shown
|
||||
|
||||
if (window.steganography && window.steganography.hasEmojiInText && window.steganography.hasEmojiInText(input)) {
|
||||
try {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+113
-52
@@ -13,16 +13,13 @@ class SplitterTool extends Tool {
|
||||
}
|
||||
|
||||
getVueData() {
|
||||
// Load favorites
|
||||
const favorites = this.loadFavorites();
|
||||
|
||||
// Load category order (same as TransformTool)
|
||||
const categoryOrder = this.getCategoryOrder();
|
||||
|
||||
return {
|
||||
// Message Splitter Tab
|
||||
splitterInput: '',
|
||||
splitterMode: 'word', // 'chunk' or 'word' - default to word
|
||||
splitterMode: 'word', // 'chunk', 'word', 'sentence', 'line', 'pattern', 'token'
|
||||
splitterChunkSize: 6,
|
||||
splitterWordSplitSide: 'left', // 'left' or 'right' for even-length words
|
||||
splitterWordSkip: 0, // number of words to skip between splits
|
||||
@@ -32,8 +29,13 @@ class SplitterTool extends Tool {
|
||||
splitterTransforms: [''], // array of transform names to apply in sequence (start with one empty slot)
|
||||
splitterStartWrap: '',
|
||||
splitterEndWrap: '',
|
||||
splitterIteratorMarker: '{n}', // marker to replace with split number
|
||||
splitterCustomPattern: '', // regex pattern for custom pattern mode
|
||||
splitterPatternIncludeDelimiter: false, // include delimiter in split for pattern mode
|
||||
splitterTokenizer: 'cl100k', // tokenizer for token-based mode
|
||||
splitterTokenCount: 3, // token count per chunk for token-based mode
|
||||
splitterPreserveEmptyLines: false, // preserve empty lines for line/sentence modes
|
||||
splitMessages: [],
|
||||
favorites: favorites,
|
||||
categoryOrder: categoryOrder
|
||||
};
|
||||
}
|
||||
@@ -97,50 +99,8 @@ class SplitterTool extends Tool {
|
||||
return [...uniqueFinal, 'randomizer'];
|
||||
}
|
||||
|
||||
loadFavorites() {
|
||||
try {
|
||||
const saved = localStorage.getItem('transformFavorites');
|
||||
if (saved) {
|
||||
const data = JSON.parse(saved);
|
||||
// Filter to only include transforms that still exist
|
||||
if (window.transforms) {
|
||||
return data.filter(transformName => {
|
||||
return Object.values(window.transforms).some(t => t.name === transformName);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load favorites:', e);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
getVueMethods() {
|
||||
return {
|
||||
/**
|
||||
* Get favorite transforms
|
||||
*/
|
||||
getFavoriteTransforms: function() {
|
||||
if (!this.favorites || this.favorites.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return this.favorites
|
||||
.map(transformName => {
|
||||
return this.transforms.find(t => t.name === transformName);
|
||||
})
|
||||
.filter(t => t !== undefined);
|
||||
},
|
||||
/**
|
||||
* Get transforms by category (excluding favorites)
|
||||
*/
|
||||
getTransformsByCategory: function(category) {
|
||||
const categoryTransforms = this.transforms.filter(t => t.category === category);
|
||||
// Exclude favorites from category lists (they're shown separately)
|
||||
if (!this.favorites || this.favorites.length === 0) {
|
||||
return categoryTransforms;
|
||||
}
|
||||
return categoryTransforms.filter(t => !this.favorites.includes(t.name));
|
||||
},
|
||||
/**
|
||||
* Get display name for category (capitalized)
|
||||
*/
|
||||
@@ -202,9 +162,9 @@ class SplitterTool extends Tool {
|
||||
|
||||
/**
|
||||
* Generate split messages from input text
|
||||
* Supports two modes: character chunks or split words in half
|
||||
* Supports multiple modes: character chunks, split words, sentence, line, pattern, token
|
||||
*/
|
||||
generateSplitMessages() {
|
||||
async generateSplitMessages() {
|
||||
// Clear previous output at the start
|
||||
this.splitMessages = [];
|
||||
|
||||
@@ -221,6 +181,99 @@ class SplitterTool extends Tool {
|
||||
for (let i = 0; i < input.length; i += chunkSize) {
|
||||
chunks.push(input.slice(i, i + chunkSize));
|
||||
}
|
||||
} else if (this.splitterMode === 'sentence') {
|
||||
// Sentence mode - split by sentence boundaries
|
||||
const sentenceRegex = /[.!?]+/g;
|
||||
const sentences = input.split(sentenceRegex).filter(s => s.trim().length > 0);
|
||||
chunks = sentences.map(s => s.trim());
|
||||
} else if (this.splitterMode === 'line') {
|
||||
// Line mode - split by newlines
|
||||
chunks = input.split(/\r?\n/).filter(line => line.trim().length > 0 || this.splitterPreserveEmptyLines);
|
||||
} else if (this.splitterMode === 'pattern') {
|
||||
// Custom pattern mode - split by regex
|
||||
const pattern = this.splitterCustomPattern || '\\s+';
|
||||
try {
|
||||
const regex = new RegExp(pattern, 'g');
|
||||
if (this.splitterPatternIncludeDelimiter) {
|
||||
// Include delimiter
|
||||
const parts = input.split(regex);
|
||||
chunks = parts.filter(p => p.length > 0);
|
||||
} else {
|
||||
// Exclude delimiter
|
||||
chunks = input.split(regex).filter(p => p.trim().length > 0);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Invalid regex pattern:', e);
|
||||
this.showNotification('Invalid regex pattern', 'error', 'fas fa-exclamation-triangle');
|
||||
return;
|
||||
}
|
||||
} else if (this.splitterMode === 'token') {
|
||||
// Token-based mode - split by token count
|
||||
try {
|
||||
if (!window.gptTok) {
|
||||
window.gptTok = await import('https://cdn.jsdelivr.net/npm/gpt-tokenizer@2/+esm');
|
||||
}
|
||||
// Map UI names to library encoding names
|
||||
// Note: p50k_base doesn't exist - using p50k_edit (for editing models like code-davinci-edit-001)
|
||||
const map = {
|
||||
cl100k: 'cl100k_base',
|
||||
o200k: 'o200k_base',
|
||||
p50k: 'p50k_edit', // p50k_base doesn't exist in gpt-tokenizer
|
||||
r50k: 'r50k_base'
|
||||
};
|
||||
const enc = map[this.splitterTokenizer] || 'cl100k_base';
|
||||
const tokenCount = Math.max(1, Math.min(1000, this.splitterTokenCount || 3));
|
||||
|
||||
// Debug: Log encoding being used
|
||||
console.log(`[Splitter] Using tokenizer: ${this.splitterTokenizer} -> ${enc}`);
|
||||
console.log(`[Splitter] gptTok object:`, window.gptTok);
|
||||
console.log(`[Splitter] encode function:`, window.gptTok?.encode);
|
||||
|
||||
// Check if the library API is different - might need encoding-specific encoder
|
||||
let tokens;
|
||||
if (window.gptTok.get_encoding) {
|
||||
// Alternative API: get_encoding(name) returns encoder object
|
||||
const encoder = window.gptTok.get_encoding(enc);
|
||||
if (!encoder) {
|
||||
throw new Error(`Encoding ${enc} not found in library`);
|
||||
}
|
||||
tokens = encoder.encode(input);
|
||||
console.log(`[Splitter] Using get_encoding API, got ${tokens.length} tokens`);
|
||||
} else if (window.gptTok.encode) {
|
||||
// Standard API: encode(text, encoding)
|
||||
if (typeof window.gptTok.encode !== 'function') {
|
||||
throw new Error('Tokenizer library not loaded correctly');
|
||||
}
|
||||
tokens = window.gptTok.encode(input, enc);
|
||||
if (!Array.isArray(tokens)) {
|
||||
throw new Error(`Tokenizer returned invalid result for ${enc}`);
|
||||
}
|
||||
console.log(`[Splitter] Using encode API, got ${tokens.length} tokens`);
|
||||
} else {
|
||||
throw new Error('Tokenizer library API not recognized');
|
||||
}
|
||||
|
||||
const tokenChunks = [];
|
||||
for (let i = 0; i < tokens.length; i += tokenCount) {
|
||||
const tokenChunk = tokens.slice(i, i + tokenCount);
|
||||
let text;
|
||||
if (window.gptTok.get_encoding) {
|
||||
const encoder = window.gptTok.get_encoding(enc);
|
||||
text = encoder.decode(tokenChunk);
|
||||
} else {
|
||||
text = window.gptTok.decode(tokenChunk, enc);
|
||||
}
|
||||
tokenChunks.push(text);
|
||||
}
|
||||
|
||||
console.log(`[Splitter] Split into ${tokenChunks.length} chunks using ${enc}`);
|
||||
chunks = tokenChunks;
|
||||
} catch (e) {
|
||||
console.error('Tokenizer error:', e);
|
||||
const errorMsg = e.message || 'Failed to tokenize text';
|
||||
this.showNotification(`Tokenizer error: ${errorMsg}`, 'error', 'fas fa-exclamation-triangle');
|
||||
return;
|
||||
}
|
||||
} else if (this.splitterMode === 'word') {
|
||||
// Word split mode - creates messages with pattern: secondHalf + wholeWords + firstHalf
|
||||
// IMPORTANT: ALL words must be included in output, never filtered out
|
||||
@@ -345,12 +398,20 @@ class SplitterTool extends Tool {
|
||||
}
|
||||
}
|
||||
|
||||
// Apply encapsulation
|
||||
// Apply encapsulation with iterator replacement
|
||||
const start = this.splitterStartWrap || '';
|
||||
const end = this.splitterEndWrap || '';
|
||||
this.splitMessages = processedChunks.map(chunk => `${start}${chunk}${end}`);
|
||||
const marker = this.splitterIteratorMarker || '{n}';
|
||||
|
||||
// Replace iterator marker with split number
|
||||
this.splitMessages = processedChunks.map((chunk, index) => {
|
||||
const num = index + 1;
|
||||
const startReplaced = start.replace(new RegExp(marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), num);
|
||||
const endReplaced = end.replace(new RegExp(marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), num);
|
||||
return `${startReplaced}${chunk}${endReplaced}`;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Copy all split messages to clipboard
|
||||
* Single line: merges messages into one continuous string (keeps encapsulation/transformations)
|
||||
|
||||
@@ -43,15 +43,56 @@ class TokenizerTool extends Tool {
|
||||
if (!window.gptTok) {
|
||||
window.gptTok = await import('https://cdn.jsdelivr.net/npm/gpt-tokenizer@2/+esm');
|
||||
}
|
||||
const map = { cl100k: 'cl100k_base', o200k: 'o200k_base', p50k: 'p50k_base', r50k: 'r50k_base' };
|
||||
// Map UI names to library encoding names
|
||||
// Note: p50k_base doesn't exist - using p50k_edit (for editing models like code-davinci-edit-001)
|
||||
const map = {
|
||||
cl100k: 'cl100k_base',
|
||||
o200k: 'o200k_base',
|
||||
p50k: 'p50k_edit', // p50k_base doesn't exist in gpt-tokenizer
|
||||
r50k: 'r50k_base'
|
||||
};
|
||||
const enc = map[engine];
|
||||
const ids = window.gptTok.encode(text, enc);
|
||||
|
||||
// Check library API - might use get_encoding() or encode() directly
|
||||
let ids;
|
||||
let decoder;
|
||||
|
||||
if (window.gptTok.get_encoding) {
|
||||
// Alternative API: get_encoding(name) returns encoder object
|
||||
const encoder = window.gptTok.get_encoding(enc);
|
||||
if (!encoder) {
|
||||
throw new Error(`Encoding ${enc} not found`);
|
||||
}
|
||||
ids = encoder.encode(text);
|
||||
decoder = encoder;
|
||||
console.log(`[Tokenizer] Using get_encoding API for ${enc}, got ${ids.length} tokens`);
|
||||
} else if (window.gptTok.encode) {
|
||||
// Standard API: encode(text, encoding)
|
||||
if (typeof window.gptTok.encode !== 'function') {
|
||||
throw new Error('Tokenizer library not loaded correctly');
|
||||
}
|
||||
ids = window.gptTok.encode(text, enc);
|
||||
if (!Array.isArray(ids)) {
|
||||
throw new Error(`Tokenizer returned invalid result for ${enc}`);
|
||||
}
|
||||
decoder = null; // Will use window.gptTok.decode
|
||||
console.log(`[Tokenizer] Using encode API for ${enc}, got ${ids.length} tokens`);
|
||||
} else {
|
||||
throw new Error('Tokenizer library API not recognized');
|
||||
}
|
||||
|
||||
for (const id of ids) {
|
||||
const piece = window.gptTok.decode([id], enc);
|
||||
let piece;
|
||||
if (decoder) {
|
||||
piece = decoder.decode([id]);
|
||||
} else {
|
||||
piece = window.gptTok.decode([id], enc);
|
||||
}
|
||||
tokens.push({ id, text: piece });
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load/use gpt-tokenizer; falling back to bytes', e);
|
||||
console.warn('Error details:', e.message, 'Encoding attempted:', engine);
|
||||
this.tokenizerEngine = 'byte';
|
||||
return this.runTokenizer();
|
||||
}
|
||||
|
||||
+141
-45
@@ -14,13 +14,22 @@ class TransformTool extends Tool {
|
||||
|
||||
getVueData() {
|
||||
const transforms = (window.transforms && Object.keys(window.transforms).length > 0)
|
||||
? Object.entries(window.transforms).map(([key, transform]) => ({
|
||||
name: transform.name,
|
||||
func: transform.func.bind(transform),
|
||||
preview: transform.preview.bind(transform),
|
||||
reverse: transform.reverse ? transform.reverse.bind(transform) : null,
|
||||
category: transform.category || 'special'
|
||||
}))
|
||||
? Object.entries(window.transforms)
|
||||
.filter(([key, transform]) => {
|
||||
// Filter out transforms that don't have required properties
|
||||
if (!transform || !transform.name || !transform.func) {
|
||||
console.warn(`Transform "${key}" is missing required properties (name or func)`, transform);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map(([key, transform]) => ({
|
||||
name: transform.name,
|
||||
func: transform.func.bind(transform),
|
||||
preview: transform.preview ? transform.preview.bind(transform) : function() { return '[preview]'; },
|
||||
reverse: transform.reverse ? transform.reverse.bind(transform) : null,
|
||||
category: transform.category || 'special'
|
||||
}))
|
||||
: [];
|
||||
|
||||
const categorySet = new Set();
|
||||
@@ -48,7 +57,7 @@ class TransformTool extends Tool {
|
||||
const favorites = this.loadFavorites();
|
||||
|
||||
return {
|
||||
transformInput: '',
|
||||
transformInput: 'Hello World',
|
||||
transformOutput: '',
|
||||
activeTransform: null,
|
||||
transforms: transforms,
|
||||
@@ -108,12 +117,18 @@ class TransformTool extends Tool {
|
||||
const saved = localStorage.getItem('transformLastUsed');
|
||||
if (saved) {
|
||||
const data = JSON.parse(saved);
|
||||
// Filter to only include transforms that still exist
|
||||
if (window.transforms) {
|
||||
return data.filter(item => {
|
||||
return Object.values(window.transforms).some(t => t.name === item.name);
|
||||
}).slice(0, 5); // Keep only top 5
|
||||
}
|
||||
if (!Array.isArray(data)) return [];
|
||||
return data
|
||||
.filter(item => {
|
||||
if (item && item.kind === 'translate') {
|
||||
return typeof item.lang === 'string' && item.lang.length > 0;
|
||||
}
|
||||
if (item && item.name && window.transforms) {
|
||||
return Object.values(window.transforms).some(t => t.name === item.name);
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.slice(0, 5);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load last used transforms:', e);
|
||||
@@ -148,12 +163,17 @@ class TransformTool extends Tool {
|
||||
const saved = localStorage.getItem('transformFavorites');
|
||||
if (saved) {
|
||||
const data = JSON.parse(saved);
|
||||
// Filter to only include transforms that still exist
|
||||
if (window.transforms) {
|
||||
return data.filter(transformName => {
|
||||
return Object.values(window.transforms).some(t => t.name === transformName);
|
||||
});
|
||||
}
|
||||
if (!Array.isArray(data)) return [];
|
||||
if (!window.transforms) return [];
|
||||
return data.filter(entry => {
|
||||
if (typeof entry === 'string') {
|
||||
return Object.values(window.transforms).some(t => t.name === entry);
|
||||
}
|
||||
if (entry && entry.kind === 'translate' && typeof entry.lang === 'string') {
|
||||
return entry.lang.length > 0;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load favorites:', e);
|
||||
@@ -177,7 +197,11 @@ class TransformTool extends Tool {
|
||||
return transform ? transform.category : 'special';
|
||||
},
|
||||
getTransformsByCategory: function(category) {
|
||||
return this.transforms.filter(transform => transform.category === category);
|
||||
const list = this.transforms.filter(transform => transform.category === category);
|
||||
if (!this.favorites || this.favorites.length === 0) return list;
|
||||
return list.filter(t =>
|
||||
!this.favorites.some(f => typeof f === 'string' && f === t.name)
|
||||
);
|
||||
},
|
||||
isSpecialCategory: function(category) {
|
||||
return category === 'randomizer';
|
||||
@@ -232,58 +256,132 @@ class TransformTool extends Tool {
|
||||
saveLastUsedTransform: function(transformName) {
|
||||
try {
|
||||
let lastUsed = this.lastUsedTransforms || [];
|
||||
|
||||
// Remove if already exists
|
||||
lastUsed = lastUsed.filter(item => item.name !== transformName);
|
||||
|
||||
// Add to front with timestamp
|
||||
lastUsed = lastUsed.filter(item => {
|
||||
if (item.kind === 'translate') return true;
|
||||
return item.name !== transformName;
|
||||
});
|
||||
lastUsed.unshift({
|
||||
name: transformName,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// Keep only last 5
|
||||
lastUsed = lastUsed.slice(0, 5);
|
||||
|
||||
this.lastUsedTransforms = lastUsed;
|
||||
this.showLastUsed = lastUsed.length > 0;
|
||||
|
||||
localStorage.setItem('transformLastUsed', JSON.stringify(lastUsed));
|
||||
} catch (e) {
|
||||
console.warn('Failed to save last used transform:', e);
|
||||
}
|
||||
},
|
||||
getLastUsedTransforms: function() {
|
||||
saveLastUsedTranslate: function(langName, isCustom) {
|
||||
try {
|
||||
let lastUsed = this.lastUsedTransforms || [];
|
||||
const c = !!isCustom;
|
||||
lastUsed = lastUsed.filter(item => {
|
||||
if (item.kind === 'translate') {
|
||||
return !(item.lang === langName && !!item.custom === c);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
lastUsed.unshift({
|
||||
kind: 'translate',
|
||||
lang: langName,
|
||||
custom: c,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
lastUsed = lastUsed.slice(0, 5);
|
||||
this.lastUsedTransforms = lastUsed;
|
||||
this.showLastUsed = lastUsed.length > 0;
|
||||
localStorage.setItem('transformLastUsed', JSON.stringify(lastUsed));
|
||||
} catch (e) {
|
||||
console.warn('Failed to save last used translate:', e);
|
||||
}
|
||||
},
|
||||
getLastUsedDisplayItems: function() {
|
||||
if (!this.lastUsedTransforms || this.lastUsedTransforms.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.lastUsedTransforms
|
||||
.map(item => {
|
||||
return this.transforms.find(t => t.name === item.name);
|
||||
})
|
||||
.filter(t => t !== undefined);
|
||||
const out = [];
|
||||
for (let i = 0; i < this.lastUsedTransforms.length; i++) {
|
||||
const item = this.lastUsedTransforms[i];
|
||||
if (item.kind === 'translate') {
|
||||
out.push({
|
||||
type: 'translate',
|
||||
key: 'lu-tx-' + item.lang + '-' + !!item.custom + '-' + (item.timestamp || i),
|
||||
langName: item.lang,
|
||||
custom: !!item.custom
|
||||
});
|
||||
} else if (item.name) {
|
||||
const t = this.transforms.find(tr => tr.name === item.name);
|
||||
if (t) {
|
||||
out.push({ type: 'transform', key: 'lu-tr-' + item.name + '-' + i, transform: t });
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
},
|
||||
getFavoriteDisplayItems: function() {
|
||||
if (!this.favorites || this.favorites.length === 0) return [];
|
||||
const out = [];
|
||||
for (let i = 0; i < this.favorites.length; i++) {
|
||||
const f = this.favorites[i];
|
||||
if (typeof f === 'string') {
|
||||
const t = this.transforms.find(tr => tr.name === f);
|
||||
if (t) out.push({ type: 'transform', key: 'fav-tr-' + f, transform: t });
|
||||
} else if (f && f.kind === 'translate' && f.lang) {
|
||||
out.push({
|
||||
type: 'translate',
|
||||
key: 'fav-tx-' + f.lang + '-' + !!f.custom,
|
||||
langName: f.lang,
|
||||
custom: !!f.custom
|
||||
});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
},
|
||||
toggleFavorite: function(transformName, event) {
|
||||
if (typeof transformName !== 'string') return;
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
const index = this.favorites.indexOf(transformName);
|
||||
if (index > -1) {
|
||||
// Remove from favorites
|
||||
this.favorites.splice(index, 1);
|
||||
this.showNotification('Removed from favorites', 'success', 'fas fa-star');
|
||||
} else {
|
||||
// Add to favorites
|
||||
this.favorites.push(transformName);
|
||||
this.showNotification('Added to favorites', 'success', 'fas fa-star');
|
||||
}
|
||||
|
||||
this.showFavorites = this.favorites.length > 0;
|
||||
this.saveFavorites(this.favorites);
|
||||
},
|
||||
toggleTranslateFavorite: function(langName, custom, event) {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
const c = !!custom;
|
||||
const idx = this.favorites.findIndex(f => {
|
||||
if (typeof f === 'string') return false;
|
||||
return f && f.kind === 'translate' && f.lang === langName && !!f.custom === c;
|
||||
});
|
||||
if (idx > -1) {
|
||||
this.favorites.splice(idx, 1);
|
||||
this.showNotification('Removed from favorites', 'success', 'fas fa-star');
|
||||
} else {
|
||||
this.favorites.push({ kind: 'translate', lang: langName, custom: c });
|
||||
this.showNotification('Added to favorites', 'success', 'fas fa-star');
|
||||
}
|
||||
this.showFavorites = this.favorites.length > 0;
|
||||
this.saveFavorites(this.favorites);
|
||||
},
|
||||
isTranslateFavorite: function(langName, custom) {
|
||||
const c = !!custom;
|
||||
return this.favorites && this.favorites.some(f =>
|
||||
f && typeof f === 'object' && f.kind === 'translate' &&
|
||||
f.lang === langName && !!f.custom === c
|
||||
);
|
||||
},
|
||||
isFavorite: function(transformName) {
|
||||
return this.favorites && this.favorites.includes(transformName);
|
||||
},
|
||||
@@ -291,11 +389,9 @@ class TransformTool extends Tool {
|
||||
if (!this.favorites || this.favorites.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.favorites
|
||||
.map(transformName => {
|
||||
return this.transforms.find(t => t.name === transformName);
|
||||
})
|
||||
.filter(f => typeof f === 'string')
|
||||
.map(transformName => this.transforms.find(t => t.name === transformName))
|
||||
.filter(t => t !== undefined);
|
||||
},
|
||||
saveFavorites: function(favorites) {
|
||||
|
||||
@@ -206,6 +206,10 @@ class TranslateTool extends Tool {
|
||||
this.transformOutput = translated;
|
||||
this.activeTransform = { name: langName + ' (' + langCode + ')', category: 'translate' };
|
||||
this.copyToClipboard(translated);
|
||||
var isCustomLang = this.translateCustomLangs.some(function(l) { return l.name === langName; });
|
||||
if (typeof this.saveLastUsedTranslate === 'function') {
|
||||
this.saveLastUsedTranslate(langName, isCustomLang);
|
||||
}
|
||||
} else {
|
||||
this.translateError = 'No translation returned. Try a different model.';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* Glitch Tokens Utilities
|
||||
* Provides functions for loading and querying glitch token data
|
||||
*/
|
||||
|
||||
window.GlitchTokenUtils = {
|
||||
// Global state
|
||||
_loaded: false,
|
||||
|
||||
/**
|
||||
* Set glitch tokens data manually (for users who have their own data)
|
||||
*/
|
||||
setGlitchTokensData(data) {
|
||||
window.glitchTokensData = data;
|
||||
this._loaded = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Load glitch tokens data
|
||||
* Uses the data from glitchTokens.js, or allows manual override via setGlitchTokensData()
|
||||
*/
|
||||
async loadGlitchTokens() {
|
||||
if (this._loaded && window.glitchTokensData) {
|
||||
return window.glitchTokensData;
|
||||
}
|
||||
|
||||
// Use the data from glitchTokens.js (or override if setGlitchTokensData was called)
|
||||
if (!window.glitchTokensData) {
|
||||
// Fallback to empty structure if data file wasn't loaded
|
||||
window.glitchTokensData = {
|
||||
_metadata: {
|
||||
name: 'AGGREGLITCH',
|
||||
version: '1.0.0',
|
||||
description: 'The Complete Glitch Token Library - All Known LLM Vocabulary Anomalies',
|
||||
total_tokens_cataloged: 0,
|
||||
last_updated: new Date().toISOString().split('T')[0]
|
||||
},
|
||||
behavior_categories: {},
|
||||
tokenizers: {},
|
||||
glitch_tokens: {}
|
||||
};
|
||||
}
|
||||
this._loaded = true;
|
||||
return window.glitchTokensData;
|
||||
},
|
||||
|
||||
/**
|
||||
* Infer behavior from category or token context
|
||||
*/
|
||||
_inferBehavior(token, category) {
|
||||
// If behavior is already set, use it
|
||||
if (token.behavior) {
|
||||
return token.behavior;
|
||||
}
|
||||
|
||||
// Infer from category name
|
||||
const catLower = category.toLowerCase();
|
||||
if (catLower.includes('control') || catLower.includes('character')) {
|
||||
return 'CONTROL_CHARACTER';
|
||||
}
|
||||
if (catLower.includes('fragment') || catLower.includes('bpe') || catLower.includes('subtoken')) {
|
||||
return 'FRAGMENT';
|
||||
}
|
||||
if (catLower.includes('corrupted') || catLower.includes('unicode') || catLower.includes('mojibake')) {
|
||||
return 'CONTEXT_CORRUPTOR';
|
||||
}
|
||||
if (catLower.includes('syntax') || catLower.includes('code')) {
|
||||
return 'UNSPEAKABLE';
|
||||
}
|
||||
if (catLower.includes('special') || token.purpose) {
|
||||
return 'SPECIAL_TOKEN';
|
||||
}
|
||||
|
||||
// Default to UNKNOWN if we can't infer
|
||||
return 'UNKNOWN';
|
||||
},
|
||||
|
||||
/**
|
||||
* Recursively extract tokens from nested structures
|
||||
*/
|
||||
_extractTokensFromValue(value, category, categoryDescription) {
|
||||
const tokens = [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// If it's an array, check if items are token objects
|
||||
value.forEach(item => {
|
||||
if (item && typeof item === 'object') {
|
||||
// Only extract if it has a 'token' property (actual token text)
|
||||
// Skip items that only have token_id, meaning, examples, etc.
|
||||
if (item.token !== undefined) {
|
||||
const behavior = this._inferBehavior(item, category);
|
||||
|
||||
tokens.push({
|
||||
...item,
|
||||
behavior: behavior,
|
||||
category: category,
|
||||
categoryDescription: categoryDescription
|
||||
});
|
||||
} else {
|
||||
// Recursively check nested structures (but skip if it's just metadata)
|
||||
// Skip common metadata keys
|
||||
if (!item.description && !item.source && !item.quote && !item.meaning && !item.purpose) {
|
||||
tokens.push(...this._extractTokensFromValue(item, category, categoryDescription));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (value && typeof value === 'object') {
|
||||
// If it's an object, recursively check all values
|
||||
// Skip metadata objects (description, source, quote, etc.)
|
||||
if (value.description && !value.token && Object.keys(value).length <= 3) {
|
||||
// This is likely a metadata object, skip it
|
||||
return tokens;
|
||||
}
|
||||
|
||||
for (const [key, val] of Object.entries(value)) {
|
||||
// Skip metadata keys
|
||||
if (key === 'description' || key === 'source' || key === 'quote' || key === 'why' || key === 'scandal') {
|
||||
continue;
|
||||
}
|
||||
tokens.push(...this._extractTokensFromValue(val, category, categoryDescription));
|
||||
}
|
||||
}
|
||||
|
||||
return tokens;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all glitch tokens flattened into a single array
|
||||
*/
|
||||
getAllGlitchTokens() {
|
||||
if (!window.glitchTokensData || !window.glitchTokensData.glitch_tokens) {
|
||||
console.warn('[GlitchTokens] No glitchTokensData found');
|
||||
return [];
|
||||
}
|
||||
|
||||
const tokens = [];
|
||||
const glitchTokens = window.glitchTokensData.glitch_tokens;
|
||||
const categoryCount = Object.keys(glitchTokens).length;
|
||||
console.log(`[GlitchTokens] Processing ${categoryCount} categories`);
|
||||
|
||||
// Iterate through all categories
|
||||
for (const [category, categoryData] of Object.entries(glitchTokens)) {
|
||||
// Skip metadata sections that don't contain tokens
|
||||
if (category === 'exploitation_techniques' ||
|
||||
category === 'detection_tools' ||
|
||||
category === 'statistics' ||
|
||||
category === 'centroid_phenomenon' ||
|
||||
category === 'special_system_tokens') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const categoryDescription = categoryData.description || categoryData.origin || '';
|
||||
|
||||
// Handle categories with tokens array
|
||||
if (categoryData.tokens) {
|
||||
if (Array.isArray(categoryData.tokens)) {
|
||||
// Simple array of tokens
|
||||
categoryData.tokens.forEach(token => {
|
||||
if (token && token.token !== undefined) {
|
||||
const behavior = this._inferBehavior(token, category);
|
||||
|
||||
tokens.push({
|
||||
...token,
|
||||
behavior: behavior,
|
||||
category: category,
|
||||
categoryDescription: categoryDescription
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (typeof categoryData.tokens === 'object') {
|
||||
// Nested structure - recursively extract tokens
|
||||
const extracted = this._extractTokensFromValue(
|
||||
categoryData.tokens,
|
||||
category,
|
||||
categoryDescription
|
||||
);
|
||||
tokens.push(...extracted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[GlitchTokens] Extracted ${tokens.length} tokens total`);
|
||||
return tokens;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get tokens by behavior category
|
||||
*/
|
||||
getTokensByBehavior(behavior) {
|
||||
const allTokens = this.getAllGlitchTokens();
|
||||
return allTokens.filter(token => token.behavior === behavior);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get tokens by tokenizer
|
||||
*/
|
||||
getTokensByTokenizer(tokenizer) {
|
||||
const allTokens = this.getAllGlitchTokens();
|
||||
return allTokens.filter(token => {
|
||||
// Check if token has token_id for this tokenizer
|
||||
// This is a simplified check - actual implementation may need tokenizer-specific lookup
|
||||
return token.token_id !== undefined;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Search tokens by text or ID
|
||||
*/
|
||||
searchGlitchTokens(query) {
|
||||
const allTokens = this.getAllGlitchTokens();
|
||||
const lowerQuery = query.toLowerCase();
|
||||
|
||||
return allTokens.filter(token => {
|
||||
const tokenText = (token.token || '').toLowerCase();
|
||||
const origin = (token.origin || '').toLowerCase();
|
||||
const observedOutput = (token.observed_output || '').toLowerCase();
|
||||
const tokenId = String(token.token_id || '');
|
||||
|
||||
return tokenText.includes(lowerQuery) ||
|
||||
origin.includes(lowerQuery) ||
|
||||
observedOutput.includes(lowerQuery) ||
|
||||
tokenId.includes(lowerQuery);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Expose functions on window for backward compatibility
|
||||
if (typeof window !== 'undefined') {
|
||||
window.setGlitchTokensData = function(data) {
|
||||
window.GlitchTokenUtils.setGlitchTokensData(data);
|
||||
};
|
||||
window.loadGlitchTokens = function() {
|
||||
return window.GlitchTokenUtils.loadGlitchTokens();
|
||||
};
|
||||
window.getAllGlitchTokens = function() {
|
||||
return window.GlitchTokenUtils.getAllGlitchTokens();
|
||||
};
|
||||
window.getTokensByBehavior = function(behavior) {
|
||||
return window.GlitchTokenUtils.getTokensByBehavior(behavior);
|
||||
};
|
||||
window.getTokensByTokenizer = function(tokenizer) {
|
||||
return window.GlitchTokenUtils.getTokensByTokenizer(tokenizer);
|
||||
};
|
||||
window.searchGlitchTokens = function(query) {
|
||||
return window.GlitchTokenUtils.searchGlitchTokens(query);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
// ADFGX cipher transform (WWI German cipher)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'ADFGX Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key: 'KEYWORD', // Default transposition key
|
||||
// ADFGX uses a 5x5 Polybius square with letters A, D, F, G, X as coordinates
|
||||
// Standard square (I and J share position)
|
||||
square: [
|
||||
['A', 'B', 'C', 'D', 'E'],
|
||||
['F', 'G', 'H', 'I', 'K'],
|
||||
['L', 'M', 'N', 'O', 'P'],
|
||||
['Q', 'R', 'S', 'T', 'U'],
|
||||
['V', 'W', 'X', 'Y', 'Z']
|
||||
],
|
||||
// Coordinate labels
|
||||
coords: ['A', 'D', 'F', 'G', 'X'],
|
||||
func: function(text) {
|
||||
const transKey = (this.key || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (transKey.length === 0) return text;
|
||||
|
||||
const cleaned = text.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (cleaned.length === 0) return text;
|
||||
|
||||
// Step 1: Convert to ADFGX coordinates (two letters per character)
|
||||
let adfgxText = '';
|
||||
for (const char of cleaned) {
|
||||
let found = false;
|
||||
for (let row = 0; row < 5; row++) {
|
||||
for (let col = 0; col < 5; col++) {
|
||||
if (this.square[row][col] === char || (char === 'J' && this.square[row][col] === 'I')) {
|
||||
adfgxText += this.coords[row] + this.coords[col];
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Columnar transposition using the key
|
||||
const keyLength = transKey.length;
|
||||
const numCols = keyLength;
|
||||
const numRows = Math.ceil(adfgxText.length / numCols);
|
||||
|
||||
// Create grid
|
||||
const grid = [];
|
||||
let textIdx = 0;
|
||||
for (let row = 0; row < numRows; row++) {
|
||||
grid[row] = [];
|
||||
for (let col = 0; col < numCols; col++) {
|
||||
grid[row][col] = textIdx < adfgxText.length ? adfgxText[textIdx++] : '';
|
||||
}
|
||||
}
|
||||
|
||||
// Sort columns by key
|
||||
const keyOrder = [];
|
||||
for (let i = 0; i < transKey.length; i++) {
|
||||
keyOrder.push({ char: transKey[i], index: i });
|
||||
}
|
||||
keyOrder.sort((a, b) => {
|
||||
if (a.char < b.char) return -1;
|
||||
if (a.char > b.char) return 1;
|
||||
return a.index - b.index;
|
||||
});
|
||||
|
||||
// Read columns in sorted order
|
||||
let result = '';
|
||||
for (const keyItem of keyOrder) {
|
||||
const col = keyItem.index;
|
||||
for (let row = 0; row < numRows; row++) {
|
||||
if (grid[row][col]) {
|
||||
result += grid[row][col];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const transKey = (this.key || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (transKey.length === 0) return text;
|
||||
|
||||
// Only process ADFGX characters
|
||||
const cleaned = text.toUpperCase().replace(/[^ADFGX]/g, '');
|
||||
if (cleaned.length === 0) return text;
|
||||
if (cleaned.length % 2 !== 0) return text; // Must be even length
|
||||
|
||||
// Step 1: Reverse columnar transposition
|
||||
const keyLength = transKey.length;
|
||||
const numCols = keyLength;
|
||||
const numRows = Math.ceil(cleaned.length / numCols);
|
||||
|
||||
// Determine column order (same as encoding)
|
||||
const keyOrder = [];
|
||||
for (let i = 0; i < transKey.length; i++) {
|
||||
keyOrder.push({ char: transKey[i], index: i });
|
||||
}
|
||||
keyOrder.sort((a, b) => {
|
||||
if (a.char < b.char) return -1;
|
||||
if (a.char > b.char) return 1;
|
||||
return a.index - b.index;
|
||||
});
|
||||
|
||||
// Calculate how many characters each original column has
|
||||
// When writing row by row, column i gets chars at positions: i, i+numCols, i+2*numCols, ...
|
||||
// So column i has: Math.ceil((totalLength - i) / numCols) characters
|
||||
|
||||
// Fill grid: write into columns in sorted order
|
||||
const grid = [];
|
||||
for (let row = 0; row < numRows; row++) {
|
||||
grid[row] = new Array(numCols);
|
||||
}
|
||||
|
||||
let textIdx = 0;
|
||||
for (const keyItem of keyOrder) {
|
||||
const col = keyItem.index;
|
||||
// This column originally had this many characters when written row by row
|
||||
const colLength = Math.ceil((cleaned.length - col) / numCols);
|
||||
|
||||
// Write characters into this column, filling top to bottom
|
||||
for (let row = 0; row < colLength && textIdx < cleaned.length; row++) {
|
||||
grid[row][col] = cleaned[textIdx++];
|
||||
}
|
||||
}
|
||||
|
||||
// Read row by row to get ADFGX text
|
||||
let adfgxText = '';
|
||||
for (let row = 0; row < numRows; row++) {
|
||||
for (let col = 0; col < numCols; col++) {
|
||||
if (grid[row] && grid[row][col]) {
|
||||
adfgxText += grid[row][col];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Convert ADFGX coordinates back to letters
|
||||
let result = '';
|
||||
for (let i = 0; i < adfgxText.length; i += 2) {
|
||||
if (i + 1 < adfgxText.length) {
|
||||
const rowChar = adfgxText[i];
|
||||
const colChar = adfgxText[i + 1];
|
||||
const row = this.coords.indexOf(rowChar);
|
||||
const col = this.coords.indexOf(colChar);
|
||||
|
||||
if (row >= 0 && row < 5 && col >= 0 && col < 5) {
|
||||
result += this.square[row][col];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[adfgx]';
|
||||
const result = this.func(text.slice(0, 5));
|
||||
return result.substring(0, 12) + (result.length > 12 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// ADFGX produces only A, D, F, G, X characters
|
||||
const cleaned = text.replace(/[\s]/g, '').toUpperCase();
|
||||
if (cleaned.length < 10) return false;
|
||||
if (!/^[ADFGX]+$/.test(cleaned)) return false;
|
||||
// Must be even length (pairs of coordinates)
|
||||
return cleaned.length % 2 === 0;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// autokey cipher transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Autokey Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key: 'KEY', // Initial key
|
||||
func: function(text) {
|
||||
const key = (this.key || 'KEY').toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (key.length === 0) return text;
|
||||
|
||||
let result = '';
|
||||
let keyIndex = 0;
|
||||
const fullKey = key + text.toUpperCase().replace(/[^A-Z]/g, ''); // Key + plaintext
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i];
|
||||
const code = c.charCodeAt(0);
|
||||
|
||||
if (code >= 65 && code <= 90) { // Uppercase
|
||||
const k = fullKey[keyIndex % fullKey.length].charCodeAt(0) - 65;
|
||||
result += String.fromCharCode(65 + ((code - 65 + k) % 26));
|
||||
keyIndex++;
|
||||
} else if (code >= 97 && code <= 122) { // Lowercase
|
||||
const k = fullKey[keyIndex % fullKey.length].charCodeAt(0) - 65;
|
||||
result += String.fromCharCode(97 + ((code - 97 + k) % 26));
|
||||
keyIndex++;
|
||||
} else {
|
||||
result += c;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const key = (this.key || 'KEY').toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (key.length === 0) return text;
|
||||
|
||||
let result = '';
|
||||
let keyIndex = 0;
|
||||
let decodedSoFar = '';
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i];
|
||||
const code = c.charCodeAt(0);
|
||||
|
||||
if (code >= 65 && code <= 90) { // Uppercase
|
||||
// Use key for first part, then decoded text
|
||||
const keyChar = keyIndex < key.length
|
||||
? key[keyIndex]
|
||||
: decodedSoFar[keyIndex - key.length];
|
||||
const k = keyChar.charCodeAt(0) - 65;
|
||||
const decoded = String.fromCharCode(65 + ((code - 65 - k + 26) % 26));
|
||||
result += decoded;
|
||||
decodedSoFar += decoded;
|
||||
keyIndex++;
|
||||
} else if (code >= 97 && code <= 122) { // Lowercase
|
||||
const keyChar = keyIndex < key.length
|
||||
? key[keyIndex]
|
||||
: decodedSoFar[keyIndex - key.length];
|
||||
const k = keyChar.charCodeAt(0) - 65;
|
||||
const decoded = String.fromCharCode(97 + ((code - 97 - k + 26) % 26));
|
||||
result += decoded;
|
||||
decodedSoFar += decoded;
|
||||
keyIndex++;
|
||||
} else {
|
||||
result += c;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[autokey]';
|
||||
return this.func(text.slice(0, 8)) + (text.length > 8 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// Autokey produces ciphertext that looks like scrambled letters
|
||||
const cleaned = text.replace(/[^A-Za-z]/g, '');
|
||||
if (cleaned.length < 10) return false;
|
||||
|
||||
// Should be mostly letters with some pattern
|
||||
const letterRatio = cleaned.length / text.length;
|
||||
return letterRatio > 0.7;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// beaufort cipher transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Beaufort Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key: 'KEY', // Default key
|
||||
func: function(text) {
|
||||
const key = (this.key || 'KEY').toUpperCase();
|
||||
const keyLength = key.length;
|
||||
let keyIndex = 0;
|
||||
|
||||
return [...text].map(c => {
|
||||
const code = c.charCodeAt(0);
|
||||
if (code >= 65 && code <= 90) { // Uppercase
|
||||
const keyChar = key[keyIndex % keyLength].charCodeAt(0) - 65;
|
||||
const plainChar = code - 65;
|
||||
// Beaufort: cipher = (key - plain) mod 26
|
||||
const result = String.fromCharCode(((keyChar - plainChar + 26) % 26) + 65);
|
||||
keyIndex++;
|
||||
return result;
|
||||
} else if (code >= 97 && code <= 122) { // Lowercase
|
||||
const keyChar = key[keyIndex % keyLength].charCodeAt(0) - 65;
|
||||
const plainChar = code - 97;
|
||||
// Beaufort: cipher = (key - plain) mod 26
|
||||
const result = String.fromCharCode(((keyChar - plainChar + 26) % 26) + 97);
|
||||
keyIndex++;
|
||||
return result;
|
||||
} else {
|
||||
return c;
|
||||
}
|
||||
}).join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Beaufort cipher is self-reciprocal (same function for encode/decode)
|
||||
return this.func(text);
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[beaufort]';
|
||||
const result = this.func(text.slice(0, 8));
|
||||
return result.substring(0, 10) + (result.length > 10 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
const cleaned = text.replace(/[\s.,!?;:'"()\-&0-9]/g, '');
|
||||
if (cleaned.length < 5) return false;
|
||||
const letterCount = (cleaned.match(/[a-zA-Z]/g) || []).length;
|
||||
return letterCount / cleaned.length > 0.7;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
// bifid cipher transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Bifid Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
period: 5, // Period for fractionation (default 5)
|
||||
// Standard Polybius square (5x5, I and J share same cell)
|
||||
square: [
|
||||
['A', 'B', 'C', 'D', 'E'],
|
||||
['F', 'G', 'H', 'I', 'K'],
|
||||
['L', 'M', 'N', 'O', 'P'],
|
||||
['Q', 'R', 'S', 'T', 'U'],
|
||||
['V', 'W', 'X', 'Y', 'Z']
|
||||
],
|
||||
func: function(text) {
|
||||
const period = parseInt(this.period) || 5;
|
||||
const cleaned = text.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (cleaned.length === 0) return text;
|
||||
|
||||
// Step 1: Convert to Polybius coordinates
|
||||
const coords = [];
|
||||
for (const char of cleaned) {
|
||||
let found = false;
|
||||
for (let row = 0; row < 5; row++) {
|
||||
for (let col = 0; col < 5; col++) {
|
||||
if (this.square[row][col] === char || (char === 'J' && this.square[row][col] === 'I')) {
|
||||
coords.push({ row: row + 1, col: col + 1 });
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Write coordinates in rows, then columns
|
||||
const rowSeq = coords.map(c => c.row).join('');
|
||||
const colSeq = coords.map(c => c.col).join('');
|
||||
|
||||
// Step 3: Group by period and read pairs
|
||||
let result = '';
|
||||
for (let i = 0; i < rowSeq.length; i += period) {
|
||||
const rowChunk = rowSeq.substring(i, i + period);
|
||||
const colChunk = colSeq.substring(i, i + period);
|
||||
|
||||
for (let j = 0; j < rowChunk.length; j++) {
|
||||
const row = parseInt(rowChunk[j]) - 1;
|
||||
const col = parseInt(colChunk[j]) - 1;
|
||||
if (row >= 0 && row < 5 && col >= 0 && col < 5) {
|
||||
result += this.square[row][col];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const period = parseInt(this.period) || 5;
|
||||
const cleaned = text.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (cleaned.length === 0) return text;
|
||||
|
||||
// Step 1: Convert letters to coordinates
|
||||
const coords = [];
|
||||
for (const char of cleaned) {
|
||||
let found = false;
|
||||
for (let row = 0; row < 5; row++) {
|
||||
for (let col = 0; col < 5; col++) {
|
||||
if (this.square[row][col] === char || (char === 'J' && this.square[row][col] === 'I')) {
|
||||
coords.push({ row: row + 1, col: col + 1 });
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Group by period, extract row and column sequences
|
||||
let rowSeq = '';
|
||||
let colSeq = '';
|
||||
|
||||
for (let i = 0; i < coords.length; i += period) {
|
||||
const chunk = coords.slice(i, i + period);
|
||||
const chunkRowSeq = chunk.map(c => c.row).join('');
|
||||
const chunkColSeq = chunk.map(c => c.col).join('');
|
||||
rowSeq += chunkRowSeq;
|
||||
colSeq += chunkColSeq;
|
||||
}
|
||||
|
||||
// Step 3: Pair up coordinates and convert back to letters
|
||||
let result = '';
|
||||
for (let i = 0; i < rowSeq.length && i < colSeq.length; i++) {
|
||||
const row = parseInt(rowSeq[i]) - 1;
|
||||
const col = parseInt(colSeq[i]) - 1;
|
||||
if (row >= 0 && row < 5 && col >= 0 && col < 5) {
|
||||
result += this.square[row][col];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[bifid]';
|
||||
const result = this.func(text.slice(0, 5));
|
||||
return result.substring(0, 10) + (result.length > 10 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// Bifid produces scrambled text (all uppercase letters, no digits)
|
||||
const cleaned = text.replace(/[\s]/g, '').toUpperCase();
|
||||
if (cleaned.length < 10) return false;
|
||||
if (!/^[A-Z]+$/.test(cleaned)) return false;
|
||||
|
||||
// Check if it looks scrambled (not readable English)
|
||||
const commonWords = ['THE', 'AND', 'FOR', 'ARE'];
|
||||
const hasCommonWords = commonWords.some(word => cleaned.includes(word));
|
||||
if (hasCommonWords && cleaned.length < 20) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
// columnar transposition cipher transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Columnar Transposition',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key: 'KEY', // Default key
|
||||
func: function(text) {
|
||||
const key = (this.key || 'KEY').toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (key.length === 0) return text;
|
||||
|
||||
// Remove spaces and convert to uppercase for processing
|
||||
const cleaned = text.replace(/\s/g, '').toUpperCase();
|
||||
const keyLength = key.length;
|
||||
const numRows = Math.ceil(cleaned.length / keyLength);
|
||||
|
||||
// Create key order (sorted positions)
|
||||
const keyOrder = key.split('')
|
||||
.map((char, idx) => ({ char, idx }))
|
||||
.sort((a, b) => a.char.localeCompare(b.char))
|
||||
.map((item, newIdx) => ({ originalIdx: item.idx, newIdx }));
|
||||
|
||||
// Fill grid
|
||||
const grid = [];
|
||||
for (let i = 0; i < numRows; i++) {
|
||||
grid[i] = [];
|
||||
for (let j = 0; j < keyLength; j++) {
|
||||
const idx = i * keyLength + j;
|
||||
grid[i][j] = idx < cleaned.length ? cleaned[idx] : 'X';
|
||||
}
|
||||
}
|
||||
|
||||
// Read columns in key order
|
||||
const result = [];
|
||||
keyOrder.forEach(({ originalIdx }) => {
|
||||
for (let i = 0; i < numRows; i++) {
|
||||
result.push(grid[i][originalIdx]);
|
||||
}
|
||||
});
|
||||
|
||||
return result.join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
const key = (this.key || 'KEY').toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (key.length === 0) return text;
|
||||
|
||||
const keyLength = key.length;
|
||||
const numRows = Math.ceil(text.length / keyLength);
|
||||
|
||||
// Create key order
|
||||
const keyOrder = key.split('')
|
||||
.map((char, idx) => ({ char, idx }))
|
||||
.sort((a, b) => a.char.localeCompare(b.char))
|
||||
.map((item, newIdx) => ({ originalIdx: item.idx, newIdx, sortedIdx: newIdx }));
|
||||
|
||||
// Reconstruct grid by reading columns in key order
|
||||
const grid = [];
|
||||
for (let i = 0; i < numRows; i++) {
|
||||
grid[i] = new Array(keyLength);
|
||||
}
|
||||
|
||||
let textIdx = 0;
|
||||
keyOrder.forEach(({ originalIdx }) => {
|
||||
for (let i = 0; i < numRows && textIdx < text.length; i++) {
|
||||
grid[i][originalIdx] = text[textIdx++];
|
||||
}
|
||||
});
|
||||
|
||||
// Read grid row by row
|
||||
const result = [];
|
||||
for (let i = 0; i < numRows; i++) {
|
||||
for (let j = 0; j < keyLength; j++) {
|
||||
if (grid[i][j]) {
|
||||
result.push(grid[i][j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.join('').replace(/X+$/, ''); // Remove padding X's
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[columnar]';
|
||||
const result = this.func(text.slice(0, 10));
|
||||
return result.substring(0, 12) + (result.length > 12 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// Columnar transposition produces text that:
|
||||
// 1. Is all uppercase (after removing spaces)
|
||||
// 2. Has no spaces (or spaces removed)
|
||||
// 3. Has a length that suggests it was transposed (not too short)
|
||||
// 4. Doesn't look like readable English (columnar transposition scrambles text)
|
||||
const cleaned = text.replace(/[\s]/g, '').toUpperCase();
|
||||
|
||||
// Too short to be meaningful
|
||||
if (cleaned.length < 10) return false;
|
||||
|
||||
// Must be mostly letters (allow punctuation anywhere, but primarily letters)
|
||||
// Remove punctuation for the main check
|
||||
const lettersOnly = cleaned.replace(/[^A-Z]/g, '');
|
||||
if (lettersOnly.length < 10) return false; // Need at least 10 letters
|
||||
if (lettersOnly.length < cleaned.length * 0.8) return false; // At least 80% letters
|
||||
|
||||
// Columnar transposition scrambles text, so it shouldn't look like readable English
|
||||
// Check for common English word patterns that would indicate readable text
|
||||
// But be careful - columnar-transposition might preserve some word fragments
|
||||
const strongEnglishPatterns = [
|
||||
/THE[A-Z]{3,}[A-Z]{3,}/, // THE followed by two words (like "THEQUICKBROWN")
|
||||
/[A-Z]{3,}AND[A-Z]{3,}/, // Word AND word (both 3+ letters)
|
||||
/[A-Z]{3,}FOR[A-Z]{3,}/, // Word FOR word (both 3+ letters)
|
||||
/HELLOWORLD/, // HELLO WORLD together
|
||||
/THEQUICK/, // THE QUICK together
|
||||
/QUICKBROWN/, // QUICK BROWN together
|
||||
];
|
||||
|
||||
// If strong English patterns match, it's probably readable English, not scrambled
|
||||
for (const pattern of strongEnglishPatterns) {
|
||||
if (pattern.test(cleaned)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for sequential letter patterns (like ABC, XYZ) which are unlikely in columnar-transposition
|
||||
if (/ABCD|BCDE|CDEF|DEFG|EFGH|FGHI|GHIJ|HIJK|IJKL|JKLM|KLMN|LMNO|MNOP|NOPQ|OPQR|PQRS|QRST|RSTU|STUV|TUVW|UVWX|VWXY|WXYZ/.test(cleaned)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check letter frequency - columnar transposition should have roughly normal letter distribution
|
||||
const letterFreq = {};
|
||||
for (const char of lettersOnly) {
|
||||
letterFreq[char] = (letterFreq[char] || 0) + 1;
|
||||
}
|
||||
const uniqueLetters = Object.keys(letterFreq).length;
|
||||
// If we have very few unique letters, it's probably not columnar-transposition
|
||||
if (uniqueLetters < 5) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
// four-square cipher transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Four-Square Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key1: 'EXAMPLE', // Top-left square key
|
||||
key2: 'KEYWORD', // Bottom-right square key
|
||||
// Standard alphabet for top-right and bottom-left squares
|
||||
standardAlphabet: 'ABCDEFGHIKLMNOPQRSTUVWXYZ',
|
||||
// Create keyed squares
|
||||
createKeyedSquare: function(key) {
|
||||
const used = new Set();
|
||||
const square = [];
|
||||
let keyIdx = 0;
|
||||
let alphaIdx = 0;
|
||||
|
||||
// Fill with key letters first
|
||||
for (let i = 0; i < 5; i++) {
|
||||
square[i] = [];
|
||||
for (let j = 0; j < 5; j++) {
|
||||
while (keyIdx < key.length && used.has(key[keyIdx])) {
|
||||
keyIdx++;
|
||||
}
|
||||
if (keyIdx < key.length) {
|
||||
square[i][j] = key[keyIdx];
|
||||
used.add(key[keyIdx]);
|
||||
keyIdx++;
|
||||
} else {
|
||||
// Fill with remaining alphabet
|
||||
while (alphaIdx < this.standardAlphabet.length && used.has(this.standardAlphabet[alphaIdx])) {
|
||||
alphaIdx++;
|
||||
}
|
||||
if (alphaIdx < this.standardAlphabet.length) {
|
||||
square[i][j] = this.standardAlphabet[alphaIdx];
|
||||
used.add(this.standardAlphabet[alphaIdx]);
|
||||
alphaIdx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return square;
|
||||
},
|
||||
func: function(text) {
|
||||
const key1 = (this.key1 || 'EXAMPLE').toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
|
||||
const key2 = (this.key2 || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
|
||||
|
||||
if (key1.length === 0 || key2.length === 0) return text;
|
||||
|
||||
let cleaned = text.toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
|
||||
if (cleaned.length === 0) return text;
|
||||
if (cleaned.length % 2 !== 0) {
|
||||
// Pad with X if odd length
|
||||
cleaned += 'X';
|
||||
}
|
||||
|
||||
// Create the four squares
|
||||
const topLeft = this.createKeyedSquare(key1);
|
||||
const topRight = this.createKeyedSquare(this.standardAlphabet);
|
||||
const bottomLeft = this.createKeyedSquare(this.standardAlphabet);
|
||||
const bottomRight = this.createKeyedSquare(key2);
|
||||
|
||||
let result = '';
|
||||
|
||||
// Process pairs of letters
|
||||
for (let i = 0; i < cleaned.length; i += 2) {
|
||||
const char1 = cleaned[i];
|
||||
const char2 = cleaned[i + 1];
|
||||
|
||||
// Find char1 in top-left, char2 in bottom-right
|
||||
let row1 = -1, col1 = -1;
|
||||
let row2 = -1, col2 = -1;
|
||||
|
||||
for (let r = 0; r < 5; r++) {
|
||||
for (let c = 0; c < 5; c++) {
|
||||
if (topLeft[r][c] === char1) {
|
||||
row1 = r;
|
||||
col1 = c;
|
||||
}
|
||||
if (bottomRight[r][c] === char2) {
|
||||
row2 = r;
|
||||
col2 = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (row1 >= 0 && col1 >= 0 && row2 >= 0 && col2 >= 0) {
|
||||
// Use row1, col2 from top-right and row2, col1 from bottom-left
|
||||
result += topRight[row1][col2] + bottomLeft[row2][col1];
|
||||
} else {
|
||||
result += char1 + char2;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const key1 = (this.key1 || 'EXAMPLE').toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
|
||||
const key2 = (this.key2 || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
|
||||
|
||||
if (key1.length === 0 || key2.length === 0) return text;
|
||||
|
||||
let cleaned = text.toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
|
||||
if (cleaned.length === 0) return text;
|
||||
if (cleaned.length % 2 !== 0) return text;
|
||||
|
||||
// Create the four squares
|
||||
const topLeft = this.createKeyedSquare(key1);
|
||||
const topRight = this.createKeyedSquare(this.standardAlphabet);
|
||||
const bottomLeft = this.createKeyedSquare(this.standardAlphabet);
|
||||
const bottomRight = this.createKeyedSquare(key2);
|
||||
|
||||
let result = '';
|
||||
|
||||
// Process pairs of letters
|
||||
for (let i = 0; i < cleaned.length; i += 2) {
|
||||
const char1 = cleaned[i];
|
||||
const char2 = cleaned[i + 1];
|
||||
|
||||
// Find char1 in top-right, char2 in bottom-left
|
||||
let row1 = -1, col1 = -1;
|
||||
let row2 = -1, col2 = -1;
|
||||
|
||||
for (let r = 0; r < 5; r++) {
|
||||
for (let c = 0; c < 5; c++) {
|
||||
if (topRight[r][c] === char1) {
|
||||
row1 = r;
|
||||
col1 = c;
|
||||
}
|
||||
if (bottomLeft[r][c] === char2) {
|
||||
row2 = r;
|
||||
col2 = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (row1 >= 0 && col1 >= 0 && row2 >= 0 && col2 >= 0) {
|
||||
// Use row1, col2 from top-left and row2, col1 from bottom-right
|
||||
result += topLeft[row1][col2] + bottomRight[row2][col1];
|
||||
} else {
|
||||
result += char1 + char2;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[four-square]';
|
||||
return this.func(text.slice(0, 4)) + (text.length > 4 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// Four-Square produces scrambled text (all uppercase letters, no digits)
|
||||
const cleaned = text.replace(/[\s]/g, '').toUpperCase();
|
||||
if (cleaned.length < 10) return false;
|
||||
if (!/^[A-Z]+$/.test(cleaned)) return false;
|
||||
if (cleaned.length % 2 !== 0) return false; // Must be even length
|
||||
|
||||
// Check if it looks scrambled (not readable English)
|
||||
const commonWords = ['THE', 'AND', 'FOR', 'ARE'];
|
||||
const hasCommonWords = commonWords.some(word => cleaned.includes(word));
|
||||
if (hasCommonWords && cleaned.length < 20) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// gronsfeld cipher transform (Vigenère with numeric key)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Gronsfeld Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key: '12345', // Default numeric key
|
||||
func: function(text) {
|
||||
const key = (this.key || '12345').replace(/[^0-9]/g, '');
|
||||
if (key.length === 0) return text;
|
||||
|
||||
let result = '';
|
||||
let keyIndex = 0;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i];
|
||||
const code = c.charCodeAt(0);
|
||||
const shift = parseInt(key[keyIndex % key.length]);
|
||||
|
||||
if (code >= 65 && code <= 90) { // Uppercase
|
||||
result += String.fromCharCode(65 + ((code - 65 + shift) % 26));
|
||||
keyIndex++;
|
||||
} else if (code >= 97 && code <= 122) { // Lowercase
|
||||
result += String.fromCharCode(97 + ((code - 97 + shift) % 26));
|
||||
keyIndex++;
|
||||
} else {
|
||||
result += c;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const key = (this.key || '12345').replace(/[^0-9]/g, '');
|
||||
if (key.length === 0) return text;
|
||||
|
||||
let result = '';
|
||||
let keyIndex = 0;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i];
|
||||
const code = c.charCodeAt(0);
|
||||
const shift = parseInt(key[keyIndex % key.length]);
|
||||
|
||||
if (code >= 65 && code <= 90) { // Uppercase
|
||||
result += String.fromCharCode(65 + ((code - 65 - shift + 26) % 26));
|
||||
keyIndex++;
|
||||
} else if (code >= 97 && code <= 122) { // Lowercase
|
||||
result += String.fromCharCode(97 + ((code - 97 - shift + 26) % 26));
|
||||
keyIndex++;
|
||||
} else {
|
||||
result += c;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[gronsfeld]';
|
||||
return this.func(text.slice(0, 8)) + (text.length > 8 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// Gronsfeld produces ciphertext that looks like scrambled letters
|
||||
const cleaned = text.replace(/[^A-Za-z]/g, '');
|
||||
if (cleaned.length < 10) return false;
|
||||
|
||||
// Should be mostly letters with some pattern
|
||||
const letterRatio = cleaned.length / text.length;
|
||||
return letterRatio > 0.7;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// hill cipher transform (matrix-based cipher)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Hill Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
// Default 2x2 key matrix (must be invertible mod 26)
|
||||
key: [[3, 3], [2, 5]], // Default key matrix
|
||||
func: function(text) {
|
||||
const key = this.key || [[3, 3], [2, 5]];
|
||||
const matrixSize = key.length;
|
||||
|
||||
// Prepare text: remove non-letters, pad with X if needed
|
||||
let prepared = text.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
while (prepared.length % matrixSize !== 0) {
|
||||
prepared += 'X';
|
||||
}
|
||||
|
||||
let result = '';
|
||||
|
||||
// Process in blocks of matrixSize
|
||||
for (let i = 0; i < prepared.length; i += matrixSize) {
|
||||
const block = prepared.slice(i, i + matrixSize);
|
||||
const blockNums = block.split('').map(c => c.charCodeAt(0) - 65);
|
||||
|
||||
// Multiply key matrix by block vector
|
||||
const resultNums = [];
|
||||
for (let row = 0; row < matrixSize; row++) {
|
||||
let sum = 0;
|
||||
for (let col = 0; col < matrixSize; col++) {
|
||||
sum += key[row][col] * blockNums[col];
|
||||
}
|
||||
resultNums.push(sum % 26);
|
||||
}
|
||||
|
||||
// Convert back to letters
|
||||
result += resultNums.map(n => String.fromCharCode(n + 65)).join('');
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const key = this.key || [[3, 3], [2, 5]];
|
||||
const matrixSize = key.length;
|
||||
|
||||
// Calculate inverse matrix mod 26
|
||||
const invKey = this.getInverseMatrix(key);
|
||||
if (!invKey) {
|
||||
console.warn('Hill cipher key matrix is not invertible');
|
||||
return text;
|
||||
}
|
||||
|
||||
let prepared = text.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (prepared.length % matrixSize !== 0) {
|
||||
prepared += 'X'.repeat(matrixSize - (prepared.length % matrixSize));
|
||||
}
|
||||
|
||||
let result = '';
|
||||
|
||||
for (let i = 0; i < prepared.length; i += matrixSize) {
|
||||
const block = prepared.slice(i, i + matrixSize);
|
||||
const blockNums = block.split('').map(c => c.charCodeAt(0) - 65);
|
||||
|
||||
const resultNums = [];
|
||||
for (let row = 0; row < matrixSize; row++) {
|
||||
let sum = 0;
|
||||
for (let col = 0; col < matrixSize; col++) {
|
||||
sum += invKey[row][col] * blockNums[col];
|
||||
}
|
||||
resultNums.push((sum % 26 + 26) % 26);
|
||||
}
|
||||
|
||||
result += resultNums.map(n => String.fromCharCode(n + 65)).join('');
|
||||
}
|
||||
|
||||
// Remove padding X's
|
||||
return result.replace(/X+$/, '');
|
||||
},
|
||||
getInverseMatrix: function(matrix) {
|
||||
// For 2x2 matrix: inverse = (1/det) * [[d, -b], [-c, a]]
|
||||
// where det = ad - bc
|
||||
if (matrix.length !== 2 || matrix[0].length !== 2) {
|
||||
return null; // Only support 2x2 for now
|
||||
}
|
||||
|
||||
const a = matrix[0][0];
|
||||
const b = matrix[0][1];
|
||||
const c = matrix[1][0];
|
||||
const d = matrix[1][1];
|
||||
|
||||
const det = (a * d - b * c) % 26;
|
||||
if (det === 0 || this.gcd(det, 26) !== 1) {
|
||||
return null; // Matrix not invertible mod 26
|
||||
}
|
||||
|
||||
// Find modular inverse of det mod 26
|
||||
const detInv = this.modInverse(det, 26);
|
||||
|
||||
return [
|
||||
[(d * detInv) % 26, (-b * detInv + 26 * 26) % 26],
|
||||
[(-c * detInv + 26 * 26) % 26, (a * detInv) % 26]
|
||||
];
|
||||
},
|
||||
gcd: function(a, b) {
|
||||
while (b !== 0) {
|
||||
[a, b] = [b, a % b];
|
||||
}
|
||||
return a;
|
||||
},
|
||||
modInverse: function(a, m) {
|
||||
// Extended Euclidean algorithm
|
||||
let [oldR, r] = [a, m];
|
||||
let [oldS, s] = [1, 0];
|
||||
|
||||
while (r !== 0) {
|
||||
const quotient = Math.floor(oldR / r);
|
||||
[oldR, r] = [r, oldR - quotient * r];
|
||||
[oldS, s] = [s, oldS - quotient * s];
|
||||
}
|
||||
|
||||
return (oldS % m + m) % m;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[hill]';
|
||||
const result = this.func(text.slice(0, 4));
|
||||
return result.substring(0, 8) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
const cleaned = text.replace(/[\s]/g, '').toUpperCase();
|
||||
return cleaned.length >= 4 && cleaned.length % 2 === 0 && /^[A-Z]+$/.test(cleaned);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// homophonic cipher transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Homophonic Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
// Simple homophonic substitution - each letter maps to multiple symbols
|
||||
map: {
|
||||
'A': ['1', '2', '3'], 'B': ['4', '5'], 'C': ['6', '7', '8'],
|
||||
'D': ['9', '10'], 'E': ['11', '12', '13', '14', '15'], 'F': ['16', '17'],
|
||||
'G': ['18', '19'], 'H': ['20', '21', '22'], 'I': ['23', '24', '25', '26'],
|
||||
'J': ['27'], 'K': ['28'], 'L': ['29', '30', '31'], 'M': ['32', '33'],
|
||||
'N': ['34', '35', '36'], 'O': ['37', '38', '39', '40'], 'P': ['41', '42'],
|
||||
'Q': ['43'], 'R': ['44', '45', '46'], 'S': ['47', '48', '49', '50'],
|
||||
'T': ['51', '52', '53', '54', '55'], 'U': ['56', '57'], 'V': ['58'],
|
||||
'W': ['59', '60'], 'X': ['61'], 'Y': ['62', '63'], 'Z': ['64']
|
||||
},
|
||||
func: function(text) {
|
||||
let result = '';
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i].toUpperCase();
|
||||
if (this.map[c]) {
|
||||
// Randomly select one of the homophones
|
||||
const options = this.map[c];
|
||||
result += options[Math.floor(Math.random() * options.length)];
|
||||
// Add space after number (but not if next char is already a space)
|
||||
if (i < text.length - 1 && text[i + 1] !== ' ') {
|
||||
result += ' ';
|
||||
}
|
||||
} else if (c === ' ') {
|
||||
// Preserve spaces - add as double space to distinguish from number separators
|
||||
result += ' ';
|
||||
} else {
|
||||
// Non-letter characters (keep as-is, no space after)
|
||||
result += text[i];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Build reverse map
|
||||
if (!this._reverseMap) {
|
||||
this._reverseMap = {};
|
||||
for (const [letter, numbers] of Object.entries(this.map)) {
|
||||
numbers.forEach(num => {
|
||||
this._reverseMap[num] = letter;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Numbers are separated by single spaces, double spaces are original spaces
|
||||
let result = '';
|
||||
// Split on double spaces first to preserve original spaces
|
||||
const sections = text.split(/\s{2,}/);
|
||||
|
||||
for (let s = 0; s < sections.length; s++) {
|
||||
const section = sections[s];
|
||||
// Split on spaces, but also handle punctuation
|
||||
const tokens = section.split(/(\s+)/);
|
||||
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const token = tokens[i];
|
||||
if (/^\s+$/.test(token)) {
|
||||
// Whitespace - skip (single spaces between numbers)
|
||||
continue;
|
||||
} else if (/^\d+$/.test(token)) {
|
||||
// Pure number - decode it
|
||||
result += this._reverseMap[token] || token;
|
||||
} else {
|
||||
// Contains non-digits - extract numbers and decode, preserve rest
|
||||
// Match numbers that are space-separated
|
||||
const parts = token.split(/(\d+)/);
|
||||
for (let j = 0; j < parts.length; j++) {
|
||||
const part = parts[j];
|
||||
if (/^\d+$/.test(part)) {
|
||||
result += this._reverseMap[part] || part;
|
||||
} else {
|
||||
result += part;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add space between sections (original spaces)
|
||||
if (s < sections.length - 1) {
|
||||
result += ' ';
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[homophonic]';
|
||||
const result = this.func(text.slice(0, 3));
|
||||
return result.substring(0, 15) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check if text is space-separated numbers (homophonic cipher output)
|
||||
const parts = text.trim().split(/\s+/);
|
||||
return parts.length >= 3 && parts.every(p => /^\d+$/.test(p));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// nihilist cipher transform (Polybius square with numeric key)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Nihilist Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key: '12345', // Default numeric key
|
||||
// Standard Polybius square (5x5, I and J share same cell)
|
||||
square: [
|
||||
['A', 'B', 'C', 'D', 'E'],
|
||||
['F', 'G', 'H', 'I', 'K'],
|
||||
['L', 'M', 'N', 'O', 'P'],
|
||||
['Q', 'R', 'S', 'T', 'U'],
|
||||
['V', 'W', 'X', 'Y', 'Z']
|
||||
],
|
||||
func: function(text) {
|
||||
const key = (this.key || '12345').replace(/[^0-9]/g, '');
|
||||
if (key.length === 0) return text;
|
||||
|
||||
const cleaned = text.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (cleaned.length === 0) return text;
|
||||
|
||||
// Step 1: Convert text to Polybius coordinates
|
||||
const coords = [];
|
||||
for (const char of cleaned) {
|
||||
let found = false;
|
||||
for (let row = 0; row < 5; row++) {
|
||||
for (let col = 0; col < 5; col++) {
|
||||
if (this.square[row][col] === char || (char === 'J' && this.square[row][col] === 'I')) {
|
||||
coords.push((row + 1) * 10 + (col + 1)); // Two-digit number
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Add key values to coordinates (repeating key)
|
||||
let result = '';
|
||||
for (let i = 0; i < coords.length; i++) {
|
||||
const keyDigit = parseInt(key[i % key.length]);
|
||||
const sum = coords[i] + keyDigit;
|
||||
result += sum.toString().padStart(2, '0') + ' ';
|
||||
}
|
||||
|
||||
return result.trim();
|
||||
},
|
||||
reverse: function(text) {
|
||||
const key = (this.key || '12345').replace(/[^0-9]/g, '');
|
||||
if (key.length === 0) return text;
|
||||
|
||||
// Extract two-digit numbers
|
||||
const numbers = text.match(/\d{2}/g) || [];
|
||||
if (numbers.length === 0) return text;
|
||||
|
||||
// Step 1: Subtract key values from numbers
|
||||
const coords = [];
|
||||
for (let i = 0; i < numbers.length; i++) {
|
||||
const num = parseInt(numbers[i]);
|
||||
const keyDigit = parseInt(key[i % key.length]);
|
||||
const coord = num - keyDigit;
|
||||
if (coord >= 11 && coord <= 55) {
|
||||
coords.push(coord);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Convert coordinates back to letters
|
||||
let result = '';
|
||||
for (const coord of coords) {
|
||||
const row = Math.floor(coord / 10) - 1;
|
||||
const col = (coord % 10) - 1;
|
||||
|
||||
if (row >= 0 && row < 5 && col >= 0 && col < 5) {
|
||||
result += this.square[row][col];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[nihilist]';
|
||||
const result = this.func(text.slice(0, 5));
|
||||
return result.substring(0, 15) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
// Nihilist produces pairs of digits (typically 11-99 range after key addition)
|
||||
const digitPairs = text.match(/\d{2}/g) || [];
|
||||
if (digitPairs.length < 3) return false;
|
||||
|
||||
// Check if pairs are in reasonable range (after key addition, could be 11-99)
|
||||
const validPairs = digitPairs.filter(pair => {
|
||||
const num = parseInt(pair);
|
||||
return num >= 11 && num <= 99;
|
||||
});
|
||||
|
||||
// At least 70% should be valid Nihilist pairs
|
||||
return validPairs.length / digitPairs.length >= 0.7;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// pigpen cipher transform (also known as Masonic or Freemason's cipher)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Pigpen Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
// Pigpen cipher uses geometric symbols arranged in grids
|
||||
// Standard Pigpen cipher mapping based on dCode.fr implementation (Original variant)
|
||||
// Reference: https://www.dcode.fr/pigpen-cipher
|
||||
// Grid 1 (A-I): L-shapes and U-shapes in 3x3 grid positions
|
||||
// Grid 2 (J-R): Same shapes as A-I but with dots
|
||||
// Grid 3 (S-Z): Caret/X shapes (some with dots)
|
||||
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': '⟑'
|
||||
},
|
||||
func: function(text) {
|
||||
return [...text].map(c => {
|
||||
const upper = c.toUpperCase();
|
||||
if (this.map[upper]) {
|
||||
// Preserve case: if original was lowercase, return lowercase symbol
|
||||
// (though symbols don't have case, we'll just use the symbol)
|
||||
return this.map[upper];
|
||||
}
|
||||
return c;
|
||||
}).join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[pigpen]';
|
||||
return this.func(text.slice(0, 5));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check if text contains Pigpen symbols (dCode.fr Unicode characters)
|
||||
const pigpenSymbols = /[ᒧ⊔ᒪ⊐☐⊏ᒣ⊓ᒥ⟓⨃ᒷ⪾🝕⪽ᒬ⩀⟔ᐯᐳᐸᐱ⟇ᑀᑅ⟑]/;
|
||||
return pigpenSymbols.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
// playfair cipher transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Playfair Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key: 'KEYWORD', // Default key
|
||||
func: function(text) {
|
||||
const key = (this.key || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (key.length === 0) return text;
|
||||
|
||||
// Create Playfair square
|
||||
const alphabet = 'ABCDEFGHIKLMNOPQRSTUVWXYZ'; // J is combined with I
|
||||
const keyChars = [...new Set(key.split(''))];
|
||||
const remaining = alphabet.split('').filter(c => !keyChars.includes(c));
|
||||
const square = [...keyChars, ...remaining];
|
||||
|
||||
// Helper to find position in square
|
||||
const findPos = (char) => {
|
||||
const idx = square.indexOf(char === 'J' ? 'I' : char);
|
||||
return { row: Math.floor(idx / 5), col: idx % 5 };
|
||||
};
|
||||
|
||||
// Helper to get char from position
|
||||
const getChar = (row, col) => square[row * 5 + col];
|
||||
|
||||
// Prepare text: remove non-letters, replace J with I, add X between double letters
|
||||
let prepared = text.toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
|
||||
if (prepared.length % 2 !== 0) prepared += 'X';
|
||||
|
||||
// Process pairs
|
||||
let result = '';
|
||||
for (let i = 0; i < prepared.length; i += 2) {
|
||||
const a = prepared[i];
|
||||
const b = prepared[i + 1];
|
||||
const posA = findPos(a);
|
||||
const posB = findPos(b);
|
||||
|
||||
if (posA.row === posB.row) {
|
||||
// Same row: shift right
|
||||
result += getChar(posA.row, (posA.col + 1) % 5);
|
||||
result += getChar(posB.row, (posB.col + 1) % 5);
|
||||
} else if (posA.col === posB.col) {
|
||||
// Same column: shift down
|
||||
result += getChar((posA.row + 1) % 5, posA.col);
|
||||
result += getChar((posB.row + 1) % 5, posB.col);
|
||||
} else {
|
||||
// Rectangle: swap columns
|
||||
result += getChar(posA.row, posB.col);
|
||||
result += getChar(posB.row, posA.col);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const key = (this.key || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (key.length === 0) return text;
|
||||
|
||||
const alphabet = 'ABCDEFGHIKLMNOPQRSTUVWXYZ';
|
||||
const keyChars = [...new Set(key.split(''))];
|
||||
const remaining = alphabet.split('').filter(c => !keyChars.includes(c));
|
||||
const square = [...keyChars, ...remaining];
|
||||
|
||||
const findPos = (char) => {
|
||||
const idx = square.indexOf(char === 'J' ? 'I' : char);
|
||||
return { row: Math.floor(idx / 5), col: idx % 5 };
|
||||
};
|
||||
|
||||
const getChar = (row, col) => square[row * 5 + col];
|
||||
|
||||
let prepared = text.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (prepared.length % 2 !== 0) prepared += 'X';
|
||||
|
||||
let result = '';
|
||||
for (let i = 0; i < prepared.length; i += 2) {
|
||||
const a = prepared[i];
|
||||
const b = prepared[i + 1];
|
||||
const posA = findPos(a);
|
||||
const posB = findPos(b);
|
||||
|
||||
if (posA.row === posB.row) {
|
||||
// Same row: shift left
|
||||
result += getChar(posA.row, (posA.col + 4) % 5);
|
||||
result += getChar(posB.row, (posB.col + 4) % 5);
|
||||
} else if (posA.col === posB.col) {
|
||||
// Same column: shift up
|
||||
result += getChar((posA.row + 4) % 5, posA.col);
|
||||
result += getChar((posB.row + 4) % 5, posB.col);
|
||||
} else {
|
||||
// Rectangle: swap columns (same as encode)
|
||||
result += getChar(posA.row, posB.col);
|
||||
result += getChar(posB.row, posA.col);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[playfair]';
|
||||
const result = this.func(text.slice(0, 8));
|
||||
return result.substring(0, 10) + (result.length > 10 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
const cleaned = text.replace(/[\s]/g, '').toUpperCase();
|
||||
return cleaned.length >= 4 && cleaned.length % 2 === 0 && /^[A-Z]+$/.test(cleaned);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// polybius square cipher transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Polybius Square',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
// Standard Polybius square (5x5, I and J share same cell)
|
||||
square: [
|
||||
['A', 'B', 'C', 'D', 'E'],
|
||||
['F', 'G', 'H', 'I', 'K'], // I and J share position
|
||||
['L', 'M', 'N', 'O', 'P'],
|
||||
['Q', 'R', 'S', 'T', 'U'],
|
||||
['V', 'W', 'X', 'Y', 'Z']
|
||||
],
|
||||
func: function(text) {
|
||||
const cleaned = text.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (cleaned.length === 0) return text;
|
||||
|
||||
let result = '';
|
||||
for (const char of cleaned) {
|
||||
let found = false;
|
||||
for (let row = 0; row < 5; row++) {
|
||||
for (let col = 0; col < 5; col++) {
|
||||
if (this.square[row][col] === char || (char === 'J' && this.square[row][col] === 'I')) {
|
||||
result += String(row + 1) + String(col + 1);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) break;
|
||||
}
|
||||
if (!found) {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Extract number pairs sequentially
|
||||
let result = '';
|
||||
let i = 0;
|
||||
|
||||
while (i < text.length) {
|
||||
// Look for two consecutive digits
|
||||
if (i + 1 < text.length && /\d/.test(text[i]) && /\d/.test(text[i + 1])) {
|
||||
const row = parseInt(text[i]) - 1;
|
||||
const col = parseInt(text[i + 1]) - 1;
|
||||
|
||||
if (row >= 0 && row < 5 && col >= 0 && col < 5) {
|
||||
result += this.square[row][col];
|
||||
i += 2;
|
||||
} else {
|
||||
result += text[i];
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
result += text[i];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[polybius]';
|
||||
const result = this.func(text.slice(0, 5));
|
||||
return result.substring(0, 10) + (result.length > 10 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// Polybius square produces pairs of digits (11-55)
|
||||
const digitPairs = text.match(/\d{2}/g) || [];
|
||||
if (digitPairs.length < 3) return false;
|
||||
|
||||
// Check if pairs are valid (1-5 for each digit)
|
||||
const validPairs = digitPairs.filter(pair => {
|
||||
const d1 = parseInt(pair[0]);
|
||||
const d2 = parseInt(pair[1]);
|
||||
return d1 >= 1 && d1 <= 5 && d2 >= 1 && d2 <= 5;
|
||||
});
|
||||
|
||||
// At least 70% should be valid Polybius pairs
|
||||
return validPairs.length / digitPairs.length >= 0.7;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
// porta cipher transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Porta Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key: 'KEY', // Default key
|
||||
func: function(text) {
|
||||
const key = (this.key || 'KEY').toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (key.length === 0) return text;
|
||||
|
||||
// Porta uses 13 reciprocal alphabets, each pair (A/B, C/D, etc.) shares a tableau
|
||||
// Each tableau is reciprocal: if A->B in encode, then B->A in decode (same operation)
|
||||
const tableaus = {
|
||||
'A': 'NOPQRSTUVWXYZABCDEFGHIJKLM', 'B': 'NOPQRSTUVWXYZABCDEFGHIJKLM',
|
||||
'C': 'OPQRSTUVWXYZABCDEFGHIJKLMN', 'D': 'OPQRSTUVWXYZABCDEFGHIJKLMN',
|
||||
'E': 'PQRSTUVWXYZABCDEFGHIJKLMNO', 'F': 'PQRSTUVWXYZABCDEFGHIJKLMNO',
|
||||
'G': 'QRSTUVWXYZABCDEFGHIJKLMNOP', 'H': 'QRSTUVWXYZABCDEFGHIJKLMNOP',
|
||||
'I': 'RSTUVWXYZABCDEFGHIJKLMNOPQ', 'J': 'RSTUVWXYZABCDEFGHIJKLMNOPQ',
|
||||
'K': 'STUVWXYZABCDEFGHIJKLMNOPQR', 'L': 'STUVWXYZABCDEFGHIJKLMNOPQR',
|
||||
'M': 'TUVWXYZABCDEFGHIJKLMNOPQRS', 'N': 'TUVWXYZABCDEFGHIJKLMNOPQRS',
|
||||
'O': 'UVWXYZABCDEFGHIJKLMNOPQRST', 'P': 'UVWXYZABCDEFGHIJKLMNOPQRST',
|
||||
'Q': 'VWXYZABCDEFGHIJKLMNOPQRSTU', 'R': 'VWXYZABCDEFGHIJKLMNOPQRSTU',
|
||||
'S': 'WXYZABCDEFGHIJKLMNOPQRSTUV', 'T': 'WXYZABCDEFGHIJKLMNOPQRSTUV',
|
||||
'U': 'XYZABCDEFGHIJKLMNOPQRSTUVW', 'V': 'XYZABCDEFGHIJKLMNOPQRSTUVW',
|
||||
'W': 'YZABCDEFGHIJKLMNOPQRSTUVWX', 'X': 'YZABCDEFGHIJKLMNOPQRSTUVWX',
|
||||
'Y': 'ZABCDEFGHIJKLMNOPQRSTUVWXY', 'Z': 'ZABCDEFGHIJKLMNOPQRSTUVWXY'
|
||||
};
|
||||
|
||||
let result = '';
|
||||
let keyIndex = 0;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i];
|
||||
const code = c.charCodeAt(0);
|
||||
|
||||
if (code >= 65 && code <= 90) { // Uppercase
|
||||
const keyChar = key[keyIndex % key.length];
|
||||
const tableau = tableaus[keyChar];
|
||||
const plainPos = code - 65;
|
||||
// Porta: cipher = tableau[plain]
|
||||
result += tableau[plainPos];
|
||||
keyIndex++;
|
||||
} else if (code >= 97 && code <= 122) { // Lowercase
|
||||
const keyChar = key[keyIndex % key.length];
|
||||
const tableau = tableaus[keyChar];
|
||||
const plainPos = code - 97;
|
||||
// Porta: cipher = tableau[plain] (lowercase)
|
||||
result += tableau[plainPos].toLowerCase();
|
||||
keyIndex++;
|
||||
} else {
|
||||
result += c;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Porta is self-reciprocal - use reverse lookup in tableau
|
||||
const key = (this.key || 'KEY').toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (key.length === 0) return text;
|
||||
|
||||
const tableaus = {
|
||||
'A': 'NOPQRSTUVWXYZABCDEFGHIJKLM', 'B': 'NOPQRSTUVWXYZABCDEFGHIJKLM',
|
||||
'C': 'OPQRSTUVWXYZABCDEFGHIJKLMN', 'D': 'OPQRSTUVWXYZABCDEFGHIJKLMN',
|
||||
'E': 'PQRSTUVWXYZABCDEFGHIJKLMNO', 'F': 'PQRSTUVWXYZABCDEFGHIJKLMNO',
|
||||
'G': 'QRSTUVWXYZABCDEFGHIJKLMNOP', 'H': 'QRSTUVWXYZABCDEFGHIJKLMNOP',
|
||||
'I': 'RSTUVWXYZABCDEFGHIJKLMNOPQ', 'J': 'RSTUVWXYZABCDEFGHIJKLMNOPQ',
|
||||
'K': 'STUVWXYZABCDEFGHIJKLMNOPQR', 'L': 'STUVWXYZABCDEFGHIJKLMNOPQR',
|
||||
'M': 'TUVWXYZABCDEFGHIJKLMNOPQRS', 'N': 'TUVWXYZABCDEFGHIJKLMNOPQRS',
|
||||
'O': 'UVWXYZABCDEFGHIJKLMNOPQRST', 'P': 'UVWXYZABCDEFGHIJKLMNOPQRST',
|
||||
'Q': 'VWXYZABCDEFGHIJKLMNOPQRSTU', 'R': 'VWXYZABCDEFGHIJKLMNOPQRSTU',
|
||||
'S': 'WXYZABCDEFGHIJKLMNOPQRSTUV', 'T': 'WXYZABCDEFGHIJKLMNOPQRSTUV',
|
||||
'U': 'XYZABCDEFGHIJKLMNOPQRSTUVW', 'V': 'XYZABCDEFGHIJKLMNOPQRSTUVW',
|
||||
'W': 'YZABCDEFGHIJKLMNOPQRSTUVWX', 'X': 'YZABCDEFGHIJKLMNOPQRSTUVWX',
|
||||
'Y': 'ZABCDEFGHIJKLMNOPQRSTUVWXY', 'Z': 'ZABCDEFGHIJKLMNOPQRSTUVWXY'
|
||||
};
|
||||
|
||||
let result = '';
|
||||
let keyIndex = 0;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i];
|
||||
const code = c.charCodeAt(0);
|
||||
|
||||
if (code >= 65 && code <= 90) { // Uppercase
|
||||
const keyChar = key[keyIndex % key.length];
|
||||
const tableau = tableaus[keyChar];
|
||||
// Find position of ciphertext char in tableau - that's the plaintext position
|
||||
const cipherChar = String.fromCharCode(code);
|
||||
const plainPos = tableau.indexOf(cipherChar);
|
||||
if (plainPos >= 0) {
|
||||
result += String.fromCharCode(plainPos + 65);
|
||||
} else {
|
||||
result += c;
|
||||
}
|
||||
keyIndex++;
|
||||
} else if (code >= 97 && code <= 122) { // Lowercase
|
||||
const keyChar = key[keyIndex % key.length];
|
||||
const tableau = tableaus[keyChar];
|
||||
const cipherChar = String.fromCharCode(code - 32); // Convert to uppercase for lookup
|
||||
const plainPos = tableau.indexOf(cipherChar);
|
||||
if (plainPos >= 0) {
|
||||
result += String.fromCharCode(plainPos + 97);
|
||||
} else {
|
||||
result += c;
|
||||
}
|
||||
keyIndex++;
|
||||
} else {
|
||||
result += c;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[porta]';
|
||||
const result = this.func(text.slice(0, 8));
|
||||
return result.substring(0, 10) + (result.length > 10 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
const cleaned = text.replace(/[\s.,!?;:'"()\-&0-9]/g, '');
|
||||
if (cleaned.length < 5) return false;
|
||||
const letterCount = (cleaned.match(/[a-zA-Z]/g) || []).length;
|
||||
return letterCount / cleaned.length > 0.7;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// ROT128 cipher transform (Extended ASCII rotation)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'ROT128',
|
||||
priority: 50,
|
||||
category: 'cipher',
|
||||
func: function(text) {
|
||||
// ROT128 rotates through Extended ASCII range 0-255
|
||||
const shift = 128;
|
||||
let result = '';
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const code = text.charCodeAt(i);
|
||||
// Rotate within 0-255 range
|
||||
if (code >= 0 && code <= 255) {
|
||||
const rotated = (code + shift) % 256;
|
||||
result += String.fromCharCode(rotated);
|
||||
} else {
|
||||
result += text[i]; // Keep characters outside range as-is
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
// ROT128 is self-reciprocal (rotating by 128 twice = full rotation)
|
||||
return this.func(text);
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[rot128]';
|
||||
return this.func(text.slice(0, 8)) + (text.length > 8 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// ROT128 produces extended ASCII characters (128-255)
|
||||
const hasExtendedAscii = /[\x80-\xFF]/.test(text);
|
||||
return hasExtendedAscii && text.length >= 5;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// ROT8000 cipher transform (Unicode rotation)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
// Build valid character codes once (excludes control chars and surrogates)
|
||||
function buildValidCodes() {
|
||||
const validCodes = [];
|
||||
for (let i = 0; i <= 0xFFFF; i++) {
|
||||
// Skip control characters (0x0000-0x001F)
|
||||
if (i >= 0x0000 && i <= 0x001F) continue;
|
||||
// Skip DEL and some controls (0x007F-0x00A0)
|
||||
if (i >= 0x007F && i <= 0x00A0) continue;
|
||||
// Skip surrogate pairs (0xD800-0xDFFF)
|
||||
if (i >= 0xD800 && i <= 0xDFFF) continue;
|
||||
validCodes.push(i);
|
||||
}
|
||||
return validCodes;
|
||||
}
|
||||
|
||||
// Cache the valid codes and mappings
|
||||
let cachedValidCodes = null;
|
||||
let cachedCodeToIndex = null;
|
||||
let cachedShift = null;
|
||||
|
||||
function getRot8000Data() {
|
||||
if (!cachedValidCodes) {
|
||||
cachedValidCodes = buildValidCodes();
|
||||
// ROT8000 uses a shift that makes it self-reciprocal (applying twice returns original)
|
||||
// The shift should be approximately 0x8000 (32768), which is half the BMP
|
||||
// For self-reciprocity: (index + shift + shift) % validCount == index
|
||||
// This means: (2 * shift) % validCount == 0, so shift = validCount / 2
|
||||
cachedShift = Math.floor(cachedValidCodes.length / 2);
|
||||
cachedCodeToIndex = new Map();
|
||||
cachedValidCodes.forEach((code, index) => {
|
||||
cachedCodeToIndex.set(code, index);
|
||||
});
|
||||
}
|
||||
return { validCodes: cachedValidCodes, codeToIndex: cachedCodeToIndex, shift: cachedShift };
|
||||
}
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'ROT8000',
|
||||
priority: 50,
|
||||
category: 'cipher',
|
||||
func: function(text) {
|
||||
// ROT8000 rotates Unicode BMP characters (0x0000-0xFFFF)
|
||||
// Excludes control characters: U+0000-U+001F, U+007F-U+00A0, U+D800-U+DFFF
|
||||
// Shift is half the valid range for self-reciprocity
|
||||
|
||||
const { validCodes, codeToIndex, shift } = getRot8000Data();
|
||||
const validCount = validCodes.length;
|
||||
|
||||
let result = '';
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const code = text.charCodeAt(i);
|
||||
|
||||
// Check if character is in valid range
|
||||
if (codeToIndex.has(code)) {
|
||||
const index = codeToIndex.get(code);
|
||||
const rotatedIndex = (index + shift) % validCount;
|
||||
const rotatedCode = validCodes[rotatedIndex];
|
||||
result += String.fromCharCode(rotatedCode);
|
||||
} else {
|
||||
// Keep invalid characters as-is (spaces, emojis, etc.)
|
||||
result += text[i];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
// ROT8000 is self-reciprocal (rotating by 0x8000 twice = full rotation)
|
||||
return this.func(text);
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[rot8000]';
|
||||
return this.func(text.slice(0, 8)) + (text.length > 8 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// ROT8000 produces characters in various Unicode ranges
|
||||
// Check for non-ASCII characters that aren't typical text
|
||||
const hasNonAscii = /[^\x00-\x7F]/.test(text);
|
||||
const hasControlChars = /[\x00-\x1F]/.test(text);
|
||||
return hasNonAscii && text.length >= 5;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// scytale cipher transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Scytale Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key: 5, // Default number of columns (wrapping width)
|
||||
func: function(text) {
|
||||
const key = parseInt(this.key) || 5;
|
||||
if (key < 2) return text;
|
||||
|
||||
// Remove spaces for encoding
|
||||
const cleaned = text.replace(/\s/g, '').toUpperCase();
|
||||
if (cleaned.length === 0) return text;
|
||||
|
||||
// Calculate number of rows needed
|
||||
const numRows = Math.ceil(cleaned.length / key);
|
||||
|
||||
// Fill grid row by row
|
||||
const grid = [];
|
||||
for (let i = 0; i < numRows; i++) {
|
||||
grid[i] = [];
|
||||
for (let j = 0; j < key; j++) {
|
||||
const idx = i * key + j;
|
||||
grid[i][j] = idx < cleaned.length ? cleaned[idx] : '';
|
||||
}
|
||||
}
|
||||
|
||||
// Read column by column
|
||||
let result = '';
|
||||
for (let j = 0; j < key; j++) {
|
||||
for (let i = 0; i < numRows; i++) {
|
||||
if (grid[i][j]) {
|
||||
result += grid[i][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const key = parseInt(this.key) || 5;
|
||||
if (key < 2) return text;
|
||||
|
||||
const cleaned = text.replace(/\s/g, '').toUpperCase();
|
||||
if (cleaned.length === 0) return text;
|
||||
|
||||
// Calculate number of rows
|
||||
const numRows = Math.ceil(cleaned.length / key);
|
||||
|
||||
// Fill grid column by column (reverse of encoding)
|
||||
const grid = [];
|
||||
for (let i = 0; i < numRows; i++) {
|
||||
grid[i] = new Array(key);
|
||||
}
|
||||
|
||||
let textIdx = 0;
|
||||
for (let j = 0; j < key; j++) {
|
||||
for (let i = 0; i < numRows && textIdx < cleaned.length; i++) {
|
||||
grid[i][j] = cleaned[textIdx++];
|
||||
}
|
||||
}
|
||||
|
||||
// Read row by row
|
||||
let result = '';
|
||||
for (let i = 0; i < numRows; i++) {
|
||||
for (let j = 0; j < key; j++) {
|
||||
if (grid[i][j]) {
|
||||
result += grid[i][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[scytale]';
|
||||
const result = this.func(text.slice(0, 10));
|
||||
return result.substring(0, 12) + (result.length > 12 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// Scytale produces scrambled text - similar to columnar transposition
|
||||
const cleaned = text.replace(/[\s]/g, '').toUpperCase();
|
||||
if (cleaned.length < 10) return false;
|
||||
if (!/^[A-Z]+$/.test(cleaned)) return false;
|
||||
|
||||
// Check if it looks scrambled (not readable English)
|
||||
const commonWords = ['THE', 'AND', 'FOR', 'ARE', 'BUT', 'NOT', 'YOU'];
|
||||
const hasCommonWords = commonWords.some(word => cleaned.includes(word));
|
||||
if (hasCommonWords && cleaned.length < 30) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
// trifid cipher transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Trifid Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
period: 5, // Period for fractionation (default 5)
|
||||
// Trifid uses a 3x3x3 cube (27 positions for A-Z and space/punctuation)
|
||||
// Structure: 3 layers, each with 3 rows and 3 columns
|
||||
cube: [
|
||||
// Layer 0
|
||||
[['A', 'B', 'C'], ['D', 'E', 'F'], ['G', 'H', 'I']],
|
||||
// Layer 1
|
||||
[['J', 'K', 'L'], ['M', 'N', 'O'], ['P', 'Q', 'R']],
|
||||
// Layer 2
|
||||
[['S', 'T', 'U'], ['V', 'W', 'X'], ['Y', 'Z', ' ']]
|
||||
],
|
||||
func: function(text) {
|
||||
const period = parseInt(this.period) || 5;
|
||||
const cleaned = text.toUpperCase().replace(/[^A-Z ]/g, '');
|
||||
if (cleaned.length === 0) return text;
|
||||
|
||||
// Step 1: Convert to Trifid coordinates (layer, row, col) - all 1-indexed
|
||||
const coords = [];
|
||||
for (const char of cleaned) {
|
||||
let found = false;
|
||||
for (let layer = 0; layer < 3; layer++) {
|
||||
for (let row = 0; row < 3; row++) {
|
||||
for (let col = 0; col < 3; col++) {
|
||||
if (this.cube[layer] && this.cube[layer][row] && this.cube[layer][row][col] === char) {
|
||||
coords.push({ layer: layer + 1, row: row + 1, col: col + 1 });
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) break;
|
||||
}
|
||||
if (found) break;
|
||||
}
|
||||
if (!found && char === ' ') {
|
||||
coords.push({ layer: 3, row: 3, col: 3 }); // Space at layer 3, row 3, col 3
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Write coordinates in sequence, then group by period
|
||||
const layerSeq = coords.map(c => c.layer).join('');
|
||||
const rowSeq = coords.map(c => c.row).join('');
|
||||
const colSeq = coords.map(c => c.col).join('');
|
||||
|
||||
// Step 3: Group by period and read triplets
|
||||
let result = '';
|
||||
for (let i = 0; i < layerSeq.length; i += period) {
|
||||
const layerChunk = layerSeq.substring(i, i + period);
|
||||
const rowChunk = rowSeq.substring(i, i + period);
|
||||
const colChunk = colSeq.substring(i, i + period);
|
||||
|
||||
for (let j = 0; j < layerChunk.length; j++) {
|
||||
const layer = parseInt(layerChunk[j]) - 1;
|
||||
const row = parseInt(rowChunk[j]) - 1;
|
||||
const col = parseInt(colChunk[j]) - 1;
|
||||
|
||||
if (layer >= 0 && layer < 3 && row >= 0 && row < 3 && col >= 0 && col < 3) {
|
||||
if (this.cube[layer] && this.cube[layer][row] && this.cube[layer][row][col]) {
|
||||
result += this.cube[layer][row][col];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const period = parseInt(this.period) || 5;
|
||||
const cleaned = text.toUpperCase().replace(/[^A-Z ]/g, '');
|
||||
if (cleaned.length === 0) return text;
|
||||
|
||||
// Step 1: Convert letters to coordinates
|
||||
const coords = [];
|
||||
for (const char of cleaned) {
|
||||
let found = false;
|
||||
for (let layer = 0; layer < 3; layer++) {
|
||||
for (let row = 0; row < 3; row++) {
|
||||
for (let col = 0; col < 3; col++) {
|
||||
if (this.cube[layer] && this.cube[layer][row] && this.cube[layer][row][col] === char) {
|
||||
coords.push({ layer: layer + 1, row: row + 1, col: col + 1 });
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) break;
|
||||
}
|
||||
if (found) break;
|
||||
}
|
||||
if (!found && char === ' ') {
|
||||
coords.push({ layer: 3, row: 3, col: 3 });
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Group by period, extract sequences
|
||||
let layerSeq = '';
|
||||
let rowSeq = '';
|
||||
let colSeq = '';
|
||||
|
||||
for (let i = 0; i < coords.length; i += period) {
|
||||
const chunk = coords.slice(i, i + period);
|
||||
const chunkLayerSeq = chunk.map(c => c.layer).join('');
|
||||
const chunkRowSeq = chunk.map(c => c.row).join('');
|
||||
const chunkColSeq = chunk.map(c => c.col).join('');
|
||||
layerSeq += chunkLayerSeq;
|
||||
rowSeq += chunkRowSeq;
|
||||
colSeq += chunkColSeq;
|
||||
}
|
||||
|
||||
// Step 3: Pair up coordinates and convert back to letters
|
||||
let result = '';
|
||||
for (let i = 0; i < layerSeq.length && i < rowSeq.length && i < colSeq.length; i++) {
|
||||
const layer = parseInt(layerSeq[i]) - 1;
|
||||
const row = parseInt(rowSeq[i]) - 1;
|
||||
const col = parseInt(colSeq[i]) - 1;
|
||||
|
||||
if (layer >= 0 && layer < 3 && row >= 0 && row < 3 && col >= 0 && col < 3) {
|
||||
if (this.cube[layer] && this.cube[layer][row] && this.cube[layer][row][col]) {
|
||||
result += this.cube[layer][row][col];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[trifid]';
|
||||
const result = this.func(text.slice(0, 5));
|
||||
return result.substring(0, 10) + (result.length > 10 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// Trifid produces scrambled text (all uppercase letters, no digits)
|
||||
const cleaned = text.replace(/[\s]/g, '').toUpperCase();
|
||||
if (cleaned.length < 10) return false;
|
||||
if (!/^[A-Z]+$/.test(cleaned)) return false;
|
||||
|
||||
// Check if it looks scrambled (not readable English)
|
||||
const commonWords = ['THE', 'AND', 'FOR', 'ARE'];
|
||||
const hasCommonWords = commonWords.some(word => cleaned.includes(word));
|
||||
if (hasCommonWords && cleaned.length < 20) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
// two-square cipher transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Two-Square Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key1: 'EXAMPLE', // Top square key
|
||||
key2: 'KEYWORD', // Bottom square key
|
||||
// Standard alphabet for reference
|
||||
standardAlphabet: 'ABCDEFGHIKLMNOPQRSTUVWXYZ',
|
||||
// Create keyed square
|
||||
createKeyedSquare: function(key) {
|
||||
const used = new Set();
|
||||
const square = [];
|
||||
let keyIdx = 0;
|
||||
let alphaIdx = 0;
|
||||
|
||||
// Fill with key letters first
|
||||
for (let i = 0; i < 5; i++) {
|
||||
square[i] = [];
|
||||
for (let j = 0; j < 5; j++) {
|
||||
while (keyIdx < key.length && used.has(key[keyIdx])) {
|
||||
keyIdx++;
|
||||
}
|
||||
if (keyIdx < key.length) {
|
||||
square[i][j] = key[keyIdx];
|
||||
used.add(key[keyIdx]);
|
||||
keyIdx++;
|
||||
} else {
|
||||
// Fill with remaining alphabet
|
||||
while (alphaIdx < this.standardAlphabet.length && used.has(this.standardAlphabet[alphaIdx])) {
|
||||
alphaIdx++;
|
||||
}
|
||||
if (alphaIdx < this.standardAlphabet.length) {
|
||||
square[i][j] = this.standardAlphabet[alphaIdx];
|
||||
used.add(this.standardAlphabet[alphaIdx]);
|
||||
alphaIdx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return square;
|
||||
},
|
||||
func: function(text) {
|
||||
const key1 = (this.key1 || 'EXAMPLE').toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
|
||||
const key2 = (this.key2 || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
|
||||
|
||||
if (key1.length === 0 || key2.length === 0) return text;
|
||||
|
||||
let cleaned = text.toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
|
||||
if (cleaned.length === 0) return text;
|
||||
if (cleaned.length % 2 !== 0) {
|
||||
// Pad with X if odd length
|
||||
cleaned += 'X';
|
||||
}
|
||||
|
||||
// Create the two squares
|
||||
const topSquare = this.createKeyedSquare(key1);
|
||||
const bottomSquare = this.createKeyedSquare(key2);
|
||||
|
||||
let result = '';
|
||||
|
||||
// Process pairs of letters
|
||||
for (let i = 0; i < cleaned.length; i += 2) {
|
||||
const char1 = cleaned[i];
|
||||
const char2 = cleaned[i + 1];
|
||||
|
||||
// Find char1 in top square, char2 in bottom square
|
||||
let row1 = -1, col1 = -1;
|
||||
let row2 = -1, col2 = -1;
|
||||
|
||||
for (let r = 0; r < 5; r++) {
|
||||
for (let c = 0; c < 5; c++) {
|
||||
if (topSquare[r][c] === char1) {
|
||||
row1 = r;
|
||||
col1 = c;
|
||||
}
|
||||
if (bottomSquare[r][c] === char2) {
|
||||
row2 = r;
|
||||
col2 = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (row1 >= 0 && col1 >= 0 && row2 >= 0 && col2 >= 0) {
|
||||
// Use row1, col2 from top square and row2, col1 from bottom square
|
||||
result += topSquare[row1][col2] + bottomSquare[row2][col1];
|
||||
} else {
|
||||
result += char1 + char2;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const key1 = (this.key1 || 'EXAMPLE').toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
|
||||
const key2 = (this.key2 || 'KEYWORD').toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
|
||||
|
||||
if (key1.length === 0 || key2.length === 0) return text;
|
||||
|
||||
let cleaned = text.toUpperCase().replace(/[^A-Z]/g, '').replace(/J/g, 'I');
|
||||
if (cleaned.length === 0) return text;
|
||||
if (cleaned.length % 2 !== 0) return text;
|
||||
|
||||
// Create the two squares
|
||||
const topSquare = this.createKeyedSquare(key1);
|
||||
const bottomSquare = this.createKeyedSquare(key2);
|
||||
|
||||
let result = '';
|
||||
|
||||
// Process pairs of letters
|
||||
for (let i = 0; i < cleaned.length; i += 2) {
|
||||
const char1 = cleaned[i];
|
||||
const char2 = cleaned[i + 1];
|
||||
|
||||
// Find char1 in top square, char2 in bottom square
|
||||
let row1 = -1, col1 = -1;
|
||||
let row2 = -1, col2 = -1;
|
||||
|
||||
for (let r = 0; r < 5; r++) {
|
||||
for (let c = 0; c < 5; c++) {
|
||||
if (topSquare[r][c] === char1) {
|
||||
row1 = r;
|
||||
col1 = c;
|
||||
}
|
||||
if (bottomSquare[r][c] === char2) {
|
||||
row2 = r;
|
||||
col2 = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (row1 >= 0 && col1 >= 0 && row2 >= 0 && col2 >= 0) {
|
||||
// Reverse: use row1, col2 from top square and row2, col1 from bottom square
|
||||
// But we need to find the original positions
|
||||
// Actually, the reverse is the same as forward for Two-Square
|
||||
result += topSquare[row1][col2] + bottomSquare[row2][col1];
|
||||
} else {
|
||||
result += char1 + char2;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[two-square]';
|
||||
return this.func(text.slice(0, 4)) + (text.length > 4 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// Two-Square produces scrambled text (all uppercase letters, no digits)
|
||||
const cleaned = text.replace(/[\s]/g, '').toUpperCase();
|
||||
if (cleaned.length < 10) return false;
|
||||
if (!/^[A-Z]+$/.test(cleaned)) return false;
|
||||
if (cleaned.length % 2 !== 0) return false; // Must be even length
|
||||
|
||||
// Check if it looks scrambled (not readable English)
|
||||
const commonWords = ['THE', 'AND', 'FOR', 'ARE'];
|
||||
const hasCommonWords = commonWords.some(word => cleaned.includes(word));
|
||||
if (hasCommonWords && cleaned.length < 20) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// XOR cipher transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'XOR Cipher',
|
||||
priority: 70,
|
||||
category: 'cipher',
|
||||
key: 'KEY', // Default key
|
||||
func: function(text) {
|
||||
const key = this.key || 'KEY';
|
||||
const keyBytes = new TextEncoder().encode(key);
|
||||
const textBytes = new TextEncoder().encode(text);
|
||||
const result = new Uint8Array(textBytes.length);
|
||||
|
||||
for (let i = 0; i < textBytes.length; i++) {
|
||||
result[i] = textBytes[i] ^ keyBytes[i % keyBytes.length];
|
||||
}
|
||||
|
||||
// Convert to hex string
|
||||
return Array.from(result)
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// XOR is self-reciprocal, but we need to convert from hex first
|
||||
try {
|
||||
const hexBytes = text.match(/.{1,2}/g) || [];
|
||||
const bytes = new Uint8Array(hexBytes.map(h => parseInt(h, 16)));
|
||||
const key = this.key || 'KEY';
|
||||
const keyBytes = new TextEncoder().encode(key);
|
||||
const result = new Uint8Array(bytes.length);
|
||||
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
result[i] = bytes[i] ^ keyBytes[i % keyBytes.length];
|
||||
}
|
||||
|
||||
return new TextDecoder().decode(result);
|
||||
} catch (e) {
|
||||
return text;
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[xor]';
|
||||
const result = this.func(text.slice(0, 4));
|
||||
return result.substring(0, 12) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check if text is hex-encoded (XOR cipher output)
|
||||
const cleaned = text.trim().replace(/\s/g, '');
|
||||
return cleaned.length >= 4 &&
|
||||
cleaned.length % 2 === 0 &&
|
||||
/^[0-9a-fA-F]+$/.test(cleaned);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
// base122 encoding (more efficient than Base64)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Base122',
|
||||
priority: 250,
|
||||
category: 'encoding',
|
||||
func: function(text) {
|
||||
// Base122 uses UTF-8 bytes and encodes them more efficiently
|
||||
// It uses 7-bit ASCII (0-127) plus some safe 2-byte UTF-8 sequences
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
let result = '';
|
||||
|
||||
let i = 0;
|
||||
while (i < bytes.length) {
|
||||
const byte = bytes[i];
|
||||
|
||||
if (byte < 128) {
|
||||
// Single byte ASCII
|
||||
result += String.fromCharCode(byte);
|
||||
i++;
|
||||
} else if (i + 1 < bytes.length) {
|
||||
// Try to encode as 2-byte sequence
|
||||
const b1 = byte;
|
||||
const b2 = bytes[i + 1];
|
||||
|
||||
// Check if it's a valid 2-byte UTF-8 sequence
|
||||
if ((b1 & 0xE0) === 0xC0 && (b2 & 0xC0) === 0x80) {
|
||||
result += String.fromCharCode(b1, b2);
|
||||
i += 2;
|
||||
} else {
|
||||
// Fallback: encode as escaped sequence
|
||||
result += String.fromCharCode(0xC2, 0x80 + (byte - 128));
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
// Last byte, encode as escaped
|
||||
result += String.fromCharCode(0xC2, 0x80 + (byte - 128));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const bytes = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < text.length) {
|
||||
const code = text.charCodeAt(i);
|
||||
|
||||
if (code < 128) {
|
||||
bytes.push(code);
|
||||
i++;
|
||||
} else if (i + 1 < text.length) {
|
||||
// Check for 2-byte sequence
|
||||
const b1 = code;
|
||||
const b2 = text.charCodeAt(i + 1);
|
||||
|
||||
if ((b1 & 0xE0) === 0xC0 && (b2 & 0xC0) === 0x80) {
|
||||
// Extract original byte from escaped sequence
|
||||
if (b1 === 0xC2 && b2 >= 0x80 && b2 < 0xC0) {
|
||||
bytes.push(b2 - 0x80);
|
||||
} else {
|
||||
bytes.push(b1, b2);
|
||||
}
|
||||
i += 2;
|
||||
} else {
|
||||
bytes.push(code);
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
bytes.push(code);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return new TextDecoder().decode(new Uint8Array(bytes));
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[base122]';
|
||||
const result = this.func(text.slice(0, 10));
|
||||
return result.substring(0, 15) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
// Base122 produces text that's mostly ASCII with some UTF-8 sequences
|
||||
// Hard to detect reliably, but check for mix of ASCII and UTF-8
|
||||
const hasAscii = /[\x00-\x7F]/.test(text);
|
||||
const hasUtf8 = /[\xC0-\xFF]/.test(text);
|
||||
return hasAscii && text.length >= 8;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// base36 encoding transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Base36',
|
||||
priority: 270,
|
||||
category: 'encoding',
|
||||
alphabet: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
||||
func: function(text) {
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
let num = 0n;
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
num = num * 256n + BigInt(bytes[i]);
|
||||
}
|
||||
|
||||
if (num === 0n) return '0';
|
||||
|
||||
let result = '';
|
||||
const base = 36n;
|
||||
while (num > 0n) {
|
||||
result = this.alphabet[Number(num % base)] + result;
|
||||
num = num / base;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
try {
|
||||
let num = 0n;
|
||||
const base = 36n;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text[i].toUpperCase();
|
||||
const idx = this.alphabet.indexOf(char);
|
||||
if (idx === -1) return text;
|
||||
num = num * base + BigInt(idx);
|
||||
}
|
||||
|
||||
// Convert back to bytes
|
||||
const bytes = [];
|
||||
while (num > 0n) {
|
||||
bytes.unshift(Number(num % 256n));
|
||||
num = num / 256n;
|
||||
}
|
||||
|
||||
return new TextDecoder().decode(new Uint8Array(bytes));
|
||||
} catch (e) {
|
||||
return text;
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[base36]';
|
||||
const result = this.func(text.slice(0, 4));
|
||||
return result.substring(0, 12) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
const cleaned = text.trim().replace(/\s/g, '').toUpperCase();
|
||||
return cleaned.length >= 4 && /^[0-9A-Z]+$/.test(cleaned);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// base91 encoding transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Base91',
|
||||
priority: 270,
|
||||
category: 'encoding',
|
||||
alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&()*+,./:;<=>?@[]^_`{|}~"',
|
||||
func: function(text) {
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
let num = 0n;
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
num = num * 256n + BigInt(bytes[i]);
|
||||
}
|
||||
|
||||
if (num === 0n) return this.alphabet[0];
|
||||
|
||||
let result = '';
|
||||
const base = 91n;
|
||||
while (num > 0n) {
|
||||
result = this.alphabet[Number(num % base)] + result;
|
||||
num = num / base;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
try {
|
||||
let num = 0n;
|
||||
const base = 91n;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const idx = this.alphabet.indexOf(text[i]);
|
||||
if (idx === -1) return text;
|
||||
num = num * base + BigInt(idx);
|
||||
}
|
||||
|
||||
// Convert back to bytes
|
||||
const bytes = [];
|
||||
while (num > 0n) {
|
||||
bytes.unshift(Number(num % 256n));
|
||||
num = num / 256n;
|
||||
}
|
||||
|
||||
return new TextDecoder().decode(new Uint8Array(bytes));
|
||||
} catch (e) {
|
||||
return text;
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[base91]';
|
||||
const result = this.func(text.slice(0, 4));
|
||||
return result.substring(0, 12) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
const cleaned = text.trim().replace(/\s/g, '');
|
||||
return cleaned.length >= 4 && /^[A-Za-z0-9!#$%&()*+,./:;<=>?@[\]^_`{|}~"]+$/.test(cleaned);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
// baudot code / ITA2 encoding (teletype code)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Baudot Code (ITA2)',
|
||||
priority: 250,
|
||||
category: 'encoding',
|
||||
// Baudot/ITA2 5-bit code (letters and figures shift)
|
||||
letters: {
|
||||
0b00000: ' ', // NULL/blank
|
||||
0b00010: 'E',
|
||||
0b00011: '\n', // Line feed
|
||||
0b00100: 'A',
|
||||
0b00101: ' ',
|
||||
0b00110: 'S',
|
||||
0b00111: 'I',
|
||||
0b01000: 'U',
|
||||
0b01001: '\r', // Carriage return
|
||||
0b01010: 'D',
|
||||
0b01011: 'R',
|
||||
0b01100: 'J',
|
||||
0b01101: 'N',
|
||||
0b01110: 'F',
|
||||
0b01111: 'C',
|
||||
0b10000: 'K',
|
||||
0b10001: 'T',
|
||||
0b10010: 'Z',
|
||||
0b10011: 'L',
|
||||
0b10100: 'W',
|
||||
0b10101: 'H',
|
||||
0b10110: 'Y',
|
||||
0b10111: 'P',
|
||||
0b11000: 'Q',
|
||||
0b11001: 'O',
|
||||
0b11010: 'B',
|
||||
0b11011: 'G',
|
||||
0b11100: 'Figures', // Shift to figures
|
||||
0b11101: 'M',
|
||||
0b11110: 'X',
|
||||
0b11111: 'V',
|
||||
},
|
||||
figures: {
|
||||
0b00000: ' ',
|
||||
0b00010: '3',
|
||||
0b00011: '\n',
|
||||
0b00100: '-',
|
||||
0b00101: ' ',
|
||||
0b00110: '\'',
|
||||
0b00111: '8',
|
||||
0b01000: '7',
|
||||
0b01001: '\r',
|
||||
0b01010: '\u0005', // ENQ
|
||||
0b01011: '4',
|
||||
0b01100: '\'', // Bell
|
||||
0b01101: ',',
|
||||
0b01110: '!',
|
||||
0b01111: ':',
|
||||
0b10000: '(',
|
||||
0b10001: '5',
|
||||
0b10010: '+',
|
||||
0b10011: ')',
|
||||
0b10100: '2',
|
||||
0b10101: '$',
|
||||
0b10110: '6',
|
||||
0b10111: '0',
|
||||
0b11000: '1',
|
||||
0b11001: '9',
|
||||
0b11010: '?',
|
||||
0b11011: '&',
|
||||
0b11100: 'Letters', // Shift to letters
|
||||
0b11101: '.',
|
||||
0b11110: '/',
|
||||
0b11111: '=',
|
||||
},
|
||||
func: function(text) {
|
||||
// Create reverse maps
|
||||
const lettersToCode = {};
|
||||
const figuresToCode = {};
|
||||
for (const [code, char] of Object.entries(this.letters)) {
|
||||
if (char !== 'Figures' && char !== 'Letters') {
|
||||
lettersToCode[char] = parseInt(code);
|
||||
}
|
||||
}
|
||||
for (const [code, char] of Object.entries(this.figures)) {
|
||||
if (char !== 'Figures' && char !== 'Letters') {
|
||||
figuresToCode[char] = parseInt(code);
|
||||
}
|
||||
}
|
||||
|
||||
let result = '';
|
||||
let inFigures = false;
|
||||
|
||||
for (const char of text.toUpperCase()) {
|
||||
// Check if we need to shift
|
||||
const isFigure = /[0-9\-'():!$?&.\/+=]/.test(char);
|
||||
|
||||
if (isFigure && !inFigures) {
|
||||
result += String.fromCharCode(0b11100); // Figures shift
|
||||
inFigures = true;
|
||||
} else if (!isFigure && inFigures) {
|
||||
result += String.fromCharCode(0b11111); // Letters shift (approximate)
|
||||
inFigures = false;
|
||||
}
|
||||
|
||||
// Encode character
|
||||
const code = inFigures ? figuresToCode[char] : lettersToCode[char];
|
||||
if (code !== undefined) {
|
||||
result += String.fromCharCode(code);
|
||||
} else {
|
||||
result += char; // Keep unmapped
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
let result = '';
|
||||
let inFigures = false;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const code = text.charCodeAt(i) & 0x1F; // 5 bits
|
||||
|
||||
if (code === 0b11100) {
|
||||
inFigures = true;
|
||||
continue;
|
||||
} else if (code === 0b11111) {
|
||||
inFigures = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
const map = inFigures ? this.figures : this.letters;
|
||||
const char = map[code];
|
||||
if (char && char !== 'Figures' && char !== 'Letters') {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[baudot]';
|
||||
return this.func(text.slice(0, 5));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Baudot uses 5-bit codes (0-31)
|
||||
// Check for characters in the 5-bit range
|
||||
const has5Bit = /[\x00-\x1F]/.test(text);
|
||||
return has5Bit && text.length >= 5;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// binary coded decimal (BCD) transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Binary Coded Decimal',
|
||||
priority: 300,
|
||||
category: 'encoding',
|
||||
func: function(text) {
|
||||
return [...text].map(c => {
|
||||
const code = c.charCodeAt(0);
|
||||
// Convert each digit of the char code to BCD
|
||||
return code.toString().split('').map(d => {
|
||||
const digit = parseInt(d);
|
||||
return digit.toString(2).padStart(4, '0');
|
||||
}).join(' ');
|
||||
}).join(' ');
|
||||
},
|
||||
reverse: function(text) {
|
||||
try {
|
||||
const bcdGroups = text.trim().split(/\s+/);
|
||||
const chars = [];
|
||||
let currentCode = '';
|
||||
|
||||
for (let i = 0; i < bcdGroups.length; i++) {
|
||||
if (bcdGroups[i].length === 4 && /^[01]+$/.test(bcdGroups[i])) {
|
||||
currentCode += parseInt(bcdGroups[i], 2).toString();
|
||||
if (currentCode.length >= 3) {
|
||||
const code = parseInt(currentCode);
|
||||
if (code >= 0 && code <= 65535) {
|
||||
chars.push(String.fromCharCode(code));
|
||||
currentCode = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return chars.join('');
|
||||
} catch (e) {
|
||||
return text;
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[bcd]';
|
||||
return this.func(text.slice(0, 2));
|
||||
},
|
||||
detector: function(text) {
|
||||
const cleaned = text.trim().replace(/\s/g, '');
|
||||
return cleaned.length >= 4 && /^[01]+$/.test(cleaned) && cleaned.length % 4 === 0;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
// EBCDIC encoding (IBM character encoding)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'EBCDIC',
|
||||
priority: 250,
|
||||
category: 'encoding',
|
||||
// EBCDIC to ASCII mapping (simplified - full EBCDIC has many variants)
|
||||
ebcdicToAscii: {
|
||||
0x40: 0x20, // Space
|
||||
0x4A: 0x21, // !
|
||||
0x4B: 0x22, // "
|
||||
0x4C: 0x23, // #
|
||||
0x4D: 0x24, // $
|
||||
0x4E: 0x25, // %
|
||||
0x4F: 0x26, // &
|
||||
0x50: 0x27, // '
|
||||
0x5A: 0x28, // (
|
||||
0x5B: 0x29, // )
|
||||
0x5C: 0x2A, // *
|
||||
0x5D: 0x2B, // +
|
||||
0x5E: 0x2C, // ,
|
||||
0x5F: 0x2D, // -
|
||||
0x60: 0x2E, // .
|
||||
0x61: 0x2F, // /
|
||||
0xF0: 0x30, // 0
|
||||
0xF1: 0x31, // 1
|
||||
0xF2: 0x32, // 2
|
||||
0xF3: 0x33, // 3
|
||||
0xF4: 0x34, // 4
|
||||
0xF5: 0x35, // 5
|
||||
0xF6: 0x36, // 6
|
||||
0xF7: 0x37, // 7
|
||||
0xF8: 0x38, // 8
|
||||
0xF9: 0x39, // 9
|
||||
0x7A: 0x3A, // :
|
||||
0x7B: 0x3B, // ;
|
||||
0x7C: 0x3C, // <
|
||||
0x7D: 0x3D, // =
|
||||
0x7E: 0x3E, // >
|
||||
0x7F: 0x3F, // ?
|
||||
0x81: 0x41, // A
|
||||
0x82: 0x42, // B
|
||||
0x83: 0x43, // C
|
||||
0x84: 0x44, // D
|
||||
0x85: 0x45, // E
|
||||
0x86: 0x46, // F
|
||||
0x87: 0x47, // G
|
||||
0x88: 0x48, // H
|
||||
0x89: 0x49, // I
|
||||
0x91: 0x4A, // J
|
||||
0x92: 0x4B, // K
|
||||
0x93: 0x4C, // L
|
||||
0x94: 0x4D, // M
|
||||
0x95: 0x4E, // N
|
||||
0x96: 0x4F, // O
|
||||
0x97: 0x50, // P
|
||||
0x98: 0x51, // Q
|
||||
0x99: 0x52, // R
|
||||
0xA2: 0x53, // S
|
||||
0xA3: 0x54, // T
|
||||
0xA4: 0x55, // U
|
||||
0xA5: 0x56, // V
|
||||
0xA6: 0x57, // W
|
||||
0xA7: 0x58, // X
|
||||
0xA8: 0x59, // Y
|
||||
0xA9: 0x5A, // Z
|
||||
},
|
||||
func: function(text) {
|
||||
// Convert ASCII to EBCDIC
|
||||
const asciiToEbcdic = {};
|
||||
for (const [ebcdic, ascii] of Object.entries(this.ebcdicToAscii)) {
|
||||
asciiToEbcdic[ascii] = parseInt(ebcdic);
|
||||
}
|
||||
|
||||
let result = '';
|
||||
for (const char of text) {
|
||||
const code = char.charCodeAt(0);
|
||||
// Convert lowercase letters to uppercase before encoding (EBCDIC is uppercase-only)
|
||||
if (code >= 0x61 && code <= 0x7A) { // a-z
|
||||
const upperCode = code - 0x20; // Convert to A-Z
|
||||
if (asciiToEbcdic[upperCode] !== undefined) {
|
||||
result += String.fromCharCode(asciiToEbcdic[upperCode]);
|
||||
} else {
|
||||
result += char; // Keep unmapped characters
|
||||
}
|
||||
} else if (asciiToEbcdic[code] !== undefined) {
|
||||
result += String.fromCharCode(asciiToEbcdic[code]);
|
||||
} else {
|
||||
result += char; // Keep unmapped characters
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
let result = '';
|
||||
for (const char of text) {
|
||||
const code = char.charCodeAt(0);
|
||||
if (this.ebcdicToAscii[code] !== undefined) {
|
||||
result += String.fromCharCode(this.ebcdicToAscii[code]);
|
||||
} else {
|
||||
result += char; // Keep unmapped characters
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[ebcdic]';
|
||||
return this.func(text.slice(0, 8)) + (text.length > 8 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
if (!text || text.length < 2) return false;
|
||||
|
||||
// EBCDIC uses specific byte ranges for letters and numbers
|
||||
// Letters: 0x81-0xA9 (A-Z)
|
||||
// Numbers: 0xF0-0xF9 (0-9)
|
||||
// Punctuation: 0x40-0x7F range
|
||||
|
||||
// Check for EBCDIC-specific character codes (letters and numbers)
|
||||
const hasEbcdicLetters = /[\x81-\x89\x91-\x99\xA2-\xA9]/.test(text); // A-Z in EBCDIC
|
||||
const hasEbcdicNumbers = /[\xF0-\xF9]/.test(text); // 0-9 in EBCDIC
|
||||
|
||||
// Must have at least some EBCDIC-specific characters
|
||||
if (!hasEbcdicLetters && !hasEbcdicNumbers) return false;
|
||||
|
||||
// Reject if text is already readable ASCII (common English words)
|
||||
// This prevents false positives on plain text
|
||||
const commonWords = /\b(the|and|for|are|but|not|you|all|can|her|was|one|our|out|day|get|has|him|his|how|man|new|now|old|see|two|way|who|boy|did|its|let|put|say|she|too|use)\b/i;
|
||||
if (commonWords.test(text)) return false;
|
||||
|
||||
// Check if decoding produces text that looks like it was encoded
|
||||
// EBCDIC-encoded text, when decoded, should have readable ASCII
|
||||
// If the input is already readable ASCII, it's not EBCDIC
|
||||
const readableAscii = /^[\x20-\x7E\s]*$/.test(text);
|
||||
if (readableAscii && !hasEbcdicLetters && !hasEbcdicNumbers) {
|
||||
// If it's all readable ASCII and has no EBCDIC-specific codes, reject
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify that at least some characters are in EBCDIC-specific ranges
|
||||
// For short strings, require at least 1 EBCDIC character
|
||||
// For longer strings, require at least 10% to be EBCDIC-specific
|
||||
const ebcdicChars = (text.match(/[\x81-\x89\x91-\x99\xA2-\xA9\xF0-\xF9]/g) || []).length;
|
||||
if (ebcdicChars === 0) return false;
|
||||
|
||||
// For short strings (<= 20 chars), just need at least 1 EBCDIC char
|
||||
if (text.length <= 20) {
|
||||
return ebcdicChars >= 1;
|
||||
}
|
||||
|
||||
// For longer strings, require at least 10% to be EBCDIC-specific
|
||||
const ebcdicRatio = ebcdicChars / text.length;
|
||||
return ebcdicRatio >= 0.1; // At least 10% must be EBCDIC-specific
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// emoji encoding transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Emoji Encoding',
|
||||
priority: 250,
|
||||
category: 'encoding',
|
||||
// Map bytes to emoji (using common emojis)
|
||||
emojiMap: [
|
||||
'😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃',
|
||||
'😉', '😊', '😇', '🥰', '😍', '🤩', '😘', '😗', '😚', '😙',
|
||||
'😋', '😛', '😜', '🤪', '😝', '🤑', '🤗', '🤭', '🤫', '🤔',
|
||||
'🤐', '🤨', '😐', '😑', '😶', '😏', '😒', '🙄', '😬', '🤥',
|
||||
'😌', '😔', '😪', '🤤', '😴', '😷', '🤒', '🤕', '🤢', '🤮',
|
||||
'🤧', '🥵', '🥶', '😶🌫️', '😵', '😵💫', '🤯', '🤠', '🥳', '😎',
|
||||
'🤓', '🧐', '😕', '😟', '🙁', '😮', '😯', '😲', '😳', '🥺',
|
||||
'😦', '😧', '😨', '😰', '😥', '😢', '😭', '😱', '😖', '😣',
|
||||
'😞', '😓', '😩', '😫', '🥱', '😤', '😡', '😠', '🤬', '😈',
|
||||
'👿', '💀', '☠️', '💩', '🤡', '👹', '👺', '👻', '👽', '👾',
|
||||
'🤖', '😺', '😸', '😹', '😻', '😼', '😽', '🙀', '😿', '😾',
|
||||
'🙈', '🙉', '🙊', '💋', '💌', '💘', '💝', '💖', '💗', '💓',
|
||||
'💞', '💕', '💟', '❣️', '💔', '❤️', '🧡', '💛', '💚', '💙',
|
||||
'💜', '🖤', '🤍', '🤎', '💯', '💢', '💥', '💫', '💦', '💨',
|
||||
'🕳️', '💣', '💬', '👁️🗨️', '🗨️', '🗯️', '💭', '💤', '👋', '🤚',
|
||||
'🖐️', '✋', '🖖', '👌', '🤌', '🤏', '✌️', '🤞', '🤟', '🤘',
|
||||
'🤙', '👈', '👉', '👆', '🖕', '👇', '☝️', '👍', '👎', '✊',
|
||||
'👊', '🤛', '🤜', '👏', '🙌', '👐', '🤲', '🤝', '🙏', '✍️',
|
||||
'💪', '🦾', '🦿', '🦵', '🦶', '👂', '🦻', '👃', '🧠', '🫀',
|
||||
'🫁', '🦷', '🦴', '👀', '👁️', '👅', '👄', '💋', '🩸', '👶',
|
||||
'🧒', '👦', '👧', '🧑', '👱', '👨', '🧔', '👨🦰', '👨🦱', '👨🦳',
|
||||
'👨🦲', '👩', '👩🦰', '👩🦱', '👩🦳', '👩🦲', '🧓', '👴', '👵', '🙍',
|
||||
'🙎', '🙅', '🙆', '💁', '🙋', '🧏', '🤦', '🤦♂️', '🤦♀️', '🤷',
|
||||
'🤷♂️', '🤷♀️', '🙇', '🙇♂️', '🙇♀️', '🤦', '🤦♂️', '🤦♀️', '🤷', '🤷♂️'
|
||||
],
|
||||
func: function(text) {
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
let result = '';
|
||||
|
||||
for (const byte of bytes) {
|
||||
result += this.emojiMap[byte % this.emojiMap.length] + ' ';
|
||||
}
|
||||
|
||||
return result.trim();
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Create reverse map
|
||||
const reverseMap = {};
|
||||
for (let i = 0; i < this.emojiMap.length; i++) {
|
||||
reverseMap[this.emojiMap[i]] = i;
|
||||
}
|
||||
|
||||
// Extract emojis (match any emoji, not just specific range)
|
||||
const emojis = text.match(/[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/gu) || [];
|
||||
const bytes = [];
|
||||
|
||||
for (const emoji of emojis) {
|
||||
if (reverseMap[emoji] !== undefined) {
|
||||
bytes.push(reverseMap[emoji]);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return new TextDecoder().decode(new Uint8Array(bytes));
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[emoji-encoding]';
|
||||
return this.func(text.slice(0, 3));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for emoji patterns (broader range)
|
||||
const emojiPattern = /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/gu;
|
||||
const matches = text.match(emojiPattern) || [];
|
||||
return matches.length >= 3;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// gray code transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Gray Code',
|
||||
priority: 300,
|
||||
category: 'encoding',
|
||||
func: function(text) {
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
const binary = Array.from(bytes)
|
||||
.map(b => b.toString(2).padStart(8, '0'))
|
||||
.join('');
|
||||
|
||||
// Convert to Gray code
|
||||
let gray = binary[0];
|
||||
for (let i = 1; i < binary.length; i++) {
|
||||
gray += (parseInt(binary[i - 1]) ^ parseInt(binary[i])).toString();
|
||||
}
|
||||
|
||||
return gray;
|
||||
},
|
||||
reverse: function(text) {
|
||||
try {
|
||||
// Convert from Gray code to binary
|
||||
if (!/^[01]+$/.test(text)) return text;
|
||||
|
||||
let binary = text[0];
|
||||
for (let i = 1; i < text.length; i++) {
|
||||
binary += (parseInt(binary[i - 1]) ^ parseInt(text[i])).toString();
|
||||
}
|
||||
|
||||
// Convert binary to bytes
|
||||
const bytes = [];
|
||||
for (let i = 0; i < binary.length; i += 8) {
|
||||
const byte = binary.slice(i, i + 8);
|
||||
if (byte.length === 8) {
|
||||
bytes.push(parseInt(byte, 2));
|
||||
}
|
||||
}
|
||||
|
||||
return new TextDecoder().decode(new Uint8Array(bytes));
|
||||
} catch (e) {
|
||||
return text;
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[gray]';
|
||||
const result = this.func(text.slice(0, 2));
|
||||
return result.substring(0, 16) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
const cleaned = text.trim().replace(/\s/g, '');
|
||||
return cleaned.length >= 8 && /^[01]+$/.test(cleaned);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// quoted-printable encoding transform (RFC 2045)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Quoted-Printable',
|
||||
priority: 70,
|
||||
category: 'encoding',
|
||||
func: function(text) {
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
let result = '';
|
||||
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
const byte = bytes[i];
|
||||
// Printable ASCII (33-126) except = (61) can be used as-is
|
||||
// Space (32) can be used, but often encoded as =20
|
||||
// = (61) must be encoded as =3D
|
||||
if (byte >= 33 && byte <= 60 || byte >= 63 && byte <= 126) {
|
||||
result += String.fromCharCode(byte);
|
||||
} else if (byte === 32) {
|
||||
// Space can be space or =20
|
||||
result += ' ';
|
||||
} else {
|
||||
// Encode as =XX
|
||||
result += '=' + byte.toString(16).toUpperCase().padStart(2, '0');
|
||||
}
|
||||
}
|
||||
|
||||
// Soft line breaks: lines should not exceed 76 chars (excluding CRLF)
|
||||
// For simplicity, we'll add = at end of long lines
|
||||
const lines = [];
|
||||
let currentLine = '';
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
if (currentLine.length >= 75) {
|
||||
lines.push(currentLine + '=');
|
||||
currentLine = result[i];
|
||||
} else {
|
||||
currentLine += result[i];
|
||||
}
|
||||
}
|
||||
if (currentLine) lines.push(currentLine);
|
||||
|
||||
return lines.join('\r\n');
|
||||
},
|
||||
reverse: function(text) {
|
||||
try {
|
||||
// Remove soft line breaks (= at end of line)
|
||||
let cleaned = text.replace(/=\r?\n/g, '').replace(/=\r/g, '');
|
||||
let result = '';
|
||||
|
||||
for (let i = 0; i < cleaned.length; i++) {
|
||||
if (cleaned[i] === '=' && i + 2 < cleaned.length) {
|
||||
const hex = cleaned.substring(i + 1, i + 3);
|
||||
const byte = parseInt(hex, 16);
|
||||
if (!isNaN(byte)) {
|
||||
result += String.fromCharCode(byte);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result += cleaned[i];
|
||||
}
|
||||
|
||||
return new TextDecoder().decode(new TextEncoder().encode(result));
|
||||
} catch (e) {
|
||||
return text;
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[qp]';
|
||||
const result = this.func(text.slice(0, 10));
|
||||
return result.substring(0, 20).replace(/\r?\n/g, ' ') + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for quoted-printable patterns (=XX hex codes)
|
||||
return /=([0-9A-F]{2})/i.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// unicode code points encoding transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Unicode Code Points',
|
||||
priority: 250,
|
||||
category: 'encoding',
|
||||
func: function(text) {
|
||||
// Encode text as Unicode code points (U+XXXX format)
|
||||
let result = '';
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const code = text.charCodeAt(i);
|
||||
result += 'U+' + code.toString(16).toUpperCase().padStart(4, '0') + ' ';
|
||||
}
|
||||
return result.trim();
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Extract U+XXXX patterns and convert back to characters
|
||||
const matches = text.match(/U\+([0-9A-Fa-f]{4,6})/g) || [];
|
||||
let result = '';
|
||||
for (const match of matches) {
|
||||
const code = parseInt(match.substring(2), 16);
|
||||
if (code >= 0 && code <= 0x10FFFF) {
|
||||
result += String.fromCharCode(code);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[unicode-points]';
|
||||
const result = this.func(text.slice(0, 3));
|
||||
return result.substring(0, 20) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for U+XXXX pattern
|
||||
const pattern = /U\+[0-9A-Fa-f]{4,6}/;
|
||||
return pattern.test(text) && text.match(/U\+[0-9A-Fa-f]{4,6}/g).length >= 2;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// uuencoding transform (Unix-to-Unix encoding)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Uuencoding',
|
||||
priority: 250,
|
||||
category: 'encoding',
|
||||
func: function(text) {
|
||||
// Uuencoding encodes 3 bytes into 4 characters
|
||||
// Each character represents 6 bits (0-63)
|
||||
const uuChars = ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_';
|
||||
|
||||
let result = '';
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
|
||||
for (let i = 0; i < bytes.length; i += 3) {
|
||||
const b1 = bytes[i] || 0;
|
||||
const b2 = bytes[i + 1] || 0;
|
||||
const b3 = bytes[i + 2] || 0;
|
||||
|
||||
// Combine 3 bytes (24 bits) into 4 6-bit values
|
||||
const val1 = (b1 >> 2) & 0x3F;
|
||||
const val2 = ((b1 << 4) | (b2 >> 4)) & 0x3F;
|
||||
const val3 = ((b2 << 2) | (b3 >> 6)) & 0x3F;
|
||||
const val4 = b3 & 0x3F;
|
||||
|
||||
result += uuChars[val1] + uuChars[val2] + uuChars[val3] + uuChars[val4];
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const uuChars = ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_';
|
||||
|
||||
const bytes = [];
|
||||
const totalChunks = Math.floor(text.length / 4);
|
||||
|
||||
for (let i = 0; i < totalChunks; i++) {
|
||||
const chunk = text.substring(i * 4, (i + 1) * 4);
|
||||
if (chunk.length < 4) break;
|
||||
|
||||
const val1 = uuChars.indexOf(chunk[0]);
|
||||
const val2 = uuChars.indexOf(chunk[1]);
|
||||
const val3 = uuChars.indexOf(chunk[2]);
|
||||
const val4 = uuChars.indexOf(chunk[3]);
|
||||
|
||||
if (val1 === -1 || val2 === -1 || val3 === -1 || val4 === -1) continue;
|
||||
|
||||
// Reconstruct 3 bytes from 4 6-bit values
|
||||
const b1 = (val1 << 2) | (val2 >> 4);
|
||||
const b2 = ((val2 << 4) | (val3 >> 2)) & 0xFF;
|
||||
const b3 = ((val3 << 6) | val4) & 0xFF;
|
||||
|
||||
bytes.push(b1);
|
||||
bytes.push(b2);
|
||||
bytes.push(b3);
|
||||
}
|
||||
|
||||
// Remove trailing null bytes (padding)
|
||||
while (bytes.length > 0 && bytes[bytes.length - 1] === 0) {
|
||||
bytes.pop();
|
||||
}
|
||||
|
||||
try {
|
||||
return new TextDecoder().decode(new Uint8Array(bytes));
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[uuencoding]';
|
||||
const result = this.func(text.slice(0, 3));
|
||||
return result.substring(0, 8) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
// Uuencoding uses specific character set: space through underscore (ASCII 32-95)
|
||||
const uuPattern = /^[ !"#$%&'()*+,\-./0-9:;<=>?@A-Z[\\\]^_]+$/;
|
||||
return text.length >= 8 && uuPattern.test(text) && text.length % 4 === 0;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// yenc encoding transform (Usenet binary encoding)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'YEnc',
|
||||
priority: 250,
|
||||
category: 'encoding',
|
||||
func: function(text) {
|
||||
// YEnc encodes bytes by adding 42 (0x2A) and escaping special characters
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
let result = '';
|
||||
|
||||
for (const byte of bytes) {
|
||||
let encoded = (byte + 42) % 256;
|
||||
|
||||
// Escape special characters: NULL (0), LF (10), CR (13), = (61)
|
||||
if (encoded === 0 || encoded === 10 || encoded === 13 || encoded === 61) {
|
||||
result += '=' + String.fromCharCode((encoded + 64) % 256);
|
||||
} else {
|
||||
result += String.fromCharCode(encoded);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const bytes = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < text.length) {
|
||||
if (text[i] === '=' && i + 1 < text.length) {
|
||||
// Escaped character
|
||||
const escaped = text.charCodeAt(i + 1);
|
||||
const decoded = (escaped - 64) % 256;
|
||||
bytes.push((decoded - 42 + 256) % 256);
|
||||
i += 2;
|
||||
} else {
|
||||
// Normal character
|
||||
const encoded = text.charCodeAt(i);
|
||||
bytes.push((encoded - 42 + 256) % 256);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return new TextDecoder().decode(new Uint8Array(bytes));
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[yenc]';
|
||||
const result = this.func(text.slice(0, 3));
|
||||
return result.substring(0, 8) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
// YEnc produces binary-like data, hard to detect reliably
|
||||
// Check for escape sequences (= followed by character)
|
||||
const escapePattern = /=[\x00-\xFF]/;
|
||||
return escapePattern.test(text) && text.length >= 8;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
// z85 encoding (ZeroMQ Base85)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Z85',
|
||||
priority: 250,
|
||||
category: 'encoding',
|
||||
// Z85 uses a different character set than standard Base85
|
||||
charset: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#',
|
||||
func: function(text) {
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
let result = '';
|
||||
const originalLength = bytes.length;
|
||||
|
||||
// Z85 encodes 4 bytes into 5 characters
|
||||
for (let i = 0; i < bytes.length; i += 4) {
|
||||
const chunk = Array.from(bytes.slice(i, i + 4));
|
||||
const chunkLength = chunk.length;
|
||||
while (chunk.length < 4) chunk.push(0);
|
||||
|
||||
// Convert 4 bytes to 32-bit integer
|
||||
let value = 0;
|
||||
for (let j = 0; j < 4; j++) {
|
||||
value = (value << 8) + chunk[j];
|
||||
}
|
||||
|
||||
// Convert to base 85 (5 digits)
|
||||
const z85Chars = [];
|
||||
for (let j = 0; j < 5; j++) {
|
||||
z85Chars.unshift(this.charset[value % 85]);
|
||||
value = Math.floor(value / 85);
|
||||
}
|
||||
|
||||
result += z85Chars.join('');
|
||||
}
|
||||
|
||||
// Store original length for decoding
|
||||
this._z85OriginalLength = originalLength;
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const bytes = [];
|
||||
|
||||
// Z85 decodes 5 characters into 4 bytes
|
||||
for (let i = 0; i < text.length; i += 5) {
|
||||
const chunk = text.substring(i, i + 5);
|
||||
if (chunk.length < 5) break;
|
||||
|
||||
// Convert 5 base-85 digits to 32-bit integer
|
||||
let value = 0;
|
||||
for (const char of chunk) {
|
||||
const idx = this.charset.indexOf(char);
|
||||
if (idx === -1) return ''; // Invalid character
|
||||
value = value * 85 + idx;
|
||||
}
|
||||
|
||||
// Extract 4 bytes
|
||||
bytes.push((value >> 24) & 0xFF);
|
||||
bytes.push((value >> 16) & 0xFF);
|
||||
bytes.push((value >> 8) & 0xFF);
|
||||
bytes.push(value & 0xFF);
|
||||
}
|
||||
|
||||
// Trim to original length if we stored it
|
||||
if (this._z85OriginalLength !== undefined) {
|
||||
bytes.length = Math.min(bytes.length, this._z85OriginalLength);
|
||||
} else {
|
||||
// Remove trailing null bytes (padding)
|
||||
while (bytes.length > 0 && bytes[bytes.length - 1] === 0) {
|
||||
bytes.pop();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return new TextDecoder().decode(new Uint8Array(bytes));
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[z85]';
|
||||
const result = this.func(text.slice(0, 4));
|
||||
return result.substring(0, 10) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
// Z85 uses specific character set
|
||||
const z85Pattern = /^[0-9a-zA-Z.\-:+=^!\/\*\?&<>()\[\]{}@%$#]+$/;
|
||||
return text.length >= 5 && z85Pattern.test(text) && text.length % 5 === 0;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,21 +5,59 @@ export default new BaseTransformer({
|
||||
name: 'Dovahzul (Dragon)',
|
||||
priority: 285,
|
||||
// Detector: Look for characteristic Dovahzul patterns (vowel expansions)
|
||||
// Dovahzul encoding expands vowels: a->ah, e->eh, i->ii, q->kw, x->ks
|
||||
// We need to detect when text actually looks like Dovahzul, not just contains these patterns
|
||||
detector: function(text) {
|
||||
if (!/[a-z]/i.test(text)) return false;
|
||||
|
||||
const dovahzulPatterns = ['ah', 'eh', 'ii', 'kw', 'ks'];
|
||||
let patternCount = 0;
|
||||
const lowerInput = text.toLowerCase();
|
||||
const textLength = text.length;
|
||||
|
||||
for (const pattern of dovahzulPatterns) {
|
||||
// Check for Dovahzul-specific patterns that are less common in regular English
|
||||
// 'kw' (from 'q') and 'ks' (from 'x') are strong indicators
|
||||
const strongPatterns = ['kw', 'ks'];
|
||||
let strongCount = 0;
|
||||
for (const pattern of strongPatterns) {
|
||||
const matches = lowerInput.match(new RegExp(pattern, 'g'));
|
||||
if (matches) patternCount += matches.length;
|
||||
if (matches) strongCount += matches.length;
|
||||
}
|
||||
|
||||
// For short inputs, require at least 1 pattern, for longer require 2+
|
||||
const minPatterns = text.length < 30 ? 1 : 2;
|
||||
return patternCount >= minPatterns;
|
||||
// Check for vowel expansions: 'ah', 'eh', 'ii'
|
||||
// These can appear anywhere in Dovahzul-encoded text
|
||||
const vowelExpansions = ['ah', 'eh', 'ii'];
|
||||
let expansionCount = 0;
|
||||
|
||||
for (const pattern of vowelExpansions) {
|
||||
const matches = lowerInput.match(new RegExp(pattern, 'g'));
|
||||
if (matches) expansionCount += matches.length;
|
||||
}
|
||||
|
||||
// Calculate pattern density
|
||||
const totalPatterns = strongCount + expansionCount;
|
||||
const patternDensity = totalPatterns / Math.max(textLength / 10, 1);
|
||||
|
||||
// Strong patterns (kw/ks) are very rare in English - even 1 is a strong indicator
|
||||
if (strongCount >= 1) return true;
|
||||
|
||||
// For vowel expansions, we need to be more careful to avoid false positives
|
||||
// Check if the patterns appear in positions that suggest Dovahzul encoding
|
||||
// rather than natural English words
|
||||
|
||||
// Common English words that contain these patterns (false positives to avoid):
|
||||
const falsePositiveWords = ['what', 'that', 'when', 'where', 'which', 'while', 'this', 'with', 'think', 'thank', 'the', 'then', 'there', 'their', 'they'];
|
||||
const words = lowerInput.split(/\s+/);
|
||||
const hasFalsePositives = words.some(word => falsePositiveWords.some(fp => word.includes(fp)));
|
||||
|
||||
// If we have false positive words and low pattern count, it's probably English
|
||||
if (hasFalsePositives && expansionCount < 3) return false;
|
||||
|
||||
// Require sufficient pattern density to indicate Dovahzul encoding
|
||||
// For short text: need at least 1 pattern with density > 0.3
|
||||
// For longer text: need at least 2 patterns with density > 0.2
|
||||
const minPatterns = textLength < 30 ? 1 : 2;
|
||||
const minDensity = textLength < 30 ? 0.3 : 0.2;
|
||||
|
||||
return expansionCount >= minPatterns && patternDensity >= minDensity;
|
||||
},
|
||||
|
||||
map: {
|
||||
|
||||
@@ -50,9 +50,72 @@ export default new BaseTransformer({
|
||||
detector: function(text) {
|
||||
// Klingon has characteristic patterns like 'ch', 'gh', 'Q' (capital Q for q sound)
|
||||
// Also uses capital letters in specific ways (D, H, I, Q, S)
|
||||
const patterns = text.match(/ch|gh|CH|GH/g);
|
||||
const capitalPattern = /[DHIQS]/.test(text) && /[a-z]/.test(text); // Mix of specific capitals with lowercase
|
||||
return (patterns && patterns.length >= 1) || capitalPattern;
|
||||
const patterns = text.match(/ch|gh|CH|GH/gi);
|
||||
const patternCount = patterns ? patterns.length : 0;
|
||||
|
||||
// Check for Klingon-specific capital letter usage
|
||||
// Klingon uses capitals D, H, I, Q, S in specific contexts
|
||||
// But we need to avoid false positives from regular English
|
||||
const klingonCapitals = text.match(/[DHIQS]/g);
|
||||
const lowercaseLetters = text.match(/[a-z]/g);
|
||||
const hasQ = /Q/.test(text);
|
||||
|
||||
// Strong indicators: 'ch' or 'gh' patterns
|
||||
if (patternCount >= 1) {
|
||||
const lowerText = text.toLowerCase();
|
||||
const commonEnglishPatterns = /(which|much|such|each|teach|reach|beach|church|chance|change|charm|chart|chase|cheap|check|cheek|cheer|cheese|chest|chick|chief|child|chill|china|chips|choke|choose|chop|chord|chore|chose|chuck|chunk|churn)/;
|
||||
const isCommonEnglish = commonEnglishPatterns.test(lowerText);
|
||||
|
||||
// If we have multiple patterns, check if they're all in common English words
|
||||
if (patternCount >= 2) {
|
||||
// If all patterns are in common English words, it's probably not Klingon
|
||||
// Count how many patterns are NOT in common English contexts
|
||||
const nonEnglishPatterns = patterns.filter(p => {
|
||||
const patternLower = p.toLowerCase();
|
||||
// Check if this pattern appears outside common English words
|
||||
const patternIndex = lowerText.indexOf(patternLower);
|
||||
if (patternIndex === -1) return false;
|
||||
// Extract surrounding context
|
||||
const start = Math.max(0, patternIndex - 5);
|
||||
const end = Math.min(lowerText.length, patternIndex + patternLower.length + 5);
|
||||
const context = lowerText.substring(start, end);
|
||||
return !commonEnglishPatterns.test(context);
|
||||
});
|
||||
// If we have patterns outside common English words, it's likely Klingon
|
||||
if (nonEnglishPatterns.length > 0) return true;
|
||||
// Even if all patterns are in common words, Q or multiple capitals suggest Klingon
|
||||
if (hasQ || (klingonCapitals && klingonCapitals.length >= 2)) return true;
|
||||
// Otherwise, it's probably just English
|
||||
return false;
|
||||
}
|
||||
|
||||
// Single pattern
|
||||
// Single pattern with Q (strong Klingon indicator)
|
||||
if (hasQ) return true;
|
||||
// Single pattern with multiple Klingon capitals (indicates encoding)
|
||||
if (klingonCapitals && klingonCapitals.length >= 2) return true;
|
||||
// Single pattern is acceptable - 'ch' and 'gh' are less common in English
|
||||
// But avoid if it's clearly English (e.g., "ch" in "which", "much")
|
||||
if (!isCommonEnglish) return true;
|
||||
// Even if it's common English, if we have Q or multiple capitals, it might be Klingon
|
||||
if (hasQ || (klingonCapitals && klingonCapitals.length >= 2)) return true;
|
||||
}
|
||||
|
||||
// Capital pattern: need multiple Klingon capitals mixed with lowercase
|
||||
// This indicates Klingon encoding, not just English with one capital letter
|
||||
if (klingonCapitals && lowercaseLetters && klingonCapitals.length >= 2) {
|
||||
// Check if capitals appear in middle/end of words (Klingon style)
|
||||
// This is different from English where capitals are usually at word start
|
||||
const midWordCapitals = text.match(/[a-z][DHIQS][a-z]/g);
|
||||
if (midWordCapitals && midWordCapitals.length >= 1) return true;
|
||||
// Or if Q is present (strong indicator)
|
||||
if (hasQ) return true;
|
||||
}
|
||||
|
||||
// Single Q with lowercase is a strong indicator
|
||||
if (hasQ && lowercaseLetters && lowercaseLetters.length >= 2) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
// bitwise NOT transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Bitwise NOT',
|
||||
priority: 100,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
// Invert all bits in each byte
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
const result = new Uint8Array(bytes.length);
|
||||
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
result[i] = ~bytes[i] & 0xFF; // NOT operation, mask to 8 bits
|
||||
}
|
||||
|
||||
try {
|
||||
return new TextDecoder().decode(result);
|
||||
} catch (e) {
|
||||
// If decoding fails, return as hex
|
||||
return Array.from(result).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Bitwise NOT is self-reciprocal (NOT NOT = original)
|
||||
return this.func(text);
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[bitwise-not]';
|
||||
return this.func(text.slice(0, 5));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Bitwise NOT produces scrambled text, hard to detect
|
||||
// Check for non-printable characters or unusual patterns
|
||||
const hasNonPrintable = /[\x00-\x1F\x7F-\x9F]/.test(text);
|
||||
return hasNonPrintable && text.length >= 5;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// boustrophedon writing transform (alternating direction)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Boustrophedon',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
const lines = text.split(/\r?\n/);
|
||||
return lines.map((line, index) => {
|
||||
// Alternate direction: even lines left-to-right, odd lines right-to-left
|
||||
if (index % 2 === 0) {
|
||||
return line;
|
||||
} else {
|
||||
return line.split('').reverse().join('');
|
||||
}
|
||||
}).join('\n');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Same function - boustrophedon is self-reciprocal
|
||||
return this.func(text);
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[boustrophedon]';
|
||||
const lines = text.split(/\r?\n/);
|
||||
if (lines.length === 0) return '';
|
||||
return this.func(lines.slice(0, 2).join('\n'));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Hard to detect - would need line analysis
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// capitalize words transform (first letter of each word uppercase)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Capitalize Words',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.replace(/\b\w/g, c => c.toUpperCase());
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - original case is lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[Capitalized]';
|
||||
return this.func(text.slice(0, 15));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Check if words start with uppercase (Title Case pattern)
|
||||
const words = text.split(/\s+/).filter(w => /[a-zA-Z]/.test(w));
|
||||
if (words.length < 2) return false;
|
||||
return words.every(w => /^[A-Z]/.test(w) || !/[a-zA-Z]/.test(w));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// indent transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Indent',
|
||||
priority: 100,
|
||||
category: 'format',
|
||||
spaces: 4, // Default indent spaces
|
||||
func: function(text) {
|
||||
const spaces = parseInt(this.spaces) || 4;
|
||||
const indent = ' '.repeat(spaces);
|
||||
|
||||
return text.split('\n').map(line => indent + line).join('\n');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Remove leading spaces from each line
|
||||
return text.split('\n').map(line => line.replace(/^\s+/, '')).join('\n');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[indent]';
|
||||
return this.func(text.slice(0, 20));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check if all lines start with same amount of whitespace
|
||||
const lines = text.split('\n').filter(l => l.trim());
|
||||
if (lines.length < 2) return false;
|
||||
|
||||
const leadingSpaces = lines.map(line => line.match(/^\s*/)[0].length);
|
||||
const allSame = leadingSpaces.every(count => count === leadingSpaces[0]);
|
||||
|
||||
return allSame && leadingSpaces[0] > 0;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// javanais transform (French slang insertion)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Javanais',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
// Insert "av" before each vowel (a, e, i, o, u, y) that follows a consonant
|
||||
const vowels = /[aeiouyAEIOUY]/;
|
||||
const consonants = /[bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ]/;
|
||||
|
||||
let result = '';
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text[i];
|
||||
const prevChar = i > 0 ? text[i - 1] : '';
|
||||
|
||||
if (vowels.test(char) && (i === 0 || consonants.test(prevChar))) {
|
||||
result += 'av' + char;
|
||||
} else {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Remove "av" before vowels that follow consonants
|
||||
return text.replace(/av([aeiouyAEIOUY])/g, '$1');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[javanais]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for "av" pattern before vowels
|
||||
return /av[aeiouyAEIOUY]/i.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// latin gibberish transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Latin Gibberish',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
// Insert "us" or "um" after consonants before vowels (simplified Latin-sounding)
|
||||
const vowels = /[aeiouyAEIOUY]/;
|
||||
const consonants = /[bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ]/;
|
||||
|
||||
let result = '';
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text[i];
|
||||
const nextChar = i + 1 < text.length ? text[i + 1] : '';
|
||||
|
||||
if (consonants.test(char) && vowels.test(nextChar)) {
|
||||
// Insert "us" after consonant before vowel
|
||||
result += char + 'us';
|
||||
} else {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Remove "us" after consonants
|
||||
return text.replace(/([bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ])us([aeiouyAEIOUY])/gi, '$1$2');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[latin]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for "us" pattern after consonants
|
||||
return /[bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ]us[aeiouyAEIOUY]/i.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// letters extraction transform (extract only letters)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Letters Only',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.replace(/[^a-zA-Z]/g, '');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - non-letters are lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[letters]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// letters and numbers only transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Letters & Numbers Only',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.replace(/[^a-zA-Z0-9]/g, '');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - other characters are lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[alphanum]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Check if text is only alphanumeric
|
||||
return /^[a-zA-Z0-9]+$/.test(text.trim()) && text.length >= 5;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// line numbering transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Line Numbers',
|
||||
priority: 100,
|
||||
category: 'format',
|
||||
start: 1, // Starting line number
|
||||
func: function(text) {
|
||||
const start = parseInt(this.start) || 1;
|
||||
const lines = text.split('\n');
|
||||
let result = '';
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const lineNum = start + i;
|
||||
result += lineNum.toString().padStart(4, ' ') + ': ' + lines[i] + '\n';
|
||||
}
|
||||
|
||||
return result.trimEnd();
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Remove line numbers (format: " 1: text" or "1: text")
|
||||
return text.split('\n').map(line => {
|
||||
return line.replace(/^\s*\d+\s*:\s*/, '');
|
||||
}).join('\n');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[line-numbers]';
|
||||
return this.func(text.slice(0, 30));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for line number pattern at start of lines
|
||||
const lines = text.split('\n');
|
||||
if (lines.length < 2) return false;
|
||||
|
||||
const hasLineNumbers = lines.filter(line => /^\s*\d+\s*:/.test(line)).length;
|
||||
return hasLineNumbers / lines.length > 0.7;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// louchebem transform (French slang)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Louchebem',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
// Move first consonant(s) to end and add "l" + "em" (or "oc", "ic", "uche", etc.)
|
||||
const words = text.split(/(\s+|[.,!?;:])/);
|
||||
|
||||
return words.map(word => {
|
||||
if (!/^[a-zA-Z]+$/.test(word)) return word;
|
||||
|
||||
const match = word.match(/^([bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ]+)([aeiouyAEIOUY].*)$/);
|
||||
if (match) {
|
||||
const [, consonants, rest] = match;
|
||||
return 'l' + rest + consonants + 'em';
|
||||
}
|
||||
return word;
|
||||
}).join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Reverse louchebem: remove "l" prefix and "em" suffix, move consonants back
|
||||
const words = text.split(/(\s+|[.,!?;:])/);
|
||||
|
||||
return words.map(word => {
|
||||
if (!/^l[a-zA-Z]+em$/i.test(word)) return word;
|
||||
|
||||
const body = word.slice(1, -2); // Remove 'l' and 'em'
|
||||
const match = body.match(/^([aeiouyAEIOUY].*?)([bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ]+)$/);
|
||||
if (match) {
|
||||
const [, rest, consonants] = match;
|
||||
return consonants + rest;
|
||||
}
|
||||
return word;
|
||||
}).join('');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[louchebem]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for "l" prefix and "em" suffix pattern
|
||||
return /\bl[a-z]+em\b/i.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// lowercase all transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Lowercase All',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.toLowerCase();
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - original case is lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[lowercase]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Check if all letters are lowercase
|
||||
const letters = text.replace(/[^a-zA-Z]/g, '');
|
||||
return letters.length > 0 && letters === letters.toLowerCase() && /[A-Z]/.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// mirror digits transform (mirror only numbers)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Mirror Digits',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.replace(/\d+/g, match => {
|
||||
return match.split('').reverse().join('');
|
||||
});
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Mirror digits is its own inverse
|
||||
return this.func(text);
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[mirror-digits]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check if text has numbers that might be mirrored
|
||||
return /\d/.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// numbers only transform (extract only numbers)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Numbers Only',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.replace(/[^0-9]/g, '');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - non-numbers are lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[numbers]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// If text is only digits, might be extracted
|
||||
return /^\d+$/.test(text.trim()) && text.length >= 3;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// remove accents/diacritics transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Remove Accents',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
map: {
|
||||
'à': 'a', 'á': 'a', 'â': 'a', 'ã': 'a', 'ä': 'a', 'å': 'a',
|
||||
'è': 'e', 'é': 'e', 'ê': 'e', 'ë': 'e',
|
||||
'ì': 'i', 'í': 'i', 'î': 'i', 'ï': 'i',
|
||||
'ò': 'o', 'ó': 'o', 'ô': 'o', 'õ': 'o', 'ö': 'o',
|
||||
'ù': 'u', 'ú': 'u', 'û': 'u', 'ü': 'u',
|
||||
'ý': 'y', 'ÿ': 'y',
|
||||
'À': 'A', 'Á': 'A', 'Â': 'A', 'Ã': 'A', 'Ä': 'A', 'Å': 'A',
|
||||
'È': 'E', 'É': 'E', 'Ê': 'E', 'Ë': 'E',
|
||||
'Ì': 'I', 'Í': 'I', 'Î': 'I', 'Ï': 'I',
|
||||
'Ò': 'O', 'Ó': 'O', 'Ô': 'O', 'Õ': 'O', 'Ö': 'O',
|
||||
'Ù': 'U', 'Ú': 'U', 'Û': 'U', 'Ü': 'U',
|
||||
'Ý': 'Y', 'Ÿ': 'Y',
|
||||
'ç': 'c', 'Ç': 'C',
|
||||
'ñ': 'n', 'Ñ': 'N',
|
||||
'ß': 'ss', 'ẞ': 'SS'
|
||||
},
|
||||
func: function(text) {
|
||||
return [...text].map(c => {
|
||||
// Check map first
|
||||
if (this.map[c]) return this.map[c];
|
||||
// Use Unicode normalization to remove combining diacritics
|
||||
return c.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
||||
}).join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - accents are lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[no-accents]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Check if text has no accented characters
|
||||
const normalized = text.normalize('NFD');
|
||||
return !/[\u0300-\u036f]/.test(normalized) && /[àáâãäåèéêëìíîïòóôõöùúûüýÿçñßÀÁÂÃÄÅÈÉÊËÌÍÎÏÒÓÔÕÖÙÚÛÜÝŸÇÑẞ]/.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// remove consonants transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Remove Consonants',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.replace(/[bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ]/g, '');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - consonants are lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[vowels-only]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// If text has only vowels/spaces/punctuation, might have consonants removed
|
||||
const cleaned = text.replace(/[\s.,!?;:'"()\-&0-9]/g, '');
|
||||
return cleaned.length > 0 && !/[bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ]/i.test(cleaned);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// remove duplicate characters transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Remove Duplicates',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
const seen = new Set();
|
||||
return [...text].filter(c => {
|
||||
if (seen.has(c)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(c);
|
||||
return true;
|
||||
}).join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - duplicates are lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[no-dupes]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Check if text has no duplicate characters
|
||||
const chars = [...text];
|
||||
const unique = new Set(chars);
|
||||
return chars.length === unique.size && text.length >= 5;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// remove extra spaces transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Remove Extra Spaces',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.replace(/[ \t]+/g, ' ').trim();
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - original spacing is lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[no-extra-spaces]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Check if text has multiple consecutive spaces
|
||||
return / +/.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// remove html tags transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Remove HTML Tags',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.replace(/<[^>]*>/g, '');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - HTML tags are lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[no-html]';
|
||||
return this.func(text.slice(0, 15));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Check if text contains HTML tags
|
||||
return /<[^>]+>/.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// remove newlines transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Remove Newlines',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.replace(/[\r\n]+/g, ' ');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - newline positions are lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[no-newlines]';
|
||||
return this.func(text.slice(0, 20));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Check if text should have newlines (has long lines)
|
||||
return !/[\r\n]/.test(text) && text.length > 50 && text.split(/\s+/).some(w => w.length > 20);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// remove numbers transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Remove Numbers',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.replace(/[0-9]/g, '');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - numbers are lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[no-numbers]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Hard to detect - would need context
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// remove punctuation transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Remove Punctuation',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.replace(/[.,!?;:'"()\-_\[\]{}@#$%^&*+=|\\\/<>~`]/g, '');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - punctuation is lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[no-punct]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Hard to detect - would need to check if text should have punctuation
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// remove tabs transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Remove Tabs',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.replace(/\t/g, ' ');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - tab positions are lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[no-tabs]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Hard to detect
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// remove zero width characters transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Remove Zero Width',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
// Remove zero-width spaces, joiners, non-joiners, and other invisible characters
|
||||
return text.replace(/[\u200B-\u200D\uFEFF\u2060]/g, '');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - zero-width characters are lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[no-zw]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Check if text contains zero-width characters
|
||||
return /[\u200B-\u200D\uFEFF\u2060]/.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// shuffle characters transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Shuffle Characters',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
// Fisher-Yates shuffle
|
||||
const chars = [...text];
|
||||
for (let i = chars.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[chars[i], chars[j]] = [chars[j], chars[i]];
|
||||
}
|
||||
return chars.join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - order is randomized
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[shuffled]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Cannot detect - random order
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// shuffle words transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Shuffle Words',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
// Split by whitespace, shuffle, rejoin
|
||||
const words = text.split(/(\s+)/);
|
||||
const wordOnly = words.filter((_, i) => i % 2 === 0);
|
||||
const spaces = words.filter((_, i) => i % 2 === 1);
|
||||
|
||||
// Fisher-Yates shuffle words
|
||||
for (let i = wordOnly.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[wordOnly[i], wordOnly[j]] = [wordOnly[j], wordOnly[i]];
|
||||
}
|
||||
|
||||
// Recombine
|
||||
let result = '';
|
||||
for (let i = 0; i < wordOnly.length; i++) {
|
||||
result += wordOnly[i];
|
||||
if (i < spaces.length) result += spaces[i];
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - order is randomized
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[shuffled-words]';
|
||||
return this.func(text.slice(0, 20));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Cannot detect - random order
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// spaces remover transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Spaces Remover',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.replace(/\s+/g, '');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - spaces are lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[no-spaces]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false
|
||||
});
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// text justification transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Text Justify',
|
||||
priority: 100,
|
||||
category: 'format',
|
||||
width: 80, // Default width
|
||||
align: 'left', // left, right, center
|
||||
func: function(text) {
|
||||
const width = parseInt(this.width) || 80;
|
||||
const align = this.align || 'left';
|
||||
|
||||
const lines = text.split('\n');
|
||||
let result = '';
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) {
|
||||
result += '\n';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trimmed.length >= width) {
|
||||
result += trimmed + '\n';
|
||||
continue;
|
||||
}
|
||||
|
||||
let justified = '';
|
||||
if (align === 'left') {
|
||||
justified = trimmed.padEnd(width);
|
||||
} else if (align === 'right') {
|
||||
justified = trimmed.padStart(width);
|
||||
} else if (align === 'center') {
|
||||
const padding = Math.floor((width - trimmed.length) / 2);
|
||||
justified = ' '.repeat(padding) + trimmed + ' '.repeat(width - trimmed.length - padding);
|
||||
} else {
|
||||
justified = trimmed;
|
||||
}
|
||||
|
||||
result += justified + '\n';
|
||||
}
|
||||
|
||||
return result.trimEnd();
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Remove padding spaces
|
||||
return text.split('\n').map(line => line.trim()).join('\n');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[text-justify]';
|
||||
return this.func(text.slice(0, 20));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for consistent line lengths with padding
|
||||
const lines = text.split('\n');
|
||||
if (lines.length < 2) return false;
|
||||
|
||||
const lengths = lines.map(l => l.length);
|
||||
const allSameLength = lengths.every(len => len === lengths[0]);
|
||||
const hasLeadingTrailingSpaces = lines.some(line => /^\s+|\s+$/.test(line));
|
||||
|
||||
return allSameLength && hasLeadingTrailingSpaces && lengths[0] > 40;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// uppercase all transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Uppercase All',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return text.toUpperCase();
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Cannot reverse - original case is lost
|
||||
return text;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[UPPERCASE]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
canDecode: false,
|
||||
detector: function(text) {
|
||||
// Check if all letters are uppercase
|
||||
const letters = text.replace(/[^a-zA-Z]/g, '');
|
||||
return letters.length > 0 && letters === letters.toUpperCase() && /[a-z]/.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// uppercase lowercase toggle transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Toggle Case',
|
||||
priority: 50,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
return [...text].map(c => {
|
||||
if (c >= 'A' && c <= 'Z') {
|
||||
return c.toLowerCase();
|
||||
} else if (c >= 'a' && c <= 'z') {
|
||||
return c.toUpperCase();
|
||||
}
|
||||
return c;
|
||||
}).join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Toggle case is its own inverse
|
||||
return this.func(text);
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[toggle]';
|
||||
return this.func(text.slice(0, 10));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Hard to detect - would need pattern analysis
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// whitespace steganography transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Whitespace Steganography',
|
||||
priority: 100,
|
||||
category: 'format',
|
||||
func: function(text) {
|
||||
// Encode text in whitespace patterns (space = 0, tab = 1)
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
let result = '';
|
||||
|
||||
for (const byte of bytes) {
|
||||
// Encode each byte as 8 characters (space or tab)
|
||||
for (let i = 7; i >= 0; i--) {
|
||||
const bit = (byte >> i) & 1;
|
||||
result += bit === 1 ? '\t' : ' ';
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Decode whitespace patterns back to text
|
||||
const bytes = [];
|
||||
let bitBuffer = '';
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text[i];
|
||||
if (char === ' ' || char === '\t') {
|
||||
bitBuffer += char === '\t' ? '1' : '0';
|
||||
|
||||
// Every 8 bits, convert to a byte
|
||||
if (bitBuffer.length === 8) {
|
||||
bytes.push(parseInt(bitBuffer, 2));
|
||||
bitBuffer = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return new TextDecoder().decode(new Uint8Array(bytes));
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[whitespace-stego]';
|
||||
return this.func(text.slice(0, 1)) + '...';
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check if text is mostly spaces and tabs in groups of 8
|
||||
const cleaned = text.replace(/[^\s\t]/g, '');
|
||||
if (cleaned.length < 8) return false;
|
||||
// Should be mostly whitespace
|
||||
return cleaned.length / text.length > 0.8;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// word wrap transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Word Wrap',
|
||||
priority: 100,
|
||||
category: 'format',
|
||||
width: 80, // Default wrap width
|
||||
func: function(text) {
|
||||
const width = parseInt(this.width) || 80;
|
||||
if (width < 1) return text;
|
||||
|
||||
const lines = text.split('\n');
|
||||
let result = '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.length <= width) {
|
||||
result += line + '\n';
|
||||
continue;
|
||||
}
|
||||
|
||||
// Word wrap: break at word boundaries
|
||||
let currentLine = '';
|
||||
const words = line.split(/(\s+)/);
|
||||
|
||||
for (const word of words) {
|
||||
if ((currentLine + word).length <= width) {
|
||||
currentLine += word;
|
||||
} else {
|
||||
if (currentLine) {
|
||||
result += currentLine.trim() + '\n';
|
||||
}
|
||||
currentLine = word;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentLine) {
|
||||
result += currentLine.trim() + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
return result.trimEnd();
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Remove line breaks (simple approach - may not be perfect)
|
||||
return text.replace(/\n/g, ' ');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[word-wrap]';
|
||||
return this.func(text.slice(0, 50));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check if text has consistent line lengths
|
||||
const lines = text.split('\n');
|
||||
if (lines.length < 2) return false;
|
||||
|
||||
const lengths = lines.map(l => l.length);
|
||||
const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
|
||||
const variance = lengths.reduce((sum, len) => sum + Math.pow(len - avgLength, 2), 0) / lengths.length;
|
||||
|
||||
// Low variance suggests word wrapping
|
||||
return variance < 100 && avgLength > 40 && avgLength < 120;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// zero-width steganography transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Zero-Width Steganography',
|
||||
priority: 100,
|
||||
category: 'format',
|
||||
// Zero-width characters: ZWSP (U+200B), ZWNJ (U+200C), ZWJ (U+200D), ZWNBSP (U+FEFF)
|
||||
zeroWidthChars: ['\u200B', '\u200C', '\u200D', '\uFEFF'],
|
||||
func: function(text) {
|
||||
// Encode text using zero-width characters
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
let result = '';
|
||||
|
||||
for (const byte of bytes) {
|
||||
// Encode each byte as 4 zero-width characters (2 bits per char)
|
||||
for (let i = 6; i >= 0; i -= 2) {
|
||||
const bits = (byte >> i) & 3; // Get 2 bits
|
||||
result += this.zeroWidthChars[bits];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Decode zero-width characters back to text
|
||||
const bytes = [];
|
||||
let bitBuffer = [];
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text[i];
|
||||
const index = this.zeroWidthChars.indexOf(char);
|
||||
if (index >= 0) {
|
||||
bitBuffer.push(index);
|
||||
|
||||
// Every 4 characters, convert to a byte
|
||||
if (bitBuffer.length === 4) {
|
||||
const byte = (bitBuffer[0] << 6) | (bitBuffer[1] << 4) | (bitBuffer[2] << 2) | bitBuffer[3];
|
||||
bytes.push(byte);
|
||||
bitBuffer = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return new TextDecoder().decode(new Uint8Array(bytes));
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[zerowidth-stego]';
|
||||
return '[zero-width encoded]';
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for zero-width characters
|
||||
return /[\u200B\u200C\u200D\uFEFF]/.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// ICAO spelling alphabet transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'ICAO Spelling Alphabet',
|
||||
priority: 200,
|
||||
category: 'technical',
|
||||
alphabet: {
|
||||
'A': 'ALFA', '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': 'XRAY', 'Y': 'YANKEE',
|
||||
'Z': 'ZULU'
|
||||
},
|
||||
func: function(text) {
|
||||
const cleaned = text.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (cleaned.length === 0) return text;
|
||||
|
||||
let result = '';
|
||||
for (const char of cleaned) {
|
||||
if (this.alphabet[char]) {
|
||||
result += this.alphabet[char] + ' ';
|
||||
} else {
|
||||
result += char + ' ';
|
||||
}
|
||||
}
|
||||
return result.trim();
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Create reverse map
|
||||
const reverseMap = {};
|
||||
for (const [letter, word] of Object.entries(this.alphabet)) {
|
||||
reverseMap[word.toUpperCase()] = letter;
|
||||
}
|
||||
|
||||
// Split by spaces and convert back
|
||||
const words = text.toUpperCase().split(/\s+/);
|
||||
let result = '';
|
||||
for (const word of words) {
|
||||
if (reverseMap[word]) {
|
||||
result += reverseMap[word];
|
||||
} else if (word.length === 1 && /[A-Z]/.test(word)) {
|
||||
result += word;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[icao]';
|
||||
return this.func(text.slice(0, 3));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for ICAO words
|
||||
const icaoWords = ['ALFA', 'BRAVO', 'CHARLIE', 'DELTA', 'ECHO', 'FOXTROT', 'GOLF', 'HOTEL', 'INDIA', 'JULIETT', 'KILO', 'LIMA', 'MIKE', 'NOVEMBER', 'OSCAR', 'PAPA', 'QUEBEC', 'ROMEO', 'SIERRA', 'TANGO', 'UNIFORM', 'VICTOR', 'WHISKEY', 'XRAY', 'YANKEE', 'ZULU'];
|
||||
const upper = text.toUpperCase();
|
||||
const matches = icaoWords.filter(word => upper.includes(word));
|
||||
return matches.length >= 2;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// ITU spelling alphabet transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'ITU Spelling Alphabet',
|
||||
priority: 200,
|
||||
category: 'technical',
|
||||
alphabet: {
|
||||
'A': 'AMSTERDAM', 'B': 'BALTIMORE', 'C': 'CASABLANCA', 'D': 'DANMARK', 'E': 'EDISON',
|
||||
'F': 'FLORIDA', 'G': 'GALLIPOLI', 'H': 'HAVANA', 'I': 'ITALIA', 'J': 'JERUSALEM',
|
||||
'K': 'KILOGRAMME', 'L': 'LIVERPOOL', 'M': 'MADRID', 'N': 'NEAPOLIS', 'O': 'OSLO',
|
||||
'P': 'PARIS', 'Q': 'QUEBEC', 'R': 'ROMA', 'S': 'SANTIAGO', 'T': 'TRIPOLI',
|
||||
'U': 'UPPSALA', 'V': 'VALENCIA', 'W': 'WASHINGTON', 'X': 'XANTIPPE', 'Y': 'YOKOHAMA',
|
||||
'Z': 'ZURICH'
|
||||
},
|
||||
func: function(text) {
|
||||
const cleaned = text.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (cleaned.length === 0) return text;
|
||||
|
||||
let result = '';
|
||||
for (const char of cleaned) {
|
||||
if (this.alphabet[char]) {
|
||||
result += this.alphabet[char] + ' ';
|
||||
} else {
|
||||
result += char + ' ';
|
||||
}
|
||||
}
|
||||
return result.trim();
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Create reverse map
|
||||
const reverseMap = {};
|
||||
for (const [letter, word] of Object.entries(this.alphabet)) {
|
||||
reverseMap[word.toUpperCase()] = letter;
|
||||
}
|
||||
|
||||
// Split by spaces and convert back
|
||||
const words = text.toUpperCase().split(/\s+/);
|
||||
let result = '';
|
||||
for (const word of words) {
|
||||
if (reverseMap[word]) {
|
||||
result += reverseMap[word];
|
||||
} else if (word.length === 1 && /[A-Z]/.test(word)) {
|
||||
result += word;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[itu]';
|
||||
return this.func(text.slice(0, 3));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for ITU words
|
||||
const ituWords = ['AMSTERDAM', 'BALTIMORE', 'CASABLANCA', 'DANMARK', 'EDISON', 'FLORIDA', 'GALLIPOLI', 'HAVANA', 'ITALIA', 'JERUSALEM', 'KILOGRAMME', 'LIVERPOOL', 'MADRID', 'NEAPOLIS', 'OSLO', 'PARIS', 'QUEBEC', 'ROMA', 'SANTIAGO', 'TRIPOLI', 'UPPSALA', 'VALENCIA', 'WASHINGTON', 'XANTIPPE', 'YOKOHAMA', 'ZURICH'];
|
||||
const upper = text.toUpperCase();
|
||||
const matches = ituWords.filter(word => upper.includes(word));
|
||||
return matches.length >= 2;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// maritime signal flags transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Maritime Signal Flags',
|
||||
priority: 200,
|
||||
category: 'technical',
|
||||
// International maritime signal flags (NATO phonetic with flag emojis)
|
||||
flags: {
|
||||
'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': '🚩'
|
||||
},
|
||||
// Using flag emojis - actual maritime flags would need proper Unicode
|
||||
// For now, using regional indicator symbols which represent flags
|
||||
flagMap: {
|
||||
'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) {
|
||||
const cleaned = text.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (cleaned.length === 0) return text;
|
||||
|
||||
let result = '';
|
||||
for (const char of cleaned) {
|
||||
if (this.flagMap[char]) {
|
||||
result += this.flagMap[char] + ' ';
|
||||
} else {
|
||||
result += char + ' ';
|
||||
}
|
||||
}
|
||||
return result.trim();
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Reverse map from flag emoji to letter
|
||||
const reverseMap = {};
|
||||
for (const [letter, flag] of Object.entries(this.flagMap)) {
|
||||
reverseMap[flag] = letter;
|
||||
}
|
||||
|
||||
let result = '';
|
||||
// Match flag emojis (regional indicators - match each one individually)
|
||||
const flagChars = Object.values(this.flagMap);
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
// Check for 2-char regional indicator sequences
|
||||
if (i + 1 < text.length) {
|
||||
const pair = text.substring(i, i + 2);
|
||||
if (reverseMap[pair]) {
|
||||
result += reverseMap[pair];
|
||||
i++; // Skip next char
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Check single char
|
||||
const char = text[i];
|
||||
if (reverseMap[char]) {
|
||||
result += reverseMap[char];
|
||||
} else if (/[A-Z]/.test(char)) {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[maritime-flags]';
|
||||
return this.func(text.slice(0, 3));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for regional indicator flag emojis (check for any flag in the map)
|
||||
const flagChars = Object.values(this.flagMap);
|
||||
return flagChars.some(flag => text.includes(flag));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// bold italic text transform (Mathematical Bold Italic)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Bold Italic',
|
||||
priority: 85,
|
||||
category: 'unicode',
|
||||
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) {
|
||||
if (!text) return '[bold-italic]';
|
||||
return this.func(text.slice(0, 8));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for Mathematical Bold Italic characters (U+1D468-U+1D49C)
|
||||
return /[\u{1D468}-\u{1D49C}]/u.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// bold text transform (Mathematical Bold)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Bold',
|
||||
priority: 85,
|
||||
category: 'unicode',
|
||||
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) {
|
||||
if (!text) return '[bold]';
|
||||
return this.func(text.slice(0, 8));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for Mathematical Bold characters (U+1D400-U+1D7FF)
|
||||
return /[\u{1D400}-\u{1D7FF}]/u.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// circled unicode transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Circled',
|
||||
priority: 150,
|
||||
category: 'unicode',
|
||||
map: {
|
||||
'A': 'Ⓐ', 'B': 'Ⓑ', 'C': 'Ⓒ', 'D': 'Ⓓ', 'E': 'Ⓔ',
|
||||
'F': 'Ⓕ', 'G': 'Ⓖ', 'H': 'Ⓗ', 'I': 'Ⓘ', 'J': 'Ⓙ',
|
||||
'K': 'Ⓚ', 'L': 'Ⓛ', 'M': 'Ⓜ', 'N': 'Ⓝ', 'O': 'Ⓞ',
|
||||
'P': 'Ⓟ', 'Q': 'Ⓠ', 'R': 'Ⓡ', 'S': 'Ⓢ', 'T': 'Ⓣ',
|
||||
'U': 'Ⓤ', 'V': 'Ⓥ', 'W': 'Ⓦ', 'X': 'Ⓧ', 'Y': 'Ⓨ',
|
||||
'Z': 'Ⓩ',
|
||||
'0': '⓪', '1': '①', '2': '②', '3': '③', '4': '④',
|
||||
'5': '⑤', '6': '⑥', '7': '⑦', '8': '⑧', '9': '⑨'
|
||||
},
|
||||
func: function(text) {
|
||||
let result = '';
|
||||
for (const char of text) {
|
||||
const upper = char.toUpperCase();
|
||||
if (this.map[upper]) {
|
||||
result += this.map[upper];
|
||||
} else if (this.map[char]) {
|
||||
result += this.map[char];
|
||||
} else {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const reverseMap = {};
|
||||
for (const [key, value] of Object.entries(this.map)) {
|
||||
reverseMap[value] = key;
|
||||
}
|
||||
|
||||
let result = '';
|
||||
for (const char of text) {
|
||||
if (reverseMap[char]) {
|
||||
result += reverseMap[char];
|
||||
} else {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[circled]';
|
||||
return this.func(text.slice(0, 5));
|
||||
},
|
||||
detector: function(text) {
|
||||
const circledChars = Object.values(this.map);
|
||||
return circledChars.some(char => text.includes(char));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// dashed underline transform (using combining characters)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Dashed Underline',
|
||||
priority: 100,
|
||||
category: 'unicode',
|
||||
func: function(text) {
|
||||
// Add dashed underline combining character (U+0320) after each character
|
||||
return [...text].map(c => c + '\u0320').join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Remove combining dashed below character
|
||||
return text.replace(/\u0320/g, '');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[dashed-underline]';
|
||||
return this.func(text.slice(0, 8));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for dashed below combining character
|
||||
return /\u0320/.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// dotted underline transform (using combining characters)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Dotted Underline',
|
||||
priority: 100,
|
||||
category: 'unicode',
|
||||
func: function(text) {
|
||||
// Add dotted underline combining character (U+0324) after each character
|
||||
return [...text].map(c => c + '\u0324').join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Remove combining dotted below character
|
||||
return text.replace(/\u0324/g, '');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[dotted-underline]';
|
||||
return this.func(text.slice(0, 8));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for dotted below combining character
|
||||
return /\u0324/.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// italic text transform (Mathematical Italic)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Italic',
|
||||
priority: 85,
|
||||
category: 'unicode',
|
||||
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) {
|
||||
if (!text) return '[italic]';
|
||||
return this.func(text.slice(0, 8));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for Mathematical Italic characters (U+1D434-U+1D468)
|
||||
return /[\u{1D434}-\u{1D468}]/u.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// negative squared unicode transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Negative Squared',
|
||||
priority: 150,
|
||||
category: 'unicode',
|
||||
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': '🆉'
|
||||
},
|
||||
func: function(text) {
|
||||
let result = '';
|
||||
for (const char of text) {
|
||||
const upper = char.toUpperCase();
|
||||
if (this.map[upper]) {
|
||||
result += this.map[upper];
|
||||
} else {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const reverseMap = {};
|
||||
for (const [key, value] of Object.entries(this.map)) {
|
||||
reverseMap[value] = key;
|
||||
}
|
||||
|
||||
let result = '';
|
||||
for (const char of text) {
|
||||
if (reverseMap[char]) {
|
||||
result += reverseMap[char];
|
||||
} else {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[negative-squared]';
|
||||
return this.func(text.slice(0, 5));
|
||||
},
|
||||
detector: function(text) {
|
||||
const negSquaredChars = Object.values(this.map);
|
||||
return negSquaredChars.some(char => text.includes(char));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// overline transform (using combining characters)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Overline',
|
||||
priority: 100,
|
||||
category: 'unicode',
|
||||
func: function(text) {
|
||||
// Add overline combining character (U+0305) after each character
|
||||
return [...text].map(c => c + '\u0305').join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Remove combining overline character
|
||||
return text.replace(/\u0305/g, '');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[overline]';
|
||||
return this.func(text.slice(0, 8));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for combining overline character
|
||||
return /\u0305/.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// parenthesized unicode transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Parenthesized',
|
||||
priority: 150,
|
||||
category: 'unicode',
|
||||
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': '⑷', '5': '⑸',
|
||||
'6': '⑹', '7': '⑺', '8': '⑻', '9': '⑼', '0': '⑽'
|
||||
},
|
||||
func: function(text) {
|
||||
let result = '';
|
||||
for (const char of text) {
|
||||
const upper = char.toUpperCase();
|
||||
if (this.map[upper]) {
|
||||
result += this.map[upper];
|
||||
} else if (this.map[char]) {
|
||||
result += this.map[char];
|
||||
} else {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const reverseMap = {};
|
||||
for (const [key, value] of Object.entries(this.map)) {
|
||||
reverseMap[value] = key;
|
||||
}
|
||||
|
||||
let result = '';
|
||||
for (const char of text) {
|
||||
if (reverseMap[char]) {
|
||||
result += reverseMap[char];
|
||||
} else {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[parenthesized]';
|
||||
return this.func(text.slice(0, 5));
|
||||
},
|
||||
detector: function(text) {
|
||||
const parenthesizedChars = Object.values(this.map);
|
||||
return parenthesizedChars.some(char => text.includes(char));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// squared unicode transform
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Squared',
|
||||
priority: 150,
|
||||
category: 'unicode',
|
||||
map: {
|
||||
'A': '🄰', 'B': '🄱', 'C': '🄲', 'D': '🄳', 'E': '🄴',
|
||||
'F': '🄵', 'G': '🄶', 'H': '🄷', 'I': '🄸', 'J': '🄹',
|
||||
'K': '🄺', 'L': '🄻', 'M': '🄼', 'N': '🄽', 'O': '🄾',
|
||||
'P': '🄿', 'Q': '🅀', 'R': '🅁', 'S': '🅂', 'T': '🅃',
|
||||
'U': '🅄', 'V': '🅅', 'W': '🅆', 'X': '🅇', 'Y': '🅈',
|
||||
'Z': '🅉',
|
||||
'0': '⓪', '1': '①', '2': '②', '3': '③', '4': '④',
|
||||
'5': '⑤', '6': '⑥', '7': '⑦', '8': '⑧', '9': '⑨'
|
||||
},
|
||||
func: function(text) {
|
||||
let result = '';
|
||||
for (const char of text) {
|
||||
const upper = char.toUpperCase();
|
||||
if (this.map[upper]) {
|
||||
result += this.map[upper];
|
||||
} else if (this.map[char]) {
|
||||
result += this.map[char];
|
||||
} else {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
reverse: function(text) {
|
||||
const reverseMap = {};
|
||||
for (const [key, value] of Object.entries(this.map)) {
|
||||
reverseMap[value] = key;
|
||||
}
|
||||
|
||||
let result = '';
|
||||
for (const char of text) {
|
||||
if (reverseMap[char]) {
|
||||
result += reverseMap[char];
|
||||
} else {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[squared]';
|
||||
return this.func(text.slice(0, 5));
|
||||
},
|
||||
detector: function(text) {
|
||||
const squaredChars = Object.values(this.map);
|
||||
return squaredChars.some(char => text.includes(char));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -17,6 +17,10 @@ export default new BaseTransformer({
|
||||
reverse: function(text) {
|
||||
// Remove combining strikethrough characters
|
||||
return text.replace(/\u0336/g, '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for combining strikethrough character (U+0336)
|
||||
return /\u0336/.test(text);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -17,6 +17,10 @@ export default new BaseTransformer({
|
||||
reverse: function(text) {
|
||||
// Remove combining underline characters
|
||||
return text.replace(/\u0332/g, '');
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for combining underline character (U+0332)
|
||||
return /\u0332/.test(text);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
// wavy underline transform (using combining characters)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Wavy Underline',
|
||||
priority: 100,
|
||||
category: 'unicode',
|
||||
func: function(text) {
|
||||
// Add wavy underline combining character (U+0330) after each character
|
||||
return [...text].map(c => c + '\u0330').join('');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Remove combining wavy below character
|
||||
return text.replace(/\u0330/g, '');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[wavy-underline]';
|
||||
return this.func(text.slice(0, 8));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check for wavy below combining character
|
||||
return /\u0330/.test(text);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// wide spacing transform (adds spaces between characters)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'Wide Spacing',
|
||||
priority: 100,
|
||||
category: 'unicode',
|
||||
func: function(text) {
|
||||
// Add space between each character
|
||||
return [...text].join(' ');
|
||||
},
|
||||
reverse: function(text) {
|
||||
// Remove all spaces
|
||||
return text.replace(/\s+/g, '');
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) return '[wide-spacing]';
|
||||
return this.func(text.slice(0, 8));
|
||||
},
|
||||
detector: function(text) {
|
||||
// Check if text has spaces between most characters
|
||||
// At least 50% of characters should be followed by a space
|
||||
if (text.length < 3) return false;
|
||||
const spaceCount = (text.match(/\s/g) || []).length;
|
||||
const charCount = text.replace(/\s/g, '').length;
|
||||
return charCount > 0 && spaceCount / (charCount + spaceCount) > 0.3;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
const fs = require('fs');
|
||||
|
||||
// The JSON data fetched from the repository
|
||||
const jsonData = {
|
||||
"_metadata": {
|
||||
"name": "AGGREGLITCH",
|
||||
"version": "1.0.0",
|
||||
"description": "The Complete Glitch Token Library - All Known LLM Vocabulary Anomalies",
|
||||
"tagline": "GOTTA CATCH 'EM ALL",
|
||||
"total_tokens_cataloged": 7895,
|
||||
"last_updated": "2025-12-27",
|
||||
"sources": [
|
||||
"SolidGoldMagikarp (LessWrong, 2023) - Rumbelow & Watkins",
|
||||
"SolidGoldMagikarp II & III Technical Details (LessWrong)",
|
||||
"Glitch Token Catalog - Full Clear (LessWrong, 2024)",
|
||||
"SmartyHeaderCode: Anomalous Tokens GPT3.5/GPT-4 (LessWrong)",
|
||||
"The petertodd/Leilan Phenomenon (LessWrong)",
|
||||
"Mapping the Semantic Void (LessWrong)",
|
||||
"BPE Subtoken Artifacts (LessWrong)",
|
||||
"Anomalous Tokens in DeepSeek-V3/r1 (Substack, 2025)",
|
||||
"Glitch Tokens in LLMs (ACM, 2024)",
|
||||
"GlitchMiner: Gradient-based Detection (arXiv, 2024)",
|
||||
"GPT-4o Chinese Token Pollution (MIT Tech Review, 2024)",
|
||||
"NVIDIA Garak LLM Vulnerability Scanner",
|
||||
"Dropbox Prompt Injection Research (2023)"
|
||||
],
|
||||
"usage": "Import this library to test LLMs for glitch token vulnerabilities"
|
||||
},
|
||||
"behavior_categories": {
|
||||
"UNSPEAKABLE": "Model CANNOT repeat these tokens - substitutes, evades, or produces garbage",
|
||||
"POLYSEMANTIC": "Token interpreted as DIFFERENT words each time, even at temperature 0",
|
||||
"GLITCHED_SPELLING": "Model CAN repeat but CANNOT spell correctly",
|
||||
"CONTEXT_CORRUPTOR": "Token corrupts surrounding context when present",
|
||||
"LOOP_INDUCER": "Causes infinite generation loops - DoS potential",
|
||||
"IDENTITY_DISRUPTOR": "Causes model to lose sense of identity",
|
||||
"FRAGMENT": "Orphaned BPE subtoken that glitches without parent",
|
||||
"UNREACHABLE": "Exists in vocabulary but pre-tokenization prevents use"
|
||||
},
|
||||
"tokenizers": {
|
||||
"r50k_base": {
|
||||
"name": "GPT-2/GPT-3 Tokenizer",
|
||||
"vocab_size": 50257,
|
||||
"models": ["GPT-2", "GPT-3", "GPT-J"]
|
||||
},
|
||||
"cl100k_base": {
|
||||
"name": "GPT-3.5/GPT-4 Tokenizer",
|
||||
"vocab_size": 100256,
|
||||
"models": ["GPT-3.5-turbo", "GPT-4", "GPT-4-turbo"]
|
||||
},
|
||||
"o200k_base": {
|
||||
"name": "GPT-4o Tokenizer",
|
||||
"vocab_size": 200000,
|
||||
"models": ["GPT-4o", "GPT-4o-mini"]
|
||||
},
|
||||
"llama": {
|
||||
"name": "LLaMA Tokenizer",
|
||||
"models": ["Llama-2-7b", "Llama-2-13b", "Llama-3"]
|
||||
},
|
||||
"deepseek": {
|
||||
"name": "DeepSeek Tokenizer",
|
||||
"models": ["DeepSeek-V3", "DeepSeek-r1"]
|
||||
}
|
||||
},
|
||||
"glitch_tokens": {}
|
||||
};
|
||||
|
||||
// Note: The full glitch_tokens data is too large to include here
|
||||
// We'll need to fetch it from the URL or copy it from the browser
|
||||
console.log('This is a placeholder. The actual data needs to be fetched from the repository.');
|
||||
console.log('Total tokens should be:', jsonData._metadata.total_tokens_cataloged);
|
||||
|
||||
@@ -14,16 +14,22 @@
|
||||
|
||||
<div class="pc-controls">
|
||||
<div class="pc-strategies">
|
||||
<label class="pc-label">Strategy</label>
|
||||
<div class="pc-strategy-grid">
|
||||
<div class="pc-label" id="pc-strategy-label">Strategy</div>
|
||||
<div
|
||||
class="pc-strategy-grid"
|
||||
role="group"
|
||||
aria-labelledby="pc-strategy-label"
|
||||
>
|
||||
<button
|
||||
v-for="s in pcStrategies"
|
||||
:key="s.id"
|
||||
type="button"
|
||||
@click="pcStrategy = s.id"
|
||||
:class="'pc-strategy-btn' + (pcStrategy === s.id ? ' active' : '')"
|
||||
:class="['pc-strategy-btn', { active: pcStrategy === s.id }]"
|
||||
:aria-pressed="pcStrategy === s.id ? 'true' : 'false'"
|
||||
:title="s.desc"
|
||||
>
|
||||
<i :class="'fas ' + s.icon"></i>
|
||||
<i :class="'fas ' + s.icon" aria-hidden="true"></i>
|
||||
<span>{{ s.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
+50
-2
@@ -26,6 +26,10 @@
|
||||
<select v-model="splitterMode">
|
||||
<option value="chunk">Character Chunks</option>
|
||||
<option value="word">Split Words</option>
|
||||
<option value="sentence">Sentences</option>
|
||||
<option value="line">Lines</option>
|
||||
<option value="pattern">Custom Pattern (Regex)</option>
|
||||
<option value="token">Token-Based</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
@@ -58,6 +62,45 @@
|
||||
<input type="checkbox" v-model="splitterSplitFirstWord" />
|
||||
<span>Split First Word</span>
|
||||
</label>
|
||||
|
||||
<!-- Sentence mode options -->
|
||||
<label v-if="splitterMode === 'sentence'" class="switch neon">
|
||||
<input type="checkbox" v-model="splitterPreserveEmptyLines" />
|
||||
<span>Preserve Empty Lines</span>
|
||||
</label>
|
||||
|
||||
<!-- Line mode options -->
|
||||
<label v-if="splitterMode === 'line'" class="switch neon">
|
||||
<input type="checkbox" v-model="splitterPreserveEmptyLines" />
|
||||
<span>Preserve Empty Lines</span>
|
||||
</label>
|
||||
|
||||
<!-- Custom pattern options -->
|
||||
<label v-if="splitterMode === 'pattern'">
|
||||
Regex Pattern
|
||||
<input type="text" v-model="splitterCustomPattern" placeholder="\\s+" />
|
||||
</label>
|
||||
|
||||
<label v-if="splitterMode === 'pattern'" class="switch neon">
|
||||
<input type="checkbox" v-model="splitterPatternIncludeDelimiter" />
|
||||
<span>Include Delimiter</span>
|
||||
</label>
|
||||
|
||||
<!-- Token-based options -->
|
||||
<label v-if="splitterMode === 'token'">
|
||||
Tokenizer
|
||||
<select v-model="splitterTokenizer">
|
||||
<option value="cl100k">cl100k (GPT-3.5/GPT-4)</option>
|
||||
<option value="o200k">o200k (GPT-4o)</option>
|
||||
<option value="p50k">p50k (Code editing models)</option>
|
||||
<option value="r50k">r50k (GPT-2/GPT-3)</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label v-if="splitterMode === 'token'">
|
||||
Tokens Per Chunk
|
||||
<input type="number" v-model.number="splitterTokenCount" min="1" max="1000" placeholder="3" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Transform Section -->
|
||||
@@ -101,12 +144,17 @@
|
||||
<div class="encapsulation-section">
|
||||
<div class="section-header">
|
||||
<h4><i class="fas fa-bracket-curly"></i> Encapsulation</h4>
|
||||
<p>Wrap each message with custom start and end strings</p>
|
||||
<p>Wrap each message with custom start and end strings. Use iterator marker to insert split numbers.</p>
|
||||
</div>
|
||||
<div class="options-grid">
|
||||
<label>
|
||||
Iterator Marker
|
||||
<input type="text" v-model="splitterIteratorMarker" placeholder="{n}" />
|
||||
<small>This marker will be replaced with the split number (1, 2, 3...)</small>
|
||||
</label>
|
||||
<label>
|
||||
Start String (optional)
|
||||
<input type="text" v-model="splitterStartWrap" placeholder="e.g., $, <, [" />
|
||||
<input type="text" v-model="splitterStartWrap" placeholder="e.g., Message {n}: " />
|
||||
</label>
|
||||
<label>
|
||||
End String (optional)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<option value="word">Naive words</option>
|
||||
<option value="cl100k">OpenAI: cl100k_base (GPT‑3.5/4)</option>
|
||||
<option value="o200k">OpenAI: o200k_base (GPT‑4o)</option>
|
||||
<option value="p50k">OpenAI: p50k_base</option>
|
||||
<option value="p50k">OpenAI: p50k_edit (Code editing models)</option>
|
||||
<option value="r50k">OpenAI: r50k_base</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
+114
-46
@@ -26,7 +26,7 @@
|
||||
<div class="transform-categories">
|
||||
<!-- Favorites Section -->
|
||||
<div
|
||||
v-if="showFavorites && getFavoriteTransforms().length > 0"
|
||||
v-if="showFavorites && getFavoriteDisplayItems().length > 0"
|
||||
id="category-favorites"
|
||||
class="transform-category-section favorites-section"
|
||||
>
|
||||
@@ -34,30 +34,52 @@
|
||||
<i class="fas fa-star"></i> Favorites
|
||||
</h4>
|
||||
<div class="transform-buttons">
|
||||
<div v-for="transform in getFavoriteTransforms()" :key="transform.name" class="transform-button-group">
|
||||
<button
|
||||
@click="applyTransform(transform, $event)"
|
||||
:class="'transform-button transform-category-' + getDisplayCategory(transform.name)"
|
||||
: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
|
||||
@click.stop="toggleFavorite(transform.name, $event)"
|
||||
:class="'fas fa-star favorite-icon' + (isFavorite(transform.name) ? ' favorited' : '')"
|
||||
:title="isFavorite(transform.name) ? 'Remove from favorites' : 'Add to favorites'"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
<template v-for="item in getFavoriteDisplayItems()" :key="item.key">
|
||||
<div v-if="item.type === 'transform'" class="transform-button-group">
|
||||
<button
|
||||
@click="applyTransform(item.transform, $event)"
|
||||
:class="'transform-button transform-category-' + getDisplayCategory(item.transform.name)"
|
||||
:class="{ active: activeTransform === item.transform }"
|
||||
:title="'Click to transform and copy: ' + item.transform.name"
|
||||
>
|
||||
{{ item.transform.name }}
|
||||
<small class="transform-preview" v-if="transformInput">
|
||||
{{ item.transform.preview(transformInput.slice(0, 10)) }}
|
||||
</small>
|
||||
<i
|
||||
@click.stop="toggleFavorite(item.transform.name, $event)"
|
||||
:class="'fas fa-star favorite-icon' + (isFavorite(item.transform.name) ? ' favorited' : '')"
|
||||
:title="isFavorite(item.transform.name) ? 'Remove from favorites' : 'Add to favorites'"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else-if="item.type === 'translate'" class="transform-button-group">
|
||||
<button
|
||||
type="button"
|
||||
class="transform-button transform-category-ancient"
|
||||
@click="translateTo(item.langName)"
|
||||
:disabled="translateLoading"
|
||||
:title="'Translate to ' + item.langName"
|
||||
>
|
||||
{{ item.langName }}
|
||||
<small class="transform-preview" v-if="transformInput">
|
||||
{{ transformInput.slice(0, 10) }}
|
||||
</small>
|
||||
<i
|
||||
@click.stop="toggleTranslateFavorite(item.langName, item.custom, $event)"
|
||||
class="fas fa-star favorite-icon"
|
||||
:class="{ favorited: isTranslateFavorite(item.langName, item.custom) }"
|
||||
:title="isTranslateFavorite(item.langName, item.custom) ? 'Remove from favorites' : 'Add to favorites'"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Last Used Section -->
|
||||
<div
|
||||
v-if="showLastUsed && getLastUsedTransforms().length > 0"
|
||||
v-if="showLastUsed && getLastUsedDisplayItems().length > 0"
|
||||
id="category-last-used"
|
||||
class="transform-category-section last-used-section"
|
||||
>
|
||||
@@ -65,24 +87,46 @@
|
||||
<i class="fas fa-clock"></i> Last Used
|
||||
</h4>
|
||||
<div class="transform-buttons">
|
||||
<div v-for="transform in getLastUsedTransforms()" :key="transform.name" class="transform-button-group">
|
||||
<button
|
||||
@click="applyTransform(transform, $event)"
|
||||
:class="'transform-button transform-category-' + getDisplayCategory(transform.name)"
|
||||
: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
|
||||
@click.stop="toggleFavorite(transform.name, $event)"
|
||||
:class="'fas fa-star favorite-icon' + (isFavorite(transform.name) ? ' favorited' : '')"
|
||||
:title="isFavorite(transform.name) ? 'Remove from favorites' : 'Add to favorites'"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
<template v-for="item in getLastUsedDisplayItems()" :key="item.key">
|
||||
<div v-if="item.type === 'transform'" class="transform-button-group">
|
||||
<button
|
||||
@click="applyTransform(item.transform, $event)"
|
||||
:class="'transform-button transform-category-' + getDisplayCategory(item.transform.name)"
|
||||
:class="{ active: activeTransform === item.transform }"
|
||||
:title="'Click to transform and copy: ' + item.transform.name"
|
||||
>
|
||||
{{ item.transform.name }}
|
||||
<small class="transform-preview" v-if="transformInput">
|
||||
{{ item.transform.preview(transformInput.slice(0, 10)) }}
|
||||
</small>
|
||||
<i
|
||||
@click.stop="toggleFavorite(item.transform.name, $event)"
|
||||
:class="'fas fa-star favorite-icon' + (isFavorite(item.transform.name) ? ' favorited' : '')"
|
||||
:title="isFavorite(item.transform.name) ? 'Remove from favorites' : 'Add to favorites'"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else-if="item.type === 'translate'" class="transform-button-group">
|
||||
<button
|
||||
type="button"
|
||||
class="transform-button transform-category-ancient"
|
||||
@click="translateTo(item.langName)"
|
||||
:disabled="translateLoading"
|
||||
:title="'Translate to ' + item.langName"
|
||||
>
|
||||
{{ item.langName }}
|
||||
<small class="transform-preview" v-if="transformInput">
|
||||
{{ transformInput.slice(0, 10) }}
|
||||
</small>
|
||||
<i
|
||||
@click.stop="toggleTranslateFavorite(item.langName, item.custom, $event)"
|
||||
class="fas fa-star favorite-icon"
|
||||
:class="{ favorited: isTranslateFavorite(item.langName, item.custom) }"
|
||||
:title="isTranslateFavorite(item.langName, item.custom) ? 'Remove from favorites' : 'Add to favorites'"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -94,7 +138,7 @@
|
||||
<small class="translate-powered-by">TranslateGemma</small>
|
||||
</h4>
|
||||
|
||||
<div v-if="translateError" class="pc-error" style="margin-bottom: 10px;">
|
||||
<div v-if="translateError" class="pc-error">
|
||||
<i class="fas fa-exclamation-triangle"></i> {{ translateError }}
|
||||
</div>
|
||||
|
||||
@@ -121,6 +165,12 @@
|
||||
>
|
||||
<span class="translate-flag">{{ translateGetFlag(lang.flag) }}</span>
|
||||
<span class="translate-name">{{ lang.name }}</span>
|
||||
<i
|
||||
@click.stop="toggleTranslateFavorite(lang.name, false, $event)"
|
||||
class="fas fa-star favorite-icon translate-lang-favorite"
|
||||
:class="{ favorited: isTranslateFavorite(lang.name, false) }"
|
||||
:title="isTranslateFavorite(lang.name, false) ? 'Remove from favorites' : 'Add to favorites'"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -138,17 +188,29 @@
|
||||
>
|
||||
<span class="translate-flag">{{ translateGetFlag(lang.flag) }}</span>
|
||||
<span class="translate-name">{{ lang.name }}</span>
|
||||
<i
|
||||
@click.stop="toggleTranslateFavorite(lang.name, false, $event)"
|
||||
class="fas fa-star favorite-icon translate-lang-favorite"
|
||||
:class="{ favorited: isTranslateFavorite(lang.name, false) }"
|
||||
:title="isTranslateFavorite(lang.name, false) ? 'Remove from favorites' : 'Add to favorites'"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="translate-subsection">
|
||||
<div class="translate-subsection-label">
|
||||
<i class="fas fa-plus-circle"></i> Custom
|
||||
<button class="translate-add-toggle" @click="translateAddingLang = !translateAddingLang" title="Add a language">
|
||||
<button
|
||||
type="button"
|
||||
class="translate-subsection-label translate-custom-label-btn"
|
||||
@click="translateAddingLang = !translateAddingLang"
|
||||
title="Add a language"
|
||||
>
|
||||
<i class="fas fa-plus-circle"></i>
|
||||
<span>Custom</span>
|
||||
<span class="translate-custom-toggle-end" aria-hidden="true">
|
||||
<i :class="translateAddingLang ? 'fas fa-minus' : 'fas fa-plus'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</span>
|
||||
</button>
|
||||
<div v-if="translateAddingLang" class="translate-add-form">
|
||||
<input
|
||||
type="text"
|
||||
@@ -169,13 +231,19 @@
|
||||
:disabled="translateLoading"
|
||||
:title="'Translate to ' + lang.name"
|
||||
>
|
||||
<span class="translate-flag">🌐</span>
|
||||
<span class="translate-flag translate-custom-flag" aria-hidden="true"><i class="fas fa-globe"></i></span>
|
||||
<span class="translate-name">{{ lang.name }}</span>
|
||||
<span class="translate-remove" @click.stop="translateRemoveCustomLang(idx)" title="Remove">×</span>
|
||||
<i
|
||||
@click.stop="toggleTranslateFavorite(lang.name, true, $event)"
|
||||
class="fas fa-star favorite-icon translate-lang-favorite"
|
||||
:class="{ favorited: isTranslateFavorite(lang.name, true) }"
|
||||
:title="isTranslateFavorite(lang.name, true) ? 'Remove from favorites' : 'Add to favorites'"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="!translateCustomLangs.length && !translateAddingLang" class="translate-empty-custom">
|
||||
<small>Click <i class="fas fa-plus"></i> to add any language.</small>
|
||||
<small>Click <strong>Custom</strong> above to add any language.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user