@willwade/aac-processors 0.2.20 → 0.3.0

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.
@@ -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.0",
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"