@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.
- package/README.md +52 -852
- package/dist/browser/core/baseProcessor.js +241 -0
- package/dist/browser/core/stringCasing.js +179 -0
- package/dist/browser/core/treeStructure.js +255 -0
- package/dist/browser/index.browser.js +73 -0
- package/dist/browser/processors/applePanelsProcessor.js +582 -0
- package/dist/browser/processors/astericsGridProcessor.js +1509 -0
- package/dist/browser/processors/dotProcessor.js +221 -0
- package/dist/browser/processors/gridset/commands.js +962 -0
- package/dist/browser/processors/gridset/crypto.js +53 -0
- package/dist/browser/processors/gridset/password.js +43 -0
- package/dist/browser/processors/gridset/pluginTypes.js +277 -0
- package/dist/browser/processors/gridset/resolver.js +137 -0
- package/dist/browser/processors/gridset/symbolAlignment.js +276 -0
- package/dist/browser/processors/gridset/symbols.js +421 -0
- package/dist/browser/processors/gridsetProcessor.js +2002 -0
- package/dist/browser/processors/obfProcessor.js +705 -0
- package/dist/browser/processors/opmlProcessor.js +274 -0
- package/dist/browser/types/aac.js +38 -0
- package/dist/browser/utilities/analytics/utils/idGenerator.js +89 -0
- package/dist/browser/utilities/translation/translationProcessor.js +200 -0
- package/dist/browser/utils/io.js +95 -0
- package/dist/browser/validation/baseValidator.js +156 -0
- package/dist/browser/validation/gridsetValidator.js +355 -0
- package/dist/browser/validation/obfValidator.js +500 -0
- package/dist/browser/validation/validationTypes.js +46 -0
- package/dist/cli/index.js +5 -5
- package/dist/core/analyze.d.ts +2 -2
- package/dist/core/analyze.js +2 -2
- package/dist/core/baseProcessor.d.ts +5 -4
- package/dist/core/baseProcessor.js +22 -27
- package/dist/core/treeStructure.d.ts +5 -5
- package/dist/core/treeStructure.js +1 -4
- package/dist/index.browser.d.ts +37 -0
- package/dist/index.browser.js +99 -0
- package/dist/index.d.ts +1 -48
- package/dist/index.js +1 -136
- package/dist/index.node.d.ts +48 -0
- package/dist/index.node.js +152 -0
- package/dist/processors/applePanelsProcessor.d.ts +5 -4
- package/dist/processors/applePanelsProcessor.js +58 -62
- package/dist/processors/astericsGridProcessor.d.ts +7 -6
- package/dist/processors/astericsGridProcessor.js +31 -42
- package/dist/processors/dotProcessor.d.ts +5 -4
- package/dist/processors/dotProcessor.js +25 -33
- package/dist/processors/excelProcessor.d.ts +4 -3
- package/dist/processors/excelProcessor.js +6 -3
- package/dist/processors/gridset/crypto.d.ts +18 -0
- package/dist/processors/gridset/crypto.js +57 -0
- package/dist/processors/gridset/helpers.d.ts +1 -1
- package/dist/processors/gridset/helpers.js +18 -8
- package/dist/processors/gridset/password.d.ts +20 -3
- package/dist/processors/gridset/password.js +17 -3
- package/dist/processors/gridset/wordlistHelpers.d.ts +3 -3
- package/dist/processors/gridset/wordlistHelpers.js +21 -20
- package/dist/processors/gridsetProcessor.d.ts +7 -12
- package/dist/processors/gridsetProcessor.js +118 -77
- package/dist/processors/obfProcessor.d.ts +9 -7
- package/dist/processors/obfProcessor.js +131 -56
- package/dist/processors/obfsetProcessor.d.ts +5 -4
- package/dist/processors/obfsetProcessor.js +10 -16
- package/dist/processors/opmlProcessor.d.ts +5 -4
- package/dist/processors/opmlProcessor.js +27 -34
- package/dist/processors/snapProcessor.d.ts +8 -7
- package/dist/processors/snapProcessor.js +15 -12
- package/dist/processors/touchchatProcessor.d.ts +8 -7
- package/dist/processors/touchchatProcessor.js +22 -17
- package/dist/types/aac.d.ts +0 -2
- package/dist/types/aac.js +2 -0
- package/dist/utils/io.d.ts +12 -0
- package/dist/utils/io.js +107 -0
- package/dist/validation/gridsetValidator.js +7 -7
- package/dist/validation/snapValidator.js +28 -35
- package/docs/BROWSER_USAGE.md +618 -0
- package/examples/README.md +77 -0
- package/examples/browser-test-server.js +81 -0
- package/examples/browser-test.html +331 -0
- package/examples/vitedemo/QUICKSTART.md +74 -0
- package/examples/vitedemo/README.md +157 -0
- package/examples/vitedemo/index.html +376 -0
- package/examples/vitedemo/package-lock.json +1221 -0
- package/examples/vitedemo/package.json +18 -0
- package/examples/vitedemo/src/main.ts +519 -0
- package/examples/vitedemo/test-files/example.dot +14 -0
- package/examples/vitedemo/test-files/example.grd +1 -0
- package/examples/vitedemo/test-files/example.gridset +0 -0
- package/examples/vitedemo/test-files/example.obz +0 -0
- package/examples/vitedemo/test-files/example.opml +18 -0
- package/examples/vitedemo/test-files/simple.obf +53 -0
- package/examples/vitedemo/tsconfig.json +24 -0
- package/examples/vitedemo/vite.config.ts +34 -0
- 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 };
|