@willwade/aac-processors 0.2.6 → 0.2.7

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.
@@ -2204,6 +2204,130 @@ class GridsetProcessor extends BaseProcessor {
2204
2204
  RowDefinition: Array(maxRows).fill({}),
2205
2205
  };
2206
2206
  }
2207
+ /**
2208
+ * Save a modified tree while preserving all original files (settings, images, assets)
2209
+ * This method only updates the grid.xml files for pages in the tree, keeping everything else intact.
2210
+ *
2211
+ * @param originalPath - Path to the original gridset file
2212
+ * @param tree - Modified AACTree with pages to save
2213
+ * @param outputPath - Path where the modified gridset should be saved
2214
+ */
2215
+ async saveModifiedTree(originalPath, tree, outputPath) {
2216
+ const { readBinaryFromInput, writeBinaryToPath } = this.options.fileAdapter;
2217
+ if (Object.keys(tree.pages).length === 0) {
2218
+ // Empty tree, just copy the original
2219
+ const originalBuffer = await readBinaryFromInput(originalPath);
2220
+ await writeBinaryToPath(outputPath, originalBuffer);
2221
+ return;
2222
+ }
2223
+ const AdmZip = (await import('adm-zip')).default;
2224
+ const originalZip = new AdmZip(originalPath);
2225
+ const outputZip = new AdmZip();
2226
+ // Collect styles from the tree for grid.xml files
2227
+ const uniqueStyles = new Map();
2228
+ let styleIdCounter = 1;
2229
+ const addStyle = (style) => {
2230
+ if (!style)
2231
+ return '';
2232
+ const normalizedStyle = { ...style };
2233
+ const styleKey = JSON.stringify(normalizedStyle);
2234
+ const existing = uniqueStyles.get(styleKey);
2235
+ if (existing)
2236
+ return existing.id;
2237
+ const styleId = `Style${styleIdCounter++}`;
2238
+ uniqueStyles.set(styleKey, { id: styleId, style: normalizedStyle });
2239
+ return styleId;
2240
+ };
2241
+ // Collect all styles from pages and buttons
2242
+ Object.values(tree.pages).forEach((page) => {
2243
+ addStyle(page.style);
2244
+ page.buttons.forEach((button) => {
2245
+ addStyle(button.style);
2246
+ });
2247
+ });
2248
+ // Track which grid files we're modifying
2249
+ const modifiedGridFiles = new Set();
2250
+ // Generate grid.xml files for pages in the tree
2251
+ const newGridFiles = new Map();
2252
+ for (const page of Object.values(tree.pages)) {
2253
+ const gridPath = `Grids/${page.name}/grid.xml`;
2254
+ modifiedGridFiles.add(gridPath);
2255
+ // Build the grid XML content
2256
+ const gridData = {
2257
+ Grid: {
2258
+ '@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
2259
+ GridGuid: page.id,
2260
+ ColumnDefinitions: this.calculateColumnDefinitions(page),
2261
+ RowDefinitions: this.calculateRowDefinitions(page, false),
2262
+ AutoContentCommands: '',
2263
+ Cells: page.buttons.length > 0
2264
+ ? {
2265
+ Cell: this.filterPageButtons(page.buttons).map((button, btnIndex) => {
2266
+ const buttonStyleId = button.style ? addStyle(button.style) : '';
2267
+ const position = this.findButtonPosition(page, button, btnIndex);
2268
+ const captionAndImage = {
2269
+ Caption: button.label || '',
2270
+ };
2271
+ // Handle image references
2272
+ if (button.image) {
2273
+ captionAndImage.Image = `${button.image}`;
2274
+ }
2275
+ const cell = {
2276
+ '@_Column': position.x,
2277
+ '@_Row': position.y,
2278
+ captionAndImage,
2279
+ };
2280
+ if (position.columnSpan > 1) {
2281
+ cell['@_ColumnSpan'] = position.columnSpan;
2282
+ }
2283
+ if (position.rowSpan > 1) {
2284
+ cell['@_RowSpan'] = position.rowSpan;
2285
+ }
2286
+ if (buttonStyleId) {
2287
+ cell.CellStyle = buttonStyleId;
2288
+ }
2289
+ if (button.message && button.message !== button.label) {
2290
+ // Use spoken message if different from label
2291
+ const spoken = button.message;
2292
+ const cellContent = {
2293
+ spoken,
2294
+ type: 'text',
2295
+ };
2296
+ cell['ContentCell'] = cellContent;
2297
+ }
2298
+ return cell;
2299
+ }),
2300
+ }
2301
+ : undefined,
2302
+ },
2303
+ };
2304
+ const gridBuilder = new XMLBuilder({
2305
+ ignoreAttributes: false,
2306
+ format: true,
2307
+ indentBy: ' ',
2308
+ suppressEmptyNode: true,
2309
+ });
2310
+ newGridFiles.set(gridPath, gridBuilder.build(gridData));
2311
+ }
2312
+ // Copy all files from original zip, replacing modified grid files
2313
+ for (const entry of originalZip.getEntries()) {
2314
+ if (entry.isDirectory)
2315
+ continue;
2316
+ // Skip grid.xml files that we're modifying
2317
+ if (modifiedGridFiles.has(entry.entryName)) {
2318
+ const newContent = newGridFiles.get(entry.entryName);
2319
+ if (newContent) {
2320
+ outputZip.addFile(entry.entryName, Buffer.from(newContent, 'utf8'));
2321
+ }
2322
+ continue;
2323
+ }
2324
+ // Copy all other files as-is
2325
+ outputZip.addFile(entry.entryName, entry.getData());
2326
+ }
2327
+ // Write the output ZIP
2328
+ const outputBuffer = outputZip.toBuffer();
2329
+ await writeBinaryToPath(outputPath, outputBuffer);
2330
+ }
2207
2331
  // Helper method to find button position with span information
2208
2332
  findButtonPosition(page, button, fallbackIndex) {
2209
2333
  if (page.grid && page.grid.length > 0) {
@@ -616,6 +616,75 @@ class ObfProcessor extends BaseProcessor {
616
616
  await writeBinaryToPath(outputPath, zipData);
617
617
  }
618
618
  }
619
+ /**
620
+ * Save a modified tree while preserving all original files (images, sounds, assets)
621
+ * This method only updates the .obf files for pages in the tree, keeping everything else intact.
622
+ *
623
+ * @param originalPath - Path to the original OBF/OBZ file
624
+ * @param tree - Modified AACTree with pages to save
625
+ * @param outputPath - Path where the modified file should be saved
626
+ */
627
+ async saveModifiedTree(originalPath, tree, outputPath) {
628
+ const { writeBinaryToPath, readBinaryFromInput } = this.options.fileAdapter;
629
+ // If output is .obf (single file), use regular save
630
+ if (outputPath.endsWith('.obf')) {
631
+ await this.saveFromTree(tree, outputPath);
632
+ return;
633
+ }
634
+ if (Object.keys(tree.pages).length === 0) {
635
+ // Empty tree, just copy the original
636
+ const originalBuffer = await readBinaryFromInput(originalPath);
637
+ await writeBinaryToPath(outputPath, originalBuffer);
638
+ return;
639
+ }
640
+ const AdmZip = (await import('adm-zip')).default;
641
+ const originalZip = new AdmZip(originalPath);
642
+ const outputZip = new AdmZip();
643
+ const getPageFilename = (id) => (id.endsWith('.obf') ? id : `${id}.obf`);
644
+ // Track which .obf files we're modifying
645
+ const modifiedObfFiles = new Set();
646
+ // Generate new .obf files for pages in the tree
647
+ const newObfFiles = new Map();
648
+ for (const page of Object.values(tree.pages)) {
649
+ const obfFilename = getPageFilename(page.id);
650
+ modifiedObfFiles.add(obfFilename);
651
+ const obfBoard = this.createObfBoardFromPage(page, 'Board', tree.metadata);
652
+ const obfContent = JSON.stringify(obfBoard, null, 2);
653
+ newObfFiles.set(obfFilename, obfContent);
654
+ }
655
+ // Generate updated manifest if we have pages
656
+ if (Object.keys(tree.pages).length > 0) {
657
+ modifiedObfFiles.add('manifest.json');
658
+ const manifest = {
659
+ format: OBF_FORMAT_VERSION,
660
+ root: tree.metadata.defaultHomePageId,
661
+ paths: {
662
+ boards: Object.fromEntries(Object.entries(tree.pages).map(([id, page]) => [id, getPageFilename(page.id)])),
663
+ images: {},
664
+ sounds: {},
665
+ },
666
+ };
667
+ newObfFiles.set('manifest.json', JSON.stringify(manifest));
668
+ }
669
+ // Copy all files from original zip, replacing modified .obf files
670
+ for (const entry of originalZip.getEntries()) {
671
+ if (entry.isDirectory)
672
+ continue;
673
+ // Skip .obf files that we're modifying
674
+ if (modifiedObfFiles.has(entry.entryName)) {
675
+ const newContent = newObfFiles.get(entry.entryName);
676
+ if (newContent) {
677
+ outputZip.addFile(entry.entryName, Buffer.from(newContent, 'utf8'));
678
+ }
679
+ continue;
680
+ }
681
+ // Copy all other files as-is (preserves images, sounds, etc.)
682
+ outputZip.addFile(entry.entryName, entry.getData());
683
+ }
684
+ // Write the output ZIP
685
+ const outputBuffer = outputZip.toBuffer();
686
+ await writeBinaryToPath(outputPath, outputBuffer);
687
+ }
619
688
  /**
620
689
  * Extract strings with metadata for aac-tools-platform compatibility
621
690
  * Uses the generic implementation from BaseProcessor
@@ -51,6 +51,15 @@ declare class GridsetProcessor extends BaseProcessor {
51
51
  saveFromTree(tree: AACTree, outputPath: string): Promise<void>;
52
52
  private calculateColumnDefinitions;
53
53
  private calculateRowDefinitions;
54
+ /**
55
+ * Save a modified tree while preserving all original files (settings, images, assets)
56
+ * This method only updates the grid.xml files for pages in the tree, keeping everything else intact.
57
+ *
58
+ * @param originalPath - Path to the original gridset file
59
+ * @param tree - Modified AACTree with pages to save
60
+ * @param outputPath - Path where the modified gridset should be saved
61
+ */
62
+ saveModifiedTree(originalPath: string, tree: AACTree, outputPath: string): Promise<void>;
54
63
  private findButtonPosition;
55
64
  /**
56
65
  * Extract strings with metadata for aac-tools-platform compatibility
@@ -1,4 +1,27 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
2
25
  Object.defineProperty(exports, "__esModule", { value: true });
3
26
  exports.GridsetProcessor = void 0;
4
27
  const baseProcessor_1 = require("../core/baseProcessor");
@@ -2207,6 +2230,130 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
2207
2230
  RowDefinition: Array(maxRows).fill({}),
2208
2231
  };
2209
2232
  }
2233
+ /**
2234
+ * Save a modified tree while preserving all original files (settings, images, assets)
2235
+ * This method only updates the grid.xml files for pages in the tree, keeping everything else intact.
2236
+ *
2237
+ * @param originalPath - Path to the original gridset file
2238
+ * @param tree - Modified AACTree with pages to save
2239
+ * @param outputPath - Path where the modified gridset should be saved
2240
+ */
2241
+ async saveModifiedTree(originalPath, tree, outputPath) {
2242
+ const { readBinaryFromInput, writeBinaryToPath } = this.options.fileAdapter;
2243
+ if (Object.keys(tree.pages).length === 0) {
2244
+ // Empty tree, just copy the original
2245
+ const originalBuffer = await readBinaryFromInput(originalPath);
2246
+ await writeBinaryToPath(outputPath, originalBuffer);
2247
+ return;
2248
+ }
2249
+ const AdmZip = (await Promise.resolve().then(() => __importStar(require('adm-zip')))).default;
2250
+ const originalZip = new AdmZip(originalPath);
2251
+ const outputZip = new AdmZip();
2252
+ // Collect styles from the tree for grid.xml files
2253
+ const uniqueStyles = new Map();
2254
+ let styleIdCounter = 1;
2255
+ const addStyle = (style) => {
2256
+ if (!style)
2257
+ return '';
2258
+ const normalizedStyle = { ...style };
2259
+ const styleKey = JSON.stringify(normalizedStyle);
2260
+ const existing = uniqueStyles.get(styleKey);
2261
+ if (existing)
2262
+ return existing.id;
2263
+ const styleId = `Style${styleIdCounter++}`;
2264
+ uniqueStyles.set(styleKey, { id: styleId, style: normalizedStyle });
2265
+ return styleId;
2266
+ };
2267
+ // Collect all styles from pages and buttons
2268
+ Object.values(tree.pages).forEach((page) => {
2269
+ addStyle(page.style);
2270
+ page.buttons.forEach((button) => {
2271
+ addStyle(button.style);
2272
+ });
2273
+ });
2274
+ // Track which grid files we're modifying
2275
+ const modifiedGridFiles = new Set();
2276
+ // Generate grid.xml files for pages in the tree
2277
+ const newGridFiles = new Map();
2278
+ for (const page of Object.values(tree.pages)) {
2279
+ const gridPath = `Grids/${page.name}/grid.xml`;
2280
+ modifiedGridFiles.add(gridPath);
2281
+ // Build the grid XML content
2282
+ const gridData = {
2283
+ Grid: {
2284
+ '@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
2285
+ GridGuid: page.id,
2286
+ ColumnDefinitions: this.calculateColumnDefinitions(page),
2287
+ RowDefinitions: this.calculateRowDefinitions(page, false),
2288
+ AutoContentCommands: '',
2289
+ Cells: page.buttons.length > 0
2290
+ ? {
2291
+ Cell: this.filterPageButtons(page.buttons).map((button, btnIndex) => {
2292
+ const buttonStyleId = button.style ? addStyle(button.style) : '';
2293
+ const position = this.findButtonPosition(page, button, btnIndex);
2294
+ const captionAndImage = {
2295
+ Caption: button.label || '',
2296
+ };
2297
+ // Handle image references
2298
+ if (button.image) {
2299
+ captionAndImage.Image = `${button.image}`;
2300
+ }
2301
+ const cell = {
2302
+ '@_Column': position.x,
2303
+ '@_Row': position.y,
2304
+ captionAndImage,
2305
+ };
2306
+ if (position.columnSpan > 1) {
2307
+ cell['@_ColumnSpan'] = position.columnSpan;
2308
+ }
2309
+ if (position.rowSpan > 1) {
2310
+ cell['@_RowSpan'] = position.rowSpan;
2311
+ }
2312
+ if (buttonStyleId) {
2313
+ cell.CellStyle = buttonStyleId;
2314
+ }
2315
+ if (button.message && button.message !== button.label) {
2316
+ // Use spoken message if different from label
2317
+ const spoken = button.message;
2318
+ const cellContent = {
2319
+ spoken,
2320
+ type: 'text',
2321
+ };
2322
+ cell['ContentCell'] = cellContent;
2323
+ }
2324
+ return cell;
2325
+ }),
2326
+ }
2327
+ : undefined,
2328
+ },
2329
+ };
2330
+ const gridBuilder = new fast_xml_parser_1.XMLBuilder({
2331
+ ignoreAttributes: false,
2332
+ format: true,
2333
+ indentBy: ' ',
2334
+ suppressEmptyNode: true,
2335
+ });
2336
+ newGridFiles.set(gridPath, gridBuilder.build(gridData));
2337
+ }
2338
+ // Copy all files from original zip, replacing modified grid files
2339
+ for (const entry of originalZip.getEntries()) {
2340
+ if (entry.isDirectory)
2341
+ continue;
2342
+ // Skip grid.xml files that we're modifying
2343
+ if (modifiedGridFiles.has(entry.entryName)) {
2344
+ const newContent = newGridFiles.get(entry.entryName);
2345
+ if (newContent) {
2346
+ outputZip.addFile(entry.entryName, Buffer.from(newContent, 'utf8'));
2347
+ }
2348
+ continue;
2349
+ }
2350
+ // Copy all other files as-is
2351
+ outputZip.addFile(entry.entryName, entry.getData());
2352
+ }
2353
+ // Write the output ZIP
2354
+ const outputBuffer = outputZip.toBuffer();
2355
+ await writeBinaryToPath(outputPath, outputBuffer);
2356
+ }
2210
2357
  // Helper method to find button position with span information
2211
2358
  findButtonPosition(page, button, fallbackIndex) {
2212
2359
  if (page.grid && page.grid.length > 0) {
@@ -23,6 +23,15 @@ declare class ObfProcessor extends BaseProcessor {
23
23
  private createObfBoardFromPage;
24
24
  processTexts(filePathOrBuffer: ProcessorInput, translations: Map<string, string>, outputPath: string): Promise<Uint8Array>;
25
25
  saveFromTree(tree: AACTree, outputPath: string): Promise<void>;
26
+ /**
27
+ * Save a modified tree while preserving all original files (images, sounds, assets)
28
+ * This method only updates the .obf files for pages in the tree, keeping everything else intact.
29
+ *
30
+ * @param originalPath - Path to the original OBF/OBZ file
31
+ * @param tree - Modified AACTree with pages to save
32
+ * @param outputPath - Path where the modified file should be saved
33
+ */
34
+ saveModifiedTree(originalPath: string, tree: AACTree, outputPath: string): Promise<void>;
26
35
  /**
27
36
  * Extract strings with metadata for aac-tools-platform compatibility
28
37
  * Uses the generic implementation from BaseProcessor
@@ -1,4 +1,27 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
2
25
  Object.defineProperty(exports, "__esModule", { value: true });
3
26
  exports.ObfProcessor = void 0;
4
27
  const baseProcessor_1 = require("../core/baseProcessor");
@@ -619,6 +642,75 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
619
642
  await writeBinaryToPath(outputPath, zipData);
620
643
  }
621
644
  }
645
+ /**
646
+ * Save a modified tree while preserving all original files (images, sounds, assets)
647
+ * This method only updates the .obf files for pages in the tree, keeping everything else intact.
648
+ *
649
+ * @param originalPath - Path to the original OBF/OBZ file
650
+ * @param tree - Modified AACTree with pages to save
651
+ * @param outputPath - Path where the modified file should be saved
652
+ */
653
+ async saveModifiedTree(originalPath, tree, outputPath) {
654
+ const { writeBinaryToPath, readBinaryFromInput } = this.options.fileAdapter;
655
+ // If output is .obf (single file), use regular save
656
+ if (outputPath.endsWith('.obf')) {
657
+ await this.saveFromTree(tree, outputPath);
658
+ return;
659
+ }
660
+ if (Object.keys(tree.pages).length === 0) {
661
+ // Empty tree, just copy the original
662
+ const originalBuffer = await readBinaryFromInput(originalPath);
663
+ await writeBinaryToPath(outputPath, originalBuffer);
664
+ return;
665
+ }
666
+ const AdmZip = (await Promise.resolve().then(() => __importStar(require('adm-zip')))).default;
667
+ const originalZip = new AdmZip(originalPath);
668
+ const outputZip = new AdmZip();
669
+ const getPageFilename = (id) => (id.endsWith('.obf') ? id : `${id}.obf`);
670
+ // Track which .obf files we're modifying
671
+ const modifiedObfFiles = new Set();
672
+ // Generate new .obf files for pages in the tree
673
+ const newObfFiles = new Map();
674
+ for (const page of Object.values(tree.pages)) {
675
+ const obfFilename = getPageFilename(page.id);
676
+ modifiedObfFiles.add(obfFilename);
677
+ const obfBoard = this.createObfBoardFromPage(page, 'Board', tree.metadata);
678
+ const obfContent = JSON.stringify(obfBoard, null, 2);
679
+ newObfFiles.set(obfFilename, obfContent);
680
+ }
681
+ // Generate updated manifest if we have pages
682
+ if (Object.keys(tree.pages).length > 0) {
683
+ modifiedObfFiles.add('manifest.json');
684
+ const manifest = {
685
+ format: OBF_FORMAT_VERSION,
686
+ root: tree.metadata.defaultHomePageId,
687
+ paths: {
688
+ boards: Object.fromEntries(Object.entries(tree.pages).map(([id, page]) => [id, getPageFilename(page.id)])),
689
+ images: {},
690
+ sounds: {},
691
+ },
692
+ };
693
+ newObfFiles.set('manifest.json', JSON.stringify(manifest));
694
+ }
695
+ // Copy all files from original zip, replacing modified .obf files
696
+ for (const entry of originalZip.getEntries()) {
697
+ if (entry.isDirectory)
698
+ continue;
699
+ // Skip .obf files that we're modifying
700
+ if (modifiedObfFiles.has(entry.entryName)) {
701
+ const newContent = newObfFiles.get(entry.entryName);
702
+ if (newContent) {
703
+ outputZip.addFile(entry.entryName, Buffer.from(newContent, 'utf8'));
704
+ }
705
+ continue;
706
+ }
707
+ // Copy all other files as-is (preserves images, sounds, etc.)
708
+ outputZip.addFile(entry.entryName, entry.getData());
709
+ }
710
+ // Write the output ZIP
711
+ const outputBuffer = outputZip.toBuffer();
712
+ await writeBinaryToPath(outputPath, outputBuffer);
713
+ }
622
714
  /**
623
715
  * Extract strings with metadata for aac-tools-platform compatibility
624
716
  * Uses the generic implementation from BaseProcessor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@willwade/aac-processors",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
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",