@ordojs/accessibility
Version:
Comprehensive accessibility system for OrdoJS with ARIA generation, automated testing, and screen reader support
838 lines (835 loc) • 27.4 kB
JavaScript
import chalk from 'chalk';
import { Command } from 'commander';
import fs from 'fs-extra';
import { EventEmitter } from 'events';
// src/cli.ts
var AccessibilityManager = class extends EventEmitter {
config;
isInitialized;
auditResults;
violations;
/**
* Create a new AccessibilityManager instance
*
* @param config - Accessibility configuration
*/
constructor(config = {}) {
super();
this.config = {
enableARIA: true,
enableTesting: true,
enableFocusManagement: true,
enableScreenReader: true,
enableKeyboardNavigation: true,
enableColorContrast: true,
enableSemanticHTML: true,
enableLiveRegions: true,
enableSkipLinks: true,
enableFocusIndicators: true,
wcagLevel: "AA",
customARIA: {},
testing: {
enabled: true,
framework: "axe-core",
rules: [],
ignoreRules: [],
timeout: 3e4,
retries: 3,
generateReports: true,
reportFormat: "json",
reportDir: "./accessibility-reports"
},
focus: {
enabled: true,
focusTrap: true,
focusIndicators: true,
skipLinks: true,
focusOrder: "tab",
focusRestoration: true,
focusDelegation: false
},
screenReader: {
enabled: true,
announcements: true,
liveRegions: true,
ariaLabels: true,
ariaDescriptions: true,
ariaLandmarks: true,
ariaRoles: true,
ariaStates: true,
ariaProperties: true
},
...config
};
this.isInitialized = false;
this.auditResults = /* @__PURE__ */ new Map();
this.violations = /* @__PURE__ */ new Map();
}
/**
* Initialize the accessibility system
*/
async initialize() {
if (this.isInitialized) {
console.warn("Accessibility system is already initialized");
return;
}
try {
if (this.config.enableARIA) {
await this.initializeARIA();
}
if (this.config.enableTesting) {
await this.initializeTesting();
}
if (this.config.enableFocusManagement) {
await this.initializeFocusManagement();
}
if (this.config.enableScreenReader) {
await this.initializeScreenReader();
}
this.isInitialized = true;
console.log("Accessibility system initialized successfully");
this.emit("initialized");
} catch (error) {
console.error("Failed to initialize accessibility system:", error);
this.emit("error", error);
throw error;
}
}
/**
* Run accessibility audit
*
* @param url - URL to audit
* @param options - Audit options
* @returns Audit result
*/
async runAudit(url, options = {}) {
if (!this.isInitialized) {
throw new Error("Accessibility system not initialized");
}
try {
const auditId = `audit_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const startTime = Date.now();
console.log(`Running accessibility audit for ${url}...`);
const testResults = [];
if (this.config.enableTesting) {
const testingResults = await this.runAccessibilityTests(url, options);
testResults.push(...testingResults);
}
const violations = [];
for (const result of testResults) {
violations.push(...result.violations);
}
const totalTests = testResults.length;
const passedTests = testResults.filter((r) => r.status === "pass").length;
const score = totalTests > 0 ? passedTests / totalTests * 100 : 0;
const level = options.level || this.config.wcagLevel;
const compliant = this.isCompliant(violations, level);
const summary = this.createAuditSummary(violations, testResults);
const audit = {
id: auditId,
timestamp: /* @__PURE__ */ new Date(),
url,
violations,
passes: testResults.filter((r) => r.status === "pass"),
inapplicable: testResults.filter((r) => r.status === "inapplicable"),
score,
level,
compliant,
summary,
metadata: {
config: this.config,
options,
duration: Date.now() - startTime
}
};
this.auditResults.set(auditId, audit);
this.violations.set(auditId, violations);
console.log(`Accessibility audit completed: ${score.toFixed(1)}% score`);
this.emit("auditCompleted", audit);
return audit;
} catch (error) {
console.error("Failed to run accessibility audit:", error);
this.emit("error", error);
throw error;
}
}
/**
* Generate ARIA attributes for an element
*
* @param element - Element to generate ARIA for
* @param context - Element context
* @returns Generated ARIA attributes
*/
generateARIA(element, context = {}) {
if (!this.config.enableARIA) {
return {};
}
const ariaAttributes = {};
if (context.role) {
ariaAttributes["role"] = context.role;
}
if (context.label) {
ariaAttributes["aria-label"] = context.label;
}
if (context.description) {
ariaAttributes["aria-describedby"] = context.description;
}
if (context.state) {
for (const [key, value] of Object.entries(context.state)) {
ariaAttributes[`aria-${key}`] = String(value);
}
}
if (context.properties) {
for (const [key, value] of Object.entries(context.properties)) {
ariaAttributes[`aria-${key}`] = String(value);
}
}
return ariaAttributes;
}
/**
* Check color contrast
*
* @param foreground - Foreground color
* @param background - Background color
* @returns Color contrast information
*/
checkColorContrast(foreground, background) {
if (!this.config.enableColorContrast) {
return {
ratio: 0,
wcagAA: false,
wcagAAA: false,
largeText: false,
uiComponent: false,
suggestions: []
};
}
const ratio = this.calculateContrastRatio(foreground, background);
const wcagAA = ratio >= 4.5;
const wcagAAA = ratio >= 7;
const largeText = ratio >= 3;
const uiComponent = ratio >= 3;
const suggestions = [];
if (!wcagAA) {
suggestions.push("Increase contrast ratio to meet WCAG AA standards (4.5:1)");
}
if (!wcagAAA) {
suggestions.push("Increase contrast ratio to meet WCAG AAA standards (7:1)");
}
return {
ratio,
wcagAA,
wcagAAA,
largeText,
uiComponent,
suggestions
};
}
/**
* Generate semantic HTML
*
* @param content - Content to make semantic
* @param options - Semantic options
* @returns Semantic HTML
*/
generateSemanticHTML(content, options = {}) {
if (!this.config.enableSemanticHTML) {
return content;
}
let semanticContent = content;
if (options.headingLevel) {
semanticContent = this.addHeadingStructure(semanticContent, options.headingLevel);
}
if (options.listType) {
semanticContent = this.addListStructure(semanticContent, options.listType);
}
if (options.tableHeaders) {
semanticContent = this.addTableStructure(semanticContent, options.tableHeaders);
}
if (options.formLabels) {
semanticContent = this.addFormStructure(semanticContent, options.formLabels);
}
return semanticContent;
}
/**
* Get accessibility statistics
*
* @returns Statistics
*/
getStats() {
const totalAudits = this.auditResults.size;
let totalViolations = 0;
let totalScore = 0;
for (const audit of this.auditResults.values()) {
totalViolations += audit.violations.length;
totalScore += audit.score;
}
const averageScore = totalAudits > 0 ? totalScore / totalAudits : 0;
const complianceRate = totalAudits > 0 ? Array.from(this.auditResults.values()).filter((a) => a.compliant).length / totalAudits : 0;
return {
totalAudits,
totalViolations,
averageScore,
complianceRate,
config: this.config
};
}
/**
* Update accessibility configuration
*
* @param newConfig - New configuration
*/
updateConfig(newConfig) {
this.config = { ...this.config, ...newConfig };
this.emit("configUpdated", this.config);
}
/**
* Get audit result by ID
*
* @param auditId - Audit ID
* @returns Audit result or undefined
*/
getAuditResult(auditId) {
return this.auditResults.get(auditId);
}
/**
* Get all audit results
*
* @returns Array of audit results
*/
getAllAuditResults() {
return Array.from(this.auditResults.values());
}
/**
* Clear audit results
*/
clearAuditResults() {
this.auditResults.clear();
this.violations.clear();
this.emit("auditResultsCleared");
}
/**
* Initialize ARIA system
*/
async initializeARIA() {
console.log("Initializing ARIA system...");
}
/**
* Initialize testing system
*/
async initializeTesting() {
console.log("Initializing testing system...");
}
/**
* Initialize focus management
*/
async initializeFocusManagement() {
console.log("Initializing focus management...");
}
/**
* Initialize screen reader support
*/
async initializeScreenReader() {
console.log("Initializing screen reader support...");
}
/**
* Run accessibility tests
*
* @param url - URL to test
* @param options - Test options
* @returns Test results
*/
async runAccessibilityTests(url, options = {}) {
const results = [];
results.push({
id: "test_1",
name: "Color Contrast",
status: "pass",
description: "Check color contrast ratios",
impact: "serious",
violations: [],
passes: [],
inapplicable: [],
timestamp: /* @__PURE__ */ new Date(),
duration: 1e3,
url,
metadata: {}
});
return results;
}
/**
* Check if violations are compliant with WCAG level
*
* @param violations - Violations to check
* @param level - WCAG level
* @returns True if compliant
*/
isCompliant(violations, level) {
const criticalViolations = violations.filter((v) => v.impact === "critical");
const seriousViolations = violations.filter((v) => v.impact === "serious");
if (criticalViolations.length > 0) {
return false;
}
switch (level) {
case "A":
return seriousViolations.length <= 5;
case "AA":
return seriousViolations.length <= 2;
case "AAA":
return seriousViolations.length === 0;
default:
return false;
}
}
/**
* Create audit summary
*
* @param violations - Violations
* @param testResults - Test results
* @returns Audit summary
*/
createAuditSummary(violations, testResults) {
const criticalViolations = violations.filter((v) => v.impact === "critical").length;
const seriousViolations = violations.filter((v) => v.impact === "serious").length;
const moderateViolations = violations.filter((v) => v.impact === "moderate").length;
const minorViolations = violations.filter((v) => v.impact === "minor").length;
const totalPasses = testResults.filter((r) => r.status === "pass").length;
const totalInapplicable = testResults.filter((r) => r.status === "inapplicable").length;
return {
totalViolations: violations.length,
criticalViolations,
seriousViolations,
moderateViolations,
minorViolations,
totalPasses,
totalInapplicable
};
}
/**
* Calculate contrast ratio
*
* @param foreground - Foreground color
* @param background - Background color
* @returns Contrast ratio
*/
calculateContrastRatio(foreground, background) {
return 4.5;
}
/**
* Add heading structure
*
* @param content - Content
* @param level - Heading level
* @returns Content with heading structure
*/
addHeadingStructure(content, level) {
return content;
}
/**
* Add list structure
*
* @param content - Content
* @param type - List type
* @returns Content with list structure
*/
addListStructure(content, type) {
return content;
}
/**
* Add table structure
*
* @param content - Content
* @param headers - Table headers
* @returns Content with table structure
*/
addTableStructure(content, headers) {
return content;
}
/**
* Add form structure
*
* @param content - Content
* @param labels - Form labels
* @returns Content with form structure
*/
addFormStructure(content, labels) {
return content;
}
};
// src/cli.ts
var AccessibilityCLI = class {
program;
manager;
/**
* Create a new AccessibilityCLI instance
*/
constructor() {
this.program = new Command();
this.manager = new AccessibilityManager();
this.setupCommands();
}
/**
* Setup CLI commands
*/
setupCommands() {
this.program.name("ordojs-a11y").description("OrdoJS Accessibility Testing and Management CLI").version("0.1.0");
this.program.command("audit").description("Run accessibility audit on a URL or file").argument("<target>", "URL or file path to audit").option("-l, --level <level>", "WCAG compliance level", "AA").option("-r, --rules <rules>", "Comma-separated list of rules to test").option("-i, --ignore <rules>", "Comma-separated list of rules to ignore").option("-t, --timeout <ms>", "Test timeout in milliseconds", "30000").option("-o, --output <format>", "Output format (json, html, csv)", "json").option("-f, --file <path>", "Output file path").action(async (target, options) => {
await this.runAudit(target, options);
});
this.program.command("test").description("Run specific accessibility tests").argument("<target>", "URL or file path to test").option("-n, --name <name>", "Specific test name to run").option("-f, --framework <framework>", "Testing framework (axe-core, puppeteer, jsdom)", "axe-core").option("-t, --timeout <ms>", "Test timeout in milliseconds", "30000").option("-r, --retries <count>", "Number of retries", "3").option("-o, --output <format>", "Output format (json, html, csv)", "json").option("-f, --file <path>", "Output file path").action(async (target, options) => {
await this.runTest(target, options);
});
this.program.command("generate-aria").description("Generate ARIA attributes for HTML elements").argument("<input>", "Input HTML file or directory").option("-o, --output <path>", "Output directory", "./aria-output").option("-f, --format <format>", "Output format (html, json)", "html").option("-v, --validate", "Validate generated ARIA attributes").action(async (input, options) => {
await this.generateARIA(input, options);
});
this.program.command("focus").description("Manage focus and keyboard navigation").option("-a, --analyze <file>", "Analyze focus order in HTML file").option("-g, --generate <file>", "Generate focus management code").option("-t, --test <file>", "Test keyboard navigation").action(async (options) => {
await this.manageFocus(options);
});
this.program.command("screen-reader").description("Manage screen reader announcements and live regions").option("-a, --announce <message>", "Create announcement").option("-l, --live-region <id>", "Create live region").option("-u, --update <id> <content>", "Update live region content").option("-r, --remove <id>", "Remove live region").action(async (options) => {
await this.manageScreenReader(options);
});
this.program.command("report").description("Generate accessibility reports").option("-i, --input <path>", "Input audit results file").option("-o, --output <path>", "Output report file").option("-f, --format <format>", "Report format (html, json, csv)", "html").option("-s, --summary", "Include summary statistics").option("-d, --detailed", "Include detailed violations").action(async (options) => {
await this.generateReport(options);
});
this.program.command("stats").description("Show accessibility statistics").option("-a, --audit <id>", "Show audit statistics").option("-t, --test <id>", "Show test statistics").option("-f, --focus", "Show focus management statistics").option("-s, --screen-reader", "Show screen reader statistics").action(async (options) => {
await this.showStats(options);
});
}
/**
* Run accessibility audit
*
* @param target - Target URL or file
* @param options - Audit options
*/
async runAudit(target, options) {
try {
console.log(chalk.blue(`Running accessibility audit on ${target}...`));
await this.manager.initialize();
const level = options.level;
const rules = options.rules ? options.rules.split(",") : [];
const ignoreRules = options.ignore ? options.ignore.split(",") : [];
const timeout = parseInt(options.timeout);
const audit = await this.manager.runAudit(target, {
level,
rules,
ignoreRules,
timeout
});
this.displayAuditResults(audit);
if (options.file) {
await this.saveResults(audit, options.file, options.output);
}
console.log(chalk.green("Audit completed successfully"));
} catch (error) {
console.error(chalk.red("Audit failed:"), error);
process.exit(1);
}
}
/**
* Run accessibility test
*
* @param target - Target URL or file
* @param options - Test options
*/
async runTest(target, options) {
try {
console.log(chalk.blue(`Running accessibility test on ${target}...`));
await this.manager.initialize();
const testName = options.name;
const framework = options.framework;
const timeout = parseInt(options.timeout);
const retries = parseInt(options.retries);
if (testName) {
console.log(chalk.yellow("Specific test functionality not yet implemented"));
} else {
console.log(chalk.yellow("Test functionality not yet implemented"));
}
console.log(chalk.green("Test completed successfully"));
} catch (error) {
console.error(chalk.red("Test failed:"), error);
process.exit(1);
}
}
/**
* Generate ARIA attributes
*
* @param input - Input file or directory
* @param options - Generation options
*/
async generateARIA(input, options) {
try {
console.log(chalk.blue(`Generating ARIA attributes for ${input}...`));
await this.manager.initialize();
if (!fs.existsSync(input)) {
throw new Error(`Input path does not exist: ${input}`);
}
const stats = fs.statSync(input);
if (stats.isFile()) {
await this.generateARIAForFile(input, options);
} else if (stats.isDirectory()) {
await this.generateARIAForDirectory(input, options);
}
console.log(chalk.green("ARIA generation completed successfully"));
} catch (error) {
console.error(chalk.red("ARIA generation failed:"), error);
process.exit(1);
}
}
/**
* Manage focus and keyboard navigation
*
* @param options - Focus options
*/
async manageFocus(options) {
try {
console.log(chalk.blue("Managing focus and keyboard navigation..."));
await this.manager.initialize();
if (options.analyze) {
await this.analyzeFocusOrder(options.analyze);
} else if (options.generate) {
await this.generateFocusCode(options.generate);
} else if (options.test) {
await this.testKeyboardNavigation(options.test);
} else {
console.log(chalk.yellow("No focus action specified. Use --analyze, --generate, or --test"));
}
console.log(chalk.green("Focus management completed successfully"));
} catch (error) {
console.error(chalk.red("Focus management failed:"), error);
process.exit(1);
}
}
/**
* Manage screen reader functionality
*
* @param options - Screen reader options
*/
async manageScreenReader(options) {
try {
console.log(chalk.blue("Managing screen reader functionality..."));
await this.manager.initialize();
if (options.announce) {
console.log(chalk.yellow("Announcement functionality not yet implemented"));
} else if (options.liveRegion) {
console.log(chalk.yellow("Live region functionality not yet implemented"));
} else if (options.update) {
console.log(chalk.yellow("Live region update functionality not yet implemented"));
} else if (options.remove) {
console.log(chalk.yellow("Live region removal functionality not yet implemented"));
} else {
console.log(chalk.yellow("No screen reader action specified"));
}
console.log(chalk.green("Screen reader management completed successfully"));
} catch (error) {
console.error(chalk.red("Screen reader management failed:"), error);
process.exit(1);
}
}
/**
* Generate accessibility report
*
* @param options - Report options
*/
async generateReport(options) {
try {
console.log(chalk.blue("Generating accessibility report..."));
if (!options.input) {
throw new Error("Input file is required");
}
const auditData = await fs.readJson(options.input);
console.log(chalk.yellow("Report generation functionality not yet implemented"));
const outputPath = options.output || `report.${options.format}`;
console.log(chalk.yellow(`Report would be saved to: ${outputPath}`));
console.log(chalk.green(`Report generated: ${outputPath}`));
} catch (error) {
console.error(chalk.red("Report generation failed:"), error);
process.exit(1);
}
}
/**
* Show accessibility statistics
*
* @param options - Stats options
*/
async showStats(options) {
try {
console.log(chalk.blue("Accessibility Statistics"));
const stats = this.manager.getStats();
console.log(chalk.cyan("\nOverall Statistics:"));
console.log(` Total Audits: ${stats.totalAudits}`);
console.log(` Total Violations: ${stats.totalViolations}`);
console.log(` Average Score: ${stats.averageScore.toFixed(1)}%`);
console.log(` Compliance Rate: ${(stats.complianceRate * 100).toFixed(1)}%`);
if (options.focus) {
const focusStats = this.manager.getStats();
console.log(chalk.cyan("\nFocus Management Statistics:"));
console.log(` Total Elements: ${focusStats.totalAudits}`);
console.log(` Focusable Elements: ${focusStats.totalViolations}`);
console.log(` Focused Elements: ${focusStats.averageScore}`);
console.log(` Focus Traps: ${focusStats.complianceRate}`);
console.log(` Skip Links: ${focusStats.config.wcagLevel}`);
}
if (options.screenReader) {
const srStats = this.manager.getStats();
console.log(chalk.cyan("\nScreen Reader Statistics:"));
console.log(` Total Announcements: ${srStats.totalAudits}`);
console.log(` Total Live Regions: ${srStats.totalViolations}`);
console.log(` Average Score: ${srStats.averageScore.toFixed(1)}%`);
console.log(` Compliance Rate: ${(srStats.complianceRate * 100).toFixed(1)}%`);
}
console.log(chalk.green("\nStatistics displayed successfully"));
} catch (error) {
console.error(chalk.red("Failed to show statistics:"), error);
process.exit(1);
}
}
/**
* Display audit results
*
* @param audit - Audit results
*/
displayAuditResults(audit) {
console.log(chalk.cyan("\n=== Accessibility Audit Results ==="));
console.log(`Score: ${audit.score.toFixed(1)}%`);
console.log(`Level: ${audit.level}`);
console.log(`Compliant: ${audit.compliant ? "Yes" : "No"}`);
console.log(`Violations: ${audit.violations.length}`);
console.log(`Passes: ${audit.passes.length}`);
console.log(`Inapplicable: ${audit.inapplicable.length}`);
if (audit.violations.length > 0) {
console.log(chalk.red("\nViolations:"));
audit.violations.forEach((violation) => {
console.log(` ${violation.impact.toUpperCase()}: ${violation.message}`);
});
}
}
/**
* Display test results
*
* @param results - Test results
*/
displayTestResults(results) {
console.log(chalk.cyan("\n=== Accessibility Test Results ==="));
results.forEach((result) => {
const status = result.status === "pass" ? chalk.green("PASS") : chalk.red("FAIL");
console.log(`${status}: ${result.name} - ${result.description}`);
});
}
/**
* Save results to file
*
* @param results - Results to save
* @param filePath - Output file path
* @param format - Output format
*/
async saveResults(results, filePath, format) {
let content;
switch (format) {
case "json":
content = JSON.stringify(results, null, 2);
break;
case "html":
content = this.generateHTMLReport(results);
break;
case "csv":
content = this.generateCSVReport(results);
break;
default:
throw new Error(`Unsupported format: ${format}`);
}
await fs.writeFile(filePath, content);
console.log(chalk.green(`Results saved to: ${filePath}`));
}
/**
* Generate ARIA for file
*
* @param filePath - File path
* @param options - Generation options
*/
async generateARIAForFile(filePath, options) {
console.log(`Generating ARIA for file: ${filePath}`);
}
/**
* Generate ARIA for directory
*
* @param dirPath - Directory path
* @param options - Generation options
*/
async generateARIAForDirectory(dirPath, options) {
console.log(`Generating ARIA for directory: ${dirPath}`);
}
/**
* Analyze focus order
*
* @param filePath - File path
*/
async analyzeFocusOrder(filePath) {
console.log(`Analyzing focus order in: ${filePath}`);
}
/**
* Generate focus code
*
* @param filePath - File path
*/
async generateFocusCode(filePath) {
console.log(`Generating focus code for: ${filePath}`);
}
/**
* Test keyboard navigation
*
* @param filePath - File path
*/
async testKeyboardNavigation(filePath) {
console.log(`Testing keyboard navigation in: ${filePath}`);
}
/**
* Generate HTML report
*
* @param results - Results
* @returns HTML report
*/
generateHTMLReport(results) {
return `
<!DOCTYPE html>
<html>
<head>
<title>Accessibility Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.header { background: #f0f0f0; padding: 20px; }
.violation { color: red; }
.pass { color: green; }
</style>
</head>
<body>
<div class="header">
<h1>Accessibility Report</h1>
<p>Generated on: ${(/* @__PURE__ */ new Date()).toISOString()}</p>
</div>
<pre>${JSON.stringify(results, null, 2)}</pre>
</body>
</html>`;
}
/**
* Generate CSV report
*
* @param results - Results
* @returns CSV report
*/
generateCSVReport(results) {
return "Type,Message,Impact\naudit,Accessibility audit completed,info\n";
}
/**
* Run the CLI
*/
async run() {
await this.program.parseAsync();
}
};
if (import.meta.url === `file://${process.argv[1]}`) {
const cli = new AccessibilityCLI();
cli.run().catch(console.error);
}
export { AccessibilityCLI };
//# sourceMappingURL=cli.mjs.map
//# sourceMappingURL=cli.mjs.map