@willwade/aac-processors 0.1.19 → 0.1.20
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.
|
@@ -120,13 +120,9 @@ class ObfProcessor extends BaseProcessor {
|
|
|
120
120
|
return 'image/png';
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
-
async processBoard(boardData, _boardPath) {
|
|
123
|
+
async processBoard(boardData, _boardPath, isZipEntry) {
|
|
124
124
|
const sourceButtons = boardData.buttons || [];
|
|
125
125
|
// Calculate page ID first (used to make button IDs unique)
|
|
126
|
-
const isZipEntry = _boardPath &&
|
|
127
|
-
_boardPath.endsWith('.obf') &&
|
|
128
|
-
!_boardPath.includes('/') &&
|
|
129
|
-
!_boardPath.includes('\\');
|
|
130
126
|
const pageId = isZipEntry
|
|
131
127
|
? _boardPath // Zip entry - use filename to match navigation paths
|
|
132
128
|
: boardData?.id
|
|
@@ -328,7 +324,7 @@ class ObfProcessor extends BaseProcessor {
|
|
|
328
324
|
const boardData = tryParseObfJson(content);
|
|
329
325
|
if (boardData) {
|
|
330
326
|
console.log('[OBF] Detected .obf file, parsed as JSON');
|
|
331
|
-
const page = await this.processBoard(boardData, filePathOrBuffer);
|
|
327
|
+
const page = await this.processBoard(boardData, filePathOrBuffer, false);
|
|
332
328
|
tree.addPage(page);
|
|
333
329
|
// Set metadata from root board
|
|
334
330
|
tree.metadata.format = 'obf';
|
|
@@ -367,7 +363,7 @@ class ObfProcessor extends BaseProcessor {
|
|
|
367
363
|
if (!asJson)
|
|
368
364
|
throw new Error('Invalid OBF content: not JSON and not ZIP');
|
|
369
365
|
console.log('[OBF] Detected buffer/string as OBF JSON');
|
|
370
|
-
const page = await this.processBoard(asJson, '[bufferOrString]');
|
|
366
|
+
const page = await this.processBoard(asJson, '[bufferOrString]', false);
|
|
371
367
|
tree.addPage(page);
|
|
372
368
|
// Set metadata from root board
|
|
373
369
|
tree.metadata.format = 'obf';
|
|
@@ -396,17 +392,42 @@ class ObfProcessor extends BaseProcessor {
|
|
|
396
392
|
// Store the ZIP file reference for image extraction
|
|
397
393
|
this.imageCache.clear(); // Clear cache for new file
|
|
398
394
|
console.log('[OBF] Detected zip archive, extracting .obf files');
|
|
399
|
-
//
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
395
|
+
// List manifest and OBF files
|
|
396
|
+
const filesInZip = this.zipFile.listFiles();
|
|
397
|
+
const manifestFile = filesInZip.filter((name) => name.toLowerCase() === 'manifest.json');
|
|
398
|
+
let obfEntries = filesInZip.filter((name) => name.toLowerCase().endsWith('.obf'));
|
|
399
|
+
// Attempt to read manifest
|
|
400
|
+
if (manifestFile && manifestFile.length === 1) {
|
|
401
|
+
try {
|
|
402
|
+
const content = await this.zipFile.readFile(manifestFile[0]);
|
|
403
|
+
const data = decodeText(content);
|
|
404
|
+
const str = typeof data === 'string' ? data : readTextFromInput(data);
|
|
405
|
+
if (!str.trim())
|
|
406
|
+
throw new Error('Manifest object missing');
|
|
407
|
+
const manifestObject = JSON.parse(str);
|
|
408
|
+
if (!manifestObject)
|
|
409
|
+
throw new Error('Manifest object is empty');
|
|
410
|
+
// Replace OBF file list
|
|
411
|
+
if (manifestObject.paths && manifestObject.paths.boards) {
|
|
412
|
+
obfEntries = Object.values(manifestObject.paths.boards);
|
|
413
|
+
}
|
|
414
|
+
// Move root board to top of list
|
|
415
|
+
if (manifestObject.root) {
|
|
416
|
+
obfEntries = obfEntries.filter((item) => item !== manifestObject.root);
|
|
417
|
+
obfEntries.unshift(manifestObject.root);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch (err) {
|
|
421
|
+
console.warn('[OBF] Error processing mainfest', err);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
403
424
|
// Process each .obf entry
|
|
404
425
|
for (const entryName of obfEntries) {
|
|
405
426
|
try {
|
|
406
427
|
const content = await this.zipFile.readFile(entryName);
|
|
407
428
|
const boardData = tryParseObfJson(decodeText(content));
|
|
408
429
|
if (boardData) {
|
|
409
|
-
const page = await this.processBoard(boardData, entryName);
|
|
430
|
+
const page = await this.processBoard(boardData, entryName, true);
|
|
410
431
|
tree.addPage(page);
|
|
411
432
|
// Set metadata if not already set (use first board as reference)
|
|
412
433
|
if (!tree.metadata.format) {
|
|
@@ -2,8 +2,9 @@ import { BaseProcessor, } from '../core/baseProcessor';
|
|
|
2
2
|
import { AACTree, AACPage, AACButton, AACSemanticCategory, AACSemanticIntent, } from '../core/treeStructure';
|
|
3
3
|
import { generateCloneId } from '../utilities/analytics/utils/idGenerator';
|
|
4
4
|
import { SnapValidator } from '../validation/snapValidator';
|
|
5
|
-
import { getFs, getNodeRequire, getPath, isNodeRuntime } from '../utils/io';
|
|
5
|
+
import { getFs, getNodeRequire, getPath, isNodeRuntime, getOs } from '../utils/io';
|
|
6
6
|
import { openSqliteDatabase, requireBetterSqlite3 } from '../utils/sqlite';
|
|
7
|
+
import { openZipFromInput } from '../utils/zip';
|
|
7
8
|
/**
|
|
8
9
|
* Convert a Buffer or Uint8Array to base64 string (browser and Node compatible)
|
|
9
10
|
* Node.js Buffers support toString('base64'), but Uint8Arrays in browser do not.
|
|
@@ -67,8 +68,37 @@ class SnapProcessor extends BaseProcessor {
|
|
|
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 = getPath().basename(filePathOrBuffer).toLowerCase();
|
|
77
|
+
if (fileName.endsWith('.sub.zip') || filePathOrBuffer.endsWith('.sub')) {
|
|
78
|
+
const fs = getFs();
|
|
79
|
+
const path = getPath();
|
|
80
|
+
const os = getOs();
|
|
81
|
+
// Extract .sub.zip to find the embedded .sps file
|
|
82
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'snap-sub-'));
|
|
83
|
+
const { zip } = await openZipFromInput(filePathOrBuffer);
|
|
84
|
+
// Find the .sps file in the archive
|
|
85
|
+
const files = zip.listFiles();
|
|
86
|
+
const spsFile = files.find((f) => f.endsWith('.sps'));
|
|
87
|
+
if (!spsFile) {
|
|
88
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
89
|
+
throw new Error('No .sps file found in .sub.zip archive');
|
|
90
|
+
}
|
|
91
|
+
// Extract the .sps file
|
|
92
|
+
const spsData = await zip.readFile(spsFile);
|
|
93
|
+
const extractedSpsPath = path.join(tempDir, path.basename(spsFile));
|
|
94
|
+
fs.writeFileSync(extractedSpsPath, Buffer.from(spsData));
|
|
95
|
+
inputFile = extractedSpsPath;
|
|
96
|
+
cleanupTempZip = () => {
|
|
97
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
dbResult = await openSqliteDatabase(inputFile, { readonly: true });
|
|
72
102
|
const db = dbResult.db;
|
|
73
103
|
const getTableColumns = (tableName) => {
|
|
74
104
|
try {
|
|
@@ -658,6 +688,15 @@ 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) {
|
|
@@ -18,6 +18,11 @@ class FileProcessor {
|
|
|
18
18
|
static detectFormat(filePathOrBuffer) {
|
|
19
19
|
if (typeof filePathOrBuffer === 'string') {
|
|
20
20
|
const ext = path_1.default.extname(filePathOrBuffer).toLowerCase();
|
|
21
|
+
const fileName = path_1.default.basename(filePathOrBuffer).toLowerCase();
|
|
22
|
+
// Handle double extensions like .sub.zip
|
|
23
|
+
if (fileName.endsWith('.sub.zip') || ext === '.sub') {
|
|
24
|
+
return 'snap';
|
|
25
|
+
}
|
|
21
26
|
switch (ext) {
|
|
22
27
|
case '.gridset':
|
|
23
28
|
case '.gridsetx':
|
|
@@ -146,13 +146,9 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
146
146
|
return 'image/png';
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
|
-
async processBoard(boardData, _boardPath) {
|
|
149
|
+
async processBoard(boardData, _boardPath, isZipEntry) {
|
|
150
150
|
const sourceButtons = boardData.buttons || [];
|
|
151
151
|
// Calculate page ID first (used to make button IDs unique)
|
|
152
|
-
const isZipEntry = _boardPath &&
|
|
153
|
-
_boardPath.endsWith('.obf') &&
|
|
154
|
-
!_boardPath.includes('/') &&
|
|
155
|
-
!_boardPath.includes('\\');
|
|
156
152
|
const pageId = isZipEntry
|
|
157
153
|
? _boardPath // Zip entry - use filename to match navigation paths
|
|
158
154
|
: boardData?.id
|
|
@@ -354,7 +350,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
354
350
|
const boardData = tryParseObfJson(content);
|
|
355
351
|
if (boardData) {
|
|
356
352
|
console.log('[OBF] Detected .obf file, parsed as JSON');
|
|
357
|
-
const page = await this.processBoard(boardData, filePathOrBuffer);
|
|
353
|
+
const page = await this.processBoard(boardData, filePathOrBuffer, false);
|
|
358
354
|
tree.addPage(page);
|
|
359
355
|
// Set metadata from root board
|
|
360
356
|
tree.metadata.format = 'obf';
|
|
@@ -393,7 +389,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
393
389
|
if (!asJson)
|
|
394
390
|
throw new Error('Invalid OBF content: not JSON and not ZIP');
|
|
395
391
|
console.log('[OBF] Detected buffer/string as OBF JSON');
|
|
396
|
-
const page = await this.processBoard(asJson, '[bufferOrString]');
|
|
392
|
+
const page = await this.processBoard(asJson, '[bufferOrString]', false);
|
|
397
393
|
tree.addPage(page);
|
|
398
394
|
// Set metadata from root board
|
|
399
395
|
tree.metadata.format = 'obf';
|
|
@@ -422,17 +418,42 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
422
418
|
// Store the ZIP file reference for image extraction
|
|
423
419
|
this.imageCache.clear(); // Clear cache for new file
|
|
424
420
|
console.log('[OBF] Detected zip archive, extracting .obf files');
|
|
425
|
-
//
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
421
|
+
// List manifest and OBF files
|
|
422
|
+
const filesInZip = this.zipFile.listFiles();
|
|
423
|
+
const manifestFile = filesInZip.filter((name) => name.toLowerCase() === 'manifest.json');
|
|
424
|
+
let obfEntries = filesInZip.filter((name) => name.toLowerCase().endsWith('.obf'));
|
|
425
|
+
// Attempt to read manifest
|
|
426
|
+
if (manifestFile && manifestFile.length === 1) {
|
|
427
|
+
try {
|
|
428
|
+
const content = await this.zipFile.readFile(manifestFile[0]);
|
|
429
|
+
const data = (0, io_1.decodeText)(content);
|
|
430
|
+
const str = typeof data === 'string' ? data : (0, io_1.readTextFromInput)(data);
|
|
431
|
+
if (!str.trim())
|
|
432
|
+
throw new Error('Manifest object missing');
|
|
433
|
+
const manifestObject = JSON.parse(str);
|
|
434
|
+
if (!manifestObject)
|
|
435
|
+
throw new Error('Manifest object is empty');
|
|
436
|
+
// Replace OBF file list
|
|
437
|
+
if (manifestObject.paths && manifestObject.paths.boards) {
|
|
438
|
+
obfEntries = Object.values(manifestObject.paths.boards);
|
|
439
|
+
}
|
|
440
|
+
// Move root board to top of list
|
|
441
|
+
if (manifestObject.root) {
|
|
442
|
+
obfEntries = obfEntries.filter((item) => item !== manifestObject.root);
|
|
443
|
+
obfEntries.unshift(manifestObject.root);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
catch (err) {
|
|
447
|
+
console.warn('[OBF] Error processing mainfest', err);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
429
450
|
// Process each .obf entry
|
|
430
451
|
for (const entryName of obfEntries) {
|
|
431
452
|
try {
|
|
432
453
|
const content = await this.zipFile.readFile(entryName);
|
|
433
454
|
const boardData = tryParseObfJson((0, io_1.decodeText)(content));
|
|
434
455
|
if (boardData) {
|
|
435
|
-
const page = await this.processBoard(boardData, entryName);
|
|
456
|
+
const page = await this.processBoard(boardData, entryName, true);
|
|
436
457
|
tree.addPage(page);
|
|
437
458
|
// Set metadata if not already set (use first board as reference)
|
|
438
459
|
if (!tree.metadata.format) {
|
|
@@ -7,6 +7,7 @@ const idGenerator_1 = require("../utilities/analytics/utils/idGenerator");
|
|
|
7
7
|
const snapValidator_1 = require("../validation/snapValidator");
|
|
8
8
|
const io_1 = require("../utils/io");
|
|
9
9
|
const sqlite_1 = require("../utils/sqlite");
|
|
10
|
+
const zip_1 = require("../utils/zip");
|
|
10
11
|
/**
|
|
11
12
|
* Convert a Buffer or Uint8Array to base64 string (browser and Node compatible)
|
|
12
13
|
* Node.js Buffers support toString('base64'), but Uint8Arrays in browser do not.
|
|
@@ -70,8 +71,37 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
70
71
|
await Promise.resolve();
|
|
71
72
|
const tree = new treeStructure_1.AACTree();
|
|
72
73
|
let dbResult = null;
|
|
74
|
+
let cleanupTempZip = null;
|
|
73
75
|
try {
|
|
74
|
-
|
|
76
|
+
// Handle .sub.zip files (Snap pageset backups containing .sps files)
|
|
77
|
+
let inputFile = filePathOrBuffer;
|
|
78
|
+
if (typeof filePathOrBuffer === 'string') {
|
|
79
|
+
const fileName = (0, io_1.getPath)().basename(filePathOrBuffer).toLowerCase();
|
|
80
|
+
if (fileName.endsWith('.sub.zip') || filePathOrBuffer.endsWith('.sub')) {
|
|
81
|
+
const fs = (0, io_1.getFs)();
|
|
82
|
+
const path = (0, io_1.getPath)();
|
|
83
|
+
const os = (0, io_1.getOs)();
|
|
84
|
+
// Extract .sub.zip to find the embedded .sps file
|
|
85
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'snap-sub-'));
|
|
86
|
+
const { zip } = await (0, zip_1.openZipFromInput)(filePathOrBuffer);
|
|
87
|
+
// Find the .sps file in the archive
|
|
88
|
+
const files = zip.listFiles();
|
|
89
|
+
const spsFile = files.find((f) => f.endsWith('.sps'));
|
|
90
|
+
if (!spsFile) {
|
|
91
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
92
|
+
throw new Error('No .sps file found in .sub.zip archive');
|
|
93
|
+
}
|
|
94
|
+
// Extract the .sps file
|
|
95
|
+
const spsData = await zip.readFile(spsFile);
|
|
96
|
+
const extractedSpsPath = path.join(tempDir, path.basename(spsFile));
|
|
97
|
+
fs.writeFileSync(extractedSpsPath, Buffer.from(spsData));
|
|
98
|
+
inputFile = extractedSpsPath;
|
|
99
|
+
cleanupTempZip = () => {
|
|
100
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
dbResult = await (0, sqlite_1.openSqliteDatabase)(inputFile, { readonly: true });
|
|
75
105
|
const db = dbResult.db;
|
|
76
106
|
const getTableColumns = (tableName) => {
|
|
77
107
|
try {
|
|
@@ -661,6 +691,15 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
661
691
|
else if (dbResult?.db) {
|
|
662
692
|
dbResult.db.close();
|
|
663
693
|
}
|
|
694
|
+
// Clean up temporary extracted .sps file from .sub.zip
|
|
695
|
+
if (cleanupTempZip) {
|
|
696
|
+
try {
|
|
697
|
+
cleanupTempZip();
|
|
698
|
+
}
|
|
699
|
+
catch (e) {
|
|
700
|
+
console.warn('[SnapProcessor] Failed to clean up temporary .sps file:', e);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
664
703
|
}
|
|
665
704
|
}
|
|
666
705
|
async processTexts(filePathOrBuffer, translations, outputPath) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@willwade/aac-processors",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"description": "A comprehensive TypeScript library for processing AAC (Augmentative and Alternative Communication) file formats with translation support",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"browser": "dist/browser/index.browser.js",
|