Skip to content

Commit ba4352b

Browse files
committed
w
1 parent 90c21b8 commit ba4352b

File tree

3 files changed

+234
-20
lines changed

3 files changed

+234
-20
lines changed

.claude/commands/mathml-general-exam.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,43 @@ After completing the audit, provide:
349349

350350
This comprehensive audit ensures legal compliance and protects the university from ADA lawsuits.
351351

352+
---
353+
354+
### AUTOMATED WCAG VERIFICATION (REQUIRED)
355+
356+
**After completing your manual audit, IMMEDIATELY run the automated verification script:**
357+
358+
```bash
359+
python3 scripts/verify_wcag.py <PATH_TO_HTML_FILE>
360+
```
361+
362+
**This script automatically checks:**
363+
- ✓ Unicode violations (U+1D400-U+1D7FF range)
364+
- ✓ H1 tag presence
365+
- ✓ `<main>` landmark presence
366+
- ✓ MathML `role="math"` attributes
367+
- ✓ Breadcrumb navigation
368+
369+
**Example:**
370+
```bash
371+
python3 scripts/verify_wcag.py graduate/exams/analysis/2017Aug.html
372+
```
373+
374+
**Expected output if compliant:**
375+
```
376+
✅ graduate/exams/analysis/2017Aug.html: PASS
377+
```
378+
379+
**If the script reports ANY failures:**
380+
1. **STOP immediately**
381+
2. Fix the violations
382+
3. Re-run the script
383+
4. Only proceed when you see `✅ PASS`
384+
385+
**This automated check is MANDATORY** - it catches violations that manual review may miss and ensures consistent compliance across all exam files.
386+
387+
---
388+
352389
## Important Notes
353390
- **CONCURRENT EXECUTION**: This command uses unique PDF_ID-based filenames in `/tmp/`, so multiple instances can run simultaneously without conflicts
354391
- **Concurrent safety**: All processing steps (Mathpix API, file conversion, HTML generation) are fully isolated per PDF

scripts/fix_unicode_violations.py

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# Unicode to HTML entity mapping
1313
UNICODE_FIXES = {
1414
'𝔻': '&Dopf;', # U+1D53B - Blackboard bold D
15+
'𝔽': '&Fopf;', # U+1D53D - Blackboard bold F
1516
'𝒜': '&Ascr;', # U+1D49C - Script A
1617
'𝐑': '&Rfr;', # U+1D411 - Bold R (should use &Ropf; if blackboard bold)
1718
'𝒞': '&Cscr;', # U+1D49E - Script C
@@ -20,23 +21,16 @@
2021
'𝕋': '&Topf;', # U+1D54B - Blackboard bold T
2122
'𝒢': '&Gscr;', # U+1D4A2 - Script G
2223
'𝐊': '&Kfr;', # U+1D40A - Bold K
24+
'𝔞': '&afr;', # U+1D51E - Fraktur lowercase a
2325
}
2426

2527
FILES_TO_FIX = [
26-
'graduate/exams/analysis/2007Aug.html',
27-
'graduate/exams/analysis/2009Aug.html',
28-
'graduate/exams/analysis/2010Aug.html',
29-
'graduate/exams/analysis/2010Jan.html',
30-
'graduate/exams/analysis/2011Aug.html',
31-
'graduate/exams/analysis/2011Jan.html',
32-
'graduate/exams/analysis/2015Aug.html',
33-
'graduate/exams/analysis/2019Aug_complex.html',
34-
'graduate/exams/analysis/2019Aug_real.html',
35-
'graduate/exams/analysis/2020Jan_complex.html',
36-
'graduate/exams/analysis/2020Jan_real.html',
37-
'graduate/exams/analysis/2021Aug_complex.html',
38-
'graduate/exams/analysis/2022Aug_complex.html',
39-
'graduate/exams/analysis/2022Jan_complex.html',
28+
'graduate/exams/algebra/2021-08.html',
29+
'graduate/exams/algebra/2022-01.html',
30+
'graduate/exams/algebra/2022-08.html',
31+
'graduate/exams/algebra/2023-08.html',
32+
'graduate/exams/algebra/2024-01.html',
33+
'graduate/exams/algebra/2024-08.html',
4034
]
4135

4236
def fix_file(filepath, dry_run=False):
@@ -72,9 +66,9 @@ def fix_file(filepath, dry_run=False):
7266
print(f" {char}{entity} ({count}x)")
7367
return True
7468

75-
# Create backup
76-
backup_path = filepath + '.backup-' + datetime.now().strftime('%Y%m%d-%H%M%S')
77-
shutil.copy2(filepath, backup_path)
69+
# Skip backup - files are in git
70+
# backup_path = filepath + '.backup-' + datetime.now().strftime('%Y%m%d-%H%M%S')
71+
# shutil.copy2(filepath, backup_path)
7872

7973
# Write fixed content
8074
with open(filepath, 'w', encoding='utf-8') as f:
@@ -122,9 +116,7 @@ def main():
122116
print(f"❌ Failed: {fail_count}/{len(FILES_TO_FIX)}")
123117
print()
124118
print("🔍 To verify fixes, run:")
125-
print(" grep -P '[\\x{1D400}-\\x{1D7FF}]' graduate/exams/analysis/*.html")
126-
print()
127-
print("📝 Backups created with .backup-YYYYMMDD-HHMMSS extension")
119+
print(" python3 scripts/verify_wcag.py")
128120
print()
129121

130122
if __name__ == '__main__':

scripts/verify_wcag.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
#!/usr/bin/env python3
2+
"""
3+
WCAG 2.1 Level AA Compliance Verification Script
4+
Checks HTML files for accessibility violations as required by the slash command.
5+
"""
6+
7+
import os
8+
import re
9+
import sys
10+
11+
def check_unicode_violations(filepath):
12+
"""Check for Unicode mathematical characters (U+1D400-U+1D7FF)."""
13+
with open(filepath, 'r', encoding='utf-8') as f:
14+
content = f.read()
15+
matches = re.findall(r'[\U0001D400-\U0001D7FF]', content)
16+
return matches
17+
18+
def check_h1_tag(filepath):
19+
"""Check for H1 tag."""
20+
with open(filepath, 'r', encoding='utf-8') as f:
21+
content = f.read()
22+
return '<h1' in content
23+
24+
def check_main_landmark(filepath):
25+
"""Check for <main> landmark."""
26+
with open(filepath, 'r', encoding='utf-8') as f:
27+
content = f.read()
28+
return '<main' in content
29+
30+
def check_mathml_role(filepath):
31+
"""Check if all <math> tags have role='math'."""
32+
with open(filepath, 'r', encoding='utf-8') as f:
33+
content = f.read()
34+
math_tags = len(re.findall(r'<math[^>]*>', content))
35+
role_math = len(re.findall(r'role="math"', content))
36+
return (math_tags, role_math, math_tags == role_math or math_tags == 0)
37+
38+
def check_breadcrumb(filepath):
39+
"""Check for breadcrumb navigation."""
40+
with open(filepath, 'r', encoding='utf-8') as f:
41+
content = f.read()
42+
return 'aria-label="Breadcrumb"' in content
43+
44+
def verify_file(filepath, verbose=True):
45+
"""
46+
Run all WCAG 2.1 Level AA checks on a single file.
47+
Returns: (passed: bool, violations: list)
48+
"""
49+
violations = []
50+
51+
# Check 1: Unicode violations
52+
unicode_chars = check_unicode_violations(filepath)
53+
if unicode_chars:
54+
violations.append(f"Unicode violations: {len(unicode_chars)} characters found")
55+
if verbose:
56+
from collections import Counter
57+
char_counts = Counter(unicode_chars)
58+
for char, count in char_counts.most_common():
59+
codepoint = f"U+{ord(char):04X}"
60+
violations.append(f" {char} ({codepoint}): {count} occurrences")
61+
62+
# Check 2: H1 tag
63+
if not check_h1_tag(filepath):
64+
violations.append("Missing H1 tag")
65+
66+
# Check 3: <main> landmark
67+
if not check_main_landmark(filepath):
68+
violations.append("Missing <main> landmark")
69+
70+
# Check 4: MathML role
71+
math_count, role_count, passed = check_mathml_role(filepath)
72+
if not passed:
73+
violations.append(f"MathML role mismatch: {math_count} <math> tags, {role_count} with role=\"math\"")
74+
75+
# Check 5: Breadcrumb
76+
if not check_breadcrumb(filepath):
77+
violations.append("Missing breadcrumb navigation")
78+
79+
return (len(violations) == 0, violations)
80+
81+
def verify_directory(directory, verbose=True):
82+
"""Verify all HTML files in a directory."""
83+
html_files = [f for f in os.listdir(directory) if f.endswith('.html')]
84+
85+
if not html_files:
86+
return (True, 0, 0, [])
87+
88+
passed_files = 0
89+
failed_files = 0
90+
all_violations = {}
91+
92+
for filename in sorted(html_files):
93+
filepath = os.path.join(directory, filename)
94+
passed, violations = verify_file(filepath, verbose=verbose)
95+
96+
if passed:
97+
passed_files += 1
98+
else:
99+
failed_files += 1
100+
all_violations[filename] = violations
101+
102+
return (failed_files == 0, passed_files, failed_files, all_violations)
103+
104+
def main():
105+
"""Main entry point for standalone verification."""
106+
import argparse
107+
108+
parser = argparse.ArgumentParser(description='Verify WCAG 2.1 Level AA compliance for HTML files')
109+
parser.add_argument('path', nargs='?', help='File or directory to check')
110+
parser.add_argument('--quiet', '-q', action='store_true', help='Minimal output')
111+
args = parser.parse_args()
112+
113+
if args.path:
114+
# Check specific file or directory
115+
if os.path.isfile(args.path):
116+
passed, violations = verify_file(args.path, verbose=not args.quiet)
117+
if passed:
118+
print(f"✅ {args.path}: PASS")
119+
return 0
120+
else:
121+
print(f"❌ {args.path}: FAIL")
122+
for v in violations:
123+
print(f" - {v}")
124+
return 1
125+
elif os.path.isdir(args.path):
126+
passed, passed_count, failed_count, violations = verify_directory(args.path, verbose=not args.quiet)
127+
if passed:
128+
print(f"✅ All {passed_count} files PASS")
129+
return 0
130+
else:
131+
print(f"❌ {failed_count} files FAIL, {passed_count} files PASS")
132+
for filename, file_violations in violations.items():
133+
print(f"\n{filename}:")
134+
for v in file_violations:
135+
print(f" - {v}")
136+
return 1
137+
else:
138+
# Check all exam directories
139+
exam_dirs = [
140+
'graduate/exams/algebra',
141+
'graduate/exams/analysis',
142+
'graduate/exams/topology'
143+
]
144+
145+
total_passed = 0
146+
total_failed = 0
147+
all_violations = {}
148+
149+
for directory in exam_dirs:
150+
if not os.path.exists(directory):
151+
continue
152+
153+
passed, passed_count, failed_count, violations = verify_directory(directory, verbose=not args.quiet)
154+
total_passed += passed_count
155+
total_failed += failed_count
156+
157+
if violations:
158+
for filename, file_violations in violations.items():
159+
all_violations[f"{directory}/{filename}"] = file_violations
160+
161+
print("=" * 80)
162+
print("WCAG 2.1 LEVEL AA VERIFICATION")
163+
print("=" * 80)
164+
print()
165+
print(f"Total files checked: {total_passed + total_failed}")
166+
print(f"✅ Passed: {total_passed}")
167+
print(f"❌ Failed: {total_failed}")
168+
print()
169+
170+
if all_violations:
171+
print("VIOLATIONS:")
172+
print()
173+
for filepath, violations in sorted(all_violations.items()):
174+
print(f"{filepath}:")
175+
for v in violations:
176+
print(f" - {v}")
177+
print()
178+
return 1
179+
else:
180+
print("✅ ALL FILES PRODUCTION READY")
181+
print("✅ Lawsuit Risk: MINIMAL")
182+
return 0
183+
184+
if __name__ == '__main__':
185+
sys.exit(main())

0 commit comments

Comments
 (0)