@willwade/aac-processors 0.1.20 → 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.
- package/dist/browser/core/baseProcessor.js +4 -0
- package/dist/browser/processors/applePanelsProcessor.js +24 -31
- package/dist/browser/processors/astericsGridProcessor.js +10 -3
- package/dist/browser/processors/dotProcessor.js +5 -2
- package/dist/browser/processors/gridset/colorUtils.js +354 -0
- package/dist/browser/processors/gridset/helpers.js +49 -45
- package/dist/browser/processors/gridset/index.js +61 -0
- package/dist/browser/processors/gridset/styleHelpers.js +205 -0
- package/dist/browser/processors/gridset/symbolExtractor.js +331 -0
- package/dist/browser/processors/gridset/symbolSearch.js +248 -0
- package/dist/browser/processors/gridset/symbols.js +35 -68
- package/dist/browser/processors/gridsetProcessor.js +32 -41
- package/dist/browser/processors/obfProcessor.js +20 -33
- package/dist/browser/processors/opmlProcessor.js +5 -2
- package/dist/browser/processors/snap/helpers.js +49 -45
- package/dist/browser/processors/snapProcessor.js +39 -42
- package/dist/browser/processors/touchchatProcessor.js +54 -45
- package/dist/browser/utilities/analytics/reference/index.js +27 -19
- package/dist/browser/utils/io.js +67 -14
- package/dist/browser/utils/sqlite.js +6 -8
- package/dist/browser/utils/zip.js +45 -43
- package/dist/browser/validation/baseValidator.js +5 -0
- package/dist/browser/validation/gridsetValidator.js +12 -20
- package/dist/browser/validation/obfValidator.js +5 -4
- package/dist/browser/validation/snapValidator.js +9 -5
- package/dist/browser/validation/touchChatValidator.js +21 -11
- package/dist/cli/index.js +10 -15
- package/dist/core/baseProcessor.d.ts +7 -7
- package/dist/core/baseProcessor.js +4 -0
- package/dist/processors/applePanelsProcessor.js +29 -36
- package/dist/processors/astericsGridProcessor.js +20 -13
- package/dist/processors/dotProcessor.js +10 -7
- package/dist/processors/excelProcessor.js +9 -12
- package/dist/processors/gridset/helpers.d.ts +9 -11
- package/dist/processors/gridset/helpers.js +49 -71
- package/dist/processors/gridset/imageDebug.d.ts +3 -5
- package/dist/processors/gridset/imageDebug.js +4 -4
- package/dist/processors/gridset/password.d.ts +1 -1
- package/dist/processors/gridset/symbolExtractor.d.ts +5 -3
- package/dist/processors/gridset/symbolExtractor.js +15 -38
- package/dist/processors/gridset/symbolSearch.d.ts +3 -2
- package/dist/processors/gridset/symbolSearch.js +12 -34
- package/dist/processors/gridset/symbols.d.ts +8 -6
- package/dist/processors/gridset/symbols.js +34 -67
- package/dist/processors/gridset/wordlistHelpers.d.ts +4 -6
- package/dist/processors/gridset/wordlistHelpers.js +15 -74
- package/dist/processors/gridsetProcessor.js +36 -68
- package/dist/processors/obfProcessor.js +26 -62
- package/dist/processors/obfsetProcessor.js +2 -2
- package/dist/processors/opmlProcessor.js +10 -7
- package/dist/processors/snap/helpers.d.ts +8 -8
- package/dist/processors/snap/helpers.js +50 -72
- package/dist/processors/snapProcessor.js +38 -41
- package/dist/processors/touchchatProcessor.js +54 -45
- package/dist/utilities/analytics/index.d.ts +3 -2
- package/dist/utilities/analytics/index.js +8 -10
- package/dist/utilities/analytics/reference/index.d.ts +5 -3
- package/dist/utilities/analytics/reference/index.js +26 -18
- package/dist/utilities/symbolTools.d.ts +4 -2
- package/dist/utilities/symbolTools.js +16 -15
- package/dist/utils/io.d.ts +24 -6
- package/dist/utils/io.js +64 -14
- package/dist/utils/sqlite.d.ts +2 -0
- package/dist/utils/sqlite.js +6 -8
- package/dist/utils/zip.d.ts +7 -3
- package/dist/utils/zip.js +45 -43
- package/dist/validation/applePanelsValidator.d.ts +2 -1
- package/dist/validation/applePanelsValidator.js +9 -11
- package/dist/validation/astericsValidator.d.ts +2 -1
- package/dist/validation/astericsValidator.js +5 -4
- package/dist/validation/baseValidator.d.ts +2 -2
- package/dist/validation/baseValidator.js +5 -0
- package/dist/validation/dotValidator.d.ts +2 -1
- package/dist/validation/dotValidator.js +5 -4
- package/dist/validation/excelValidator.d.ts +2 -1
- package/dist/validation/excelValidator.js +5 -4
- package/dist/validation/gridsetValidator.d.ts +2 -1
- package/dist/validation/gridsetValidator.js +11 -22
- package/dist/validation/index.d.ts +2 -2
- package/dist/validation/index.js +5 -4
- package/dist/validation/obfValidator.d.ts +2 -1
- package/dist/validation/obfValidator.js +5 -4
- package/dist/validation/obfsetValidator.d.ts +2 -1
- package/dist/validation/obfsetValidator.js +5 -4
- package/dist/validation/opmlValidator.d.ts +2 -1
- package/dist/validation/opmlValidator.js +5 -4
- package/dist/validation/snapValidator.d.ts +2 -1
- package/dist/validation/snapValidator.js +9 -5
- package/dist/validation/touchChatValidator.d.ts +4 -6
- package/dist/validation/touchChatValidator.js +21 -11
- package/dist/validation/validationTypes.d.ts +8 -1
- package/package.json +1 -1
- package/dist/core/fileProcessor.d.ts +0 -7
- package/dist/core/fileProcessor.js +0 -57
|
@@ -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
|
+
}
|