@willwade/aac-processors 0.1.21 → 0.2.1

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 (87) hide show
  1. package/README.md +3 -2
  2. package/dist/browser/processors/applePanelsProcessor.js +24 -24
  3. package/dist/browser/processors/astericsGridProcessor.js +22 -24
  4. package/dist/browser/processors/dotProcessor.js +6 -10
  5. package/dist/browser/processors/gridset/helpers.js +33 -30
  6. package/dist/browser/processors/gridset/symbolExtractor.js +2 -2
  7. package/dist/browser/processors/gridset/symbolSearch.js +22 -22
  8. package/dist/browser/processors/gridset/symbols.js +14 -14
  9. package/dist/browser/processors/gridsetProcessor.js +7 -7
  10. package/dist/browser/processors/obfProcessor.js +54 -47
  11. package/dist/browser/processors/opmlProcessor.js +6 -10
  12. package/dist/browser/processors/snap/helpers.js +34 -30
  13. package/dist/browser/processors/snapProcessor.js +28 -28
  14. package/dist/browser/processors/touchchatProcessor.js +24 -25
  15. package/dist/browser/utilities/analytics/history.js +24 -18
  16. package/dist/browser/utilities/analytics/metrics/comparison.js +16 -16
  17. package/dist/browser/utilities/analytics/metrics/vocabulary.js +2 -2
  18. package/dist/browser/utilities/analytics/reference/browser.js +16 -16
  19. package/dist/browser/utilities/analytics/reference/index.js +25 -24
  20. package/dist/browser/utils/io.js +29 -25
  21. package/dist/browser/utils/sqlite.js +5 -5
  22. package/dist/browser/utils/zip.js +2 -4
  23. package/dist/browser/validation/gridsetValidator.js +2 -2
  24. package/dist/browser/validation/obfValidator.js +2 -2
  25. package/dist/browser/validation/snapValidator.js +3 -3
  26. package/dist/browser/validation/touchChatValidator.js +3 -3
  27. package/dist/cli/index.js +19 -16
  28. package/dist/core/baseProcessor.d.ts +1 -1
  29. package/dist/processors/applePanelsProcessor.js +24 -24
  30. package/dist/processors/astericsGridProcessor.d.ts +4 -4
  31. package/dist/processors/astericsGridProcessor.js +22 -24
  32. package/dist/processors/dotProcessor.js +6 -10
  33. package/dist/processors/excelProcessor.d.ts +3 -3
  34. package/dist/processors/excelProcessor.js +10 -13
  35. package/dist/processors/gridset/helpers.d.ts +9 -9
  36. package/dist/processors/gridset/helpers.js +33 -30
  37. package/dist/processors/gridset/symbolExtractor.d.ts +1 -1
  38. package/dist/processors/gridset/symbolExtractor.js +2 -2
  39. package/dist/processors/gridset/symbolSearch.d.ts +10 -10
  40. package/dist/processors/gridset/symbolSearch.js +22 -22
  41. package/dist/processors/gridset/symbols.d.ts +3 -3
  42. package/dist/processors/gridset/symbols.js +14 -14
  43. package/dist/processors/gridsetProcessor.d.ts +2 -2
  44. package/dist/processors/gridsetProcessor.js +7 -7
  45. package/dist/processors/obfProcessor.d.ts +2 -2
  46. package/dist/processors/obfProcessor.js +54 -47
  47. package/dist/processors/obfsetProcessor.js +1 -2
  48. package/dist/processors/opmlProcessor.js +6 -10
  49. package/dist/processors/snap/helpers.d.ts +8 -8
  50. package/dist/processors/snap/helpers.js +34 -30
  51. package/dist/processors/snapProcessor.d.ts +2 -2
  52. package/dist/processors/snapProcessor.js +28 -28
  53. package/dist/processors/touchchatProcessor.d.ts +2 -2
  54. package/dist/processors/touchchatProcessor.js +24 -25
  55. package/dist/types/aac.d.ts +2 -2
  56. package/dist/utilities/analytics/history.d.ts +8 -8
  57. package/dist/utilities/analytics/history.js +24 -18
  58. package/dist/utilities/analytics/index.d.ts +1 -1
  59. package/dist/utilities/analytics/index.js +3 -2
  60. package/dist/utilities/analytics/metrics/comparison.d.ts +1 -1
  61. package/dist/utilities/analytics/metrics/comparison.js +16 -16
  62. package/dist/utilities/analytics/metrics/vocabulary.d.ts +1 -1
  63. package/dist/utilities/analytics/metrics/vocabulary.js +2 -2
  64. package/dist/utilities/analytics/reference/browser.d.ts +9 -9
  65. package/dist/utilities/analytics/reference/browser.js +16 -16
  66. package/dist/utilities/analytics/reference/index.d.ts +21 -21
  67. package/dist/utilities/analytics/reference/index.js +25 -24
  68. package/dist/utilities/symbolTools.d.ts +5 -5
  69. package/dist/utilities/symbolTools.js +10 -8
  70. package/dist/utils/io.d.ts +11 -11
  71. package/dist/utils/io.js +29 -25
  72. package/dist/utils/sqlite.d.ts +1 -1
  73. package/dist/utils/sqlite.js +5 -5
  74. package/dist/utils/zip.js +2 -4
  75. package/dist/validation/applePanelsValidator.js +7 -6
  76. package/dist/validation/astericsValidator.js +2 -2
  77. package/dist/validation/dotValidator.js +2 -2
  78. package/dist/validation/excelValidator.js +2 -2
  79. package/dist/validation/gridsetValidator.js +2 -2
  80. package/dist/validation/index.js +2 -2
  81. package/dist/validation/obfValidator.js +2 -2
  82. package/dist/validation/obfsetValidator.js +2 -2
  83. package/dist/validation/opmlValidator.js +2 -2
  84. package/dist/validation/snapValidator.js +3 -3
  85. package/dist/validation/touchChatValidator.js +3 -3
  86. package/docs/BROWSER_USAGE.md +0 -40
  87. package/package.json +1 -1
@@ -93,12 +93,12 @@ export function isSymbolReference(reference) {
93
93
  * Get the default Grid 3 installation path for the current platform
94
94
  * @returns Default Grid 3 path or empty string if not found
95
95
  */
96
- export function getDefaultGrid3Path(fileAdapter) {
96
+ export async function getDefaultGrid3Path(fileAdapter) {
97
97
  const { pathExists } = fileAdapter ?? defaultFileAdapter;
98
98
  const platform = (typeof process !== 'undefined' && process.platform ? process.platform : 'unknown');
99
99
  const defaultPath = DEFAULT_GRID3_PATHS[platform] || '';
100
100
  try {
101
- if (defaultPath && pathExists(defaultPath)) {
101
+ if (defaultPath && (await pathExists(defaultPath))) {
102
102
  return defaultPath;
103
103
  }
104
104
  // Try to find Grid 3 in common locations
@@ -110,7 +110,7 @@ export function getDefaultGrid3Path(fileAdapter) {
110
110
  '/opt/smartbox/grid3',
111
111
  ];
112
112
  for (const testPath of commonPaths) {
113
- if (pathExists(testPath)) {
113
+ if (await pathExists(testPath)) {
114
114
  return testPath;
115
115
  }
116
116
  }
@@ -146,22 +146,22 @@ export function getSymbolSearchIndexesDir(grid3Path, locale = DEFAULT_LOCALE, fi
146
146
  * @param options - Resolution options
147
147
  * @returns Array of symbol library information
148
148
  */
149
- export function getAvailableSymbolLibraries(options = {}, fileAdapter) {
149
+ export async function getAvailableSymbolLibraries(options = {}, fileAdapter) {
150
150
  const { pathExists, getFileSize, listDir, join, basename } = fileAdapter ?? defaultFileAdapter;
151
- const grid3Path = options.grid3Path || options.symbolDir || getDefaultGrid3Path();
151
+ const grid3Path = options.grid3Path || options.symbolDir || (await getDefaultGrid3Path());
152
152
  if (!grid3Path) {
153
153
  return [];
154
154
  }
155
155
  const symbolsDir = getSymbolLibrariesDir(grid3Path, fileAdapter);
156
- if (!pathExists(symbolsDir)) {
156
+ if (!(await pathExists(symbolsDir))) {
157
157
  return [];
158
158
  }
159
159
  const libraries = [];
160
- const files = listDir(symbolsDir);
160
+ const files = await listDir(symbolsDir);
161
161
  for (const file of files) {
162
162
  if (file.endsWith('.symbols')) {
163
163
  const fullPath = join(symbolsDir, file);
164
- const size = getFileSize(fullPath);
164
+ const size = await getFileSize(fullPath);
165
165
  const libraryName = basename(file, '.symbols');
166
166
  libraries.push({
167
167
  name: libraryName,
@@ -180,9 +180,9 @@ export function getAvailableSymbolLibraries(options = {}, fileAdapter) {
180
180
  * @param options - Resolution options
181
181
  * @returns Symbol library info or undefined if not found
182
182
  */
183
- export function getSymbolLibraryInfo(libraryName, options = {}, fileAdapter) {
183
+ export async function getSymbolLibraryInfo(libraryName, options = {}, fileAdapter) {
184
184
  const { pathExists, getFileSize, join } = fileAdapter ?? defaultFileAdapter;
185
- const grid3Path = options.grid3Path || options.symbolDir || getDefaultGrid3Path();
185
+ const grid3Path = options.grid3Path || options.symbolDir || (await getDefaultGrid3Path());
186
186
  if (!grid3Path) {
187
187
  return undefined;
188
188
  }
@@ -196,8 +196,8 @@ export function getSymbolLibraryInfo(libraryName, options = {}, fileAdapter) {
196
196
  ];
197
197
  for (const file of variations) {
198
198
  const fullPath = join(symbolsDir, file);
199
- if (pathExists(fullPath)) {
200
- const size = getFileSize(fullPath);
199
+ if (await pathExists(fullPath)) {
200
+ const size = await getFileSize(fullPath);
201
201
  return {
202
202
  name: libraryName,
203
203
  pixFile: fullPath,
@@ -224,7 +224,7 @@ export async function resolveSymbolReference(reference, options = {}, fileAdapte
224
224
  error: 'Invalid symbol reference format',
225
225
  };
226
226
  }
227
- const grid3Path = options.grid3Path || getDefaultGrid3Path();
227
+ const grid3Path = options.grid3Path || (await getDefaultGrid3Path());
228
228
  if (!grid3Path) {
229
229
  return {
230
230
  reference: parsed,
@@ -232,7 +232,7 @@ export async function resolveSymbolReference(reference, options = {}, fileAdapte
232
232
  error: 'Grid 3 installation not found. Please specify grid3Path.',
233
233
  };
234
234
  }
235
- const libraryInfo = getSymbolLibraryInfo(parsed.library, { grid3Path });
235
+ const libraryInfo = await getSymbolLibraryInfo(parsed.library, { grid3Path });
236
236
  if (!libraryInfo || !libraryInfo.exists) {
237
237
  return {
238
238
  reference: parsed,
@@ -398,7 +398,7 @@ class GridsetProcessor extends BaseProcessor {
398
398
  const tree = new AACTree();
399
399
  let zipResult;
400
400
  try {
401
- const zipInput = readBinaryFromInput(filePathOrBuffer);
401
+ const zipInput = await readBinaryFromInput(filePathOrBuffer);
402
402
  zipResult = await this.options.zipAdapter(zipInput);
403
403
  }
404
404
  catch (error) {
@@ -1742,7 +1742,7 @@ class GridsetProcessor extends BaseProcessor {
1742
1742
  });
1743
1743
  // Save the translated tree and return its content
1744
1744
  await this.saveFromTree(tree, outputPath);
1745
- return readBinaryFromInput(outputPath);
1745
+ return await readBinaryFromInput(outputPath);
1746
1746
  }
1747
1747
  /**
1748
1748
  * Extract symbol information from a gridset for LLM-based translation.
@@ -1751,7 +1751,7 @@ class GridsetProcessor extends BaseProcessor {
1751
1751
  * This method uses shared translation utilities that work across all AAC formats.
1752
1752
  *
1753
1753
  * @param filePathOrBuffer - Path to gridset file or buffer
1754
- * @returns Array of symbol information for LLM processing
1754
+ * @returns Promise resolving to symbol information for LLM processing
1755
1755
  */
1756
1756
  async extractSymbolsForLLM(filePathOrBuffer) {
1757
1757
  const tree = await this.loadIntoTree(filePathOrBuffer);
@@ -1781,7 +1781,7 @@ class GridsetProcessor extends BaseProcessor {
1781
1781
  * @param llmTranslations - Array of LLM translations with symbol info
1782
1782
  * @param outputPath - Where to save the translated gridset
1783
1783
  * @param options - Translation options (e.g., allowPartial for testing)
1784
- * @returns Buffer of the translated gridset
1784
+ * @returns Promise resolving to a buffer of the translated gridset
1785
1785
  */
1786
1786
  async processLLMTranslations(filePathOrBuffer, llmTranslations, outputPath, options) {
1787
1787
  const { readBinaryFromInput } = this.options.fileAdapter;
@@ -1823,7 +1823,7 @@ class GridsetProcessor extends BaseProcessor {
1823
1823
  });
1824
1824
  // Save and return
1825
1825
  await this.saveFromTree(tree, outputPath);
1826
- return readBinaryFromInput(outputPath);
1826
+ return await readBinaryFromInput(outputPath);
1827
1827
  }
1828
1828
  async saveFromTree(tree, outputPath) {
1829
1829
  const files = [];
@@ -1832,7 +1832,7 @@ class GridsetProcessor extends BaseProcessor {
1832
1832
  if (Object.keys(tree.pages).length === 0) {
1833
1833
  // Create empty zip for empty tree
1834
1834
  const zipBuffer = await zip.writeFiles([]);
1835
- writeBinaryToPath(outputPath, zipBuffer);
1835
+ await writeBinaryToPath(outputPath, zipBuffer);
1836
1836
  return;
1837
1837
  }
1838
1838
  // Collect all unique styles from pages and buttons
@@ -2142,7 +2142,7 @@ class GridsetProcessor extends BaseProcessor {
2142
2142
  });
2143
2143
  // Write the zip file
2144
2144
  const zipBuffer = await zip.writeFiles(files);
2145
- writeBinaryToPath(outputPath, zipBuffer);
2145
+ await writeBinaryToPath(outputPath, zipBuffer);
2146
2146
  }
2147
2147
  // Helper method to calculate column definitions based on page layout
2148
2148
  calculateColumnDefinitions(page) {
@@ -3,7 +3,6 @@ import { AACTree, AACPage, AACButton, AACSemanticCategory, AACSemanticIntent, }
3
3
  import { generateCloneId } from '../utilities/analytics/utils/idGenerator';
4
4
  import { extractAllButtonsForTranslation, validateTranslationResults, } from '../utilities/translation/translationProcessor';
5
5
  import { encodeBase64, decodeText } from '../utils/io';
6
- import { getZipAdapter } from '../utils/zip';
7
6
  const OBF_FORMAT_VERSION = 'open-board-0.1';
8
7
  /**
9
8
  * Map OBF hidden value to AAC standard visibility
@@ -63,35 +62,42 @@ class ObfProcessor extends BaseProcessor {
63
62
  if (this.imageCache.has(imageId)) {
64
63
  return this.imageCache.get(imageId) ?? null;
65
64
  }
66
- if (!this.zipFile || !images) {
65
+ if (!images)
67
66
  return null;
68
- }
69
67
  // Find the image metadata
70
68
  const imageData = images.find((img) => img.id === imageId);
71
69
  if (!imageData) {
72
70
  return null;
73
71
  }
74
- // Try to get the image file from the ZIP
75
- // Images are typically stored in an 'images' folder or root
76
- const possiblePaths = [
77
- imageData.path, // Explicit path if provided
78
- `images/${imageData.filename || imageId}`, // Standard images folder
79
- imageData.id, // Just the ID
80
- ].filter(Boolean);
81
- for (const imagePath of possiblePaths) {
82
- try {
83
- const buffer = await this.zipFile.readFile(imagePath);
84
- if (buffer) {
85
- const contentType = imageData.content_type ||
86
- this.getMimeTypeFromFilename(imagePath);
87
- const dataUrl = `data:${contentType};base64,${encodeBase64(buffer)}`;
88
- this.imageCache.set(imageId, dataUrl);
89
- return dataUrl;
72
+ // If image has data property, use that
73
+ if (imageData.data) {
74
+ const dataUrl = imageData.data;
75
+ this.imageCache.set(imageId, dataUrl);
76
+ return dataUrl;
77
+ }
78
+ if (this.zipFile) {
79
+ // Try to get the image file from the ZIP
80
+ // Images are typically stored in an 'images' folder or root
81
+ const possiblePaths = [
82
+ imageData.path, // Explicit path if provided
83
+ `images/${imageData.filename || imageId}`, // Standard images folder
84
+ imageData.id, // Just the ID
85
+ ].filter(Boolean);
86
+ for (const imagePath of possiblePaths) {
87
+ try {
88
+ const buffer = await this.zipFile.readFile(imagePath);
89
+ if (buffer) {
90
+ const contentType = imageData.content_type ||
91
+ this.getMimeTypeFromFilename(imagePath);
92
+ const dataUrl = `data:${contentType};base64,${encodeBase64(buffer)}`;
93
+ this.imageCache.set(imageId, dataUrl);
94
+ return dataUrl;
95
+ }
96
+ }
97
+ catch (err) {
98
+ // Continue to next path
99
+ continue;
90
100
  }
91
- }
92
- catch (err) {
93
- // Continue to next path
94
- continue;
95
101
  }
96
102
  }
97
103
  // If image has a URL, use that as fallback
@@ -167,8 +173,7 @@ class ObfProcessor extends BaseProcessor {
167
173
  buttonParameters.image_id = btn.image_id;
168
174
  }
169
175
  return new AACButton({
170
- // Make button ID unique by combining page ID and button ID
171
- id: `${pageId}::${btn?.id || ''}`,
176
+ id: String(btn.id),
172
177
  label: String(btn?.label || ''),
173
178
  message: String(btn?.vocalization || btn?.label || ''),
174
179
  visibility: mapObfVisibility(btn.hidden),
@@ -217,7 +222,7 @@ class ObfProcessor extends BaseProcessor {
217
222
  return;
218
223
  if (rowIndex >= rows || colIndex >= cols)
219
224
  return;
220
- const aacBtn = buttonMap.get(`${pageId}::${cellId}`);
225
+ const aacBtn = buttonMap.get(String(cellId));
221
226
  if (aacBtn) {
222
227
  grid[rowIndex][colIndex] = aacBtn;
223
228
  }
@@ -230,7 +235,7 @@ class ObfProcessor extends BaseProcessor {
230
235
  const row = Math.floor(btn.box_id / cols);
231
236
  const col = btn.box_id % cols;
232
237
  if (row < rows && col < cols) {
233
- const aacBtn = buttonMap.get(`${pageId}::${btn.id}`);
238
+ const aacBtn = buttonMap.get(String(btn.id));
234
239
  if (aacBtn) {
235
240
  grid[row][col] = aacBtn;
236
241
  }
@@ -287,7 +292,7 @@ class ObfProcessor extends BaseProcessor {
287
292
  // Detailed logging for debugging input
288
293
  const bufferLength = typeof filePathOrBuffer === 'string'
289
294
  ? null
290
- : readBinaryFromInput(filePathOrBuffer).byteLength;
295
+ : (await readBinaryFromInput(filePathOrBuffer)).byteLength;
291
296
  console.log('[OBF] loadIntoTree called with:', {
292
297
  type: typeof filePathOrBuffer,
293
298
  isBuffer: typeof Buffer !== 'undefined' && Buffer.isBuffer(filePathOrBuffer),
@@ -297,9 +302,9 @@ class ObfProcessor extends BaseProcessor {
297
302
  });
298
303
  const tree = new AACTree();
299
304
  // Helper: try to parse JSON OBF
300
- function tryParseObfJson(data) {
305
+ async function tryParseObfJson(data) {
301
306
  try {
302
- const str = typeof data === 'string' ? data : readTextFromInput(data);
307
+ const str = typeof data === 'string' ? data : await readTextFromInput(data);
303
308
  // Check for empty or whitespace-only content
304
309
  if (!str.trim()) {
305
310
  return null;
@@ -321,8 +326,8 @@ class ObfProcessor extends BaseProcessor {
321
326
  // If input is a string path and ends with .obf, treat as JSON
322
327
  if (typeof filePathOrBuffer === 'string' && filePathOrBuffer.toLowerCase().endsWith('.obf')) {
323
328
  try {
324
- const content = readTextFromInput(filePathOrBuffer);
325
- const boardData = tryParseObfJson(content);
329
+ const content = await readTextFromInput(filePathOrBuffer);
330
+ const boardData = await tryParseObfJson(content);
326
331
  if (boardData) {
327
332
  console.log('[OBF] Detected .obf file, parsed as JSON');
328
333
  const page = await this.processBoard(boardData, filePathOrBuffer, false);
@@ -350,17 +355,17 @@ class ObfProcessor extends BaseProcessor {
350
355
  }
351
356
  }
352
357
  // Detect likely zip signature first
353
- function isLikelyZip(input) {
358
+ async function isLikelyZip(input) {
354
359
  if (typeof input === 'string') {
355
360
  const lowered = input.toLowerCase();
356
361
  return lowered.endsWith('.zip') || lowered.endsWith('.obz');
357
362
  }
358
- const bytes = readBinaryFromInput(input);
363
+ const bytes = await readBinaryFromInput(input);
359
364
  return bytes.length >= 2 && bytes[0] === 0x50 && bytes[1] === 0x4b;
360
365
  }
361
366
  // Check if input is a buffer or string that parses as OBF JSON; throw if neither JSON nor ZIP
362
- if (!isLikelyZip(filePathOrBuffer)) {
363
- const asJson = tryParseObfJson(filePathOrBuffer);
367
+ if (!(await isLikelyZip(filePathOrBuffer))) {
368
+ const asJson = await tryParseObfJson(filePathOrBuffer);
364
369
  if (!asJson)
365
370
  throw new Error('Invalid OBF content: not JSON and not ZIP');
366
371
  console.log('[OBF] Detected buffer/string as OBF JSON');
@@ -399,7 +404,7 @@ class ObfProcessor extends BaseProcessor {
399
404
  try {
400
405
  const content = await this.zipFile.readFile(manifestFile[0]);
401
406
  const data = decodeText(content);
402
- const str = typeof data === 'string' ? data : readTextFromInput(data);
407
+ const str = typeof data === 'string' ? data : await readTextFromInput(data);
403
408
  if (!str.trim())
404
409
  throw new Error('Manifest object missing');
405
410
  const manifestObject = JSON.parse(str);
@@ -423,7 +428,7 @@ class ObfProcessor extends BaseProcessor {
423
428
  for (const entryName of obfEntries) {
424
429
  try {
425
430
  const content = await this.zipFile.readFile(entryName);
426
- const boardData = tryParseObfJson(decodeText(content));
431
+ const boardData = await tryParseObfJson(decodeText(content));
427
432
  if (boardData) {
428
433
  const page = await this.processBoard(boardData, entryName, true);
429
434
  tree.addPage(page);
@@ -530,6 +535,7 @@ class ObfProcessor extends BaseProcessor {
530
535
  border_color: button.style?.borderColor,
531
536
  box_id: buttonPositions.get(String(button.id ?? '')),
532
537
  image_id: imageId,
538
+ hidden: button.visibility === 'Hidden' || false,
533
539
  };
534
540
  }),
535
541
  images: Array.isArray(page.images) ? page.images : [],
@@ -567,10 +573,10 @@ class ObfProcessor extends BaseProcessor {
567
573
  });
568
574
  // Save the translated tree and return its content
569
575
  await this.saveFromTree(tree, outputPath);
570
- return readBinaryFromInput(outputPath);
576
+ return await readBinaryFromInput(outputPath);
571
577
  }
572
578
  async saveFromTree(tree, outputPath) {
573
- const { writeTextToPath, writeBinaryToPath } = this.options.fileAdapter;
579
+ const { writeTextToPath, writeBinaryToPath, pathExists } = this.options.fileAdapter;
574
580
  if (outputPath.endsWith('.obf')) {
575
581
  // Save as single OBF JSON file
576
582
  const rootPage = tree.rootId ? tree.getPage(tree.rootId) : Object.values(tree.pages)[0];
@@ -578,7 +584,7 @@ class ObfProcessor extends BaseProcessor {
578
584
  throw new Error('No pages to save');
579
585
  }
580
586
  const obfBoard = this.createObfBoardFromPage(rootPage, 'Exported Board', tree.metadata);
581
- writeTextToPath(outputPath, JSON.stringify(obfBoard, null, 2));
587
+ await writeTextToPath(outputPath, JSON.stringify(obfBoard, null, 2));
582
588
  }
583
589
  else {
584
590
  const files = Object.values(tree.pages).map((page) => {
@@ -590,9 +596,10 @@ class ObfProcessor extends BaseProcessor {
590
596
  data: new TextEncoder().encode(obfContent),
591
597
  };
592
598
  });
593
- const zip = await getZipAdapter(undefined, this.options.fileAdapter);
594
- const zipData = await zip.writeFiles(files);
595
- writeBinaryToPath(outputPath, zipData);
599
+ const fileExists = await pathExists(outputPath);
600
+ this.zipFile = await this.options.zipAdapter(fileExists ? outputPath : undefined, this.options.fileAdapter);
601
+ const zipData = await this.zipFile.writeFiles(files);
602
+ await writeBinaryToPath(outputPath, zipData);
596
603
  }
597
604
  }
598
605
  /**
@@ -625,7 +632,7 @@ class ObfProcessor extends BaseProcessor {
625
632
  * This method uses shared translation utilities that work across all AAC formats.
626
633
  *
627
634
  * @param filePathOrBuffer - Path to OBF/OBZ file or buffer
628
- * @returns Array of symbol information for LLM processing
635
+ * @returns Promise resolving to symbol information for LLM processing
629
636
  */
630
637
  async extractSymbolsForLLM(filePathOrBuffer) {
631
638
  const tree = await this.loadIntoTree(filePathOrBuffer);
@@ -655,7 +662,7 @@ class ObfProcessor extends BaseProcessor {
655
662
  * @param llmTranslations - Array of LLM translations with symbol info
656
663
  * @param outputPath - Where to save the translated OBF/OBZ file
657
664
  * @param options - Translation options (e.g., allowPartial for testing)
658
- * @returns Buffer of the translated OBF/OBZ file
665
+ * @returns Promise resolving to a buffer of the translated OBF/OBZ file
659
666
  */
660
667
  async processLLMTranslations(filePathOrBuffer, llmTranslations, outputPath, options) {
661
668
  const { readBinaryFromInput } = this.options.fileAdapter;
@@ -697,7 +704,7 @@ class ObfProcessor extends BaseProcessor {
697
704
  });
698
705
  // Save and return
699
706
  await this.saveFromTree(tree, outputPath);
700
- return readBinaryFromInput(outputPath);
707
+ return await readBinaryFromInput(outputPath);
701
708
  }
702
709
  getObfValidator() {
703
710
  try {
@@ -50,8 +50,7 @@ class OpmlProcessor extends BaseProcessor {
50
50
  }
51
51
  async extractTexts(filePathOrBuffer) {
52
52
  const { readTextFromInput } = this.options.fileAdapter;
53
- await Promise.resolve();
54
- const content = readTextFromInput(filePathOrBuffer);
53
+ const content = await readTextFromInput(filePathOrBuffer);
55
54
  const parser = new XMLParser({ ignoreAttributes: false });
56
55
  const data = parser.parse(content);
57
56
  const texts = [];
@@ -83,10 +82,9 @@ class OpmlProcessor extends BaseProcessor {
83
82
  }
84
83
  async loadIntoTree(filePathOrBuffer) {
85
84
  const { readBinaryFromInput, readTextFromInput } = this.options.fileAdapter;
86
- await Promise.resolve();
87
85
  const filename = typeof filePathOrBuffer === 'string' ? getBasename(filePathOrBuffer) : 'upload.opml';
88
- const buffer = readBinaryFromInput(filePathOrBuffer);
89
- const content = readTextFromInput(buffer);
86
+ const buffer = await readBinaryFromInput(filePathOrBuffer);
87
+ const content = await readTextFromInput(buffer);
90
88
  try {
91
89
  if (!content || !content.trim()) {
92
90
  const validationResult = buildValidationResultFromMessage({
@@ -170,8 +168,7 @@ class OpmlProcessor extends BaseProcessor {
170
168
  }
171
169
  async processTexts(filePathOrBuffer, translations, outputPath) {
172
170
  const { writeBinaryToPath, readTextFromInput } = this.options.fileAdapter;
173
- await Promise.resolve();
174
- const content = readTextFromInput(filePathOrBuffer);
171
+ const content = await readTextFromInput(filePathOrBuffer);
175
172
  let translatedContent = content;
176
173
  // Apply translations to text attributes in OPML outline elements
177
174
  translations.forEach((translation, originalText) => {
@@ -182,12 +179,11 @@ class OpmlProcessor extends BaseProcessor {
182
179
  }
183
180
  });
184
181
  const resultBuffer = encodeText(translatedContent);
185
- writeBinaryToPath(outputPath, resultBuffer);
182
+ await writeBinaryToPath(outputPath, resultBuffer);
186
183
  return resultBuffer;
187
184
  }
188
185
  async saveFromTree(tree, outputPath) {
189
186
  const { writeTextToPath } = this.options.fileAdapter;
190
- await Promise.resolve();
191
187
  // Helper to recursively build outline nodes with cycle detection
192
188
  function buildOutline(page, visited = new Set()) {
193
189
  // Prevent infinite recursion by tracking visited pages
@@ -257,7 +253,7 @@ class OpmlProcessor extends BaseProcessor {
257
253
  attributeNamePrefix: '@_',
258
254
  });
259
255
  const xml = '<?xml version="1.0" encoding="UTF-8"?>\n' + builder.build(opmlObj);
260
- writeTextToPath(outputPath, xml);
256
+ await writeTextToPath(outputPath, xml);
261
257
  }
262
258
  /**
263
259
  * Extract strings with metadata for aac-tools-platform compatibility
@@ -10,7 +10,7 @@ import { requireBetterSqlite3 } from '../../utils/sqlite';
10
10
  // We extract PNG/JPEG images but skip vector graphics (requires renderer).
11
11
  // NOTE: Snap buttons currently do not populate resolvedImageEntry; these helpers
12
12
  // therefore return empty collections until image resolution is implemented.
13
- function collectFiles(root, matcher, maxDepth = 3, fileAdapter = defaultFileAdapter) {
13
+ async function collectFiles(root, matcher, maxDepth = 3, fileAdapter = defaultFileAdapter) {
14
14
  const { listDir, join, isDirectory } = fileAdapter;
15
15
  const results = new Set();
16
16
  const stack = [{ dir: root, depth: 0 }];
@@ -22,14 +22,14 @@ function collectFiles(root, matcher, maxDepth = 3, fileAdapter = defaultFileAdap
22
22
  continue;
23
23
  let entries;
24
24
  try {
25
- entries = listDir(current.dir);
25
+ entries = await listDir(current.dir);
26
26
  }
27
27
  catch (error) {
28
28
  continue;
29
29
  }
30
30
  for (const entry of entries) {
31
31
  const fullPath = join(current.dir, entry);
32
- if (isDirectory(entry)) {
32
+ if (await isDirectory(entry)) {
33
33
  stack.push({ dir: fullPath, depth: current.depth + 1 });
34
34
  }
35
35
  else if (matcher(fullPath)) {
@@ -83,15 +83,15 @@ export function getAllowedImageEntries(tree) {
83
83
  * @param entryPath Symbol identifier (e.g., "SYM:12345")
84
84
  * @returns Image data buffer or null if not found
85
85
  */
86
- export function openImage(dbOrFile, entryPath, fileAdapter = defaultFileAdapter) {
86
+ export async function openImage(dbOrFile, entryPath, fileAdapter = defaultFileAdapter) {
87
87
  const { mkTempDir, join, writeBinaryToPath, removePath, dirname } = fileAdapter;
88
88
  let dbPath;
89
89
  let cleanupNeeded = false;
90
90
  // Handle Buffer input by writing to temp file
91
91
  if (Buffer.isBuffer(dbOrFile)) {
92
- const tempDir = mkTempDir(join(process.cwd(), 'snap-'));
92
+ const tempDir = await mkTempDir(join(process.cwd(), 'snap-'));
93
93
  dbPath = join(tempDir, 'temp.sps');
94
- writeBinaryToPath(dbPath, dbOrFile);
94
+ await writeBinaryToPath(dbPath, dbOrFile);
95
95
  cleanupNeeded = true;
96
96
  }
97
97
  else if (typeof dbOrFile === 'string') {
@@ -126,9 +126,9 @@ export function openImage(dbOrFile, entryPath, fileAdapter = defaultFileAdapter)
126
126
  }
127
127
  if (cleanupNeeded && dbPath) {
128
128
  try {
129
- removePath(dbPath);
129
+ await removePath(dbPath);
130
130
  const dir = dirname(dbPath);
131
- removePath(dir);
131
+ await removePath(dir);
132
132
  }
133
133
  catch (e) {
134
134
  // Ignore cleanup errors
@@ -142,7 +142,7 @@ export function openImage(dbOrFile, entryPath, fileAdapter = defaultFileAdapter)
142
142
  * @param packageNamePattern Optional pattern to filter package names (default: 'TobiiDynavox')
143
143
  * @returns Array of Snap package path information
144
144
  */
145
- export function findSnapPackages(packageNamePattern = 'TobiiDynavox', fileAdapter = defaultFileAdapter) {
145
+ export async function findSnapPackages(packageNamePattern = 'TobiiDynavox', fileAdapter = defaultFileAdapter) {
146
146
  const { join, listDir, isDirectory, pathExists } = fileAdapter;
147
147
  const results = [];
148
148
  // Only works on Windows
@@ -156,13 +156,13 @@ export function findSnapPackages(packageNamePattern = 'TobiiDynavox', fileAdapte
156
156
  }
157
157
  const packagesPath = join(localAppData, 'Packages');
158
158
  // Check if Packages directory exists
159
- if (!pathExists(packagesPath)) {
159
+ if (!(await pathExists(packagesPath))) {
160
160
  return results;
161
161
  }
162
162
  // Enumerate packages
163
- const packages = listDir(packagesPath);
163
+ const packages = await listDir(packagesPath);
164
164
  for (const packageDir of packages) {
165
- if (!isDirectory(packageDir))
165
+ if (!(await isDirectory(packageDir)))
166
166
  continue;
167
167
  const packageName = packageDir;
168
168
  // Filter by pattern
@@ -185,8 +185,8 @@ export function findSnapPackages(packageNamePattern = 'TobiiDynavox', fileAdapte
185
185
  * @param packageNamePattern Optional pattern to filter package names (default: 'TobiiDynavox')
186
186
  * @returns Path to the first matching Snap package, or null if not found
187
187
  */
188
- export function findSnapPackagePath(packageNamePattern = 'TobiiDynavox', fileAdapter) {
189
- const packages = findSnapPackages(packageNamePattern, fileAdapter);
188
+ export async function findSnapPackagePath(packageNamePattern = 'TobiiDynavox', fileAdapter) {
189
+ const packages = await findSnapPackages(packageNamePattern, fileAdapter);
190
190
  return packages.length > 0 ? packages[0].packagePath : null;
191
191
  }
192
192
  /**
@@ -196,28 +196,28 @@ export function findSnapPackagePath(packageNamePattern = 'TobiiDynavox', fileAda
196
196
  * @param packageNamePattern Optional package filter (default TobiiDynavox)
197
197
  * @returns Array of user info with vocab paths
198
198
  */
199
- export function findSnapUsers(packageNamePattern = 'TobiiDynavox', fileAdapter = defaultFileAdapter) {
199
+ export async function findSnapUsers(packageNamePattern = 'TobiiDynavox', fileAdapter = defaultFileAdapter) {
200
200
  const { join, listDir, isDirectory, pathExists } = fileAdapter;
201
201
  const results = [];
202
202
  if (process.platform !== 'win32') {
203
203
  return results;
204
204
  }
205
- const packagePath = findSnapPackagePath(packageNamePattern, fileAdapter);
205
+ const packagePath = await findSnapPackagePath(packageNamePattern, fileAdapter);
206
206
  if (!packagePath) {
207
207
  return results;
208
208
  }
209
209
  const usersRoot = join(packagePath, 'LocalState', 'Users');
210
- if (!pathExists(usersRoot)) {
210
+ if (!(await pathExists(usersRoot))) {
211
211
  return results;
212
212
  }
213
- const entries = listDir(usersRoot);
213
+ const entries = await listDir(usersRoot);
214
214
  for (const entry of entries) {
215
- if (!isDirectory(entry))
215
+ if (!(await isDirectory(entry)))
216
216
  continue;
217
217
  if (entry.toLowerCase().startsWith('swiftkey'))
218
218
  continue;
219
219
  const userPath = join(usersRoot, entry);
220
- const vocabPaths = collectFiles(userPath, (full) => {
220
+ const vocabPaths = await collectFiles(userPath, (full) => {
221
221
  const ext = extname(full).toLowerCase();
222
222
  return ext === '.sps' || ext === '.spb';
223
223
  }, 2, fileAdapter);
@@ -235,8 +235,9 @@ export function findSnapUsers(packageNamePattern = 'TobiiDynavox', fileAdapter =
235
235
  * @param packageNamePattern Optional package filter
236
236
  * @returns Array of vocab file paths
237
237
  */
238
- export function findSnapUserVocabularies(userId, packageNamePattern = 'TobiiDynavox', fileAdapter) {
239
- const users = findSnapUsers(packageNamePattern, fileAdapter).filter((u) => !userId || u.userId === userId);
238
+ export async function findSnapUserVocabularies(userId, packageNamePattern = 'TobiiDynavox', fileAdapter) {
239
+ const allUsers = await findSnapUsers(packageNamePattern, fileAdapter);
240
+ const users = allUsers.filter((u) => !userId || u.userId === userId);
240
241
  return users.flatMap((u) => u.vocabPaths);
241
242
  }
242
243
  /**
@@ -246,12 +247,13 @@ export function findSnapUserVocabularies(userId, packageNamePattern = 'TobiiDyna
246
247
  * @param packageNamePattern Optional package filter
247
248
  * @returns Array of history file paths (may be empty if not found)
248
249
  */
249
- export function findSnapUserHistory(userId, packageNamePattern = 'TobiiDynavox', fileAdapter = defaultFileAdapter) {
250
+ export async function findSnapUserHistory(userId, packageNamePattern = 'TobiiDynavox', fileAdapter = defaultFileAdapter) {
250
251
  const { basename } = fileAdapter;
251
- const user = findSnapUsers(packageNamePattern, fileAdapter).find((u) => u.userId === userId);
252
+ const allUsers = await findSnapUsers(packageNamePattern, fileAdapter);
253
+ const user = allUsers.find((u) => u.userId === userId);
252
254
  if (!user)
253
255
  return [];
254
- return collectFiles(user.userPath, (full) => basename(full).toLowerCase().includes('history'), 2, fileAdapter);
256
+ return await collectFiles(user.userPath, (full) => basename(full).toLowerCase().includes('history'), 2, fileAdapter);
255
257
  }
256
258
  /**
257
259
  * Check whether TD Snap appears to be installed (Windows only)
@@ -264,9 +266,9 @@ export function isSnapInstalled(packageNamePattern = 'TobiiDynavox') {
264
266
  /**
265
267
  * Read Snap usage history from a pageset file (.sps/.spb)
266
268
  */
267
- export function readSnapUsage(pagesetPath, fileAdapter = defaultFileAdapter) {
269
+ export async function readSnapUsage(pagesetPath, fileAdapter = defaultFileAdapter) {
268
270
  const { pathExists } = fileAdapter;
269
- if (!pathExists(pagesetPath))
271
+ if (!(await pathExists(pagesetPath)))
270
272
  return [];
271
273
  const Database = requireBetterSqlite3();
272
274
  const db = new Database(pagesetPath, { readonly: true });
@@ -323,8 +325,10 @@ export function readSnapUsage(pagesetPath, fileAdapter = defaultFileAdapter) {
323
325
  /**
324
326
  * Read Snap usage history for a user (all pagesets)
325
327
  */
326
- export function readSnapUsageForUser(userId, packageNamePattern = 'TobiiDynavox') {
327
- const users = findSnapUsers(packageNamePattern).filter((u) => !userId || u.userId === userId);
328
+ export async function readSnapUsageForUser(userId, packageNamePattern = 'TobiiDynavox') {
329
+ const allUsers = await findSnapUsers(packageNamePattern);
330
+ const users = allUsers.filter((u) => !userId || u.userId === userId);
328
331
  const pagesets = users.flatMap((u) => u.vocabPaths);
329
- return pagesets.flatMap((p) => readSnapUsage(p));
332
+ const usage = await Promise.all(pagesets.map(async (p) => await readSnapUsage(p)));
333
+ return usage.flat();
330
334
  }