@willwade/aac-processors 0.0.29 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -852
- package/dist/browser/core/baseProcessor.js +241 -0
- package/dist/browser/core/stringCasing.js +179 -0
- package/dist/browser/core/treeStructure.js +255 -0
- package/dist/browser/index.browser.js +73 -0
- package/dist/browser/processors/applePanelsProcessor.js +582 -0
- package/dist/browser/processors/astericsGridProcessor.js +1509 -0
- package/dist/browser/processors/dotProcessor.js +221 -0
- package/dist/browser/processors/gridset/commands.js +962 -0
- package/dist/browser/processors/gridset/crypto.js +53 -0
- package/dist/browser/processors/gridset/password.js +43 -0
- package/dist/browser/processors/gridset/pluginTypes.js +277 -0
- package/dist/browser/processors/gridset/resolver.js +137 -0
- package/dist/browser/processors/gridset/symbolAlignment.js +276 -0
- package/dist/browser/processors/gridset/symbols.js +421 -0
- package/dist/browser/processors/gridsetProcessor.js +2002 -0
- package/dist/browser/processors/obfProcessor.js +705 -0
- package/dist/browser/processors/opmlProcessor.js +274 -0
- package/dist/browser/types/aac.js +38 -0
- package/dist/browser/utilities/analytics/utils/idGenerator.js +89 -0
- package/dist/browser/utilities/translation/translationProcessor.js +200 -0
- package/dist/browser/utils/io.js +95 -0
- package/dist/browser/validation/baseValidator.js +156 -0
- package/dist/browser/validation/gridsetValidator.js +355 -0
- package/dist/browser/validation/obfValidator.js +500 -0
- package/dist/browser/validation/validationTypes.js +46 -0
- package/dist/cli/index.js +5 -5
- package/dist/core/analyze.d.ts +2 -2
- package/dist/core/analyze.js +2 -2
- package/dist/core/baseProcessor.d.ts +5 -4
- package/dist/core/baseProcessor.js +22 -27
- package/dist/core/treeStructure.d.ts +5 -5
- package/dist/core/treeStructure.js +1 -4
- package/dist/index.browser.d.ts +37 -0
- package/dist/index.browser.js +99 -0
- package/dist/index.d.ts +1 -48
- package/dist/index.js +1 -136
- package/dist/index.node.d.ts +48 -0
- package/dist/index.node.js +152 -0
- package/dist/processors/applePanelsProcessor.d.ts +5 -4
- package/dist/processors/applePanelsProcessor.js +58 -62
- package/dist/processors/astericsGridProcessor.d.ts +7 -6
- package/dist/processors/astericsGridProcessor.js +31 -42
- package/dist/processors/dotProcessor.d.ts +5 -4
- package/dist/processors/dotProcessor.js +25 -33
- package/dist/processors/excelProcessor.d.ts +4 -3
- package/dist/processors/excelProcessor.js +6 -3
- package/dist/processors/gridset/crypto.d.ts +18 -0
- package/dist/processors/gridset/crypto.js +57 -0
- package/dist/processors/gridset/helpers.d.ts +1 -1
- package/dist/processors/gridset/helpers.js +18 -8
- package/dist/processors/gridset/password.d.ts +20 -3
- package/dist/processors/gridset/password.js +17 -3
- package/dist/processors/gridset/wordlistHelpers.d.ts +3 -3
- package/dist/processors/gridset/wordlistHelpers.js +21 -20
- package/dist/processors/gridsetProcessor.d.ts +7 -12
- package/dist/processors/gridsetProcessor.js +118 -77
- package/dist/processors/obfProcessor.d.ts +9 -7
- package/dist/processors/obfProcessor.js +131 -56
- package/dist/processors/obfsetProcessor.d.ts +5 -4
- package/dist/processors/obfsetProcessor.js +10 -16
- package/dist/processors/opmlProcessor.d.ts +5 -4
- package/dist/processors/opmlProcessor.js +27 -34
- package/dist/processors/snapProcessor.d.ts +8 -7
- package/dist/processors/snapProcessor.js +15 -12
- package/dist/processors/touchchatProcessor.d.ts +8 -7
- package/dist/processors/touchchatProcessor.js +22 -17
- package/dist/types/aac.d.ts +0 -2
- package/dist/types/aac.js +2 -0
- package/dist/utils/io.d.ts +12 -0
- package/dist/utils/io.js +107 -0
- package/dist/validation/gridsetValidator.js +7 -7
- package/dist/validation/snapValidator.js +28 -35
- package/docs/BROWSER_USAGE.md +618 -0
- package/examples/README.md +77 -0
- package/examples/browser-test-server.js +81 -0
- package/examples/browser-test.html +331 -0
- package/examples/vitedemo/QUICKSTART.md +74 -0
- package/examples/vitedemo/README.md +157 -0
- package/examples/vitedemo/index.html +376 -0
- package/examples/vitedemo/package-lock.json +1221 -0
- package/examples/vitedemo/package.json +18 -0
- package/examples/vitedemo/src/main.ts +519 -0
- package/examples/vitedemo/test-files/example.dot +14 -0
- package/examples/vitedemo/test-files/example.grd +1 -0
- package/examples/vitedemo/test-files/example.gridset +0 -0
- package/examples/vitedemo/test-files/example.obz +0 -0
- package/examples/vitedemo/test-files/example.opml +18 -0
- package/examples/vitedemo/test-files/simple.obf +53 -0
- package/examples/vitedemo/tsconfig.json +24 -0
- package/examples/vitedemo/vite.config.ts +34 -0
- package/package.json +20 -4
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol Alignment for Translation
|
|
3
|
+
*
|
|
4
|
+
* Utilities to preserve symbol positions during text translation.
|
|
5
|
+
* When translating AAC gridset messages that contain symbols attached
|
|
6
|
+
* to specific words, we need to maintain the symbol-to-word associations
|
|
7
|
+
* across languages.
|
|
8
|
+
*
|
|
9
|
+
* Example:
|
|
10
|
+
* English: "I want apple juice" with apple symbol on "apple"
|
|
11
|
+
* Spanish: "Yo quiero jugo de manzana" with apple symbol on "manzana"
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Parse a message to extract text and symbol anchors.
|
|
15
|
+
*
|
|
16
|
+
* This handles various formats:
|
|
17
|
+
* 1. Plain text with no symbols
|
|
18
|
+
* 2. Rich text with embedded symbol markers (future enhancement)
|
|
19
|
+
* 3. Text where symbols are tracked separately (via richText.symbols)
|
|
20
|
+
*
|
|
21
|
+
* For now, this assumes symbols are tracked separately in the richText structure.
|
|
22
|
+
* The text itself is plain, and we need to tokenize it to find word positions.
|
|
23
|
+
*
|
|
24
|
+
* @param message - The message text (may contain words or be plain)
|
|
25
|
+
* @param richTextSymbols - Optional symbols from richText.symbols array
|
|
26
|
+
* @returns Parsed message with word positions and symbol anchors
|
|
27
|
+
*/
|
|
28
|
+
export function parseMessageWithSymbols(message, richTextSymbols) {
|
|
29
|
+
// Normalize whitespace for consistent tokenization
|
|
30
|
+
const normalizedMessage = message.trim().replace(/\s+/g, ' ');
|
|
31
|
+
// Tokenize into words, preserving punctuation
|
|
32
|
+
const words = [];
|
|
33
|
+
const wordPositions = [];
|
|
34
|
+
// Split by whitespace but track positions
|
|
35
|
+
let currentPos = 0;
|
|
36
|
+
const parts = normalizedMessage.split(/(\s+)/); // Keep delimiters
|
|
37
|
+
for (const part of parts) {
|
|
38
|
+
if (part.trim().length > 0) {
|
|
39
|
+
// This is a word
|
|
40
|
+
const startPos = currentPos;
|
|
41
|
+
const endPos = currentPos + part.length;
|
|
42
|
+
words.push(part);
|
|
43
|
+
wordPositions.push({ start: startPos, end: endPos, word: part });
|
|
44
|
+
currentPos = endPos;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// This is whitespace
|
|
48
|
+
currentPos += part.length;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Extract symbol anchors from richText.symbols if provided
|
|
52
|
+
const symbols = [];
|
|
53
|
+
if (richTextSymbols && richTextSymbols.length > 0) {
|
|
54
|
+
for (const sym of richTextSymbols) {
|
|
55
|
+
// Find which word this symbol is attached to
|
|
56
|
+
const wordIndex = words.findIndex((w) => w === sym.text);
|
|
57
|
+
if (wordIndex !== -1) {
|
|
58
|
+
const pos = wordPositions[wordIndex];
|
|
59
|
+
symbols.push({
|
|
60
|
+
symbolRef: sym.image || '',
|
|
61
|
+
wordIndex,
|
|
62
|
+
originalWord: sym.text,
|
|
63
|
+
startPos: pos.start,
|
|
64
|
+
endPos: pos.end,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// Fuzzy match - find closest word (handles case differences, punctuation)
|
|
69
|
+
const normalizedSymText = sym.text.toLowerCase().replace(/[^\w]/g, '');
|
|
70
|
+
const fuzzyIndex = words.findIndex((w) => w.toLowerCase().replace(/[^\w]/g, '') === normalizedSymText);
|
|
71
|
+
if (fuzzyIndex !== -1) {
|
|
72
|
+
const pos = wordPositions[fuzzyIndex];
|
|
73
|
+
symbols.push({
|
|
74
|
+
symbolRef: sym.image || '',
|
|
75
|
+
wordIndex: fuzzyIndex,
|
|
76
|
+
originalWord: words[fuzzyIndex],
|
|
77
|
+
startPos: pos.start,
|
|
78
|
+
endPos: pos.end,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
text: normalizedMessage,
|
|
86
|
+
words,
|
|
87
|
+
symbols,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Align words from original text to translated text.
|
|
92
|
+
*
|
|
93
|
+
* This is a simple alignment strategy that works for many cases:
|
|
94
|
+
* 1. Exact word matching (for cognates, names, numbers)
|
|
95
|
+
* 2. Position-based alignment (assumes similar word order)
|
|
96
|
+
*
|
|
97
|
+
* For more accurate alignment, you could integrate with:
|
|
98
|
+
* - Translation APIs that return alignment (e.g., Google Translate's word alignment)
|
|
99
|
+
* - Statistical machine translation alignment tools
|
|
100
|
+
* - Bilingual dictionaries
|
|
101
|
+
*
|
|
102
|
+
* @param originalWords - Words from the original text
|
|
103
|
+
* @param translatedWords - Words from the translated text
|
|
104
|
+
* @returns Alignment mapping between original and translated word indices
|
|
105
|
+
*/
|
|
106
|
+
export function alignWords(originalWords, translatedWords) {
|
|
107
|
+
const alignment = [];
|
|
108
|
+
// Strategy 1: Try to match identical words (numbers, names, cognates)
|
|
109
|
+
const matchedTranslatedIndices = new Set();
|
|
110
|
+
for (let origIdx = 0; origIdx < originalWords.length; origIdx++) {
|
|
111
|
+
const origWord = originalWords[origIdx];
|
|
112
|
+
const normalizedOrig = origWord.toLowerCase().replace(/[^\w]/g, '');
|
|
113
|
+
// Try to find this word in the translation
|
|
114
|
+
for (let transIdx = 0; transIdx < translatedWords.length; transIdx++) {
|
|
115
|
+
if (matchedTranslatedIndices.has(transIdx))
|
|
116
|
+
continue;
|
|
117
|
+
const transWord = translatedWords[transIdx];
|
|
118
|
+
const normalizedTrans = transWord.toLowerCase().replace(/[^\w]/g, '');
|
|
119
|
+
// Exact match (case-insensitive, ignoring punctuation)
|
|
120
|
+
if (normalizedOrig === normalizedTrans && normalizedOrig.length > 0) {
|
|
121
|
+
alignment.push({
|
|
122
|
+
originalWord: origWord,
|
|
123
|
+
translatedWord: transWord,
|
|
124
|
+
originalIndex: origIdx,
|
|
125
|
+
translatedIndex: transIdx,
|
|
126
|
+
});
|
|
127
|
+
matchedTranslatedIndices.add(transIdx);
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Strategy 2: For unmatched words, use positional alignment
|
|
133
|
+
// This is a simple fallback that assumes similar word order
|
|
134
|
+
for (let origIdx = 0; origIdx < originalWords.length; origIdx++) {
|
|
135
|
+
if (alignment.find((a) => a.originalIndex === origIdx))
|
|
136
|
+
continue; // Already matched
|
|
137
|
+
// Find the closest unmatched position in translation
|
|
138
|
+
let bestTransIdx = -1;
|
|
139
|
+
let minDistance = Infinity;
|
|
140
|
+
for (let transIdx = 0; transIdx < translatedWords.length; transIdx++) {
|
|
141
|
+
if (matchedTranslatedIndices.has(transIdx))
|
|
142
|
+
continue;
|
|
143
|
+
// Calculate relative position
|
|
144
|
+
const relativeOrigPos = origIdx / originalWords.length;
|
|
145
|
+
const relativeTransPos = transIdx / translatedWords.length;
|
|
146
|
+
const distance = Math.abs(relativeOrigPos - relativeTransPos);
|
|
147
|
+
if (distance < minDistance) {
|
|
148
|
+
minDistance = distance;
|
|
149
|
+
bestTransIdx = transIdx;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (bestTransIdx !== -1) {
|
|
153
|
+
alignment.push({
|
|
154
|
+
originalWord: originalWords[origIdx],
|
|
155
|
+
translatedWord: translatedWords[bestTransIdx],
|
|
156
|
+
originalIndex: origIdx,
|
|
157
|
+
translatedIndex: bestTransIdx,
|
|
158
|
+
});
|
|
159
|
+
matchedTranslatedIndices.add(bestTransIdx);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return alignment;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Reattach symbols to translated text based on word alignment.
|
|
166
|
+
*
|
|
167
|
+
* @param translatedText - The translated plain text
|
|
168
|
+
* @param originalParsed - The original parsed message with symbols
|
|
169
|
+
* @param alignment - Word alignment between original and translation
|
|
170
|
+
* @returns Translated text with symbols embedded (as rich text structure)
|
|
171
|
+
*/
|
|
172
|
+
export function reattachSymbols(translatedText, originalParsed, alignment) {
|
|
173
|
+
// Tokenize the translated text
|
|
174
|
+
const translatedWords = translatedText
|
|
175
|
+
.trim()
|
|
176
|
+
.replace(/\s+/g, ' ')
|
|
177
|
+
.split(/\s+/)
|
|
178
|
+
.filter((w) => w.length > 0);
|
|
179
|
+
// Create the rich text symbols array
|
|
180
|
+
const richTextSymbols = [];
|
|
181
|
+
for (const symbol of originalParsed.symbols) {
|
|
182
|
+
// Find the alignment for this word
|
|
183
|
+
const wordAlignment = alignment.find((a) => a.originalIndex === symbol.wordIndex);
|
|
184
|
+
if (wordAlignment && wordAlignment.translatedIndex < translatedWords.length) {
|
|
185
|
+
const translatedWord = translatedWords[wordAlignment.translatedIndex];
|
|
186
|
+
// Attach the symbol to the translated word
|
|
187
|
+
richTextSymbols.push({
|
|
188
|
+
text: translatedWord,
|
|
189
|
+
image: symbol.symbolRef,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
// Fallback: keep symbol on original word if no alignment found
|
|
194
|
+
richTextSymbols.push({
|
|
195
|
+
text: symbol.originalWord,
|
|
196
|
+
image: symbol.symbolRef,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
text: translatedText,
|
|
202
|
+
richTextSymbols,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Complete pipeline: translate a message while preserving symbol positions.
|
|
207
|
+
*
|
|
208
|
+
* @param originalMessage - The original message text
|
|
209
|
+
* @param translatedText - The translated text (from translation API)
|
|
210
|
+
* @param richTextSymbols - Original symbols from richText.symbols
|
|
211
|
+
* @returns Object with translated text and aligned symbols
|
|
212
|
+
*/
|
|
213
|
+
export function translateWithSymbols(originalMessage, translatedText, richTextSymbols) {
|
|
214
|
+
// Step 1: Parse original message
|
|
215
|
+
const parsedOriginal = parseMessageWithSymbols(originalMessage, richTextSymbols);
|
|
216
|
+
// If no symbols, return as-is
|
|
217
|
+
if (parsedOriginal.symbols.length === 0) {
|
|
218
|
+
return {
|
|
219
|
+
text: translatedText,
|
|
220
|
+
richTextSymbols: [],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
// Step 2: Tokenize translated text
|
|
224
|
+
const translatedWords = translatedText
|
|
225
|
+
.trim()
|
|
226
|
+
.replace(/\s+/g, ' ')
|
|
227
|
+
.split(/\s+/)
|
|
228
|
+
.filter((w) => w.length > 0);
|
|
229
|
+
// Step 3: Align words
|
|
230
|
+
const alignment = alignWords(parsedOriginal.words, translatedWords);
|
|
231
|
+
// Step 4: Reattach symbols
|
|
232
|
+
const result = reattachSymbols(translatedText, parsedOriginal, alignment);
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Extract symbols from a button for use during translation.
|
|
237
|
+
*
|
|
238
|
+
* This helper extracts symbols from either:
|
|
239
|
+
* - button.semanticAction.richText.symbols
|
|
240
|
+
* - button.image (if it's a symbol library reference)
|
|
241
|
+
*
|
|
242
|
+
* @param button - The AAC button
|
|
243
|
+
* @returns Array of symbol attachments
|
|
244
|
+
*/
|
|
245
|
+
export function extractSymbolsFromButton(button) {
|
|
246
|
+
// First check richText structure
|
|
247
|
+
if (button.semanticAction?.richText?.symbols) {
|
|
248
|
+
return button.semanticAction.richText.symbols;
|
|
249
|
+
}
|
|
250
|
+
// Check if button has a symbol library reference as image
|
|
251
|
+
if (button.symbolLibrary && button.symbolPath) {
|
|
252
|
+
// Create a symbol attachment for the label/message
|
|
253
|
+
const text = button.label || button.message || '';
|
|
254
|
+
if (text) {
|
|
255
|
+
return [
|
|
256
|
+
{
|
|
257
|
+
text,
|
|
258
|
+
image: `[${button.symbolLibrary}]${button.symbolPath}`,
|
|
259
|
+
},
|
|
260
|
+
];
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Check if image field contains a symbol reference
|
|
264
|
+
if (button.image && button.image.startsWith('[')) {
|
|
265
|
+
const text = button.label || button.message || '';
|
|
266
|
+
if (text) {
|
|
267
|
+
return [
|
|
268
|
+
{
|
|
269
|
+
text,
|
|
270
|
+
image: button.image,
|
|
271
|
+
},
|
|
272
|
+
];
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grid 3 Symbol Library Resolution
|
|
3
|
+
*
|
|
4
|
+
* Grid 3 uses symbol libraries stored as .pix files in the installation directory.
|
|
5
|
+
* Symbol references in Grid files use the format: [library]/path/to/symbol.png
|
|
6
|
+
*
|
|
7
|
+
* Examples:
|
|
8
|
+
* - [widgit]/food/apple.png
|
|
9
|
+
* - [tawasl]/above bw.png
|
|
10
|
+
* - [ssnaps]963.jpg
|
|
11
|
+
* - [grid3x]/folder/document.png
|
|
12
|
+
*
|
|
13
|
+
* This module provides symbol resolution and metadata extraction.
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import AdmZip from 'adm-zip';
|
|
18
|
+
/**
|
|
19
|
+
* Default Grid 3 installation paths by platform
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_GRID3_PATHS = {
|
|
22
|
+
win32: 'C:\\Program Files (x86)\\Smartbox\\Grid 3',
|
|
23
|
+
darwin: '/Applications/Grid 3.app/Contents/Resources',
|
|
24
|
+
linux: '/opt/smartbox/grid3',
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Path to Symbols directory within Grid 3 installation
|
|
28
|
+
* Contains .symbols ZIP archives with actual images
|
|
29
|
+
*/
|
|
30
|
+
const SYMBOLS_SUBDIR = 'Resources\\Symbols';
|
|
31
|
+
/**
|
|
32
|
+
* Path to symbol search indexes within Grid 3 installation
|
|
33
|
+
* Contains .pix index files for searching
|
|
34
|
+
*/
|
|
35
|
+
const SYMBOLSEARCH_SUBDIR = 'Locale';
|
|
36
|
+
/**
|
|
37
|
+
* Known symbol libraries in Grid 3
|
|
38
|
+
*/
|
|
39
|
+
export const SYMBOL_LIBRARIES = {
|
|
40
|
+
WIDGIT: 'widgit',
|
|
41
|
+
TAWASL: 'tawasl',
|
|
42
|
+
SSNAPS: 'ssnaps',
|
|
43
|
+
GRID3X: 'grid3x',
|
|
44
|
+
GRID2X: 'grid2x',
|
|
45
|
+
BLISSX: 'blissx',
|
|
46
|
+
EYEGAZ: 'eyegaz',
|
|
47
|
+
INTERL: 'interl',
|
|
48
|
+
METACM: 'metacm',
|
|
49
|
+
MJPCS: 'mjpcs#',
|
|
50
|
+
PCSHC: 'pcshc#',
|
|
51
|
+
PCSTL: 'pcstl#',
|
|
52
|
+
SESENS: 'sesens',
|
|
53
|
+
SSTIX: 'sstix#',
|
|
54
|
+
SYMOJI: 'symoji',
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Default locale to use
|
|
58
|
+
*/
|
|
59
|
+
export const DEFAULT_LOCALE = 'en-GB';
|
|
60
|
+
/**
|
|
61
|
+
* Parse a symbol reference string
|
|
62
|
+
* @param reference - Symbol reference like "[widgit]/food/apple.png"
|
|
63
|
+
* @returns Parsed symbol reference
|
|
64
|
+
*/
|
|
65
|
+
export function parseSymbolReference(reference) {
|
|
66
|
+
const trimmed = reference.trim();
|
|
67
|
+
// Match pattern: [library]/path or [library]path
|
|
68
|
+
const match = trimmed.match(/^\[([^\]]+)\](.+)$/);
|
|
69
|
+
if (!match) {
|
|
70
|
+
return {
|
|
71
|
+
library: '',
|
|
72
|
+
path: trimmed,
|
|
73
|
+
fullReference: trimmed,
|
|
74
|
+
isValid: false,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const [, library, symbolPath] = match;
|
|
78
|
+
return {
|
|
79
|
+
library: library.toLowerCase(),
|
|
80
|
+
path: symbolPath.replace(/^\\+/, '').trim(), // Remove leading slashes
|
|
81
|
+
fullReference: trimmed,
|
|
82
|
+
isValid: true,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Check if a string is a symbol library reference
|
|
87
|
+
* @param reference - String to check
|
|
88
|
+
* @returns True if it's a symbol reference like [widgit]/...
|
|
89
|
+
*/
|
|
90
|
+
export function isSymbolReference(reference) {
|
|
91
|
+
return reference.trim().startsWith('[');
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Get the default Grid 3 installation path for the current platform
|
|
95
|
+
* @returns Default Grid 3 path or empty string if not found
|
|
96
|
+
*/
|
|
97
|
+
export function getDefaultGrid3Path() {
|
|
98
|
+
const platform = process.platform;
|
|
99
|
+
const defaultPath = DEFAULT_GRID3_PATHS[platform] || '';
|
|
100
|
+
if (defaultPath && fs.existsSync(defaultPath)) {
|
|
101
|
+
return defaultPath;
|
|
102
|
+
}
|
|
103
|
+
// Try to find Grid 3 in common locations
|
|
104
|
+
const commonPaths = [
|
|
105
|
+
'C:\\Program Files (x86)\\Smartbox\\Grid 3',
|
|
106
|
+
'C:\\Program Files\\Smartbox\\Grid 3',
|
|
107
|
+
'C:\\Program Files\\Smartbox\\Grid 3',
|
|
108
|
+
'/Applications/Grid 3.app',
|
|
109
|
+
'/opt/smartbox/grid3',
|
|
110
|
+
];
|
|
111
|
+
for (const testPath of commonPaths) {
|
|
112
|
+
if (fs.existsSync(testPath)) {
|
|
113
|
+
return testPath;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return '';
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Get the Symbol Libraries directory path
|
|
120
|
+
* Contains .symbols ZIP archives with actual image files
|
|
121
|
+
* @param grid3Path - Grid 3 installation path
|
|
122
|
+
* @returns Path to Symbol Libraries directory (e.g., "C:\...\Grid 3\Resources\Symbols")
|
|
123
|
+
*/
|
|
124
|
+
export function getSymbolLibrariesDir(grid3Path) {
|
|
125
|
+
return path.join(grid3Path, SYMBOLS_SUBDIR);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Get the symbol search indexes directory path for a given locale
|
|
129
|
+
* Contains .pix index files for searching symbols
|
|
130
|
+
* @param grid3Path - Grid 3 installation path
|
|
131
|
+
* @param locale - Locale code (e.g., 'en-GB')
|
|
132
|
+
* @returns Path to symbol search indexes directory (e.g., "C:\...\Grid 3\Locale\en-GB\symbolsearch")
|
|
133
|
+
*/
|
|
134
|
+
export function getSymbolSearchIndexesDir(grid3Path, locale = DEFAULT_LOCALE) {
|
|
135
|
+
return path.join(grid3Path, SYMBOLSEARCH_SUBDIR, locale, 'symbolsearch');
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Get all available symbol libraries in the Grid 3 installation
|
|
139
|
+
* @param options - Resolution options
|
|
140
|
+
* @returns Array of symbol library information
|
|
141
|
+
*/
|
|
142
|
+
export function getAvailableSymbolLibraries(options = {}) {
|
|
143
|
+
const grid3Path = options.grid3Path || options.symbolDir || getDefaultGrid3Path();
|
|
144
|
+
if (!grid3Path) {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
const symbolsDir = getSymbolLibrariesDir(grid3Path);
|
|
148
|
+
if (!fs.existsSync(symbolsDir)) {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
const libraries = [];
|
|
152
|
+
const files = fs.readdirSync(symbolsDir);
|
|
153
|
+
for (const file of files) {
|
|
154
|
+
if (file.endsWith('.symbols')) {
|
|
155
|
+
const fullPath = path.join(symbolsDir, file);
|
|
156
|
+
const stats = fs.statSync(fullPath);
|
|
157
|
+
const libraryName = path.basename(file, '.symbols');
|
|
158
|
+
libraries.push({
|
|
159
|
+
name: libraryName,
|
|
160
|
+
pixFile: fullPath, // Reuse this field for the .symbols file path
|
|
161
|
+
exists: true,
|
|
162
|
+
size: stats.size,
|
|
163
|
+
locale: 'global', // .symbols files are not locale-specific
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return libraries.sort((a, b) => a.name.localeCompare(b.name));
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Check if a symbol library exists
|
|
171
|
+
* @param libraryName - Name of the library (e.g., 'widgit', 'tawasl')
|
|
172
|
+
* @param options - Resolution options
|
|
173
|
+
* @returns Symbol library info or undefined if not found
|
|
174
|
+
*/
|
|
175
|
+
export function getSymbolLibraryInfo(libraryName, options = {}) {
|
|
176
|
+
const grid3Path = options.grid3Path || options.symbolDir || getDefaultGrid3Path();
|
|
177
|
+
if (!grid3Path) {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
const symbolsDir = getSymbolLibrariesDir(grid3Path);
|
|
181
|
+
const normalizedLibName = libraryName.toLowerCase();
|
|
182
|
+
// Try different case variations
|
|
183
|
+
const variations = [
|
|
184
|
+
normalizedLibName + '.symbols',
|
|
185
|
+
normalizedLibName.toUpperCase() + '.symbols',
|
|
186
|
+
libraryName + '.symbols',
|
|
187
|
+
];
|
|
188
|
+
for (const file of variations) {
|
|
189
|
+
const fullPath = path.join(symbolsDir, file);
|
|
190
|
+
if (fs.existsSync(fullPath)) {
|
|
191
|
+
const stats = fs.statSync(fullPath);
|
|
192
|
+
return {
|
|
193
|
+
name: libraryName,
|
|
194
|
+
pixFile: fullPath,
|
|
195
|
+
exists: true,
|
|
196
|
+
size: stats.size,
|
|
197
|
+
locale: 'global',
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Resolve a symbol reference to extract the actual image data
|
|
205
|
+
* @param reference - Symbol reference like "[tawasl]/above bw.png"
|
|
206
|
+
* @param options - Resolution options
|
|
207
|
+
* @returns Resolution result with image data if found
|
|
208
|
+
*/
|
|
209
|
+
export function resolveSymbolReference(reference, options = {}) {
|
|
210
|
+
const parsed = parseSymbolReference(reference);
|
|
211
|
+
if (!parsed.isValid) {
|
|
212
|
+
return {
|
|
213
|
+
reference: parsed,
|
|
214
|
+
found: false,
|
|
215
|
+
error: 'Invalid symbol reference format',
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const grid3Path = options.grid3Path || getDefaultGrid3Path();
|
|
219
|
+
if (!grid3Path) {
|
|
220
|
+
return {
|
|
221
|
+
reference: parsed,
|
|
222
|
+
found: false,
|
|
223
|
+
error: 'Grid 3 installation not found. Please specify grid3Path.',
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const libraryInfo = getSymbolLibraryInfo(parsed.library, { grid3Path });
|
|
227
|
+
if (!libraryInfo || !libraryInfo.exists) {
|
|
228
|
+
return {
|
|
229
|
+
reference: parsed,
|
|
230
|
+
found: false,
|
|
231
|
+
error: `Symbol library '${parsed.library}' not found at ${libraryInfo?.pixFile || 'unknown'}`,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
// .symbols files are ZIP archives
|
|
236
|
+
const zip = new AdmZip(libraryInfo.pixFile);
|
|
237
|
+
// The path in the symbol reference becomes the path within the symbols/ folder
|
|
238
|
+
// e.g., [tawasl]/above bw.png becomes symbols/above bw.png
|
|
239
|
+
const symbolPath = `symbols/${parsed.path}`;
|
|
240
|
+
const entry = zip.getEntry(symbolPath);
|
|
241
|
+
if (!entry) {
|
|
242
|
+
// Try without the symbols/ prefix (in case reference already includes it)
|
|
243
|
+
const altPath = parsed.path.startsWith('symbols/') ? parsed.path : `symbols/${parsed.path}`;
|
|
244
|
+
const altEntry = zip.getEntry(altPath);
|
|
245
|
+
if (!altEntry) {
|
|
246
|
+
return {
|
|
247
|
+
reference: parsed,
|
|
248
|
+
found: false,
|
|
249
|
+
error: `Symbol '${parsed.path}' not found in library '${parsed.library}'`,
|
|
250
|
+
path: libraryInfo.pixFile,
|
|
251
|
+
libraryInfo,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
// Found with alternate path
|
|
255
|
+
const data = altEntry.getData();
|
|
256
|
+
return {
|
|
257
|
+
reference: parsed,
|
|
258
|
+
found: true,
|
|
259
|
+
path: libraryInfo.pixFile,
|
|
260
|
+
data,
|
|
261
|
+
libraryInfo,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
// Found the symbol!
|
|
265
|
+
const data = entry.getData();
|
|
266
|
+
return {
|
|
267
|
+
reference: parsed,
|
|
268
|
+
found: true,
|
|
269
|
+
path: libraryInfo.pixFile,
|
|
270
|
+
data,
|
|
271
|
+
libraryInfo,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
return {
|
|
276
|
+
reference: parsed,
|
|
277
|
+
found: false,
|
|
278
|
+
error: `Failed to extract symbol: ${error.message}`,
|
|
279
|
+
path: libraryInfo.pixFile,
|
|
280
|
+
libraryInfo,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Get all symbol references from a gridset
|
|
286
|
+
* This scans button images for symbol references
|
|
287
|
+
* @param tree - AAC tree from loaded gridset
|
|
288
|
+
* @returns Array of unique symbol references
|
|
289
|
+
*/
|
|
290
|
+
export function extractSymbolReferences(tree) {
|
|
291
|
+
const references = new Set();
|
|
292
|
+
for (const pageId in tree.pages) {
|
|
293
|
+
const page = tree.pages[pageId];
|
|
294
|
+
if (page.buttons) {
|
|
295
|
+
for (const button of page.buttons) {
|
|
296
|
+
if (button.image && isSymbolReference(String(button.image))) {
|
|
297
|
+
references.add(String(button.image));
|
|
298
|
+
}
|
|
299
|
+
// Check for symbol library metadata
|
|
300
|
+
if (button.symbolLibrary) {
|
|
301
|
+
const ref = `[${button.symbolLibrary}]${button.symbolPath || ''}`;
|
|
302
|
+
references.add(ref);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return Array.from(references).sort();
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Create a symbol reference from library and path
|
|
311
|
+
* @param library - Library name
|
|
312
|
+
* @param symbolPath - Path within the library
|
|
313
|
+
* @returns Formatted symbol reference
|
|
314
|
+
*/
|
|
315
|
+
export function createSymbolReference(library, symbolPath) {
|
|
316
|
+
const normalizedLib = library.toLowerCase().replace(/\[|\]/g, '');
|
|
317
|
+
const normalizedPath = symbolPath.replace(/^\\+/, '');
|
|
318
|
+
return `[${normalizedLib}]${normalizedPath}`;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Get the library name from a symbol reference
|
|
322
|
+
* @param reference - Symbol reference
|
|
323
|
+
* @returns Library name or empty string
|
|
324
|
+
*/
|
|
325
|
+
export function getSymbolLibraryName(reference) {
|
|
326
|
+
const parsed = parseSymbolReference(reference);
|
|
327
|
+
return parsed.library;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Get the symbol path from a symbol reference
|
|
331
|
+
* @param reference - Symbol reference
|
|
332
|
+
* @returns Symbol path or empty string
|
|
333
|
+
*/
|
|
334
|
+
export function getSymbolPath(reference) {
|
|
335
|
+
const parsed = parseSymbolReference(reference);
|
|
336
|
+
return parsed.path;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Check if a symbol library is one of the known Grid 3 libraries
|
|
340
|
+
* @param libraryName - Library name to check
|
|
341
|
+
* @returns True if it's a known library
|
|
342
|
+
*/
|
|
343
|
+
export function isKnownSymbolLibrary(libraryName) {
|
|
344
|
+
const normalized = libraryName.toLowerCase().replace(/\[|\]/g, '');
|
|
345
|
+
return Object.values(SYMBOL_LIBRARIES).includes(normalized);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Get display name for a symbol library
|
|
349
|
+
* @param libraryName - Library name
|
|
350
|
+
* @returns Human-readable display name
|
|
351
|
+
*/
|
|
352
|
+
export function getSymbolLibraryDisplayName(libraryName) {
|
|
353
|
+
const normalized = libraryName.toLowerCase().replace(/\[|\]/g, '');
|
|
354
|
+
const displayNames = {
|
|
355
|
+
widgit: 'Widgit Symbols',
|
|
356
|
+
tawasl: 'Tawasol (Arabic)',
|
|
357
|
+
ssnaps: 'Smartbox Symbol Snapshots',
|
|
358
|
+
grid3x: 'Grid 3 Extended',
|
|
359
|
+
grid2x: 'Grid 2 Extended',
|
|
360
|
+
blissx: 'Blissymbols',
|
|
361
|
+
eyegaz: 'Eye Gaze Symbols',
|
|
362
|
+
interl: 'International Symbols',
|
|
363
|
+
metacm: 'MetaComm',
|
|
364
|
+
mjpcs: 'Mayer-Johnson PCS',
|
|
365
|
+
pcshc: 'PCS High Contrast',
|
|
366
|
+
pcstl: 'PCS Thin Line',
|
|
367
|
+
sesens: 'Sensory Software',
|
|
368
|
+
sstix: 'Smartbox TIX',
|
|
369
|
+
symoji: 'Symbol Emoji',
|
|
370
|
+
};
|
|
371
|
+
return displayNames[normalized] || normalized.charAt(0).toUpperCase() + normalized.slice(1);
|
|
372
|
+
}
|
|
373
|
+
export function analyzeSymbolUsage(tree) {
|
|
374
|
+
const references = extractSymbolReferences(tree);
|
|
375
|
+
const byLibrary = {};
|
|
376
|
+
const libraries = new Set();
|
|
377
|
+
for (const ref of references) {
|
|
378
|
+
const lib = getSymbolLibraryName(ref);
|
|
379
|
+
byLibrary[lib] = (byLibrary[lib] || 0) + 1;
|
|
380
|
+
if (lib) {
|
|
381
|
+
libraries.add(lib);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
totalSymbols: references.length,
|
|
386
|
+
byLibrary,
|
|
387
|
+
uniqueReferences: references,
|
|
388
|
+
librariesUsed: Array.from(libraries).sort(),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Convert symbol reference to filename for embedded images
|
|
393
|
+
* Grid 3 sometimes embeds symbols with special naming
|
|
394
|
+
* @param reference - Symbol reference
|
|
395
|
+
* @param cellX - Cell X coordinate
|
|
396
|
+
* @param cellY - Cell Y coordinate
|
|
397
|
+
* @returns Generated filename
|
|
398
|
+
*/
|
|
399
|
+
export function symbolReferenceToFilename(reference, cellX, cellY) {
|
|
400
|
+
const parsed = parseSymbolReference(reference);
|
|
401
|
+
const ext = path.extname(parsed.path) || '.png';
|
|
402
|
+
// Grid 3 format: {x}-{y}-0-text-0.{ext}
|
|
403
|
+
return `${cellX}-${cellY}-0-text-0${ext}`;
|
|
404
|
+
}
|
|
405
|
+
// ============================================================================
|
|
406
|
+
// BACKWARD COMPATIBILITY ALIASES
|
|
407
|
+
// ============================================================================
|
|
408
|
+
/**
|
|
409
|
+
* @deprecated Use getSymbolLibrariesDir() instead - more descriptive name
|
|
410
|
+
* Get the Symbols directory path (where .symbols ZIP archives are)
|
|
411
|
+
*/
|
|
412
|
+
export function getSymbolsDir(grid3Path) {
|
|
413
|
+
return getSymbolLibrariesDir(grid3Path);
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* @deprecated Use getSymbolSearchIndexesDir() instead - more descriptive name
|
|
417
|
+
* Get the symbol search directory for a given locale (where .pix index files are)
|
|
418
|
+
*/
|
|
419
|
+
export function getSymbolSearchDir(grid3Path, locale = DEFAULT_LOCALE) {
|
|
420
|
+
return getSymbolSearchIndexesDir(grid3Path, locale);
|
|
421
|
+
}
|