@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,556 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.ExcelProcessor = void 0;
30
+ const fs_1 = __importDefault(require("fs"));
31
+ const path_1 = __importDefault(require("path"));
32
+ const ExcelJS = __importStar(require("exceljs"));
33
+ const baseProcessor_1 = require("../core/baseProcessor");
34
+ const treeStructure_1 = require("../core/treeStructure");
35
+ const treeStructure_2 = require("../core/treeStructure");
36
+ /**
37
+ * Excel Processor for converting AAC grids to Excel format
38
+ * Converts AAC tree structures to Excel workbooks with each page as a worksheet
39
+ * Supports visual styling, navigation links, and vocabulary analysis workflows
40
+ */
41
+ class ExcelProcessor extends baseProcessor_1.BaseProcessor {
42
+ /**
43
+ * Extract all text content from an Excel file
44
+ * @param filePathOrBuffer - Path to Excel file or Buffer containing Excel data
45
+ * @returns Array of all text content found in the Excel file
46
+ */
47
+ extractTexts(filePathOrBuffer) {
48
+ const texts = [];
49
+ try {
50
+ const workbook = new ExcelJS.Workbook();
51
+ if (Buffer.isBuffer(filePathOrBuffer)) {
52
+ // For buffer input, we need to read it differently
53
+ // This is a placeholder - actual implementation would need to handle buffer reading
54
+ throw new Error('Buffer input not yet implemented for Excel files');
55
+ }
56
+ else {
57
+ // Read from file path
58
+ if (!fs_1.default.existsSync(filePathOrBuffer)) {
59
+ return texts;
60
+ }
61
+ // Note: ExcelJS readFile is async, but we need sync for this interface
62
+ // This is a limitation that would need to be addressed in a real implementation
63
+ throw new Error('Synchronous Excel reading not yet implemented');
64
+ }
65
+ }
66
+ catch (error) {
67
+ console.warn(`Failed to extract texts from Excel file: ${error}`);
68
+ return texts;
69
+ }
70
+ }
71
+ /**
72
+ * Load Excel file into AACTree structure
73
+ * @param filePathOrBuffer - Path to Excel file or Buffer containing Excel data
74
+ * @returns AACTree representation of the Excel file
75
+ */
76
+ loadIntoTree(filePathOrBuffer) {
77
+ const tree = new treeStructure_1.AACTree();
78
+ try {
79
+ // For now, return empty tree as Excel -> AAC conversion is not the primary use case
80
+ // This would be implemented if bidirectional conversion is needed
81
+ console.warn('Excel to AAC conversion not implemented - returning empty tree');
82
+ return tree;
83
+ }
84
+ catch (error) {
85
+ console.warn(`Failed to load Excel file into tree: ${error}`);
86
+ return tree;
87
+ }
88
+ }
89
+ /**
90
+ * Process texts in Excel file (apply translations)
91
+ * @param filePathOrBuffer - Path to Excel file or Buffer containing Excel data
92
+ * @param translations - Map of original text to translated text
93
+ * @param outputPath - Path where translated Excel file should be saved
94
+ * @returns Buffer containing the translated Excel file
95
+ */
96
+ processTexts(filePathOrBuffer, translations, outputPath) {
97
+ try {
98
+ // Load the Excel file, apply translations, and save
99
+ // This would involve reading the Excel file, finding text cells,
100
+ // applying translations, and saving to outputPath
101
+ throw new Error('Excel text processing not yet implemented');
102
+ }
103
+ catch (error) {
104
+ console.warn(`Failed to process Excel texts: ${error}`);
105
+ // Return empty buffer as fallback
106
+ return Buffer.alloc(0);
107
+ }
108
+ }
109
+ /**
110
+ * Convert an AAC page to an Excel worksheet
111
+ * @param workbook - Excel workbook to add worksheet to
112
+ * @param page - AAC page to convert
113
+ * @param tree - Full AAC tree for navigation context
114
+ * @param usedNames - Set of already used worksheet names to avoid duplicates
115
+ */
116
+ async convertPageToWorksheet(workbook, page, tree, usedNames = new Set()) {
117
+ // Create worksheet with page name (sanitized for Excel and unique)
118
+ const worksheetName = this.getUniqueWorksheetName(page.name || page.id, usedNames);
119
+ const worksheet = workbook.addWorksheet(worksheetName);
120
+ // Determine grid dimensions
121
+ const { rows, cols } = this.calculateGridDimensions(page);
122
+ // Add navigation row if enabled (optional feature)
123
+ let startRow = 1;
124
+ if (this.shouldAddNavigationRow()) {
125
+ await this.addNavigationRow(worksheet, page, tree);
126
+ startRow = 2; // Start content after navigation row
127
+ }
128
+ // Convert grid layout if available
129
+ if (page.grid && page.grid.length > 0) {
130
+ await this.convertGridLayout(worksheet, page.grid, startRow);
131
+ }
132
+ else {
133
+ // Convert button list to grid layout
134
+ await this.convertButtonsToGrid(worksheet, page.buttons, rows, cols, startRow);
135
+ }
136
+ // Apply worksheet formatting
137
+ this.formatWorksheet(worksheet, rows, cols, startRow);
138
+ }
139
+ /**
140
+ * Calculate optimal grid dimensions for buttons
141
+ * @param page - AAC page to analyze
142
+ * @returns Object with rows and cols dimensions
143
+ */
144
+ calculateGridDimensions(page) {
145
+ // If grid is defined, use its dimensions
146
+ if (page.grid && page.grid.length > 0) {
147
+ return {
148
+ rows: page.grid.length,
149
+ cols: page.grid[0]?.length || 0,
150
+ };
151
+ }
152
+ // Calculate optimal grid for button list
153
+ const buttonCount = page.buttons.length;
154
+ if (buttonCount === 0) {
155
+ return { rows: 1, cols: 1 };
156
+ }
157
+ // Try to create a roughly square grid
158
+ const cols = Math.ceil(Math.sqrt(buttonCount));
159
+ const rows = Math.ceil(buttonCount / cols);
160
+ return { rows, cols };
161
+ }
162
+ /**
163
+ * Convert grid layout to Excel cells
164
+ * @param worksheet - Excel worksheet
165
+ * @param grid - 2D array of AAC buttons
166
+ * @param startRow - Starting row number
167
+ */
168
+ async convertGridLayout(worksheet, grid, startRow) {
169
+ for (let row = 0; row < grid.length; row++) {
170
+ for (let col = 0; col < grid[row].length; col++) {
171
+ const button = grid[row][col];
172
+ if (button) {
173
+ const excelRow = startRow + row;
174
+ const excelCol = col + 1; // Excel columns are 1-based
175
+ await this.setButtonCell(worksheet, button, excelRow, excelCol);
176
+ }
177
+ }
178
+ }
179
+ }
180
+ /**
181
+ * Convert button list to grid layout in Excel
182
+ * @param worksheet - Excel worksheet
183
+ * @param buttons - Array of AAC buttons
184
+ * @param rows - Number of rows in grid
185
+ * @param cols - Number of columns in grid
186
+ * @param startRow - Starting row number
187
+ */
188
+ async convertButtonsToGrid(worksheet, buttons, rows, cols, startRow) {
189
+ for (let i = 0; i < buttons.length; i++) {
190
+ const button = buttons[i];
191
+ const row = Math.floor(i / cols);
192
+ const col = i % cols;
193
+ if (row < rows) {
194
+ const excelRow = startRow + row;
195
+ const excelCol = col + 1; // Excel columns are 1-based
196
+ await this.setButtonCell(worksheet, button, excelRow, excelCol);
197
+ }
198
+ }
199
+ }
200
+ /**
201
+ * Set button data and formatting for an Excel cell
202
+ * @param worksheet - Excel worksheet
203
+ * @param button - AAC button to represent
204
+ * @param row - Excel row number
205
+ * @param col - Excel column number
206
+ */
207
+ async setButtonCell(worksheet, button, row, col) {
208
+ const cell = worksheet.getCell(row, col);
209
+ // Set cell value to button label
210
+ cell.value = button.label || '';
211
+ // Add button message as cell comment if different from label
212
+ if (button.message && button.message !== button.label) {
213
+ cell.note = button.message;
214
+ }
215
+ // Apply button styling
216
+ if (button.style) {
217
+ this.applyCellStyling(cell, button.style);
218
+ }
219
+ // Add navigation link if this is a navigation button
220
+ if (button.semanticAction?.intent === treeStructure_2.AACSemanticIntent.NAVIGATE_TO && button.targetPageId) {
221
+ this.addNavigationLink(cell, button.targetPageId);
222
+ }
223
+ // Set cell size for better visibility
224
+ this.setCellSize(worksheet, row, col);
225
+ }
226
+ /**
227
+ * Apply AAC button styling to Excel cell
228
+ * @param cell - Excel cell to style
229
+ * @param style - AAC style object
230
+ */
231
+ applyCellStyling(cell, style) {
232
+ const fill = {};
233
+ const font = {};
234
+ const border = {};
235
+ // Background color
236
+ if (style.backgroundColor) {
237
+ fill.type = 'pattern';
238
+ fill.pattern = 'solid';
239
+ fill.fgColor = { argb: this.convertColorToArgb(style.backgroundColor) };
240
+ }
241
+ // Font color
242
+ if (style.fontColor) {
243
+ font.color = { argb: this.convertColorToArgb(style.fontColor) };
244
+ }
245
+ // Font size
246
+ if (style.fontSize) {
247
+ font.size = style.fontSize;
248
+ }
249
+ // Font family
250
+ if (style.fontFamily) {
251
+ font.name = style.fontFamily;
252
+ }
253
+ // Font weight
254
+ if (style.fontWeight === 'bold') {
255
+ font.bold = true;
256
+ }
257
+ // Font style
258
+ if (style.fontStyle === 'italic') {
259
+ font.italic = true;
260
+ }
261
+ // Text underline
262
+ if (style.textUnderline) {
263
+ font.underline = true;
264
+ }
265
+ // Border
266
+ if (style.borderColor || style.borderWidth) {
267
+ const borderStyle = style.borderWidth > 1 ? 'thick' : 'thin';
268
+ const borderColor = style.borderColor
269
+ ? { argb: this.convertColorToArgb(style.borderColor) }
270
+ : { argb: 'FF000000' }; // Default black
271
+ border.top = { style: borderStyle, color: borderColor };
272
+ border.left = { style: borderStyle, color: borderColor };
273
+ border.bottom = { style: borderStyle, color: borderColor };
274
+ border.right = { style: borderStyle, color: borderColor };
275
+ }
276
+ // Apply styling to cell
277
+ if (Object.keys(fill).length > 0) {
278
+ cell.fill = fill;
279
+ }
280
+ if (Object.keys(font).length > 0) {
281
+ cell.font = font;
282
+ }
283
+ if (Object.keys(border).length > 0) {
284
+ cell.border = border;
285
+ }
286
+ // Center align text
287
+ cell.alignment = {
288
+ vertical: 'middle',
289
+ horizontal: 'center',
290
+ wrapText: true,
291
+ };
292
+ }
293
+ /**
294
+ * Convert color string to ARGB format for Excel
295
+ * @param color - Color string (hex, rgb, etc.)
296
+ * @returns ARGB color string
297
+ */
298
+ convertColorToArgb(color) {
299
+ if (!color)
300
+ return 'FFFFFFFF'; // Default white
301
+ // Remove any whitespace
302
+ color = color.trim();
303
+ // If already in hex format
304
+ if (color.startsWith('#')) {
305
+ const hex = color.substring(1);
306
+ if (hex.length === 6) {
307
+ return 'FF' + hex.toUpperCase(); // Add alpha channel
308
+ }
309
+ else if (hex.length === 8) {
310
+ return hex.toUpperCase(); // Already has alpha
311
+ }
312
+ }
313
+ // Handle rgb() format
314
+ const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
315
+ if (rgbMatch) {
316
+ const r = parseInt(rgbMatch[1]).toString(16).padStart(2, '0');
317
+ const g = parseInt(rgbMatch[2]).toString(16).padStart(2, '0');
318
+ const b = parseInt(rgbMatch[3]).toString(16).padStart(2, '0');
319
+ return 'FF' + r.toUpperCase() + g.toUpperCase() + b.toUpperCase();
320
+ }
321
+ // Handle rgba() format
322
+ const rgbaMatch = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)/);
323
+ if (rgbaMatch) {
324
+ const r = parseInt(rgbaMatch[1]).toString(16).padStart(2, '0');
325
+ const g = parseInt(rgbaMatch[2]).toString(16).padStart(2, '0');
326
+ const b = parseInt(rgbaMatch[3]).toString(16).padStart(2, '0');
327
+ const a = Math.round(parseFloat(rgbaMatch[4]) * 255)
328
+ .toString(16)
329
+ .padStart(2, '0');
330
+ return a.toUpperCase() + r.toUpperCase() + g.toUpperCase() + b.toUpperCase();
331
+ }
332
+ // Default fallback
333
+ return 'FFFFFFFF';
334
+ }
335
+ /**
336
+ * Add navigation link to cell for worksheet navigation
337
+ * @param cell - Excel cell to add link to
338
+ * @param targetPageId - Target page ID to link to
339
+ */
340
+ addNavigationLink(cell, targetPageId) {
341
+ // Create internal link to another worksheet
342
+ const sanitizedTargetName = this.sanitizeWorksheetName(targetPageId);
343
+ cell.value = {
344
+ text: cell.value?.toString() || '',
345
+ hyperlink: `#'${sanitizedTargetName}'!A1`,
346
+ };
347
+ }
348
+ /**
349
+ * Set appropriate cell size for button representation
350
+ * @param worksheet - Excel worksheet
351
+ * @param row - Row number
352
+ * @param col - Column number
353
+ */
354
+ setCellSize(worksheet, row, col) {
355
+ // Set column width (approximately 15 characters wide)
356
+ const column = worksheet.getColumn(col);
357
+ if (!column.width || column.width < 15) {
358
+ column.width = 15;
359
+ }
360
+ // Set row height (approximately 30 points high)
361
+ const worksheetRow = worksheet.getRow(row);
362
+ if (!worksheetRow.height || worksheetRow.height < 30) {
363
+ worksheetRow.height = 30;
364
+ }
365
+ }
366
+ /**
367
+ * Add navigation row with standard AAC navigation buttons
368
+ * @param worksheet - Excel worksheet
369
+ * @param page - Current AAC page
370
+ * @param tree - Full AAC tree for navigation context
371
+ */
372
+ async addNavigationRow(worksheet, page, tree) {
373
+ const navButtons = ExcelProcessor.NAVIGATION_BUTTONS;
374
+ for (let i = 0; i < navButtons.length; i++) {
375
+ const cell = worksheet.getCell(1, i + 1);
376
+ cell.value = navButtons[i];
377
+ // Style navigation buttons differently
378
+ cell.fill = {
379
+ type: 'pattern',
380
+ pattern: 'solid',
381
+ fgColor: { argb: 'FFE0E0E0' }, // Light gray background
382
+ };
383
+ cell.font = {
384
+ bold: true,
385
+ color: { argb: 'FF000000' }, // Black text
386
+ };
387
+ cell.border = {
388
+ top: { style: 'thin', color: { argb: 'FF000000' } },
389
+ left: { style: 'thin', color: { argb: 'FF000000' } },
390
+ bottom: { style: 'thin', color: { argb: 'FF000000' } },
391
+ right: { style: 'thin', color: { argb: 'FF000000' } },
392
+ };
393
+ cell.alignment = {
394
+ vertical: 'middle',
395
+ horizontal: 'center',
396
+ };
397
+ // Add navigation functionality for specific buttons
398
+ if (navButtons[i] === 'Home' && tree.rootId) {
399
+ this.addNavigationLink(cell, tree.rootId);
400
+ }
401
+ else if (navButtons[i] === 'Back' && page.parentId) {
402
+ this.addNavigationLink(cell, page.parentId);
403
+ }
404
+ }
405
+ }
406
+ /**
407
+ * Apply general formatting to the worksheet
408
+ * @param worksheet - Excel worksheet
409
+ * @param rows - Number of content rows
410
+ * @param cols - Number of content columns
411
+ * @param startRow - Starting row for content
412
+ */
413
+ formatWorksheet(worksheet, rows, cols, startRow) {
414
+ // Set default column widths
415
+ for (let col = 1; col <= cols; col++) {
416
+ const column = worksheet.getColumn(col);
417
+ if (!column.width) {
418
+ column.width = 15;
419
+ }
420
+ }
421
+ // Set default row heights
422
+ for (let row = startRow; row < startRow + rows; row++) {
423
+ const worksheetRow = worksheet.getRow(row);
424
+ if (!worksheetRow.height) {
425
+ worksheetRow.height = 30;
426
+ }
427
+ }
428
+ // Freeze navigation row if present
429
+ if (startRow > 1) {
430
+ worksheet.views = [{ state: 'frozen', ySplit: 1 }];
431
+ }
432
+ }
433
+ /**
434
+ * Sanitize worksheet name for Excel compatibility
435
+ * @param name - Original name
436
+ * @returns Sanitized name safe for Excel worksheet
437
+ */
438
+ sanitizeWorksheetName(name) {
439
+ // Excel worksheet name restrictions:
440
+ // - Max 31 characters
441
+ // - Cannot contain: \ / ? * [ ] :
442
+ // - Cannot be empty
443
+ const cleaned = (name || '')
444
+ .replace(/[\\\/\?\*\[\]:]/g, '_')
445
+ .substring(0, 31);
446
+ if (cleaned.length === 0) {
447
+ return 'Sheet1';
448
+ }
449
+ return cleaned;
450
+ }
451
+ /**
452
+ * Get a unique worksheet name by appending a number if needed
453
+ * @param name - Original name
454
+ * @param usedNames - Set of already used names (case-insensitive)
455
+ * @returns Unique worksheet name
456
+ */
457
+ getUniqueWorksheetName(name, usedNames) {
458
+ const baseName = this.sanitizeWorksheetName(name);
459
+ const normalize = (value) => value.toLowerCase();
460
+ let uniqueName = baseName;
461
+ let counter = 1;
462
+ // Keep trying with incrementing numbers until we find a unique name
463
+ // Names are already normalized to lowercase by sanitization
464
+ while (usedNames.has(normalize(uniqueName))) {
465
+ // Calculate how much space we need for the counter suffix
466
+ const suffix = ` (${counter})`;
467
+ const maxBaseLength = 31 - suffix.length;
468
+ // Truncate base name if needed to make room for suffix
469
+ const truncatedBase = baseName.substring(0, maxBaseLength);
470
+ uniqueName = truncatedBase + suffix;
471
+ counter++;
472
+ // Safety check to prevent infinite loop
473
+ if (counter > 1000) {
474
+ uniqueName = `Sheet${Date.now()}`;
475
+ break;
476
+ }
477
+ }
478
+ // Add the unique name to the set (already normalized to lowercase)
479
+ usedNames.add(normalize(uniqueName));
480
+ return uniqueName;
481
+ }
482
+ /**
483
+ * Check if navigation row should be added (configurable feature)
484
+ * @returns True if navigation row should be added
485
+ */
486
+ shouldAddNavigationRow() {
487
+ // This could be made configurable via processor options
488
+ // For now, default to true as specified in requirements
489
+ return true;
490
+ }
491
+ /**
492
+ * Override saveFromTree to handle async nature of Excel operations
493
+ * Note: This method is async but maintains the sync interface for compatibility
494
+ */
495
+ async saveFromTree(tree, outputPath) {
496
+ const outputDir = path_1.default.dirname(outputPath);
497
+ fs_1.default.mkdirSync(outputDir, { recursive: true });
498
+ try {
499
+ await this.saveFromTreeAsync(tree, outputPath);
500
+ }
501
+ catch (error) {
502
+ console.error('Failed to save Excel file:', error);
503
+ try {
504
+ const fallbackPath = outputPath.replace(/\.xlsx$/i, '_error.txt');
505
+ fs_1.default.mkdirSync(path_1.default.dirname(fallbackPath), { recursive: true });
506
+ fs_1.default.writeFileSync(fallbackPath, `Error saving Excel file: ${error?.message || String(error)}`);
507
+ }
508
+ catch (writeError) {
509
+ console.error('Failed to write Excel error file:', writeError);
510
+ }
511
+ }
512
+ }
513
+ /**
514
+ * Async version of saveFromTree for internal use
515
+ */
516
+ async saveFromTreeAsync(tree, outputPath) {
517
+ const workbook = new ExcelJS.Workbook();
518
+ // Set workbook properties
519
+ workbook.creator = 'AACProcessors';
520
+ workbook.lastModifiedBy = 'AACProcessors';
521
+ workbook.created = new Date();
522
+ workbook.modified = new Date();
523
+ // If no pages, create a default empty worksheet
524
+ if (Object.keys(tree.pages).length === 0) {
525
+ const worksheet = workbook.addWorksheet('Empty');
526
+ worksheet.getCell('A1').value = 'No AAC pages found';
527
+ await workbook.xlsx.writeFile(outputPath);
528
+ return;
529
+ }
530
+ // Track used worksheet names to handle duplicates
531
+ const usedNames = new Set();
532
+ // Convert each AAC page to an Excel worksheet
533
+ for (const pageId in tree.pages) {
534
+ const page = tree.pages[pageId];
535
+ await this.convertPageToWorksheet(workbook, page, tree, usedNames);
536
+ }
537
+ // Save the workbook
538
+ await workbook.xlsx.writeFile(outputPath);
539
+ }
540
+ /**
541
+ * Extract strings with metadata for aac-tools-platform compatibility
542
+ * Uses the generic implementation from BaseProcessor
543
+ */
544
+ extractStringsWithMetadata(filePath) {
545
+ return this.extractStringsWithMetadataGeneric(filePath);
546
+ }
547
+ /**
548
+ * Generate translated download for aac-tools-platform compatibility
549
+ * Uses the generic implementation from BaseProcessor
550
+ */
551
+ generateTranslatedDownload(filePath, translatedStrings, sourceStrings) {
552
+ return this.generateTranslatedDownloadGeneric(filePath, translatedStrings, sourceStrings);
553
+ }
554
+ }
555
+ exports.ExcelProcessor = ExcelProcessor;
556
+ ExcelProcessor.NAVIGATION_BUTTONS = ['Home', 'Message Bar', 'Delete', 'Back', 'Clear'];
@@ -0,0 +1,4 @@
1
+ import { AACTree } from '../../core/treeStructure';
2
+ export declare function getPageTokenImageMap(tree: AACTree, pageId: string): Map<string, string>;
3
+ export declare function getAllowedImageEntries(tree: AACTree): Set<string>;
4
+ export declare function openImage(gridsetBuffer: Buffer, entryPath: string): Buffer | null;
@@ -0,0 +1,48 @@
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.getPageTokenImageMap = getPageTokenImageMap;
7
+ exports.getAllowedImageEntries = getAllowedImageEntries;
8
+ exports.openImage = openImage;
9
+ const adm_zip_1 = __importDefault(require("adm-zip"));
10
+ function normalizeZipPath(p) {
11
+ const unified = p.replace(/\\/g, '/');
12
+ try {
13
+ return unified.normalize('NFC');
14
+ }
15
+ catch {
16
+ return unified;
17
+ }
18
+ }
19
+ function getPageTokenImageMap(tree, pageId) {
20
+ const map = new Map();
21
+ const page = tree.getPage(pageId);
22
+ if (!page)
23
+ return map;
24
+ for (const btn of page.buttons) {
25
+ if (btn.resolvedImageEntry) {
26
+ map.set(btn.id, normalizeZipPath(String(btn.resolvedImageEntry)));
27
+ }
28
+ }
29
+ return map;
30
+ }
31
+ function getAllowedImageEntries(tree) {
32
+ const out = new Set();
33
+ Object.values(tree.pages).forEach((page) => {
34
+ page.buttons.forEach((btn) => {
35
+ if (btn.resolvedImageEntry)
36
+ out.add(normalizeZipPath(String(btn.resolvedImageEntry)));
37
+ });
38
+ });
39
+ return out;
40
+ }
41
+ function openImage(gridsetBuffer, entryPath) {
42
+ const zip = new adm_zip_1.default(gridsetBuffer);
43
+ const want = normalizeZipPath(entryPath);
44
+ const entry = zip.getEntries().find((e) => normalizeZipPath(e.entryName) === want);
45
+ if (!entry)
46
+ return null;
47
+ return entry.getData();
48
+ }
@@ -0,0 +1,8 @@
1
+ export declare function resolveGrid3CellImage(zip: any, args: {
2
+ baseDir: string;
3
+ imageName?: string;
4
+ x?: number;
5
+ y?: number;
6
+ dynamicFiles?: string[];
7
+ builtinHandler?: (name: string) => string | null;
8
+ }): string | null;