name: docs-accessibility description: 文档可访问性验证与修复。检查WCAG 2.1合规性,验证替代文本,分析标题层级,验证颜色对比度,并生成可访问性报告。 allowed-tools: Read, Write, Edit, Bash, Glob, Grep backlog-id: SK-019 metadata: author: babysitter-sdk version: “1.0.0”
文档可访问性技能
文档可访问性验证与修复。
功能
- WCAG 2.1合规性检查
- 图像替代文本验证
- 标题层级分析
- 颜色对比度验证
- 屏幕阅读器兼容性测试
- 键盘导航验证
- ARIA地标检查
- 可访问性报告生成
使用场景
在以下情况下调用此技能:
- 审核文档的可访问性
- 验证图像替代文本
- 检查标题结构
- 验证颜色对比度比率
- 生成可访问性报告
输入参数
| 参数 | 类型 | 必填 | 描述 |
|---|---|---|---|
| inputPath | string | 是 | 文档或构建站点的路径 |
| action | string | 是 | audit, validate-images, check-headings |
| standard | string | 否 | WCAG级别 (A, AA, AAA) |
| outputFormat | string | 否 | json, html, sarif |
| fix | boolean | 否 | 自动修复可能的问题 |
输入示例
{
"inputPath": "./docs/_build/html",
"action": "audit",
"standard": "AA",
"outputFormat": "json"
}
输出结构
可访问性报告
{
"summary": {
"total": 156,
"passed": 142,
"failed": 14,
"level": "AA",
"score": 91
},
"byCategory": {
"images": { "passed": 45, "failed": 3 },
"headings": { "passed": 28, "failed": 2 },
"contrast": { "passed": 52, "failed": 5 },
"navigation": { "passed": 17, "failed": 4 }
},
"issues": [
{
"id": "img-alt-missing",
"wcag": "1.1.1",
"level": "A",
"impact": "critical",
"description": "图像缺少替代文本",
"location": {
"file": "docs/guide/setup.md",
"line": 42,
"element": "<img src=\"diagram.png\">"
},
"suggestion": "添加描述性替代文本:alt=\"系统架构图显示...\""
},
{
"id": "heading-skip",
"wcag": "1.3.1",
"level": "A",
"impact": "moderate",
"description": "标题级别应仅增加一级",
"location": {
"file": "docs/api/users.md",
"line": 15,
"element": "<h4>用户属性</h4>"
},
"context": "H2 -> H4 (跳过H3)",
"suggestion": "更改为<h3>或在上面添加缺失的<h3>"
},
{
"id": "color-contrast",
"wcag": "1.4.3",
"level": "AA",
"impact": "serious",
"description": "文本不符合对比度要求",
"location": {
"file": "docs/_static/custom.css",
"line": 28,
"element": ".note { color: #999; }"
},
"details": {
"foreground": "#999999",
"background": "#ffffff",
"ratio": "2.85:1",
"required": "4.5:1"
},
"suggestion": "更改为#767676或更深的颜色以达到4.5:1对比度"
}
],
"wcagCompliance": {
"A": { "passed": 48, "failed": 6 },
"AA": { "passed": 35, "failed": 8 },
"AAA": { "passed": 12, "failed": 0 }
}
}
检查的WCAG指南
可感知性(原则1)
1.1.1 - 非文本内容:
- 图像有替代文本
- 装饰性图像有空替代文本
- 复杂图像有长描述
- 图标有可访问名称
1.3.1 - 信息和关系:
- 标题结构正确
- 列表正确标记
- 表格有表头
- 表单标签关联
1.4.1 - 颜色的使用:
- 颜色不是唯一指示器
- 链接可区分
1.4.3 - 对比度(最低):
- 文本:4.5:1比率
- 大文本:3:1比率
- UI组件:3:1比率
可操作性(原则2)
2.1.1 - 键盘:
- 所有功能可通过键盘访问
- 无键盘陷阱
- 存在跳过链接
2.4.1 - 绕过块:
- 跳过导航链接
- 地标区域
2.4.2 - 页面标题:
- 描述性页面标题
2.4.6 - 标题和标签:
- 描述性标题
- 清晰标签
2.4.7 - 焦点可见:
- 可见焦点指示器
可理解性(原则3)
3.1.1 - 页面语言:
- 存在lang属性
3.2.3 - 一致性导航:
- 跨页面导航一致
3.3.2 - 标签或说明:
- 表单输入有标签
图像替代文本验证
替代文本规则
const altTextRules = {
// 必须有alt属性
required: {
test: (img) => img.hasAttribute('alt'),
message: '图像必须有alt属性'
},
// 替代文本应具有描述性
descriptive: {
test: (img) => {
const alt = img.getAttribute('alt');
const badPatterns = [
/^image$/i,
/^photo$/i,
/^picture$/i,
/^graphic$/i,
/\.(?:png|jpg|gif|svg)$/i,
/^untitled/i
];
return !badPatterns.some(p => p.test(alt));
},
message: '替代文本应描述图像内容'
},
// 不要太长
length: {
test: (img) => {
const alt = img.getAttribute('alt');
return alt.length <= 125;
},
message: '替代文本应简洁(少于125个字符)'
},
// 装饰性图像应有空替代文本
decorative: {
test: (img) => {
if (img.hasAttribute('role') && img.getAttribute('role') === 'presentation') {
return img.getAttribute('alt') === '';
}
return true;
},
message: '装饰性图像应有空alt=""'
}
};
替代文本建议
function suggestAltText(imagePath, context) {
const suggestions = [];
// 基于文件名
const filename = path.basename(imagePath, path.extname(imagePath));
if (filename.includes('diagram')) {
suggestions.push(`显示${extractContext(context)}的图表`);
}
if (filename.includes('screenshot')) {
suggestions.push(`${extractContext(context)}的截图`);
}
if (filename.includes('logo')) {
suggestions.push(`${extractBrand(filename)}标志`);
}
// 基于周围文本
const heading = findNearestHeading(context);
if (heading) {
suggestions.push(`${heading}的插图`);
}
return suggestions;
}
标题结构分析
标题层级检查
function analyzeHeadings(content) {
const headings = extractHeadings(content);
const issues = [];
let lastLevel = 0;
headings.forEach((heading, index) => {
const level = heading.level;
// 检查跳过的级别
if (level > lastLevel + 1 && lastLevel !== 0) {
issues.push({
type: 'heading-skip',
heading: heading.text,
line: heading.line,
expected: lastLevel + 1,
actual: level
});
}
// 检查多个H1
if (level === 1 && index > 0) {
issues.push({
type: 'multiple-h1',
heading: heading.text,
line: heading.line
});
}
lastLevel = level;
});
return {
structure: buildHeadingTree(headings),
issues
};
}
颜色对比度检查
对比度比率计算
function getContrastRatio(foreground, background) {
const fgLuminance = getRelativeLuminance(foreground);
const bgLuminance = getRelativeLuminance(background);
const lighter = Math.max(fgLuminance, bgLuminance);
const darker = Math.min(fgLuminance, bgLuminance);
return (lighter + 0.05) / (darker + 0.05);
}
function meetsContrastRequirement(ratio, isLargeText, level = 'AA') {
const requirements = {
'AA': { normal: 4.5, large: 3 },
'AAA': { normal: 7, large: 4.5 }
};
const threshold = isLargeText
? requirements[level].large
: requirements[level].normal;
return ratio >= threshold;
}
CSS分析
async function analyzeStylesheet(cssPath) {
const css = await fs.readFile(cssPath, 'utf8');
const ast = postcss.parse(css);
const issues = [];
ast.walkDecls('color', (decl) => {
const rule = decl.parent;
const bgColor = findBackgroundColor(rule) || '#ffffff';
const fgColor = decl.value;
const ratio = getContrastRatio(fgColor, bgColor);
if (!meetsContrastRequirement(ratio, false, 'AA')) {
issues.push({
selector: rule.selector,
foreground: fgColor,
background: bgColor,
ratio: ratio.toFixed(2),
line: decl.source.start.line,
suggestion: suggestAccessibleColor(fgColor, bgColor)
});
}
});
return issues;
}
键盘导航
焦点测试
async function testKeyboardNavigation(page) {
const issues = [];
// 获取所有可聚焦元素
const focusable = await page.$$('a, button, input, select, textarea, [tabindex]');
for (const element of focusable) {
await element.focus();
// 检查焦点可见性
const hasFocusStyle = await page.evaluate((el) => {
const styles = window.getComputedStyle(el);
const focusStyles = window.getComputedStyle(el, ':focus');
return (
styles.outline !== 'none' ||
styles.boxShadow !== 'none' ||
focusStyles.outline !== 'none'
);
}, element);
if (!hasFocusStyle) {
issues.push({
type: 'focus-not-visible',
element: await element.evaluate(el => el.outerHTML.substring(0, 100))
});
}
}
return issues;
}
工作流程
- 解析内容 - 加载文档文件或构建的HTML
- 提取元素 - 查找图像、标题、链接等
- 检查图像 - 验证替代文本
- 分析标题 - 检查层级结构
- 测试对比度 - 验证颜色比率
- 检查导航 - 验证键盘访问
- 生成报告 - 输出发现
依赖项
{
"devDependencies": {
"axe-core": "^4.8.0",
"pa11y": "^6.2.0",
"lighthouse": "^11.0.0",
"puppeteer": "^21.0.0",
"color-contrast-checker": "^2.1.0"
}
}
CLI命令
# 运行axe-core审核
npx axe ./docs/_build/html --rules wcag2aa
# 运行pa11y
npx pa11y https://docs.example.com --standard WCAG2AA
# Lighthouse可访问性审核
npx lighthouse https://docs.example.com --only-categories=accessibility
# 检查单个页面
npx axe https://docs.example.com/guide --save report.json
应用的最佳实践
- 始终为信息性图像提供替代文本
- 装饰性图像使用空替代文本
- 保持逻辑标题层级
- 确保普通文本4.5:1对比度
- 提供可见焦点指示器
- 包含跳过导航链接
- 使用语义HTML元素
参考资料
- WCAG 2.1: https://www.w3.org/WAI/WCAG21/quickref/
- axe-core: https://github.com/dequelabs/axe-core
- pa11y: https://pa11y.org/
- WebAIM对比度检查器: https://webaim.org/resources/contrastchecker/
目标流程
- docs-testing.js
- docs-audit.js
- style-guide-enforcement.js