@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
|
@@ -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:
|
|
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);
|