""" KWCAG (Korean Web Content Accessibility Guidelines) mapping module. Provides bidirectional mapping between KWCAG 2.1/2.2 and WCAG criteria, enabling axe-core results to be re-interpreted under Korean accessibility standards. KWCAG 2.2 contains 33 inspection items across 4 principles. KWCAG 2.1 contains 24 inspection items (a subset of 2.2). KWCAG 2.2 aligns with WCAG 2.1 Level AA and includes additional items. """ import logging from typing import Optional logger = logging.getLogger(__name__) # ============================================================================= # KWCAG 2.2 Full Mapping (33 items) # ============================================================================= # Key: KWCAG criterion number # Value: { # "wcag_criteria": [list of corresponding WCAG criterion numbers], # "name": Korean name of the KWCAG item, # "principle": KWCAG principle name, # "kwcag_21": True if this item also exists in KWCAG 2.1, # } KWCAG_TO_WCAG: dict[str, dict] = { # ======================================== # 원칙 1. 인식의 용이성 # ======================================== "1.1.1": { "wcag_criteria": ["1.1.1"], "name": "적절한 대체 텍스트", "principle": "인식의 용이성", "kwcag_21": True, }, "1.2.1": { "wcag_criteria": ["1.2.2", "1.2.4"], "name": "자막 제공", "principle": "인식의 용이성", "kwcag_21": True, }, "1.3.1": { "wcag_criteria": ["1.4.1"], "name": "색에 무관한 콘텐츠 인식", "principle": "인식의 용이성", "kwcag_21": True, }, "1.4.1": { "wcag_criteria": ["1.3.3"], "name": "명확한 지시 사항", "principle": "인식의 용이성", "kwcag_21": True, }, "1.5.1": { "wcag_criteria": ["1.4.3"], "name": "텍스트 콘텐츠의 명도 대비", "principle": "인식의 용이성", "kwcag_21": True, }, "1.6.1": { "wcag_criteria": ["1.4.2"], "name": "자동 재생 금지", "principle": "인식의 용이성", "kwcag_21": True, }, "1.7.1": { "wcag_criteria": ["1.3.1"], "name": "콘텐츠 간의 구분", "principle": "인식의 용이성", "kwcag_21": True, }, # ======================================== # 원칙 2. 운용의 용이성 # ======================================== "2.1.1": { "wcag_criteria": ["2.1.1", "2.1.2"], "name": "키보드 사용 보장", "principle": "운용의 용이성", "kwcag_21": True, }, "2.2.1": { "wcag_criteria": ["2.4.3", "2.4.7"], "name": "초점 이동과 표시", "principle": "운용의 용이성", "kwcag_21": True, }, "2.3.1": { "wcag_criteria": ["2.5.1"], "name": "조작 가능", "principle": "운용의 용이성", "kwcag_21": False, }, "2.4.1": { "wcag_criteria": ["2.2.1"], "name": "응답시간 조절", "principle": "운용의 용이성", "kwcag_21": True, }, "2.5.1": { "wcag_criteria": ["2.2.2"], "name": "정지 기능 제공", "principle": "운용의 용이성", "kwcag_21": True, }, "2.6.1": { "wcag_criteria": ["2.3.1"], "name": "깜빡임과 번쩍임 사용 제한", "principle": "운용의 용이성", "kwcag_21": True, }, "2.7.1": { "wcag_criteria": ["2.4.1"], "name": "반복 영역 건너뛰기", "principle": "운용의 용이성", "kwcag_21": True, }, "2.8.1": { "wcag_criteria": ["2.4.2", "2.4.5", "2.4.6", "2.4.8"], "name": "쉬운 내비게이션", "principle": "운용의 용이성", "kwcag_21": True, }, # ======================================== # 원칙 3. 이해의 용이성 # ======================================== "3.1.1": { "wcag_criteria": ["3.1.1"], "name": "기본 언어 표시", "principle": "이해의 용이성", "kwcag_21": True, }, "3.2.1": { "wcag_criteria": ["3.2.1", "3.2.2"], "name": "사용자 요구에 따른 실행", "principle": "이해의 용이성", "kwcag_21": True, }, "3.3.1": { "wcag_criteria": ["1.3.2"], "name": "콘텐츠의 선형구조", "principle": "이해의 용이성", "kwcag_21": True, }, "3.4.1": { "wcag_criteria": ["1.3.1"], "name": "표의 구성", "principle": "이해의 용이성", "kwcag_21": True, }, "3.5.1": { "wcag_criteria": ["1.3.1", "3.3.2"], "name": "레이블 제공", "principle": "이해의 용이성", "kwcag_21": True, }, "3.6.1": { "wcag_criteria": ["3.3.1", "3.3.3"], "name": "오류 정정", "principle": "이해의 용이성", "kwcag_21": True, }, # ======================================== # 원칙 4. 견고성 # ======================================== "4.1.1": { "wcag_criteria": ["4.1.1"], "name": "마크업 오류 방지", "principle": "견고성", "kwcag_21": True, }, "4.2.1": { "wcag_criteria": ["4.1.2"], "name": "웹 애플리케이션 접근성 준수", "principle": "견고성", "kwcag_21": True, }, # ======================================== # KWCAG 2.2 추가 항목 (KWCAG 2.1에 없는 항목) # ======================================== "1.1.2": { "wcag_criteria": ["1.1.1"], "name": "대체 콘텐츠의 동등성", "principle": "인식의 용이성", "kwcag_21": False, }, "1.2.2": { "wcag_criteria": ["1.2.1", "1.2.3"], "name": "음성 및 영상 대체 수단", "principle": "인식의 용이성", "kwcag_21": False, }, "1.5.2": { "wcag_criteria": ["1.4.11"], "name": "비텍스트 콘텐츠의 명도 대비", "principle": "인식의 용이성", "kwcag_21": False, }, "2.1.2": { "wcag_criteria": ["2.1.4"], "name": "키보드 단축키", "principle": "운용의 용이성", "kwcag_21": False, }, "2.2.2": { "wcag_criteria": ["2.4.11"], "name": "초점 감춤 방지", "principle": "운용의 용이성", "kwcag_21": False, }, "2.3.2": { "wcag_criteria": ["2.5.1", "2.5.2"], "name": "포인터 입력의 취소", "principle": "운용의 용이성", "kwcag_21": False, }, "2.3.3": { "wcag_criteria": ["2.5.8"], "name": "포인터 대상 크기", "principle": "운용의 용이성", "kwcag_21": False, }, "2.8.2": { "wcag_criteria": ["2.4.4"], "name": "링크 텍스트", "principle": "운용의 용이성", "kwcag_21": False, }, "3.2.2": { "wcag_criteria": ["3.2.6"], "name": "일관된 도움 제공", "principle": "이해의 용이성", "kwcag_21": False, }, "3.6.2": { "wcag_criteria": ["3.3.8"], "name": "접근 가능한 인증", "principle": "이해의 용이성", "kwcag_21": False, }, "3.6.3": { "wcag_criteria": ["3.3.7"], "name": "중복 입력 방지", "principle": "이해의 용이성", "kwcag_21": False, }, } # ============================================================================= # Reverse mapping: WCAG criterion -> KWCAG criterion(s) # Built at module load time for O(1) lookup during result conversion. # ============================================================================= def _build_wcag_to_kwcag_map() -> dict[str, list[str]]: """Build reverse mapping from WCAG criterion to KWCAG criterion(s).""" reverse_map: dict[str, list[str]] = {} for kwcag_id, info in KWCAG_TO_WCAG.items(): for wcag_id in info["wcag_criteria"]: if wcag_id not in reverse_map: reverse_map[wcag_id] = [] reverse_map[wcag_id].append(kwcag_id) return reverse_map WCAG_TO_KWCAG: dict[str, list[str]] = _build_wcag_to_kwcag_map() # ============================================================================= # axe-core tag sets for KWCAG versions # ============================================================================= # KWCAG 2.2 -> WCAG 2.1 AA level + selected 2.2 AA rules # KWCAG 2.1 -> WCAG 2.0 AA level + selected 2.1 AA rules _KWCAG_AXE_TAGS: dict[str, list[str]] = { "kwcag_2.1": ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"], "kwcag_2.2": ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"], } def get_kwcag_axe_tags(version: str) -> list[str]: """ Get axe-core tag list for a KWCAG version. Args: version: KWCAG version string, e.g. "kwcag_2.1" or "kwcag_2.2" Returns: List of axe-core tags to run. """ return _KWCAG_AXE_TAGS.get(version, _KWCAG_AXE_TAGS["kwcag_2.2"]) # ============================================================================= # Utility functions for result conversion # ============================================================================= def convert_wcag_issue_to_kwcag( issue_dict: dict, kwcag_version: str = "kwcag_2.2", ) -> dict: """ Convert a single issue's wcag_criterion field from WCAG to KWCAG numbering. If the WCAG criterion maps to a KWCAG item, the issue is updated with: - kwcag_criterion: The KWCAG criterion number (e.g. "1.1.1") - kwcag_name: The Korean name of the KWCAG item - kwcag_principle: The KWCAG principle name The original wcag_criterion field is preserved. If no mapping exists, the issue is returned unchanged. Args: issue_dict: Dictionary representing a single inspection issue. kwcag_version: "kwcag_2.1" or "kwcag_2.2" Returns: The issue_dict with KWCAG fields added (mutated in place and returned). """ wcag_criterion = issue_dict.get("wcag_criterion") if not wcag_criterion: return issue_dict kwcag_ids = WCAG_TO_KWCAG.get(wcag_criterion, []) if not kwcag_ids: return issue_dict # Filter to items that exist in the requested KWCAG version is_21 = kwcag_version == "kwcag_2.1" valid_ids = [] for kid in kwcag_ids: info = KWCAG_TO_WCAG.get(kid) if info is None: continue if is_21 and not info.get("kwcag_21", False): continue valid_ids.append(kid) if not valid_ids: return issue_dict # Use the first matching KWCAG criterion primary_kwcag_id = valid_ids[0] info = KWCAG_TO_WCAG[primary_kwcag_id] issue_dict["kwcag_criterion"] = primary_kwcag_id issue_dict["kwcag_name"] = info["name"] issue_dict["kwcag_principle"] = info["principle"] # If multiple KWCAG items match, include all if len(valid_ids) > 1: issue_dict["kwcag_criteria_all"] = valid_ids return issue_dict def get_kwcag_label(version: str) -> str: """ Get human-readable label for a KWCAG version. Args: version: "kwcag_2.1" or "kwcag_2.2" Returns: Korean label string, e.g. "KWCAG 2.2 (한국형 웹 콘텐츠 접근성 지침 2.2)" """ labels = { "kwcag_2.1": "KWCAG 2.1 (한국형 웹 콘텐츠 접근성 지침 2.1)", "kwcag_2.2": "KWCAG 2.2 (한국형 웹 콘텐츠 접근성 지침 2.2)", } return labels.get(version, labels["kwcag_2.2"]) def get_kwcag_item_count(version: str) -> int: """ Get the total number of inspection items for a KWCAG version. Args: version: "kwcag_2.1" or "kwcag_2.2" Returns: Number of items: 24 for KWCAG 2.1, 33 for KWCAG 2.2 """ if version == "kwcag_2.1": return sum(1 for info in KWCAG_TO_WCAG.values() if info.get("kwcag_21", False)) return len(KWCAG_TO_WCAG)