@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.
Files changed (32) hide show
  1. package/README.md +19 -27
  2. package/dist/core/treeStructure.d.ts +2 -2
  3. package/dist/core/treeStructure.js +4 -1
  4. package/dist/processors/applePanelsProcessor.js +166 -123
  5. package/dist/processors/astericsGridProcessor.js +121 -105
  6. package/dist/processors/dotProcessor.js +83 -65
  7. package/dist/processors/gridsetProcessor.js +2 -0
  8. package/dist/processors/obfProcessor.js +11 -4
  9. package/dist/processors/opmlProcessor.js +82 -44
  10. package/dist/processors/snapProcessor.js +19 -9
  11. package/dist/processors/touchchatProcessor.js +72 -21
  12. package/dist/utilities/analytics/metrics/core.d.ts +1 -1
  13. package/dist/utilities/analytics/metrics/core.js +191 -212
  14. package/dist/validation/applePanelsValidator.d.ts +10 -0
  15. package/dist/validation/applePanelsValidator.js +124 -0
  16. package/dist/validation/astericsValidator.d.ts +16 -0
  17. package/dist/validation/astericsValidator.js +115 -0
  18. package/dist/validation/dotValidator.d.ts +10 -0
  19. package/dist/validation/dotValidator.js +113 -0
  20. package/dist/validation/excelValidator.d.ts +10 -0
  21. package/dist/validation/excelValidator.js +89 -0
  22. package/dist/validation/index.d.ts +14 -1
  23. package/dist/validation/index.js +104 -1
  24. package/dist/validation/obfsetValidator.d.ts +10 -0
  25. package/dist/validation/obfsetValidator.js +103 -0
  26. package/dist/validation/opmlValidator.d.ts +10 -0
  27. package/dist/validation/opmlValidator.js +107 -0
  28. package/dist/validation/validationTypes.d.ts +22 -0
  29. package/dist/validation/validationTypes.js +38 -1
  30. package/dist/validation.d.ts +8 -2
  31. package/dist/validation.js +16 -1
  32. 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 { ObfProcessor, GridsetProcessor } from "aac-processors";
270
+ import { validateFileOrBuffer, getValidatorForFile } from "@willwade/aac-processors/validation";
271
271
 
272
- // Validate OBF/OBZ files
273
- const obfProcessor = new ObfProcessor();
274
- const result = await obfProcessor.validate("board.obf");
275
-
276
- console.log(`Valid: ${result.valid}`);
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
- // Validate Gridset files (with optional password for encrypted files)
290
- const gridsetProcessor = new GridsetProcessor({
291
- gridsetPassword: "optional-password",
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
- - Required fields (format, id, locale, buttons, grid, images, sounds)
316
- - Grid structure (rows, columns, order)
317
- - Button references (image_id, sound_id, load_board paths)
318
- - Color formats (RGB/RGBA)
319
- - Cross-reference validation
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
- let content;
96
- if (Buffer.isBuffer(filePathOrBuffer)) {
97
- content = filePathOrBuffer.toString('utf8');
98
- }
99
- else if (typeof filePathOrBuffer === 'string') {
100
- // Check if it's a .ascconfig folder or a direct .plist file
101
- if (filePathOrBuffer.endsWith('.ascconfig')) {
102
- // Read from proper Apple Panels structure: *.ascconfig/Contents/Resources/PanelDefinitions.plist
103
- const panelDefsPath = `${filePathOrBuffer}/Contents/Resources/PanelDefinitions.plist`;
104
- if (fs_1.default.existsSync(panelDefsPath)) {
105
- content = fs_1.default.readFileSync(panelDefsPath, 'utf8');
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
- throw new Error(`Apple Panels file not found: ${panelDefsPath}`);
121
+ buffer = fs_1.default.readFileSync(filePathOrBuffer);
109
122
  }
110
123
  }
111
124
  else {
112
- // Fallback: treat as direct .plist file
113
- content = fs_1.default.readFileSync(filePathOrBuffer, 'utf8');
114
- }
115
- }
116
- else {
117
- throw new Error('Invalid input: expected string path or Buffer');
118
- }
119
- const parsedData = plist_1.default.parse(content);
120
- // Handle both old format (panels array) and new Apple Panels format (Panels dict)
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
- panel.buttons.forEach((btn, idx) => {
159
- // Create semantic action from Apple Panels button
160
- let semanticAction;
161
- if (btn.targetPanel) {
162
- semanticAction = {
163
- category: treeStructure_1.AACSemanticCategory.NAVIGATION,
164
- intent: treeStructure_1.AACSemanticIntent.NAVIGATE_TO,
165
- targetId: btn.targetPanel,
166
- platformData: {
167
- applePanels: {
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
- else {
179
- semanticAction = {
180
- category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
181
- intent: treeStructure_1.AACSemanticIntent.SPEAK_TEXT,
182
- text: btn.message || btn.label,
183
- platformData: {
184
- applePanels: {
185
- actionType: 'ActionPressKeyCharSequence',
186
- parameters: {
187
- CharString: btn.message || btn.label || '',
188
- isStickyKey: false,
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
- fallback: {
193
- type: 'SPEAK',
194
- message: btn.message || btn.label,
195
- },
196
- };
197
- }
198
- const button = new treeStructure_1.AACButton({
199
- id: `${panel.id}_btn_${idx}`,
200
- label: btn.label,
201
- message: btn.message || btn.label,
202
- targetPageId: btn.targetPanel,
203
- semanticAction: semanticAction,
204
- style: {
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
- // Set the page's grid layout
230
- page.grid = gridLayout;
231
- tree.addPage(page);
232
- });
233
- return tree;
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