마크다운 접근성 검사기 스킬
Wikiprompt, 무료 프롬프트 백과사전에서
마크다운 접근성 검사기 스킬 Markdown 문서의 접근성을 검토하고, 대체 텍스트를 작성하며, 수정 목록 또는 수정된 파일을 반환하는 AI 에이전트를 위한 완전한 스킬 정의입니다. 워크플로우, 참조 가이드, 템플릿, 예시 검토, 그리고 Python 체커 스크립트를 포함합니다.
프롬프트 내용저장
🌐
---
name: markdown-accessibility-checker
description: 마크다운 문서(README, 문서 사이트, 위키, 블로그 게시물, 변경 로그)의 접근성을 검토합니다 - 누락되거나 부실한 이미지 대체 텍스트, 제목 구조, 모호하거나 중복된 링크 텍스트, 링크가 없는 URL, 빈 테이블 헤더, 색상만으로 전달하는 지침, ALL CAPS, 이모지 불릿 - 그런 다음 각 이미지에 대한 좋은 대체 텍스트를 문맥에서 작성하고 수정 목록 또는 수정된 파일을 반환합니다. 사용자가 "내 README 접근성 확인해줘", "이 이미지들에 대체 텍스트 작성해줘", "우리 문서를 스크린 리더 친화적으로 만들어줘"라고 요청하거나 CI에서 마크다운 접근성 검사를 원할 때 사용합니다.
---
# 마크다운 접근성 검사기
당신은 스크린 리더, 키보드 탐색, 확대, 또는 읽기 보조 도구를 사용하는 사람들을 포함한 모든 사람이 마크다운 문서를 사용할 수 있게 만듭니다. 스크립트가 감지할 수 있는 문제를 찾은 다음, 스크립트가 할 수 없는 부분, 즉 문서의 목적에 맞는 의미 있는 대체 텍스트와 링크 텍스트를 작성합니다.
## 이 스킬의 파일들
- `scripts/check_md_a11y.py` - 마크다운 파일 또는 폴더를 스캔하고 줄 번호와 심각도와 함께 문제를 보고합니다 (Python 3 표준 라이브러리만 사용)
- `references/alt-text-guide.md` - 이미지 유형별 대체 텍스트 작성 방법 (스크린샷, 다이어그램, 차트, 사진, 로고, 배지, 장식용 이미지)
- `references/structure-and-links.md` - 제목, 링크 텍스트, 테이블, 목록, 강조 및 스크립트의 각 규칙이 의미하는 바
- `templates/a11y-review.md` - 검토 보고서 및 대체 텍스트 워크시트
- `examples/example-readme-review.md` - 프로젝트 README에 대한 작업된 검토 예시
## 작업 흐름
### 1. 검토 범위 설정
검토할 파일이나 폴더, 독자(개발자, 최종 사용자, 학생), 사용자가 보고서, 수정된 파일 또는 둘 다를 원하는지 물어보십시오. 문서가 사이트 생성기에 의해 렌더링되는 경우, 생성기가 자체 페이지 제목을 추가하는지 물어보십시오 (그렇다면 마크다운에 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`를 사용하여 목적을 결정하십시오:
- 주변 텍스트를 읽으십시오: 문서의 이 지점에서 이미지의 용도는 무엇입니까?
- 이미지를 볼 수 있으면 그 목적에 중요한 것을 설명하십시오. 볼 수 없으면 사용자에게 묻거나 파일 이름과 문맥에서 명확하게 표시된 초안을 작성하십시오 (예: "DRAFT: 이미지와 대조 확인").
- 장식용 이미지는 빈 대체 텍스트를 얻습니다; 마크다운에서는 제거하거나 HTML `<img alt="">`를 사용하는 것이 좋습니다.
`templates/a11y-review.md`의 대체 텍스트 워크시트를 작성하십시오.
### 4. 구조 및 링크 수정
`references/structure-and-links.md`를 적용하십시오: H1 하나, 건너뛴 수준 없음, 굵은 줄 대신 실제 제목, 설명적인 고유 링크 텍스트, 헤더가 있는 테이블, 색상에만 의존하지 않는 지침.
### 5. 전달
- 보고서: `examples/example-readme-review.md`에서와 같이 `templates/a11y-review.md`를 가장 중요한 수정 사항을 먼저 넣어 작성하십시오.
- 수정된 파일: 수정 사항이 적용된 전체 마크다운과 간단한 변경 목록을 반환하십시오. 의미, 코드 샘플 또는 링크 대상을 변경하지 마십시오.
- 수정된 파일에 검사기를 다시 실행하고 전후 개수를 포함하십시오.
## 규칙
- 이미지가 무엇을 보여주는지 절대 추측하지 마십시오. 보지 못했다면 대체 텍스트를 초안으로 표시하십시오.
- 대체 텍스트를 짧게 유지하십시오(보통 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=""`를 사용하십시오. 마크다운에서 ``는 모호하므로(종종 실수) 이미지를 제거하거나 `<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`로 시작하십시오. 사이트 생성기가 프론트 매터에서 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 | ``는 대안을 제공하지 않습니다; 일부 독자는 파일 이름을 발표합니다. |
| img-no-alt | HIGH | alt 속성이 없는 `<img>`; 스크린 리더는 파일 이름으로 대체합니다. |
| 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): 긴 ALL CAPS 텍스트는 읽기 어렵고 일부 스크린 리더는 짧은 대문자 단어를 문자별로 철자합니다. 일반 대소문자와 굵게를 강조에 사용하십시오.
- emoji-bullet (INFO): 이모지는 이름으로 소리 내어 읽힙니다("white heavy check mark Hourly forecasts"). 일반 목록 마커를 사용하고 이모지를 드물고 의미 있게 유지하십시오.
## 스크립트로 확인되지 않음 (수동으로 확인)
- 대체 텍스트가 정확하고 유용한지.
- 색상 대비 및 이미지 내부 텍스트(작은 텍스트가 있는 스크린샷, 대비가 낮은 차트).
- 복잡한 레이아웃의 읽기 순서 및 마크다운에 포함된 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>
## 대체 텍스트 워크시트
| 파일:줄 | 이미지 | 문서에서의 목적 | 현재 대체 텍스트 | 새 대체 텍스트 | 이미지 확인? |
|---|---|---|---|---|---|
| | | 정보 제공 / 기능 / 장식 / 복잡 | | | 예 / 초안 |
## 제목
- 현재 개요: <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 코드 블록 내부의 ``와 마지막 줄의 인라인 코드 예제는 올바르게 무시되었습니다.
## 검토 (작성된 템플릿, 축약됨)
# 접근성 검토: 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 | 정보 제공: 제품 표시 | (비어 있음) | Trailhead Weather on a phone, showing a three-day forecast for a mountain pass. | 사용자가 설명함 |
| README.md:11 | app-main.png | 정보 제공: 기본 화면 표시 | screenshot | Main screen with hourly temperature, wind, and snow line for the selected trailhead. | 사용자가 설명함 |
| README.md:13 | badge.svg | 정보 제공: 빌드 상태 | (alt 없음) | Build: passing | 사용자가 설명함 |
### 제목
- 이전: H1 Trailhead Weather / H2 Install / H4 Configuration / H1 Contributing (굵은 "Features" 줄 포함).
- 이후: 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 | 굵은 메모 내부의 일반 대소문자. |
| 17 | color-only | "Select Save (the green button at the bottom of Settings)". |
| 21-22 | emoji-bullet | 일반 "-" 목록 항목. |
| 24 | table-empty-header | 첫 번째 열 헤더 "Feature". |
### 수정된 README (발췌)
```markdown
# Trailhead Weather

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())
전체 프롬프트를 보려면 로그인하세요
Continue with:
By logging in, you agree to our Terms of Use and Privacy Policy
사용법
이 프롬프트는 productivity와 함께 사용하도록 설계되었습니다. 위의 프롬프트 내용을 복사하여 원하는 AI 도구에 붙여넣으세요.
최상의 결과를 얻으려면 자리 표시자(대괄호 또는 대문자로 표시)를 특정 요구 사항으로 사용자 지정할 수 있습니다.
토론
댓글 0개