@willwade/aac-processors 0.1.20 → 0.2.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 (113) hide show
  1. package/dist/browser/core/baseProcessor.js +4 -0
  2. package/dist/browser/processors/applePanelsProcessor.js +33 -40
  3. package/dist/browser/processors/astericsGridProcessor.js +31 -26
  4. package/dist/browser/processors/dotProcessor.js +11 -12
  5. package/dist/browser/processors/gridset/colorUtils.js +354 -0
  6. package/dist/browser/processors/gridset/helpers.js +60 -53
  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 +39 -72
  12. package/dist/browser/processors/gridsetProcessor.js +39 -48
  13. package/dist/browser/processors/obfProcessor.js +39 -53
  14. package/dist/browser/processors/opmlProcessor.js +11 -12
  15. package/dist/browser/processors/snap/helpers.js +57 -49
  16. package/dist/browser/processors/snapProcessor.js +48 -51
  17. package/dist/browser/processors/touchchatProcessor.js +60 -52
  18. package/dist/browser/utilities/analytics/history.js +24 -18
  19. package/dist/browser/utilities/analytics/metrics/comparison.js +16 -16
  20. package/dist/browser/utilities/analytics/metrics/vocabulary.js +2 -2
  21. package/dist/browser/utilities/analytics/reference/browser.js +16 -16
  22. package/dist/browser/utilities/analytics/reference/index.js +44 -35
  23. package/dist/browser/utils/io.js +78 -21
  24. package/dist/browser/utils/sqlite.js +8 -10
  25. package/dist/browser/utils/zip.js +43 -43
  26. package/dist/browser/validation/baseValidator.js +5 -0
  27. package/dist/browser/validation/gridsetValidator.js +12 -20
  28. package/dist/browser/validation/obfValidator.js +6 -5
  29. package/dist/browser/validation/snapValidator.js +11 -7
  30. package/dist/browser/validation/touchChatValidator.js +23 -13
  31. package/dist/cli/index.js +22 -24
  32. package/dist/core/baseProcessor.d.ts +7 -7
  33. package/dist/core/baseProcessor.js +4 -0
  34. package/dist/processors/applePanelsProcessor.js +32 -39
  35. package/dist/processors/astericsGridProcessor.d.ts +4 -4
  36. package/dist/processors/astericsGridProcessor.js +30 -25
  37. package/dist/processors/dotProcessor.js +10 -11
  38. package/dist/processors/excelProcessor.d.ts +3 -3
  39. package/dist/processors/excelProcessor.js +14 -20
  40. package/dist/processors/gridset/helpers.d.ts +12 -14
  41. package/dist/processors/gridset/helpers.js +60 -79
  42. package/dist/processors/gridset/imageDebug.d.ts +3 -5
  43. package/dist/processors/gridset/imageDebug.js +4 -4
  44. package/dist/processors/gridset/password.d.ts +1 -1
  45. package/dist/processors/gridset/symbolExtractor.d.ts +5 -3
  46. package/dist/processors/gridset/symbolExtractor.js +15 -38
  47. package/dist/processors/gridset/symbolSearch.d.ts +11 -10
  48. package/dist/processors/gridset/symbolSearch.js +29 -51
  49. package/dist/processors/gridset/symbols.d.ts +8 -6
  50. package/dist/processors/gridset/symbols.js +38 -71
  51. package/dist/processors/gridset/wordlistHelpers.d.ts +4 -6
  52. package/dist/processors/gridset/wordlistHelpers.js +15 -74
  53. package/dist/processors/gridsetProcessor.d.ts +2 -2
  54. package/dist/processors/gridsetProcessor.js +38 -70
  55. package/dist/processors/obfProcessor.d.ts +2 -2
  56. package/dist/processors/obfProcessor.js +38 -75
  57. package/dist/processors/obfsetProcessor.js +2 -3
  58. package/dist/processors/opmlProcessor.js +10 -11
  59. package/dist/processors/snap/helpers.d.ts +9 -9
  60. package/dist/processors/snap/helpers.js +58 -76
  61. package/dist/processors/snapProcessor.d.ts +2 -2
  62. package/dist/processors/snapProcessor.js +47 -50
  63. package/dist/processors/touchchatProcessor.d.ts +2 -2
  64. package/dist/processors/touchchatProcessor.js +59 -51
  65. package/dist/types/aac.d.ts +2 -2
  66. package/dist/utilities/analytics/history.d.ts +8 -8
  67. package/dist/utilities/analytics/history.js +24 -18
  68. package/dist/utilities/analytics/index.d.ts +3 -2
  69. package/dist/utilities/analytics/index.js +9 -10
  70. package/dist/utilities/analytics/metrics/comparison.d.ts +1 -1
  71. package/dist/utilities/analytics/metrics/comparison.js +16 -16
  72. package/dist/utilities/analytics/metrics/vocabulary.d.ts +1 -1
  73. package/dist/utilities/analytics/metrics/vocabulary.js +2 -2
  74. package/dist/utilities/analytics/reference/browser.d.ts +9 -9
  75. package/dist/utilities/analytics/reference/browser.js +16 -16
  76. package/dist/utilities/analytics/reference/index.d.ts +25 -23
  77. package/dist/utilities/analytics/reference/index.js +43 -34
  78. package/dist/utilities/symbolTools.d.ts +8 -6
  79. package/dist/utilities/symbolTools.js +21 -18
  80. package/dist/utils/io.d.ts +24 -6
  81. package/dist/utils/io.js +79 -25
  82. package/dist/utils/sqlite.d.ts +3 -1
  83. package/dist/utils/sqlite.js +7 -9
  84. package/dist/utils/zip.d.ts +7 -3
  85. package/dist/utils/zip.js +43 -43
  86. package/dist/validation/applePanelsValidator.d.ts +2 -1
  87. package/dist/validation/applePanelsValidator.js +10 -11
  88. package/dist/validation/astericsValidator.d.ts +2 -1
  89. package/dist/validation/astericsValidator.js +5 -4
  90. package/dist/validation/baseValidator.d.ts +2 -2
  91. package/dist/validation/baseValidator.js +5 -0
  92. package/dist/validation/dotValidator.d.ts +2 -1
  93. package/dist/validation/dotValidator.js +5 -4
  94. package/dist/validation/excelValidator.d.ts +2 -1
  95. package/dist/validation/excelValidator.js +5 -4
  96. package/dist/validation/gridsetValidator.d.ts +2 -1
  97. package/dist/validation/gridsetValidator.js +11 -22
  98. package/dist/validation/index.d.ts +2 -2
  99. package/dist/validation/index.js +5 -4
  100. package/dist/validation/obfValidator.d.ts +2 -1
  101. package/dist/validation/obfValidator.js +5 -4
  102. package/dist/validation/obfsetValidator.d.ts +2 -1
  103. package/dist/validation/obfsetValidator.js +5 -4
  104. package/dist/validation/opmlValidator.d.ts +2 -1
  105. package/dist/validation/opmlValidator.js +5 -4
  106. package/dist/validation/snapValidator.d.ts +2 -1
  107. package/dist/validation/snapValidator.js +10 -6
  108. package/dist/validation/touchChatValidator.d.ts +4 -6
  109. package/dist/validation/touchChatValidator.js +22 -12
  110. package/dist/validation/validationTypes.d.ts +8 -1
  111. package/package.json +1 -1
  112. package/dist/core/fileProcessor.d.ts +0 -7
  113. package/dist/core/fileProcessor.js +0 -57
@@ -2,8 +2,7 @@ import { BaseProcessor, } from '../core/baseProcessor';
2
2
  import { AACTree, AACPage, AACButton, AACSemanticCategory, AACSemanticIntent, } from '../core/treeStructure';
3
3
  import { generateCloneId } from '../utilities/analytics/utils/idGenerator';
4
4
  import { extractAllButtonsForTranslation, validateTranslationResults, } from '../utilities/translation/translationProcessor';
5
- import { readBinaryFromInput, readTextFromInput, writeTextToPath, encodeBase64, decodeText, getNodeRequire, isNodeRuntime, } from '../utils/io';
6
- import { openZipFromInput } from '../utils/zip';
5
+ import { encodeBase64, decodeText } from '../utils/io';
7
6
  const OBF_FORMAT_VERSION = 'open-board-0.1';
8
7
  /**
9
8
  * Map OBF hidden value to AAC standard visibility
@@ -167,8 +166,7 @@ class ObfProcessor extends BaseProcessor {
167
166
  buttonParameters.image_id = btn.image_id;
168
167
  }
169
168
  return new AACButton({
170
- // Make button ID unique by combining page ID and button ID
171
- id: `${pageId}::${btn?.id || ''}`,
169
+ id: String(btn.id),
172
170
  label: String(btn?.label || ''),
173
171
  message: String(btn?.vocalization || btn?.label || ''),
174
172
  visibility: mapObfVisibility(btn.hidden),
@@ -217,7 +215,7 @@ class ObfProcessor extends BaseProcessor {
217
215
  return;
218
216
  if (rowIndex >= rows || colIndex >= cols)
219
217
  return;
220
- const aacBtn = buttonMap.get(`${pageId}::${cellId}`);
218
+ const aacBtn = buttonMap.get(String(cellId));
221
219
  if (aacBtn) {
222
220
  grid[rowIndex][colIndex] = aacBtn;
223
221
  }
@@ -230,7 +228,7 @@ class ObfProcessor extends BaseProcessor {
230
228
  const row = Math.floor(btn.box_id / cols);
231
229
  const col = btn.box_id % cols;
232
230
  if (row < rows && col < cols) {
233
- const aacBtn = buttonMap.get(`${pageId}::${btn.id}`);
231
+ const aacBtn = buttonMap.get(String(btn.id));
234
232
  if (aacBtn) {
235
233
  grid[row][col] = aacBtn;
236
234
  }
@@ -283,10 +281,11 @@ class ObfProcessor extends BaseProcessor {
283
281
  return texts;
284
282
  }
285
283
  async loadIntoTree(filePathOrBuffer) {
284
+ const { readBinaryFromInput, readTextFromInput } = this.options.fileAdapter;
286
285
  // Detailed logging for debugging input
287
286
  const bufferLength = typeof filePathOrBuffer === 'string'
288
287
  ? null
289
- : readBinaryFromInput(filePathOrBuffer).byteLength;
288
+ : (await readBinaryFromInput(filePathOrBuffer)).byteLength;
290
289
  console.log('[OBF] loadIntoTree called with:', {
291
290
  type: typeof filePathOrBuffer,
292
291
  isBuffer: typeof Buffer !== 'undefined' && Buffer.isBuffer(filePathOrBuffer),
@@ -296,9 +295,9 @@ class ObfProcessor extends BaseProcessor {
296
295
  });
297
296
  const tree = new AACTree();
298
297
  // Helper: try to parse JSON OBF
299
- function tryParseObfJson(data) {
298
+ async function tryParseObfJson(data) {
300
299
  try {
301
- const str = typeof data === 'string' ? data : readTextFromInput(data);
300
+ const str = typeof data === 'string' ? data : await readTextFromInput(data);
302
301
  // Check for empty or whitespace-only content
303
302
  if (!str.trim()) {
304
303
  return null;
@@ -320,8 +319,8 @@ class ObfProcessor extends BaseProcessor {
320
319
  // If input is a string path and ends with .obf, treat as JSON
321
320
  if (typeof filePathOrBuffer === 'string' && filePathOrBuffer.toLowerCase().endsWith('.obf')) {
322
321
  try {
323
- const content = readTextFromInput(filePathOrBuffer);
324
- const boardData = tryParseObfJson(content);
322
+ const content = await readTextFromInput(filePathOrBuffer);
323
+ const boardData = await tryParseObfJson(content);
325
324
  if (boardData) {
326
325
  console.log('[OBF] Detected .obf file, parsed as JSON');
327
326
  const page = await this.processBoard(boardData, filePathOrBuffer, false);
@@ -349,17 +348,17 @@ class ObfProcessor extends BaseProcessor {
349
348
  }
350
349
  }
351
350
  // Detect likely zip signature first
352
- function isLikelyZip(input) {
351
+ async function isLikelyZip(input) {
353
352
  if (typeof input === 'string') {
354
353
  const lowered = input.toLowerCase();
355
354
  return lowered.endsWith('.zip') || lowered.endsWith('.obz');
356
355
  }
357
- const bytes = readBinaryFromInput(input);
356
+ const bytes = await readBinaryFromInput(input);
358
357
  return bytes.length >= 2 && bytes[0] === 0x50 && bytes[1] === 0x4b;
359
358
  }
360
359
  // Check if input is a buffer or string that parses as OBF JSON; throw if neither JSON nor ZIP
361
- if (!isLikelyZip(filePathOrBuffer)) {
362
- const asJson = tryParseObfJson(filePathOrBuffer);
360
+ if (!(await isLikelyZip(filePathOrBuffer))) {
361
+ const asJson = await tryParseObfJson(filePathOrBuffer);
363
362
  if (!asJson)
364
363
  throw new Error('Invalid OBF content: not JSON and not ZIP');
365
364
  console.log('[OBF] Detected buffer/string as OBF JSON');
@@ -380,10 +379,7 @@ class ObfProcessor extends BaseProcessor {
380
379
  return tree;
381
380
  }
382
381
  try {
383
- const zipResult = this.options.zipAdapter
384
- ? await this.options.zipAdapter(filePathOrBuffer)
385
- : await openZipFromInput(filePathOrBuffer);
386
- this.zipFile = zipResult.zip;
382
+ this.zipFile = await this.options.zipAdapter(filePathOrBuffer);
387
383
  }
388
384
  catch (err) {
389
385
  console.error('[OBF] Error loading ZIP:', err);
@@ -401,7 +397,7 @@ class ObfProcessor extends BaseProcessor {
401
397
  try {
402
398
  const content = await this.zipFile.readFile(manifestFile[0]);
403
399
  const data = decodeText(content);
404
- const str = typeof data === 'string' ? data : readTextFromInput(data);
400
+ const str = typeof data === 'string' ? data : await readTextFromInput(data);
405
401
  if (!str.trim())
406
402
  throw new Error('Manifest object missing');
407
403
  const manifestObject = JSON.parse(str);
@@ -425,7 +421,7 @@ class ObfProcessor extends BaseProcessor {
425
421
  for (const entryName of obfEntries) {
426
422
  try {
427
423
  const content = await this.zipFile.readFile(entryName);
428
- const boardData = tryParseObfJson(decodeText(content));
424
+ const boardData = await tryParseObfJson(decodeText(content));
429
425
  if (boardData) {
430
426
  const page = await this.processBoard(boardData, entryName, true);
431
427
  tree.addPage(page);
@@ -539,6 +535,7 @@ class ObfProcessor extends BaseProcessor {
539
535
  };
540
536
  }
541
537
  async processTexts(filePathOrBuffer, translations, outputPath) {
538
+ const { readBinaryFromInput } = this.options.fileAdapter;
542
539
  // Load the tree, apply translations, and save to new file
543
540
  const tree = await this.loadIntoTree(filePathOrBuffer);
544
541
  // Apply translations to all text content
@@ -568,9 +565,10 @@ class ObfProcessor extends BaseProcessor {
568
565
  });
569
566
  // Save the translated tree and return its content
570
567
  await this.saveFromTree(tree, outputPath);
571
- return readBinaryFromInput(outputPath);
568
+ return await readBinaryFromInput(outputPath);
572
569
  }
573
570
  async saveFromTree(tree, outputPath) {
571
+ const { writeTextToPath, writeBinaryToPath, pathExists } = this.options.fileAdapter;
574
572
  if (outputPath.endsWith('.obf')) {
575
573
  // Save as single OBF JSON file
576
574
  const rootPage = tree.rootId ? tree.getPage(tree.rootId) : Object.values(tree.pages)[0];
@@ -578,35 +576,22 @@ class ObfProcessor extends BaseProcessor {
578
576
  throw new Error('No pages to save');
579
577
  }
580
578
  const obfBoard = this.createObfBoardFromPage(rootPage, 'Exported Board', tree.metadata);
581
- writeTextToPath(outputPath, JSON.stringify(obfBoard, null, 2));
579
+ await writeTextToPath(outputPath, JSON.stringify(obfBoard, null, 2));
582
580
  }
583
581
  else {
584
- // Save as OBZ (zip with multiple OBF files)
585
- if (isNodeRuntime()) {
586
- const AdmZip = getNodeRequire()('adm-zip');
587
- const zip = new AdmZip();
588
- Object.values(tree.pages).forEach((page) => {
589
- const obfBoard = this.createObfBoardFromPage(page, 'Board', tree.metadata);
590
- const obfContent = JSON.stringify(obfBoard, null, 2);
591
- zip.addFile(`${page.id}.obf`, Buffer.from(obfContent, 'utf8'));
592
- });
593
- const zipBuffer = zip.toBuffer();
594
- const { writeBinaryToPath } = await import('../utils/io');
595
- writeBinaryToPath(outputPath, zipBuffer);
596
- }
597
- else {
598
- const module = await import('jszip');
599
- const JSZip = module.default || module;
600
- const zip = new JSZip();
601
- Object.values(tree.pages).forEach((page) => {
602
- const obfBoard = this.createObfBoardFromPage(page, 'Board', tree.metadata);
603
- const obfContent = JSON.stringify(obfBoard, null, 2);
604
- zip.file(`${page.id}.obf`, obfContent);
605
- });
606
- const zipBuffer = await zip.generateAsync({ type: 'uint8array' });
607
- const { writeBinaryToPath } = await import('../utils/io');
608
- writeBinaryToPath(outputPath, zipBuffer);
609
- }
582
+ const files = Object.values(tree.pages).map((page) => {
583
+ const obfBoard = this.createObfBoardFromPage(page, 'Board', tree.metadata);
584
+ const obfContent = JSON.stringify(obfBoard, null, 2);
585
+ const name = page.id.endsWith('.obf') ? page.id : `${page.id}.obf`;
586
+ return {
587
+ name,
588
+ data: new TextEncoder().encode(obfContent),
589
+ };
590
+ });
591
+ const fileExists = await pathExists(outputPath);
592
+ this.zipFile = await this.options.zipAdapter(fileExists ? outputPath : undefined, this.options.fileAdapter);
593
+ const zipData = await this.zipFile.writeFiles(files);
594
+ await writeBinaryToPath(outputPath, zipData);
610
595
  }
611
596
  }
612
597
  /**
@@ -630,7 +615,7 @@ class ObfProcessor extends BaseProcessor {
630
615
  */
631
616
  async validate(filePath) {
632
617
  const ObfValidator = this.getObfValidator();
633
- return ObfValidator.validateFile(filePath);
618
+ return ObfValidator.validateFile(filePath, this.options.fileAdapter);
634
619
  }
635
620
  /**
636
621
  * Extract symbol information from an OBF/OBZ file for LLM-based translation.
@@ -639,7 +624,7 @@ class ObfProcessor extends BaseProcessor {
639
624
  * This method uses shared translation utilities that work across all AAC formats.
640
625
  *
641
626
  * @param filePathOrBuffer - Path to OBF/OBZ file or buffer
642
- * @returns Array of symbol information for LLM processing
627
+ * @returns Promise resolving to symbol information for LLM processing
643
628
  */
644
629
  async extractSymbolsForLLM(filePathOrBuffer) {
645
630
  const tree = await this.loadIntoTree(filePathOrBuffer);
@@ -669,9 +654,10 @@ class ObfProcessor extends BaseProcessor {
669
654
  * @param llmTranslations - Array of LLM translations with symbol info
670
655
  * @param outputPath - Where to save the translated OBF/OBZ file
671
656
  * @param options - Translation options (e.g., allowPartial for testing)
672
- * @returns Buffer of the translated OBF/OBZ file
657
+ * @returns Promise resolving to a buffer of the translated OBF/OBZ file
673
658
  */
674
659
  async processLLMTranslations(filePathOrBuffer, llmTranslations, outputPath, options) {
660
+ const { readBinaryFromInput } = this.options.fileAdapter;
675
661
  const tree = await this.loadIntoTree(filePathOrBuffer);
676
662
  // Validate translations using shared utility
677
663
  const buttonIds = Object.values(tree.pages).flatMap((page) => page.buttons.map((b) => b.id));
@@ -710,7 +696,7 @@ class ObfProcessor extends BaseProcessor {
710
696
  });
711
697
  // Save and return
712
698
  await this.saveFromTree(tree, outputPath);
713
- return readBinaryFromInput(outputPath);
699
+ return await readBinaryFromInput(outputPath);
714
700
  }
715
701
  getObfValidator() {
716
702
  try {
@@ -1,9 +1,8 @@
1
1
  import { BaseProcessor, } from '../core/baseProcessor';
2
2
  import { AACTree, AACPage, AACButton, AACSemanticIntent } from '../core/treeStructure';
3
- // Removed unused import: FileProcessor
4
3
  import { XMLParser, XMLValidator, XMLBuilder } from 'fast-xml-parser';
5
4
  import { ValidationFailureError, buildValidationResultFromMessage, } from '../validation/validationTypes';
6
- import { getBasename, readBinaryFromInput, readTextFromInput, writeBinaryToPath, writeTextToPath, encodeText, } from '../utils/io';
5
+ import { getBasename, encodeText } from '../utils/io';
7
6
  class OpmlProcessor extends BaseProcessor {
8
7
  constructor(options) {
9
8
  super(options);
@@ -50,8 +49,8 @@ class OpmlProcessor extends BaseProcessor {
50
49
  return { page, childPages };
51
50
  }
52
51
  async extractTexts(filePathOrBuffer) {
53
- await Promise.resolve();
54
- const content = readTextFromInput(filePathOrBuffer);
52
+ const { readTextFromInput } = this.options.fileAdapter;
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 = [];
@@ -82,10 +81,10 @@ class OpmlProcessor extends BaseProcessor {
82
81
  return texts;
83
82
  }
84
83
  async loadIntoTree(filePathOrBuffer) {
85
- await Promise.resolve();
84
+ const { readBinaryFromInput, readTextFromInput } = this.options.fileAdapter;
86
85
  const filename = typeof filePathOrBuffer === 'string' ? getBasename(filePathOrBuffer) : 'upload.opml';
87
- const buffer = readBinaryFromInput(filePathOrBuffer);
88
- const content = readTextFromInput(buffer);
86
+ const buffer = await readBinaryFromInput(filePathOrBuffer);
87
+ const content = await readTextFromInput(buffer);
89
88
  try {
90
89
  if (!content || !content.trim()) {
91
90
  const validationResult = buildValidationResultFromMessage({
@@ -168,8 +167,8 @@ class OpmlProcessor extends BaseProcessor {
168
167
  }
169
168
  }
170
169
  async processTexts(filePathOrBuffer, translations, outputPath) {
171
- await Promise.resolve();
172
- const content = readTextFromInput(filePathOrBuffer);
170
+ const { writeBinaryToPath, readTextFromInput } = this.options.fileAdapter;
171
+ const content = await readTextFromInput(filePathOrBuffer);
173
172
  let translatedContent = content;
174
173
  // Apply translations to text attributes in OPML outline elements
175
174
  translations.forEach((translation, originalText) => {
@@ -180,11 +179,11 @@ class OpmlProcessor extends BaseProcessor {
180
179
  }
181
180
  });
182
181
  const resultBuffer = encodeText(translatedContent);
183
- writeBinaryToPath(outputPath, resultBuffer);
182
+ await writeBinaryToPath(outputPath, resultBuffer);
184
183
  return resultBuffer;
185
184
  }
186
185
  async saveFromTree(tree, outputPath) {
187
- await Promise.resolve();
186
+ const { writeTextToPath } = this.options.fileAdapter;
188
187
  // Helper to recursively build outline nodes with cycle detection
189
188
  function buildOutline(page, visited = new Set()) {
190
189
  // Prevent infinite recursion by tracking visited pages
@@ -254,7 +253,7 @@ class OpmlProcessor extends BaseProcessor {
254
253
  attributeNamePrefix: '@_',
255
254
  });
256
255
  const xml = '<?xml version="1.0" encoding="UTF-8"?>\n' + builder.build(opmlObj);
257
- writeTextToPath(outputPath, xml);
256
+ await writeTextToPath(outputPath, xml);
258
257
  }
259
258
  /**
260
259
  * Extract strings with metadata for aac-tools-platform compatibility
@@ -1,8 +1,7 @@
1
1
  import { AACSemanticCategory, AACSemanticIntent, } from '../../core/treeStructure';
2
- import * as fs from 'fs';
3
- import * as path from 'path';
4
- import Database from 'better-sqlite3';
5
2
  import { dotNetTicksToDate } from '../../utils/dotnetTicks';
3
+ import { defaultFileAdapter, extname, getNodeRequire, } from '../../utils/io';
4
+ import { requireBetterSqlite3 } from '../../utils/sqlite';
6
5
  // Minimal Snap helpers (stubs) to align with processors/<engine>/helpers pattern
7
6
  // NOTE: Snap files can store different types of image data in PageSetData:
8
7
  // - PNG/JPEG binaries: Actual images that can be displayed
@@ -11,7 +10,8 @@ import { dotNetTicksToDate } from '../../utils/dotnetTicks';
11
10
  // We extract PNG/JPEG images but skip vector graphics (requires renderer).
12
11
  // NOTE: Snap buttons currently do not populate resolvedImageEntry; these helpers
13
12
  // therefore return empty collections until image resolution is implemented.
14
- function collectFiles(root, matcher, maxDepth = 3) {
13
+ async function collectFiles(root, matcher, maxDepth = 3, fileAdapter = defaultFileAdapter) {
14
+ const { listDir, join, isDirectory } = fileAdapter;
15
15
  const results = new Set();
16
16
  const stack = [{ dir: root, depth: 0 }];
17
17
  while (stack.length > 0) {
@@ -22,14 +22,14 @@ function collectFiles(root, matcher, maxDepth = 3) {
22
22
  continue;
23
23
  let entries;
24
24
  try {
25
- entries = fs.readdirSync(current.dir, { withFileTypes: true });
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
- const fullPath = path.join(current.dir, entry.name);
32
- if (entry.isDirectory()) {
31
+ const fullPath = join(current.dir, 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,17 +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) {
86
+ export async function openImage(dbOrFile, entryPath, fileAdapter = defaultFileAdapter) {
87
+ const { mkTempDir, join, writeBinaryToPath, removePath, dirname } = fileAdapter;
87
88
  let dbPath;
88
89
  let cleanupNeeded = false;
89
90
  // Handle Buffer input by writing to temp file
90
91
  if (Buffer.isBuffer(dbOrFile)) {
91
- if (typeof fs.mkdtempSync !== 'function') {
92
- return null; // Not in Node environment
93
- }
94
- const tempDir = fs.mkdtempSync(path.join(process.cwd(), 'snap-'));
95
- dbPath = path.join(tempDir, 'temp.sps');
96
- fs.writeFileSync(dbPath, dbOrFile);
92
+ const tempDir = await mkTempDir(join(process.cwd(), 'snap-'));
93
+ dbPath = join(tempDir, 'temp.sps');
94
+ await writeBinaryToPath(dbPath, dbOrFile);
97
95
  cleanupNeeded = true;
98
96
  }
99
97
  else if (typeof dbOrFile === 'string') {
@@ -102,9 +100,10 @@ export function openImage(dbOrFile, entryPath) {
102
100
  else {
103
101
  return null;
104
102
  }
103
+ const better_sqlite3 = getNodeRequire()('better-sqlite3');
105
104
  let db = null;
106
105
  try {
107
- db = new Database(dbPath, { readonly: true });
106
+ db = new better_sqlite3.Database(dbPath, { readonly: true });
108
107
  // Query PageSetData for the symbol
109
108
  const row = db
110
109
  .prepare('SELECT Id, Identifier, Data FROM PageSetData WHERE Identifier = ?')
@@ -127,9 +126,9 @@ export function openImage(dbOrFile, entryPath) {
127
126
  }
128
127
  if (cleanupNeeded && dbPath) {
129
128
  try {
130
- fs.unlinkSync(dbPath);
131
- const dir = path.dirname(dbPath);
132
- fs.rmdirSync(dir);
129
+ await removePath(dbPath);
130
+ const dir = dirname(dbPath);
131
+ await removePath(dir);
133
132
  }
134
133
  catch (e) {
135
134
  // Ignore cleanup errors
@@ -143,7 +142,8 @@ export function openImage(dbOrFile, entryPath) {
143
142
  * @param packageNamePattern Optional pattern to filter package names (default: 'TobiiDynavox')
144
143
  * @returns Array of Snap package path information
145
144
  */
146
- export function findSnapPackages(packageNamePattern = 'TobiiDynavox') {
145
+ export async function findSnapPackages(packageNamePattern = 'TobiiDynavox', fileAdapter = defaultFileAdapter) {
146
+ const { join, listDir, isDirectory, pathExists } = fileAdapter;
147
147
  const results = [];
148
148
  // Only works on Windows
149
149
  if (process.platform !== 'win32') {
@@ -154,22 +154,22 @@ export function findSnapPackages(packageNamePattern = 'TobiiDynavox') {
154
154
  if (!localAppData) {
155
155
  return results;
156
156
  }
157
- const packagesPath = path.join(localAppData, 'Packages');
157
+ const packagesPath = join(localAppData, 'Packages');
158
158
  // Check if Packages directory exists
159
- if (!fs.existsSync(packagesPath)) {
159
+ if (!(await pathExists(packagesPath))) {
160
160
  return results;
161
161
  }
162
162
  // Enumerate packages
163
- const packages = fs.readdirSync(packagesPath, { withFileTypes: true });
163
+ const packages = await listDir(packagesPath);
164
164
  for (const packageDir of packages) {
165
- if (!packageDir.isDirectory())
165
+ if (!(await isDirectory(packageDir)))
166
166
  continue;
167
- const packageName = packageDir.name;
167
+ const packageName = packageDir;
168
168
  // Filter by pattern
169
169
  if (packageName.includes(packageNamePattern)) {
170
170
  results.push({
171
171
  packageName,
172
- packagePath: path.join(packagesPath, packageName),
172
+ packagePath: join(packagesPath, packageName),
173
173
  });
174
174
  }
175
175
  }
@@ -185,8 +185,8 @@ export function findSnapPackages(packageNamePattern = 'TobiiDynavox') {
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') {
189
- const packages = findSnapPackages(packageNamePattern);
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,32 +196,33 @@ export function findSnapPackagePath(packageNamePattern = 'TobiiDynavox') {
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') {
199
+ export async function findSnapUsers(packageNamePattern = 'TobiiDynavox', fileAdapter = defaultFileAdapter) {
200
+ const { join, listDir, isDirectory, pathExists } = fileAdapter;
200
201
  const results = [];
201
202
  if (process.platform !== 'win32') {
202
203
  return results;
203
204
  }
204
- const packagePath = findSnapPackagePath(packageNamePattern);
205
+ const packagePath = await findSnapPackagePath(packageNamePattern, fileAdapter);
205
206
  if (!packagePath) {
206
207
  return results;
207
208
  }
208
- const usersRoot = path.join(packagePath, 'LocalState', 'Users');
209
- if (!fs.existsSync(usersRoot)) {
209
+ const usersRoot = join(packagePath, 'LocalState', 'Users');
210
+ if (!(await pathExists(usersRoot))) {
210
211
  return results;
211
212
  }
212
- const entries = fs.readdirSync(usersRoot, { withFileTypes: true });
213
+ const entries = await listDir(usersRoot);
213
214
  for (const entry of entries) {
214
- if (!entry.isDirectory())
215
+ if (!(await isDirectory(entry)))
215
216
  continue;
216
- if (entry.name.toLowerCase().startsWith('swiftkey'))
217
+ if (entry.toLowerCase().startsWith('swiftkey'))
217
218
  continue;
218
- const userPath = path.join(usersRoot, entry.name);
219
- const vocabPaths = collectFiles(userPath, (full) => {
220
- const ext = path.extname(full).toLowerCase();
219
+ const userPath = join(usersRoot, entry);
220
+ const vocabPaths = await collectFiles(userPath, (full) => {
221
+ const ext = extname(full).toLowerCase();
221
222
  return ext === '.sps' || ext === '.spb';
222
- }, 2);
223
+ }, 2, fileAdapter);
223
224
  results.push({
224
- userId: entry.name,
225
+ userId: entry,
225
226
  userPath,
226
227
  vocabPaths,
227
228
  });
@@ -234,8 +235,9 @@ export function findSnapUsers(packageNamePattern = 'TobiiDynavox') {
234
235
  * @param packageNamePattern Optional package filter
235
236
  * @returns Array of vocab file paths
236
237
  */
237
- export function findSnapUserVocabularies(userId, packageNamePattern = 'TobiiDynavox') {
238
- const users = findSnapUsers(packageNamePattern).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);
239
241
  return users.flatMap((u) => u.vocabPaths);
240
242
  }
241
243
  /**
@@ -245,11 +247,13 @@ export function findSnapUserVocabularies(userId, packageNamePattern = 'TobiiDyna
245
247
  * @param packageNamePattern Optional package filter
246
248
  * @returns Array of history file paths (may be empty if not found)
247
249
  */
248
- export function findSnapUserHistory(userId, packageNamePattern = 'TobiiDynavox') {
249
- const user = findSnapUsers(packageNamePattern).find((u) => u.userId === userId);
250
+ export async function findSnapUserHistory(userId, packageNamePattern = 'TobiiDynavox', fileAdapter = defaultFileAdapter) {
251
+ const { basename } = fileAdapter;
252
+ const allUsers = await findSnapUsers(packageNamePattern, fileAdapter);
253
+ const user = allUsers.find((u) => u.userId === userId);
250
254
  if (!user)
251
255
  return [];
252
- return collectFiles(user.userPath, (full) => path.basename(full).toLowerCase().includes('history'), 2);
256
+ return await collectFiles(user.userPath, (full) => basename(full).toLowerCase().includes('history'), 2, fileAdapter);
253
257
  }
254
258
  /**
255
259
  * Check whether TD Snap appears to be installed (Windows only)
@@ -262,9 +266,11 @@ export function isSnapInstalled(packageNamePattern = 'TobiiDynavox') {
262
266
  /**
263
267
  * Read Snap usage history from a pageset file (.sps/.spb)
264
268
  */
265
- export function readSnapUsage(pagesetPath) {
266
- if (!fs.existsSync(pagesetPath))
269
+ export async function readSnapUsage(pagesetPath, fileAdapter = defaultFileAdapter) {
270
+ const { pathExists } = fileAdapter;
271
+ if (!(await pathExists(pagesetPath)))
267
272
  return [];
273
+ const Database = requireBetterSqlite3();
268
274
  const db = new Database(pagesetPath, { readonly: true });
269
275
  const tableCheck = db
270
276
  .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('ButtonUsage','Button')")
@@ -319,8 +325,10 @@ export function readSnapUsage(pagesetPath) {
319
325
  /**
320
326
  * Read Snap usage history for a user (all pagesets)
321
327
  */
322
- export function readSnapUsageForUser(userId, packageNamePattern = 'TobiiDynavox') {
323
- 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);
324
331
  const pagesets = users.flatMap((u) => u.vocabPaths);
325
- return pagesets.flatMap((p) => readSnapUsage(p));
332
+ const usage = await Promise.all(pagesets.map(async (p) => await readSnapUsage(p)));
333
+ return usage.flat();
326
334
  }