1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115export interface ParsedViolation {
id: string; // generated unique id for key
rule: string;
description: string;
helpUrl?: string;
raw: string;
}
// Dictionary of friendly titles for common rules
const FRIENDLY_TITLES: Record<string, string> = {
'aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty': 'Missing Label',
'Element has insufficient color contrast': 'Low Color Contrast',
'Link Name Rule': 'Missing Link Text',
'Image Alt Rule': 'Missing Image Alt Text',
'Form element does not have an implicit (wrapped) <label>': 'Missing Form Label',
'Form element does not have an explicit <label>': 'Missing Form Label',
'Element has no title attribute': 'Missing Title Attribute',
'aria-label attribute does not exist or is empty': 'Missing ARIA Label',
'Element does not have text that is visible to screen readers': 'No Screen Reader Text',
'Frame Title Rule': 'Missing Frame Title',
'Button Name Rule': 'Missing Button Name',
'Link In Text Block Rule': 'Poor Link Distinction',
'Select Name Rule': 'Missing Select Name',
'Aria Roles Rule': 'Invalid ARIA Role',
'Nested Interactive Rule': 'Nested Interactive Controls',
"Element's default semantics were not overridden with role=\"none\" or role=\"presentation\"": 'Redundant Image',
'Aria Hidden Focus Rule': 'Focusable Hidden Element',
'Role Img Alt Rule': 'Missing Role=Img Alt Text',
'Aria Required Children Rule': 'Missing Required ARIA Children'
};
export const parseMessages = (rawMessage: string): ParsedViolation[] => {
if (!rawMessage) return [];
// Split by " | " sequence
const parts = rawMessage.split(' | ');
return parts.map((part, index) => {
let clean = part.trim();
// Remove leading "violation: "
if (clean.startsWith('violation: ')) {
clean = clean.substring(11).trim();
}
// Remove extra quotes if present
if (clean.startsWith('"') && clean.endsWith('"')) {
clean = clean.substring(1, clean.length - 1);
clean = clean.replace(/""/g, '"');
}
// Extract Help URL
let helpUrl: string | undefined;
const urlMatch = clean.match(/More information: (https?:\/\/[^\s]+)/);
if (urlMatch) {
helpUrl = urlMatch[1];
clean = clean.replace(urlMatch[0], '').trim();
}
// Determine Rule Title
let rule = 'General Violation';
// 1. Extract bracketed tag if it exists (e.g. [empty-alt-tag])
const bracketMatch = clean.match(/^\[([^\]]+)\]/);
if (bracketMatch) {
rule = bracketMatch[1];
// Remove the bracketed tag from the description if it's there
clean = clean.replace(bracketMatch[0], '').trim();
} else {
// 2. Exact match or prefix match in dictionary
for (const [key, title] of Object.entries(FRIENDLY_TITLES)) {
if (clean.includes(key)) {
rule = title;
break;
}
}
// 3. Fallback heuristic if no dictionary match
if (rule === 'General Violation') {
const colonIndex = clean.indexOf(':');
if (colonIndex > 0 && colonIndex < 50) {
const distinctName = clean.substring(0, colonIndex);
if (!distinctName.includes('\n') && distinctName.length > 3) {
rule = distinctName.trim();
}
}
}
}
let description = clean;
// Clean up description if it blindly contains the key
// Optional: we can shorten the description if it's identical to the key,
// but often the description has details (like contrast ratio).
// Remove trailing periods
if (description.endsWith('.')) {
description = description.substring(0, description.length - 1);
}
return {
id: `viol-${index}`,
rule,
description,
helpUrl,
raw: part
};
});
};
export const getGroupTitle = (messages: string): string => {
const parsed = parseMessages(messages);
const rules = Array.from(new Set(parsed.map(p => p.rule))).join(', ');
return rules;
};