@willwade/aac-processors 0.2.11 → 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 -0
  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 -0
  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
@@ -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)) {
@@ -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);
@@ -34,6 +34,11 @@ function mapTouchChatVisibility(visible) {
34
34
  class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
35
35
  constructor(options) {
36
36
  super(options);
37
+ this.capabilities = {
38
+ wordList: 'none',
39
+ preservesAssetsOnSave: false,
40
+ newCellCreation: 'allowed',
41
+ };
37
42
  this.tree = null;
38
43
  this.sourceFile = null;
39
44
  }
@@ -206,3 +206,57 @@ export interface AACProcessor {
206
206
  extractTexts(filePath: string | Buffer): Promise<string[]>;
207
207
  loadIntoTree(filePath: string | Buffer): Promise<AACTree>;
208
208
  }
209
+ /**
210
+ * Word List Item for dynamic content cells (e.g., Grid 3 WordLists)
211
+ */
212
+ export interface AACWordListItem {
213
+ text: string;
214
+ image?: string;
215
+ partOfSpeech?: string;
216
+ }
217
+ /**
218
+ * Mutation types for page modifications
219
+ */
220
+ export type AACPageMutation = {
221
+ type: 'addButton';
222
+ button: AACButton;
223
+ } | {
224
+ type: 'removeButton';
225
+ buttonId: string;
226
+ } | {
227
+ type: 'updateButton';
228
+ buttonId: string;
229
+ patch: Partial<AACButton>;
230
+ } | {
231
+ type: 'addWordListItem';
232
+ item: AACWordListItem;
233
+ } | {
234
+ type: 'removeWordListItem';
235
+ match: string | ((item: AACWordListItem) => boolean);
236
+ } | {
237
+ type: 'clearWordList';
238
+ };
239
+ /**
240
+ * Processor capabilities declaration
241
+ */
242
+ export interface ProcessorCapabilities {
243
+ /**
244
+ * WordList support level
245
+ * - 'native': addWordListItem writes a real WordList structure on disk
246
+ * - 'fallback': addWordListItem becomes addButton (still useful, just not dynamic)
247
+ * - 'none': addWordListItem throws CapabilityError
248
+ */
249
+ wordList: 'native' | 'fallback' | 'none';
250
+ /**
251
+ * Whether the processor has a real saveModifiedTree that keeps original images/settings
252
+ * If false, saveModifiedTree falls back to saveFromTree with a warning
253
+ */
254
+ preservesAssetsOnSave: boolean;
255
+ /**
256
+ * Rules for creating new cells
257
+ * - 'allowed': addButton at any (x,y) creates a cell on save
258
+ * - 'restricted': addButton routes to a WordList if (x,y) is a WordList cell, else dropped with warning
259
+ * - 'forbidden': addButton requires explicit (x,y) of an existing cell; otherwise CapabilityError
260
+ */
261
+ newCellCreation: 'allowed' | 'restricted' | 'forbidden';
262
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@willwade/aac-processors",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
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",