155 lines
No EOL
5.8 KiB
TypeScript
155 lines
No EOL
5.8 KiB
TypeScript
import type { ImpactValue } from 'axe-core';
|
|
import { IMPACT_SCORES, COMPLIANCE_THRESHOLDS, WCAG_TAGS } from './constants';
|
|
import type { AccessibilityScanResult, ScanSummary, WCAGLevel } from './types';
|
|
|
|
export function calculateComplianceScore(result: AccessibilityScanResult): number {
|
|
const { violations, passes } = result;
|
|
const totalChecks = violations.length + passes.length;
|
|
|
|
if (totalChecks === 0) return 100;
|
|
|
|
let weightedViolations = 0;
|
|
violations.forEach(violation => {
|
|
const impactScore = IMPACT_SCORES[violation.impact as keyof typeof IMPACT_SCORES] || 50;
|
|
weightedViolations += (impactScore / 100) * violation.nodes.length;
|
|
});
|
|
|
|
const score = Math.max(0, 100 - (weightedViolations / totalChecks) * 100);
|
|
return Math.round(score * 100) / 100;
|
|
}
|
|
|
|
export function isCompliantWithLevel(score: number, level: WCAGLevel['level']): boolean {
|
|
return score >= COMPLIANCE_THRESHOLDS[level] * 100;
|
|
}
|
|
|
|
export function getWCAGTags(level: WCAGLevel): string[] {
|
|
const key = `WCAG_${level.version.replace('.', '_')}_${level.level}` as keyof typeof WCAG_TAGS;
|
|
return [...(WCAG_TAGS[key] || [])];
|
|
}
|
|
|
|
export function summarizeViolations(violations: AccessibilityScanResult['violations']): Partial<ScanSummary> {
|
|
const summary = {
|
|
criticalIssues: 0,
|
|
seriousIssues: 0,
|
|
moderateIssues: 0,
|
|
minorIssues: 0,
|
|
};
|
|
|
|
violations.forEach(violation => {
|
|
const count = violation.nodes.length;
|
|
switch (violation.impact) {
|
|
case 'critical':
|
|
summary.criticalIssues += count;
|
|
break;
|
|
case 'serious':
|
|
summary.seriousIssues += count;
|
|
break;
|
|
case 'moderate':
|
|
summary.moderateIssues += count;
|
|
break;
|
|
case 'minor':
|
|
summary.minorIssues += count;
|
|
break;
|
|
}
|
|
});
|
|
|
|
return summary;
|
|
}
|
|
|
|
export function generateFixSuggestion(violationId: string): string {
|
|
const suggestions: Record<string, string> = {
|
|
'color-contrast': 'Increase the contrast ratio between text and background colors to meet WCAG standards.',
|
|
'image-alt': 'Add descriptive alt text to images. Use alt="" for decorative images.',
|
|
'label': 'Add labels to form controls using <label> elements or aria-label attributes.',
|
|
'button-name': 'Provide accessible names for buttons using text content, aria-label, or aria-labelledby.',
|
|
'link-name': 'Ensure links have descriptive text that explains their purpose.',
|
|
'heading-order': 'Use heading levels in sequential order without skipping levels.',
|
|
'landmark-one-main': 'Ensure the page has one main landmark using <main> or role="main".',
|
|
'page-has-heading-one': 'Add a single <h1> element to identify the main content of the page.',
|
|
'meta-viewport': 'Avoid disabling zoom by not using maximum-scale=1.0 in viewport meta tag.',
|
|
'duplicate-id': 'Ensure all id attributes are unique within the page.',
|
|
'aria-required-attr': 'Add all required ARIA attributes for the role being used.',
|
|
'aria-valid-attr': 'Use only valid ARIA attributes that are spelled correctly.',
|
|
'html-has-lang': 'Add a lang attribute to the <html> element to specify the page language.',
|
|
'document-title': 'Provide a descriptive <title> element in the document head.',
|
|
'list': 'Ensure <ul> and <ol> elements only contain <li>, <script>, or <template> elements.',
|
|
};
|
|
|
|
return suggestions[violationId] || 'Review the element and ensure it meets accessibility standards.';
|
|
}
|
|
|
|
export function formatViolationTarget(target: string[]): string {
|
|
return target.join(' > ');
|
|
}
|
|
|
|
export function estimateFixTime(impact: ImpactValue, nodeCount: number): number {
|
|
const baseTime: Record<ImpactValue, number> = {
|
|
critical: 30,
|
|
serious: 20,
|
|
moderate: 15,
|
|
minor: 10,
|
|
};
|
|
|
|
return (baseTime[impact] || 15) * nodeCount;
|
|
}
|
|
|
|
export function groupViolationsByWCAGCriteria(
|
|
violations: AccessibilityScanResult['violations']
|
|
): Record<string, AccessibilityScanResult['violations']> {
|
|
const grouped: Record<string, AccessibilityScanResult['violations']> = {};
|
|
|
|
violations.forEach(violation => {
|
|
violation.wcagCriteria?.forEach(criterion => {
|
|
if (!grouped[criterion]) {
|
|
grouped[criterion] = [];
|
|
}
|
|
grouped[criterion].push(violation);
|
|
});
|
|
});
|
|
|
|
return grouped;
|
|
}
|
|
|
|
export function generateComplianceStatement(result: AccessibilityScanResult): string {
|
|
const { wcagCompliance, summary } = result;
|
|
const score = result.summary.score;
|
|
const level = wcagCompliance.level;
|
|
|
|
if (wcagCompliance.isCompliant) {
|
|
return `This page meets WCAG ${level.version} Level ${level.level} compliance standards with a score of ${score}%.`;
|
|
}
|
|
|
|
const majorIssues = summary.criticalIssues + summary.seriousIssues;
|
|
return `This page does not meet WCAG ${level.version} Level ${level.level} compliance. ` +
|
|
`Found ${majorIssues} major accessibility issues that need to be addressed. ` +
|
|
`Current compliance score: ${score}%.`;
|
|
}
|
|
|
|
export function extractPageMetadata(html: string): Partial<AccessibilityScanResult['pageMetadata']> {
|
|
const metadata: Partial<AccessibilityScanResult['pageMetadata']> = {};
|
|
|
|
// Extract title
|
|
const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i);
|
|
if (titleMatch) {
|
|
metadata.title = titleMatch[1].trim();
|
|
}
|
|
|
|
// Extract language
|
|
const langMatch = html.match(/<html[^>]*lang=["']([^"']+)["']/i);
|
|
if (langMatch) {
|
|
metadata.language = langMatch[1];
|
|
}
|
|
|
|
// Count elements
|
|
metadata.imagesCount = (html.match(/<img[^>]*>/gi) || []).length;
|
|
metadata.formsCount = (html.match(/<form[^>]*>/gi) || []).length;
|
|
metadata.linksCount = (html.match(/<a[^>]*>/gi) || []).length;
|
|
|
|
// Check for landmarks
|
|
metadata.hasLandmarks = /<(main|nav|aside|header|footer)[^>]*>|role=["'](main|navigation|complementary|banner|contentinfo)["']/i.test(html);
|
|
|
|
// Check for headings
|
|
metadata.hasHeadings = /<h[1-6][^>]*>/i.test(html);
|
|
|
|
return metadata;
|
|
} |