@willwade/aac-processors 0.0.29 → 0.1.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.
Files changed (92) hide show
  1. package/README.md +52 -852
  2. package/dist/browser/core/baseProcessor.js +241 -0
  3. package/dist/browser/core/stringCasing.js +179 -0
  4. package/dist/browser/core/treeStructure.js +255 -0
  5. package/dist/browser/index.browser.js +73 -0
  6. package/dist/browser/processors/applePanelsProcessor.js +582 -0
  7. package/dist/browser/processors/astericsGridProcessor.js +1509 -0
  8. package/dist/browser/processors/dotProcessor.js +221 -0
  9. package/dist/browser/processors/gridset/commands.js +962 -0
  10. package/dist/browser/processors/gridset/crypto.js +53 -0
  11. package/dist/browser/processors/gridset/password.js +43 -0
  12. package/dist/browser/processors/gridset/pluginTypes.js +277 -0
  13. package/dist/browser/processors/gridset/resolver.js +137 -0
  14. package/dist/browser/processors/gridset/symbolAlignment.js +276 -0
  15. package/dist/browser/processors/gridset/symbols.js +421 -0
  16. package/dist/browser/processors/gridsetProcessor.js +2002 -0
  17. package/dist/browser/processors/obfProcessor.js +705 -0
  18. package/dist/browser/processors/opmlProcessor.js +274 -0
  19. package/dist/browser/types/aac.js +38 -0
  20. package/dist/browser/utilities/analytics/utils/idGenerator.js +89 -0
  21. package/dist/browser/utilities/translation/translationProcessor.js +200 -0
  22. package/dist/browser/utils/io.js +95 -0
  23. package/dist/browser/validation/baseValidator.js +156 -0
  24. package/dist/browser/validation/gridsetValidator.js +355 -0
  25. package/dist/browser/validation/obfValidator.js +500 -0
  26. package/dist/browser/validation/validationTypes.js +46 -0
  27. package/dist/cli/index.js +5 -5
  28. package/dist/core/analyze.d.ts +2 -2
  29. package/dist/core/analyze.js +2 -2
  30. package/dist/core/baseProcessor.d.ts +5 -4
  31. package/dist/core/baseProcessor.js +22 -27
  32. package/dist/core/treeStructure.d.ts +5 -5
  33. package/dist/core/treeStructure.js +1 -4
  34. package/dist/index.browser.d.ts +37 -0
  35. package/dist/index.browser.js +99 -0
  36. package/dist/index.d.ts +1 -48
  37. package/dist/index.js +1 -136
  38. package/dist/index.node.d.ts +48 -0
  39. package/dist/index.node.js +152 -0
  40. package/dist/processors/applePanelsProcessor.d.ts +5 -4
  41. package/dist/processors/applePanelsProcessor.js +58 -62
  42. package/dist/processors/astericsGridProcessor.d.ts +7 -6
  43. package/dist/processors/astericsGridProcessor.js +31 -42
  44. package/dist/processors/dotProcessor.d.ts +5 -4
  45. package/dist/processors/dotProcessor.js +25 -33
  46. package/dist/processors/excelProcessor.d.ts +4 -3
  47. package/dist/processors/excelProcessor.js +6 -3
  48. package/dist/processors/gridset/crypto.d.ts +18 -0
  49. package/dist/processors/gridset/crypto.js +57 -0
  50. package/dist/processors/gridset/helpers.d.ts +1 -1
  51. package/dist/processors/gridset/helpers.js +18 -8
  52. package/dist/processors/gridset/password.d.ts +20 -3
  53. package/dist/processors/gridset/password.js +17 -3
  54. package/dist/processors/gridset/wordlistHelpers.d.ts +3 -3
  55. package/dist/processors/gridset/wordlistHelpers.js +21 -20
  56. package/dist/processors/gridsetProcessor.d.ts +7 -12
  57. package/dist/processors/gridsetProcessor.js +118 -77
  58. package/dist/processors/obfProcessor.d.ts +9 -7
  59. package/dist/processors/obfProcessor.js +131 -56
  60. package/dist/processors/obfsetProcessor.d.ts +5 -4
  61. package/dist/processors/obfsetProcessor.js +10 -16
  62. package/dist/processors/opmlProcessor.d.ts +5 -4
  63. package/dist/processors/opmlProcessor.js +27 -34
  64. package/dist/processors/snapProcessor.d.ts +8 -7
  65. package/dist/processors/snapProcessor.js +15 -12
  66. package/dist/processors/touchchatProcessor.d.ts +8 -7
  67. package/dist/processors/touchchatProcessor.js +22 -17
  68. package/dist/types/aac.d.ts +0 -2
  69. package/dist/types/aac.js +2 -0
  70. package/dist/utils/io.d.ts +12 -0
  71. package/dist/utils/io.js +107 -0
  72. package/dist/validation/gridsetValidator.js +7 -7
  73. package/dist/validation/snapValidator.js +28 -35
  74. package/docs/BROWSER_USAGE.md +618 -0
  75. package/examples/README.md +77 -0
  76. package/examples/browser-test-server.js +81 -0
  77. package/examples/browser-test.html +331 -0
  78. package/examples/vitedemo/QUICKSTART.md +74 -0
  79. package/examples/vitedemo/README.md +157 -0
  80. package/examples/vitedemo/index.html +376 -0
  81. package/examples/vitedemo/package-lock.json +1221 -0
  82. package/examples/vitedemo/package.json +18 -0
  83. package/examples/vitedemo/src/main.ts +519 -0
  84. package/examples/vitedemo/test-files/example.dot +14 -0
  85. package/examples/vitedemo/test-files/example.grd +1 -0
  86. package/examples/vitedemo/test-files/example.gridset +0 -0
  87. package/examples/vitedemo/test-files/example.obz +0 -0
  88. package/examples/vitedemo/test-files/example.opml +18 -0
  89. package/examples/vitedemo/test-files/simple.obf +53 -0
  90. package/examples/vitedemo/tsconfig.json +24 -0
  91. package/examples/vitedemo/vite.config.ts +34 -0
  92. package/package.json +20 -4
@@ -0,0 +1,500 @@
1
+ /* eslint-disable @typescript-eslint/require-await */
2
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
3
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
4
+ /* eslint-disable @typescript-eslint/no-unsafe-return */
5
+ /* eslint-disable @typescript-eslint/restrict-template-expressions */
6
+ import JSZip from 'jszip';
7
+ import { BaseValidator } from './baseValidator';
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ const OBF_FORMAT = 'open-board-0.1';
11
+ const OBF_FORMAT_CURRENT_VERSION = 0.1;
12
+ /**
13
+ * Validator for Open Board Format (OBF/OBZ) files
14
+ */
15
+ export class ObfValidator extends BaseValidator {
16
+ constructor() {
17
+ super();
18
+ }
19
+ /**
20
+ * Validate an OBF file from disk
21
+ */
22
+ static async validateFile(filePath) {
23
+ const validator = new ObfValidator();
24
+ const content = fs.readFileSync(filePath);
25
+ const stats = fs.statSync(filePath);
26
+ return validator.validate(content, path.basename(filePath), stats.size);
27
+ }
28
+ /**
29
+ * Check if content is OBF format
30
+ */
31
+ static async identifyFormat(content, filename) {
32
+ const name = filename.toLowerCase();
33
+ if (name.endsWith('.obf') || name.endsWith('.obz')) {
34
+ return true;
35
+ }
36
+ // Try to parse as JSON and check format
37
+ try {
38
+ const contentStr = Buffer.isBuffer(content) ? content.toString() : content;
39
+ const json = JSON.parse(contentStr);
40
+ return json && json.format && json.format.startsWith('open-board-');
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ /**
47
+ * Main validation method
48
+ */
49
+ async validate(content, filename, filesize) {
50
+ this.reset();
51
+ // Determine if it's OBF or OBZ
52
+ const isObz = filename.toLowerCase().endsWith('.obz');
53
+ if (isObz) {
54
+ return await this.validateObz(content, filename, filesize);
55
+ }
56
+ else {
57
+ return await this.validateObf(content, filename, filesize);
58
+ }
59
+ }
60
+ /**
61
+ * Validate OBF content
62
+ */
63
+ async validateObf(content, filename, filesize) {
64
+ await this.add_check('filename', 'file name', async () => {
65
+ if (!filename.match(/\.obf$/)) {
66
+ this.warn('filename should end with .obf');
67
+ }
68
+ });
69
+ let json = null;
70
+ await this.add_check('valid_json', 'JSON file', async () => {
71
+ try {
72
+ json = JSON.parse(content.toString());
73
+ }
74
+ catch {
75
+ this.err("Couldn't parse as JSON", true);
76
+ }
77
+ });
78
+ if (!json) {
79
+ return this.buildResult(filename, filesize, 'obf');
80
+ }
81
+ await this.validateBoardStructure(json);
82
+ return this.buildResult(filename, filesize, 'obf');
83
+ }
84
+ /**
85
+ * Validate OBZ (zip) content
86
+ */
87
+ async validateObz(content, filename, filesize) {
88
+ await this.add_check('filename', 'file name', async () => {
89
+ if (!filename.match(/\.obz$/)) {
90
+ this.warn('filename should end with .obz');
91
+ }
92
+ });
93
+ let zip = null;
94
+ let validZip = false;
95
+ await this.add_check('zip', 'valid zip', async () => {
96
+ try {
97
+ zip = await JSZip.loadAsync(content);
98
+ validZip = true;
99
+ }
100
+ catch {
101
+ this.err('file is not a valid zip package');
102
+ }
103
+ });
104
+ if (validZip && zip) {
105
+ await this.validateObzStructure(zip);
106
+ }
107
+ return this.buildResult(filename, filesize, 'obz');
108
+ }
109
+ /**
110
+ * Validate OBF board structure
111
+ */
112
+ async validateBoardStructure(board) {
113
+ await this.add_check('format_version', 'format version', async () => {
114
+ if (!board.format) {
115
+ this.err(`format attribute is required, set to ${OBF_FORMAT}`);
116
+ return;
117
+ }
118
+ const version = parseFloat(board.format.split('-').pop() || '0');
119
+ if (version > OBF_FORMAT_CURRENT_VERSION) {
120
+ this.err(`format version (${version}) is invalid, current version is ${OBF_FORMAT_CURRENT_VERSION}`);
121
+ }
122
+ else if (version < OBF_FORMAT_CURRENT_VERSION) {
123
+ this.warn(`format version (${version}) is old, consider updating to ${OBF_FORMAT_CURRENT_VERSION}`);
124
+ }
125
+ });
126
+ await this.add_check('id', 'board ID', async () => {
127
+ if (!board.id) {
128
+ this.err('id attribute is required');
129
+ }
130
+ });
131
+ await this.add_check('locale', 'locale', async () => {
132
+ if (!board.locale) {
133
+ this.err('locale attribute is required, please set to "en" for English');
134
+ }
135
+ });
136
+ await this.add_check('extras', 'extra attributes', async () => {
137
+ const attrs = [
138
+ 'format',
139
+ 'id',
140
+ 'locale',
141
+ 'url',
142
+ 'data_url',
143
+ 'name',
144
+ 'description_html',
145
+ 'default_layout',
146
+ 'buttons',
147
+ 'images',
148
+ 'sounds',
149
+ 'grid',
150
+ 'license',
151
+ ];
152
+ Object.keys(board).forEach((key) => {
153
+ if (!attrs.includes(key) && !key.startsWith('ext_')) {
154
+ this.warn(`${key} attribute is not defined in the spec, should be prefixed with ext_yourapp_`);
155
+ }
156
+ });
157
+ });
158
+ await this.add_check('description', 'descriptive attributes', async () => {
159
+ if (!board.name) {
160
+ this.warn('name attribute is strongly recommended');
161
+ }
162
+ if (!board.description_html) {
163
+ this.warn('description_html attribute is recommended');
164
+ }
165
+ });
166
+ await this.add_check('background', 'background attribute', async () => {
167
+ if (board.background && typeof board.background !== 'object') {
168
+ this.err('background attribute must be a hash');
169
+ }
170
+ });
171
+ await this.add_check('buttons', 'buttons attribute', async () => {
172
+ if (!board.buttons) {
173
+ this.err('buttons attribute is required');
174
+ }
175
+ else if (!Array.isArray(board.buttons)) {
176
+ this.err('buttons attribute must be an array');
177
+ }
178
+ });
179
+ await this.add_check('grid', 'grid attribute', async () => {
180
+ if (!board.grid) {
181
+ this.err('grid attribute is required');
182
+ return;
183
+ }
184
+ if (typeof board.grid !== 'object') {
185
+ this.err('grid attribute must be a hash');
186
+ return;
187
+ }
188
+ if (typeof board.grid.rows !== 'number' || board.grid.rows < 1) {
189
+ this.err('grid.rows must be a positive number');
190
+ }
191
+ if (typeof board.grid.columns !== 'number' || board.grid.columns < 1) {
192
+ this.err('grid.columns must be a positive number');
193
+ }
194
+ if (!board.grid.order || !Array.isArray(board.grid.order)) {
195
+ this.err('grid.order must be an array of arrays');
196
+ return;
197
+ }
198
+ if (board.grid.order.length !== board.grid.rows) {
199
+ this.err(`grid.order length (${board.grid.order.length}) must match grid.rows (${board.grid.rows})`);
200
+ }
201
+ if (!board.grid.order.every((r) => Array.isArray(r) && r.length === board.grid.columns)) {
202
+ this.err(`grid.order must contain ${board.grid.rows} arrays each of size ${board.grid.columns}`);
203
+ }
204
+ });
205
+ await this.add_check('grid_ids', 'button IDs in grid.order attribute', async () => {
206
+ const buttonIds = (board.buttons || []).map((b) => b.id);
207
+ const usedButtonIds = [];
208
+ if (board.grid && board.grid.order) {
209
+ board.grid.order.forEach((row) => {
210
+ if (Array.isArray(row)) {
211
+ row.forEach((id) => {
212
+ if (id !== null && id !== undefined) {
213
+ usedButtonIds.push(id);
214
+ if (!buttonIds.includes(id)) {
215
+ this.err(`grid.order references button with id ${id} but no button with that id found in buttons attribute`);
216
+ }
217
+ }
218
+ });
219
+ }
220
+ });
221
+ }
222
+ if (usedButtonIds.length === 0) {
223
+ this.warn('board has no buttons defined in the grid');
224
+ }
225
+ const unusedIds = buttonIds.filter((id) => !usedButtonIds.includes(id));
226
+ if (unusedIds.length > 0) {
227
+ this.warn(`not all defined buttons were included in the grid order (${unusedIds.join(',')})`);
228
+ }
229
+ });
230
+ await this.add_check('images', 'images attribute', async () => {
231
+ if (!board.images) {
232
+ this.err('images attribute is required');
233
+ }
234
+ else if (!Array.isArray(board.images)) {
235
+ this.err('images attribute must be an array');
236
+ }
237
+ });
238
+ if (Array.isArray(board.images)) {
239
+ for (let i = 0; i < board.images.length; i++) {
240
+ const image = board.images[i];
241
+ await this.add_check(`image[${i}]`, `image at images[${i}]`, async () => {
242
+ if (typeof image !== 'object') {
243
+ this.err('image must be a hash');
244
+ return;
245
+ }
246
+ if (!image.id) {
247
+ this.err('image.id is required');
248
+ }
249
+ if (!image.width || typeof image.width !== 'number' || image.width < 1) {
250
+ this.warn('image.width should be a valid positive number');
251
+ }
252
+ if (!image.height || typeof image.height !== 'number' || image.height < 1) {
253
+ this.warn('image.height should be a valid positive number');
254
+ }
255
+ if (!image.content_type || !image.content_type.match(/^image\/.+$/)) {
256
+ this.err('image.content_type must be a valid image mime type');
257
+ }
258
+ if (!image.url && !image.data && !image.symbol && !image.path) {
259
+ this.err('image must have data, url, path or symbol attribute defined');
260
+ }
261
+ const imageAttrs = [
262
+ 'id',
263
+ 'width',
264
+ 'height',
265
+ 'content_type',
266
+ 'data',
267
+ 'url',
268
+ 'symbol',
269
+ 'path',
270
+ 'data_url',
271
+ 'license',
272
+ ];
273
+ Object.keys(image).forEach((key) => {
274
+ if (!imageAttrs.includes(key) && !key.startsWith('ext_')) {
275
+ this.warn(`image.${key} attribute is not defined in the spec, should be prefixed with ext_yourapp_`);
276
+ }
277
+ });
278
+ });
279
+ }
280
+ }
281
+ await this.add_check('sounds', 'sounds attribute', async () => {
282
+ if (!board.sounds) {
283
+ this.err('sounds attribute is required');
284
+ }
285
+ else if (!Array.isArray(board.sounds)) {
286
+ this.err('sounds attribute must be an array');
287
+ }
288
+ });
289
+ if (Array.isArray(board.sounds)) {
290
+ for (let i = 0; i < board.sounds.length; i++) {
291
+ const sound = board.sounds[i];
292
+ await this.add_check(`sounds[${i}]`, `sound at sounds[${i}]`, async () => {
293
+ if (typeof sound !== 'object') {
294
+ this.err('sound must be a hash');
295
+ return;
296
+ }
297
+ if (!sound.id) {
298
+ this.err('sound.id is required');
299
+ }
300
+ if (sound.duration !== undefined &&
301
+ (typeof sound.duration !== 'number' || sound.duration < 0)) {
302
+ this.err('sound.duration must be a valid positive number');
303
+ }
304
+ if (!sound.content_type || !sound.content_type.match(/^audio\/.+$/)) {
305
+ this.err('sound.content_type must be a valid audio mime type');
306
+ }
307
+ if (!sound.url && !sound.data && !sound.path) {
308
+ this.err('sound must have data, url, or path attribute defined');
309
+ }
310
+ });
311
+ }
312
+ }
313
+ if (Array.isArray(board.buttons)) {
314
+ for (let i = 0; i < board.buttons.length; i++) {
315
+ const button = board.buttons[i];
316
+ await this.add_check(`buttons[${i}]`, `button at buttons[${i}]`, async () => {
317
+ await this.validateButton(button);
318
+ });
319
+ }
320
+ }
321
+ }
322
+ /**
323
+ * Validate a single button
324
+ */
325
+ async validateButton(button) {
326
+ if (typeof button !== 'object') {
327
+ this.err('button must be a hash');
328
+ return;
329
+ }
330
+ if (!button.id) {
331
+ this.err('button.id is required');
332
+ }
333
+ if (!button.label) {
334
+ this.err('button.label is required');
335
+ }
336
+ ['top', 'left', 'width', 'height'].forEach((attr) => {
337
+ if (button[attr] !== undefined && (typeof button[attr] !== 'number' || button[attr] < 0)) {
338
+ this.warn(`button.${attr} should be a positive number`);
339
+ }
340
+ });
341
+ ['background_color', 'border_color'].forEach((color) => {
342
+ if (button[color]) {
343
+ if (!button[color].match(/^\s*rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*(,\s*[01]?\.?\d*)?\)\s*/)) {
344
+ this.err(`button.${color} must be a valid rgb or rgba value if defined ("${button[color]}" is invalid)`);
345
+ }
346
+ }
347
+ });
348
+ if (button.hidden !== undefined && typeof button.hidden !== 'boolean') {
349
+ this.err('button.hidden must be a boolean if defined');
350
+ }
351
+ if (!button.image_id) {
352
+ this.warn('button.image_id is recommended');
353
+ }
354
+ if (button.action && typeof button.action === 'string' && !button.action.match(/^(:|\+)/)) {
355
+ this.err('button.action must start with either : or + if defined');
356
+ }
357
+ if (button.actions && !Array.isArray(button.actions)) {
358
+ this.err('button.actions must be an array of strings');
359
+ }
360
+ const buttonAttrs = [
361
+ 'id',
362
+ 'label',
363
+ 'vocalization',
364
+ 'image_id',
365
+ 'sound_id',
366
+ 'hidden',
367
+ 'background_color',
368
+ 'border_color',
369
+ 'action',
370
+ 'actions',
371
+ 'load_board',
372
+ 'top',
373
+ 'left',
374
+ 'width',
375
+ 'height',
376
+ ];
377
+ Object.keys(button).forEach((key) => {
378
+ if (!buttonAttrs.includes(key) && !key.startsWith('ext_')) {
379
+ this.warn(`button.${key} attribute is not defined in the spec, should be prefixed with ext_yourapp_`);
380
+ }
381
+ });
382
+ }
383
+ /**
384
+ * Validate OBZ structure
385
+ */
386
+ async validateObzStructure(zip) {
387
+ let json = null;
388
+ await this.add_check('manifest', 'manifest.json', async () => {
389
+ const manifestFile = zip.file('manifest.json');
390
+ if (!manifestFile) {
391
+ this.err('manifest.json is required in the zip package');
392
+ return;
393
+ }
394
+ try {
395
+ const manifestStr = await manifestFile.async('string');
396
+ json = JSON.parse(manifestStr);
397
+ }
398
+ catch {
399
+ json = null;
400
+ }
401
+ if (!json) {
402
+ this.err('manifest.json must contain a valid JSON structure');
403
+ }
404
+ });
405
+ if (json) {
406
+ await this.validateManifest(json, zip);
407
+ }
408
+ }
409
+ /**
410
+ * Validate manifest structure
411
+ */
412
+ async validateManifest(manifest, zip) {
413
+ await this.add_check('manifest_format', 'manifest.json format version', async () => {
414
+ if (!manifest.format) {
415
+ this.err(`format attribute is required, set to ${OBF_FORMAT}`);
416
+ return;
417
+ }
418
+ const version = parseFloat(manifest.format.split('-').pop());
419
+ if (version > OBF_FORMAT_CURRENT_VERSION) {
420
+ this.err(`format version (${version}) is invalid, current version is ${OBF_FORMAT_CURRENT_VERSION}`);
421
+ }
422
+ else if (version < OBF_FORMAT_CURRENT_VERSION) {
423
+ this.warn(`format version (${version}) is old, consider updating to ${OBF_FORMAT_CURRENT_VERSION}`);
424
+ }
425
+ });
426
+ await this.add_check('manifest_root', 'manifest.json root attribute', async () => {
427
+ if (!manifest.root) {
428
+ this.err('root attribute is required');
429
+ }
430
+ if (!zip.file(manifest.root)) {
431
+ this.err('root attribute must reference a file in the package');
432
+ }
433
+ });
434
+ await this.add_check('manifest_paths', 'manifest.json paths attribute', async () => {
435
+ if (!manifest.paths || typeof manifest.paths !== 'object') {
436
+ this.err('paths attribute must be a valid hash');
437
+ }
438
+ if (!manifest.paths.boards || typeof manifest.paths.boards !== 'object') {
439
+ this.err('paths.boards must be a valid hash');
440
+ }
441
+ });
442
+ await this.add_check('manifest_extras', 'manifest.json extra attributes', async () => {
443
+ const attrs = ['format', 'root', 'paths'];
444
+ Object.keys(manifest).forEach((key) => {
445
+ if (!attrs.includes(key) && !key.startsWith('ext_')) {
446
+ this.warn(`${key} attribute is not defined in the spec, should be prefixed with ext_yourapp_`);
447
+ }
448
+ });
449
+ const pathAttrs = ['boards', 'images', 'sounds'];
450
+ Object.keys(manifest.paths || {}).forEach((key) => {
451
+ if (!pathAttrs.includes(key) && !key.startsWith('ext_')) {
452
+ this.warn(`paths.${key} attribute is not defined in the spec, should be prefixed with ext_yourapp_`);
453
+ }
454
+ });
455
+ });
456
+ // Validate boards referenced in manifest
457
+ if (manifest.paths && manifest.paths.boards) {
458
+ for (const [id, boardPath] of Object.entries(manifest.paths.boards)) {
459
+ await this.add_check(`manifest_boards[${id}]`, `manifest.json path.boards.${id}`, async () => {
460
+ const bFile = zip.file(boardPath);
461
+ if (!bFile) {
462
+ this.err(`board path (${boardPath}) not found in the zip package`);
463
+ return;
464
+ }
465
+ try {
466
+ const boardStr = await bFile.async('string');
467
+ const boardJson = JSON.parse(boardStr);
468
+ if (!boardJson || boardJson.id !== id) {
469
+ const boardId = (boardJson && boardJson.id) || 'null';
470
+ this.err(`board at path (${boardPath}) defined in manifest with id "${id}" but actually has id "${boardId}"`);
471
+ }
472
+ }
473
+ catch {
474
+ this.err(`could not parse board at path (${boardPath})`);
475
+ }
476
+ });
477
+ }
478
+ }
479
+ // Validate images referenced in manifest
480
+ if (manifest.paths && manifest.paths.images) {
481
+ for (const [id, imgPath] of Object.entries(manifest.paths.images)) {
482
+ await this.add_check(`manifest_images[${id}]`, `manifest.json path.images.${id}`, async () => {
483
+ if (!zip.file(imgPath)) {
484
+ this.err(`image path (${imgPath}) not found in the zip package`);
485
+ }
486
+ });
487
+ }
488
+ }
489
+ // Validate sounds referenced in manifest
490
+ if (manifest.paths && manifest.paths.sounds) {
491
+ for (const [id, soundPath] of Object.entries(manifest.paths.sounds)) {
492
+ await this.add_check(`manifest_sounds[${id}]`, `manifest.json path.sounds.${id}`, async () => {
493
+ if (!zip.file(soundPath)) {
494
+ this.err(`sound path (${soundPath}) not found in the zip package`);
495
+ }
496
+ });
497
+ }
498
+ }
499
+ }
500
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Custom error class for validation errors
3
+ * Can be marked as a blocker to stop validation immediately
4
+ */
5
+ export class ValidationError extends Error {
6
+ constructor(message, blocker = false) {
7
+ super(message);
8
+ this.name = 'ValidationError';
9
+ this.blocker = blocker;
10
+ }
11
+ }
12
+ /**
13
+ * Error wrapper that carries a structured ValidationResult so callers
14
+ * can surface actionable details instead of generic exceptions.
15
+ */
16
+ export class ValidationFailureError extends Error {
17
+ constructor(message, validationResult, originalError) {
18
+ super(message);
19
+ this.name = 'ValidationFailureError';
20
+ this.validationResult = validationResult;
21
+ this.originalError = originalError;
22
+ }
23
+ }
24
+ /**
25
+ * Build a minimal ValidationResult for situations where we cannot run
26
+ * the full validator (e.g., early parse failure) but still want
27
+ * structured feedback for the caller.
28
+ */
29
+ export function buildValidationResultFromMessage(params) {
30
+ return {
31
+ filename: params.filename,
32
+ filesize: params.filesize,
33
+ format: params.format,
34
+ valid: false,
35
+ errors: 1,
36
+ warnings: 0,
37
+ results: [
38
+ {
39
+ type: params.type || 'parse',
40
+ description: params.description || 'parse',
41
+ valid: false,
42
+ error: params.message,
43
+ },
44
+ ],
45
+ };
46
+ }
package/dist/cli/index.js CHANGED
@@ -74,14 +74,14 @@ commander_1.program
74
74
  .option('--no-exclude-system', "Don't exclude system buttons (Delete, Clear, etc.)")
75
75
  .option('--exclude-buttons <list>', 'Comma-separated list of button labels/terms to exclude')
76
76
  .option('--gridset-password <password>', 'Password for encrypted Grid3 archives (.gridsetx)')
77
- .action((file, options) => {
77
+ .action(async (file, options) => {
78
78
  try {
79
79
  // Parse filtering options
80
80
  const filteringOptions = parseFilteringOptions(options);
81
81
  // Auto-detect format if not specified
82
82
  const format = options.format || detectFormat(file);
83
83
  const processor = (0, analyze_1.getProcessor)(format, filteringOptions);
84
- const tree = processor.loadIntoTree(file);
84
+ const tree = await processor.loadIntoTree(file);
85
85
  const result = {
86
86
  format,
87
87
  tree,
@@ -109,14 +109,14 @@ commander_1.program
109
109
  .option('--no-exclude-system', "Don't exclude system buttons (Delete, Clear, etc.)")
110
110
  .option('--exclude-buttons <list>', 'Comma-separated list of button labels/terms to exclude')
111
111
  .option('--gridset-password <password>', 'Password for encrypted Grid3 archives (.gridsetx)')
112
- .action((file, options) => {
112
+ .action(async (file, options) => {
113
113
  try {
114
114
  // Parse filtering options
115
115
  const filteringOptions = parseFilteringOptions(options);
116
116
  // Auto-detect format if not specified
117
117
  const format = options.format || detectFormat(file);
118
118
  const processor = (0, analyze_1.getProcessor)(format, filteringOptions);
119
- const texts = processor.extractTexts(file);
119
+ const texts = await processor.extractTexts(file);
120
120
  if (!options.quiet) {
121
121
  if (options.verbose) {
122
122
  console.log(`Extracting texts from ${file} (format: ${format})`);
@@ -167,7 +167,7 @@ commander_1.program
167
167
  const inputFormat = detectFormat(input);
168
168
  const inputProcessor = (0, analyze_1.getProcessor)(inputFormat, filteringOptions);
169
169
  // Load the tree (handle both files and folders)
170
- const tree = inputProcessor.loadIntoTree(input);
170
+ const tree = await inputProcessor.loadIntoTree(input);
171
171
  // Save using output format with same filtering options
172
172
  const outputProcessor = (0, analyze_1.getProcessor)(options.format, filteringOptions);
173
173
  await outputProcessor.saveFromTree(tree, output);
@@ -11,6 +11,6 @@ export declare function getProcessor(format: string, options?: ProcessorOptions)
11
11
  * @param file Path to the source file
12
12
  * @param format Format key or extension (passed to getProcessor)
13
13
  */
14
- export declare function analyze(file: string, format: string): {
14
+ export declare function analyze(file: string, format: string): Promise<{
15
15
  tree: AACTree;
16
- };
16
+ }>;
@@ -54,8 +54,8 @@ function getProcessor(format, options) {
54
54
  * @param file Path to the source file
55
55
  * @param format Format key or extension (passed to getProcessor)
56
56
  */
57
- function analyze(file, format) {
57
+ async function analyze(file, format) {
58
58
  const processor = getProcessor(format);
59
- const tree = processor.loadIntoTree(file);
59
+ const tree = await processor.loadIntoTree(file);
60
60
  return { tree };
61
61
  }
@@ -42,6 +42,7 @@
42
42
  import { AACTree, AACButton } from './treeStructure';
43
43
  import { StringCasing } from './stringCasing';
44
44
  import { ValidationResult } from '../validation/validationTypes';
45
+ import { BinaryOutput, ProcessorInput } from '../utils/io';
45
46
  export interface ProcessorOptions {
46
47
  excludeNavigationButtons?: boolean;
47
48
  excludeSystemButtons?: boolean;
@@ -86,10 +87,10 @@ export interface SourceString {
86
87
  declare abstract class BaseProcessor {
87
88
  protected options: ProcessorOptions;
88
89
  constructor(options?: ProcessorOptions);
89
- abstract extractTexts(filePathOrBuffer: string | Buffer): string[];
90
- abstract loadIntoTree(filePathOrBuffer: string | Buffer): AACTree;
91
- abstract processTexts(filePathOrBuffer: string | Buffer, translations: Map<string, string>, outputPath: string): Buffer;
92
- abstract saveFromTree(tree: AACTree, outputPath: string): void | Promise<void>;
90
+ abstract extractTexts(filePathOrBuffer: ProcessorInput): Promise<string[]>;
91
+ abstract loadIntoTree(filePathOrBuffer: ProcessorInput): Promise<AACTree>;
92
+ abstract processTexts(filePathOrBuffer: ProcessorInput, translations: Map<string, string>, outputPath: string): Promise<BinaryOutput>;
93
+ abstract saveFromTree(tree: AACTree, outputPath: string): Promise<void>;
93
94
  validate?(filePath: string): Promise<ValidationResult>;
94
95
  /**
95
96
  * Extract strings with metadata for external platform integration