hpcompaq435/accessaudit-scanner
0
1import { chromium } from "playwright";2import type { ScanResult } from "./types.js";3import type { RemediatedViolation, Priority } from "./remediate.js";4 5export interface ReportInput extends Omit<ScanResult, "violations"> {6 violations: RemediatedViolation[];7}8export interface ReportOpts {9 orgName?: string;10 contactEmail?: string;11}12 13const PRIORITY_LABEL: Record<Priority, string> = {14 now: "Fix now",15 soon: "Fix soon",16 later: "Later",17};18 19function esc(s: unknown): string {20 return String(s ?? "").replace(21 /[&<>"']/g,22 (c) =>23 ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]!,24 );25}26 27function safeHost(url: string): string {28 try {29 return new URL(url).host;30 } catch {31 return url;32 }33}34 35function fmtDate(iso: string): string {36 return new Date(iso).toLocaleDateString("en-GB", {37 year: "numeric",38 month: "long",39 day: "numeric",40 });41}42 43/** A publishable accessibility statement the site owner can put on their site. */44export function buildAccessibilityStatement(45 report: ReportInput,46 opts: ReportOpts = {},47): string {48 const org = opts.orgName ?? safeHost(report.url);49 const contact = opts.contactEmail ?? "[your contact email]";50 const date = fmtDate(report.scannedAt);51 const total = report.summary.total;52 const status = total === 0 ? "fully conformant" : "partially conformant";53 return `54<h2>Accessibility Statement</h2>55<p>${esc(org)} is committed to making its website accessible in accordance with56the European Accessibility Act and the Web Content Accessibility Guidelines57(WCAG) 2.2 level AA.</p>58<p><strong>Conformance status:</strong> This website is <strong>${status}</strong>59with WCAG 2.2 AA. ${60 total === 061 ? "Our latest automated audit found no outstanding issues."62 : `Our latest automated audit identified ${total} issue type(s) that are being addressed.`63 }</p>64<p><strong>Last reviewed:</strong> ${date}.</p>65<p><strong>Feedback:</strong> If you encounter accessibility barriers, contact us66at ${esc(contact)} and we will respond promptly.</p>67<p class="muted">This statement reflects an automated assessment. Automated68testing does not detect all accessibility issues; some require human review.</p>`;69}70 71function violationBlock(v: RemediatedViolation): string {72 const r = v.remediation;73 return `74<div class="v">75 <div class="vhead">76 <h3>${esc(v.help)}</h3>77 <span class="chip ${r?.priority ?? "later"}">${78 PRIORITY_LABEL[r?.priority ?? "later"]79 }</span>80 </div>81 <p class="meta">${esc(v.id)} · ${esc(v.impact)} · ${v.nodeCount} element(s) · ${esc(82 (v.wcagTags ?? []).join(", "),83 )}</p>84 ${r ? `<p><b>Who it affects:</b> ${esc(r.whoItAffects)}</p>85 <p>${esc(r.plainExplanation)}</p>86 <p><b>How to fix:</b> ${esc(r.howToFix)}</p>87 ${r.codeExample ? `<pre>${esc(r.codeExample)}</pre>` : ""}` : ""}88</div>`;89}90 91export function buildReportHtml(92 report: ReportInput,93 opts: ReportOpts = {},94): string {95 const s = report.summary;96 const violations = [...report.violations].sort(97 (a, b) =>98 ({ now: 0, soon: 1, later: 2 })[a.remediation?.priority ?? "later"] -99 ({ now: 0, soon: 1, later: 2 })[b.remediation?.priority ?? "later"],100 );101 return `<!doctype html>102<html lang="en"><head><meta charset="utf-8"><style>103 @page { margin: 0; }104 * { box-sizing: border-box; }105 body { font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;106 color: #14130f; margin: 0; font-size: 12px; line-height: 1.5; }107 .band { background: #14130f; color: #f5f3ec; padding: 22px 26px; }108 .band .brand { font-weight: 800; font-size: 18px; letter-spacing: -0.02em; }109 .band .brand i { color: #16c77e; font-style: normal; }110 .band .url { word-break: break-all; font-size: 13px; margin-top: 6px; }111 .body { padding: 22px 26px; }112 .scoreRow { display: flex; gap: 28px; align-items: center; border: 1px solid #d9d5c8;113 border-radius: 6px; padding: 16px 20px; margin-bottom: 18px; }114 .score { font-size: 44px; font-weight: 800; line-height: 1; }115 .sum { display: flex; gap: 20px; }116 .sum div b { display: block; font-size: 20px; }117 .sum div span { color: #6b675e; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; }118 h2 { font-size: 16px; margin: 22px 0 8px; }119 h3 { font-size: 13px; margin: 0; }120 .v { border-top: 1px solid #e7e3d8; padding: 12px 0; }121 .vhead { display: flex; justify-content: space-between; align-items: center; gap: 10px; }122 .meta { color: #6b675e; font-family: ui-monospace, monospace; font-size: 10px; margin: 4px 0 8px; }123 .chip { font-size: 10px; text-transform: uppercase; letter-spacing: .06em; color: #fff;124 padding: 2px 8px; border-radius: 99px; white-space: nowrap; }125 .chip.now { background: #b23b2e; } .chip.soon { background: #b07d12; } .chip.later { background: #4a4740; }126 pre { background: #0f0e0b; color: #f2efe6; padding: 10px; border-radius: 4px; overflow: hidden;127 white-space: pre-wrap; word-break: break-word; font-size: 10.5px; }128 .muted { color: #6b675e; font-size: 10.5px; }129 .statement { page-break-before: always; padding-top: 8px; }130 .foot { margin-top: 18px; border-top: 1px solid #d9d5c8; padding-top: 8px; }131</style></head><body>132 <div class="band">133 <div class="brand">Access<i>Audit</i></div>134 <div class="url">${esc(report.url)}</div>135 <div style="font-size:11px;opacity:.8;margin-top:4px">WCAG 2.2 AA · EAA · ${fmtDate(136 report.scannedAt,137 )}</div>138 </div>139 <div class="body">140 <div class="scoreRow">141 <div><div class="score">${report.score}</div><div class="muted">/ 100</div></div>142 <div class="sum">143 <div><b>${s.critical}</b><span>critical</span></div>144 <div><b>${s.serious}</b><span>serious</span></div>145 <div><b>${s.moderate}</b><span>moderate</span></div>146 <div><b>${s.minor}</b><span>minor</span></div>147 </div>148 </div>149 <h2>Issues & recommended fixes</h2>150 ${violations.length ? violations.map(violationBlock).join("") : '<p class="muted">No automated issues found.</p>'}151 <div class="foot muted">152 AccessAudit is an auditing and monitoring tool, not a legal service, and does153 not guarantee legal compliance. Automated testing does not detect every issue;154 complex cases require human review.155 </div>156 <div class="statement">${buildAccessibilityStatement(report, opts)}</div>157 </div>158</body></html>`;159}160 161/** Render the report HTML to a PDF buffer using the bundled Chromium. */162export async function renderReportPdf(163 report: ReportInput,164 opts: ReportOpts = {},165): Promise<Buffer> {166 const html = buildReportHtml(report, opts);167 const browser = await chromium.launch({168 args: ["--no-sandbox", "--disable-dev-shm-usage", "--disable-gpu"],169 });170 try {171 const page = await browser.newPage();172 await page.setContent(html, { waitUntil: "networkidle" });173 return await page.pdf({174 format: "A4",175 printBackground: true,176 });177 } finally {178 await browser.close();179 }180}181 