UNPKG

a11yanalyze

Version:

A command-line tool for developers and QA engineers to test web pages and websites for WCAG 2.2 AA accessibility compliance

174 lines (167 loc) 7.06 kB
import { ScanResult, AccessibilityIssue } from '../types'; import Handlebars from 'handlebars'; // Register built-in Handlebars helpers for template customization Handlebars.registerHelper('formatDate', function(date: string | Date, format?: string) { const d = date ? new Date(date) : new Date(); return d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); }); Handlebars.registerHelper('ifEquals', function(this: any, a: any, b: any, options: Handlebars.HelperOptions) { return a === b ? options.fn(this) : options.inverse(this); }); Handlebars.registerHelper('toUpper', function(str: string) { return (str || '').toUpperCase(); }); Handlebars.registerHelper('toLower', function(str: string) { return (str || '').toLowerCase(); }); Handlebars.registerHelper('toTitle', function(str: string) { return (str || '').replace(/\w\S*/g, (txt: string) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()); }); Handlebars.registerHelper('pluralize', function(count: number, singular: string, plural?: string) { return count === 1 ? singular : (plural || singular + 's'); }); Handlebars.registerHelper('ifOdd', function(this: any, index: number, options: Handlebars.HelperOptions) { return index % 2 === 1 ? options.fn(this) : options.inverse(this); }); // Export a function to allow user-defined helpers in the future export function registerCustomHelpers(helpers: Record<string, Handlebars.HelperDelegate>) { Object.entries(helpers).forEach(([name, fn]) => Handlebars.registerHelper(name, fn)); } export interface VpatCriterion { id: string; name: string; status: 'Supports' | 'Partially Supports' | 'Does Not Support' | 'Not Applicable' | 'Not Evaluated'; remarks: string; } export interface VpatReport { component: string; vpatVersion: string; criteria: VpatCriterion[]; summary: { supports: number; partiallySupports: number; doesNotSupport: number; notApplicable: number; notEvaluated: number; }; } export class VpatReporter { static generateJsonReport(component: string, scanResult: any, vpatVersion = 'TastySoft Design System VPAT2 Form 2025'): VpatReport { // Map scan results to VPAT criteria (stub: real mapping would be more detailed) const criteria: VpatCriterion[] = scanResult.issues.map((issue: AccessibilityIssue) => ({ id: issue.wcagReference || issue.id, name: issue.message || issue.id, status: VpatReporter.mapSeverityToStatus(issue.severity), remarks: (issue.remediation || '') + (issue.helpUrl ? ` [More info](${issue.helpUrl})` : ''), })); // Add Not Applicable/Not Evaluated for missing criteria (stub) // ... const keyboardNav = scanResult.keyboardNavigation; const screenReaderSim = scanResult.screenReaderSimulation; const cognitive = scanResult.cognitiveAccessibility; const summary = VpatReporter.summarize(criteria); const report: any = { component, vpatVersion, criteria, summary }; if (keyboardNav) { report.keyboardNavigation = keyboardNav; } if (screenReaderSim) { report.screenReaderSimulation = screenReaderSim; } if (cognitive) { report.cognitiveAccessibility = cognitive; } return report; } static generateMarkdownReport(report: any): string { let md = `# ${report.vpatVersion} - ${report.component}\n\n`; md += '| Criterion | Name | Status | Remarks/Explanations |\n'; md += '|-----------|------|--------|----------------------|\n'; for (const c of report.criteria) { md += `| ${c.id} | ${c.name} | ${c.status} | ${c.remarks.replace(/\n/g, ' ')} |\n`; } md += '\n**Summary:** \n'; md += `- Supports: ${report.summary.supports} \n`; md += `- Partially Supports: ${report.summary.partiallySupports} \n`; md += `- Does Not Support: ${report.summary.doesNotSupport} \n`; md += `- Not Applicable: ${report.summary.notApplicable} \n`; md += `- Not Evaluated: ${report.summary.notEvaluated} \n`; if (report.keyboardNavigation) { md += '\n**Keyboard Navigation Findings:**\n'; md += `- Issues: ${report.keyboardNavigation.issues.length}\n`; md += `- Unreachable Elements: ${report.keyboardNavigation.unreachableElements.length}\n`; md += `- Focus Traps: ${report.keyboardNavigation.focusTraps.length}\n`; md += `- Missing Focus Indicators: ${report.keyboardNavigation.missingFocusIndicators.length}\n`; // Focus management details const focusRelated = report.keyboardNavigation.issues.filter((i: string) => i.includes('focus') || i.includes('indicator')); if (focusRelated.length > 0) { md += '\n - Focus Management/Indicator Issues:'; focusRelated.forEach((issue: string) => { md += `\n - ${issue}`; }); } if (report.keyboardNavigation.issues.length > 0) { md += '\n'; report.keyboardNavigation.issues.forEach((issue: string) => { md += ` - ${issue}\n`; }); } } if (report.screenReaderSimulation) { md += '\n**Screen Reader Simulation Findings:**\n'; md += `- Findings: ${report.screenReaderSimulation.summary.length}\n`; md += `- Missing Names: ${report.screenReaderSimulation.missingNames.length}\n`; md += `- Ambiguous Roles: ${report.screenReaderSimulation.ambiguousRoles.length}\n`; md += `- Skipped Elements: ${report.screenReaderSimulation.skippedElements.length}\n`; if (report.screenReaderSimulation.summary.length > 0) { md += '\n'; report.screenReaderSimulation.summary.forEach((issue: string) => { md += ` - ${issue}\n`; }); } } if (report.cognitiveAccessibility) { md += '\n**Cognitive Accessibility Findings:**\n'; report.cognitiveAccessibility.summary.forEach((line: string) => { md += `- ${line}\n`; }); } return md; } static generateCustomReport(report: VpatReport, template: string): string { const compiled = Handlebars.compile(template); return compiled(report); } static mapSeverityToStatus(severity: string): VpatCriterion['status'] { switch (severity) { case 'critical': case 'serious': return 'Does Not Support'; case 'moderate': return 'Partially Supports'; case 'minor': case 'warning': return 'Supports'; default: return 'Supports'; } } static summarize(criteria: VpatCriterion[]): VpatReport['summary'] { const summary = { supports: 0, partiallySupports: 0, doesNotSupport: 0, notApplicable: 0, notEvaluated: 0, }; for (const c of criteria) { switch (c.status) { case 'Supports': summary.supports++; break; case 'Partially Supports': summary.partiallySupports++; break; case 'Does Not Support': summary.doesNotSupport++; break; case 'Not Applicable': summary.notApplicable++; break; case 'Not Evaluated': summary.notEvaluated++; break; } } return summary; } }