a11y-moda 0.1.0a1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- a11y_moda-0.1.0a1/LICENSE +21 -0
- a11y_moda-0.1.0a1/PKG-INFO +177 -0
- a11y_moda-0.1.0a1/README.md +139 -0
- a11y_moda-0.1.0a1/pyproject.toml +54 -0
- a11y_moda-0.1.0a1/setup.cfg +4 -0
- a11y_moda-0.1.0a1/src/a11y_moda/__init__.py +3 -0
- a11y_moda-0.1.0a1/src/a11y_moda/_security.py +92 -0
- a11y_moda-0.1.0a1/src/a11y_moda/cli.py +311 -0
- a11y_moda-0.1.0a1/src/a11y_moda/crawler.py +246 -0
- a11y_moda-0.1.0a1/src/a11y_moda/css_utils.py +108 -0
- a11y_moda-0.1.0a1/src/a11y_moda/fetcher.py +103 -0
- a11y_moda-0.1.0a1/src/a11y_moda/llm.py +312 -0
- a11y_moda-0.1.0a1/src/a11y_moda/models.py +72 -0
- a11y_moda-0.1.0a1/src/a11y_moda/report/__init__.py +10 -0
- a11y_moda-0.1.0a1/src/a11y_moda/report/_aggregate.py +73 -0
- a11y_moda-0.1.0a1/src/a11y_moda/report/_common.py +36 -0
- a11y_moda-0.1.0a1/src/a11y_moda/report/_html.py +202 -0
- a11y_moda-0.1.0a1/src/a11y_moda/report/_markdown.py +154 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/__init__.py +20 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/_lib/__init__.py +1 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/_lib/aaa.py +8 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/_lib/contrast_focus.py +28 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/_lib/css.py +31 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/_lib/extension_aria_status.py +9 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/_lib/extension_keyboard.py +17 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/_lib/extension_media.py +8 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/_lib/extension_misc.py +11 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/_lib/extension_navigation.py +8 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/_lib/extension_presentation.py +9 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/_lib/extension_responsive.py +9 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/_lib/forms.py +71 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/_lib/img_alt.py +19 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/_lib/lang.py +8 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/_lib/llm_common.py +73 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/_lib/llm_consistency.py +9 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/_lib/llm_forms.py +10 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/_lib/llm_headings.py +9 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/_lib/llm_images.py +11 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/_lib/llm_links.py +9 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/_lib/llm_structure.py +9 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/_lib/nav_links.py +22 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/_lib/origin.py +80 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/_lib/page_meta.py +8 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/_lib/structure.py +23 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/_lib/vision_rules.py +34 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/base.py +114 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/__init__.py +1 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/aria/AR2410300E.py +38 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/aria/AR2410301E.py +43 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/aria/AR2410302E.py +43 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/aria/AR3130600E.py +38 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/aria/FA2410303E.py +40 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/aria/__init__.py +1 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/consistency/GN1320200E.py +42 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/consistency/GN2320300E.py +33 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/consistency/GN2320400E.py +33 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/consistency/__init__.py +1 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/contrast/GN2140300E.py +38 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/contrast/GN3140600E.py +43 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/contrast/__init__.py +1 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/css/CS2140401C.py +53 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/css/CS3140801C.py +48 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/css/CS3140802C.py +26 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/css/__init__.py +1 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/focus/CS1140101E.py +53 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/focus/CS2240700E.py +30 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/focus/FA2141104E.py +31 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/focus/GN1240301E.py +33 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/focus/__init__.py +1 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/forms/GN1110111E.py +79 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/forms/GN1330100E.py +53 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/forms/GN1330101E.py +74 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/forms/GN1330102E.py +51 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/forms/GN1330200E.py +45 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/forms/GN1330201E.py +34 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/forms/GN1330202E.py +58 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/forms/GN1330203E.py +41 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/forms/GN1330204E.py +28 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/forms/GN1330205E.py +38 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/forms/GN2240601E.py +61 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/forms/GN2330300E.py +31 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/forms/GN3330602E.py +80 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/forms/HM1130103C.py +43 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/forms/HM1130103C_1.py +48 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/forms/HM1130104C.py +53 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/forms/HM2130500E.py +52 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/forms/HM3330500C.py +25 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/forms/__init__.py +1 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/headings/GN2240600E.py +58 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/headings/HM1130100C.py +78 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/headings/HM1130104E.py +48 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/headings/HM3241000C.py +27 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/headings/__init__.py +1 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/images/HM1110100C.py +45 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/images/HM1110100E.py +63 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/images/HM1110101C.py +41 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/images/HM1110101E.py +54 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/images/HM1110102E.py +45 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/images/HM1110103E.py +51 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/images/HM1110104C.py +39 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/images/HM1110104E.py +44 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/images/HM1110105C.py +49 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/images/HM1110105E.py +46 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/images/HM1110106C.py +33 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/images/HM1110106E.py +45 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/images/HM1110108E.py +45 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/images/HM1110112E.py +51 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/images/__init__.py +1 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/keyboard/FA1250202E.py +29 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/keyboard/GN1210100E.py +37 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/keyboard/GN1210101E.py +71 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/keyboard/GN1210200E.py +30 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/keyboard/GN1220200E.py +37 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/keyboard/GN1250101E.py +32 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/keyboard/GN1250201E.py +32 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/keyboard/GN1320100E.py +34 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/keyboard/__init__.py +1 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/lang/HM1130200C.py +37 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/lang/HM2310200C.py +45 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/lang/__init__.py +1 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/links/GN1240401E.py +64 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/links/HM1240400C.py +38 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/links/HM1240400E.py +47 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/links/HM1240401C.py +63 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/links/HM1240402E.py +41 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/links/HM1240403E.py +59 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/links/HM1240404E.py +44 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/links/HM3240900C.py +50 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/links/__init__.py +1 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/media/GN1140200E.py +33 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/media/GN3120600.py +42 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/media/__init__.py +1 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/meta/HM1240200C.py +37 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/meta/HM1240200E.py +49 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/meta/HM1310100C.py +29 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/meta/__init__.py +1 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/misc/GN1320202E.py +40 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/misc/__init__.py +1 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/navigation/GN1240100E.py +34 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/navigation/GN1240101E.py +32 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/navigation/GN1240102E.py +29 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/navigation/GN1240103E.py +30 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/navigation/GN1240104E.py +32 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/navigation/HM1240102C.py +29 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/navigation/HM3240800E.py +65 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/navigation/__init__.py +1 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/presentation/CS1110113E.py +30 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/presentation/CS1110114E.py +36 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/presentation/CS1130103E.py +33 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/presentation/CS1130202E.py +29 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/presentation/GN1130201E.py +28 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/presentation/__init__.py +1 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/responsive/CS2140500E.py +29 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/responsive/CS2141000E.py +38 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/responsive/CS2141001E.py +27 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/responsive/CS2141002E.py +29 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/responsive/CS2141003E.py +30 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/responsive/CS2141005E.py +35 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/responsive/CS2141006E.py +30 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/responsive/CS2141007E.py +35 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/responsive/CS2141200E.py +29 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/responsive/CS2141201E.py +26 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/responsive/CS2141202E.py +26 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/responsive/CS2141203E.py +26 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/responsive/CS2141204E.py +34 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/responsive/FA2130401E.py +34 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/responsive/FA2130402E.py +26 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/responsive/GN2140400E.py +30 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/responsive/GN2140401E.py +27 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/responsive/GN2141100E.py +38 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/responsive/GN2141101E.py +53 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/responsive/GN2141102E.py +34 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/responsive/GN2141103E.py +26 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/responsive/SC2141004E.py +34 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/responsive/__init__.py +1 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/semantic/HM1410200C.py +81 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/semantic/HM1410201C.py +36 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/semantic/__init__.py +1 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/structure/GN1130200E.py +43 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/structure/HM1130105E.py +23 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/structure/__init__.py +1 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/tables/HM1130101C.py +36 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/tables/HM1130102C.py +65 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/tables/HM1130107E.py +31 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/tables/HM1130108E.py +51 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/tables/HM1130109E.py +30 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/tables/HM1130110E.py +39 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/tables/__init__.py +1 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/vision/CS1110113E_V.py +47 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/vision/CS1130203E.py +48 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/vision/GN1130102E.py +45 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/vision/GN1130300E.py +49 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/vision/GN1140100E.py +58 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/vision/GN1140102E.py +44 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/vision/GN1240500E.py +44 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/vision/GN2140304E.py +50 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/codes/vision/__init__.py +1 -0
- a11y_moda-0.1.0a1/src/a11y_moda/rules/helpers.py +40 -0
- a11y_moda-0.1.0a1/src/a11y_moda/scanner.py +203 -0
- a11y_moda-0.1.0a1/src/a11y_moda/tools/__init__.py +0 -0
- a11y_moda-0.1.0a1/src/a11y_moda/tools/_session.py +71 -0
- a11y_moda-0.1.0a1/src/a11y_moda/tools/contrast.py +226 -0
- a11y_moda-0.1.0a1/src/a11y_moda/tools/form_probe.py +302 -0
- a11y_moda-0.1.0a1/src/a11y_moda/tools/tab_walk.py +77 -0
- a11y_moda-0.1.0a1/src/a11y_moda.egg-info/PKG-INFO +177 -0
- a11y_moda-0.1.0a1/src/a11y_moda.egg-info/SOURCES.txt +208 -0
- a11y_moda-0.1.0a1/src/a11y_moda.egg-info/dependency_links.txt +1 -0
- a11y_moda-0.1.0a1/src/a11y_moda.egg-info/entry_points.txt +2 -0
- a11y_moda-0.1.0a1/src/a11y_moda.egg-info/requires.txt +9 -0
- a11y_moda-0.1.0a1/src/a11y_moda.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 睞特股份有限公司
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: a11y-moda
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Taiwan MODA accessibility CLI · WCAG A/AA/AAA · zh-TW · Freego complement
|
|
5
|
+
Author-email: 睞特股份有限公司 <maple@light-design.com.tw>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/light-design-tw/a11y-moda
|
|
8
|
+
Project-URL: Repository, https://github.com/light-design-tw/a11y-moda
|
|
9
|
+
Project-URL: Issues, https://github.com/light-design-tw/a11y-moda/issues
|
|
10
|
+
Project-URL: Documentation, https://github.com/light-design-tw/a11y-moda#readme
|
|
11
|
+
Project-URL: Changelog, https://github.com/light-design-tw/a11y-moda/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: accessibility,wcag,a11y,moda,taiwan,freego,audit,scanner
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Natural Language :: Chinese (Traditional)
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Site Management
|
|
23
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
24
|
+
Classifier: Topic :: Software Development :: Testing
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
29
|
+
Requires-Dist: beautifulsoup4<5.0,>=4.12
|
|
30
|
+
Requires-Dist: lxml<7.0,>=6.1
|
|
31
|
+
Requires-Dist: click<9.0,>=8.1
|
|
32
|
+
Requires-Dist: playwright<2.0,>=1.59
|
|
33
|
+
Requires-Dist: wcag-contrast-ratio<1.0,>=0.9
|
|
34
|
+
Requires-Dist: tinycss2<2.0,>=1.3
|
|
35
|
+
Requires-Dist: defusedxml<1.0,>=0.7
|
|
36
|
+
Requires-Dist: python-dotenv<2.0,>=1.2.2
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# a11y-moda
|
|
40
|
+
|
|
41
|
+
> 台灣 MODA 無障礙標章自評用 Python CLI · WCAG A / AA / AAA · zh-TW 報告
|
|
42
|
+
|
|
43
|
+
**繁體中文** · [English](https://github.com/light-design-tw/a11y-moda/blob/main/README.en.md)
|
|
44
|
+
|
|
45
|
+
> ⚠️ **非官方社群工具。** 與數位發展部 (MODA / 數位發展部) 無從屬關係,不替代官方 [Freego](https://accessibility.moda.gov.tw/) 與正式審查流程。本工具僅為開發者便利之用。
|
|
46
|
+
|
|
47
|
+
## 為什麼做這個
|
|
48
|
+
|
|
49
|
+
MODA 官方工具 [Freego](https://accessibility.moda.gov.tw/) 是 Java GUI,沒有 CLI、Docker、API 介面。`a11y-moda` 補這個缺口,給 **CI/CD pipeline** 跟 **AI 協作開發** 用。實作 MODA 公布的規則編碼 (HM / GN / CS / AR / FA / SC),每筆 issue 標注對應 MODA rule_id 跟 WCAG 2.1 success criterion。
|
|
50
|
+
|
|
51
|
+
人工判斷類規則 (E codes) 接 OpenAI 相容 API。OpenAI、Anthropic、OpenRouter、Ollama、vLLM、LM Studio、llama.cpp server — 任何吐 `/v1/chat/completions` 的端點都接得起來。
|
|
52
|
+
|
|
53
|
+
## 功能
|
|
54
|
+
|
|
55
|
+
- 靜態掃描 (httpx + BeautifulSoup) 或渲染掃描 (Playwright / headless Chromium,給 SPA 用)
|
|
56
|
+
- A / AA / AAA 等級過濾
|
|
57
|
+
- 全站爬取:先 sitemap.xml,找不到就 BFS
|
|
58
|
+
- 輸出:JSON / Markdown / HTML (HTML 一律渲染依規則 / 依 WCAG / 依 URL 三種 view)
|
|
59
|
+
- LLM 判斷結果存本地 cache — 重跑安全,只有改動的規則重打模型
|
|
60
|
+
- 視覺模型 (VLM) 從截圖驗證版面 / 圖片類規則
|
|
61
|
+
- `--freego-compat` 對齊官方工具回報格式,便於交叉比對
|
|
62
|
+
|
|
63
|
+
## MODA AAA 自評涵蓋率 (20/20)
|
|
64
|
+
|
|
65
|
+
實作 MODA AAA 自評表全部 20 題。官方工具對這些 E (人工判斷) 規則的覆蓋率是 **0** — 送件人要逐題手動勾選。本工具自動化 **18 / 20**,剩下 **2** 以 informative caveat 標記,提供人工複查線索。
|
|
66
|
+
|
|
67
|
+
機制拆解:
|
|
68
|
+
|
|
69
|
+
| 機制 | 數量 | 規則 |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| **純 DOM** (無外部依賴,毫秒級) | 9 | Q2 GN1110111E (CAPTCHA alt) · Q3 GN3120600 (影片偵測 + caveat) · Q6 AR3130600E (landmark) · Q7 HM1130110E (複雜表格) · Q8 GN1210101E (鍵盤可達) · Q10 GN1240100E (skip link) · Q13 HM3240800E (麵包屑) · Q14 CS2141204E (em 單位) · Q18 HM2130500E (autocomplete) |
|
|
72
|
+
| **LLM (文字)** — OpenAI 相容 | 5 | Q1 HM1110103E (長文 alt) · Q4 HM1130104E (標題巢狀) · Q5 GN2240600E (描述性標題) · Q9 HM1240402E (圖連結文字) · Q17 GN1330201E (必填欄位標示) |
|
|
73
|
+
| **VLM (視覺)** — 多模態 | 1 | Q11 GN1240500E (從首頁截圖偵測網站地圖) |
|
|
74
|
+
| **瀏覽器 probe** (Playwright,無 LLM) | 5 | Q12 CS2240700E (focus visible) · Q15 GN2140300E (AA 對比 4.5:1) · Q16 GN3140600E (AAA 對比 7:1) · Q19 GN3330602E (modal-aware 表單偵測) · Q20 GN2330300E (空送出 → focus 第一個必填無效欄位) |
|
|
75
|
+
|
|
76
|
+
**20 條中 70% 不需要任何 LLM/VLM 呼叫** — 只有 6/20 需要外部模型。LLM 全關,14 條規則照樣有判斷結果。LLM / VLM 端點可指向本地模型 (Ollama, vLLM, LM Studio, qwen3-vl-8b 等),request 不離開內網。
|
|
77
|
+
|
|
78
|
+
> 套件總註冊規則數:**129** (涵蓋 Freego 的 C 類機器檢查 + 我們補的 E 類擴充規則)。上表只是 AAA 自評子集。
|
|
79
|
+
|
|
80
|
+
## 安裝
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install a11y-moda # PyPI
|
|
84
|
+
playwright install chromium # --render 跟 Playwright probe 都要這個
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Python ≥ 3.10。
|
|
88
|
+
|
|
89
|
+
> ⚠️ `pip install` **不會自動下載 Chromium**。第一次跑 `--render` 前必須執行 `playwright install chromium`,否則會噴 `Executable doesn't exist` 錯誤。
|
|
90
|
+
|
|
91
|
+
開發安裝 (從 source clone):
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
git clone https://github.com/light-design-tw/a11y-moda
|
|
95
|
+
cd a11y-moda
|
|
96
|
+
pip install -e .
|
|
97
|
+
playwright install chromium
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## 快速開始
|
|
101
|
+
|
|
102
|
+
掃單一 URL:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
a11y-moda scan https://example.com --level AA
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
爬取整站 + 渲染 JS + 用本地 VLM + 輸出 HTML 報告:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
a11y-moda site https://example.com \
|
|
112
|
+
--level AAA --max-pages 30 --render \
|
|
113
|
+
--llm-base-url http://localhost:8000/v1 --llm-model qwen3-vl-8b \
|
|
114
|
+
--format html -o report.html
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
LLM endpoint 用環境變數 (沒給 `--llm-*` flag 時 fallback):
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
export A11Y_LLM_BASE_URL=https://api.openai.com/v1
|
|
121
|
+
export A11Y_LLM_KEY=sk-...
|
|
122
|
+
export A11Y_LLM_MODEL=gpt-4o-mini
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## 指令參考
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
a11y-moda scan <URL> 掃單頁
|
|
129
|
+
a11y-moda site <URL> 探索並掃整站
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
常用選項:
|
|
133
|
+
|
|
134
|
+
| Flag | 預設 | 說明 |
|
|
135
|
+
|---|---|---|
|
|
136
|
+
| `--level A\|AA\|AAA` | `AA` | 掃描等級 |
|
|
137
|
+
| `--render` | off | 用 headless Chromium 渲染 JS 頁面 |
|
|
138
|
+
| `--max-pages N` | 30 | `site` 上限 |
|
|
139
|
+
| `--source sitemap\|crawl\|auto` | `auto` | URL 探索策略 |
|
|
140
|
+
| `--workers N` | 4 | 並行 worker (僅靜態掃描;render 強制序列化) |
|
|
141
|
+
| `--rps N` | 0 | 全域速率上限 (req/s),0 = 不限 |
|
|
142
|
+
| `--ignore RULE_ID` | — | 可重複;跳過指定 rule_id |
|
|
143
|
+
| `--freego-only` | off | 只跑官方工具有的機器檢查規則 |
|
|
144
|
+
| `--freego-compat` | off | 對齊官方工具回報 (CS2140401C / CS3140801C / CS3140802C) |
|
|
145
|
+
| `--format json\|md\|html` | `json` | 輸出格式 (也會從 `-o` 副檔名自動判斷) |
|
|
146
|
+
| `-o FILE` | stdout | 純檔名 → `./reports/FILE` |
|
|
147
|
+
|
|
148
|
+
## 規則怎麼運作
|
|
149
|
+
|
|
150
|
+
每個 MODA rule_id 一個 Python 檔,放 `src/a11y_moda/rules/codes/<主題>/<RULE_ID>.py`。套件用 `pkgutil.iter_modules` 自動探索;用 `@register` decorator 自動註冊,不用改清單。
|
|
151
|
+
|
|
152
|
+
樣板:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from ....models import Level
|
|
156
|
+
from ...base import Rule, RuleMeta, register
|
|
157
|
+
from ...helpers import should_skip, truncate
|
|
158
|
+
|
|
159
|
+
@register
|
|
160
|
+
class MyRule(Rule):
|
|
161
|
+
meta = RuleMeta(rule_id="XX1234567E", guideline="1.1.1", level=Level.A,
|
|
162
|
+
desc="...", source="extension")
|
|
163
|
+
def _check(self, soup, report, *, html, url, ctx) -> None:
|
|
164
|
+
...
|
|
165
|
+
report.add(self._issue(message="...", snippet="...", status="fail"))
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`source = "freego"` → 對應官方工具有的機器檢查規則
|
|
169
|
+
`source = "extension"` → E (人工判斷) 規則被我們程式化的版本
|
|
170
|
+
|
|
171
|
+
## 專案狀態
|
|
172
|
+
|
|
173
|
+
Pre-1.0。規則覆蓋率對齊 MODA 公布的規則集;LLM 類規則需外部 LLM 接取。1.0 前輸出 schema 可能會變動。
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
[MIT](https://github.com/light-design-tw/a11y-moda/blob/main/LICENSE)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# a11y-moda
|
|
2
|
+
|
|
3
|
+
> 台灣 MODA 無障礙標章自評用 Python CLI · WCAG A / AA / AAA · zh-TW 報告
|
|
4
|
+
|
|
5
|
+
**繁體中文** · [English](https://github.com/light-design-tw/a11y-moda/blob/main/README.en.md)
|
|
6
|
+
|
|
7
|
+
> ⚠️ **非官方社群工具。** 與數位發展部 (MODA / 數位發展部) 無從屬關係,不替代官方 [Freego](https://accessibility.moda.gov.tw/) 與正式審查流程。本工具僅為開發者便利之用。
|
|
8
|
+
|
|
9
|
+
## 為什麼做這個
|
|
10
|
+
|
|
11
|
+
MODA 官方工具 [Freego](https://accessibility.moda.gov.tw/) 是 Java GUI,沒有 CLI、Docker、API 介面。`a11y-moda` 補這個缺口,給 **CI/CD pipeline** 跟 **AI 協作開發** 用。實作 MODA 公布的規則編碼 (HM / GN / CS / AR / FA / SC),每筆 issue 標注對應 MODA rule_id 跟 WCAG 2.1 success criterion。
|
|
12
|
+
|
|
13
|
+
人工判斷類規則 (E codes) 接 OpenAI 相容 API。OpenAI、Anthropic、OpenRouter、Ollama、vLLM、LM Studio、llama.cpp server — 任何吐 `/v1/chat/completions` 的端點都接得起來。
|
|
14
|
+
|
|
15
|
+
## 功能
|
|
16
|
+
|
|
17
|
+
- 靜態掃描 (httpx + BeautifulSoup) 或渲染掃描 (Playwright / headless Chromium,給 SPA 用)
|
|
18
|
+
- A / AA / AAA 等級過濾
|
|
19
|
+
- 全站爬取:先 sitemap.xml,找不到就 BFS
|
|
20
|
+
- 輸出:JSON / Markdown / HTML (HTML 一律渲染依規則 / 依 WCAG / 依 URL 三種 view)
|
|
21
|
+
- LLM 判斷結果存本地 cache — 重跑安全,只有改動的規則重打模型
|
|
22
|
+
- 視覺模型 (VLM) 從截圖驗證版面 / 圖片類規則
|
|
23
|
+
- `--freego-compat` 對齊官方工具回報格式,便於交叉比對
|
|
24
|
+
|
|
25
|
+
## MODA AAA 自評涵蓋率 (20/20)
|
|
26
|
+
|
|
27
|
+
實作 MODA AAA 自評表全部 20 題。官方工具對這些 E (人工判斷) 規則的覆蓋率是 **0** — 送件人要逐題手動勾選。本工具自動化 **18 / 20**,剩下 **2** 以 informative caveat 標記,提供人工複查線索。
|
|
28
|
+
|
|
29
|
+
機制拆解:
|
|
30
|
+
|
|
31
|
+
| 機制 | 數量 | 規則 |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| **純 DOM** (無外部依賴,毫秒級) | 9 | Q2 GN1110111E (CAPTCHA alt) · Q3 GN3120600 (影片偵測 + caveat) · Q6 AR3130600E (landmark) · Q7 HM1130110E (複雜表格) · Q8 GN1210101E (鍵盤可達) · Q10 GN1240100E (skip link) · Q13 HM3240800E (麵包屑) · Q14 CS2141204E (em 單位) · Q18 HM2130500E (autocomplete) |
|
|
34
|
+
| **LLM (文字)** — OpenAI 相容 | 5 | Q1 HM1110103E (長文 alt) · Q4 HM1130104E (標題巢狀) · Q5 GN2240600E (描述性標題) · Q9 HM1240402E (圖連結文字) · Q17 GN1330201E (必填欄位標示) |
|
|
35
|
+
| **VLM (視覺)** — 多模態 | 1 | Q11 GN1240500E (從首頁截圖偵測網站地圖) |
|
|
36
|
+
| **瀏覽器 probe** (Playwright,無 LLM) | 5 | Q12 CS2240700E (focus visible) · Q15 GN2140300E (AA 對比 4.5:1) · Q16 GN3140600E (AAA 對比 7:1) · Q19 GN3330602E (modal-aware 表單偵測) · Q20 GN2330300E (空送出 → focus 第一個必填無效欄位) |
|
|
37
|
+
|
|
38
|
+
**20 條中 70% 不需要任何 LLM/VLM 呼叫** — 只有 6/20 需要外部模型。LLM 全關,14 條規則照樣有判斷結果。LLM / VLM 端點可指向本地模型 (Ollama, vLLM, LM Studio, qwen3-vl-8b 等),request 不離開內網。
|
|
39
|
+
|
|
40
|
+
> 套件總註冊規則數:**129** (涵蓋 Freego 的 C 類機器檢查 + 我們補的 E 類擴充規則)。上表只是 AAA 自評子集。
|
|
41
|
+
|
|
42
|
+
## 安裝
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install a11y-moda # PyPI
|
|
46
|
+
playwright install chromium # --render 跟 Playwright probe 都要這個
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Python ≥ 3.10。
|
|
50
|
+
|
|
51
|
+
> ⚠️ `pip install` **不會自動下載 Chromium**。第一次跑 `--render` 前必須執行 `playwright install chromium`,否則會噴 `Executable doesn't exist` 錯誤。
|
|
52
|
+
|
|
53
|
+
開發安裝 (從 source clone):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
git clone https://github.com/light-design-tw/a11y-moda
|
|
57
|
+
cd a11y-moda
|
|
58
|
+
pip install -e .
|
|
59
|
+
playwright install chromium
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 快速開始
|
|
63
|
+
|
|
64
|
+
掃單一 URL:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
a11y-moda scan https://example.com --level AA
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
爬取整站 + 渲染 JS + 用本地 VLM + 輸出 HTML 報告:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
a11y-moda site https://example.com \
|
|
74
|
+
--level AAA --max-pages 30 --render \
|
|
75
|
+
--llm-base-url http://localhost:8000/v1 --llm-model qwen3-vl-8b \
|
|
76
|
+
--format html -o report.html
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
LLM endpoint 用環境變數 (沒給 `--llm-*` flag 時 fallback):
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
export A11Y_LLM_BASE_URL=https://api.openai.com/v1
|
|
83
|
+
export A11Y_LLM_KEY=sk-...
|
|
84
|
+
export A11Y_LLM_MODEL=gpt-4o-mini
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 指令參考
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
a11y-moda scan <URL> 掃單頁
|
|
91
|
+
a11y-moda site <URL> 探索並掃整站
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
常用選項:
|
|
95
|
+
|
|
96
|
+
| Flag | 預設 | 說明 |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `--level A\|AA\|AAA` | `AA` | 掃描等級 |
|
|
99
|
+
| `--render` | off | 用 headless Chromium 渲染 JS 頁面 |
|
|
100
|
+
| `--max-pages N` | 30 | `site` 上限 |
|
|
101
|
+
| `--source sitemap\|crawl\|auto` | `auto` | URL 探索策略 |
|
|
102
|
+
| `--workers N` | 4 | 並行 worker (僅靜態掃描;render 強制序列化) |
|
|
103
|
+
| `--rps N` | 0 | 全域速率上限 (req/s),0 = 不限 |
|
|
104
|
+
| `--ignore RULE_ID` | — | 可重複;跳過指定 rule_id |
|
|
105
|
+
| `--freego-only` | off | 只跑官方工具有的機器檢查規則 |
|
|
106
|
+
| `--freego-compat` | off | 對齊官方工具回報 (CS2140401C / CS3140801C / CS3140802C) |
|
|
107
|
+
| `--format json\|md\|html` | `json` | 輸出格式 (也會從 `-o` 副檔名自動判斷) |
|
|
108
|
+
| `-o FILE` | stdout | 純檔名 → `./reports/FILE` |
|
|
109
|
+
|
|
110
|
+
## 規則怎麼運作
|
|
111
|
+
|
|
112
|
+
每個 MODA rule_id 一個 Python 檔,放 `src/a11y_moda/rules/codes/<主題>/<RULE_ID>.py`。套件用 `pkgutil.iter_modules` 自動探索;用 `@register` decorator 自動註冊,不用改清單。
|
|
113
|
+
|
|
114
|
+
樣板:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from ....models import Level
|
|
118
|
+
from ...base import Rule, RuleMeta, register
|
|
119
|
+
from ...helpers import should_skip, truncate
|
|
120
|
+
|
|
121
|
+
@register
|
|
122
|
+
class MyRule(Rule):
|
|
123
|
+
meta = RuleMeta(rule_id="XX1234567E", guideline="1.1.1", level=Level.A,
|
|
124
|
+
desc="...", source="extension")
|
|
125
|
+
def _check(self, soup, report, *, html, url, ctx) -> None:
|
|
126
|
+
...
|
|
127
|
+
report.add(self._issue(message="...", snippet="...", status="fail"))
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`source = "freego"` → 對應官方工具有的機器檢查規則
|
|
131
|
+
`source = "extension"` → E (人工判斷) 規則被我們程式化的版本
|
|
132
|
+
|
|
133
|
+
## 專案狀態
|
|
134
|
+
|
|
135
|
+
Pre-1.0。規則覆蓋率對齊 MODA 公布的規則集;LLM 類規則需外部 LLM 接取。1.0 前輸出 schema 可能會變動。
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
[MIT](https://github.com/light-design-tw/a11y-moda/blob/main/LICENSE)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "a11y-moda"
|
|
3
|
+
version = "0.1.0a1"
|
|
4
|
+
description = "Taiwan MODA accessibility CLI · WCAG A/AA/AAA · zh-TW · Freego complement"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "睞特股份有限公司", email = "maple@light-design.com.tw" },
|
|
10
|
+
]
|
|
11
|
+
keywords = ["accessibility", "wcag", "a11y", "moda", "taiwan", "freego", "audit", "scanner"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Environment :: Console",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Natural Language :: Chinese (Traditional)",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Internet :: WWW/HTTP :: Site Management",
|
|
23
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
24
|
+
"Topic :: Software Development :: Testing",
|
|
25
|
+
]
|
|
26
|
+
requires-python = ">=3.10"
|
|
27
|
+
dependencies = [
|
|
28
|
+
"httpx>=0.27,<1.0",
|
|
29
|
+
"beautifulsoup4>=4.12,<5.0",
|
|
30
|
+
"lxml>=6.1,<7.0", # CVE-2026-41066 (XXE) fixed in 6.1
|
|
31
|
+
"click>=8.1,<9.0",
|
|
32
|
+
"playwright>=1.59,<2.0", # CVE-2026-2441 Chromium use-after-free patched in 1.59
|
|
33
|
+
"wcag-contrast-ratio>=0.9,<1.0",
|
|
34
|
+
"tinycss2>=1.3,<2.0",
|
|
35
|
+
"defusedxml>=0.7,<1.0",
|
|
36
|
+
"python-dotenv>=1.2.2,<2.0", # CVE-2026-28684 symlink in set_key/unset_key
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/light-design-tw/a11y-moda"
|
|
41
|
+
Repository = "https://github.com/light-design-tw/a11y-moda"
|
|
42
|
+
Issues = "https://github.com/light-design-tw/a11y-moda/issues"
|
|
43
|
+
Documentation = "https://github.com/light-design-tw/a11y-moda#readme"
|
|
44
|
+
Changelog = "https://github.com/light-design-tw/a11y-moda/blob/main/CHANGELOG.md"
|
|
45
|
+
|
|
46
|
+
[project.scripts]
|
|
47
|
+
a11y-moda = "a11y_moda.cli:main"
|
|
48
|
+
|
|
49
|
+
[build-system]
|
|
50
|
+
requires = ["setuptools>=77"]
|
|
51
|
+
build-backend = "setuptools.build_meta"
|
|
52
|
+
|
|
53
|
+
[tool.setuptools.packages.find]
|
|
54
|
+
where = ["src"]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""URL safety checks (SSRF defence)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import ipaddress
|
|
4
|
+
import os
|
|
5
|
+
import socket
|
|
6
|
+
from urllib.parse import urlparse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
_BLOCKED_HOSTS = {"localhost", "0.0.0.0", "::", "::1", "ip6-localhost", "ip6-loopback"}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UnsafeURLError(ValueError):
|
|
13
|
+
"""URL was rejected because it points at a private / loopback / non-http host."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _allow_private_default() -> bool:
|
|
17
|
+
"""Operator can opt in via env var when scanning intranets."""
|
|
18
|
+
return os.environ.get("A11Y_ALLOW_PRIVATE_HOSTS", "").strip() == "1"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _normalise_ip(ip: ipaddress.IPv4Address | ipaddress.IPv6Address):
|
|
22
|
+
"""Unwrap IPv4-mapped IPv6 (`::ffff:127.0.0.1`) so private/loopback checks
|
|
23
|
+
land on the embedded IPv4. ipaddress's is_loopback is False on the wrapper.
|
|
24
|
+
"""
|
|
25
|
+
if isinstance(ip, ipaddress.IPv6Address) and ip.ipv4_mapped is not None:
|
|
26
|
+
return ip.ipv4_mapped
|
|
27
|
+
return ip
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _ip_is_private(ip: ipaddress.IPv4Address | ipaddress.IPv6Address) -> bool:
|
|
31
|
+
ip = _normalise_ip(ip)
|
|
32
|
+
return (ip.is_private or ip.is_loopback or ip.is_link_local
|
|
33
|
+
or ip.is_reserved or ip.is_multicast or ip.is_unspecified)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _resolve_all(host: str) -> list[ipaddress.IPv4Address | ipaddress.IPv6Address]:
|
|
37
|
+
"""All IPs the resolver returns. Empty list on failure (callers treat as unsafe)."""
|
|
38
|
+
try:
|
|
39
|
+
infos = socket.getaddrinfo(host, None)
|
|
40
|
+
except socket.gaierror:
|
|
41
|
+
return []
|
|
42
|
+
out = []
|
|
43
|
+
for info in infos:
|
|
44
|
+
ip_str = info[4][0]
|
|
45
|
+
try:
|
|
46
|
+
out.append(ipaddress.ip_address(ip_str))
|
|
47
|
+
except ValueError:
|
|
48
|
+
continue
|
|
49
|
+
return out
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def is_safe_http_url(url: str, *, allow_private: bool | None = None) -> bool:
|
|
53
|
+
"""True when URL is safe to fetch from arbitrary callers.
|
|
54
|
+
|
|
55
|
+
Rejects: non-http(s) schemes, missing host, loopback / private / link-local
|
|
56
|
+
/ reserved / multicast IP literals, IPv4-mapped IPv6 wrappers around such
|
|
57
|
+
IPs, and hostnames where ANY resolved IP falls in those ranges (mitigates
|
|
58
|
+
DNS rebinding where the resolver returns both a public and a private IP).
|
|
59
|
+
|
|
60
|
+
Pass allow_private=True (or set A11Y_ALLOW_PRIVATE_HOSTS=1) to permit
|
|
61
|
+
intranet scans.
|
|
62
|
+
"""
|
|
63
|
+
if allow_private is None:
|
|
64
|
+
allow_private = _allow_private_default()
|
|
65
|
+
try:
|
|
66
|
+
p = urlparse(url)
|
|
67
|
+
except Exception:
|
|
68
|
+
return False
|
|
69
|
+
if p.scheme not in ("http", "https"):
|
|
70
|
+
return False
|
|
71
|
+
host = (p.hostname or "").lower()
|
|
72
|
+
if not host:
|
|
73
|
+
return False
|
|
74
|
+
if allow_private:
|
|
75
|
+
return True
|
|
76
|
+
if host in _BLOCKED_HOSTS:
|
|
77
|
+
return False
|
|
78
|
+
try:
|
|
79
|
+
ip = ipaddress.ip_address(host)
|
|
80
|
+
return not _ip_is_private(ip)
|
|
81
|
+
except ValueError:
|
|
82
|
+
pass # hostname, resolve below
|
|
83
|
+
ips = _resolve_all(host)
|
|
84
|
+
if not ips:
|
|
85
|
+
return False
|
|
86
|
+
return not any(_ip_is_private(ip) for ip in ips)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def require_safe_http_url(url: str, *, allow_private: bool | None = None) -> None:
|
|
90
|
+
"""Raise UnsafeURLError if the URL is not safe."""
|
|
91
|
+
if not is_safe_http_url(url, allow_private=allow_private):
|
|
92
|
+
raise UnsafeURLError(f"refused unsafe URL (private/loopback/non-http): {url!r}")
|