Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions examples/vitedemo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,48 @@
font-size: 13px;
}

.validation-panel {
margin-top: 15px;
}

.validation-summary {
background: #f8f9fa;
border-radius: 8px;
padding: 10px 12px;
font-size: 13px;
font-weight: 600;
margin-bottom: 10px;
}

.validation-summary.success {
border-left: 4px solid #2ecc71;
color: #2d7a4f;
}

.validation-summary.error {
border-left: 4px solid #e74c3c;
color: #b03a2e;
}

.validation-list {
max-height: 180px;
overflow-y: auto;
font-size: 12px;
}

.validation-item {
padding: 6px 0;
border-bottom: 1px solid #ececec;
}

.validation-item.warn {
color: #c18401;
}

.validation-item.error {
color: #b03a2e;
}

.processor-name {
font-weight: 600;
color: #333;
Expand Down Expand Up @@ -437,6 +479,7 @@ <h1>🎯 AAC Processors Browser Demo</h1>
</div>

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

Expand Down Expand Up @@ -467,6 +510,12 @@ <h1>🎯 AAC Processors Browser Demo</h1>
<div class="panel-title">Test Results</div>
<div id="testList"></div>
</div>

<div class="validation-panel" id="validationPanel" style="display: none;">
<div class="panel-title">Validation</div>
<div class="validation-summary" id="validationSummary"></div>
<div class="validation-list" id="validationList"></div>
</div>
</div>

<!-- Right Panel: Results -->
Expand Down
84 changes: 84 additions & 0 deletions examples/vitedemo/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import {
AACPage,
AACButton
} from 'aac-processors';
import { validateFileOrBuffer, type ValidationResult } from 'aac-processors/validation';

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

Expand All @@ -92,6 +93,7 @@ configureSqlJs({
const dropArea = document.getElementById('dropArea') as HTMLElement;
const fileInput = document.getElementById('fileInput') as HTMLInputElement;
const processBtn = document.getElementById('processBtn') as HTMLButtonElement;
const validateBtn = document.getElementById('validateBtn') as HTMLButtonElement;
const runTestsBtn = document.getElementById('runTestsBtn') as HTMLButtonElement;
const clearBtn = document.getElementById('clearBtn') as HTMLButtonElement;
const fileInfo = document.getElementById('fileInfo') as HTMLElement;
Expand All @@ -102,6 +104,9 @@ const results = document.getElementById('results') as HTMLElement;
const logPanel = document.getElementById('logPanel') as HTMLElement;
const testResults = document.getElementById('testResults') as HTMLElement;
const testList = document.getElementById('testList') as HTMLElement;
const validationPanel = document.getElementById('validationPanel') as HTMLElement;
const validationSummary = document.getElementById('validationSummary') as HTMLElement;
const validationList = document.getElementById('validationList') as HTMLElement;
const tabButtons = document.querySelectorAll('.tab-btn') as NodeListOf<HTMLButtonElement>;
const inspectTab = document.getElementById('inspectTab') as HTMLElement;
const pagesetTab = document.getElementById('pagesetTab') as HTMLElement;
Expand Down Expand Up @@ -218,6 +223,7 @@ function handleFile(file: File) {
fileDetails.textContent = extension;
fileInfo.style.display = 'block';
processBtn.disabled = true;
validateBtn.disabled = true;
return;
}

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

log(`Using processor: ${currentProcessor.constructor.name}`, 'success');
} catch (error) {
log(`Error getting processor: ${(error as Error).message}`, 'error');
processBtn.disabled = true;
validateBtn.disabled = true;
}
}

Expand Down Expand Up @@ -314,6 +322,79 @@ processBtn.addEventListener('click', async () => {
}
});

function collectValidationMessages(
result: ValidationResult,
prefix = ''
): Array<{ type: 'error' | 'warn'; message: string }> {
const messages: Array<{ type: 'error' | 'warn'; message: string }> = [];
const label = prefix ? `${prefix}: ` : '';
result.results.forEach((check) => {
if (!check.valid && check.error) {
messages.push({ type: 'error', message: `${label}${check.description}: ${check.error}` });
}
if (check.warnings?.length) {
check.warnings.forEach((warning) => {
messages.push({ type: 'warn', message: `${label}${check.description}: ${warning}` });
});
}
});
result.sub_results?.forEach((sub) => {
const nextPrefix = `${label}${sub.filename || sub.format}`;
messages.push(...collectValidationMessages(sub, nextPrefix));
});
return messages;
}

function renderValidationResult(result: ValidationResult) {
validationPanel.style.display = 'block';
validationSummary.classList.remove('success', 'error');
validationSummary.classList.add(result.valid ? 'success' : 'error');
validationSummary.textContent = `${result.valid ? '✅ Valid' : '❌ Invalid'} • ${result.format.toUpperCase()} • ${result.errors} errors, ${result.warnings} warnings`;

validationList.innerHTML = '';
const messages = collectValidationMessages(result).slice(0, 30);
if (messages.length === 0) {
const empty = document.createElement('div');
empty.className = 'validation-item';
empty.textContent = 'No issues reported.';
validationList.appendChild(empty);
return;
}

messages.forEach((entry) => {
const item = document.createElement('div');
item.className = `validation-item ${entry.type}`;
item.textContent = entry.message;
validationList.appendChild(item);
});
}

validateBtn.addEventListener('click', async () => {
if (!currentFile) return;
log('Validating file...', 'info');

try {
validateBtn.disabled = true;
const arrayBuffer = await currentFile.arrayBuffer();
const result = await validateFileOrBuffer(new Uint8Array(arrayBuffer), currentFile.name);
renderValidationResult(result);
log(
`${result.valid ? '✅' : '❌'} Validation complete: ${result.errors} errors, ${result.warnings} warnings`,
result.valid ? 'success' : 'warn'
);
} catch (error) {
const errorMsg = (error as Error).message;
validationPanel.style.display = 'block';
validationSummary.classList.remove('success');
validationSummary.classList.add('error');
validationSummary.textContent = `❌ Validation failed: ${errorMsg}`;
validationList.innerHTML = '';
log(`❌ Validation failed: ${errorMsg}`, 'error');
} finally {
validateBtn.disabled = !currentFile;
}
});

// Display results
function displayResults(tree: AACTree) {
results.innerHTML = '';
Expand Down Expand Up @@ -423,6 +504,9 @@ clearBtn.addEventListener('click', () => {
stats.style.display = 'none';
results.innerHTML = '<p style="color: #999; text-align: center; padding: 40px;">Load a file to see its contents here</p>';
testResults.style.display = 'none';
validationPanel.style.display = 'none';
validationSummary.textContent = '';
validationList.innerHTML = '';
logPanel.innerHTML = '<div class="log-entry log-info">Cleared. Ready to process files...</div>';
pagesetOutput.textContent = 'Generate or convert a pageset to preview the output JSON.';
updateConvertButtons();
Expand Down
33 changes: 26 additions & 7 deletions examples/vitedemo/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,32 @@ import path from 'path';

export default defineConfig({
resolve: {
alias: {
'aac-processors': path.resolve(__dirname, '../../src/index.browser.ts'),
stream: path.resolve(__dirname, 'node_modules/stream-browserify'),
events: path.resolve(__dirname, 'node_modules/events'),
timers: path.resolve(__dirname, 'node_modules/timers-browserify'),
util: path.resolve(__dirname, 'node_modules/util')
}
alias: [
{
find: /^aac-processors\/validation$/,
replacement: path.resolve(__dirname, '../../src/validation.ts'),
},
{
find: /^aac-processors$/,
replacement: path.resolve(__dirname, '../../src/index.browser.ts'),
},
{
find: /^stream$/,
replacement: path.resolve(__dirname, 'node_modules/stream-browserify'),
},
{
find: /^events$/,
replacement: path.resolve(__dirname, 'node_modules/events'),
},
{
find: /^timers$/,
replacement: path.resolve(__dirname, 'node_modules/timers-browserify'),
},
{
find: /^util$/,
replacement: path.resolve(__dirname, 'node_modules/util'),
},
],
},
optimizeDeps: {
exclude: ['aac-processors'],
Expand Down
20 changes: 18 additions & 2 deletions src/utils/io.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,24 @@ export function isNodeRuntime(): boolean {
}

export function getBasename(filePath: string): string {
const parts = filePath.split(/[/\\]/);
return parts[parts.length - 1] || filePath;
const trimmed = filePath.replace(/[/\\]+$/, '') || filePath;
const parts = trimmed.split(/[/\\]/);
return parts[parts.length - 1] || trimmed;
}

export function toUint8Array(input: Uint8Array | ArrayBuffer | Buffer): Uint8Array {
if (input instanceof Uint8Array) {
return input;
}
return new Uint8Array(input);
}

export function toArrayBuffer(input: Uint8Array | ArrayBuffer | Buffer): ArrayBuffer {
if (input instanceof ArrayBuffer) {
return input;
}
const view = input instanceof Uint8Array ? input : new Uint8Array(input);
return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
}

export function decodeText(input: Uint8Array): string {
Expand Down
18 changes: 13 additions & 5 deletions src/validation/applePanelsValidator.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
/* eslint-disable @typescript-eslint/require-await */
import * as fs from 'fs';
import * as path from 'path';
import plist from 'plist';
import { BaseValidator } from './baseValidator';
import { ValidationResult } from './validationTypes';
import { decodeText, getBasename, getFs, getPath, toUint8Array } from '../utils/io';

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

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

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

try {
const str = Buffer.isBuffer(content) ? content.toString('utf-8') : String(content);
if (
typeof content !== 'string' &&
!(content instanceof ArrayBuffer) &&
!(content instanceof Uint8Array)
) {
return false;
}
const str = typeof content === 'string' ? content : decodeText(toUint8Array(content));
const parsed = plist.parse(str) as PanelsContainer;
return Boolean(parsed.panels || parsed.Panels);
} catch {
Expand All @@ -64,7 +72,7 @@ export class ApplePanelsValidator extends BaseValidator {
let parsed: PanelsContainer | null = null;
await this.add_check('plist_parse', 'valid plist/XML', async () => {
try {
const str = Buffer.isBuffer(content) ? content.toString('utf-8') : String(content);
const str = decodeText(content);
parsed = plist.parse(str) as PanelsContainer;
} catch (e: any) {
this.err(`Failed to parse plist: ${e.message}`, true);
Expand Down
20 changes: 13 additions & 7 deletions src/validation/astericsValidator.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
/* eslint-disable @typescript-eslint/require-await */
import * as fs from 'fs';
import * as path from 'path';
import { BaseValidator } from './baseValidator';
import { ValidationResult } from './validationTypes';
import { decodeText, getBasename, getFs, readBinaryFromInput, toUint8Array } from '../utils/io';

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

/**
Expand All @@ -28,7 +27,14 @@ export class AstericsGridValidator extends BaseValidator {
}

try {
const str = Buffer.isBuffer(content) ? content.toString('utf-8') : String(content);
if (
typeof content !== 'string' &&
!(content instanceof ArrayBuffer) &&
!(content instanceof Uint8Array)
) {
return false;
}
const str = typeof content === 'string' ? content : decodeText(toUint8Array(content));
const json = JSON.parse(str);
return Array.isArray(json?.grids);
} catch {
Expand All @@ -52,7 +58,7 @@ export class AstericsGridValidator extends BaseValidator {
let json: any = null;
await this.add_check('json_parse', 'valid JSON', async () => {
try {
let str = Buffer.isBuffer(content) ? content.toString('utf-8') : String(content);
let str = decodeText(content);
if (str.charCodeAt(0) === 0xfeff) {
str = str.slice(1);
}
Expand Down
Loading