@willwade/aac-processors 0.0.21 → 0.0.23
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 +19 -27
- package/dist/core/treeStructure.d.ts +2 -2
- package/dist/core/treeStructure.js +4 -1
- package/dist/processors/applePanelsProcessor.js +166 -123
- package/dist/processors/astericsGridProcessor.js +121 -105
- package/dist/processors/dotProcessor.js +83 -65
- package/dist/processors/gridsetProcessor.js +2 -0
- package/dist/processors/obfProcessor.js +11 -4
- package/dist/processors/opmlProcessor.js +82 -44
- package/dist/processors/snapProcessor.js +19 -9
- package/dist/processors/touchchatProcessor.js +72 -21
- package/dist/utilities/analytics/metrics/core.d.ts +1 -1
- package/dist/utilities/analytics/metrics/core.js +191 -212
- package/dist/validation/applePanelsValidator.d.ts +10 -0
- package/dist/validation/applePanelsValidator.js +124 -0
- package/dist/validation/astericsValidator.d.ts +16 -0
- package/dist/validation/astericsValidator.js +115 -0
- package/dist/validation/dotValidator.d.ts +10 -0
- package/dist/validation/dotValidator.js +113 -0
- package/dist/validation/excelValidator.d.ts +10 -0
- package/dist/validation/excelValidator.js +89 -0
- package/dist/validation/index.d.ts +14 -1
- package/dist/validation/index.js +104 -1
- package/dist/validation/obfsetValidator.d.ts +10 -0
- package/dist/validation/obfsetValidator.js +103 -0
- package/dist/validation/opmlValidator.d.ts +10 -0
- package/dist/validation/opmlValidator.js +107 -0
- package/dist/validation/validationTypes.d.ts +22 -0
- package/dist/validation/validationTypes.js +38 -1
- package/dist/validation.d.ts +8 -2
- package/dist/validation.js +16 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -267,30 +267,18 @@ console.log(`Average Effort: ${result.total_words}`);
|
|
|
267
267
|
Validate AAC files against format specifications to ensure data integrity:
|
|
268
268
|
|
|
269
269
|
```typescript
|
|
270
|
-
import {
|
|
270
|
+
import { validateFileOrBuffer, getValidatorForFile } from "@willwade/aac-processors/validation";
|
|
271
271
|
|
|
272
|
-
//
|
|
273
|
-
const
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
console.log(`Errors: ${result.errors}`);
|
|
278
|
-
console.log(`Warnings: ${result.warnings}`);
|
|
279
|
-
|
|
280
|
-
// Detailed validation results
|
|
281
|
-
if (!result.valid) {
|
|
282
|
-
result.results
|
|
283
|
-
.filter((check) => !check.valid)
|
|
284
|
-
.forEach((check) => {
|
|
285
|
-
console.log(`✗ ${check.description}: ${check.error}`);
|
|
286
|
-
});
|
|
287
|
-
}
|
|
272
|
+
// Works in Node, Vite, and esbuild (pass Buffers from the browser/CLI)
|
|
273
|
+
const fileName = "board.obf";
|
|
274
|
+
const validator = getValidatorForFile(fileName);
|
|
275
|
+
const bufferOrPath = new Uint8Array(await file.arrayBuffer()); // or fs path in Node
|
|
276
|
+
const result = await validateFileOrBuffer(bufferOrPath, fileName);
|
|
288
277
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
278
|
+
console.log(result.valid, result.errors, result.warnings);
|
|
279
|
+
result.results.forEach((check) => {
|
|
280
|
+
if (!check.valid) console.log(`✗ ${check.description}: ${check.error}`);
|
|
292
281
|
});
|
|
293
|
-
const gridsetResult = await gridsetProcessor.validate("vocab.gridsetx");
|
|
294
282
|
```
|
|
295
283
|
|
|
296
284
|
#### Using the CLI
|
|
@@ -312,11 +300,15 @@ aacprocessors validate board.gridsetx --gridset-password <password>
|
|
|
312
300
|
#### What Gets Validated?
|
|
313
301
|
|
|
314
302
|
- **OBF/OBZ**: Spec compliance (Open Board Format)
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
303
|
+
- **Gridset/Gridsetx**: ZIP/XML structure, required Smartbox assets
|
|
304
|
+
- **Snap**: ZIP/package content, settings/pages/images
|
|
305
|
+
- **TouchChat**: ZIP structure, vocab metadata, nested boards
|
|
306
|
+
- **Asterics (.grd)**: JSON parse, grids, elements, coordinates
|
|
307
|
+
- **Excel (.xlsx/.xls)**: Workbook readability and worksheet content
|
|
308
|
+
- **OPML**: XML validity and outline hierarchy
|
|
309
|
+
- **DOT**: Graph nodes/edges present and text content
|
|
310
|
+
- **Apple Panels (.plist/.ascconfig)**: PanelDefinitions presence and buttons
|
|
311
|
+
- **OBFSet**: Bundled board layout checks
|
|
320
312
|
|
|
321
313
|
- **Gridset**: XML structure
|
|
322
314
|
- Required elements (gridset, pages, cells)
|
|
@@ -901,4 +893,4 @@ Want to help with any of these items? See our [Contributing Guidelines](#-contri
|
|
|
901
893
|
|
|
902
894
|
### Credits
|
|
903
895
|
|
|
904
|
-
Some of the OBF work is directly from https://github.com/open-aac/obf and https://github.com/open-aac/aac-metrics - OBLA too https://www.openboardformat.org/logs
|
|
896
|
+
Some of the OBF work is directly from https://github.com/open-aac/obf and https://github.com/open-aac/aac-metrics - OBLA too https://www.openboardformat.org/logs
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AACButton as IAACButton, AACPage as IAACPage, AACTree as IAACTree, AACTreeMetadata, SnapMetadata, GridSetMetadata, AstericsGridMetadata, TouchChatMetadata, AACStyle } from '../types/aac';
|
|
2
|
-
export { AACTreeMetadata, SnapMetadata, GridSetMetadata, AstericsGridMetadata, TouchChatMetadata };
|
|
1
|
+
import { AACButton as IAACButton, AACPage as IAACPage, AACTree as IAACTree, AACTreeMetadata, SnapMetadata, GridSetMetadata, AstericsGridMetadata, TouchChatMetadata, AACStyle, CellScanningOrder, ScanningSelectionMethod } from '../types/aac';
|
|
2
|
+
export { AACTreeMetadata, SnapMetadata, GridSetMetadata, AstericsGridMetadata, TouchChatMetadata, CellScanningOrder, ScanningSelectionMethod, };
|
|
3
3
|
export declare enum AACSemanticCategory {
|
|
4
4
|
COMMUNICATION = "communication",// Speech, text output
|
|
5
5
|
NAVIGATION = "navigation",// Page/grid navigation
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.AACTree = exports.AACPage = exports.AACButton = exports.AACScanType = exports.AACSemanticIntent = exports.AACSemanticCategory = void 0;
|
|
3
|
+
exports.AACTree = exports.AACPage = exports.AACButton = exports.AACScanType = exports.AACSemanticIntent = exports.AACSemanticCategory = exports.ScanningSelectionMethod = exports.CellScanningOrder = void 0;
|
|
4
|
+
const aac_1 = require("../types/aac");
|
|
5
|
+
Object.defineProperty(exports, "CellScanningOrder", { enumerable: true, get: function () { return aac_1.CellScanningOrder; } });
|
|
6
|
+
Object.defineProperty(exports, "ScanningSelectionMethod", { enumerable: true, get: function () { return aac_1.ScanningSelectionMethod; } });
|
|
4
7
|
// Semantic action categories for cross-platform compatibility
|
|
5
8
|
var AACSemanticCategory;
|
|
6
9
|
(function (AACSemanticCategory) {
|
|
@@ -10,6 +10,7 @@ const treeStructure_1 = require("../core/treeStructure");
|
|
|
10
10
|
const plist_1 = __importDefault(require("plist"));
|
|
11
11
|
const fs_1 = __importDefault(require("fs"));
|
|
12
12
|
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const validation_1 = require("../validation");
|
|
13
14
|
function isNormalizedPanel(panel) {
|
|
14
15
|
return typeof panel.id === 'string';
|
|
15
16
|
}
|
|
@@ -92,145 +93,187 @@ class ApplePanelsProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
92
93
|
return texts;
|
|
93
94
|
}
|
|
94
95
|
loadIntoTree(filePathOrBuffer) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (filePathOrBuffer
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
96
|
+
const filename = typeof filePathOrBuffer === 'string' ? path_1.default.basename(filePathOrBuffer) : 'upload.plist';
|
|
97
|
+
let buffer;
|
|
98
|
+
try {
|
|
99
|
+
if (Buffer.isBuffer(filePathOrBuffer)) {
|
|
100
|
+
buffer = filePathOrBuffer;
|
|
101
|
+
}
|
|
102
|
+
else if (typeof filePathOrBuffer === 'string') {
|
|
103
|
+
if (filePathOrBuffer.endsWith('.ascconfig')) {
|
|
104
|
+
const panelDefsPath = `${filePathOrBuffer}/Contents/Resources/PanelDefinitions.plist`;
|
|
105
|
+
if (fs_1.default.existsSync(panelDefsPath)) {
|
|
106
|
+
buffer = fs_1.default.readFileSync(panelDefsPath);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const validation = (0, validation_1.buildValidationResultFromMessage)({
|
|
110
|
+
filename,
|
|
111
|
+
filesize: 0,
|
|
112
|
+
format: 'applepanels',
|
|
113
|
+
message: `Apple Panels file not found: ${panelDefsPath}`,
|
|
114
|
+
type: 'missing',
|
|
115
|
+
description: 'PanelDefinitions.plist',
|
|
116
|
+
});
|
|
117
|
+
throw new validation_1.ValidationFailureError('Apple Panels file not found', validation);
|
|
118
|
+
}
|
|
106
119
|
}
|
|
107
120
|
else {
|
|
108
|
-
|
|
121
|
+
buffer = fs_1.default.readFileSync(filePathOrBuffer);
|
|
109
122
|
}
|
|
110
123
|
}
|
|
111
124
|
else {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
let panelsData = [];
|
|
122
|
-
if (Array.isArray(parsedData.panels)) {
|
|
123
|
-
panelsData = parsedData.panels.map((panel, index) => {
|
|
124
|
-
if (isNormalizedPanel(panel)) {
|
|
125
|
-
return panel;
|
|
126
|
-
}
|
|
127
|
-
const panelData = panel || {
|
|
128
|
-
PanelObjects: [],
|
|
129
|
-
};
|
|
130
|
-
return normalizePanel(panelData, `panel_${index}`);
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
else if (parsedData.Panels) {
|
|
134
|
-
const panelsDict = parsedData.Panels;
|
|
135
|
-
panelsData = Object.keys(panelsDict).map((panelId) => {
|
|
136
|
-
const rawPanel = panelsDict[panelId] || { PanelObjects: [] };
|
|
137
|
-
return normalizePanel(rawPanel, panelId);
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
const data = { panels: panelsData };
|
|
141
|
-
const tree = new treeStructure_1.AACTree();
|
|
142
|
-
tree.metadata.format = 'applepanels';
|
|
143
|
-
data.panels.forEach((panel) => {
|
|
144
|
-
const page = new treeStructure_1.AACPage({
|
|
145
|
-
id: panel.id,
|
|
146
|
-
name: panel.name,
|
|
147
|
-
grid: [],
|
|
148
|
-
buttons: [],
|
|
149
|
-
parentId: null,
|
|
150
|
-
});
|
|
151
|
-
// Create a 2D grid to track button positions
|
|
152
|
-
const gridLayout = [];
|
|
153
|
-
const maxRows = 20; // Reasonable default for Apple Panels
|
|
154
|
-
const maxCols = 20;
|
|
155
|
-
for (let r = 0; r < maxRows; r++) {
|
|
156
|
-
gridLayout[r] = new Array(maxCols).fill(null);
|
|
125
|
+
const validation = (0, validation_1.buildValidationResultFromMessage)({
|
|
126
|
+
filename,
|
|
127
|
+
filesize: 0,
|
|
128
|
+
format: 'applepanels',
|
|
129
|
+
message: 'Invalid input: expected string path or Buffer',
|
|
130
|
+
type: 'input',
|
|
131
|
+
description: 'Apple Panels input',
|
|
132
|
+
});
|
|
133
|
+
throw new validation_1.ValidationFailureError('Invalid Apple Panels input', validation);
|
|
157
134
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
actionType: 'ActionOpenPanel',
|
|
169
|
-
parameters: { PanelID: `USER.${btn.targetPanel}` },
|
|
170
|
-
},
|
|
171
|
-
},
|
|
172
|
-
fallback: {
|
|
173
|
-
type: 'NAVIGATE',
|
|
174
|
-
targetPageId: btn.targetPanel,
|
|
175
|
-
},
|
|
135
|
+
const content = buffer.toString('utf8');
|
|
136
|
+
const parsedData = plist_1.default.parse(content);
|
|
137
|
+
let panelsData = [];
|
|
138
|
+
if (Array.isArray(parsedData.panels)) {
|
|
139
|
+
panelsData = parsedData.panels.map((panel, index) => {
|
|
140
|
+
if (isNormalizedPanel(panel)) {
|
|
141
|
+
return panel;
|
|
142
|
+
}
|
|
143
|
+
const panelData = panel || {
|
|
144
|
+
PanelObjects: [],
|
|
176
145
|
};
|
|
146
|
+
return normalizePanel(panelData, `panel_${index}`);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else if (parsedData.Panels) {
|
|
150
|
+
const panelsDict = parsedData.Panels;
|
|
151
|
+
panelsData = Object.keys(panelsDict).map((panelId) => {
|
|
152
|
+
const rawPanel = panelsDict[panelId] || { PanelObjects: [] };
|
|
153
|
+
return normalizePanel(rawPanel, panelId);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if (panelsData.length === 0) {
|
|
157
|
+
const validation = (0, validation_1.buildValidationResultFromMessage)({
|
|
158
|
+
filename,
|
|
159
|
+
filesize: buffer.byteLength,
|
|
160
|
+
format: 'applepanels',
|
|
161
|
+
message: 'No panels found in Apple Panels file',
|
|
162
|
+
type: 'structure',
|
|
163
|
+
description: 'Panels definition',
|
|
164
|
+
});
|
|
165
|
+
throw new validation_1.ValidationFailureError('Apple Panels has no panels', validation);
|
|
166
|
+
}
|
|
167
|
+
const data = { panels: panelsData };
|
|
168
|
+
const tree = new treeStructure_1.AACTree();
|
|
169
|
+
tree.metadata.format = 'applepanels';
|
|
170
|
+
data.panels.forEach((panel) => {
|
|
171
|
+
const page = new treeStructure_1.AACPage({
|
|
172
|
+
id: panel.id,
|
|
173
|
+
name: panel.name,
|
|
174
|
+
grid: [],
|
|
175
|
+
buttons: [],
|
|
176
|
+
parentId: null,
|
|
177
|
+
});
|
|
178
|
+
const gridLayout = [];
|
|
179
|
+
const maxRows = 20;
|
|
180
|
+
const maxCols = 20;
|
|
181
|
+
for (let r = 0; r < maxRows; r++) {
|
|
182
|
+
gridLayout[r] = new Array(maxCols).fill(null);
|
|
177
183
|
}
|
|
178
|
-
|
|
179
|
-
semanticAction
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
184
|
+
panel.buttons.forEach((btn, idx) => {
|
|
185
|
+
let semanticAction;
|
|
186
|
+
if (btn.targetPanel) {
|
|
187
|
+
semanticAction = {
|
|
188
|
+
category: treeStructure_1.AACSemanticCategory.NAVIGATION,
|
|
189
|
+
intent: treeStructure_1.AACSemanticIntent.NAVIGATE_TO,
|
|
190
|
+
targetId: btn.targetPanel,
|
|
191
|
+
platformData: {
|
|
192
|
+
applePanels: {
|
|
193
|
+
actionType: 'ActionOpenPanel',
|
|
194
|
+
parameters: { PanelID: `USER.${btn.targetPanel}` },
|
|
189
195
|
},
|
|
190
196
|
},
|
|
197
|
+
fallback: {
|
|
198
|
+
type: 'NAVIGATE',
|
|
199
|
+
targetPageId: btn.targetPanel,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
semanticAction = {
|
|
205
|
+
category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
|
|
206
|
+
intent: treeStructure_1.AACSemanticIntent.SPEAK_TEXT,
|
|
207
|
+
text: btn.message || btn.label,
|
|
208
|
+
platformData: {
|
|
209
|
+
applePanels: {
|
|
210
|
+
actionType: 'ActionPressKeyCharSequence',
|
|
211
|
+
parameters: {
|
|
212
|
+
CharString: btn.message || btn.label || '',
|
|
213
|
+
isStickyKey: false,
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
fallback: {
|
|
218
|
+
type: 'SPEAK',
|
|
219
|
+
message: btn.message || btn.label,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const button = new treeStructure_1.AACButton({
|
|
224
|
+
id: `${panel.id}_btn_${idx}`,
|
|
225
|
+
label: btn.label,
|
|
226
|
+
message: btn.message || btn.label,
|
|
227
|
+
targetPageId: btn.targetPanel,
|
|
228
|
+
semanticAction: semanticAction,
|
|
229
|
+
style: {
|
|
230
|
+
backgroundColor: btn.DisplayColor,
|
|
231
|
+
fontSize: btn.FontSize,
|
|
232
|
+
fontWeight: btn.DisplayImageWeight === 'bold' ? 'bold' : 'normal',
|
|
191
233
|
},
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
backgroundColor: btn.DisplayColor,
|
|
206
|
-
fontSize: btn.FontSize,
|
|
207
|
-
fontWeight: btn.DisplayImageWeight === 'bold' ? 'bold' : 'normal',
|
|
208
|
-
},
|
|
209
|
-
});
|
|
210
|
-
page.addButton(button);
|
|
211
|
-
// Place button in grid layout using Rect position data
|
|
212
|
-
if (btn.Rect) {
|
|
213
|
-
const rect = this.parseRect(btn.Rect);
|
|
214
|
-
if (rect) {
|
|
215
|
-
const gridPos = this.pixelToGrid(rect.x, rect.y);
|
|
216
|
-
const gridWidth = Math.max(1, Math.ceil(rect.width / 25));
|
|
217
|
-
const gridHeight = Math.max(1, Math.ceil(rect.height / 25));
|
|
218
|
-
// Place button in grid (handle width/height span)
|
|
219
|
-
for (let r = gridPos.gridY; r < gridPos.gridY + gridHeight && r < maxRows; r++) {
|
|
220
|
-
for (let c = gridPos.gridX; c < gridPos.gridX + gridWidth && c < maxCols; c++) {
|
|
221
|
-
if (gridLayout[r] && gridLayout[r][c] === null) {
|
|
222
|
-
gridLayout[r][c] = button;
|
|
234
|
+
});
|
|
235
|
+
page.addButton(button);
|
|
236
|
+
if (btn.Rect) {
|
|
237
|
+
const rect = this.parseRect(btn.Rect);
|
|
238
|
+
if (rect) {
|
|
239
|
+
const gridPos = this.pixelToGrid(rect.x, rect.y);
|
|
240
|
+
const gridWidth = Math.max(1, Math.ceil(rect.width / 25));
|
|
241
|
+
const gridHeight = Math.max(1, Math.ceil(rect.height / 25));
|
|
242
|
+
for (let r = gridPos.gridY; r < gridPos.gridY + gridHeight && r < maxRows; r++) {
|
|
243
|
+
for (let c = gridPos.gridX; c < gridPos.gridX + gridWidth && c < maxCols; c++) {
|
|
244
|
+
if (gridLayout[r] && gridLayout[r][c] === null) {
|
|
245
|
+
gridLayout[r][c] = button;
|
|
246
|
+
}
|
|
223
247
|
}
|
|
224
248
|
}
|
|
225
249
|
}
|
|
226
250
|
}
|
|
227
|
-
}
|
|
251
|
+
});
|
|
252
|
+
page.grid = gridLayout;
|
|
253
|
+
tree.addPage(page);
|
|
228
254
|
});
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
255
|
+
return tree;
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
if (err instanceof validation_1.ValidationFailureError) {
|
|
259
|
+
throw err;
|
|
260
|
+
}
|
|
261
|
+
const validation = (0, validation_1.buildValidationResultFromMessage)({
|
|
262
|
+
filename,
|
|
263
|
+
filesize: Buffer.isBuffer(filePathOrBuffer)
|
|
264
|
+
? filePathOrBuffer.byteLength
|
|
265
|
+
: typeof filePathOrBuffer === 'string'
|
|
266
|
+
? fs_1.default.existsSync(filePathOrBuffer)
|
|
267
|
+
? fs_1.default.statSync(filePathOrBuffer).size
|
|
268
|
+
: 0
|
|
269
|
+
: 0,
|
|
270
|
+
format: 'applepanels',
|
|
271
|
+
message: err?.message || 'Failed to parse Apple Panels file',
|
|
272
|
+
type: 'parse',
|
|
273
|
+
description: 'Parse Apple Panels plist',
|
|
274
|
+
});
|
|
275
|
+
throw new validation_1.ValidationFailureError('Failed to load Apple Panels file', validation, err);
|
|
276
|
+
}
|
|
234
277
|
}
|
|
235
278
|
processTexts(filePathOrBuffer, translations, outputPath) {
|
|
236
279
|
// Load the tree, apply translations, and save to new file
|