wcag-a11y 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/ai/fallback-explanation.js +67 -0
- package/dist/ai/gemini.js +8 -3
- package/dist/ai/ollama.js +8 -3
- package/dist/ai/openai.js +8 -3
- package/dist/ai/prompt.js +3 -3
- package/dist/cli.js +16 -6
- package/dist/reporter/markdown.js +26 -3
- package/dist/reporter/terminal.js +21 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -87,14 +87,14 @@ Each command creates an `a11y.config.json` pre-wired for that provider. Fill in
|
|
|
87
87
|
Scan a built-in page with 10 intentional violations. No dev server or config required — useful for trying the tool before pointing it at your own project.
|
|
88
88
|
|
|
89
89
|
```bash
|
|
90
|
-
wcag-a11y demo # violations
|
|
91
|
-
wcag-a11y demo --ai
|
|
92
|
-
wcag-a11y demo --
|
|
90
|
+
wcag-a11y demo # violations + AI fixes (default, requires config)
|
|
91
|
+
wcag-a11y demo --no-ai # violations only, no AI — faster
|
|
92
|
+
wcag-a11y demo --report # + save a11y-report.md
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
| Flag | Description |
|
|
96
96
|
|---|---|
|
|
97
|
-
| `--ai` |
|
|
97
|
+
| `--no-ai` | Skip AI fix generation — prints violations only, no prompts |
|
|
98
98
|
| `-r, --report` | Save the full report to `a11y-report.md` in the current directory |
|
|
99
99
|
|
|
100
100
|
---
|
|
@@ -122,7 +122,7 @@ Scan a running dev server for accessibility violations.
|
|
|
122
122
|
```bash
|
|
123
123
|
wcag-a11y scan -u http://localhost:3000
|
|
124
124
|
wcag-a11y scan -u http://localhost:3000 --pages / /about /contact
|
|
125
|
-
wcag-a11y scan -u http://localhost:3000 --crawl --
|
|
125
|
+
wcag-a11y scan -u http://localhost:3000 --crawl --report
|
|
126
126
|
wcag-a11y scan -u http://localhost:3000 --no-ai --ci
|
|
127
127
|
```
|
|
128
128
|
|
|
@@ -132,7 +132,7 @@ wcag-a11y scan -u http://localhost:3000 --no-ai --ci
|
|
|
132
132
|
| `-p, --pages <pages...>` | `/` | One or more paths to scan. Separate with spaces: `--pages / /about /contact` |
|
|
133
133
|
| `-c, --crawl` | off | Follow same-origin links and scan all reachable pages automatically |
|
|
134
134
|
| `-r, --report` | off | Save the full scan output to `a11y-report.md` |
|
|
135
|
-
| `--
|
|
135
|
+
| `--no-ai` | on | Skip AI fix generation — scan runs faster and prints violations only |
|
|
136
136
|
| `--no-explain` | off | Print only the ready-to-paste prompt for each fix, without the AI explanation |
|
|
137
137
|
| `--group <strategy>` | `rule` | `rule` (default) groups all violations of the same type into one fix prompt. `none` produces a separate prompt per element. Use `none` when violations of the same rule need different fixes |
|
|
138
138
|
| `--ci` | off | Exit with code `1` if any violations are found. Use this to fail a CI pipeline |
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const EXPLANATIONS = {
|
|
2
|
+
// Text alternatives
|
|
3
|
+
'img-alt': 'Screen reader users hear nothing for this image — any information it conveys (branding, instructions, data) is completely invisible to them.',
|
|
4
|
+
'input-image-alt': 'Screen readers announce this button by its file name (e.g. "submit.png") instead of its purpose, so users cannot tell what the button does.',
|
|
5
|
+
'svg-title': 'Screen reader users receive no label for this SVG; if it conveys meaning (an icon, a chart), that meaning is entirely lost.',
|
|
6
|
+
'object-alt': 'Screen reader users receive no description for this embedded object; its content is skipped as if it does not exist.',
|
|
7
|
+
'role-img-alt': 'Elements marked with `role="img"` are announced as "image" with no label, giving screen reader users no context about what is depicted.',
|
|
8
|
+
'image-redundant-alt': 'Screen reader users hear the same text twice — once from the alt attribute and once from adjacent text — creating a disorienting, repetitive experience.',
|
|
9
|
+
// Tables
|
|
10
|
+
'table-headers': 'Screen reader users navigating a data table cell by cell hear only isolated values with no column or row context — like reading a spreadsheet with all headers removed.',
|
|
11
|
+
'table-scope-valid': 'Invalid `scope` values cause screen readers to misidentify which headers apply to which cells, giving users incorrect data relationships.',
|
|
12
|
+
'td-headers-attr': '`headers` attributes pointing to non-existent IDs are silently ignored, leaving table cells without any header association for screen reader users.',
|
|
13
|
+
'table-duplicate-name': 'When a table\'s caption and summary say the same thing, screen reader users hear the description twice, wasting time and causing confusion.',
|
|
14
|
+
// Structure
|
|
15
|
+
'heading-order': 'Skipping heading levels (e.g. h1 → h3) breaks the document outline; screen reader users navigating by headings lose the logical hierarchy and cannot tell which sections are sub-sections of others.',
|
|
16
|
+
'page-title': 'Screen reader users hear a blank or generic title when switching tabs and cannot identify the page without reading its full content.',
|
|
17
|
+
'landmark-one-main': 'Without a `<main>` landmark, screen reader users cannot skip directly to page content and must tab through all navigation on every single page load.',
|
|
18
|
+
'list-structure': 'Content that looks like a list but uses `<div>` or `<p>` instead of `<ul>`/`<li>` is read as plain text; screen readers won\'t announce item count or allow list-navigation shortcuts.',
|
|
19
|
+
'region-landmark': 'Content outside any landmark region is missed by screen reader users who navigate by landmarks — `<header>`, `<main>`, `<nav>`, `<footer>` etc.',
|
|
20
|
+
'duplicate-id': 'Duplicate IDs silently break ARIA relationships (`aria-labelledby`, `aria-describedby`): the wrong element may be read, or the association ignored entirely.',
|
|
21
|
+
'frame-title': 'Screen reader users hear "frame" with no description and cannot tell if a frame contains a cookie banner, an advertisement, or a critical widget without entering it.',
|
|
22
|
+
'meta-viewport': '`user-scalable=no` or `maximum-scale` prevents users with low vision from pinch-zooming — removing the browser\'s most basic accessibility tool for text size.',
|
|
23
|
+
'marquee': '`<marquee>` moves content automatically with no pause control; users with cognitive disabilities, ADHD, or vestibular disorders find it disorienting or impossible to read.',
|
|
24
|
+
'p-as-heading': 'Visual headings styled as bold `<p>` tags are not exposed as headings in the accessibility tree; screen reader users navigating by headings will skip this section entirely.',
|
|
25
|
+
// Media
|
|
26
|
+
'video-captions': 'Deaf and hard-of-hearing users cannot access any spoken content in the video — dialogue, narration, and audio cues are entirely unavailable to them.',
|
|
27
|
+
'audio-description': 'Blind users miss visual-only information in the video (on-screen text, actions, scene changes) that is never described in the audio track.',
|
|
28
|
+
'audio-transcript': 'Deaf users cannot access audio-only content (e.g. podcasts, voice instructions) without a text transcript; the content is completely inaccessible to them.',
|
|
29
|
+
// Links
|
|
30
|
+
'link-name': 'Screen reader users navigating by links hear only "link" with no destination — they cannot determine where it leads without reading the surrounding paragraph.',
|
|
31
|
+
'link-empty': 'An anchor with no text content is announced as an empty unlabeled link; screen reader users encounter a dead spot that provides no usable information.',
|
|
32
|
+
'identical-links-different-purpose': 'Multiple links that read the same (e.g. "Read more") but go to different destinations give screen reader users no way to distinguish them when browsing the links list.',
|
|
33
|
+
'link-new-window-warn': 'Links that silently open new tabs disorient keyboard and screen reader users — the Back button no longer works and the unexpected context shift is never announced.',
|
|
34
|
+
// Forms
|
|
35
|
+
'label-missing': 'Screen reader users tab to this field and hear only the input type (e.g. "text, edit"); they receive no indication of what information the field expects.',
|
|
36
|
+
'label-empty': 'The `<label>` exists but contains no text; screen reader users hear the input type only, with no indication of the field\'s purpose.',
|
|
37
|
+
'error-identification': 'Form validation errors are conveyed visually only; screen reader users receive no programmatic notification and cannot identify which fields failed or what went wrong.',
|
|
38
|
+
'autocomplete': 'Without the correct `autocomplete` attribute, password managers and browser autofill cannot identify the field, forcing users with motor disabilities to type credentials manually every time.',
|
|
39
|
+
'input-button-name': 'Screen reader users hear "button" with no label and cannot determine what action the button performs.',
|
|
40
|
+
'fieldset-legend': 'Related form controls have no group label; screen reader users navigating into the group have no context for what the inputs belong to (e.g. "Is this billing or shipping?").',
|
|
41
|
+
'form-field-required-label': 'Required fields are not programmatically marked as required; screen reader users are not warned the field is mandatory and may submit an incomplete form unexpectedly.',
|
|
42
|
+
// Language
|
|
43
|
+
'html-lang': 'Without a `lang` attribute, screen readers use the system language to read all content; foreign-language text is mispronounced and can be incomprehensible.',
|
|
44
|
+
'html-lang-valid': 'An unrecognized `lang` value causes screen readers to fall back to the system language, mispronouncing content and breaking language-specific text-to-speech features.',
|
|
45
|
+
// Color contrast
|
|
46
|
+
'color-contrast-text': 'Users with low vision or color blindness cannot distinguish this text from its background at the current contrast ratio; the text becomes difficult or impossible to read without assistive magnification.',
|
|
47
|
+
'color-contrast-large-text': 'Despite large text having a more relaxed contrast requirement, this element still fails that threshold; users with moderate low vision cannot read it reliably.',
|
|
48
|
+
// Keyboard
|
|
49
|
+
'no-positive-tabindex': 'Positive `tabindex` values override the natural tab order, causing keyboard users to jump erratically around the page instead of following its logical structure.',
|
|
50
|
+
'interactive-not-focusable': 'Keyboard users and screen reader users cannot reach this interactive element by tabbing — it is effectively invisible and unusable to anyone not using a mouse.',
|
|
51
|
+
'skip-link': 'Without a "skip to main content" link, keyboard users must tab through every navigation item on every page load before reaching the main content — often 20–40 tab presses on complex sites.',
|
|
52
|
+
'focus-visible': 'Keyboard users cannot see which element currently has focus; they lose their place on the page and cannot tell where their next keypress will act.',
|
|
53
|
+
'scrollable-region-focusable': 'A scrollable area that cannot receive keyboard focus traps keyboard users — they can see there is more content but cannot scroll to it without a mouse.',
|
|
54
|
+
'accesskey-unique': 'Duplicate `accesskey` values cause browsers to cycle through elements unpredictably; keyboard shortcut users cannot rely on the shortcut to reach the intended element.',
|
|
55
|
+
// ARIA
|
|
56
|
+
'aria-valid-role': 'An unrecognized ARIA role is ignored by assistive technologies; the element is presented without semantic meaning and screen reader users may navigate past it or interact with it incorrectly.',
|
|
57
|
+
'aria-required-attr': 'This ARIA widget is missing required attributes (e.g. `aria-checked` on `role="checkbox"`); screen readers cannot communicate the element\'s state, so users don\'t know if it is active, checked, or selected.',
|
|
58
|
+
'aria-hidden-focus': 'A focusable element inside `aria-hidden` receives keyboard focus but is invisible to screen readers; keyboard users land on an element and the screen reader announces nothing.',
|
|
59
|
+
'button-name': 'Screen reader users hear "button" with no label and cannot determine what action the button performs without exploring surrounding content visually.',
|
|
60
|
+
'aria-required-children': 'This ARIA container is missing required child roles (e.g. a `listbox` with no `option` children); screen readers either skip the widget or announce it in an unpredictable way.',
|
|
61
|
+
'aria-required-parent': 'This ARIA element is outside its required parent context; screen readers cannot establish the correct ownership relationship and may misreport the element\'s role or state.',
|
|
62
|
+
'aria-prohibited-attr': 'ARIA attributes applied where they are prohibited override the element\'s built-in semantics, causing screen readers to misannounce the element\'s role or state.',
|
|
63
|
+
};
|
|
64
|
+
export function fallbackExplanation(ruleId, description, wcag, level) {
|
|
65
|
+
return EXPLANATIONS[ruleId]
|
|
66
|
+
?? `${description} — this prevents some users from accessing or understanding content on this page (WCAG 2.1 SC ${wcag}, Level ${level}).`;
|
|
67
|
+
}
|
package/dist/ai/gemini.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { buildPrompt } from './prompt.js';
|
|
2
2
|
import { groupViolations } from './group.js';
|
|
3
|
+
import { fallbackExplanation } from './fallback-explanation.js';
|
|
3
4
|
export class GeminiProvider {
|
|
4
5
|
apiKey;
|
|
5
6
|
model;
|
|
@@ -45,15 +46,19 @@ export class GeminiProvider {
|
|
|
45
46
|
}
|
|
46
47
|
fallbackFix(g) {
|
|
47
48
|
const v = g.representative;
|
|
48
|
-
const
|
|
49
|
+
const selectorList = g.selectors.map((s) => `- ${s}`).join('\n');
|
|
50
|
+
const explanation = fallbackExplanation(g.ruleId, g.description, g.wcag, g.level);
|
|
51
|
+
const prompt = g.count > 1
|
|
52
|
+
? `Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected elements (${g.count} instances):\n${selectorList}\n\nRepresentative HTML:\n\`${v.html.slice(0, 300)}\`\n\nApply the fix to all ${g.count} instances in the codebase to comply with WCAG 2.1 SC ${g.wcag}.`
|
|
53
|
+
: `Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected element:\n- Selector: \`${g.selectors[0]}\`\n- HTML: \`${v.html.slice(0, 300)}\`\n\nApply the fix to comply with WCAG 2.1 SC ${g.wcag}.`;
|
|
49
54
|
return {
|
|
50
55
|
ruleId: g.ruleId,
|
|
51
56
|
selectors: g.selectors,
|
|
52
57
|
instanceCount: g.count,
|
|
53
|
-
explanation
|
|
58
|
+
explanation,
|
|
54
59
|
fixedCode: v.html,
|
|
55
60
|
wcagReference: `WCAG 2.1 SC ${g.wcag}`,
|
|
56
|
-
optimalPrompt:
|
|
61
|
+
optimalPrompt: prompt,
|
|
57
62
|
};
|
|
58
63
|
}
|
|
59
64
|
}
|
package/dist/ai/ollama.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { buildPrompt } from './prompt.js';
|
|
2
2
|
import { groupViolations } from './group.js';
|
|
3
|
+
import { fallbackExplanation } from './fallback-explanation.js';
|
|
3
4
|
export class OllamaProvider {
|
|
4
5
|
baseUrl;
|
|
5
6
|
model;
|
|
@@ -43,15 +44,19 @@ export class OllamaProvider {
|
|
|
43
44
|
}
|
|
44
45
|
fallbackFix(g) {
|
|
45
46
|
const v = g.representative;
|
|
46
|
-
const
|
|
47
|
+
const selectorList = g.selectors.map((s) => `- ${s}`).join('\n');
|
|
48
|
+
const explanation = fallbackExplanation(g.ruleId, g.description, g.wcag, g.level);
|
|
49
|
+
const prompt = g.count > 1
|
|
50
|
+
? `Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected elements (${g.count} instances):\n${selectorList}\n\nRepresentative HTML:\n\`${v.html.slice(0, 300)}\`\n\nApply the fix to all ${g.count} instances in the codebase to comply with WCAG 2.1 SC ${g.wcag}.`
|
|
51
|
+
: `Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected element:\n- Selector: \`${g.selectors[0]}\`\n- HTML: \`${v.html.slice(0, 300)}\`\n\nApply the fix to comply with WCAG 2.1 SC ${g.wcag}.`;
|
|
47
52
|
return {
|
|
48
53
|
ruleId: g.ruleId,
|
|
49
54
|
selectors: g.selectors,
|
|
50
55
|
instanceCount: g.count,
|
|
51
|
-
explanation
|
|
56
|
+
explanation,
|
|
52
57
|
fixedCode: v.html,
|
|
53
58
|
wcagReference: `WCAG 2.1 SC ${g.wcag}`,
|
|
54
|
-
optimalPrompt:
|
|
59
|
+
optimalPrompt: prompt,
|
|
55
60
|
};
|
|
56
61
|
}
|
|
57
62
|
}
|
package/dist/ai/openai.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { buildPrompt } from './prompt.js';
|
|
2
2
|
import { groupViolations } from './group.js';
|
|
3
|
+
import { fallbackExplanation } from './fallback-explanation.js';
|
|
3
4
|
export class OpenAIProvider {
|
|
4
5
|
apiKey;
|
|
5
6
|
model;
|
|
@@ -49,15 +50,19 @@ export class OpenAIProvider {
|
|
|
49
50
|
}
|
|
50
51
|
fallbackFix(g) {
|
|
51
52
|
const v = g.representative;
|
|
52
|
-
const
|
|
53
|
+
const selectorList = g.selectors.map((s) => `- ${s}`).join('\n');
|
|
54
|
+
const explanation = fallbackExplanation(g.ruleId, g.description, g.wcag, g.level);
|
|
55
|
+
const prompt = g.count > 1
|
|
56
|
+
? `Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected elements (${g.count} instances):\n${selectorList}\n\nRepresentative HTML:\n\`${v.html.slice(0, 300)}\`\n\nApply the fix to all ${g.count} instances in the codebase to comply with WCAG 2.1 SC ${g.wcag}.`
|
|
57
|
+
: `Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected element:\n- Selector: \`${g.selectors[0]}\`\n- HTML: \`${v.html.slice(0, 300)}\`\n\nApply the fix to comply with WCAG 2.1 SC ${g.wcag}.`;
|
|
53
58
|
return {
|
|
54
59
|
ruleId: g.ruleId,
|
|
55
60
|
selectors: g.selectors,
|
|
56
61
|
instanceCount: g.count,
|
|
57
|
-
explanation
|
|
62
|
+
explanation,
|
|
58
63
|
fixedCode: v.html,
|
|
59
64
|
wcagReference: `WCAG 2.1 SC ${g.wcag}`,
|
|
60
|
-
optimalPrompt:
|
|
65
|
+
optimalPrompt: prompt,
|
|
61
66
|
};
|
|
62
67
|
}
|
|
63
68
|
}
|
package/dist/ai/prompt.js
CHANGED
|
@@ -11,10 +11,10 @@ export function buildPrompt(groups) {
|
|
|
11
11
|
Each item must have:
|
|
12
12
|
- "ruleId": the rule id from the input
|
|
13
13
|
- "selectors": array of CSS selectors affected (copy from input)
|
|
14
|
-
- "explanation": 1-2 sentences
|
|
14
|
+
- "explanation": 1-2 sentences on concrete user impact — name exactly who is affected (e.g. "screen reader users", "keyboard-only users", "users with low vision") and what they cannot do because of this specific violation. Do not write generic statements like "users with disabilities may be affected."
|
|
15
15
|
- "fixedCode": the corrected HTML snippet only (no explanation, just code)
|
|
16
|
-
- "wcagReference": e.g. "WCAG 2.1 SC 1.1.1
|
|
17
|
-
- "optimalPrompt": a ready-to-paste prompt
|
|
16
|
+
- "wcagReference": the full criterion name, e.g. "WCAG 2.1 SC 1.1.1 Non-text Content (Level A)"
|
|
17
|
+
- "optimalPrompt": a ready-to-paste prompt for an AI coding assistant (Cursor, Copilot, Claude). Structure it as: (1) state the WCAG 2.1/2.2 criterion being violated (e.g. "WCAG 2.1 SC 1.1.1 Non-text Content, Level A"), (2) list the affected selectors and HTML snippets, (3) state the exact code change needed to comply. Focus solely on the fix — do not explain why WCAG exists or what accessibility is. Be precise and actionable.
|
|
18
18
|
|
|
19
19
|
Return ONLY a valid JSON array. No markdown, no code fences, no explanation outside the JSON.
|
|
20
20
|
|
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Command } from 'commander';
|
|
|
3
3
|
import { loadConfig, initConfig } from './config.js';
|
|
4
4
|
import { crawl } from './crawler.js';
|
|
5
5
|
import { createAIProvider } from './ai/index.js';
|
|
6
|
+
import { groupViolations } from './ai/group.js';
|
|
6
7
|
import { printTerminalReport, printAIPrompts } from './reporter/terminal.js';
|
|
7
8
|
import { generateMarkdownReport } from './reporter/markdown.js';
|
|
8
9
|
import { runDemo } from './demo.js';
|
|
@@ -10,7 +11,7 @@ const program = new Command();
|
|
|
10
11
|
program
|
|
11
12
|
.name('wcag-a11y')
|
|
12
13
|
.description('WCAG 2.1/2.2 accessibility auditor with AI-powered fixes')
|
|
13
|
-
.version('0.
|
|
14
|
+
.version('0.3.0');
|
|
14
15
|
program
|
|
15
16
|
.command('init')
|
|
16
17
|
.description('Create a11y.config.json in the current directory')
|
|
@@ -27,6 +28,8 @@ program
|
|
|
27
28
|
.option('-r, --report', 'Save a full markdown report to a11y-report.md', false)
|
|
28
29
|
.option('--no-ai', 'Skip AI fix generation (faster, violations only)')
|
|
29
30
|
.option('--no-explain', 'Hide AI fix explanations in terminal output')
|
|
31
|
+
.option('--no-terminal', 'Suppress terminal output (violations summary)')
|
|
32
|
+
.option('--fast-mode', 'Output only AI fix prompts — no summaries or explanations', false)
|
|
30
33
|
.option('--group <strategy>', 'Group violations by rule or show individually (rule|none)', 'rule')
|
|
31
34
|
.option('--ci', 'Exit with code 1 if any violations are found (for CI/CD pipelines)', false)
|
|
32
35
|
.option('--provider <name>', 'Override the AI provider from config (gemini|openai|ollama)')
|
|
@@ -34,7 +37,9 @@ program
|
|
|
34
37
|
try {
|
|
35
38
|
console.log(`\nScanning ${opts.url}...`);
|
|
36
39
|
const result = await crawl({ url: opts.url, pages: opts.pages, crawl: opts.crawl });
|
|
37
|
-
|
|
40
|
+
if (opts.terminal && !opts.fastMode) {
|
|
41
|
+
printTerminalReport(result);
|
|
42
|
+
}
|
|
38
43
|
const strategy = opts.group === 'none' ? 'none' : 'rule';
|
|
39
44
|
if (opts.ai && result.totalViolations > 0) {
|
|
40
45
|
const config = loadConfig();
|
|
@@ -43,15 +48,20 @@ program
|
|
|
43
48
|
}
|
|
44
49
|
const provider = createAIProvider(config);
|
|
45
50
|
const allViolations = result.pages.flatMap((p) => p.violations);
|
|
46
|
-
|
|
51
|
+
const ruleGroups = groupViolations(allViolations, strategy);
|
|
52
|
+
if (!opts.fastMode) {
|
|
53
|
+
console.log(`\nGenerating AI fixes for ${ruleGroups.length} rule groups (${allViolations.length} violations)...`);
|
|
54
|
+
}
|
|
47
55
|
const fixes = await provider.generateFixes(allViolations, strategy);
|
|
48
|
-
|
|
56
|
+
if (opts.terminal) {
|
|
57
|
+
printAIPrompts(fixes, { explain: opts.explain, fastMode: opts.fastMode });
|
|
58
|
+
}
|
|
49
59
|
if (opts.report) {
|
|
50
|
-
generateMarkdownReport(result, fixes);
|
|
60
|
+
generateMarkdownReport(result, fixes, { fastMode: opts.fastMode });
|
|
51
61
|
}
|
|
52
62
|
}
|
|
53
63
|
else if (opts.report) {
|
|
54
|
-
generateMarkdownReport(result, []);
|
|
64
|
+
generateMarkdownReport(result, [], { fastMode: opts.fastMode });
|
|
55
65
|
}
|
|
56
66
|
if (opts.ci && result.totalViolations > 0) {
|
|
57
67
|
process.exit(1);
|
|
@@ -5,7 +5,31 @@ const IMPACT_EMOJI = {
|
|
|
5
5
|
moderate: '🟡',
|
|
6
6
|
minor: '🟢',
|
|
7
7
|
};
|
|
8
|
-
export function generateMarkdownReport(result, fixes, outputPath = 'a11y-report.md') {
|
|
8
|
+
export function generateMarkdownReport(result, fixes, opts = {}, outputPath = 'a11y-report.md') {
|
|
9
|
+
const lines = opts.fastMode
|
|
10
|
+
? buildFastReport(fixes)
|
|
11
|
+
: buildFullReport(result, fixes);
|
|
12
|
+
writeFileSync(outputPath, lines.join('\n'), 'utf-8');
|
|
13
|
+
console.log(`\nReport saved → ${outputPath}`);
|
|
14
|
+
}
|
|
15
|
+
function buildFastReport(fixes) {
|
|
16
|
+
const lines = [
|
|
17
|
+
'# WCAG A11y — Fix Prompts',
|
|
18
|
+
`> Generated: ${new Date().toLocaleString()}`,
|
|
19
|
+
'',
|
|
20
|
+
];
|
|
21
|
+
for (const fix of fixes) {
|
|
22
|
+
const countLabel = fix.instanceCount > 1 ? ` ×${fix.instanceCount}` : '';
|
|
23
|
+
lines.push(`## \`${fix.ruleId}\`${countLabel}`, '');
|
|
24
|
+
lines.push('**Selectors:**');
|
|
25
|
+
for (const sel of fix.selectors) {
|
|
26
|
+
lines.push(`- \`${sel}\``);
|
|
27
|
+
}
|
|
28
|
+
lines.push('', '```', fix.optimalPrompt, '```', '', '---', '');
|
|
29
|
+
}
|
|
30
|
+
return lines;
|
|
31
|
+
}
|
|
32
|
+
function buildFullReport(result, fixes) {
|
|
9
33
|
const lines = [
|
|
10
34
|
'# WCAG A11y Report',
|
|
11
35
|
`> Generated: ${new Date().toLocaleString()}`,
|
|
@@ -37,6 +61,5 @@ export function generateMarkdownReport(result, fixes, outputPath = 'a11y-report.
|
|
|
37
61
|
lines.push('---', '');
|
|
38
62
|
}
|
|
39
63
|
}
|
|
40
|
-
|
|
41
|
-
console.log(`\nReport saved → ${outputPath}`);
|
|
64
|
+
return lines;
|
|
42
65
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { groupViolations } from '../ai/group.js';
|
|
2
3
|
const IMPACT_COLOR = {
|
|
3
4
|
critical: chalk.red,
|
|
4
5
|
serious: chalk.yellow,
|
|
@@ -22,12 +23,17 @@ export function printTerminalReport(result) {
|
|
|
22
23
|
m > 0 ? chalk.blue(`${m} moderate`) : '',
|
|
23
24
|
].filter(Boolean).join(' ');
|
|
24
25
|
console.log(`\n ${chalk.red('✖')} ${url} ${counts}`);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
const groups = groupViolations(page.violations, 'rule');
|
|
27
|
+
for (const g of groups) {
|
|
28
|
+
const color = IMPACT_COLOR[g.impact] ?? chalk.white;
|
|
29
|
+
const countSuffix = g.count > 1 ? chalk.gray(` ×${g.count}`) : '';
|
|
30
|
+
const tag = color(`[${g.impact.toUpperCase()}]`) + countSuffix;
|
|
31
|
+
const wcag = chalk.gray(`WCAG ${g.wcag}`);
|
|
32
|
+
console.log(` ${tag} ${g.description} ${wcag}`);
|
|
33
|
+
console.log(` ${chalk.gray('→')} ${chalk.dim(g.selectors[0])}`);
|
|
34
|
+
if (g.count > 1) {
|
|
35
|
+
console.log(` ${chalk.gray(` …and ${g.count - 1} more`)}`);
|
|
36
|
+
}
|
|
31
37
|
}
|
|
32
38
|
}
|
|
33
39
|
console.log('\n' + chalk.gray('─'.repeat(60)));
|
|
@@ -37,6 +43,15 @@ export function printTerminalReport(result) {
|
|
|
37
43
|
export function printAIPrompts(fixes, opts) {
|
|
38
44
|
if (fixes.length === 0)
|
|
39
45
|
return;
|
|
46
|
+
if (opts.fastMode) {
|
|
47
|
+
for (let i = 0; i < fixes.length; i++) {
|
|
48
|
+
console.log(fixes[i].optimalPrompt);
|
|
49
|
+
if (i < fixes.length - 1)
|
|
50
|
+
console.log('\n' + chalk.gray('─'.repeat(60)));
|
|
51
|
+
}
|
|
52
|
+
console.log('');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
40
55
|
console.log('\n' + chalk.bold.magenta('AI Fix Prompts') + chalk.gray(' — paste any of these into Cursor, Copilot, or Claude'));
|
|
41
56
|
console.log(chalk.gray('─'.repeat(60)));
|
|
42
57
|
for (const fix of fixes) {
|