-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathcheck_accessibility.py
More file actions
150 lines (126 loc) · 5.47 KB
/
Copy pathcheck_accessibility.py
File metadata and controls
150 lines (126 loc) · 5.47 KB
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#!/usr/bin/env python3
"""
Accessibility Checker
Validates markdown for accessibility issues:
- Alt text for images
- Descriptive link text
- Proper heading hierarchy
- Table descriptions
"""
import os
import re
import sys
import json
from pathlib import Path
TABLE_SEPARATOR_RE = re.compile(r'^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$')
def _is_table_header(line, next_line):
"""A line is a GFM table header when it contains '|' and the next line is
a valid separator row (e.g. '|---|---|'). Matching on the header plus its
separator - not on every row that merely contains '|' - means a 10-row
table produces one finding instead of ten (support#61)."""
return '|' in line and bool(TABLE_SEPARATOR_RE.match(next_line))
def check_accessibility(filepath):
"""Check accessibility issues in a markdown file."""
issues = []
try:
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
lines = content.split('\n')
except Exception as e:
return [{'file': filepath, 'line': 0, 'type': 'error', 'message': f'Error reading: {e}'}]
for line_num, line in enumerate(lines, 1):
# Check for images without alt text
img_pattern = r'!\[(.*?)\]\((.*?)\)'
img_matches = re.finditer(img_pattern, line)
for match in img_matches:
alt_text = match.group(1)
img_url = match.group(2)
# Check if alt text is empty or just whitespace
if not alt_text or not alt_text.strip():
issues.append({
'file': filepath,
'line': line_num,
'type': 'error',
'title': 'Missing Image Alt Text',
'message': f'Image has no description: {img_url}',
'fix': 'Add descriptive alt text. Replace  with . Describe what the image shows.'
})
elif len(alt_text) < 5:
issues.append({
'file': filepath,
'line': line_num,
'type': 'warning',
'title': 'Image Alt Text Too Short',
'message': f'Alt text "{alt_text}" is too brief. Describe the image content.',
'fix': 'Expand alt text to explain what someone would see in the image.'
})
# Check for poor link text
link_pattern = r'\[([^\]]+)\]\(([^)]+)\)'
link_matches = re.finditer(link_pattern, line)
poor_phrases = [
('click here', 'Click here'),
('read more', 'Read more'),
('here', 'Here'),
('link', 'Link'),
('more', 'More'),
('learn more', 'Learn more'),
('this', 'This'),
('for more info', 'For more info'),
]
for match in link_matches:
text = match.group(1).lower()
url = match.group(2)
for poor, proper in poor_phrases:
if text == poor:
issues.append({
'file': filepath,
'line': line_num,
'type': 'error',
'title': 'Non-Descriptive Link Text',
'message': f'Link text "{proper}" doesn\'t describe where the link goes.',
'fix': f'Replace "[{proper}]({url})" with descriptive text like "[Topic Name]({url})"'
})
break
# Check for tables without descriptions. Only the header line is
# inspected (paired with the separator row that follows it), so a
# single table produces at most one finding regardless of row count.
if line_num < len(lines) and _is_table_header(line, lines[line_num]):
if line_num > 1:
prev_line = lines[line_num - 2].strip()
if not prev_line or prev_line.startswith('|'):
issues.append({
'file': filepath,
'line': line_num,
'type': 'suggestion',
'title': 'Table Description',
'message': 'Consider adding a brief description before tables explaining their content.',
'fix': 'Add one sentence before the table explaining what data it contains.'
})
return issues
def main():
root_dir = sys.argv[1] if len(sys.argv) > 1 else '.'
all_issues = []
# Find all markdown files
for md_file in Path(root_dir).rglob('*.md'):
if '.github' in md_file.parts or 'node_modules' in md_file.parts:
continue
issues = check_accessibility(str(md_file))
all_issues.extend(issues)
# Separate by severity
errors = [i for i in all_issues if i.get('type') == 'error']
warnings = [i for i in all_issues if i.get('type') in ['warning', 'suggestion']]
feedback = {
'accessibility': errors + warnings,
'errors': [],
'warnings': []
}
with open('validation-feedback.json', 'w') as f:
json.dump(feedback, f, indent=2)
if errors:
print(f"Found {len(errors)} accessibility errors and {len(warnings)} suggestions")
sys.exit(1)
else:
print(f"✅ Accessibility check passed! ({len(warnings)} suggestions for improvement)")
sys.exit(0)
if __name__ == '__main__':
main()