@willwade/aac-processors 0.0.22 → 0.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -27
- package/dist/processors/applePanelsProcessor.js +166 -123
- package/dist/processors/astericsGridProcessor.js +121 -105
- package/dist/processors/dotProcessor.js +83 -65
- package/dist/processors/gridsetProcessor.js +43 -77
- package/dist/processors/opmlProcessor.js +82 -44
- package/dist/validation/applePanelsValidator.d.ts +10 -0
- package/dist/validation/applePanelsValidator.js +124 -0
- package/dist/validation/astericsValidator.d.ts +16 -0
- package/dist/validation/astericsValidator.js +115 -0
- package/dist/validation/dotValidator.d.ts +10 -0
- package/dist/validation/dotValidator.js +113 -0
- package/dist/validation/excelValidator.d.ts +10 -0
- package/dist/validation/excelValidator.js +89 -0
- package/dist/validation/index.d.ts +14 -1
- package/dist/validation/index.js +104 -1
- package/dist/validation/obfsetValidator.d.ts +10 -0
- package/dist/validation/obfsetValidator.js +103 -0
- package/dist/validation/opmlValidator.d.ts +10 -0
- package/dist/validation/opmlValidator.js +107 -0
- package/dist/validation/validationTypes.d.ts +22 -0
- package/dist/validation/validationTypes.js +38 -1
- package/dist/validation.d.ts +8 -2
- package/dist/validation.js +16 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -267,30 +267,18 @@ console.log(`Average Effort: ${result.total_words}`);
|
|
|
267
267
|
Validate AAC files against format specifications to ensure data integrity:
|
|
268
268
|
|
|
269
269
|
```typescript
|
|
270
|
-
import {
|
|
270
|
+
import { validateFileOrBuffer, getValidatorForFile } from "@willwade/aac-processors/validation";
|
|
271
271
|
|
|
272
|
-
//
|
|
273
|
-
const
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
console.log(`Errors: ${result.errors}`);
|
|
278
|
-
console.log(`Warnings: ${result.warnings}`);
|
|
279
|
-
|
|
280
|
-
// Detailed validation results
|
|
281
|
-
if (!result.valid) {
|
|
282
|
-
result.results
|
|
283
|
-
.filter((check) => !check.valid)
|
|
284
|
-
.forEach((check) => {
|
|
285
|
-
console.log(`✗ ${check.description}: ${check.error}`);
|
|
286
|
-
});
|
|
287
|
-
}
|
|
272
|
+
// Works in Node, Vite, and esbuild (pass Buffers from the browser/CLI)
|
|
273
|
+
const fileName = "board.obf";
|
|
274
|
+
const validator = getValidatorForFile(fileName);
|
|
275
|
+
const bufferOrPath = new Uint8Array(await file.arrayBuffer()); // or fs path in Node
|
|
276
|
+
const result = await validateFileOrBuffer(bufferOrPath, fileName);
|
|
288
277
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
278
|
+
console.log(result.valid, result.errors, result.warnings);
|
|
279
|
+
result.results.forEach((check) => {
|
|
280
|
+
if (!check.valid) console.log(`✗ ${check.description}: ${check.error}`);
|
|
292
281
|
});
|
|
293
|
-
const gridsetResult = await gridsetProcessor.validate("vocab.gridsetx");
|
|
294
282
|
```
|
|
295
283
|
|
|
296
284
|
#### Using the CLI
|
|
@@ -312,11 +300,15 @@ aacprocessors validate board.gridsetx --gridset-password <password>
|
|
|
312
300
|
#### What Gets Validated?
|
|
313
301
|
|
|
314
302
|
- **OBF/OBZ**: Spec compliance (Open Board Format)
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
303
|
+
- **Gridset/Gridsetx**: ZIP/XML structure, required Smartbox assets
|
|
304
|
+
- **Snap**: ZIP/package content, settings/pages/images
|
|
305
|
+
- **TouchChat**: ZIP structure, vocab metadata, nested boards
|
|
306
|
+
- **Asterics (.grd)**: JSON parse, grids, elements, coordinates
|
|
307
|
+
- **Excel (.xlsx/.xls)**: Workbook readability and worksheet content
|
|
308
|
+
- **OPML**: XML validity and outline hierarchy
|
|
309
|
+
- **DOT**: Graph nodes/edges present and text content
|
|
310
|
+
- **Apple Panels (.plist/.ascconfig)**: PanelDefinitions presence and buttons
|
|
311
|
+
- **OBFSet**: Bundled board layout checks
|
|
320
312
|
|
|
321
313
|
- **Gridset**: XML structure
|
|
322
314
|
- Required elements (gridset, pages, cells)
|
|
@@ -901,4 +893,4 @@ Want to help with any of these items? See our [Contributing Guidelines](#-contri
|
|
|
901
893
|
|
|
902
894
|
### Credits
|
|
903
895
|
|
|
904
|
-
Some of the OBF work is directly from https://github.com/open-aac/obf and https://github.com/open-aac/aac-metrics - OBLA too https://www.openboardformat.org/logs
|
|
896
|
+
Some of the OBF work is directly from https://github.com/open-aac/obf and https://github.com/open-aac/aac-metrics - OBLA too https://www.openboardformat.org/logs
|
|
@@ -10,6 +10,7 @@ const treeStructure_1 = require("../core/treeStructure");
|
|
|
10
10
|
const plist_1 = __importDefault(require("plist"));
|
|
11
11
|
const fs_1 = __importDefault(require("fs"));
|
|
12
12
|
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const validation_1 = require("../validation");
|
|
13
14
|
function isNormalizedPanel(panel) {
|
|
14
15
|
return typeof panel.id === 'string';
|
|
15
16
|
}
|
|
@@ -92,145 +93,187 @@ class ApplePanelsProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
92
93
|
return texts;
|
|
93
94
|
}
|
|
94
95
|
loadIntoTree(filePathOrBuffer) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (filePathOrBuffer
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
96
|
+
const filename = typeof filePathOrBuffer === 'string' ? path_1.default.basename(filePathOrBuffer) : 'upload.plist';
|
|
97
|
+
let buffer;
|
|
98
|
+
try {
|
|
99
|
+
if (Buffer.isBuffer(filePathOrBuffer)) {
|
|
100
|
+
buffer = filePathOrBuffer;
|
|
101
|
+
}
|
|
102
|
+
else if (typeof filePathOrBuffer === 'string') {
|
|
103
|
+
if (filePathOrBuffer.endsWith('.ascconfig')) {
|
|
104
|
+
const panelDefsPath = `${filePathOrBuffer}/Contents/Resources/PanelDefinitions.plist`;
|
|
105
|
+
if (fs_1.default.existsSync(panelDefsPath)) {
|
|
106
|
+
buffer = fs_1.default.readFileSync(panelDefsPath);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const validation = (0, validation_1.buildValidationResultFromMessage)({
|
|
110
|
+
filename,
|
|
111
|
+
filesize: 0,
|
|
112
|
+
format: 'applepanels',
|
|
113
|
+
message: `Apple Panels file not found: ${panelDefsPath}`,
|
|
114
|
+
type: 'missing',
|
|
115
|
+
description: 'PanelDefinitions.plist',
|
|
116
|
+
});
|
|
117
|
+
throw new validation_1.ValidationFailureError('Apple Panels file not found', validation);
|
|
118
|
+
}
|
|
106
119
|
}
|
|
107
120
|
else {
|
|
108
|
-
|
|
121
|
+
buffer = fs_1.default.readFileSync(filePathOrBuffer);
|
|
109
122
|
}
|
|
110
123
|
}
|
|
111
124
|
else {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
let panelsData = [];
|
|
122
|
-
if (Array.isArray(parsedData.panels)) {
|
|
123
|
-
panelsData = parsedData.panels.map((panel, index) => {
|
|
124
|
-
if (isNormalizedPanel(panel)) {
|
|
125
|
-
return panel;
|
|
126
|
-
}
|
|
127
|
-
const panelData = panel || {
|
|
128
|
-
PanelObjects: [],
|
|
129
|
-
};
|
|
130
|
-
return normalizePanel(panelData, `panel_${index}`);
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
else if (parsedData.Panels) {
|
|
134
|
-
const panelsDict = parsedData.Panels;
|
|
135
|
-
panelsData = Object.keys(panelsDict).map((panelId) => {
|
|
136
|
-
const rawPanel = panelsDict[panelId] || { PanelObjects: [] };
|
|
137
|
-
return normalizePanel(rawPanel, panelId);
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
const data = { panels: panelsData };
|
|
141
|
-
const tree = new treeStructure_1.AACTree();
|
|
142
|
-
tree.metadata.format = 'applepanels';
|
|
143
|
-
data.panels.forEach((panel) => {
|
|
144
|
-
const page = new treeStructure_1.AACPage({
|
|
145
|
-
id: panel.id,
|
|
146
|
-
name: panel.name,
|
|
147
|
-
grid: [],
|
|
148
|
-
buttons: [],
|
|
149
|
-
parentId: null,
|
|
150
|
-
});
|
|
151
|
-
// Create a 2D grid to track button positions
|
|
152
|
-
const gridLayout = [];
|
|
153
|
-
const maxRows = 20; // Reasonable default for Apple Panels
|
|
154
|
-
const maxCols = 20;
|
|
155
|
-
for (let r = 0; r < maxRows; r++) {
|
|
156
|
-
gridLayout[r] = new Array(maxCols).fill(null);
|
|
125
|
+
const validation = (0, validation_1.buildValidationResultFromMessage)({
|
|
126
|
+
filename,
|
|
127
|
+
filesize: 0,
|
|
128
|
+
format: 'applepanels',
|
|
129
|
+
message: 'Invalid input: expected string path or Buffer',
|
|
130
|
+
type: 'input',
|
|
131
|
+
description: 'Apple Panels input',
|
|
132
|
+
});
|
|
133
|
+
throw new validation_1.ValidationFailureError('Invalid Apple Panels input', validation);
|
|
157
134
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
actionType: 'ActionOpenPanel',
|
|
169
|
-
parameters: { PanelID: `USER.${btn.targetPanel}` },
|
|
170
|
-
},
|
|
171
|
-
},
|
|
172
|
-
fallback: {
|
|
173
|
-
type: 'NAVIGATE',
|
|
174
|
-
targetPageId: btn.targetPanel,
|
|
175
|
-
},
|
|
135
|
+
const content = buffer.toString('utf8');
|
|
136
|
+
const parsedData = plist_1.default.parse(content);
|
|
137
|
+
let panelsData = [];
|
|
138
|
+
if (Array.isArray(parsedData.panels)) {
|
|
139
|
+
panelsData = parsedData.panels.map((panel, index) => {
|
|
140
|
+
if (isNormalizedPanel(panel)) {
|
|
141
|
+
return panel;
|
|
142
|
+
}
|
|
143
|
+
const panelData = panel || {
|
|
144
|
+
PanelObjects: [],
|
|
176
145
|
};
|
|
146
|
+
return normalizePanel(panelData, `panel_${index}`);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else if (parsedData.Panels) {
|
|
150
|
+
const panelsDict = parsedData.Panels;
|
|
151
|
+
panelsData = Object.keys(panelsDict).map((panelId) => {
|
|
152
|
+
const rawPanel = panelsDict[panelId] || { PanelObjects: [] };
|
|
153
|
+
return normalizePanel(rawPanel, panelId);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if (panelsData.length === 0) {
|
|
157
|
+
const validation = (0, validation_1.buildValidationResultFromMessage)({
|
|
158
|
+
filename,
|
|
159
|
+
filesize: buffer.byteLength,
|
|
160
|
+
format: 'applepanels',
|
|
161
|
+
message: 'No panels found in Apple Panels file',
|
|
162
|
+
type: 'structure',
|
|
163
|
+
description: 'Panels definition',
|
|
164
|
+
});
|
|
165
|
+
throw new validation_1.ValidationFailureError('Apple Panels has no panels', validation);
|
|
166
|
+
}
|
|
167
|
+
const data = { panels: panelsData };
|
|
168
|
+
const tree = new treeStructure_1.AACTree();
|
|
169
|
+
tree.metadata.format = 'applepanels';
|
|
170
|
+
data.panels.forEach((panel) => {
|
|
171
|
+
const page = new treeStructure_1.AACPage({
|
|
172
|
+
id: panel.id,
|
|
173
|
+
name: panel.name,
|
|
174
|
+
grid: [],
|
|
175
|
+
buttons: [],
|
|
176
|
+
parentId: null,
|
|
177
|
+
});
|
|
178
|
+
const gridLayout = [];
|
|
179
|
+
const maxRows = 20;
|
|
180
|
+
const maxCols = 20;
|
|
181
|
+
for (let r = 0; r < maxRows; r++) {
|
|
182
|
+
gridLayout[r] = new Array(maxCols).fill(null);
|
|
177
183
|
}
|
|
178
|
-
|
|
179
|
-
semanticAction
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
184
|
+
panel.buttons.forEach((btn, idx) => {
|
|
185
|
+
let semanticAction;
|
|
186
|
+
if (btn.targetPanel) {
|
|
187
|
+
semanticAction = {
|
|
188
|
+
category: treeStructure_1.AACSemanticCategory.NAVIGATION,
|
|
189
|
+
intent: treeStructure_1.AACSemanticIntent.NAVIGATE_TO,
|
|
190
|
+
targetId: btn.targetPanel,
|
|
191
|
+
platformData: {
|
|
192
|
+
applePanels: {
|
|
193
|
+
actionType: 'ActionOpenPanel',
|
|
194
|
+
parameters: { PanelID: `USER.${btn.targetPanel}` },
|
|
189
195
|
},
|
|
190
196
|
},
|
|
197
|
+
fallback: {
|
|
198
|
+
type: 'NAVIGATE',
|
|
199
|
+
targetPageId: btn.targetPanel,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
semanticAction = {
|
|
205
|
+
category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
|
|
206
|
+
intent: treeStructure_1.AACSemanticIntent.SPEAK_TEXT,
|
|
207
|
+
text: btn.message || btn.label,
|
|
208
|
+
platformData: {
|
|
209
|
+
applePanels: {
|
|
210
|
+
actionType: 'ActionPressKeyCharSequence',
|
|
211
|
+
parameters: {
|
|
212
|
+
CharString: btn.message || btn.label || '',
|
|
213
|
+
isStickyKey: false,
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
fallback: {
|
|
218
|
+
type: 'SPEAK',
|
|
219
|
+
message: btn.message || btn.label,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const button = new treeStructure_1.AACButton({
|
|
224
|
+
id: `${panel.id}_btn_${idx}`,
|
|
225
|
+
label: btn.label,
|
|
226
|
+
message: btn.message || btn.label,
|
|
227
|
+
targetPageId: btn.targetPanel,
|
|
228
|
+
semanticAction: semanticAction,
|
|
229
|
+
style: {
|
|
230
|
+
backgroundColor: btn.DisplayColor,
|
|
231
|
+
fontSize: btn.FontSize,
|
|
232
|
+
fontWeight: btn.DisplayImageWeight === 'bold' ? 'bold' : 'normal',
|
|
191
233
|
},
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
backgroundColor: btn.DisplayColor,
|
|
206
|
-
fontSize: btn.FontSize,
|
|
207
|
-
fontWeight: btn.DisplayImageWeight === 'bold' ? 'bold' : 'normal',
|
|
208
|
-
},
|
|
209
|
-
});
|
|
210
|
-
page.addButton(button);
|
|
211
|
-
// Place button in grid layout using Rect position data
|
|
212
|
-
if (btn.Rect) {
|
|
213
|
-
const rect = this.parseRect(btn.Rect);
|
|
214
|
-
if (rect) {
|
|
215
|
-
const gridPos = this.pixelToGrid(rect.x, rect.y);
|
|
216
|
-
const gridWidth = Math.max(1, Math.ceil(rect.width / 25));
|
|
217
|
-
const gridHeight = Math.max(1, Math.ceil(rect.height / 25));
|
|
218
|
-
// Place button in grid (handle width/height span)
|
|
219
|
-
for (let r = gridPos.gridY; r < gridPos.gridY + gridHeight && r < maxRows; r++) {
|
|
220
|
-
for (let c = gridPos.gridX; c < gridPos.gridX + gridWidth && c < maxCols; c++) {
|
|
221
|
-
if (gridLayout[r] && gridLayout[r][c] === null) {
|
|
222
|
-
gridLayout[r][c] = button;
|
|
234
|
+
});
|
|
235
|
+
page.addButton(button);
|
|
236
|
+
if (btn.Rect) {
|
|
237
|
+
const rect = this.parseRect(btn.Rect);
|
|
238
|
+
if (rect) {
|
|
239
|
+
const gridPos = this.pixelToGrid(rect.x, rect.y);
|
|
240
|
+
const gridWidth = Math.max(1, Math.ceil(rect.width / 25));
|
|
241
|
+
const gridHeight = Math.max(1, Math.ceil(rect.height / 25));
|
|
242
|
+
for (let r = gridPos.gridY; r < gridPos.gridY + gridHeight && r < maxRows; r++) {
|
|
243
|
+
for (let c = gridPos.gridX; c < gridPos.gridX + gridWidth && c < maxCols; c++) {
|
|
244
|
+
if (gridLayout[r] && gridLayout[r][c] === null) {
|
|
245
|
+
gridLayout[r][c] = button;
|
|
246
|
+
}
|
|
223
247
|
}
|
|
224
248
|
}
|
|
225
249
|
}
|
|
226
250
|
}
|
|
227
|
-
}
|
|
251
|
+
});
|
|
252
|
+
page.grid = gridLayout;
|
|
253
|
+
tree.addPage(page);
|
|
228
254
|
});
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
255
|
+
return tree;
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
if (err instanceof validation_1.ValidationFailureError) {
|
|
259
|
+
throw err;
|
|
260
|
+
}
|
|
261
|
+
const validation = (0, validation_1.buildValidationResultFromMessage)({
|
|
262
|
+
filename,
|
|
263
|
+
filesize: Buffer.isBuffer(filePathOrBuffer)
|
|
264
|
+
? filePathOrBuffer.byteLength
|
|
265
|
+
: typeof filePathOrBuffer === 'string'
|
|
266
|
+
? fs_1.default.existsSync(filePathOrBuffer)
|
|
267
|
+
? fs_1.default.statSync(filePathOrBuffer).size
|
|
268
|
+
: 0
|
|
269
|
+
: 0,
|
|
270
|
+
format: 'applepanels',
|
|
271
|
+
message: err?.message || 'Failed to parse Apple Panels file',
|
|
272
|
+
type: 'parse',
|
|
273
|
+
description: 'Parse Apple Panels plist',
|
|
274
|
+
});
|
|
275
|
+
throw new validation_1.ValidationFailureError('Failed to load Apple Panels file', validation, err);
|
|
276
|
+
}
|
|
234
277
|
}
|
|
235
278
|
processTexts(filePathOrBuffer, translations, outputPath) {
|
|
236
279
|
// Load the tree, apply translations, and save to new file
|
|
@@ -12,6 +12,8 @@ exports.getContrastingTextColor = getContrastingTextColor;
|
|
|
12
12
|
const baseProcessor_1 = require("../core/baseProcessor");
|
|
13
13
|
const treeStructure_1 = require("../core/treeStructure");
|
|
14
14
|
const fs_1 = __importDefault(require("fs"));
|
|
15
|
+
const path_1 = __importDefault(require("path"));
|
|
16
|
+
const validation_1 = require("../validation");
|
|
15
17
|
const DEFAULT_COLOR_SCHEME_DEFINITIONS = [
|
|
16
18
|
{
|
|
17
19
|
name: 'CS_MODIFIED_FITZGERALD_KEY_VERY_LIGHT',
|
|
@@ -664,122 +666,136 @@ class AstericsGridProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
664
666
|
}
|
|
665
667
|
loadIntoTree(filePathOrBuffer) {
|
|
666
668
|
const tree = new treeStructure_1.AACTree();
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
content =
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
return tree;
|
|
677
|
-
}
|
|
678
|
-
const rawColorConfig = grdFile.metadata?.colorConfig;
|
|
679
|
-
const colorConfig = isRecord(rawColorConfig)
|
|
680
|
-
? rawColorConfig
|
|
681
|
-
: undefined;
|
|
682
|
-
const activeColorSchemeDefinition = getActiveColorSchemeDefinition(colorConfig);
|
|
683
|
-
// First pass: create all pages
|
|
684
|
-
grdFile.grids.forEach((grid) => {
|
|
685
|
-
const page = new treeStructure_1.AACPage({
|
|
686
|
-
id: grid.id,
|
|
687
|
-
name: this.getLocalizedLabel(grid.label) || grid.id,
|
|
688
|
-
grid: [],
|
|
689
|
-
buttons: [],
|
|
690
|
-
parentId: null,
|
|
691
|
-
style: {
|
|
692
|
-
backgroundColor: colorConfig?.gridBackgroundColor || '#FFFFFF',
|
|
693
|
-
borderColor: colorConfig?.elementBorderColor || '#CCCCCC',
|
|
694
|
-
borderWidth: colorConfig?.borderWidth || 1,
|
|
695
|
-
fontFamily: colorConfig?.fontFamily || 'Arial',
|
|
696
|
-
fontSize: colorConfig?.fontSizePct ? colorConfig.fontSizePct * 16 : 16, // Convert percentage to pixels, default to 16
|
|
697
|
-
fontColor: colorConfig?.fontColor || '#000000',
|
|
698
|
-
},
|
|
699
|
-
});
|
|
700
|
-
tree.addPage(page);
|
|
701
|
-
});
|
|
702
|
-
// Second pass: add buttons and establish navigation
|
|
703
|
-
grdFile.grids.forEach((grid) => {
|
|
704
|
-
const page = tree.getPage(grid.id);
|
|
705
|
-
if (!page)
|
|
706
|
-
return;
|
|
707
|
-
// Create a 2D grid to track button positions
|
|
708
|
-
const gridLayout = [];
|
|
709
|
-
const maxRows = Math.max(10, grid.rowCount || 10);
|
|
710
|
-
const maxCols = Math.max(10, grid.minColumnCount || 10);
|
|
711
|
-
for (let r = 0; r < maxRows; r++) {
|
|
712
|
-
gridLayout[r] = new Array(maxCols).fill(null);
|
|
669
|
+
const filename = typeof filePathOrBuffer === 'string' ? path_1.default.basename(filePathOrBuffer) : 'upload.grd';
|
|
670
|
+
const buffer = Buffer.isBuffer(filePathOrBuffer)
|
|
671
|
+
? filePathOrBuffer
|
|
672
|
+
: fs_1.default.readFileSync(filePathOrBuffer);
|
|
673
|
+
try {
|
|
674
|
+
let content = buffer.toString('utf-8');
|
|
675
|
+
// Remove BOM if present
|
|
676
|
+
if (content.charCodeAt(0) === 0xfeff) {
|
|
677
|
+
content = content.slice(1);
|
|
713
678
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
679
|
+
const grdFile = JSON.parse(content);
|
|
680
|
+
if (!grdFile.grids) {
|
|
681
|
+
const validationResult = (0, validation_1.buildValidationResultFromMessage)({
|
|
682
|
+
filename,
|
|
683
|
+
filesize: buffer.byteLength,
|
|
684
|
+
format: 'asterics',
|
|
685
|
+
message: 'Missing grids array in Asterics .grd file',
|
|
686
|
+
type: 'structure',
|
|
687
|
+
description: 'Asterics grid collection',
|
|
688
|
+
});
|
|
689
|
+
throw new validation_1.ValidationFailureError('Invalid Asterics grid file', validationResult);
|
|
690
|
+
}
|
|
691
|
+
const rawColorConfig = grdFile.metadata?.colorConfig;
|
|
692
|
+
const colorConfig = isRecord(rawColorConfig)
|
|
693
|
+
? rawColorConfig
|
|
694
|
+
: undefined;
|
|
695
|
+
const activeColorSchemeDefinition = getActiveColorSchemeDefinition(colorConfig);
|
|
696
|
+
grdFile.grids.forEach((grid) => {
|
|
697
|
+
const page = new treeStructure_1.AACPage({
|
|
698
|
+
id: grid.id,
|
|
699
|
+
name: this.getLocalizedLabel(grid.label) || grid.id,
|
|
700
|
+
grid: [],
|
|
701
|
+
buttons: [],
|
|
702
|
+
parentId: null,
|
|
703
|
+
style: {
|
|
704
|
+
backgroundColor: colorConfig?.gridBackgroundColor || '#FFFFFF',
|
|
705
|
+
borderColor: colorConfig?.elementBorderColor || '#CCCCCC',
|
|
706
|
+
borderWidth: colorConfig?.borderWidth || 1,
|
|
707
|
+
fontFamily: colorConfig?.fontFamily || 'Arial',
|
|
708
|
+
fontSize: colorConfig?.fontSizePct ? colorConfig.fontSizePct * 16 : 16,
|
|
709
|
+
fontColor: colorConfig?.fontColor || '#000000',
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
tree.addPage(page);
|
|
713
|
+
});
|
|
714
|
+
grdFile.grids.forEach((grid) => {
|
|
715
|
+
const page = tree.getPage(grid.id);
|
|
716
|
+
if (!page)
|
|
717
|
+
return;
|
|
718
|
+
const gridLayout = [];
|
|
719
|
+
const maxRows = Math.max(10, grid.rowCount || 10);
|
|
720
|
+
const maxCols = Math.max(10, grid.minColumnCount || 10);
|
|
721
|
+
for (let r = 0; r < maxRows; r++) {
|
|
722
|
+
gridLayout[r] = new Array(maxCols).fill(null);
|
|
723
|
+
}
|
|
724
|
+
grid.gridElements.forEach((element) => {
|
|
725
|
+
const button = this.createButtonFromElement(element, colorConfig, activeColorSchemeDefinition);
|
|
726
|
+
page.addButton(button);
|
|
727
|
+
const buttonX = element.x || 0;
|
|
728
|
+
const buttonY = element.y || 0;
|
|
729
|
+
const buttonWidth = element.width || 1;
|
|
730
|
+
const buttonHeight = element.height || 1;
|
|
731
|
+
for (let r = buttonY; r < buttonY + buttonHeight && r < maxRows; r++) {
|
|
732
|
+
for (let c = buttonX; c < buttonX + buttonWidth && c < maxCols; c++) {
|
|
733
|
+
if (gridLayout[r] && gridLayout[r][c] === null) {
|
|
734
|
+
gridLayout[r][c] = button;
|
|
735
|
+
}
|
|
727
736
|
}
|
|
728
737
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
targetPage.parentId = page.id;
|
|
738
|
+
const navAction = element.actions.find((a) => a.modelName === 'GridActionNavigate');
|
|
739
|
+
const targetGridId = navAction && typeof navAction.toGridId === 'string' ? navAction.toGridId : undefined;
|
|
740
|
+
if (targetGridId) {
|
|
741
|
+
const targetPage = tree.getPage(targetGridId);
|
|
742
|
+
if (targetPage) {
|
|
743
|
+
targetPage.parentId = page.id;
|
|
744
|
+
}
|
|
737
745
|
}
|
|
738
|
-
}
|
|
746
|
+
});
|
|
747
|
+
page.grid = gridLayout;
|
|
739
748
|
});
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
// Extract all unique languages from all grids and elements
|
|
751
|
-
const languages = new Set();
|
|
752
|
-
grdFile.grids.forEach((grid) => {
|
|
753
|
-
if (grid.label) {
|
|
754
|
-
Object.keys(grid.label).forEach((lang) => languages.add(lang));
|
|
755
|
-
}
|
|
756
|
-
grid.gridElements?.forEach((element) => {
|
|
757
|
-
if (element.label) {
|
|
758
|
-
Object.keys(element.label).forEach((lang) => languages.add(lang));
|
|
749
|
+
const astericsMetadata = {
|
|
750
|
+
format: 'asterics',
|
|
751
|
+
hasGlobalGrid: false,
|
|
752
|
+
};
|
|
753
|
+
if (grdFile.grids && grdFile.grids.length > 0) {
|
|
754
|
+
astericsMetadata.name = this.getLocalizedLabel(grdFile.grids[0].label);
|
|
755
|
+
const languages = new Set();
|
|
756
|
+
grdFile.grids.forEach((grid) => {
|
|
757
|
+
if (grid.label) {
|
|
758
|
+
Object.keys(grid.label).forEach((lang) => languages.add(lang));
|
|
759
759
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
760
|
+
grid.gridElements?.forEach((element) => {
|
|
761
|
+
if (element.label) {
|
|
762
|
+
Object.keys(element.label).forEach((lang) => languages.add(lang));
|
|
763
|
+
}
|
|
764
|
+
element.wordForms?.forEach((wf) => {
|
|
765
|
+
if (wf.lang)
|
|
766
|
+
languages.add(wf.lang);
|
|
767
|
+
});
|
|
764
768
|
});
|
|
765
769
|
});
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
770
|
+
if (languages.size > 0) {
|
|
771
|
+
astericsMetadata.languages = Array.from(languages).sort();
|
|
772
|
+
astericsMetadata.locale = languages.has('en')
|
|
773
|
+
? 'en'
|
|
774
|
+
: languages.has('de')
|
|
775
|
+
? 'de'
|
|
776
|
+
: astericsMetadata.languages[0];
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
tree.metadata = astericsMetadata;
|
|
780
|
+
if (grdFile.metadata && grdFile.metadata.homeGridId) {
|
|
781
|
+
tree.rootId = grdFile.metadata.homeGridId;
|
|
775
782
|
}
|
|
783
|
+
return tree;
|
|
776
784
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
785
|
+
catch (err) {
|
|
786
|
+
if (err instanceof validation_1.ValidationFailureError) {
|
|
787
|
+
throw err;
|
|
788
|
+
}
|
|
789
|
+
const validationResult = (0, validation_1.buildValidationResultFromMessage)({
|
|
790
|
+
filename,
|
|
791
|
+
filesize: buffer.byteLength,
|
|
792
|
+
format: 'asterics',
|
|
793
|
+
message: err?.message || 'Failed to parse Asterics grid file',
|
|
794
|
+
type: 'parse',
|
|
795
|
+
description: 'Parse Asterics grid JSON',
|
|
796
|
+
});
|
|
797
|
+
throw new validation_1.ValidationFailureError('Failed to load Asterics grid', validationResult, err);
|
|
781
798
|
}
|
|
782
|
-
return tree;
|
|
783
799
|
}
|
|
784
800
|
getLocalizedLabel(labelMap) {
|
|
785
801
|
if (!labelMap)
|