@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
- // Collect all .obf entries
400
- const obfEntries = this.zipFile
401
- .listFiles()
402
- .filter((name) => name.toLowerCase().endsWith('.obf'));
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
- dbResult = await openSqliteDatabase(filePathOrBuffer, { readonly: true });
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
- // Collect all .obf entries
426
- const obfEntries = this.zipFile
427
- .listFiles()
428
- .filter((name) => name.toLowerCase().endsWith('.obf'));
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
- dbResult = await (0, sqlite_1.openSqliteDatabase)(filePathOrBuffer, { readonly: true });
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.19",
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",