@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,9 +2,8 @@ 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 { SnapValidator } from '../validation/snapValidator';
5
- import { getFs, getNodeRequire, getPath, isNodeRuntime, getOs } from '../utils/io';
5
+ import { getNodeRequire, isNodeRuntime } from '../utils/io';
6
6
  import { openSqliteDatabase, requireBetterSqlite3 } from '../utils/sqlite';
7
- import { openZipFromInput } from '../utils/zip';
8
7
  /**
9
8
  * Convert a Buffer or Uint8Array to base64 string (browser and Node compatible)
10
9
  * Node.js Buffers support toString('base64'), but Uint8Arrays in browser do not.
@@ -36,15 +35,15 @@ function mapSnapVisibility(visible) {
36
35
  return visible === 0 ? 'Hidden' : 'Visible';
37
36
  }
38
37
  class SnapProcessor extends BaseProcessor {
39
- constructor(symbolResolver = null, options = {}) {
38
+ constructor(symbolResolver = null, options) {
40
39
  super(options);
41
40
  this.symbolResolver = null;
42
41
  this.loadAudio = false;
43
42
  this.pageLayoutPreference = 'scanning'; // Default to scanning for metrics
44
43
  this.symbolResolver = symbolResolver;
45
- this.loadAudio = options.loadAudio !== undefined ? options.loadAudio : true;
44
+ this.loadAudio = options?.loadAudio !== undefined ? options.loadAudio : true;
46
45
  this.pageLayoutPreference =
47
- options.pageLayoutPreference !== undefined ? options.pageLayoutPreference : 'scanning'; // Default to scanning
46
+ options?.pageLayoutPreference !== undefined ? options.pageLayoutPreference : 'scanning'; // Default to scanning
48
47
  }
49
48
  async extractTexts(filePathOrBuffer) {
50
49
  const tree = await this.loadIntoTree(filePathOrBuffer);
@@ -65,7 +64,7 @@ class SnapProcessor extends BaseProcessor {
65
64
  return texts;
66
65
  }
67
66
  async loadIntoTree(filePathOrBuffer) {
68
- await Promise.resolve();
67
+ const { writeBinaryToPath, removePath, mkTempDir, basename, join } = this.options.fileAdapter;
69
68
  const tree = new AACTree();
70
69
  let dbResult = null;
71
70
  let cleanupTempZip = null;
@@ -73,32 +72,32 @@ class SnapProcessor extends BaseProcessor {
73
72
  // Handle .sub.zip files (Snap pageset backups containing .sps files)
74
73
  let inputFile = filePathOrBuffer;
75
74
  if (typeof filePathOrBuffer === 'string') {
76
- const fileName = getPath().basename(filePathOrBuffer).toLowerCase();
75
+ const fileName = basename(filePathOrBuffer).toLowerCase();
77
76
  if (fileName.endsWith('.sub.zip') || filePathOrBuffer.endsWith('.sub')) {
78
- const fs = getFs();
79
- const path = getPath();
80
- const os = getOs();
81
77
  // Extract .sub.zip to find the embedded .sps file
82
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'snap-sub-'));
83
- const { zip } = await openZipFromInput(filePathOrBuffer);
78
+ const tempDir = await mkTempDir('snap-sub-');
79
+ const zip = await this.options.zipAdapter(filePathOrBuffer);
84
80
  // Find the .sps file in the archive
85
81
  const files = zip.listFiles();
86
82
  const spsFile = files.find((f) => f.endsWith('.sps'));
87
83
  if (!spsFile) {
88
- fs.rmSync(tempDir, { recursive: true, force: true });
84
+ await removePath(tempDir, { recursive: true, force: true });
89
85
  throw new Error('No .sps file found in .sub.zip archive');
90
86
  }
91
87
  // Extract the .sps file
92
88
  const spsData = await zip.readFile(spsFile);
93
- const extractedSpsPath = path.join(tempDir, path.basename(spsFile));
94
- fs.writeFileSync(extractedSpsPath, Buffer.from(spsData));
89
+ const extractedSpsPath = join(tempDir, basename(spsFile));
90
+ await writeBinaryToPath(extractedSpsPath, Buffer.from(spsData));
95
91
  inputFile = extractedSpsPath;
96
- cleanupTempZip = () => {
97
- fs.rmSync(tempDir, { recursive: true, force: true });
92
+ cleanupTempZip = async () => {
93
+ await removePath(tempDir, { recursive: true, force: true });
98
94
  };
99
95
  }
100
96
  }
101
- dbResult = await openSqliteDatabase(inputFile, { readonly: true });
97
+ dbResult = await openSqliteDatabase(inputFile, {
98
+ readonly: true,
99
+ fileAdapter: this.options.fileAdapter,
100
+ });
102
101
  const db = dbResult.db;
103
102
  const getTableColumns = (tableName) => {
104
103
  try {
@@ -683,7 +682,7 @@ class SnapProcessor extends BaseProcessor {
683
682
  }
684
683
  finally {
685
684
  if (dbResult?.cleanup) {
686
- dbResult.cleanup();
685
+ await dbResult.cleanup();
687
686
  }
688
687
  else if (dbResult?.db) {
689
688
  dbResult.db.close();
@@ -691,7 +690,7 @@ class SnapProcessor extends BaseProcessor {
691
690
  // Clean up temporary extracted .sps file from .sub.zip
692
691
  if (cleanupTempZip) {
693
692
  try {
694
- cleanupTempZip();
693
+ await cleanupTempZip();
695
694
  }
696
695
  catch (e) {
697
696
  console.warn('[SnapProcessor] Failed to clean up temporary .sps file:', e);
@@ -700,21 +699,21 @@ class SnapProcessor extends BaseProcessor {
700
699
  }
701
700
  }
702
701
  async processTexts(filePathOrBuffer, translations, outputPath) {
702
+ const { pathExists, mkDir, writeBinaryToPath, readBinaryFromInput, removePath, dirname } = this.options.fileAdapter;
703
703
  if (!isNodeRuntime()) {
704
704
  throw new Error('processTexts is only supported in Node.js environments for Snap files.');
705
705
  }
706
- const fs = getFs();
707
- const path = getPath();
708
706
  if (typeof filePathOrBuffer === 'string') {
709
707
  const inputPath = filePathOrBuffer;
710
- const outputDir = path.dirname(outputPath);
711
- if (!fs.existsSync(outputDir)) {
712
- fs.mkdirSync(outputDir, { recursive: true });
708
+ const outputDir = dirname(outputPath);
709
+ const dirExists = await pathExists(outputDir);
710
+ if (!dirExists) {
711
+ await mkDir(outputDir, { recursive: true });
713
712
  }
714
- if (fs.existsSync(outputPath)) {
715
- fs.unlinkSync(outputPath);
713
+ if (await pathExists(outputPath)) {
714
+ await removePath(outputPath);
716
715
  }
717
- fs.copyFileSync(inputPath, outputPath);
716
+ await writeBinaryToPath(outputPath, await readBinaryFromInput(inputPath));
718
717
  const Database = requireBetterSqlite3();
719
718
  const db = new Database(outputPath, { readonly: false });
720
719
  try {
@@ -776,7 +775,7 @@ class SnapProcessor extends BaseProcessor {
776
775
  finally {
777
776
  db.close();
778
777
  }
779
- return fs.readFileSync(outputPath);
778
+ return await readBinaryFromInput(outputPath);
780
779
  }
781
780
  // Fallback for buffer inputs: rebuild from tree (may drop Snap assets)
782
781
  const tree = await this.loadIntoTree(filePathOrBuffer);
@@ -803,21 +802,20 @@ class SnapProcessor extends BaseProcessor {
803
802
  });
804
803
  });
805
804
  await this.saveFromTree(tree, outputPath);
806
- return fs.readFileSync(outputPath);
805
+ return await readBinaryFromInput(outputPath);
807
806
  }
808
807
  async saveFromTree(tree, outputPath) {
808
+ const { pathExists, mkDir, removePath, dirname } = this.options.fileAdapter;
809
809
  if (!isNodeRuntime()) {
810
810
  throw new Error('saveFromTree is only supported in Node.js environments for Snap files.');
811
811
  }
812
- await Promise.resolve();
813
- const fs = getFs();
814
- const path = getPath();
815
- const outputDir = path.dirname(outputPath);
816
- if (!fs.existsSync(outputDir)) {
817
- fs.mkdirSync(outputDir, { recursive: true });
812
+ const outputDir = dirname(outputPath);
813
+ const dirExists = await pathExists(outputDir);
814
+ if (!dirExists) {
815
+ await mkDir(outputDir, { recursive: true });
818
816
  }
819
- if (fs.existsSync(outputPath)) {
820
- fs.unlinkSync(outputPath);
817
+ if (await pathExists(outputPath)) {
818
+ await removePath(outputPath);
821
819
  }
822
820
  // Create a new SQLite database for Snap format
823
821
  const Database = requireBetterSqlite3();
@@ -1052,7 +1050,6 @@ class SnapProcessor extends BaseProcessor {
1052
1050
  * Add audio recording to a button in the database
1053
1051
  */
1054
1052
  async addAudioToButton(dbPath, buttonId, audioData, metadata) {
1055
- await Promise.resolve();
1056
1053
  if (!isNodeRuntime()) {
1057
1054
  throw new Error('addAudioToButton is only supported in Node.js environments.');
1058
1055
  }
@@ -1090,7 +1087,7 @@ class SnapProcessor extends BaseProcessor {
1090
1087
  const updateButton = db.prepare('UPDATE Button SET MessageRecordingId = ?, UseMessageRecording = 1, SerializedMessageSoundMetadata = ? WHERE Id = ?');
1091
1088
  const metadataJson = metadata ? JSON.stringify({ FileName: metadata }) : null;
1092
1089
  updateButton.run(audioId, metadataJson, buttonId);
1093
- return audioId;
1090
+ return Promise.resolve(audioId);
1094
1091
  }
1095
1092
  finally {
1096
1093
  db.close();
@@ -1100,12 +1097,12 @@ class SnapProcessor extends BaseProcessor {
1100
1097
  * Create a copy of the pageset with audio recordings added
1101
1098
  */
1102
1099
  async createAudioEnhancedPageset(sourceDbPath, targetDbPath, audioMappings) {
1100
+ const { writeBinaryToPath, readBinaryFromInput } = this.options.fileAdapter;
1103
1101
  if (!isNodeRuntime()) {
1104
1102
  throw new Error('createAudioEnhancedPageset is only supported in Node.js environments.');
1105
1103
  }
1106
- const fs = getFs();
1107
1104
  // Copy the source database to target
1108
- fs.copyFileSync(sourceDbPath, targetDbPath);
1105
+ await writeBinaryToPath(targetDbPath, await readBinaryFromInput(sourceDbPath));
1109
1106
  // Add audio recordings to the copy
1110
1107
  for (const [buttonId, audioInfo] of audioMappings.entries()) {
1111
1108
  await this.addAudioToButton(targetDbPath, buttonId, audioInfo.audioData, audioInfo.metadata);
@@ -1167,23 +1164,22 @@ class SnapProcessor extends BaseProcessor {
1167
1164
  * @returns Promise with validation result
1168
1165
  */
1169
1166
  async validate(filePath) {
1170
- return SnapValidator.validateFile(filePath);
1167
+ return SnapValidator.validateFile(filePath, this.options.fileAdapter);
1171
1168
  }
1172
1169
  /**
1173
1170
  * Get available PageLayouts for a Snap file
1174
1171
  * Useful for UI components that want to let users select layout size
1175
1172
  * @param filePath - Path to the Snap file
1176
- * @returns Array of available PageLayouts with their dimensions
1173
+ * @returns Promise resolving to available PageLayouts with their dimensions
1177
1174
  */
1178
- getAvailablePageLayouts(filePath) {
1175
+ async getAvailablePageLayouts(filePath) {
1176
+ const { writeBinaryToPath, removePath, pathExists, join } = this.options.fileAdapter;
1179
1177
  if (!isNodeRuntime()) {
1180
1178
  throw new Error('getAvailablePageLayouts is only supported in Node.js environments.');
1181
1179
  }
1182
- const fs = getFs();
1183
- const path = getPath();
1184
- const dbPath = typeof filePath === 'string' ? filePath : path.join(process.cwd(), 'temp.spb');
1180
+ const dbPath = typeof filePath === 'string' ? filePath : join(process.cwd(), 'temp.spb');
1185
1181
  if (Buffer.isBuffer(filePath)) {
1186
- fs.writeFileSync(dbPath, filePath);
1182
+ await writeBinaryToPath(dbPath, filePath);
1187
1183
  }
1188
1184
  let db = null;
1189
1185
  try {
@@ -1234,9 +1230,10 @@ class SnapProcessor extends BaseProcessor {
1234
1230
  db.close();
1235
1231
  }
1236
1232
  // Clean up temporary file if created from buffer
1237
- if (Buffer.isBuffer(filePath) && fs.existsSync(dbPath)) {
1233
+ const exists = await pathExists(dbPath);
1234
+ if (Buffer.isBuffer(filePath) && exists) {
1238
1235
  try {
1239
- fs.unlinkSync(dbPath);
1236
+ await removePath(dbPath);
1240
1237
  }
1241
1238
  catch (e) {
1242
1239
  console.warn('Failed to clean up temporary file:', e);
@@ -3,10 +3,10 @@ import { AACTree, AACPage, AACButton, AACSemanticCategory, AACSemanticIntent, }
3
3
  import { generateCloneId } from '../utilities/analytics/utils/idGenerator';
4
4
  import { detectCasing, isNumericOrEmpty } from '../core/stringCasing';
5
5
  import { TouchChatValidator } from '../validation/touchChatValidator';
6
- import { getFs, getNodeRequire, getOs, getPath, isNodeRuntime, readBinaryFromInput, } from '../utils/io';
6
+ import { isNodeRuntime } from '../utils/io';
7
7
  import { extractAllButtonsForTranslation, validateTranslationResults, } from '../utilities/translation/translationProcessor';
8
8
  import { openSqliteDatabase, requireBetterSqlite3, } from '../utils/sqlite';
9
- import { openZipFromInput } from '../utils/zip';
9
+ import { getZipEntriesFromAdapter } from './gridset';
10
10
  const toNumberOrUndefined = (value) => typeof value === 'number' ? value : undefined;
11
11
  const toStringOrUndefined = (value) => typeof value === 'string' && value.length > 0 ? value : undefined;
12
12
  const toBooleanOrUndefined = (value) => typeof value === 'number' ? value !== 0 : undefined;
@@ -55,7 +55,7 @@ class TouchChatProcessor extends BaseProcessor {
55
55
  return texts;
56
56
  }
57
57
  async loadIntoTree(filePathOrBuffer) {
58
- await Promise.resolve();
58
+ const { readBinaryFromInput } = this.options.fileAdapter;
59
59
  // Unzip .ce file, extract the .c4v SQLite DB, and parse pages/buttons
60
60
  let db = null;
61
61
  let cleanup;
@@ -63,16 +63,17 @@ class TouchChatProcessor extends BaseProcessor {
63
63
  // Store source file path or buffer
64
64
  this.sourceFile = filePathOrBuffer;
65
65
  // Step 1: Unzip
66
- const zipInput = readBinaryFromInput(filePathOrBuffer);
67
- const { zip } = this.options.zipAdapter
68
- ? await this.options.zipAdapter(zipInput)
69
- : await openZipFromInput(zipInput);
66
+ const zipInput = await readBinaryFromInput(filePathOrBuffer);
67
+ const zip = await this.options.zipAdapter(zipInput);
70
68
  const vocabEntry = zip.listFiles().find((name) => name.endsWith('.c4v'));
71
69
  if (!vocabEntry) {
72
70
  throw new Error('No .c4v vocab DB found in TouchChat export');
73
71
  }
74
72
  const dbBuffer = await zip.readFile(vocabEntry);
75
- const dbResult = await openSqliteDatabase(dbBuffer, { readonly: true });
73
+ const dbResult = await openSqliteDatabase(dbBuffer, {
74
+ readonly: true,
75
+ fileAdapter: this.options.fileAdapter,
76
+ });
76
77
  db = dbResult.db;
77
78
  cleanup = dbResult.cleanup;
78
79
  // Step 3: Create tree and load pages
@@ -467,7 +468,7 @@ class TouchChatProcessor extends BaseProcessor {
467
468
  finally {
468
469
  // Clean up
469
470
  if (cleanup) {
470
- cleanup();
471
+ await cleanup();
471
472
  }
472
473
  else if (db) {
473
474
  db.close();
@@ -475,6 +476,7 @@ class TouchChatProcessor extends BaseProcessor {
475
476
  }
476
477
  }
477
478
  async processTexts(filePathOrBuffer, translations, outputPath) {
479
+ const { pathExists, mkDir, removePath, mkTempDir, writeBinaryToPath, readBinaryFromInput, dirname, join, } = this.options.fileAdapter;
478
480
  if (!isNodeRuntime()) {
479
481
  throw new Error('processTexts is only supported in Node.js environments for TouchChat files.');
480
482
  }
@@ -487,28 +489,25 @@ class TouchChatProcessor extends BaseProcessor {
487
489
  * within the embedded SQLite database, ensuring assets and metadata remain intact.
488
490
  */
489
491
  if (typeof filePathOrBuffer === 'string') {
490
- const fs = getFs();
491
- const path = getPath();
492
- const os = getOs();
493
- const AdmZip = getNodeRequire()('adm-zip');
494
492
  const inputPath = filePathOrBuffer;
495
- const outputDir = path.dirname(outputPath);
496
- if (!fs.existsSync(outputDir)) {
497
- fs.mkdirSync(outputDir, { recursive: true });
493
+ const outputDir = dirname(outputPath);
494
+ const dirExists = await pathExists(outputDir);
495
+ if (!dirExists) {
496
+ await mkDir(outputDir, { recursive: true });
498
497
  }
499
- if (fs.existsSync(outputPath)) {
500
- fs.unlinkSync(outputPath);
498
+ if (await pathExists(outputPath)) {
499
+ await removePath(outputPath);
501
500
  }
502
- const zip = new AdmZip(inputPath);
503
- const entries = zip.getEntries();
501
+ const zip = await this.options.zipAdapter(inputPath);
502
+ const entries = getZipEntriesFromAdapter(zip);
504
503
  const vocabEntry = entries.find((entry) => entry.entryName.endsWith('.c4v'));
505
504
  if (!vocabEntry) {
506
505
  throw new Error('No .c4v vocab DB found in TouchChat export');
507
506
  }
508
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'touchchat-translate-'));
509
- const dbPath = path.join(tempDir, 'vocab.c4v');
507
+ const tempDir = await mkTempDir('touchchat-translate-');
508
+ const dbPath = join(tempDir, 'vocab.c4v');
510
509
  try {
511
- fs.writeFileSync(dbPath, vocabEntry.getData());
510
+ await writeBinaryToPath(dbPath, await vocabEntry.getData());
512
511
  const Database = requireBetterSqlite3();
513
512
  const db = new Database(dbPath, { readonly: false });
514
513
  try {
@@ -561,26 +560,34 @@ class TouchChatProcessor extends BaseProcessor {
561
560
  finally {
562
561
  db.close();
563
562
  }
564
- const outputZip = new AdmZip();
565
- entries.forEach((entry) => {
563
+ const outputZip = await this.options.zipAdapter();
564
+ const files = [];
565
+ for (const entry of entries) {
566
566
  if (entry.entryName === vocabEntry.entryName) {
567
- return;
567
+ continue;
568
568
  }
569
- const data = entry.isDirectory ? Buffer.alloc(0) : entry.getData();
570
- outputZip.addFile(entry.entryName, data, entry.comment || '');
569
+ const data = await entry.getData();
570
+ files.push({
571
+ name: entry.entryName,
572
+ data,
573
+ });
574
+ }
575
+ files.push({
576
+ name: vocabEntry.entryName,
577
+ data: await readBinaryFromInput(dbPath),
571
578
  });
572
- outputZip.addFile(vocabEntry.entryName, fs.readFileSync(dbPath));
573
- outputZip.writeZip(outputPath);
579
+ const zipData = await outputZip.writeFiles(files);
580
+ await writeBinaryToPath(outputPath, zipData);
574
581
  }
575
582
  finally {
576
583
  try {
577
- fs.rmSync(tempDir, { recursive: true, force: true });
584
+ await removePath(tempDir, { recursive: true, force: true });
578
585
  }
579
586
  catch {
580
587
  // Best-effort cleanup
581
588
  }
582
589
  }
583
- return fs.readFileSync(outputPath);
590
+ return await readBinaryFromInput(outputPath);
584
591
  }
585
592
  // Fallback for buffer inputs: rebuild from tree (may drop TouchChat metadata)
586
593
  const tree = await this.loadIntoTree(filePathOrBuffer);
@@ -607,20 +614,16 @@ class TouchChatProcessor extends BaseProcessor {
607
614
  });
608
615
  });
609
616
  await this.saveFromTree(tree, outputPath);
610
- const fs = getFs();
611
- return fs.readFileSync(outputPath);
617
+ return await readBinaryFromInput(outputPath);
612
618
  }
613
619
  async saveFromTree(tree, outputPath) {
614
- await Promise.resolve();
620
+ const { writeBinaryToPath, mkTempDir, readBinaryFromInput, pathExists, removePath, join } = this.options.fileAdapter;
615
621
  if (!isNodeRuntime()) {
616
622
  throw new Error('saveFromTree is only supported in Node.js environments for TouchChat files.');
617
623
  }
618
- const fs = getFs();
619
- const path = getPath();
620
- const os = getOs();
621
624
  // Create a TouchChat database that matches the expected schema for loading
622
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'touchchat-export-'));
623
- const dbPath = path.join(tmpDir, 'vocab.c4v');
625
+ const tmpDir = await mkTempDir('touchchat-export-');
626
+ const dbPath = join(tmpDir, 'vocab.c4v');
624
627
  try {
625
628
  const Database = requireBetterSqlite3();
626
629
  const db = new Database(dbPath);
@@ -913,15 +916,20 @@ class TouchChatProcessor extends BaseProcessor {
913
916
  }
914
917
  db.close();
915
918
  // Create zip file with the database
916
- const AdmZip = getNodeRequire()('adm-zip');
917
- const zip = new AdmZip();
918
- zip.addLocalFile(dbPath, '', 'vocab.c4v');
919
- zip.writeZip(outputPath);
919
+ const zip = await this.options.zipAdapter();
920
+ const data = await readBinaryFromInput(dbPath);
921
+ const zipData = await zip.writeFiles([
922
+ {
923
+ name: 'vocab.c4v',
924
+ data,
925
+ },
926
+ ]);
927
+ await writeBinaryToPath(outputPath, zipData);
920
928
  }
921
929
  finally {
922
930
  // Clean up
923
- if (fs.existsSync(tmpDir)) {
924
- fs.rmSync(tmpDir, { recursive: true, force: true });
931
+ if (await pathExists(tmpDir)) {
932
+ await removePath(tmpDir, { recursive: true, force: true });
925
933
  }
926
934
  }
927
935
  }
@@ -1004,7 +1012,7 @@ class TouchChatProcessor extends BaseProcessor {
1004
1012
  const outputPath = filePath.replace(/\.ce$/, '_translated.ce');
1005
1013
  // Use existing processTexts method
1006
1014
  await this.processTexts(filePath, translations, outputPath);
1007
- return Promise.resolve(outputPath);
1015
+ return outputPath;
1008
1016
  }
1009
1017
  catch (error) {
1010
1018
  return Promise.reject(new Error(`Failed to generate translated download: ${error instanceof Error ? error.message : 'Unknown error'}`));
@@ -1016,7 +1024,7 @@ class TouchChatProcessor extends BaseProcessor {
1016
1024
  * @returns Promise with validation result
1017
1025
  */
1018
1026
  async validate(filePath) {
1019
- return TouchChatValidator.validateFile(filePath);
1027
+ return await TouchChatValidator.validateFile(filePath, this.options.fileAdapter);
1020
1028
  }
1021
1029
  /**
1022
1030
  * Extract symbol information from a TouchChat file for LLM-based translation.
@@ -1025,7 +1033,7 @@ class TouchChatProcessor extends BaseProcessor {
1025
1033
  * This method uses shared translation utilities that work across all AAC formats.
1026
1034
  *
1027
1035
  * @param filePathOrBuffer - Path to TouchChat .ce file or buffer
1028
- * @returns Array of symbol information for LLM processing
1036
+ * @returns Promise resolving to symbol information for LLM processing
1029
1037
  */
1030
1038
  async extractSymbolsForLLM(filePathOrBuffer) {
1031
1039
  const tree = await this.loadIntoTree(filePathOrBuffer);
@@ -1055,9 +1063,10 @@ class TouchChatProcessor extends BaseProcessor {
1055
1063
  * @param llmTranslations - Array of LLM translations with symbol info
1056
1064
  * @param outputPath - Where to save the translated TouchChat file
1057
1065
  * @param options - Translation options (e.g., allowPartial for testing)
1058
- * @returns Buffer of the translated TouchChat file
1066
+ * @returns Promise resolving to a buffer of the translated TouchChat file
1059
1067
  */
1060
1068
  async processLLMTranslations(filePathOrBuffer, llmTranslations, outputPath, options) {
1069
+ const { readBinaryFromInput } = this.options.fileAdapter;
1061
1070
  if (!isNodeRuntime()) {
1062
1071
  throw new Error('processLLMTranslations is only supported in Node.js environments for TouchChat files.');
1063
1072
  }
@@ -1099,8 +1108,7 @@ class TouchChatProcessor extends BaseProcessor {
1099
1108
  });
1100
1109
  // Save and return
1101
1110
  await this.saveFromTree(tree, outputPath);
1102
- const fs = getFs();
1103
- return fs.readFileSync(outputPath);
1111
+ return await readBinaryFromInput(outputPath);
1104
1112
  }
1105
1113
  }
1106
1114
  export { TouchChatProcessor };
@@ -60,8 +60,9 @@ export function exportHistoryToBaton(entries, options) {
60
60
  /**
61
61
  * Read Grid 3 phrase history from a history.sqlite database and tag entries with their source.
62
62
  */
63
- export function readGrid3History(historyDbPath) {
64
- return readGrid3HistoryImpl(historyDbPath).map((e) => ({
63
+ export async function readGrid3History(historyDbPath) {
64
+ const history = await readGrid3HistoryImpl(historyDbPath);
65
+ return history.map((e) => ({
65
66
  ...e,
66
67
  source: 'Grid',
67
68
  }));
@@ -69,8 +70,9 @@ export function readGrid3History(historyDbPath) {
69
70
  /**
70
71
  * Read Grid 3 history for a specific user/language combination.
71
72
  */
72
- export function readGrid3HistoryForUser(userName, langCode) {
73
- return readGrid3HistoryForUserImpl(userName, langCode).map((e) => ({
73
+ export async function readGrid3HistoryForUser(userName, langCode) {
74
+ const history = await readGrid3HistoryForUserImpl(userName, langCode);
75
+ return history.map((e) => ({
74
76
  ...e,
75
77
  source: 'Grid',
76
78
  }));
@@ -78,39 +80,43 @@ export function readGrid3HistoryForUser(userName, langCode) {
78
80
  /**
79
81
  * Read every available Grid 3 history database on the machine.
80
82
  */
81
- export function readAllGrid3History() {
82
- return readAllGrid3HistoryImpl().map((e) => ({ ...e, source: 'Grid' }));
83
+ export async function readAllGrid3History() {
84
+ const history = await readAllGrid3HistoryImpl();
85
+ return history.map((e) => ({ ...e, source: 'Grid' }));
83
86
  }
84
87
  /**
85
88
  * Read Snap button usage from a pageset database and tag entries with source.
86
89
  */
87
- export function readSnapUsage(pagesetPath) {
88
- return readSnapUsageImpl(pagesetPath).map((e) => ({ ...e, source: 'Snap' }));
90
+ export async function readSnapUsage(pagesetPath) {
91
+ const usage = await readSnapUsageImpl(pagesetPath);
92
+ return usage.map((e) => ({ ...e, source: 'Snap' }));
89
93
  }
90
94
  /**
91
95
  * Read Snap usage for a specific user across all discovered pagesets.
92
96
  */
93
- export function readSnapUsageForUser(userId, packageNamePattern = 'TobiiDynavox') {
94
- return readSnapUsageForUserImpl(userId, packageNamePattern).map((e) => ({
97
+ export async function readSnapUsageForUser(userId, packageNamePattern = 'TobiiDynavox') {
98
+ const usage = await readSnapUsageForUserImpl(userId, packageNamePattern);
99
+ return usage.map((e) => ({
95
100
  ...e,
96
101
  source: 'Snap',
97
102
  }));
98
103
  }
99
- export function listSnapUsers() {
100
- return findSnapUsers();
104
+ export async function listSnapUsers() {
105
+ return await findSnapUsers();
101
106
  }
102
107
  /**
103
108
  * List Grid 3 users on the current machine.
104
109
  */
105
- export function listGrid3Users() {
106
- return findGrid3Users();
110
+ export async function listGrid3Users() {
111
+ return await findGrid3Users();
107
112
  }
108
113
  /**
109
114
  * Convenience helper to gather all available history across Grid 3 and Snap.
110
115
  * Returns an empty array if no history files are present.
111
116
  */
112
- export function collectUnifiedHistory() {
113
- const gridHistory = readAllGrid3History();
114
- const snapHistory = findSnapUsers().flatMap((u) => readSnapUsageForUser(u.userId));
115
- return [...gridHistory, ...snapHistory];
117
+ export async function collectUnifiedHistory() {
118
+ const gridHistory = await readAllGrid3History();
119
+ const users = await findSnapUsers();
120
+ const snapHistory = await Promise.all(users.map(async (u) => await readSnapUsageForUser(u.userId)));
121
+ return [...gridHistory, ...snapHistory.flat()];
116
122
  }
@@ -23,7 +23,7 @@ export class ComparisonAnalyzer {
23
23
  /**
24
24
  * Compare two board sets
25
25
  */
26
- compare(targetResult, compareResult, options) {
26
+ async compare(targetResult, compareResult, options) {
27
27
  // Create base result from target
28
28
  const baseResult = { ...targetResult };
29
29
  // Create word maps with normalized keys
@@ -77,7 +77,7 @@ export class ComparisonAnalyzer {
77
77
  };
78
78
  });
79
79
  // Calculate CARE components
80
- const careComponents = this.calculateCareComponents(targetResult, compareResult, overlappingWords, options);
80
+ const careComponents = await this.calculateCareComponents(targetResult, compareResult, overlappingWords, options);
81
81
  // Analyze high/low effort words
82
82
  const highEffortWords = [];
83
83
  const lowEffortWords = [];
@@ -110,7 +110,7 @@ export class ComparisonAnalyzer {
110
110
  // Sentence analysis
111
111
  let sentences = [];
112
112
  if (options?.includeSentences) {
113
- const testSentences = this.referenceLoader.loadSentences();
113
+ const testSentences = await this.referenceLoader.loadSentences();
114
114
  const targetSentences = this.sentenceAnalyzer.analyzeSentences(targetResult, testSentences);
115
115
  const compareSentences = this.sentenceAnalyzer.analyzeSentences(compareResult, testSentences);
116
116
  sentences = targetSentences.map((ts, idx) => ({
@@ -123,7 +123,7 @@ export class ComparisonAnalyzer {
123
123
  }));
124
124
  }
125
125
  // Core vocabulary analysis
126
- const coreLists = this.referenceLoader.loadCoreLists();
126
+ const coreLists = await this.referenceLoader.loadCoreLists();
127
127
  const cores = {};
128
128
  coreLists.forEach((list) => {
129
129
  let targetTotal = 0;
@@ -171,8 +171,8 @@ export class ComparisonAnalyzer {
171
171
  }
172
172
  });
173
173
  // Fringe vocabulary analysis
174
- const fringeWords = this.analyzeFringe(targetWords, compareWords);
175
- const commonFringeWords = this.analyzeCommonFringe(targetWords, compareWords);
174
+ const fringeWords = await this.analyzeFringe(targetWords, compareWords);
175
+ const commonFringeWords = await this.analyzeCommonFringe(targetWords, compareWords);
176
176
  return {
177
177
  ...baseResult,
178
178
  buttons: enrichedButtons,
@@ -214,9 +214,9 @@ export class ComparisonAnalyzer {
214
214
  /**
215
215
  * Calculate CARE component scores
216
216
  */
217
- calculateCareComponents(targetResult, compareResult, _overlappingWords, options) {
217
+ async calculateCareComponents(targetResult, compareResult, _overlappingWords, options) {
218
218
  // Load common words with baseline efforts (matching Ruby line 527-534)
219
- const commonWordsData = this.referenceLoader.loadCommonWords();
219
+ const commonWordsData = await this.referenceLoader.loadCommonWords();
220
220
  const commonWords = new Map();
221
221
  commonWordsData.words.forEach((word) => {
222
222
  commonWords.set(word.toLowerCase(), commonWordsData.efforts[word] || 0);
@@ -268,10 +268,10 @@ export class ComparisonAnalyzer {
268
268
  }
269
269
  });
270
270
  // Load reference data
271
- const coreLists = this.referenceLoader.loadCoreLists();
272
- const fringe = this.referenceLoader.loadFringe();
273
- const commonFringe = this.referenceLoader.loadCommonFringe();
274
- const sentences = this.referenceLoader.loadSentences();
271
+ const coreLists = await this.referenceLoader.loadCoreLists();
272
+ const fringe = await this.referenceLoader.loadFringe();
273
+ const commonFringe = await this.referenceLoader.loadCommonFringe();
274
+ const sentences = await this.referenceLoader.loadSentences();
275
275
  // Calculate core coverage and effort (matching Ruby lines 609-647)
276
276
  let coreCount = 0;
277
277
  let compCoreCount = 0;
@@ -423,8 +423,8 @@ export class ComparisonAnalyzer {
423
423
  /**
424
424
  * Analyze fringe vocabulary
425
425
  */
426
- analyzeFringe(targetWords, compareWords) {
427
- const fringe = this.referenceLoader.loadFringe();
426
+ async analyzeFringe(targetWords, compareWords) {
427
+ const fringe = await this.referenceLoader.loadFringe();
428
428
  const result = [];
429
429
  fringe.forEach((word) => {
430
430
  const key = this.normalize(word);
@@ -444,8 +444,8 @@ export class ComparisonAnalyzer {
444
444
  /**
445
445
  * Analyze common fringe vocabulary
446
446
  */
447
- analyzeCommonFringe(targetWords, compareWords) {
448
- const fringe = this.referenceLoader.loadFringe();
447
+ async analyzeCommonFringe(targetWords, compareWords) {
448
+ const fringe = await this.referenceLoader.loadFringe();
449
449
  const result = [];
450
450
  fringe.forEach((word) => {
451
451
  const key = this.normalize(word);