UNPKG

@ordojs/accessibility

Version:

Comprehensive accessibility system for OrdoJS with ARIA generation, automated testing, and screen reader support

846 lines (840 loc) 28.7 kB
'use strict'; var chalk = require('chalk'); var commander = require('commander'); var fs = require('fs-extra'); var events = require('events'); var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null; function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var chalk__default = /*#__PURE__*/_interopDefault(chalk); var fs__default = /*#__PURE__*/_interopDefault(fs); // src/cli.ts var AccessibilityManager = class extends events.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 commander.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__default.default.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__default.default.green("Audit completed successfully")); } catch (error) { console.error(chalk__default.default.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__default.default.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__default.default.yellow("Specific test functionality not yet implemented")); } else { console.log(chalk__default.default.yellow("Test functionality not yet implemented")); } console.log(chalk__default.default.green("Test completed successfully")); } catch (error) { console.error(chalk__default.default.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__default.default.blue(`Generating ARIA attributes for ${input}...`)); await this.manager.initialize(); if (!fs__default.default.existsSync(input)) { throw new Error(`Input path does not exist: ${input}`); } const stats = fs__default.default.statSync(input); if (stats.isFile()) { await this.generateARIAForFile(input, options); } else if (stats.isDirectory()) { await this.generateARIAForDirectory(input, options); } console.log(chalk__default.default.green("ARIA generation completed successfully")); } catch (error) { console.error(chalk__default.default.red("ARIA generation failed:"), error); process.exit(1); } } /** * Manage focus and keyboard navigation * * @param options - Focus options */ async manageFocus(options) { try { console.log(chalk__default.default.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__default.default.yellow("No focus action specified. Use --analyze, --generate, or --test")); } console.log(chalk__default.default.green("Focus management completed successfully")); } catch (error) { console.error(chalk__default.default.red("Focus management failed:"), error); process.exit(1); } } /** * Manage screen reader functionality * * @param options - Screen reader options */ async manageScreenReader(options) { try { console.log(chalk__default.default.blue("Managing screen reader functionality...")); await this.manager.initialize(); if (options.announce) { console.log(chalk__default.default.yellow("Announcement functionality not yet implemented")); } else if (options.liveRegion) { console.log(chalk__default.default.yellow("Live region functionality not yet implemented")); } else if (options.update) { console.log(chalk__default.default.yellow("Live region update functionality not yet implemented")); } else if (options.remove) { console.log(chalk__default.default.yellow("Live region removal functionality not yet implemented")); } else { console.log(chalk__default.default.yellow("No screen reader action specified")); } console.log(chalk__default.default.green("Screen reader management completed successfully")); } catch (error) { console.error(chalk__default.default.red("Screen reader management failed:"), error); process.exit(1); } } /** * Generate accessibility report * * @param options - Report options */ async generateReport(options) { try { console.log(chalk__default.default.blue("Generating accessibility report...")); if (!options.input) { throw new Error("Input file is required"); } const auditData = await fs__default.default.readJson(options.input); console.log(chalk__default.default.yellow("Report generation functionality not yet implemented")); const outputPath = options.output || `report.${options.format}`; console.log(chalk__default.default.yellow(`Report would be saved to: ${outputPath}`)); console.log(chalk__default.default.green(`Report generated: ${outputPath}`)); } catch (error) { console.error(chalk__default.default.red("Report generation failed:"), error); process.exit(1); } } /** * Show accessibility statistics * * @param options - Stats options */ async showStats(options) { try { console.log(chalk__default.default.blue("Accessibility Statistics")); const stats = this.manager.getStats(); console.log(chalk__default.default.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__default.default.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__default.default.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__default.default.green("\nStatistics displayed successfully")); } catch (error) { console.error(chalk__default.default.red("Failed to show statistics:"), error); process.exit(1); } } /** * Display audit results * * @param audit - Audit results */ displayAuditResults(audit) { console.log(chalk__default.default.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__default.default.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__default.default.cyan("\n=== Accessibility Test Results ===")); results.forEach((result) => { const status = result.status === "pass" ? chalk__default.default.green("PASS") : chalk__default.default.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__default.default.writeFile(filePath, content); console.log(chalk__default.default.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 ((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.js', document.baseURI).href)) === `file://${process.argv[1]}`) { const cli = new AccessibilityCLI(); cli.run().catch(console.error); } exports.AccessibilityCLI = AccessibilityCLI; //# sourceMappingURL=cli.js.map //# sourceMappingURL=cli.js.map