wcag-a11y 0.2.0 → 0.3.0
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/gemini.js +7 -3
- package/dist/ai/ollama.js +7 -3
- package/dist/ai/openai.js +7 -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 |
|
package/dist/ai/gemini.js
CHANGED
|
@@ -45,15 +45,19 @@ export class GeminiProvider {
|
|
|
45
45
|
}
|
|
46
46
|
fallbackFix(g) {
|
|
47
47
|
const v = g.representative;
|
|
48
|
-
const
|
|
48
|
+
const selectorList = g.selectors.map((s) => `- ${s}`).join('\n');
|
|
49
|
+
const explanation = `Users relying on assistive technologies are affected: ${g.description.toLowerCase()}. This fails WCAG 2.1 SC ${g.wcag} (Level ${g.level}).`;
|
|
50
|
+
const prompt = g.count > 1
|
|
51
|
+
? `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}.`
|
|
52
|
+
: `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
53
|
return {
|
|
50
54
|
ruleId: g.ruleId,
|
|
51
55
|
selectors: g.selectors,
|
|
52
56
|
instanceCount: g.count,
|
|
53
|
-
explanation
|
|
57
|
+
explanation,
|
|
54
58
|
fixedCode: v.html,
|
|
55
59
|
wcagReference: `WCAG 2.1 SC ${g.wcag}`,
|
|
56
|
-
optimalPrompt:
|
|
60
|
+
optimalPrompt: prompt,
|
|
57
61
|
};
|
|
58
62
|
}
|
|
59
63
|
}
|
package/dist/ai/ollama.js
CHANGED
|
@@ -43,15 +43,19 @@ export class OllamaProvider {
|
|
|
43
43
|
}
|
|
44
44
|
fallbackFix(g) {
|
|
45
45
|
const v = g.representative;
|
|
46
|
-
const
|
|
46
|
+
const selectorList = g.selectors.map((s) => `- ${s}`).join('\n');
|
|
47
|
+
const explanation = `Users relying on assistive technologies are affected: ${g.description.toLowerCase()}. This fails WCAG 2.1 SC ${g.wcag} (Level ${g.level}).`;
|
|
48
|
+
const prompt = g.count > 1
|
|
49
|
+
? `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}.`
|
|
50
|
+
: `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
51
|
return {
|
|
48
52
|
ruleId: g.ruleId,
|
|
49
53
|
selectors: g.selectors,
|
|
50
54
|
instanceCount: g.count,
|
|
51
|
-
explanation
|
|
55
|
+
explanation,
|
|
52
56
|
fixedCode: v.html,
|
|
53
57
|
wcagReference: `WCAG 2.1 SC ${g.wcag}`,
|
|
54
|
-
optimalPrompt:
|
|
58
|
+
optimalPrompt: prompt,
|
|
55
59
|
};
|
|
56
60
|
}
|
|
57
61
|
}
|
package/dist/ai/openai.js
CHANGED
|
@@ -49,15 +49,19 @@ export class OpenAIProvider {
|
|
|
49
49
|
}
|
|
50
50
|
fallbackFix(g) {
|
|
51
51
|
const v = g.representative;
|
|
52
|
-
const
|
|
52
|
+
const selectorList = g.selectors.map((s) => `- ${s}`).join('\n');
|
|
53
|
+
const explanation = `Users relying on assistive technologies are affected: ${g.description.toLowerCase()}. This fails WCAG 2.1 SC ${g.wcag} (Level ${g.level}).`;
|
|
54
|
+
const prompt = g.count > 1
|
|
55
|
+
? `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}.`
|
|
56
|
+
: `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
57
|
return {
|
|
54
58
|
ruleId: g.ruleId,
|
|
55
59
|
selectors: g.selectors,
|
|
56
60
|
instanceCount: g.count,
|
|
57
|
-
explanation
|
|
61
|
+
explanation,
|
|
58
62
|
fixedCode: v.html,
|
|
59
63
|
wcagReference: `WCAG 2.1 SC ${g.wcag}`,
|
|
60
|
-
optimalPrompt:
|
|
64
|
+
optimalPrompt: prompt,
|
|
61
65
|
};
|
|
62
66
|
}
|
|
63
67
|
}
|
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) {
|