@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.
- package/dist/browser/processors/obfProcessor.js +106 -49
- package/dist/browser/utilities/analytics/morphology/index.js +0 -1
- 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
|
},
|
|
@@ -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.
|
|
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
|
-
|
|
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 =
|
|
159
|
-
|
|
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 &&
|
|
187
|
-
resolvedImage =
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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 (
|
|
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]'
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
|
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
|
|
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]) => [
|
|
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
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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]) => [
|
|
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.
|
|
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.
|
|
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",
|