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