@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.
- package/dist/browser/processors/gridsetProcessor.js +243 -77
- package/dist/browser/processors/obfProcessor.js +106 -49
- package/dist/browser/utilities/analytics/morphology/index.js +0 -1
- package/dist/processors/gridsetProcessor.d.ts +5 -0
- package/dist/processors/gridsetProcessor.js +243 -77
- package/dist/processors/obfProcessor.d.ts +2 -1
- package/dist/processors/obfProcessor.js +106 -49
- package/dist/utilities/analytics/morphology/index.d.ts +0 -1
- package/dist/utilities/analytics/morphology/index.js +1 -3
- package/package.json +1 -1
|
@@ -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.
|
|
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
|
-
|
|
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 =
|
|
133
|
-
|
|
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 &&
|
|
161
|
-
resolvedImage =
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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 (
|
|
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]'
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
|
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
|
|
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]) => [
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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]) => [
|
|
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
|
},
|
|
@@ -54,12 +54,17 @@ declare class GridsetProcessor extends BaseProcessor {
|
|
|
54
54
|
/**
|
|
55
55
|
* Save a modified tree while preserving all original files (settings, images, assets)
|
|
56
56
|
* This method only updates the grid.xml files for pages in the tree, keeping everything else intact.
|
|
57
|
+
* It preserves the original grid structure and only updates button labels and messages.
|
|
57
58
|
*
|
|
58
59
|
* @param originalPath - Path to the original gridset file
|
|
59
60
|
* @param tree - Modified AACTree with pages to save
|
|
60
61
|
* @param outputPath - Path where the modified gridset should be saved
|
|
61
62
|
*/
|
|
62
63
|
saveModifiedTree(originalPath: string, tree: AACTree, outputPath: string): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Create a basic grid XML for a page when original doesn't exist
|
|
66
|
+
*/
|
|
67
|
+
private createBasicGridXml;
|
|
63
68
|
private findButtonPosition;
|
|
64
69
|
/**
|
|
65
70
|
* Extract strings with metadata for aac-tools-platform compatibility
|