@willwade/aac-processors 0.0.9 → 0.0.11

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 (75) hide show
  1. package/README.md +85 -11
  2. package/dist/cli/index.js +87 -0
  3. package/dist/core/analyze.js +1 -0
  4. package/dist/core/baseProcessor.d.ts +6 -0
  5. package/dist/core/fileProcessor.js +1 -0
  6. package/dist/core/treeStructure.d.ts +3 -1
  7. package/dist/core/treeStructure.js +3 -1
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.js +3 -0
  10. package/dist/optional/symbolTools.js +4 -2
  11. package/dist/processors/gridset/colorUtils.d.ts +18 -0
  12. package/dist/processors/gridset/colorUtils.js +36 -0
  13. package/dist/processors/gridset/commands.d.ts +103 -0
  14. package/dist/processors/gridset/commands.js +958 -0
  15. package/dist/processors/gridset/helpers.d.ts +1 -1
  16. package/dist/processors/gridset/helpers.js +5 -3
  17. package/dist/processors/gridset/index.d.ts +45 -0
  18. package/dist/processors/gridset/index.js +153 -0
  19. package/dist/processors/gridset/password.d.ts +11 -0
  20. package/dist/processors/gridset/password.js +37 -0
  21. package/dist/processors/gridset/pluginTypes.d.ts +109 -0
  22. package/dist/processors/gridset/pluginTypes.js +285 -0
  23. package/dist/processors/gridset/resolver.d.ts +14 -1
  24. package/dist/processors/gridset/resolver.js +47 -5
  25. package/dist/processors/gridset/styleHelpers.d.ts +22 -0
  26. package/dist/processors/gridset/styleHelpers.js +35 -1
  27. package/dist/processors/gridset/symbolExtractor.d.ts +121 -0
  28. package/dist/processors/gridset/symbolExtractor.js +362 -0
  29. package/dist/processors/gridset/symbolSearch.d.ts +117 -0
  30. package/dist/processors/gridset/symbolSearch.js +280 -0
  31. package/dist/processors/gridset/symbols.d.ts +199 -0
  32. package/dist/processors/gridset/symbols.js +468 -0
  33. package/dist/processors/gridset/wordlistHelpers.d.ts +2 -2
  34. package/dist/processors/gridset/wordlistHelpers.js +7 -4
  35. package/dist/processors/gridsetProcessor.d.ts +15 -1
  36. package/dist/processors/gridsetProcessor.js +98 -22
  37. package/dist/processors/index.d.ts +10 -1
  38. package/dist/processors/index.js +94 -2
  39. package/dist/processors/obfProcessor.d.ts +7 -0
  40. package/dist/processors/obfProcessor.js +9 -0
  41. package/dist/processors/snapProcessor.d.ts +7 -0
  42. package/dist/processors/snapProcessor.js +9 -0
  43. package/dist/processors/touchchatProcessor.d.ts +7 -0
  44. package/dist/processors/touchchatProcessor.js +9 -0
  45. package/dist/types/aac.d.ts +17 -0
  46. package/dist/utilities/screenshotConverter.d.ts +69 -0
  47. package/dist/utilities/screenshotConverter.js +453 -0
  48. package/dist/validation/baseValidator.d.ts +80 -0
  49. package/dist/validation/baseValidator.js +160 -0
  50. package/dist/validation/gridsetValidator.d.ts +36 -0
  51. package/dist/validation/gridsetValidator.js +288 -0
  52. package/dist/validation/index.d.ts +13 -0
  53. package/dist/validation/index.js +69 -0
  54. package/dist/validation/obfValidator.d.ts +44 -0
  55. package/dist/validation/obfValidator.js +530 -0
  56. package/dist/validation/snapValidator.d.ts +33 -0
  57. package/dist/validation/snapValidator.js +237 -0
  58. package/dist/validation/touchChatValidator.d.ts +33 -0
  59. package/dist/validation/touchChatValidator.js +229 -0
  60. package/dist/validation/validationTypes.d.ts +64 -0
  61. package/dist/validation/validationTypes.js +15 -0
  62. package/examples/README.md +7 -0
  63. package/examples/demo.js +143 -0
  64. package/examples/obf/aboutme.json +376 -0
  65. package/examples/obf/array.json +6 -0
  66. package/examples/obf/hash.json +4 -0
  67. package/examples/obf/links.obz +0 -0
  68. package/examples/obf/simple.obf +53 -0
  69. package/examples/package-lock.json +1326 -0
  70. package/examples/package.json +10 -0
  71. package/examples/styling-example.ts +316 -0
  72. package/examples/translate.js +39 -0
  73. package/examples/translate_demo.js +254 -0
  74. package/examples/typescript-demo.ts +251 -0
  75. package/package.json +3 -1
@@ -0,0 +1,237 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.SnapValidator = void 0;
30
+ /* eslint-disable @typescript-eslint/require-await */
31
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
32
+ const fs = __importStar(require("fs"));
33
+ const path = __importStar(require("path"));
34
+ const xml2js = __importStar(require("xml2js"));
35
+ const adm_zip_1 = __importDefault(require("adm-zip"));
36
+ const baseValidator_1 = require("./baseValidator");
37
+ /**
38
+ * Validator for Snap files (.spb, .sps)
39
+ * Snap files are zipped packages containing XML configuration
40
+ */
41
+ class SnapValidator extends baseValidator_1.BaseValidator {
42
+ constructor() {
43
+ super();
44
+ }
45
+ /**
46
+ * Validate a Snap file from disk
47
+ */
48
+ static async validateFile(filePath) {
49
+ const validator = new SnapValidator();
50
+ const content = fs.readFileSync(filePath);
51
+ const stats = fs.statSync(filePath);
52
+ return validator.validate(content, path.basename(filePath), stats.size);
53
+ }
54
+ /**
55
+ * Check if content is Snap format
56
+ */
57
+ // eslint-disable-next-line @typescript-eslint/require-await
58
+ static async identifyFormat(content, filename) {
59
+ const name = filename.toLowerCase();
60
+ if (name.endsWith('.spb') || name.endsWith('.sps')) {
61
+ return true;
62
+ }
63
+ // Try to parse as ZIP and check for Snap structure
64
+ try {
65
+ const zip = new adm_zip_1.default(content);
66
+ const entries = zip.getEntries();
67
+ // Snap packages typically have settings.xml or similar
68
+ return entries.some((e) => e.entryName.includes('settings') || e.entryName.includes('.xml'));
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ }
74
+ /**
75
+ * Main validation method
76
+ */
77
+ async validate(content, filename, filesize) {
78
+ this.reset();
79
+ await this.add_check('filename', 'file extension', async () => {
80
+ if (!filename.match(/\.(spb|sps)$/)) {
81
+ this.warn('filename should end with .spb or .sps');
82
+ }
83
+ });
84
+ let zip = null;
85
+ let validZip = false;
86
+ await this.add_check('zip', 'valid zip package', async () => {
87
+ try {
88
+ // Ensure content is a Buffer for AdmZip
89
+ const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content);
90
+ zip = new adm_zip_1.default(buffer);
91
+ const entries = zip.getEntries();
92
+ validZip = entries.length > 0;
93
+ }
94
+ catch (e) {
95
+ this.err(`file is not a valid zip package: ${e.message}`, true);
96
+ }
97
+ });
98
+ if (!validZip || !zip) {
99
+ return this.buildResult(filename, filesize, 'snap');
100
+ }
101
+ await this.validateSnapStructure(zip, filename);
102
+ return this.buildResult(filename, filesize, 'snap');
103
+ }
104
+ /**
105
+ * Validate Snap package structure
106
+ */
107
+ async validateSnapStructure(zip, _filename) {
108
+ // Check for required files
109
+ await this.add_check('required_files', 'required package files', async () => {
110
+ const entries = zip.getEntries();
111
+ const entryNames = entries.map((e) => e.entryName);
112
+ // Look for common Snap files
113
+ const hasSettings = entryNames.some((n) => n.toLowerCase().includes('settings'));
114
+ const hasXml = entryNames.some((n) => n.toLowerCase().endsWith('.xml'));
115
+ if (!hasSettings && !hasXml) {
116
+ this.err('Snap package must contain settings.xml or similar configuration file');
117
+ }
118
+ if (entries.length === 0) {
119
+ this.err('Snap package is empty');
120
+ }
121
+ });
122
+ // Try to parse and validate the main settings file
123
+ const settingsEntry = zip
124
+ .getEntries()
125
+ .find((e) => e.entryName.toLowerCase().includes('settings'));
126
+ if (settingsEntry) {
127
+ await this.validateSettingsFile(zip, settingsEntry);
128
+ }
129
+ // Check for pages
130
+ const pageEntries = zip.getEntries().filter((e) => e.entryName.toLowerCase().includes('page'));
131
+ await this.add_check('pages', 'pages in package', async () => {
132
+ if (pageEntries.length === 0) {
133
+ this.warn('Snap package should contain at least one page file');
134
+ }
135
+ });
136
+ // Validate a sample of pages
137
+ const samplePages = pageEntries.slice(0, 5); // Limit to first 5 pages
138
+ for (let i = 0; i < samplePages.length; i++) {
139
+ await this.validatePageFile(zip, samplePages[i], i);
140
+ }
141
+ // Check for images
142
+ const imageEntries = zip
143
+ .getEntries()
144
+ .filter((e) => e.entryName.toLowerCase().match(/\.(png|jpg|jpeg|gif|bmp)$/i));
145
+ await this.add_check('images', 'image files', async () => {
146
+ if (imageEntries.length === 0) {
147
+ this.warn('Snap package should contain image files for buttons');
148
+ }
149
+ });
150
+ // Check for audio files
151
+ const audioEntries = zip
152
+ .getEntries()
153
+ .filter((e) => e.entryName.toLowerCase().match(/\.(wav|mp3|m4a|ogg)$/i));
154
+ await this.add_check('audio', 'audio files', async () => {
155
+ // Audio files are optional, so just warn if missing
156
+ if (audioEntries.length === 0) {
157
+ // This is informational, not a warning
158
+ }
159
+ });
160
+ // Check for unexpected files
161
+ await this.add_check('unexpected_files', 'unexpected file types', async () => {
162
+ const entries = zip.getEntries();
163
+ const unexpectedFiles = entries.filter((e) => {
164
+ const name = e.entryName.toLowerCase();
165
+ // Skip common system files and directories
166
+ if (name.startsWith('__macosx') || name.startsWith('.ds_store')) {
167
+ return false;
168
+ }
169
+ // Allowed file types
170
+ return !name.match(/\.(xml|png|jpg|jpeg|gif|bmp|wav|mp3|m4a|ogg|json)$/i);
171
+ });
172
+ if (unexpectedFiles.length > 0) {
173
+ const unexpectedNames = unexpectedFiles.map((f) => f.entryName).slice(0, 5);
174
+ this.warn(`Package contains unexpected file types: ${unexpectedNames.join(', ')}`);
175
+ }
176
+ });
177
+ }
178
+ /**
179
+ * Validate the main settings file
180
+ */
181
+ async validateSettingsFile(zip, entry) {
182
+ await this.add_check('settings_format', 'settings file format', async () => {
183
+ try {
184
+ const content = zip.readAsText(entry.entryName);
185
+ const parser = new xml2js.Parser();
186
+ const xml = await parser.parseStringPromise(content);
187
+ // Check for expected root element
188
+ if (!xml.settings && !xml.Settings && !xml.page && !xml.Page) {
189
+ this.warn('settings file does not contain expected root element');
190
+ }
191
+ // Check for required settings attributes if present
192
+ const settings = xml.settings || xml.Settings;
193
+ if (settings) {
194
+ const id = settings.$?.id || settings.$?.Id;
195
+ const name = settings.$?.name || settings.$?.Name;
196
+ if (!id && !name) {
197
+ this.warn('settings should have an id or name attribute');
198
+ }
199
+ }
200
+ }
201
+ catch (e) {
202
+ this.err(`Failed to parse settings file: ${e.message}`);
203
+ }
204
+ });
205
+ }
206
+ /**
207
+ * Validate a page file
208
+ */
209
+ async validatePageFile(zip, entry, index) {
210
+ await this.add_check(`page[${index}]`, `page file ${index}: ${entry.entryName}`, async () => {
211
+ try {
212
+ const content = zip.readAsText(entry.entryName);
213
+ const parser = new xml2js.Parser();
214
+ const xml = await parser.parseStringPromise(content);
215
+ const page = xml.page || xml.Page;
216
+ if (!page) {
217
+ this.err(`Page file ${entry.entryName} does not contain a page element`);
218
+ return;
219
+ }
220
+ // Check page attributes
221
+ const pageId = page.$?.id || page.$?.Id;
222
+ if (!pageId) {
223
+ this.warn(`Page ${entry.entryName} is missing an id attribute`);
224
+ }
225
+ // Check for cells/buttons
226
+ const cells = page.cells || page.Cells || page.button || page.Button;
227
+ if (!cells || (Array.isArray(cells) && cells.length === 0)) {
228
+ this.warn(`Page ${entry.entryName} has no cells or buttons`);
229
+ }
230
+ }
231
+ catch (e) {
232
+ this.err(`Failed to parse page file ${entry.entryName}: ${e.message}`);
233
+ }
234
+ });
235
+ }
236
+ }
237
+ exports.SnapValidator = SnapValidator;
@@ -0,0 +1,33 @@
1
+ import { BaseValidator } from './baseValidator';
2
+ import { ValidationResult } from './validationTypes';
3
+ /**
4
+ * Validator for TouchChat files (.ce)
5
+ * TouchChat files are XML-based
6
+ */
7
+ export declare class TouchChatValidator extends BaseValidator {
8
+ constructor();
9
+ /**
10
+ * Validate a TouchChat file from disk
11
+ */
12
+ static validateFile(filePath: string): Promise<ValidationResult>;
13
+ /**
14
+ * Check if content is TouchChat format
15
+ */
16
+ static identifyFormat(content: any, filename: string): Promise<boolean>;
17
+ /**
18
+ * Main validation method
19
+ */
20
+ validate(content: Buffer | Uint8Array, filename: string, filesize: number): Promise<ValidationResult>;
21
+ /**
22
+ * Validate TouchChat structure
23
+ */
24
+ private validateTouchChatStructure;
25
+ /**
26
+ * Validate a single page
27
+ */
28
+ private validatePage;
29
+ /**
30
+ * Validate a single button
31
+ */
32
+ private validateButton;
33
+ }
@@ -0,0 +1,229 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.TouchChatValidator = void 0;
27
+ /* eslint-disable @typescript-eslint/require-await */
28
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
29
+ /* eslint-disable @typescript-eslint/no-unsafe-return */
30
+ const fs = __importStar(require("fs"));
31
+ const path = __importStar(require("path"));
32
+ const xml2js = __importStar(require("xml2js"));
33
+ const baseValidator_1 = require("./baseValidator");
34
+ /**
35
+ * Validator for TouchChat files (.ce)
36
+ * TouchChat files are XML-based
37
+ */
38
+ class TouchChatValidator extends baseValidator_1.BaseValidator {
39
+ constructor() {
40
+ super();
41
+ }
42
+ /**
43
+ * Validate a TouchChat file from disk
44
+ */
45
+ static async validateFile(filePath) {
46
+ const validator = new TouchChatValidator();
47
+ const content = fs.readFileSync(filePath);
48
+ const stats = fs.statSync(filePath);
49
+ return validator.validate(content, path.basename(filePath), stats.size);
50
+ }
51
+ /**
52
+ * Check if content is TouchChat format
53
+ */
54
+ static async identifyFormat(content, filename) {
55
+ const name = filename.toLowerCase();
56
+ if (name.endsWith('.ce')) {
57
+ return true;
58
+ }
59
+ // Try to parse as XML and check for TouchChat structure
60
+ try {
61
+ const contentStr = Buffer.isBuffer(content) ? content.toString('utf-8') : content;
62
+ const parser = new xml2js.Parser();
63
+ const result = await parser.parseStringPromise(contentStr);
64
+ // TouchChat files typically have specific structure
65
+ return result && (result.PageSet || result.Pageset || result.page || result.Page);
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ }
71
+ /**
72
+ * Main validation method
73
+ */
74
+ async validate(content, filename, filesize) {
75
+ this.reset();
76
+ await this.add_check('filename', 'file extension', async () => {
77
+ if (!filename.match(/\.ce$/i)) {
78
+ this.warn('filename should end with .ce');
79
+ }
80
+ });
81
+ let xmlObj = null;
82
+ await this.add_check('xml_parse', 'valid XML', async () => {
83
+ try {
84
+ const parser = new xml2js.Parser();
85
+ const contentStr = content.toString('utf-8');
86
+ xmlObj = await parser.parseStringPromise(contentStr);
87
+ }
88
+ catch (e) {
89
+ this.err(`Failed to parse XML: ${e.message}`, true);
90
+ }
91
+ });
92
+ if (!xmlObj) {
93
+ return this.buildResult(filename, filesize, 'touchchat');
94
+ }
95
+ await this.add_check('xml_structure', 'TouchChat root element', async () => {
96
+ // TouchChat can have different root elements
97
+ const hasValidRoot = xmlObj.PageSet ||
98
+ xmlObj.Pageset ||
99
+ xmlObj.page ||
100
+ xmlObj.Page ||
101
+ xmlObj.pages ||
102
+ xmlObj.Pages;
103
+ if (!hasValidRoot) {
104
+ this.err('file does not contain a recognized TouchChat structure');
105
+ }
106
+ });
107
+ const root = xmlObj.PageSet ||
108
+ xmlObj.Pageset ||
109
+ xmlObj.page ||
110
+ xmlObj.Page ||
111
+ xmlObj.pages ||
112
+ xmlObj.Pages;
113
+ if (root) {
114
+ await this.validateTouchChatStructure(root);
115
+ }
116
+ return this.buildResult(filename, filesize, 'touchchat');
117
+ }
118
+ /**
119
+ * Validate TouchChat structure
120
+ */
121
+ async validateTouchChatStructure(root) {
122
+ // Check for ID
123
+ await this.add_check('root_id', 'root element ID', async () => {
124
+ const id = root.$?.id || root.$?.Id;
125
+ if (!id) {
126
+ this.warn('root element should have an id attribute');
127
+ }
128
+ });
129
+ // Check for name
130
+ await this.add_check('root_name', 'root element name', async () => {
131
+ const name = root.$?.name || root.$?.Name || root.name?.[0];
132
+ if (!name) {
133
+ this.warn('root element should have a name');
134
+ }
135
+ });
136
+ // Check for pages
137
+ await this.add_check('pages', 'pages collection', async () => {
138
+ const pages = root.page || root.Page || root.pages || root.Pages;
139
+ if (!pages) {
140
+ this.err('TouchChat file must contain pages');
141
+ }
142
+ else if (!Array.isArray(pages) || pages.length === 0) {
143
+ this.err('TouchChat file must contain at least one page');
144
+ }
145
+ });
146
+ // Validate individual pages
147
+ const pages = root.page || root.Page || root.pages || root.Pages;
148
+ if (pages && Array.isArray(pages)) {
149
+ await this.add_check('page_count', 'page count', async () => {
150
+ if (pages.length === 0) {
151
+ this.err('Must contain at least one page');
152
+ }
153
+ });
154
+ // Sample first few pages
155
+ const sampleSize = Math.min(pages.length, 5);
156
+ for (let i = 0; i < sampleSize; i++) {
157
+ await this.validatePage(pages[i], i);
158
+ }
159
+ }
160
+ }
161
+ /**
162
+ * Validate a single page
163
+ */
164
+ async validatePage(page, index) {
165
+ await this.add_check(`page[${index}]_id`, `page ${index} ID`, async () => {
166
+ const id = page.$?.id || page.$?.Id;
167
+ if (!id) {
168
+ this.warn(`page ${index} is missing an id attribute`);
169
+ }
170
+ });
171
+ await this.add_check(`page[${index}]_name`, `page ${index} name`, async () => {
172
+ const name = page.$?.name || page.$?.Name || page.name?.[0];
173
+ if (!name) {
174
+ this.warn(`page ${index} should have a name`);
175
+ }
176
+ });
177
+ // Check for buttons/items
178
+ await this.add_check(`page[${index}]_buttons`, `page ${index} buttons`, async () => {
179
+ const buttons = page.button || page.Button || page.item || page.Item;
180
+ if (!buttons) {
181
+ this.warn(`page ${index} has no buttons/items`);
182
+ }
183
+ else if (Array.isArray(buttons) && buttons.length === 0) {
184
+ this.warn(`page ${index} should contain at least one button`);
185
+ }
186
+ });
187
+ // Validate button references
188
+ const buttons = page.button || page.Button || page.item || page.Item;
189
+ if (buttons && Array.isArray(buttons)) {
190
+ const sampleSize = Math.min(buttons.length, 3);
191
+ for (let i = 0; i < sampleSize; i++) {
192
+ await this.validateButton(buttons[i], index, i);
193
+ }
194
+ }
195
+ }
196
+ /**
197
+ * Validate a single button
198
+ */
199
+ async validateButton(button, pageIdx, buttonIdx) {
200
+ await this.add_check(`page[${pageIdx}]_button[${buttonIdx}]_label`, `button label`, async () => {
201
+ const label = button.$?.label || button.$?.Label || button.label?.[0];
202
+ if (!label) {
203
+ this.warn(`button ${buttonIdx} on page ${pageIdx} should have a label`);
204
+ }
205
+ });
206
+ await this.add_check(`page[${pageIdx}]_button[${buttonIdx}]_vocalization`, `button vocalization`, async () => {
207
+ const vocalization = button.$?.vocalization || button.$?.Vocalization || button.vocalization?.[0];
208
+ if (!vocalization) {
209
+ // Vocalization is optional, so just info
210
+ }
211
+ });
212
+ // Check for image reference
213
+ await this.add_check(`page[${pageIdx}]_button[${buttonIdx}]_image`, `button image`, async () => {
214
+ const image = button.$?.image || button.$?.Image || button.img?.[0];
215
+ if (!image) {
216
+ this.warn(`button ${buttonIdx} on page ${pageIdx} should have an image reference`);
217
+ }
218
+ });
219
+ // Check for link/action
220
+ await this.add_check(`page[${pageIdx}]_button[${buttonIdx}]_action`, `button action`, async () => {
221
+ const link = button.$?.link || button.$?.Link;
222
+ const action = button.$?.action || button.$?.Action;
223
+ if (!link && !action) {
224
+ // Not all buttons need actions, they can just speak
225
+ }
226
+ });
227
+ }
228
+ }
229
+ exports.TouchChatValidator = TouchChatValidator;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Custom error class for validation errors
3
+ * Can be marked as a blocker to stop validation immediately
4
+ */
5
+ export declare class ValidationError extends Error {
6
+ blocker: boolean;
7
+ constructor(message: string, blocker?: boolean);
8
+ }
9
+ /**
10
+ * Represents a single validation check with its result
11
+ */
12
+ export interface ValidationCheck {
13
+ /** Type/category of the check (e.g., 'json_parse', 'grid', 'buttons') */
14
+ type: string;
15
+ /** Human-readable description of what is being checked */
16
+ description: string;
17
+ /** Whether the check passed */
18
+ valid: boolean;
19
+ /** Error message if the check failed */
20
+ error?: string;
21
+ /** Non-blocking warnings */
22
+ warnings?: string[];
23
+ }
24
+ /**
25
+ * Complete validation result for a file
26
+ */
27
+ export interface ValidationResult {
28
+ /** Name of the file that was validated */
29
+ filename: string;
30
+ /** Size of the file in bytes */
31
+ filesize: number;
32
+ /** Format identifier (e.g., 'obf', 'gridset', 'snap') */
33
+ format: string;
34
+ /** Overall validity - true if no errors */
35
+ valid: boolean;
36
+ /** Total number of errors found */
37
+ errors: number;
38
+ /** Total number of warnings found */
39
+ warnings: number;
40
+ /** Array of individual validation checks */
41
+ results: ValidationCheck[];
42
+ /** Nested validation results (e.g., boards within an OBZ) */
43
+ sub_results?: ValidationResult[];
44
+ }
45
+ /**
46
+ * Options for validation behavior
47
+ */
48
+ export interface ValidationOptions {
49
+ /** Whether to include warnings in validation (default: true) */
50
+ includeWarnings?: boolean;
51
+ /** Whether to stop on first blocker error (default: true) */
52
+ stopOnBlocker?: boolean;
53
+ /** Custom validation rules to apply */
54
+ customRules?: ValidationRule[];
55
+ }
56
+ /**
57
+ * Custom validation rule that can be added
58
+ */
59
+ export interface ValidationRule {
60
+ type: string;
61
+ description: string;
62
+ check: (data: any) => Promise<boolean> | boolean;
63
+ errorMessage?: string;
64
+ }
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ValidationError = void 0;
4
+ /**
5
+ * Custom error class for validation errors
6
+ * Can be marked as a blocker to stop validation immediately
7
+ */
8
+ class ValidationError extends Error {
9
+ constructor(message, blocker = false) {
10
+ super(message);
11
+ this.name = 'ValidationError';
12
+ this.blocker = blocker;
13
+ }
14
+ }
15
+ exports.ValidationError = ValidationError;
@@ -19,6 +19,13 @@ This directory contains example AAC pagesets in various formats used for testing
19
19
  - **example.obf** - OBF pageset (JSON-based)
20
20
  - **example.obz** - OBZ pageset (compressed)
21
21
 
22
+ **obf/** - Directory containing validation test samples from the obf-node project:
23
+ - **simple.obf** - Simple, valid OBF file for basic validation tests
24
+ - **aboutme.json** - Invalid OBF (missing locale field) for error testing
25
+ - **hash.json** - Non-OBF JSON structure for format detection tests
26
+ - **array.json** - JSON array (not object) for structure validation tests
27
+ - **links.obz** - OBZ package with links for zip archive validation
28
+
22
29
  ### Asterics Grid Format (.grd)
23
30
  - **example.grd** - Asterics Grid pageset
24
31
  - **example2.grd** - Alternative Asterics Grid pageset