@willwade/aac-processors 0.0.30 → 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.
Files changed (92) hide show
  1. package/README.md +52 -852
  2. package/dist/browser/core/baseProcessor.js +241 -0
  3. package/dist/browser/core/stringCasing.js +179 -0
  4. package/dist/browser/core/treeStructure.js +255 -0
  5. package/dist/browser/index.browser.js +73 -0
  6. package/dist/browser/processors/applePanelsProcessor.js +582 -0
  7. package/dist/browser/processors/astericsGridProcessor.js +1509 -0
  8. package/dist/browser/processors/dotProcessor.js +221 -0
  9. package/dist/browser/processors/gridset/commands.js +962 -0
  10. package/dist/browser/processors/gridset/crypto.js +53 -0
  11. package/dist/browser/processors/gridset/password.js +43 -0
  12. package/dist/browser/processors/gridset/pluginTypes.js +277 -0
  13. package/dist/browser/processors/gridset/resolver.js +137 -0
  14. package/dist/browser/processors/gridset/symbolAlignment.js +276 -0
  15. package/dist/browser/processors/gridset/symbols.js +421 -0
  16. package/dist/browser/processors/gridsetProcessor.js +2002 -0
  17. package/dist/browser/processors/obfProcessor.js +705 -0
  18. package/dist/browser/processors/opmlProcessor.js +274 -0
  19. package/dist/browser/types/aac.js +38 -0
  20. package/dist/browser/utilities/analytics/utils/idGenerator.js +89 -0
  21. package/dist/browser/utilities/translation/translationProcessor.js +200 -0
  22. package/dist/browser/utils/io.js +95 -0
  23. package/dist/browser/validation/baseValidator.js +156 -0
  24. package/dist/browser/validation/gridsetValidator.js +355 -0
  25. package/dist/browser/validation/obfValidator.js +500 -0
  26. package/dist/browser/validation/validationTypes.js +46 -0
  27. package/dist/cli/index.js +5 -5
  28. package/dist/core/analyze.d.ts +2 -2
  29. package/dist/core/analyze.js +2 -2
  30. package/dist/core/baseProcessor.d.ts +5 -4
  31. package/dist/core/baseProcessor.js +22 -27
  32. package/dist/core/treeStructure.d.ts +5 -5
  33. package/dist/core/treeStructure.js +1 -4
  34. package/dist/index.browser.d.ts +37 -0
  35. package/dist/index.browser.js +99 -0
  36. package/dist/index.d.ts +1 -48
  37. package/dist/index.js +1 -136
  38. package/dist/index.node.d.ts +48 -0
  39. package/dist/index.node.js +152 -0
  40. package/dist/processors/applePanelsProcessor.d.ts +5 -4
  41. package/dist/processors/applePanelsProcessor.js +58 -62
  42. package/dist/processors/astericsGridProcessor.d.ts +7 -6
  43. package/dist/processors/astericsGridProcessor.js +31 -42
  44. package/dist/processors/dotProcessor.d.ts +5 -4
  45. package/dist/processors/dotProcessor.js +25 -33
  46. package/dist/processors/excelProcessor.d.ts +4 -3
  47. package/dist/processors/excelProcessor.js +6 -3
  48. package/dist/processors/gridset/crypto.d.ts +18 -0
  49. package/dist/processors/gridset/crypto.js +57 -0
  50. package/dist/processors/gridset/helpers.d.ts +1 -1
  51. package/dist/processors/gridset/helpers.js +18 -8
  52. package/dist/processors/gridset/password.d.ts +20 -3
  53. package/dist/processors/gridset/password.js +17 -3
  54. package/dist/processors/gridset/wordlistHelpers.d.ts +3 -3
  55. package/dist/processors/gridset/wordlistHelpers.js +21 -20
  56. package/dist/processors/gridsetProcessor.d.ts +7 -12
  57. package/dist/processors/gridsetProcessor.js +116 -77
  58. package/dist/processors/obfProcessor.d.ts +9 -7
  59. package/dist/processors/obfProcessor.js +131 -56
  60. package/dist/processors/obfsetProcessor.d.ts +5 -4
  61. package/dist/processors/obfsetProcessor.js +10 -16
  62. package/dist/processors/opmlProcessor.d.ts +5 -4
  63. package/dist/processors/opmlProcessor.js +27 -34
  64. package/dist/processors/snapProcessor.d.ts +8 -7
  65. package/dist/processors/snapProcessor.js +15 -12
  66. package/dist/processors/touchchatProcessor.d.ts +8 -7
  67. package/dist/processors/touchchatProcessor.js +22 -17
  68. package/dist/types/aac.d.ts +0 -2
  69. package/dist/types/aac.js +2 -0
  70. package/dist/utils/io.d.ts +12 -0
  71. package/dist/utils/io.js +107 -0
  72. package/dist/validation/gridsetValidator.js +7 -7
  73. package/dist/validation/snapValidator.js +28 -35
  74. package/docs/BROWSER_USAGE.md +618 -0
  75. package/examples/README.md +77 -0
  76. package/examples/browser-test-server.js +81 -0
  77. package/examples/browser-test.html +331 -0
  78. package/examples/vitedemo/QUICKSTART.md +74 -0
  79. package/examples/vitedemo/README.md +157 -0
  80. package/examples/vitedemo/index.html +376 -0
  81. package/examples/vitedemo/package-lock.json +1221 -0
  82. package/examples/vitedemo/package.json +18 -0
  83. package/examples/vitedemo/src/main.ts +519 -0
  84. package/examples/vitedemo/test-files/example.dot +14 -0
  85. package/examples/vitedemo/test-files/example.grd +1 -0
  86. package/examples/vitedemo/test-files/example.gridset +0 -0
  87. package/examples/vitedemo/test-files/example.obz +0 -0
  88. package/examples/vitedemo/test-files/example.opml +18 -0
  89. package/examples/vitedemo/test-files/simple.obf +53 -0
  90. package/examples/vitedemo/tsconfig.json +24 -0
  91. package/examples/vitedemo/vite.config.ts +34 -0
  92. 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
+ }