@willwade/aac-processors 0.1.19 → 0.1.21

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 (94) hide show
  1. package/dist/browser/core/baseProcessor.js +4 -0
  2. package/dist/browser/processors/applePanelsProcessor.js +24 -31
  3. package/dist/browser/processors/astericsGridProcessor.js +10 -3
  4. package/dist/browser/processors/dotProcessor.js +5 -2
  5. package/dist/browser/processors/gridset/colorUtils.js +354 -0
  6. package/dist/browser/processors/gridset/helpers.js +49 -45
  7. package/dist/browser/processors/gridset/index.js +61 -0
  8. package/dist/browser/processors/gridset/styleHelpers.js +205 -0
  9. package/dist/browser/processors/gridset/symbolExtractor.js +331 -0
  10. package/dist/browser/processors/gridset/symbolSearch.js +248 -0
  11. package/dist/browser/processors/gridset/symbols.js +35 -68
  12. package/dist/browser/processors/gridsetProcessor.js +32 -41
  13. package/dist/browser/processors/obfProcessor.js +53 -45
  14. package/dist/browser/processors/opmlProcessor.js +5 -2
  15. package/dist/browser/processors/snap/helpers.js +49 -45
  16. package/dist/browser/processors/snapProcessor.js +67 -31
  17. package/dist/browser/processors/touchchatProcessor.js +54 -45
  18. package/dist/browser/utilities/analytics/reference/index.js +27 -19
  19. package/dist/browser/utils/io.js +67 -14
  20. package/dist/browser/utils/sqlite.js +6 -8
  21. package/dist/browser/utils/zip.js +45 -43
  22. package/dist/browser/validation/baseValidator.js +5 -0
  23. package/dist/browser/validation/gridsetValidator.js +12 -20
  24. package/dist/browser/validation/obfValidator.js +5 -4
  25. package/dist/browser/validation/snapValidator.js +9 -5
  26. package/dist/browser/validation/touchChatValidator.js +21 -11
  27. package/dist/cli/index.js +10 -15
  28. package/dist/core/baseProcessor.d.ts +7 -7
  29. package/dist/core/baseProcessor.js +4 -0
  30. package/dist/processors/applePanelsProcessor.js +29 -36
  31. package/dist/processors/astericsGridProcessor.js +20 -13
  32. package/dist/processors/dotProcessor.js +10 -7
  33. package/dist/processors/excelProcessor.js +9 -12
  34. package/dist/processors/gridset/helpers.d.ts +9 -11
  35. package/dist/processors/gridset/helpers.js +49 -71
  36. package/dist/processors/gridset/imageDebug.d.ts +3 -5
  37. package/dist/processors/gridset/imageDebug.js +4 -4
  38. package/dist/processors/gridset/password.d.ts +1 -1
  39. package/dist/processors/gridset/symbolExtractor.d.ts +5 -3
  40. package/dist/processors/gridset/symbolExtractor.js +15 -38
  41. package/dist/processors/gridset/symbolSearch.d.ts +3 -2
  42. package/dist/processors/gridset/symbolSearch.js +12 -34
  43. package/dist/processors/gridset/symbols.d.ts +8 -6
  44. package/dist/processors/gridset/symbols.js +34 -67
  45. package/dist/processors/gridset/wordlistHelpers.d.ts +4 -6
  46. package/dist/processors/gridset/wordlistHelpers.js +15 -74
  47. package/dist/processors/gridsetProcessor.js +36 -68
  48. package/dist/processors/obfProcessor.js +58 -73
  49. package/dist/processors/obfsetProcessor.js +2 -2
  50. package/dist/processors/opmlProcessor.js +10 -7
  51. package/dist/processors/snap/helpers.d.ts +8 -8
  52. package/dist/processors/snap/helpers.js +50 -72
  53. package/dist/processors/snapProcessor.js +66 -30
  54. package/dist/processors/touchchatProcessor.js +54 -45
  55. package/dist/utilities/analytics/index.d.ts +3 -2
  56. package/dist/utilities/analytics/index.js +8 -10
  57. package/dist/utilities/analytics/reference/index.d.ts +5 -3
  58. package/dist/utilities/analytics/reference/index.js +26 -18
  59. package/dist/utilities/symbolTools.d.ts +4 -2
  60. package/dist/utilities/symbolTools.js +16 -15
  61. package/dist/utils/io.d.ts +24 -6
  62. package/dist/utils/io.js +64 -14
  63. package/dist/utils/sqlite.d.ts +2 -0
  64. package/dist/utils/sqlite.js +6 -8
  65. package/dist/utils/zip.d.ts +7 -3
  66. package/dist/utils/zip.js +45 -43
  67. package/dist/validation/applePanelsValidator.d.ts +2 -1
  68. package/dist/validation/applePanelsValidator.js +9 -11
  69. package/dist/validation/astericsValidator.d.ts +2 -1
  70. package/dist/validation/astericsValidator.js +5 -4
  71. package/dist/validation/baseValidator.d.ts +2 -2
  72. package/dist/validation/baseValidator.js +5 -0
  73. package/dist/validation/dotValidator.d.ts +2 -1
  74. package/dist/validation/dotValidator.js +5 -4
  75. package/dist/validation/excelValidator.d.ts +2 -1
  76. package/dist/validation/excelValidator.js +5 -4
  77. package/dist/validation/gridsetValidator.d.ts +2 -1
  78. package/dist/validation/gridsetValidator.js +11 -22
  79. package/dist/validation/index.d.ts +2 -2
  80. package/dist/validation/index.js +5 -4
  81. package/dist/validation/obfValidator.d.ts +2 -1
  82. package/dist/validation/obfValidator.js +5 -4
  83. package/dist/validation/obfsetValidator.d.ts +2 -1
  84. package/dist/validation/obfsetValidator.js +5 -4
  85. package/dist/validation/opmlValidator.d.ts +2 -1
  86. package/dist/validation/opmlValidator.js +5 -4
  87. package/dist/validation/snapValidator.d.ts +2 -1
  88. package/dist/validation/snapValidator.js +9 -5
  89. package/dist/validation/touchChatValidator.d.ts +4 -6
  90. package/dist/validation/touchChatValidator.js +21 -11
  91. package/dist/validation/validationTypes.d.ts +8 -1
  92. package/package.json +1 -1
  93. package/dist/core/fileProcessor.d.ts +0 -7
  94. package/dist/core/fileProcessor.js +0 -52
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Grid 3 Symbol Extraction Strategy
3
+ *
4
+ * For converting Grid 3 gridsets to other formats (like Asterics),
5
+ * we need to handle symbol library references properly.
6
+ *
7
+ * Strategy:
8
+ * 1. Check if image is embedded in gridset (extract directly)
9
+ * 2. If symbol library reference:
10
+ * a. Check if we can extract from .pix file (limited support)
11
+ * b. Provide reference/URL for manual resolution
12
+ * c. For Tawasol: provide alternative sources
13
+ */
14
+ import { resolveSymbolReference, parseSymbolReference } from './symbols';
15
+ import { defaultFileAdapter } from '../../utils/io';
16
+ import { getZipAdapter } from '../../utils/zip';
17
+ /**
18
+ * Known open-license symbol sources
19
+ */
20
+ const OPEN_LICENSE_SYMBOLS = {
21
+ tawasl: {
22
+ name: 'Tawasol',
23
+ attribution: 'Tawasol symbols by Mada (Qatar Assistive Technology Center)',
24
+ license: 'CC BY-SA 4.0',
25
+ url: 'https://mada.org.qa/en/resources/tawasol-symbols',
26
+ alternativeSources: ['https://github.com/mada-qatar/Tawasol'],
27
+ },
28
+ blissx: {
29
+ name: 'Blissymbols',
30
+ attribution: 'Blissymbolics Communication International',
31
+ license: 'CC BY-ND 3.0',
32
+ url: 'https://blissymbolics.org',
33
+ },
34
+ symoji: {
35
+ name: 'Symoji',
36
+ attribution: 'Smartbox Assistive Technology',
37
+ license: 'Proprietary - Free use in Grid 3',
38
+ },
39
+ };
40
+ /**
41
+ * Extract image data for a button
42
+ * @param gridsetBuffer - Gridset ZIP buffer
43
+ * @param resolvedImageEntry - Path to embedded image in gridset
44
+ * @param symbolReference - Symbol library reference
45
+ * @param options - Extraction options
46
+ * @returns Extracted image data
47
+ */
48
+ export async function extractButtonImage(gridsetBuffer, resolvedImageEntry, symbolReference, options = {}, fileAdapter = defaultFileAdapter, zipAdapter) {
49
+ // Priority 1: Use embedded image if available
50
+ if (resolvedImageEntry && options.preferEmbedded !== false) {
51
+ try {
52
+ const zip = zipAdapter
53
+ ? await zipAdapter(gridsetBuffer)
54
+ : await getZipAdapter(gridsetBuffer, fileAdapter);
55
+ const entries = zip.listFiles();
56
+ const entry = entries.find((e) => e === resolvedImageEntry);
57
+ if (entry) {
58
+ const data = Buffer.from(await zip.readFile(entry));
59
+ const format = detectImageFormat(data);
60
+ return {
61
+ found: true,
62
+ data,
63
+ format,
64
+ source: 'embedded',
65
+ reference: resolvedImageEntry,
66
+ };
67
+ }
68
+ }
69
+ catch (error) {
70
+ console.warn(`Failed to extract embedded image: ${String(error)}`);
71
+ }
72
+ }
73
+ // Priority 2: Check symbol library reference
74
+ if (symbolReference) {
75
+ return await extractSymbolLibraryImage(symbolReference, options);
76
+ }
77
+ // Not found
78
+ return {
79
+ found: false,
80
+ source: 'not-found',
81
+ };
82
+ }
83
+ /**
84
+ * Extract image from symbol library
85
+ * @param reference - Symbol reference like "[tawasl]/food/apple.png"
86
+ * @param options - Extraction options
87
+ * @returns Extracted image or reference info
88
+ */
89
+ export async function extractSymbolLibraryImage(reference, options = {}) {
90
+ const ref = parseSymbolReferenceSafe(reference);
91
+ if (!ref || !ref.isValid) {
92
+ return {
93
+ found: false,
94
+ source: 'not-found',
95
+ reference,
96
+ };
97
+ }
98
+ // Get library metadata
99
+ const libInfo = OPEN_LICENSE_SYMBOLS[ref.library];
100
+ // Resolve symbol reference and extract from .symbols file
101
+ const resolved = await resolveSymbolReference(reference, {
102
+ grid3Path: options.grid3Path,
103
+ });
104
+ const metadata = {
105
+ library: ref.library,
106
+ symbolPath: ref.path,
107
+ attribution: libInfo?.attribution,
108
+ license: libInfo?.license,
109
+ };
110
+ if (!resolved.found) {
111
+ // Symbol not found in library
112
+ if (options.onMissingSymbol) {
113
+ options.onMissingSymbol(ref);
114
+ }
115
+ return {
116
+ found: false,
117
+ source: 'symbol-library',
118
+ reference: reference,
119
+ metadata,
120
+ error: resolved.error,
121
+ };
122
+ }
123
+ // Successfully extracted!
124
+ const data = resolved.data;
125
+ const format = data ? detectImageFormat(data) : 'unknown';
126
+ return {
127
+ found: true,
128
+ data,
129
+ format,
130
+ source: 'symbol-library',
131
+ reference: reference,
132
+ metadata,
133
+ };
134
+ }
135
+ /**
136
+ * Convert extracted image to Asterics Grid format
137
+ * @param extracted - Extracted image
138
+ * @returns GridImage object for Asterics
139
+ */
140
+ export function convertToAstericsImage(extracted) {
141
+ const image = {};
142
+ if (extracted.found && extracted.data) {
143
+ // Embed as base64
144
+ image.data = Buffer.from(extracted.data).toString('base64');
145
+ }
146
+ // Even if embedded, add attribution for symbol libraries
147
+ if (extracted.source === 'symbol-library') {
148
+ if (extracted.metadata?.attribution) {
149
+ image.author = extracted.metadata.attribution;
150
+ }
151
+ if (extracted.metadata?.license) {
152
+ image.searchProviderName = extracted.metadata.license;
153
+ }
154
+ }
155
+ // If not found but we have a reference, keep it for manual handling
156
+ if (!extracted.found && extracted.reference) {
157
+ image.url = `symbol:${extracted.reference}`;
158
+ if (extracted.metadata?.attribution) {
159
+ image.author = extracted.metadata.attribution;
160
+ }
161
+ }
162
+ return image;
163
+ }
164
+ /**
165
+ * Analyze symbol usage for a gridset
166
+ * @param tree - AAC tree
167
+ * @returns Symbol usage report
168
+ */
169
+ export function analyzeSymbolExtraction(tree) {
170
+ const report = {
171
+ total: 0,
172
+ embedded: 0,
173
+ symbolLibraries: 0,
174
+ notFound: 0,
175
+ byLibrary: {},
176
+ missingSymbols: [],
177
+ };
178
+ for (const pageId in tree.pages) {
179
+ const page = tree.pages[pageId];
180
+ if (page.buttons) {
181
+ for (const button of page.buttons) {
182
+ report.total++;
183
+ // Embedded image
184
+ if (button.resolvedImageEntry && !button.symbolLibrary) {
185
+ report.embedded++;
186
+ continue;
187
+ }
188
+ // Symbol library reference
189
+ if (button.symbolLibrary) {
190
+ report.symbolLibraries++;
191
+ report.byLibrary[button.symbolLibrary] =
192
+ (report.byLibrary[button.symbolLibrary] || 0) + 1;
193
+ const ref = `[${button.symbolLibrary}]${button.symbolPath || ''}`;
194
+ const libInfo = OPEN_LICENSE_SYMBOLS[button.symbolLibrary];
195
+ report.missingSymbols.push({
196
+ reference: ref,
197
+ library: button.symbolLibrary,
198
+ path: button.symbolPath || '',
199
+ attribution: libInfo?.attribution,
200
+ license: libInfo?.license,
201
+ });
202
+ continue;
203
+ }
204
+ // Not found
205
+ if (!button.resolvedImageEntry && !button.symbolLibrary) {
206
+ report.notFound++;
207
+ }
208
+ }
209
+ }
210
+ }
211
+ return report;
212
+ }
213
+ /**
214
+ * Suggest extraction strategy based on report
215
+ */
216
+ export function suggestExtractionStrategy(report) {
217
+ const suggestions = [];
218
+ if (report.embedded > 0) {
219
+ suggestions.push(`✓ Can extract ${report.embedded} embedded images directly`);
220
+ }
221
+ if (report.symbolLibraries > 0) {
222
+ suggestions.push(`⚠ ${report.symbolLibraries} symbol library references found:`);
223
+ Object.entries(report.byLibrary).forEach(([lib, count]) => {
224
+ const libInfo = OPEN_LICENSE_SYMBOLS[lib];
225
+ if (libInfo) {
226
+ suggestions.push(` - ${lib}: ${count} symbols (${libInfo.license})`);
227
+ if (libInfo.alternativeSources) {
228
+ suggestions.push(` Alternative: ${libInfo.alternativeSources.join(', ')}`);
229
+ }
230
+ }
231
+ else {
232
+ suggestions.push(` - ${lib}: ${count} symbols (Proprietary - requires Grid 3)`);
233
+ }
234
+ });
235
+ }
236
+ if (report.notFound > 0) {
237
+ suggestions.push(`✗ ${report.notFound} images not found`);
238
+ }
239
+ return suggestions.join('\n');
240
+ }
241
+ /**
242
+ * Detect image format from buffer
243
+ */
244
+ function detectImageFormat(buffer) {
245
+ if (buffer.length < 4)
246
+ return 'unknown';
247
+ // PNG: 89 50 4E 47
248
+ if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) {
249
+ return 'png';
250
+ }
251
+ // JPEG: FF D8 FF
252
+ if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
253
+ return 'jpg';
254
+ }
255
+ // GIF: 47 49 46 38
256
+ if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x38) {
257
+ return 'gif';
258
+ }
259
+ // SVG (check for <svg text)
260
+ const header = buffer.slice(0, Math.min(100, buffer.length)).toString('ascii').toLowerCase();
261
+ if (header.includes('<svg')) {
262
+ return 'svg';
263
+ }
264
+ return 'unknown';
265
+ }
266
+ /**
267
+ * Safe parse of symbol reference
268
+ */
269
+ function parseSymbolReferenceSafe(reference) {
270
+ try {
271
+ return parseSymbolReference(reference);
272
+ }
273
+ catch {
274
+ return null;
275
+ }
276
+ }
277
+ /**
278
+ * Export symbol references to CSV for manual extraction
279
+ */
280
+ export function exportSymbolReferencesToCsv(report, outputPath, fileAdapter = defaultFileAdapter) {
281
+ const { writeTextToPath } = fileAdapter;
282
+ const lines = ['Reference,Library,Path,Attribution,License'];
283
+ for (const symbol of report.missingSymbols) {
284
+ lines.push(`"${symbol.reference}","${symbol.library}","${symbol.path}","${symbol.attribution || ''}","${symbol.license || ''}"`);
285
+ }
286
+ writeTextToPath(outputPath, lines.join('\n'));
287
+ }
288
+ export function createSymbolManifest(tree, gridsetName) {
289
+ const manifest = {
290
+ generatedAt: new Date().toISOString(),
291
+ gridset: gridsetName,
292
+ totalSymbols: 0,
293
+ embedded: 0,
294
+ fromLibraries: 0,
295
+ libraries: {},
296
+ symbols: [],
297
+ };
298
+ for (const pageId in tree.pages) {
299
+ const page = tree.pages[pageId];
300
+ if (page.buttons) {
301
+ for (const button of page.buttons) {
302
+ manifest.totalSymbols++;
303
+ if (button.resolvedImageEntry && !button.symbolLibrary) {
304
+ manifest.embedded++;
305
+ continue;
306
+ }
307
+ if (button.symbolLibrary) {
308
+ manifest.fromLibraries++;
309
+ if (!manifest.libraries[button.symbolLibrary]) {
310
+ const libInfo = OPEN_LICENSE_SYMBOLS[button.symbolLibrary];
311
+ manifest.libraries[button.symbolLibrary] = {
312
+ count: 0,
313
+ attribution: libInfo?.attribution,
314
+ license: libInfo?.license,
315
+ url: libInfo?.url,
316
+ };
317
+ }
318
+ manifest.libraries[button.symbolLibrary].count++;
319
+ const ref = `[${button.symbolLibrary}]${button.symbolPath || ''}`;
320
+ manifest.symbols.push({
321
+ pageId,
322
+ buttonId: button.id,
323
+ reference: ref,
324
+ label: button.label,
325
+ });
326
+ }
327
+ }
328
+ }
329
+ }
330
+ return manifest;
331
+ }
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Grid 3 Symbol Search Implementation
3
+ *
4
+ * The .pix files are simple text mappings:
5
+ * searchTerm=symbolFilename=searchTerm
6
+ *
7
+ * Example:
8
+ * above bw=above bw.png=above bw
9
+ * active family=active family.png=active family
10
+ */
11
+ import { defaultFileAdapter } from '../../utils/io';
12
+ /**
13
+ * Parse a .pix file into search index
14
+ * @param pixFilePath - Path to .pix file
15
+ * @returns Search index
16
+ */
17
+ export function parsePixFile(pixFilePath, fileAdapter = defaultFileAdapter) {
18
+ const { readTextFromInput, basename } = fileAdapter;
19
+ const content = readTextFromInput(pixFilePath);
20
+ const library = basename(pixFilePath, '.pix');
21
+ const searchTerms = new Map();
22
+ const filenames = new Map();
23
+ const lines = content.split('\n');
24
+ for (const line of lines) {
25
+ const trimmed = line.trim();
26
+ if (!trimmed || trimmed.startsWith('encoding=')) {
27
+ continue;
28
+ }
29
+ // Format: searchTerm=symbolFilename=searchTerm
30
+ const parts = trimmed.split('=');
31
+ if (parts.length >= 3) {
32
+ const searchTerm = parts[0];
33
+ const symbolFilename = parts[1];
34
+ const displayName = parts[2];
35
+ searchTerms.set(searchTerm.toLowerCase(), symbolFilename);
36
+ filenames.set(symbolFilename, displayName || searchTerm);
37
+ }
38
+ }
39
+ return { library, searchTerms, filenames };
40
+ }
41
+ /**
42
+ * Load search indexes for all available libraries
43
+ * @param options - Search options
44
+ * @returns Map of library name to search index
45
+ */
46
+ export function loadSearchIndexes(options = {}, fileAdapter = defaultFileAdapter) {
47
+ const { listDir, pathExists, join, basename } = fileAdapter;
48
+ const { grid3Path, locale = 'en-GB', libraries: specifiedLibs } = options;
49
+ if (!grid3Path) {
50
+ throw new Error('grid3Path is required for symbol search');
51
+ }
52
+ const searchIndexesDir = join(grid3Path, 'Locale', locale, 'symbolsearch');
53
+ if (!pathExists(searchIndexesDir)) {
54
+ throw new Error(`Symbol search directory not found: ${searchIndexesDir}`);
55
+ }
56
+ const indexes = new Map();
57
+ const files = listDir(searchIndexesDir);
58
+ for (const file of files) {
59
+ if (!file.endsWith('.pix')) {
60
+ continue;
61
+ }
62
+ const libraryName = basename(file, '.pix');
63
+ // Filter libraries if specified
64
+ if (specifiedLibs && specifiedLibs.length > 0) {
65
+ if (!specifiedLibs.some((lib) => lib.toLowerCase() === libraryName.toLowerCase())) {
66
+ continue;
67
+ }
68
+ }
69
+ try {
70
+ const pixFilePath = join(searchIndexesDir, file);
71
+ const index = parsePixFile(pixFilePath);
72
+ indexes.set(libraryName, index);
73
+ }
74
+ catch (error) {
75
+ console.warn(`Failed to load index for ${libraryName}:`, error);
76
+ }
77
+ }
78
+ return indexes;
79
+ }
80
+ /**
81
+ * Search for symbols by term
82
+ * @param searchTerm - Term to search for
83
+ * @param options - Search options
84
+ * @returns Array of search results
85
+ */
86
+ export function searchSymbols(searchTerm, options = {}) {
87
+ const indexes = loadSearchIndexes(options);
88
+ const results = [];
89
+ const lowerSearchTerm = searchTerm.toLowerCase().trim();
90
+ const limit = options.limit || 100;
91
+ for (const [libraryName, index] of indexes.entries()) {
92
+ // Exact match first
93
+ if (index.searchTerms.has(lowerSearchTerm)) {
94
+ const symbolFilename = index.searchTerms.get(lowerSearchTerm);
95
+ if (symbolFilename) {
96
+ results.push({
97
+ searchTerm: lowerSearchTerm,
98
+ symbolFilename,
99
+ displayName: index.filenames.get(symbolFilename) || lowerSearchTerm,
100
+ library: libraryName,
101
+ exactMatch: true,
102
+ });
103
+ }
104
+ }
105
+ // Fuzzy match if enabled
106
+ if (options.fuzzyMatch !== false) {
107
+ for (const [term, symbolFilename] of index.searchTerms.entries()) {
108
+ if (term.includes(lowerSearchTerm) || lowerSearchTerm.includes(term)) {
109
+ // Skip if already added as exact match
110
+ if (results.some((r) => r.library === libraryName && r.symbolFilename === symbolFilename)) {
111
+ continue;
112
+ }
113
+ results.push({
114
+ searchTerm: lowerSearchTerm,
115
+ symbolFilename,
116
+ displayName: index.filenames.get(symbolFilename) || term,
117
+ library: libraryName,
118
+ exactMatch: false,
119
+ });
120
+ }
121
+ }
122
+ }
123
+ }
124
+ // Sort by exact match first, then by library
125
+ results.sort((a, b) => {
126
+ if (a.exactMatch !== b.exactMatch) {
127
+ return a.exactMatch ? -1 : 1;
128
+ }
129
+ return a.library.localeCompare(b.library);
130
+ });
131
+ return results.slice(0, limit);
132
+ }
133
+ /**
134
+ * Get symbol filename for a specific search term
135
+ * @param searchTerm - Search term to look up
136
+ * @param library - Library name
137
+ * @param options - Search options
138
+ * @returns Symbol filename or undefined
139
+ */
140
+ export function getSymbolFilename(searchTerm, library, options = {}) {
141
+ const indexes = loadSearchIndexes({
142
+ ...options,
143
+ libraries: [library],
144
+ });
145
+ const index = indexes.get(library.toLowerCase());
146
+ if (!index) {
147
+ return undefined;
148
+ }
149
+ return index.searchTerms.get(searchTerm.toLowerCase());
150
+ }
151
+ /**
152
+ * Get display name for a symbol filename
153
+ * @param symbolFilename - Symbol filename (e.g., "above bw.png")
154
+ * @param library - Library name
155
+ * @param options - Search options
156
+ * @returns Display name or undefined
157
+ */
158
+ export function getSymbolDisplayName(symbolFilename, library, options = {}) {
159
+ const indexes = loadSearchIndexes({
160
+ ...options,
161
+ libraries: [library],
162
+ });
163
+ const index = indexes.get(library.toLowerCase());
164
+ if (!index) {
165
+ return undefined;
166
+ }
167
+ return index.filenames.get(symbolFilename);
168
+ }
169
+ /**
170
+ * Get all search terms for a library
171
+ * @param library - Library name
172
+ * @param options - Search options
173
+ * @returns Array of search terms
174
+ */
175
+ export function getAllSearchTerms(library, options = {}) {
176
+ const indexes = loadSearchIndexes({
177
+ ...options,
178
+ libraries: [library],
179
+ });
180
+ const index = indexes.get(library.toLowerCase());
181
+ if (!index) {
182
+ return [];
183
+ }
184
+ return Array.from(index.searchTerms.keys());
185
+ }
186
+ /**
187
+ * Search suggestions (autocomplete)
188
+ * @param partialTerm - Partial search term
189
+ * @param options - Search options
190
+ * @returns Array of suggested terms
191
+ */
192
+ export function getSearchSuggestions(partialTerm, options = {}) {
193
+ const indexes = loadSearchIndexes(options);
194
+ const suggestions = new Set();
195
+ const lowerPartial = partialTerm.toLowerCase().trim();
196
+ for (const index of indexes.values()) {
197
+ for (const term of index.searchTerms.keys()) {
198
+ if (term.startsWith(lowerPartial)) {
199
+ suggestions.add(term);
200
+ }
201
+ }
202
+ }
203
+ return Array.from(suggestions).sort().slice(0, 20);
204
+ }
205
+ /**
206
+ * Search for symbols and return results with library references
207
+ * @param searchTerm - Term to search for
208
+ * @param options - Search options
209
+ * @returns Array of full symbol references
210
+ */
211
+ export function searchSymbolsWithReferences(searchTerm, options = {}) {
212
+ const results = searchSymbols(searchTerm, options);
213
+ return results.map((r) => `[${r.library}]${r.symbolFilename}`);
214
+ }
215
+ /**
216
+ * Count symbols in each library
217
+ * @param options - Search options
218
+ * @returns Map of library name to symbol count
219
+ */
220
+ export function countLibrarySymbols(options = {}) {
221
+ const indexes = loadSearchIndexes(options);
222
+ const counts = new Map();
223
+ for (const [libraryName, index] of indexes.entries()) {
224
+ counts.set(libraryName, index.searchTerms.size);
225
+ }
226
+ return counts;
227
+ }
228
+ /**
229
+ * Get symbol search statistics
230
+ * @param options - Search options
231
+ * @returns Statistics about available symbols
232
+ */
233
+ export function getSymbolSearchStats(options = {}) {
234
+ const indexes = loadSearchIndexes(options);
235
+ const stats = {
236
+ totalLibraries: indexes.size,
237
+ totalSymbols: 0,
238
+ libraries: {},
239
+ };
240
+ for (const [libraryName, index] of indexes.entries()) {
241
+ stats.totalSymbols += index.searchTerms.size;
242
+ stats.libraries[libraryName] = {
243
+ symbolCount: index.searchTerms.size,
244
+ exampleTerms: Array.from(index.searchTerms.keys()).slice(0, 10),
245
+ };
246
+ }
247
+ return stats;
248
+ }