@willwade/aac-processors 0.2.12 → 0.2.14
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 +60 -0
- package/dist/browser/processors/applePanelsProcessor.js +7 -1
- package/dist/browser/processors/astericsGridProcessor.js +7 -1
- package/dist/browser/processors/dotProcessor.js +9 -2
- package/dist/browser/processors/gridset/saveMutations.js +212 -0
- package/dist/browser/processors/gridsetProcessor.js +37 -39
- package/dist/browser/processors/obfProcessor.js +51 -1
- package/dist/browser/processors/opmlProcessor.js +7 -1
- package/dist/browser/processors/snapProcessor.js +7 -1
- package/dist/browser/processors/touchchatProcessor.js +8 -3
- package/dist/core/baseProcessor.d.ts +2 -0
- package/dist/core/treeStructure.d.ts +43 -2
- package/dist/core/treeStructure.js +60 -0
- package/dist/processors/applePanelsProcessor.d.ts +5 -0
- package/dist/processors/applePanelsProcessor.js +7 -1
- package/dist/processors/astericsGridProcessor.d.ts +5 -0
- package/dist/processors/astericsGridProcessor.js +7 -1
- package/dist/processors/dotProcessor.d.ts +5 -0
- package/dist/processors/dotProcessor.js +9 -2
- 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 +37 -39
- 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 +7 -1
- package/dist/processors/snapProcessor.d.ts +5 -0
- package/dist/processors/snapProcessor.js +7 -1
- package/dist/processors/touchchatProcessor.d.ts +5 -0
- package/dist/processors/touchchatProcessor.js +8 -3
- package/dist/types/aac.d.ts +54 -0
- package/package.json +1 -1
|
@@ -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,46 @@ export declare class AACPage {
|
|
|
248
249
|
scanType?: AACScanType;
|
|
249
250
|
});
|
|
250
251
|
addButton(button: AACButton): void;
|
|
252
|
+
/**
|
|
253
|
+
* Internal load-path button push: adds a button to the page WITHOUT recording a mutation.
|
|
254
|
+
* Used by processors during loadIntoTree so the loaded baseline isn't treated as user changes.
|
|
255
|
+
* Not part of the public API — consumers should always use addButton.
|
|
256
|
+
*/
|
|
257
|
+
_loadButton(button: AACButton): void;
|
|
258
|
+
/**
|
|
259
|
+
* Discard all recorded mutations on this page.
|
|
260
|
+
* Useful as an escape hatch after loadIntoTree if the consumer wants a clean baseline.
|
|
261
|
+
*/
|
|
262
|
+
clearMutations(): void;
|
|
263
|
+
/**
|
|
264
|
+
* Get the list of pending mutations for this page (read-only)
|
|
265
|
+
*/
|
|
266
|
+
get pendingMutations(): readonly AACPageMutation[];
|
|
267
|
+
/**
|
|
268
|
+
* Remove a button by ID
|
|
269
|
+
* @param buttonId - The ID of the button to remove
|
|
270
|
+
*/
|
|
271
|
+
removeButton(buttonId: string): void;
|
|
272
|
+
/**
|
|
273
|
+
* Update a button by merging a patch
|
|
274
|
+
* @param buttonId - The ID of the button to update
|
|
275
|
+
* @param patch - Partial button object with fields to update
|
|
276
|
+
*/
|
|
277
|
+
updateButton(buttonId: string, patch: Partial<AACButton>): void;
|
|
278
|
+
/**
|
|
279
|
+
* Add an item to the page's WordList (for formats with dynamic content cells)
|
|
280
|
+
* @param item - WordList item to add
|
|
281
|
+
*/
|
|
282
|
+
addWordListItem(item: AACWordListItem): void;
|
|
283
|
+
/**
|
|
284
|
+
* Remove items from the page's WordList
|
|
285
|
+
* @param textOrPredicate - Text to match or predicate function to filter items
|
|
286
|
+
*/
|
|
287
|
+
removeWordListItem(textOrPredicate: string | ((item: AACWordListItem) => boolean)): void;
|
|
288
|
+
/**
|
|
289
|
+
* Clear all items from the page's WordList
|
|
290
|
+
*/
|
|
291
|
+
clearWordList(): void;
|
|
251
292
|
}
|
|
252
293
|
export declare class AACTree {
|
|
253
294
|
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,64 @@ 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
|
+
* Internal load-path button push: adds a button to the page WITHOUT recording a mutation.
|
|
210
|
+
* Used by processors during loadIntoTree so the loaded baseline isn't treated as user changes.
|
|
211
|
+
* Not part of the public API — consumers should always use addButton.
|
|
212
|
+
*/
|
|
213
|
+
_loadButton(button) {
|
|
214
|
+
this.buttons.push(button);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Discard all recorded mutations on this page.
|
|
218
|
+
* Useful as an escape hatch after loadIntoTree if the consumer wants a clean baseline.
|
|
219
|
+
*/
|
|
220
|
+
clearMutations() {
|
|
221
|
+
this._pendingMutations = [];
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Get the list of pending mutations for this page (read-only)
|
|
225
|
+
*/
|
|
226
|
+
get pendingMutations() {
|
|
227
|
+
return Object.freeze([...this._pendingMutations]);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Remove a button by ID
|
|
231
|
+
* @param buttonId - The ID of the button to remove
|
|
232
|
+
*/
|
|
233
|
+
removeButton(buttonId) {
|
|
234
|
+
this._pendingMutations.push({ type: 'removeButton', buttonId });
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Update a button by merging a patch
|
|
238
|
+
* @param buttonId - The ID of the button to update
|
|
239
|
+
* @param patch - Partial button object with fields to update
|
|
240
|
+
*/
|
|
241
|
+
updateButton(buttonId, patch) {
|
|
242
|
+
this._pendingMutations.push({ type: 'updateButton', buttonId, patch });
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Add an item to the page's WordList (for formats with dynamic content cells)
|
|
246
|
+
* @param item - WordList item to add
|
|
247
|
+
*/
|
|
248
|
+
addWordListItem(item) {
|
|
249
|
+
this._pendingMutations.push({ type: 'addWordListItem', item });
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Remove items from the page's WordList
|
|
253
|
+
* @param textOrPredicate - Text to match or predicate function to filter items
|
|
254
|
+
*/
|
|
255
|
+
removeWordListItem(textOrPredicate) {
|
|
256
|
+
this._pendingMutations.push({ type: 'removeWordListItem', match: textOrPredicate });
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Clear all items from the page's WordList
|
|
260
|
+
*/
|
|
261
|
+
clearWordList() {
|
|
262
|
+
this._pendingMutations.push({ type: 'clearWordList' });
|
|
203
263
|
}
|
|
204
264
|
}
|
|
205
265
|
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;
|
|
@@ -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) {
|
|
@@ -220,7 +225,8 @@ class ApplePanelsProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
220
225
|
fontWeight: btn.DisplayImageWeight === 'bold' ? 'bold' : 'normal',
|
|
221
226
|
},
|
|
222
227
|
});
|
|
223
|
-
|
|
228
|
+
// Load path: do not record as a user mutation
|
|
229
|
+
page._loadButton(button);
|
|
224
230
|
if (btn.Rect) {
|
|
225
231
|
const rect = this.parseRect(btn.Rect);
|
|
226
232
|
if (rect) {
|
|
@@ -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
|
}
|
|
@@ -717,7 +722,8 @@ class AstericsGridProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
717
722
|
}
|
|
718
723
|
grid.gridElements.forEach((element) => {
|
|
719
724
|
const button = this.createButtonFromElement(element, colorConfig, activeColorSchemeDefinition);
|
|
720
|
-
|
|
725
|
+
// Load path: do not record as a user mutation
|
|
726
|
+
page._loadButton(button);
|
|
721
727
|
const buttonX = element.x || 0;
|
|
722
728
|
const buttonY = element.y || 0;
|
|
723
729
|
const buttonWidth = element.width || 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();
|
|
@@ -115,7 +120,8 @@ class DotProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
115
120
|
});
|
|
116
121
|
tree.addPage(page);
|
|
117
122
|
// Add a self button so single-node graphs yield one button
|
|
118
|
-
|
|
123
|
+
// Load path: do not record as a user mutation
|
|
124
|
+
page._loadButton(new treeStructure_1.AACButton({
|
|
119
125
|
id: `${node.id}_self`,
|
|
120
126
|
label: node.label,
|
|
121
127
|
message: node.label,
|
|
@@ -136,7 +142,8 @@ class DotProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
136
142
|
message: '',
|
|
137
143
|
targetPageId: edge.to,
|
|
138
144
|
});
|
|
139
|
-
|
|
145
|
+
// Load path: do not record as a user mutation
|
|
146
|
+
fromPage._loadButton(button);
|
|
140
147
|
}
|
|
141
148
|
}
|
|
142
149
|
return tree;
|
|
@@ -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) {
|
|
@@ -1591,8 +1597,8 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
1591
1597
|
...(imageData ? { imageData, image_id: resolvedImageEntry } : {}),
|
|
1592
1598
|
},
|
|
1593
1599
|
});
|
|
1594
|
-
// Add button to page
|
|
1595
|
-
page.
|
|
1600
|
+
// Add button to page (load path: do not record as a user mutation)
|
|
1601
|
+
page._loadButton(button);
|
|
1596
1602
|
// Place button in grid layout (handle colspan/rowspan)
|
|
1597
1603
|
for (let r = cellY; r < cellY + rowSpan && r < maxRows; r++) {
|
|
1598
1604
|
for (let c = cellX; c < cellX + colSpan && c < maxCols; c++) {
|
|
@@ -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);
|