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