Skip to content

Commit 37dcb16

Browse files
authored
Merge pull request #17 from willwade/codex/refactor-validator-code-for-environment-compatibility
Refactor validators to use shared IO helpers for file access
2 parents 89ad805 + 4ab93f3 commit 37dcb16

15 files changed

Lines changed: 296 additions & 107 deletions

examples/vitedemo/index.html

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,48 @@
191191
font-size: 13px;
192192
}
193193

194+
.validation-panel {
195+
margin-top: 15px;
196+
}
197+
198+
.validation-summary {
199+
background: #f8f9fa;
200+
border-radius: 8px;
201+
padding: 10px 12px;
202+
font-size: 13px;
203+
font-weight: 600;
204+
margin-bottom: 10px;
205+
}
206+
207+
.validation-summary.success {
208+
border-left: 4px solid #2ecc71;
209+
color: #2d7a4f;
210+
}
211+
212+
.validation-summary.error {
213+
border-left: 4px solid #e74c3c;
214+
color: #b03a2e;
215+
}
216+
217+
.validation-list {
218+
max-height: 180px;
219+
overflow-y: auto;
220+
font-size: 12px;
221+
}
222+
223+
.validation-item {
224+
padding: 6px 0;
225+
border-bottom: 1px solid #ececec;
226+
}
227+
228+
.validation-item.warn {
229+
color: #c18401;
230+
}
231+
232+
.validation-item.error {
233+
color: #b03a2e;
234+
}
235+
194236
.processor-name {
195237
font-weight: 600;
196238
color: #333;
@@ -437,6 +479,7 @@ <h1>🎯 AAC Processors Browser Demo</h1>
437479
</div>
438480

439481
<button class="btn" id="processBtn" disabled>🚀 Process File</button>
482+
<button class="btn btn-secondary" id="validateBtn" disabled>✅ Validate File</button>
440483
<button class="btn btn-secondary" id="runTestsBtn">🧪 Run Compatibility Tests</button>
441484
<button class="btn btn-secondary" id="clearBtn">🗑️ Clear Results</button>
442485

@@ -467,6 +510,12 @@ <h1>🎯 AAC Processors Browser Demo</h1>
467510
<div class="panel-title">Test Results</div>
468511
<div id="testList"></div>
469512
</div>
513+
514+
<div class="validation-panel" id="validationPanel" style="display: none;">
515+
<div class="panel-title">Validation</div>
516+
<div class="validation-summary" id="validationSummary"></div>
517+
<div class="validation-list" id="validationList"></div>
518+
</div>
470519
</div>
471520

472521
<!-- Right Panel: Results -->

examples/vitedemo/src/main.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import {
8181
AACPage,
8282
AACButton
8383
} from 'aac-processors';
84+
import { validateFileOrBuffer, type ValidationResult } from 'aac-processors/validation';
8485

8586
import sqlWasmUrl from 'sql.js/dist/sql-wasm.wasm?url';
8687

@@ -92,6 +93,7 @@ configureSqlJs({
9293
const dropArea = document.getElementById('dropArea') as HTMLElement;
9394
const fileInput = document.getElementById('fileInput') as HTMLInputElement;
9495
const processBtn = document.getElementById('processBtn') as HTMLButtonElement;
96+
const validateBtn = document.getElementById('validateBtn') as HTMLButtonElement;
9597
const runTestsBtn = document.getElementById('runTestsBtn') as HTMLButtonElement;
9698
const clearBtn = document.getElementById('clearBtn') as HTMLButtonElement;
9799
const fileInfo = document.getElementById('fileInfo') as HTMLElement;
@@ -102,6 +104,9 @@ const results = document.getElementById('results') as HTMLElement;
102104
const logPanel = document.getElementById('logPanel') as HTMLElement;
103105
const testResults = document.getElementById('testResults') as HTMLElement;
104106
const testList = document.getElementById('testList') as HTMLElement;
107+
const validationPanel = document.getElementById('validationPanel') as HTMLElement;
108+
const validationSummary = document.getElementById('validationSummary') as HTMLElement;
109+
const validationList = document.getElementById('validationList') as HTMLElement;
105110
const tabButtons = document.querySelectorAll('.tab-btn') as NodeListOf<HTMLButtonElement>;
106111
const inspectTab = document.getElementById('inspectTab') as HTMLElement;
107112
const pagesetTab = document.getElementById('pagesetTab') as HTMLElement;
@@ -218,6 +223,7 @@ function handleFile(file: File) {
218223
fileDetails.textContent = extension;
219224
fileInfo.style.display = 'block';
220225
processBtn.disabled = true;
226+
validateBtn.disabled = true;
221227
return;
222228
}
223229

@@ -228,12 +234,14 @@ function handleFile(file: File) {
228234
fileDetails.textContent = `${file.name}${formatFileSize(file.size)}`;
229235
fileInfo.style.display = 'block';
230236
processBtn.disabled = false;
237+
validateBtn.disabled = false;
231238
currentSourceLabel = file.name;
232239

233240
log(`Using processor: ${currentProcessor.constructor.name}`, 'success');
234241
} catch (error) {
235242
log(`Error getting processor: ${(error as Error).message}`, 'error');
236243
processBtn.disabled = true;
244+
validateBtn.disabled = true;
237245
}
238246
}
239247

@@ -314,6 +322,79 @@ processBtn.addEventListener('click', async () => {
314322
}
315323
});
316324

325+
function collectValidationMessages(
326+
result: ValidationResult,
327+
prefix = ''
328+
): Array<{ type: 'error' | 'warn'; message: string }> {
329+
const messages: Array<{ type: 'error' | 'warn'; message: string }> = [];
330+
const label = prefix ? `${prefix}: ` : '';
331+
result.results.forEach((check) => {
332+
if (!check.valid && check.error) {
333+
messages.push({ type: 'error', message: `${label}${check.description}: ${check.error}` });
334+
}
335+
if (check.warnings?.length) {
336+
check.warnings.forEach((warning) => {
337+
messages.push({ type: 'warn', message: `${label}${check.description}: ${warning}` });
338+
});
339+
}
340+
});
341+
result.sub_results?.forEach((sub) => {
342+
const nextPrefix = `${label}${sub.filename || sub.format}`;
343+
messages.push(...collectValidationMessages(sub, nextPrefix));
344+
});
345+
return messages;
346+
}
347+
348+
function renderValidationResult(result: ValidationResult) {
349+
validationPanel.style.display = 'block';
350+
validationSummary.classList.remove('success', 'error');
351+
validationSummary.classList.add(result.valid ? 'success' : 'error');
352+
validationSummary.textContent = `${result.valid ? '✅ Valid' : '❌ Invalid'}${result.format.toUpperCase()}${result.errors} errors, ${result.warnings} warnings`;
353+
354+
validationList.innerHTML = '';
355+
const messages = collectValidationMessages(result).slice(0, 30);
356+
if (messages.length === 0) {
357+
const empty = document.createElement('div');
358+
empty.className = 'validation-item';
359+
empty.textContent = 'No issues reported.';
360+
validationList.appendChild(empty);
361+
return;
362+
}
363+
364+
messages.forEach((entry) => {
365+
const item = document.createElement('div');
366+
item.className = `validation-item ${entry.type}`;
367+
item.textContent = entry.message;
368+
validationList.appendChild(item);
369+
});
370+
}
371+
372+
validateBtn.addEventListener('click', async () => {
373+
if (!currentFile) return;
374+
log('Validating file...', 'info');
375+
376+
try {
377+
validateBtn.disabled = true;
378+
const arrayBuffer = await currentFile.arrayBuffer();
379+
const result = await validateFileOrBuffer(new Uint8Array(arrayBuffer), currentFile.name);
380+
renderValidationResult(result);
381+
log(
382+
`${result.valid ? '✅' : '❌'} Validation complete: ${result.errors} errors, ${result.warnings} warnings`,
383+
result.valid ? 'success' : 'warn'
384+
);
385+
} catch (error) {
386+
const errorMsg = (error as Error).message;
387+
validationPanel.style.display = 'block';
388+
validationSummary.classList.remove('success');
389+
validationSummary.classList.add('error');
390+
validationSummary.textContent = `❌ Validation failed: ${errorMsg}`;
391+
validationList.innerHTML = '';
392+
log(`❌ Validation failed: ${errorMsg}`, 'error');
393+
} finally {
394+
validateBtn.disabled = !currentFile;
395+
}
396+
});
397+
317398
// Display results
318399
function displayResults(tree: AACTree) {
319400
results.innerHTML = '';
@@ -423,6 +504,9 @@ clearBtn.addEventListener('click', () => {
423504
stats.style.display = 'none';
424505
results.innerHTML = '<p style="color: #999; text-align: center; padding: 40px;">Load a file to see its contents here</p>';
425506
testResults.style.display = 'none';
507+
validationPanel.style.display = 'none';
508+
validationSummary.textContent = '';
509+
validationList.innerHTML = '';
426510
logPanel.innerHTML = '<div class="log-entry log-info">Cleared. Ready to process files...</div>';
427511
pagesetOutput.textContent = 'Generate or convert a pageset to preview the output JSON.';
428512
updateConvertButtons();

examples/vitedemo/vite.config.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,32 @@ import path from 'path';
33

44
export default defineConfig({
55
resolve: {
6-
alias: {
7-
'aac-processors': path.resolve(__dirname, '../../src/index.browser.ts'),
8-
stream: path.resolve(__dirname, 'node_modules/stream-browserify'),
9-
events: path.resolve(__dirname, 'node_modules/events'),
10-
timers: path.resolve(__dirname, 'node_modules/timers-browserify'),
11-
util: path.resolve(__dirname, 'node_modules/util')
12-
}
6+
alias: [
7+
{
8+
find: /^aac-processors\/validation$/,
9+
replacement: path.resolve(__dirname, '../../src/validation.ts'),
10+
},
11+
{
12+
find: /^aac-processors$/,
13+
replacement: path.resolve(__dirname, '../../src/index.browser.ts'),
14+
},
15+
{
16+
find: /^stream$/,
17+
replacement: path.resolve(__dirname, 'node_modules/stream-browserify'),
18+
},
19+
{
20+
find: /^events$/,
21+
replacement: path.resolve(__dirname, 'node_modules/events'),
22+
},
23+
{
24+
find: /^timers$/,
25+
replacement: path.resolve(__dirname, 'node_modules/timers-browserify'),
26+
},
27+
{
28+
find: /^util$/,
29+
replacement: path.resolve(__dirname, 'node_modules/util'),
30+
},
31+
],
1332
},
1433
optimizeDeps: {
1534
exclude: ['aac-processors'],

src/utils/io.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,24 @@ export function isNodeRuntime(): boolean {
7979
}
8080

8181
export function getBasename(filePath: string): string {
82-
const parts = filePath.split(/[/\\]/);
83-
return parts[parts.length - 1] || filePath;
82+
const trimmed = filePath.replace(/[/\\]+$/, '') || filePath;
83+
const parts = trimmed.split(/[/\\]/);
84+
return parts[parts.length - 1] || trimmed;
85+
}
86+
87+
export function toUint8Array(input: Uint8Array | ArrayBuffer | Buffer): Uint8Array {
88+
if (input instanceof Uint8Array) {
89+
return input;
90+
}
91+
return new Uint8Array(input);
92+
}
93+
94+
export function toArrayBuffer(input: Uint8Array | ArrayBuffer | Buffer): ArrayBuffer {
95+
if (input instanceof ArrayBuffer) {
96+
return input;
97+
}
98+
const view = input instanceof Uint8Array ? input : new Uint8Array(input);
99+
return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
84100
}
85101

86102
export function decodeText(input: Uint8Array): string {

src/validation/applePanelsValidator.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
/* eslint-disable @typescript-eslint/require-await */
2-
import * as fs from 'fs';
3-
import * as path from 'path';
42
import plist from 'plist';
53
import { BaseValidator } from './baseValidator';
64
import { ValidationResult } from './validationTypes';
5+
import { decodeText, getBasename, getFs, getPath, toUint8Array } from '../utils/io';
76

87
type PanelsContainer = { panels?: any; Panels?: Record<string, any> };
98

@@ -13,8 +12,10 @@ type PanelsContainer = { panels?: any; Panels?: Record<string, any> };
1312
export class ApplePanelsValidator extends BaseValidator {
1413
static async validateFile(filePath: string): Promise<ValidationResult> {
1514
const validator = new ApplePanelsValidator();
15+
const fs = getFs();
16+
const path = getPath();
1617
let content: Buffer;
17-
const filename = path.basename(filePath);
18+
const filename = getBasename(filePath);
1819
let size = 0;
1920

2021
const stats = fs.existsSync(filePath) ? fs.statSync(filePath) : null;
@@ -40,7 +41,14 @@ export class ApplePanelsValidator extends BaseValidator {
4041
}
4142

4243
try {
43-
const str = Buffer.isBuffer(content) ? content.toString('utf-8') : String(content);
44+
if (
45+
typeof content !== 'string' &&
46+
!(content instanceof ArrayBuffer) &&
47+
!(content instanceof Uint8Array)
48+
) {
49+
return false;
50+
}
51+
const str = typeof content === 'string' ? content : decodeText(toUint8Array(content));
4452
const parsed = plist.parse(str) as PanelsContainer;
4553
return Boolean(parsed.panels || parsed.Panels);
4654
} catch {
@@ -64,7 +72,7 @@ export class ApplePanelsValidator extends BaseValidator {
6472
let parsed: PanelsContainer | null = null;
6573
await this.add_check('plist_parse', 'valid plist/XML', async () => {
6674
try {
67-
const str = Buffer.isBuffer(content) ? content.toString('utf-8') : String(content);
75+
const str = decodeText(content);
6876
parsed = plist.parse(str) as PanelsContainer;
6977
} catch (e: any) {
7078
this.err(`Failed to parse plist: ${e.message}`, true);

src/validation/astericsValidator.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
/* eslint-disable @typescript-eslint/require-await */
2-
import * as fs from 'fs';
3-
import * as path from 'path';
42
import { BaseValidator } from './baseValidator';
53
import { ValidationResult } from './validationTypes';
4+
import { decodeText, getBasename, getFs, readBinaryFromInput, toUint8Array } from '../utils/io';
65

76
/**
87
* Validator for Asterics Grid (.grd) JSON files
@@ -13,9 +12,9 @@ export class AstericsGridValidator extends BaseValidator {
1312
*/
1413
static async validateFile(filePath: string): Promise<ValidationResult> {
1514
const validator = new AstericsGridValidator();
16-
const content = fs.readFileSync(filePath);
17-
const stats = fs.statSync(filePath);
18-
return validator.validate(content, path.basename(filePath), stats.size);
15+
const content = readBinaryFromInput(filePath);
16+
const stats = getFs().statSync(filePath);
17+
return validator.validate(content, getBasename(filePath), stats.size);
1918
}
2019

2120
/**
@@ -28,7 +27,14 @@ export class AstericsGridValidator extends BaseValidator {
2827
}
2928

3029
try {
31-
const str = Buffer.isBuffer(content) ? content.toString('utf-8') : String(content);
30+
if (
31+
typeof content !== 'string' &&
32+
!(content instanceof ArrayBuffer) &&
33+
!(content instanceof Uint8Array)
34+
) {
35+
return false;
36+
}
37+
const str = typeof content === 'string' ? content : decodeText(toUint8Array(content));
3238
const json = JSON.parse(str);
3339
return Array.isArray(json?.grids);
3440
} catch {
@@ -52,7 +58,7 @@ export class AstericsGridValidator extends BaseValidator {
5258
let json: any = null;
5359
await this.add_check('json_parse', 'valid JSON', async () => {
5460
try {
55-
let str = Buffer.isBuffer(content) ? content.toString('utf-8') : String(content);
61+
let str = decodeText(content);
5662
if (str.charCodeAt(0) === 0xfeff) {
5763
str = str.slice(1);
5864
}

0 commit comments

Comments
 (0)