@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.
- package/LICENSE +674 -0
- package/README.md +787 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +189 -0
- package/dist/cli/prettyPrint.d.ts +2 -0
- package/dist/cli/prettyPrint.js +28 -0
- package/dist/core/analyze.d.ts +6 -0
- package/dist/core/analyze.js +49 -0
- package/dist/core/baseProcessor.d.ts +94 -0
- package/dist/core/baseProcessor.js +208 -0
- package/dist/core/fileProcessor.d.ts +7 -0
- package/dist/core/fileProcessor.js +51 -0
- package/dist/core/stringCasing.d.ts +37 -0
- package/dist/core/stringCasing.js +174 -0
- package/dist/core/treeStructure.d.ts +190 -0
- package/dist/core/treeStructure.js +223 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +96 -0
- package/dist/optional/symbolTools.d.ts +28 -0
- package/dist/optional/symbolTools.js +126 -0
- package/dist/processors/applePanelsProcessor.d.ts +23 -0
- package/dist/processors/applePanelsProcessor.js +521 -0
- package/dist/processors/astericsGridProcessor.d.ts +49 -0
- package/dist/processors/astericsGridProcessor.js +1427 -0
- package/dist/processors/dotProcessor.d.ts +21 -0
- package/dist/processors/dotProcessor.js +191 -0
- package/dist/processors/excelProcessor.d.ts +145 -0
- package/dist/processors/excelProcessor.js +556 -0
- package/dist/processors/gridset/helpers.d.ts +4 -0
- package/dist/processors/gridset/helpers.js +48 -0
- package/dist/processors/gridset/resolver.d.ts +8 -0
- package/dist/processors/gridset/resolver.js +100 -0
- package/dist/processors/gridsetProcessor.d.ts +28 -0
- package/dist/processors/gridsetProcessor.js +1339 -0
- package/dist/processors/index.d.ts +14 -0
- package/dist/processors/index.js +42 -0
- package/dist/processors/obfProcessor.d.ts +21 -0
- package/dist/processors/obfProcessor.js +278 -0
- package/dist/processors/opmlProcessor.d.ts +21 -0
- package/dist/processors/opmlProcessor.js +235 -0
- package/dist/processors/snap/helpers.d.ts +4 -0
- package/dist/processors/snap/helpers.js +27 -0
- package/dist/processors/snapProcessor.d.ts +44 -0
- package/dist/processors/snapProcessor.js +586 -0
- package/dist/processors/touchchat/helpers.d.ts +4 -0
- package/dist/processors/touchchat/helpers.js +27 -0
- package/dist/processors/touchchatProcessor.d.ts +27 -0
- package/dist/processors/touchchatProcessor.js +768 -0
- package/dist/types/aac.d.ts +47 -0
- package/dist/types/aac.js +2 -0
- package/docs/.keep +1 -0
- package/docs/ApplePanels.md +309 -0
- package/docs/Grid3-XML-Format.md +1788 -0
- package/docs/TobiiDynavox-Snap-Details.md +394 -0
- package/docs/asterics-Grid-fileformat-details.md +443 -0
- package/docs/obf_.obz Open Board File Formats.md +432 -0
- package/docs/touchchat.md +520 -0
- package/examples/.coverage +0 -0
- package/examples/.keep +1 -0
- package/examples/README.md +31 -0
- package/examples/communikate.dot +2637 -0
- package/examples/demo.js +143 -0
- package/examples/example-images.gridset +0 -0
- package/examples/example.ce +0 -0
- package/examples/example.dot +14 -0
- package/examples/example.grd +1 -0
- package/examples/example.gridset +0 -0
- package/examples/example.obf +27 -0
- package/examples/example.obz +0 -0
- package/examples/example.opml +18 -0
- package/examples/example.spb +0 -0
- package/examples/example.sps +0 -0
- package/examples/example2.grd +1 -0
- package/examples/gemini_response.txt +845 -0
- package/examples/image-map.js +45 -0
- package/examples/package-lock.json +1326 -0
- package/examples/package.json +10 -0
- package/examples/styled-output/converted-snap-to-touchchat.ce +0 -0
- package/examples/styled-output/styled-example.ce +0 -0
- package/examples/styled-output/styled-example.gridset +0 -0
- package/examples/styled-output/styled-example.obf +37 -0
- package/examples/styled-output/styled-example.spb +0 -0
- package/examples/styling-example.ts +316 -0
- package/examples/translate.js +39 -0
- package/examples/translate_demo.js +254 -0
- package/examples/translation_cache.json +44894 -0
- package/examples/typescript-demo.ts +251 -0
- package/examples/unified-interface-demo.ts +183 -0
- 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
|
+
}
|