@willwade/aac-processors 0.0.9 → 0.0.10

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 (56) 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 +3 -0
  5. package/dist/core/fileProcessor.js +1 -0
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.js +3 -0
  8. package/dist/optional/symbolTools.js +4 -2
  9. package/dist/processors/gridset/helpers.d.ts +1 -1
  10. package/dist/processors/gridset/helpers.js +5 -3
  11. package/dist/processors/gridset/password.d.ts +11 -0
  12. package/dist/processors/gridset/password.js +37 -0
  13. package/dist/processors/gridset/resolver.d.ts +1 -1
  14. package/dist/processors/gridset/resolver.js +8 -4
  15. package/dist/processors/gridset/wordlistHelpers.d.ts +2 -2
  16. package/dist/processors/gridset/wordlistHelpers.js +7 -4
  17. package/dist/processors/gridsetProcessor.d.ts +15 -1
  18. package/dist/processors/gridsetProcessor.js +64 -22
  19. package/dist/processors/index.d.ts +1 -0
  20. package/dist/processors/index.js +5 -2
  21. package/dist/processors/obfProcessor.d.ts +7 -0
  22. package/dist/processors/obfProcessor.js +9 -0
  23. package/dist/processors/snapProcessor.d.ts +7 -0
  24. package/dist/processors/snapProcessor.js +9 -0
  25. package/dist/processors/touchchatProcessor.d.ts +7 -0
  26. package/dist/processors/touchchatProcessor.js +9 -0
  27. package/dist/utilities/screenshotConverter.d.ts +69 -0
  28. package/dist/utilities/screenshotConverter.js +453 -0
  29. package/dist/validation/baseValidator.d.ts +80 -0
  30. package/dist/validation/baseValidator.js +160 -0
  31. package/dist/validation/gridsetValidator.d.ts +36 -0
  32. package/dist/validation/gridsetValidator.js +288 -0
  33. package/dist/validation/index.d.ts +13 -0
  34. package/dist/validation/index.js +69 -0
  35. package/dist/validation/obfValidator.d.ts +44 -0
  36. package/dist/validation/obfValidator.js +530 -0
  37. package/dist/validation/snapValidator.d.ts +33 -0
  38. package/dist/validation/snapValidator.js +237 -0
  39. package/dist/validation/touchChatValidator.d.ts +33 -0
  40. package/dist/validation/touchChatValidator.js +229 -0
  41. package/dist/validation/validationTypes.d.ts +64 -0
  42. package/dist/validation/validationTypes.js +15 -0
  43. package/examples/README.md +7 -0
  44. package/examples/demo.js +143 -0
  45. package/examples/obf/aboutme.json +376 -0
  46. package/examples/obf/array.json +6 -0
  47. package/examples/obf/hash.json +4 -0
  48. package/examples/obf/links.obz +0 -0
  49. package/examples/obf/simple.obf +53 -0
  50. package/examples/package-lock.json +1326 -0
  51. package/examples/package.json +10 -0
  52. package/examples/styling-example.ts +316 -0
  53. package/examples/translate.js +39 -0
  54. package/examples/translate_demo.js +254 -0
  55. package/examples/typescript-demo.ts +251 -0
  56. package/package.json +3 -1
@@ -0,0 +1,453 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ScreenshotConverter = void 0;
7
+ const treeStructure_1 = require("../core/treeStructure");
8
+ const path_1 = __importDefault(require("path"));
9
+ class ScreenshotConverter {
10
+ /**
11
+ * Parse filename to extract page hierarchy and names
12
+ * Examples:
13
+ * - "Home.png" → pageName: "Home", parentPath: ""
14
+ * - "Home->Fragen.png" → pageName: "Fragen", parentPath: "Home"
15
+ * - "Home->Settings->Profile.jpg" → pageName: "Profile", parentPath: "Home->Settings"
16
+ */
17
+ static parseFilename(filename, delimiter = '->') {
18
+ const baseName = path_1.default.parse(filename).name;
19
+ const parts = baseName.split(delimiter).map((part) => part.trim());
20
+ return {
21
+ pageName: parts[parts.length - 1] || baseName,
22
+ parentPath: parts.slice(0, -1).join(delimiter),
23
+ };
24
+ }
25
+ /**
26
+ * Build page hierarchy from an array of screenshots
27
+ */
28
+ static buildPageHierarchy(screenshots) {
29
+ const hierarchy = {};
30
+ const delimiter = this.defaultOptions.filenameDelimiter || '->';
31
+ // First pass: parse all filenames
32
+ screenshots.forEach((screenshot, index) => {
33
+ const { pageName, parentPath } = this.parseFilename(screenshot.filename, delimiter);
34
+ screenshot.pageName = pageName;
35
+ screenshot.parentPath = parentPath;
36
+ const pageId = `page_${index}`;
37
+ hierarchy[pageId] = {
38
+ page: screenshot,
39
+ children: [],
40
+ parent: undefined,
41
+ };
42
+ });
43
+ // Second pass: establish parent-child relationships
44
+ Object.entries(hierarchy).forEach(([pageId, entry]) => {
45
+ const parentPath = entry.page.parentPath;
46
+ if (parentPath) {
47
+ // Find parent by matching the full path
48
+ const parent = Object.values(hierarchy).find((h) => h.page.pageName === parentPath.split(delimiter).pop());
49
+ if (parent) {
50
+ entry.parent = Object.keys(hierarchy).find((key) => hierarchy[key] === parent);
51
+ parent.children.push(pageId);
52
+ }
53
+ }
54
+ });
55
+ return hierarchy;
56
+ }
57
+ static parseOCRText(ocrResult) {
58
+ const lines = ocrResult.split('\n').filter((line) => line.trim());
59
+ const cells = [];
60
+ const categories = new Set();
61
+ // Skip header metadata
62
+ const contentStart = lines.findIndex((line) => line.includes('ich möchte') && !line.includes('ich möchte ich'));
63
+ if (contentStart === -1) {
64
+ // Try another approach if the first pattern doesn't match
65
+ const gridStart = lines.findIndex((line) => line.includes('ich möchte') && line.split(/\s+/).length > 2);
66
+ if (gridStart === -1)
67
+ return { rows: 6, cols: 11, cells: [], categories: [] };
68
+ }
69
+ // Find the line with the grid content (usually has tab-separated values)
70
+ const gridLineIndex = lines.findIndex((line) => line.includes('ich möchte') && line.includes('\t') && line.split(/\s+/).length > 5);
71
+ let rows = 6;
72
+ let cols = 11;
73
+ // If we found a properly formatted grid line
74
+ if (gridLineIndex !== -1) {
75
+ const gridLine = lines[gridLineIndex];
76
+ // Split by tabs to get individual cell values
77
+ const tokens = gridLine
78
+ .split('\t')
79
+ .map((t) => t.trim())
80
+ .filter((t) => t);
81
+ cols = Math.max(tokens.length, cols);
82
+ // Create first row from the main grid line
83
+ tokens.forEach((token, col) => {
84
+ const isCategory = this.isCategoryToken(token);
85
+ const isNavigation = this.isNavigationToken(token);
86
+ const isEmpty = !token || token === '...' || token === '';
87
+ if (isCategory)
88
+ categories.add(token);
89
+ if (!isEmpty) {
90
+ cells.push({
91
+ text: token,
92
+ row: 0,
93
+ col,
94
+ isCategory,
95
+ isNavigation,
96
+ isEmpty,
97
+ });
98
+ }
99
+ });
100
+ // Process subsequent lines
101
+ let currentRow = 1;
102
+ for (let i = gridLineIndex + 1; i < lines.length; i++) {
103
+ const line = lines[i].trim();
104
+ if (!line)
105
+ continue;
106
+ // Skip lines that look like headers or metadata
107
+ if (line.match(/^\d+:\d+/) || line.match(/[A-Z][a-z]{2},\s+\d+/) || line.includes('%'))
108
+ continue;
109
+ // Skip duplicate "ich möchte" at start
110
+ if (line === 'ich möchte' && currentRow === 1) {
111
+ currentRow = 0;
112
+ continue;
113
+ }
114
+ const tokens = line
115
+ .split('\t')
116
+ .map((t) => t.trim())
117
+ .filter((t) => t);
118
+ tokens.forEach((token, col) => {
119
+ const isCategory = this.isCategoryToken(token);
120
+ const isNavigation = this.isNavigationToken(token);
121
+ const isEmpty = !token || token === '...' || token === '';
122
+ if (isCategory)
123
+ categories.add(token);
124
+ if (!isEmpty) {
125
+ cells.push({
126
+ text: token,
127
+ row: currentRow,
128
+ col,
129
+ isCategory,
130
+ isNavigation,
131
+ isEmpty,
132
+ });
133
+ }
134
+ });
135
+ currentRow++;
136
+ if (currentRow >= rows)
137
+ break;
138
+ }
139
+ }
140
+ else {
141
+ // Fallback: simple whitespace parsing for unstructured OCR
142
+ let currentRow = 0;
143
+ lines.forEach((line, _lineIndex) => {
144
+ if (!line.trim())
145
+ return;
146
+ // Skip metadata
147
+ if (line.includes('%') || line.match(/\d+:\d+/) || line.match(/[A-Z][a-z]{2},\s+\d+/))
148
+ return;
149
+ const tokens = line.trim().split(/\s+/);
150
+ tokens.forEach((token, tokenIndex) => {
151
+ if (tokenIndex >= cols)
152
+ return; // Skip if beyond expected columns
153
+ const isCategory = this.isCategoryToken(token);
154
+ const isNavigation = this.isNavigationToken(token);
155
+ const isEmpty = !token || token.trim() === '' || token === '...';
156
+ if (isCategory)
157
+ categories.add(token);
158
+ if (!isEmpty) {
159
+ cells.push({
160
+ text: token,
161
+ row: currentRow,
162
+ col: tokenIndex,
163
+ isCategory,
164
+ isNavigation,
165
+ isEmpty,
166
+ });
167
+ }
168
+ });
169
+ currentRow++;
170
+ });
171
+ }
172
+ // Auto-detect actual grid dimensions
173
+ if (cells.length > 0) {
174
+ rows = Math.max(...cells.map((c) => c.row)) + 1;
175
+ cols = Math.max(...cells.map((c) => c.col)) + 1;
176
+ }
177
+ return {
178
+ rows,
179
+ cols,
180
+ cells,
181
+ categories: Array.from(categories),
182
+ };
183
+ }
184
+ static isCategoryToken(token) {
185
+ const knownCategories = [
186
+ // English categories
187
+ 'Questions',
188
+ 'Meetings',
189
+ 'Praise',
190
+ 'Complaints',
191
+ 'Phrases',
192
+ 'Conversations',
193
+ 'Verbs',
194
+ 'People',
195
+ 'Messages',
196
+ 'Properties',
197
+ 'Feelings',
198
+ 'Actions',
199
+ 'Activities',
200
+ 'Food',
201
+ 'Drink',
202
+ 'Colors',
203
+ 'Shapes',
204
+ 'Settings',
205
+ 'Home',
206
+ 'Back',
207
+ 'Next',
208
+ 'Menu',
209
+ // German categories
210
+ 'Fragen',
211
+ 'Treffen',
212
+ 'Lob',
213
+ 'Beschwerde',
214
+ 'Sprüche',
215
+ 'Gespräche',
216
+ 'Verben',
217
+ 'Leute',
218
+ 'Mitteilungen',
219
+ 'Eigenschaften',
220
+ 'Gefühle',
221
+ 'Spielen',
222
+ 'Multimedia',
223
+ 'Essen',
224
+ 'Trinken',
225
+ 'Farben/Formen',
226
+ ];
227
+ // Check for known categories
228
+ if (knownCategories.includes(token)) {
229
+ return true;
230
+ }
231
+ // Check for common category patterns
232
+ const categoryPatterns = [
233
+ /.*Questions?$/,
234
+ /.*Category.*/,
235
+ /.*Menu.*/,
236
+ /.*Settings?$/,
237
+ /.*Options?$/,
238
+ /谈话/i, // Chinese
239
+ /質問/i, // Japanese
240
+ /preguntas/i, // Spanish
241
+ ];
242
+ return categoryPatterns.some((pattern) => pattern.test(token));
243
+ }
244
+ static isNavigationToken(token) {
245
+ const navTokens = [
246
+ // English
247
+ 'Home',
248
+ 'Back',
249
+ 'Next',
250
+ 'Previous',
251
+ 'Menu',
252
+ 'Settings',
253
+ 'Exit',
254
+ 'Close',
255
+ 'OK',
256
+ 'Cancel',
257
+ 'Yes',
258
+ 'No',
259
+ 'Help',
260
+ 'Search',
261
+ // German
262
+ 'Home',
263
+ 'Zurück',
264
+ 'Weiter',
265
+ 'Menü',
266
+ 'Einstellungen',
267
+ 'Beenden',
268
+ 'Schließen',
269
+ 'Hilfe',
270
+ 'Suche',
271
+ // Navigation indicators
272
+ '←',
273
+ '→',
274
+ '↑',
275
+ '↓',
276
+ '◀',
277
+ '▶',
278
+ '▲',
279
+ '▼',
280
+ ];
281
+ return (navTokens.includes(token) || token === '←' || token === '→' || token === '↑' || token === '↓');
282
+ }
283
+ static convertToAACPage(screenshotPage, pageHierarchy, options) {
284
+ const opts = {
285
+ ...this.defaultOptions,
286
+ ...options,
287
+ };
288
+ const buttons = [];
289
+ // Convert cells to AAC buttons
290
+ screenshotPage.grid.cells.forEach((cell) => {
291
+ if (cell.isEmpty && !opts.includeEmptyCells)
292
+ return;
293
+ const button = new treeStructure_1.AACButton({
294
+ id: `cell_${cell.row}_${cell.col}`,
295
+ label: cell.text,
296
+ message: cell.text,
297
+ style: {
298
+ backgroundColor: cell.isCategory ? '#4CAF50' : cell.isNavigation ? '#2196F3' : '#FFFFFF',
299
+ fontColor: cell.isCategory || cell.isNavigation ? '#FFFFFF' : '#000000',
300
+ borderColor: '#CCCCCC',
301
+ borderWidth: 1,
302
+ },
303
+ semanticAction: this.createSemanticAction(cell, screenshotPage, pageHierarchy, opts),
304
+ x: cell.col,
305
+ y: cell.row,
306
+ });
307
+ buttons.push(button);
308
+ });
309
+ return new treeStructure_1.AACPage({
310
+ id: screenshotPage.pageName || 'screenshot_page',
311
+ name: screenshotPage.pageTitle || screenshotPage.pageName || 'Screenshot Page',
312
+ buttons,
313
+ grid: {
314
+ columns: screenshotPage.grid.cols,
315
+ rows: screenshotPage.grid.rows,
316
+ },
317
+ style: {
318
+ backgroundColor: '#F5F5F5',
319
+ },
320
+ parentId: null,
321
+ });
322
+ }
323
+ static createSemanticAction(cell, screenshotPage, pageHierarchy, options) {
324
+ if (cell.isEmpty)
325
+ return undefined;
326
+ const _opts = {
327
+ ...this.defaultOptions,
328
+ ...options,
329
+ };
330
+ if (cell.isCategory) {
331
+ // Try to find target page in hierarchy based on category name
332
+ let targetId = `category_${cell.text.toLowerCase().replace(/\s+/g, '_')}`;
333
+ if (pageHierarchy) {
334
+ // Look for a page that matches this category
335
+ const matchingPage = Object.values(pageHierarchy).find((h) => h.page.pageName?.toLowerCase() === cell.text.toLowerCase());
336
+ if (matchingPage) {
337
+ targetId = matchingPage.page.pageName || targetId;
338
+ }
339
+ }
340
+ return {
341
+ category: treeStructure_1.AACSemanticCategory.NAVIGATION,
342
+ intent: treeStructure_1.AACSemanticIntent.NAVIGATE_TO,
343
+ targetId,
344
+ parameters: { category: cell.text },
345
+ };
346
+ }
347
+ if (cell.isNavigation) {
348
+ const text = cell.text.toLowerCase();
349
+ // Home navigation
350
+ if (text === 'home' || text === '⌂') {
351
+ return {
352
+ category: treeStructure_1.AACSemanticCategory.NAVIGATION,
353
+ intent: treeStructure_1.AACSemanticIntent.GO_HOME,
354
+ };
355
+ }
356
+ // Back navigation
357
+ if (text === 'back' || text === 'zurück' || text === '←' || text === '◀') {
358
+ return {
359
+ category: treeStructure_1.AACSemanticCategory.NAVIGATION,
360
+ intent: treeStructure_1.AACSemanticIntent.GO_BACK,
361
+ };
362
+ }
363
+ // Next/forward navigation
364
+ if (text === 'next' || text === 'weiter' || text === '→' || text === '▶') {
365
+ // If we have hierarchy, navigate to parent
366
+ if (pageHierarchy && screenshotPage.parentPath) {
367
+ const parentId = Object.keys(pageHierarchy).find((key) => pageHierarchy[key].page.pageName === screenshotPage.parentPath?.split('->').pop());
368
+ if (parentId) {
369
+ return {
370
+ category: treeStructure_1.AACSemanticCategory.NAVIGATION,
371
+ intent: treeStructure_1.AACSemanticIntent.NAVIGATE_TO,
372
+ targetId: parentId,
373
+ };
374
+ }
375
+ }
376
+ return {
377
+ category: treeStructure_1.AACSemanticCategory.NAVIGATION,
378
+ intent: treeStructure_1.AACSemanticIntent.NAVIGATE_TO,
379
+ targetId: 'next_page',
380
+ parameters: { direction: 'next' },
381
+ };
382
+ }
383
+ // Menu navigation
384
+ if (text === 'menu' || text === 'menü') {
385
+ return {
386
+ category: treeStructure_1.AACSemanticCategory.NAVIGATION,
387
+ intent: treeStructure_1.AACSemanticIntent.NAVIGATE_TO,
388
+ targetId: 'main_menu',
389
+ };
390
+ }
391
+ }
392
+ // Default to speaking the text
393
+ return {
394
+ category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
395
+ intent: treeStructure_1.AACSemanticIntent.SPEAK_IMMEDIATE,
396
+ text: cell.text,
397
+ richText: {
398
+ text: cell.text,
399
+ },
400
+ };
401
+ }
402
+ static convertToAACTree(screenshotPages, options) {
403
+ const opts = { ...this.defaultOptions, ...options };
404
+ const tree = new treeStructure_1.AACTree();
405
+ // Build page hierarchy
406
+ const pageHierarchy = this.buildPageHierarchy(screenshotPages);
407
+ // Set metadata on tree
408
+ tree.version = '1.0';
409
+ tree.metadata = {
410
+ name: 'Screenshot Conversion',
411
+ author: 'AAC Processors',
412
+ description: 'Converted from screenshot images',
413
+ language: opts.language,
414
+ };
415
+ // Convert each screenshot page to AAC page
416
+ screenshotPages.forEach((screenshotPage, index) => {
417
+ const page = this.convertToAACPage(screenshotPage, pageHierarchy, opts);
418
+ // Ensure unique ID by using page name if available
419
+ if (screenshotPage.pageName) {
420
+ page.id = this.sanitizePageId(screenshotPage.pageName);
421
+ }
422
+ else {
423
+ page.id = `screenshot_page_${index}`;
424
+ }
425
+ tree.addPage(page);
426
+ });
427
+ // Set root page to the one with no parent
428
+ const rootPage = Object.entries(pageHierarchy).find(([_, entry]) => !entry.parent);
429
+ if (rootPage) {
430
+ const rootPageId = this.sanitizePageId(rootPage[1].page.pageName || 'home');
431
+ if (tree.pages[rootPageId]) {
432
+ tree.rootId = rootPageId;
433
+ }
434
+ }
435
+ return tree;
436
+ }
437
+ static sanitizePageId(pageName) {
438
+ return pageName
439
+ .toLowerCase()
440
+ .replace(/[^a-z0-9]/g, '_')
441
+ .replace(/_+/g, '_')
442
+ .replace(/^_|_$/g, '');
443
+ }
444
+ }
445
+ exports.ScreenshotConverter = ScreenshotConverter;
446
+ ScreenshotConverter.defaultOptions = {
447
+ includeEmptyCells: false,
448
+ generateIds: true,
449
+ targetPlatform: 'grid3',
450
+ language: 'en',
451
+ fallbackCategory: 'General',
452
+ filenameDelimiter: '->',
453
+ };
@@ -0,0 +1,80 @@
1
+ import { ValidationResult, ValidationCheck, ValidationOptions } from './validationTypes';
2
+ /**
3
+ * Base class for all format validators
4
+ * Provides the check-based validation system
5
+ */
6
+ export declare abstract class BaseValidator {
7
+ protected _errors: number;
8
+ protected _warnings: number;
9
+ protected _checks: ValidationCheck[];
10
+ protected _sub_checks: ValidationResult[];
11
+ protected _blocked: boolean;
12
+ protected _options: ValidationOptions;
13
+ constructor(options?: ValidationOptions);
14
+ /**
15
+ * Reset validator state
16
+ */
17
+ protected reset(): void;
18
+ /**
19
+ * Add a validation check that will be executed
20
+ * @param type - Category of the check
21
+ * @param description - Human-readable description
22
+ * @param checkFn - Async function that performs the check
23
+ */
24
+ protected add_check(type: string, description: string, checkFn: () => Promise<void>): Promise<void>;
25
+ /**
26
+ * Add a synchronous validation check
27
+ */
28
+ protected add_check_sync(type: string, description: string, checkFn: () => void): void;
29
+ /**
30
+ * Throw a validation error
31
+ * @param message - Error message
32
+ * @param blocker - If true, stop further validation
33
+ */
34
+ protected err(message: string, blocker?: boolean): never;
35
+ /**
36
+ * Add a warning to the last check
37
+ * @param message - Warning message
38
+ */
39
+ protected warn(message: string): void;
40
+ /**
41
+ * Get the current error count
42
+ */
43
+ get errors(): number;
44
+ /**
45
+ * Get the current warning count
46
+ */
47
+ get warnings(): number;
48
+ /**
49
+ * Get all checks performed so far
50
+ */
51
+ get checks(): ValidationCheck[];
52
+ /**
53
+ * Get sub-validation results
54
+ */
55
+ get sub_checks(): ValidationResult[];
56
+ /**
57
+ * Check if validation has been blocked
58
+ */
59
+ get isBlocked(): boolean;
60
+ /**
61
+ * Build the final validation result
62
+ */
63
+ protected buildResult(filename: string, filesize: number, format: string): ValidationResult;
64
+ /**
65
+ * Abstract method - each validator must implement this
66
+ * @param content - The content to validate (can be string, buffer, object, etc.)
67
+ * @param filename - Name of the file being validated
68
+ * @param filesize - Size of the file in bytes
69
+ */
70
+ abstract validate(content: any, filename: string, filesize: number): Promise<ValidationResult>;
71
+ /**
72
+ * Static helper to validate from file path
73
+ * Must be implemented by subclasses if they support file-based validation
74
+ */
75
+ static validateFile(_filePath: string): Promise<ValidationResult>;
76
+ /**
77
+ * Static helper to identify if content is this validator's format
78
+ */
79
+ static identifyFormat(_content: any, _filename: string): Promise<boolean>;
80
+ }
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BaseValidator = void 0;
4
+ const validationTypes_1 = require("./validationTypes");
5
+ /**
6
+ * Base class for all format validators
7
+ * Provides the check-based validation system
8
+ */
9
+ class BaseValidator {
10
+ constructor(options = {}) {
11
+ this._errors = 0;
12
+ this._warnings = 0;
13
+ this._checks = [];
14
+ this._sub_checks = [];
15
+ this._blocked = false;
16
+ this._options = {
17
+ includeWarnings: options.includeWarnings ?? true,
18
+ stopOnBlocker: options.stopOnBlocker ?? true,
19
+ customRules: options.customRules || [],
20
+ };
21
+ this.reset();
22
+ }
23
+ /**
24
+ * Reset validator state
25
+ */
26
+ reset() {
27
+ this._errors = 0;
28
+ this._warnings = 0;
29
+ this._checks = [];
30
+ this._sub_checks = [];
31
+ this._blocked = false;
32
+ }
33
+ /**
34
+ * Add a validation check that will be executed
35
+ * @param type - Category of the check
36
+ * @param description - Human-readable description
37
+ * @param checkFn - Async function that performs the check
38
+ */
39
+ async add_check(type, description, checkFn) {
40
+ // Skip if blocked by a previous error
41
+ if (this._blocked && this._options.stopOnBlocker) {
42
+ return;
43
+ }
44
+ const checkObj = {
45
+ type,
46
+ description,
47
+ valid: true,
48
+ };
49
+ this._checks.push(checkObj);
50
+ try {
51
+ await checkFn();
52
+ }
53
+ catch (e) {
54
+ if (e instanceof validationTypes_1.ValidationError) {
55
+ this._errors++;
56
+ checkObj.valid = false;
57
+ checkObj.error = e.message;
58
+ if (e.blocker) {
59
+ this._blocked = true;
60
+ }
61
+ }
62
+ else {
63
+ // Re-throw non-ValidationError exceptions
64
+ throw e;
65
+ }
66
+ }
67
+ }
68
+ /**
69
+ * Add a synchronous validation check
70
+ */
71
+ add_check_sync(type, description, checkFn) {
72
+ // Convert sync to async for consistency
73
+ // eslint-disable-next-line @typescript-eslint/require-await
74
+ void this.add_check(type, description, async () => checkFn());
75
+ }
76
+ /**
77
+ * Throw a validation error
78
+ * @param message - Error message
79
+ * @param blocker - If true, stop further validation
80
+ */
81
+ err(message, blocker = false) {
82
+ throw new validationTypes_1.ValidationError(message, blocker);
83
+ }
84
+ /**
85
+ * Add a warning to the last check
86
+ * @param message - Warning message
87
+ */
88
+ warn(message) {
89
+ if (!this._options.includeWarnings) {
90
+ return;
91
+ }
92
+ this._warnings++;
93
+ const lastCheck = this._checks[this._checks.length - 1];
94
+ if (lastCheck) {
95
+ lastCheck.warnings = lastCheck.warnings || [];
96
+ lastCheck.warnings.push(message);
97
+ }
98
+ }
99
+ /**
100
+ * Get the current error count
101
+ */
102
+ get errors() {
103
+ return this._errors;
104
+ }
105
+ /**
106
+ * Get the current warning count
107
+ */
108
+ get warnings() {
109
+ return this._warnings;
110
+ }
111
+ /**
112
+ * Get all checks performed so far
113
+ */
114
+ get checks() {
115
+ return this._checks;
116
+ }
117
+ /**
118
+ * Get sub-validation results
119
+ */
120
+ get sub_checks() {
121
+ return this._sub_checks;
122
+ }
123
+ /**
124
+ * Check if validation has been blocked
125
+ */
126
+ get isBlocked() {
127
+ return this._blocked;
128
+ }
129
+ /**
130
+ * Build the final validation result
131
+ */
132
+ buildResult(filename, filesize, format) {
133
+ return {
134
+ filename,
135
+ filesize,
136
+ format,
137
+ valid: this._errors === 0,
138
+ errors: this._errors,
139
+ warnings: this._warnings,
140
+ results: this._checks,
141
+ sub_results: this._sub_checks.length > 0 ? this._sub_checks : undefined,
142
+ };
143
+ }
144
+ /**
145
+ * Static helper to validate from file path
146
+ * Must be implemented by subclasses if they support file-based validation
147
+ */
148
+ // eslint-disable-next-line @typescript-eslint/require-await
149
+ static async validateFile(_filePath) {
150
+ throw new Error('validateFile must be implemented by subclass');
151
+ }
152
+ /**
153
+ * Static helper to identify if content is this validator's format
154
+ */
155
+ // eslint-disable-next-line @typescript-eslint/require-await
156
+ static async identifyFormat(_content, _filename) {
157
+ throw new Error('identifyFormat must be implemented by subclass');
158
+ }
159
+ }
160
+ exports.BaseValidator = BaseValidator;