@willwade/aac-processors 0.2.20 → 0.3.1

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.
@@ -147,14 +147,21 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
147
147
  const pageId = boardData?.id ? String(boardData.id) : _boardPath?.split(/[/\\]/).pop() || '';
148
148
  const images = boardData.images;
149
149
  const buttons = await Promise.all(sourceButtons.map(async (btn) => {
150
+ // Resolve load_board.path to a board ID (strip .obf suffix if present).
151
+ // The OBF spec says load_board.path is a file path, but internally we
152
+ // store the board ID as targetPageId.
153
+ const rawPath = btn.load_board?.path;
154
+ const boardRef = rawPath
155
+ ? rawPath.replace(/\.obf$/i, '').replace(/\.obz$/i, '')
156
+ : undefined;
150
157
  const semanticAction = btn.load_board
151
158
  ? {
152
159
  category: treeStructure_1.AACSemanticCategory.NAVIGATION,
153
160
  intent: treeStructure_1.AACSemanticIntent.NAVIGATE_TO,
154
- targetId: btn.load_board.path,
161
+ targetId: boardRef,
155
162
  fallback: {
156
163
  type: 'NAVIGATE',
157
- targetPageId: btn.load_board.path,
164
+ targetPageId: boardRef,
158
165
  },
159
166
  }
160
167
  : {
@@ -202,7 +209,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
202
209
  resolvedImageEntry: resolvedImage,
203
210
  parameters: Object.keys(buttonParameters).length > 0 ? buttonParameters : undefined,
204
211
  semanticAction,
205
- targetPageId: btn.load_board?.path,
212
+ targetPageId: boardRef,
206
213
  semantic_id: btn.semantic_id, // Extract semantic_id if present
207
214
  });
208
215
  }));
@@ -621,7 +628,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
621
628
  vocalization: button.message || button.label,
622
629
  load_board: button.semanticAction?.intent === treeStructure_1.AACSemanticIntent.NAVIGATE_TO && button.targetPageId
623
630
  ? {
624
- path: button.targetPageId,
631
+ path: this.getPageFilename(button.targetPageId, metadata),
625
632
  }
626
633
  : undefined,
627
634
  background_color: button.style?.backgroundColor,
@@ -691,7 +698,9 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
691
698
  });
692
699
  const manifest = {
693
700
  format: OBF_FORMAT_VERSION,
694
- root: tree.metadata.defaultHomePageId,
701
+ root: tree.metadata.defaultHomePageId
702
+ ? this.getPageFilename(tree.metadata.defaultHomePageId, tree.metadata)
703
+ : undefined,
695
704
  paths: {
696
705
  boards: Object.fromEntries(Object.entries(tree.pages).map(([id, page]) => [
697
706
  id,
@@ -758,7 +767,9 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
758
767
  if (Object.keys(tree.pages).length > 0) {
759
768
  const manifest = {
760
769
  format: OBF_FORMAT_VERSION,
761
- root: tree.metadata.defaultHomePageId,
770
+ root: tree.metadata.defaultHomePageId
771
+ ? this.getPageFilename(tree.metadata.defaultHomePageId, tree.metadata)
772
+ : undefined,
762
773
  paths: {
763
774
  boards: Object.fromEntries(Object.entries(tree.pages).map(([id, page]) => [
764
775
  id,
@@ -23,27 +23,20 @@ export declare class GridsetValidator extends BaseValidator {
23
23
  */
24
24
  private isZip;
25
25
  /**
26
- * Validate a single XML file (legacy or exploded format)
26
+ * Validate a single grid.xml file (exploded/non-zip format)
27
27
  */
28
28
  private validateSingleXml;
29
29
  /**
30
30
  * Validate a ZIP archive (.gridset)
31
+ *
32
+ * Real Grid 3 archives do NOT contain a top-level gridset.xml. The required
33
+ * structure is: Settings0/settings.xml + FileMap.xml + Grids/<name>/grid.xml
34
+ * (+ optional Styles, images, thumbnails).
31
35
  */
32
36
  private validateZipArchive;
33
37
  /**
34
- * Validate Gridset structure
38
+ * Validate a real Grid 3 <Grid> element (from grid.xml).
39
+ * Checks for GridGuid, Cells, and basic cell structure.
35
40
  */
36
- private validateGridsetStructure;
37
- /**
38
- * Validate a single page
39
- */
40
- private validatePage;
41
- /**
42
- * Validate a single cell
43
- */
44
- private validateCell;
45
- /**
46
- * Validate color format (Grid3 uses ARGB format)
47
- */
48
- private isValidGrid3Color;
41
+ private validateGridStructure;
49
42
  }
@@ -55,12 +55,12 @@ class GridsetValidator extends baseValidator_1.BaseValidator {
55
55
  if (name.endsWith('.gridset') || name.endsWith('.gridsetx')) {
56
56
  return true;
57
57
  }
58
- // Try to parse as XML and check for gridset structure
58
+ // Try to parse as XML and check for a Grid 3 <Grid> root element
59
59
  try {
60
60
  const contentStr = typeof content === 'string' ? content : (0, io_1.decodeText)((0, io_1.toUint8Array)(content));
61
61
  const parser = new xml2js.Parser();
62
62
  const result = await parser.parseStringPromise(contentStr);
63
- return result && (result.gridset || result.Gridset);
63
+ return !!(result && (result.Grid || result.grid));
64
64
  }
65
65
  catch {
66
66
  return false;
@@ -104,7 +104,7 @@ class GridsetValidator extends baseValidator_1.BaseValidator {
104
104
  return content[0] === 0x50 && content[1] === 0x4b && content[2] === 0x03 && content[3] === 0x04;
105
105
  }
106
106
  /**
107
- * Validate a single XML file (legacy or exploded format)
107
+ * Validate a single grid.xml file (exploded/non-zip format)
108
108
  */
109
109
  async validateSingleXml(content, filename, _filesize) {
110
110
  let xmlObj = null;
@@ -120,49 +120,57 @@ class GridsetValidator extends baseValidator_1.BaseValidator {
120
120
  });
121
121
  if (!xmlObj)
122
122
  return;
123
- await this.add_check('xml_structure', 'gridset root element', async () => {
124
- if (!xmlObj.gridset && !xmlObj.Gridset) {
125
- this.err('missing root gridset element', true);
123
+ await this.add_check('xml_structure', 'Grid root element', async () => {
124
+ if (!xmlObj.Grid && !xmlObj.grid) {
125
+ this.err('missing root Grid element', true);
126
126
  }
127
127
  });
128
- const gridset = xmlObj.gridset || xmlObj.Gridset;
129
- if (gridset) {
130
- await this.validateGridsetStructure(gridset, filename, content);
128
+ const grid = xmlObj.Grid || xmlObj.grid;
129
+ if (grid) {
130
+ await this.validateGridStructure(grid, filename);
131
131
  }
132
132
  }
133
133
  /**
134
134
  * Validate a ZIP archive (.gridset)
135
+ *
136
+ * Real Grid 3 archives do NOT contain a top-level gridset.xml. The required
137
+ * structure is: Settings0/settings.xml + FileMap.xml + Grids/<name>/grid.xml
138
+ * (+ optional Styles, images, thumbnails).
135
139
  */
136
- async validateZipArchive(content, filename, _filesize) {
140
+ async validateZipArchive(content, _filename, _filesize) {
137
141
  const zip = await this._options.zipAdapter(content);
138
142
  const entries = zip.listFiles();
139
- // Check for gridset.xml (required)
140
- await this.add_check('gridset_xml_presence', 'gridset.xml presence', async () => {
141
- const gridsetEntry = entries.find((e) => e.toLowerCase() === 'gridset.xml');
142
- if (!gridsetEntry) {
143
- this.err('Missing gridset.xml in archive', true);
143
+ // --- Required: at least one Grids/<name>/grid.xml ---
144
+ const gridEntries = entries.filter((e) => /^Grids\/.+\/grid\.xml$/i.test(e));
145
+ await this.add_check('grid_xml_presence', 'Grids/*/grid.xml presence', async () => {
146
+ if (gridEntries.length === 0) {
147
+ this.err('No Grids/<name>/grid.xml found in archive', true);
144
148
  }
145
- else {
149
+ });
150
+ // Validate the <Grid> structure of each grid.xml (sample up to 5)
151
+ for (let i = 0; i < Math.min(gridEntries.length, 5); i++) {
152
+ const entry = gridEntries[i];
153
+ await this.add_check(`grid_xml[${entry}]`, `grid structure: ${entry}`, async () => {
146
154
  try {
147
- const gridsetXml = await zip.readFile(gridsetEntry);
155
+ const gridXml = await zip.readFile(entry);
148
156
  const parser = new xml2js.Parser();
149
- const xmlObj = await parser.parseStringPromise(gridsetXml);
150
- const gridset = xmlObj.gridset || xmlObj.Gridset;
151
- if (!gridset) {
152
- this.err('Invalid gridset.xml structure', true);
157
+ const xmlObj = await parser.parseStringPromise(gridXml);
158
+ const grid = xmlObj.Grid || xmlObj.grid;
159
+ if (!grid) {
160
+ this.err(`${entry}: missing root <Grid> element`);
153
161
  }
154
162
  else {
155
- await this.validateGridsetStructure(gridset, filename, new Uint8Array());
163
+ await this.validateGridStructure(grid, entry);
156
164
  }
157
165
  }
158
166
  catch (e) {
159
- this.err(`Failed to parse gridset.xml: ${e.message}`, true);
167
+ this.err(`${entry}: failed to parse XML: ${e.message}`);
160
168
  }
161
- }
162
- });
163
- // Check for settings.xml (highly recommended/required for metadata)
169
+ });
170
+ }
171
+ // --- Recommended: Settings0/settings.xml ---
164
172
  await this.add_check('settings_xml_presence', 'settings.xml presence', async () => {
165
- const settingsEntry = entries.find((e) => e.toLowerCase() === 'settings.xml');
173
+ const settingsEntry = entries.find((e) => e.toLowerCase() === 'settings.xml' || e.toLowerCase().endsWith('/settings.xml'));
166
174
  if (!settingsEntry) {
167
175
  this.warn('Missing settings.xml in archive (required for full metadata)');
168
176
  }
@@ -187,188 +195,36 @@ class GridsetValidator extends baseValidator_1.BaseValidator {
187
195
  }
188
196
  }
189
197
  });
190
- }
191
- /**
192
- * Validate Gridset structure
193
- */
194
- async validateGridsetStructure(gridset, _filename, _content) {
195
- // Check for required elements
196
- await this.add_check('gridset_id', 'gridset id', async () => {
197
- const id = gridset.$.id || gridset.$.Id;
198
- if (!id) {
199
- this.warn('gridset should have an id attribute');
200
- }
201
- });
202
- await this.add_check('gridset_name', 'gridset name', async () => {
203
- const name = gridset.$.name || gridset.$.Name || gridset.name?.[0];
204
- if (!name) {
205
- this.warn('gridset should have a name attribute or element');
206
- }
207
- });
208
- // Check for pages
209
- await this.add_check('pages', 'pages element', async () => {
210
- if (!gridset.pages && !gridset.Pages) {
211
- this.err('gridset must have a pages element');
212
- }
213
- else {
214
- const pages = gridset.pages || gridset.Pages;
215
- if (!pages[0] || !Array.isArray(pages[0].page)) {
216
- this.warn('pages should contain at least one page element');
217
- }
218
- }
219
- });
220
- // Validate individual pages
221
- const pages = gridset.pages?.[0] || gridset.Pages?.[0];
222
- if (pages && Array.isArray(pages.page)) {
223
- await this.add_check('page_count', 'page count', async () => {
224
- if (pages.page.length === 0) {
225
- this.err('gridset must contain at least one page');
226
- }
227
- });
228
- for (let i = 0; i < Math.min(pages.page.length, 10); i++) {
229
- // Limit to first 10 pages to avoid excessive validation
230
- const page = pages.page[i];
231
- await this.validatePage(page, i);
232
- }
233
- }
234
- // Check for fixedCellSize
235
- await this.add_check('fixed_cell_size', 'fixedCellSize element', async () => {
236
- const fixedSize = gridset.fixedCellSize || gridset.FixedCellSize;
237
- if (!fixedSize) {
238
- this.warn('gridset should have a fixedCellSize element for consistency');
239
- }
240
- else {
241
- // Validate fixedCellSize structure
242
- const size = fixedSize[0];
243
- if (size) {
244
- const width = size.$.width || size.$.Width;
245
- const height = size.$.height || size.$.Height;
246
- if (!width || !height) {
247
- this.warn('fixedCellSize should have both width and height attributes');
248
- }
249
- else if (isNaN(parseInt(width)) || isNaN(parseInt(height))) {
250
- this.err('fixedCellSize width and height must be valid numbers');
251
- }
252
- }
253
- }
254
- });
255
- // Check for styles
256
- await this.add_check('styles', 'styles element', async () => {
257
- const styles = gridset.styles || gridset.Styles;
258
- if (!styles) {
259
- this.warn('gridset should have a styles element for consistent formatting');
198
+ // --- Recommended: FileMap.xml (used for image resolution) ---
199
+ await this.add_check('filemap_presence', 'FileMap.xml presence', async () => {
200
+ const filemapEntry = entries.find((e) => e.toLowerCase() === 'filemap.xml');
201
+ if (!filemapEntry) {
202
+ this.warn('Missing FileMap.xml in archive (used for image resolution)');
260
203
  }
261
204
  });
262
205
  }
263
206
  /**
264
- * Validate a single page
207
+ * Validate a real Grid 3 <Grid> element (from grid.xml).
208
+ * Checks for GridGuid, Cells, and basic cell structure.
265
209
  */
266
- async validatePage(page, index) {
267
- await this.add_check(`page[${index}]_id`, `page ${index} id`, async () => {
268
- const id = page.$.id || page.$.Id;
269
- if (!id) {
270
- this.err(`page at index ${index} is missing id attribute`);
210
+ async validateGridStructure(grid, label) {
211
+ await this.add_check(`${label}_gridguid`, 'GridGuid', async () => {
212
+ const guid = grid.GridGuid || grid.gridGuid;
213
+ if (!guid) {
214
+ this.warn(`${label}: missing <GridGuid> element`);
271
215
  }
272
216
  });
273
- await this.add_check(`page[${index}]_name`, `page ${index} name`, async () => {
274
- const name = page.$.name || page.$.Name || page.name?.[0];
275
- if (!name) {
276
- this.warn(`page ${index} should have a name`);
277
- }
278
- });
279
- // Check for cells
280
- await this.add_check(`page[${index}]_cells`, `page ${index} cells`, async () => {
281
- const cells = page.cells || page.Cells;
217
+ await this.add_check(`${label}_cells`, 'Cells element', async () => {
218
+ const cells = grid.Cells || grid.cells;
282
219
  if (!cells) {
283
- this.warn(`page ${index} should have a cells element`);
220
+ this.warn(`${label}: missing <Cells> element`);
221
+ return;
284
222
  }
285
- else {
286
- const cellArray = cells[0]?.cell || cells[0]?.Cell;
287
- if (!cellArray || !Array.isArray(cellArray) || cellArray.length === 0) {
288
- this.warn(`page ${index} should contain at least one cell`);
289
- }
223
+ const cellArray = cells[0]?.Cell || cells[0]?.cell;
224
+ if (!cellArray || !Array.isArray(cellArray) || cellArray.length === 0) {
225
+ this.warn(`${label}: <Cells> should contain at least one <Cell>`);
290
226
  }
291
227
  });
292
- // Validate cells if present
293
- const cells = page.cells?.[0] || page.Cells?.[0];
294
- if (cells) {
295
- const cellArray = cells.cell || cells.Cell;
296
- if (Array.isArray(cellArray) && cellArray.length > 0) {
297
- // Sample a few cells to validate
298
- const sampleSize = Math.min(cellArray.length, 5);
299
- for (let i = 0; i < sampleSize; i++) {
300
- await this.validateCell(cellArray[i], index, i);
301
- }
302
- }
303
- }
304
- }
305
- /**
306
- * Validate a single cell
307
- */
308
- async validateCell(cell, pageIdx, cellIdx) {
309
- await this.add_check(`page[${pageIdx}]_cell[${cellIdx}]_id`, `cell id`, async () => {
310
- const id = cell.$.id || cell.$.Id;
311
- if (!id) {
312
- this.warn(`cell ${cellIdx} on page ${pageIdx} is missing id attribute`);
313
- }
314
- });
315
- await this.add_check(`page[${pageIdx}]_cell[${cellIdx}]_content`, `cell content`, async () => {
316
- const label = cell.$.label || cell.$.Label;
317
- const image = cell.$.image || cell.$.Image;
318
- if (!label && !image) {
319
- this.warn(`cell ${cellIdx} on page ${pageIdx} should have a label or image`);
320
- }
321
- });
322
- // Validate scan block number (Grid 3 attribute)
323
- await this.add_check(`page[${pageIdx}]_cell[${cellIdx}]_scanblock`, `cell scan block`, async () => {
324
- const scanBlock = cell.$.scanBlock || cell.$.ScanBlock;
325
- if (scanBlock !== undefined) {
326
- const blockNum = parseInt(scanBlock, 10);
327
- if (isNaN(blockNum) || blockNum < 1 || blockNum > 8) {
328
- this.err(`cell ${cellIdx} on page ${pageIdx} has invalid scanBlock value: ${scanBlock} (must be 1-8)`, false);
329
- }
330
- }
331
- });
332
- // Check for color attributes
333
- const backgroundColor = cell.$.backgroundColor || cell.$.BackgroundColor;
334
- const _color = cell.$.color || cell.$.Color;
335
- if (backgroundColor) {
336
- await this.add_check(`page[${pageIdx}]_cell[${cellIdx}]_bg_color`, `cell background color`, async () => {
337
- // Grid3 colors can be in various formats: named colors, hex, ARGB
338
- // We just check it's not empty
339
- if (backgroundColor.length === 0) {
340
- this.warn(`cell ${cellIdx} has empty background color`);
341
- }
342
- });
343
- }
344
- // Check for valid jump references
345
- const jump = cell.$.jump || cell.$.Jump;
346
- if (jump) {
347
- await this.add_check(`page[${pageIdx}]_cell[${cellIdx}]_jump`, `cell jump reference`, async () => {
348
- if (typeof jump !== 'string' || jump.length === 0) {
349
- this.warn(`cell ${cellIdx} has invalid jump reference`);
350
- }
351
- });
352
- }
353
- }
354
- /**
355
- * Validate color format (Grid3 uses ARGB format)
356
- */
357
- isValidGrid3Color(color) {
358
- if (!color || color.length === 0)
359
- return false;
360
- // Named colors are valid
361
- if (/^[a-zA-Z]+$/.test(color))
362
- return true;
363
- // ARGB format: #AARRGGBB or #RRGGBB
364
- if (color.startsWith('#')) {
365
- return color.length === 7 || color.length === 9;
366
- }
367
- // RGB format: rgb(r,g,b) or rgba(r,g,b,a)
368
- if (color.startsWith('rgb')) {
369
- return true; // Simplified check
370
- }
371
- return false;
372
228
  }
373
229
  }
374
230
  exports.GridsetValidator = GridsetValidator;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@willwade/aac-processors",
3
- "version": "0.2.20",
3
+ "version": "0.3.1",
4
4
  "description": "A comprehensive TypeScript library for processing AAC (Augmentative and Alternative Communication) file formats with translation support",
5
5
  "main": "dist/index.js",
6
6
  "browser": "dist/browser/index.browser.js",
@@ -60,6 +60,10 @@
60
60
  "types": "./dist/astericsGrid.d.ts",
61
61
  "default": "./dist/astericsGrid.js"
62
62
  },
63
+ "./gotalkNow": {
64
+ "types": "./dist/gotalkNow.d.ts",
65
+ "default": "./dist/gotalkNow.js"
66
+ },
63
67
  "./translation": {
64
68
  "types": "./dist/translation.d.ts",
65
69
  "default": "./dist/translation.js"