@willwade/aac-processors 0.0.22 → 0.0.24

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.
@@ -8,6 +8,8 @@ const baseProcessor_1 = require("../core/baseProcessor");
8
8
  const treeStructure_1 = require("../core/treeStructure");
9
9
  // Removed unused import: FileProcessor
10
10
  const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const validation_1 = require("../validation");
11
13
  class DotProcessor extends baseProcessor_1.BaseProcessor {
12
14
  constructor(options) {
13
15
  super(options);
@@ -72,78 +74,94 @@ class DotProcessor extends baseProcessor_1.BaseProcessor {
72
74
  return texts;
73
75
  }
74
76
  loadIntoTree(filePathOrBuffer) {
75
- let content;
77
+ const filename = typeof filePathOrBuffer === 'string' ? path_1.default.basename(filePathOrBuffer) : 'upload.dot';
78
+ const buffer = Buffer.isBuffer(filePathOrBuffer)
79
+ ? filePathOrBuffer
80
+ : fs_1.default.readFileSync(filePathOrBuffer);
81
+ const filesize = buffer.byteLength;
76
82
  try {
77
- content =
78
- typeof filePathOrBuffer === 'string'
79
- ? fs_1.default.readFileSync(filePathOrBuffer, 'utf8')
80
- : filePathOrBuffer.toString('utf8');
83
+ const content = buffer.toString('utf8');
84
+ if (!content || content.trim().length === 0) {
85
+ const validation = (0, validation_1.buildValidationResultFromMessage)({
86
+ filename,
87
+ filesize,
88
+ format: 'dot',
89
+ message: 'DOT file is empty',
90
+ type: 'content',
91
+ description: 'DOT file content',
92
+ });
93
+ throw new validation_1.ValidationFailureError('Empty DOT content', validation);
94
+ }
95
+ // Check for binary data (contains null bytes or non-printable characters)
96
+ const head = content.substring(0, 100);
97
+ for (let i = 0; i < head.length; i++) {
98
+ const code = head.charCodeAt(i);
99
+ if (code === 0 || (code >= 0 && code <= 8) || (code >= 14 && code <= 31)) {
100
+ const validation = (0, validation_1.buildValidationResultFromMessage)({
101
+ filename,
102
+ filesize,
103
+ format: 'dot',
104
+ message: 'DOT appears to be binary data',
105
+ type: 'content',
106
+ description: 'DOT file content',
107
+ });
108
+ throw new validation_1.ValidationFailureError('Invalid DOT content', validation);
109
+ }
110
+ }
111
+ const { nodes, edges } = this.parseDotFile(content);
112
+ const tree = new treeStructure_1.AACTree();
113
+ tree.metadata.format = 'dot';
114
+ // Create pages for each node and add a self button representing the node label
115
+ for (const node of nodes) {
116
+ const page = new treeStructure_1.AACPage({
117
+ id: node.id,
118
+ name: node.label,
119
+ grid: [],
120
+ buttons: [],
121
+ parentId: null,
122
+ });
123
+ tree.addPage(page);
124
+ // Add a self button so single-node graphs yield one button
125
+ page.addButton(new treeStructure_1.AACButton({
126
+ id: `${node.id}_self`,
127
+ label: node.label,
128
+ message: node.label,
129
+ semanticAction: {
130
+ intent: treeStructure_1.AACSemanticIntent.SPEAK_TEXT,
131
+ text: node.label,
132
+ fallback: { type: 'SPEAK', message: node.label },
133
+ },
134
+ }));
135
+ }
136
+ // Create navigation buttons based on edges
137
+ for (const edge of edges) {
138
+ const fromPage = tree.getPage(edge.from);
139
+ if (fromPage) {
140
+ const button = new treeStructure_1.AACButton({
141
+ id: `nav_${edge.from}_${edge.to}`,
142
+ label: edge.label || edge.to,
143
+ message: '',
144
+ targetPageId: edge.to,
145
+ });
146
+ fromPage.addButton(button);
147
+ }
148
+ }
149
+ return tree;
81
150
  }
82
151
  catch (error) {
83
- // Re-throw file system errors (like file not found)
84
- if (typeof filePathOrBuffer === 'string') {
152
+ if (error instanceof validation_1.ValidationFailureError) {
85
153
  throw error;
86
154
  }
87
- // For buffer errors, return empty tree
88
- return new treeStructure_1.AACTree();
89
- }
90
- // Check if content looks like text and is non-empty
91
- if (!content || content.trim().length === 0) {
92
- return new treeStructure_1.AACTree();
93
- }
94
- // Check for binary data (contains null bytes or non-printable characters) without control-regex
95
- const head = content.substring(0, 100);
96
- let hasControl = false;
97
- for (let i = 0; i < head.length; i++) {
98
- const code = head.charCodeAt(i);
99
- // Allow UTF-8 characters (code >= 127)
100
- if (code === 0 || (code >= 0 && code <= 8) || (code >= 14 && code <= 31)) {
101
- hasControl = true;
102
- break;
103
- }
104
- }
105
- if (hasControl) {
106
- return new treeStructure_1.AACTree();
107
- }
108
- const { nodes, edges } = this.parseDotFile(content);
109
- const tree = new treeStructure_1.AACTree();
110
- tree.metadata.format = 'dot';
111
- // Create pages for each node and add a self button representing the node label
112
- for (const node of nodes) {
113
- const page = new treeStructure_1.AACPage({
114
- id: node.id,
115
- name: node.label,
116
- grid: [],
117
- buttons: [],
118
- parentId: null,
155
+ const validation = (0, validation_1.buildValidationResultFromMessage)({
156
+ filename,
157
+ filesize,
158
+ format: 'dot',
159
+ message: error?.message || 'Failed to parse DOT file',
160
+ type: 'parse',
161
+ description: 'Parse DOT graph',
119
162
  });
120
- tree.addPage(page);
121
- // Add a self button so single-node graphs yield one button
122
- page.addButton(new treeStructure_1.AACButton({
123
- id: `${node.id}_self`,
124
- label: node.label,
125
- message: node.label,
126
- semanticAction: {
127
- intent: treeStructure_1.AACSemanticIntent.SPEAK_TEXT,
128
- text: node.label,
129
- fallback: { type: 'SPEAK', message: node.label },
130
- },
131
- }));
132
- }
133
- // Create navigation buttons based on edges
134
- for (const edge of edges) {
135
- const fromPage = tree.getPage(edge.from);
136
- if (fromPage) {
137
- const button = new treeStructure_1.AACButton({
138
- id: `nav_${edge.from}_${edge.to}`,
139
- label: edge.label || edge.to,
140
- message: '',
141
- targetPageId: edge.to,
142
- });
143
- fromPage.addButton(button);
144
- }
163
+ throw new validation_1.ValidationFailureError('Failed to load DOT file', validation, error);
145
164
  }
146
- return tree;
147
165
  }
148
166
  processTexts(filePathOrBuffer, translations, outputPath) {
149
167
  const safeBuffer = Buffer.isBuffer(filePathOrBuffer)
@@ -561,7 +561,9 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
561
561
  for (let r = 0; r < maxRows; r++) {
562
562
  gridLayout[r] = new Array(maxCols).fill(null);
563
563
  }
564
- const pagePredictedWords = new Set();
564
+ // Track grid-level prediction wordlists so we can attach them to AutoContent
565
+ const gridPredictionWords = [];
566
+ let predictionCellCounter = 0;
565
567
  // Extract words from grid-level AutoContentCommands (e.g., Prediction Bar)
566
568
  if (grid.AutoContentCommands) {
567
569
  const collections = grid.AutoContentCommands.AutoContentCommandCollection;
@@ -581,7 +583,7 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
581
583
  const wordListParam = paramArr.find((p) => (p['@_Key'] || p.Key || p.key) === 'wordlist');
582
584
  if (wordListParam) {
583
585
  const words = this._extractWordsFromWordList(wordListParam);
584
- words.forEach((w) => pagePredictedWords.add(w));
586
+ gridPredictionWords.push(...words);
585
587
  }
586
588
  }
587
589
  });
@@ -657,6 +659,14 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
657
659
  const message = label; // Use caption as message
658
660
  // Detect plugin cell type (Workspace, LiveCell, AutoContent)
659
661
  const pluginMetadata = (0, pluginTypes_1.detectPluginCellType)(content);
662
+ // Default labels for prediction cells so they don't render blank
663
+ if (pluginMetadata.cellType === pluginTypes_1.Grid3CellType.AutoContent &&
664
+ pluginMetadata.autoContentType === 'Prediction') {
665
+ predictionCellCounter += 1;
666
+ if (!label) {
667
+ label = `Prediction ${predictionCellCounter}`;
668
+ }
669
+ }
660
670
  // Parse all command types from Grid3 and create semantic actions
661
671
  let semanticAction;
662
672
  let legacyAction = null;
@@ -664,6 +674,7 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
664
674
  let navigationTarget;
665
675
  let detectedCommands = []; // Store detected command metadata
666
676
  const commands = content.Commands?.Command || content.commands?.command;
677
+ let predictionWords;
667
678
  // Resolve image for this cell using FileMap and coordinate heuristics
668
679
  const imageCandidate = captionAndImage?.Image ||
669
680
  captionAndImage?.image ||
@@ -705,7 +716,9 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
705
716
  }
706
717
  if (wlP) {
707
718
  const words = this._extractWordsFromWordList(wlP);
708
- words.forEach((w) => pagePredictedWords.add(w));
719
+ if (words.length > 0) {
720
+ predictionWords = words;
721
+ }
709
722
  }
710
723
  }
711
724
  });
@@ -746,20 +759,21 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
746
759
  // Skip PredictThis in primary action loop as it was handled in pre-pass
747
760
  // unless we need a primary action and nothing else exists
748
761
  if (commandId === 'Prediction.PredictThis') {
749
- if (!semanticAction) {
750
- const wlParam = getRawParam('wordlist');
751
- if (wlParam) {
752
- const words = this._extractWordsFromWordList(wlParam);
753
- semanticAction = {
754
- category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
755
- intent: treeStructure_1.AACSemanticIntent.PLATFORM_SPECIFIC,
756
- text: words.slice(0, 3).join(', '),
757
- platformData: {
758
- grid3: { commandId, parameters: { wordlist: words } },
759
- },
760
- fallback: { type: 'ACTION', message: 'Predict words' },
761
- };
762
- }
762
+ const wlParam = getRawParam('wordlist');
763
+ const words = wlParam ? this._extractWordsFromWordList(wlParam) : [];
764
+ if (words.length > 0) {
765
+ predictionWords = words;
766
+ }
767
+ if (!semanticAction && words.length > 0) {
768
+ semanticAction = {
769
+ category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
770
+ intent: treeStructure_1.AACSemanticIntent.PLATFORM_SPECIFIC,
771
+ text: words.slice(0, 3).join(', '),
772
+ platformData: {
773
+ grid3: { commandId, parameters: { wordlist: words } },
774
+ },
775
+ fallback: { type: 'ACTION', message: 'Predict words' },
776
+ };
763
777
  }
764
778
  continue;
765
779
  }
@@ -881,13 +895,9 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
881
895
  }
882
896
  case 'Prediction.PredictThis': {
883
897
  const wlParam = getRawParam('wordlist');
884
- if (wlParam) {
885
- const words = this._extractWordsFromWordList(wlParam);
886
- // Add to page-wide set of predicted words
887
- words.forEach((w) => pagePredictedWords.add(w));
888
- // Store words in a way that analyzer can find them
889
- // For now, we'll attach to semanticAction so it can be used later
890
- // We only set this as the primary action if we don't have one yet
898
+ const words = wlParam ? this._extractWordsFromWordList(wlParam) : [];
899
+ if (words.length > 0) {
900
+ predictionWords = words;
891
901
  if (!semanticAction) {
892
902
  semanticAction = {
893
903
  category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
@@ -1214,6 +1224,15 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
1214
1224
  symbolLibraryRef: symbolLibraryRef, // Store full symbol reference
1215
1225
  grammar: isSmartGrammarCell ? grammar : undefined,
1216
1226
  isSmartGrammarCell: isSmartGrammarCell,
1227
+ predictions: predictionWords?.length
1228
+ ? [...predictionWords]
1229
+ : gridPredictionWords.length > 0
1230
+ ? [...gridPredictionWords]
1231
+ : undefined,
1232
+ predictionSlot: pluginMetadata.cellType === pluginTypes_1.Grid3CellType.AutoContent &&
1233
+ pluginMetadata.autoContentType === 'Prediction'
1234
+ ? predictionCellCounter
1235
+ : undefined,
1217
1236
  },
1218
1237
  });
1219
1238
  // Add button to page
@@ -1227,59 +1246,6 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
1227
1246
  }
1228
1247
  }
1229
1248
  });
1230
- // Process predicted words: Populate AutoContent slots first, then add virtual buttons at bottom
1231
- if (pagePredictedWords.size > 0) {
1232
- const extraWords = Array.from(pagePredictedWords).filter((w) => w.trim().length > 0);
1233
- if (extraWords.length > 0) {
1234
- let wordIdx = 0;
1235
- // Step 1: Fill dedicated AutoContent prediction slots (e.g. at the top)
1236
- page.buttons.forEach((btn) => {
1237
- if (btn.contentType === 'AutoContent' &&
1238
- btn.contentSubType === 'Prediction' &&
1239
- wordIdx < extraWords.length) {
1240
- const word = extraWords[wordIdx++];
1241
- btn.label = word;
1242
- btn.message = word;
1243
- btn.semanticAction = {
1244
- category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
1245
- intent: treeStructure_1.AACSemanticIntent.INSERT_TEXT,
1246
- text: word,
1247
- fallback: { type: 'SPEAK', message: word },
1248
- };
1249
- }
1250
- });
1251
- // Step 2: Add remaining words as virtual buttons at the bottom
1252
- if (wordIdx < extraWords.length) {
1253
- const remainingWords = extraWords.slice(wordIdx);
1254
- const extraRowsCount = Math.ceil(remainingWords.length / maxCols);
1255
- for (let r = 0; r < extraRowsCount; r++) {
1256
- const row = new Array(maxCols).fill(null);
1257
- for (let c = 0; c < maxCols; c++) {
1258
- const idx = r * maxCols + c;
1259
- if (idx < remainingWords.length) {
1260
- const word = remainingWords[idx];
1261
- const vBtn = new treeStructure_1.AACButton({
1262
- id: `${gridId}_vpredict_${wordIdx + idx}`,
1263
- label: word,
1264
- message: word,
1265
- x: c,
1266
- y: maxRows + r,
1267
- semanticAction: {
1268
- category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
1269
- intent: treeStructure_1.AACSemanticIntent.INSERT_TEXT,
1270
- text: word,
1271
- fallback: { type: 'SPEAK', message: word },
1272
- },
1273
- });
1274
- row[c] = vBtn;
1275
- page.addButton(vBtn);
1276
- }
1277
- }
1278
- gridLayout.push(row);
1279
- }
1280
- }
1281
- }
1282
- }
1283
1249
  // Set the page's grid layout
1284
1250
  page.grid = gridLayout;
1285
1251
  // Generate clone_id for each button in the grid
@@ -9,6 +9,8 @@ const treeStructure_1 = require("../core/treeStructure");
9
9
  // Removed unused import: FileProcessor
10
10
  const fast_xml_parser_1 = require("fast-xml-parser");
11
11
  const fs_1 = __importDefault(require("fs"));
12
+ const path_1 = __importDefault(require("path"));
13
+ const validation_1 = require("../validation");
12
14
  class OpmlProcessor extends baseProcessor_1.BaseProcessor {
13
15
  constructor(options) {
14
16
  super(options);
@@ -88,55 +90,91 @@ class OpmlProcessor extends baseProcessor_1.BaseProcessor {
88
90
  return texts;
89
91
  }
90
92
  loadIntoTree(filePathOrBuffer) {
91
- const content = typeof filePathOrBuffer === 'string'
92
- ? fs_1.default.readFileSync(filePathOrBuffer, 'utf8')
93
- : filePathOrBuffer.toString('utf8');
94
- if (!content || !content.trim()) {
95
- throw new Error('Empty OPML content');
96
- }
97
- // Validate XML before parsing, fast-xml-parser is permissive by default
98
- const validationResult = fast_xml_parser_1.XMLValidator.validate(content);
99
- if (validationResult !== true) {
100
- const reason = validationResult?.err?.msg || JSON.stringify(validationResult);
101
- throw new Error(`Invalid OPML XML: ${reason}`);
102
- }
103
- const parser = new fast_xml_parser_1.XMLParser({ ignoreAttributes: false });
104
- let data;
93
+ const filename = typeof filePathOrBuffer === 'string' ? path_1.default.basename(filePathOrBuffer) : 'upload.opml';
94
+ const buffer = Buffer.isBuffer(filePathOrBuffer)
95
+ ? filePathOrBuffer
96
+ : fs_1.default.readFileSync(filePathOrBuffer);
97
+ const content = buffer.toString('utf8');
105
98
  try {
106
- data = parser.parse(content);
107
- }
108
- catch (e) {
109
- throw new Error(`Invalid OPML XML: ${e?.message || String(e)}`);
110
- }
111
- const tree = new treeStructure_1.AACTree();
112
- tree.metadata.format = 'opml';
113
- // Handle case where body.outline might not exist or be in different formats
114
- const bodyOutline = data.opml?.body?.outline;
115
- if (!bodyOutline) {
116
- return tree; // Return empty tree if no outline data
117
- }
118
- const outlines = Array.isArray(bodyOutline) ? bodyOutline : [bodyOutline];
119
- let firstRootId = null;
120
- outlines.forEach((outline) => {
121
- const { page, childPages } = this.processOutline(outline);
122
- if (page && page.id) {
123
- tree.addPage(page);
124
- if (!firstRootId)
125
- firstRootId = page.id;
99
+ if (!content || !content.trim()) {
100
+ const validationResult = (0, validation_1.buildValidationResultFromMessage)({
101
+ filename,
102
+ filesize: buffer.byteLength,
103
+ format: 'opml',
104
+ message: 'Empty OPML content',
105
+ type: 'content',
106
+ description: 'OPML content is empty',
107
+ });
108
+ throw new validation_1.ValidationFailureError('Empty OPML content', validationResult);
126
109
  }
127
- childPages.forEach((childPage) => {
128
- if (childPage && childPage.id)
129
- tree.addPage(childPage);
110
+ // Validate XML before parsing, fast-xml-parser is permissive by default
111
+ const validationResult = fast_xml_parser_1.XMLValidator.validate(content);
112
+ if (validationResult !== true) {
113
+ const reason = validationResult?.err?.msg || JSON.stringify(validationResult);
114
+ const structured = (0, validation_1.buildValidationResultFromMessage)({
115
+ filename,
116
+ filesize: buffer.byteLength,
117
+ format: 'opml',
118
+ message: `Invalid OPML XML: ${reason}`,
119
+ type: 'xml',
120
+ description: 'OPML XML validation',
121
+ });
122
+ throw new validation_1.ValidationFailureError('Invalid OPML XML', structured);
123
+ }
124
+ const parser = new fast_xml_parser_1.XMLParser({ ignoreAttributes: false });
125
+ const data = parser.parse(content);
126
+ const tree = new treeStructure_1.AACTree();
127
+ tree.metadata.format = 'opml';
128
+ // Handle case where body.outline might not exist or be in different formats
129
+ const bodyOutline = data.opml?.body?.outline;
130
+ if (!bodyOutline) {
131
+ const structured = (0, validation_1.buildValidationResultFromMessage)({
132
+ filename,
133
+ filesize: buffer.byteLength,
134
+ format: 'opml',
135
+ message: 'Missing body.outline in OPML document',
136
+ type: 'structure',
137
+ description: 'OPML outline root',
138
+ });
139
+ throw new validation_1.ValidationFailureError('Invalid OPML structure', structured);
140
+ }
141
+ const outlines = Array.isArray(bodyOutline) ? bodyOutline : [bodyOutline];
142
+ let firstRootId = null;
143
+ outlines.forEach((outline) => {
144
+ const { page, childPages } = this.processOutline(outline);
145
+ if (page && page.id) {
146
+ tree.addPage(page);
147
+ if (!firstRootId)
148
+ firstRootId = page.id;
149
+ }
150
+ childPages.forEach((childPage) => {
151
+ if (childPage && childPage.id)
152
+ tree.addPage(childPage);
153
+ });
130
154
  });
131
- });
132
- // Set rootId to first root page, or fallback to first page if any exist
133
- if (firstRootId) {
134
- tree.rootId = firstRootId;
155
+ // Set rootId to first root page, or fallback to first page if any exist
156
+ if (firstRootId) {
157
+ tree.rootId = firstRootId;
158
+ }
159
+ else if (Object.keys(tree.pages).length > 0) {
160
+ tree.rootId = Object.keys(tree.pages)[0];
161
+ }
162
+ return tree;
135
163
  }
136
- else if (Object.keys(tree.pages).length > 0) {
137
- tree.rootId = Object.keys(tree.pages)[0];
164
+ catch (err) {
165
+ if (err instanceof validation_1.ValidationFailureError) {
166
+ throw err;
167
+ }
168
+ const validationResult = (0, validation_1.buildValidationResultFromMessage)({
169
+ filename,
170
+ filesize: buffer.byteLength,
171
+ format: 'opml',
172
+ message: err?.message || 'Failed to parse OPML',
173
+ type: 'parse',
174
+ description: 'Parse OPML XML',
175
+ });
176
+ throw new validation_1.ValidationFailureError('Failed to load OPML file', validationResult, err);
138
177
  }
139
- return tree;
140
178
  }
141
179
  processTexts(filePathOrBuffer, translations, outputPath) {
142
180
  const content = typeof filePathOrBuffer === 'string'
@@ -0,0 +1,10 @@
1
+ import { BaseValidator } from './baseValidator';
2
+ import { ValidationResult } from './validationTypes';
3
+ /**
4
+ * Validator for Apple Panels (.plist or .ascconfig directory)
5
+ */
6
+ export declare class ApplePanelsValidator extends BaseValidator {
7
+ static validateFile(filePath: string): Promise<ValidationResult>;
8
+ static identifyFormat(content: any, filename: string): Promise<boolean>;
9
+ validate(content: Buffer | Uint8Array, filename: string, filesize: number): Promise<ValidationResult>;
10
+ }
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.ApplePanelsValidator = void 0;
30
+ /* eslint-disable @typescript-eslint/require-await */
31
+ const fs = __importStar(require("fs"));
32
+ const path = __importStar(require("path"));
33
+ const plist_1 = __importDefault(require("plist"));
34
+ const baseValidator_1 = require("./baseValidator");
35
+ /**
36
+ * Validator for Apple Panels (.plist or .ascconfig directory)
37
+ */
38
+ class ApplePanelsValidator extends baseValidator_1.BaseValidator {
39
+ static async validateFile(filePath) {
40
+ const validator = new ApplePanelsValidator();
41
+ let content;
42
+ const filename = path.basename(filePath);
43
+ let size = 0;
44
+ const stats = fs.existsSync(filePath) ? fs.statSync(filePath) : null;
45
+ if (stats?.isDirectory() && filename.toLowerCase().endsWith('.ascconfig')) {
46
+ const panelPath = path.join(filePath, 'Contents', 'Resources', 'PanelDefinitions.plist');
47
+ if (!fs.existsSync(panelPath)) {
48
+ return validator.validate(Buffer.alloc(0), filename, 0);
49
+ }
50
+ content = fs.readFileSync(panelPath);
51
+ size = fs.statSync(panelPath).size;
52
+ }
53
+ else {
54
+ content = fs.readFileSync(filePath);
55
+ size = stats?.size || content.byteLength;
56
+ }
57
+ return validator.validate(content, filename, size);
58
+ }
59
+ static async identifyFormat(content, filename) {
60
+ const name = filename.toLowerCase();
61
+ if (name.endsWith('.plist') || name.endsWith('.ascconfig')) {
62
+ return true;
63
+ }
64
+ try {
65
+ const str = Buffer.isBuffer(content) ? content.toString('utf-8') : String(content);
66
+ const parsed = plist_1.default.parse(str);
67
+ return Boolean(parsed.panels || parsed.Panels);
68
+ }
69
+ catch {
70
+ return false;
71
+ }
72
+ }
73
+ async validate(content, filename, filesize) {
74
+ this.reset();
75
+ await this.add_check('filename', 'file extension', async () => {
76
+ if (!filename.toLowerCase().match(/\.(plist|ascconfig)$/)) {
77
+ this.warn('filename should end with .plist or .ascconfig');
78
+ }
79
+ });
80
+ let parsed = null;
81
+ await this.add_check('plist_parse', 'valid plist/XML', async () => {
82
+ try {
83
+ const str = Buffer.isBuffer(content) ? content.toString('utf-8') : String(content);
84
+ parsed = plist_1.default.parse(str);
85
+ }
86
+ catch (e) {
87
+ this.err(`Failed to parse plist: ${e.message}`, true);
88
+ }
89
+ });
90
+ if (!parsed) {
91
+ return this.buildResult(filename, filesize, 'applepanels');
92
+ }
93
+ let panels = [];
94
+ await this.add_check('panels', 'panels present', async () => {
95
+ if (Array.isArray(parsed?.panels)) {
96
+ panels = parsed?.panels;
97
+ }
98
+ else if (parsed?.Panels && typeof parsed.Panels === 'object') {
99
+ panels = Object.values(parsed.Panels);
100
+ }
101
+ else {
102
+ this.err('missing panels/PanelDefinitions content', true);
103
+ }
104
+ });
105
+ panels.slice(0, 5).forEach((panel, idx) => {
106
+ const prefix = `panel[${idx}]`;
107
+ this.add_check_sync(`${prefix}_id`, `${prefix} id`, () => {
108
+ if (!panel?.ID && !panel?.id) {
109
+ this.err('panel missing ID');
110
+ }
111
+ });
112
+ this.add_check_sync(`${prefix}_buttons`, `${prefix} buttons`, () => {
113
+ const buttons = Array.isArray(panel?.PanelObjects)
114
+ ? panel.PanelObjects.filter((obj) => obj?.PanelObjectType === 'Button')
115
+ : [];
116
+ if (buttons.length === 0) {
117
+ this.warn('panel has no buttons');
118
+ }
119
+ });
120
+ });
121
+ return this.buildResult(filename, filesize, 'applepanels');
122
+ }
123
+ }
124
+ exports.ApplePanelsValidator = ApplePanelsValidator;
@@ -0,0 +1,16 @@
1
+ import { BaseValidator } from './baseValidator';
2
+ import { ValidationResult } from './validationTypes';
3
+ /**
4
+ * Validator for Asterics Grid (.grd) JSON files
5
+ */
6
+ export declare class AstericsGridValidator extends BaseValidator {
7
+ /**
8
+ * Validate from disk
9
+ */
10
+ static validateFile(filePath: string): Promise<ValidationResult>;
11
+ /**
12
+ * Identify whether the content appears to be an Asterics .grd file
13
+ */
14
+ static identifyFormat(content: any, filename: string): Promise<boolean>;
15
+ validate(content: Buffer | Uint8Array, filename: string, filesize: number): Promise<ValidationResult>;
16
+ }