@willwade/aac-processors 0.1.20 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +20 -33
  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 +39 -42
  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 +26 -62
  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 +38 -41
  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 -57
@@ -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,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,6 +64,7 @@ class SnapProcessor extends BaseProcessor {
65
64
  return texts;
66
65
  }
67
66
  async loadIntoTree(filePathOrBuffer) {
67
+ const { writeBinaryToPath, removePath, mkTempDir, basename, join } = this.options.fileAdapter;
68
68
  await Promise.resolve();
69
69
  const tree = new AACTree();
70
70
  let dbResult = null;
@@ -73,32 +73,32 @@ class SnapProcessor extends BaseProcessor {
73
73
  // Handle .sub.zip files (Snap pageset backups containing .sps files)
74
74
  let inputFile = filePathOrBuffer;
75
75
  if (typeof filePathOrBuffer === 'string') {
76
- const fileName = getPath().basename(filePathOrBuffer).toLowerCase();
76
+ const fileName = basename(filePathOrBuffer).toLowerCase();
77
77
  if (fileName.endsWith('.sub.zip') || filePathOrBuffer.endsWith('.sub')) {
78
- const fs = getFs();
79
- const path = getPath();
80
- const os = getOs();
81
78
  // 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);
79
+ const tempDir = mkTempDir('snap-sub-');
80
+ const zip = await this.options.zipAdapter(filePathOrBuffer);
84
81
  // Find the .sps file in the archive
85
82
  const files = zip.listFiles();
86
83
  const spsFile = files.find((f) => f.endsWith('.sps'));
87
84
  if (!spsFile) {
88
- fs.rmSync(tempDir, { recursive: true, force: true });
85
+ removePath(tempDir, { recursive: true, force: true });
89
86
  throw new Error('No .sps file found in .sub.zip archive');
90
87
  }
91
88
  // Extract the .sps file
92
89
  const spsData = await zip.readFile(spsFile);
93
- const extractedSpsPath = path.join(tempDir, path.basename(spsFile));
94
- fs.writeFileSync(extractedSpsPath, Buffer.from(spsData));
90
+ const extractedSpsPath = join(tempDir, basename(spsFile));
91
+ writeBinaryToPath(extractedSpsPath, Buffer.from(spsData));
95
92
  inputFile = extractedSpsPath;
96
93
  cleanupTempZip = () => {
97
- fs.rmSync(tempDir, { recursive: true, force: true });
94
+ removePath(tempDir, { recursive: true, force: true });
98
95
  };
99
96
  }
100
97
  }
101
- dbResult = await openSqliteDatabase(inputFile, { readonly: true });
98
+ dbResult = await openSqliteDatabase(inputFile, {
99
+ readonly: true,
100
+ fileAdapter: this.options.fileAdapter,
101
+ });
102
102
  const db = dbResult.db;
103
103
  const getTableColumns = (tableName) => {
104
104
  try {
@@ -700,21 +700,20 @@ class SnapProcessor extends BaseProcessor {
700
700
  }
701
701
  }
702
702
  async processTexts(filePathOrBuffer, translations, outputPath) {
703
+ const { pathExists, mkDir, writeBinaryToPath, readBinaryFromInput, removePath, dirname } = this.options.fileAdapter;
703
704
  if (!isNodeRuntime()) {
704
705
  throw new Error('processTexts is only supported in Node.js environments for Snap files.');
705
706
  }
706
- const fs = getFs();
707
- const path = getPath();
708
707
  if (typeof filePathOrBuffer === 'string') {
709
708
  const inputPath = filePathOrBuffer;
710
- const outputDir = path.dirname(outputPath);
711
- if (!fs.existsSync(outputDir)) {
712
- fs.mkdirSync(outputDir, { recursive: true });
709
+ const outputDir = dirname(outputPath);
710
+ if (!pathExists(outputDir)) {
711
+ mkDir(outputDir, { recursive: true });
713
712
  }
714
- if (fs.existsSync(outputPath)) {
715
- fs.unlinkSync(outputPath);
713
+ if (pathExists(outputPath)) {
714
+ removePath(outputPath);
716
715
  }
717
- fs.copyFileSync(inputPath, outputPath);
716
+ writeBinaryToPath(outputPath, 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 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 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
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 });
813
+ const outputDir = dirname(outputPath);
814
+ if (!pathExists(outputDir)) {
815
+ mkDir(outputDir, { recursive: true });
818
816
  }
819
- if (fs.existsSync(outputPath)) {
820
- fs.unlinkSync(outputPath);
817
+ if (pathExists(outputPath)) {
818
+ removePath(outputPath);
821
819
  }
822
820
  // Create a new SQLite database for Snap format
823
821
  const Database = requireBetterSqlite3();
@@ -1100,12 +1098,12 @@ class SnapProcessor extends BaseProcessor {
1100
1098
  * Create a copy of the pageset with audio recordings added
1101
1099
  */
1102
1100
  async createAudioEnhancedPageset(sourceDbPath, targetDbPath, audioMappings) {
1101
+ const { writeBinaryToPath, readBinaryFromInput } = this.options.fileAdapter;
1103
1102
  if (!isNodeRuntime()) {
1104
1103
  throw new Error('createAudioEnhancedPageset is only supported in Node.js environments.');
1105
1104
  }
1106
- const fs = getFs();
1107
1105
  // Copy the source database to target
1108
- fs.copyFileSync(sourceDbPath, targetDbPath);
1106
+ writeBinaryToPath(targetDbPath, readBinaryFromInput(sourceDbPath));
1109
1107
  // Add audio recordings to the copy
1110
1108
  for (const [buttonId, audioInfo] of audioMappings.entries()) {
1111
1109
  await this.addAudioToButton(targetDbPath, buttonId, audioInfo.audioData, audioInfo.metadata);
@@ -1167,7 +1165,7 @@ class SnapProcessor extends BaseProcessor {
1167
1165
  * @returns Promise with validation result
1168
1166
  */
1169
1167
  async validate(filePath) {
1170
- return SnapValidator.validateFile(filePath);
1168
+ return SnapValidator.validateFile(filePath, this.options.fileAdapter);
1171
1169
  }
1172
1170
  /**
1173
1171
  * Get available PageLayouts for a Snap file
@@ -1176,14 +1174,13 @@ class SnapProcessor extends BaseProcessor {
1176
1174
  * @returns Array of available PageLayouts with their dimensions
1177
1175
  */
1178
1176
  getAvailablePageLayouts(filePath) {
1177
+ const { writeBinaryToPath, removePath, pathExists, join } = this.options.fileAdapter;
1179
1178
  if (!isNodeRuntime()) {
1180
1179
  throw new Error('getAvailablePageLayouts is only supported in Node.js environments.');
1181
1180
  }
1182
- const fs = getFs();
1183
- const path = getPath();
1184
- const dbPath = typeof filePath === 'string' ? filePath : path.join(process.cwd(), 'temp.spb');
1181
+ const dbPath = typeof filePath === 'string' ? filePath : join(process.cwd(), 'temp.spb');
1185
1182
  if (Buffer.isBuffer(filePath)) {
1186
- fs.writeFileSync(dbPath, filePath);
1183
+ writeBinaryToPath(dbPath, filePath);
1187
1184
  }
1188
1185
  let db = null;
1189
1186
  try {
@@ -1234,9 +1231,9 @@ class SnapProcessor extends BaseProcessor {
1234
1231
  db.close();
1235
1232
  }
1236
1233
  // Clean up temporary file if created from buffer
1237
- if (Buffer.isBuffer(filePath) && fs.existsSync(dbPath)) {
1234
+ if (Buffer.isBuffer(filePath) && pathExists(dbPath)) {
1238
1235
  try {
1239
- fs.unlinkSync(dbPath);
1236
+ 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,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 };