@willwade/aac-processors 0.2.6 → 0.2.8

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.
@@ -1137,9 +1137,11 @@ class GridsetProcessor extends BaseProcessor {
1137
1137
  }
1138
1138
  break;
1139
1139
  case 'Jump.ToKeyboard': {
1140
- // Navigate to the set keyboard if we found one in settings
1140
+ // Prefer explicit keyboard page metadata when available.
1141
+ // Some Gridsets resolve the keyboard page in metadata
1142
+ // without preserving tree.keyboardGridName during parse.
1141
1143
  const keyboardGridName = tree.keyboardGridName;
1142
- const keyboardPageId = gridNameToIdMap.get(keyboardGridName);
1144
+ const keyboardPageId = tree.metadata?.defaultKeyboardPageId || gridNameToIdMap.get(keyboardGridName);
1143
1145
  if (keyboardPageId && !navigationTarget) {
1144
1146
  navigationTarget = keyboardPageId;
1145
1147
  }
@@ -1699,6 +1701,7 @@ class GridsetProcessor extends BaseProcessor {
1699
1701
  settingsData?.gridSetSettings?.keyboardGrid ||
1700
1702
  settingsData?.GridsetSettings?.KeyboardGrid;
1701
1703
  if (keyboardGridName && typeof keyboardGridName === 'string') {
1704
+ tree.keyboardGridName = keyboardGridName;
1702
1705
  metadata.defaultKeyboardPageId = gridNameToIdMap.get(keyboardGridName);
1703
1706
  }
1704
1707
  }
@@ -1708,6 +1711,22 @@ class GridsetProcessor extends BaseProcessor {
1708
1711
  }
1709
1712
  // Set metadata on tree
1710
1713
  tree.metadata = metadata;
1714
+ if (metadata.defaultKeyboardPageId) {
1715
+ Object.values(tree.pages).forEach((page) => {
1716
+ page.buttons.forEach((button) => {
1717
+ if (button?.semanticAction?.platformData?.grid3?.commandId === 'Jump.ToKeyboard' &&
1718
+ !button.targetPageId) {
1719
+ button.targetPageId = metadata.defaultKeyboardPageId;
1720
+ if (button.semanticAction) {
1721
+ button.semanticAction.targetId = metadata.defaultKeyboardPageId;
1722
+ if (button.semanticAction.fallback?.type === 'NAVIGATE') {
1723
+ button.semanticAction.fallback.targetPageId = metadata.defaultKeyboardPageId;
1724
+ }
1725
+ }
1726
+ }
1727
+ });
1728
+ });
1729
+ }
1711
1730
  return tree;
1712
1731
  }
1713
1732
  async processTexts(filePathOrBuffer, translations, outputPath) {
@@ -2204,6 +2223,130 @@ class GridsetProcessor extends BaseProcessor {
2204
2223
  RowDefinition: Array(maxRows).fill({}),
2205
2224
  };
2206
2225
  }
2226
+ /**
2227
+ * Save a modified tree while preserving all original files (settings, images, assets)
2228
+ * This method only updates the grid.xml files for pages in the tree, keeping everything else intact.
2229
+ *
2230
+ * @param originalPath - Path to the original gridset file
2231
+ * @param tree - Modified AACTree with pages to save
2232
+ * @param outputPath - Path where the modified gridset should be saved
2233
+ */
2234
+ async saveModifiedTree(originalPath, tree, outputPath) {
2235
+ const { readBinaryFromInput, writeBinaryToPath } = this.options.fileAdapter;
2236
+ if (Object.keys(tree.pages).length === 0) {
2237
+ // Empty tree, just copy the original
2238
+ const originalBuffer = await readBinaryFromInput(originalPath);
2239
+ await writeBinaryToPath(outputPath, originalBuffer);
2240
+ return;
2241
+ }
2242
+ const AdmZip = (await import('adm-zip')).default;
2243
+ const originalZip = new AdmZip(originalPath);
2244
+ const outputZip = new AdmZip();
2245
+ // Collect styles from the tree for grid.xml files
2246
+ const uniqueStyles = new Map();
2247
+ let styleIdCounter = 1;
2248
+ const addStyle = (style) => {
2249
+ if (!style)
2250
+ return '';
2251
+ const normalizedStyle = { ...style };
2252
+ const styleKey = JSON.stringify(normalizedStyle);
2253
+ const existing = uniqueStyles.get(styleKey);
2254
+ if (existing)
2255
+ return existing.id;
2256
+ const styleId = `Style${styleIdCounter++}`;
2257
+ uniqueStyles.set(styleKey, { id: styleId, style: normalizedStyle });
2258
+ return styleId;
2259
+ };
2260
+ // Collect all styles from pages and buttons
2261
+ Object.values(tree.pages).forEach((page) => {
2262
+ addStyle(page.style);
2263
+ page.buttons.forEach((button) => {
2264
+ addStyle(button.style);
2265
+ });
2266
+ });
2267
+ // Track which grid files we're modifying
2268
+ const modifiedGridFiles = new Set();
2269
+ // Generate grid.xml files for pages in the tree
2270
+ const newGridFiles = new Map();
2271
+ for (const page of Object.values(tree.pages)) {
2272
+ const gridPath = `Grids/${page.name}/grid.xml`;
2273
+ modifiedGridFiles.add(gridPath);
2274
+ // Build the grid XML content
2275
+ const gridData = {
2276
+ Grid: {
2277
+ '@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
2278
+ GridGuid: page.id,
2279
+ ColumnDefinitions: this.calculateColumnDefinitions(page),
2280
+ RowDefinitions: this.calculateRowDefinitions(page, false),
2281
+ AutoContentCommands: '',
2282
+ Cells: page.buttons.length > 0
2283
+ ? {
2284
+ Cell: this.filterPageButtons(page.buttons).map((button, btnIndex) => {
2285
+ const buttonStyleId = button.style ? addStyle(button.style) : '';
2286
+ const position = this.findButtonPosition(page, button, btnIndex);
2287
+ const captionAndImage = {
2288
+ Caption: button.label || '',
2289
+ };
2290
+ // Handle image references
2291
+ if (button.image) {
2292
+ captionAndImage.Image = `${button.image}`;
2293
+ }
2294
+ const cell = {
2295
+ '@_Column': position.x,
2296
+ '@_Row': position.y,
2297
+ captionAndImage,
2298
+ };
2299
+ if (position.columnSpan > 1) {
2300
+ cell['@_ColumnSpan'] = position.columnSpan;
2301
+ }
2302
+ if (position.rowSpan > 1) {
2303
+ cell['@_RowSpan'] = position.rowSpan;
2304
+ }
2305
+ if (buttonStyleId) {
2306
+ cell.CellStyle = buttonStyleId;
2307
+ }
2308
+ if (button.message && button.message !== button.label) {
2309
+ // Use spoken message if different from label
2310
+ const spoken = button.message;
2311
+ const cellContent = {
2312
+ spoken,
2313
+ type: 'text',
2314
+ };
2315
+ cell['ContentCell'] = cellContent;
2316
+ }
2317
+ return cell;
2318
+ }),
2319
+ }
2320
+ : undefined,
2321
+ },
2322
+ };
2323
+ const gridBuilder = new XMLBuilder({
2324
+ ignoreAttributes: false,
2325
+ format: true,
2326
+ indentBy: ' ',
2327
+ suppressEmptyNode: true,
2328
+ });
2329
+ newGridFiles.set(gridPath, gridBuilder.build(gridData));
2330
+ }
2331
+ // Copy all files from original zip, replacing modified grid files
2332
+ for (const entry of originalZip.getEntries()) {
2333
+ if (entry.isDirectory)
2334
+ continue;
2335
+ // Skip grid.xml files that we're modifying
2336
+ if (modifiedGridFiles.has(entry.entryName)) {
2337
+ const newContent = newGridFiles.get(entry.entryName);
2338
+ if (newContent) {
2339
+ outputZip.addFile(entry.entryName, Buffer.from(newContent, 'utf8'));
2340
+ }
2341
+ continue;
2342
+ }
2343
+ // Copy all other files as-is
2344
+ outputZip.addFile(entry.entryName, entry.getData());
2345
+ }
2346
+ // Write the output ZIP
2347
+ const outputBuffer = outputZip.toBuffer();
2348
+ await writeBinaryToPath(outputPath, outputBuffer);
2349
+ }
2207
2350
  // Helper method to find button position with span information
2208
2351
  findButtonPosition(page, button, fallbackIndex) {
2209
2352
  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");
@@ -1140,9 +1163,11 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
1140
1163
  }
1141
1164
  break;
1142
1165
  case 'Jump.ToKeyboard': {
1143
- // Navigate to the set keyboard if we found one in settings
1166
+ // Prefer explicit keyboard page metadata when available.
1167
+ // Some Gridsets resolve the keyboard page in metadata
1168
+ // without preserving tree.keyboardGridName during parse.
1144
1169
  const keyboardGridName = tree.keyboardGridName;
1145
- const keyboardPageId = gridNameToIdMap.get(keyboardGridName);
1170
+ const keyboardPageId = tree.metadata?.defaultKeyboardPageId || gridNameToIdMap.get(keyboardGridName);
1146
1171
  if (keyboardPageId && !navigationTarget) {
1147
1172
  navigationTarget = keyboardPageId;
1148
1173
  }
@@ -1702,6 +1727,7 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
1702
1727
  settingsData?.gridSetSettings?.keyboardGrid ||
1703
1728
  settingsData?.GridsetSettings?.KeyboardGrid;
1704
1729
  if (keyboardGridName && typeof keyboardGridName === 'string') {
1730
+ tree.keyboardGridName = keyboardGridName;
1705
1731
  metadata.defaultKeyboardPageId = gridNameToIdMap.get(keyboardGridName);
1706
1732
  }
1707
1733
  }
@@ -1711,6 +1737,22 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
1711
1737
  }
1712
1738
  // Set metadata on tree
1713
1739
  tree.metadata = metadata;
1740
+ if (metadata.defaultKeyboardPageId) {
1741
+ Object.values(tree.pages).forEach((page) => {
1742
+ page.buttons.forEach((button) => {
1743
+ if (button?.semanticAction?.platformData?.grid3?.commandId === 'Jump.ToKeyboard' &&
1744
+ !button.targetPageId) {
1745
+ button.targetPageId = metadata.defaultKeyboardPageId;
1746
+ if (button.semanticAction) {
1747
+ button.semanticAction.targetId = metadata.defaultKeyboardPageId;
1748
+ if (button.semanticAction.fallback?.type === 'NAVIGATE') {
1749
+ button.semanticAction.fallback.targetPageId = metadata.defaultKeyboardPageId;
1750
+ }
1751
+ }
1752
+ }
1753
+ });
1754
+ });
1755
+ }
1714
1756
  return tree;
1715
1757
  }
1716
1758
  async processTexts(filePathOrBuffer, translations, outputPath) {
@@ -2207,6 +2249,130 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
2207
2249
  RowDefinition: Array(maxRows).fill({}),
2208
2250
  };
2209
2251
  }
2252
+ /**
2253
+ * Save a modified tree while preserving all original files (settings, images, assets)
2254
+ * This method only updates the grid.xml files for pages in the tree, keeping everything else intact.
2255
+ *
2256
+ * @param originalPath - Path to the original gridset file
2257
+ * @param tree - Modified AACTree with pages to save
2258
+ * @param outputPath - Path where the modified gridset should be saved
2259
+ */
2260
+ async saveModifiedTree(originalPath, tree, outputPath) {
2261
+ const { readBinaryFromInput, writeBinaryToPath } = this.options.fileAdapter;
2262
+ if (Object.keys(tree.pages).length === 0) {
2263
+ // Empty tree, just copy the original
2264
+ const originalBuffer = await readBinaryFromInput(originalPath);
2265
+ await writeBinaryToPath(outputPath, originalBuffer);
2266
+ return;
2267
+ }
2268
+ const AdmZip = (await Promise.resolve().then(() => __importStar(require('adm-zip')))).default;
2269
+ const originalZip = new AdmZip(originalPath);
2270
+ const outputZip = new AdmZip();
2271
+ // Collect styles from the tree for grid.xml files
2272
+ const uniqueStyles = new Map();
2273
+ let styleIdCounter = 1;
2274
+ const addStyle = (style) => {
2275
+ if (!style)
2276
+ return '';
2277
+ const normalizedStyle = { ...style };
2278
+ const styleKey = JSON.stringify(normalizedStyle);
2279
+ const existing = uniqueStyles.get(styleKey);
2280
+ if (existing)
2281
+ return existing.id;
2282
+ const styleId = `Style${styleIdCounter++}`;
2283
+ uniqueStyles.set(styleKey, { id: styleId, style: normalizedStyle });
2284
+ return styleId;
2285
+ };
2286
+ // Collect all styles from pages and buttons
2287
+ Object.values(tree.pages).forEach((page) => {
2288
+ addStyle(page.style);
2289
+ page.buttons.forEach((button) => {
2290
+ addStyle(button.style);
2291
+ });
2292
+ });
2293
+ // Track which grid files we're modifying
2294
+ const modifiedGridFiles = new Set();
2295
+ // Generate grid.xml files for pages in the tree
2296
+ const newGridFiles = new Map();
2297
+ for (const page of Object.values(tree.pages)) {
2298
+ const gridPath = `Grids/${page.name}/grid.xml`;
2299
+ modifiedGridFiles.add(gridPath);
2300
+ // Build the grid XML content
2301
+ const gridData = {
2302
+ Grid: {
2303
+ '@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
2304
+ GridGuid: page.id,
2305
+ ColumnDefinitions: this.calculateColumnDefinitions(page),
2306
+ RowDefinitions: this.calculateRowDefinitions(page, false),
2307
+ AutoContentCommands: '',
2308
+ Cells: page.buttons.length > 0
2309
+ ? {
2310
+ Cell: this.filterPageButtons(page.buttons).map((button, btnIndex) => {
2311
+ const buttonStyleId = button.style ? addStyle(button.style) : '';
2312
+ const position = this.findButtonPosition(page, button, btnIndex);
2313
+ const captionAndImage = {
2314
+ Caption: button.label || '',
2315
+ };
2316
+ // Handle image references
2317
+ if (button.image) {
2318
+ captionAndImage.Image = `${button.image}`;
2319
+ }
2320
+ const cell = {
2321
+ '@_Column': position.x,
2322
+ '@_Row': position.y,
2323
+ captionAndImage,
2324
+ };
2325
+ if (position.columnSpan > 1) {
2326
+ cell['@_ColumnSpan'] = position.columnSpan;
2327
+ }
2328
+ if (position.rowSpan > 1) {
2329
+ cell['@_RowSpan'] = position.rowSpan;
2330
+ }
2331
+ if (buttonStyleId) {
2332
+ cell.CellStyle = buttonStyleId;
2333
+ }
2334
+ if (button.message && button.message !== button.label) {
2335
+ // Use spoken message if different from label
2336
+ const spoken = button.message;
2337
+ const cellContent = {
2338
+ spoken,
2339
+ type: 'text',
2340
+ };
2341
+ cell['ContentCell'] = cellContent;
2342
+ }
2343
+ return cell;
2344
+ }),
2345
+ }
2346
+ : undefined,
2347
+ },
2348
+ };
2349
+ const gridBuilder = new fast_xml_parser_1.XMLBuilder({
2350
+ ignoreAttributes: false,
2351
+ format: true,
2352
+ indentBy: ' ',
2353
+ suppressEmptyNode: true,
2354
+ });
2355
+ newGridFiles.set(gridPath, gridBuilder.build(gridData));
2356
+ }
2357
+ // Copy all files from original zip, replacing modified grid files
2358
+ for (const entry of originalZip.getEntries()) {
2359
+ if (entry.isDirectory)
2360
+ continue;
2361
+ // Skip grid.xml files that we're modifying
2362
+ if (modifiedGridFiles.has(entry.entryName)) {
2363
+ const newContent = newGridFiles.get(entry.entryName);
2364
+ if (newContent) {
2365
+ outputZip.addFile(entry.entryName, Buffer.from(newContent, 'utf8'));
2366
+ }
2367
+ continue;
2368
+ }
2369
+ // Copy all other files as-is
2370
+ outputZip.addFile(entry.entryName, entry.getData());
2371
+ }
2372
+ // Write the output ZIP
2373
+ const outputBuffer = outputZip.toBuffer();
2374
+ await writeBinaryToPath(outputPath, outputBuffer);
2375
+ }
2210
2376
  // Helper method to find button position with span information
2211
2377
  findButtonPosition(page, button, fallbackIndex) {
2212
2378
  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.8",
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",