added alphabet tool, updated packages, a few transform fixes

This commit is contained in:
ph1r3754r73r
2026-06-13 10:47:52 -07:00
parent 730ce238a8
commit 31308cd598
17 changed files with 1620 additions and 274 deletions
+10 -1
View File
@@ -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 AZ, 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-...`)
+7 -3
View File
@@ -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) {
+2 -1
View File
@@ -21,7 +21,8 @@ const templateFiles = [
'tokenizer.html',
'bijection.html',
'splitter.html',
'gibberish.html'
'gibberish.html',
'spellingalphabet.html'
];
+132
View File
@@ -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);
}
+3
View File
@@ -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>
+139
View File
@@ -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);
+272
View File
@@ -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 AZ 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 13 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 AZ 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);
+3
View File
@@ -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());
}
+1
View File
@@ -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' }
];
+331
View File
@@ -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
View File
@@ -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');
+6 -6
View File
@@ -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": [
{
+185
View File
@@ -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;
}
});
+134 -94
View File
@@ -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
View File
@@ -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));
}
});
})();
+129
View File
@@ -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>
+5
View File
@@ -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,