@willwade/aac-processors 0.0.29 → 0.1.0

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