@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.
- package/dist/browser/core/treeStructure.js +45 -0
- package/dist/browser/processors/applePanelsProcessor.js +5 -0
- package/dist/browser/processors/astericsGridProcessor.js +5 -0
- package/dist/browser/processors/dotProcessor.js +5 -0
- package/dist/browser/processors/gridset/saveMutations.js +212 -0
- package/dist/browser/processors/gridsetProcessor.js +35 -37
- package/dist/browser/processors/obfProcessor.js +51 -1
- package/dist/browser/processors/opmlProcessor.js +5 -0
- package/dist/browser/processors/snapProcessor.js +5 -0
- package/dist/browser/processors/touchchatProcessor.js +5 -0
- package/dist/core/baseProcessor.d.ts +2 -0
- package/dist/core/treeStructure.d.ts +32 -2
- package/dist/core/treeStructure.js +45 -0
- package/dist/processors/applePanelsProcessor.d.ts +5 -0
- package/dist/processors/applePanelsProcessor.js +5 -0
- package/dist/processors/astericsGridProcessor.d.ts +5 -0
- package/dist/processors/astericsGridProcessor.js +5 -0
- package/dist/processors/dotProcessor.d.ts +5 -0
- package/dist/processors/dotProcessor.js +5 -0
- package/dist/processors/excelProcessor.d.ts +5 -0
- package/dist/processors/excelProcessor.js +8 -0
- package/dist/processors/gridset/saveMutations.d.ts +39 -0
- package/dist/processors/gridset/saveMutations.js +216 -0
- package/dist/processors/gridsetProcessor.d.ts +5 -0
- package/dist/processors/gridsetProcessor.js +35 -37
- package/dist/processors/obfProcessor.d.ts +14 -0
- package/dist/processors/obfProcessor.js +51 -1
- package/dist/processors/obfsetProcessor.d.ts +5 -0
- package/dist/processors/obfsetProcessor.js +5 -0
- package/dist/processors/opmlProcessor.d.ts +5 -0
- package/dist/processors/opmlProcessor.js +5 -0
- package/dist/processors/snapProcessor.d.ts +5 -0
- package/dist/processors/snapProcessor.js +5 -0
- package/dist/processors/touchchatProcessor.d.ts +5 -0
- package/dist/processors/touchchatProcessor.js +5 -0
- package/dist/types/aac.d.ts +54 -0
- 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:
|
|
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;
|