@willwade/aac-processors 0.0.22 → 0.0.23

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 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 { ObfProcessor, GridsetProcessor } from "aac-processors";
270
+ import { validateFileOrBuffer, getValidatorForFile } from "@willwade/aac-processors/validation";
271
271
 
272
- // Validate OBF/OBZ files
273
- const obfProcessor = new ObfProcessor();
274
- const result = await obfProcessor.validate("board.obf");
275
-
276
- console.log(`Valid: ${result.valid}`);
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
- // Validate Gridset files (with optional password for encrypted files)
290
- const gridsetProcessor = new GridsetProcessor({
291
- gridsetPassword: "optional-password",
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
- - Required fields (format, id, locale, buttons, grid, images, sounds)
316
- - Grid structure (rows, columns, order)
317
- - Button references (image_id, sound_id, load_board paths)
318
- - Color formats (RGB/RGBA)
319
- - Cross-reference validation
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
- let content;
96
- if (Buffer.isBuffer(filePathOrBuffer)) {
97
- content = filePathOrBuffer.toString('utf8');
98
- }
99
- else if (typeof filePathOrBuffer === 'string') {
100
- // Check if it's a .ascconfig folder or a direct .plist file
101
- if (filePathOrBuffer.endsWith('.ascconfig')) {
102
- // Read from proper Apple Panels structure: *.ascconfig/Contents/Resources/PanelDefinitions.plist
103
- const panelDefsPath = `${filePathOrBuffer}/Contents/Resources/PanelDefinitions.plist`;
104
- if (fs_1.default.existsSync(panelDefsPath)) {
105
- content = fs_1.default.readFileSync(panelDefsPath, 'utf8');
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
- throw new Error(`Apple Panels file not found: ${panelDefsPath}`);
121
+ buffer = fs_1.default.readFileSync(filePathOrBuffer);
109
122
  }
110
123
  }
111
124
  else {
112
- // Fallback: treat as direct .plist file
113
- content = fs_1.default.readFileSync(filePathOrBuffer, 'utf8');
114
- }
115
- }
116
- else {
117
- throw new Error('Invalid input: expected string path or Buffer');
118
- }
119
- const parsedData = plist_1.default.parse(content);
120
- // Handle both old format (panels array) and new Apple Panels format (Panels dict)
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
- panel.buttons.forEach((btn, idx) => {
159
- // Create semantic action from Apple Panels button
160
- let semanticAction;
161
- if (btn.targetPanel) {
162
- semanticAction = {
163
- category: treeStructure_1.AACSemanticCategory.NAVIGATION,
164
- intent: treeStructure_1.AACSemanticIntent.NAVIGATE_TO,
165
- targetId: btn.targetPanel,
166
- platformData: {
167
- applePanels: {
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
- else {
179
- semanticAction = {
180
- category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
181
- intent: treeStructure_1.AACSemanticIntent.SPEAK_TEXT,
182
- text: btn.message || btn.label,
183
- platformData: {
184
- applePanels: {
185
- actionType: 'ActionPressKeyCharSequence',
186
- parameters: {
187
- CharString: btn.message || btn.label || '',
188
- isStickyKey: false,
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
- fallback: {
193
- type: 'SPEAK',
194
- message: btn.message || btn.label,
195
- },
196
- };
197
- }
198
- const button = new treeStructure_1.AACButton({
199
- id: `${panel.id}_btn_${idx}`,
200
- label: btn.label,
201
- message: btn.message || btn.label,
202
- targetPageId: btn.targetPanel,
203
- semanticAction: semanticAction,
204
- style: {
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
- // Set the page's grid layout
230
- page.grid = gridLayout;
231
- tree.addPage(page);
232
- });
233
- return tree;
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
- let content = Buffer.isBuffer(filePathOrBuffer)
668
- ? filePathOrBuffer.toString('utf-8')
669
- : fs_1.default.readFileSync(filePathOrBuffer, 'utf-8');
670
- // Remove BOM if present
671
- if (content.charCodeAt(0) === 0xfeff) {
672
- content = content.slice(1);
673
- }
674
- const grdFile = JSON.parse(content);
675
- if (!grdFile.grids) {
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
- grid.gridElements.forEach((element) => {
715
- const button = this.createButtonFromElement(element, colorConfig, activeColorSchemeDefinition);
716
- page.addButton(button);
717
- // Place button in grid layout using its x,y coordinates
718
- const buttonX = element.x || 0;
719
- const buttonY = element.y || 0;
720
- const buttonWidth = element.width || 1;
721
- const buttonHeight = element.height || 1;
722
- // Place button in grid (handle width/height span)
723
- for (let r = buttonY; r < buttonY + buttonHeight && r < maxRows; r++) {
724
- for (let c = buttonX; c < buttonX + buttonWidth && c < maxCols; c++) {
725
- if (gridLayout[r] && gridLayout[r][c] === null) {
726
- gridLayout[r][c] = button;
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
- // Handle navigation relationships
731
- const navAction = element.actions.find((a) => a.modelName === 'GridActionNavigate');
732
- const targetGridId = navAction && typeof navAction.toGridId === 'string' ? navAction.toGridId : undefined;
733
- if (targetGridId) {
734
- const targetPage = tree.getPage(targetGridId);
735
- if (targetPage) {
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
- // Set the page's grid layout
741
- page.grid = gridLayout;
742
- });
743
- // Set metadata for Asterics Grid files
744
- const astericsMetadata = {
745
- format: 'asterics',
746
- hasGlobalGrid: false, // Can be extended in the future
747
- };
748
- if (grdFile.grids && grdFile.grids.length > 0) {
749
- astericsMetadata.name = this.getLocalizedLabel(grdFile.grids[0].label);
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
- // Also check word forms for languages
761
- element.wordForms?.forEach((wf) => {
762
- if (wf.lang)
763
- languages.add(wf.lang);
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
- if (languages.size > 0) {
768
- astericsMetadata.languages = Array.from(languages).sort();
769
- // Set primary locale to English if available, otherwise the first language found
770
- astericsMetadata.locale = languages.has('en')
771
- ? 'en'
772
- : languages.has('de')
773
- ? 'de'
774
- : astericsMetadata.languages[0];
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
- tree.metadata = astericsMetadata;
778
- // Set the home page from metadata.homeGridId
779
- if (grdFile.metadata && grdFile.metadata.homeGridId) {
780
- tree.rootId = grdFile.metadata.homeGridId;
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)