@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.
- package/dist/browser/core/baseProcessor.js +4 -0
- package/dist/browser/processors/applePanelsProcessor.js +24 -31
- package/dist/browser/processors/astericsGridProcessor.js +10 -3
- package/dist/browser/processors/dotProcessor.js +5 -2
- package/dist/browser/processors/gridset/colorUtils.js +354 -0
- package/dist/browser/processors/gridset/helpers.js +49 -45
- package/dist/browser/processors/gridset/index.js +61 -0
- package/dist/browser/processors/gridset/styleHelpers.js +205 -0
- package/dist/browser/processors/gridset/symbolExtractor.js +331 -0
- package/dist/browser/processors/gridset/symbolSearch.js +248 -0
- package/dist/browser/processors/gridset/symbols.js +35 -68
- package/dist/browser/processors/gridsetProcessor.js +32 -41
- package/dist/browser/processors/obfProcessor.js +53 -45
- package/dist/browser/processors/opmlProcessor.js +5 -2
- package/dist/browser/processors/snap/helpers.js +49 -45
- package/dist/browser/processors/snapProcessor.js +67 -31
- package/dist/browser/processors/touchchatProcessor.js +54 -45
- package/dist/browser/utilities/analytics/reference/index.js +27 -19
- package/dist/browser/utils/io.js +67 -14
- package/dist/browser/utils/sqlite.js +6 -8
- package/dist/browser/utils/zip.js +45 -43
- package/dist/browser/validation/baseValidator.js +5 -0
- package/dist/browser/validation/gridsetValidator.js +12 -20
- package/dist/browser/validation/obfValidator.js +5 -4
- package/dist/browser/validation/snapValidator.js +9 -5
- package/dist/browser/validation/touchChatValidator.js +21 -11
- package/dist/cli/index.js +10 -15
- package/dist/core/baseProcessor.d.ts +7 -7
- package/dist/core/baseProcessor.js +4 -0
- package/dist/processors/applePanelsProcessor.js +29 -36
- package/dist/processors/astericsGridProcessor.js +20 -13
- package/dist/processors/dotProcessor.js +10 -7
- package/dist/processors/excelProcessor.js +9 -12
- package/dist/processors/gridset/helpers.d.ts +9 -11
- package/dist/processors/gridset/helpers.js +49 -71
- package/dist/processors/gridset/imageDebug.d.ts +3 -5
- package/dist/processors/gridset/imageDebug.js +4 -4
- package/dist/processors/gridset/password.d.ts +1 -1
- package/dist/processors/gridset/symbolExtractor.d.ts +5 -3
- package/dist/processors/gridset/symbolExtractor.js +15 -38
- package/dist/processors/gridset/symbolSearch.d.ts +3 -2
- package/dist/processors/gridset/symbolSearch.js +12 -34
- package/dist/processors/gridset/symbols.d.ts +8 -6
- package/dist/processors/gridset/symbols.js +34 -67
- package/dist/processors/gridset/wordlistHelpers.d.ts +4 -6
- package/dist/processors/gridset/wordlistHelpers.js +15 -74
- package/dist/processors/gridsetProcessor.js +36 -68
- package/dist/processors/obfProcessor.js +58 -73
- package/dist/processors/obfsetProcessor.js +2 -2
- package/dist/processors/opmlProcessor.js +10 -7
- package/dist/processors/snap/helpers.d.ts +8 -8
- package/dist/processors/snap/helpers.js +50 -72
- package/dist/processors/snapProcessor.js +66 -30
- package/dist/processors/touchchatProcessor.js +54 -45
- package/dist/utilities/analytics/index.d.ts +3 -2
- package/dist/utilities/analytics/index.js +8 -10
- package/dist/utilities/analytics/reference/index.d.ts +5 -3
- package/dist/utilities/analytics/reference/index.js +26 -18
- package/dist/utilities/symbolTools.d.ts +4 -2
- package/dist/utilities/symbolTools.js +16 -15
- package/dist/utils/io.d.ts +24 -6
- package/dist/utils/io.js +64 -14
- package/dist/utils/sqlite.d.ts +2 -0
- package/dist/utils/sqlite.js +6 -8
- package/dist/utils/zip.d.ts +7 -3
- package/dist/utils/zip.js +45 -43
- package/dist/validation/applePanelsValidator.d.ts +2 -1
- package/dist/validation/applePanelsValidator.js +9 -11
- package/dist/validation/astericsValidator.d.ts +2 -1
- package/dist/validation/astericsValidator.js +5 -4
- package/dist/validation/baseValidator.d.ts +2 -2
- package/dist/validation/baseValidator.js +5 -0
- package/dist/validation/dotValidator.d.ts +2 -1
- package/dist/validation/dotValidator.js +5 -4
- package/dist/validation/excelValidator.d.ts +2 -1
- package/dist/validation/excelValidator.js +5 -4
- package/dist/validation/gridsetValidator.d.ts +2 -1
- package/dist/validation/gridsetValidator.js +11 -22
- package/dist/validation/index.d.ts +2 -2
- package/dist/validation/index.js +5 -4
- package/dist/validation/obfValidator.d.ts +2 -1
- package/dist/validation/obfValidator.js +5 -4
- package/dist/validation/obfsetValidator.d.ts +2 -1
- package/dist/validation/obfsetValidator.js +5 -4
- package/dist/validation/opmlValidator.d.ts +2 -1
- package/dist/validation/opmlValidator.js +5 -4
- package/dist/validation/snapValidator.d.ts +2 -1
- package/dist/validation/snapValidator.js +9 -5
- package/dist/validation/touchChatValidator.d.ts +4 -6
- package/dist/validation/touchChatValidator.js +21 -11
- package/dist/validation/validationTypes.d.ts +8 -1
- package/package.json +1 -1
- package/dist/core/fileProcessor.d.ts +0 -7
- 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 =
|
|
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 =
|
|
32
|
-
if (
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
131
|
-
const dir =
|
|
132
|
-
|
|
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 =
|
|
157
|
+
const packagesPath = join(localAppData, 'Packages');
|
|
158
158
|
// Check if Packages directory exists
|
|
159
|
-
if (!
|
|
159
|
+
if (!pathExists(packagesPath)) {
|
|
160
160
|
return results;
|
|
161
161
|
}
|
|
162
162
|
// Enumerate packages
|
|
163
|
-
const packages =
|
|
163
|
+
const packages = listDir(packagesPath);
|
|
164
164
|
for (const packageDir of packages) {
|
|
165
|
-
if (!
|
|
165
|
+
if (!isDirectory(packageDir))
|
|
166
166
|
continue;
|
|
167
|
-
const packageName = packageDir
|
|
167
|
+
const packageName = packageDir;
|
|
168
168
|
// Filter by pattern
|
|
169
169
|
if (packageName.includes(packageNamePattern)) {
|
|
170
170
|
results.push({
|
|
171
171
|
packageName,
|
|
172
|
-
packagePath:
|
|
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 =
|
|
209
|
-
if (!
|
|
209
|
+
const usersRoot = join(packagePath, 'LocalState', 'Users');
|
|
210
|
+
if (!pathExists(usersRoot)) {
|
|
210
211
|
return results;
|
|
211
212
|
}
|
|
212
|
-
const entries =
|
|
213
|
+
const entries = listDir(usersRoot);
|
|
213
214
|
for (const entry of entries) {
|
|
214
|
-
if (!
|
|
215
|
+
if (!isDirectory(entry))
|
|
215
216
|
continue;
|
|
216
|
-
if (entry.
|
|
217
|
+
if (entry.toLowerCase().startsWith('swiftkey'))
|
|
217
218
|
continue;
|
|
218
|
-
const userPath =
|
|
219
|
+
const userPath = join(usersRoot, entry);
|
|
219
220
|
const vocabPaths = collectFiles(userPath, (full) => {
|
|
220
|
-
const ext =
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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 {
|
|
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
|
|
44
|
+
this.loadAudio = options?.loadAudio !== undefined ? options.loadAudio : true;
|
|
45
45
|
this.pageLayoutPreference =
|
|
46
|
-
options
|
|
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
|
-
|
|
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 =
|
|
672
|
-
if (!
|
|
673
|
-
|
|
709
|
+
const outputDir = dirname(outputPath);
|
|
710
|
+
if (!pathExists(outputDir)) {
|
|
711
|
+
mkDir(outputDir, { recursive: true });
|
|
674
712
|
}
|
|
675
|
-
if (
|
|
676
|
-
|
|
713
|
+
if (pathExists(outputPath)) {
|
|
714
|
+
removePath(outputPath);
|
|
677
715
|
}
|
|
678
|
-
|
|
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
|
|
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
|
|
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
|
|
775
|
-
|
|
776
|
-
|
|
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 (
|
|
781
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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) &&
|
|
1234
|
+
if (Buffer.isBuffer(filePath) && pathExists(dbPath)) {
|
|
1199
1235
|
try {
|
|
1200
|
-
|
|
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 {
|
|
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 {
|
|
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
|
|
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, {
|
|
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 =
|
|
496
|
-
if (!
|
|
497
|
-
|
|
494
|
+
const outputDir = dirname(outputPath);
|
|
495
|
+
if (!pathExists(outputDir)) {
|
|
496
|
+
mkDir(outputDir, { recursive: true });
|
|
498
497
|
}
|
|
499
|
-
if (
|
|
500
|
-
|
|
498
|
+
if (pathExists(outputPath)) {
|
|
499
|
+
removePath(outputPath);
|
|
501
500
|
}
|
|
502
|
-
const zip =
|
|
503
|
-
const entries = zip
|
|
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 =
|
|
509
|
-
const dbPath =
|
|
507
|
+
const tempDir = mkTempDir('touchchat-translate-');
|
|
508
|
+
const dbPath = join(tempDir, 'vocab.c4v');
|
|
510
509
|
try {
|
|
511
|
-
|
|
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 =
|
|
565
|
-
|
|
563
|
+
const outputZip = await this.options.zipAdapter();
|
|
564
|
+
const files = [];
|
|
565
|
+
for (const entry of entries) {
|
|
566
566
|
if (entry.entryName === vocabEntry.entryName) {
|
|
567
|
-
|
|
567
|
+
continue;
|
|
568
568
|
}
|
|
569
|
-
const data =
|
|
570
|
-
|
|
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.
|
|
573
|
-
|
|
579
|
+
const zipData = await outputZip.writeFiles(files);
|
|
580
|
+
writeBinaryToPath(outputPath, zipData);
|
|
574
581
|
}
|
|
575
582
|
finally {
|
|
576
583
|
try {
|
|
577
|
-
|
|
584
|
+
removePath(tempDir, { recursive: true, force: true });
|
|
578
585
|
}
|
|
579
586
|
catch {
|
|
580
587
|
// Best-effort cleanup
|
|
581
588
|
}
|
|
582
589
|
}
|
|
583
|
-
return
|
|
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
|
-
|
|
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 =
|
|
623
|
-
const dbPath =
|
|
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
|
|
917
|
-
const
|
|
918
|
-
zip.
|
|
919
|
-
|
|
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 (
|
|
924
|
-
|
|
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
|
-
|
|
1103
|
-
return fs.readFileSync(outputPath);
|
|
1112
|
+
return readBinaryFromInput(outputPath);
|
|
1104
1113
|
}
|
|
1105
1114
|
}
|
|
1106
1115
|
export { TouchChatProcessor };
|