@willwade/aac-processors 0.2.8 → 0.2.10

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.
@@ -106,7 +106,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
106
106
  // Images are typically stored in an 'images' folder or root
107
107
  const possiblePaths = [
108
108
  imageData.path, // Explicit path if provided
109
- `images/${imageData.filename || imageId}`, // Standard images folder
109
+ `images/${imageData.path || imageId}`, // Standard images folder
110
110
  imageData.id, // Just the ID
111
111
  ].filter(Boolean);
112
112
  for (const imagePath of possiblePaths) {
@@ -152,14 +152,18 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
152
152
  return 'image/png';
153
153
  }
154
154
  }
155
- async processBoard(boardData, _boardPath, isZipEntry) {
155
+ getPageFilename(id, metadata) {
156
+ if (metadata._obfPagePaths && id in metadata._obfPagePaths)
157
+ return metadata._obfPagePaths[id];
158
+ if (id.endsWith('.obf'))
159
+ return id;
160
+ return `${id}.obf`;
161
+ }
162
+ async processBoard(boardData, _boardPath) {
156
163
  const sourceButtons = boardData.buttons || [];
157
164
  // Calculate page ID first (used to make button IDs unique)
158
- const pageId = isZipEntry
159
- ? _boardPath // Zip entry - use filename to match navigation paths
160
- : boardData?.id
161
- ? String(boardData.id)
162
- : _boardPath?.split(/[/\\]/).pop() || '';
165
+ const pageId = boardData?.id ? String(boardData.id) : _boardPath?.split(/[/\\]/).pop() || '';
166
+ const images = boardData.images;
163
167
  const buttons = await Promise.all(sourceButtons.map(async (btn) => {
164
168
  const semanticAction = btn.load_board
165
169
  ? {
@@ -183,11 +187,16 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
183
187
  // Resolve image if image_id is present
184
188
  let resolvedImage;
185
189
  let imageBuffer;
186
- if (btn.image_id && boardData.images) {
187
- resolvedImage =
188
- (await this.extractImageAsDataUrl(btn.image_id, boardData.images)) || undefined;
189
- imageBuffer =
190
- (await this.extractImageAsBuffer(btn.image_id, boardData.images)) || undefined;
190
+ if (btn.image_id && images) {
191
+ resolvedImage = (await this.extractImageAsDataUrl(btn.image_id, images)) || undefined;
192
+ imageBuffer = (await this.extractImageAsBuffer(btn.image_id, images)) || undefined;
193
+ // save image data
194
+ if (images) {
195
+ const imageIndex = images?.findIndex((img) => img.id === btn.image_id);
196
+ if (imageIndex !== -1) {
197
+ images[imageIndex].data = resolvedImage;
198
+ }
199
+ }
191
200
  }
192
201
  // Build parameters object for Grid3 export compatibility
193
202
  const buttonParameters = {};
@@ -224,7 +233,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
224
233
  parentId: null,
225
234
  locale: boardData.locale,
226
235
  descriptionHtml: boardData.description_html,
227
- images: boardData.images,
236
+ images,
228
237
  sounds: boardData.sounds,
229
238
  });
230
239
  // Process grid layout if available
@@ -314,7 +323,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
314
323
  return texts;
315
324
  }
316
325
  async loadIntoTree(filePathOrBuffer) {
317
- const { readBinaryFromInput, readTextFromInput } = this.options.fileAdapter;
326
+ const { readBinaryFromInput, readTextFromInput, listDir, join, isDirectory } = this.options.fileAdapter;
318
327
  // Detailed logging for debugging input
319
328
  const bufferLength = typeof filePathOrBuffer === 'string'
320
329
  ? null
@@ -356,7 +365,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
356
365
  const boardData = await tryParseObfJson(content);
357
366
  if (boardData) {
358
367
  console.log('[OBF] Detected .obf file, parsed as JSON');
359
- const page = await this.processBoard(boardData, filePathOrBuffer, false);
368
+ const page = await this.processBoard(boardData, filePathOrBuffer);
360
369
  tree.addPage(page);
361
370
  // Set metadata from root board
362
371
  tree.metadata.format = 'obf';
@@ -380,22 +389,30 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
380
389
  throw err;
381
390
  }
382
391
  }
383
- // Detect likely zip signature first
384
- async function isLikelyZip(input) {
385
- if (typeof input === 'string') {
386
- const lowered = input.toLowerCase();
387
- return lowered.endsWith('.zip') || lowered.endsWith('.obz');
392
+ // Determine if input is ZIP, directory, or OBF JSON string/buffer
393
+ let fileType = 'obf';
394
+ if (typeof filePathOrBuffer !== 'string') {
395
+ const bytes = await readBinaryFromInput(filePathOrBuffer);
396
+ if (bytes.length >= 2 && bytes[0] === 0x50 && bytes[1] === 0x4b)
397
+ fileType = 'zip';
398
+ }
399
+ else {
400
+ if (await isDirectory(filePathOrBuffer)) {
401
+ fileType = 'dir';
402
+ }
403
+ else {
404
+ const lowered = filePathOrBuffer.toLowerCase();
405
+ if (lowered.endsWith('.zip') || lowered.endsWith('.obz'))
406
+ fileType = 'zip';
388
407
  }
389
- const bytes = await readBinaryFromInput(input);
390
- return bytes.length >= 2 && bytes[0] === 0x50 && bytes[1] === 0x4b;
391
408
  }
392
409
  // Check if input is a buffer or string that parses as OBF JSON; throw if neither JSON nor ZIP
393
- if (!(await isLikelyZip(filePathOrBuffer))) {
410
+ if (fileType === 'obf') {
394
411
  const asJson = await tryParseObfJson(filePathOrBuffer);
395
412
  if (!asJson)
396
413
  throw new Error('Invalid OBF content: not JSON and not ZIP');
397
414
  console.log('[OBF] Detected buffer/string as OBF JSON');
398
- const page = await this.processBoard(asJson, '[bufferOrString]', false);
415
+ const page = await this.processBoard(asJson, '[bufferOrString]');
399
416
  tree.addPage(page);
400
417
  // Set metadata from root board
401
418
  tree.metadata.format = 'obf';
@@ -411,18 +428,31 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
411
428
  tree.rootId = page.id;
412
429
  return tree;
413
430
  }
414
- try {
415
- this.zipFile = await this.options.zipAdapter(filePathOrBuffer);
416
- }
417
- catch (err) {
418
- console.error('[OBF] Error loading ZIP:', err);
419
- throw err;
431
+ this.zipFile = {
432
+ readFile: async (name) => {
433
+ return await readBinaryFromInput(join(filePathOrBuffer, name));
434
+ },
435
+ listFiles: () => {
436
+ throw new Error('Not implemented for directory input');
437
+ },
438
+ writeFiles: () => {
439
+ throw new Error('Not implemented for directory input');
440
+ },
441
+ };
442
+ if (fileType === 'zip') {
443
+ try {
444
+ this.zipFile = await this.options.zipAdapter(filePathOrBuffer);
445
+ }
446
+ catch (err) {
447
+ console.error('[OBF] Error loading ZIP:', err);
448
+ throw err;
449
+ }
420
450
  }
421
451
  // Store the ZIP file reference for image extraction
422
452
  this.imageCache.clear(); // Clear cache for new file
423
- console.log('[OBF] Detected zip archive, extracting .obf files');
453
+ console.log('[OBF] Detected zip archive or directory, extracting .obf files');
424
454
  // List manifest and OBF files
425
- const filesInZip = this.zipFile.listFiles();
455
+ const filesInZip = fileType === 'zip' ? this.zipFile.listFiles() : await listDir(filePathOrBuffer);
426
456
  const manifestFile = filesInZip.filter((name) => name.toLowerCase() === 'manifest.json');
427
457
  let obfEntries = filesInZip.filter((name) => name.toLowerCase().endsWith('.obf'));
428
458
  // Attempt to read manifest
@@ -456,7 +486,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
456
486
  const content = await this.zipFile.readFile(entryName);
457
487
  const boardData = await tryParseObfJson((0, io_1.decodeText)(content));
458
488
  if (boardData) {
459
- const page = await this.processBoard(boardData, entryName, true);
489
+ const page = await this.processBoard(boardData, entryName);
460
490
  tree.addPage(page);
461
491
  // Set metadata if not already set (use first board as reference)
462
492
  if (!tree.metadata.format) {
@@ -465,12 +495,16 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
465
495
  tree.metadata.description = boardData.description_html;
466
496
  tree.metadata.locale = boardData.locale;
467
497
  tree.metadata.id = boardData.id;
498
+ tree.metadata._obfPagePaths = { [page.id]: entryName };
468
499
  if (boardData.url)
469
500
  tree.metadata.url = boardData.url;
470
501
  if (boardData.locale)
471
502
  tree.metadata.languages = [boardData.locale];
472
503
  tree.rootId = page.id;
473
504
  }
505
+ else {
506
+ tree.metadata._obfPagePaths[page.id] = entryName;
507
+ }
474
508
  }
475
509
  else {
476
510
  console.warn('[OBF] Skipped entry (not valid OBF JSON):', entryName);
@@ -523,11 +557,18 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
523
557
  }
524
558
  return { rows: totalRows, columns: totalColumns, order, buttonPositions };
525
559
  }
526
- createObfBoardFromPage(page, fallbackName, metadata) {
560
+ createObfBoardFromPage(page, fallbackName, metadata, embedData = false) {
527
561
  const { rows, columns, order, buttonPositions } = this.buildGridMetadata(page);
528
562
  const boardName = metadata?.name && page.id === metadata?.defaultHomePageId
529
563
  ? metadata.name
530
564
  : page.name || fallbackName;
565
+ let images = Array.isArray(page.images) ? page.images : [];
566
+ if (!embedData) {
567
+ images = images.map((image) => {
568
+ delete image.data;
569
+ return image;
570
+ });
571
+ }
531
572
  return {
532
573
  format: OBF_FORMAT_VERSION,
533
574
  id: page.id,
@@ -564,7 +605,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
564
605
  hidden: button.visibility === 'Hidden' || false,
565
606
  };
566
607
  }),
567
- images: Array.isArray(page.images) ? page.images : [],
608
+ images,
568
609
  sounds: Array.isArray(page.sounds) ? page.sounds : [],
569
610
  };
570
611
  }
@@ -601,23 +642,22 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
601
642
  await this.saveFromTree(tree, outputPath);
602
643
  return await readBinaryFromInput(outputPath);
603
644
  }
604
- async saveFromTree(tree, outputPath) {
605
- const { writeTextToPath, writeBinaryToPath, pathExists } = this.options.fileAdapter;
645
+ async saveFromTree(tree, outputPath, embedData = false) {
646
+ const { writeTextToPath, writeBinaryToPath, pathExists, mkDir, join } = this.options.fileAdapter;
606
647
  if (outputPath.endsWith('.obf')) {
607
648
  // Save as single OBF JSON file
608
649
  const rootPage = tree.rootId ? tree.getPage(tree.rootId) : Object.values(tree.pages)[0];
609
650
  if (!rootPage) {
610
651
  throw new Error('No pages to save');
611
652
  }
612
- const obfBoard = this.createObfBoardFromPage(rootPage, 'Exported Board', tree.metadata);
653
+ const obfBoard = this.createObfBoardFromPage(rootPage, 'Exported Board', tree.metadata, embedData);
613
654
  await writeTextToPath(outputPath, JSON.stringify(obfBoard, null, 2));
614
655
  }
615
656
  else {
616
- const getPageFilename = (id) => (id.endsWith('.obf') ? id : `${id}.obf`);
617
657
  const files = Object.values(tree.pages).map((page) => {
618
- const obfBoard = this.createObfBoardFromPage(page, 'Board', tree.metadata);
658
+ const obfBoard = this.createObfBoardFromPage(page, 'Board', tree.metadata, embedData);
619
659
  const obfContent = JSON.stringify(obfBoard, null, 2);
620
- const name = getPageFilename(page.id);
660
+ const name = this.getPageFilename(page.id, tree.metadata);
621
661
  return {
622
662
  name,
623
663
  data: new TextEncoder().encode(obfContent),
@@ -627,7 +667,10 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
627
667
  format: OBF_FORMAT_VERSION,
628
668
  root: tree.metadata.defaultHomePageId,
629
669
  paths: {
630
- boards: Object.fromEntries(Object.entries(tree.pages).map(([id, page]) => [id, getPageFilename(page.id)])),
670
+ boards: Object.fromEntries(Object.entries(tree.pages).map(([id, page]) => [
671
+ id,
672
+ this.getPageFilename(page.id, tree.metadata),
673
+ ])),
631
674
  images: {}, //TODO Add support for saving images as files
632
675
  sounds: {}, //TODO Add support for saving sounds as files
633
676
  },
@@ -636,10 +679,22 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
636
679
  name: 'manifest.json',
637
680
  data: new TextEncoder().encode(JSON.stringify(manifest)),
638
681
  });
639
- const fileExists = await pathExists(outputPath);
640
- this.zipFile = await this.options.zipAdapter(fileExists ? outputPath : undefined, this.options.fileAdapter);
641
- const zipData = await this.zipFile.writeFiles(files);
642
- await writeBinaryToPath(outputPath, zipData);
682
+ if (outputPath.endsWith('.obz') || outputPath.endsWith('.zip')) {
683
+ console.log('[OBF] Saving to ZIP file:', outputPath);
684
+ const fileExists = await pathExists(outputPath);
685
+ this.zipFile = await this.options.zipAdapter(fileExists ? outputPath : undefined, this.options.fileAdapter);
686
+ const zipData = await this.zipFile.writeFiles(files);
687
+ await writeBinaryToPath(outputPath, zipData);
688
+ }
689
+ else {
690
+ console.log('[OBF] Saving to directory:', outputPath);
691
+ if (!(await pathExists(outputPath)))
692
+ await mkDir(outputPath);
693
+ for (const file of files) {
694
+ const filePath = join(outputPath, file.name);
695
+ await writeBinaryToPath(filePath, file.data);
696
+ }
697
+ }
643
698
  }
644
699
  }
645
700
  /**
@@ -666,13 +721,12 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
666
721
  const AdmZip = (await Promise.resolve().then(() => __importStar(require('adm-zip')))).default;
667
722
  const originalZip = new AdmZip(originalPath);
668
723
  const outputZip = new AdmZip();
669
- const getPageFilename = (id) => (id.endsWith('.obf') ? id : `${id}.obf`);
670
724
  // Track which .obf files we're modifying
671
725
  const modifiedObfFiles = new Set();
672
726
  // Generate new .obf files for pages in the tree
673
727
  const newObfFiles = new Map();
674
728
  for (const page of Object.values(tree.pages)) {
675
- const obfFilename = getPageFilename(page.id);
729
+ const obfFilename = this.getPageFilename(page.id, tree.metadata);
676
730
  modifiedObfFiles.add(obfFilename);
677
731
  const obfBoard = this.createObfBoardFromPage(page, 'Board', tree.metadata);
678
732
  const obfContent = JSON.stringify(obfBoard, null, 2);
@@ -685,7 +739,10 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
685
739
  format: OBF_FORMAT_VERSION,
686
740
  root: tree.metadata.defaultHomePageId,
687
741
  paths: {
688
- boards: Object.fromEntries(Object.entries(tree.pages).map(([id, page]) => [id, getPageFilename(page.id)])),
742
+ boards: Object.fromEntries(Object.entries(tree.pages).map(([id, page]) => [
743
+ id,
744
+ this.getPageFilename(page.id, tree.metadata),
745
+ ])),
689
746
  images: {},
690
747
  sounds: {},
691
748
  },
@@ -1,5 +1,4 @@
1
1
  export { MorphologyEngine } from './engine';
2
- export { Grid3VerbsParser } from './grid3VerbsParser';
3
2
  export { WordFormGenerator } from './wordFormGenerator';
4
3
  export type { MorphRuleSet, MorphRule, MorphWordForms, AstericsWordForm, VerbFormWithConditions, Grid3VerbFormsDetailed, } from './types';
5
4
  export type { Grid3VerbForms } from './grid3VerbsParser';
@@ -1,9 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.WordFormGenerator = exports.Grid3VerbsParser = exports.MorphologyEngine = void 0;
3
+ exports.WordFormGenerator = exports.MorphologyEngine = void 0;
4
4
  var engine_1 = require("./engine");
5
5
  Object.defineProperty(exports, "MorphologyEngine", { enumerable: true, get: function () { return engine_1.MorphologyEngine; } });
6
- var grid3VerbsParser_1 = require("./grid3VerbsParser");
7
- Object.defineProperty(exports, "Grid3VerbsParser", { enumerable: true, get: function () { return grid3VerbsParser_1.Grid3VerbsParser; } });
8
6
  var wordFormGenerator_1 = require("./wordFormGenerator");
9
7
  Object.defineProperty(exports, "WordFormGenerator", { enumerable: true, get: function () { return wordFormGenerator_1.WordFormGenerator; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@willwade/aac-processors",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
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",