mirror of
https://github.com/elder-plinius/P4RS3LT0NGV3.git
synced 2026-07-11 23:16:34 +02:00
added alphabet tool, updated packages, a few transform fixes
This commit is contained in:
@@ -32,6 +32,7 @@ Categories match the Transform tab and the folders under `src/transformers/` (ea
|
||||
|
||||
#### **Cipher**
|
||||
- **ADFGX Cipher** - WWI ADFGVX-style polybius + column transposition
|
||||
- **N7AX Cipher** - ADFGX-style polybius with N7AX coordinates + column transposition
|
||||
- **Affine Cipher** - Affine substitution (ax + b mod 26)
|
||||
- **Atbash Cipher** - Reverse-alphabet substitution (A↔Z)
|
||||
- **Autokey Cipher** - Key stream mixed with plaintext (autokey)
|
||||
@@ -266,6 +267,14 @@ Tabs appear in **UI order** below. **OpenRouter** (optional or required per tool
|
||||
- **Transform chain**: Optionally run transforms on each piece.
|
||||
- **Wrapping**: Start/end templates; `{n}` iterator marker; single-line vs multiline copy.
|
||||
|
||||
### 🔤 **Spelling Alphabets**
|
||||
|
||||
- **Custom ICAO-style alphabets**: One word per letter A–Z, like NATO/ICAO phonetic spelling.
|
||||
- **OpenRouter (optional)**: Enter a category/theme and generate a full alphabet; edit any letter before saving.
|
||||
- **Manual mode**: No API key required — fill in all 26 letters yourself.
|
||||
- **Saved locally**: Alphabets persist in browser `localStorage` as JSON.
|
||||
- **Transforms integration**: Each saved alphabet appears on the Transforms page under `custom_spelling`.
|
||||
|
||||
### 💬 **Gibberish**
|
||||
|
||||
- **Dictionary mode**: Seeded random gibberish over a configurable character set.
|
||||
@@ -296,7 +305,7 @@ models
|
||||
|
||||
### 🔑 **OpenRouter API Key Setup**
|
||||
|
||||
**AI Translation**, **PromptCraft**, and **Anti-Classifier** require an [OpenRouter](https://openrouter.ai/) API key. **Decoder**’s optional “translate to English” also uses OpenRouter when enabled.
|
||||
**AI Translation**, **PromptCraft**, **Anti-Classifier**, and **Spelling Alphabets** (optional generate) require an [OpenRouter](https://openrouter.ai/) API key. **Decoder**’s optional “translate to English” also uses OpenRouter when enabled. The Spelling Alphabets tool works fully without a key if you enter letters manually.
|
||||
|
||||
1. Create an account at [openrouter.ai](https://openrouter.ai/)
|
||||
2. Generate an API key (starts with `sk-or-...`)
|
||||
|
||||
@@ -95,14 +95,18 @@ for (const [name, filePath] of Object.entries(transforms)) {
|
||||
}
|
||||
|
||||
// Extract the object definition (remove comments, import, and export statements)
|
||||
const cleanContent = content
|
||||
let cleanContent = content
|
||||
.replace(/^\/\/.*$/gm, '') // Remove single-line comments
|
||||
.replace(/import\s+.*?from\s+['"].*?['"]\s*;?\s*/g, '') // Remove import statements
|
||||
.replace(/export default\s*/g, '') // Remove export statement
|
||||
.trim();
|
||||
|
||||
|
||||
output += `// ${name} (from ${filePath})\n`;
|
||||
output += `transforms['${name}'] = ${cleanContent}\n\n`;
|
||||
if (/^new BaseTransformer\s*\(/.test(cleanContent)) {
|
||||
output += `transforms['${name}'] = ${cleanContent}\n\n`;
|
||||
} else {
|
||||
output += `transforms['${name}'] = ${cleanContent}\n\n`;
|
||||
}
|
||||
|
||||
console.log(`✅ Bundled: ${name} (category: ${category})`);
|
||||
} catch (error) {
|
||||
|
||||
@@ -21,7 +21,8 @@ const templateFiles = [
|
||||
'tokenizer.html',
|
||||
'bijection.html',
|
||||
'splitter.html',
|
||||
'gibberish.html'
|
||||
'gibberish.html',
|
||||
'spellingalphabet.html'
|
||||
];
|
||||
|
||||
|
||||
|
||||
+132
@@ -49,6 +49,8 @@
|
||||
--randomizer-color-rgb: 156, 39, 176;
|
||||
--case-color: #9575cd; /* Indigo for case transformations */
|
||||
--case-color-rgb: 149, 117, 205;
|
||||
--custom-spelling-color: #ff7043; /* Deep orange for user spelling alphabets */
|
||||
--custom-spelling-color-rgb: 255, 112, 67;
|
||||
|
||||
/* Active state gradient end colors */
|
||||
--encoding-active-end: #9575cd;
|
||||
@@ -62,6 +64,7 @@
|
||||
--technical-active-end: #26c6da;
|
||||
--randomizer-active-end: #ab47bc;
|
||||
--case-active-end: #b39ddb;
|
||||
--custom-spelling-active-end: #ff8a65;
|
||||
|
||||
/* Spacing scale */
|
||||
--spacing-xs: 4px;
|
||||
@@ -1509,6 +1512,10 @@ body.dark-theme .mobile-tool-dropdown {
|
||||
border-left-color: var(--case-color);
|
||||
}
|
||||
|
||||
.legend-item.transform-category-custom_spelling {
|
||||
border-left-color: var(--custom-spelling-color);
|
||||
}
|
||||
|
||||
.transform-section:focus-within {
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: var(--focus-shadow);
|
||||
@@ -1675,6 +1682,10 @@ body.dark-theme .mobile-tool-dropdown {
|
||||
border-left-color: var(--case-color);
|
||||
}
|
||||
|
||||
.category-title.transform-category-custom_spelling {
|
||||
border-left-color: var(--custom-spelling-color);
|
||||
}
|
||||
|
||||
.category-title.transform-category-translate {
|
||||
border-left-color: var(--ancient-color);
|
||||
text-transform: none;
|
||||
@@ -2605,6 +2616,10 @@ button.endsequence-item:hover .endsequence-copy-affordance {
|
||||
color: rgba(var(--case-color-rgb), 0.9);
|
||||
}
|
||||
|
||||
.transform-category-custom_spelling .transform-preview {
|
||||
color: rgba(var(--custom-spelling-color-rgb), 0.9);
|
||||
}
|
||||
|
||||
.transform-button:hover .transform-preview {
|
||||
background-color: rgba(0, 0, 0, 0.18);
|
||||
opacity: 1;
|
||||
@@ -2740,6 +2755,15 @@ button.endsequence-item:hover .endsequence-copy-affordance {
|
||||
box-shadow: 0 3px 10px rgba(var(--case-color-rgb), 0.2);
|
||||
}
|
||||
|
||||
.transform-category-custom_spelling {
|
||||
background: linear-gradient(to right, rgba(var(--custom-spelling-color-rgb), 0.05), var(--button-bg));
|
||||
}
|
||||
|
||||
.transform-category-custom_spelling:hover {
|
||||
background: linear-gradient(to right, rgba(var(--custom-spelling-color-rgb), 0.15), var(--button-hover-bg));
|
||||
box-shadow: 0 3px 10px rgba(var(--custom-spelling-color-rgb), 0.2);
|
||||
}
|
||||
|
||||
.transform-button:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -2834,6 +2858,11 @@ button.endsequence-item:hover .endsequence-copy-affordance {
|
||||
box-shadow: 0 3px 12px rgba(var(--case-color-rgb), 0.4);
|
||||
}
|
||||
|
||||
.transform-category-custom_spelling.active {
|
||||
background: linear-gradient(to right, var(--custom-spelling-color), var(--custom-spelling-active-end));
|
||||
box-shadow: 0 3px 12px rgba(var(--custom-spelling-color-rgb), 0.4);
|
||||
}
|
||||
|
||||
/* Add a subtle indicator for clickable buttons */
|
||||
.transform-button:after {
|
||||
content: '';
|
||||
@@ -5276,3 +5305,106 @@ html {
|
||||
margin-left: var(--spacing-md);
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom Spelling Alphabet tool */
|
||||
.spelling-alphabet-layout .sa-empty-state {
|
||||
padding: var(--spacing-md);
|
||||
border: 1px dashed var(--input-border);
|
||||
border-radius: 8px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.sa-saved-list {
|
||||
display: grid;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.sa-saved-card {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-md);
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: var(--spacing-md);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 8px;
|
||||
background: var(--secondary-bg);
|
||||
}
|
||||
|
||||
.sa-saved-card h4 {
|
||||
margin: 0 0 var(--spacing-sm);
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.sa-saved-preview,
|
||||
.sa-saved-card p {
|
||||
margin: 0 0 var(--spacing-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.sa-saved-card-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.sa-delete-btn {
|
||||
color: #ef5350;
|
||||
}
|
||||
|
||||
.sa-meta-grid {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.sa-generate-row {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.sa-hint {
|
||||
margin-top: var(--spacing-sm);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.sa-error {
|
||||
margin-bottom: var(--spacing-md);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ef5350;
|
||||
background: rgba(239, 83, 80, 0.12);
|
||||
color: #ffabab;
|
||||
}
|
||||
|
||||
.sa-progress {
|
||||
margin-bottom: var(--spacing-md);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.sa-letter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.sa-letter-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.sa-letter-label {
|
||||
font-weight: 700;
|
||||
color: var(--custom-spelling-color);
|
||||
}
|
||||
|
||||
.sa-letter-field input {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.sa-live-preview {
|
||||
margin-top: var(--spacing-md);
|
||||
padding: var(--spacing-md);
|
||||
border-radius: 8px;
|
||||
background: rgba(var(--custom-spelling-color-rgb), 0.08);
|
||||
border: 1px solid rgba(var(--custom-spelling-color-rgb), 0.25);
|
||||
}
|
||||
|
||||
@@ -404,6 +404,8 @@
|
||||
|
||||
<!-- Generated bundles -->
|
||||
<script src="js/bundles/transforms-bundle.js"></script>
|
||||
<script src="js/core/spellingAlphabetTransform.js"></script>
|
||||
<script src="js/core/customSpellingAlphabets.js"></script>
|
||||
|
||||
<!-- Glitch Tokens Data -->
|
||||
<script src="js/data/glitchTokens.js"></script>
|
||||
@@ -438,6 +440,7 @@
|
||||
<script src="js/tools/GibberishTool.js"></script>
|
||||
<script src="js/tools/MutationTool.js"></script>
|
||||
<script src="js/tools/PromptCraftTool.js"></script>
|
||||
<script src="js/tools/SpellingAlphabetTool.js"></script>
|
||||
<script src="js/tools/SplitterTool.js"></script>
|
||||
<script src="js/tools/TokenadeTool.js"></script>
|
||||
<script src="js/tools/TokenizerTool.js"></script>
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Persist user-created spelling alphabets in localStorage and register them on window.transforms.
|
||||
*/
|
||||
(function(global) {
|
||||
'use strict';
|
||||
|
||||
var STORAGE_KEY = 'customSpellingAlphabets';
|
||||
var TRANSFORM_KEY_PREFIX = 'custom_spelling_';
|
||||
|
||||
function createId() {
|
||||
if (global.crypto && typeof global.crypto.randomUUID === 'function') {
|
||||
return global.crypto.randomUUID();
|
||||
}
|
||||
return 'sa_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 10);
|
||||
}
|
||||
|
||||
function loadAll() {
|
||||
try {
|
||||
var raw = global.localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
var parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) {
|
||||
return [];
|
||||
}
|
||||
return parsed.filter(function(entry) {
|
||||
return entry && entry.id && entry.name && entry.alphabet;
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('Failed to load custom spelling alphabets:', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function saveAll(entries) {
|
||||
global.localStorage.setItem(STORAGE_KEY, JSON.stringify(entries));
|
||||
}
|
||||
|
||||
function transformKeyForId(id) {
|
||||
return TRANSFORM_KEY_PREFIX + id.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
}
|
||||
|
||||
function removeRegisteredCustomTransforms() {
|
||||
if (!global.transforms) {
|
||||
return;
|
||||
}
|
||||
Object.keys(global.transforms).forEach(function(key) {
|
||||
if (key.indexOf(TRANSFORM_KEY_PREFIX) === 0) {
|
||||
delete global.transforms[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function registerCustomTransforms() {
|
||||
if (!global.transforms) {
|
||||
global.transforms = {};
|
||||
}
|
||||
if (!global.SpellingAlphabetTransform) {
|
||||
console.warn('SpellingAlphabetTransform module missing; custom alphabets not registered.');
|
||||
return;
|
||||
}
|
||||
|
||||
removeRegisteredCustomTransforms();
|
||||
|
||||
loadAll().forEach(function(entry) {
|
||||
var key = transformKeyForId(entry.id);
|
||||
global.transforms[key] = global.SpellingAlphabetTransform.create({
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
category: 'custom_spelling',
|
||||
alphabet: entry.alphabet,
|
||||
priority: 150
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function syncCustomSpellingAlphabets() {
|
||||
registerCustomTransforms();
|
||||
}
|
||||
|
||||
function saveMapping(mapping) {
|
||||
var entries = loadAll();
|
||||
var now = Date.now();
|
||||
var normalizedAlphabet = global.SpellingAlphabetTransform
|
||||
? global.SpellingAlphabetTransform.normalizeAlphabet(mapping.alphabet)
|
||||
: mapping.alphabet;
|
||||
var payload = {
|
||||
id: mapping.id || createId(),
|
||||
name: String(mapping.name || '').trim(),
|
||||
category: String(mapping.category || '').trim(),
|
||||
alphabet: normalizedAlphabet,
|
||||
createdAt: mapping.createdAt || now,
|
||||
updatedAt: now
|
||||
};
|
||||
|
||||
var index = entries.findIndex(function(entry) {
|
||||
return entry.id === payload.id;
|
||||
});
|
||||
if (index >= 0) {
|
||||
payload.createdAt = entries[index].createdAt || payload.createdAt;
|
||||
entries[index] = payload;
|
||||
} else {
|
||||
entries.push(payload);
|
||||
}
|
||||
|
||||
saveAll(entries);
|
||||
syncCustomSpellingAlphabets();
|
||||
return payload;
|
||||
}
|
||||
|
||||
function deleteMapping(id) {
|
||||
var entries = loadAll().filter(function(entry) {
|
||||
return entry.id !== id;
|
||||
});
|
||||
saveAll(entries);
|
||||
syncCustomSpellingAlphabets();
|
||||
}
|
||||
|
||||
function getById(id) {
|
||||
return loadAll().find(function(entry) {
|
||||
return entry.id === id;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
global.CustomSpellingAlphabets = {
|
||||
STORAGE_KEY: STORAGE_KEY,
|
||||
loadAll: loadAll,
|
||||
saveAll: saveAll,
|
||||
saveMapping: saveMapping,
|
||||
deleteMapping: deleteMapping,
|
||||
getById: getById,
|
||||
syncCustomSpellingAlphabets: syncCustomSpellingAlphabets
|
||||
};
|
||||
|
||||
global.syncCustomSpellingAlphabets = syncCustomSpellingAlphabets;
|
||||
|
||||
syncCustomSpellingAlphabets();
|
||||
})(typeof window !== 'undefined' ? window : globalThis);
|
||||
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* Factory for ICAO-style spelling alphabet transforms (built-in or user-saved).
|
||||
*/
|
||||
(function(global) {
|
||||
'use strict';
|
||||
|
||||
var LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
||||
var SAMPLE_TEXT = 'hello world';
|
||||
|
||||
function emptyAlphabet() {
|
||||
var alphabet = {};
|
||||
LETTERS.forEach(function(letter) {
|
||||
alphabet[letter] = '';
|
||||
});
|
||||
return alphabet;
|
||||
}
|
||||
|
||||
function normalizeWord(word) {
|
||||
return word ? String(word).toUpperCase().replace(/[^A-Z0-9]/g, '') : '';
|
||||
}
|
||||
|
||||
function countFilled(alphabet) {
|
||||
return LETTERS.filter(function(letter) {
|
||||
return !!alphabet[letter];
|
||||
}).length;
|
||||
}
|
||||
|
||||
function mergeAlphabet(target, source) {
|
||||
LETTERS.forEach(function(letter) {
|
||||
var word = normalizeWord(source && source[letter]);
|
||||
if (word && !target[letter]) {
|
||||
target[letter] = word;
|
||||
}
|
||||
});
|
||||
return target;
|
||||
}
|
||||
|
||||
function tryParseJsonObject(text) {
|
||||
var start = text.indexOf('{');
|
||||
var end = text.lastIndexOf('}');
|
||||
if (start === -1 || end <= start) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var candidates = [
|
||||
text.slice(start, end + 1),
|
||||
text.slice(start, end + 1).replace(/,\s*([}\]])/g, '$1')
|
||||
];
|
||||
|
||||
for (var i = 0; i < candidates.length; i++) {
|
||||
try {
|
||||
var parsed = JSON.parse(candidates[i]);
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
} catch (e) {
|
||||
// try next candidate
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractPairsFromText(text) {
|
||||
var alphabet = emptyAlphabet();
|
||||
var patterns = [
|
||||
/"([A-Z])"\s*:\s*"([^"]+)"/g,
|
||||
/'([A-Z])'\s*:\s*'([^']+)'/g,
|
||||
/(?:^|[\n,{])\s*([A-Z])\s*[:=\-–]\s*"?([A-Za-z0-9]+)"?/g,
|
||||
/(?:^|\n)\s*([A-Z])\s+[—–-]\s+([A-Za-z0-9]+)/g
|
||||
];
|
||||
|
||||
patterns.forEach(function(pattern) {
|
||||
var match;
|
||||
while ((match = pattern.exec(text)) !== null) {
|
||||
var letter = match[1].toUpperCase();
|
||||
var word = normalizeWord(match[2]);
|
||||
if (LETTERS.indexOf(letter) !== -1 && word && !alphabet[letter]) {
|
||||
alphabet[letter] = word;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return alphabet;
|
||||
}
|
||||
|
||||
function parseAlphabetResponse(rawText) {
|
||||
var text = String(rawText || '').trim();
|
||||
if (!text) {
|
||||
throw new Error('Empty response from model.');
|
||||
}
|
||||
|
||||
var fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
||||
if (fenced) {
|
||||
text = fenced[1].trim();
|
||||
}
|
||||
|
||||
var merged = emptyAlphabet();
|
||||
var jsonObject = tryParseJsonObject(text);
|
||||
if (jsonObject) {
|
||||
mergeAlphabet(merged, normalizeAlphabet(jsonObject));
|
||||
}
|
||||
mergeAlphabet(merged, extractPairsFromText(text));
|
||||
|
||||
var filledCount = countFilled(merged);
|
||||
if (filledCount === 0) {
|
||||
throw new Error('Model did not return JSON. Try again or fill letters manually.');
|
||||
}
|
||||
|
||||
return {
|
||||
alphabet: merged,
|
||||
filledCount: filledCount,
|
||||
partial: filledCount < LETTERS.length
|
||||
};
|
||||
}
|
||||
|
||||
function extractMessageContent(message) {
|
||||
if (!message) {
|
||||
return '';
|
||||
}
|
||||
var content = message.content;
|
||||
if (typeof content === 'string') {
|
||||
return content;
|
||||
}
|
||||
if (Array.isArray(content)) {
|
||||
return content.map(function(part) {
|
||||
if (typeof part === 'string') {
|
||||
return part;
|
||||
}
|
||||
if (part && typeof part.text === 'string') {
|
||||
return part.text;
|
||||
}
|
||||
return '';
|
||||
}).join('\n');
|
||||
}
|
||||
return content == null ? '' : String(content);
|
||||
}
|
||||
|
||||
function buildAlphabetPrompts(category) {
|
||||
var theme = String(category || '').trim() || 'general';
|
||||
|
||||
var system = [
|
||||
'You create NATO/ICAO-style spelling alphabets.',
|
||||
'',
|
||||
'Task: given a theme, produce one codeword per letter A–Z for spelling text aloud.',
|
||||
'',
|
||||
'Hard rules:',
|
||||
'- Output ONLY valid JSON — one object, no markdown, no code fences, no explanation.',
|
||||
'- Exactly 26 keys: "A" through "Z" (uppercase).',
|
||||
'- Each value is ONE uppercase English word with no spaces.',
|
||||
'- Every word MUST start with its key letter (A→ANCHOR, not A→HARBOR).',
|
||||
'- All 26 words must be unique.',
|
||||
'- Words must fit the theme and be easy to say aloud (prefer 1–3 syllables).',
|
||||
'- Avoid obscure jargon unless the theme requires it.',
|
||||
'',
|
||||
'Letter tips:',
|
||||
'- Q, X, and Z are hard: pick the best theme word that starts with that letter.',
|
||||
'- For X, XRAY or a theme word starting with X is fine.',
|
||||
'',
|
||||
'Example theme "nautical" (format only — use different words for other themes):',
|
||||
'{"A":"ANCHOR","B":"BUOY","C":"CORAL","D":"DOCK","E":"EDDY","F":"FOG","G":"GALLEY","H":"HARBOR","I":"ISLAND","J":"JIB","K":"KNOT","L":"LAGOON","M":"MAST","N":"NAVY","O":"OCEAN","P":"PORT","Q":"QUAY","R":"REEF","S":"SAIL","T":"TIDE","U":"UNDERTOW","V":"VOYAGE","W":"WHARF","X":"XRAY","Y":"YACHT","Z":"ZEPHYR"}'
|
||||
].join('\n');
|
||||
|
||||
var user = [
|
||||
'Theme: "' + theme + '"',
|
||||
'',
|
||||
'Return the complete A–Z JSON object now.',
|
||||
'Double-check: 26 keys, each word starts with its letter, all unique, theme-appropriate.'
|
||||
].join('\n');
|
||||
|
||||
return { system: system, user: user };
|
||||
}
|
||||
|
||||
function buildAlphabetPrompt(category) {
|
||||
return buildAlphabetPrompts(category).user;
|
||||
}
|
||||
|
||||
function normalizeAlphabet(alphabet) {
|
||||
var normalized = emptyAlphabet();
|
||||
if (!alphabet || typeof alphabet !== 'object') {
|
||||
return normalized;
|
||||
}
|
||||
LETTERS.forEach(function(letter) {
|
||||
var word = alphabet[letter] || alphabet[letter.toLowerCase()];
|
||||
normalized[letter] = normalizeWord(word);
|
||||
});
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function createSpellingAlphabetTransform(config) {
|
||||
var alphabet = normalizeAlphabet(config.alphabet);
|
||||
var name = config.name || 'Spelling Alphabet';
|
||||
var category = config.category || 'custom_spelling';
|
||||
var priority = typeof config.priority === 'number' ? config.priority : 200;
|
||||
var customId = config.id || null;
|
||||
|
||||
var transform = {
|
||||
name: name,
|
||||
priority: priority,
|
||||
category: category,
|
||||
alphabet: alphabet,
|
||||
customSpellingId: customId,
|
||||
func: function(text) {
|
||||
var cleaned = text.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (cleaned.length === 0) {
|
||||
return text;
|
||||
}
|
||||
|
||||
var result = '';
|
||||
for (var i = 0; i < cleaned.length; i++) {
|
||||
var char = cleaned[i];
|
||||
if (this.alphabet[char]) {
|
||||
result += this.alphabet[char] + ' ';
|
||||
} else {
|
||||
result += char + ' ';
|
||||
}
|
||||
}
|
||||
return result.trim();
|
||||
},
|
||||
reverse: function(text) {
|
||||
var reverseMap = {};
|
||||
for (var letter in this.alphabet) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.alphabet, letter)) {
|
||||
reverseMap[this.alphabet[letter].toUpperCase()] = letter;
|
||||
}
|
||||
}
|
||||
|
||||
var words = text.toUpperCase().split(/\s+/);
|
||||
var decoded = '';
|
||||
for (var j = 0; j < words.length; j++) {
|
||||
var word = words[j];
|
||||
if (reverseMap[word]) {
|
||||
decoded += reverseMap[word];
|
||||
} else if (word.length === 1 && /[A-Z]/.test(word)) {
|
||||
decoded += word;
|
||||
}
|
||||
}
|
||||
return decoded;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) {
|
||||
return '[spelling]';
|
||||
}
|
||||
return this.func(text.slice(0, 3));
|
||||
},
|
||||
detector: function(text) {
|
||||
var words = Object.values(this.alphabet).filter(Boolean);
|
||||
if (words.length < 2) {
|
||||
return false;
|
||||
}
|
||||
var upper = text.toUpperCase();
|
||||
var matches = words.filter(function(word) {
|
||||
return upper.includes(word);
|
||||
});
|
||||
return matches.length >= 2;
|
||||
}
|
||||
};
|
||||
|
||||
return transform;
|
||||
}
|
||||
|
||||
global.SpellingAlphabetTransform = {
|
||||
LETTERS: LETTERS,
|
||||
SAMPLE_TEXT: SAMPLE_TEXT,
|
||||
emptyAlphabet: emptyAlphabet,
|
||||
normalizeAlphabet: normalizeAlphabet,
|
||||
parseAlphabetResponse: parseAlphabetResponse,
|
||||
extractMessageContent: extractMessageContent,
|
||||
buildAlphabetPrompts: buildAlphabetPrompts,
|
||||
buildAlphabetPrompt: buildAlphabetPrompt,
|
||||
create: createSpellingAlphabetTransform
|
||||
};
|
||||
})(typeof window !== 'undefined' ? window : globalThis);
|
||||
@@ -196,6 +196,9 @@ if (typeof MutationTool !== 'undefined') {
|
||||
if (typeof PromptCraftTool !== 'undefined') {
|
||||
window.toolRegistry.register(new PromptCraftTool());
|
||||
}
|
||||
if (typeof SpellingAlphabetTool !== 'undefined') {
|
||||
window.toolRegistry.register(new SpellingAlphabetTool());
|
||||
}
|
||||
if (typeof SplitterTool !== 'undefined') {
|
||||
window.toolRegistry.register(new SplitterTool());
|
||||
}
|
||||
|
||||
@@ -46,5 +46,6 @@ window.OPENROUTER_MODELS = [
|
||||
{ id: 'nousresearch/hermes-3-llama-3.1-405b', name: 'Hermes 3 405B', provider: 'Nous' },
|
||||
{ id: 'perplexity/sonar-deep-research', name: 'Sonar Deep Research', provider: 'Perplexity' },
|
||||
{ id: 'perplexity/sonar-pro', name: 'Sonar Pro', provider: 'Perplexity' },
|
||||
{ id: 'openrouter/free', name: 'Free (auto-pick)', provider: 'OpenRouter' },
|
||||
{ id: 'openrouter/auto', name: 'Auto (best for price)', provider: 'OpenRouter' }
|
||||
];
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
/**
|
||||
* Spelling Alphabet Tool — create custom ICAO-style alphabets (OpenRouter or manual).
|
||||
*/
|
||||
class SpellingAlphabetTool extends Tool {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'spellingalphabet',
|
||||
name: 'Spelling',
|
||||
icon: 'fa-spell-check',
|
||||
title: 'Custom spelling alphabets',
|
||||
order: 8
|
||||
});
|
||||
}
|
||||
|
||||
getVueData() {
|
||||
var freeModels = [
|
||||
{ id: 'openrouter/free', name: 'Free (auto-pick)', provider: 'OpenRouter' },
|
||||
{ id: 'google/gemma-3-27b-it:free', name: 'Gemma 3 27B (free)', provider: 'Google' },
|
||||
{ id: 'google/gemma-3-12b-it:free', name: 'Gemma 3 12B (free)', provider: 'Google' },
|
||||
{ id: 'google/gemma-3-4b-it:free', name: 'Gemma 3 4B (free)', provider: 'Google' },
|
||||
{ id: 'meta-llama/llama-3.3-70b-instruct:free', name: 'Llama 3.3 70B (free)', provider: 'Meta' }
|
||||
];
|
||||
var allModels = (typeof window !== 'undefined' && window.OPENROUTER_MODELS && window.OPENROUTER_MODELS.length)
|
||||
? window.OPENROUTER_MODELS
|
||||
: [{ id: 'openrouter/auto', name: 'Auto (best for price)', provider: 'OpenRouter' }];
|
||||
var seen = {};
|
||||
var saModels = [];
|
||||
freeModels.forEach(function(model) {
|
||||
if (!seen[model.id]) {
|
||||
seen[model.id] = true;
|
||||
saModels.push(model);
|
||||
}
|
||||
});
|
||||
allModels.forEach(function(model) {
|
||||
if (!seen[model.id]) {
|
||||
seen[model.id] = true;
|
||||
saModels.push(model);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
saView: 'list',
|
||||
saAlphabets: [],
|
||||
saEditingId: null,
|
||||
saName: '',
|
||||
saCategory: '',
|
||||
saAlphabet: SpellingAlphabetTransform.emptyAlphabet(),
|
||||
saLoading: false,
|
||||
saError: '',
|
||||
saModel: localStorage.getItem('sa-model') || 'openrouter/free',
|
||||
saLetters: SpellingAlphabetTransform.LETTERS,
|
||||
saModels: saModels
|
||||
};
|
||||
}
|
||||
|
||||
getVueMethods() {
|
||||
return {
|
||||
saGetApiKey: function() {
|
||||
var key = localStorage.getItem('openrouter-api-key') ||
|
||||
localStorage.getItem('plinyos-api-key') ||
|
||||
localStorage.getItem('openrouter_api_key') || '';
|
||||
if (!key && this.openrouterApiKey) {
|
||||
key = this.openrouterApiKey;
|
||||
localStorage.setItem('openrouter-api-key', key.trim());
|
||||
}
|
||||
return key.trim();
|
||||
},
|
||||
saHasApiKey: function() {
|
||||
return !!this.saGetApiKey();
|
||||
},
|
||||
saLoadAlphabets: function() {
|
||||
this.saAlphabets = CustomSpellingAlphabets.loadAll();
|
||||
},
|
||||
saStartNew: function() {
|
||||
this.saView = 'edit';
|
||||
this.saEditingId = null;
|
||||
this.saName = '';
|
||||
this.saCategory = '';
|
||||
this.saAlphabet = SpellingAlphabetTransform.emptyAlphabet();
|
||||
this.saError = '';
|
||||
},
|
||||
saEditAlphabet: function(entry) {
|
||||
this.saView = 'edit';
|
||||
this.saEditingId = entry.id;
|
||||
this.saName = entry.name;
|
||||
this.saCategory = entry.category || '';
|
||||
this.saAlphabet = Object.assign(
|
||||
SpellingAlphabetTransform.emptyAlphabet(),
|
||||
SpellingAlphabetTransform.normalizeAlphabet(entry.alphabet)
|
||||
);
|
||||
this.saError = '';
|
||||
},
|
||||
saCancelEdit: function() {
|
||||
this.saView = 'list';
|
||||
this.saEditingId = null;
|
||||
this.saError = '';
|
||||
},
|
||||
saDeleteAlphabet: function(entry) {
|
||||
if (!entry || !entry.id) {
|
||||
return;
|
||||
}
|
||||
if (!window.confirm('Delete "' + entry.name + '"? This removes it from the Transforms page too.')) {
|
||||
return;
|
||||
}
|
||||
CustomSpellingAlphabets.deleteMapping(entry.id);
|
||||
this.saLoadAlphabets();
|
||||
this.saRefreshTransforms();
|
||||
if (typeof this.showNotification === 'function') {
|
||||
this.showNotification('Spelling alphabet deleted', 'success', 'fas fa-trash');
|
||||
}
|
||||
},
|
||||
saSuggestName: function() {
|
||||
var category = String(this.saCategory || '').trim();
|
||||
if (!category) {
|
||||
return 'Custom Spelling Alphabet';
|
||||
}
|
||||
return category.replace(/\b\w/g, function(c) { return c.toUpperCase(); }) + ' Spelling Alphabet';
|
||||
},
|
||||
saParseAlphabetJson: function(rawText) {
|
||||
return SpellingAlphabetTransform.parseAlphabetResponse(rawText);
|
||||
},
|
||||
saBuildGenerationRequest: function(category) {
|
||||
var prompts = SpellingAlphabetTransform.buildAlphabetPrompts(category);
|
||||
var body = {
|
||||
model: this.saModel,
|
||||
temperature: 0.2,
|
||||
max_tokens: 1200,
|
||||
messages: [
|
||||
{ role: 'system', content: prompts.system },
|
||||
{ role: 'user', content: prompts.user }
|
||||
]
|
||||
};
|
||||
|
||||
if (this.saModel !== 'openrouter/free') {
|
||||
body.response_format = { type: 'json_object' };
|
||||
}
|
||||
|
||||
return body;
|
||||
},
|
||||
saGenerateAlphabet: function() {
|
||||
var category = String(this.saCategory || '').trim();
|
||||
if (!category) {
|
||||
this.saError = 'Enter a category or theme first (e.g. nautical, cooking, astronomy).';
|
||||
return;
|
||||
}
|
||||
|
||||
var apiKey = this.saGetApiKey();
|
||||
if (!apiKey) {
|
||||
this.saError = 'No OpenRouter API key. Add one in Advanced Settings, or fill in letters manually below.';
|
||||
return;
|
||||
}
|
||||
|
||||
this.saLoading = true;
|
||||
this.saError = '';
|
||||
|
||||
var self = this;
|
||||
var requestBody = this.saBuildGenerationRequest(category);
|
||||
|
||||
fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + apiKey,
|
||||
'Content-Type': 'application/json',
|
||||
'HTTP-Referer': window.location.origin,
|
||||
'X-Title': 'P4RS3LT0NGV3 Spelling Alphabet'
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
})
|
||||
.then(function(response) {
|
||||
if (response.status === 401) {
|
||||
throw new Error('Invalid API key. Check your OpenRouter key in Advanced Settings.');
|
||||
}
|
||||
if (response.status === 402) {
|
||||
throw new Error('Insufficient credits on your OpenRouter account.');
|
||||
}
|
||||
if (response.status === 400 && requestBody.response_format) {
|
||||
delete requestBody.response_format;
|
||||
return fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + apiKey,
|
||||
'Content-Type': 'application/json',
|
||||
'HTTP-Referer': window.location.origin,
|
||||
'X-Title': 'P4RS3LT0NGV3 Spelling Alphabet'
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error('OpenRouter request failed (HTTP ' + response.status + ').');
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.then(function(response) {
|
||||
if (!response.ok) {
|
||||
throw new Error('OpenRouter request failed (HTTP ' + response.status + ').');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
var message = data &&
|
||||
data.choices &&
|
||||
data.choices[0] &&
|
||||
data.choices[0].message;
|
||||
var content = SpellingAlphabetTransform.extractMessageContent(message);
|
||||
var parsed = self.saParseAlphabetJson(content);
|
||||
self.saAlphabet = parsed.alphabet;
|
||||
if (!self.saName.trim()) {
|
||||
self.saName = self.saSuggestName();
|
||||
}
|
||||
if (parsed.partial) {
|
||||
self.saError = 'Parsed ' + parsed.filledCount + '/26 letters. Fill in the rest below.';
|
||||
} else {
|
||||
self.saError = '';
|
||||
}
|
||||
if (typeof self.showNotification === 'function') {
|
||||
var note = parsed.partial
|
||||
? 'Partial alphabet generated — complete missing letters before saving'
|
||||
: 'Alphabet generated — review and edit letters before saving';
|
||||
self.showNotification(note, parsed.partial ? 'warning' : 'success', 'fas fa-wand-magic-sparkles');
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
self.saError = err.message || 'Failed to generate alphabet.';
|
||||
})
|
||||
.finally(function() {
|
||||
self.saLoading = false;
|
||||
});
|
||||
},
|
||||
saValidateAlphabet: function() {
|
||||
var name = String(this.saName || '').trim();
|
||||
if (!name) {
|
||||
return 'Enter a name for this spelling alphabet.';
|
||||
}
|
||||
|
||||
var missing = this.saLetters.filter(function(letter) {
|
||||
return !String(this.saAlphabet[letter] || '').trim();
|
||||
}, this);
|
||||
if (missing.length) {
|
||||
return 'Fill in all 26 letters. Missing: ' + missing.join(', ');
|
||||
}
|
||||
|
||||
var seen = {};
|
||||
for (var i = 0; i < this.saLetters.length; i++) {
|
||||
var letter = this.saLetters[i];
|
||||
var word = String(this.saAlphabet[letter] || '').toUpperCase();
|
||||
if (seen[word]) {
|
||||
return 'Duplicate word "' + word + '" for letters ' + seen[word] + ' and ' + letter + '.';
|
||||
}
|
||||
seen[word] = letter;
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
saSaveAlphabet: function() {
|
||||
var validationError = this.saValidateAlphabet();
|
||||
if (validationError) {
|
||||
this.saError = validationError;
|
||||
return;
|
||||
}
|
||||
|
||||
var saved = CustomSpellingAlphabets.saveMapping({
|
||||
id: this.saEditingId,
|
||||
name: this.saName.trim(),
|
||||
category: this.saCategory.trim(),
|
||||
alphabet: this.saAlphabet
|
||||
});
|
||||
|
||||
localStorage.setItem('sa-model', this.saModel);
|
||||
this.saLoadAlphabets();
|
||||
this.saRefreshTransforms();
|
||||
this.saView = 'list';
|
||||
this.saEditingId = saved.id;
|
||||
this.saError = '';
|
||||
|
||||
if (typeof this.showNotification === 'function') {
|
||||
this.showNotification('Saved — find it on the Transforms page under custom_spelling', 'success', 'fas fa-check');
|
||||
}
|
||||
},
|
||||
saRefreshTransforms: function() {
|
||||
if (typeof this.refreshCustomSpellingTransforms === 'function') {
|
||||
this.refreshCustomSpellingTransforms();
|
||||
}
|
||||
},
|
||||
saPreviewSample: function() {
|
||||
var sample = SpellingAlphabetTransform.SAMPLE_TEXT;
|
||||
if (!window.SpellingAlphabetTransform) {
|
||||
return '';
|
||||
}
|
||||
var temp = SpellingAlphabetTransform.create({
|
||||
name: this.saName || 'Preview',
|
||||
alphabet: this.saAlphabet
|
||||
});
|
||||
return temp.func(sample);
|
||||
},
|
||||
saSampleForEntry: function(entry) {
|
||||
if (!entry || !window.SpellingAlphabetTransform) {
|
||||
return '';
|
||||
}
|
||||
var temp = SpellingAlphabetTransform.create({
|
||||
name: entry.name,
|
||||
alphabet: entry.alphabet
|
||||
});
|
||||
return temp.func(SpellingAlphabetTransform.SAMPLE_TEXT);
|
||||
},
|
||||
saFilledLetterCount: function() {
|
||||
return this.saLetters.filter(function(letter) {
|
||||
return String(this.saAlphabet[letter] || '').trim().length > 0;
|
||||
}, this).length;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getVueLifecycle() {
|
||||
return {
|
||||
mounted: function() {
|
||||
this.saLoadAlphabets();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
onActivate(vueInstance) {
|
||||
vueInstance.saLoadAlphabets();
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = SpellingAlphabetTool;
|
||||
} else {
|
||||
window.SpellingAlphabetTool = SpellingAlphabetTool;
|
||||
}
|
||||
+67
-21
@@ -13,27 +13,7 @@ class TransformTool extends Tool {
|
||||
}
|
||||
|
||||
getVueData() {
|
||||
const transforms = (window.transforms && Object.keys(window.transforms).length > 0)
|
||||
? 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',
|
||||
configurableOptions: transform.configurableOptions || [],
|
||||
hasConfigurableOptions: Array.isArray(transform.configurableOptions) && transform.configurableOptions.length > 0,
|
||||
inputKind: transform.inputKind === 'text' ? 'text' : 'textarea'
|
||||
}))
|
||||
: [];
|
||||
const transforms = this.buildTransformsFromWindow();
|
||||
|
||||
const categorySet = new Set();
|
||||
transforms.forEach(transform => {
|
||||
@@ -77,6 +57,55 @@ class TransformTool extends Tool {
|
||||
transformOptionsDraft: {}
|
||||
};
|
||||
}
|
||||
|
||||
buildTransformsFromWindow() {
|
||||
if (typeof window !== 'undefined' && typeof window.syncCustomSpellingAlphabets === 'function') {
|
||||
window.syncCustomSpellingAlphabets();
|
||||
}
|
||||
|
||||
if (!window.transforms || Object.keys(window.transforms).length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.entries(window.transforms)
|
||||
.filter(([key, transform]) => {
|
||||
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',
|
||||
configurableOptions: transform.configurableOptions || [],
|
||||
hasConfigurableOptions: Array.isArray(transform.configurableOptions) && transform.configurableOptions.length > 0,
|
||||
inputKind: transform.inputKind === 'text' ? 'text' : 'textarea'
|
||||
}));
|
||||
}
|
||||
|
||||
rebuildTransformCategories(transforms) {
|
||||
const categorySet = new Set();
|
||||
transforms.forEach(transform => {
|
||||
if (transform.category) {
|
||||
categorySet.add(transform.category);
|
||||
}
|
||||
});
|
||||
|
||||
const allCategories = Array.from(categorySet);
|
||||
const categoriesWithoutRandomizer = allCategories.filter(c => c !== 'randomizer');
|
||||
const legendCategories = [...categoriesWithoutRandomizer.sort((a, b) => a.localeCompare(b)), 'randomizer'];
|
||||
|
||||
const savedOrder = this.loadCategoryOrder();
|
||||
const sectionCategories = savedOrder && savedOrder.length > 0
|
||||
? this.mergeCategoryOrder(allCategories, savedOrder)
|
||||
: [...legendCategories];
|
||||
|
||||
return { legendCategories, sectionCategories };
|
||||
}
|
||||
|
||||
loadTransformOptionPrefs() {
|
||||
try {
|
||||
@@ -609,6 +638,23 @@ class TransformTool extends Tool {
|
||||
this.transformOutput = window.EmojiUtils.joinEmojis(transformedSegments);
|
||||
}
|
||||
},
|
||||
refreshCustomSpellingTransforms: function() {
|
||||
const transformTool = window.toolRegistry && window.toolRegistry.get('transforms');
|
||||
if (!transformTool || typeof transformTool.buildTransformsFromWindow !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.transforms = transformTool.buildTransformsFromWindow();
|
||||
const categories = transformTool.rebuildTransformCategories(this.transforms);
|
||||
this.legendCategories = categories.legendCategories;
|
||||
this.categories = categories.sectionCategories;
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (typeof this.initializeCategoryNavigation === 'function') {
|
||||
this.initializeCategoryNavigation();
|
||||
}
|
||||
});
|
||||
},
|
||||
initializeCategoryNavigation: function() {
|
||||
this.$nextTick(() => {
|
||||
const legendItems = document.querySelectorAll('.transform-category-legend .legend-item');
|
||||
|
||||
Generated
+6
-6
@@ -176,9 +176,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"version": "1.1.15",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
|
||||
"integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -452,9 +452,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
// N7AX cipher — Polybius + columnar transposition (ADFGX-style with N7AX coordinates)
|
||||
import BaseTransformer from '../BaseTransformer.js';
|
||||
|
||||
export default new BaseTransformer({
|
||||
name: 'N7AX Cipher',
|
||||
priority: 60,
|
||||
category: 'cipher',
|
||||
key: 'N7AX',
|
||||
configurableOptions: [
|
||||
{
|
||||
id: 'key',
|
||||
label: 'Transposition keyword',
|
||||
type: 'text',
|
||||
default: 'N7AX'
|
||||
}
|
||||
],
|
||||
_transKey: function(options) {
|
||||
const k = options && options.key !== undefined && options.key !== null
|
||||
? String(options.key)
|
||||
: null;
|
||||
return (k || this.key || 'N7AX').toUpperCase().replace(/[^A-Z0-9]/g, '');
|
||||
},
|
||||
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']
|
||||
],
|
||||
coords: ['N', '7', 'A', 'X', '5'],
|
||||
_toCoordText: function(text) {
|
||||
let result = '';
|
||||
for (const char of text) {
|
||||
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 += this.coords[row] + this.coords[col];
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
_transposeEncode: function(text, transKey) {
|
||||
const keyLength = transKey.length;
|
||||
const numCols = keyLength;
|
||||
const numRows = Math.ceil(text.length / numCols);
|
||||
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 < text.length ? text[textIdx++] : '';
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
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;
|
||||
},
|
||||
_transposeDecode: function(text, transKey) {
|
||||
const keyLength = transKey.length;
|
||||
const numCols = keyLength;
|
||||
const numRows = Math.ceil(text.length / numCols);
|
||||
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;
|
||||
});
|
||||
|
||||
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;
|
||||
const colLength = Math.ceil((text.length - col) / numCols);
|
||||
for (let row = 0; row < colLength && textIdx < text.length; row++) {
|
||||
grid[row][col] = text[textIdx++];
|
||||
}
|
||||
}
|
||||
|
||||
let result = '';
|
||||
for (let row = 0; row < numRows; row++) {
|
||||
for (let col = 0; col < numCols; col++) {
|
||||
if (grid[row] && grid[row][col]) {
|
||||
result += grid[row][col];
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
_fromCoordText: function(text) {
|
||||
let result = '';
|
||||
for (let i = 0; i < text.length; i += 2) {
|
||||
if (i + 1 >= text.length) {
|
||||
break;
|
||||
}
|
||||
const row = this.coords.indexOf(text[i]);
|
||||
const col = this.coords.indexOf(text[i + 1]);
|
||||
if (row >= 0 && row < 5 && col >= 0 && col < 5) {
|
||||
result += this.square[row][col];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
func: function(text, options) {
|
||||
options = options || {};
|
||||
const transKey = this._transKey(options);
|
||||
if (transKey.length === 0) {
|
||||
return text;
|
||||
}
|
||||
|
||||
const cleaned = text.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
if (cleaned.length === 0) {
|
||||
return text;
|
||||
}
|
||||
|
||||
const coordText = this._toCoordText(cleaned);
|
||||
return this._transposeEncode(coordText, transKey);
|
||||
},
|
||||
reverse: function(text, options) {
|
||||
options = options || {};
|
||||
const transKey = this._transKey(options);
|
||||
if (transKey.length === 0) {
|
||||
return text;
|
||||
}
|
||||
|
||||
const cleaned = text.toUpperCase().replace(/[^N7AX5]/g, '');
|
||||
if (cleaned.length === 0 || cleaned.length % 2 !== 0) {
|
||||
return text;
|
||||
}
|
||||
|
||||
const coordText = this._transposeDecode(cleaned, transKey);
|
||||
return this._fromCoordText(coordText);
|
||||
},
|
||||
preview: function(text, options) {
|
||||
if (!text) {
|
||||
return '[n7ax]';
|
||||
}
|
||||
const result = this.func(text.slice(0, 5), options);
|
||||
return result.substring(0, 12) + (result.length > 12 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
const cleaned = text.replace(/[\s]/g, '').toUpperCase();
|
||||
if (cleaned.length < 10) {
|
||||
return false;
|
||||
}
|
||||
if (!/^[N7AX5]+$/.test(cleaned)) {
|
||||
return false;
|
||||
}
|
||||
return cleaned.length % 2 === 0;
|
||||
}
|
||||
});
|
||||
@@ -1,97 +1,137 @@
|
||||
// base122 encoding (more efficient than Base64)
|
||||
// Base122 encoding (Kevin Albs) — UTF-8 binary-to-text, ~14% smaller 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;
|
||||
}
|
||||
});
|
||||
export default (function() {
|
||||
const kIllegals = [
|
||||
0, 10, 13, 34, 38, 92
|
||||
];
|
||||
const kShortened = 0b111;
|
||||
|
||||
function encodeBase122Bytes(bytes) {
|
||||
let curIndex = 0;
|
||||
let curBit = 0;
|
||||
const outData = [];
|
||||
|
||||
function get7() {
|
||||
if (curIndex >= bytes.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const firstByte = bytes[curIndex];
|
||||
let firstPart = ((0b11111110 >>> curBit) & firstByte) << curBit;
|
||||
firstPart >>= 1;
|
||||
curBit += 7;
|
||||
|
||||
if (curBit < 8) {
|
||||
return firstPart;
|
||||
}
|
||||
|
||||
curBit -= 8;
|
||||
curIndex++;
|
||||
|
||||
if (curIndex >= bytes.length) {
|
||||
return firstPart;
|
||||
}
|
||||
|
||||
const secondByte = bytes[curIndex];
|
||||
let secondPart = ((0xFF00 >>> curBit) & secondByte) & 0xFF;
|
||||
secondPart >>= 8 - curBit;
|
||||
return firstPart | secondPart;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const bits = get7();
|
||||
if (bits === false) {
|
||||
break;
|
||||
}
|
||||
|
||||
const illegalIndex = kIllegals.indexOf(bits);
|
||||
if (illegalIndex !== -1) {
|
||||
let nextBits = get7();
|
||||
let b1 = 0b11000010;
|
||||
let b2 = 0b10000000;
|
||||
|
||||
if (nextBits === false) {
|
||||
b1 |= (kShortened & 0b111) << 2;
|
||||
nextBits = bits;
|
||||
} else {
|
||||
b1 |= (illegalIndex & 0b111) << 2;
|
||||
}
|
||||
|
||||
const firstBit = (nextBits & 0b01000000) > 0 ? 1 : 0;
|
||||
b1 |= firstBit;
|
||||
b2 |= nextBits & 0b00111111;
|
||||
outData.push(b1, b2);
|
||||
} else {
|
||||
outData.push(bits);
|
||||
}
|
||||
}
|
||||
|
||||
return new TextDecoder('utf-8').decode(new Uint8Array(outData));
|
||||
}
|
||||
|
||||
function decodeBase122String(strData) {
|
||||
const decoded = [];
|
||||
let curByte = 0;
|
||||
let bitOfByte = 0;
|
||||
|
||||
function push7(byte) {
|
||||
byte <<= 1;
|
||||
curByte |= (byte >>> bitOfByte);
|
||||
bitOfByte += 7;
|
||||
|
||||
if (bitOfByte >= 8) {
|
||||
decoded.push(curByte);
|
||||
bitOfByte -= 8;
|
||||
curByte = (byte << (7 - bitOfByte)) & 255;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < strData.length; i++) {
|
||||
const c = strData.charCodeAt(i);
|
||||
|
||||
if (c > 127) {
|
||||
const illegalIndex = (c >>> 8) & 7;
|
||||
if (illegalIndex !== kShortened) {
|
||||
push7(kIllegals[illegalIndex]);
|
||||
}
|
||||
push7(c & 127);
|
||||
} else {
|
||||
push7(c);
|
||||
}
|
||||
}
|
||||
|
||||
return new Uint8Array(decoded);
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Base122',
|
||||
priority: 250,
|
||||
category: 'encoding',
|
||||
func: function(text) {
|
||||
const bytes = new TextEncoder().encode(text);
|
||||
return encodeBase122Bytes(bytes);
|
||||
},
|
||||
reverse: function(text) {
|
||||
try {
|
||||
const bytes = decodeBase122String(text);
|
||||
return new TextDecoder().decode(bytes);
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) {
|
||||
return '[base122]';
|
||||
}
|
||||
const result = this.func(text.slice(0, 10));
|
||||
return result.substring(0, 15) + (result.length > 15 ? '...' : '');
|
||||
},
|
||||
detector: function(text) {
|
||||
if (!text || text.length < 4) {
|
||||
return false;
|
||||
}
|
||||
return /[\u0080-\uFFFF]/.test(text) || (text.length >= 8 && text !== text.trim());
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
+194
-148
@@ -1,151 +1,197 @@
|
||||
// baudot code / ITA2 encoding (teletype code)
|
||||
// Baudot / ITA2 telegraph code — 5-bit letters/figures with shift codes
|
||||
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;
|
||||
}
|
||||
});
|
||||
export default (function() {
|
||||
const FIGS = 0b11011;
|
||||
const LTRS = 0b11111;
|
||||
|
||||
const LETTERS = {
|
||||
'A': 0b00100,
|
||||
'B': 0b11010,
|
||||
'C': 0b01111,
|
||||
'D': 0b01010,
|
||||
'E': 0b00010,
|
||||
'F': 0b01110,
|
||||
'G': 0b00001,
|
||||
'H': 0b10101,
|
||||
'I': 0b00111,
|
||||
'J': 0b01100,
|
||||
'K': 0b10000,
|
||||
'L': 0b10011,
|
||||
'M': 0b11101,
|
||||
'N': 0b01101,
|
||||
'O': 0b11001,
|
||||
'P': 0b10111,
|
||||
'Q': 0b11000,
|
||||
'R': 0b01011,
|
||||
'S': 0b00110,
|
||||
'T': 0b10001,
|
||||
'U': 0b01000,
|
||||
'V': 0b11100,
|
||||
'W': 0b10100,
|
||||
'X': 0b11110,
|
||||
'Y': 0b10110,
|
||||
'Z': 0b10010
|
||||
};
|
||||
|
||||
const FIGURES = {
|
||||
'-': 0b00100,
|
||||
'?': 0b11010,
|
||||
':': 0b01111,
|
||||
'$': 0b01010,
|
||||
'3': 0b00010,
|
||||
'!': 0b01110,
|
||||
'&': 0b11011,
|
||||
'8': 0b00111,
|
||||
'7': 0b01000,
|
||||
'4': 0b01011,
|
||||
',': 0b01101,
|
||||
'(': 0b10000,
|
||||
')': 0b10011,
|
||||
'.': 0b11101,
|
||||
'0': 0b10111,
|
||||
'1': 0b11000,
|
||||
'9': 0b11001,
|
||||
'5': 0b10001,
|
||||
'+': 0b10010,
|
||||
'2': 0b10100,
|
||||
'6': 0b10110,
|
||||
'/': 0b11110
|
||||
};
|
||||
|
||||
const LETTER_BY_CODE = {};
|
||||
const FIGURE_BY_CODE = {};
|
||||
|
||||
for (const [char, code] of Object.entries(LETTERS)) {
|
||||
LETTER_BY_CODE[code] = char;
|
||||
}
|
||||
for (const [char, code] of Object.entries(FIGURES)) {
|
||||
if (char !== '&') {
|
||||
FIGURE_BY_CODE[code] = char;
|
||||
}
|
||||
}
|
||||
FIGURE_BY_CODE[FIGS] = '&';
|
||||
|
||||
function isFigureChar(char) {
|
||||
return Object.prototype.hasOwnProperty.call(FIGURES, char);
|
||||
}
|
||||
|
||||
function isLetterChar(char) {
|
||||
return Object.prototype.hasOwnProperty.call(LETTERS, char);
|
||||
}
|
||||
|
||||
function codesToDisplay(codes) {
|
||||
return codes.map(code => code.toString(2).padStart(5, '0')).join(' ');
|
||||
}
|
||||
|
||||
function parseDisplayCodes(text) {
|
||||
return text.trim().split(/\s+/).map(token => {
|
||||
if (!/^[01]{5}$/.test(token)) {
|
||||
return null;
|
||||
}
|
||||
return parseInt(token, 2);
|
||||
}).filter(code => code !== null);
|
||||
}
|
||||
|
||||
return new BaseTransformer({
|
||||
name: 'Baudot Code (ITA2)',
|
||||
priority: 250,
|
||||
category: 'encoding',
|
||||
func: function(text) {
|
||||
const upper = text.toUpperCase();
|
||||
const codes = [];
|
||||
let inFigures = false;
|
||||
|
||||
for (const char of upper) {
|
||||
if (char === '\n') {
|
||||
codes.push(0b00011);
|
||||
continue;
|
||||
}
|
||||
if (char === '\r') {
|
||||
codes.push(0b01001);
|
||||
continue;
|
||||
}
|
||||
if (char === ' ') {
|
||||
codes.push(0b00101);
|
||||
continue;
|
||||
}
|
||||
|
||||
const wantsFigure = isFigureChar(char);
|
||||
const wantsLetter = isLetterChar(char);
|
||||
|
||||
if (wantsFigure && !inFigures) {
|
||||
codes.push(FIGS);
|
||||
inFigures = true;
|
||||
} else if (wantsLetter && inFigures && char !== '&') {
|
||||
codes.push(LTRS);
|
||||
inFigures = false;
|
||||
}
|
||||
|
||||
if (char === '&' && !inFigures) {
|
||||
codes.push(FIGS);
|
||||
inFigures = true;
|
||||
}
|
||||
|
||||
const map = inFigures ? FIGURES : LETTERS;
|
||||
if (Object.prototype.hasOwnProperty.call(map, char)) {
|
||||
codes.push(map[char]);
|
||||
}
|
||||
}
|
||||
|
||||
return codesToDisplay(codes);
|
||||
},
|
||||
reverse: function(text) {
|
||||
const codes = parseDisplayCodes(text);
|
||||
if (codes.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let result = '';
|
||||
let inFigures = false;
|
||||
|
||||
for (const code of codes) {
|
||||
if (code === FIGS && !inFigures) {
|
||||
inFigures = true;
|
||||
continue;
|
||||
}
|
||||
if (code === LTRS && inFigures) {
|
||||
inFigures = false;
|
||||
continue;
|
||||
}
|
||||
if (code === 0b00011) {
|
||||
result += '\n';
|
||||
continue;
|
||||
}
|
||||
if (code === 0b01001) {
|
||||
result += '\r';
|
||||
continue;
|
||||
}
|
||||
if (code === 0b00101) {
|
||||
result += ' ';
|
||||
continue;
|
||||
}
|
||||
|
||||
const map = inFigures ? FIGURE_BY_CODE : LETTER_BY_CODE;
|
||||
const char = map[code];
|
||||
if (char) {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
preview: function(text) {
|
||||
if (!text) {
|
||||
return '[baudot]';
|
||||
}
|
||||
return this.func(text.slice(0, 5));
|
||||
},
|
||||
detector: function(text) {
|
||||
const tokens = text.trim().split(/\s+/);
|
||||
if (tokens.length < 4) {
|
||||
return false;
|
||||
}
|
||||
return tokens.every(token => /^[01]{5}$/.test(token));
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
<div v-if="activeTab === 'spellingalphabet'" class="tab-content">
|
||||
<div class="transform-layout spelling-alphabet-layout">
|
||||
<div class="transform-section">
|
||||
<div class="section-header-card">
|
||||
<div class="section-header-card-title">
|
||||
<i class="fas fa-spell-check"></i>
|
||||
<h3>Custom Spelling Alphabets</h3>
|
||||
</div>
|
||||
<p class="section-header-card-description">
|
||||
Build ICAO-style spelling alphabets from any theme. Generate words with OpenRouter, or fill in all 26 letters manually.
|
||||
Saved alphabets appear on the Transforms page under <code>custom_spelling</code>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Saved list -->
|
||||
<div v-if="saView === 'list'">
|
||||
<div class="tool-toolbar u-mb-16">
|
||||
<button type="button" class="transform-button tool-primary-btn" @click="saStartNew">
|
||||
<i class="fas fa-plus"></i> New Spelling Alphabet
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="saAlphabets.length === 0" class="sa-empty-state">
|
||||
<p>No saved spelling alphabets yet. Create one manually or generate from a category with OpenRouter.</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="sa-saved-list">
|
||||
<div v-for="entry in saAlphabets" :key="entry.id" class="sa-saved-card">
|
||||
<div class="sa-saved-card-body">
|
||||
<h4>{{ entry.name }}</h4>
|
||||
<p v-if="entry.category"><strong>Category:</strong> {{ entry.category }}</p>
|
||||
<p class="sa-saved-preview">
|
||||
<strong>Sample (hello world):</strong>
|
||||
{{ saSampleForEntry(entry) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="sa-saved-card-actions">
|
||||
<button type="button" class="btn btn-secondary" @click="saEditAlphabet(entry)">
|
||||
<i class="fas fa-pen"></i> Edit
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary sa-delete-btn" @click="saDeleteAlphabet(entry)">
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Editor -->
|
||||
<div v-if="saView === 'edit'" class="sa-editor">
|
||||
<div class="tool-toolbar u-mb-16">
|
||||
<button type="button" class="btn btn-secondary" @click="saCancelEdit">
|
||||
<i class="fas fa-arrow-left"></i> Back to list
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="options-grid sa-meta-grid">
|
||||
<label>
|
||||
Transform name
|
||||
<input type="text" v-model="saName" placeholder="e.g. Nautical Spelling Alphabet" />
|
||||
</label>
|
||||
<label>
|
||||
Category / theme
|
||||
<input type="text" v-model="saCategory" placeholder="e.g. nautical, cooking, astronomy" />
|
||||
</label>
|
||||
<label v-if="saHasApiKey()">
|
||||
OpenRouter model
|
||||
<select v-model="saModel">
|
||||
<option v-for="model in saModels" :key="model.id" :value="model.id">
|
||||
{{ model.name }} ({{ model.provider }})
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="sa-generate-row">
|
||||
<button
|
||||
type="button"
|
||||
class="transform-button tool-primary-btn"
|
||||
@click="saGenerateAlphabet"
|
||||
:disabled="saLoading || !saCategory.trim()"
|
||||
>
|
||||
<i class="fas fa-wand-magic-sparkles"></i>
|
||||
{{ saLoading ? 'Generating…' : 'Generate with OpenRouter' }}
|
||||
</button>
|
||||
<p v-if="!saHasApiKey()" class="sa-hint">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
No OpenRouter key — add one in Advanced Settings, or type each letter below manually.
|
||||
</p>
|
||||
<p v-else class="sa-hint">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Defaults to free OpenRouter models (no credits needed). Generation fills the grid below — edit any letter before saving.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="saError" class="sa-error">{{ saError }}</div>
|
||||
|
||||
<div class="sa-progress">
|
||||
{{ saFilledLetterCount() }} / 26 letters filled
|
||||
<span v-if="saFilledLetterCount() === 26"> — ready to save</span>
|
||||
</div>
|
||||
|
||||
<div class="sa-letter-grid">
|
||||
<label v-for="letter in saLetters" :key="letter" class="sa-letter-field">
|
||||
<span class="sa-letter-label">{{ letter }}</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="saAlphabet[letter]"
|
||||
:placeholder="letter"
|
||||
autocapitalize="characters"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="saFilledLetterCount() > 0" class="sa-live-preview">
|
||||
<strong>Preview (hello world):</strong> {{ saPreviewSample() }}
|
||||
</div>
|
||||
|
||||
<div class="tool-toolbar u-mt-16">
|
||||
<button type="button" class="transform-button tool-primary-btn" @click="saSaveAlphabet">
|
||||
<i class="fas fa-save"></i> Save Spelling Alphabet
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" @click="saCancelEdit">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -463,6 +463,11 @@ const limitations = {
|
||||
acceptPartial: true,
|
||||
normalize: { stripNonLetters: true, stripWhitespace: true }
|
||||
},
|
||||
'n7ax': {
|
||||
issues: 'Requires key, only encodes A-Z, removes spaces and punctuation',
|
||||
acceptPartial: true,
|
||||
normalize: { stripNonLetters: true, stripWhitespace: true }
|
||||
},
|
||||
'polybius': {
|
||||
issues: 'Only encodes A-Z, removes spaces and punctuation',
|
||||
acceptPartial: true,
|
||||
|
||||
Reference in New Issue
Block a user