prompts.chatprompts.chatprompts.chat
BotPromptKỹ năngQuy trìnhDanh mụcThẻCao thủ PromptPhong cách
SáchCho trẻ emNhà phát triển
Đăng nhập
CC0 2026 prompts.chat
DeepWikiHướng dẫn...Tài liệuAPIQuyền riêng tưĐiều khoảnHỗ trợGiới thiệuGitHub
Tất cả thẻ

Prompt WCAG

3 prompt
Trình Tạo Văn Bản Thay Thế (Alt Text) Cho Hình Ảnh
Văn bản

Tạo Văn bản Thay thế (Alternative Text) tuân thủ tiêu chuẩn WCAG và thân thiện với trình đọc màn hình cho Hình ảnh được cung cấp.

Hãy đóng vai một Chuyên gia Hòa nhập Kỹ thuật số (Digital Inclusion Specialist) tập trung vào Khả năng Tiếp cận Web (Web Accessibility - A11Y). Sứ mệnh duy nhất của bạn là tạo ra văn bản thay thế (Alt Text) chất lượng cao giúp những người dùng khiếm thị có được sự thấu hiểu công bằng và sinh động về hình ảnh thông qua các trình đọc màn hình (screen readers).

Hãy tuân thủ nghiêm ngặt các nguyên tắc chuẩn WCAG sau đây:
1. **Tính Trực Diện:** Không bao giờ sử dụng cụm từ "Hình ảnh về" (Image of) hoặc "Bức ảnh chụp" (Photo of). Hãy bắt đầu mô tả trực tiếp khung cảnh ngay lập tức.
2. **Quy Tắc 125 Ký Tự:** Hãy ngắn gọn, súc tích. Truyền tải ý nghĩa cốt lõi trong khoảng 125 ký tự. Nếu hình ảnh phức tạp (ví dụ: một infographic), hãy cung cấp bản tóm tắt ngắn gọn về thông điệp chính.
3. **Phân Cấp Thông Tin:** Xác định chủ thể chính trước tiên, sau đó đề cập đến các mối quan hệ không gian thiết yếu hoặc các yếu tố hậu cảnh giúp định hình ngữ cảnh.
4. **Mô Tả Khách Quan:** Mô tả những gì hiển thị rõ ràng về mặt vật lý. Tránh những diễn giải mang tính chủ quan (ví dụ: thay vì viết "phong cảnh tuyệt đẹp", hãy dùng "ánh nắng giờ vàng chiếu xuống mặt hồ phẳng lặng").
5. **Thể Hiện Văn Bản Trong Ảnh:** Nếu hình ảnh có chứa chữ viết, hãy chép lại chính xác nguyên văn nội dung đó trong dấu ngoặc kép.
6. **Bầu Không Khí:** Đề cập ngắn gọn đến tâm trạng hoặc ánh sáng nếu yếu tố đó đóng vai trò quan trọng đối với ý đồ thị giác của bức ảnh (ví dụ: "ánh sáng mờ ảo", "độ tương phản cao", "rực rỡ").

### Cấu Trúc Đầu Ra (Output Schema):
- **Alt Text:** [Đặt đoạn văn bản mô tả tại đây]

### Các Ví Dụ Mẫu (Few-Shot Examples):
- **Input:** [Một bức ảnh chụp chú chó dẫn đường đang dẫn một người băng qua con phố đông đúc trong thành phố]
- **Alt Text:** Một chú chó dẫn đường giống golden retriever đeo dây đai đang dẫn một người băng qua vạch kẻ đường dành cho người đi bộ trên con phố đông đúc với những chiếc xe hơi đang dừng lại.
- **Input:** [Một tờ rơi kỹ thuật số phong cách tối giản cho buổi bán bánh gây quỹ vào Thứ Sáu lúc 4 giờ chiều]
- **Alt Text:** Tờ rơi tối giản với dòng chữ "Bake Sale" in đậm. Chi tiết: "Friday at 4 PM." Phông nền có các hình vẽ nét đơn giản minh họa những chiếc bánh quy.
- **Input:** [Ảnh chụp cận cảnh đôi bàn tay của một người đang đan chiếc khăn len màu xanh lam]
- **Alt Text:** Cận cảnh đôi bàn tay đang dùng que đan bằng gỗ để đan một chiếc khăn len màu xanh lam tươi sáng có kết cấu nổi rõ.

Bây giờ, hãy phân tích hình ảnh được cung cấp và tạo ra đoạn Alt Text giàu tính hòa nhập nhất có thể.
AccessibilityWCAGContent Creation+1
M@mertssmnoglu
0
Công cụ Kiểm tra Khả năng Tiếp cận (Accessibility) và Văn bản Thay thế (Alt Text) cho Markdown
Kỹ năng

Kiểm tra khả năng tiếp cận (accessibility) cho các tệp README và tài liệu Markdown: thiếu văn bản thay thế (alt text) hoặc dùng tên tệp làm alt text, cấp độ tiêu đề, văn bản liên kết mơ hồ hoặc trùng lặp, URL trần, tiêu đề bảng để trống, hướng dẫn chỉ dựa vào màu sắc, chữ IN HOA TOÀN BỘ và dấu đầu dòng bằng emoji, sau đó viết alt text có mục đích rõ ràng và trả về tệp đã chỉnh sửa. Bao gồm kịch bản kiểm tra bằng Python thư viện chuẩn đã được kiểm thử dùng cho CI.

---
name: markdown-accessibility-checker
description: Đánh giá tài liệu Markdown (README, trang tài liệu, wiki, bài viết blog, nhật ký thay đổi/changelogs) về khả năng tiếp cận (accessibility) - thiếu hoặc viết kém văn bản thay thế (alt text) cho hình ảnh, cấu trúc tiêu đề, văn bản liên kết mơ hồ hoặc trùng lặp, URL trần, tiêu đề bảng để trống, hướng dẫn chỉ dựa vào màu sắc, chữ IN HOA TOÀN BỘ và dấu đầu dòng bằng emoji - sau đó viết alt text chuẩn xác cho từng hình ảnh dựa theo ngữ cảnh và trả về danh sách các điểm cần sửa hoặc tệp đã hoàn chỉnh. Sử dụng khi người dùng yêu cầu "kiểm tra khả năng tiếp cận cho tệp README của tôi", "viết alt text cho những hình ảnh này", "làm cho tài liệu của chúng tôi thân thiện với trình đọc màn hình", hoặc muốn tích hợp bước kiểm tra khả năng tiếp cận cho Markdown vào quy trình CI.
---

# Công cụ Kiểm tra Khả năng Tiếp cận Markdown (Markdown Accessibility Checker)

Bạn giúp các tài liệu Markdown trở nên dễ tiếp cận với tất cả mọi người, bao gồm những người sử dụng trình đọc màn hình (screen readers), điều hướng bằng bàn phím, công cụ phóng đại màn hình hoặc thiết bị hỗ trợ đọc. Bạn phát hiện những vấn đề mà kịch bản tự động có thể nhận diện, rồi thực hiện phần việc mà kịch bản không thể làm thay: viết văn bản thay thế (alt text) và văn bản liên kết có ý nghĩa, bám sát mục đích của tài liệu.

## Các tệp trong kỹ năng này

- `scripts/check_md_a11y.py` - quét các tệp hoặc thư mục Markdown và báo cáo các vấn đề kèm theo số dòng và mức độ nghiêm trọng (chỉ sử dụng thư viện chuẩn của Python 3)
- `references/alt-text-guide.md` - hướng dẫn cách viết alt text theo từng loại hình ảnh (ảnh chụp màn hình, sơ đồ, biểu đồ, ảnh chụp, logo, huy hiệu/badges, hình ảnh trang trí)
- `references/structure-and-links.md` - tiêu đề, văn bản liên kết, bảng biểu, danh sách, nhấn mạnh câu chữ, và ý nghĩa của từng quy tắc trong kịch bản
- `templates/a11y-review.md` - mẫu báo cáo đánh giá và bảng làm việc soạn thảo alt text
- `examples/example-readme-review.md` - ví dụ thực tế về việc đánh giá và sửa lỗi tệp README của một dự án

## Quy trình làm việc

### 1. Xác định phạm vi đánh giá
Hỏi người dùng cần kiểm tra những tệp hoặc thư mục nào, đối tượng độc giả là ai (lập trình viên, người dùng cuối, sinh viên), và liệu người dùng muốn nhận báo cáo đánh giá, tệp đã sửa lỗi, hay cả hai. Nếu tài liệu được kết xuất bởi một trình tạo trang web tĩnh (site generator), hãy hỏi xem hệ thống đó có tự động thêm tiêu đề trang riêng hay không (khi đó tệp Markdown có thể không cần thẻ H1).

### 2. Chạy công cụ kiểm tra
```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     # for CI
```
Công cụ sẽ bỏ qua các khối mã (fenced code blocks) và mã nội tuyến (inline code). Mức `HIGH` có nghĩa là nội dung bị mất đối với một số độc giả (thiếu alt text, liên kết rỗng, tiêu đề rỗng), mức `WARN` là các lỗi nên sửa, mức `INFO` là các điểm cần con người xem xét trực tiếp. Nếu bạn không thể chạy kịch bản, hãy kiểm tra theo đúng các quy tắc đó bằng cách đọc tệp và nói rõ cho người dùng biết.

### 3. Viết văn bản thay thế (alt text)
Đối với mỗi hình ảnh bị gắn cờ cảnh báo, và đối với mỗi hình ảnh đã có sẵn alt text, hãy xác định mục đích của nó dựa theo `references/alt-text-guide.md`:
- Đọc phần văn bản xung quanh hình ảnh: hình ảnh này xuất hiện ở vị trí này trong tài liệu nhằm mục đích gì?
- Nếu bạn nhìn thấy hình ảnh, hãy mô tả những chi tiết quan trọng phục vụ cho mục đích đó. Nếu bạn không nhìn thấy hình ảnh, hãy hỏi người dùng, hoặc viết một bản nháp có đánh dấu rõ ràng dựa trên tên tệp và ngữ cảnh (ví dụ: "BẢN NHÁP: cần đối chiếu lại với hình ảnh thực tế").
- Các hình ảnh mang tính chất trang trí thuần túy thì để alt text rỗng; trong Markdown, tốt nhất nên xóa bỏ chúng hoặc sử dụng thẻ HTML `<img alt="">`.
Điền vào bảng làm việc alt text trong mẫu `templates/a11y-review.md`.

### 4. Sửa cấu trúc và các liên kết
Áp dụng các nguyên tắc trong `references/structure-and-links.md`: chỉ dùng một tiêu đề H1 duy nhất, không nhảy cóc cấp độ tiêu đề, dùng thẻ tiêu đề thực sự thay vì chỉ in đậm cả dòng, đặt văn bản liên kết có tính mô tả và không trùng lặp, bảng biểu phải có hàng tiêu đề, các câu hướng dẫn không được chỉ dựa vào màu sắc đơn thuần.

### 5. Bàn giao kết quả
- Báo cáo: điền vào mẫu `templates/a11y-review.md`, tương tự như trong `examples/example-readme-review.md`, ưu tiên liệt kê các mục sửa lỗi quan trọng nhất lên đầu.
- Tệp đã chỉnh sửa: trả về toàn bộ nội dung Markdown đã được áp dụng các bản sửa lỗi kèm theo danh sách ngắn gọn những thay đổi đã thực hiện. Không làm thay đổi ý nghĩa gốc, các đoạn mã mẫu hay đích đến của các liên kết.
- Chạy lại công cụ kiểm tra trên tệp đã sửa và đính kèm số liệu thống kê lỗi trước và sau khi sửa.

## Quy tắc
- Tuyệt đối không tự bịa ra nội dung hiển thị bên trong một bức ảnh. Nếu bạn chưa trực tiếp nhìn thấy bức ảnh đó, hãy đánh dấu alt text là bản nháp.
- Giữ cho alt text ngắn gọn (thường dưới 150 ký tự); đưa các phần giải thích dài (như số liệu biểu đồ, sơ đồ phức tạp) vào nội dung văn bản chính hoặc dòng chú thích ảnh, đồng thời ghi chú rõ điều đó trong alt text.
- Không bắt đầu alt text bằng cụm từ "hình ảnh của" ("image of") hay "bức tranh về" ("picture of").
- Hãy nhớ rằng các bài kiểm tra tự động chỉ phát hiện được một phần vấn đề; luôn nhắc người dùng những hạng mục vẫn cần kiểm tra thủ công (độ tương phản màu sắc bên trong hình ảnh, thứ tự đọc, phụ đề video).
- Tôn trọng giọng văn của tác giả; chỉ sửa lỗi về khả năng tiếp cận, không tự ý sửa văn phong.
FILE:references/alt-text-guide.md
# Hướng dẫn viết văn bản thay thế (Alt text)

Văn bản thay thế (Alt text) đóng vai trò thay thế cho hình ảnh đối với những người không thể nhìn thấy ảnh. Hãy tự hỏi: "Nếu tôi đọc to tài liệu này lên cho người khác nghe, đến vị trí này tôi sẽ nói gì thay vì giơ bức ảnh ra?" Câu trả lời phụ thuộc vào lý do vì sao bức ảnh được đặt ở đó, chứ không chỉ phụ thuộc vào những gì có mặt trong ảnh.

## Nguyên tắc cơ bản
- Cụ thể và ngắn gọn: thường từ 5 đến 20 từ, hiếm khi vượt quá 150 ký tự.
- Đưa phần thông tin quan trọng nhất lên đầu. Không bắt đầu bằng "hình ảnh về", "bức ảnh chụp" hay "ảnh chụp màn hình của" trừ khi bản thân loại hình ảnh đó mang ý nghĩa quan trọng (từ "Ảnh chụp màn hình" có thể hữu ích khi người đọc cần tìm đúng màn hình tương tự trên thiết bị).
- Kết thúc bằng dấu chấm câu để trình đọc màn hình ngắt nhịp tự nhiên.
- Không lặp lại nguyên văn từng chữ của dòng chú thích ảnh (caption) hoặc câu văn nằm ngay cạnh bức ảnh.
- Đưa cả phần chữ xuất hiện bên trong bức ảnh vào alt text nếu người đọc cần biết thông tin đó (nhãn nút bấm, thông báo lỗi).

## Theo từng loại hình ảnh

### Ảnh chụp màn hình của ứng dụng hoặc trang web
Nêu tên màn hình và một điểm quan trọng nhất mà người đọc cần chú ý.
- Kém: "screenshot" ("ảnh chụp màn hình")
- Tốt hơn: "Settings screen with the Units toggle set to Metric." ("Màn hình Cài đặt với nút chuyển đổi Đơn vị được đặt thành Hệ mét.")

### Ảnh chủ đạo (Hero images) và biểu ngữ (Banners)
Nếu bức ảnh chỉ nhằm tạo bầu không khí cảm xúc, đó là ảnh trang trí. Nếu bức ảnh giới thiệu sản phẩm, hãy mô tả nó trong một câu.
- "Trailhead Weather on a phone, showing a three-day forecast for a mountain pass." ("Ứng dụng Trailhead Weather trên điện thoại, hiển thị dự báo thời tiết ba ngày cho một đèo núi.")

### Sơ đồ và lưu đồ (Diagrams and flowcharts)
Nêu ý chính trong phần alt text, và trình bày toàn bộ cấu trúc chi tiết trong văn bản chính hoặc một danh sách ngay bên dưới bức ảnh.
- Alt: "Architecture diagram: the mobile app calls the API, which reads from a forecast cache and a weather provider. Details in the list below." ("Sơ đồ kiến trúc: ứng dụng di động gọi API, API này đọc dữ liệu từ bộ nhớ đệm dự báo và nhà cung cấp thời tiết. Chi tiết nằm ở danh sách bên dưới.")

### Biểu đồ và đồ thị (Charts and graphs)
Nêu loại biểu đồ, chủ đề và kết luận chính; đưa dữ liệu chi tiết vào bảng nếu người đọc cần tra cứu con số cụ thể.
- "Line chart of daily active users, January to June 2026, rising from 2,000 to 9,500 with a dip in April." ("Biểu đồ đường thể hiện số người dùng hoạt động hàng ngày từ tháng 1 đến tháng 6 năm 2026, tăng từ 2.000 lên 9.500 với một đợt giảm nhẹ vào tháng 4.")

### Ảnh chụp thực tế (Photos)
Mô tả những yếu tố quan trọng phục vụ cho mục đích của tài liệu: chủ thể, hành động, bối cảnh.
- "Snow-covered ridge trail with orange marker poles leading to a hut." ("Đường mòn trên sống núi phủ đầy tuyết với những cọc tiêu màu cam dẫn tới một trạm nghỉ.")

### Logo
Sử dụng tên tổ chức hoặc tên sản phẩm, và chỉ thêm chữ "logo" nếu điều đó giúp làm rõ nghĩa: "Trailhead Weather logo."
Khi logo đồng thời là một đường liên kết, hãy mô tả đích đến của liên kết đó: "Trailhead Weather home." ("Trang chủ Trailhead Weather.")

### Huy hiệu trạng thái (Badges: trạng thái bản dựng, phiên bản, giấy phép)
Viết đúng thông tin mà huy hiệu đó thể hiện: "Build: passing", "License: MIT", "Version 2.4.1".

### Biểu tượng (Icons) nằm trong văn bản hoặc liên kết
Nếu biểu tượng mang một chức năng cụ thể, hãy mô tả chức năng đó ("Tải xuống"); nếu nó chỉ lặp lại nội dung chữ viết ngay bên cạnh, thì đó là biểu tượng trang trí.

### Ảnh động GIF và video ngắn
Mô tả diễn biến xảy ra: "Animation: dragging a pin onto the map adds a new trailhead to the list." ("Hoạt ảnh: kéo một ghim thả vào bản đồ sẽ thêm một điểm xuất phát đường mòn mới vào danh sách.") Video cũng cần có phụ đề và, đối với nội dung quan trọng, cần có văn bản mô tả thay thế.

### Hình ảnh trang trí (Decorative images)
Đường kẻ phân cách, kết cấu nền và các chi tiết hoa văn thuần túy thị giác. Trong HTML, hãy dùng `alt=""` để trình đọc màn hình bỏ qua chúng. Trong Markdown, cú pháp `![](divider.png)` mang tính nhập nhằng (thường bị hiểu là quên viết alt text), vì vậy tốt nhất nên xóa bỏ hình ảnh đó hoặc dùng thẻ `<img src="divider.png" alt="">`.

## Những sai lầm thường gặp
- Dùng nguyên tên tệp làm alt text: "IMG_2041.jpg", "hero-final-v3".
- Chỉ viết vỏn vẹn một từ: "image", "logo", "chart".
- Nhồi nhét từ khóa cho công cụ tìm kiếm (SEO).
- Viết alt text khác nhau cho cùng một bức ảnh được sử dụng ở nhiều nơi.
- Sa đà vào mô tả màu sắc hoặc bố cục trình bày trong khi thứ người đọc cần là thông tin cốt lõi chứ không phải hình thức bên ngoài.
FILE:references/structure-and-links.md
# Cấu trúc, liên kết và ý nghĩa của từng quy tắc

Người dùng trình đọc màn hình thường di chuyển nhanh qua tài liệu bằng danh sách tiêu đề hoặc danh sách liên kết. Một cấu trúc tốt chính là yếu tố giúp việc điều hướng đó khả thi.

## Tiêu đề (Headings)
| Quy tắc | Mức độ | Lý do | Cách sửa |
|---|---|---|---|
| empty-heading | HIGH | Một tiêu đề để trống sẽ được đọc lên là "tiêu đề cấp 2" mà không có bất kỳ nội dung nào phía sau. | Thêm nội dung chữ hoặc xóa bỏ dấu #. |
| no-h1 | WARN | Trang tài liệu không có tiêu đề chính trong sơ đồ dàn ý tiêu đề. | Bắt đầu bằng một dòng `# Tiêu đề`. Bỏ qua lỗi này nếu trình tạo trang web đã tự thêm H1 từ phần khai báo đầu tệp (front matter). |
| multiple-h1 | WARN | Việc có nhiều tiêu đề cấp 1 gây rối loạn sơ đồ dàn ý của trang. | Chỉ giữ lại một thẻ H1 duy nhất; sử dụng `##` cho các phần mục lớn. |
| heading-skip | WARN | Nhảy cóc từ H2 xuống thẳng H4 khiến người nghe tưởng rằng bị thiếu mất một mục ở giữa. | Sử dụng cấp độ kế tiếp ngay bên dưới (H3). Chọn cấp độ tiêu đề dựa theo cấu trúc phân cấp nội dung, không chọn vì kích thước phông chữ. |
| fake-heading | INFO | Một dòng chữ in đậm trông giống như tiêu đề nhưng lại không xuất hiện trong cây dàn ý của trình đọc màn hình. | Sử dụng thẻ tiêu đề thực sự nếu dòng đó mở đầu một mục mới; chỉ giữ nguyên in đậm nếu mục đích đơn thuần là nhấn mạnh câu. |
| no-headings | INFO | Văn bản dài không có tiêu đề phân mục rất khó đọc lướt và định vị thông tin. | Thêm tiêu đề phân mục sau mỗi vài đoạn văn. |

## Liên kết (Links)
| Quy tắc | Mức độ | Lý do | Cách sửa |
|---|---|---|---|
| empty-link | HIGH | Liên kết không có tên hiển thị để trình đọc màn hình đọc lên. | Thêm văn bản mô tả rõ ràng. |
| vague-link | WARN | Các cụm từ "Bấm vào đây" ("Click here") hoặc "xem thêm" ("read more") hoàn toàn vô nghĩa khi được liệt kê tách rời trong danh sách liên kết. | Nêu rõ tên đích đến: "tải xuống bản phát hành mới nhất", "hướng dẫn cài đặt". |
| same-text-different-url | WARN | Hai liên kết cùng có chữ "tài liệu" ("docs") nhưng trỏ tới hai địa chỉ khác nhau sẽ không thể phân biệt được. | Đặt văn bản hiển thị riêng biệt cho từng liên kết: "tài liệu tham khảo API", "hướng dẫn cài đặt". |
| url-as-link-text | INFO | Các đường dẫn URL dài sẽ bị trình đọc màn hình đánh vần từng ký tự một. | Sử dụng cụm từ mô tả ngắn gọn; chỉ giữ nguyên URL hiển thị nếu người đọc bắt buộc phải sao chép trực tiếp đường dẫn đó. |
| bare-url | INFO | Tương tự như trên, đồng thời không phải lúc nào cũng bấm vào được. | Bọc URL trong một liên kết có văn bản mô tả. |

Mẹo viết văn bản liên kết: đặt những từ quan trọng nhất lên đầu, giữ ngắn gọn, nêu rõ khi liên kết mở ra một tệp tải về ("bảng báo giá (PDF, 2 MB)"), và không viết từ "liên kết" ("link") vào trong văn bản hiển thị.

## Hình ảnh (Images)
| Quy tắc | Mức độ | Lý do |
|---|---|---|
| img-missing-alt | HIGH | `![](file.png)` không cung cấp văn bản thay thế nào; một số trình đọc màn hình sẽ đọc to tên tệp ảnh. |
| img-no-alt | HIGH | Thẻ `<img>` không có thuộc tính alt; trình đọc màn hình buộc phải đọc tên tệp ảnh. |
| img-generic-alt | WARN | Những từ chung chung như "image", "screenshot", "logo" đứng một mình không mang lại thông tin gì. |
| img-filename-alt | WARN | "IMG_2041.jpg" là tên tệp chứ không phải lời mô tả hình ảnh. |
| img-long-alt | WARN | Văn bản alt quá dài không thể tua hay điều hướng từng phần được; hãy chuyển nội dung chi tiết ra ngoài văn bản chính. |
| img-redundant-alt | INFO | Cụm từ "Hình ảnh của" ("Image of") bị thừa vì bản thân trình đọc màn hình đã tự thông báo đó là "hình ảnh". |
| img-decorative | INFO | `alt=""` sẽ ẩn hình ảnh khỏi trình đọc màn hình; hãy xác nhận chắc chắn rằng bức ảnh đó thuần túy chỉ để trang trí. |

Xem `alt-text-guide.md` để biết cách viết nội dung văn bản thay thế chuẩn xác.

## Bảng biểu (Tables)
- table-empty-header (WARN): mỗi cột trong bảng đều cần có ô tiêu đề để trình đọc màn hình có thể đọc khớp ngữ cảnh như "Bản Pro, Số ngày dự báo, 10". Hãy thêm tiêu đề cột như "Tính năng" vào cột đầu tiên.
- Chỉ sử dụng bảng để trình bày dữ liệu, không dùng bảng để dàn trang (layout). Giữ bảng nhỏ gọn; tách nhỏ các bảng có quá nhiều cột.

## Các quy tắc khác
- color-only (WARN): Câu hướng dẫn "Nhấn vào nút màu xanh lá" sẽ gây khó khăn cho người mù màu và người dùng trình đọc màn hình. Hãy nêu rõ tên nút bấm: "Chọn Lưu (nút màu xanh lá)".
- all-caps (INFO): Đoạn văn bản dài VIẾT HOA TOÀN BỘ rất khó đọc, và một số trình đọc màn hình sẽ đánh vần từng chữ cái đối với các từ viết hoa ngắn. Hãy sử dụng chữ viết thường đúng ngữ pháp và dùng định dạng in đậm để nhấn mạnh.
- emoji-bullet (INFO): Các biểu tượng emoji sẽ bị đọc to bằng tên mô tả dài dòng ("dấu kiểm đậm màu trắng Dự báo hàng giờ"). Hãy sử dụng dấu đầu dòng danh sách tiêu chuẩn, đồng thời hạn chế dùng emoji và chỉ dùng khi thực sự mang lại ý nghĩa.

## Các hạng mục kịch bản không kiểm tra được (cần kiểm tra thủ công)
- Nội dung văn bản thay thế (alt text) có thực sự chính xác và hữu ích hay không.
- Độ tương phản màu sắc và chữ viết nằm bên trong hình ảnh (ảnh chụp màn hình có cỡ chữ quá nhỏ, biểu đồ có độ tương phản thấp).
- Thứ tự đọc của các bố cục phức tạp và các khối mã HTML nhúng bên trong Markdown.
- Phụ đề (captions) và bản chép lời (transcripts) cho video và âm thanh.
- Ngôn ngữ giản dị, dễ hiểu: câu văn ngắn gọn, giải thích rõ các thuật ngữ chuyên ngành và đặt tên các phần mục mang tính mô tả cao.
FILE:templates/a11y-review.md
# Báo cáo Đánh giá Khả năng Tiếp cận: <tài liệu hoặc thư mục>

Ngày: <YYYY-MM-DD>
Số tệp đã kiểm tra: <n> (<danh sách hoặc thư mục>)
Đối tượng độc giả: <ai là người đọc các tài liệu này>
Kết quả chạy công cụ kiểm tra: `python3 scripts/check_md_a11y.py <paths>` -> HIGH <n>, WARN <n>, INFO <n>

## Các mục ưu tiên sửa hàng đầu (tác động lớn nhất xếp trước)
1. <nội dung sửa> (<tệp:dòng>)
2. <nội dung sửa>
3. <nội dung sửa>

## Bảng làm việc văn bản thay thế (Alt text worksheet)
| Tệp:dòng | Hình ảnh | Mục đích trong tài liệu | Alt hiện tại | Alt mới | Đã xem ảnh trực tiếp? |
|---|---|---|---|---|---|
| | | cung cấp thông tin / chức năng / trang trí / phức tạp | | | đã xem / bản nháp |

## Tiêu đề (Headings)
- Dàn ý hiện tại: <H1 ... / H2 ... / H4 ...>
- Dàn ý đề xuất: <H1 ... / H2 ... / H3 ...>

## Liên kết (Links)
| Tệp:dòng | Văn bản hiện tại | Văn bản mới | Đích đến |
|---|---|---|---|
| | | | |

## Các phát hiện khác
| Tệp:dòng | Quy tắc | Thay đổi thực hiện |
|---|---|---|
| | | |

## Sau khi sửa lỗi
Chạy lại công cụ kiểm tra: HIGH <n>, WARN <n>, INFO <n> (các mục INFO còn lại được giải thích bên dưới).
- <mục INFO: lý do vì sao có thể chấp nhận giữ nguyên>

## Các hạng mục vẫn cần kiểm tra thủ công
- <ví dụ: độ tương phản của chữ bên trong ảnh chụp màn hình, phụ đề video>
FILE:examples/example-readme-review.md
# Ví dụ: đánh giá khả năng tiếp cận cho tệp README của một dự án

## Yêu cầu từ người dùng
"Tệp README của ứng dụng mã nguồn mở bên tôi có lượng truy cập rất lớn. Một người dùng sử dụng trình đọc màn hình phản ánh rằng tài liệu rất khó theo dõi. Bạn có thể kiểm tra và sửa giúp tôi được không? Ảnh hero là hình ứng dụng chạy trên điện thoại hiển thị dự báo 3 ngày; app-main.png là màn hình chính có nhiệt độ hàng giờ, sức gió và đường ranh giới tuyết rơi; còn chiếc huy hiệu (badge) là trạng thái bản dựng (build status) của chúng tôi."

## Chạy công cụ kiểm tra (trước khi sửa)
```
$ 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.
```
Cú pháp `![](not-an-image.png)` nằm bên trong khối mã bash và ví dụ mã nội tuyến ở dòng cuối cùng đã được công cụ bỏ qua một cách chính xác.

## Báo cáo đánh giá (đã điền theo mẫu, rút gọn)

# Báo cáo Đánh giá Khả năng Tiếp cận: README.md

Đối tượng độc giả: người đi bộ đường dài (hikers) và các cộng tác viên mã nguồn mở, nhiều người tìm đến từ công cụ tìm kiếm.
Kết quả chạy công cụ kiểm tra: HIGH 2, WARN 8, INFO 5.

### Các mục ưu tiên sửa hàng đầu
1. Cung cấp văn bản thay thế (alt text) thực chất cho ảnh hero và huy hiệu trạng thái bản dựng (dòng 3 và 13).
2. Thay thế cụm từ "here" và "click here" bằng văn bản liên kết nêu rõ tên đích đến (dòng 9).
3. Sửa lại dàn ý tiêu đề: chuyển "Configuration" thành H3, chuyển "Features" thành thẻ tiêu đề H3 thực sự, và hạ thẻ H1 thứ hai "Contributing" xuống thành H2.
4. Nêu rõ tên nút Save thay vì chỉ mô tả màu sắc của nút (dòng 17).
5. Thêm tiêu đề cho cột đầu tiên của bảng (dòng 24).

### Bảng làm việc văn bản thay thế (Alt text worksheet)
| Tệp:dòng | Hình ảnh | Mục đích | Alt hiện tại | Alt mới | Đã xem ảnh? |
|---|---|---|---|---|---|
| README.md:3 | hero.png | cung cấp thông tin: giới thiệu sản phẩm | (trống) | Trailhead Weather on a phone, showing a three-day forecast for a mountain pass. | do người dùng mô tả |
| README.md:11 | app-main.png | cung cấp thông tin: màn hình chính hiển thị những gì | screenshot | Main screen with hourly temperature, wind, and snow line for the selected trailhead. | do người dùng mô tả |
| README.md:13 | badge.svg | cung cấp thông tin: trạng thái bản dựng | (không có thuộc tính alt) | Build: passing | do người dùng mô tả |

### Tiêu đề (Headings)
- Trước khi sửa: H1 Trailhead Weather / H2 Install / H4 Configuration / H1 Contributing (cộng thêm một dòng chữ in đậm "Features").
- Sau khi sửa: H1 Trailhead Weather / H2 Install / H3 Configuration / H3 Features / H2 Contributing.

### Liên kết (Links)
| Dòng | Văn bản hiện tại | Văn bản mới | Đích đến |
|---|---|---|---|
| 9 | here | Download the latest release | https://example.org/releases |
| 9 | click here | setup guide | docs/guides/setup.md |
| 28 | docs (URL trần + "docs") | API reference | https://example.org/docs |
| 28 | docs | setup guide | docs/guides/setup.md |

### Các phát hiện khác
| Dòng | Quy tắc | Thay đổi thực hiện |
|---|---|---|
| 5 | all-caps | Chuyển về chữ thường đúng ngữ pháp bên trong phần ghi chú in đậm. |
| 17 | color-only | Đổi thành "Select Save (the green button at the bottom of Settings)". |
| 21-22 | emoji-bullet | Đổi thành các mục danh sách gạch đầu dòng "-" thông thường. |
| 24 | table-empty-header | Đặt tiêu đề cột đầu tiên là "Feature". |

### Tệp README sau khi sửa (trích đoạn)
```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 |
```

### Sau khi sửa lỗi
```
$ python3 scripts/check_md_a11y.py README-fixed.md
Markdown accessibility check: 1 file(s), 3 image(s)
HIGH 0  WARN 0  INFO 0
```

### Các hạng mục vẫn cần kiểm tra thủ công
- Ảnh hero có phần chữ nhỏ hiển thị trên màn hình điện thoại; văn bản thay thế (alt text) đã truyền tải đầy đủ thông điệp cốt lõi nên ở đây như vậy là ổn.
- Nếu sau này tệp README được đưa lên thành một trang trên website tài liệu, hãy kiểm tra để chắc chắn hệ thống tạo trang không tự động chèn thêm một thẻ H1 thứ hai.

## Vì sao đây là một phương án xử lý tốt
- Văn bản thay thế (alt text) mô tả đúng mục đích của từng bức ảnh dựa trên thông tin người dùng cung cấp thay vì tự suy đoán.
- Các liên kết và tiêu đề được chỉnh sửa phù hợp với cách thức điều hướng thực tế của người dùng trình đọc màn hình: duyệt theo danh sách liên kết và sơ đồ dàn ý tiêu đề.
- Bản sửa lỗi được kiểm chứng lại bằng cách chạy công cụ kiểm tra một lần nữa, đồng thời liệt kê trung thực những hạng mục vẫn cần con người kiểm tra thủ công.
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 put meaning in words.")
        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())
PythonDocumentationAccessibility+2
F@f
0
Công cụ Kiểm toán Độ tương phản Màu sắc cho Khả năng Tiếp cận
Kỹ năng

Kiểm toán các cặp màu tiền cảnh/hậu cảnh theo độ tương phản WCAG 2.x (mức AA và AAA cho văn bản thường và văn bản lớn), báo cáo tỷ lệ và kết quả đạt/không đạt, đồng thời lưu ý các rủi ro đối với người mù màu. Sử dụng khi kiểm tra bảng màu giao diện người dùng (UI palette), design token, hoặc hỏi "màu xám trên nền trắng này có dễ đọc không?". Bao gồm công cụ kiểm tra độ tương phản bằng thư viện chuẩn Python có hỗ trợ --self-test.

---
name: accessibility-color-contrast-auditor
description: Kiểm toán các cặp màu tiền cảnh/hậu cảnh theo độ tương phản WCAG 2.x (mức AA và AAA cho văn bản thường và văn bản lớn), báo cáo tỷ lệ và kết quả đạt/không đạt, đồng thời lưu ý các rủi ro đối với người mù màu. Sử dụng khi kiểm tra bảng màu giao diện người dùng (UI palette), design token, hoặc hỏi "màu xám trên nền trắng này có dễ đọc không?". Bao gồm công cụ kiểm tra độ tương phản bằng thư viện chuẩn Python có hỗ trợ --self-test.
---

# Công cụ Kiểm toán Độ tương phản Màu sắc cho Khả năng Tiếp cận

Bạn giúp các nhà thiết kế và kỹ sư frontend kiểm tra xem màu sắc của văn bản và giao diện người dùng (UI) có đáp ứng tỷ lệ tương phản WCAG 2.x hay không. Bạn tính toán độ tương phản độ chói tương đối (relative luminance contrast) cho các cặp màu tiền cảnh/hậu cảnh, đối chiếu kết quả với tiêu chuẩn AA/AAA cho văn bản thường và văn bản lớn, đồng thời gắn cờ cảnh báo các cặp màu chỉ dựa vào sắc độ (hue) để truyền tải ý nghĩa.

## Các tệp trong kỹ năng này

- `scripts/contrast_check.py` - tính toán tỷ lệ tương phản cho các cặp mã màu hex và in ra bảng kết quả đạt/không đạt (chỉ dùng thư viện chuẩn Python 3)
- `references/wcag-contrast.md` - các ngưỡng tiêu chuẩn và quy định thế nào được tính là văn bản lớn
- `references/color-blindness-notes.md` - lý do tại sao các màu trạng thái chỉ phân biệt bằng sắc độ lại gây lỗi với người dùng thực tế
- `templates/audit-table.md` - bảng markdown phục vụ đánh giá thiết kế
- `examples/palette.json` - các cặp token mẫu

## Quy trình làm việc

### 1. Thu thập các cặp màu
Yêu cầu cung cấp các mã màu hex (`#RGB`, `#RRGGBB`, hoặc `#RRGGBBAA` với kênh alpha được bỏ qua trên nền đục). Ghi chú rõ màu nào là văn bản/biểu tượng (fg) và màu nào là bề mặt nền (bg).

### 2. Chạy công cụ kiểm tra
```
python3 scripts/contrast_check.py --fg '#1a1a1a' --bg '#ffffff'
python3 scripts/contrast_check.py --palette examples/palette.json
python3 scripts/contrast_check.py --self-test
```

### 3. Diễn giải kết quả
- Văn bản thường mức AA cần **4.5:1**; mức AAA cần **7:1**.
- Văn bản lớn (18pt+ hoặc 14pt+ in đậm) mức AA cần **3:1**; mức AAA cần **4.5:1**.
- Các thành phần giao diện người dùng phi văn bản / đối tượng đồ họa mang ý nghĩa cần đạt **3:1** so với các màu liền kề (WCAG 2.1 SC 1.4.11).

### 4. Trả lời
Đối với mỗi cặp màu: nêu tỷ lệ (làm tròn một chữ số thập phân), kết quả AA/AAA cho văn bản thường, AA/AAA cho văn bản lớn, và gợi ý khắc phục ngắn gọn (làm tối màu fg, làm sáng màu bg, hoặc tăng độ đậm/kích thước chữ). Nhắc đến vấn đề mù màu nếu trạng thái chỉ phụ thuộc vào sự khác biệt giữa màu đỏ và màu xanh lá.

## Quy tắc

- Sử dụng công thức độ chói tương đối của WCAG; không ước lượng cảm tính bằng mắt thường ("eyeballing").
- Bỏ qua các tuyên bố tiếp thị về "chế độ hỗ trợ tiếp cận" trừ khi có hiển thị tỷ lệ tương phản cụ thể.
- Nếu có pha trộn kênh alpha (alpha blending), hãy yêu cầu mã màu hex thực tế sau khi đã làm phẳng trên nền thật.
FILE:references/wcag-contrast.md
# Các ngưỡng tương phản WCAG (WCAG 2.1 / 2.2)

| Nội dung | AA | AAA |
| --- | --- | --- |
| Văn bản thường | 4.5:1 | 7:1 |
| Văn bản lớn | 3:1 | 4.5:1 |
| Thành phần UI & đối tượng đồ họa (1.4.11) | 3:1 | — |

## Văn bản lớn

Văn bản lớn là văn bản có kích thước tối thiểu **18 CSS pt** (thường là 24px) đối với chữ thường, hoặc **14 CSS pt** (thường là 18.66px) đối với chữ in đậm.

## Độ chói tương đối (Relative luminance)

Đối với kênh màu sRGB C_srgb trong khoảng 0..1:

- nếu C_srgb <= 0.04045: C = C_srgb / 12.92
- ngược lại: C = ((C_srgb + 0.055) / 1.055) ^ 2.4

L = 0.2126 R + 0.7152 G + 0.0722 B

Tỷ lệ tương phản = (L_lighter + 0.05) / (L_darker + 0.05)

## Ghi chú

- Văn bản giữ chỗ (placeholder) / văn bản bị vô hiệu hóa (disabled) được miễn trừ khỏi các tiêu chí thành công (SC) về độ tương phản văn bản nhưng vẫn nên đảm bảo sự rõ ràng.
- Các chỉ báo tiêu điểm (focus indicators) cần đạt tỷ lệ 3:1 so với các màu liền kề (2.4.11 / 2.4.13 tùy theo cấp độ).
FILE:references/color-blindness-notes.md
# Ghi chú về chứng mù màu đối với bảng màu

Chỉ riêng tỷ lệ tương phản là không đủ khi ý nghĩa chỉ được truyền tải thông qua sắc độ (hue).

## Các lỗi thường gặp

- **Trạng thái đỏ so với xanh lá** không kèm biểu tượng hoặc nhãn văn bản: người mắc chứng mù màu lục/đỏ (deuteranomaly/protanomaly) có thể không phân biệt được chúng.
- **Các chuỗi dữ liệu biểu đồ** chỉ phân biệt bằng màu sắc: hãy thêm họa tiết, hình dạng, hoặc nhãn trực tiếp.
- **Màu liên kết (link)** có cùng độ sáng với văn bản thân bài: đường gạch chân hoặc độ đậm của chữ phải được giữ nguyên khi loại bỏ yếu tố màu sắc.
- **Viền báo lỗi** chỉ có sắc đỏ nhạt: hãy đảm bảo tỷ lệ 3:1 so với màu nền và kèm theo văn bản/biểu tượng.

## Các mẫu thiết kế an toàn hơn

1. Kết hợp màu sắc với văn bản ("Error", "Success") hoặc biểu tượng.
2. Sử dụng sự chênh lệch về độ sáng (lightness), chứ không chỉ dịch chuyển sắc độ (hue).
3. Kiểm tra bằng ảnh chụp màn hình giao diện ở chế độ thang độ xám (grayscale).
4. Tránh chỉ dùng mỗi màu đỏ để biểu thị trạng thái trường bắt buộc nhập.

## Mẹo thân thiện với người mắc chứng mù màu lục (Deuteranopia)

Ưu tiên sử dụng các cặp màu xanh dương/cam hoặc xanh dương/nâu thay vì đỏ/xanh lá cho dữ liệu phân loại khi bạn bắt buộc phải dùng hai sắc độ.
FILE:templates/audit-table.md
# Kiểm toán độ tương phản

- **Sản phẩm / màn hình**:
- **Ngày**:
- **Tiêu chuẩn**: WCAG 2.2 AA (mặc định) / AAA ở những nơi được ghi chú

| Token / mục đích sử dụng | FG | BG | Tỷ lệ | AA thường | AAA thường | AA lớn | Ghi chú / cách sửa |
| --- | --- | --- | --- | --- | --- | --- | --- |
| văn bản thân bài | `#1a1a1a` | `#ffffff` | 16.1 | pass | pass | pass | |
| chú thích mờ | `#9ca3af` | `#ffffff` | 2.5 | fail | fail | fail | Làm tối xuống khoảng ~`#6b7280` |

## Quyết định

- [ ] Cập nhật các token
- [ ] Chấp nhận chỉ sử dụng cho văn bản lớn
- [ ] Thêm dấu hiệu nhận biết phi màu sắc
FILE:examples/palette.json
{
  "pairs": [
    {"name": "body_on_white", "fg": "#1a1a1a", "bg": "#ffffff"},
    {"name": "muted_on_white", "fg": "#9ca3af", "bg": "#ffffff"},
    {"name": "link_on_white", "fg": "#2563eb", "bg": "#ffffff"},
    {"name": "button_label", "fg": "#ffffff", "bg": "#2563eb"},
    {"name": "error_on_white", "fg": "#dc2626", "bg": "#ffffff"},
    {"name": "white_on_light_gray", "fg": "#ffffff", "bg": "#e5e7eb"}
  ]
}
FILE:scripts/contrast_check.py
#!/usr/bin/env python3
"""WCAG contrast ratio checker for hex fg/bg pairs.

Usage:
    python3 contrast_check.py --fg '#1a1a1a' --bg '#ffffff'
    python3 contrast_check.py --palette palette.json
    python3 contrast_check.py --self-test

Python 3 standard library only.
"""
from __future__ import annotations

import argparse
import json
import math
import sys
from typing import Dict, List, Tuple


def parse_hex(color: str) -> Tuple[float, float, float]:
    c = color.strip().lstrip("#")
    if len(c) == 3:
        c = "".join(ch * 2 for ch in c)
    if len(c) == 8:
        c = c[:6]  # ignore alpha
    if len(c) != 6:
        raise ValueError(f"Bad hex color: {color!r}")
    r = int(c[0:2], 16) / 255.0
    g = int(c[2:4], 16) / 255.0
    b = int(c[4:6], 16) / 255.0
    return r, g, b


def channel(c: float) -> float:
    if c <= 0.04045:
        return c / 12.92
    return ((c + 0.055) / 1.055) ** 2.4


def relative_luminance(rgb: Tuple[float, float, float]) -> float:
    r, g, b = rgb
    return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b)


def contrast_ratio(fg: str, bg: str) -> float:
    l1 = relative_luminance(parse_hex(fg))
    l2 = relative_luminance(parse_hex(bg))
    lighter, darker = max(l1, l2), min(l1, l2)
    return (lighter + 0.05) / (darker + 0.05)


def evaluate(ratio: float) -> Dict[str, bool]:
    return {
        "aa_normal": ratio >= 4.5,
        "aaa_normal": ratio >= 7.0,
        "aa_large": ratio >= 3.0,
        "aaa_large": ratio >= 4.5,
        "ui_3_1": ratio >= 3.0,
    }


def format_row(name: str, fg: str, bg: str) -> str:
    ratio = contrast_ratio(fg, bg)
    ev = evaluate(ratio)
    flags = " ".join(
        f"{k}={'PASS' if v else 'FAIL'}"
        for k, v in ev.items()
        if k in ("aa_normal", "aaa_normal", "aa_large")
    )
    return f"{name}: {fg} on {bg} -> {ratio:.2f}:1  {flags}"


def self_test() -> int:
    cases = [
        ("black_white", contrast_ratio("#000000", "#ffffff"), 21.0),
        ("white_black", contrast_ratio("#ffffff", "#000000"), 21.0),
        ("body", contrast_ratio("#1a1a1a", "#ffffff") > 12.0, True),
        ("muted_fail_aa", evaluate(contrast_ratio("#9ca3af", "#ffffff"))["aa_normal"], False),
        ("link_aa", evaluate(contrast_ratio("#2563eb", "#ffffff"))["aa_normal"], True),
        ("same_color", abs(contrast_ratio("#ffffff", "#ffffff") - 1.0) < 1e-9, True),
        ("short_hex", abs(contrast_ratio("#000", "#fff") - 21.0) < 1e-9, True),
        ("parse3", parse_hex("#abc") == parse_hex("#aabbcc"), True),
        ("ui_pass", evaluate(3.0)["ui_3_1"], True),
        ("aaa_border", evaluate(7.0)["aaa_normal"], True),
        ("just_under_aa", evaluate(4.49)["aa_normal"], False),
    ]
    failed = 0
    for name, got, expected in cases:
        if isinstance(expected, float):
            ok = abs(float(got) - expected) < 0.05
        else:
            ok = got == expected
        failed += not ok
        print(f"{'PASS' if ok else 'FAIL'}  {name}: got={got!r} expected={expected!r}")
    print(f"\n{len(cases) - failed}/{len(cases)} passed")
    return 1 if failed else 0


def main(argv=None) -> int:
    p = argparse.ArgumentParser(description="Check WCAG contrast for fg/bg hex pairs.")
    p.add_argument("--fg")
    p.add_argument("--bg")
    p.add_argument("--palette", help="JSON file with {pairs:[{name,fg,bg}]}")
    p.add_argument("--json", action="store_true")
    p.add_argument("--self-test", action="store_true")
    a = p.parse_args(argv)
    if a.self_test:
        return self_test()
    rows = []
    if a.palette:
        data = json.load(open(a.palette, encoding="utf-8"))
        for pair in data.get("pairs", []):
            rows.append(pair)
    elif a.fg and a.bg:
        rows.append({"name": "pair", "fg": a.fg, "bg": a.bg})
    else:
        p.error("provide --fg and --bg, or --palette, or --self-test")
    out = []
    any_fail = False
    for row in rows:
        ratio = contrast_ratio(row["fg"], row["bg"])
        ev = evaluate(ratio)
        if not ev["aa_normal"]:
            any_fail = True
        item = {"name": row.get("name", "pair"), "fg": row["fg"], "bg": row["bg"], "ratio": round(ratio, 2), **ev}
        out.append(item)
        if not a.json:
            print(format_row(item["name"], item["fg"], item["bg"]))
    if a.json:
        print(json.dumps(out, indent=2))
    return 1 if any_fail else 0


if __name__ == "__main__":
    sys.exit(main())
Best PracticesFrontendPython+4
F@f
0