@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,1427 @@
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.AstericsGridProcessor = void 0;
7
+ const baseProcessor_1 = require("../core/baseProcessor");
8
+ const treeStructure_1 = require("../core/treeStructure");
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const DEFAULT_COLOR_SCHEME_DEFINITIONS = [
11
+ {
12
+ name: 'CS_MODIFIED_FITZGERALD_KEY_VERY_LIGHT',
13
+ categories: [
14
+ 'CC_PRONOUN_PERSON_NAME',
15
+ 'CC_NOUN',
16
+ 'CC_VERB',
17
+ 'CC_DESCRIPTOR',
18
+ 'CC_SOCIAL_EXPRESSIONS',
19
+ 'CC_MISC',
20
+ 'CC_PLACE',
21
+ 'CC_CATEGORY',
22
+ 'CC_IMPORTANT',
23
+ 'CC_OTHERS',
24
+ ],
25
+ colors: [
26
+ '#fafad0',
27
+ '#fbf3e4',
28
+ '#dff4df',
29
+ '#eaeffd',
30
+ '#fff0f6',
31
+ '#ffffff',
32
+ '#fbf2ff',
33
+ '#ddccc1',
34
+ '#FCE8E8',
35
+ '#e4e4e4',
36
+ ],
37
+ mappings: {
38
+ CC_ADJECTIVE: 'CC_DESCRIPTOR',
39
+ CC_ADVERB: 'CC_DESCRIPTOR',
40
+ CC_ARTICLE: 'CC_MISC',
41
+ CC_PREPOSITION: 'CC_MISC',
42
+ CC_CONJUNCTION: 'CC_MISC',
43
+ CC_INTERJECTION: 'CC_SOCIAL_EXPRESSIONS',
44
+ },
45
+ },
46
+ {
47
+ name: 'CS_MODIFIED_FITZGERALD_KEY_LIGHT',
48
+ categories: [
49
+ 'CC_PRONOUN_PERSON_NAME',
50
+ 'CC_NOUN',
51
+ 'CC_VERB',
52
+ 'CC_DESCRIPTOR',
53
+ 'CC_SOCIAL_EXPRESSIONS',
54
+ 'CC_MISC',
55
+ 'CC_PLACE',
56
+ 'CC_CATEGORY',
57
+ 'CC_IMPORTANT',
58
+ 'CC_OTHERS',
59
+ ],
60
+ colors: [
61
+ '#fdfd96',
62
+ '#ffda89',
63
+ '#c7f3c7',
64
+ '#84b6f4',
65
+ '#fdcae1',
66
+ '#ffffff',
67
+ '#bc98f3',
68
+ '#d8af97',
69
+ '#ff9688',
70
+ '#bdbfbf',
71
+ ],
72
+ mappings: {
73
+ CC_ADJECTIVE: 'CC_DESCRIPTOR',
74
+ CC_ADVERB: 'CC_DESCRIPTOR',
75
+ CC_ARTICLE: 'CC_MISC',
76
+ CC_PREPOSITION: 'CC_MISC',
77
+ CC_CONJUNCTION: 'CC_MISC',
78
+ CC_INTERJECTION: 'CC_SOCIAL_EXPRESSIONS',
79
+ },
80
+ },
81
+ {
82
+ name: 'CS_MODIFIED_FITZGERALD_KEY_MEDIUM',
83
+ categories: [
84
+ 'CC_PRONOUN_PERSON_NAME',
85
+ 'CC_NOUN',
86
+ 'CC_VERB',
87
+ 'CC_DESCRIPTOR',
88
+ 'CC_SOCIAL_EXPRESSIONS',
89
+ 'CC_MISC',
90
+ 'CC_PLACE',
91
+ 'CC_CATEGORY',
92
+ 'CC_IMPORTANT',
93
+ 'CC_OTHERS',
94
+ ],
95
+ colors: [
96
+ '#ffff6b',
97
+ '#ffb56b',
98
+ '#b5ff6b',
99
+ '#6bb5ff',
100
+ '#ff6bff',
101
+ '#ffffff',
102
+ '#ce6bff',
103
+ '#bf9075',
104
+ '#ff704d',
105
+ '#a3a3a3',
106
+ ],
107
+ mappings: {
108
+ CC_ADJECTIVE: 'CC_DESCRIPTOR',
109
+ CC_ADVERB: 'CC_DESCRIPTOR',
110
+ CC_ARTICLE: 'CC_MISC',
111
+ CC_PREPOSITION: 'CC_MISC',
112
+ CC_CONJUNCTION: 'CC_MISC',
113
+ CC_INTERJECTION: 'CC_SOCIAL_EXPRESSIONS',
114
+ },
115
+ },
116
+ {
117
+ name: 'CS_MODIFIED_FITZGERALD_KEY_DARK',
118
+ categories: [
119
+ 'CC_PRONOUN_PERSON_NAME',
120
+ 'CC_NOUN',
121
+ 'CC_VERB',
122
+ 'CC_DESCRIPTOR',
123
+ 'CC_SOCIAL_EXPRESSIONS',
124
+ 'CC_MISC',
125
+ 'CC_PLACE',
126
+ 'CC_CATEGORY',
127
+ 'CC_IMPORTANT',
128
+ 'CC_OTHERS',
129
+ ],
130
+ colors: [
131
+ '#79791F',
132
+ '#804c26',
133
+ '#4c8026',
134
+ '#264c80',
135
+ '#802680',
136
+ '#747474',
137
+ '#602680',
138
+ '#52331f',
139
+ '#80261a',
140
+ '#464646',
141
+ ],
142
+ mappings: {
143
+ CC_ADJECTIVE: 'CC_DESCRIPTOR',
144
+ CC_ADVERB: 'CC_DESCRIPTOR',
145
+ CC_ARTICLE: 'CC_MISC',
146
+ CC_PREPOSITION: 'CC_MISC',
147
+ CC_CONJUNCTION: 'CC_MISC',
148
+ CC_INTERJECTION: 'CC_SOCIAL_EXPRESSIONS',
149
+ },
150
+ },
151
+ {
152
+ name: 'CS_GOOSENS_VERY_LIGHT',
153
+ categories: [
154
+ 'CC_VERB',
155
+ 'CC_DESCRIPTOR',
156
+ 'CC_PREPOSITION',
157
+ 'CC_NOUN',
158
+ 'CC_QUESTION_NEGATION_PRONOUN',
159
+ ],
160
+ colors: ['#fff0f6', '#eaeffd', '#dff4df', '#fafad0', '#fbf3e4'],
161
+ },
162
+ {
163
+ name: 'CS_GOOSENS_LIGHT',
164
+ categories: [
165
+ 'CC_VERB',
166
+ 'CC_DESCRIPTOR',
167
+ 'CC_PREPOSITION',
168
+ 'CC_NOUN',
169
+ 'CC_QUESTION_NEGATION_PRONOUN',
170
+ ],
171
+ colors: ['#fdcae1', '#84b6f4', '#c7f3c7', '#fdfd96', '#ffda89'],
172
+ },
173
+ {
174
+ name: 'CS_GOOSENS_MEDIUM',
175
+ categories: [
176
+ 'CC_VERB',
177
+ 'CC_DESCRIPTOR',
178
+ 'CC_PREPOSITION',
179
+ 'CC_NOUN',
180
+ 'CC_QUESTION_NEGATION_PRONOUN',
181
+ ],
182
+ colors: ['#ff6bff', '#6bb5ff', '#b5ff6b', '#ffff6b', '#ffb56b'],
183
+ },
184
+ {
185
+ name: 'CS_GOOSENS_DARK',
186
+ categories: [
187
+ 'CC_VERB',
188
+ 'CC_DESCRIPTOR',
189
+ 'CC_PREPOSITION',
190
+ 'CC_NOUN',
191
+ 'CC_QUESTION_NEGATION_PRONOUN',
192
+ ],
193
+ colors: ['#802680', '#264c80', '#4c8026', '#79791F', '#804c26'],
194
+ },
195
+ {
196
+ name: 'CS_MONTESSORI_VERY_LIGHT',
197
+ categories: [
198
+ 'CC_NOUN',
199
+ 'CC_ARTICLE',
200
+ 'CC_ADJECTIVE',
201
+ 'CC_VERB',
202
+ 'CC_PREPOSITION',
203
+ 'CC_ADVERB',
204
+ 'CC_PRONOUN_PERSON_NAME',
205
+ 'CC_CONJUNCTION',
206
+ 'CC_INTERJECTION',
207
+ 'CC_CATEGORY',
208
+ ],
209
+ colors: [
210
+ '#ffffff',
211
+ '#e3f5fa',
212
+ '#eaeffd',
213
+ '#FCE8E8',
214
+ '#dff4df',
215
+ '#fbf3e4',
216
+ '#fbf2ff',
217
+ '#fff0f6',
218
+ '#fbf7e4',
219
+ '#e4e4e4',
220
+ ],
221
+ customBorders: {
222
+ CC_NOUN: '#353535',
223
+ },
224
+ },
225
+ {
226
+ name: 'CS_MONTESSORI_LIGHT',
227
+ categories: [
228
+ 'CC_NOUN',
229
+ 'CC_ARTICLE',
230
+ 'CC_ADJECTIVE',
231
+ 'CC_VERB',
232
+ 'CC_PREPOSITION',
233
+ 'CC_ADVERB',
234
+ 'CC_PRONOUN_PERSON_NAME',
235
+ 'CC_CONJUNCTION',
236
+ 'CC_INTERJECTION',
237
+ 'CC_CATEGORY',
238
+ ],
239
+ colors: [
240
+ '#afafaf',
241
+ '#a8e0f0',
242
+ '#a5bbf7',
243
+ '#f4a8a8',
244
+ '#ace3ac',
245
+ '#f2d7a6',
246
+ '#e4a5ff',
247
+ '#ffa5c9',
248
+ '#f2e5a6',
249
+ '#d1d1d1',
250
+ ],
251
+ },
252
+ {
253
+ name: 'CS_MONTESSORI_MEDIUM',
254
+ categories: [
255
+ 'CC_NOUN',
256
+ 'CC_ARTICLE',
257
+ 'CC_ADJECTIVE',
258
+ 'CC_VERB',
259
+ 'CC_PREPOSITION',
260
+ 'CC_ADVERB',
261
+ 'CC_PRONOUN_PERSON_NAME',
262
+ 'CC_CONJUNCTION',
263
+ 'CC_INTERJECTION',
264
+ 'CC_CATEGORY',
265
+ ],
266
+ colors: [
267
+ '#000000',
268
+ '#4ca6d9',
269
+ '#1347ae',
270
+ '#e73a0f',
271
+ '#04bf82',
272
+ '#fd9030',
273
+ '#6118a2',
274
+ '#f1c9d1',
275
+ '#aa996b',
276
+ '#d1d1d1',
277
+ ],
278
+ },
279
+ {
280
+ name: 'CS_MONTESSORI_DARK',
281
+ categories: [
282
+ 'CC_NOUN',
283
+ 'CC_ARTICLE',
284
+ 'CC_ADJECTIVE',
285
+ 'CC_VERB',
286
+ 'CC_PREPOSITION',
287
+ 'CC_ADVERB',
288
+ 'CC_PRONOUN_PERSON_NAME',
289
+ 'CC_CONJUNCTION',
290
+ 'CC_INTERJECTION',
291
+ 'CC_CATEGORY',
292
+ ],
293
+ colors: [
294
+ '#464646',
295
+ '#18728c',
296
+ '#0d3298',
297
+ '#931212',
298
+ '#287728',
299
+ '#BC5800',
300
+ '#7500a7',
301
+ '#a70043',
302
+ '#807351',
303
+ '#747474',
304
+ ],
305
+ },
306
+ ];
307
+ const COLOR_SCHEME_ALIASES = {
308
+ CS_DEFAULT: 'CS_MODIFIED_FITZGERALD_KEY_LIGHT',
309
+ CS_MONTESSORI: 'CS_MONTESSORI_LIGHT',
310
+ CS_MONTESSORI_LIGHT: 'CS_MONTESSORI_LIGHT',
311
+ CS_MONTESSORI_MEDIUM: 'CS_MONTESSORI_MEDIUM',
312
+ CS_MONTESSORI_DARK: 'CS_MONTESSORI_DARK',
313
+ CS_MONTESSORI_VERY_LIGHT: 'CS_MONTESSORI_VERY_LIGHT',
314
+ CS_MODIFIED_FITZGERALD_KEY: 'CS_MODIFIED_FITZGERALD_KEY_LIGHT',
315
+ CS_MODIFIED_FITZGERALD_KEY_LIGHT: 'CS_MODIFIED_FITZGERALD_KEY_LIGHT',
316
+ CS_MODIFIED_FITZGERALD_KEY_MEDIUM: 'CS_MODIFIED_FITZGERALD_KEY_MEDIUM',
317
+ CS_MODIFIED_FITZGERALD_KEY_DARK: 'CS_MODIFIED_FITZGERALD_KEY_DARK',
318
+ CS_MODIFIED_FITZGERALD_KEY_VERY_LIGHT: 'CS_MODIFIED_FITZGERALD_KEY_VERY_LIGHT',
319
+ CS_GOOSENS: 'CS_GOOSENS_LIGHT',
320
+ CS_GOOSENS_LIGHT: 'CS_GOOSENS_LIGHT',
321
+ CS_GOOSENS_MEDIUM: 'CS_GOOSENS_MEDIUM',
322
+ CS_GOOSENS_DARK: 'CS_GOOSENS_DARK',
323
+ CS_GOOSENS_VERY_LIGHT: 'CS_GOOSENS_VERY_LIGHT',
324
+ };
325
+ function normalizeHexColor(hexColor) {
326
+ if (!hexColor || typeof hexColor !== 'string')
327
+ return null;
328
+ let value = hexColor.trim().toLowerCase();
329
+ if (!value.startsWith('#')) {
330
+ return null;
331
+ }
332
+ value = value.slice(1);
333
+ if (value.length === 3) {
334
+ value = value
335
+ .split('')
336
+ .map((ch) => ch + ch)
337
+ .join('');
338
+ }
339
+ if (value.length !== 6 || /[^0-9a-f]/.test(value)) {
340
+ return null;
341
+ }
342
+ return `#${value}`;
343
+ }
344
+ function adjustHexColor(hexColor, amount) {
345
+ const normalized = normalizeHexColor(hexColor);
346
+ if (!normalized)
347
+ return hexColor;
348
+ const hex = normalized.slice(1);
349
+ const num = parseInt(hex, 16);
350
+ const clamp = (value) => Math.max(0, Math.min(255, value));
351
+ const r = clamp(((num >> 16) & 0xff) + amount);
352
+ const g = clamp(((num >> 8) & 0xff) + amount);
353
+ const b = clamp((num & 0xff) + amount);
354
+ return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
355
+ }
356
+ function getHighContrastNeutralColor(backgroundColor) {
357
+ const normalized = normalizeHexColor(backgroundColor);
358
+ if (!normalized) {
359
+ return '#808080';
360
+ }
361
+ return calculateLuminance(normalized) < 0.5 ? '#f5f5f5' : '#808080';
362
+ }
363
+ function normalizeColorScheme(raw) {
364
+ if (!raw || typeof raw !== 'object')
365
+ return null;
366
+ const scheme = raw;
367
+ const nameCandidate = [scheme.name, scheme.key, scheme.id].find((value) => typeof value === 'string' && value.length > 0);
368
+ if (!nameCandidate)
369
+ return null;
370
+ let categories = [];
371
+ let colors = [];
372
+ if (Array.isArray(scheme.categories) && Array.isArray(scheme.colors)) {
373
+ categories = scheme.categories.filter((value) => typeof value === 'string');
374
+ colors = scheme.colors.filter((value) => typeof value === 'string');
375
+ }
376
+ else if (scheme.colorMap && typeof scheme.colorMap === 'object') {
377
+ categories = Object.keys(scheme.colorMap);
378
+ colors = categories.map((category) => {
379
+ const colorValue = scheme.colorMap[category];
380
+ return typeof colorValue === 'string' ? colorValue : '#ffffff';
381
+ });
382
+ }
383
+ if (!categories.length || !colors.length) {
384
+ return null;
385
+ }
386
+ const mappingsCandidate = (typeof scheme.mappings === 'object' && scheme.mappings) ||
387
+ (typeof scheme.categoryMappings === 'object' && scheme.categoryMappings) ||
388
+ (typeof scheme.categoryMapping === 'object' && scheme.categoryMapping) ||
389
+ undefined;
390
+ const customBordersCandidate = typeof scheme.customBorders === 'object' && scheme.customBorders ? scheme.customBorders : undefined;
391
+ return {
392
+ name: nameCandidate,
393
+ categories,
394
+ colors,
395
+ mappings: mappingsCandidate,
396
+ customBorders: customBordersCandidate,
397
+ };
398
+ }
399
+ function getAllColorSchemeDefinitions(colorConfig) {
400
+ const additional = Array.isArray(colorConfig?.additionalColorSchemes)
401
+ ? colorConfig.additionalColorSchemes
402
+ .map((scheme) => normalizeColorScheme(scheme))
403
+ .filter((value) => Boolean(value))
404
+ : [];
405
+ return [...DEFAULT_COLOR_SCHEME_DEFINITIONS, ...additional];
406
+ }
407
+ function getActiveColorSchemeDefinition(colorConfig) {
408
+ if (!colorConfig || colorConfig.colorSchemesActivated === false) {
409
+ return null;
410
+ }
411
+ const schemes = getAllColorSchemeDefinitions(colorConfig);
412
+ if (!schemes.length) {
413
+ return null;
414
+ }
415
+ const activeName = (typeof colorConfig.activeColorScheme === 'string' && colorConfig.activeColorScheme) || undefined;
416
+ const normalizedName = activeName ? COLOR_SCHEME_ALIASES[activeName] || activeName : undefined;
417
+ if (normalizedName) {
418
+ const match = schemes.find((scheme) => scheme.name === normalizedName);
419
+ if (match) {
420
+ return match;
421
+ }
422
+ }
423
+ return schemes[0];
424
+ }
425
+ function getSchemeColorForCategory(category, scheme, fallback) {
426
+ if (!scheme || !category)
427
+ return fallback;
428
+ let index = scheme.categories.indexOf(category);
429
+ if (index === -1 && scheme.mappings && scheme.mappings[category]) {
430
+ index = scheme.categories.indexOf(scheme.mappings[category]);
431
+ }
432
+ if (index === -1) {
433
+ return fallback;
434
+ }
435
+ const color = scheme.colors[index];
436
+ return typeof color === 'string' ? color : fallback;
437
+ }
438
+ function resolveBorderColor(element, colorConfig, scheme, backgroundColor, schemeColor, fallbackBorder) {
439
+ const defaultBorderColor = (fallbackBorder || '#808080').toLowerCase();
440
+ const colorMode = colorConfig?.colorMode || 'COLOR_MODE_BACKGROUND';
441
+ if (colorMode === 'COLOR_MODE_BORDER') {
442
+ return (getSchemeColorForCategory(element.colorCategory, scheme, fallbackBorder || '#808080') || fallbackBorder || '#808080');
443
+ }
444
+ if (colorMode === 'COLOR_MODE_BOTH') {
445
+ if (!element.colorCategory) {
446
+ return 'transparent';
447
+ }
448
+ const customBorder = scheme?.customBorders?.[element.colorCategory];
449
+ if (typeof customBorder === 'string') {
450
+ return customBorder;
451
+ }
452
+ const baseColor = schemeColor ||
453
+ getSchemeColorForCategory(element.colorCategory, scheme, backgroundColor) ||
454
+ backgroundColor;
455
+ const isDark = calculateLuminance(baseColor) < 0.5;
456
+ const adjustment = isDark ? 60 : -40;
457
+ return adjustHexColor(baseColor, adjustment);
458
+ }
459
+ if (defaultBorderColor !== '#808080') {
460
+ return fallbackBorder || '#808080';
461
+ }
462
+ const gridBackground = colorConfig?.gridBackgroundColor || '#ffffff';
463
+ return getHighContrastNeutralColor(gridBackground);
464
+ }
465
+ function resolveButtonColors(element, colorConfig, scheme) {
466
+ const fallbackBackground = colorConfig?.elementBackgroundColor || '#FFFFFF';
467
+ const fallbackBorder = colorConfig?.elementBorderColor || '#808080';
468
+ const colorMode = colorConfig?.colorMode || 'COLOR_MODE_BACKGROUND';
469
+ const isSchemeActive = colorConfig?.colorSchemesActivated !== false;
470
+ const schemeColor = isSchemeActive && colorMode !== 'COLOR_MODE_BORDER'
471
+ ? getSchemeColorForCategory(element.colorCategory, scheme || null)
472
+ : undefined;
473
+ const backgroundColor = element.backgroundColor || schemeColor || fallbackBackground || '#FFFFFF';
474
+ const borderColor = resolveBorderColor(element, colorConfig || {}, scheme || null, backgroundColor, schemeColor, fallbackBorder);
475
+ const fontColor = element.fontColor ||
476
+ colorConfig?.fontColor ||
477
+ getContrastingTextColor(backgroundColor);
478
+ return {
479
+ backgroundColor,
480
+ borderColor,
481
+ fontColor,
482
+ };
483
+ }
484
+ /**
485
+ * Calculate relative luminance of a color using WCAG formula
486
+ * @param hexColor - Hex color string (e.g., "#1d90ff")
487
+ * @returns Relative luminance value between 0 and 1
488
+ */
489
+ function calculateLuminance(hexColor) {
490
+ // Remove # if present
491
+ const hex = hexColor.replace('#', '');
492
+ // Parse RGB values
493
+ const r = parseInt(hex.substring(0, 2), 16) / 255;
494
+ const g = parseInt(hex.substring(2, 4), 16) / 255;
495
+ const b = parseInt(hex.substring(4, 6), 16) / 255;
496
+ // Apply sRGB gamma correction
497
+ const rsRGB = r <= 0.03928 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4);
498
+ const gsRGB = g <= 0.03928 ? g / 12.92 : Math.pow((g + 0.055) / 1.055, 2.4);
499
+ const bsRGB = b <= 0.03928 ? b / 12.92 : Math.pow((b + 0.055) / 1.055, 2.4);
500
+ // Calculate relative luminance
501
+ return 0.2126 * rsRGB + 0.7152 * gsRGB + 0.0722 * bsRGB;
502
+ }
503
+ /**
504
+ * Choose white or black text color based on background luminance for optimal contrast
505
+ * @param backgroundColor - Background color hex string
506
+ * @returns "#FFFFFF" for dark backgrounds, "#000000" for light backgrounds
507
+ */
508
+ function getContrastingTextColor(backgroundColor) {
509
+ const luminance = calculateLuminance(backgroundColor);
510
+ // WCAG threshold: use white text if luminance < 0.5, black otherwise
511
+ return luminance < 0.5 ? '#FFFFFF' : '#000000';
512
+ }
513
+ class AstericsGridProcessor extends baseProcessor_1.BaseProcessor {
514
+ constructor(options = {}) {
515
+ super(options);
516
+ this.loadAudio = false;
517
+ this.loadAudio = options.loadAudio || false;
518
+ }
519
+ extractTexts(filePathOrBuffer) {
520
+ const tree = this.loadIntoTree(filePathOrBuffer);
521
+ const texts = [];
522
+ for (const pageId in tree.pages) {
523
+ const page = tree.pages[pageId];
524
+ if (page.name)
525
+ texts.push(page.name);
526
+ page.buttons.forEach((btn) => {
527
+ if (btn.label)
528
+ texts.push(btn.label);
529
+ if (btn.message && btn.message !== btn.label)
530
+ texts.push(btn.message);
531
+ });
532
+ }
533
+ // Also extract texts from the raw file for comprehensive coverage
534
+ const rawTexts = this.extractRawTexts(filePathOrBuffer);
535
+ rawTexts.forEach((text) => {
536
+ if (text && !texts.includes(text)) {
537
+ texts.push(text);
538
+ }
539
+ });
540
+ return texts;
541
+ }
542
+ extractRawTexts(filePathOrBuffer) {
543
+ let content = Buffer.isBuffer(filePathOrBuffer)
544
+ ? filePathOrBuffer.toString('utf-8')
545
+ : fs_1.default.readFileSync(filePathOrBuffer, 'utf-8');
546
+ // Remove BOM if present
547
+ if (content.charCodeAt(0) === 0xfeff) {
548
+ content = content.slice(1);
549
+ }
550
+ const texts = [];
551
+ try {
552
+ const grdFile = JSON.parse(content);
553
+ grdFile.grids.forEach((grid) => {
554
+ // Extract grid labels
555
+ Object.values(grid.label || {}).forEach((label) => {
556
+ if (label && typeof label === 'string')
557
+ texts.push(label);
558
+ });
559
+ // Extract element texts
560
+ grid.gridElements.forEach((element) => {
561
+ // Element labels
562
+ Object.values(element.label || {}).forEach((label) => {
563
+ if (label && typeof label === 'string')
564
+ texts.push(label);
565
+ });
566
+ // Word forms
567
+ element.wordForms?.forEach((wordForm) => {
568
+ if (wordForm.value)
569
+ texts.push(wordForm.value);
570
+ });
571
+ // Action-specific texts
572
+ element.actions.forEach((action) => {
573
+ this.extractActionTexts(action, texts);
574
+ });
575
+ });
576
+ });
577
+ }
578
+ catch (error) {
579
+ // If JSON parsing fails, return empty array
580
+ }
581
+ return texts;
582
+ }
583
+ extractActionTexts(action, texts) {
584
+ switch (action.modelName) {
585
+ case 'GridActionSpeakCustom':
586
+ if (action.speakText && typeof action.speakText === 'object') {
587
+ Object.values(action.speakText).forEach((text) => {
588
+ if (text && typeof text === 'string')
589
+ texts.push(text);
590
+ });
591
+ }
592
+ break;
593
+ case 'GridActionChangeLang':
594
+ if (action.language && typeof action.language === 'string') {
595
+ texts.push(action.language);
596
+ }
597
+ if (action.voice && typeof action.voice === 'string') {
598
+ texts.push(action.voice);
599
+ }
600
+ break;
601
+ case 'GridActionHTTP':
602
+ if (action.restUrl && typeof action.restUrl === 'string') {
603
+ texts.push(action.restUrl);
604
+ }
605
+ if (action.body && typeof action.body === 'string') {
606
+ texts.push(action.body);
607
+ }
608
+ break;
609
+ case 'GridActionOpenWebpage':
610
+ if (action.openURL && typeof action.openURL === 'string') {
611
+ texts.push(action.openURL);
612
+ }
613
+ break;
614
+ case 'GridActionMatrix':
615
+ if (action.sendText && typeof action.sendText === 'string') {
616
+ texts.push(action.sendText);
617
+ }
618
+ break;
619
+ // Add more action types as needed
620
+ }
621
+ }
622
+ loadIntoTree(filePathOrBuffer) {
623
+ const tree = new treeStructure_1.AACTree();
624
+ let content = Buffer.isBuffer(filePathOrBuffer)
625
+ ? filePathOrBuffer.toString('utf-8')
626
+ : fs_1.default.readFileSync(filePathOrBuffer, 'utf-8');
627
+ // Remove BOM if present
628
+ if (content.charCodeAt(0) === 0xfeff) {
629
+ content = content.slice(1);
630
+ }
631
+ const grdFile = JSON.parse(content);
632
+ if (!grdFile.grids) {
633
+ return tree;
634
+ }
635
+ const colorConfig = grdFile.metadata?.colorConfig;
636
+ const activeColorSchemeDefinition = getActiveColorSchemeDefinition(colorConfig);
637
+ // First pass: create all pages
638
+ grdFile.grids.forEach((grid) => {
639
+ const page = new treeStructure_1.AACPage({
640
+ id: grid.id,
641
+ name: this.getLocalizedLabel(grid.label) || grid.id,
642
+ grid: [],
643
+ buttons: [],
644
+ parentId: null,
645
+ style: {
646
+ backgroundColor: colorConfig?.gridBackgroundColor || '#FFFFFF',
647
+ borderColor: colorConfig?.elementBorderColor || '#CCCCCC',
648
+ borderWidth: colorConfig?.borderWidth || 1,
649
+ fontFamily: colorConfig?.fontFamily || 'Arial',
650
+ fontSize: colorConfig?.fontSizePct ? colorConfig.fontSizePct * 16 : 16, // Convert percentage to pixels, default to 16
651
+ fontColor: colorConfig?.fontColor || '#000000',
652
+ },
653
+ });
654
+ tree.addPage(page);
655
+ });
656
+ // Second pass: add buttons and establish navigation
657
+ grdFile.grids.forEach((grid) => {
658
+ const page = tree.getPage(grid.id);
659
+ if (!page)
660
+ return;
661
+ // Create a 2D grid to track button positions
662
+ const gridLayout = [];
663
+ const maxRows = Math.max(10, grid.rowCount || 10);
664
+ const maxCols = Math.max(10, grid.minColumnCount || 10);
665
+ for (let r = 0; r < maxRows; r++) {
666
+ gridLayout[r] = new Array(maxCols).fill(null);
667
+ }
668
+ grid.gridElements.forEach((element) => {
669
+ const button = this.createButtonFromElement(element, colorConfig, activeColorSchemeDefinition);
670
+ page.addButton(button);
671
+ // Place button in grid layout using its x,y coordinates
672
+ const buttonX = element.x || 0;
673
+ const buttonY = element.y || 0;
674
+ const buttonWidth = element.width || 1;
675
+ const buttonHeight = element.height || 1;
676
+ // Place button in grid (handle width/height span)
677
+ for (let r = buttonY; r < buttonY + buttonHeight && r < maxRows; r++) {
678
+ for (let c = buttonX; c < buttonX + buttonWidth && c < maxCols; c++) {
679
+ if (gridLayout[r] && gridLayout[r][c] === null) {
680
+ gridLayout[r][c] = button;
681
+ }
682
+ }
683
+ }
684
+ // Handle navigation relationships
685
+ const navAction = element.actions.find((a) => a.modelName === 'GridActionNavigate');
686
+ if (navAction && navAction.toGridId) {
687
+ const targetPage = tree.getPage(navAction.toGridId);
688
+ if (targetPage) {
689
+ targetPage.parentId = page.id;
690
+ }
691
+ }
692
+ });
693
+ // Set the page's grid layout
694
+ page.grid = gridLayout;
695
+ });
696
+ // Set the home page from metadata.homeGridId
697
+ if (grdFile.metadata && grdFile.metadata.homeGridId) {
698
+ tree.rootId = grdFile.metadata.homeGridId;
699
+ }
700
+ return tree;
701
+ }
702
+ getLocalizedLabel(labelMap) {
703
+ if (!labelMap)
704
+ return '';
705
+ // Prefer English, then any available language
706
+ return labelMap.en || labelMap.de || labelMap.es || Object.values(labelMap)[0] || '';
707
+ }
708
+ getLocalizedText(text) {
709
+ if (typeof text === 'string')
710
+ return text;
711
+ if (typeof text === 'object' && text) {
712
+ return text.en || text.de || text.es || Object.values(text)[0] || '';
713
+ }
714
+ return '';
715
+ }
716
+ createButtonFromElement(element, colorConfig, activeColorScheme) {
717
+ let audioRecording;
718
+ if (this.loadAudio) {
719
+ const audioAction = element.actions.find((a) => a.modelName === 'GridActionAudio');
720
+ if (audioAction && audioAction.dataBase64) {
721
+ audioRecording = {
722
+ id: parseInt(audioAction.id) || undefined,
723
+ data: Buffer.from(audioAction.dataBase64, 'base64'),
724
+ identifier: audioAction.filename,
725
+ metadata: JSON.stringify({
726
+ mimeType: audioAction.mimeType,
727
+ durationMs: audioAction.durationMs,
728
+ }),
729
+ };
730
+ }
731
+ }
732
+ const colorStyles = resolveButtonColors(element, colorConfig, activeColorScheme);
733
+ const navAction = element.actions.find((a) => a.modelName === 'GridActionNavigate');
734
+ const targetPageId = navAction ? navAction.toGridId : null;
735
+ // Determine button type based on actions
736
+ let buttonType = 'SPEAK';
737
+ if (targetPageId) {
738
+ buttonType = 'NAVIGATE';
739
+ }
740
+ const label = this.getLocalizedLabel(element.label);
741
+ // Create semantic action from AstericsGrid element
742
+ let semanticAction;
743
+ let legacyAction = null;
744
+ // Find the primary action
745
+ const primaryAction = element.actions[0]; // AstericsGrid typically has one primary action
746
+ if (navAction && targetPageId) {
747
+ semanticAction = {
748
+ category: treeStructure_1.AACSemanticCategory.NAVIGATION,
749
+ intent: treeStructure_1.AACSemanticIntent.NAVIGATE_TO,
750
+ targetId: targetPageId,
751
+ platformData: {
752
+ astericsGrid: {
753
+ modelName: navAction.modelName,
754
+ properties: navAction,
755
+ },
756
+ },
757
+ fallback: {
758
+ type: 'NAVIGATE',
759
+ targetPageId: targetPageId,
760
+ },
761
+ };
762
+ legacyAction = {
763
+ type: 'NAVIGATE',
764
+ targetPageId: targetPageId,
765
+ };
766
+ }
767
+ else {
768
+ // Check for other action types
769
+ const collectAction = element.actions.find((a) => a.modelName === 'GridActionCollectElement');
770
+ if (collectAction) {
771
+ // Handle text editing actions
772
+ switch (collectAction.action) {
773
+ case 'COLLECT_ACTION_REMOVE_WORD':
774
+ semanticAction = {
775
+ category: treeStructure_1.AACSemanticCategory.TEXT_EDITING,
776
+ intent: treeStructure_1.AACSemanticIntent.DELETE_WORD,
777
+ platformData: {
778
+ astericsGrid: {
779
+ modelName: collectAction.modelName,
780
+ properties: collectAction,
781
+ },
782
+ },
783
+ fallback: {
784
+ type: 'ACTION',
785
+ message: 'Delete word',
786
+ },
787
+ };
788
+ legacyAction = {
789
+ type: 'DELETE_WORD',
790
+ };
791
+ break;
792
+ case 'COLLECT_ACTION_REMOVE_CHAR':
793
+ semanticAction = {
794
+ category: treeStructure_1.AACSemanticCategory.TEXT_EDITING,
795
+ intent: treeStructure_1.AACSemanticIntent.DELETE_CHARACTER,
796
+ platformData: {
797
+ astericsGrid: {
798
+ modelName: collectAction.modelName,
799
+ properties: collectAction,
800
+ },
801
+ },
802
+ fallback: {
803
+ type: 'ACTION',
804
+ message: 'Delete character',
805
+ },
806
+ };
807
+ legacyAction = {
808
+ type: 'DELETE_CHARACTER',
809
+ };
810
+ break;
811
+ case 'COLLECT_ACTION_CLEAR':
812
+ semanticAction = {
813
+ category: treeStructure_1.AACSemanticCategory.TEXT_EDITING,
814
+ intent: treeStructure_1.AACSemanticIntent.CLEAR_TEXT,
815
+ platformData: {
816
+ astericsGrid: {
817
+ modelName: collectAction.modelName,
818
+ properties: collectAction,
819
+ },
820
+ },
821
+ fallback: {
822
+ type: 'ACTION',
823
+ message: 'Clear text',
824
+ },
825
+ };
826
+ legacyAction = {
827
+ type: 'CLEAR_TEXT',
828
+ };
829
+ break;
830
+ }
831
+ }
832
+ // Check for navigation actions with special nav types
833
+ if (!semanticAction && navAction) {
834
+ switch (navAction.navType) {
835
+ case 'TO_LAST':
836
+ semanticAction = {
837
+ category: treeStructure_1.AACSemanticCategory.NAVIGATION,
838
+ intent: treeStructure_1.AACSemanticIntent.GO_BACK,
839
+ platformData: {
840
+ astericsGrid: {
841
+ modelName: navAction.modelName,
842
+ properties: navAction,
843
+ },
844
+ },
845
+ fallback: {
846
+ type: 'ACTION',
847
+ message: 'Go back',
848
+ },
849
+ };
850
+ legacyAction = {
851
+ type: 'GO_BACK',
852
+ };
853
+ break;
854
+ case 'TO_HOME':
855
+ semanticAction = {
856
+ category: treeStructure_1.AACSemanticCategory.NAVIGATION,
857
+ intent: treeStructure_1.AACSemanticIntent.GO_HOME,
858
+ platformData: {
859
+ astericsGrid: {
860
+ modelName: navAction.modelName,
861
+ properties: navAction,
862
+ },
863
+ },
864
+ fallback: {
865
+ type: 'ACTION',
866
+ message: 'Go home',
867
+ },
868
+ };
869
+ legacyAction = {
870
+ type: 'GO_HOME',
871
+ };
872
+ break;
873
+ }
874
+ }
875
+ // Check for speak actions if no other semantic action was found
876
+ if (!semanticAction) {
877
+ const speakAction = element.actions.find((a) => a.modelName === 'GridActionSpeakCustom' || a.modelName === 'GridActionSpeak');
878
+ if (speakAction) {
879
+ const speakText = speakAction.modelName === 'GridActionSpeakCustom'
880
+ ? this.getLocalizedText(speakAction.speakText)
881
+ : label;
882
+ semanticAction = {
883
+ category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
884
+ intent: treeStructure_1.AACSemanticIntent.SPEAK_TEXT,
885
+ text: speakText,
886
+ platformData: {
887
+ astericsGrid: {
888
+ modelName: speakAction.modelName,
889
+ properties: speakAction,
890
+ },
891
+ },
892
+ fallback: {
893
+ type: 'SPEAK',
894
+ message: speakText,
895
+ },
896
+ };
897
+ }
898
+ else {
899
+ // Default speak action
900
+ semanticAction = {
901
+ category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
902
+ intent: treeStructure_1.AACSemanticIntent.SPEAK_TEXT,
903
+ text: label,
904
+ platformData: {
905
+ astericsGrid: {
906
+ modelName: 'GridActionSpeak',
907
+ properties: {},
908
+ },
909
+ },
910
+ fallback: {
911
+ type: 'SPEAK',
912
+ message: label,
913
+ },
914
+ };
915
+ }
916
+ }
917
+ }
918
+ // Determine the final background color
919
+ const finalBackgroundColor = element.backgroundColor || colorStyles.backgroundColor || colorConfig?.elementBackgroundColor || '#FFFFFF';
920
+ // Determine font color with priority:
921
+ // 1. Explicit element.fontColor (highest priority)
922
+ // 2. Resolved color from color category
923
+ // 3. Global colorConfig.fontColor
924
+ // 4. Automatic contrast calculation based on background (lowest priority)
925
+ const fontColor = element.fontColor ||
926
+ colorStyles.fontColor ||
927
+ colorConfig?.fontColor ||
928
+ getContrastingTextColor(finalBackgroundColor);
929
+ // Extract image data if present
930
+ let imageData;
931
+ let imageName;
932
+ if (element.image && element.image.data) {
933
+ // Asterics Grid stores images as Data URLs (e.g., "data:image/png;base64,...")
934
+ // We need to strip the Data URL prefix before decoding
935
+ try {
936
+ let base64Data = element.image.data;
937
+ let imageFormat = 'png'; // Default format
938
+ // Check if this is a Data URL and extract the base64 part
939
+ const dataUrlMatch = base64Data.match(/^data:image\/(png|jpeg|jpg|gif|svg\+xml);base64,(.+)/);
940
+ if (dataUrlMatch) {
941
+ imageFormat = dataUrlMatch[1];
942
+ base64Data = dataUrlMatch[2]; // Use only the base64 part, not the prefix
943
+ }
944
+ // Decode the base64 data
945
+ imageData = Buffer.from(base64Data, 'base64');
946
+ // Use detected format for filename
947
+ imageName = element.image.id || `image.${imageFormat}`;
948
+ }
949
+ catch (e) {
950
+ // Invalid base64 data, skip image
951
+ }
952
+ }
953
+ return new treeStructure_1.AACButton({
954
+ id: element.id,
955
+ label: label,
956
+ message: label,
957
+ targetPageId: targetPageId,
958
+ semanticAction: semanticAction,
959
+ audioRecording: audioRecording,
960
+ image: imageName, // Store image filename/reference
961
+ parameters: imageData ? {
962
+ ...{ imageData: imageData } // Store actual image data in parameters for conversion
963
+ } : undefined,
964
+ style: {
965
+ backgroundColor: finalBackgroundColor,
966
+ borderColor: colorStyles.borderColor ||
967
+ colorConfig?.elementBorderColor ||
968
+ '#CCCCCC',
969
+ borderWidth: colorConfig?.borderWidth || 1,
970
+ fontFamily: colorConfig?.fontFamily || 'Arial',
971
+ fontSize: colorConfig?.fontSizePct ? colorConfig.fontSizePct * 16 : 16, // Default to 16px
972
+ fontColor: fontColor,
973
+ },
974
+ });
975
+ }
976
+ processTexts(filePathOrBuffer, translations, outputPath) {
977
+ // Load and parse the original file
978
+ let content = Buffer.isBuffer(filePathOrBuffer)
979
+ ? filePathOrBuffer.toString('utf-8')
980
+ : fs_1.default.readFileSync(filePathOrBuffer, 'utf-8');
981
+ // Remove BOM if present
982
+ if (content.charCodeAt(0) === 0xfeff) {
983
+ content = content.slice(1);
984
+ }
985
+ const grdFile = JSON.parse(content);
986
+ // Apply translations directly to the JSON structure for comprehensive coverage
987
+ this.applyTranslationsToGridFile(grdFile, translations);
988
+ // Write the translated file
989
+ fs_1.default.writeFileSync(outputPath, JSON.stringify(grdFile, null, 2));
990
+ return fs_1.default.readFileSync(outputPath);
991
+ }
992
+ applyTranslationsToGridFile(grdFile, translations) {
993
+ grdFile.grids.forEach((grid) => {
994
+ // Translate grid labels
995
+ if (grid.label) {
996
+ Object.keys(grid.label).forEach((lang) => {
997
+ const originalText = grid.label[lang];
998
+ if (originalText && translations.has(originalText)) {
999
+ grid.label[lang] = translations.get(originalText);
1000
+ }
1001
+ });
1002
+ }
1003
+ // Translate grid elements
1004
+ grid.gridElements.forEach((element) => {
1005
+ // Translate element labels
1006
+ if (element.label) {
1007
+ Object.keys(element.label).forEach((lang) => {
1008
+ const originalText = element.label[lang];
1009
+ if (originalText && translations.has(originalText)) {
1010
+ element.label[lang] = translations.get(originalText);
1011
+ }
1012
+ });
1013
+ }
1014
+ // Translate word forms
1015
+ if (element.wordForms) {
1016
+ element.wordForms.forEach((wordForm) => {
1017
+ if (wordForm.value && translations.has(wordForm.value)) {
1018
+ wordForm.value = translations.get(wordForm.value);
1019
+ }
1020
+ });
1021
+ }
1022
+ // Translate action-specific texts
1023
+ element.actions.forEach((action) => {
1024
+ this.applyTranslationsToAction(action, translations);
1025
+ });
1026
+ });
1027
+ });
1028
+ }
1029
+ applyTranslationsToAction(action, translations) {
1030
+ switch (action.modelName) {
1031
+ case 'GridActionSpeakCustom':
1032
+ if (action.speakText && typeof action.speakText === 'object') {
1033
+ Object.keys(action.speakText).forEach((lang) => {
1034
+ const originalText = action.speakText[lang];
1035
+ if (typeof originalText === 'string' && translations.has(originalText)) {
1036
+ const translation = translations.get(originalText);
1037
+ if (translation) {
1038
+ action.speakText[lang] = translation;
1039
+ }
1040
+ }
1041
+ });
1042
+ }
1043
+ break;
1044
+ case 'GridActionChangeLang':
1045
+ if (typeof action.language === 'string' && translations.has(action.language)) {
1046
+ const translation = translations.get(action.language);
1047
+ if (translation) {
1048
+ action.language = translation;
1049
+ }
1050
+ }
1051
+ if (typeof action.voice === 'string' && translations.has(action.voice)) {
1052
+ const translation = translations.get(action.voice);
1053
+ if (translation) {
1054
+ action.voice = translation;
1055
+ }
1056
+ }
1057
+ break;
1058
+ case 'GridActionHTTP':
1059
+ if (typeof action.restUrl === 'string' && translations.has(action.restUrl)) {
1060
+ const translation = translations.get(action.restUrl);
1061
+ if (translation) {
1062
+ action.restUrl = translation;
1063
+ }
1064
+ }
1065
+ if (typeof action.body === 'string' && translations.has(action.body)) {
1066
+ const translation = translations.get(action.body);
1067
+ if (translation) {
1068
+ action.body = translation;
1069
+ }
1070
+ }
1071
+ break;
1072
+ case 'GridActionOpenWebpage':
1073
+ if (typeof action.openURL === 'string' && translations.has(action.openURL)) {
1074
+ const translation = translations.get(action.openURL);
1075
+ if (translation) {
1076
+ action.openURL = translation;
1077
+ }
1078
+ }
1079
+ break;
1080
+ case 'GridActionMatrix':
1081
+ if (typeof action.sendText === 'string' && translations.has(action.sendText)) {
1082
+ const translation = translations.get(action.sendText);
1083
+ if (translation) {
1084
+ action.sendText = translation;
1085
+ }
1086
+ }
1087
+ break;
1088
+ // Add more action types as needed
1089
+ }
1090
+ }
1091
+ saveFromTree(tree, outputPath) {
1092
+ // Use default Asterics Grid styling instead of taking from first page
1093
+ // This prevents issues where the first page has unusual colors (like purple)
1094
+ const defaultPageStyle = {
1095
+ backgroundColor: '#FFFFFF', // White background by default
1096
+ borderColor: '#CCCCCC',
1097
+ borderWidth: 1,
1098
+ fontFamily: 'Arial',
1099
+ fontSize: 16,
1100
+ fontColor: '#000000',
1101
+ };
1102
+ const grids = Object.values(tree.pages).map((page) => {
1103
+ // Create a map of button positions from the grid layout
1104
+ const buttonPositions = new Map();
1105
+ // Extract positions from the 2D grid if available
1106
+ if (page.grid && page.grid.length > 0) {
1107
+ page.grid.forEach((row, y) => {
1108
+ row.forEach((button, x) => {
1109
+ if (button) {
1110
+ buttonPositions.set(button.id, { x, y });
1111
+ }
1112
+ });
1113
+ });
1114
+ }
1115
+ // Filter out navigation/system buttons if configured
1116
+ const filteredButtons = this.filterPageButtons(page.buttons);
1117
+ const gridElements = filteredButtons.map((button, index) => {
1118
+ // Use grid position if available, otherwise arrange in rows of 4
1119
+ const gridWidth = 4;
1120
+ const position = buttonPositions.get(button.id);
1121
+ const calculatedX = position ? position.x : index % gridWidth;
1122
+ const calculatedY = position ? position.y : Math.floor(index / gridWidth);
1123
+ const actions = [];
1124
+ // Add appropriate actions - prefer semantic actions
1125
+ if (button.semanticAction?.platformData?.astericsGrid) {
1126
+ // Use original AstericsGrid action data
1127
+ const astericsData = button.semanticAction.platformData.astericsGrid;
1128
+ actions.push({
1129
+ id: `grid-action-${button.id}`,
1130
+ ...astericsData.properties,
1131
+ modelName: astericsData.modelName,
1132
+ modelVersion: astericsData.properties.modelVersion || '{"major": 5, "minor": 0, "patch": 0}',
1133
+ });
1134
+ }
1135
+ else if (button.semanticAction?.intent === treeStructure_1.AACSemanticIntent.NAVIGATE_TO) {
1136
+ // Create navigation action from semantic data
1137
+ const targetId = button.semanticAction.targetId || button.targetPageId;
1138
+ actions.push({
1139
+ id: `grid-action-navigate-${button.id}`,
1140
+ modelName: 'GridActionNavigate',
1141
+ modelVersion: '{"major": 5, "minor": 0, "patch": 0}',
1142
+ navType: 'navigateToGrid',
1143
+ toGridId: targetId,
1144
+ });
1145
+ }
1146
+ else if (button.semanticAction?.intent === treeStructure_1.AACSemanticIntent.GO_BACK) {
1147
+ // Create back navigation action
1148
+ actions.push({
1149
+ id: `grid-action-navigate-back-${button.id}`,
1150
+ modelName: 'GridActionNavigate',
1151
+ modelVersion: '{"major": 5, "minor": 0, "patch": 0}',
1152
+ navType: 'TO_LAST',
1153
+ });
1154
+ }
1155
+ else if (button.semanticAction?.intent === treeStructure_1.AACSemanticIntent.GO_HOME) {
1156
+ // Create home navigation action
1157
+ actions.push({
1158
+ id: `grid-action-navigate-home-${button.id}`,
1159
+ modelName: 'GridActionNavigate',
1160
+ modelVersion: '{"major": 5, "minor": 0, "patch": 0}',
1161
+ navType: 'TO_HOME',
1162
+ });
1163
+ }
1164
+ else if (button.semanticAction?.intent === treeStructure_1.AACSemanticIntent.DELETE_WORD) {
1165
+ // Create delete word action
1166
+ actions.push({
1167
+ id: `grid-action-delete-word-${button.id}`,
1168
+ modelName: 'GridActionCollectElement',
1169
+ modelVersion: '{"major": 5, "minor": 0, "patch": 0}',
1170
+ action: 'COLLECT_ACTION_REMOVE_WORD',
1171
+ });
1172
+ }
1173
+ else if (button.semanticAction?.intent === treeStructure_1.AACSemanticIntent.DELETE_CHARACTER) {
1174
+ // Create delete character action
1175
+ actions.push({
1176
+ id: `grid-action-delete-char-${button.id}`,
1177
+ modelName: 'GridActionCollectElement',
1178
+ modelVersion: '{"major": 5, "minor": 0, "patch": 0}',
1179
+ action: 'COLLECT_ACTION_REMOVE_CHAR',
1180
+ });
1181
+ }
1182
+ else if (button.semanticAction?.intent === treeStructure_1.AACSemanticIntent.CLEAR_TEXT) {
1183
+ // Create clear text action
1184
+ actions.push({
1185
+ id: `grid-action-clear-${button.id}`,
1186
+ modelName: 'GridActionCollectElement',
1187
+ modelVersion: '{"major": 5, "minor": 0, "patch": 0}',
1188
+ action: 'COLLECT_ACTION_CLEAR',
1189
+ });
1190
+ }
1191
+ else if (button.semanticAction?.intent === treeStructure_1.AACSemanticIntent.SPEAK_TEXT) {
1192
+ // Create speak action from semantic data
1193
+ if (button.semanticAction.text && button.semanticAction.text !== button.label) {
1194
+ actions.push({
1195
+ id: `grid-action-speak-${button.id}`,
1196
+ modelName: 'GridActionSpeakCustom',
1197
+ modelVersion: '{"major": 5, "minor": 0, "patch": 0}',
1198
+ speakText: { en: button.semanticAction.text },
1199
+ });
1200
+ }
1201
+ else {
1202
+ actions.push({
1203
+ id: `grid-action-speak-${button.id}`,
1204
+ modelName: 'GridActionSpeak',
1205
+ modelVersion: '{"major": 5, "minor": 0, "patch": 0}',
1206
+ });
1207
+ }
1208
+ }
1209
+ else {
1210
+ // Default to speak action if no semantic action
1211
+ actions.push({
1212
+ id: `grid-action-speak-${button.id}`,
1213
+ modelName: 'GridActionSpeak',
1214
+ modelVersion: '{"major": 5, "minor": 0, "patch": 0}',
1215
+ });
1216
+ }
1217
+ // Add audio action if present
1218
+ if (button.audioRecording && button.audioRecording.data) {
1219
+ const metadata = JSON.parse(button.audioRecording.metadata || '{}');
1220
+ actions.push({
1221
+ id: button.audioRecording.id?.toString() || `grid-action-audio-${button.id}`,
1222
+ modelName: 'GridActionAudio',
1223
+ modelVersion: '{"major": 5, "minor": 0, "patch": 0}',
1224
+ dataBase64: button.audioRecording.data.toString('base64'),
1225
+ mimeType: metadata.mimeType || 'audio/wav',
1226
+ durationMs: metadata.durationMs || 0,
1227
+ filename: button.audioRecording.identifier || `audio-${button.id}`,
1228
+ });
1229
+ }
1230
+ return {
1231
+ id: button.id,
1232
+ modelName: 'GridElement',
1233
+ modelVersion: '{"major": 5, "minor": 0, "patch": 0}',
1234
+ width: 1,
1235
+ height: 1,
1236
+ x: calculatedX,
1237
+ y: calculatedY,
1238
+ label: { en: button.label },
1239
+ wordForms: [],
1240
+ image: {
1241
+ data: null,
1242
+ author: undefined,
1243
+ authorURL: undefined,
1244
+ },
1245
+ actions: actions,
1246
+ type: 'ELEMENT_TYPE_NORMAL',
1247
+ additionalProps: {},
1248
+ backgroundColor: button.style?.backgroundColor ||
1249
+ page.style?.backgroundColor ||
1250
+ defaultPageStyle.backgroundColor,
1251
+ };
1252
+ });
1253
+ // Calculate grid dimensions based on button count
1254
+ const gridWidth = 4;
1255
+ const buttonCount = page.buttons.length;
1256
+ const calculatedRows = Math.max(3, Math.ceil(buttonCount / gridWidth));
1257
+ const calculatedCols = Math.max(3, Math.min(gridWidth, buttonCount));
1258
+ return {
1259
+ id: page.id,
1260
+ modelName: 'GridData',
1261
+ modelVersion: '{"major": 5, "minor": 0, "patch": 0}',
1262
+ label: { en: page.name },
1263
+ rowCount: calculatedRows,
1264
+ minColumnCount: calculatedCols,
1265
+ gridElements: gridElements,
1266
+ };
1267
+ });
1268
+ // Determine the home grid ID from tree.rootId, fallback to first grid
1269
+ const homeGridId = tree.rootId || (grids.length > 0 ? grids[0].id : undefined);
1270
+ const grdFile = {
1271
+ grids: grids,
1272
+ metadata: {
1273
+ homeGridId: homeGridId,
1274
+ colorConfig: {
1275
+ gridBackgroundColor: defaultPageStyle.backgroundColor,
1276
+ elementBackgroundColor: defaultPageStyle.backgroundColor,
1277
+ elementBorderColor: defaultPageStyle.borderColor,
1278
+ borderWidth: defaultPageStyle.borderWidth,
1279
+ fontFamily: defaultPageStyle.fontFamily,
1280
+ fontSizePct: defaultPageStyle.fontSize / 16, // Convert pixels to percentage
1281
+ fontColor: defaultPageStyle.fontColor,
1282
+ // Add additional properties that might be useful
1283
+ elementMargin: 2, // Default margin
1284
+ borderRadius: 4, // Default border radius
1285
+ colorMode: 'default',
1286
+ lineHeight: 1.2,
1287
+ maxLines: 2,
1288
+ textPosition: 'center',
1289
+ fittingMode: 'fit',
1290
+ },
1291
+ },
1292
+ };
1293
+ fs_1.default.writeFileSync(outputPath, JSON.stringify(grdFile, null, 2));
1294
+ }
1295
+ /**
1296
+ * Add audio recording to a specific grid element
1297
+ */
1298
+ addAudioToElement(filePath, elementId, audioData, metadata) {
1299
+ let content = fs_1.default.readFileSync(filePath, 'utf-8');
1300
+ // Remove BOM if present
1301
+ if (content.charCodeAt(0) === 0xfeff) {
1302
+ content = content.slice(1);
1303
+ }
1304
+ const grdFile = JSON.parse(content);
1305
+ // Find the element and add audio action
1306
+ let elementFound = false;
1307
+ grdFile.grids.forEach((grid) => {
1308
+ grid.gridElements.forEach((element) => {
1309
+ if (element.id === elementId) {
1310
+ elementFound = true;
1311
+ // Remove existing audio action if present
1312
+ element.actions = element.actions.filter((a) => a.modelName !== 'GridActionAudio');
1313
+ // Add new audio action
1314
+ const audioAction = {
1315
+ id: `grid-action-audio-${elementId}`,
1316
+ modelName: 'GridActionAudio',
1317
+ modelVersion: '{"major": 5, "minor": 0, "patch": 0}',
1318
+ dataBase64: audioData.toString('base64'),
1319
+ mimeType: 'audio/wav',
1320
+ durationMs: 0, // Could be calculated from audio data
1321
+ filename: `audio-${elementId}.wav`,
1322
+ };
1323
+ if (metadata) {
1324
+ try {
1325
+ const parsedMetadata = JSON.parse(metadata);
1326
+ audioAction.mimeType = parsedMetadata.mimeType || audioAction.mimeType;
1327
+ audioAction.durationMs = parsedMetadata.durationMs || audioAction.durationMs;
1328
+ audioAction.filename = parsedMetadata.filename || audioAction.filename;
1329
+ }
1330
+ catch (e) {
1331
+ // Use defaults if metadata parsing fails
1332
+ }
1333
+ }
1334
+ element.actions.push(audioAction);
1335
+ }
1336
+ });
1337
+ });
1338
+ if (!elementFound) {
1339
+ throw new Error(`Element with ID ${elementId} not found`);
1340
+ }
1341
+ // Write back to file
1342
+ fs_1.default.writeFileSync(filePath, JSON.stringify(grdFile, null, 2));
1343
+ }
1344
+ /**
1345
+ * Create a copy of the grid file with audio recordings added
1346
+ */
1347
+ createAudioEnhancedGridFile(sourceFilePath, targetFilePath, audioMappings) {
1348
+ // Copy the source file to target
1349
+ fs_1.default.copyFileSync(sourceFilePath, targetFilePath);
1350
+ // Add audio recordings to the copy
1351
+ audioMappings.forEach((audioInfo, elementId) => {
1352
+ try {
1353
+ this.addAudioToElement(targetFilePath, elementId, audioInfo.audioData, audioInfo.metadata);
1354
+ }
1355
+ catch (error) {
1356
+ // Failed to add audio to element - continue with others
1357
+ console.warn(`Failed to add audio to element ${elementId}:`, error);
1358
+ }
1359
+ });
1360
+ }
1361
+ /**
1362
+ * Extract all element IDs from the grid file for audio mapping
1363
+ */
1364
+ getElementIds(filePathOrBuffer) {
1365
+ let content = Buffer.isBuffer(filePathOrBuffer)
1366
+ ? filePathOrBuffer.toString('utf-8')
1367
+ : fs_1.default.readFileSync(filePathOrBuffer, 'utf-8');
1368
+ // Remove BOM if present
1369
+ if (content.charCodeAt(0) === 0xfeff) {
1370
+ content = content.slice(1);
1371
+ }
1372
+ const elementIds = [];
1373
+ try {
1374
+ const grdFile = JSON.parse(content);
1375
+ grdFile.grids.forEach((grid) => {
1376
+ grid.gridElements.forEach((element) => {
1377
+ elementIds.push(element.id);
1378
+ });
1379
+ });
1380
+ }
1381
+ catch (error) {
1382
+ // If JSON parsing fails, return empty array
1383
+ }
1384
+ return elementIds;
1385
+ }
1386
+ /**
1387
+ * Check if an element has audio recording
1388
+ */
1389
+ hasAudioRecording(filePathOrBuffer, elementId) {
1390
+ let content = Buffer.isBuffer(filePathOrBuffer)
1391
+ ? filePathOrBuffer.toString('utf-8')
1392
+ : fs_1.default.readFileSync(filePathOrBuffer, 'utf-8');
1393
+ // Remove BOM if present
1394
+ if (content.charCodeAt(0) === 0xfeff) {
1395
+ content = content.slice(1);
1396
+ }
1397
+ try {
1398
+ const grdFile = JSON.parse(content);
1399
+ for (const grid of grdFile.grids) {
1400
+ for (const element of grid.gridElements) {
1401
+ if (element.id === elementId) {
1402
+ return element.actions.some((action) => action.modelName === 'GridActionAudio');
1403
+ }
1404
+ }
1405
+ }
1406
+ }
1407
+ catch (error) {
1408
+ // If JSON parsing fails, return false
1409
+ }
1410
+ return false;
1411
+ }
1412
+ /**
1413
+ * Extract strings with metadata for aac-tools-platform compatibility
1414
+ * Uses the generic implementation from BaseProcessor
1415
+ */
1416
+ extractStringsWithMetadata(filePath) {
1417
+ return this.extractStringsWithMetadataGeneric(filePath);
1418
+ }
1419
+ /**
1420
+ * Generate translated download for aac-tools-platform compatibility
1421
+ * Uses the generic implementation from BaseProcessor
1422
+ */
1423
+ generateTranslatedDownload(filePath, translatedStrings, sourceStrings) {
1424
+ return this.generateTranslatedDownloadGeneric(filePath, translatedStrings, sourceStrings);
1425
+ }
1426
+ }
1427
+ exports.AstericsGridProcessor = AstericsGridProcessor;