@willwade/aac-processors 0.2.12 → 0.2.13

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.
Files changed (37) hide show
  1. package/dist/browser/core/treeStructure.js +45 -0
  2. package/dist/browser/processors/applePanelsProcessor.js +5 -0
  3. package/dist/browser/processors/astericsGridProcessor.js +5 -0
  4. package/dist/browser/processors/dotProcessor.js +5 -0
  5. package/dist/browser/processors/gridset/saveMutations.js +212 -0
  6. package/dist/browser/processors/gridsetProcessor.js +35 -37
  7. package/dist/browser/processors/obfProcessor.js +51 -1
  8. package/dist/browser/processors/opmlProcessor.js +5 -0
  9. package/dist/browser/processors/snapProcessor.js +5 -0
  10. package/dist/browser/processors/touchchatProcessor.js +5 -0
  11. package/dist/core/baseProcessor.d.ts +2 -0
  12. package/dist/core/treeStructure.d.ts +32 -2
  13. package/dist/core/treeStructure.js +45 -0
  14. package/dist/processors/applePanelsProcessor.d.ts +5 -0
  15. package/dist/processors/applePanelsProcessor.js +5 -0
  16. package/dist/processors/astericsGridProcessor.d.ts +5 -0
  17. package/dist/processors/astericsGridProcessor.js +5 -0
  18. package/dist/processors/dotProcessor.d.ts +5 -0
  19. package/dist/processors/dotProcessor.js +5 -0
  20. package/dist/processors/excelProcessor.d.ts +5 -0
  21. package/dist/processors/excelProcessor.js +8 -0
  22. package/dist/processors/gridset/saveMutations.d.ts +39 -0
  23. package/dist/processors/gridset/saveMutations.js +216 -0
  24. package/dist/processors/gridsetProcessor.d.ts +5 -0
  25. package/dist/processors/gridsetProcessor.js +35 -37
  26. package/dist/processors/obfProcessor.d.ts +14 -0
  27. package/dist/processors/obfProcessor.js +51 -1
  28. package/dist/processors/obfsetProcessor.d.ts +5 -0
  29. package/dist/processors/obfsetProcessor.js +5 -0
  30. package/dist/processors/opmlProcessor.d.ts +5 -0
  31. package/dist/processors/opmlProcessor.js +5 -0
  32. package/dist/processors/snapProcessor.d.ts +5 -0
  33. package/dist/processors/snapProcessor.js +5 -0
  34. package/dist/processors/touchchatProcessor.d.ts +5 -0
  35. package/dist/processors/touchchatProcessor.js +5 -0
  36. package/dist/types/aac.d.ts +54 -0
  37. package/package.json +1 -1
@@ -51,6 +51,11 @@ function normalizeActionParameters(input) {
51
51
  class ApplePanelsProcessor extends baseProcessor_1.BaseProcessor {
52
52
  constructor(options) {
53
53
  super(options);
54
+ this.capabilities = {
55
+ wordList: 'none',
56
+ preservesAssetsOnSave: false,
57
+ newCellCreation: 'allowed',
58
+ };
54
59
  }
55
60
  // Helper function to parse Apple Panels Rect format "{{x, y}, {width, height}}"
56
61
  parseRect(rectString) {
@@ -17,6 +17,11 @@ export declare function calculateLuminance(hexColor: string): number;
17
17
  */
18
18
  export declare function getContrastingTextColor(backgroundColor: string): string;
19
19
  declare class AstericsGridProcessor extends BaseProcessor {
20
+ readonly capabilities: {
21
+ wordList: "none";
22
+ preservesAssetsOnSave: boolean;
23
+ newCellCreation: "allowed";
24
+ };
20
25
  private loadAudio;
21
26
  constructor(options?: ProcessorOptions & {
22
27
  loadAudio?: boolean;
@@ -552,6 +552,11 @@ function mapAstericsVisibility(hidden) {
552
552
  class AstericsGridProcessor extends baseProcessor_1.BaseProcessor {
553
553
  constructor(options = {}) {
554
554
  super(options);
555
+ this.capabilities = {
556
+ wordList: 'none',
557
+ preservesAssetsOnSave: false,
558
+ newCellCreation: 'allowed',
559
+ };
555
560
  this.loadAudio = false;
556
561
  this.loadAudio = options.loadAudio || false;
557
562
  }
@@ -2,6 +2,11 @@ import { BaseProcessor, ProcessorOptions, ExtractStringsResult, TranslatedString
2
2
  import { AACTree } from '../core/treeStructure';
3
3
  import { ProcessorInput } from '../utils/io';
4
4
  declare class DotProcessor extends BaseProcessor {
5
+ readonly capabilities: {
6
+ wordList: "none";
7
+ preservesAssetsOnSave: boolean;
8
+ newCellCreation: "allowed";
9
+ };
5
10
  constructor(options?: ProcessorOptions);
6
11
  private parseDotFile;
7
12
  extractTexts(filePathOrBuffer: ProcessorInput): Promise<string[]>;
@@ -8,6 +8,11 @@ const io_1 = require("../utils/io");
8
8
  class DotProcessor extends baseProcessor_1.BaseProcessor {
9
9
  constructor(options) {
10
10
  super(options);
11
+ this.capabilities = {
12
+ wordList: 'none',
13
+ preservesAssetsOnSave: false,
14
+ newCellCreation: 'allowed',
15
+ };
11
16
  }
12
17
  parseDotFile(content) {
13
18
  const nodes = new Map();
@@ -7,6 +7,11 @@ import { AACTree } from '../core/treeStructure';
7
7
  * Supports visual styling, navigation links, and vocabulary analysis workflows
8
8
  */
9
9
  export declare class ExcelProcessor extends BaseProcessor {
10
+ readonly capabilities: {
11
+ wordList: "none";
12
+ preservesAssetsOnSave: boolean;
13
+ newCellCreation: "allowed";
14
+ };
10
15
  private static readonly NAVIGATION_BUTTONS;
11
16
  /**
12
17
  * Extract all text content from an Excel file
@@ -33,6 +33,14 @@ const treeStructure_1 = require("../core/treeStructure");
33
33
  * Supports visual styling, navigation links, and vocabulary analysis workflows
34
34
  */
35
35
  class ExcelProcessor extends baseProcessor_1.BaseProcessor {
36
+ constructor() {
37
+ super(...arguments);
38
+ this.capabilities = {
39
+ wordList: 'none',
40
+ preservesAssetsOnSave: false,
41
+ newCellCreation: 'allowed',
42
+ };
43
+ }
36
44
  /**
37
45
  * Extract all text content from an Excel file
38
46
  * @param filePathOrBuffer - Path to Excel file or Buffer containing Excel data
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Gridset Save Mutations Module
3
+ *
4
+ * Handles saving AACTree mutations back to Gridset files.
5
+ * This module extracts the save logic from gridsetProcessor for better modularity.
6
+ */
7
+ import { AACTree, AACPage } from '../../core/treeStructure';
8
+ import type { AACButton } from '../../types/aac';
9
+ export declare class GridsetSaveHandler {
10
+ private AdmZip;
11
+ private XMLParser;
12
+ private XMLBuilder;
13
+ constructor();
14
+ /**
15
+ * Show deprecation warning for legacy save path
16
+ */
17
+ static warnLegacySave(): void;
18
+ /**
19
+ * Save using mutation-based logic
20
+ * Fixes bugs A, B, C by processing explicit mutations
21
+ */
22
+ static saveWithMutations(tree: AACTree, originalZip: any, outputZip: any, parser: any, gridBuilder: any, createBasicGridXml: (page: AACPage) => string): void;
23
+ /**
24
+ * Apply button changes to a cell
25
+ */
26
+ static applyButtonToCell(cell: any, button: AACButton, patch?: Partial<AACButton>): void;
27
+ /**
28
+ * Add an item to the WordList with de-duplication (Bug A fix)
29
+ */
30
+ static addWordListItemToGrid(grid: any, item: {
31
+ text: string;
32
+ image?: string;
33
+ partOfSpeech?: string;
34
+ }): void;
35
+ /**
36
+ * Remove items from the WordList
37
+ */
38
+ static removeWordListItemFromGrid(grid: any, match: string | ((item: any) => boolean)): void;
39
+ }
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+ /**
3
+ * Gridset Save Mutations Module
4
+ *
5
+ * Handles saving AACTree mutations back to Gridset files.
6
+ * This module extracts the save logic from gridsetProcessor for better modularity.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.GridsetSaveHandler = void 0;
10
+ const xmlFormatter_1 = require("./xmlFormatter");
11
+ class GridsetSaveHandler {
12
+ constructor() {
13
+ // Dynamic imports for browser compatibility
14
+ }
15
+ /**
16
+ * Show deprecation warning for legacy save path
17
+ */
18
+ static warnLegacySave() {
19
+ const key = 'gridset_legacy_save_warned';
20
+ if (!global[key]) {
21
+ console.warn('saveModifiedTree: detected button changes without recorded mutations. ' +
22
+ 'This will continue to work in 0.x but is deprecated. ' +
23
+ 'Use page.addButton / page.addWordListItem to make changes explicit.');
24
+ global[key] = true;
25
+ }
26
+ }
27
+ /**
28
+ * Save using mutation-based logic
29
+ * Fixes bugs A, B, C by processing explicit mutations
30
+ */
31
+ static saveWithMutations(tree, originalZip, outputZip, parser, gridBuilder, createBasicGridXml) {
32
+ for (const page of Object.values(tree.pages)) {
33
+ // Skip pages with no mutations
34
+ if (page.pendingMutations.length === 0) {
35
+ continue;
36
+ }
37
+ const gridPath = `Grids/${page.name}/grid.xml`;
38
+ // Load or create grid.xml
39
+ const originalEntry = originalZip.getEntry(gridPath);
40
+ let originalGrid;
41
+ if (originalEntry) {
42
+ const originalContent = originalEntry.getData().toString('utf-8');
43
+ originalGrid = parser.parse(originalContent);
44
+ if (!originalGrid.Grid) {
45
+ originalGrid = null;
46
+ }
47
+ }
48
+ if (!originalGrid || !originalGrid.Grid) {
49
+ const basicGrid = createBasicGridXml(page);
50
+ const buffer = Buffer.from(basicGrid, 'utf8');
51
+ outputZip.addFile(gridPath, buffer);
52
+ continue;
53
+ }
54
+ // Index original cells by position
55
+ const cellsByPosition = new Map();
56
+ const cellArray = Array.isArray(originalGrid.Grid.Cells?.Cell)
57
+ ? originalGrid.Grid.Cells.Cell
58
+ : originalGrid.Grid.Cells?.Cell
59
+ ? [originalGrid.Grid.Cells.Cell]
60
+ : [];
61
+ for (const cell of cellArray) {
62
+ const x = cell['@_X'] !== undefined ? parseInt(String(cell['@_X']), 10) : undefined;
63
+ const y = parseInt(String(cell['@_Y'] || cell['@_Row'] || '0'), 10);
64
+ if (x !== undefined) {
65
+ cellsByPosition.set(`${x},${y}`, cell);
66
+ }
67
+ }
68
+ // Process mutations in order
69
+ for (const mutation of page.pendingMutations) {
70
+ switch (mutation.type) {
71
+ case 'addButton': {
72
+ const button = mutation.button;
73
+ const x = button.x ?? 0;
74
+ const y = button.y ?? 0;
75
+ const cell = cellsByPosition.get(`${x},${y}`);
76
+ if (cell && cell.Content) {
77
+ GridsetSaveHandler.applyButtonToCell(cell, button);
78
+ }
79
+ else {
80
+ // Bug C fix: warn instead of silently dropping
81
+ console.warn(`[Gridset] Cannot add button at (${x},${y}) - cell does not exist. ` +
82
+ `Use addWordListItem for dynamic content.`);
83
+ }
84
+ break;
85
+ }
86
+ case 'removeButton': {
87
+ const button = page.buttons.find((b) => b.id === mutation.buttonId);
88
+ if (button) {
89
+ const x = button.x ?? 0;
90
+ const y = button.y ?? 0;
91
+ const cell = cellsByPosition.get(`${x},${y}`);
92
+ if (cell && cell.Content) {
93
+ cell.Content.Visibility = 'Hidden';
94
+ }
95
+ }
96
+ break;
97
+ }
98
+ case 'updateButton': {
99
+ const button = page.buttons.find((b) => b.id === mutation.buttonId);
100
+ if (button) {
101
+ const x = button.x ?? 0;
102
+ const y = button.y ?? 0;
103
+ const cell = cellsByPosition.get(`${x},${y}`);
104
+ if (cell && cell.Content) {
105
+ GridsetSaveHandler.applyButtonToCell(cell, button, mutation.patch);
106
+ }
107
+ }
108
+ break;
109
+ }
110
+ case 'addWordListItem': {
111
+ GridsetSaveHandler.addWordListItemToGrid(originalGrid.Grid, mutation.item);
112
+ break;
113
+ }
114
+ case 'removeWordListItem': {
115
+ GridsetSaveHandler.removeWordListItemFromGrid(originalGrid.Grid, mutation.match);
116
+ break;
117
+ }
118
+ case 'clearWordList': {
119
+ if (originalGrid.Grid.WordList && originalGrid.Grid.WordList.Items) {
120
+ originalGrid.Grid.WordList.Items.WordListItem = [];
121
+ }
122
+ break;
123
+ }
124
+ }
125
+ }
126
+ // Build and write the updated grid XML
127
+ let builtXml = gridBuilder.build(originalGrid);
128
+ builtXml = (0, xmlFormatter_1.formatGrid3XmlComplete)(builtXml);
129
+ outputZip.addFile(gridPath, Buffer.from(builtXml, 'utf8'));
130
+ }
131
+ }
132
+ /**
133
+ * Apply button changes to a cell
134
+ */
135
+ static applyButtonToCell(cell, button, patch) {
136
+ const updates = patch ? { ...button, ...patch } : button;
137
+ const isPlaceholderLabel = !updates.label ||
138
+ updates.label.startsWith('Cell_') ||
139
+ updates.label.startsWith('AutoContent_') ||
140
+ updates.label.startsWith('Prediction ');
141
+ if (cell.Content.CaptionAndImage || cell.Content.captionAndImage) {
142
+ const captionAndImage = cell.Content.CaptionAndImage || cell.Content.captionAndImage;
143
+ if (!isPlaceholderLabel && updates.label) {
144
+ captionAndImage.Caption = updates.label;
145
+ if (captionAndImage['@_xsi:nil'] || captionAndImage['xsi:nil']) {
146
+ delete captionAndImage['@_xsi:nil'];
147
+ delete captionAndImage['xsi:nil'];
148
+ }
149
+ }
150
+ if (updates.image) {
151
+ captionAndImage.Image = updates.image;
152
+ }
153
+ }
154
+ const isPlaceholderMessage = !updates.message ||
155
+ updates.message.startsWith('Cell_') ||
156
+ updates.message.startsWith('AutoContent_') ||
157
+ updates.message.startsWith('Prediction ');
158
+ if (!isPlaceholderMessage &&
159
+ updates.message &&
160
+ updates.message !== updates.label &&
161
+ !cell.Content.Commands) {
162
+ cell.Content['#text'] = updates.message;
163
+ }
164
+ }
165
+ /**
166
+ * Add an item to the WordList with de-duplication (Bug A fix)
167
+ */
168
+ static addWordListItemToGrid(grid, item) {
169
+ if (!grid.WordList) {
170
+ grid.WordList = {};
171
+ }
172
+ if (!grid.WordList.Items) {
173
+ grid.WordList.Items = {};
174
+ }
175
+ const existingItems = grid.WordList.Items.WordListItem || grid.WordList.Items.wordlistitem || [];
176
+ const itemsArray = Array.isArray(existingItems) ? existingItems : [existingItems];
177
+ // De-duplicate by text
178
+ const existingTexts = new Set(itemsArray
179
+ .map((item) => {
180
+ if (typeof item.Text === 'string')
181
+ return item.Text;
182
+ return item.Text?.p?.s?.r || '';
183
+ })
184
+ .filter(Boolean));
185
+ if (!existingTexts.has(item.text)) {
186
+ itemsArray.push({
187
+ Text: { p: { s: { r: item.text } } },
188
+ Image: item.image || '',
189
+ PartOfSpeech: item.partOfSpeech || 'Unknown',
190
+ });
191
+ grid.WordList.Items.WordListItem = itemsArray;
192
+ }
193
+ }
194
+ /**
195
+ * Remove items from the WordList
196
+ */
197
+ static removeWordListItemFromGrid(grid, match) {
198
+ if (!grid.WordList || !grid.WordList.Items) {
199
+ return;
200
+ }
201
+ const existingItems = grid.WordList.Items.WordListItem || grid.WordList.Items.wordlistitem || [];
202
+ const itemsArray = Array.isArray(existingItems) ? existingItems : [existingItems];
203
+ let filteredItems;
204
+ if (typeof match === 'string') {
205
+ filteredItems = itemsArray.filter((item) => {
206
+ const text = item.Text?.p?.s?.r || item.Text || '';
207
+ return text !== match;
208
+ });
209
+ }
210
+ else {
211
+ filteredItems = itemsArray.filter(match);
212
+ }
213
+ grid.WordList.Items.WordListItem = filteredItems;
214
+ }
215
+ }
216
+ exports.GridsetSaveHandler = GridsetSaveHandler;
@@ -4,6 +4,11 @@ import { type ButtonForTranslation, type LLMLTranslationResult } from '../utilit
4
4
  import { ValidationResult } from '../validation/validationTypes';
5
5
  import { ProcessorInput } from '../utils/io';
6
6
  declare class GridsetProcessor extends BaseProcessor {
7
+ readonly capabilities: {
8
+ wordList: "native";
9
+ preservesAssetsOnSave: boolean;
10
+ newCellCreation: "restricted";
11
+ };
7
12
  constructor(options?: ProcessorOptions);
8
13
  private getGridsetPassword;
9
14
  private ensureAlphaChannel;
@@ -32,6 +32,7 @@ const translationProcessor_1 = require("../utilities/translation/translationProc
32
32
  const password_1 = require("./gridset/password");
33
33
  const crypto_1 = require("./gridset/crypto");
34
34
  const xmlFormatter_1 = require("./gridset/xmlFormatter");
35
+ const saveMutations_1 = require("./gridset/saveMutations");
35
36
  const gridCalculations_1 = require("./gridset/gridCalculations");
36
37
  const cellHelpers_1 = require("./gridset/cellHelpers");
37
38
  const gridsetValidator_1 = require("../validation/gridsetValidator");
@@ -46,6 +47,11 @@ const io_1 = require("../utils/io");
46
47
  class GridsetProcessor extends baseProcessor_1.BaseProcessor {
47
48
  constructor(options) {
48
49
  super(options);
50
+ this.capabilities = {
51
+ wordList: 'native',
52
+ preservesAssetsOnSave: true,
53
+ newCellCreation: 'restricted',
54
+ };
49
55
  }
50
56
  // Determine password to use when opening encrypted gridset archives (.gridsetx)
51
57
  getGridsetPassword(source) {
@@ -2250,6 +2256,35 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
2250
2256
  const AdmZip = (await Promise.resolve().then(() => __importStar(require('adm-zip')))).default;
2251
2257
  const originalZip = new AdmZip(originalPath);
2252
2258
  const outputZip = new AdmZip();
2259
+ // Check if any page has pending mutations
2260
+ const hasPendingMutations = Object.values(tree.pages).some((page) => page.pendingMutations.length > 0);
2261
+ if (hasPendingMutations) {
2262
+ // NEW: Use mutation-based save path
2263
+ const parser = new fast_xml_parser_1.XMLParser({
2264
+ ignoreAttributes: false,
2265
+ attributeNamePrefix: '@_',
2266
+ });
2267
+ const gridBuilder = new fast_xml_parser_1.XMLBuilder({
2268
+ ignoreAttributes: false,
2269
+ format: true,
2270
+ indentBy: ' ',
2271
+ suppressEmptyNode: true,
2272
+ suppressBooleanAttributes: false,
2273
+ });
2274
+ saveMutations_1.GridsetSaveHandler.saveWithMutations(tree, originalZip, outputZip, parser, gridBuilder, (page) => this.createBasicGridXml(page));
2275
+ // Copy remaining files
2276
+ for (const entry of originalZip.getEntries()) {
2277
+ if (entry.isDirectory)
2278
+ continue;
2279
+ if (!outputZip.getEntry(entry.entryName)) {
2280
+ outputZip.addFile(entry.entryName, entry.getData());
2281
+ }
2282
+ }
2283
+ const outputBuffer = outputZip.toBuffer();
2284
+ await writeBinaryToPath(outputPath, outputBuffer);
2285
+ return;
2286
+ }
2287
+ // LEGACY: Original position-based logic continues below...
2253
2288
  // Create a map of pages by name for easy lookup
2254
2289
  const pagesByName = new Map();
2255
2290
  for (const page of Object.values(tree.pages)) {
@@ -2443,43 +2478,6 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
2443
2478
  originalGrid.Grid.WordList.Items.WordListItem = allItems;
2444
2479
  }
2445
2480
  }
2446
- // Process WordList items attached to the page (from personalisation)
2447
- // These are tracked separately and shouldn't create new cells
2448
- // Use a known symbol key to check for WordList items
2449
- const WORDLIST_ITEMS_KEY = 'wordListItems';
2450
- const wordListItems = page[WORDLIST_ITEMS_KEY];
2451
- if (wordListItems && wordListItems.length > 0) {
2452
- // Ensure WordList structure exists
2453
- if (!originalGrid.Grid) {
2454
- originalGrid.Grid = {};
2455
- }
2456
- if (!originalGrid.Grid.WordList) {
2457
- originalGrid.Grid.WordList = {};
2458
- }
2459
- if (!originalGrid.Grid.WordList.Items) {
2460
- originalGrid.Grid.WordList.Items = {};
2461
- }
2462
- const existingItems = originalGrid.Grid.WordList.Items.WordListItem ||
2463
- originalGrid.Grid.WordList.Items.wordlistitem ||
2464
- [];
2465
- const itemsArray = Array.isArray(existingItems) ? existingItems : [existingItems];
2466
- // Add new WordList items with proper Grid 3 format
2467
- for (const item of wordListItems) {
2468
- itemsArray.push({
2469
- Text: {
2470
- p: {
2471
- s: {
2472
- r: item.label,
2473
- },
2474
- },
2475
- },
2476
- Image: '',
2477
- PartOfSpeech: 'Unknown',
2478
- });
2479
- }
2480
- // Update the WordList
2481
- originalGrid.Grid.WordList.Items.WordListItem = itemsArray;
2482
- }
2483
2481
  // Build the updated grid XML and format for Grid 3 compatibility
2484
2482
  let builtXml = gridBuilder.build(originalGrid);
2485
2483
  builtXml = (0, xmlFormatter_1.formatGrid3XmlComplete)(builtXml);
@@ -4,6 +4,11 @@ import { ValidationResult } from '../validation/validationTypes';
4
4
  import { type ButtonForTranslation, type LLMLTranslationResult } from '../utilities/translation/translationProcessor';
5
5
  import { ProcessorInput } from '../utils/io';
6
6
  declare class ObfProcessor extends BaseProcessor {
7
+ readonly capabilities: {
8
+ wordList: "none";
9
+ preservesAssetsOnSave: boolean;
10
+ newCellCreation: "allowed";
11
+ };
7
12
  private zipFile?;
8
13
  private imageCache;
9
14
  constructor(options?: ProcessorOptions);
@@ -21,6 +26,15 @@ declare class ObfProcessor extends BaseProcessor {
21
26
  extractTexts(filePathOrBuffer: ProcessorInput): Promise<string[]>;
22
27
  loadIntoTree(filePathOrBuffer: ProcessorInput): Promise<AACTree>;
23
28
  private buildGridMetadata;
29
+ /**
30
+ * Apply mutations to a buttons array for OBF export
31
+ * Returns a modified copy of the buttons array with mutations applied
32
+ *
33
+ * Note: addButton mutations are NOT applied because the button is already
34
+ * in the buttons array (added by page.addButton()). We only apply
35
+ * removeButton and updateButton mutations.
36
+ */
37
+ private applyMutationsToButtons;
24
38
  private createObfBoardFromPage;
25
39
  processTexts(filePathOrBuffer: ProcessorInput, translations: Map<string, string>, outputPath: string): Promise<Uint8Array>;
26
40
  saveFromTree(tree: AACTree, outputPath: string, embedData?: boolean): Promise<void>;
@@ -44,6 +44,11 @@ function mapObfVisibility(hidden) {
44
44
  class ObfProcessor extends baseProcessor_1.BaseProcessor {
45
45
  constructor(options) {
46
46
  super(options);
47
+ this.capabilities = {
48
+ wordList: 'none',
49
+ preservesAssetsOnSave: true,
50
+ newCellCreation: 'allowed',
51
+ };
47
52
  this.imageCache = new Map(); // Cache for data URLs
48
53
  }
49
54
  /**
@@ -557,6 +562,46 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
557
562
  }
558
563
  return { rows: totalRows, columns: totalColumns, order, buttonPositions };
559
564
  }
565
+ /**
566
+ * Apply mutations to a buttons array for OBF export
567
+ * Returns a modified copy of the buttons array with mutations applied
568
+ *
569
+ * Note: addButton mutations are NOT applied because the button is already
570
+ * in the buttons array (added by page.addButton()). We only apply
571
+ * removeButton and updateButton mutations.
572
+ */
573
+ applyMutationsToButtons(buttons, mutations) {
574
+ let modifiedButtons = [...buttons];
575
+ for (const mutation of mutations) {
576
+ switch (mutation.type) {
577
+ case 'addButton':
578
+ // Skip - button is already in the array from page.addButton()
579
+ break;
580
+ case 'removeButton': {
581
+ modifiedButtons = modifiedButtons.filter((b) => b.id !== mutation.buttonId);
582
+ break;
583
+ }
584
+ case 'updateButton': {
585
+ modifiedButtons = modifiedButtons.map((b) => {
586
+ if (b.id === mutation.buttonId) {
587
+ // Create a new AACButton instance with the patched properties
588
+ const patched = Object.create(Object.getPrototypeOf(b));
589
+ Object.assign(patched, b, mutation.patch);
590
+ return patched;
591
+ }
592
+ return b;
593
+ });
594
+ break;
595
+ }
596
+ case 'addWordListItem':
597
+ case 'removeWordListItem':
598
+ case 'clearWordList':
599
+ // OBF doesn't have WordList - skip
600
+ break;
601
+ }
602
+ }
603
+ return modifiedButtons;
604
+ }
560
605
  createObfBoardFromPage(page, fallbackName, metadata, embedData = false) {
561
606
  const { rows, columns, order, buttonPositions } = this.buildGridMetadata(page);
562
607
  const boardName = metadata?.name && page.id === metadata?.defaultHomePageId
@@ -569,6 +614,10 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
569
614
  return image;
570
615
  });
571
616
  }
617
+ // Apply mutations if present
618
+ const buttons = page.pendingMutations.length > 0
619
+ ? this.applyMutationsToButtons(page.buttons, page.pendingMutations)
620
+ : page.buttons;
572
621
  return {
573
622
  format: OBF_FORMAT_VERSION,
574
623
  id: page.id,
@@ -583,7 +632,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
583
632
  columns,
584
633
  order,
585
634
  },
586
- buttons: page.buttons.map((button) => {
635
+ buttons: buttons.map((button) => {
587
636
  const extraButtonInfo = button;
588
637
  const imageId = button.parameters?.image_id ||
589
638
  button.parameters?.imageId ||
@@ -728,6 +777,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
728
777
  for (const page of Object.values(tree.pages)) {
729
778
  const obfFilename = this.getPageFilename(page.id, tree.metadata);
730
779
  modifiedObfFiles.add(obfFilename);
780
+ // createObfBoardFromPage will automatically apply mutations if present
731
781
  const obfBoard = this.createObfBoardFromPage(page, 'Board', tree.metadata);
732
782
  const obfContent = JSON.stringify(obfBoard, null, 2);
733
783
  newObfFiles.set(obfFilename, obfContent);
@@ -6,6 +6,11 @@ import { AACTree } from '../core/treeStructure';
6
6
  import { BaseProcessor, ProcessorOptions } from '../core/baseProcessor';
7
7
  import { ProcessorInput } from '../utils/io';
8
8
  export declare class ObfsetProcessor extends BaseProcessor {
9
+ readonly capabilities: {
10
+ wordList: "none";
11
+ preservesAssetsOnSave: boolean;
12
+ newCellCreation: "allowed";
13
+ };
9
14
  constructor(options?: ProcessorOptions);
10
15
  /**
11
16
  * Extract all text content
@@ -11,6 +11,11 @@ const baseProcessor_1 = require("../core/baseProcessor");
11
11
  class ObfsetProcessor extends baseProcessor_1.BaseProcessor {
12
12
  constructor(options = {}) {
13
13
  super(options);
14
+ this.capabilities = {
15
+ wordList: 'none',
16
+ preservesAssetsOnSave: false,
17
+ newCellCreation: 'allowed',
18
+ };
14
19
  }
15
20
  /**
16
21
  * Extract all text content
@@ -2,6 +2,11 @@ import { BaseProcessor, ProcessorOptions, ExtractStringsResult, TranslatedString
2
2
  import { AACTree } from '../core/treeStructure';
3
3
  import { ProcessorInput } from '../utils/io';
4
4
  declare class OpmlProcessor extends BaseProcessor {
5
+ readonly capabilities: {
6
+ wordList: "none";
7
+ preservesAssetsOnSave: boolean;
8
+ newCellCreation: "allowed";
9
+ };
5
10
  constructor(options?: ProcessorOptions);
6
11
  private processOutline;
7
12
  extractTexts(filePathOrBuffer: ProcessorInput): Promise<string[]>;
@@ -9,6 +9,11 @@ const io_1 = require("../utils/io");
9
9
  class OpmlProcessor extends baseProcessor_1.BaseProcessor {
10
10
  constructor(options) {
11
11
  super(options);
12
+ this.capabilities = {
13
+ wordList: 'none',
14
+ preservesAssetsOnSave: false,
15
+ newCellCreation: 'allowed',
16
+ };
12
17
  }
13
18
  processOutline(outline, parentId = null) {
14
19
  if (!outline || typeof outline !== 'object') {
@@ -3,6 +3,11 @@ import { AACTree } from '../core/treeStructure';
3
3
  import { ValidationResult } from '../validation/validationTypes';
4
4
  import { ProcessorInput } from '../utils/io';
5
5
  declare class SnapProcessor extends BaseProcessor {
6
+ readonly capabilities: {
7
+ wordList: "none";
8
+ preservesAssetsOnSave: boolean;
9
+ newCellCreation: "allowed";
10
+ };
6
11
  private symbolResolver;
7
12
  private loadAudio;
8
13
  private pageLayoutPreference;
@@ -40,6 +40,11 @@ function mapSnapVisibility(visible) {
40
40
  class SnapProcessor extends baseProcessor_1.BaseProcessor {
41
41
  constructor(symbolResolver = null, options) {
42
42
  super(options);
43
+ this.capabilities = {
44
+ wordList: 'none',
45
+ preservesAssetsOnSave: false,
46
+ newCellCreation: 'allowed',
47
+ };
43
48
  this.symbolResolver = null;
44
49
  this.loadAudio = false;
45
50
  this.pageLayoutPreference = 'scanning'; // Default to scanning for metrics
@@ -4,6 +4,11 @@ import { ValidationResult } from '../validation/validationTypes';
4
4
  import { ProcessorInput } from '../utils/io';
5
5
  import { type ButtonForTranslation, type LLMLTranslationResult } from '../utilities/translation/translationProcessor';
6
6
  declare class TouchChatProcessor extends BaseProcessor {
7
+ readonly capabilities: {
8
+ wordList: "none";
9
+ preservesAssetsOnSave: boolean;
10
+ newCellCreation: "allowed";
11
+ };
7
12
  private tree;
8
13
  private sourceFile;
9
14
  constructor(options?: ProcessorOptions);