Files
libguide-review-aider/check_wcag_curl.py
T

144 lines
4.4 KiB
Python

#!/usr/bin/env python3
"""
Audit each guide URL in guides.csv for WCAG 2.1 AA accessibility issues.
This variant fetches every page with **curl** first (avoiding the headless
browser network request) and then runs **pa11y** on the downloaded HTML
file. Issues that appear in *every* guide are filtered out; all others
are reported.
Output: output_curl.txt
Usage:
python check_wcag_curl.py
Prerequisites:
• curl (command-line tool, usually pre-installed)
• pa11y (npm install -g pa11y # or available via npx)
"""
from __future__ import annotations
import csv
import json
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Dict, List, Set
GUIDES_CSV = Path(__file__).with_name("guides.csv")
OUTPUT_FILE = Path("output_curl.txt")
def fetch_html(url: str) -> str | None:
"""Return HTML for the URL using curl, or None on failure."""
result = subprocess.run(
["curl", "-L", "-sS", url],
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
print(f"[ERROR] curl failed ({result.returncode}) for {url}", file=sys.stderr)
if result.stderr:
print(result.stderr, file=sys.stderr)
return None
return result.stdout
def run_pa11y_on_html(html: str) -> List[dict]:
"""Run pa11y on the provided HTML string and return parsed issues list."""
# Write HTML to a temporary file
with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as tmp:
tmp.write(html.encode("utf-8"))
tmp_path = Path(tmp.name)
cmd_base = ["pa11y"] if shutil.which("pa11y") else ["npx", "pa11y"]
result = subprocess.run(
[*cmd_base, f"file://{tmp_path}", "--standard", "WCAG2AA", "--reporter", "json"],
capture_output=True,
text=True,
check=False,
)
# Clean up temp file
tmp_path.unlink(missing_ok=True)
# pa11y exits 0 (no issues) or 1 (issues found) → both acceptable
if result.returncode not in (0, 1):
print(f"[ERROR] pa11y failed ({result.returncode})", file=sys.stderr)
if result.stderr:
print(result.stderr, file=sys.stderr)
return []
if not result.stdout or not result.stdout.strip():
print("[WARN] pa11y produced no JSON output", file=sys.stderr)
if result.stderr:
print(result.stderr, file=sys.stderr)
return []
try:
return json.loads(result.stdout)
except json.JSONDecodeError:
print(result.stdout, file=sys.stderr)
print("[WARN] Could not parse pa11y JSON output", file=sys.stderr)
return []
def read_guides(csv_path: Path) -> List[Dict[str, str]]:
with csv_path.open(newline="", encoding="utf-8-sig") as fh:
return list(csv.DictReader(fh))
def compute_common_codes(issues_by_guide: Dict[str, List[dict]]) -> Set[str]:
common: Set[str] | None = None
for issues in issues_by_guide.values():
codes = {i["code"] for i in issues}
common = codes if common is None else common & codes
return common or set()
def format_issue(issue: dict) -> str:
selector = (issue.get("selector") or "").strip() or "N/A"
return f"[{issue['type']}] {issue['code']}: {issue['message']} (selector: {selector})"
def main() -> None:
guides = read_guides(GUIDES_CSV)
issues_by_guide: Dict[str, List[dict]] = {}
print("Auditing guides with curl + pa11y…\n")
for guide in guides:
name, url = guide["Name"], guide["URL"]
print(f"• {name}{url}")
html = fetch_html(url)
if html is None:
issues_by_guide[name] = []
continue
issues_by_guide[name] = run_pa11y_on_html(html)
common_codes = compute_common_codes(issues_by_guide)
lines: List[str] = []
lines.append("Accessibility report (WCAG 2.1 AA — unique issues only, curl fetch)")
lines.append("=" * 65)
for name, issues in issues_by_guide.items():
unique = [i for i in issues if i["code"] not in common_codes]
lines.append(f"\nGuide: {name}")
if not unique:
lines.append(" ✔ No unique issues found.")
continue
for issue in unique:
lines.append(f" • {format_issue(issue)}")
report = "\n".join(lines)
print("\n" + report)
OUTPUT_FILE.write_text(report, encoding="utf-8")
print(f"\nReport written to {OUTPUT_FILE.resolve()}")
if __name__ == "__main__":
main()