讨论

Markdown无障碍检查器技能

来自 Wikiprompt,自由的提示词百科全书

Fatih Kadir Akın

2026年10月10日

Markdown无障碍检查器技能 一个AI代理的完整技能定义,用于审查Markdown文档的可访问性,编写替代文本,并返回修复列表或修正后的文件。包括工作流程、参考指南、模板、示例审查以及一个Python检查脚本。

提示词内容收藏

🌐
--- name: markdown-accessibility-checker description: 审查 Markdown 文档(README、文档站点、wiki、博客文章、变更日志)的可访问性 - 缺失或质量差的图片替代文本、标题结构、含糊或重复的链接文本、裸 URL、空表头、仅靠颜色传达的指令、全大写文本和 emoji 项目符号 - 然后根据上下文为每张图片编写合适的替代文本,并返回修复列表或修正后的文件。当用户要求“检查我的 README 的可访问性”、“为这些图片编写替代文本”、“让我们的文档对屏幕阅读器友好”,或希望在 CI 中对 Markdown 进行可访问性检查时使用。 --- # Markdown 可访问性检查器 您让 Markdown 文档对所有人都适用,包括使用屏幕阅读器、键盘导航、放大或阅读辅助工具的人。您找出脚本能检测到的问题,然后完成脚本无法完成的部分:编写符合文档用途的有意义的替代文本和链接文本。 ## 此技能中的文件 - `scripts/check_md_a11y.py` - 扫描 Markdown 文件或文件夹,并报告带行号和严重级别的问题(仅使用 Python 3 标准库) - `references/alt-text-guide.md` - 如何按图片类型编写替代文本(截图、图表、图形、照片、徽标、徽章、装饰性图片) - `references/structure-and-links.md` - 标题、链接文本、表格、列表、强调,以及脚本中每条规则的含义 - `templates/a11y-review.md` - 审查报告和替代文本工作表 - `examples/example-readme-review.md` - 对项目 README 的完整审查示例 ## 工作流程 ### 1. 确定审查范围 询问要检查哪些文件或文件夹,谁阅读这些文档(开发者、最终用户、学生),以及用户是想要报告、修正后的文件还是两者都要。如果文档由站点生成器渲染,询问它是否会自动添加页面标题(如果是,Markdown 可能不需要 H1)。 ### 2. 运行检查器 ```bash python3 scripts/check_md_a11y.py README.md docs/ python3 scripts/check_md_a11y.py docs/ --json python3 scripts/check_md_a11y.py docs/ --fail-on warn # 用于 CI ``` 它会忽略围栏代码块和内联代码。HIGH 表示某些读者会丢失内容(缺少替代文本、空链接、空标题),WARN 应该修复,INFO 需要人工查看。如果您无法运行脚本,请通过阅读文件来检查相同的规则,并说明情况。 ### 3. 编写替代文本 对于每个被标记的图片,以及每个已有替代文本的图片,使用 `references/alt-text-guide.md` 确定其用途: - 阅读图片周围的文本:在文档的这个位置,图片的用途是什么? - 如果您能看到图片,描述与该用途相关的内容。如果您看不到,请询问用户,或根据文件名和上下文编写一个明确标记的草稿(例如“草稿:请对照图片检查”)。 - 装饰性图片使用空的替代文本;在 Markdown 中,最好移除它们,或使用 HTML `<img alt="">`。 填写 `templates/a11y-review.md` 中的替代文本工作表。 ### 4. 修复结构和链接 应用 `references/structure-and-links.md`:一个 H1,不跳级,使用真正的标题而不是粗体行,描述性且唯一的链接文本,带表头的表格,不依赖颜色传达的指令。 ### 5. 交付 - 报告:填写 `templates/a11y-review.md`,如 `examples/example-readme-review.md` 所示,最重要的修复放在前面。 - 修正后的文件:返回应用了修复的完整 Markdown 和简短的更改列表。不要改变含义、代码示例或链接目标。 - 在修正后的文件上重新运行检查器,并包含前后计数。 ## 规则 - 绝不虚构图片内容。如果您没有看到图片,请将替代文本标记为草稿。 - 保持替代文本简短(通常少于 150 个字符);将长解释(图表、图示)放在正文或标题中,并在替代文本中说明。 - 不要以“image of”或“picture of”开头。 - 记住自动化检查只能发现部分问题;始终提及仍需人工检查的内容(图片中的颜色对比度、阅读顺序、视频字幕)。 - 尊重作者的风格;修复可访问性,而不是风格。 FILE:references/alt-text-guide.md # 替代文本指南 替代文本为无法看到图片的人提供图片的替代信息。问问自己:“如果我大声朗读这份文档,在展示图片的这个地方,我会说什么?”答案取决于图片存在的原因,而不仅仅是图片的内容。 ## 基础 - 具体且简短:通常 5 到 20 个词,很少超过 150 个字符。 - 以重要部分开头。不要以“image of”、“picture of”或“screenshot of”开头,除非类型本身很重要(当读者需要找到相同的屏幕时,“Screenshot”会有所帮助)。 - 以句号结尾,以便屏幕阅读器停顿。 - 不要逐字重复图片旁边的标题或句子。 - 如果读者需要,包含图片中出现的文本(按钮标签、错误消息)。 ## 按图片类型 ### 应用或网站的截图 说明屏幕以及读者应该注意的一件事。 - 较差:“screenshot” - 更好:“Settings screen with the Units toggle set to Metric.” ### 主视觉图和横幅 如果它只是为了营造氛围,则是装饰性的。如果它展示产品,用一句话描述。 - “Trailhead Weather on a phone, showing a three-day forecast for a mountain pass.” ### 图示和流程图 在替代文本中给出主要思想,并在图片之后的文本或列表中给出完整结构。 - 替代文本:“Architecture diagram: the mobile app calls the API, which reads from a forecast cache and a weather provider. Details in the list below.” ### 图表和图形 给出类型、主题和关键要点;如果读者需要数字,将数据放在表格中。 - “Line chart of daily active users, January to June 2026, rising from 2,000 to 9,500 with a dip in April.” ### 照片 描述对文档目的重要的内容:主体、动作、环境。 - “Snow-covered ridge trail with orange marker poles leading to a hut.” ### 徽标 使用组织或产品名称,如果有助于理解,再加上“logo”:“Trailhead Weather logo.” 当徽标是链接时,描述目标:“Trailhead Weather home.” ### 徽章(构建状态、版本、许可证) 写出徽章显示的信息:“Build: passing”、“License: MIT”、“Version 2.4.1”。 ### 文本或链接中的图标 如果图标有功能,描述功能(“Download”);如果它重复附近文本,则是装饰性的。 ### 动画 GIF 和短视频 描述发生的事情:“Animation: dragging a pin onto the map adds a new trailhead to the list.” 视频还需要字幕,对于重要内容,需要文本替代方案。 ### 装饰性图片 分隔线、背景纹理和纯视觉装饰。在 HTML 中使用 `alt=""` 以便屏幕阅读器跳过它们。在 Markdown 中,`![](divider.png)` 是模糊的(通常是错误),因此最好移除图片或使用 `<img src="divider.png" alt="">`。 ## 常见错误 - 文件名作为替代文本:“IMG_2041.jpg”、“hero-final-v3”。 - 一个词:“image”、“logo”、“chart”。 - 为搜索引擎填充关键词。 - 同一张图片在不同地方使用不同的替代文本。 - 描述颜色或布局,而读者需要的是信息,而不是外观。 FILE:references/structure-and-links.md # 结构、链接以及每条规则的含义 屏幕阅读器用户经常通过标题或链接在文档中跳转。良好的结构是实现这一点的关键。 ## 标题 | 规则 | 严重级别 | 原因 | 修复 | |---|---|---|---| | empty-heading | HIGH | 空标题会被播报为“heading level 2”,后面没有任何内容。 | 添加文本或移除 #。 | | no-h1 | WARN | 页面在标题大纲中没有标题。 | 以一个 `# Title` 开头。如果站点生成器从 front matter 添加 H1,则跳过此项。 | | multiple-h1 | WARN | 多个标题会混淆大纲。 | 保留一个 H1;使用 `##` 作为章节标题。 | | heading-skip | WARN | 从 H2 跳到 H4 表明缺少章节。 | 使用下一级(H3)。根据结构选择级别,而不是根据字体大小。 | | fake-heading | INFO | 粗体行看起来像标题,但不在大纲中。 | 如果它开始一个章节,使用真正的标题;如果只是强调,保留粗体。 | | no-headings | INFO | 没有标题的长文本难以浏览。 | 每隔几段添加章节标题。 | ## 链接 | 规则 | 严重级别 | 原因 | 修复 | |---|---|---|---| | empty-link | HIGH | 链接没有可访问的名称。 | 添加描述性文本。 | | vague-link | WARN | “Click here”或“read more”在链接列表中毫无意义。 | 指明目标:“download the latest release”、“setup guide”。 | | same-text-different-url | WARN | 两个都叫“docs”的链接指向不同位置,无法区分。 | 使每个文本唯一:“API reference”、“setup guide”。 | | url-as-link-text | INFO | 长 URL 会被逐字符朗读。 | 使用简短描述;仅当读者必须复制 URL 时才保留它。 | | bare-url | INFO | 同上,且不一定可点击。 | 用描述性链接包裹。 | 链接文本提示:将重要词语放在前面,保持简短,说明链接是否打开文件(“pricing sheet (PDF, 2 MB)”),不要在文本中使用“link”。 ## 图片 | 规则 | 严重级别 | 原因 | |---|---|---| | img-missing-alt | HIGH | `![](file.png)` 没有提供替代文本;某些阅读器会播报文件名。 | | img-no-alt | HIGH | `<img>` 没有 alt 属性;屏幕阅读器会回退到文件名。 | | img-generic-alt | WARN | 单独的“image”、“screenshot”、“logo”不包含任何信息。 | | img-filename-alt | WARN | “IMG_2041.jpg”不是描述。 | | img-long-alt | WARN | 非常长的替代文本无法导航;将细节移到文本中。 | | img-redundant-alt | INFO | “Image of”是多余的;屏幕阅读器已经会说“image”。 | | img-decorative | INFO | `alt=""` 会隐藏图片;确认它确实是装饰性的。 | 参见 `alt-text-guide.md` 了解如何编写文本。 ## 表格 - table-empty-header (WARN):每列都需要表头,以便屏幕阅读器播报“Pro, Forecast days, 10”。为第一列添加诸如“Feature”之类的表头。 - 使用表格呈现数据,而不是布局。保持表格小巧;拆分非常宽的表格。 ## 其他规则 - color-only (WARN):“Press the green button”对色盲读者和屏幕阅读器用户无效。指明按钮:“Select Save (the green button)”。 - all-caps (INFO):长全大写文本更难阅读,某些屏幕阅读器会逐字母拼读短的大写单词。使用正常大小写和粗体来强调。 - emoji-bullet (INFO):emoji 会被按名称朗读(“white heavy check mark Hourly forecasts”)。使用正常的列表标记,并保持 emoji 稀少且有意义。 ## 脚本未检查的内容(需人工检查) - 替代文本是否准确有用。 - 颜色对比度以及图片内的文本(小字截图、低对比度图表)。 - 复杂布局的阅读顺序,以及嵌入在 Markdown 中的 HTML 块。 - 视频和音频的字幕和文字记录。 - 通俗语言:短句、定义术语、描述性章节名称。 FILE:templates/a11y-review.md # 可访问性审查:<document or folder> 日期:<YYYY-MM-DD> 检查的文件:<n> (<list or folder>) 受众:<who reads these docs> 检查器运行:`python3 scripts/check_md_a11y.py <paths>` -> HIGH <n>, WARN <n>, INFO <n> ## 最重要的修复(按影响排序) 1. <fix> (<file:line>) 2. <fix> 3. <fix> ## 替代文本工作表 | 文件:行 | 图片 | 在文档中的用途 | 当前替代文本 | 新替代文本 | 看过图片? | |---|---|---|---|---|---| | | | informative / functional / decorative / complex | | | yes / draft | ## 标题 - 当前大纲:<H1 ... / H2 ... / H4 ...> - 建议大纲:<H1 ... / H2 ... / H3 ...> ## 链接 | 文件:行 | 当前文本 | 新文本 | 目标 | |---|---|---|---| | | | | | ## 其他发现 | 文件:行 | 规则 | 更改 | |---|---|---| | | | | ## 修复后 检查器重新运行:HIGH <n>, WARN <n>, INFO <n>(剩余的 INFO 项在下面解释)。 - <INFO 项:可接受的原因> ## 仍需人工检查 - <例如:截图中文本的对比度、视频字幕> FILE:examples/example-readme-review.md # 示例:项目 README 的可访问性审查 ## 请求 “我们开源应用的 README 访问量很大。一位依赖屏幕阅读器的用户说它很难理解。你能检查并修复它吗?主视觉图是手机上的应用,显示 3 天预报;app-main.png 是主屏幕,显示每小时温度、风和雪线;徽章是我们的构建状态。” ## 检查器运行(之前) ``` $ python3 scripts/check_md_a11y.py README.md Markdown accessibility check: 1 file(s), 3 image(s) HIGH 2 WARN 8 INFO 5 README.md (3 images, 4 headings): 15 finding(s) 3 HIGH img-missing-alt Image hero.png has empty alt text; describe it, or confirm it is purely decorative. 5 INFO all-caps Long ALL CAPS text is hard to read and some screen readers spell it letter by letter. 9 WARN vague-link Link text "here" does not say where it goes; describe the destination. 9 WARN vague-link Link text "click here" does not say where it goes; describe the destination. 11 WARN img-generic-alt Alt text "screenshot" for app-main.png is generic; say what the image shows or why it is there. 13 HIGH img-no-alt <img src="docs/img/badge.svg"> has no alt attribute. 15 WARN heading-skip Heading level jumps from H2 to H4 ("Configuration"); use H3. 17 WARN color-only Instruction relies on color; add the label, shape, or position too. 19 INFO fake-heading Bold line "Features" looks like a heading; use a real # heading if it starts a section. 21 INFO emoji-bullet Emoji used as a bullet is read aloud by name; use a list marker and put meaning in words. 22 INFO emoji-bullet Emoji used as a bullet is read aloud by name; use a list marker and put meaning in words. 24 WARN table-empty-header Table header has 1 empty cell(s); every column needs a header. 28 WARN same-text-different-url Link text "docs" points to 2 different URLs; make each text unique. 28 INFO bare-url Bare URL https://example.org/docs; screen readers spell it out. Use [descriptive text](https://example.org/docs). 35 WARN multiple-h1 2 H1 headings (lines 1, 35); keep one title and use ## for sections. ``` bash 代码块中的 `![](not-an-image.png)` 和最后一行的内联代码示例被正确忽略。 ## 审查(填写的模板,已缩短) # 可访问性审查:README.md 受众:徒步旅行者和贡献者,许多人从搜索进入。 检查器运行:HIGH 2, WARN 8, INFO 5。 ### 最重要的修复 1. 为主视觉图和构建徽章提供真正的替代文本(第 3 行和第 13 行)。 2. 将“here”和“click here”替换为指明目标的链接文本(第 9 行)。 3. 修复大纲:“Configuration”变为 H3,“Features”变为真正的 H3,第二个 H1“Contributing”变为 H2。 4. 指明 Save 按钮,而不仅仅是它的颜色(第 17 行)。 5. 为表格第一列添加表头(第 24 行)。 ### 替代文本工作表 | 文件:行 | 图片 | 用途 | 当前替代文本 | 新替代文本 | 看过? | |---|---|---|---|---|---| | README.md:3 | hero.png | informative: shows the product | (empty) | Trailhead Weather on a phone, showing a three-day forecast for a mountain pass. | described by the user | | README.md:11 | app-main.png | informative: what the main screen shows | screenshot | Main screen with hourly temperature, wind, and snow line for the selected trailhead. | described by the user | | README.md:13 | badge.svg | informative: build status | (no alt) | Build: passing | described by the user | ### 标题 - 之前:H1 Trailhead Weather / H2 Install / H4 Configuration / H1 Contributing (plus a bold "Features" line). - 之后:H1 Trailhead Weather / H2 Install / H3 Configuration / H3 Features / H2 Contributing. ### 链接 | 行 | 当前文本 | 新文本 | 目标 | |---|---|---|---| | 9 | here | Download the latest release | https://example.org/releases | | 9 | click here | setup guide | docs/guides/setup.md | | 28 | docs (bare URL + "docs") | API reference | https://example.org/docs | | 28 | docs | setup guide | docs/guides/setup.md | ### 其他发现 | 行 | 规则 | 更改 | |---|---|---| | 5 | all-caps | Normal case inside the bold note. | | 17 | color-only | "Select Save (the green button at the bottom of Settings)". | | 21-22 | emoji-bullet | Normal "-" list items. | | 24 | table-empty-header | First column header "Feature". | ### 修正后的 README(节选) ```markdown # Trailhead Weather ![Trailhead Weather on a phone, showing a three-day forecast for a mountain pass.](docs/img/hero.png) Trailhead Weather shows mountain forecasts for hikers. **Note: this project is still in beta and things will break often.** ## Install [Download the latest release](https://example.org/releases), or follow the [setup guide](docs/guides/setup.md). <img src="docs/img/badge.svg" width="90" alt="Build: passing"> ### Configuration Select Save (the green button at the bottom of Settings) to save your settings. ### Features - Hourly forecasts - Avalanche bulletins | Feature | Free | Pro | |---|---|---| | Forecast days | 3 | 10 | ``` ### 修复后 ``` $ python3 scripts/check_md_a11y.py README-fixed.md Markdown accessibility check: 1 file(s), 3 image(s) HIGH 0 WARN 0 INFO 0 ``` ### 仍需人工检查 - 主视觉图在手机屏幕上有小文本;替代文本传达了关键信息,所以这里没问题。 - 如果 README 将来被转换为文档站点页面,请确认站点不会添加第二个 H1。 ## 为什么这是一个好的回答 - 替代文本描述了每张图片的用途,使用了用户描述的图片内容,而不是猜测。 - 链接和标题的修复针对屏幕阅读器用户的导航方式:通过链接列表和标题大纲。 - 通过重新运行检查器验证修复,并诚实列出剩余的人工检查项。 FILE:scripts/check_md_a11y.py #!/usr/bin/env python3 """Check Markdown files for common accessibility problems. Usage: python3 check_md_a11y.py README.md docs/ # files and folders (*.md, *.markdown) python3 check_md_a11y.py docs/ --json cat page.md | python3 check_md_a11y.py - python3 check_md_a11y.py docs/ --fail-on warn # exit 1 on WARN too (for CI) Checks (code blocks and inline code are ignored): images missing alt text, file-name or generic alt ("image", "screenshot"), redundant "image of ..." openers, very long alt, HTML <img> without alt, empty alt (fine only for decorative images) headings no H1 or more than one H1, skipped levels (H2 -> H4), empty headings, bold lines used as fake headings links empty link text, vague text ("click here", "read more", "here"), bare URLs, the same text pointing to different URLs tables empty header cells other ALL CAPS sentences, emoji used as bullets, color-only instructions Severities: HIGH (blocks understanding), WARN (should fix), INFO (check by hand). Exit code: 0 no HIGH, 1 HIGH found (or WARN with --fail-on warn), 2 usage error. Standard library only. """ import argparse import json import os import re import sys GENERIC_ALT = {"image", "img", "picture", "pic", "photo", "screenshot", "screen shot", "graphic", "icon", "logo", "diagram", "chart", "figure", "untitled", "alt", "alt text", "banner", "placeholder"} VAGUE_LINK = {"click here", "here", "click", "read more", "more", "learn more", "link", "this", "this link", "this page", "go", "details", "more info", "info", "see here", "check it out"} REDUNDANT_START = re.compile(r"^(an? )?(image|picture|photo|screenshot|graphic) (of|showing)\b", re.I) FILE_ALT = re.compile(r"^[\w\-. ]+\.(png|jpe?g|gif|svg|webp|bmp|tiff?)$|^(img|image|dsc|screenshot)[_\- ]?\d+", re.I) IMG_MD = re.compile(r"!\[([^\]]*)\]\(\s*<?([^)\s>]*)>?(?:\s+\"[^\"]*\")?\s*\)") IMG_REF = re.compile(r"!\[([^\]]*)\]\[[^\]]*\]") LINK_MD = re.compile(r"(?<!!)\[([^\]]*)\]\(\s*<?([^)\s>]*)>?(?:\s+\"[^\"]*\")?\s*\)") HTML_IMG = re.compile(r"<img\b[^>]*>", re.I) BARE_URL = re.compile(r"(?<![(<\"'=\[])\bhttps?://[^\s)>\]]+") COLOR_ONLY = re.compile(r"\b(click|press|tap|select|choose|use)\b[^.]{0,30}\b(red|green|blue|yellow|orange|purple)\b(?! *\()", re.I) EMOJI_BULLET = re.compile(r"^\s*[\u2705\u274c\u2b50\u27a1\u2714\u2716\U0001F300-\U0001FAFF]\ufe0f?\s+\S") def strip_inline_code(line): return re.sub(r"`+[^`]*`+", lambda m: " " * len(m.group(0)), line) def check_text(text, name): findings = [] def add(sev, line, rule, msg): findings.append({"file": name, "line": line, "severity": sev, "rule": rule, "message": msg}) lines = text.replace("\r\n", "\n").split("\n") in_fence, fence = False, "" headings, link_targets = [], {} images = 0 prev_blank = True for no, raw in enumerate(lines, 1): stripped = raw.strip() m = re.match(r"^\s{0,3}(`{3,}|~{3,})", raw) if m: if not in_fence: in_fence, fence = True, m.group(1)[0] * 3 elif stripped.startswith(fence): in_fence = False continue if in_fence or raw.startswith(" ") and prev_blank and not stripped.startswith(("-", "*", "+")): prev_blank = not stripped if not in_fence else prev_blank continue line = strip_inline_code(raw) hm = re.match(r"^\s{0,3}(#{1,6})(\s+|$)(.*?)\s*#*\s*$", line) if hm: level, title = len(hm.group(1)), hm.group(3).strip() headings.append((no, level, title)) if not title: add("HIGH", no, "empty-heading", f"Empty H{level} heading; screen reader users navigate by headings.") elif re.fullmatch(r"\s*(\*\*|__)[^*_]{2,80}(\*\*|__):?\s*", line) and prev_blank: add("INFO", no, "fake-heading", f"Bold line \"{stripped.strip('*_:')}\" looks like a heading; use a real # heading if it starts a section.") images += len(IMG_MD.findall(line)) + len(IMG_REF.findall(line)) + len(HTML_IMG.findall(line)) for alt, src in IMG_MD.findall(line): check_alt(add, no, alt, src) for alt in IMG_REF.findall(line): check_alt(add, no, alt, "(reference)") for tag in HTML_IMG.findall(line): am = re.search(r"\balt\s*=\s*(\"([^\"]*)\"|'([^']*)'|(\S+))", tag, re.I) src = re.search(r"\bsrc\s*=\s*[\"']?([^\"'\s>]+)", tag, re.I) src = src.group(1) if src else "?" if not am: add("HIGH", no, "img-no-alt", f"<img src=\"{src}\"> has no alt attribute.") else: check_alt(add, no, am.group(2) if am.group(2) is not None else (am.group(3) if am.group(3) is not None else am.group(4)), src, html=True) no_images = IMG_MD.sub(" ", line) for text_, url in LINK_MD.findall(no_images): label = re.sub(r"[*_`]", "", text_).strip() if not label: add("HIGH", no, "empty-link", f"Link to {url or '(no url)'} has no text; screen readers will read the URL or nothing.") elif label.lower().rstrip(".!:") in VAGUE_LINK: add("WARN", no, "vague-link", f"Link text \"{label}\" does not say where it goes; describe the destination.") elif re.fullmatch(r"https?://\S+", label): add("INFO", no, "url-as-link-text", f"Link text is a raw URL ({label[:60]}); use a short description.") if label: link_targets.setdefault(label.lower(), {}).setdefault(url, no) for url in BARE_URL.findall(LINK_MD.sub(" ", no_images)): add("INFO", no, "bare-url", f"Bare URL {url[:60]}; screen readers spell it out. Use [descriptive text]({url[:40]}).") if "|" in line and re.match(r"^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$", line) and no > 1: header = lines[no - 2].strip().strip("|") cells = [c.strip() for c in header.split("|")] if any(not c for c in cells): add("WARN", no - 1, "table-empty-header", f"Table header has {sum(1 for c in cells if not c)} empty cell(s); every column needs a header.") run = longest = 0 for w in re.findall(r"[A-Za-z]+", line): run = run + 1 if w.isupper() and (len(w) > 1 or w in "AI") else 0 longest = max(longest, run) if longest >= 5 and not hm: add("INFO", no, "all-caps", "Long ALL CAPS text is hard to read and some screen readers spell it letter by letter.") if EMOJI_BULLET.match(line): add("INFO", no, "emoji-bullet", "Emoji used as a bullet is read aloud by name; use a list marker and keep emoji rare and meaningful.") cm = COLOR_ONLY.search(line) if cm: between = line[cm.end(1):cm.start(2)] inside_parens = line[:cm.start(2)].count("(") > line[:cm.start(2)].count(")") names_label = re.match(r"\s+(\*\*|\"|')?[A-Z]", between) # "Select Save ..." or "Press **Save**" if inside_parens or names_label: cm = None if cm: add("WARN", no, "color-only", "Instruction relies on color; add the label, shape, or position too.") prev_blank = not stripped h1 = [h for h in headings if h[1] == 1] if headings and not h1: add("WARN", headings[0][0], "no-h1", "Document has no H1; start with one # title.") if len(h1) > 1: add("WARN", h1[1][0], "multiple-h1", f"{len(h1)} H1 headings (lines {', '.join(str(h[0]) for h in h1)}); keep one title and use ## for sections.") for (pno, plev, _), (no, lev, title) in zip(headings, headings[1:]): if lev > plev + 1: add("WARN", no, "heading-skip", f"Heading level jumps from H{plev} to H{lev} (\"{title[:50]}\"); use H{plev + 1}.") if not headings and len([l for l in lines if l.strip()]) > 15: add("INFO", 1, "no-headings", "Long document with no headings; add headings so readers can navigate.") for label, targets in link_targets.items(): if len(targets) > 1 and label not in VAGUE_LINK: first = min(targets.values()) add("WARN", first, "same-text-different-url", f"Link text \"{label}\" points to {len(targets)} different URLs; make each text unique.") order = {"HIGH": 0, "WARN": 1, "INFO": 2} findings.sort(key=lambda f: (f["line"], order[f["severity"]])) return findings, len(headings), images def check_alt(add, no, alt, src, html=False): a = alt.strip() base = os.path.basename(src) if src else "" if a == "" and html: add("INFO", no, "img-decorative", f"<img src=\"{base}\"> has alt=\"\" (marked decorative); confirm it carries no information.") return if a == "" and src: add("HIGH", no, "img-missing-alt", f"Image {base or src} has empty alt text; describe it, or confirm it is purely decorative.") return low = a.lower().rstrip(".") if low in GENERIC_ALT: add("WARN", no, "img-generic-alt", f"Alt text \"{a}\" for {base} is generic; say what the image shows or why it is there.") elif FILE_ALT.search(a) or (base and low == os.path.splitext(base)[0].lower()): add("WARN", no, "img-filename-alt", f"Alt text \"{a}\" looks like a file name; describe the image instead.") elif REDUNDANT_START.match(a): add("INFO", no, "img-redundant-alt", f"Alt text starts with \"{' '.join(a.split()[:3])}\"; screen readers already say 'image', so start with the content.") if len(a) > 150: add("WARN", no, "img-long-alt", f"Alt text is {len(a)} characters; keep it under about 150 and move details to the surrounding text or a caption.") def collect(paths): files = [] for p in paths: if p == "-": files.append("-") elif os.path.isdir(p): for root, dirs, names in os.walk(p): dirs[:] = sorted(d for d in dirs if not d.startswith(".") and d != "node_modules") files += [os.path.join(root, n) for n in sorted(names) if n.lower().endswith((".md", ".markdown"))] elif os.path.isfile(p): files.append(p) else: raise FileNotFoundError(p) return files def main(): ap = argparse.ArgumentParser(description="Markdown accessibility checker") ap.add_argument("paths", nargs="+", help="Markdown files, folders, or - for stdin") ap.add_argument("--json", action="store_true") ap.add_argument("--fail-on", choices=["high", "warn"], default="high") args = ap.parse_args() try: files = collect(args.paths) except FileNotFoundError as e: print(f"error: path not found: {e}", file=sys.stderr) return 2 if not files: print("error: no Markdown files (*.md, *.markdown) found", file=sys.stderr) return 2 all_findings, stats = [], [] for f in files: text = sys.stdin.read() if f == "-" else open(f, encoding="utf-8", errors="replace").read() name = "<stdin>" if f == "-" else f found, nhead, images = check_text(text, name) stats.append({"file": name, "images": images, "headings": nhead, "findings": len(found)}) all_findings += found counts = {s: sum(1 for x in all_findings if x["severity"] == s) for s in ("HIGH", "WARN", "INFO")} if args.json: print(json.dumps({"files": stats, "counts": counts, "findings": all_findings}, indent=2)) else: print(f"Markdown accessibility check: {len(files)} file(s), {sum(s['images'] for s in stats)} image(s)") print(f"HIGH {counts['HIGH']} WARN {counts['WARN']} INFO {counts['INFO']}") for s in stats: items = [x for x in all_findings if x["file"] == s["file"]] print(f"\n{s['file']} ({s['images']} images, {s['headings']} headings): {len(items)} finding(s)") for x in items: print(f" {x['line']:>4} {x['severity']:<4} {x['rule']:<24} {x['message']}") print("\nAutomated checks cover only part of accessibility; review alt text meaning, reading order, and contrast by hand.") bad = counts["HIGH"] + (counts["WARN"] if args.fail_on == "warn" else 0) return 1 if bad else 0 if __name__ == "__main__": sys.exit(main())

登录以查看完整提示词

继续使用:

登录即表示你同意我们的 使用条款 和 隐私政策

用法

此提示词专为 productivity 设计。复制上方内容并粘贴到你常用的 AI 工具中。

为获得最佳效果,可将占位符(方括号或大写字母标示)替换为你的具体需求。

参考资料

分类:productivity| prompts.chat| markdown| accessibility

讨论