@willwade/aac-processors 0.2.9 → 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.
@@ -80,7 +80,7 @@ class ObfProcessor extends BaseProcessor {
80
80
  // Images are typically stored in an 'images' folder or root
81
81
  const possiblePaths = [
82
82
  imageData.path, // Explicit path if provided
83
- `images/${imageData.filename || imageId}`, // Standard images folder
83
+ `images/${imageData.path || imageId}`, // Standard images folder
84
84
  imageData.id, // Just the ID
85
85
  ].filter(Boolean);
86
86
  for (const imagePath of possiblePaths) {
@@ -126,14 +126,18 @@ class ObfProcessor extends BaseProcessor {
126
126
  return 'image/png';
127
127
  }
128
128
  }
129
- async processBoard(boardData, _boardPath, isZipEntry) {
129
+ getPageFilename(id, metadata) {
130
+ if (metadata._obfPagePaths && id in metadata._obfPagePaths)
131
+ return metadata._obfPagePaths[id];
132
+ if (id.endsWith('.obf'))
133
+ return id;
134
+ return `${id}.obf`;
135
+ }
136
+ async processBoard(boardData, _boardPath) {
130
137
  const sourceButtons = boardData.buttons || [];
131
138
  // Calculate page ID first (used to make button IDs unique)
132
- const pageId = isZipEntry
133
- ? _boardPath // Zip entry - use filename to match navigation paths
134
- : boardData?.id
135
- ? String(boardData.id)
136
- : _boardPath?.split(/[/\\]/).pop() || '';
139
+ const pageId = boardData?.id ? String(boardData.id) : _boardPath?.split(/[/\\]/).pop() || '';
140
+ const images = boardData.images;
137
141
  const buttons = await Promise.all(sourceButtons.map(async (btn) => {
138
142
  const semanticAction = btn.load_board
139
143
  ? {
@@ -157,11 +161,16 @@ class ObfProcessor extends BaseProcessor {
157
161
  // Resolve image if image_id is present
158
162
  let resolvedImage;
159
163
  let imageBuffer;
160
- if (btn.image_id && boardData.images) {
161
- resolvedImage =
162
- (await this.extractImageAsDataUrl(btn.image_id, boardData.images)) || undefined;
163
- imageBuffer =
164
- (await this.extractImageAsBuffer(btn.image_id, boardData.images)) || undefined;
164
+ if (btn.image_id && images) {
165
+ resolvedImage = (await this.extractImageAsDataUrl(btn.image_id, images)) || undefined;
166
+ imageBuffer = (await this.extractImageAsBuffer(btn.image_id, images)) || undefined;
167
+ // save image data
168
+ if (images) {
169
+ const imageIndex = images?.findIndex((img) => img.id === btn.image_id);
170
+ if (imageIndex !== -1) {
171
+ images[imageIndex].data = resolvedImage;
172
+ }
173
+ }
165
174
  }
166
175
  // Build parameters object for Grid3 export compatibility
167
176
  const buttonParameters = {};
@@ -198,7 +207,7 @@ class ObfProcessor extends BaseProcessor {
198
207
  parentId: null,
199
208
  locale: boardData.locale,
200
209
  descriptionHtml: boardData.description_html,
201
- images: boardData.images,
210
+ images,
202
211
  sounds: boardData.sounds,
203
212
  });
204
213
  // Process grid layout if available
@@ -288,7 +297,7 @@ class ObfProcessor extends BaseProcessor {
288
297
  return texts;
289
298
  }
290
299
  async loadIntoTree(filePathOrBuffer) {
291
- const { readBinaryFromInput, readTextFromInput } = this.options.fileAdapter;
300
+ const { readBinaryFromInput, readTextFromInput, listDir, join, isDirectory } = this.options.fileAdapter;
292
301
  // Detailed logging for debugging input
293
302
  const bufferLength = typeof filePathOrBuffer === 'string'
294
303
  ? null
@@ -330,7 +339,7 @@ class ObfProcessor extends BaseProcessor {
330
339
  const boardData = await tryParseObfJson(content);
331
340
  if (boardData) {
332
341
  console.log('[OBF] Detected .obf file, parsed as JSON');
333
- const page = await this.processBoard(boardData, filePathOrBuffer, false);
342
+ const page = await this.processBoard(boardData, filePathOrBuffer);
334
343
  tree.addPage(page);
335
344
  // Set metadata from root board
336
345
  tree.metadata.format = 'obf';
@@ -354,22 +363,30 @@ class ObfProcessor extends BaseProcessor {
354
363
  throw err;
355
364
  }
356
365
  }
357
- // Detect likely zip signature first
358
- async function isLikelyZip(input) {
359
- if (typeof input === 'string') {
360
- const lowered = input.toLowerCase();
361
- return lowered.endsWith('.zip') || lowered.endsWith('.obz');
366
+ // Determine if input is ZIP, directory, or OBF JSON string/buffer
367
+ let fileType = 'obf';
368
+ if (typeof filePathOrBuffer !== 'string') {
369
+ const bytes = await readBinaryFromInput(filePathOrBuffer);
370
+ if (bytes.length >= 2 && bytes[0] === 0x50 && bytes[1] === 0x4b)
371
+ fileType = 'zip';
372
+ }
373
+ else {
374
+ if (await isDirectory(filePathOrBuffer)) {
375
+ fileType = 'dir';
376
+ }
377
+ else {
378
+ const lowered = filePathOrBuffer.toLowerCase();
379
+ if (lowered.endsWith('.zip') || lowered.endsWith('.obz'))
380
+ fileType = 'zip';
362
381
  }
363
- const bytes = await readBinaryFromInput(input);
364
- return bytes.length >= 2 && bytes[0] === 0x50 && bytes[1] === 0x4b;
365
382
  }
366
383
  // Check if input is a buffer or string that parses as OBF JSON; throw if neither JSON nor ZIP
367
- if (!(await isLikelyZip(filePathOrBuffer))) {
384
+ if (fileType === 'obf') {
368
385
  const asJson = await tryParseObfJson(filePathOrBuffer);
369
386
  if (!asJson)
370
387
  throw new Error('Invalid OBF content: not JSON and not ZIP');
371
388
  console.log('[OBF] Detected buffer/string as OBF JSON');
372
- const page = await this.processBoard(asJson, '[bufferOrString]', false);
389
+ const page = await this.processBoard(asJson, '[bufferOrString]');
373
390
  tree.addPage(page);
374
391
  // Set metadata from root board
375
392
  tree.metadata.format = 'obf';
@@ -385,18 +402,31 @@ class ObfProcessor extends BaseProcessor {
385
402
  tree.rootId = page.id;
386
403
  return tree;
387
404
  }
388
- try {
389
- this.zipFile = await this.options.zipAdapter(filePathOrBuffer);
390
- }
391
- catch (err) {
392
- console.error('[OBF] Error loading ZIP:', err);
393
- throw err;
405
+ this.zipFile = {
406
+ readFile: async (name) => {
407
+ return await readBinaryFromInput(join(filePathOrBuffer, name));
408
+ },
409
+ listFiles: () => {
410
+ throw new Error('Not implemented for directory input');
411
+ },
412
+ writeFiles: () => {
413
+ throw new Error('Not implemented for directory input');
414
+ },
415
+ };
416
+ if (fileType === 'zip') {
417
+ try {
418
+ this.zipFile = await this.options.zipAdapter(filePathOrBuffer);
419
+ }
420
+ catch (err) {
421
+ console.error('[OBF] Error loading ZIP:', err);
422
+ throw err;
423
+ }
394
424
  }
395
425
  // Store the ZIP file reference for image extraction
396
426
  this.imageCache.clear(); // Clear cache for new file
397
- console.log('[OBF] Detected zip archive, extracting .obf files');
427
+ console.log('[OBF] Detected zip archive or directory, extracting .obf files');
398
428
  // List manifest and OBF files
399
- const filesInZip = this.zipFile.listFiles();
429
+ const filesInZip = fileType === 'zip' ? this.zipFile.listFiles() : await listDir(filePathOrBuffer);
400
430
  const manifestFile = filesInZip.filter((name) => name.toLowerCase() === 'manifest.json');
401
431
  let obfEntries = filesInZip.filter((name) => name.toLowerCase().endsWith('.obf'));
402
432
  // Attempt to read manifest
@@ -430,7 +460,7 @@ class ObfProcessor extends BaseProcessor {
430
460
  const content = await this.zipFile.readFile(entryName);
431
461
  const boardData = await tryParseObfJson(decodeText(content));
432
462
  if (boardData) {
433
- const page = await this.processBoard(boardData, entryName, true);
463
+ const page = await this.processBoard(boardData, entryName);
434
464
  tree.addPage(page);
435
465
  // Set metadata if not already set (use first board as reference)
436
466
  if (!tree.metadata.format) {
@@ -439,12 +469,16 @@ class ObfProcessor extends BaseProcessor {
439
469
  tree.metadata.description = boardData.description_html;
440
470
  tree.metadata.locale = boardData.locale;
441
471
  tree.metadata.id = boardData.id;
472
+ tree.metadata._obfPagePaths = { [page.id]: entryName };
442
473
  if (boardData.url)
443
474
  tree.metadata.url = boardData.url;
444
475
  if (boardData.locale)
445
476
  tree.metadata.languages = [boardData.locale];
446
477
  tree.rootId = page.id;
447
478
  }
479
+ else {
480
+ tree.metadata._obfPagePaths[page.id] = entryName;
481
+ }
448
482
  }
449
483
  else {
450
484
  console.warn('[OBF] Skipped entry (not valid OBF JSON):', entryName);
@@ -497,11 +531,18 @@ class ObfProcessor extends BaseProcessor {
497
531
  }
498
532
  return { rows: totalRows, columns: totalColumns, order, buttonPositions };
499
533
  }
500
- createObfBoardFromPage(page, fallbackName, metadata) {
534
+ createObfBoardFromPage(page, fallbackName, metadata, embedData = false) {
501
535
  const { rows, columns, order, buttonPositions } = this.buildGridMetadata(page);
502
536
  const boardName = metadata?.name && page.id === metadata?.defaultHomePageId
503
537
  ? metadata.name
504
538
  : page.name || fallbackName;
539
+ let images = Array.isArray(page.images) ? page.images : [];
540
+ if (!embedData) {
541
+ images = images.map((image) => {
542
+ delete image.data;
543
+ return image;
544
+ });
545
+ }
505
546
  return {
506
547
  format: OBF_FORMAT_VERSION,
507
548
  id: page.id,
@@ -538,7 +579,7 @@ class ObfProcessor extends BaseProcessor {
538
579
  hidden: button.visibility === 'Hidden' || false,
539
580
  };
540
581
  }),
541
- images: Array.isArray(page.images) ? page.images : [],
582
+ images,
542
583
  sounds: Array.isArray(page.sounds) ? page.sounds : [],
543
584
  };
544
585
  }
@@ -575,23 +616,22 @@ class ObfProcessor extends BaseProcessor {
575
616
  await this.saveFromTree(tree, outputPath);
576
617
  return await readBinaryFromInput(outputPath);
577
618
  }
578
- async saveFromTree(tree, outputPath) {
579
- const { writeTextToPath, writeBinaryToPath, pathExists } = this.options.fileAdapter;
619
+ async saveFromTree(tree, outputPath, embedData = false) {
620
+ const { writeTextToPath, writeBinaryToPath, pathExists, mkDir, join } = this.options.fileAdapter;
580
621
  if (outputPath.endsWith('.obf')) {
581
622
  // Save as single OBF JSON file
582
623
  const rootPage = tree.rootId ? tree.getPage(tree.rootId) : Object.values(tree.pages)[0];
583
624
  if (!rootPage) {
584
625
  throw new Error('No pages to save');
585
626
  }
586
- const obfBoard = this.createObfBoardFromPage(rootPage, 'Exported Board', tree.metadata);
627
+ const obfBoard = this.createObfBoardFromPage(rootPage, 'Exported Board', tree.metadata, embedData);
587
628
  await writeTextToPath(outputPath, JSON.stringify(obfBoard, null, 2));
588
629
  }
589
630
  else {
590
- const getPageFilename = (id) => (id.endsWith('.obf') ? id : `${id}.obf`);
591
631
  const files = Object.values(tree.pages).map((page) => {
592
- const obfBoard = this.createObfBoardFromPage(page, 'Board', tree.metadata);
632
+ const obfBoard = this.createObfBoardFromPage(page, 'Board', tree.metadata, embedData);
593
633
  const obfContent = JSON.stringify(obfBoard, null, 2);
594
- const name = getPageFilename(page.id);
634
+ const name = this.getPageFilename(page.id, tree.metadata);
595
635
  return {
596
636
  name,
597
637
  data: new TextEncoder().encode(obfContent),
@@ -601,7 +641,10 @@ class ObfProcessor extends BaseProcessor {
601
641
  format: OBF_FORMAT_VERSION,
602
642
  root: tree.metadata.defaultHomePageId,
603
643
  paths: {
604
- boards: Object.fromEntries(Object.entries(tree.pages).map(([id, page]) => [id, getPageFilename(page.id)])),
644
+ boards: Object.fromEntries(Object.entries(tree.pages).map(([id, page]) => [
645
+ id,
646
+ this.getPageFilename(page.id, tree.metadata),
647
+ ])),
605
648
  images: {}, //TODO Add support for saving images as files
606
649
  sounds: {}, //TODO Add support for saving sounds as files
607
650
  },
@@ -610,10 +653,22 @@ class ObfProcessor extends BaseProcessor {
610
653
  name: 'manifest.json',
611
654
  data: new TextEncoder().encode(JSON.stringify(manifest)),
612
655
  });
613
- const fileExists = await pathExists(outputPath);
614
- this.zipFile = await this.options.zipAdapter(fileExists ? outputPath : undefined, this.options.fileAdapter);
615
- const zipData = await this.zipFile.writeFiles(files);
616
- await writeBinaryToPath(outputPath, zipData);
656
+ if (outputPath.endsWith('.obz') || outputPath.endsWith('.zip')) {
657
+ console.log('[OBF] Saving to ZIP file:', outputPath);
658
+ const fileExists = await pathExists(outputPath);
659
+ this.zipFile = await this.options.zipAdapter(fileExists ? outputPath : undefined, this.options.fileAdapter);
660
+ const zipData = await this.zipFile.writeFiles(files);
661
+ await writeBinaryToPath(outputPath, zipData);
662
+ }
663
+ else {
664
+ console.log('[OBF] Saving to directory:', outputPath);
665
+ if (!(await pathExists(outputPath)))
666
+ await mkDir(outputPath);
667
+ for (const file of files) {
668
+ const filePath = join(outputPath, file.name);
669
+ await writeBinaryToPath(filePath, file.data);
670
+ }
671
+ }
617
672
  }
618
673
  }
619
674
  /**
@@ -640,13 +695,12 @@ class ObfProcessor extends BaseProcessor {
640
695
  const AdmZip = (await import('adm-zip')).default;
641
696
  const originalZip = new AdmZip(originalPath);
642
697
  const outputZip = new AdmZip();
643
- const getPageFilename = (id) => (id.endsWith('.obf') ? id : `${id}.obf`);
644
698
  // Track which .obf files we're modifying
645
699
  const modifiedObfFiles = new Set();
646
700
  // Generate new .obf files for pages in the tree
647
701
  const newObfFiles = new Map();
648
702
  for (const page of Object.values(tree.pages)) {
649
- const obfFilename = getPageFilename(page.id);
703
+ const obfFilename = this.getPageFilename(page.id, tree.metadata);
650
704
  modifiedObfFiles.add(obfFilename);
651
705
  const obfBoard = this.createObfBoardFromPage(page, 'Board', tree.metadata);
652
706
  const obfContent = JSON.stringify(obfBoard, null, 2);
@@ -659,7 +713,10 @@ class ObfProcessor extends BaseProcessor {
659
713
  format: OBF_FORMAT_VERSION,
660
714
  root: tree.metadata.defaultHomePageId,
661
715
  paths: {
662
- boards: Object.fromEntries(Object.entries(tree.pages).map(([id, page]) => [id, getPageFilename(page.id)])),
716
+ boards: Object.fromEntries(Object.entries(tree.pages).map(([id, page]) => [
717
+ id,
718
+ this.getPageFilename(page.id, tree.metadata),
719
+ ])),
663
720
  images: {},
664
721
  sounds: {},
665
722
  },
@@ -1,3 +1,2 @@
1
1
  export { MorphologyEngine } from './engine';
2
- export { Grid3VerbsParser } from './grid3VerbsParser';
3
2
  export { WordFormGenerator } from './wordFormGenerator';
@@ -16,13 +16,14 @@ declare class ObfProcessor extends BaseProcessor {
16
16
  */
17
17
  private extractImageAsDataUrl;
18
18
  private getMimeTypeFromFilename;
19
+ private getPageFilename;
19
20
  private processBoard;
20
21
  extractTexts(filePathOrBuffer: ProcessorInput): Promise<string[]>;
21
22
  loadIntoTree(filePathOrBuffer: ProcessorInput): Promise<AACTree>;
22
23
  private buildGridMetadata;
23
24
  private createObfBoardFromPage;
24
25
  processTexts(filePathOrBuffer: ProcessorInput, translations: Map<string, string>, outputPath: string): Promise<Uint8Array>;
25
- saveFromTree(tree: AACTree, outputPath: string): Promise<void>;
26
+ saveFromTree(tree: AACTree, outputPath: string, embedData?: boolean): Promise<void>;
26
27
  /**
27
28
  * Save a modified tree while preserving all original files (images, sounds, assets)
28
29
  * This method only updates the .obf files for pages in the tree, keeping everything else intact.
@@ -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.9",
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",