CoolFace
Apppublic

hpcompaq435/accessaudit-scanner

sourceHugging Faceupdated 4mo agoView on Hugging Face
0likes
report.ts181 linesDownload Raw Back to src
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      ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[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 &amp; 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