@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
@@ -168,6 +168,8 @@ export class AACButton {
168
168
  }
169
169
  export class AACPage {
170
170
  constructor({ id, name = '', grid = [], buttons = [], parentId = null, style, locale, descriptionHtml, images, sounds, semantic_ids, clone_ids, scanningConfig, scanBlocksConfig, scanType, }) {
171
+ // Mutation tracking
172
+ this._pendingMutations = [];
171
173
  this.id = id;
172
174
  this.name = name;
173
175
  if (Array.isArray(grid)) {
@@ -196,6 +198,49 @@ export class AACPage {
196
198
  }
197
199
  addButton(button) {
198
200
  this.buttons.push(button);
201
+ // Record the mutation
202
+ this._pendingMutations.push({ type: 'addButton', button });
203
+ }
204
+ /**
205
+ * Get the list of pending mutations for this page (read-only)
206
+ */
207
+ get pendingMutations() {
208
+ return Object.freeze([...this._pendingMutations]);
209
+ }
210
+ /**
211
+ * Remove a button by ID
212
+ * @param buttonId - The ID of the button to remove
213
+ */
214
+ removeButton(buttonId) {
215
+ this._pendingMutations.push({ type: 'removeButton', buttonId });
216
+ }
217
+ /**
218
+ * Update a button by merging a patch
219
+ * @param buttonId - The ID of the button to update
220
+ * @param patch - Partial button object with fields to update
221
+ */
222
+ updateButton(buttonId, patch) {
223
+ this._pendingMutations.push({ type: 'updateButton', buttonId, patch });
224
+ }
225
+ /**
226
+ * Add an item to the page's WordList (for formats with dynamic content cells)
227
+ * @param item - WordList item to add
228
+ */
229
+ addWordListItem(item) {
230
+ this._pendingMutations.push({ type: 'addWordListItem', item });
231
+ }
232
+ /**
233
+ * Remove items from the page's WordList
234
+ * @param textOrPredicate - Text to match or predicate function to filter items
235
+ */
236
+ removeWordListItem(textOrPredicate) {
237
+ this._pendingMutations.push({ type: 'removeWordListItem', match: textOrPredicate });
238
+ }
239
+ /**
240
+ * Clear all items from the page's WordList
241
+ */
242
+ clearWordList() {
243
+ this._pendingMutations.push({ type: 'clearWordList' });
199
244
  }
200
245
  }
201
246
  export class AACTree {
@@ -45,6 +45,11 @@ function normalizeActionParameters(input) {
45
45
  class ApplePanelsProcessor extends BaseProcessor {
46
46
  constructor(options) {
47
47
  super(options);
48
+ this.capabilities = {
49
+ wordList: 'none',
50
+ preservesAssetsOnSave: false,
51
+ newCellCreation: 'allowed',
52
+ };
48
53
  }
49
54
  // Helper function to parse Apple Panels Rect format "{{x, y}, {width, height}}"
50
55
  parseRect(rectString) {
@@ -544,6 +544,11 @@ function mapAstericsVisibility(hidden) {
544
544
  class AstericsGridProcessor extends BaseProcessor {
545
545
  constructor(options = {}) {
546
546
  super(options);
547
+ this.capabilities = {
548
+ wordList: 'none',
549
+ preservesAssetsOnSave: false,
550
+ newCellCreation: 'allowed',
551
+ };
547
552
  this.loadAudio = false;
548
553
  this.loadAudio = options.loadAudio || false;
549
554
  }
@@ -5,6 +5,11 @@ import { getBasename, encodeText } from '../utils/io';
5
5
  class DotProcessor extends BaseProcessor {
6
6
  constructor(options) {
7
7
  super(options);
8
+ this.capabilities = {
9
+ wordList: 'none',
10
+ preservesAssetsOnSave: false,
11
+ newCellCreation: 'allowed',
12
+ };
8
13
  }
9
14
  parseDotFile(content) {
10
15
  const nodes = new Map();
@@ -0,0 +1,212 @@
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 { formatGrid3XmlComplete } from './xmlFormatter';
8
+ export class GridsetSaveHandler {
9
+ constructor() {
10
+ // Dynamic imports for browser compatibility
11
+ }
12
+ /**
13
+ * Show deprecation warning for legacy save path
14
+ */
15
+ static warnLegacySave() {
16
+ const key = 'gridset_legacy_save_warned';
17
+ if (!global[key]) {
18
+ console.warn('saveModifiedTree: detected button changes without recorded mutations. ' +
19
+ 'This will continue to work in 0.x but is deprecated. ' +
20
+ 'Use page.addButton / page.addWordListItem to make changes explicit.');
21
+ global[key] = true;
22
+ }
23
+ }
24
+ /**
25
+ * Save using mutation-based logic
26
+ * Fixes bugs A, B, C by processing explicit mutations
27
+ */
28
+ static saveWithMutations(tree, originalZip, outputZip, parser, gridBuilder, createBasicGridXml) {
29
+ for (const page of Object.values(tree.pages)) {
30
+ // Skip pages with no mutations
31
+ if (page.pendingMutations.length === 0) {
32
+ continue;
33
+ }
34
+ const gridPath = `Grids/${page.name}/grid.xml`;
35
+ // Load or create grid.xml
36
+ const originalEntry = originalZip.getEntry(gridPath);
37
+ let originalGrid;
38
+ if (originalEntry) {
39
+ const originalContent = originalEntry.getData().toString('utf-8');
40
+ originalGrid = parser.parse(originalContent);
41
+ if (!originalGrid.Grid) {
42
+ originalGrid = null;
43
+ }
44
+ }
45
+ if (!originalGrid || !originalGrid.Grid) {
46
+ const basicGrid = createBasicGridXml(page);
47
+ const buffer = Buffer.from(basicGrid, 'utf8');
48
+ outputZip.addFile(gridPath, buffer);
49
+ continue;
50
+ }
51
+ // Index original cells by position
52
+ const cellsByPosition = new Map();
53
+ const cellArray = Array.isArray(originalGrid.Grid.Cells?.Cell)
54
+ ? originalGrid.Grid.Cells.Cell
55
+ : originalGrid.Grid.Cells?.Cell
56
+ ? [originalGrid.Grid.Cells.Cell]
57
+ : [];
58
+ for (const cell of cellArray) {
59
+ const x = cell['@_X'] !== undefined ? parseInt(String(cell['@_X']), 10) : undefined;
60
+ const y = parseInt(String(cell['@_Y'] || cell['@_Row'] || '0'), 10);
61
+ if (x !== undefined) {
62
+ cellsByPosition.set(`${x},${y}`, cell);
63
+ }
64
+ }
65
+ // Process mutations in order
66
+ for (const mutation of page.pendingMutations) {
67
+ switch (mutation.type) {
68
+ case 'addButton': {
69
+ const button = mutation.button;
70
+ const x = button.x ?? 0;
71
+ const y = button.y ?? 0;
72
+ const cell = cellsByPosition.get(`${x},${y}`);
73
+ if (cell && cell.Content) {
74
+ GridsetSaveHandler.applyButtonToCell(cell, button);
75
+ }
76
+ else {
77
+ // Bug C fix: warn instead of silently dropping
78
+ console.warn(`[Gridset] Cannot add button at (${x},${y}) - cell does not exist. ` +
79
+ `Use addWordListItem for dynamic content.`);
80
+ }
81
+ break;
82
+ }
83
+ case 'removeButton': {
84
+ const button = page.buttons.find((b) => b.id === mutation.buttonId);
85
+ if (button) {
86
+ const x = button.x ?? 0;
87
+ const y = button.y ?? 0;
88
+ const cell = cellsByPosition.get(`${x},${y}`);
89
+ if (cell && cell.Content) {
90
+ cell.Content.Visibility = 'Hidden';
91
+ }
92
+ }
93
+ break;
94
+ }
95
+ case 'updateButton': {
96
+ const button = page.buttons.find((b) => b.id === mutation.buttonId);
97
+ if (button) {
98
+ const x = button.x ?? 0;
99
+ const y = button.y ?? 0;
100
+ const cell = cellsByPosition.get(`${x},${y}`);
101
+ if (cell && cell.Content) {
102
+ GridsetSaveHandler.applyButtonToCell(cell, button, mutation.patch);
103
+ }
104
+ }
105
+ break;
106
+ }
107
+ case 'addWordListItem': {
108
+ GridsetSaveHandler.addWordListItemToGrid(originalGrid.Grid, mutation.item);
109
+ break;
110
+ }
111
+ case 'removeWordListItem': {
112
+ GridsetSaveHandler.removeWordListItemFromGrid(originalGrid.Grid, mutation.match);
113
+ break;
114
+ }
115
+ case 'clearWordList': {
116
+ if (originalGrid.Grid.WordList && originalGrid.Grid.WordList.Items) {
117
+ originalGrid.Grid.WordList.Items.WordListItem = [];
118
+ }
119
+ break;
120
+ }
121
+ }
122
+ }
123
+ // Build and write the updated grid XML
124
+ let builtXml = gridBuilder.build(originalGrid);
125
+ builtXml = formatGrid3XmlComplete(builtXml);
126
+ outputZip.addFile(gridPath, Buffer.from(builtXml, 'utf8'));
127
+ }
128
+ }
129
+ /**
130
+ * Apply button changes to a cell
131
+ */
132
+ static applyButtonToCell(cell, button, patch) {
133
+ const updates = patch ? { ...button, ...patch } : button;
134
+ const isPlaceholderLabel = !updates.label ||
135
+ updates.label.startsWith('Cell_') ||
136
+ updates.label.startsWith('AutoContent_') ||
137
+ updates.label.startsWith('Prediction ');
138
+ if (cell.Content.CaptionAndImage || cell.Content.captionAndImage) {
139
+ const captionAndImage = cell.Content.CaptionAndImage || cell.Content.captionAndImage;
140
+ if (!isPlaceholderLabel && updates.label) {
141
+ captionAndImage.Caption = updates.label;
142
+ if (captionAndImage['@_xsi:nil'] || captionAndImage['xsi:nil']) {
143
+ delete captionAndImage['@_xsi:nil'];
144
+ delete captionAndImage['xsi:nil'];
145
+ }
146
+ }
147
+ if (updates.image) {
148
+ captionAndImage.Image = updates.image;
149
+ }
150
+ }
151
+ const isPlaceholderMessage = !updates.message ||
152
+ updates.message.startsWith('Cell_') ||
153
+ updates.message.startsWith('AutoContent_') ||
154
+ updates.message.startsWith('Prediction ');
155
+ if (!isPlaceholderMessage &&
156
+ updates.message &&
157
+ updates.message !== updates.label &&
158
+ !cell.Content.Commands) {
159
+ cell.Content['#text'] = updates.message;
160
+ }
161
+ }
162
+ /**
163
+ * Add an item to the WordList with de-duplication (Bug A fix)
164
+ */
165
+ static addWordListItemToGrid(grid, item) {
166
+ if (!grid.WordList) {
167
+ grid.WordList = {};
168
+ }
169
+ if (!grid.WordList.Items) {
170
+ grid.WordList.Items = {};
171
+ }
172
+ const existingItems = grid.WordList.Items.WordListItem || grid.WordList.Items.wordlistitem || [];
173
+ const itemsArray = Array.isArray(existingItems) ? existingItems : [existingItems];
174
+ // De-duplicate by text
175
+ const existingTexts = new Set(itemsArray
176
+ .map((item) => {
177
+ if (typeof item.Text === 'string')
178
+ return item.Text;
179
+ return item.Text?.p?.s?.r || '';
180
+ })
181
+ .filter(Boolean));
182
+ if (!existingTexts.has(item.text)) {
183
+ itemsArray.push({
184
+ Text: { p: { s: { r: item.text } } },
185
+ Image: item.image || '',
186
+ PartOfSpeech: item.partOfSpeech || 'Unknown',
187
+ });
188
+ grid.WordList.Items.WordListItem = itemsArray;
189
+ }
190
+ }
191
+ /**
192
+ * Remove items from the WordList
193
+ */
194
+ static removeWordListItemFromGrid(grid, match) {
195
+ if (!grid.WordList || !grid.WordList.Items) {
196
+ return;
197
+ }
198
+ const existingItems = grid.WordList.Items.WordListItem || grid.WordList.Items.wordlistitem || [];
199
+ const itemsArray = Array.isArray(existingItems) ? existingItems : [existingItems];
200
+ let filteredItems;
201
+ if (typeof match === 'string') {
202
+ filteredItems = itemsArray.filter((item) => {
203
+ const text = item.Text?.p?.s?.r || item.Text || '';
204
+ return text !== match;
205
+ });
206
+ }
207
+ else {
208
+ filteredItems = itemsArray.filter(match);
209
+ }
210
+ grid.WordList.Items.WordListItem = filteredItems;
211
+ }
212
+ }
@@ -6,6 +6,7 @@ import { extractAllButtonsForTranslation, validateTranslationResults, } from '..
6
6
  import { getZipEntriesFromAdapter, resolveGridsetPassword, } from './gridset/password';
7
7
  import { decryptGridsetEntry } from './gridset/crypto';
8
8
  import { formatGrid3XmlComplete } from './gridset/xmlFormatter';
9
+ import { GridsetSaveHandler } from './gridset/saveMutations';
9
10
  import { calculateColumnDefinitions as calcColumnDefs, calculateRowDefinitions as calcRowDefs, } from './gridset/gridCalculations';
10
11
  import { findButtonPosition as findButtonPos } from './gridset/cellHelpers';
11
12
  import { GridsetValidator } from '../validation/gridsetValidator';
@@ -20,6 +21,11 @@ import { decodeText } from '../utils/io';
20
21
  class GridsetProcessor extends BaseProcessor {
21
22
  constructor(options) {
22
23
  super(options);
24
+ this.capabilities = {
25
+ wordList: 'native',
26
+ preservesAssetsOnSave: true,
27
+ newCellCreation: 'restricted',
28
+ };
23
29
  }
24
30
  // Determine password to use when opening encrypted gridset archives (.gridsetx)
25
31
  getGridsetPassword(source) {
@@ -2224,6 +2230,35 @@ class GridsetProcessor extends BaseProcessor {
2224
2230
  const AdmZip = (await import('adm-zip')).default;
2225
2231
  const originalZip = new AdmZip(originalPath);
2226
2232
  const outputZip = new AdmZip();
2233
+ // Check if any page has pending mutations
2234
+ const hasPendingMutations = Object.values(tree.pages).some((page) => page.pendingMutations.length > 0);
2235
+ if (hasPendingMutations) {
2236
+ // NEW: Use mutation-based save path
2237
+ const parser = new XMLParser({
2238
+ ignoreAttributes: false,
2239
+ attributeNamePrefix: '@_',
2240
+ });
2241
+ const gridBuilder = new XMLBuilder({
2242
+ ignoreAttributes: false,
2243
+ format: true,
2244
+ indentBy: ' ',
2245
+ suppressEmptyNode: true,
2246
+ suppressBooleanAttributes: false,
2247
+ });
2248
+ GridsetSaveHandler.saveWithMutations(tree, originalZip, outputZip, parser, gridBuilder, (page) => this.createBasicGridXml(page));
2249
+ // Copy remaining files
2250
+ for (const entry of originalZip.getEntries()) {
2251
+ if (entry.isDirectory)
2252
+ continue;
2253
+ if (!outputZip.getEntry(entry.entryName)) {
2254
+ outputZip.addFile(entry.entryName, entry.getData());
2255
+ }
2256
+ }
2257
+ const outputBuffer = outputZip.toBuffer();
2258
+ await writeBinaryToPath(outputPath, outputBuffer);
2259
+ return;
2260
+ }
2261
+ // LEGACY: Original position-based logic continues below...
2227
2262
  // Create a map of pages by name for easy lookup
2228
2263
  const pagesByName = new Map();
2229
2264
  for (const page of Object.values(tree.pages)) {
@@ -2417,43 +2452,6 @@ class GridsetProcessor extends BaseProcessor {
2417
2452
  originalGrid.Grid.WordList.Items.WordListItem = allItems;
2418
2453
  }
2419
2454
  }
2420
- // Process WordList items attached to the page (from personalisation)
2421
- // These are tracked separately and shouldn't create new cells
2422
- // Use a known symbol key to check for WordList items
2423
- const WORDLIST_ITEMS_KEY = 'wordListItems';
2424
- const wordListItems = page[WORDLIST_ITEMS_KEY];
2425
- if (wordListItems && wordListItems.length > 0) {
2426
- // Ensure WordList structure exists
2427
- if (!originalGrid.Grid) {
2428
- originalGrid.Grid = {};
2429
- }
2430
- if (!originalGrid.Grid.WordList) {
2431
- originalGrid.Grid.WordList = {};
2432
- }
2433
- if (!originalGrid.Grid.WordList.Items) {
2434
- originalGrid.Grid.WordList.Items = {};
2435
- }
2436
- const existingItems = originalGrid.Grid.WordList.Items.WordListItem ||
2437
- originalGrid.Grid.WordList.Items.wordlistitem ||
2438
- [];
2439
- const itemsArray = Array.isArray(existingItems) ? existingItems : [existingItems];
2440
- // Add new WordList items with proper Grid 3 format
2441
- for (const item of wordListItems) {
2442
- itemsArray.push({
2443
- Text: {
2444
- p: {
2445
- s: {
2446
- r: item.label,
2447
- },
2448
- },
2449
- },
2450
- Image: '',
2451
- PartOfSpeech: 'Unknown',
2452
- });
2453
- }
2454
- // Update the WordList
2455
- originalGrid.Grid.WordList.Items.WordListItem = itemsArray;
2456
- }
2457
2455
  // Build the updated grid XML and format for Grid 3 compatibility
2458
2456
  let builtXml = gridBuilder.build(originalGrid);
2459
2457
  builtXml = formatGrid3XmlComplete(builtXml);
@@ -18,6 +18,11 @@ function mapObfVisibility(hidden) {
18
18
  class ObfProcessor extends BaseProcessor {
19
19
  constructor(options) {
20
20
  super(options);
21
+ this.capabilities = {
22
+ wordList: 'none',
23
+ preservesAssetsOnSave: true,
24
+ newCellCreation: 'allowed',
25
+ };
21
26
  this.imageCache = new Map(); // Cache for data URLs
22
27
  }
23
28
  /**
@@ -531,6 +536,46 @@ class ObfProcessor extends BaseProcessor {
531
536
  }
532
537
  return { rows: totalRows, columns: totalColumns, order, buttonPositions };
533
538
  }
539
+ /**
540
+ * Apply mutations to a buttons array for OBF export
541
+ * Returns a modified copy of the buttons array with mutations applied
542
+ *
543
+ * Note: addButton mutations are NOT applied because the button is already
544
+ * in the buttons array (added by page.addButton()). We only apply
545
+ * removeButton and updateButton mutations.
546
+ */
547
+ applyMutationsToButtons(buttons, mutations) {
548
+ let modifiedButtons = [...buttons];
549
+ for (const mutation of mutations) {
550
+ switch (mutation.type) {
551
+ case 'addButton':
552
+ // Skip - button is already in the array from page.addButton()
553
+ break;
554
+ case 'removeButton': {
555
+ modifiedButtons = modifiedButtons.filter((b) => b.id !== mutation.buttonId);
556
+ break;
557
+ }
558
+ case 'updateButton': {
559
+ modifiedButtons = modifiedButtons.map((b) => {
560
+ if (b.id === mutation.buttonId) {
561
+ // Create a new AACButton instance with the patched properties
562
+ const patched = Object.create(Object.getPrototypeOf(b));
563
+ Object.assign(patched, b, mutation.patch);
564
+ return patched;
565
+ }
566
+ return b;
567
+ });
568
+ break;
569
+ }
570
+ case 'addWordListItem':
571
+ case 'removeWordListItem':
572
+ case 'clearWordList':
573
+ // OBF doesn't have WordList - skip
574
+ break;
575
+ }
576
+ }
577
+ return modifiedButtons;
578
+ }
534
579
  createObfBoardFromPage(page, fallbackName, metadata, embedData = false) {
535
580
  const { rows, columns, order, buttonPositions } = this.buildGridMetadata(page);
536
581
  const boardName = metadata?.name && page.id === metadata?.defaultHomePageId
@@ -543,6 +588,10 @@ class ObfProcessor extends BaseProcessor {
543
588
  return image;
544
589
  });
545
590
  }
591
+ // Apply mutations if present
592
+ const buttons = page.pendingMutations.length > 0
593
+ ? this.applyMutationsToButtons(page.buttons, page.pendingMutations)
594
+ : page.buttons;
546
595
  return {
547
596
  format: OBF_FORMAT_VERSION,
548
597
  id: page.id,
@@ -557,7 +606,7 @@ class ObfProcessor extends BaseProcessor {
557
606
  columns,
558
607
  order,
559
608
  },
560
- buttons: page.buttons.map((button) => {
609
+ buttons: buttons.map((button) => {
561
610
  const extraButtonInfo = button;
562
611
  const imageId = button.parameters?.image_id ||
563
612
  button.parameters?.imageId ||
@@ -702,6 +751,7 @@ class ObfProcessor extends BaseProcessor {
702
751
  for (const page of Object.values(tree.pages)) {
703
752
  const obfFilename = this.getPageFilename(page.id, tree.metadata);
704
753
  modifiedObfFiles.add(obfFilename);
754
+ // createObfBoardFromPage will automatically apply mutations if present
705
755
  const obfBoard = this.createObfBoardFromPage(page, 'Board', tree.metadata);
706
756
  const obfContent = JSON.stringify(obfBoard, null, 2);
707
757
  newObfFiles.set(obfFilename, obfContent);
@@ -6,6 +6,11 @@ import { getBasename, encodeText } from '../utils/io';
6
6
  class OpmlProcessor extends BaseProcessor {
7
7
  constructor(options) {
8
8
  super(options);
9
+ this.capabilities = {
10
+ wordList: 'none',
11
+ preservesAssetsOnSave: false,
12
+ newCellCreation: 'allowed',
13
+ };
9
14
  }
10
15
  processOutline(outline, parentId = null) {
11
16
  if (!outline || typeof outline !== 'object') {
@@ -37,6 +37,11 @@ function mapSnapVisibility(visible) {
37
37
  class SnapProcessor extends BaseProcessor {
38
38
  constructor(symbolResolver = null, options) {
39
39
  super(options);
40
+ this.capabilities = {
41
+ wordList: 'none',
42
+ preservesAssetsOnSave: false,
43
+ newCellCreation: 'allowed',
44
+ };
40
45
  this.symbolResolver = null;
41
46
  this.loadAudio = false;
42
47
  this.pageLayoutPreference = 'scanning'; // Default to scanning for metrics
@@ -31,6 +31,11 @@ function mapTouchChatVisibility(visible) {
31
31
  class TouchChatProcessor extends BaseProcessor {
32
32
  constructor(options) {
33
33
  super(options);
34
+ this.capabilities = {
35
+ wordList: 'none',
36
+ preservesAssetsOnSave: false,
37
+ newCellCreation: 'allowed',
38
+ };
34
39
  this.tree = null;
35
40
  this.sourceFile = null;
36
41
  }
@@ -44,6 +44,7 @@ import { StringCasing } from './stringCasing';
44
44
  import { ValidationResult } from '../validation/validationTypes';
45
45
  import { BinaryOutput, FileAdapter, ProcessorInput } from '../utils/io';
46
46
  import { ZipAdapter } from '../utils/zip';
47
+ import type { ProcessorCapabilities } from '../types/aac';
47
48
  export interface ProcessorConfig {
48
49
  excludeNavigationButtons?: boolean;
49
50
  excludeSystemButtons?: boolean;
@@ -90,6 +91,7 @@ export interface SourceString {
90
91
  }
91
92
  declare abstract class BaseProcessor {
92
93
  protected options: ProcessorConfig;
94
+ abstract readonly capabilities: ProcessorCapabilities;
93
95
  constructor(options?: ProcessorOptions);
94
96
  abstract extractTexts(filePathOrBuffer: ProcessorInput): Promise<string[]>;
95
97
  abstract loadIntoTree(filePathOrBuffer: ProcessorInput): Promise<AACTree>;
@@ -1,5 +1,5 @@
1
- import type { AACStyle, AACTreeMetadata, SnapMetadata, GridSetMetadata, AstericsGridMetadata, TouchChatMetadata, CellScanningOrder, ScanningSelectionMethod } from '../types/aac';
2
- export type { AACStyle, AACTreeMetadata, SnapMetadata, GridSetMetadata, AstericsGridMetadata, TouchChatMetadata, CellScanningOrder, ScanningSelectionMethod, };
1
+ import type { AACStyle, AACTreeMetadata, SnapMetadata, GridSetMetadata, AstericsGridMetadata, TouchChatMetadata, CellScanningOrder, ScanningSelectionMethod, AACWordListItem, AACPageMutation, ProcessorCapabilities } from '../types/aac';
2
+ export type { AACStyle, AACTreeMetadata, SnapMetadata, GridSetMetadata, AstericsGridMetadata, TouchChatMetadata, CellScanningOrder, ScanningSelectionMethod, AACWordListItem, AACPageMutation, ProcessorCapabilities, };
3
3
  export declare enum AACSemanticCategory {
4
4
  COMMUNICATION = "communication",// Speech, text output
5
5
  NAVIGATION = "navigation",// Page/grid navigation
@@ -227,6 +227,7 @@ export declare class AACPage {
227
227
  scanningConfig?: import('../types/aac').ScanningConfig;
228
228
  scanType?: AACScanType;
229
229
  scanBlocksConfig?: AACScanBlock[];
230
+ private _pendingMutations;
230
231
  constructor({ id, name, grid, buttons, parentId, style, locale, descriptionHtml, images, sounds, semantic_ids, clone_ids, scanningConfig, scanBlocksConfig, scanType, }: {
231
232
  id: string;
232
233
  name?: string;
@@ -248,6 +249,35 @@ export declare class AACPage {
248
249
  scanType?: AACScanType;
249
250
  });
250
251
  addButton(button: AACButton): void;
252
+ /**
253
+ * Get the list of pending mutations for this page (read-only)
254
+ */
255
+ get pendingMutations(): readonly AACPageMutation[];
256
+ /**
257
+ * Remove a button by ID
258
+ * @param buttonId - The ID of the button to remove
259
+ */
260
+ removeButton(buttonId: string): void;
261
+ /**
262
+ * Update a button by merging a patch
263
+ * @param buttonId - The ID of the button to update
264
+ * @param patch - Partial button object with fields to update
265
+ */
266
+ updateButton(buttonId: string, patch: Partial<AACButton>): void;
267
+ /**
268
+ * Add an item to the page's WordList (for formats with dynamic content cells)
269
+ * @param item - WordList item to add
270
+ */
271
+ addWordListItem(item: AACWordListItem): void;
272
+ /**
273
+ * Remove items from the page's WordList
274
+ * @param textOrPredicate - Text to match or predicate function to filter items
275
+ */
276
+ removeWordListItem(textOrPredicate: string | ((item: AACWordListItem) => boolean)): void;
277
+ /**
278
+ * Clear all items from the page's WordList
279
+ */
280
+ clearWordList(): void;
251
281
  }
252
282
  export declare class AACTree {
253
283
  pages: {
@@ -172,6 +172,8 @@ class AACButton {
172
172
  exports.AACButton = AACButton;
173
173
  class AACPage {
174
174
  constructor({ id, name = '', grid = [], buttons = [], parentId = null, style, locale, descriptionHtml, images, sounds, semantic_ids, clone_ids, scanningConfig, scanBlocksConfig, scanType, }) {
175
+ // Mutation tracking
176
+ this._pendingMutations = [];
175
177
  this.id = id;
176
178
  this.name = name;
177
179
  if (Array.isArray(grid)) {
@@ -200,6 +202,49 @@ class AACPage {
200
202
  }
201
203
  addButton(button) {
202
204
  this.buttons.push(button);
205
+ // Record the mutation
206
+ this._pendingMutations.push({ type: 'addButton', button });
207
+ }
208
+ /**
209
+ * Get the list of pending mutations for this page (read-only)
210
+ */
211
+ get pendingMutations() {
212
+ return Object.freeze([...this._pendingMutations]);
213
+ }
214
+ /**
215
+ * Remove a button by ID
216
+ * @param buttonId - The ID of the button to remove
217
+ */
218
+ removeButton(buttonId) {
219
+ this._pendingMutations.push({ type: 'removeButton', buttonId });
220
+ }
221
+ /**
222
+ * Update a button by merging a patch
223
+ * @param buttonId - The ID of the button to update
224
+ * @param patch - Partial button object with fields to update
225
+ */
226
+ updateButton(buttonId, patch) {
227
+ this._pendingMutations.push({ type: 'updateButton', buttonId, patch });
228
+ }
229
+ /**
230
+ * Add an item to the page's WordList (for formats with dynamic content cells)
231
+ * @param item - WordList item to add
232
+ */
233
+ addWordListItem(item) {
234
+ this._pendingMutations.push({ type: 'addWordListItem', item });
235
+ }
236
+ /**
237
+ * Remove items from the page's WordList
238
+ * @param textOrPredicate - Text to match or predicate function to filter items
239
+ */
240
+ removeWordListItem(textOrPredicate) {
241
+ this._pendingMutations.push({ type: 'removeWordListItem', match: textOrPredicate });
242
+ }
243
+ /**
244
+ * Clear all items from the page's WordList
245
+ */
246
+ clearWordList() {
247
+ this._pendingMutations.push({ type: 'clearWordList' });
203
248
  }
204
249
  }
205
250
  exports.AACPage = AACPage;
@@ -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 ApplePanelsProcessor extends BaseProcessor {
5
+ readonly capabilities: {
6
+ wordList: "none";
7
+ preservesAssetsOnSave: boolean;
8
+ newCellCreation: "allowed";
9
+ };
5
10
  constructor(options?: ProcessorOptions);
6
11
  private parseRect;
7
12
  private pixelToGrid;