@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.
- package/README.md +52 -852
- package/dist/browser/core/baseProcessor.js +241 -0
- package/dist/browser/core/stringCasing.js +179 -0
- package/dist/browser/core/treeStructure.js +255 -0
- package/dist/browser/index.browser.js +73 -0
- package/dist/browser/processors/applePanelsProcessor.js +582 -0
- package/dist/browser/processors/astericsGridProcessor.js +1509 -0
- package/dist/browser/processors/dotProcessor.js +221 -0
- package/dist/browser/processors/gridset/commands.js +962 -0
- package/dist/browser/processors/gridset/crypto.js +53 -0
- package/dist/browser/processors/gridset/password.js +43 -0
- package/dist/browser/processors/gridset/pluginTypes.js +277 -0
- package/dist/browser/processors/gridset/resolver.js +137 -0
- package/dist/browser/processors/gridset/symbolAlignment.js +276 -0
- package/dist/browser/processors/gridset/symbols.js +421 -0
- package/dist/browser/processors/gridsetProcessor.js +2002 -0
- package/dist/browser/processors/obfProcessor.js +705 -0
- package/dist/browser/processors/opmlProcessor.js +274 -0
- package/dist/browser/types/aac.js +38 -0
- package/dist/browser/utilities/analytics/utils/idGenerator.js +89 -0
- package/dist/browser/utilities/translation/translationProcessor.js +200 -0
- package/dist/browser/utils/io.js +95 -0
- package/dist/browser/validation/baseValidator.js +156 -0
- package/dist/browser/validation/gridsetValidator.js +355 -0
- package/dist/browser/validation/obfValidator.js +500 -0
- package/dist/browser/validation/validationTypes.js +46 -0
- package/dist/cli/index.js +5 -5
- package/dist/core/analyze.d.ts +2 -2
- package/dist/core/analyze.js +2 -2
- package/dist/core/baseProcessor.d.ts +5 -4
- package/dist/core/baseProcessor.js +22 -27
- package/dist/core/treeStructure.d.ts +5 -5
- package/dist/core/treeStructure.js +1 -4
- package/dist/index.browser.d.ts +37 -0
- package/dist/index.browser.js +99 -0
- package/dist/index.d.ts +1 -48
- package/dist/index.js +1 -136
- package/dist/index.node.d.ts +48 -0
- package/dist/index.node.js +152 -0
- package/dist/processors/applePanelsProcessor.d.ts +5 -4
- package/dist/processors/applePanelsProcessor.js +58 -62
- package/dist/processors/astericsGridProcessor.d.ts +7 -6
- package/dist/processors/astericsGridProcessor.js +31 -42
- package/dist/processors/dotProcessor.d.ts +5 -4
- package/dist/processors/dotProcessor.js +25 -33
- package/dist/processors/excelProcessor.d.ts +4 -3
- package/dist/processors/excelProcessor.js +6 -3
- package/dist/processors/gridset/crypto.d.ts +18 -0
- package/dist/processors/gridset/crypto.js +57 -0
- package/dist/processors/gridset/helpers.d.ts +1 -1
- package/dist/processors/gridset/helpers.js +18 -8
- package/dist/processors/gridset/password.d.ts +20 -3
- package/dist/processors/gridset/password.js +17 -3
- package/dist/processors/gridset/wordlistHelpers.d.ts +3 -3
- package/dist/processors/gridset/wordlistHelpers.js +21 -20
- package/dist/processors/gridsetProcessor.d.ts +7 -12
- package/dist/processors/gridsetProcessor.js +118 -77
- package/dist/processors/obfProcessor.d.ts +9 -7
- package/dist/processors/obfProcessor.js +131 -56
- package/dist/processors/obfsetProcessor.d.ts +5 -4
- package/dist/processors/obfsetProcessor.js +10 -16
- package/dist/processors/opmlProcessor.d.ts +5 -4
- package/dist/processors/opmlProcessor.js +27 -34
- package/dist/processors/snapProcessor.d.ts +8 -7
- package/dist/processors/snapProcessor.js +15 -12
- package/dist/processors/touchchatProcessor.d.ts +8 -7
- package/dist/processors/touchchatProcessor.js +22 -17
- package/dist/types/aac.d.ts +0 -2
- package/dist/types/aac.js +2 -0
- package/dist/utils/io.d.ts +12 -0
- package/dist/utils/io.js +107 -0
- package/dist/validation/gridsetValidator.js +7 -7
- package/dist/validation/snapValidator.js +28 -35
- package/docs/BROWSER_USAGE.md +618 -0
- package/examples/README.md +77 -0
- package/examples/browser-test-server.js +81 -0
- package/examples/browser-test.html +331 -0
- package/examples/vitedemo/QUICKSTART.md +74 -0
- package/examples/vitedemo/README.md +157 -0
- package/examples/vitedemo/index.html +376 -0
- package/examples/vitedemo/package-lock.json +1221 -0
- package/examples/vitedemo/package.json +18 -0
- package/examples/vitedemo/src/main.ts +519 -0
- package/examples/vitedemo/test-files/example.dot +14 -0
- package/examples/vitedemo/test-files/example.grd +1 -0
- package/examples/vitedemo/test-files/example.gridset +0 -0
- package/examples/vitedemo/test-files/example.obz +0 -0
- package/examples/vitedemo/test-files/example.opml +18 -0
- package/examples/vitedemo/test-files/simple.obf +53 -0
- package/examples/vitedemo/tsconfig.json +24 -0
- package/examples/vitedemo/vite.config.ts +34 -0
- 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
|
+
}
|