@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,1339 @@
|
|
|
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.GridsetProcessor = void 0;
|
|
7
|
+
const baseProcessor_1 = require("../core/baseProcessor");
|
|
8
|
+
const treeStructure_1 = require("../core/treeStructure");
|
|
9
|
+
const adm_zip_1 = __importDefault(require("adm-zip"));
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const fast_xml_parser_1 = require("fast-xml-parser");
|
|
12
|
+
const resolver_1 = require("./gridset/resolver");
|
|
13
|
+
class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
14
|
+
constructor(options) {
|
|
15
|
+
super(options);
|
|
16
|
+
}
|
|
17
|
+
// Helper function to ensure color has alpha channel (Grid3 format)
|
|
18
|
+
ensureAlphaChannel(color) {
|
|
19
|
+
if (!color)
|
|
20
|
+
return '#FFFFFFFF';
|
|
21
|
+
// If already 8 digits (with alpha), return as is
|
|
22
|
+
if (color.match(/^#[0-9A-Fa-f]{8}$/))
|
|
23
|
+
return color;
|
|
24
|
+
// If 6 digits (no alpha), add FF for fully opaque
|
|
25
|
+
if (color.match(/^#[0-9A-Fa-f]{6}$/))
|
|
26
|
+
return color + 'FF';
|
|
27
|
+
// If 3 digits (shorthand), expand to 8
|
|
28
|
+
if (color.match(/^#[0-9A-Fa-f]{3}$/)) {
|
|
29
|
+
const r = color[1];
|
|
30
|
+
const g = color[2];
|
|
31
|
+
const b = color[3];
|
|
32
|
+
return `#${r}${r}${g}${g}${b}${b}FF`;
|
|
33
|
+
}
|
|
34
|
+
// Invalid or unknown format, return white
|
|
35
|
+
return '#FFFFFFFF';
|
|
36
|
+
}
|
|
37
|
+
// Helper function to generate Grid3 commands from semantic actions
|
|
38
|
+
generateCommandsFromSemanticAction(button, tree) {
|
|
39
|
+
const semanticAction = button.semanticAction;
|
|
40
|
+
if (!semanticAction) {
|
|
41
|
+
// Default to insert text action with structured XML format
|
|
42
|
+
// Use two <s> elements: one for the word, one for the space (CDATA preserves whitespace)
|
|
43
|
+
let text = button.message || button.label || '';
|
|
44
|
+
// Remove trailing space from message if present (we'll add it as separate segment)
|
|
45
|
+
if (text.endsWith(' ')) {
|
|
46
|
+
text = text.slice(0, -1);
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
Command: {
|
|
50
|
+
'@_ID': 'Action.InsertText',
|
|
51
|
+
Parameter: {
|
|
52
|
+
'@_Key': 'text',
|
|
53
|
+
p: {
|
|
54
|
+
s: [
|
|
55
|
+
{
|
|
56
|
+
r: text,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
r: { '__cdata': ' ' },
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// Use platform-specific Grid3 data if available
|
|
68
|
+
if (semanticAction.platformData?.grid3) {
|
|
69
|
+
const grid3Data = semanticAction.platformData.grid3;
|
|
70
|
+
const params = Object.entries(grid3Data.parameters || {}).map(([key, value]) => ({
|
|
71
|
+
'@_Key': key,
|
|
72
|
+
'#text': String(value),
|
|
73
|
+
}));
|
|
74
|
+
return {
|
|
75
|
+
Command: {
|
|
76
|
+
'@_ID': grid3Data.commandId,
|
|
77
|
+
...(params.length > 0 ? { Parameter: params } : {}),
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// Convert semantic actions to Grid3 commands
|
|
82
|
+
const intentStr = String(semanticAction.intent);
|
|
83
|
+
switch (intentStr) {
|
|
84
|
+
case 'NAVIGATE_TO': {
|
|
85
|
+
// For Grid3, we need to use the grid name, not the ID
|
|
86
|
+
let targetGridName = semanticAction.targetId || '';
|
|
87
|
+
if (tree && semanticAction.targetId) {
|
|
88
|
+
const targetPage = tree.getPage(semanticAction.targetId);
|
|
89
|
+
if (targetPage) {
|
|
90
|
+
targetGridName = targetPage.name || targetPage.id;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
Command: {
|
|
95
|
+
'@_ID': 'Jump.To',
|
|
96
|
+
Parameter: {
|
|
97
|
+
'@_Key': 'grid',
|
|
98
|
+
'#text': targetGridName,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
case 'GO_BACK':
|
|
104
|
+
return {
|
|
105
|
+
Command: {
|
|
106
|
+
'@_ID': 'Jump.Back',
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
case 'GO_HOME':
|
|
110
|
+
return {
|
|
111
|
+
Command: {
|
|
112
|
+
'@_ID': 'Jump.Home',
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
case 'DELETE_WORD':
|
|
116
|
+
return {
|
|
117
|
+
Command: {
|
|
118
|
+
'@_ID': 'Action.DeleteWord',
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
case 'DELETE_CHARACTER':
|
|
122
|
+
return {
|
|
123
|
+
Command: {
|
|
124
|
+
'@_ID': 'Action.DeleteLetter',
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
case 'CLEAR_TEXT':
|
|
128
|
+
return {
|
|
129
|
+
Command: {
|
|
130
|
+
'@_ID': 'Action.Clear',
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
case 'SPEAK_TEXT':
|
|
134
|
+
case 'SPEAK_IMMEDIATE':
|
|
135
|
+
// For communication buttons, insert text into message bar (sentence building)
|
|
136
|
+
// Grid3 requires explicit trailing space for automatic word spacing
|
|
137
|
+
// Use two <s> elements: one for the word, one for the space (CDATA preserves whitespace)
|
|
138
|
+
// Users can speak the complete sentence with a dedicated Speak button
|
|
139
|
+
{
|
|
140
|
+
let text = semanticAction.text || button.message || button.label || '';
|
|
141
|
+
// Remove trailing space from message if present (we'll add it as separate segment)
|
|
142
|
+
if (text.endsWith(' ')) {
|
|
143
|
+
text = text.slice(0, -1);
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
Command: {
|
|
147
|
+
'@_ID': 'Action.InsertText',
|
|
148
|
+
Parameter: {
|
|
149
|
+
'@_Key': 'text',
|
|
150
|
+
p: {
|
|
151
|
+
s: [
|
|
152
|
+
{
|
|
153
|
+
r: text,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
r: { '__cdata': ' ' },
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
case 'INSERT_TEXT':
|
|
165
|
+
// Add trailing space for word buttons to enable sentence building
|
|
166
|
+
// Use two <s> elements: one for the word, one for the space (CDATA preserves whitespace)
|
|
167
|
+
{
|
|
168
|
+
let text = semanticAction.text || button.message || button.label || '';
|
|
169
|
+
// Remove trailing space from message if present (we'll add it as separate segment)
|
|
170
|
+
if (text.endsWith(' ')) {
|
|
171
|
+
text = text.slice(0, -1);
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
Command: {
|
|
175
|
+
'@_ID': 'Action.InsertText',
|
|
176
|
+
Parameter: {
|
|
177
|
+
'@_Key': 'text',
|
|
178
|
+
p: {
|
|
179
|
+
s: [
|
|
180
|
+
{
|
|
181
|
+
r: text,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
r: { '__cdata': ' ' },
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
default:
|
|
193
|
+
// Fallback to insert text with structured XML format
|
|
194
|
+
// Use two <s> elements: one for the word, one for the space (CDATA preserves whitespace)
|
|
195
|
+
{
|
|
196
|
+
let text = semanticAction.text || button.message || button.label || '';
|
|
197
|
+
// Remove trailing space from message if present (we'll add it as separate segment)
|
|
198
|
+
if (text.endsWith(' ')) {
|
|
199
|
+
text = text.slice(0, -1);
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
Command: {
|
|
203
|
+
'@_ID': 'Action.InsertText',
|
|
204
|
+
Parameter: {
|
|
205
|
+
'@_Key': 'text',
|
|
206
|
+
p: {
|
|
207
|
+
s: [
|
|
208
|
+
{
|
|
209
|
+
r: text,
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
r: { '__cdata': ' ' },
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Helper function to convert Grid 3 style to AACStyle
|
|
223
|
+
convertGrid3StyleToAACStyle(grid3Style) {
|
|
224
|
+
if (!grid3Style)
|
|
225
|
+
return {};
|
|
226
|
+
return {
|
|
227
|
+
backgroundColor: grid3Style.BackColour || grid3Style.TileColour,
|
|
228
|
+
borderColor: grid3Style.BorderColour,
|
|
229
|
+
fontColor: grid3Style.FontColour,
|
|
230
|
+
fontFamily: grid3Style.FontName,
|
|
231
|
+
fontSize: grid3Style.FontSize ? parseInt(String(grid3Style.FontSize)) : undefined,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
// Helper function to get style by ID or return default
|
|
235
|
+
getStyleById(styles, styleId) {
|
|
236
|
+
if (!styleId || !styles.has(styleId)) {
|
|
237
|
+
return {};
|
|
238
|
+
}
|
|
239
|
+
return this.convertGrid3StyleToAACStyle(styles.get(styleId));
|
|
240
|
+
}
|
|
241
|
+
// Helper to safely extract text from XML parser values
|
|
242
|
+
textOf(val) {
|
|
243
|
+
if (!val)
|
|
244
|
+
return undefined;
|
|
245
|
+
if (typeof val === 'string')
|
|
246
|
+
return val;
|
|
247
|
+
if (typeof val === 'object' && '#text' in val)
|
|
248
|
+
return String(val['#text']);
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
extractTexts(filePathOrBuffer) {
|
|
252
|
+
const buffer = Buffer.isBuffer(filePathOrBuffer)
|
|
253
|
+
? filePathOrBuffer
|
|
254
|
+
: fs_1.default.readFileSync(filePathOrBuffer);
|
|
255
|
+
const tree = this.loadIntoTree(buffer);
|
|
256
|
+
const texts = [];
|
|
257
|
+
for (const pageId in tree.pages) {
|
|
258
|
+
const page = tree.pages[pageId];
|
|
259
|
+
if (page.name)
|
|
260
|
+
texts.push(page.name);
|
|
261
|
+
page.buttons.forEach((btn) => {
|
|
262
|
+
if (btn.label)
|
|
263
|
+
texts.push(btn.label);
|
|
264
|
+
if (btn.message && btn.message !== btn.label)
|
|
265
|
+
texts.push(btn.message);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return texts;
|
|
269
|
+
}
|
|
270
|
+
loadIntoTree(filePathOrBuffer) {
|
|
271
|
+
const tree = new treeStructure_1.AACTree();
|
|
272
|
+
let zip;
|
|
273
|
+
try {
|
|
274
|
+
zip = new adm_zip_1.default(filePathOrBuffer);
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
throw new Error(`Invalid ZIP file format: ${error.message}`);
|
|
278
|
+
}
|
|
279
|
+
const parser = new fast_xml_parser_1.XMLParser({ ignoreAttributes: false });
|
|
280
|
+
// Parse FileMap.xml if present to index dynamic files per grid
|
|
281
|
+
const fileMapIndex = new Map();
|
|
282
|
+
try {
|
|
283
|
+
const fmEntry = zip.getEntries().find((e) => e.entryName.endsWith('FileMap.xml'));
|
|
284
|
+
if (fmEntry) {
|
|
285
|
+
const fmXml = fmEntry.getData().toString('utf8');
|
|
286
|
+
const fmData = parser.parse(fmXml);
|
|
287
|
+
const entries = fmData?.FileMap?.Entries?.Entry || fmData?.fileMap?.entries?.entry;
|
|
288
|
+
if (entries) {
|
|
289
|
+
const arr = Array.isArray(entries) ? entries : [entries];
|
|
290
|
+
for (const ent of arr) {
|
|
291
|
+
const staticFile = (ent['@_StaticFile'] ||
|
|
292
|
+
ent.StaticFile ||
|
|
293
|
+
ent.staticFile ||
|
|
294
|
+
'').replace(/\\/g, '/');
|
|
295
|
+
if (!staticFile)
|
|
296
|
+
continue;
|
|
297
|
+
const df = ent.DynamicFiles || ent.dynamicFiles;
|
|
298
|
+
const candidates = df?.File || df?.file || df?.Files || df?.files;
|
|
299
|
+
const list = Array.isArray(candidates) ? candidates : candidates ? [candidates] : [];
|
|
300
|
+
const files = [];
|
|
301
|
+
for (const v of list) {
|
|
302
|
+
if (!v)
|
|
303
|
+
continue;
|
|
304
|
+
if (typeof v === 'string')
|
|
305
|
+
files.push(v.replace(/\\/g, '/'));
|
|
306
|
+
else if (typeof v === 'object' && '#text' in v)
|
|
307
|
+
files.push(String(v['#text']).replace(/\\/g, '/'));
|
|
308
|
+
}
|
|
309
|
+
fileMapIndex.set(staticFile, files);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch (e) {
|
|
315
|
+
/* ignore: optional FileMap.xml may be missing or malformed */
|
|
316
|
+
}
|
|
317
|
+
// First, load styles from Settings0/Styles/styles.xml (Grid3 format)
|
|
318
|
+
const styles = new Map();
|
|
319
|
+
const styleEntry = zip
|
|
320
|
+
.getEntries()
|
|
321
|
+
.find((entry) => entry.entryName.endsWith('styles.xml') || entry.entryName.endsWith('style.xml'));
|
|
322
|
+
if (styleEntry) {
|
|
323
|
+
try {
|
|
324
|
+
const styleXmlContent = styleEntry.getData().toString('utf8');
|
|
325
|
+
const styleData = parser.parse(styleXmlContent);
|
|
326
|
+
// Parse styles and store them in the map
|
|
327
|
+
// Grid3 uses StyleData.Styles.Style with Key attribute
|
|
328
|
+
if (styleData.StyleData?.Styles?.Style) {
|
|
329
|
+
const styleArray = Array.isArray(styleData.StyleData.Styles.Style)
|
|
330
|
+
? styleData.StyleData.Styles.Style
|
|
331
|
+
: [styleData.StyleData.Styles.Style];
|
|
332
|
+
styleArray.forEach((style) => {
|
|
333
|
+
if (style['@_Key']) {
|
|
334
|
+
styles.set(String(style['@_Key']), style);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
// Also handle legacy format with @_ID
|
|
339
|
+
else if (styleData.Styles?.Style) {
|
|
340
|
+
const styleArray = Array.isArray(styleData.Styles.Style)
|
|
341
|
+
? styleData.Styles.Style
|
|
342
|
+
: [styleData.Styles.Style];
|
|
343
|
+
styleArray.forEach((style) => {
|
|
344
|
+
if (style['@_ID']) {
|
|
345
|
+
styles.set(String(style['@_ID']), style);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
catch (e) {
|
|
351
|
+
console.warn('Failed to parse styles.xml:', e);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// Debug: log all entry names
|
|
355
|
+
// console.log('Gridset zip entries:', zip.getEntries().map(e => e.entryName));
|
|
356
|
+
// First pass: collect all grid names and IDs for navigation resolution
|
|
357
|
+
const gridNameToIdMap = new Map();
|
|
358
|
+
const gridIdToNameMap = new Map();
|
|
359
|
+
zip.getEntries().forEach((entry) => {
|
|
360
|
+
if (entry.entryName.startsWith('Grids/') && entry.entryName.endsWith('grid.xml')) {
|
|
361
|
+
try {
|
|
362
|
+
const xmlContent = entry.getData().toString('utf8');
|
|
363
|
+
const data = parser.parse(xmlContent);
|
|
364
|
+
const grid = data.Grid || data.grid;
|
|
365
|
+
if (!grid)
|
|
366
|
+
return;
|
|
367
|
+
const gridId = this.textOf(grid.GridGuid || grid.gridGuid || grid.id);
|
|
368
|
+
let gridName = this.textOf(grid.Name) || this.textOf(grid.name) || this.textOf(grid['@_Name']);
|
|
369
|
+
if (!gridName) {
|
|
370
|
+
const match = entry.entryName.match(/^Grids\/([^/]+)\//);
|
|
371
|
+
if (match)
|
|
372
|
+
gridName = match[1];
|
|
373
|
+
}
|
|
374
|
+
if (gridId && gridName) {
|
|
375
|
+
gridNameToIdMap.set(gridName, gridId);
|
|
376
|
+
gridIdToNameMap.set(gridId, gridName);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
catch (e) {
|
|
380
|
+
// Skip errors in first pass
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
// Second pass: process each grid file in the gridset
|
|
385
|
+
zip.getEntries().forEach((entry) => {
|
|
386
|
+
// Only process files named grid.xml under Grids/ (any subdir)
|
|
387
|
+
if (entry.entryName.startsWith('Grids/') && entry.entryName.endsWith('grid.xml')) {
|
|
388
|
+
let xmlContent;
|
|
389
|
+
try {
|
|
390
|
+
xmlContent = entry.getData().toString('utf8');
|
|
391
|
+
}
|
|
392
|
+
catch (e) {
|
|
393
|
+
// Skip unreadable files
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
let data;
|
|
397
|
+
try {
|
|
398
|
+
data = parser.parse(xmlContent);
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
// Skip malformed XML but log the specific error
|
|
402
|
+
console.warn(`Malformed XML in ${entry.entryName}: ${error.message}`);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
// Grid3 XML: <Grid> root
|
|
406
|
+
const grid = data.Grid || data.grid;
|
|
407
|
+
if (!grid) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
// Defensive: GridGuid and Name required
|
|
411
|
+
const gridId = this.textOf(grid.GridGuid || grid.gridGuid || grid.id);
|
|
412
|
+
let gridName = this.textOf(grid.Name) || this.textOf(grid.name) || this.textOf(grid['@_Name']);
|
|
413
|
+
if (!gridName) {
|
|
414
|
+
// Fallback: get folder name from entry path
|
|
415
|
+
const match = entry.entryName.match(/^Grids\/([^/]+)\//);
|
|
416
|
+
if (match)
|
|
417
|
+
gridName = match[1];
|
|
418
|
+
}
|
|
419
|
+
if (!gridId || !gridName) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const page = new treeStructure_1.AACPage({
|
|
423
|
+
id: String(gridId),
|
|
424
|
+
name: String(gridName),
|
|
425
|
+
grid: [],
|
|
426
|
+
buttons: [],
|
|
427
|
+
parentId: null,
|
|
428
|
+
style: {
|
|
429
|
+
backgroundColor: grid.BackgroundColour || grid.backgroundColour,
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
// Calculate grid dimensions from ColumnDefinitions and RowDefinitions
|
|
433
|
+
const columnDefs = grid.ColumnDefinitions?.ColumnDefinition || [];
|
|
434
|
+
const rowDefs = grid.RowDefinitions?.RowDefinition || [];
|
|
435
|
+
const maxCols = Array.isArray(columnDefs) ? columnDefs.length : columnDefs ? 1 : 5;
|
|
436
|
+
const maxRows = Array.isArray(rowDefs) ? rowDefs.length : rowDefs ? 1 : 4;
|
|
437
|
+
// Process buttons: <Cells><Cell>
|
|
438
|
+
const cells = grid.Cells?.Cell || grid.cells?.cell;
|
|
439
|
+
if (cells) {
|
|
440
|
+
// Cells may be array or single object
|
|
441
|
+
const cellArr = Array.isArray(cells) ? cells : [cells];
|
|
442
|
+
// Create a 2D grid to track button positions
|
|
443
|
+
const gridLayout = [];
|
|
444
|
+
for (let r = 0; r < maxRows; r++) {
|
|
445
|
+
gridLayout[r] = new Array(maxCols).fill(null);
|
|
446
|
+
}
|
|
447
|
+
cellArr.forEach((cell, idx) => {
|
|
448
|
+
if (!cell || !cell.Content)
|
|
449
|
+
return;
|
|
450
|
+
// Extract position information from cell attributes
|
|
451
|
+
// Grid3 uses 1-based coordinates, convert to 0-based for internal use
|
|
452
|
+
const cellX = Math.max(0, parseInt(String(cell['@_X'] || '1'), 10) - 1);
|
|
453
|
+
const cellY = Math.max(0, parseInt(String(cell['@_Y'] || '1'), 10) - 1);
|
|
454
|
+
const colSpan = parseInt(String(cell['@_ColumnSpan'] || '1'), 10);
|
|
455
|
+
const rowSpan = parseInt(String(cell['@_RowSpan'] || '1'), 10);
|
|
456
|
+
// Extract label from CaptionAndImage/Caption
|
|
457
|
+
const content = cell.Content;
|
|
458
|
+
const captionAndImage = content.CaptionAndImage || content.captionAndImage;
|
|
459
|
+
let label = captionAndImage?.Caption || captionAndImage?.caption || '';
|
|
460
|
+
// If no caption, try other sources or create a placeholder
|
|
461
|
+
if (!label) {
|
|
462
|
+
// For cells without captions (like AutoContent cells), create a meaningful label
|
|
463
|
+
if (content.ContentType === 'AutoContent') {
|
|
464
|
+
label = `AutoContent_${idx}`;
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
return; // Skip cells without labels
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const message = label; // Use caption as message
|
|
471
|
+
// Parse all command types from Grid3 and create semantic actions
|
|
472
|
+
let semanticAction;
|
|
473
|
+
let legacyAction = null;
|
|
474
|
+
// infer action type implicitly from commands; no explicit enum needed
|
|
475
|
+
let navigationTarget;
|
|
476
|
+
const commands = content.Commands?.Command || content.commands?.command;
|
|
477
|
+
// Resolve image for this cell using FileMap and coordinate heuristics
|
|
478
|
+
const imageCandidate = captionAndImage?.Image ||
|
|
479
|
+
captionAndImage?.image ||
|
|
480
|
+
captionAndImage?.ImageName ||
|
|
481
|
+
captionAndImage?.imageName ||
|
|
482
|
+
captionAndImage?.Symbol ||
|
|
483
|
+
captionAndImage?.symbol;
|
|
484
|
+
const declaredImageName = imageCandidate ? this.textOf(imageCandidate) : undefined;
|
|
485
|
+
const gridEntryPath = entry.entryName.replace(/\\/g, '/');
|
|
486
|
+
const baseDir = gridEntryPath.replace(/\/grid\.xml$/, '/');
|
|
487
|
+
const dynamicFiles = fileMapIndex.get(gridEntryPath) || [];
|
|
488
|
+
const resolvedImageEntry = (0, resolver_1.resolveGrid3CellImage)(zip, {
|
|
489
|
+
baseDir,
|
|
490
|
+
imageName: declaredImageName,
|
|
491
|
+
x: cellX + 1,
|
|
492
|
+
y: cellY + 1,
|
|
493
|
+
dynamicFiles,
|
|
494
|
+
}) || undefined;
|
|
495
|
+
if (commands) {
|
|
496
|
+
const commandArr = Array.isArray(commands) ? commands : [commands];
|
|
497
|
+
for (const command of commandArr) {
|
|
498
|
+
const commandId = command['@_ID'] || command.ID || command.id;
|
|
499
|
+
const parameters = command.Parameter || command.parameter;
|
|
500
|
+
const paramArr = parameters
|
|
501
|
+
? Array.isArray(parameters)
|
|
502
|
+
? parameters
|
|
503
|
+
: [parameters]
|
|
504
|
+
: [];
|
|
505
|
+
// Helper to extract text from Grid3's structured format <p><s><r>text</r></s></p>
|
|
506
|
+
const extractStructuredText = (param) => {
|
|
507
|
+
// Try to extract from nested p.s structure
|
|
508
|
+
if (param.p) {
|
|
509
|
+
const p = param.p;
|
|
510
|
+
// Handle p.s array or single s element
|
|
511
|
+
const sElements = Array.isArray(p.s) ? p.s : p.s ? [p.s] : [];
|
|
512
|
+
// Extract all r values and concatenate
|
|
513
|
+
const parts = [];
|
|
514
|
+
for (const s of sElements) {
|
|
515
|
+
if (s && s.r !== undefined) {
|
|
516
|
+
parts.push(String(s.r));
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (parts.length > 0) {
|
|
520
|
+
return parts.join('');
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return undefined;
|
|
524
|
+
};
|
|
525
|
+
// Helper to get parameter value
|
|
526
|
+
const getParam = (key) => {
|
|
527
|
+
if (!parameters)
|
|
528
|
+
return undefined;
|
|
529
|
+
for (const param of paramArr) {
|
|
530
|
+
if (param['@_Key'] === key || param.Key === key || param.key === key) {
|
|
531
|
+
// First try simple #text value
|
|
532
|
+
const simpleValue = param['#text'] ?? param.text ?? param.value;
|
|
533
|
+
if (typeof simpleValue === 'string') {
|
|
534
|
+
return simpleValue;
|
|
535
|
+
}
|
|
536
|
+
// Try to extract from structured format (Grid3's <p><s><r> format)
|
|
537
|
+
const structuredValue = extractStructuredText(param);
|
|
538
|
+
if (structuredValue !== undefined) {
|
|
539
|
+
return structuredValue;
|
|
540
|
+
}
|
|
541
|
+
// Fallback to string conversion
|
|
542
|
+
if (typeof param === 'string') {
|
|
543
|
+
return param;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return undefined;
|
|
548
|
+
};
|
|
549
|
+
switch (commandId) {
|
|
550
|
+
case 'Jump.To': {
|
|
551
|
+
const gridTarget = getParam('grid');
|
|
552
|
+
if (gridTarget) {
|
|
553
|
+
// Resolve grid name to grid ID for navigation
|
|
554
|
+
const targetGridId = gridNameToIdMap.get(gridTarget) || gridTarget;
|
|
555
|
+
navigationTarget = targetGridId;
|
|
556
|
+
// navigate action
|
|
557
|
+
semanticAction = {
|
|
558
|
+
category: treeStructure_1.AACSemanticCategory.NAVIGATION,
|
|
559
|
+
intent: treeStructure_1.AACSemanticIntent.NAVIGATE_TO,
|
|
560
|
+
targetId: targetGridId,
|
|
561
|
+
platformData: {
|
|
562
|
+
grid3: {
|
|
563
|
+
commandId,
|
|
564
|
+
parameters: { grid: gridTarget },
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
fallback: {
|
|
568
|
+
type: 'NAVIGATE',
|
|
569
|
+
targetPageId: targetGridId,
|
|
570
|
+
},
|
|
571
|
+
};
|
|
572
|
+
legacyAction = {
|
|
573
|
+
type: 'NAVIGATE',
|
|
574
|
+
targetPageId: targetGridId,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
case 'Jump.Back':
|
|
580
|
+
// action
|
|
581
|
+
semanticAction = {
|
|
582
|
+
category: treeStructure_1.AACSemanticCategory.NAVIGATION,
|
|
583
|
+
intent: treeStructure_1.AACSemanticIntent.GO_BACK,
|
|
584
|
+
platformData: {
|
|
585
|
+
grid3: {
|
|
586
|
+
commandId,
|
|
587
|
+
parameters: {},
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
fallback: {
|
|
591
|
+
type: 'ACTION',
|
|
592
|
+
message: 'Go back',
|
|
593
|
+
},
|
|
594
|
+
};
|
|
595
|
+
legacyAction = {
|
|
596
|
+
type: 'GO_BACK',
|
|
597
|
+
};
|
|
598
|
+
break;
|
|
599
|
+
case 'Jump.Home':
|
|
600
|
+
// action
|
|
601
|
+
semanticAction = {
|
|
602
|
+
category: treeStructure_1.AACSemanticCategory.NAVIGATION,
|
|
603
|
+
intent: treeStructure_1.AACSemanticIntent.GO_HOME,
|
|
604
|
+
platformData: {
|
|
605
|
+
grid3: {
|
|
606
|
+
commandId,
|
|
607
|
+
parameters: {},
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
fallback: {
|
|
611
|
+
type: 'ACTION',
|
|
612
|
+
message: 'Go home',
|
|
613
|
+
},
|
|
614
|
+
};
|
|
615
|
+
legacyAction = {
|
|
616
|
+
type: 'GO_HOME',
|
|
617
|
+
};
|
|
618
|
+
break;
|
|
619
|
+
case 'Action.Speak': {
|
|
620
|
+
// speak
|
|
621
|
+
const speakUnit = getParam('unit');
|
|
622
|
+
const moveCaret = getParam('movecaret');
|
|
623
|
+
semanticAction = {
|
|
624
|
+
category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
|
|
625
|
+
intent: treeStructure_1.AACSemanticIntent.SPEAK_TEXT,
|
|
626
|
+
platformData: {
|
|
627
|
+
grid3: {
|
|
628
|
+
commandId,
|
|
629
|
+
parameters: {
|
|
630
|
+
unit: speakUnit,
|
|
631
|
+
movecaret: moveCaret,
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
},
|
|
635
|
+
fallback: {
|
|
636
|
+
type: 'SPEAK',
|
|
637
|
+
message: 'Speak text',
|
|
638
|
+
},
|
|
639
|
+
};
|
|
640
|
+
legacyAction = {
|
|
641
|
+
type: 'SPEAK',
|
|
642
|
+
unit: speakUnit,
|
|
643
|
+
moveCaret: moveCaret ? parseInt(String(moveCaret)) : undefined,
|
|
644
|
+
};
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
case 'Action.InsertText': {
|
|
648
|
+
// speak
|
|
649
|
+
const insertText = getParam('text');
|
|
650
|
+
semanticAction = {
|
|
651
|
+
category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
|
|
652
|
+
intent: treeStructure_1.AACSemanticIntent.INSERT_TEXT,
|
|
653
|
+
text: insertText,
|
|
654
|
+
platformData: {
|
|
655
|
+
grid3: {
|
|
656
|
+
commandId,
|
|
657
|
+
parameters: { text: insertText },
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
fallback: {
|
|
661
|
+
type: 'SPEAK',
|
|
662
|
+
message: insertText,
|
|
663
|
+
},
|
|
664
|
+
};
|
|
665
|
+
legacyAction = {
|
|
666
|
+
type: 'INSERT_TEXT',
|
|
667
|
+
text: insertText,
|
|
668
|
+
};
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
case 'Action.DeleteWord':
|
|
672
|
+
// action
|
|
673
|
+
semanticAction = {
|
|
674
|
+
category: treeStructure_1.AACSemanticCategory.TEXT_EDITING,
|
|
675
|
+
intent: treeStructure_1.AACSemanticIntent.DELETE_WORD,
|
|
676
|
+
platformData: {
|
|
677
|
+
grid3: {
|
|
678
|
+
commandId,
|
|
679
|
+
parameters: {},
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
fallback: {
|
|
683
|
+
type: 'ACTION',
|
|
684
|
+
message: 'Delete word',
|
|
685
|
+
},
|
|
686
|
+
};
|
|
687
|
+
legacyAction = {
|
|
688
|
+
type: 'DELETE_WORD',
|
|
689
|
+
};
|
|
690
|
+
break;
|
|
691
|
+
case 'Action.DeleteLetter':
|
|
692
|
+
// action
|
|
693
|
+
semanticAction = {
|
|
694
|
+
category: treeStructure_1.AACSemanticCategory.TEXT_EDITING,
|
|
695
|
+
intent: treeStructure_1.AACSemanticIntent.DELETE_CHARACTER,
|
|
696
|
+
platformData: {
|
|
697
|
+
grid3: {
|
|
698
|
+
commandId,
|
|
699
|
+
parameters: {},
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
fallback: {
|
|
703
|
+
type: 'ACTION',
|
|
704
|
+
message: 'Delete character',
|
|
705
|
+
},
|
|
706
|
+
};
|
|
707
|
+
legacyAction = {
|
|
708
|
+
type: 'DELETE_CHARACTER',
|
|
709
|
+
};
|
|
710
|
+
break;
|
|
711
|
+
case 'Action.Clear':
|
|
712
|
+
// action
|
|
713
|
+
semanticAction = {
|
|
714
|
+
category: treeStructure_1.AACSemanticCategory.TEXT_EDITING,
|
|
715
|
+
intent: treeStructure_1.AACSemanticIntent.CLEAR_TEXT,
|
|
716
|
+
platformData: {
|
|
717
|
+
grid3: {
|
|
718
|
+
commandId,
|
|
719
|
+
parameters: {},
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
fallback: {
|
|
723
|
+
type: 'ACTION',
|
|
724
|
+
message: 'Clear text',
|
|
725
|
+
},
|
|
726
|
+
};
|
|
727
|
+
legacyAction = {
|
|
728
|
+
type: 'CLEAR_TEXT',
|
|
729
|
+
};
|
|
730
|
+
break;
|
|
731
|
+
case 'Action.Letter': {
|
|
732
|
+
// action
|
|
733
|
+
const letter = getParam('letter');
|
|
734
|
+
semanticAction = {
|
|
735
|
+
category: treeStructure_1.AACSemanticCategory.TEXT_EDITING,
|
|
736
|
+
intent: treeStructure_1.AACSemanticIntent.INSERT_TEXT,
|
|
737
|
+
text: letter,
|
|
738
|
+
platformData: {
|
|
739
|
+
grid3: {
|
|
740
|
+
commandId,
|
|
741
|
+
parameters: { letter },
|
|
742
|
+
},
|
|
743
|
+
},
|
|
744
|
+
fallback: {
|
|
745
|
+
type: 'ACTION',
|
|
746
|
+
message: letter,
|
|
747
|
+
},
|
|
748
|
+
};
|
|
749
|
+
legacyAction = {
|
|
750
|
+
type: 'INSERT_LETTER',
|
|
751
|
+
letter,
|
|
752
|
+
};
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
case 'Settings.RestAll':
|
|
756
|
+
// action
|
|
757
|
+
semanticAction = {
|
|
758
|
+
category: treeStructure_1.AACSemanticCategory.CUSTOM,
|
|
759
|
+
intent: treeStructure_1.AACSemanticIntent.PLATFORM_SPECIFIC,
|
|
760
|
+
platformData: {
|
|
761
|
+
grid3: {
|
|
762
|
+
commandId,
|
|
763
|
+
parameters: {
|
|
764
|
+
indicatorenabled: getParam('indicatorenabled'),
|
|
765
|
+
action: getParam('action'),
|
|
766
|
+
},
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
fallback: {
|
|
770
|
+
type: 'ACTION',
|
|
771
|
+
message: 'Settings action',
|
|
772
|
+
},
|
|
773
|
+
};
|
|
774
|
+
legacyAction = {
|
|
775
|
+
type: 'SETTINGS',
|
|
776
|
+
indicatorEnabled: getParam('indicatorenabled') === '1',
|
|
777
|
+
settingsAction: getParam('action'),
|
|
778
|
+
};
|
|
779
|
+
break;
|
|
780
|
+
case 'AutoContent.Activate':
|
|
781
|
+
// action
|
|
782
|
+
semanticAction = {
|
|
783
|
+
category: treeStructure_1.AACSemanticCategory.CUSTOM,
|
|
784
|
+
intent: treeStructure_1.AACSemanticIntent.PLATFORM_SPECIFIC,
|
|
785
|
+
platformData: {
|
|
786
|
+
grid3: {
|
|
787
|
+
commandId,
|
|
788
|
+
parameters: {
|
|
789
|
+
autocontenttype: getParam('autocontenttype'),
|
|
790
|
+
},
|
|
791
|
+
},
|
|
792
|
+
},
|
|
793
|
+
fallback: {
|
|
794
|
+
type: 'ACTION',
|
|
795
|
+
message: 'Auto content',
|
|
796
|
+
},
|
|
797
|
+
};
|
|
798
|
+
legacyAction = {
|
|
799
|
+
type: 'AUTO_CONTENT',
|
|
800
|
+
autoContentType: getParam('autocontenttype'),
|
|
801
|
+
};
|
|
802
|
+
break;
|
|
803
|
+
default:
|
|
804
|
+
// Unknown command - preserve as generic action
|
|
805
|
+
if (commandId) {
|
|
806
|
+
// action
|
|
807
|
+
const allParams = Object.fromEntries(paramArr.map((p) => [p.Key || p.key, p['#text']]));
|
|
808
|
+
semanticAction = {
|
|
809
|
+
category: treeStructure_1.AACSemanticCategory.CUSTOM,
|
|
810
|
+
intent: treeStructure_1.AACSemanticIntent.PLATFORM_SPECIFIC,
|
|
811
|
+
platformData: {
|
|
812
|
+
grid3: {
|
|
813
|
+
commandId,
|
|
814
|
+
parameters: allParams,
|
|
815
|
+
},
|
|
816
|
+
},
|
|
817
|
+
fallback: {
|
|
818
|
+
type: 'ACTION',
|
|
819
|
+
message: 'Unknown command',
|
|
820
|
+
},
|
|
821
|
+
};
|
|
822
|
+
legacyAction = {
|
|
823
|
+
type: 'SPEAK',
|
|
824
|
+
parameters: { commandId, ...allParams },
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
// Use first recognized command
|
|
830
|
+
if (semanticAction || legacyAction)
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
// Create default semantic action if none was created from commands
|
|
835
|
+
if (!semanticAction) {
|
|
836
|
+
semanticAction = {
|
|
837
|
+
category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
|
|
838
|
+
intent: treeStructure_1.AACSemanticIntent.SPEAK_TEXT,
|
|
839
|
+
text: String(message),
|
|
840
|
+
fallback: {
|
|
841
|
+
type: 'SPEAK',
|
|
842
|
+
message: String(message),
|
|
843
|
+
},
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
// Get style information from cell attributes and Content.Style
|
|
847
|
+
let cellStyleId = cell['@_StyleID'] || cell['@_styleid'];
|
|
848
|
+
// Grid3 format: check Content.Style.BasedOnStyle
|
|
849
|
+
if (!cellStyleId && content.Style?.BasedOnStyle) {
|
|
850
|
+
cellStyleId = content.Style.BasedOnStyle;
|
|
851
|
+
}
|
|
852
|
+
const cellStyle = this.getStyleById(styles, cellStyleId ? String(cellStyleId) : undefined);
|
|
853
|
+
// Also check for inline style overrides
|
|
854
|
+
const inlineStyle = {};
|
|
855
|
+
if (cell['@_BackColour'])
|
|
856
|
+
inlineStyle.backgroundColor = cell['@_BackColour'];
|
|
857
|
+
if (cell['@_FontColour'])
|
|
858
|
+
inlineStyle.fontColor = cell['@_FontColour'];
|
|
859
|
+
if (cell['@_BorderColour'])
|
|
860
|
+
inlineStyle.borderColor = cell['@_BorderColour'];
|
|
861
|
+
// Grid3 inline styles from Content.Style
|
|
862
|
+
if (content.Style) {
|
|
863
|
+
if (content.Style.BackColour)
|
|
864
|
+
inlineStyle.backgroundColor = content.Style.BackColour;
|
|
865
|
+
if (content.Style.FontColour)
|
|
866
|
+
inlineStyle.fontColor = content.Style.FontColour;
|
|
867
|
+
if (content.Style.BorderColour)
|
|
868
|
+
inlineStyle.borderColor = content.Style.BorderColour;
|
|
869
|
+
if (content.Style.FontName)
|
|
870
|
+
inlineStyle.fontFamily = content.Style.FontName;
|
|
871
|
+
if (content.Style.FontSize)
|
|
872
|
+
inlineStyle.fontSize = parseInt(String(content.Style.FontSize));
|
|
873
|
+
}
|
|
874
|
+
const button = new treeStructure_1.AACButton({
|
|
875
|
+
id: `${gridId}_btn_${idx}`,
|
|
876
|
+
label: String(label),
|
|
877
|
+
message: String(message),
|
|
878
|
+
targetPageId: navigationTarget ? String(navigationTarget) : undefined,
|
|
879
|
+
semanticAction: semanticAction,
|
|
880
|
+
image: declaredImageName,
|
|
881
|
+
resolvedImageEntry: resolvedImageEntry,
|
|
882
|
+
x: cellX,
|
|
883
|
+
y: cellY,
|
|
884
|
+
columnSpan: colSpan,
|
|
885
|
+
rowSpan: rowSpan,
|
|
886
|
+
style: {
|
|
887
|
+
...cellStyle,
|
|
888
|
+
...inlineStyle, // Inline styles override referenced styles
|
|
889
|
+
},
|
|
890
|
+
});
|
|
891
|
+
// Add button to page
|
|
892
|
+
page.addButton(button);
|
|
893
|
+
// Place button in grid layout (handle colspan/rowspan)
|
|
894
|
+
for (let r = cellY; r < cellY + rowSpan && r < maxRows; r++) {
|
|
895
|
+
for (let c = cellX; c < cellX + colSpan && c < maxCols; c++) {
|
|
896
|
+
if (gridLayout[r] && gridLayout[r][c] === null) {
|
|
897
|
+
gridLayout[r][c] = button;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
// Set the page's grid layout
|
|
903
|
+
page.grid = gridLayout;
|
|
904
|
+
}
|
|
905
|
+
tree.addPage(page);
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
// After all pages are loaded, set parentId for navigation targets
|
|
909
|
+
for (const pageId in tree.pages) {
|
|
910
|
+
const page = tree.pages[pageId];
|
|
911
|
+
page.buttons.forEach((btn) => {
|
|
912
|
+
if (btn.semanticAction?.intent === treeStructure_1.AACSemanticIntent.NAVIGATE_TO && btn.targetPageId) {
|
|
913
|
+
const targetPage = tree.getPage(btn.targetPageId);
|
|
914
|
+
if (targetPage) {
|
|
915
|
+
targetPage.parentId = page.id;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
// Read settings.xml to get the StartGrid (home page)
|
|
921
|
+
try {
|
|
922
|
+
const settingsEntry = zip.getEntries().find((e) => e.entryName.endsWith('settings.xml'));
|
|
923
|
+
if (settingsEntry) {
|
|
924
|
+
const settingsXml = settingsEntry.getData().toString('utf8');
|
|
925
|
+
const settingsData = parser.parse(settingsXml);
|
|
926
|
+
const startGridName = settingsData?.GridSetSettings?.StartGrid ||
|
|
927
|
+
settingsData?.gridSetSettings?.startGrid ||
|
|
928
|
+
settingsData?.GridsetSettings?.StartGrid;
|
|
929
|
+
if (startGridName && typeof startGridName === 'string') {
|
|
930
|
+
// Resolve the grid name to grid ID
|
|
931
|
+
const homeGridId = gridNameToIdMap.get(startGridName);
|
|
932
|
+
if (homeGridId) {
|
|
933
|
+
tree.rootId = homeGridId;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
catch (e) {
|
|
939
|
+
// If settings.xml parsing fails, tree.rootId will default to first page
|
|
940
|
+
}
|
|
941
|
+
return tree;
|
|
942
|
+
}
|
|
943
|
+
processTexts(filePathOrBuffer, translations, outputPath) {
|
|
944
|
+
// Load the tree, apply translations, and save to new file
|
|
945
|
+
const buffer = Buffer.isBuffer(filePathOrBuffer)
|
|
946
|
+
? filePathOrBuffer
|
|
947
|
+
: fs_1.default.readFileSync(filePathOrBuffer);
|
|
948
|
+
const tree = this.loadIntoTree(buffer);
|
|
949
|
+
// Apply translations to all text content
|
|
950
|
+
Object.values(tree.pages).forEach((page) => {
|
|
951
|
+
// Translate page names
|
|
952
|
+
if (page.name && translations.has(page.name)) {
|
|
953
|
+
const tPage = translations.get(page.name);
|
|
954
|
+
if (tPage)
|
|
955
|
+
page.name = tPage;
|
|
956
|
+
}
|
|
957
|
+
// Translate button labels and messages
|
|
958
|
+
page.buttons.forEach((button) => {
|
|
959
|
+
if (button.label && translations.has(button.label)) {
|
|
960
|
+
const tLabel = translations.get(button.label);
|
|
961
|
+
if (tLabel)
|
|
962
|
+
button.label = tLabel;
|
|
963
|
+
}
|
|
964
|
+
if (button.message && translations.has(button.message)) {
|
|
965
|
+
const tMsg = translations.get(button.message);
|
|
966
|
+
if (tMsg)
|
|
967
|
+
button.message = tMsg;
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
});
|
|
971
|
+
// Save the translated tree and return its content
|
|
972
|
+
this.saveFromTree(tree, outputPath);
|
|
973
|
+
return fs_1.default.readFileSync(outputPath);
|
|
974
|
+
}
|
|
975
|
+
saveFromTree(tree, outputPath) {
|
|
976
|
+
const zip = new adm_zip_1.default();
|
|
977
|
+
if (Object.keys(tree.pages).length === 0) {
|
|
978
|
+
// Create empty zip for empty tree
|
|
979
|
+
zip.writeZip(outputPath);
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
// Collect all unique styles from pages and buttons
|
|
983
|
+
const uniqueStyles = new Map();
|
|
984
|
+
let styleIdCounter = 1;
|
|
985
|
+
// Track images that need to be written to the ZIP
|
|
986
|
+
// Maps button ID to image data for buttons with images
|
|
987
|
+
const buttonImages = new Map();
|
|
988
|
+
// Helper function to add style and return its ID
|
|
989
|
+
const addStyle = (style) => {
|
|
990
|
+
if (!style || typeof style !== 'object')
|
|
991
|
+
return '';
|
|
992
|
+
const obj = style;
|
|
993
|
+
if (Object.keys(obj).length === 0)
|
|
994
|
+
return '';
|
|
995
|
+
const styleKey = JSON.stringify(obj);
|
|
996
|
+
const existing = uniqueStyles.get(styleKey);
|
|
997
|
+
if (existing)
|
|
998
|
+
return existing.id;
|
|
999
|
+
const styleId = `Style${styleIdCounter++}`;
|
|
1000
|
+
uniqueStyles.set(styleKey, { id: styleId, style: obj });
|
|
1001
|
+
return styleId;
|
|
1002
|
+
};
|
|
1003
|
+
// Collect styles from all pages and buttons
|
|
1004
|
+
Object.values(tree.pages).forEach((page) => {
|
|
1005
|
+
if (page.style)
|
|
1006
|
+
addStyle(page.style);
|
|
1007
|
+
page.buttons.forEach((button) => {
|
|
1008
|
+
if (button.style)
|
|
1009
|
+
addStyle(button.style);
|
|
1010
|
+
});
|
|
1011
|
+
});
|
|
1012
|
+
// Get the home/start grid from tree.rootId, fallback to first page
|
|
1013
|
+
const pages = Object.values(tree.pages);
|
|
1014
|
+
let startGrid = '';
|
|
1015
|
+
if (tree.rootId) {
|
|
1016
|
+
const homePage = tree.getPage(tree.rootId);
|
|
1017
|
+
if (homePage) {
|
|
1018
|
+
startGrid = homePage.name || homePage.id;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
// Fallback to first page if no rootId or page not found
|
|
1022
|
+
if (!startGrid && pages.length > 0) {
|
|
1023
|
+
startGrid = pages[0].name || pages[0].id;
|
|
1024
|
+
}
|
|
1025
|
+
// Create Settings0/settings.xml with proper Grid3 structure
|
|
1026
|
+
const settingsData = {
|
|
1027
|
+
'?xml': { '@_version': '1.0', '@_encoding': 'UTF-8' },
|
|
1028
|
+
GridSetSettings: {
|
|
1029
|
+
'@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
|
1030
|
+
StartGrid: startGrid,
|
|
1031
|
+
// Add other common Grid3 settings
|
|
1032
|
+
ScanEnabled: 'false',
|
|
1033
|
+
ScanTimeoutMs: '2000',
|
|
1034
|
+
HoverEnabled: 'false',
|
|
1035
|
+
HoverTimeoutMs: '1000',
|
|
1036
|
+
MouseclickEnabled: 'true',
|
|
1037
|
+
Language: 'en-US',
|
|
1038
|
+
},
|
|
1039
|
+
};
|
|
1040
|
+
const settingsBuilder = new fast_xml_parser_1.XMLBuilder({
|
|
1041
|
+
ignoreAttributes: false,
|
|
1042
|
+
format: true,
|
|
1043
|
+
indentBy: ' ',
|
|
1044
|
+
suppressEmptyNode: true,
|
|
1045
|
+
});
|
|
1046
|
+
const settingsXmlContent = settingsBuilder.build(settingsData);
|
|
1047
|
+
zip.addFile('Settings0/settings.xml', Buffer.from(settingsXmlContent, 'utf8'));
|
|
1048
|
+
// Create Settings0/Styles/style.xml if there are styles
|
|
1049
|
+
if (uniqueStyles.size > 0) {
|
|
1050
|
+
const stylesArray = Array.from(uniqueStyles.values()).map(({ id, style }) => {
|
|
1051
|
+
const styleObj = {
|
|
1052
|
+
'@_Key': id,
|
|
1053
|
+
// When TileColour is present, BackColour is the surround (outer area)
|
|
1054
|
+
// For "None" surround, just use BackColour for the fill (no TileColour)
|
|
1055
|
+
BackColour: this.ensureAlphaChannel(style.backgroundColor),
|
|
1056
|
+
BorderColour: this.ensureAlphaChannel(style.borderColor) || '#000000FF',
|
|
1057
|
+
FontColour: this.ensureAlphaChannel(style.fontColor) || '#000000FF',
|
|
1058
|
+
FontName: style.fontFamily || 'Arial',
|
|
1059
|
+
FontSize: style.fontSize?.toString() || '16',
|
|
1060
|
+
};
|
|
1061
|
+
// Don't add TileColour - just use BackColour as the fill color
|
|
1062
|
+
return styleObj;
|
|
1063
|
+
});
|
|
1064
|
+
const styleData = {
|
|
1065
|
+
'?xml': { '@_version': '1.0', '@_encoding': 'UTF-8' },
|
|
1066
|
+
StyleData: {
|
|
1067
|
+
'@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
|
1068
|
+
Styles: {
|
|
1069
|
+
Style: stylesArray,
|
|
1070
|
+
},
|
|
1071
|
+
},
|
|
1072
|
+
};
|
|
1073
|
+
const styleBuilder = new fast_xml_parser_1.XMLBuilder({
|
|
1074
|
+
ignoreAttributes: false,
|
|
1075
|
+
format: true,
|
|
1076
|
+
indentBy: ' ',
|
|
1077
|
+
});
|
|
1078
|
+
const styleXmlContent = styleBuilder.build(styleData);
|
|
1079
|
+
zip.addFile('Settings0/Styles/styles.xml', Buffer.from(styleXmlContent, 'utf8'));
|
|
1080
|
+
}
|
|
1081
|
+
// Collect grid file paths for FileMap.xml
|
|
1082
|
+
const gridFilePaths = [];
|
|
1083
|
+
// Create a grid for each page
|
|
1084
|
+
Object.values(tree.pages).forEach((page, index) => {
|
|
1085
|
+
const gridData = {
|
|
1086
|
+
Grid: {
|
|
1087
|
+
'@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
|
1088
|
+
GridGuid: page.id,
|
|
1089
|
+
// Calculate grid dimensions based on actual layout
|
|
1090
|
+
ColumnDefinitions: this.calculateColumnDefinitions(page),
|
|
1091
|
+
RowDefinitions: this.calculateRowDefinitions(page),
|
|
1092
|
+
AutoContentCommands: '',
|
|
1093
|
+
Cells: page.buttons.length > 0
|
|
1094
|
+
? {
|
|
1095
|
+
Cell: [
|
|
1096
|
+
// Add workspace/message bar cell at the top of ALL pages
|
|
1097
|
+
// Grid3 uses 0-based coordinates; omit X and Y to use defaults (0, 0)
|
|
1098
|
+
{
|
|
1099
|
+
'@_ColumnSpan': 4,
|
|
1100
|
+
Content: {
|
|
1101
|
+
ContentType: 'Workspace',
|
|
1102
|
+
ContentSubType: 'Chat',
|
|
1103
|
+
Style: {
|
|
1104
|
+
BasedOnStyle: 'Workspace',
|
|
1105
|
+
},
|
|
1106
|
+
},
|
|
1107
|
+
},
|
|
1108
|
+
// Regular button cells
|
|
1109
|
+
...this.filterPageButtons(page.buttons).map((button, btnIndex) => {
|
|
1110
|
+
const buttonStyleId = button.style ? addStyle(button.style) : '';
|
|
1111
|
+
// Find button position in grid layout
|
|
1112
|
+
const position = this.findButtonPosition(page, button, btnIndex);
|
|
1113
|
+
// Shift all buttons down by 1 row to make room for workspace
|
|
1114
|
+
const yOffset = 1;
|
|
1115
|
+
// Build CaptionAndImage object
|
|
1116
|
+
const captionAndImage = {
|
|
1117
|
+
Caption: button.label || '',
|
|
1118
|
+
};
|
|
1119
|
+
// Add image reference if button has an image
|
|
1120
|
+
// Grid3 uses coordinate-based naming: {x}-{y}-0-text-0.{ext}
|
|
1121
|
+
if (button.image) {
|
|
1122
|
+
// Try to determine file extension from image name or default to PNG
|
|
1123
|
+
let imageExt = 'png';
|
|
1124
|
+
if (button.image.match(/\.(png|jpg|jpeg|gif|svg)$/i)) {
|
|
1125
|
+
imageExt = button.image.match(/\.(png|jpg|jpeg|gif|svg)$/i)[1].toLowerCase();
|
|
1126
|
+
}
|
|
1127
|
+
// Grid3 dynamically constructs image filenames by prepending cell coordinates
|
|
1128
|
+
// The XML should only contain the suffix: -0-text-0.{ext}
|
|
1129
|
+
// Grid3 automatically adds the X-Y prefix based on the Cell's position
|
|
1130
|
+
captionAndImage.Image = `-0-text-0.${imageExt}`;
|
|
1131
|
+
// Extract image data from button parameters if available
|
|
1132
|
+
// (AstericsGridProcessor stores it there during loadIntoTree)
|
|
1133
|
+
let imageData = Buffer.alloc(0);
|
|
1134
|
+
if (button.parameters && button.parameters.imageData && Buffer.isBuffer(button.parameters.imageData)) {
|
|
1135
|
+
imageData = button.parameters.imageData;
|
|
1136
|
+
}
|
|
1137
|
+
// Store image data for later writing to ZIP
|
|
1138
|
+
buttonImages.set(button.id, {
|
|
1139
|
+
imageData: imageData,
|
|
1140
|
+
ext: imageExt,
|
|
1141
|
+
pageName: page.name || page.id,
|
|
1142
|
+
x: position.x,
|
|
1143
|
+
y: position.y + yOffset,
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
const cellData = {
|
|
1147
|
+
'@_X': position.x, // Grid3 uses 0-based X coordinates (defaults to 0 when omitted)
|
|
1148
|
+
'@_Y': position.y + yOffset, // Grid3 uses 0-based Y coordinates with workspace offset
|
|
1149
|
+
'@_ColumnSpan': position.columnSpan,
|
|
1150
|
+
'@_RowSpan': position.rowSpan,
|
|
1151
|
+
Content: {
|
|
1152
|
+
Commands: this.generateCommandsFromSemanticAction(button, tree),
|
|
1153
|
+
CaptionAndImage: captionAndImage,
|
|
1154
|
+
},
|
|
1155
|
+
};
|
|
1156
|
+
// Add style reference and inline color overrides if available
|
|
1157
|
+
// Some Grid3 versions need inline colors in addition to style references
|
|
1158
|
+
if (buttonStyleId || button.style) {
|
|
1159
|
+
const styleObj = {};
|
|
1160
|
+
// Add style reference if we have one
|
|
1161
|
+
if (buttonStyleId) {
|
|
1162
|
+
styleObj.BasedOnStyle = buttonStyleId;
|
|
1163
|
+
}
|
|
1164
|
+
// Add inline color overrides for better Grid3 compatibility
|
|
1165
|
+
if (button.style?.backgroundColor) {
|
|
1166
|
+
// Use BackColour for fill (no TileColour means no surround, just the fill)
|
|
1167
|
+
styleObj.BackColour = this.ensureAlphaChannel(button.style.backgroundColor);
|
|
1168
|
+
}
|
|
1169
|
+
if (button.style?.borderColor) {
|
|
1170
|
+
styleObj.BorderColour = this.ensureAlphaChannel(button.style.borderColor);
|
|
1171
|
+
}
|
|
1172
|
+
if (button.style?.fontColor) {
|
|
1173
|
+
styleObj.FontColour = this.ensureAlphaChannel(button.style.fontColor);
|
|
1174
|
+
}
|
|
1175
|
+
if (button.style?.fontFamily) {
|
|
1176
|
+
styleObj.FontName = button.style.fontFamily;
|
|
1177
|
+
}
|
|
1178
|
+
if (button.style?.fontSize) {
|
|
1179
|
+
styleObj.FontSize = button.style.fontSize;
|
|
1180
|
+
}
|
|
1181
|
+
cellData.Content.Style = styleObj;
|
|
1182
|
+
}
|
|
1183
|
+
return cellData;
|
|
1184
|
+
}),
|
|
1185
|
+
]
|
|
1186
|
+
}
|
|
1187
|
+
: { Cell: [] },
|
|
1188
|
+
},
|
|
1189
|
+
};
|
|
1190
|
+
// Convert to XML
|
|
1191
|
+
const builder = new fast_xml_parser_1.XMLBuilder({
|
|
1192
|
+
ignoreAttributes: false,
|
|
1193
|
+
format: true,
|
|
1194
|
+
indentBy: ' ',
|
|
1195
|
+
suppressEmptyNode: true,
|
|
1196
|
+
cdataPropName: '__cdata',
|
|
1197
|
+
});
|
|
1198
|
+
const xmlContent = builder.build(gridData);
|
|
1199
|
+
// Add to zip in Grids folder with proper Grid3 naming
|
|
1200
|
+
const gridPath = `Grids\\${page.name || page.id}\\grid.xml`;
|
|
1201
|
+
gridFilePaths.push(gridPath);
|
|
1202
|
+
zip.addFile(gridPath, Buffer.from(xmlContent, 'utf8'));
|
|
1203
|
+
});
|
|
1204
|
+
// Write image files to ZIP
|
|
1205
|
+
buttonImages.forEach((imgData, buttonId) => {
|
|
1206
|
+
if (imgData.imageData && imgData.imageData.length > 0) {
|
|
1207
|
+
// Create image path in the grid's directory
|
|
1208
|
+
const imagePath = `Grids\\${imgData.pageName}\\${imgData.x}-${imgData.y}-0-text-0.${imgData.ext}`;
|
|
1209
|
+
zip.addFile(imagePath, imgData.imageData);
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1212
|
+
// Create FileMap.xml to map all grid files with their dynamic image files
|
|
1213
|
+
const fileMapData = {
|
|
1214
|
+
'?xml': { '@_version': '1.0', '@_encoding': 'UTF-8' },
|
|
1215
|
+
FileMap: {
|
|
1216
|
+
'@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
|
1217
|
+
Entries: {
|
|
1218
|
+
Entry: gridFilePaths.map((gridPath) => {
|
|
1219
|
+
// Find all image files for this grid
|
|
1220
|
+
const gridName = gridPath.match(/Grids\\([^\\]+)\\grid\.xml$/)?.[1] || '';
|
|
1221
|
+
const imageFiles = [];
|
|
1222
|
+
// Collect image filenames for buttons on this page
|
|
1223
|
+
// IMPORTANT: FileMap.xml requires full paths like "Grids\PageName\1-5-0-text-0.png"
|
|
1224
|
+
buttonImages.forEach((imgData, buttonId) => {
|
|
1225
|
+
if (imgData.pageName === gridName && imgData.imageData.length > 0) {
|
|
1226
|
+
const imagePath = `Grids\\${gridName}\\${imgData.x}-${imgData.y}-0-text-0.${imgData.ext}`;
|
|
1227
|
+
imageFiles.push(imagePath);
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
return {
|
|
1231
|
+
'@_StaticFile': gridPath,
|
|
1232
|
+
DynamicFiles: imageFiles.length > 0 ? {
|
|
1233
|
+
File: imageFiles
|
|
1234
|
+
} : {},
|
|
1235
|
+
};
|
|
1236
|
+
}),
|
|
1237
|
+
},
|
|
1238
|
+
},
|
|
1239
|
+
};
|
|
1240
|
+
const fileMapBuilder = new fast_xml_parser_1.XMLBuilder({
|
|
1241
|
+
ignoreAttributes: false,
|
|
1242
|
+
format: true,
|
|
1243
|
+
indentBy: ' ',
|
|
1244
|
+
});
|
|
1245
|
+
const fileMapXmlContent = fileMapBuilder.build(fileMapData);
|
|
1246
|
+
zip.addFile('FileMap.xml', Buffer.from(fileMapXmlContent, 'utf8'));
|
|
1247
|
+
// Write the zip file
|
|
1248
|
+
zip.writeZip(outputPath);
|
|
1249
|
+
}
|
|
1250
|
+
// Helper method to calculate column definitions based on page layout
|
|
1251
|
+
calculateColumnDefinitions(page) {
|
|
1252
|
+
let maxCols = 4; // Default minimum
|
|
1253
|
+
if (page.grid && page.grid.length > 0) {
|
|
1254
|
+
maxCols = Math.max(maxCols, page.grid[0]?.length || 0);
|
|
1255
|
+
}
|
|
1256
|
+
else {
|
|
1257
|
+
// Fallback: estimate from button count
|
|
1258
|
+
maxCols = Math.max(4, Math.ceil(Math.sqrt(page.buttons.length)));
|
|
1259
|
+
}
|
|
1260
|
+
return {
|
|
1261
|
+
ColumnDefinition: Array(maxCols).fill({}),
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
// Helper method to calculate row definitions based on page layout
|
|
1265
|
+
calculateRowDefinitions(page) {
|
|
1266
|
+
let maxRows = 4; // Default minimum
|
|
1267
|
+
if (page.grid && page.grid.length > 0) {
|
|
1268
|
+
maxRows = Math.max(maxRows, page.grid.length);
|
|
1269
|
+
}
|
|
1270
|
+
else {
|
|
1271
|
+
// Fallback: estimate from button count
|
|
1272
|
+
const estimatedCols = Math.ceil(Math.sqrt(page.buttons.length));
|
|
1273
|
+
maxRows = Math.max(4, Math.ceil(page.buttons.length / estimatedCols));
|
|
1274
|
+
}
|
|
1275
|
+
return {
|
|
1276
|
+
RowDefinition: Array(maxRows).fill({}),
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
// Helper method to find button position with span information
|
|
1280
|
+
findButtonPosition(page, button, fallbackIndex) {
|
|
1281
|
+
if (page.grid && page.grid.length > 0) {
|
|
1282
|
+
// Search for button in grid layout and calculate span
|
|
1283
|
+
for (let y = 0; y < page.grid.length; y++) {
|
|
1284
|
+
for (let x = 0; x < page.grid[y].length; x++) {
|
|
1285
|
+
const current = page.grid[y][x];
|
|
1286
|
+
if (current && current.id === button.id) {
|
|
1287
|
+
// Calculate span by checking how far the same button extends
|
|
1288
|
+
let columnSpan = 1;
|
|
1289
|
+
let rowSpan = 1;
|
|
1290
|
+
// Check column span (rightward)
|
|
1291
|
+
while (x + columnSpan < page.grid[y].length) {
|
|
1292
|
+
const right = page.grid[y][x + columnSpan];
|
|
1293
|
+
if (right && right.id === button.id) {
|
|
1294
|
+
columnSpan++;
|
|
1295
|
+
}
|
|
1296
|
+
else {
|
|
1297
|
+
break;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
// Check row span (downward)
|
|
1301
|
+
while (y + rowSpan < page.grid.length) {
|
|
1302
|
+
const below = page.grid[y + rowSpan][x];
|
|
1303
|
+
if (below && below.id === button.id) {
|
|
1304
|
+
rowSpan++;
|
|
1305
|
+
}
|
|
1306
|
+
else {
|
|
1307
|
+
break;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
return { x, y, columnSpan, rowSpan };
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
// Fallback positioning
|
|
1316
|
+
const gridCols = page.grid?.[0]?.length || Math.ceil(Math.sqrt(page.buttons.length));
|
|
1317
|
+
return {
|
|
1318
|
+
x: fallbackIndex % gridCols,
|
|
1319
|
+
y: Math.floor(fallbackIndex / gridCols),
|
|
1320
|
+
columnSpan: 1,
|
|
1321
|
+
rowSpan: 1,
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Extract strings with metadata for aac-tools-platform compatibility
|
|
1326
|
+
* Uses the generic implementation from BaseProcessor
|
|
1327
|
+
*/
|
|
1328
|
+
extractStringsWithMetadata(filePath) {
|
|
1329
|
+
return this.extractStringsWithMetadataGeneric(filePath);
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Generate translated download for aac-tools-platform compatibility
|
|
1333
|
+
* Uses the generic implementation from BaseProcessor
|
|
1334
|
+
*/
|
|
1335
|
+
generateTranslatedDownload(filePath, translatedStrings, sourceStrings) {
|
|
1336
|
+
return this.generateTranslatedDownloadGeneric(filePath, translatedStrings, sourceStrings);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
exports.GridsetProcessor = GridsetProcessor;
|