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