@willwade/aac-processors 0.1.5 → 0.1.6
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/README.md +14 -0
- package/dist/browser/index.browser.js +15 -1
- package/dist/browser/processors/gridset/password.js +11 -0
- package/dist/browser/processors/gridsetProcessor.js +42 -46
- package/dist/browser/processors/obfProcessor.js +47 -63
- package/dist/browser/processors/snapProcessor.js +1031 -0
- package/dist/browser/processors/touchchatProcessor.js +1004 -0
- package/dist/browser/utils/io.js +20 -0
- package/dist/browser/utils/sqlite.js +109 -0
- package/dist/browser/utils/zip.js +54 -0
- package/dist/browser/validation/snapValidator.js +200 -0
- package/dist/browser/validation/touchChatValidator.js +202 -0
- package/dist/index.browser.d.ts +7 -0
- package/dist/index.browser.js +19 -2
- package/dist/processors/gridset/helpers.js +3 -4
- package/dist/processors/gridset/index.d.ts +1 -1
- package/dist/processors/gridset/index.js +3 -2
- package/dist/processors/gridset/password.d.ts +3 -2
- package/dist/processors/gridset/password.js +12 -0
- package/dist/processors/gridset/wordlistHelpers.js +107 -51
- package/dist/processors/gridsetProcessor.js +40 -44
- package/dist/processors/obfProcessor.js +46 -62
- package/dist/processors/snapProcessor.js +60 -54
- package/dist/processors/touchchatProcessor.js +38 -36
- package/dist/utils/io.d.ts +2 -0
- package/dist/utils/io.js +22 -0
- package/dist/utils/sqlite.d.ts +21 -0
- package/dist/utils/sqlite.js +137 -0
- package/dist/utils/zip.d.ts +7 -0
- package/dist/utils/zip.js +80 -0
- package/docs/BROWSER_USAGE.md +2 -10
- package/examples/README.md +3 -75
- package/examples/vitedemo/README.md +13 -7
- package/examples/vitedemo/index.html +2 -2
- package/examples/vitedemo/package-lock.json +9 -0
- package/examples/vitedemo/package.json +1 -0
- package/examples/vitedemo/src/main.ts +48 -2
- package/examples/vitedemo/src/vite-env.d.ts +1 -0
- package/package.json +3 -1
- package/examples/browser-test-server.js +0 -81
|
@@ -29,31 +29,7 @@ const treeStructure_1 = require("../core/treeStructure");
|
|
|
29
29
|
const idGenerator_1 = require("../utilities/analytics/utils/idGenerator");
|
|
30
30
|
const translationProcessor_1 = require("../utilities/translation/translationProcessor");
|
|
31
31
|
const io_1 = require("../utils/io");
|
|
32
|
-
|
|
33
|
-
async function getJSZipObf() {
|
|
34
|
-
if (!JSZipModuleObf) {
|
|
35
|
-
try {
|
|
36
|
-
// Try ES module import first (browser/Vite)
|
|
37
|
-
const module = await Promise.resolve().then(() => __importStar(require('jszip')));
|
|
38
|
-
JSZipModuleObf = module.default || module;
|
|
39
|
-
}
|
|
40
|
-
catch (error) {
|
|
41
|
-
// Fall back to CommonJS require (Node.js)
|
|
42
|
-
try {
|
|
43
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
44
|
-
const module = require('jszip');
|
|
45
|
-
JSZipModuleObf = module.default || module;
|
|
46
|
-
}
|
|
47
|
-
catch (err2) {
|
|
48
|
-
throw new Error('Zip handling requires JSZip in this environment.');
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
if (!JSZipModuleObf) {
|
|
53
|
-
throw new Error('Zip handling requires JSZip in this environment.');
|
|
54
|
-
}
|
|
55
|
-
return JSZipModuleObf;
|
|
56
|
-
}
|
|
32
|
+
const zip_1 = require("../utils/zip");
|
|
57
33
|
const OBF_FORMAT_VERSION = 'open-board-0.1';
|
|
58
34
|
/**
|
|
59
35
|
* Map OBF hidden value to AAC standard visibility
|
|
@@ -91,10 +67,12 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
91
67
|
].filter(Boolean);
|
|
92
68
|
for (const imagePath of possiblePaths) {
|
|
93
69
|
try {
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
70
|
+
const buffer = await this.zipFile.readFile(imagePath);
|
|
71
|
+
if (buffer) {
|
|
72
|
+
if (typeof Buffer !== 'undefined') {
|
|
73
|
+
return Buffer.from(buffer);
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
98
76
|
}
|
|
99
77
|
}
|
|
100
78
|
catch (err) {
|
|
@@ -128,9 +106,8 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
128
106
|
].filter(Boolean);
|
|
129
107
|
for (const imagePath of possiblePaths) {
|
|
130
108
|
try {
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
133
|
-
const buffer = await file.async('uint8array');
|
|
109
|
+
const buffer = await this.zipFile.readFile(imagePath);
|
|
110
|
+
if (buffer) {
|
|
134
111
|
const contentType = imageData.content_type ||
|
|
135
112
|
this.getMimeTypeFromFilename(imagePath);
|
|
136
113
|
const dataUrl = `data:${contentType};base64,${(0, io_1.encodeBase64)(buffer)}`;
|
|
@@ -433,36 +410,28 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
433
410
|
if (!isLikelyZip(filePathOrBuffer)) {
|
|
434
411
|
throw new Error('Invalid OBF content: not JSON and not ZIP');
|
|
435
412
|
}
|
|
436
|
-
const JSZip = await getJSZipObf();
|
|
437
|
-
let zip;
|
|
438
413
|
try {
|
|
439
|
-
const
|
|
440
|
-
|
|
414
|
+
const zipResult = await (0, zip_1.openZipFromInput)(filePathOrBuffer);
|
|
415
|
+
this.zipFile = zipResult.zip;
|
|
441
416
|
}
|
|
442
417
|
catch (err) {
|
|
443
|
-
console.error('[OBF] Error loading ZIP
|
|
418
|
+
console.error('[OBF] Error loading ZIP:', err);
|
|
444
419
|
throw err;
|
|
445
420
|
}
|
|
446
421
|
// Store the ZIP file reference for image extraction
|
|
447
|
-
this.zipFile = zip;
|
|
448
422
|
this.imageCache.clear(); // Clear cache for new file
|
|
449
423
|
console.log('[OBF] Detected zip archive, extracting .obf files');
|
|
450
424
|
// Collect all .obf entries
|
|
451
|
-
const obfEntries =
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
return;
|
|
455
|
-
if (relativePath.toLowerCase().endsWith('.obf')) {
|
|
456
|
-
obfEntries.push({ name: relativePath, file });
|
|
457
|
-
}
|
|
458
|
-
});
|
|
425
|
+
const obfEntries = this.zipFile
|
|
426
|
+
.listFiles()
|
|
427
|
+
.filter((name) => name.toLowerCase().endsWith('.obf'));
|
|
459
428
|
// Process each .obf entry
|
|
460
|
-
for (const
|
|
429
|
+
for (const entryName of obfEntries) {
|
|
461
430
|
try {
|
|
462
|
-
const content = await
|
|
463
|
-
const boardData = tryParseObfJson(content);
|
|
431
|
+
const content = await this.zipFile.readFile(entryName);
|
|
432
|
+
const boardData = tryParseObfJson((0, io_1.decodeText)(content));
|
|
464
433
|
if (boardData) {
|
|
465
|
-
const page = await this.processBoard(boardData,
|
|
434
|
+
const page = await this.processBoard(boardData, entryName);
|
|
466
435
|
tree.addPage(page);
|
|
467
436
|
// Set metadata if not already set (use first board as reference)
|
|
468
437
|
if (!tree.metadata.format) {
|
|
@@ -479,11 +448,11 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
479
448
|
}
|
|
480
449
|
}
|
|
481
450
|
else {
|
|
482
|
-
console.warn('[OBF] Skipped entry (not valid OBF JSON):',
|
|
451
|
+
console.warn('[OBF] Skipped entry (not valid OBF JSON):', entryName);
|
|
483
452
|
}
|
|
484
453
|
}
|
|
485
454
|
catch (err) {
|
|
486
|
-
console.warn('[OBF] Error processing entry:',
|
|
455
|
+
console.warn('[OBF] Error processing entry:', entryName, err);
|
|
487
456
|
}
|
|
488
457
|
}
|
|
489
458
|
return tree;
|
|
@@ -609,16 +578,31 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
609
578
|
}
|
|
610
579
|
else {
|
|
611
580
|
// Save as OBZ (zip with multiple OBF files)
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
581
|
+
if ((0, io_1.isNodeRuntime)()) {
|
|
582
|
+
const AdmZip = (0, io_1.getNodeRequire)()('adm-zip');
|
|
583
|
+
const zip = new AdmZip();
|
|
584
|
+
Object.values(tree.pages).forEach((page) => {
|
|
585
|
+
const obfBoard = this.createObfBoardFromPage(page, 'Board', tree.metadata);
|
|
586
|
+
const obfContent = JSON.stringify(obfBoard, null, 2);
|
|
587
|
+
zip.addFile(`${page.id}.obf`, Buffer.from(obfContent, 'utf8'));
|
|
588
|
+
});
|
|
589
|
+
const zipBuffer = zip.toBuffer();
|
|
590
|
+
const { writeBinaryToPath } = await Promise.resolve().then(() => __importStar(require('../utils/io')));
|
|
591
|
+
writeBinaryToPath(outputPath, zipBuffer);
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
const module = await Promise.resolve().then(() => __importStar(require('jszip')));
|
|
595
|
+
const JSZip = module.default || module;
|
|
596
|
+
const zip = new JSZip();
|
|
597
|
+
Object.values(tree.pages).forEach((page) => {
|
|
598
|
+
const obfBoard = this.createObfBoardFromPage(page, 'Board', tree.metadata);
|
|
599
|
+
const obfContent = JSON.stringify(obfBoard, null, 2);
|
|
600
|
+
zip.file(`${page.id}.obf`, obfContent);
|
|
601
|
+
});
|
|
602
|
+
const zipBuffer = await zip.generateAsync({ type: 'uint8array' });
|
|
603
|
+
const { writeBinaryToPath } = await Promise.resolve().then(() => __importStar(require('../utils/io')));
|
|
604
|
+
writeBinaryToPath(outputPath, zipBuffer);
|
|
605
|
+
}
|
|
622
606
|
}
|
|
623
607
|
}
|
|
624
608
|
/**
|
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.SnapProcessor = void 0;
|
|
7
4
|
const baseProcessor_1 = require("../core/baseProcessor");
|
|
8
5
|
const treeStructure_1 = require("../core/treeStructure");
|
|
9
6
|
const idGenerator_1 = require("../utilities/analytics/utils/idGenerator");
|
|
10
|
-
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
11
|
-
const path_1 = __importDefault(require("path"));
|
|
12
|
-
const fs_1 = __importDefault(require("fs"));
|
|
13
|
-
const crypto_1 = __importDefault(require("crypto"));
|
|
14
|
-
const os_1 = __importDefault(require("os"));
|
|
15
7
|
const snapValidator_1 = require("../validation/snapValidator");
|
|
8
|
+
const io_1 = require("../utils/io");
|
|
9
|
+
const sqlite_1 = require("../utils/sqlite");
|
|
16
10
|
/**
|
|
17
11
|
* Map Snap Visible value to AAC standard visibility
|
|
18
12
|
* Snap: 0 = hidden, 1 (or non-zero) = visible
|
|
@@ -56,22 +50,10 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
56
50
|
async loadIntoTree(filePathOrBuffer) {
|
|
57
51
|
await Promise.resolve();
|
|
58
52
|
const tree = new treeStructure_1.AACTree();
|
|
59
|
-
let
|
|
60
|
-
const filePath = typeof filePathOrBuffer !== 'string'
|
|
61
|
-
? (() => {
|
|
62
|
-
tempDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'snap-'));
|
|
63
|
-
return path_1.default.join(tempDir, 'input.spb');
|
|
64
|
-
})()
|
|
65
|
-
: filePathOrBuffer;
|
|
66
|
-
if (typeof filePathOrBuffer !== 'string') {
|
|
67
|
-
const buffer = Buffer.isBuffer(filePathOrBuffer)
|
|
68
|
-
? filePathOrBuffer
|
|
69
|
-
: Buffer.from(filePathOrBuffer);
|
|
70
|
-
fs_1.default.writeFileSync(filePath, buffer);
|
|
71
|
-
}
|
|
72
|
-
let db = null;
|
|
53
|
+
let dbResult = null;
|
|
73
54
|
try {
|
|
74
|
-
|
|
55
|
+
dbResult = await (0, sqlite_1.openSqliteDatabase)(filePathOrBuffer, { readonly: true });
|
|
56
|
+
const db = dbResult.db;
|
|
75
57
|
const getTableColumns = (tableName) => {
|
|
76
58
|
try {
|
|
77
59
|
const rows = db.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
@@ -338,10 +320,12 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
338
320
|
${hasCommandSequence ? 'LEFT JOIN CommandSequence cs ON b.Id = cs.ButtonId' : ''}
|
|
339
321
|
WHERE er.PageId = ? ${selectedPageLayoutId ? 'AND ep.PageLayoutId = ?' : ''}
|
|
340
322
|
`;
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
323
|
+
if (selectedPageLayoutId) {
|
|
324
|
+
buttons = db.prepare(buttonQuery).all(pageRow.Id, selectedPageLayoutId);
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
buttons = db.prepare(buttonQuery).all(pageRow.Id);
|
|
328
|
+
}
|
|
345
329
|
}
|
|
346
330
|
catch (err) {
|
|
347
331
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
@@ -606,22 +590,18 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
606
590
|
}
|
|
607
591
|
}
|
|
608
592
|
finally {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
db.close();
|
|
593
|
+
if (dbResult?.cleanup) {
|
|
594
|
+
dbResult.cleanup();
|
|
612
595
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
try {
|
|
616
|
-
fs_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
617
|
-
}
|
|
618
|
-
catch (e) {
|
|
619
|
-
console.warn('Failed to clean up temporary files:', e);
|
|
620
|
-
}
|
|
596
|
+
else if (dbResult?.db) {
|
|
597
|
+
dbResult.db.close();
|
|
621
598
|
}
|
|
622
599
|
}
|
|
623
600
|
}
|
|
624
601
|
async processTexts(filePathOrBuffer, translations, outputPath) {
|
|
602
|
+
if (!(0, io_1.isNodeRuntime)()) {
|
|
603
|
+
throw new Error('processTexts is only supported in Node.js environments for Snap files.');
|
|
604
|
+
}
|
|
625
605
|
// Load the tree, apply translations, and save to new file
|
|
626
606
|
const tree = await this.loadIntoTree(filePathOrBuffer);
|
|
627
607
|
// Apply translations to all text content
|
|
@@ -651,19 +631,26 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
651
631
|
});
|
|
652
632
|
// Save the translated tree and return its content
|
|
653
633
|
await this.saveFromTree(tree, outputPath);
|
|
654
|
-
|
|
634
|
+
const fs = (0, io_1.getFs)();
|
|
635
|
+
return fs.readFileSync(outputPath);
|
|
655
636
|
}
|
|
656
637
|
async saveFromTree(tree, outputPath) {
|
|
638
|
+
if (!(0, io_1.isNodeRuntime)()) {
|
|
639
|
+
throw new Error('saveFromTree is only supported in Node.js environments for Snap files.');
|
|
640
|
+
}
|
|
657
641
|
await Promise.resolve();
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
642
|
+
const fs = (0, io_1.getFs)();
|
|
643
|
+
const path = (0, io_1.getPath)();
|
|
644
|
+
const outputDir = path.dirname(outputPath);
|
|
645
|
+
if (!fs.existsSync(outputDir)) {
|
|
646
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
661
647
|
}
|
|
662
|
-
if (
|
|
663
|
-
|
|
648
|
+
if (fs.existsSync(outputPath)) {
|
|
649
|
+
fs.unlinkSync(outputPath);
|
|
664
650
|
}
|
|
665
651
|
// Create a new SQLite database for Snap format
|
|
666
|
-
const
|
|
652
|
+
const Database = (0, sqlite_1.requireBetterSqlite3)();
|
|
653
|
+
const db = new Database(outputPath, { readonly: false });
|
|
667
654
|
try {
|
|
668
655
|
// Create basic Snap database schema (simplified)
|
|
669
656
|
db.exec(`
|
|
@@ -852,7 +839,12 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
852
839
|
*/
|
|
853
840
|
async addAudioToButton(dbPath, buttonId, audioData, metadata) {
|
|
854
841
|
await Promise.resolve();
|
|
855
|
-
|
|
842
|
+
if (!(0, io_1.isNodeRuntime)()) {
|
|
843
|
+
throw new Error('addAudioToButton is only supported in Node.js environments.');
|
|
844
|
+
}
|
|
845
|
+
const Database = (0, sqlite_1.requireBetterSqlite3)();
|
|
846
|
+
const crypto = (0, io_1.getNodeRequire)()('crypto');
|
|
847
|
+
const db = new Database(dbPath, { fileMustExist: true });
|
|
856
848
|
try {
|
|
857
849
|
// Ensure PageSetData table exists
|
|
858
850
|
db.exec(`
|
|
@@ -863,7 +855,7 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
863
855
|
);
|
|
864
856
|
`);
|
|
865
857
|
// Generate SHA1 hash for the identifier
|
|
866
|
-
const sha1Hash =
|
|
858
|
+
const sha1Hash = crypto.createHash('sha1').update(audioData).digest('hex');
|
|
867
859
|
const identifier = `SND:${sha1Hash}`;
|
|
868
860
|
// Check if audio with this identifier already exists
|
|
869
861
|
let audioId;
|
|
@@ -894,8 +886,12 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
894
886
|
* Create a copy of the pageset with audio recordings added
|
|
895
887
|
*/
|
|
896
888
|
async createAudioEnhancedPageset(sourceDbPath, targetDbPath, audioMappings) {
|
|
889
|
+
if (!(0, io_1.isNodeRuntime)()) {
|
|
890
|
+
throw new Error('createAudioEnhancedPageset is only supported in Node.js environments.');
|
|
891
|
+
}
|
|
892
|
+
const fs = (0, io_1.getFs)();
|
|
897
893
|
// Copy the source database to target
|
|
898
|
-
|
|
894
|
+
fs.copyFileSync(sourceDbPath, targetDbPath);
|
|
899
895
|
// Add audio recordings to the copy
|
|
900
896
|
for (const [buttonId, audioInfo] of audioMappings.entries()) {
|
|
901
897
|
await this.addAudioToButton(targetDbPath, buttonId, audioInfo.audioData, audioInfo.metadata);
|
|
@@ -905,7 +901,11 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
905
901
|
* Extract buttons from a specific page that need audio recordings
|
|
906
902
|
*/
|
|
907
903
|
extractButtonsForAudio(dbPath, pageUniqueId) {
|
|
908
|
-
|
|
904
|
+
if (!(0, io_1.isNodeRuntime)()) {
|
|
905
|
+
throw new Error('extractButtonsForAudio is only supported in Node.js environments.');
|
|
906
|
+
}
|
|
907
|
+
const Database = (0, sqlite_1.requireBetterSqlite3)();
|
|
908
|
+
const db = new Database(dbPath, { readonly: true });
|
|
909
909
|
try {
|
|
910
910
|
// Find the page by UniqueId
|
|
911
911
|
const page = db.prepare('SELECT * FROM Page WHERE UniqueId = ?').get(pageUniqueId);
|
|
@@ -962,13 +962,19 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
962
962
|
* @returns Array of available PageLayouts with their dimensions
|
|
963
963
|
*/
|
|
964
964
|
getAvailablePageLayouts(filePath) {
|
|
965
|
-
|
|
965
|
+
if (!(0, io_1.isNodeRuntime)()) {
|
|
966
|
+
throw new Error('getAvailablePageLayouts is only supported in Node.js environments.');
|
|
967
|
+
}
|
|
968
|
+
const fs = (0, io_1.getFs)();
|
|
969
|
+
const path = (0, io_1.getPath)();
|
|
970
|
+
const dbPath = typeof filePath === 'string' ? filePath : path.join(process.cwd(), 'temp.spb');
|
|
966
971
|
if (Buffer.isBuffer(filePath)) {
|
|
967
|
-
|
|
972
|
+
fs.writeFileSync(dbPath, filePath);
|
|
968
973
|
}
|
|
969
974
|
let db = null;
|
|
970
975
|
try {
|
|
971
|
-
|
|
976
|
+
const Database = (0, sqlite_1.requireBetterSqlite3)();
|
|
977
|
+
db = new Database(dbPath, { readonly: true });
|
|
972
978
|
// Get unique PageLayouts based on PageLayoutSetting (dimensions)
|
|
973
979
|
const pageLayouts = db
|
|
974
980
|
.prepare(`
|
|
@@ -1014,9 +1020,9 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
1014
1020
|
db.close();
|
|
1015
1021
|
}
|
|
1016
1022
|
// Clean up temporary file if created from buffer
|
|
1017
|
-
if (Buffer.isBuffer(filePath) &&
|
|
1023
|
+
if (Buffer.isBuffer(filePath) && fs.existsSync(dbPath)) {
|
|
1018
1024
|
try {
|
|
1019
|
-
|
|
1025
|
+
fs.unlinkSync(dbPath);
|
|
1020
1026
|
}
|
|
1021
1027
|
catch (e) {
|
|
1022
1028
|
console.warn('Failed to clean up temporary file:', e);
|
|
@@ -1,21 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.TouchChatProcessor = void 0;
|
|
7
4
|
const baseProcessor_1 = require("../core/baseProcessor");
|
|
8
5
|
const treeStructure_1 = require("../core/treeStructure");
|
|
9
6
|
const idGenerator_1 = require("../utilities/analytics/utils/idGenerator");
|
|
10
7
|
const stringCasing_1 = require("../core/stringCasing");
|
|
11
|
-
const adm_zip_1 = __importDefault(require("adm-zip"));
|
|
12
|
-
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
13
|
-
const path_1 = __importDefault(require("path"));
|
|
14
|
-
const fs_1 = __importDefault(require("fs"));
|
|
15
|
-
const os_1 = __importDefault(require("os"));
|
|
16
8
|
const touchChatValidator_1 = require("../validation/touchChatValidator");
|
|
17
9
|
const io_1 = require("../utils/io");
|
|
18
10
|
const translationProcessor_1 = require("../utilities/translation/translationProcessor");
|
|
11
|
+
const sqlite_1 = require("../utils/sqlite");
|
|
12
|
+
const zip_1 = require("../utils/zip");
|
|
19
13
|
const toNumberOrUndefined = (value) => typeof value === 'number' ? value : undefined;
|
|
20
14
|
const toStringOrUndefined = (value) => typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
21
15
|
const toBooleanOrUndefined = (value) => typeof value === 'number' ? value !== 0 : undefined;
|
|
@@ -66,25 +60,22 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
66
60
|
async loadIntoTree(filePathOrBuffer) {
|
|
67
61
|
await Promise.resolve();
|
|
68
62
|
// Unzip .ce file, extract the .c4v SQLite DB, and parse pages/buttons
|
|
69
|
-
let tmpDir = null;
|
|
70
63
|
let db = null;
|
|
64
|
+
let cleanup;
|
|
71
65
|
try {
|
|
72
66
|
// Store source file path or buffer
|
|
73
67
|
this.sourceFile = filePathOrBuffer;
|
|
74
68
|
// Step 1: Unzip
|
|
75
|
-
tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'touchchat-'));
|
|
76
69
|
const zipInput = (0, io_1.readBinaryFromInput)(filePathOrBuffer);
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
// Step 2: Find and open SQLite DB
|
|
81
|
-
const files = fs_1.default.readdirSync(tmpDir);
|
|
82
|
-
const vocabFile = files.find((f) => f.endsWith('.c4v'));
|
|
83
|
-
if (!vocabFile) {
|
|
70
|
+
const { zip } = await (0, zip_1.openZipFromInput)(zipInput);
|
|
71
|
+
const vocabEntry = zip.listFiles().find((name) => name.endsWith('.c4v'));
|
|
72
|
+
if (!vocabEntry) {
|
|
84
73
|
throw new Error('No .c4v vocab DB found in TouchChat export');
|
|
85
74
|
}
|
|
86
|
-
const
|
|
87
|
-
|
|
75
|
+
const dbBuffer = await zip.readFile(vocabEntry);
|
|
76
|
+
const dbResult = await (0, sqlite_1.openSqliteDatabase)(dbBuffer, { readonly: true });
|
|
77
|
+
db = dbResult.db;
|
|
78
|
+
cleanup = dbResult.cleanup;
|
|
88
79
|
// Step 3: Create tree and load pages
|
|
89
80
|
const tree = new treeStructure_1.AACTree();
|
|
90
81
|
// Set root ID to the first page ID (will be updated if we find a better root)
|
|
@@ -476,20 +467,18 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
476
467
|
}
|
|
477
468
|
finally {
|
|
478
469
|
// Clean up
|
|
479
|
-
if (
|
|
480
|
-
|
|
470
|
+
if (cleanup) {
|
|
471
|
+
cleanup();
|
|
481
472
|
}
|
|
482
|
-
if (
|
|
483
|
-
|
|
484
|
-
fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
|
|
485
|
-
}
|
|
486
|
-
catch (e) {
|
|
487
|
-
console.warn('Failed to clean up temp directory:', e);
|
|
488
|
-
}
|
|
473
|
+
else if (db) {
|
|
474
|
+
db.close();
|
|
489
475
|
}
|
|
490
476
|
}
|
|
491
477
|
}
|
|
492
478
|
async processTexts(filePathOrBuffer, translations, outputPath) {
|
|
479
|
+
if (!(0, io_1.isNodeRuntime)()) {
|
|
480
|
+
throw new Error('processTexts is only supported in Node.js environments for TouchChat files.');
|
|
481
|
+
}
|
|
493
482
|
// Load the tree, apply translations, and save to new file
|
|
494
483
|
const tree = await this.loadIntoTree(filePathOrBuffer);
|
|
495
484
|
// Apply translations to all text content
|
|
@@ -519,15 +508,23 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
519
508
|
});
|
|
520
509
|
// Save the translated tree and return its content
|
|
521
510
|
await this.saveFromTree(tree, outputPath);
|
|
522
|
-
|
|
511
|
+
const fs = (0, io_1.getFs)();
|
|
512
|
+
return fs.readFileSync(outputPath);
|
|
523
513
|
}
|
|
524
514
|
async saveFromTree(tree, outputPath) {
|
|
525
515
|
await Promise.resolve();
|
|
516
|
+
if (!(0, io_1.isNodeRuntime)()) {
|
|
517
|
+
throw new Error('saveFromTree is only supported in Node.js environments for TouchChat files.');
|
|
518
|
+
}
|
|
519
|
+
const fs = (0, io_1.getFs)();
|
|
520
|
+
const path = (0, io_1.getPath)();
|
|
521
|
+
const os = (0, io_1.getOs)();
|
|
526
522
|
// Create a TouchChat database that matches the expected schema for loading
|
|
527
|
-
const tmpDir =
|
|
528
|
-
const dbPath =
|
|
523
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'touchchat-export-'));
|
|
524
|
+
const dbPath = path.join(tmpDir, 'vocab.c4v');
|
|
529
525
|
try {
|
|
530
|
-
const
|
|
526
|
+
const Database = (0, sqlite_1.requireBetterSqlite3)();
|
|
527
|
+
const db = new Database(dbPath);
|
|
531
528
|
// Create schema that matches what loadIntoTree expects
|
|
532
529
|
db.exec(`
|
|
533
530
|
CREATE TABLE IF NOT EXISTS resources (
|
|
@@ -817,14 +814,15 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
817
814
|
}
|
|
818
815
|
db.close();
|
|
819
816
|
// Create zip file with the database
|
|
820
|
-
const
|
|
817
|
+
const AdmZip = (0, io_1.getNodeRequire)()('adm-zip');
|
|
818
|
+
const zip = new AdmZip();
|
|
821
819
|
zip.addLocalFile(dbPath, '', 'vocab.c4v');
|
|
822
820
|
zip.writeZip(outputPath);
|
|
823
821
|
}
|
|
824
822
|
finally {
|
|
825
823
|
// Clean up
|
|
826
|
-
if (
|
|
827
|
-
|
|
824
|
+
if (fs.existsSync(tmpDir)) {
|
|
825
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
828
826
|
}
|
|
829
827
|
}
|
|
830
828
|
}
|
|
@@ -961,6 +959,9 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
961
959
|
* @returns Buffer of the translated TouchChat file
|
|
962
960
|
*/
|
|
963
961
|
async processLLMTranslations(filePathOrBuffer, llmTranslations, outputPath, options) {
|
|
962
|
+
if (!(0, io_1.isNodeRuntime)()) {
|
|
963
|
+
throw new Error('processLLMTranslations is only supported in Node.js environments for TouchChat files.');
|
|
964
|
+
}
|
|
964
965
|
const tree = await this.loadIntoTree(filePathOrBuffer);
|
|
965
966
|
// Validate translations using shared utility
|
|
966
967
|
const buttonIds = Object.values(tree.pages).flatMap((page) => page.buttons.map((b) => b.id));
|
|
@@ -999,7 +1000,8 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
999
1000
|
});
|
|
1000
1001
|
// Save and return
|
|
1001
1002
|
await this.saveFromTree(tree, outputPath);
|
|
1002
|
-
|
|
1003
|
+
const fs = (0, io_1.getFs)();
|
|
1004
|
+
return fs.readFileSync(outputPath);
|
|
1003
1005
|
}
|
|
1004
1006
|
}
|
|
1005
1007
|
exports.TouchChatProcessor = TouchChatProcessor;
|
package/dist/utils/io.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ type NodeRequire = (id: string) => any;
|
|
|
4
4
|
export declare function getNodeRequire(): NodeRequire;
|
|
5
5
|
export declare function getFs(): typeof import('fs');
|
|
6
6
|
export declare function getPath(): typeof import('path');
|
|
7
|
+
export declare function getOs(): typeof import('os');
|
|
8
|
+
export declare function isNodeRuntime(): boolean;
|
|
7
9
|
export declare function getBasename(filePath: string): string;
|
|
8
10
|
export declare function decodeText(input: Uint8Array): string;
|
|
9
11
|
export declare function encodeBase64(input: Uint8Array): string;
|
package/dist/utils/io.js
CHANGED
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.getNodeRequire = getNodeRequire;
|
|
4
4
|
exports.getFs = getFs;
|
|
5
5
|
exports.getPath = getPath;
|
|
6
|
+
exports.getOs = getOs;
|
|
7
|
+
exports.isNodeRuntime = isNodeRuntime;
|
|
6
8
|
exports.getBasename = getBasename;
|
|
7
9
|
exports.decodeText = decodeText;
|
|
8
10
|
exports.encodeBase64 = encodeBase64;
|
|
@@ -13,6 +15,7 @@ exports.writeBinaryToPath = writeBinaryToPath;
|
|
|
13
15
|
exports.writeTextToPath = writeTextToPath;
|
|
14
16
|
let cachedFs = null;
|
|
15
17
|
let cachedPath = null;
|
|
18
|
+
let cachedOs = null;
|
|
16
19
|
let cachedRequire = undefined;
|
|
17
20
|
function getNodeRequire() {
|
|
18
21
|
if (cachedRequire === undefined) {
|
|
@@ -64,6 +67,25 @@ function getPath() {
|
|
|
64
67
|
}
|
|
65
68
|
return cachedPath;
|
|
66
69
|
}
|
|
70
|
+
function getOs() {
|
|
71
|
+
if (!cachedOs) {
|
|
72
|
+
try {
|
|
73
|
+
const nodeRequire = getNodeRequire();
|
|
74
|
+
const osModule = 'os';
|
|
75
|
+
cachedOs = nodeRequire(osModule);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
throw new Error('OS utilities are not available in this environment.');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!cachedOs) {
|
|
82
|
+
throw new Error('OS utilities are not available in this environment.');
|
|
83
|
+
}
|
|
84
|
+
return cachedOs;
|
|
85
|
+
}
|
|
86
|
+
function isNodeRuntime() {
|
|
87
|
+
return typeof process !== 'undefined' && !!process.versions?.node;
|
|
88
|
+
}
|
|
67
89
|
function getBasename(filePath) {
|
|
68
90
|
const parts = filePath.split(/[/\\]/);
|
|
69
91
|
return parts[parts.length - 1] || filePath;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SqlJsConfig } from 'sql.js';
|
|
2
|
+
export interface SqliteStatementAdapter {
|
|
3
|
+
all(...params: unknown[]): any[];
|
|
4
|
+
get(...params: unknown[]): any;
|
|
5
|
+
run(...params: unknown[]): any;
|
|
6
|
+
}
|
|
7
|
+
export interface SqliteDatabaseAdapter {
|
|
8
|
+
prepare(sql: string): SqliteStatementAdapter;
|
|
9
|
+
exec(sql: string): void;
|
|
10
|
+
close(): void;
|
|
11
|
+
}
|
|
12
|
+
export interface SqliteOpenOptions {
|
|
13
|
+
readonly?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface SqliteOpenResult {
|
|
16
|
+
db: SqliteDatabaseAdapter;
|
|
17
|
+
cleanup?: () => void;
|
|
18
|
+
}
|
|
19
|
+
export declare function configureSqlJs(config: SqlJsConfig): void;
|
|
20
|
+
export declare function requireBetterSqlite3(): typeof import('better-sqlite3');
|
|
21
|
+
export declare function openSqliteDatabase(input: string | Uint8Array | ArrayBuffer | Buffer, options?: SqliteOpenOptions): Promise<SqliteOpenResult>;
|