@willwade/aac-processors 0.1.19 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/browser/core/baseProcessor.js +4 -0
  2. package/dist/browser/processors/applePanelsProcessor.js +24 -31
  3. package/dist/browser/processors/astericsGridProcessor.js +10 -3
  4. package/dist/browser/processors/dotProcessor.js +5 -2
  5. package/dist/browser/processors/gridset/colorUtils.js +354 -0
  6. package/dist/browser/processors/gridset/helpers.js +49 -45
  7. package/dist/browser/processors/gridset/index.js +61 -0
  8. package/dist/browser/processors/gridset/styleHelpers.js +205 -0
  9. package/dist/browser/processors/gridset/symbolExtractor.js +331 -0
  10. package/dist/browser/processors/gridset/symbolSearch.js +248 -0
  11. package/dist/browser/processors/gridset/symbols.js +35 -68
  12. package/dist/browser/processors/gridsetProcessor.js +32 -41
  13. package/dist/browser/processors/obfProcessor.js +53 -45
  14. package/dist/browser/processors/opmlProcessor.js +5 -2
  15. package/dist/browser/processors/snap/helpers.js +49 -45
  16. package/dist/browser/processors/snapProcessor.js +67 -31
  17. package/dist/browser/processors/touchchatProcessor.js +54 -45
  18. package/dist/browser/utilities/analytics/reference/index.js +27 -19
  19. package/dist/browser/utils/io.js +67 -14
  20. package/dist/browser/utils/sqlite.js +6 -8
  21. package/dist/browser/utils/zip.js +45 -43
  22. package/dist/browser/validation/baseValidator.js +5 -0
  23. package/dist/browser/validation/gridsetValidator.js +12 -20
  24. package/dist/browser/validation/obfValidator.js +5 -4
  25. package/dist/browser/validation/snapValidator.js +9 -5
  26. package/dist/browser/validation/touchChatValidator.js +21 -11
  27. package/dist/cli/index.js +10 -15
  28. package/dist/core/baseProcessor.d.ts +7 -7
  29. package/dist/core/baseProcessor.js +4 -0
  30. package/dist/processors/applePanelsProcessor.js +29 -36
  31. package/dist/processors/astericsGridProcessor.js +20 -13
  32. package/dist/processors/dotProcessor.js +10 -7
  33. package/dist/processors/excelProcessor.js +9 -12
  34. package/dist/processors/gridset/helpers.d.ts +9 -11
  35. package/dist/processors/gridset/helpers.js +49 -71
  36. package/dist/processors/gridset/imageDebug.d.ts +3 -5
  37. package/dist/processors/gridset/imageDebug.js +4 -4
  38. package/dist/processors/gridset/password.d.ts +1 -1
  39. package/dist/processors/gridset/symbolExtractor.d.ts +5 -3
  40. package/dist/processors/gridset/symbolExtractor.js +15 -38
  41. package/dist/processors/gridset/symbolSearch.d.ts +3 -2
  42. package/dist/processors/gridset/symbolSearch.js +12 -34
  43. package/dist/processors/gridset/symbols.d.ts +8 -6
  44. package/dist/processors/gridset/symbols.js +34 -67
  45. package/dist/processors/gridset/wordlistHelpers.d.ts +4 -6
  46. package/dist/processors/gridset/wordlistHelpers.js +15 -74
  47. package/dist/processors/gridsetProcessor.js +36 -68
  48. package/dist/processors/obfProcessor.js +58 -73
  49. package/dist/processors/obfsetProcessor.js +2 -2
  50. package/dist/processors/opmlProcessor.js +10 -7
  51. package/dist/processors/snap/helpers.d.ts +8 -8
  52. package/dist/processors/snap/helpers.js +50 -72
  53. package/dist/processors/snapProcessor.js +66 -30
  54. package/dist/processors/touchchatProcessor.js +54 -45
  55. package/dist/utilities/analytics/index.d.ts +3 -2
  56. package/dist/utilities/analytics/index.js +8 -10
  57. package/dist/utilities/analytics/reference/index.d.ts +5 -3
  58. package/dist/utilities/analytics/reference/index.js +26 -18
  59. package/dist/utilities/symbolTools.d.ts +4 -2
  60. package/dist/utilities/symbolTools.js +16 -15
  61. package/dist/utils/io.d.ts +24 -6
  62. package/dist/utils/io.js +64 -14
  63. package/dist/utils/sqlite.d.ts +2 -0
  64. package/dist/utils/sqlite.js +6 -8
  65. package/dist/utils/zip.d.ts +7 -3
  66. package/dist/utils/zip.js +45 -43
  67. package/dist/validation/applePanelsValidator.d.ts +2 -1
  68. package/dist/validation/applePanelsValidator.js +9 -11
  69. package/dist/validation/astericsValidator.d.ts +2 -1
  70. package/dist/validation/astericsValidator.js +5 -4
  71. package/dist/validation/baseValidator.d.ts +2 -2
  72. package/dist/validation/baseValidator.js +5 -0
  73. package/dist/validation/dotValidator.d.ts +2 -1
  74. package/dist/validation/dotValidator.js +5 -4
  75. package/dist/validation/excelValidator.d.ts +2 -1
  76. package/dist/validation/excelValidator.js +5 -4
  77. package/dist/validation/gridsetValidator.d.ts +2 -1
  78. package/dist/validation/gridsetValidator.js +11 -22
  79. package/dist/validation/index.d.ts +2 -2
  80. package/dist/validation/index.js +5 -4
  81. package/dist/validation/obfValidator.d.ts +2 -1
  82. package/dist/validation/obfValidator.js +5 -4
  83. package/dist/validation/obfsetValidator.d.ts +2 -1
  84. package/dist/validation/obfsetValidator.js +5 -4
  85. package/dist/validation/opmlValidator.d.ts +2 -1
  86. package/dist/validation/opmlValidator.js +5 -4
  87. package/dist/validation/snapValidator.d.ts +2 -1
  88. package/dist/validation/snapValidator.js +9 -5
  89. package/dist/validation/touchChatValidator.d.ts +4 -6
  90. package/dist/validation/touchChatValidator.js +21 -11
  91. package/dist/validation/validationTypes.d.ts +8 -1
  92. package/package.json +1 -1
  93. package/dist/core/fileProcessor.d.ts +0 -7
  94. package/dist/core/fileProcessor.js +0 -52
@@ -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
+ 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 = 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 (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 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 = mkTempDir(join(process.cwd(), 'snap-'));
93
+ dbPath = join(tempDir, 'temp.sps');
94
+ 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
+ removePath(dbPath);
130
+ const dir = dirname(dbPath);
131
+ 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 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 (!pathExists(packagesPath)) {
160
160
  return results;
161
161
  }
162
162
  // Enumerate packages
163
- const packages = fs.readdirSync(packagesPath, { withFileTypes: true });
163
+ const packages = listDir(packagesPath);
164
164
  for (const packageDir of packages) {
165
- if (!packageDir.isDirectory())
165
+ if (!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 function findSnapPackagePath(packageNamePattern = 'TobiiDynavox', fileAdapter) {
189
+ const packages = 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 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 = 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 (!pathExists(usersRoot)) {
210
211
  return results;
211
212
  }
212
- const entries = fs.readdirSync(usersRoot, { withFileTypes: true });
213
+ const entries = listDir(usersRoot);
213
214
  for (const entry of entries) {
214
- if (!entry.isDirectory())
215
+ if (!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 userPath = join(usersRoot, entry);
219
220
  const vocabPaths = collectFiles(userPath, (full) => {
220
- const ext = path.extname(full).toLowerCase();
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,8 @@ 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 function findSnapUserVocabularies(userId, packageNamePattern = 'TobiiDynavox', fileAdapter) {
239
+ const users = findSnapUsers(packageNamePattern, fileAdapter).filter((u) => !userId || u.userId === userId);
239
240
  return users.flatMap((u) => u.vocabPaths);
240
241
  }
241
242
  /**
@@ -245,11 +246,12 @@ export function findSnapUserVocabularies(userId, packageNamePattern = 'TobiiDyna
245
246
  * @param packageNamePattern Optional package filter
246
247
  * @returns Array of history file paths (may be empty if not found)
247
248
  */
248
- export function findSnapUserHistory(userId, packageNamePattern = 'TobiiDynavox') {
249
- const user = findSnapUsers(packageNamePattern).find((u) => u.userId === userId);
249
+ export function findSnapUserHistory(userId, packageNamePattern = 'TobiiDynavox', fileAdapter = defaultFileAdapter) {
250
+ const { basename } = fileAdapter;
251
+ const user = findSnapUsers(packageNamePattern, fileAdapter).find((u) => u.userId === userId);
250
252
  if (!user)
251
253
  return [];
252
- return collectFiles(user.userPath, (full) => path.basename(full).toLowerCase().includes('history'), 2);
254
+ return collectFiles(user.userPath, (full) => basename(full).toLowerCase().includes('history'), 2, fileAdapter);
253
255
  }
254
256
  /**
255
257
  * Check whether TD Snap appears to be installed (Windows only)
@@ -262,9 +264,11 @@ export function isSnapInstalled(packageNamePattern = 'TobiiDynavox') {
262
264
  /**
263
265
  * Read Snap usage history from a pageset file (.sps/.spb)
264
266
  */
265
- export function readSnapUsage(pagesetPath) {
266
- if (!fs.existsSync(pagesetPath))
267
+ export function readSnapUsage(pagesetPath, fileAdapter = defaultFileAdapter) {
268
+ const { pathExists } = fileAdapter;
269
+ if (!pathExists(pagesetPath))
267
270
  return [];
271
+ const Database = requireBetterSqlite3();
268
272
  const db = new Database(pagesetPath, { readonly: true });
269
273
  const tableCheck = db
270
274
  .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('ButtonUsage','Button')")
@@ -2,7 +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 { SnapValidator } from '../validation/snapValidator';
5
- import { getFs, getNodeRequire, getPath, isNodeRuntime } from '../utils/io';
5
+ import { getNodeRequire, isNodeRuntime } from '../utils/io';
6
6
  import { openSqliteDatabase, requireBetterSqlite3 } from '../utils/sqlite';
7
7
  /**
8
8
  * Convert a Buffer or Uint8Array to base64 string (browser and Node compatible)
@@ -35,15 +35,15 @@ function mapSnapVisibility(visible) {
35
35
  return visible === 0 ? 'Hidden' : 'Visible';
36
36
  }
37
37
  class SnapProcessor extends BaseProcessor {
38
- constructor(symbolResolver = null, options = {}) {
38
+ constructor(symbolResolver = null, options) {
39
39
  super(options);
40
40
  this.symbolResolver = null;
41
41
  this.loadAudio = false;
42
42
  this.pageLayoutPreference = 'scanning'; // Default to scanning for metrics
43
43
  this.symbolResolver = symbolResolver;
44
- this.loadAudio = options.loadAudio !== undefined ? options.loadAudio : true;
44
+ this.loadAudio = options?.loadAudio !== undefined ? options.loadAudio : true;
45
45
  this.pageLayoutPreference =
46
- options.pageLayoutPreference !== undefined ? options.pageLayoutPreference : 'scanning'; // Default to scanning
46
+ options?.pageLayoutPreference !== undefined ? options.pageLayoutPreference : 'scanning'; // Default to scanning
47
47
  }
48
48
  async extractTexts(filePathOrBuffer) {
49
49
  const tree = await this.loadIntoTree(filePathOrBuffer);
@@ -64,11 +64,41 @@ class SnapProcessor extends BaseProcessor {
64
64
  return texts;
65
65
  }
66
66
  async loadIntoTree(filePathOrBuffer) {
67
+ const { writeBinaryToPath, removePath, mkTempDir, basename, join } = this.options.fileAdapter;
67
68
  await Promise.resolve();
68
69
  const tree = new AACTree();
69
70
  let dbResult = null;
71
+ let cleanupTempZip = null;
70
72
  try {
71
- dbResult = await openSqliteDatabase(filePathOrBuffer, { readonly: true });
73
+ // Handle .sub.zip files (Snap pageset backups containing .sps files)
74
+ let inputFile = filePathOrBuffer;
75
+ if (typeof filePathOrBuffer === 'string') {
76
+ const fileName = basename(filePathOrBuffer).toLowerCase();
77
+ if (fileName.endsWith('.sub.zip') || filePathOrBuffer.endsWith('.sub')) {
78
+ // Extract .sub.zip to find the embedded .sps file
79
+ const tempDir = mkTempDir('snap-sub-');
80
+ const zip = await this.options.zipAdapter(filePathOrBuffer);
81
+ // Find the .sps file in the archive
82
+ const files = zip.listFiles();
83
+ const spsFile = files.find((f) => f.endsWith('.sps'));
84
+ if (!spsFile) {
85
+ removePath(tempDir, { recursive: true, force: true });
86
+ throw new Error('No .sps file found in .sub.zip archive');
87
+ }
88
+ // Extract the .sps file
89
+ const spsData = await zip.readFile(spsFile);
90
+ const extractedSpsPath = join(tempDir, basename(spsFile));
91
+ writeBinaryToPath(extractedSpsPath, Buffer.from(spsData));
92
+ inputFile = extractedSpsPath;
93
+ cleanupTempZip = () => {
94
+ removePath(tempDir, { recursive: true, force: true });
95
+ };
96
+ }
97
+ }
98
+ dbResult = await openSqliteDatabase(inputFile, {
99
+ readonly: true,
100
+ fileAdapter: this.options.fileAdapter,
101
+ });
72
102
  const db = dbResult.db;
73
103
  const getTableColumns = (tableName) => {
74
104
  try {
@@ -658,24 +688,32 @@ class SnapProcessor extends BaseProcessor {
658
688
  else if (dbResult?.db) {
659
689
  dbResult.db.close();
660
690
  }
691
+ // Clean up temporary extracted .sps file from .sub.zip
692
+ if (cleanupTempZip) {
693
+ try {
694
+ cleanupTempZip();
695
+ }
696
+ catch (e) {
697
+ console.warn('[SnapProcessor] Failed to clean up temporary .sps file:', e);
698
+ }
699
+ }
661
700
  }
662
701
  }
663
702
  async processTexts(filePathOrBuffer, translations, outputPath) {
703
+ const { pathExists, mkDir, writeBinaryToPath, readBinaryFromInput, removePath, dirname } = this.options.fileAdapter;
664
704
  if (!isNodeRuntime()) {
665
705
  throw new Error('processTexts is only supported in Node.js environments for Snap files.');
666
706
  }
667
- const fs = getFs();
668
- const path = getPath();
669
707
  if (typeof filePathOrBuffer === 'string') {
670
708
  const inputPath = filePathOrBuffer;
671
- const outputDir = path.dirname(outputPath);
672
- if (!fs.existsSync(outputDir)) {
673
- fs.mkdirSync(outputDir, { recursive: true });
709
+ const outputDir = dirname(outputPath);
710
+ if (!pathExists(outputDir)) {
711
+ mkDir(outputDir, { recursive: true });
674
712
  }
675
- if (fs.existsSync(outputPath)) {
676
- fs.unlinkSync(outputPath);
713
+ if (pathExists(outputPath)) {
714
+ removePath(outputPath);
677
715
  }
678
- fs.copyFileSync(inputPath, outputPath);
716
+ writeBinaryToPath(outputPath, readBinaryFromInput(inputPath));
679
717
  const Database = requireBetterSqlite3();
680
718
  const db = new Database(outputPath, { readonly: false });
681
719
  try {
@@ -737,7 +775,7 @@ class SnapProcessor extends BaseProcessor {
737
775
  finally {
738
776
  db.close();
739
777
  }
740
- return fs.readFileSync(outputPath);
778
+ return readBinaryFromInput(outputPath);
741
779
  }
742
780
  // Fallback for buffer inputs: rebuild from tree (may drop Snap assets)
743
781
  const tree = await this.loadIntoTree(filePathOrBuffer);
@@ -764,21 +802,20 @@ class SnapProcessor extends BaseProcessor {
764
802
  });
765
803
  });
766
804
  await this.saveFromTree(tree, outputPath);
767
- return fs.readFileSync(outputPath);
805
+ return readBinaryFromInput(outputPath);
768
806
  }
769
807
  async saveFromTree(tree, outputPath) {
808
+ const { pathExists, mkDir, removePath, dirname } = this.options.fileAdapter;
770
809
  if (!isNodeRuntime()) {
771
810
  throw new Error('saveFromTree is only supported in Node.js environments for Snap files.');
772
811
  }
773
812
  await Promise.resolve();
774
- const fs = getFs();
775
- const path = getPath();
776
- const outputDir = path.dirname(outputPath);
777
- if (!fs.existsSync(outputDir)) {
778
- fs.mkdirSync(outputDir, { recursive: true });
813
+ const outputDir = dirname(outputPath);
814
+ if (!pathExists(outputDir)) {
815
+ mkDir(outputDir, { recursive: true });
779
816
  }
780
- if (fs.existsSync(outputPath)) {
781
- fs.unlinkSync(outputPath);
817
+ if (pathExists(outputPath)) {
818
+ removePath(outputPath);
782
819
  }
783
820
  // Create a new SQLite database for Snap format
784
821
  const Database = requireBetterSqlite3();
@@ -1061,12 +1098,12 @@ class SnapProcessor extends BaseProcessor {
1061
1098
  * Create a copy of the pageset with audio recordings added
1062
1099
  */
1063
1100
  async createAudioEnhancedPageset(sourceDbPath, targetDbPath, audioMappings) {
1101
+ const { writeBinaryToPath, readBinaryFromInput } = this.options.fileAdapter;
1064
1102
  if (!isNodeRuntime()) {
1065
1103
  throw new Error('createAudioEnhancedPageset is only supported in Node.js environments.');
1066
1104
  }
1067
- const fs = getFs();
1068
1105
  // Copy the source database to target
1069
- fs.copyFileSync(sourceDbPath, targetDbPath);
1106
+ writeBinaryToPath(targetDbPath, readBinaryFromInput(sourceDbPath));
1070
1107
  // Add audio recordings to the copy
1071
1108
  for (const [buttonId, audioInfo] of audioMappings.entries()) {
1072
1109
  await this.addAudioToButton(targetDbPath, buttonId, audioInfo.audioData, audioInfo.metadata);
@@ -1128,7 +1165,7 @@ class SnapProcessor extends BaseProcessor {
1128
1165
  * @returns Promise with validation result
1129
1166
  */
1130
1167
  async validate(filePath) {
1131
- return SnapValidator.validateFile(filePath);
1168
+ return SnapValidator.validateFile(filePath, this.options.fileAdapter);
1132
1169
  }
1133
1170
  /**
1134
1171
  * Get available PageLayouts for a Snap file
@@ -1137,14 +1174,13 @@ class SnapProcessor extends BaseProcessor {
1137
1174
  * @returns Array of available PageLayouts with their dimensions
1138
1175
  */
1139
1176
  getAvailablePageLayouts(filePath) {
1177
+ const { writeBinaryToPath, removePath, pathExists, join } = this.options.fileAdapter;
1140
1178
  if (!isNodeRuntime()) {
1141
1179
  throw new Error('getAvailablePageLayouts is only supported in Node.js environments.');
1142
1180
  }
1143
- const fs = getFs();
1144
- const path = getPath();
1145
- const dbPath = typeof filePath === 'string' ? filePath : path.join(process.cwd(), 'temp.spb');
1181
+ const dbPath = typeof filePath === 'string' ? filePath : join(process.cwd(), 'temp.spb');
1146
1182
  if (Buffer.isBuffer(filePath)) {
1147
- fs.writeFileSync(dbPath, filePath);
1183
+ writeBinaryToPath(dbPath, filePath);
1148
1184
  }
1149
1185
  let db = null;
1150
1186
  try {
@@ -1195,9 +1231,9 @@ class SnapProcessor extends BaseProcessor {
1195
1231
  db.close();
1196
1232
  }
1197
1233
  // Clean up temporary file if created from buffer
1198
- if (Buffer.isBuffer(filePath) && fs.existsSync(dbPath)) {
1234
+ if (Buffer.isBuffer(filePath) && pathExists(dbPath)) {
1199
1235
  try {
1200
- fs.unlinkSync(dbPath);
1236
+ removePath(dbPath);
1201
1237
  }
1202
1238
  catch (e) {
1203
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,6 +55,7 @@ class TouchChatProcessor extends BaseProcessor {
55
55
  return texts;
56
56
  }
57
57
  async loadIntoTree(filePathOrBuffer) {
58
+ const { readBinaryFromInput } = this.options.fileAdapter;
58
59
  await Promise.resolve();
59
60
  // Unzip .ce file, extract the .c4v SQLite DB, and parse pages/buttons
60
61
  let db = null;
@@ -64,15 +65,16 @@ class TouchChatProcessor extends BaseProcessor {
64
65
  this.sourceFile = filePathOrBuffer;
65
66
  // Step 1: Unzip
66
67
  const zipInput = readBinaryFromInput(filePathOrBuffer);
67
- const { zip } = this.options.zipAdapter
68
- ? await this.options.zipAdapter(zipInput)
69
- : await openZipFromInput(zipInput);
68
+ const zip = await this.options.zipAdapter(zipInput);
70
69
  const vocabEntry = zip.listFiles().find((name) => name.endsWith('.c4v'));
71
70
  if (!vocabEntry) {
72
71
  throw new Error('No .c4v vocab DB found in TouchChat export');
73
72
  }
74
73
  const dbBuffer = await zip.readFile(vocabEntry);
75
- const dbResult = await openSqliteDatabase(dbBuffer, { readonly: true });
74
+ const dbResult = await openSqliteDatabase(dbBuffer, {
75
+ readonly: true,
76
+ fileAdapter: this.options.fileAdapter,
77
+ });
76
78
  db = dbResult.db;
77
79
  cleanup = dbResult.cleanup;
78
80
  // Step 3: Create tree and load pages
@@ -475,6 +477,7 @@ class TouchChatProcessor extends BaseProcessor {
475
477
  }
476
478
  }
477
479
  async processTexts(filePathOrBuffer, translations, outputPath) {
480
+ const { pathExists, mkDir, removePath, mkTempDir, writeBinaryToPath, readBinaryFromInput, dirname, join, } = this.options.fileAdapter;
478
481
  if (!isNodeRuntime()) {
479
482
  throw new Error('processTexts is only supported in Node.js environments for TouchChat files.');
480
483
  }
@@ -487,28 +490,24 @@ class TouchChatProcessor extends BaseProcessor {
487
490
  * within the embedded SQLite database, ensuring assets and metadata remain intact.
488
491
  */
489
492
  if (typeof filePathOrBuffer === 'string') {
490
- const fs = getFs();
491
- const path = getPath();
492
- const os = getOs();
493
- const AdmZip = getNodeRequire()('adm-zip');
494
493
  const inputPath = filePathOrBuffer;
495
- const outputDir = path.dirname(outputPath);
496
- if (!fs.existsSync(outputDir)) {
497
- fs.mkdirSync(outputDir, { recursive: true });
494
+ const outputDir = dirname(outputPath);
495
+ if (!pathExists(outputDir)) {
496
+ mkDir(outputDir, { recursive: true });
498
497
  }
499
- if (fs.existsSync(outputPath)) {
500
- fs.unlinkSync(outputPath);
498
+ if (pathExists(outputPath)) {
499
+ 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 = mkTempDir('touchchat-translate-');
508
+ const dbPath = join(tempDir, 'vocab.c4v');
510
509
  try {
511
- fs.writeFileSync(dbPath, vocabEntry.getData());
510
+ 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: readBinaryFromInput(dbPath),
571
578
  });
572
- outputZip.addFile(vocabEntry.entryName, fs.readFileSync(dbPath));
573
- outputZip.writeZip(outputPath);
579
+ const zipData = await outputZip.writeFiles(files);
580
+ writeBinaryToPath(outputPath, zipData);
574
581
  }
575
582
  finally {
576
583
  try {
577
- fs.rmSync(tempDir, { recursive: true, force: true });
584
+ 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 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,17 @@ 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 readBinaryFromInput(outputPath);
612
618
  }
613
619
  async saveFromTree(tree, outputPath) {
620
+ const { writeBinaryToPath, mkTempDir, readBinaryFromInput, pathExists, removePath, join } = this.options.fileAdapter;
614
621
  await Promise.resolve();
615
622
  if (!isNodeRuntime()) {
616
623
  throw new Error('saveFromTree is only supported in Node.js environments for TouchChat files.');
617
624
  }
618
- const fs = getFs();
619
- const path = getPath();
620
- const os = getOs();
621
625
  // 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');
626
+ const tmpDir = mkTempDir('touchchat-export-');
627
+ const dbPath = join(tmpDir, 'vocab.c4v');
624
628
  try {
625
629
  const Database = requireBetterSqlite3();
626
630
  const db = new Database(dbPath);
@@ -913,15 +917,20 @@ class TouchChatProcessor extends BaseProcessor {
913
917
  }
914
918
  db.close();
915
919
  // 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);
920
+ const zip = await this.options.zipAdapter();
921
+ const data = readBinaryFromInput(dbPath);
922
+ const zipData = await zip.writeFiles([
923
+ {
924
+ name: 'vocab.c4v',
925
+ data,
926
+ },
927
+ ]);
928
+ writeBinaryToPath(outputPath, zipData);
920
929
  }
921
930
  finally {
922
931
  // Clean up
923
- if (fs.existsSync(tmpDir)) {
924
- fs.rmSync(tmpDir, { recursive: true, force: true });
932
+ if (pathExists(tmpDir)) {
933
+ removePath(tmpDir, { recursive: true, force: true });
925
934
  }
926
935
  }
927
936
  }
@@ -1016,7 +1025,7 @@ class TouchChatProcessor extends BaseProcessor {
1016
1025
  * @returns Promise with validation result
1017
1026
  */
1018
1027
  async validate(filePath) {
1019
- return TouchChatValidator.validateFile(filePath);
1028
+ return TouchChatValidator.validateFile(filePath, this.options.fileAdapter);
1020
1029
  }
1021
1030
  /**
1022
1031
  * Extract symbol information from a TouchChat file for LLM-based translation.
@@ -1058,6 +1067,7 @@ class TouchChatProcessor extends BaseProcessor {
1058
1067
  * @returns Buffer of the translated TouchChat file
1059
1068
  */
1060
1069
  async processLLMTranslations(filePathOrBuffer, llmTranslations, outputPath, options) {
1070
+ const { readBinaryFromInput } = this.options.fileAdapter;
1061
1071
  if (!isNodeRuntime()) {
1062
1072
  throw new Error('processLLMTranslations is only supported in Node.js environments for TouchChat files.');
1063
1073
  }
@@ -1099,8 +1109,7 @@ class TouchChatProcessor extends BaseProcessor {
1099
1109
  });
1100
1110
  // Save and return
1101
1111
  await this.saveFromTree(tree, outputPath);
1102
- const fs = getFs();
1103
- return fs.readFileSync(outputPath);
1112
+ return readBinaryFromInput(outputPath);
1104
1113
  }
1105
1114
  }
1106
1115
  export { TouchChatProcessor };