@willwade/aac-processors 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +787 -0
  3. package/dist/cli/index.d.ts +2 -0
  4. package/dist/cli/index.js +189 -0
  5. package/dist/cli/prettyPrint.d.ts +2 -0
  6. package/dist/cli/prettyPrint.js +28 -0
  7. package/dist/core/analyze.d.ts +6 -0
  8. package/dist/core/analyze.js +49 -0
  9. package/dist/core/baseProcessor.d.ts +94 -0
  10. package/dist/core/baseProcessor.js +208 -0
  11. package/dist/core/fileProcessor.d.ts +7 -0
  12. package/dist/core/fileProcessor.js +51 -0
  13. package/dist/core/stringCasing.d.ts +37 -0
  14. package/dist/core/stringCasing.js +174 -0
  15. package/dist/core/treeStructure.d.ts +190 -0
  16. package/dist/core/treeStructure.js +223 -0
  17. package/dist/index.d.ts +23 -0
  18. package/dist/index.js +96 -0
  19. package/dist/optional/symbolTools.d.ts +28 -0
  20. package/dist/optional/symbolTools.js +126 -0
  21. package/dist/processors/applePanelsProcessor.d.ts +23 -0
  22. package/dist/processors/applePanelsProcessor.js +521 -0
  23. package/dist/processors/astericsGridProcessor.d.ts +49 -0
  24. package/dist/processors/astericsGridProcessor.js +1427 -0
  25. package/dist/processors/dotProcessor.d.ts +21 -0
  26. package/dist/processors/dotProcessor.js +191 -0
  27. package/dist/processors/excelProcessor.d.ts +145 -0
  28. package/dist/processors/excelProcessor.js +556 -0
  29. package/dist/processors/gridset/helpers.d.ts +4 -0
  30. package/dist/processors/gridset/helpers.js +48 -0
  31. package/dist/processors/gridset/resolver.d.ts +8 -0
  32. package/dist/processors/gridset/resolver.js +100 -0
  33. package/dist/processors/gridsetProcessor.d.ts +28 -0
  34. package/dist/processors/gridsetProcessor.js +1339 -0
  35. package/dist/processors/index.d.ts +14 -0
  36. package/dist/processors/index.js +42 -0
  37. package/dist/processors/obfProcessor.d.ts +21 -0
  38. package/dist/processors/obfProcessor.js +278 -0
  39. package/dist/processors/opmlProcessor.d.ts +21 -0
  40. package/dist/processors/opmlProcessor.js +235 -0
  41. package/dist/processors/snap/helpers.d.ts +4 -0
  42. package/dist/processors/snap/helpers.js +27 -0
  43. package/dist/processors/snapProcessor.d.ts +44 -0
  44. package/dist/processors/snapProcessor.js +586 -0
  45. package/dist/processors/touchchat/helpers.d.ts +4 -0
  46. package/dist/processors/touchchat/helpers.js +27 -0
  47. package/dist/processors/touchchatProcessor.d.ts +27 -0
  48. package/dist/processors/touchchatProcessor.js +768 -0
  49. package/dist/types/aac.d.ts +47 -0
  50. package/dist/types/aac.js +2 -0
  51. package/docs/.keep +1 -0
  52. package/docs/ApplePanels.md +309 -0
  53. package/docs/Grid3-XML-Format.md +1788 -0
  54. package/docs/TobiiDynavox-Snap-Details.md +394 -0
  55. package/docs/asterics-Grid-fileformat-details.md +443 -0
  56. package/docs/obf_.obz Open Board File Formats.md +432 -0
  57. package/docs/touchchat.md +520 -0
  58. package/examples/.coverage +0 -0
  59. package/examples/.keep +1 -0
  60. package/examples/README.md +31 -0
  61. package/examples/communikate.dot +2637 -0
  62. package/examples/demo.js +143 -0
  63. package/examples/example-images.gridset +0 -0
  64. package/examples/example.ce +0 -0
  65. package/examples/example.dot +14 -0
  66. package/examples/example.grd +1 -0
  67. package/examples/example.gridset +0 -0
  68. package/examples/example.obf +27 -0
  69. package/examples/example.obz +0 -0
  70. package/examples/example.opml +18 -0
  71. package/examples/example.spb +0 -0
  72. package/examples/example.sps +0 -0
  73. package/examples/example2.grd +1 -0
  74. package/examples/gemini_response.txt +845 -0
  75. package/examples/image-map.js +45 -0
  76. package/examples/package-lock.json +1326 -0
  77. package/examples/package.json +10 -0
  78. package/examples/styled-output/converted-snap-to-touchchat.ce +0 -0
  79. package/examples/styled-output/styled-example.ce +0 -0
  80. package/examples/styled-output/styled-example.gridset +0 -0
  81. package/examples/styled-output/styled-example.obf +37 -0
  82. package/examples/styled-output/styled-example.spb +0 -0
  83. package/examples/styling-example.ts +316 -0
  84. package/examples/translate.js +39 -0
  85. package/examples/translate_demo.js +254 -0
  86. package/examples/translation_cache.json +44894 -0
  87. package/examples/typescript-demo.ts +251 -0
  88. package/examples/unified-interface-demo.ts +183 -0
  89. package/package.json +106 -0
@@ -0,0 +1,1339 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.GridsetProcessor = void 0;
7
+ const baseProcessor_1 = require("../core/baseProcessor");
8
+ const treeStructure_1 = require("../core/treeStructure");
9
+ const adm_zip_1 = __importDefault(require("adm-zip"));
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const fast_xml_parser_1 = require("fast-xml-parser");
12
+ const resolver_1 = require("./gridset/resolver");
13
+ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
14
+ constructor(options) {
15
+ super(options);
16
+ }
17
+ // Helper function to ensure color has alpha channel (Grid3 format)
18
+ ensureAlphaChannel(color) {
19
+ if (!color)
20
+ return '#FFFFFFFF';
21
+ // If already 8 digits (with alpha), return as is
22
+ if (color.match(/^#[0-9A-Fa-f]{8}$/))
23
+ return color;
24
+ // If 6 digits (no alpha), add FF for fully opaque
25
+ if (color.match(/^#[0-9A-Fa-f]{6}$/))
26
+ return color + 'FF';
27
+ // If 3 digits (shorthand), expand to 8
28
+ if (color.match(/^#[0-9A-Fa-f]{3}$/)) {
29
+ const r = color[1];
30
+ const g = color[2];
31
+ const b = color[3];
32
+ return `#${r}${r}${g}${g}${b}${b}FF`;
33
+ }
34
+ // Invalid or unknown format, return white
35
+ return '#FFFFFFFF';
36
+ }
37
+ // Helper function to generate Grid3 commands from semantic actions
38
+ generateCommandsFromSemanticAction(button, tree) {
39
+ const semanticAction = button.semanticAction;
40
+ if (!semanticAction) {
41
+ // Default to insert text action with structured XML format
42
+ // Use two <s> elements: one for the word, one for the space (CDATA preserves whitespace)
43
+ let text = button.message || button.label || '';
44
+ // Remove trailing space from message if present (we'll add it as separate segment)
45
+ if (text.endsWith(' ')) {
46
+ text = text.slice(0, -1);
47
+ }
48
+ return {
49
+ Command: {
50
+ '@_ID': 'Action.InsertText',
51
+ Parameter: {
52
+ '@_Key': 'text',
53
+ p: {
54
+ s: [
55
+ {
56
+ r: text,
57
+ },
58
+ {
59
+ r: { '__cdata': ' ' },
60
+ },
61
+ ],
62
+ },
63
+ },
64
+ },
65
+ };
66
+ }
67
+ // Use platform-specific Grid3 data if available
68
+ if (semanticAction.platformData?.grid3) {
69
+ const grid3Data = semanticAction.platformData.grid3;
70
+ const params = Object.entries(grid3Data.parameters || {}).map(([key, value]) => ({
71
+ '@_Key': key,
72
+ '#text': String(value),
73
+ }));
74
+ return {
75
+ Command: {
76
+ '@_ID': grid3Data.commandId,
77
+ ...(params.length > 0 ? { Parameter: params } : {}),
78
+ },
79
+ };
80
+ }
81
+ // Convert semantic actions to Grid3 commands
82
+ const intentStr = String(semanticAction.intent);
83
+ switch (intentStr) {
84
+ case 'NAVIGATE_TO': {
85
+ // For Grid3, we need to use the grid name, not the ID
86
+ let targetGridName = semanticAction.targetId || '';
87
+ if (tree && semanticAction.targetId) {
88
+ const targetPage = tree.getPage(semanticAction.targetId);
89
+ if (targetPage) {
90
+ targetGridName = targetPage.name || targetPage.id;
91
+ }
92
+ }
93
+ return {
94
+ Command: {
95
+ '@_ID': 'Jump.To',
96
+ Parameter: {
97
+ '@_Key': 'grid',
98
+ '#text': targetGridName,
99
+ },
100
+ },
101
+ };
102
+ }
103
+ case 'GO_BACK':
104
+ return {
105
+ Command: {
106
+ '@_ID': 'Jump.Back',
107
+ },
108
+ };
109
+ case 'GO_HOME':
110
+ return {
111
+ Command: {
112
+ '@_ID': 'Jump.Home',
113
+ },
114
+ };
115
+ case 'DELETE_WORD':
116
+ return {
117
+ Command: {
118
+ '@_ID': 'Action.DeleteWord',
119
+ },
120
+ };
121
+ case 'DELETE_CHARACTER':
122
+ return {
123
+ Command: {
124
+ '@_ID': 'Action.DeleteLetter',
125
+ },
126
+ };
127
+ case 'CLEAR_TEXT':
128
+ return {
129
+ Command: {
130
+ '@_ID': 'Action.Clear',
131
+ },
132
+ };
133
+ case 'SPEAK_TEXT':
134
+ case 'SPEAK_IMMEDIATE':
135
+ // For communication buttons, insert text into message bar (sentence building)
136
+ // Grid3 requires explicit trailing space for automatic word spacing
137
+ // Use two <s> elements: one for the word, one for the space (CDATA preserves whitespace)
138
+ // Users can speak the complete sentence with a dedicated Speak button
139
+ {
140
+ let text = semanticAction.text || button.message || button.label || '';
141
+ // Remove trailing space from message if present (we'll add it as separate segment)
142
+ if (text.endsWith(' ')) {
143
+ text = text.slice(0, -1);
144
+ }
145
+ return {
146
+ Command: {
147
+ '@_ID': 'Action.InsertText',
148
+ Parameter: {
149
+ '@_Key': 'text',
150
+ p: {
151
+ s: [
152
+ {
153
+ r: text,
154
+ },
155
+ {
156
+ r: { '__cdata': ' ' },
157
+ },
158
+ ],
159
+ },
160
+ },
161
+ },
162
+ };
163
+ }
164
+ case 'INSERT_TEXT':
165
+ // Add trailing space for word buttons to enable sentence building
166
+ // Use two <s> elements: one for the word, one for the space (CDATA preserves whitespace)
167
+ {
168
+ let text = semanticAction.text || button.message || button.label || '';
169
+ // Remove trailing space from message if present (we'll add it as separate segment)
170
+ if (text.endsWith(' ')) {
171
+ text = text.slice(0, -1);
172
+ }
173
+ return {
174
+ Command: {
175
+ '@_ID': 'Action.InsertText',
176
+ Parameter: {
177
+ '@_Key': 'text',
178
+ p: {
179
+ s: [
180
+ {
181
+ r: text,
182
+ },
183
+ {
184
+ r: { '__cdata': ' ' },
185
+ },
186
+ ],
187
+ },
188
+ },
189
+ },
190
+ };
191
+ }
192
+ default:
193
+ // Fallback to insert text with structured XML format
194
+ // Use two <s> elements: one for the word, one for the space (CDATA preserves whitespace)
195
+ {
196
+ let text = semanticAction.text || button.message || button.label || '';
197
+ // Remove trailing space from message if present (we'll add it as separate segment)
198
+ if (text.endsWith(' ')) {
199
+ text = text.slice(0, -1);
200
+ }
201
+ return {
202
+ Command: {
203
+ '@_ID': 'Action.InsertText',
204
+ Parameter: {
205
+ '@_Key': 'text',
206
+ p: {
207
+ s: [
208
+ {
209
+ r: text,
210
+ },
211
+ {
212
+ r: { '__cdata': ' ' },
213
+ },
214
+ ],
215
+ },
216
+ },
217
+ },
218
+ };
219
+ }
220
+ }
221
+ }
222
+ // Helper function to convert Grid 3 style to AACStyle
223
+ convertGrid3StyleToAACStyle(grid3Style) {
224
+ if (!grid3Style)
225
+ return {};
226
+ return {
227
+ backgroundColor: grid3Style.BackColour || grid3Style.TileColour,
228
+ borderColor: grid3Style.BorderColour,
229
+ fontColor: grid3Style.FontColour,
230
+ fontFamily: grid3Style.FontName,
231
+ fontSize: grid3Style.FontSize ? parseInt(String(grid3Style.FontSize)) : undefined,
232
+ };
233
+ }
234
+ // Helper function to get style by ID or return default
235
+ getStyleById(styles, styleId) {
236
+ if (!styleId || !styles.has(styleId)) {
237
+ return {};
238
+ }
239
+ return this.convertGrid3StyleToAACStyle(styles.get(styleId));
240
+ }
241
+ // Helper to safely extract text from XML parser values
242
+ textOf(val) {
243
+ if (!val)
244
+ return undefined;
245
+ if (typeof val === 'string')
246
+ return val;
247
+ if (typeof val === 'object' && '#text' in val)
248
+ return String(val['#text']);
249
+ return undefined;
250
+ }
251
+ extractTexts(filePathOrBuffer) {
252
+ const buffer = Buffer.isBuffer(filePathOrBuffer)
253
+ ? filePathOrBuffer
254
+ : fs_1.default.readFileSync(filePathOrBuffer);
255
+ const tree = this.loadIntoTree(buffer);
256
+ const texts = [];
257
+ for (const pageId in tree.pages) {
258
+ const page = tree.pages[pageId];
259
+ if (page.name)
260
+ texts.push(page.name);
261
+ page.buttons.forEach((btn) => {
262
+ if (btn.label)
263
+ texts.push(btn.label);
264
+ if (btn.message && btn.message !== btn.label)
265
+ texts.push(btn.message);
266
+ });
267
+ }
268
+ return texts;
269
+ }
270
+ loadIntoTree(filePathOrBuffer) {
271
+ const tree = new treeStructure_1.AACTree();
272
+ let zip;
273
+ try {
274
+ zip = new adm_zip_1.default(filePathOrBuffer);
275
+ }
276
+ catch (error) {
277
+ throw new Error(`Invalid ZIP file format: ${error.message}`);
278
+ }
279
+ const parser = new fast_xml_parser_1.XMLParser({ ignoreAttributes: false });
280
+ // Parse FileMap.xml if present to index dynamic files per grid
281
+ const fileMapIndex = new Map();
282
+ try {
283
+ const fmEntry = zip.getEntries().find((e) => e.entryName.endsWith('FileMap.xml'));
284
+ if (fmEntry) {
285
+ const fmXml = fmEntry.getData().toString('utf8');
286
+ const fmData = parser.parse(fmXml);
287
+ const entries = fmData?.FileMap?.Entries?.Entry || fmData?.fileMap?.entries?.entry;
288
+ if (entries) {
289
+ const arr = Array.isArray(entries) ? entries : [entries];
290
+ for (const ent of arr) {
291
+ const staticFile = (ent['@_StaticFile'] ||
292
+ ent.StaticFile ||
293
+ ent.staticFile ||
294
+ '').replace(/\\/g, '/');
295
+ if (!staticFile)
296
+ continue;
297
+ const df = ent.DynamicFiles || ent.dynamicFiles;
298
+ const candidates = df?.File || df?.file || df?.Files || df?.files;
299
+ const list = Array.isArray(candidates) ? candidates : candidates ? [candidates] : [];
300
+ const files = [];
301
+ for (const v of list) {
302
+ if (!v)
303
+ continue;
304
+ if (typeof v === 'string')
305
+ files.push(v.replace(/\\/g, '/'));
306
+ else if (typeof v === 'object' && '#text' in v)
307
+ files.push(String(v['#text']).replace(/\\/g, '/'));
308
+ }
309
+ fileMapIndex.set(staticFile, files);
310
+ }
311
+ }
312
+ }
313
+ }
314
+ catch (e) {
315
+ /* ignore: optional FileMap.xml may be missing or malformed */
316
+ }
317
+ // First, load styles from Settings0/Styles/styles.xml (Grid3 format)
318
+ const styles = new Map();
319
+ const styleEntry = zip
320
+ .getEntries()
321
+ .find((entry) => entry.entryName.endsWith('styles.xml') || entry.entryName.endsWith('style.xml'));
322
+ if (styleEntry) {
323
+ try {
324
+ const styleXmlContent = styleEntry.getData().toString('utf8');
325
+ const styleData = parser.parse(styleXmlContent);
326
+ // Parse styles and store them in the map
327
+ // Grid3 uses StyleData.Styles.Style with Key attribute
328
+ if (styleData.StyleData?.Styles?.Style) {
329
+ const styleArray = Array.isArray(styleData.StyleData.Styles.Style)
330
+ ? styleData.StyleData.Styles.Style
331
+ : [styleData.StyleData.Styles.Style];
332
+ styleArray.forEach((style) => {
333
+ if (style['@_Key']) {
334
+ styles.set(String(style['@_Key']), style);
335
+ }
336
+ });
337
+ }
338
+ // Also handle legacy format with @_ID
339
+ else if (styleData.Styles?.Style) {
340
+ const styleArray = Array.isArray(styleData.Styles.Style)
341
+ ? styleData.Styles.Style
342
+ : [styleData.Styles.Style];
343
+ styleArray.forEach((style) => {
344
+ if (style['@_ID']) {
345
+ styles.set(String(style['@_ID']), style);
346
+ }
347
+ });
348
+ }
349
+ }
350
+ catch (e) {
351
+ console.warn('Failed to parse styles.xml:', e);
352
+ }
353
+ }
354
+ // Debug: log all entry names
355
+ // console.log('Gridset zip entries:', zip.getEntries().map(e => e.entryName));
356
+ // First pass: collect all grid names and IDs for navigation resolution
357
+ const gridNameToIdMap = new Map();
358
+ const gridIdToNameMap = new Map();
359
+ zip.getEntries().forEach((entry) => {
360
+ if (entry.entryName.startsWith('Grids/') && entry.entryName.endsWith('grid.xml')) {
361
+ try {
362
+ const xmlContent = entry.getData().toString('utf8');
363
+ const data = parser.parse(xmlContent);
364
+ const grid = data.Grid || data.grid;
365
+ if (!grid)
366
+ return;
367
+ const gridId = this.textOf(grid.GridGuid || grid.gridGuid || grid.id);
368
+ let gridName = this.textOf(grid.Name) || this.textOf(grid.name) || this.textOf(grid['@_Name']);
369
+ if (!gridName) {
370
+ const match = entry.entryName.match(/^Grids\/([^/]+)\//);
371
+ if (match)
372
+ gridName = match[1];
373
+ }
374
+ if (gridId && gridName) {
375
+ gridNameToIdMap.set(gridName, gridId);
376
+ gridIdToNameMap.set(gridId, gridName);
377
+ }
378
+ }
379
+ catch (e) {
380
+ // Skip errors in first pass
381
+ }
382
+ }
383
+ });
384
+ // Second pass: process each grid file in the gridset
385
+ zip.getEntries().forEach((entry) => {
386
+ // Only process files named grid.xml under Grids/ (any subdir)
387
+ if (entry.entryName.startsWith('Grids/') && entry.entryName.endsWith('grid.xml')) {
388
+ let xmlContent;
389
+ try {
390
+ xmlContent = entry.getData().toString('utf8');
391
+ }
392
+ catch (e) {
393
+ // Skip unreadable files
394
+ return;
395
+ }
396
+ let data;
397
+ try {
398
+ data = parser.parse(xmlContent);
399
+ }
400
+ catch (error) {
401
+ // Skip malformed XML but log the specific error
402
+ console.warn(`Malformed XML in ${entry.entryName}: ${error.message}`);
403
+ return;
404
+ }
405
+ // Grid3 XML: <Grid> root
406
+ const grid = data.Grid || data.grid;
407
+ if (!grid) {
408
+ return;
409
+ }
410
+ // Defensive: GridGuid and Name required
411
+ const gridId = this.textOf(grid.GridGuid || grid.gridGuid || grid.id);
412
+ let gridName = this.textOf(grid.Name) || this.textOf(grid.name) || this.textOf(grid['@_Name']);
413
+ if (!gridName) {
414
+ // Fallback: get folder name from entry path
415
+ const match = entry.entryName.match(/^Grids\/([^/]+)\//);
416
+ if (match)
417
+ gridName = match[1];
418
+ }
419
+ if (!gridId || !gridName) {
420
+ return;
421
+ }
422
+ const page = new treeStructure_1.AACPage({
423
+ id: String(gridId),
424
+ name: String(gridName),
425
+ grid: [],
426
+ buttons: [],
427
+ parentId: null,
428
+ style: {
429
+ backgroundColor: grid.BackgroundColour || grid.backgroundColour,
430
+ },
431
+ });
432
+ // Calculate grid dimensions from ColumnDefinitions and RowDefinitions
433
+ const columnDefs = grid.ColumnDefinitions?.ColumnDefinition || [];
434
+ const rowDefs = grid.RowDefinitions?.RowDefinition || [];
435
+ const maxCols = Array.isArray(columnDefs) ? columnDefs.length : columnDefs ? 1 : 5;
436
+ const maxRows = Array.isArray(rowDefs) ? rowDefs.length : rowDefs ? 1 : 4;
437
+ // Process buttons: <Cells><Cell>
438
+ const cells = grid.Cells?.Cell || grid.cells?.cell;
439
+ if (cells) {
440
+ // Cells may be array or single object
441
+ const cellArr = Array.isArray(cells) ? cells : [cells];
442
+ // Create a 2D grid to track button positions
443
+ const gridLayout = [];
444
+ for (let r = 0; r < maxRows; r++) {
445
+ gridLayout[r] = new Array(maxCols).fill(null);
446
+ }
447
+ cellArr.forEach((cell, idx) => {
448
+ if (!cell || !cell.Content)
449
+ return;
450
+ // Extract position information from cell attributes
451
+ // Grid3 uses 1-based coordinates, convert to 0-based for internal use
452
+ const cellX = Math.max(0, parseInt(String(cell['@_X'] || '1'), 10) - 1);
453
+ const cellY = Math.max(0, parseInt(String(cell['@_Y'] || '1'), 10) - 1);
454
+ const colSpan = parseInt(String(cell['@_ColumnSpan'] || '1'), 10);
455
+ const rowSpan = parseInt(String(cell['@_RowSpan'] || '1'), 10);
456
+ // Extract label from CaptionAndImage/Caption
457
+ const content = cell.Content;
458
+ const captionAndImage = content.CaptionAndImage || content.captionAndImage;
459
+ let label = captionAndImage?.Caption || captionAndImage?.caption || '';
460
+ // If no caption, try other sources or create a placeholder
461
+ if (!label) {
462
+ // For cells without captions (like AutoContent cells), create a meaningful label
463
+ if (content.ContentType === 'AutoContent') {
464
+ label = `AutoContent_${idx}`;
465
+ }
466
+ else {
467
+ return; // Skip cells without labels
468
+ }
469
+ }
470
+ const message = label; // Use caption as message
471
+ // Parse all command types from Grid3 and create semantic actions
472
+ let semanticAction;
473
+ let legacyAction = null;
474
+ // infer action type implicitly from commands; no explicit enum needed
475
+ let navigationTarget;
476
+ const commands = content.Commands?.Command || content.commands?.command;
477
+ // Resolve image for this cell using FileMap and coordinate heuristics
478
+ const imageCandidate = captionAndImage?.Image ||
479
+ captionAndImage?.image ||
480
+ captionAndImage?.ImageName ||
481
+ captionAndImage?.imageName ||
482
+ captionAndImage?.Symbol ||
483
+ captionAndImage?.symbol;
484
+ const declaredImageName = imageCandidate ? this.textOf(imageCandidate) : undefined;
485
+ const gridEntryPath = entry.entryName.replace(/\\/g, '/');
486
+ const baseDir = gridEntryPath.replace(/\/grid\.xml$/, '/');
487
+ const dynamicFiles = fileMapIndex.get(gridEntryPath) || [];
488
+ const resolvedImageEntry = (0, resolver_1.resolveGrid3CellImage)(zip, {
489
+ baseDir,
490
+ imageName: declaredImageName,
491
+ x: cellX + 1,
492
+ y: cellY + 1,
493
+ dynamicFiles,
494
+ }) || undefined;
495
+ if (commands) {
496
+ const commandArr = Array.isArray(commands) ? commands : [commands];
497
+ for (const command of commandArr) {
498
+ const commandId = command['@_ID'] || command.ID || command.id;
499
+ const parameters = command.Parameter || command.parameter;
500
+ const paramArr = parameters
501
+ ? Array.isArray(parameters)
502
+ ? parameters
503
+ : [parameters]
504
+ : [];
505
+ // Helper to extract text from Grid3's structured format <p><s><r>text</r></s></p>
506
+ const extractStructuredText = (param) => {
507
+ // Try to extract from nested p.s structure
508
+ if (param.p) {
509
+ const p = param.p;
510
+ // Handle p.s array or single s element
511
+ const sElements = Array.isArray(p.s) ? p.s : p.s ? [p.s] : [];
512
+ // Extract all r values and concatenate
513
+ const parts = [];
514
+ for (const s of sElements) {
515
+ if (s && s.r !== undefined) {
516
+ parts.push(String(s.r));
517
+ }
518
+ }
519
+ if (parts.length > 0) {
520
+ return parts.join('');
521
+ }
522
+ }
523
+ return undefined;
524
+ };
525
+ // Helper to get parameter value
526
+ const getParam = (key) => {
527
+ if (!parameters)
528
+ return undefined;
529
+ for (const param of paramArr) {
530
+ if (param['@_Key'] === key || param.Key === key || param.key === key) {
531
+ // First try simple #text value
532
+ const simpleValue = param['#text'] ?? param.text ?? param.value;
533
+ if (typeof simpleValue === 'string') {
534
+ return simpleValue;
535
+ }
536
+ // Try to extract from structured format (Grid3's <p><s><r> format)
537
+ const structuredValue = extractStructuredText(param);
538
+ if (structuredValue !== undefined) {
539
+ return structuredValue;
540
+ }
541
+ // Fallback to string conversion
542
+ if (typeof param === 'string') {
543
+ return param;
544
+ }
545
+ }
546
+ }
547
+ return undefined;
548
+ };
549
+ switch (commandId) {
550
+ case 'Jump.To': {
551
+ const gridTarget = getParam('grid');
552
+ if (gridTarget) {
553
+ // Resolve grid name to grid ID for navigation
554
+ const targetGridId = gridNameToIdMap.get(gridTarget) || gridTarget;
555
+ navigationTarget = targetGridId;
556
+ // navigate action
557
+ semanticAction = {
558
+ category: treeStructure_1.AACSemanticCategory.NAVIGATION,
559
+ intent: treeStructure_1.AACSemanticIntent.NAVIGATE_TO,
560
+ targetId: targetGridId,
561
+ platformData: {
562
+ grid3: {
563
+ commandId,
564
+ parameters: { grid: gridTarget },
565
+ },
566
+ },
567
+ fallback: {
568
+ type: 'NAVIGATE',
569
+ targetPageId: targetGridId,
570
+ },
571
+ };
572
+ legacyAction = {
573
+ type: 'NAVIGATE',
574
+ targetPageId: targetGridId,
575
+ };
576
+ }
577
+ break;
578
+ }
579
+ case 'Jump.Back':
580
+ // action
581
+ semanticAction = {
582
+ category: treeStructure_1.AACSemanticCategory.NAVIGATION,
583
+ intent: treeStructure_1.AACSemanticIntent.GO_BACK,
584
+ platformData: {
585
+ grid3: {
586
+ commandId,
587
+ parameters: {},
588
+ },
589
+ },
590
+ fallback: {
591
+ type: 'ACTION',
592
+ message: 'Go back',
593
+ },
594
+ };
595
+ legacyAction = {
596
+ type: 'GO_BACK',
597
+ };
598
+ break;
599
+ case 'Jump.Home':
600
+ // action
601
+ semanticAction = {
602
+ category: treeStructure_1.AACSemanticCategory.NAVIGATION,
603
+ intent: treeStructure_1.AACSemanticIntent.GO_HOME,
604
+ platformData: {
605
+ grid3: {
606
+ commandId,
607
+ parameters: {},
608
+ },
609
+ },
610
+ fallback: {
611
+ type: 'ACTION',
612
+ message: 'Go home',
613
+ },
614
+ };
615
+ legacyAction = {
616
+ type: 'GO_HOME',
617
+ };
618
+ break;
619
+ case 'Action.Speak': {
620
+ // speak
621
+ const speakUnit = getParam('unit');
622
+ const moveCaret = getParam('movecaret');
623
+ semanticAction = {
624
+ category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
625
+ intent: treeStructure_1.AACSemanticIntent.SPEAK_TEXT,
626
+ platformData: {
627
+ grid3: {
628
+ commandId,
629
+ parameters: {
630
+ unit: speakUnit,
631
+ movecaret: moveCaret,
632
+ },
633
+ },
634
+ },
635
+ fallback: {
636
+ type: 'SPEAK',
637
+ message: 'Speak text',
638
+ },
639
+ };
640
+ legacyAction = {
641
+ type: 'SPEAK',
642
+ unit: speakUnit,
643
+ moveCaret: moveCaret ? parseInt(String(moveCaret)) : undefined,
644
+ };
645
+ break;
646
+ }
647
+ case 'Action.InsertText': {
648
+ // speak
649
+ const insertText = getParam('text');
650
+ semanticAction = {
651
+ category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
652
+ intent: treeStructure_1.AACSemanticIntent.INSERT_TEXT,
653
+ text: insertText,
654
+ platformData: {
655
+ grid3: {
656
+ commandId,
657
+ parameters: { text: insertText },
658
+ },
659
+ },
660
+ fallback: {
661
+ type: 'SPEAK',
662
+ message: insertText,
663
+ },
664
+ };
665
+ legacyAction = {
666
+ type: 'INSERT_TEXT',
667
+ text: insertText,
668
+ };
669
+ break;
670
+ }
671
+ case 'Action.DeleteWord':
672
+ // action
673
+ semanticAction = {
674
+ category: treeStructure_1.AACSemanticCategory.TEXT_EDITING,
675
+ intent: treeStructure_1.AACSemanticIntent.DELETE_WORD,
676
+ platformData: {
677
+ grid3: {
678
+ commandId,
679
+ parameters: {},
680
+ },
681
+ },
682
+ fallback: {
683
+ type: 'ACTION',
684
+ message: 'Delete word',
685
+ },
686
+ };
687
+ legacyAction = {
688
+ type: 'DELETE_WORD',
689
+ };
690
+ break;
691
+ case 'Action.DeleteLetter':
692
+ // action
693
+ semanticAction = {
694
+ category: treeStructure_1.AACSemanticCategory.TEXT_EDITING,
695
+ intent: treeStructure_1.AACSemanticIntent.DELETE_CHARACTER,
696
+ platformData: {
697
+ grid3: {
698
+ commandId,
699
+ parameters: {},
700
+ },
701
+ },
702
+ fallback: {
703
+ type: 'ACTION',
704
+ message: 'Delete character',
705
+ },
706
+ };
707
+ legacyAction = {
708
+ type: 'DELETE_CHARACTER',
709
+ };
710
+ break;
711
+ case 'Action.Clear':
712
+ // action
713
+ semanticAction = {
714
+ category: treeStructure_1.AACSemanticCategory.TEXT_EDITING,
715
+ intent: treeStructure_1.AACSemanticIntent.CLEAR_TEXT,
716
+ platformData: {
717
+ grid3: {
718
+ commandId,
719
+ parameters: {},
720
+ },
721
+ },
722
+ fallback: {
723
+ type: 'ACTION',
724
+ message: 'Clear text',
725
+ },
726
+ };
727
+ legacyAction = {
728
+ type: 'CLEAR_TEXT',
729
+ };
730
+ break;
731
+ case 'Action.Letter': {
732
+ // action
733
+ const letter = getParam('letter');
734
+ semanticAction = {
735
+ category: treeStructure_1.AACSemanticCategory.TEXT_EDITING,
736
+ intent: treeStructure_1.AACSemanticIntent.INSERT_TEXT,
737
+ text: letter,
738
+ platformData: {
739
+ grid3: {
740
+ commandId,
741
+ parameters: { letter },
742
+ },
743
+ },
744
+ fallback: {
745
+ type: 'ACTION',
746
+ message: letter,
747
+ },
748
+ };
749
+ legacyAction = {
750
+ type: 'INSERT_LETTER',
751
+ letter,
752
+ };
753
+ break;
754
+ }
755
+ case 'Settings.RestAll':
756
+ // action
757
+ semanticAction = {
758
+ category: treeStructure_1.AACSemanticCategory.CUSTOM,
759
+ intent: treeStructure_1.AACSemanticIntent.PLATFORM_SPECIFIC,
760
+ platformData: {
761
+ grid3: {
762
+ commandId,
763
+ parameters: {
764
+ indicatorenabled: getParam('indicatorenabled'),
765
+ action: getParam('action'),
766
+ },
767
+ },
768
+ },
769
+ fallback: {
770
+ type: 'ACTION',
771
+ message: 'Settings action',
772
+ },
773
+ };
774
+ legacyAction = {
775
+ type: 'SETTINGS',
776
+ indicatorEnabled: getParam('indicatorenabled') === '1',
777
+ settingsAction: getParam('action'),
778
+ };
779
+ break;
780
+ case 'AutoContent.Activate':
781
+ // action
782
+ semanticAction = {
783
+ category: treeStructure_1.AACSemanticCategory.CUSTOM,
784
+ intent: treeStructure_1.AACSemanticIntent.PLATFORM_SPECIFIC,
785
+ platformData: {
786
+ grid3: {
787
+ commandId,
788
+ parameters: {
789
+ autocontenttype: getParam('autocontenttype'),
790
+ },
791
+ },
792
+ },
793
+ fallback: {
794
+ type: 'ACTION',
795
+ message: 'Auto content',
796
+ },
797
+ };
798
+ legacyAction = {
799
+ type: 'AUTO_CONTENT',
800
+ autoContentType: getParam('autocontenttype'),
801
+ };
802
+ break;
803
+ default:
804
+ // Unknown command - preserve as generic action
805
+ if (commandId) {
806
+ // action
807
+ const allParams = Object.fromEntries(paramArr.map((p) => [p.Key || p.key, p['#text']]));
808
+ semanticAction = {
809
+ category: treeStructure_1.AACSemanticCategory.CUSTOM,
810
+ intent: treeStructure_1.AACSemanticIntent.PLATFORM_SPECIFIC,
811
+ platformData: {
812
+ grid3: {
813
+ commandId,
814
+ parameters: allParams,
815
+ },
816
+ },
817
+ fallback: {
818
+ type: 'ACTION',
819
+ message: 'Unknown command',
820
+ },
821
+ };
822
+ legacyAction = {
823
+ type: 'SPEAK',
824
+ parameters: { commandId, ...allParams },
825
+ };
826
+ }
827
+ break;
828
+ }
829
+ // Use first recognized command
830
+ if (semanticAction || legacyAction)
831
+ break;
832
+ }
833
+ }
834
+ // Create default semantic action if none was created from commands
835
+ if (!semanticAction) {
836
+ semanticAction = {
837
+ category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
838
+ intent: treeStructure_1.AACSemanticIntent.SPEAK_TEXT,
839
+ text: String(message),
840
+ fallback: {
841
+ type: 'SPEAK',
842
+ message: String(message),
843
+ },
844
+ };
845
+ }
846
+ // Get style information from cell attributes and Content.Style
847
+ let cellStyleId = cell['@_StyleID'] || cell['@_styleid'];
848
+ // Grid3 format: check Content.Style.BasedOnStyle
849
+ if (!cellStyleId && content.Style?.BasedOnStyle) {
850
+ cellStyleId = content.Style.BasedOnStyle;
851
+ }
852
+ const cellStyle = this.getStyleById(styles, cellStyleId ? String(cellStyleId) : undefined);
853
+ // Also check for inline style overrides
854
+ const inlineStyle = {};
855
+ if (cell['@_BackColour'])
856
+ inlineStyle.backgroundColor = cell['@_BackColour'];
857
+ if (cell['@_FontColour'])
858
+ inlineStyle.fontColor = cell['@_FontColour'];
859
+ if (cell['@_BorderColour'])
860
+ inlineStyle.borderColor = cell['@_BorderColour'];
861
+ // Grid3 inline styles from Content.Style
862
+ if (content.Style) {
863
+ if (content.Style.BackColour)
864
+ inlineStyle.backgroundColor = content.Style.BackColour;
865
+ if (content.Style.FontColour)
866
+ inlineStyle.fontColor = content.Style.FontColour;
867
+ if (content.Style.BorderColour)
868
+ inlineStyle.borderColor = content.Style.BorderColour;
869
+ if (content.Style.FontName)
870
+ inlineStyle.fontFamily = content.Style.FontName;
871
+ if (content.Style.FontSize)
872
+ inlineStyle.fontSize = parseInt(String(content.Style.FontSize));
873
+ }
874
+ const button = new treeStructure_1.AACButton({
875
+ id: `${gridId}_btn_${idx}`,
876
+ label: String(label),
877
+ message: String(message),
878
+ targetPageId: navigationTarget ? String(navigationTarget) : undefined,
879
+ semanticAction: semanticAction,
880
+ image: declaredImageName,
881
+ resolvedImageEntry: resolvedImageEntry,
882
+ x: cellX,
883
+ y: cellY,
884
+ columnSpan: colSpan,
885
+ rowSpan: rowSpan,
886
+ style: {
887
+ ...cellStyle,
888
+ ...inlineStyle, // Inline styles override referenced styles
889
+ },
890
+ });
891
+ // Add button to page
892
+ page.addButton(button);
893
+ // Place button in grid layout (handle colspan/rowspan)
894
+ for (let r = cellY; r < cellY + rowSpan && r < maxRows; r++) {
895
+ for (let c = cellX; c < cellX + colSpan && c < maxCols; c++) {
896
+ if (gridLayout[r] && gridLayout[r][c] === null) {
897
+ gridLayout[r][c] = button;
898
+ }
899
+ }
900
+ }
901
+ });
902
+ // Set the page's grid layout
903
+ page.grid = gridLayout;
904
+ }
905
+ tree.addPage(page);
906
+ }
907
+ });
908
+ // After all pages are loaded, set parentId for navigation targets
909
+ for (const pageId in tree.pages) {
910
+ const page = tree.pages[pageId];
911
+ page.buttons.forEach((btn) => {
912
+ if (btn.semanticAction?.intent === treeStructure_1.AACSemanticIntent.NAVIGATE_TO && btn.targetPageId) {
913
+ const targetPage = tree.getPage(btn.targetPageId);
914
+ if (targetPage) {
915
+ targetPage.parentId = page.id;
916
+ }
917
+ }
918
+ });
919
+ }
920
+ // Read settings.xml to get the StartGrid (home page)
921
+ try {
922
+ const settingsEntry = zip.getEntries().find((e) => e.entryName.endsWith('settings.xml'));
923
+ if (settingsEntry) {
924
+ const settingsXml = settingsEntry.getData().toString('utf8');
925
+ const settingsData = parser.parse(settingsXml);
926
+ const startGridName = settingsData?.GridSetSettings?.StartGrid ||
927
+ settingsData?.gridSetSettings?.startGrid ||
928
+ settingsData?.GridsetSettings?.StartGrid;
929
+ if (startGridName && typeof startGridName === 'string') {
930
+ // Resolve the grid name to grid ID
931
+ const homeGridId = gridNameToIdMap.get(startGridName);
932
+ if (homeGridId) {
933
+ tree.rootId = homeGridId;
934
+ }
935
+ }
936
+ }
937
+ }
938
+ catch (e) {
939
+ // If settings.xml parsing fails, tree.rootId will default to first page
940
+ }
941
+ return tree;
942
+ }
943
+ processTexts(filePathOrBuffer, translations, outputPath) {
944
+ // Load the tree, apply translations, and save to new file
945
+ const buffer = Buffer.isBuffer(filePathOrBuffer)
946
+ ? filePathOrBuffer
947
+ : fs_1.default.readFileSync(filePathOrBuffer);
948
+ const tree = this.loadIntoTree(buffer);
949
+ // Apply translations to all text content
950
+ Object.values(tree.pages).forEach((page) => {
951
+ // Translate page names
952
+ if (page.name && translations.has(page.name)) {
953
+ const tPage = translations.get(page.name);
954
+ if (tPage)
955
+ page.name = tPage;
956
+ }
957
+ // Translate button labels and messages
958
+ page.buttons.forEach((button) => {
959
+ if (button.label && translations.has(button.label)) {
960
+ const tLabel = translations.get(button.label);
961
+ if (tLabel)
962
+ button.label = tLabel;
963
+ }
964
+ if (button.message && translations.has(button.message)) {
965
+ const tMsg = translations.get(button.message);
966
+ if (tMsg)
967
+ button.message = tMsg;
968
+ }
969
+ });
970
+ });
971
+ // Save the translated tree and return its content
972
+ this.saveFromTree(tree, outputPath);
973
+ return fs_1.default.readFileSync(outputPath);
974
+ }
975
+ saveFromTree(tree, outputPath) {
976
+ const zip = new adm_zip_1.default();
977
+ if (Object.keys(tree.pages).length === 0) {
978
+ // Create empty zip for empty tree
979
+ zip.writeZip(outputPath);
980
+ return;
981
+ }
982
+ // Collect all unique styles from pages and buttons
983
+ const uniqueStyles = new Map();
984
+ let styleIdCounter = 1;
985
+ // Track images that need to be written to the ZIP
986
+ // Maps button ID to image data for buttons with images
987
+ const buttonImages = new Map();
988
+ // Helper function to add style and return its ID
989
+ const addStyle = (style) => {
990
+ if (!style || typeof style !== 'object')
991
+ return '';
992
+ const obj = style;
993
+ if (Object.keys(obj).length === 0)
994
+ return '';
995
+ const styleKey = JSON.stringify(obj);
996
+ const existing = uniqueStyles.get(styleKey);
997
+ if (existing)
998
+ return existing.id;
999
+ const styleId = `Style${styleIdCounter++}`;
1000
+ uniqueStyles.set(styleKey, { id: styleId, style: obj });
1001
+ return styleId;
1002
+ };
1003
+ // Collect styles from all pages and buttons
1004
+ Object.values(tree.pages).forEach((page) => {
1005
+ if (page.style)
1006
+ addStyle(page.style);
1007
+ page.buttons.forEach((button) => {
1008
+ if (button.style)
1009
+ addStyle(button.style);
1010
+ });
1011
+ });
1012
+ // Get the home/start grid from tree.rootId, fallback to first page
1013
+ const pages = Object.values(tree.pages);
1014
+ let startGrid = '';
1015
+ if (tree.rootId) {
1016
+ const homePage = tree.getPage(tree.rootId);
1017
+ if (homePage) {
1018
+ startGrid = homePage.name || homePage.id;
1019
+ }
1020
+ }
1021
+ // Fallback to first page if no rootId or page not found
1022
+ if (!startGrid && pages.length > 0) {
1023
+ startGrid = pages[0].name || pages[0].id;
1024
+ }
1025
+ // Create Settings0/settings.xml with proper Grid3 structure
1026
+ const settingsData = {
1027
+ '?xml': { '@_version': '1.0', '@_encoding': 'UTF-8' },
1028
+ GridSetSettings: {
1029
+ '@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
1030
+ StartGrid: startGrid,
1031
+ // Add other common Grid3 settings
1032
+ ScanEnabled: 'false',
1033
+ ScanTimeoutMs: '2000',
1034
+ HoverEnabled: 'false',
1035
+ HoverTimeoutMs: '1000',
1036
+ MouseclickEnabled: 'true',
1037
+ Language: 'en-US',
1038
+ },
1039
+ };
1040
+ const settingsBuilder = new fast_xml_parser_1.XMLBuilder({
1041
+ ignoreAttributes: false,
1042
+ format: true,
1043
+ indentBy: ' ',
1044
+ suppressEmptyNode: true,
1045
+ });
1046
+ const settingsXmlContent = settingsBuilder.build(settingsData);
1047
+ zip.addFile('Settings0/settings.xml', Buffer.from(settingsXmlContent, 'utf8'));
1048
+ // Create Settings0/Styles/style.xml if there are styles
1049
+ if (uniqueStyles.size > 0) {
1050
+ const stylesArray = Array.from(uniqueStyles.values()).map(({ id, style }) => {
1051
+ const styleObj = {
1052
+ '@_Key': id,
1053
+ // When TileColour is present, BackColour is the surround (outer area)
1054
+ // For "None" surround, just use BackColour for the fill (no TileColour)
1055
+ BackColour: this.ensureAlphaChannel(style.backgroundColor),
1056
+ BorderColour: this.ensureAlphaChannel(style.borderColor) || '#000000FF',
1057
+ FontColour: this.ensureAlphaChannel(style.fontColor) || '#000000FF',
1058
+ FontName: style.fontFamily || 'Arial',
1059
+ FontSize: style.fontSize?.toString() || '16',
1060
+ };
1061
+ // Don't add TileColour - just use BackColour as the fill color
1062
+ return styleObj;
1063
+ });
1064
+ const styleData = {
1065
+ '?xml': { '@_version': '1.0', '@_encoding': 'UTF-8' },
1066
+ StyleData: {
1067
+ '@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
1068
+ Styles: {
1069
+ Style: stylesArray,
1070
+ },
1071
+ },
1072
+ };
1073
+ const styleBuilder = new fast_xml_parser_1.XMLBuilder({
1074
+ ignoreAttributes: false,
1075
+ format: true,
1076
+ indentBy: ' ',
1077
+ });
1078
+ const styleXmlContent = styleBuilder.build(styleData);
1079
+ zip.addFile('Settings0/Styles/styles.xml', Buffer.from(styleXmlContent, 'utf8'));
1080
+ }
1081
+ // Collect grid file paths for FileMap.xml
1082
+ const gridFilePaths = [];
1083
+ // Create a grid for each page
1084
+ Object.values(tree.pages).forEach((page, index) => {
1085
+ const gridData = {
1086
+ Grid: {
1087
+ '@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
1088
+ GridGuid: page.id,
1089
+ // Calculate grid dimensions based on actual layout
1090
+ ColumnDefinitions: this.calculateColumnDefinitions(page),
1091
+ RowDefinitions: this.calculateRowDefinitions(page),
1092
+ AutoContentCommands: '',
1093
+ Cells: page.buttons.length > 0
1094
+ ? {
1095
+ Cell: [
1096
+ // Add workspace/message bar cell at the top of ALL pages
1097
+ // Grid3 uses 0-based coordinates; omit X and Y to use defaults (0, 0)
1098
+ {
1099
+ '@_ColumnSpan': 4,
1100
+ Content: {
1101
+ ContentType: 'Workspace',
1102
+ ContentSubType: 'Chat',
1103
+ Style: {
1104
+ BasedOnStyle: 'Workspace',
1105
+ },
1106
+ },
1107
+ },
1108
+ // Regular button cells
1109
+ ...this.filterPageButtons(page.buttons).map((button, btnIndex) => {
1110
+ const buttonStyleId = button.style ? addStyle(button.style) : '';
1111
+ // Find button position in grid layout
1112
+ const position = this.findButtonPosition(page, button, btnIndex);
1113
+ // Shift all buttons down by 1 row to make room for workspace
1114
+ const yOffset = 1;
1115
+ // Build CaptionAndImage object
1116
+ const captionAndImage = {
1117
+ Caption: button.label || '',
1118
+ };
1119
+ // Add image reference if button has an image
1120
+ // Grid3 uses coordinate-based naming: {x}-{y}-0-text-0.{ext}
1121
+ if (button.image) {
1122
+ // Try to determine file extension from image name or default to PNG
1123
+ let imageExt = 'png';
1124
+ if (button.image.match(/\.(png|jpg|jpeg|gif|svg)$/i)) {
1125
+ imageExt = button.image.match(/\.(png|jpg|jpeg|gif|svg)$/i)[1].toLowerCase();
1126
+ }
1127
+ // Grid3 dynamically constructs image filenames by prepending cell coordinates
1128
+ // The XML should only contain the suffix: -0-text-0.{ext}
1129
+ // Grid3 automatically adds the X-Y prefix based on the Cell's position
1130
+ captionAndImage.Image = `-0-text-0.${imageExt}`;
1131
+ // Extract image data from button parameters if available
1132
+ // (AstericsGridProcessor stores it there during loadIntoTree)
1133
+ let imageData = Buffer.alloc(0);
1134
+ if (button.parameters && button.parameters.imageData && Buffer.isBuffer(button.parameters.imageData)) {
1135
+ imageData = button.parameters.imageData;
1136
+ }
1137
+ // Store image data for later writing to ZIP
1138
+ buttonImages.set(button.id, {
1139
+ imageData: imageData,
1140
+ ext: imageExt,
1141
+ pageName: page.name || page.id,
1142
+ x: position.x,
1143
+ y: position.y + yOffset,
1144
+ });
1145
+ }
1146
+ const cellData = {
1147
+ '@_X': position.x, // Grid3 uses 0-based X coordinates (defaults to 0 when omitted)
1148
+ '@_Y': position.y + yOffset, // Grid3 uses 0-based Y coordinates with workspace offset
1149
+ '@_ColumnSpan': position.columnSpan,
1150
+ '@_RowSpan': position.rowSpan,
1151
+ Content: {
1152
+ Commands: this.generateCommandsFromSemanticAction(button, tree),
1153
+ CaptionAndImage: captionAndImage,
1154
+ },
1155
+ };
1156
+ // Add style reference and inline color overrides if available
1157
+ // Some Grid3 versions need inline colors in addition to style references
1158
+ if (buttonStyleId || button.style) {
1159
+ const styleObj = {};
1160
+ // Add style reference if we have one
1161
+ if (buttonStyleId) {
1162
+ styleObj.BasedOnStyle = buttonStyleId;
1163
+ }
1164
+ // Add inline color overrides for better Grid3 compatibility
1165
+ if (button.style?.backgroundColor) {
1166
+ // Use BackColour for fill (no TileColour means no surround, just the fill)
1167
+ styleObj.BackColour = this.ensureAlphaChannel(button.style.backgroundColor);
1168
+ }
1169
+ if (button.style?.borderColor) {
1170
+ styleObj.BorderColour = this.ensureAlphaChannel(button.style.borderColor);
1171
+ }
1172
+ if (button.style?.fontColor) {
1173
+ styleObj.FontColour = this.ensureAlphaChannel(button.style.fontColor);
1174
+ }
1175
+ if (button.style?.fontFamily) {
1176
+ styleObj.FontName = button.style.fontFamily;
1177
+ }
1178
+ if (button.style?.fontSize) {
1179
+ styleObj.FontSize = button.style.fontSize;
1180
+ }
1181
+ cellData.Content.Style = styleObj;
1182
+ }
1183
+ return cellData;
1184
+ }),
1185
+ ]
1186
+ }
1187
+ : { Cell: [] },
1188
+ },
1189
+ };
1190
+ // Convert to XML
1191
+ const builder = new fast_xml_parser_1.XMLBuilder({
1192
+ ignoreAttributes: false,
1193
+ format: true,
1194
+ indentBy: ' ',
1195
+ suppressEmptyNode: true,
1196
+ cdataPropName: '__cdata',
1197
+ });
1198
+ const xmlContent = builder.build(gridData);
1199
+ // Add to zip in Grids folder with proper Grid3 naming
1200
+ const gridPath = `Grids\\${page.name || page.id}\\grid.xml`;
1201
+ gridFilePaths.push(gridPath);
1202
+ zip.addFile(gridPath, Buffer.from(xmlContent, 'utf8'));
1203
+ });
1204
+ // Write image files to ZIP
1205
+ buttonImages.forEach((imgData, buttonId) => {
1206
+ if (imgData.imageData && imgData.imageData.length > 0) {
1207
+ // Create image path in the grid's directory
1208
+ const imagePath = `Grids\\${imgData.pageName}\\${imgData.x}-${imgData.y}-0-text-0.${imgData.ext}`;
1209
+ zip.addFile(imagePath, imgData.imageData);
1210
+ }
1211
+ });
1212
+ // Create FileMap.xml to map all grid files with their dynamic image files
1213
+ const fileMapData = {
1214
+ '?xml': { '@_version': '1.0', '@_encoding': 'UTF-8' },
1215
+ FileMap: {
1216
+ '@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
1217
+ Entries: {
1218
+ Entry: gridFilePaths.map((gridPath) => {
1219
+ // Find all image files for this grid
1220
+ const gridName = gridPath.match(/Grids\\([^\\]+)\\grid\.xml$/)?.[1] || '';
1221
+ const imageFiles = [];
1222
+ // Collect image filenames for buttons on this page
1223
+ // IMPORTANT: FileMap.xml requires full paths like "Grids\PageName\1-5-0-text-0.png"
1224
+ buttonImages.forEach((imgData, buttonId) => {
1225
+ if (imgData.pageName === gridName && imgData.imageData.length > 0) {
1226
+ const imagePath = `Grids\\${gridName}\\${imgData.x}-${imgData.y}-0-text-0.${imgData.ext}`;
1227
+ imageFiles.push(imagePath);
1228
+ }
1229
+ });
1230
+ return {
1231
+ '@_StaticFile': gridPath,
1232
+ DynamicFiles: imageFiles.length > 0 ? {
1233
+ File: imageFiles
1234
+ } : {},
1235
+ };
1236
+ }),
1237
+ },
1238
+ },
1239
+ };
1240
+ const fileMapBuilder = new fast_xml_parser_1.XMLBuilder({
1241
+ ignoreAttributes: false,
1242
+ format: true,
1243
+ indentBy: ' ',
1244
+ });
1245
+ const fileMapXmlContent = fileMapBuilder.build(fileMapData);
1246
+ zip.addFile('FileMap.xml', Buffer.from(fileMapXmlContent, 'utf8'));
1247
+ // Write the zip file
1248
+ zip.writeZip(outputPath);
1249
+ }
1250
+ // Helper method to calculate column definitions based on page layout
1251
+ calculateColumnDefinitions(page) {
1252
+ let maxCols = 4; // Default minimum
1253
+ if (page.grid && page.grid.length > 0) {
1254
+ maxCols = Math.max(maxCols, page.grid[0]?.length || 0);
1255
+ }
1256
+ else {
1257
+ // Fallback: estimate from button count
1258
+ maxCols = Math.max(4, Math.ceil(Math.sqrt(page.buttons.length)));
1259
+ }
1260
+ return {
1261
+ ColumnDefinition: Array(maxCols).fill({}),
1262
+ };
1263
+ }
1264
+ // Helper method to calculate row definitions based on page layout
1265
+ calculateRowDefinitions(page) {
1266
+ let maxRows = 4; // Default minimum
1267
+ if (page.grid && page.grid.length > 0) {
1268
+ maxRows = Math.max(maxRows, page.grid.length);
1269
+ }
1270
+ else {
1271
+ // Fallback: estimate from button count
1272
+ const estimatedCols = Math.ceil(Math.sqrt(page.buttons.length));
1273
+ maxRows = Math.max(4, Math.ceil(page.buttons.length / estimatedCols));
1274
+ }
1275
+ return {
1276
+ RowDefinition: Array(maxRows).fill({}),
1277
+ };
1278
+ }
1279
+ // Helper method to find button position with span information
1280
+ findButtonPosition(page, button, fallbackIndex) {
1281
+ if (page.grid && page.grid.length > 0) {
1282
+ // Search for button in grid layout and calculate span
1283
+ for (let y = 0; y < page.grid.length; y++) {
1284
+ for (let x = 0; x < page.grid[y].length; x++) {
1285
+ const current = page.grid[y][x];
1286
+ if (current && current.id === button.id) {
1287
+ // Calculate span by checking how far the same button extends
1288
+ let columnSpan = 1;
1289
+ let rowSpan = 1;
1290
+ // Check column span (rightward)
1291
+ while (x + columnSpan < page.grid[y].length) {
1292
+ const right = page.grid[y][x + columnSpan];
1293
+ if (right && right.id === button.id) {
1294
+ columnSpan++;
1295
+ }
1296
+ else {
1297
+ break;
1298
+ }
1299
+ }
1300
+ // Check row span (downward)
1301
+ while (y + rowSpan < page.grid.length) {
1302
+ const below = page.grid[y + rowSpan][x];
1303
+ if (below && below.id === button.id) {
1304
+ rowSpan++;
1305
+ }
1306
+ else {
1307
+ break;
1308
+ }
1309
+ }
1310
+ return { x, y, columnSpan, rowSpan };
1311
+ }
1312
+ }
1313
+ }
1314
+ }
1315
+ // Fallback positioning
1316
+ const gridCols = page.grid?.[0]?.length || Math.ceil(Math.sqrt(page.buttons.length));
1317
+ return {
1318
+ x: fallbackIndex % gridCols,
1319
+ y: Math.floor(fallbackIndex / gridCols),
1320
+ columnSpan: 1,
1321
+ rowSpan: 1,
1322
+ };
1323
+ }
1324
+ /**
1325
+ * Extract strings with metadata for aac-tools-platform compatibility
1326
+ * Uses the generic implementation from BaseProcessor
1327
+ */
1328
+ extractStringsWithMetadata(filePath) {
1329
+ return this.extractStringsWithMetadataGeneric(filePath);
1330
+ }
1331
+ /**
1332
+ * Generate translated download for aac-tools-platform compatibility
1333
+ * Uses the generic implementation from BaseProcessor
1334
+ */
1335
+ generateTranslatedDownload(filePath, translatedStrings, sourceStrings) {
1336
+ return this.generateTranslatedDownloadGeneric(filePath, translatedStrings, sourceStrings);
1337
+ }
1338
+ }
1339
+ exports.GridsetProcessor = GridsetProcessor;