wcag-a11y 0.3.2 → 0.3.4
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/dist/ai/gemini.js +10 -9
- package/dist/ai/ollama.js +11 -10
- package/dist/ai/openai.js +10 -9
- package/dist/ai/prompt.js +26 -5
- package/dist/cli.js +2 -2
- package/dist/crawler.js +35 -0
- package/dist/demo.js +1 -1
- package/dist/reporter/markdown.js +17 -1
- package/package.json +1 -1
package/dist/ai/gemini.js
CHANGED
|
@@ -8,11 +8,11 @@ export class GeminiProvider {
|
|
|
8
8
|
this.apiKey = apiKey;
|
|
9
9
|
this.model = model;
|
|
10
10
|
}
|
|
11
|
-
async generateFixes(violations, strategy = 'rule') {
|
|
11
|
+
async generateFixes(violations, strategy = 'rule', framework) {
|
|
12
12
|
if (violations.length === 0)
|
|
13
13
|
return [];
|
|
14
14
|
const groups = groupViolations(violations, strategy);
|
|
15
|
-
const prompt = buildPrompt(groups);
|
|
15
|
+
const prompt = buildPrompt(groups, framework);
|
|
16
16
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${this.model}:generateContent?key=${this.apiKey}`;
|
|
17
17
|
const response = await fetch(url, {
|
|
18
18
|
method: 'POST',
|
|
@@ -27,9 +27,9 @@ export class GeminiProvider {
|
|
|
27
27
|
}
|
|
28
28
|
const data = await response.json();
|
|
29
29
|
const text = data.candidates?.[0]?.content?.parts?.[0]?.text ?? '[]';
|
|
30
|
-
return this.parse(text, groups);
|
|
30
|
+
return this.parse(text, groups, framework);
|
|
31
31
|
}
|
|
32
|
-
parse(text, groups) {
|
|
32
|
+
parse(text, groups, framework) {
|
|
33
33
|
try {
|
|
34
34
|
const jsonMatch = text.match(/\[[\s\S]*\]/);
|
|
35
35
|
const fixes = jsonMatch ? JSON.parse(jsonMatch[0]) : [];
|
|
@@ -37,20 +37,21 @@ export class GeminiProvider {
|
|
|
37
37
|
const found = fixes.find((f) => f.ruleId === g.ruleId);
|
|
38
38
|
return found
|
|
39
39
|
? { ...found, selectors: g.selectors, instanceCount: g.count }
|
|
40
|
-
: this.fallbackFix(g);
|
|
40
|
+
: this.fallbackFix(g, framework);
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
43
|
catch {
|
|
44
|
-
return groups.map((g) => this.fallbackFix(g));
|
|
44
|
+
return groups.map((g) => this.fallbackFix(g, framework));
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
fallbackFix(g) {
|
|
47
|
+
fallbackFix(g, framework) {
|
|
48
48
|
const v = g.representative;
|
|
49
49
|
const selectorList = g.selectors.map((s) => `- ${s}`).join('\n');
|
|
50
50
|
const explanation = fallbackExplanation(g.ruleId, g.description, g.wcag, g.level);
|
|
51
|
+
const fwNote = framework ? `This project uses ${framework}. ` : '';
|
|
51
52
|
const prompt = g.count > 1
|
|
52
|
-
?
|
|
53
|
-
:
|
|
53
|
+
? `${fwNote}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}.`
|
|
54
|
+
: `${fwNote}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}.`;
|
|
54
55
|
return {
|
|
55
56
|
ruleId: g.ruleId,
|
|
56
57
|
selectors: g.selectors,
|
package/dist/ai/ollama.js
CHANGED
|
@@ -8,11 +8,11 @@ export class OllamaProvider {
|
|
|
8
8
|
this.baseUrl = baseUrl;
|
|
9
9
|
this.model = model;
|
|
10
10
|
}
|
|
11
|
-
async generateFixes(violations, strategy = 'rule') {
|
|
11
|
+
async generateFixes(violations, strategy = 'rule', framework) {
|
|
12
12
|
if (violations.length === 0)
|
|
13
13
|
return [];
|
|
14
14
|
const groups = groupViolations(violations, strategy);
|
|
15
|
-
const prompt = buildPrompt(groups);
|
|
15
|
+
const prompt = buildPrompt(groups, framework);
|
|
16
16
|
const response = await fetch(`${this.baseUrl}/api/generate`, {
|
|
17
17
|
method: 'POST',
|
|
18
18
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -26,29 +26,30 @@ export class OllamaProvider {
|
|
|
26
26
|
try {
|
|
27
27
|
const jsonMatch = text.match(/\[[\s\S]*\]/);
|
|
28
28
|
if (!jsonMatch)
|
|
29
|
-
return this.fallback(groups);
|
|
29
|
+
return this.fallback(groups, framework);
|
|
30
30
|
const fixes = JSON.parse(jsonMatch[0]);
|
|
31
31
|
return groups.map((g) => {
|
|
32
32
|
const found = fixes.find((f) => f.ruleId === g.ruleId);
|
|
33
33
|
return found
|
|
34
34
|
? { ...found, selectors: g.selectors, instanceCount: g.count }
|
|
35
|
-
: this.fallbackFix(g);
|
|
35
|
+
: this.fallbackFix(g, framework);
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
38
|
catch {
|
|
39
|
-
return this.fallback(groups);
|
|
39
|
+
return this.fallback(groups, framework);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
fallback(groups) {
|
|
43
|
-
return groups.map((g) => this.fallbackFix(g));
|
|
42
|
+
fallback(groups, framework) {
|
|
43
|
+
return groups.map((g) => this.fallbackFix(g, framework));
|
|
44
44
|
}
|
|
45
|
-
fallbackFix(g) {
|
|
45
|
+
fallbackFix(g, framework) {
|
|
46
46
|
const v = g.representative;
|
|
47
47
|
const selectorList = g.selectors.map((s) => `- ${s}`).join('\n');
|
|
48
48
|
const explanation = fallbackExplanation(g.ruleId, g.description, g.wcag, g.level);
|
|
49
|
+
const fwNote = framework ? `This project uses ${framework}. ` : '';
|
|
49
50
|
const prompt = g.count > 1
|
|
50
|
-
?
|
|
51
|
-
:
|
|
51
|
+
? `${fwNote}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
|
+
: `${fwNote}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}.`;
|
|
52
53
|
return {
|
|
53
54
|
ruleId: g.ruleId,
|
|
54
55
|
selectors: g.selectors,
|
package/dist/ai/openai.js
CHANGED
|
@@ -8,11 +8,11 @@ export class OpenAIProvider {
|
|
|
8
8
|
this.apiKey = apiKey;
|
|
9
9
|
this.model = model;
|
|
10
10
|
}
|
|
11
|
-
async generateFixes(violations, strategy = 'rule') {
|
|
11
|
+
async generateFixes(violations, strategy = 'rule', framework) {
|
|
12
12
|
if (violations.length === 0)
|
|
13
13
|
return [];
|
|
14
14
|
const groups = groupViolations(violations, strategy);
|
|
15
|
-
const prompt = buildPrompt(groups);
|
|
15
|
+
const prompt = buildPrompt(groups, framework);
|
|
16
16
|
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
17
17
|
method: 'POST',
|
|
18
18
|
headers: {
|
|
@@ -31,9 +31,9 @@ export class OpenAIProvider {
|
|
|
31
31
|
}
|
|
32
32
|
const data = await response.json();
|
|
33
33
|
const text = data.choices?.[0]?.message?.content ?? '[]';
|
|
34
|
-
return this.parse(text, groups);
|
|
34
|
+
return this.parse(text, groups, framework);
|
|
35
35
|
}
|
|
36
|
-
parse(text, groups) {
|
|
36
|
+
parse(text, groups, framework) {
|
|
37
37
|
try {
|
|
38
38
|
const jsonMatch = text.match(/\[[\s\S]*\]/);
|
|
39
39
|
const fixes = jsonMatch ? JSON.parse(jsonMatch[0]) : [];
|
|
@@ -41,20 +41,21 @@ export class OpenAIProvider {
|
|
|
41
41
|
const found = fixes.find((f) => f.ruleId === g.ruleId);
|
|
42
42
|
return found
|
|
43
43
|
? { ...found, selectors: g.selectors, instanceCount: g.count }
|
|
44
|
-
: this.fallbackFix(g);
|
|
44
|
+
: this.fallbackFix(g, framework);
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
catch {
|
|
48
|
-
return groups.map((g) => this.fallbackFix(g));
|
|
48
|
+
return groups.map((g) => this.fallbackFix(g, framework));
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
fallbackFix(g) {
|
|
51
|
+
fallbackFix(g, framework) {
|
|
52
52
|
const v = g.representative;
|
|
53
53
|
const selectorList = g.selectors.map((s) => `- ${s}`).join('\n');
|
|
54
54
|
const explanation = fallbackExplanation(g.ruleId, g.description, g.wcag, g.level);
|
|
55
|
+
const fwNote = framework ? `This project uses ${framework}. ` : '';
|
|
55
56
|
const prompt = g.count > 1
|
|
56
|
-
?
|
|
57
|
-
:
|
|
57
|
+
? `${fwNote}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}.`
|
|
58
|
+
: `${fwNote}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}.`;
|
|
58
59
|
return {
|
|
59
60
|
ruleId: g.ruleId,
|
|
60
61
|
selectors: g.selectors,
|
package/dist/ai/prompt.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
const FRAMEWORK_SYNTAX = {
|
|
2
|
+
'Next.js': 'React/TSX (JSX)',
|
|
3
|
+
'Gatsby': 'React/JSX',
|
|
4
|
+
'Remix': 'React/TSX',
|
|
5
|
+
'Nuxt.js': 'Vue 3 SFC (.vue files)',
|
|
6
|
+
'Vue 3': 'Vue 3 SFC (.vue files)',
|
|
7
|
+
'Vue 2': 'Vue 2 SFC (.vue files)',
|
|
8
|
+
'Angular': 'Angular template',
|
|
9
|
+
'React': 'React JSX/TSX',
|
|
10
|
+
'Svelte': 'Svelte (.svelte files)',
|
|
11
|
+
};
|
|
12
|
+
export function buildPrompt(groups, framework) {
|
|
13
|
+
const syntax = framework ? (FRAMEWORK_SYNTAX[framework] ?? 'JSX/TSX') : null;
|
|
2
14
|
const items = groups
|
|
3
15
|
.map((g, i) => `${i + 1}. Rule: ${g.ruleId} | WCAG ${g.wcag} (Level ${g.level}) | Impact: ${g.impact}
|
|
4
16
|
Page: ${g.page}
|
|
@@ -7,14 +19,23 @@ export function buildPrompt(groups) {
|
|
|
7
19
|
Representative Element: ${g.representative.html}
|
|
8
20
|
Problem: ${g.description}`)
|
|
9
21
|
.join('\n\n');
|
|
10
|
-
|
|
22
|
+
const frameworkLine = framework
|
|
23
|
+
? `\nFramework: ${framework}. All "fixedCode" values must use ${syntax} syntax — not raw HTML.\n`
|
|
24
|
+
: '';
|
|
25
|
+
const fixedCodeInstruction = syntax
|
|
26
|
+
? `"fixedCode": the corrected snippet in ${syntax} syntax (component/template code only — no imports, no surrounding boilerplate)`
|
|
27
|
+
: `"fixedCode": the corrected HTML snippet only (no explanation, just code)`;
|
|
28
|
+
const optimalPromptInstruction = framework
|
|
29
|
+
? `"optimalPrompt": a ready-to-paste prompt for an AI coding assistant (Cursor, Copilot, Claude) working in a ${framework} codebase. Structure it as: (1) state the WCAG 2.1/2.2 criterion being violated, (2) list the affected selectors and HTML snippets, (3) state the exact change needed in ${syntax} syntax. Focus solely on the fix.`
|
|
30
|
+
: `"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, (2) list the affected selectors and HTML snippets, (3) state the exact change needed. Focus solely on the fix.`;
|
|
31
|
+
return `You are a WCAG accessibility expert. Analyze these violations and return a JSON array.${frameworkLine}
|
|
11
32
|
Each item must have:
|
|
12
33
|
- "ruleId": the rule id from the input
|
|
13
34
|
- "selectors": array of CSS selectors affected (copy from input)
|
|
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
|
|
15
|
-
-
|
|
35
|
+
- "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.
|
|
36
|
+
- ${fixedCodeInstruction}
|
|
16
37
|
- "wcagReference": the full criterion name, e.g. "WCAG 2.1 SC 1.1.1 Non-text Content (Level A)"
|
|
17
|
-
-
|
|
38
|
+
- ${optimalPromptInstruction}
|
|
18
39
|
|
|
19
40
|
Return ONLY a valid JSON array. No markdown, no code fences, no explanation outside the JSON.
|
|
20
41
|
|
package/dist/cli.js
CHANGED
|
@@ -11,7 +11,7 @@ const program = new Command();
|
|
|
11
11
|
program
|
|
12
12
|
.name('wcag-a11y')
|
|
13
13
|
.description('WCAG 2.1/2.2 accessibility auditor with AI-powered fixes')
|
|
14
|
-
.version('0.3.
|
|
14
|
+
.version('0.3.4');
|
|
15
15
|
program
|
|
16
16
|
.command('init')
|
|
17
17
|
.description('Create a11y.config.json in the current directory')
|
|
@@ -52,7 +52,7 @@ program
|
|
|
52
52
|
if (!opts.fastMode) {
|
|
53
53
|
console.log(`\nGenerating AI fixes for ${ruleGroups.length} rule groups (${allViolations.length} violations)...`);
|
|
54
54
|
}
|
|
55
|
-
const fixes = await provider.generateFixes(allViolations, strategy);
|
|
55
|
+
const fixes = await provider.generateFixes(allViolations, strategy, result.framework);
|
|
56
56
|
if (opts.terminal) {
|
|
57
57
|
printAIPrompts(fixes, { explain: opts.explain, fastMode: opts.fastMode });
|
|
58
58
|
}
|
package/dist/crawler.js
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
import { chromium } from 'playwright';
|
|
2
2
|
import { scanPage } from './engine/index.js';
|
|
3
|
+
async function detectFramework(page) {
|
|
4
|
+
try {
|
|
5
|
+
return await page.evaluate(() => {
|
|
6
|
+
const w = window;
|
|
7
|
+
if (w['__NEXT_DATA__'])
|
|
8
|
+
return 'Next.js';
|
|
9
|
+
if (w['___gatsby'])
|
|
10
|
+
return 'Gatsby';
|
|
11
|
+
if (w['__remixContext'])
|
|
12
|
+
return 'Remix';
|
|
13
|
+
if (w['__nuxt'] || w['$nuxt'])
|
|
14
|
+
return 'Nuxt.js';
|
|
15
|
+
if (w['__vue_app__'])
|
|
16
|
+
return 'Vue 3';
|
|
17
|
+
if (w['Vue'])
|
|
18
|
+
return 'Vue 2';
|
|
19
|
+
if (document.querySelector('[ng-version]') !== null)
|
|
20
|
+
return 'Angular';
|
|
21
|
+
const root = document.getElementById('root') ?? document.getElementById('app') ?? document.body;
|
|
22
|
+
if (root && Object.keys(root).some((k) => k.startsWith('__reactFiber') || k.startsWith('__reactContainer'))) {
|
|
23
|
+
return 'React';
|
|
24
|
+
}
|
|
25
|
+
if (document.querySelector('[data-svelte-h]') !== null)
|
|
26
|
+
return 'Svelte';
|
|
27
|
+
return undefined;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
3
34
|
export async function crawl(options) {
|
|
4
35
|
const { url, pages = ['/'], crawl: autoCrawl = false } = options;
|
|
5
36
|
const baseUrl = url.replace(/\/$/, '');
|
|
@@ -23,10 +54,13 @@ export async function crawl(options) {
|
|
|
23
54
|
}
|
|
24
55
|
}
|
|
25
56
|
const results = [];
|
|
57
|
+
let framework;
|
|
26
58
|
for (const pageUrl of pagesToVisit) {
|
|
27
59
|
const page = await context.newPage();
|
|
28
60
|
try {
|
|
29
61
|
await page.goto(pageUrl, { waitUntil: 'networkidle', timeout: 30000 });
|
|
62
|
+
if (!framework)
|
|
63
|
+
framework = await detectFramework(page);
|
|
30
64
|
const result = await scanPage(page, pageUrl);
|
|
31
65
|
results.push(result);
|
|
32
66
|
}
|
|
@@ -46,6 +80,7 @@ export async function crawl(options) {
|
|
|
46
80
|
seriousCount: allViolations.filter((v) => v.impact === 'serious').length,
|
|
47
81
|
moderateCount: allViolations.filter((v) => v.impact === 'moderate').length,
|
|
48
82
|
minorCount: allViolations.filter((v) => v.impact === 'minor').length,
|
|
83
|
+
framework,
|
|
49
84
|
};
|
|
50
85
|
}
|
|
51
86
|
finally {
|
package/dist/demo.js
CHANGED
|
@@ -74,7 +74,7 @@ export async function runDemo(opts) {
|
|
|
74
74
|
const provider = createAIProvider(config);
|
|
75
75
|
const violations = result.pages.flatMap((p) => p.violations);
|
|
76
76
|
console.log(`\nGenerating AI fixes for ${violations.length} violations...`);
|
|
77
|
-
const fixes = await provider.generateFixes(violations, 'rule');
|
|
77
|
+
const fixes = await provider.generateFixes(violations, 'rule', result.framework);
|
|
78
78
|
printAIPrompts(fixes, { explain: true });
|
|
79
79
|
if (opts.report)
|
|
80
80
|
generateMarkdownReport(result, fixes);
|
|
@@ -52,9 +52,25 @@ function buildFullReport(result, fixes) {
|
|
|
52
52
|
lines.push('✅ No violations found.', '', '---', '');
|
|
53
53
|
continue;
|
|
54
54
|
}
|
|
55
|
+
// Group violations by ruleId so grouped fixes aren't repeated per instance
|
|
56
|
+
const byRule = new Map();
|
|
55
57
|
for (const v of page.violations) {
|
|
58
|
+
const group = byRule.get(v.ruleId) ?? [];
|
|
59
|
+
group.push(v);
|
|
60
|
+
byRule.set(v.ruleId, group);
|
|
61
|
+
}
|
|
62
|
+
for (const group of byRule.values()) {
|
|
63
|
+
const v = group[0];
|
|
56
64
|
const fix = fixes.find((f) => f.ruleId === v.ruleId);
|
|
57
|
-
|
|
65
|
+
const others = group.slice(1);
|
|
66
|
+
lines.push(`### ${IMPACT_EMOJI[v.impact] ?? '⚪'} [${v.impact.toUpperCase()}] ${v.description}`, '', `**Rule:** \`${v.ruleId}\` `, `**WCAG:** ${fix?.wcagReference ?? `SC ${v.wcag} (Level ${v.level})`} `, `**Instances:** ${group.length}`, '', '**Representative element:**', `\`${v.selector}\``, '```html', v.html, '```', '');
|
|
67
|
+
if (others.length > 0) {
|
|
68
|
+
lines.push(`**Also affects ${others.length} more element${others.length > 1 ? 's' : ''} on this page:**`);
|
|
69
|
+
for (const o of others) {
|
|
70
|
+
lines.push(`- \`${o.selector}\``);
|
|
71
|
+
}
|
|
72
|
+
lines.push('');
|
|
73
|
+
}
|
|
58
74
|
if (fix) {
|
|
59
75
|
lines.push('**Why it matters:**', fix.explanation, '', '**Fixed code:**', '```html', fix.fixedCode, '```', '', '**📋 Prompt for your AI assistant (Cursor / Copilot / Claude):**', '```', fix.optimalPrompt, '```', '');
|
|
60
76
|
}
|