#!/usr/bin/env python3 """ HTML Report Generator — converts JSON accessibility results to standalone HTML. Usage: python report_generator.py --input results.json --output report.html python report_generator.py --input results.json # prints to stdout """ import json import argparse import sys from datetime import datetime from pathlib import Path def severity_color(severity: str) -> str: return { "CRITICAL": "#dc2626", "ERROR": "#ef4444", "WARNING": "#f59e0b", "INFO": "#3b82f6", "SUCCESS": "#10b981", }.get(severity, "#6b7280") def severity_icon(severity: str) -> str: return { "CRITICAL": "🚨", "ERROR": "❌", "WARNING": "⚠️", "INFO": "ℹ️", "SUCCESS": "✅", }.get(severity, "") def grade_from_score(score: int) -> str: if score >= 90: return "A" if score >= 80: return "B" if score >= 70: return "C" if score >= 60: return "D" return "F" def generate_html(data: dict) -> str: """Generate a standalone HTML report from JSON results.""" score = data.get("accessibility_score", 0) grade = grade_from_score(score) sc = data.get("severity_counts", {}) issues = [i for i in data.get("issues", []) if not i.get("dismissed")] checks = data.get("checks_performed", []) filename = data.get("filename", "Unknown") total_pages = data.get("total_pages", 0) stats = data.get("stats", {}) now = datetime.now().strftime("%Y-%m-%d %H:%M") is_adjusted = data.get("score_breakdown", {}).get("adjusted", False) # Score ring color if score >= 80: ring_color = "#10b981" elif score >= 60: ring_color = "#f59e0b" else: ring_color = "#ef4444" # Build issue rows issue_rows = [] for i, issue in enumerate(issues, 1): sev = issue.get("severity", "INFO") color = severity_color(sev) icon = severity_icon(sev) page = issue.get("page_number", "—") wcag = issue.get("wcag_criterion", "") rec = issue.get("recommendation", "") wcag_cell = f'{wcag}' if wcag else '—' issue_rows.append(f""" {i} {icon} {sev} {issue.get('category', '')} {issue.get('description', '')} {page if page != '—' else ''} {wcag_cell} {rec} """) issues_html = "\n".join(issue_rows) if issue_rows else 'No issues found' # Build checks table check_rows = [] for ch in checks: if ch.get("manual"): status = "Manual Pass" status_color = "#d97706" elif ch.get("passed"): status = "PASS" status_color = "#10b981" else: status = "FAIL" status_color = "#ef4444" dur = f"{ch.get('duration', 0):.2f}s" check_rows.append(f""" {ch.get('name', '')} {status} {dur} """) checks_html = "\n".join(check_rows) if check_rows else "" # WCAG compliance section compliance = data.get('wcag_compliance', {}) if compliance: a_pass = compliance.get('level_a', False) aa_pass = compliance.get('level_aa', False) a_icon = '✓' if a_pass else '✗' aa_icon = '✓' if aa_pass else '✗' a_color = '#059669' if a_pass else '#dc2626' aa_color = '#059669' if aa_pass else '#dc2626' a_bg = '#d1fae5' if a_pass else '#fee2e2' aa_bg = '#d1fae5' if aa_pass else '#fee2e2' a_fails = ', '.join(compliance.get('level_a_failures', [])) aa_fails = ', '.join(compliance.get('level_aa_failures', [])) compliance_html = f"""

WCAG 2.1 Conformance

WCAG 2.1 Level A
{a_icon} {'Pass' if a_pass else 'Fail'}
WCAG 2.1 Level AA
{aa_icon} {'Pass' if aa_pass else 'Fail'}
{f'

Level A failing criteria: {a_fails}

' if a_fails else ''} {f'

Level AA failing criteria: {aa_fails}

' if aa_fails and not a_fails else ''}
""" else: compliance_html = '' # Next steps section next_steps = data.get('next_steps', []) if next_steps: priority_colors = {1: '#dc2626', 2: '#ef4444', 3: '#f59e0b'} priority_labels = {1: 'Critical', 2: 'Error', 3: 'Warning'} step_rows = '' for i, s in enumerate(next_steps, 1): pc = priority_colors.get(s.get('priority', 3), '#6b7280') pl = priority_labels.get(s.get('priority', 3), '') step_rows += f""" {i} {pl} {s.get('category','')} {s.get('action','')} {s.get('wcag','')} """ next_steps_html = f"""

Recommended Next Steps

{step_rows}
Prioritised accessibility remediation actions
# Priority Category Action WCAG
""" else: next_steps_html = '' duration = stats.get("duration", 0) api_calls = stats.get("api_calls", 0) cost = stats.get("total_cost_estimate", 0) html = f""" Accessibility Report — {filename}

PDF Accessibility Report

{filename} — {total_pages} page{"s" if total_pages != 1 else ""} — Generated {now}

Accessibility Score

{sc.get('critical',0)}
Critical
{sc.get('error',0)}
Errors
{sc.get('warning',0)}
Warnings
{sc.get('info',0)}
Info
{sc.get('success',0)}
Passed
Duration: {duration:.1f}s API calls: {api_calls} Estimated cost: ${cost:.2f} Total issues: {len(issues)}
{compliance_html} {next_steps_html}

Issues & Recommendations ({len(issues)})

{issues_html}
Accessibility issues found in the document
# Severity Category Description Page WCAG Recommendation
{"" if not checks_html else f'''

Checks Performed ({len(checks)})

{checks_html}
Individual WCAG check results and durations
CheckResultDuration
'''}
""" return html def generate_pdf(data: dict) -> bytes: """Generate a PAC-style PDF report using WeasyPrint.""" try: from weasyprint import HTML, CSS except ImportError: raise RuntimeError("WeasyPrint not installed. Run: pip install weasyprint>=60.0") score = data.get("accessibility_score", 0) grade = grade_from_score(score) sc = data.get("severity_counts", {}) issues = [i for i in data.get("issues", []) if not i.get("dismissed")] checks = data.get("checks_performed", []) filename = data.get("filename", "Unknown") total_pages = data.get("total_pages", 0) now = datetime.now().strftime("%Y-%m-%d %H:%M") matterhorn = data.get("matterhorn_summary", {}) breakdown = data.get("score_breakdown", {}) is_adjusted = breakdown.get("adjusted", False) score_color = "#059669" if score >= 80 else "#d97706" if score >= 60 else "#dc2626" sections_html = "" # Build accessible Matterhorn table with scope attrs if matterhorn and matterhorn.get("checkpoints"): mh_rows = "" for cp in matterhorn["checkpoints"]: status = cp["status"] if status == "PASS" and cp.get("manual"): status_cell = 'Manual Pass' elif status == "PASS": status_cell = 'PASS' elif status == "FAIL": status_cell = 'FAIL' else: status_cell = 'Not tested' mh_rows += f'CP{cp["id"]} {cp["name"]}{cp["how"]}{status_cell}' overall = "FULFILLED" if matterhorn.get("overall_passed") else "NOT FULFILLED" overall_cls = "pass" if matterhorn.get("overall_passed") else "fail" sections_html = f"""

Matterhorn Protocol — PDF/UA-1

{mh_rows}
Matterhorn Protocol checkpoint results
CheckpointHowStatus
""" if issues: issue_rows = "" for iss in issues: sev = iss.get("severity", "INFO") issue_rows += f""" {sev} {iss.get("category", "")} {iss.get("page_number") or "—"} {iss.get("description", "")} """ sections_html += f"""

Issues ({len(issues)})

{issue_rows}
Accessibility issues found in the document
SeverityCategoryPageDescription
""" # Compliance section for PDF compliance = data.get('wcag_compliance', {}) if compliance: a_pass = compliance.get('level_a', False) aa_pass = compliance.get('level_aa', False) a_cls = 'pass' if a_pass else 'fail' aa_cls = 'pass' if aa_pass else 'fail' a_text = '✓ Pass' if a_pass else '✗ Fail' aa_text = '✓ Pass' if aa_pass else '✗ Fail' sections_html += f"""

WCAG 2.1 Conformance

""" next_steps = data.get('next_steps', []) if next_steps: ns_rows = '' for i, s in enumerate(next_steps, 1): pl = {1: 'Critical', 2: 'Error', 3: 'Warning'}.get(s.get('priority', 3), '') ns_rows += f'{i}{pl}{s.get("category","")}{s.get("action","")}' sections_html += f"""

Recommended Next Steps

{ns_rows}
Prioritised remediation actions
#PriorityCategoryAction
""" html_content = f""" Accessibility Report — {filename}
{sc.get('critical',0)}
Critical
{sc.get('error',0)}
Errors
{sc.get('warning',0)}
Warnings
{sc.get('info',0)}
Info
{sections_html}
""" pdf_bytes = HTML(string=html_content).write_pdf() return pdf_bytes def main(): parser = argparse.ArgumentParser( description="HTML Report Generator — converts JSON accessibility results to standalone HTML." ) parser.add_argument("--input", "-i", required=True, help="Input JSON results file") parser.add_argument("--output", "-o", help="Output file (default: stdout)") parser.add_argument("--format", "-f", choices=["html", "pdf"], default="html", help="Output format: html (default) or pdf") args = parser.parse_args() input_path = Path(args.input) if not input_path.exists(): print(f"Error: {input_path} not found", file=sys.stderr) sys.exit(1) with open(input_path) as f: data = json.load(f) if args.format == "pdf": pdf_bytes = generate_pdf(data) if args.output: output_path = Path(args.output) output_path.parent.mkdir(parents=True, exist_ok=True) output_path.write_bytes(pdf_bytes) print(f"Report saved to {args.output}", file=sys.stderr) else: sys.stdout.buffer.write(pdf_bytes) else: html = generate_html(data) if args.output: output_path = Path(args.output) output_path.parent.mkdir(parents=True, exist_ok=True) output_path.write_text(html, encoding="utf-8") print(f"Report saved to {args.output}", file=sys.stderr) else: sys.stdout.write(html) if __name__ == "__main__": main()