CoolFace
Apppublic

hpcompaq435/accessaudit-scanner

sourceHugging Faceupdated 4mo agoView on Hugging Face
0likes
remediate.ts320 linesDownload Raw Back to src
1import type { ScannedViolation, Impact } from "./types.js";2 3export type Priority = "now" | "soon" | "later";4 5export interface Remediation {6  ruleId: string;7  priority: Priority;8  /** Who is hurt by this issue, in plain language */9  whoItAffects: string;10  /** Plain-English explanation of the problem */11  plainExplanation: string;12  /** Step-by-step fix instructions */13  howToFix: string;14  /** A concrete code example (may be empty) */15  codeExample: string;16}17 18export interface RemediatedViolation extends ScannedViolation {19  remediation: Remediation;20}21 22const PRIORITY_BY_IMPACT: Record<Impact, Priority> = {23  critical: "now",24  serious: "now",25  moderate: "soon",26  minor: "later",27};28 29const PRIORITY_ORDER: Record<Priority, number> = { now: 0, soon: 1, later: 2 };30 31type Template = Omit<Remediation, "ruleId" | "priority">;32 33/**34 * Curated, deterministic remediation guidance keyed by axe-core rule id.35 * Covers the most common WCAG findings. Anything not listed falls back to a36 * sensible generic message built from axe's own description + help URL, so37 * every violation still gets a useful card. No LLM, no API, no data leaves38 * the server — and no risk of hallucinated compliance advice.39 */40const TEMPLATES: Record<string, Template> = {41  "image-alt": {42    whoItAffects: "Blind and low-vision people using screen readers.",43    plainExplanation:44      "Images are missing alt text, so screen readers cannot describe them — they announce nothing or just the file name.",45    howToFix:46      "Add a descriptive alt attribute to each meaningful image. For purely decorative images, use an empty alt (alt=\"\") so screen readers skip them.",47    codeExample:48      '<img src="chart.png" alt="Revenue rose 20% in Q3">\n<!-- decorative image: --> <img src="divider.png" alt="">',49  },50  "input-image-alt": {51    whoItAffects: "Screen-reader users operating image buttons.",52    plainExplanation:53      'An <input type="image"> button has no alt text, so its purpose is not announced.',54    howToFix: "Add an alt attribute describing the button's action.",55    codeExample: '<input type="image" src="search.png" alt="Search">',56  },57  "area-alt": {58    whoItAffects: "Screen-reader users navigating image maps.",59    plainExplanation: "An image-map <area> link has no accessible name.",60    howToFix: "Add alt text describing where the area links to.",61    codeExample: '<area shape="rect" coords="0,0,80,40" href="/home" alt="Home">',62  },63  "color-contrast": {64    whoItAffects:65      "People with low vision or colour-blindness, and anyone in bright sunlight.",66    plainExplanation:67      "Text does not have enough contrast against its background. WCAG AA requires at least 4.5:1 for normal text and 3:1 for large text.",68    howToFix:69      "Darken the text or lighten the background until the ratio passes. Verify with a contrast checker.",70    codeExample:71      "/* before: #999 on #fff = 2.8:1 (fails) */\n/* after:  */ color: #595959; /* on #fff = 7:1 (passes) */",72  },73  "link-name": {74    whoItAffects: "Screen-reader and voice-control users.",75    plainExplanation:76      'A link has no discernible text (often an icon-only link), so it is announced as just "link".',77    howToFix:78      "Add visible text, or an aria-label / visually-hidden text describing the destination.",79    codeExample: '<a href="/cart" aria-label="View cart"><svg aria-hidden="true">…</svg></a>',80  },81  "button-name": {82    whoItAffects: "Screen-reader and voice-control users.",83    plainExplanation:84      "A button has no accessible name, so its purpose is not announced.",85    howToFix: "Put text inside the button, or add an aria-label.",86    codeExample: '<button aria-label="Close dialog"><svg aria-hidden="true">…</svg></button>',87  },88  label: {89    whoItAffects: "Screen-reader users filling in forms.",90    plainExplanation:91      "A form field has no associated label, so its purpose is not announced when focused.",92    howToFix:93      "Add a <label> linked via for/id, or wrap the input inside the label.",94    codeExample: '<label for="email">Email</label>\n<input id="email" type="email">',95  },96  "select-name": {97    whoItAffects: "Screen-reader users.",98    plainExplanation: "A <select> dropdown has no accessible name.",99    howToFix: "Associate a <label> with the select via for/id.",100    codeExample: '<label for="country">Country</label>\n<select id="country">…</select>',101  },102  "document-title": {103    whoItAffects:104      "Everyone — especially screen-reader users and people with many tabs open.",105    plainExplanation:106      "The page has no <title>, so it cannot be identified in browser tabs, history, or by screen readers.",107    howToFix: "Add a unique, descriptive <title> in the <head>.",108    codeExample: "<title>Checkout — Acme Shop</title>",109  },110  "html-has-lang": {111    whoItAffects:112      "Screen-reader users (for correct pronunciation) and translation tools.",113    plainExplanation:114      "The <html> element has no lang attribute, so assistive tech cannot tell what language the page is in.",115    howToFix: "Add a lang attribute to <html>.",116    codeExample: '<html lang="en">',117  },118  "html-lang-valid": {119    whoItAffects: "Screen-reader users.",120    plainExplanation: "The lang attribute value is not a valid language code.",121    howToFix: "Use a valid BCP-47 code, e.g. en, de, id.",122    codeExample: '<html lang="de">',123  },124  "landmark-one-main": {125    whoItAffects: "Screen-reader users.",126    plainExplanation:127      "The page has no <main> landmark, so users cannot jump straight to the primary content.",128    howToFix: "Wrap the primary content in a single <main> element.",129    codeExample: "<main>\n  <!-- primary page content -->\n</main>",130  },131  region: {132    whoItAffects: "Screen-reader users who navigate by region/landmark.",133    plainExplanation:134      "Some content sits outside any landmark region, making it harder to navigate the page quickly.",135    howToFix:136      "Wrap page sections in landmarks: <header>, <nav>, <main>, <footer>.",137    codeExample: "<header>…</header>\n<main>…</main>\n<footer>…</footer>",138  },139  "heading-order": {140    whoItAffects: "Screen-reader users who navigate by headings.",141    plainExplanation:142      "Heading levels skip (e.g. an h1 is followed by an h3), breaking the document outline.",143    howToFix:144      "Use heading levels in order without skipping; change size with CSS, not by picking a different level.",145    codeExample: "<h1>Title</h1>\n  <h2>Section</h2>\n    <h3>Subsection</h3>",146  },147  "empty-heading": {148    whoItAffects: "Screen-reader users.",149    plainExplanation:150      "A heading element is empty, creating a confusing contentless entry in the outline.",151    howToFix: "Add text to the heading, or remove it.",152    codeExample: "<h2>Pricing</h2>",153  },154  "page-has-heading-one": {155    whoItAffects: "Screen-reader users.",156    plainExplanation:157      "The page has no <h1>, so there is no clear top-level title in the outline.",158    howToFix: "Add one <h1> describing the page's main topic.",159    codeExample: "<h1>Accessibility report</h1>",160  },161  list: {162    whoItAffects: "Screen-reader users.",163    plainExplanation:164      "A <ul>/<ol> contains elements other than <li>, so the list semantics break.",165    howToFix:166      "Ensure only <li> elements are direct children of the list.",167    codeExample: "<ul>\n  <li>One</li>\n  <li>Two</li>\n</ul>",168  },169  listitem: {170    whoItAffects: "Screen-reader users.",171    plainExplanation:172      "An <li> is not contained in a <ul> or <ol>, so it is not announced as a list item.",173    howToFix: "Wrap <li> elements in a <ul> or <ol>.",174    codeExample: "<ul><li>Item</li></ul>",175  },176  "duplicate-id-active": {177    whoItAffects: "Screen-reader users and scripts relying on ids.",178    plainExplanation:179      "The same id is used by more than one active element, which breaks label/ARIA associations and scripting.",180    howToFix: "Make every id unique on the page.",181    codeExample: '<input id="email-1"> … <input id="email-2">',182  },183  "duplicate-id-aria": {184    whoItAffects: "Screen-reader users.",185    plainExplanation:186      "An id referenced by ARIA (e.g. aria-labelledby) is used more than once, so the wrong element may be referenced.",187    howToFix: "Make ARIA-referenced ids unique.",188    codeExample: '<h2 id="sec-billing">Billing</h2>\n<section aria-labelledby="sec-billing">…</section>',189  },190  "aria-required-attr": {191    whoItAffects: "Screen-reader users.",192    plainExplanation:193      "An element with an ARIA role is missing an attribute that role requires, so its state is not announced.",194    howToFix:195      "Add the required attribute(s) for the role (see the rule reference), or use a native HTML element instead.",196    codeExample: '<div role="checkbox" aria-checked="false" tabindex="0">…</div>',197  },198  "aria-valid-attr-value": {199    whoItAffects: "Screen-reader users.",200    plainExplanation:201      "An ARIA attribute has an invalid value (or points to an id that does not exist), so assistive tech may ignore it.",202    howToFix:203      "Use a valid value, and make sure any id referenced by aria-* exists on the page.",204    codeExample: '<button aria-expanded="false" aria-controls="menu">Menu</button>',205  },206  "aria-roles": {207    whoItAffects: "Screen-reader users.",208    plainExplanation: "An element uses an ARIA role that is not valid.",209    howToFix:210      "Use a valid ARIA role, or prefer a native HTML element with built-in semantics.",211    codeExample: "<nav>…</nav> <!-- instead of <div role=\"navigation\"> -->",212  },213  "aria-hidden-focus": {214    whoItAffects: "Screen-reader users.",215    plainExplanation:216      'A focusable element sits inside an aria-hidden="true" container, so it is reachable by keyboard but invisible to screen readers.',217    howToFix:218      "Remove aria-hidden from the container, or make the inner element non-focusable while hidden (tabindex=-1).",219    codeExample: '<div aria-hidden="true"><button tabindex="-1">…</button></div>',220  },221  "frame-title": {222    whoItAffects: "Screen-reader users.",223    plainExplanation:224      "An <iframe> has no title, so its purpose is not announced.",225    howToFix: "Add a descriptive title attribute to the iframe.",226    codeExample: '<iframe src="…" title="Payment form"></iframe>',227  },228  bypass: {229    whoItAffects: "Keyboard and screen-reader users.",230    plainExplanation:231      "There is no way to skip repeated blocks (like navigation) to reach the main content.",232    howToFix:233      "Add a skip link at the top of the page that targets <main>, and/or provide landmarks and headings.",234    codeExample:235      '<a class="skip-link" href="#main">Skip to content</a>\n…\n<main id="main">…</main>',236  },237  "meta-viewport": {238    whoItAffects: "Low-vision users who need to zoom.",239    plainExplanation:240      "The viewport meta tag disables zooming (user-scalable=no or a low maximum-scale), preventing people from enlarging text.",241    howToFix:242      "Remove user-scalable=no and do not cap maximum-scale below 5.",243    codeExample: '<meta name="viewport" content="width=device-width, initial-scale=1">',244  },245  "th-has-data-cells": {246    whoItAffects: "Screen-reader users reading data tables.",247    plainExplanation:248      "Table header cells are not properly associated with data cells, so the relationships are lost.",249    howToFix:250      'Use <th scope="col"> / scope="row", and ensure each header has matching data cells.',251    codeExample: '<th scope="col">Price</th>',252  },253  "video-caption": {254    whoItAffects: "Deaf and hard-of-hearing users.",255    plainExplanation: "A <video> has no captions track.",256    howToFix: 'Provide captions via a <track kind="captions"> element.',257    codeExample:258      '<video><track kind="captions" src="cc.vtt" srclang="en" label="English"></video>',259  },260  "nested-interactive": {261    whoItAffects: "Screen-reader and keyboard users.",262    plainExplanation:263      "Interactive controls are nested inside each other (e.g. a button inside a link), causing unpredictable focus.",264    howToFix: "Keep one control per interactive element; do not nest them.",265    codeExample: "<button>Buy</button> <!-- not <a><button>…</button></a> -->",266  },267  tabindex: {268    whoItAffects: "Keyboard users.",269    plainExplanation:270      "A positive tabindex forces an unnatural tab order that is hard to follow.",271    howToFix:272      'Use tabindex="0" (or none) and rely on DOM order; avoid positive values.',273    codeExample: '<div tabindex="0">…</div>',274  },275};276 277function genericRemediation(v: ScannedViolation): Template {278  const who =279    v.impact === "critical" || v.impact === "serious"280      ? "People using screen readers or keyboard navigation may be blocked."281      : v.impact === "moderate"282        ? "Some users with disabilities will have a noticeably degraded experience."283        : "A small number of users may be inconvenienced.";284  return {285    whoItAffects: who,286    plainExplanation: v.description || v.help,287    howToFix: `Follow the detailed guidance and examples for this rule: ${v.helpUrl}`,288    codeExample: "",289  };290}291 292function buildRemediation(v: ScannedViolation): Remediation {293  const t = TEMPLATES[v.id] ?? genericRemediation(v);294  return { ruleId: v.id, priority: PRIORITY_BY_IMPACT[v.impact], ...t };295}296 297/**298 * Enrich every violation with deterministic, rule-based remediation guidance.299 * Dedupes by rule id (repeated findings share one lookup) and sorts by priority.300 * Synchronous, but safe to `await` at call sites.301 */302export function remediateAll(303  violations: ScannedViolation[],304): RemediatedViolation[] {305  const fixes = new Map<string, Remediation>();306  for (const v of violations) {307    if (!fixes.has(v.id)) fixes.set(v.id, buildRemediation(v));308  }309  return violations310    .map((v) => ({ ...v, remediation: fixes.get(v.id)! }))311    .sort(312      (a, b) =>313        PRIORITY_ORDER[a.remediation.priority] -314        PRIORITY_ORDER[b.remediation.priority],315    );316}317 318/** Exposed for tests / tooling. */319export const TEMPLATED_RULE_IDS = Object.keys(TEMPLATES);320