@willwade/aac-processors 0.0.30 → 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 +116 -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,95 @@
1
+ let cachedFs = null;
2
+ let cachedPath = null;
3
+ export function getFs() {
4
+ if (!cachedFs) {
5
+ try {
6
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
7
+ cachedFs = require('fs');
8
+ }
9
+ catch {
10
+ throw new Error('File system access is not available in this environment.');
11
+ }
12
+ }
13
+ if (!cachedFs) {
14
+ throw new Error('File system access is not available in this environment.');
15
+ }
16
+ return cachedFs;
17
+ }
18
+ export function getPath() {
19
+ if (!cachedPath) {
20
+ try {
21
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
22
+ cachedPath = require('path');
23
+ }
24
+ catch {
25
+ throw new Error('Path utilities are not available in this environment.');
26
+ }
27
+ }
28
+ if (!cachedPath) {
29
+ throw new Error('Path utilities are not available in this environment.');
30
+ }
31
+ return cachedPath;
32
+ }
33
+ export function getBasename(filePath) {
34
+ const parts = filePath.split(/[/\\]/);
35
+ return parts[parts.length - 1] || filePath;
36
+ }
37
+ export function decodeText(input) {
38
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(input)) {
39
+ return input.toString('utf8');
40
+ }
41
+ const decoder = new TextDecoder('utf-8');
42
+ return decoder.decode(input);
43
+ }
44
+ export function encodeBase64(input) {
45
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(input)) {
46
+ return input.toString('base64');
47
+ }
48
+ // Browser fallback using btoa
49
+ let binary = '';
50
+ const len = input.byteLength;
51
+ for (let i = 0; i < len; i++) {
52
+ binary += String.fromCharCode(input[i]);
53
+ }
54
+ return btoa(binary);
55
+ }
56
+ export function encodeText(text) {
57
+ if (typeof Buffer !== 'undefined') {
58
+ return Buffer.from(text, 'utf8');
59
+ }
60
+ return new TextEncoder().encode(text);
61
+ }
62
+ export function readBinaryFromInput(input) {
63
+ if (typeof input === 'string') {
64
+ const fs = getFs();
65
+ return fs.readFileSync(input);
66
+ }
67
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(input)) {
68
+ return input;
69
+ }
70
+ if (input instanceof ArrayBuffer) {
71
+ return new Uint8Array(input);
72
+ }
73
+ return input;
74
+ }
75
+ export function readTextFromInput(input, encoding = 'utf8') {
76
+ if (typeof input === 'string') {
77
+ const fs = getFs();
78
+ return fs.readFileSync(input, encoding);
79
+ }
80
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(input)) {
81
+ return input.toString(encoding);
82
+ }
83
+ if (input instanceof ArrayBuffer) {
84
+ return decodeText(new Uint8Array(input));
85
+ }
86
+ return decodeText(input);
87
+ }
88
+ export function writeBinaryToPath(outputPath, data) {
89
+ const fs = getFs();
90
+ fs.writeFileSync(outputPath, data);
91
+ }
92
+ export function writeTextToPath(outputPath, text) {
93
+ const fs = getFs();
94
+ fs.writeFileSync(outputPath, text, 'utf8');
95
+ }
@@ -0,0 +1,156 @@
1
+ import { ValidationError, } from './validationTypes';
2
+ /**
3
+ * Base class for all format validators
4
+ * Provides the check-based validation system
5
+ */
6
+ export class BaseValidator {
7
+ constructor(options = {}) {
8
+ this._errors = 0;
9
+ this._warnings = 0;
10
+ this._checks = [];
11
+ this._sub_checks = [];
12
+ this._blocked = false;
13
+ this._options = {
14
+ includeWarnings: options.includeWarnings ?? true,
15
+ stopOnBlocker: options.stopOnBlocker ?? true,
16
+ customRules: options.customRules || [],
17
+ };
18
+ this.reset();
19
+ }
20
+ /**
21
+ * Reset validator state
22
+ */
23
+ reset() {
24
+ this._errors = 0;
25
+ this._warnings = 0;
26
+ this._checks = [];
27
+ this._sub_checks = [];
28
+ this._blocked = false;
29
+ }
30
+ /**
31
+ * Add a validation check that will be executed
32
+ * @param type - Category of the check
33
+ * @param description - Human-readable description
34
+ * @param checkFn - Async function that performs the check
35
+ */
36
+ async add_check(type, description, checkFn) {
37
+ // Skip if blocked by a previous error
38
+ if (this._blocked && this._options.stopOnBlocker) {
39
+ return;
40
+ }
41
+ const checkObj = {
42
+ type,
43
+ description,
44
+ valid: true,
45
+ };
46
+ this._checks.push(checkObj);
47
+ try {
48
+ await checkFn();
49
+ }
50
+ catch (e) {
51
+ if (e instanceof ValidationError) {
52
+ this._errors++;
53
+ checkObj.valid = false;
54
+ checkObj.error = e.message;
55
+ if (e.blocker) {
56
+ this._blocked = true;
57
+ }
58
+ }
59
+ else {
60
+ // Re-throw non-ValidationError exceptions
61
+ throw e;
62
+ }
63
+ }
64
+ }
65
+ /**
66
+ * Add a synchronous validation check
67
+ */
68
+ add_check_sync(type, description, checkFn) {
69
+ // Convert sync to async for consistency
70
+ // eslint-disable-next-line @typescript-eslint/require-await
71
+ void this.add_check(type, description, async () => checkFn());
72
+ }
73
+ /**
74
+ * Throw a validation error
75
+ * @param message - Error message
76
+ * @param blocker - If true, stop further validation
77
+ */
78
+ err(message, blocker = false) {
79
+ throw new ValidationError(message, blocker);
80
+ }
81
+ /**
82
+ * Add a warning to the last check
83
+ * @param message - Warning message
84
+ */
85
+ warn(message) {
86
+ if (!this._options.includeWarnings) {
87
+ return;
88
+ }
89
+ this._warnings++;
90
+ const lastCheck = this._checks[this._checks.length - 1];
91
+ if (lastCheck) {
92
+ lastCheck.warnings = lastCheck.warnings || [];
93
+ lastCheck.warnings.push(message);
94
+ }
95
+ }
96
+ /**
97
+ * Get the current error count
98
+ */
99
+ get errors() {
100
+ return this._errors;
101
+ }
102
+ /**
103
+ * Get the current warning count
104
+ */
105
+ get warnings() {
106
+ return this._warnings;
107
+ }
108
+ /**
109
+ * Get all checks performed so far
110
+ */
111
+ get checks() {
112
+ return this._checks;
113
+ }
114
+ /**
115
+ * Get sub-validation results
116
+ */
117
+ get sub_checks() {
118
+ return this._sub_checks;
119
+ }
120
+ /**
121
+ * Check if validation has been blocked
122
+ */
123
+ get isBlocked() {
124
+ return this._blocked;
125
+ }
126
+ /**
127
+ * Build the final validation result
128
+ */
129
+ buildResult(filename, filesize, format) {
130
+ return {
131
+ filename,
132
+ filesize,
133
+ format,
134
+ valid: this._errors === 0,
135
+ errors: this._errors,
136
+ warnings: this._warnings,
137
+ results: this._checks,
138
+ sub_results: this._sub_checks.length > 0 ? this._sub_checks : undefined,
139
+ };
140
+ }
141
+ /**
142
+ * Static helper to validate from file path
143
+ * Must be implemented by subclasses if they support file-based validation
144
+ */
145
+ // eslint-disable-next-line @typescript-eslint/require-await
146
+ static async validateFile(_filePath) {
147
+ throw new Error('validateFile must be implemented by subclass');
148
+ }
149
+ /**
150
+ * Static helper to identify if content is this validator's format
151
+ */
152
+ // eslint-disable-next-line @typescript-eslint/require-await
153
+ static async identifyFormat(_content, _filename) {
154
+ throw new Error('identifyFormat must be implemented by subclass');
155
+ }
156
+ }
@@ -0,0 +1,355 @@
1
+ /* eslint-disable @typescript-eslint/require-await */
2
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
3
+ /* eslint-disable @typescript-eslint/no-unsafe-return */
4
+ import * as fs from 'fs';
5
+ import * as path from 'path';
6
+ import * as xml2js from 'xml2js';
7
+ import JSZip from 'jszip';
8
+ import { BaseValidator } from './baseValidator';
9
+ /**
10
+ * Validator for Grid3/Smartbox Gridset files (.gridset, .gridsetx)
11
+ */
12
+ export class GridsetValidator extends BaseValidator {
13
+ constructor() {
14
+ super();
15
+ }
16
+ /**
17
+ * Validate a Gridset file from disk
18
+ */
19
+ static async validateFile(filePath) {
20
+ const validator = new GridsetValidator();
21
+ const content = fs.readFileSync(filePath);
22
+ const stats = fs.statSync(filePath);
23
+ return validator.validate(content, path.basename(filePath), stats.size);
24
+ }
25
+ /**
26
+ * Check if content is Gridset format
27
+ */
28
+ static async identifyFormat(content, filename) {
29
+ const name = filename.toLowerCase();
30
+ if (name.endsWith('.gridset') || name.endsWith('.gridsetx')) {
31
+ return true;
32
+ }
33
+ // Try to parse as XML and check for gridset structure
34
+ try {
35
+ const contentStr = Buffer.isBuffer(content) ? content.toString('utf-8') : content;
36
+ const parser = new xml2js.Parser();
37
+ const result = await parser.parseStringPromise(contentStr);
38
+ return result && (result.gridset || result.Gridset);
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ /**
45
+ * Main validation method
46
+ */
47
+ async validate(content, filename, filesize) {
48
+ this.reset();
49
+ const isEncrypted = filename.toLowerCase().endsWith('.gridsetx');
50
+ // eslint-disable-next-line @typescript-eslint/require-await
51
+ await this.add_check('filename', 'file extension', async () => {
52
+ if (!filename.match(/\.gridsetx?$/)) {
53
+ this.warn('filename should end with .gridset or .gridsetx');
54
+ }
55
+ });
56
+ // For encrypted .gridsetx files, we can't validate the content
57
+ if (isEncrypted) {
58
+ // eslint-disable-next-line @typescript-eslint/require-await
59
+ await this.add_check('encrypted_format', 'encrypted gridsetx file', async () => {
60
+ this.warn('gridsetx files are encrypted and cannot be fully validated');
61
+ });
62
+ return this.buildResult(filename, filesize, 'gridset');
63
+ }
64
+ const isZip = this.isZip(content);
65
+ if (isZip) {
66
+ await this.validateZipArchive(content, filename, filesize);
67
+ }
68
+ else {
69
+ await this.validateSingleXml(content, filename, filesize);
70
+ }
71
+ return this.buildResult(filename, filesize, 'gridset');
72
+ }
73
+ /**
74
+ * Check if the buffer is a zip archive
75
+ */
76
+ isZip(content) {
77
+ if (content.length < 4)
78
+ return false;
79
+ return content[0] === 0x50 && content[1] === 0x4b && content[2] === 0x03 && content[3] === 0x04;
80
+ }
81
+ /**
82
+ * Validate a single XML file (legacy or exploded format)
83
+ */
84
+ async validateSingleXml(content, filename, _filesize) {
85
+ let xmlObj = null;
86
+ await this.add_check('xml_parse', 'valid XML', async () => {
87
+ try {
88
+ const parser = new xml2js.Parser();
89
+ const contentStr = content.toString('utf-8');
90
+ xmlObj = await parser.parseStringPromise(contentStr);
91
+ }
92
+ catch (e) {
93
+ this.err(`Failed to parse XML: ${e.message}`, true);
94
+ }
95
+ });
96
+ if (!xmlObj)
97
+ return;
98
+ await this.add_check('xml_structure', 'gridset root element', async () => {
99
+ if (!xmlObj.gridset && !xmlObj.Gridset) {
100
+ this.err('missing root gridset element', true);
101
+ }
102
+ });
103
+ const gridset = xmlObj.gridset || xmlObj.Gridset;
104
+ if (gridset) {
105
+ await this.validateGridsetStructure(gridset, filename, content);
106
+ }
107
+ }
108
+ /**
109
+ * Validate a ZIP archive (.gridset)
110
+ */
111
+ async validateZipArchive(content, filename, _filesize) {
112
+ let zip;
113
+ try {
114
+ zip = await JSZip.loadAsync(Buffer.from(content));
115
+ }
116
+ catch (e) {
117
+ this.err(`Failed to open ZIP archive: ${e.message}`, true);
118
+ return;
119
+ }
120
+ const entries = Object.values(zip.files).filter((entry) => !entry.dir);
121
+ // Check for gridset.xml (required)
122
+ await this.add_check('gridset_xml_presence', 'gridset.xml presence', async () => {
123
+ const gridsetEntry = entries.find((e) => e.name.toLowerCase() === 'gridset.xml');
124
+ if (!gridsetEntry) {
125
+ this.err('Missing gridset.xml in archive', true);
126
+ }
127
+ else {
128
+ try {
129
+ const gridsetXml = await gridsetEntry.async('string');
130
+ const parser = new xml2js.Parser();
131
+ const xmlObj = await parser.parseStringPromise(gridsetXml);
132
+ const gridset = xmlObj.gridset || xmlObj.Gridset;
133
+ if (!gridset) {
134
+ this.err('Invalid gridset.xml structure', true);
135
+ }
136
+ else {
137
+ await this.validateGridsetStructure(gridset, filename, Buffer.from(gridsetXml));
138
+ }
139
+ }
140
+ catch (e) {
141
+ this.err(`Failed to parse gridset.xml: ${e.message}`, true);
142
+ }
143
+ }
144
+ });
145
+ // Check for settings.xml (highly recommended/required for metadata)
146
+ await this.add_check('settings_xml_presence', 'settings.xml presence', async () => {
147
+ const settingsEntry = entries.find((e) => e.name.toLowerCase() === 'settings.xml');
148
+ if (!settingsEntry) {
149
+ this.warn('Missing settings.xml in archive (required for full metadata)');
150
+ }
151
+ else {
152
+ try {
153
+ const settingsXml = await settingsEntry.async('string');
154
+ const parser = new xml2js.Parser();
155
+ const xmlObj = await parser.parseStringPromise(settingsXml);
156
+ const settings = xmlObj.GridSetSettings || xmlObj.gridSetSettings || xmlObj.GridsetSettings;
157
+ if (!settings) {
158
+ this.warn('Invalid settings.xml structure');
159
+ }
160
+ else {
161
+ // Basic validation of settings.xml
162
+ if (!settings.StartGrid && !settings.startGrid) {
163
+ this.warn('settings.xml missing StartGrid element');
164
+ }
165
+ }
166
+ }
167
+ catch (e) {
168
+ this.warn(`Failed to parse settings.xml: ${e.message}`);
169
+ }
170
+ }
171
+ });
172
+ }
173
+ /**
174
+ * Validate Gridset structure
175
+ */
176
+ async validateGridsetStructure(gridset, _filename, _content) {
177
+ // Check for required elements
178
+ await this.add_check('gridset_id', 'gridset id', async () => {
179
+ const id = gridset.$.id || gridset.$.Id;
180
+ if (!id) {
181
+ this.warn('gridset should have an id attribute');
182
+ }
183
+ });
184
+ await this.add_check('gridset_name', 'gridset name', async () => {
185
+ const name = gridset.$.name || gridset.$.Name || gridset.name?.[0];
186
+ if (!name) {
187
+ this.warn('gridset should have a name attribute or element');
188
+ }
189
+ });
190
+ // Check for pages
191
+ await this.add_check('pages', 'pages element', async () => {
192
+ if (!gridset.pages && !gridset.Pages) {
193
+ this.err('gridset must have a pages element');
194
+ }
195
+ else {
196
+ const pages = gridset.pages || gridset.Pages;
197
+ if (!pages[0] || !Array.isArray(pages[0].page)) {
198
+ this.warn('pages should contain at least one page element');
199
+ }
200
+ }
201
+ });
202
+ // Validate individual pages
203
+ const pages = gridset.pages?.[0] || gridset.Pages?.[0];
204
+ if (pages && Array.isArray(pages.page)) {
205
+ await this.add_check('page_count', 'page count', async () => {
206
+ if (pages.page.length === 0) {
207
+ this.err('gridset must contain at least one page');
208
+ }
209
+ });
210
+ for (let i = 0; i < Math.min(pages.page.length, 10); i++) {
211
+ // Limit to first 10 pages to avoid excessive validation
212
+ const page = pages.page[i];
213
+ await this.validatePage(page, i);
214
+ }
215
+ }
216
+ // Check for fixedCellSize
217
+ await this.add_check('fixed_cell_size', 'fixedCellSize element', async () => {
218
+ const fixedSize = gridset.fixedCellSize || gridset.FixedCellSize;
219
+ if (!fixedSize) {
220
+ this.warn('gridset should have a fixedCellSize element for consistency');
221
+ }
222
+ else {
223
+ // Validate fixedCellSize structure
224
+ const size = fixedSize[0];
225
+ if (size) {
226
+ const width = size.$.width || size.$.Width;
227
+ const height = size.$.height || size.$.Height;
228
+ if (!width || !height) {
229
+ this.warn('fixedCellSize should have both width and height attributes');
230
+ }
231
+ else if (isNaN(parseInt(width)) || isNaN(parseInt(height))) {
232
+ this.err('fixedCellSize width and height must be valid numbers');
233
+ }
234
+ }
235
+ }
236
+ });
237
+ // Check for styles
238
+ await this.add_check('styles', 'styles element', async () => {
239
+ const styles = gridset.styles || gridset.Styles;
240
+ if (!styles) {
241
+ this.warn('gridset should have a styles element for consistent formatting');
242
+ }
243
+ });
244
+ }
245
+ /**
246
+ * Validate a single page
247
+ */
248
+ async validatePage(page, index) {
249
+ await this.add_check(`page[${index}]_id`, `page ${index} id`, async () => {
250
+ const id = page.$.id || page.$.Id;
251
+ if (!id) {
252
+ this.err(`page at index ${index} is missing id attribute`);
253
+ }
254
+ });
255
+ await this.add_check(`page[${index}]_name`, `page ${index} name`, async () => {
256
+ const name = page.$.name || page.$.Name || page.name?.[0];
257
+ if (!name) {
258
+ this.warn(`page ${index} should have a name`);
259
+ }
260
+ });
261
+ // Check for cells
262
+ await this.add_check(`page[${index}]_cells`, `page ${index} cells`, async () => {
263
+ const cells = page.cells || page.Cells;
264
+ if (!cells) {
265
+ this.warn(`page ${index} should have a cells element`);
266
+ }
267
+ else {
268
+ const cellArray = cells[0]?.cell || cells[0]?.Cell;
269
+ if (!cellArray || !Array.isArray(cellArray) || cellArray.length === 0) {
270
+ this.warn(`page ${index} should contain at least one cell`);
271
+ }
272
+ }
273
+ });
274
+ // Validate cells if present
275
+ const cells = page.cells?.[0] || page.Cells?.[0];
276
+ if (cells) {
277
+ const cellArray = cells.cell || cells.Cell;
278
+ if (Array.isArray(cellArray) && cellArray.length > 0) {
279
+ // Sample a few cells to validate
280
+ const sampleSize = Math.min(cellArray.length, 5);
281
+ for (let i = 0; i < sampleSize; i++) {
282
+ await this.validateCell(cellArray[i], index, i);
283
+ }
284
+ }
285
+ }
286
+ }
287
+ /**
288
+ * Validate a single cell
289
+ */
290
+ async validateCell(cell, pageIdx, cellIdx) {
291
+ await this.add_check(`page[${pageIdx}]_cell[${cellIdx}]_id`, `cell id`, async () => {
292
+ const id = cell.$.id || cell.$.Id;
293
+ if (!id) {
294
+ this.warn(`cell ${cellIdx} on page ${pageIdx} is missing id attribute`);
295
+ }
296
+ });
297
+ await this.add_check(`page[${pageIdx}]_cell[${cellIdx}]_content`, `cell content`, async () => {
298
+ const label = cell.$.label || cell.$.Label;
299
+ const image = cell.$.image || cell.$.Image;
300
+ if (!label && !image) {
301
+ this.warn(`cell ${cellIdx} on page ${pageIdx} should have a label or image`);
302
+ }
303
+ });
304
+ // Validate scan block number (Grid 3 attribute)
305
+ await this.add_check(`page[${pageIdx}]_cell[${cellIdx}]_scanblock`, `cell scan block`, async () => {
306
+ const scanBlock = cell.$.scanBlock || cell.$.ScanBlock;
307
+ if (scanBlock !== undefined) {
308
+ const blockNum = parseInt(scanBlock, 10);
309
+ if (isNaN(blockNum) || blockNum < 1 || blockNum > 8) {
310
+ this.err(`cell ${cellIdx} on page ${pageIdx} has invalid scanBlock value: ${scanBlock} (must be 1-8)`, false);
311
+ }
312
+ }
313
+ });
314
+ // Check for color attributes
315
+ const backgroundColor = cell.$.backgroundColor || cell.$.BackgroundColor;
316
+ const _color = cell.$.color || cell.$.Color;
317
+ if (backgroundColor) {
318
+ await this.add_check(`page[${pageIdx}]_cell[${cellIdx}]_bg_color`, `cell background color`, async () => {
319
+ // Grid3 colors can be in various formats: named colors, hex, ARGB
320
+ // We just check it's not empty
321
+ if (backgroundColor.length === 0) {
322
+ this.warn(`cell ${cellIdx} has empty background color`);
323
+ }
324
+ });
325
+ }
326
+ // Check for valid jump references
327
+ const jump = cell.$.jump || cell.$.Jump;
328
+ if (jump) {
329
+ await this.add_check(`page[${pageIdx}]_cell[${cellIdx}]_jump`, `cell jump reference`, async () => {
330
+ if (typeof jump !== 'string' || jump.length === 0) {
331
+ this.warn(`cell ${cellIdx} has invalid jump reference`);
332
+ }
333
+ });
334
+ }
335
+ }
336
+ /**
337
+ * Validate color format (Grid3 uses ARGB format)
338
+ */
339
+ isValidGrid3Color(color) {
340
+ if (!color || color.length === 0)
341
+ return false;
342
+ // Named colors are valid
343
+ if (/^[a-zA-Z]+$/.test(color))
344
+ return true;
345
+ // ARGB format: #AARRGGBB or #RRGGBB
346
+ if (color.startsWith('#')) {
347
+ return color.length === 7 || color.length === 9;
348
+ }
349
+ // RGB format: rgb(r,g,b) or rgba(r,g,b,a)
350
+ if (color.startsWith('rgb')) {
351
+ return true; // Simplified check
352
+ }
353
+ return false;
354
+ }
355
+ }