@mcptoolshop/accessibility-suite 0.1.0
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.
- package/.github/workflows/ci.yml +63 -0
- package/LICENSE +21 -0
- package/README.md +37 -0
- package/docs/prov-spec/.github/workflows/ci.yml +68 -0
- package/docs/prov-spec/CHANGELOG.md +69 -0
- package/docs/prov-spec/CODE_OF_CONDUCT.md +129 -0
- package/docs/prov-spec/CONFORMANCE_LEVELS.md +223 -0
- package/docs/prov-spec/CONTRIBUTING.md +145 -0
- package/docs/prov-spec/IMPLEMENTER_CHECKLIST.md +137 -0
- package/docs/prov-spec/LICENSE +21 -0
- package/docs/prov-spec/PRESS_RELEASE.md +74 -0
- package/docs/prov-spec/README.md +182 -0
- package/docs/prov-spec/SETUP.md +135 -0
- package/docs/prov-spec/WHY.md +86 -0
- package/docs/prov-spec/examples/artifact.example.json +14 -0
- package/docs/prov-spec/examples/artifact.ref.example.json +9 -0
- package/docs/prov-spec/examples/evidence.example.json +6 -0
- package/docs/prov-spec/examples/mcp.envelope.example.json +97 -0
- package/docs/prov-spec/examples/mcp.request.example.json +28 -0
- package/docs/prov-spec/examples/prov.record.example.json +35 -0
- package/docs/prov-spec/interop/PROOF_NODE_ENGINE.md +114 -0
- package/docs/prov-spec/spec/MCP_COMPATIBILITY.md +241 -0
- package/docs/prov-spec/spec/PROV_METHODS_CATALOG.md +142 -0
- package/docs/prov-spec/spec/PROV_METHODS_SPEC.md +397 -0
- package/docs/prov-spec/spec/methods.json +213 -0
- package/docs/prov-spec/spec/schemas/artifact.ref.schema.v0.1.json +58 -0
- package/docs/prov-spec/spec/schemas/artifact.schema.v0.1.json +61 -0
- package/docs/prov-spec/spec/schemas/assist.request.schema.v0.1.json +52 -0
- package/docs/prov-spec/spec/schemas/assist.response.schema.v0.1.json +70 -0
- package/docs/prov-spec/spec/schemas/cli.error.schema.v0.1.json +78 -0
- package/docs/prov-spec/spec/schemas/evidence.schema.v0.1.json +37 -0
- package/docs/prov-spec/spec/schemas/mcp.envelope.schema.v0.1.json +141 -0
- package/docs/prov-spec/spec/schemas/mcp.request.schema.v0.1.json +79 -0
- package/docs/prov-spec/spec/schemas/methods.schema.json +93 -0
- package/docs/prov-spec/spec/schemas/prov-capabilities.schema.json +122 -0
- package/docs/prov-spec/spec/schemas/prov.record.schema.v0.1.json +133 -0
- package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/expected.json +4 -0
- package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/input.json +1 -0
- package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/negative/double_wrapped.json +14 -0
- package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/negative/wrong_schema_version.json +11 -0
- package/docs/prov-spec/spec/vectors/engine.extract.evidence.json_pointer/expected.json +24 -0
- package/docs/prov-spec/spec/vectors/engine.extract.evidence.json_pointer/input.json +8 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/expected.json +7 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/input.json +1 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/negative/non_hex_chars.json +16 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/negative/uppercase_hex.json +16 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/negative/wrong_length.json +16 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/negative/hyphen_separator.json +8 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/negative/reserved_namespace.json +8 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/negative/starts_with_digit.json +8 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/negative/uppercase.json +8 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/positive/valid_ids.json +18 -0
- package/docs/prov-spec/tools/python/prov_validator.py +428 -0
- package/examples/a11y-demo-site/.github/workflows/a11y-artifacts.yml +81 -0
- package/examples/a11y-demo-site/.github/workflows/a11y.yml +34 -0
- package/examples/a11y-demo-site/CODE_OF_CONDUCT.md +129 -0
- package/examples/a11y-demo-site/CONTRIBUTING.md +83 -0
- package/examples/a11y-demo-site/LICENSE +21 -0
- package/examples/a11y-demo-site/README.md +155 -0
- package/examples/a11y-demo-site/html/contact.html +15 -0
- package/examples/a11y-demo-site/html/index.html +20 -0
- package/examples/a11y-demo-site/scripts/a11y.sh +20 -0
- package/package.json +26 -0
- package/src/a11y-assist/.github/workflows/publish.yml +52 -0
- package/src/a11y-assist/.github/workflows/test.yml +30 -0
- package/src/a11y-assist/A11Y_ASSIST_TEST_COVERAGE_REQUIREMENTS.md +104 -0
- package/src/a11y-assist/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-assist/CONTRIBUTING.md +98 -0
- package/src/a11y-assist/ENGINE.md +363 -0
- package/src/a11y-assist/LICENSE +21 -0
- package/src/a11y-assist/PRESS_RELEASE.md +71 -0
- package/src/a11y-assist/QUICKSTART.md +101 -0
- package/src/a11y-assist/README.md +192 -0
- package/src/a11y-assist/RELEASE_NOTES.md +319 -0
- package/src/a11y-assist/a11y_assist/__init__.py +3 -0
- package/src/a11y-assist/a11y_assist/cli.py +599 -0
- package/src/a11y-assist/a11y_assist/from_cli_error.py +149 -0
- package/src/a11y-assist/a11y_assist/guard.py +444 -0
- package/src/a11y-assist/a11y_assist/ingest.py +407 -0
- package/src/a11y-assist/a11y_assist/methods.py +137 -0
- package/src/a11y-assist/a11y_assist/parse_raw.py +71 -0
- package/src/a11y-assist/a11y_assist/profiles/__init__.py +29 -0
- package/src/a11y-assist/a11y_assist/profiles/cognitive_load.py +245 -0
- package/src/a11y-assist/a11y_assist/profiles/cognitive_load_render.py +86 -0
- package/src/a11y-assist/a11y_assist/profiles/dyslexia.py +144 -0
- package/src/a11y-assist/a11y_assist/profiles/dyslexia_render.py +77 -0
- package/src/a11y-assist/a11y_assist/profiles/plain_language.py +119 -0
- package/src/a11y-assist/a11y_assist/profiles/plain_language_render.py +66 -0
- package/src/a11y-assist/a11y_assist/profiles/screen_reader.py +348 -0
- package/src/a11y-assist/a11y_assist/profiles/screen_reader_render.py +89 -0
- package/src/a11y-assist/a11y_assist/render.py +95 -0
- package/src/a11y-assist/a11y_assist/schemas/assist.request.schema.v0.1.json +52 -0
- package/src/a11y-assist/a11y_assist/schemas/assist.response.schema.v0.1.json +70 -0
- package/src/a11y-assist/a11y_assist/schemas/cli.error.schema.v0.1.json +78 -0
- package/src/a11y-assist/a11y_assist/storage.py +31 -0
- package/src/a11y-assist/pyproject.toml +60 -0
- package/src/a11y-assist/tests/__init__.py +1 -0
- package/src/a11y-assist/tests/fixtures/base_inputs/cli_error_high.json +18 -0
- package/src/a11y-assist/tests/fixtures/base_inputs/cli_error_medium.json +16 -0
- package/src/a11y-assist/tests/fixtures/base_inputs/raw_text_low.txt +3 -0
- package/src/a11y-assist/tests/fixtures/cli_error_good.json +9 -0
- package/src/a11y-assist/tests/fixtures/cli_error_missing_id.json +7 -0
- package/src/a11y-assist/tests/fixtures/cli_error_string_format.json +7 -0
- package/src/a11y-assist/tests/fixtures/expected/cognitive_load_high.txt +20 -0
- package/src/a11y-assist/tests/fixtures/expected/dyslexia_high.txt +20 -0
- package/src/a11y-assist/tests/fixtures/expected/lowvision_high.txt +18 -0
- package/src/a11y-assist/tests/fixtures/expected/plain_language_high.txt +14 -0
- package/src/a11y-assist/tests/fixtures/expected/screen_reader_high.txt +19 -0
- package/src/a11y-assist/tests/fixtures/golden_screen_reader_cli_error.txt +16 -0
- package/src/a11y-assist/tests/fixtures/golden_screen_reader_raw_no_id.txt +14 -0
- package/src/a11y-assist/tests/fixtures/golden_screen_reader_raw_with_id.txt +14 -0
- package/src/a11y-assist/tests/fixtures/raw_good.txt +11 -0
- package/src/a11y-assist/tests/fixtures/raw_no_id.txt +2 -0
- package/src/a11y-assist/tests/test_cognitive_load.py +469 -0
- package/src/a11y-assist/tests/test_dyslexia.py +337 -0
- package/src/a11y-assist/tests/test_explain.py +74 -0
- package/src/a11y-assist/tests/test_golden.py +127 -0
- package/src/a11y-assist/tests/test_guard.py +819 -0
- package/src/a11y-assist/tests/test_guard_integration.py +457 -0
- package/src/a11y-assist/tests/test_ingest.py +311 -0
- package/src/a11y-assist/tests/test_methods_metadata.py +236 -0
- package/src/a11y-assist/tests/test_plain_language.py +348 -0
- package/src/a11y-assist/tests/test_render.py +117 -0
- package/src/a11y-assist/tests/test_screen_reader.py +703 -0
- package/src/a11y-assist/tests/test_storage_last.py +61 -0
- package/src/a11y-assist/tests/test_triage.py +86 -0
- package/src/a11y-ci/.github/workflows/ci.yml +43 -0
- package/src/a11y-ci/.github/workflows/test.yml +30 -0
- package/src/a11y-ci/A11Y_CI_TEST_COVERAGE_REQUIREMENTS.md +94 -0
- package/src/a11y-ci/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-ci/CONTRIBUTING.md +142 -0
- package/src/a11y-ci/LICENSE +21 -0
- package/src/a11y-ci/README.md +105 -0
- package/src/a11y-ci/a11y_ci/__init__.py +3 -0
- package/src/a11y-ci/a11y_ci/allowlist.py +83 -0
- package/src/a11y-ci/a11y_ci/cli.py +145 -0
- package/src/a11y-ci/a11y_ci/gate.py +131 -0
- package/src/a11y-ci/a11y_ci/render.py +48 -0
- package/src/a11y-ci/a11y_ci/schemas/allowlist.schema.json +24 -0
- package/src/a11y-ci/a11y_ci/scorecard.py +99 -0
- package/src/a11y-ci/npm/package.json +35 -0
- package/src/a11y-ci/pyproject.toml +64 -0
- package/src/a11y-ci/tests/__init__.py +1 -0
- package/src/a11y-ci/tests/fixtures/allowlist_expired.json +10 -0
- package/src/a11y-ci/tests/fixtures/allowlist_ok.json +10 -0
- package/src/a11y-ci/tests/fixtures/baseline_ok.json +7 -0
- package/src/a11y-ci/tests/fixtures/current_fail.json +6 -0
- package/src/a11y-ci/tests/fixtures/current_ok.json +6 -0
- package/src/a11y-ci/tests/fixtures/current_regresses.json +7 -0
- package/src/a11y-ci/tests/test_gate.py +134 -0
- package/src/a11y-evidence-engine/.github/workflows/ci.yml +53 -0
- package/src/a11y-evidence-engine/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-evidence-engine/CONTRIBUTING.md +128 -0
- package/src/a11y-evidence-engine/LICENSE +21 -0
- package/src/a11y-evidence-engine/README.md +71 -0
- package/src/a11y-evidence-engine/bin/a11y-engine.js +11 -0
- package/src/a11y-evidence-engine/fixtures/bad/button-no-name.html +30 -0
- package/src/a11y-evidence-engine/fixtures/bad/img-missing-alt.html +19 -0
- package/src/a11y-evidence-engine/fixtures/bad/input-missing-label.html +26 -0
- package/src/a11y-evidence-engine/fixtures/bad/missing-lang.html +11 -0
- package/src/a11y-evidence-engine/fixtures/good/index.html +29 -0
- package/src/a11y-evidence-engine/package-lock.json +109 -0
- package/src/a11y-evidence-engine/package.json +45 -0
- package/src/a11y-evidence-engine/src/cli.js +74 -0
- package/src/a11y-evidence-engine/src/evidence/canonicalize.js +52 -0
- package/src/a11y-evidence-engine/src/evidence/json_pointer.js +34 -0
- package/src/a11y-evidence-engine/src/evidence/prov_emit.js +153 -0
- package/src/a11y-evidence-engine/src/fswalk.js +56 -0
- package/src/a11y-evidence-engine/src/html_parse.js +117 -0
- package/src/a11y-evidence-engine/src/ids.js +53 -0
- package/src/a11y-evidence-engine/src/rules/document_missing_lang.js +50 -0
- package/src/a11y-evidence-engine/src/rules/form_control_missing_label.js +105 -0
- package/src/a11y-evidence-engine/src/rules/img_missing_alt.js +77 -0
- package/src/a11y-evidence-engine/src/rules/index.js +37 -0
- package/src/a11y-evidence-engine/src/rules/interactive_missing_name.js +129 -0
- package/src/a11y-evidence-engine/src/scan.js +128 -0
- package/src/a11y-evidence-engine/test/scan.test.js +149 -0
- package/src/a11y-evidence-engine/test/vectors.test.js +200 -0
- package/src/a11y-lint/.github/workflows/ci.yml +46 -0
- package/src/a11y-lint/.github/workflows/test.yml +34 -0
- package/src/a11y-lint/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-lint/CONTRIBUTING.md +70 -0
- package/src/a11y-lint/GOVERNANCE.md +57 -0
- package/src/a11y-lint/LICENSE +21 -0
- package/src/a11y-lint/PRESS_RELEASE.md +50 -0
- package/src/a11y-lint/README.md +276 -0
- package/src/a11y-lint/RELEASE_NOTES.md +57 -0
- package/src/a11y-lint/RELEASING.md +57 -0
- package/src/a11y-lint/a11y_lint/__init__.py +64 -0
- package/src/a11y-lint/a11y_lint/cli.py +319 -0
- package/src/a11y-lint/a11y_lint/errors.py +252 -0
- package/src/a11y-lint/a11y_lint/render.py +293 -0
- package/src/a11y-lint/a11y_lint/report_md.py +289 -0
- package/src/a11y-lint/a11y_lint/scan_cli_text.py +434 -0
- package/src/a11y-lint/a11y_lint/schemas/cli.error.schema.v0.1.json +83 -0
- package/src/a11y-lint/a11y_lint/scorecard.py +244 -0
- package/src/a11y-lint/a11y_lint/validate.py +225 -0
- package/src/a11y-lint/pyproject.toml +75 -0
- package/src/a11y-lint/tests/__init__.py +1 -0
- package/src/a11y-lint/tests/test_cli.py +200 -0
- package/src/a11y-lint/tests/test_errors.py +188 -0
- package/src/a11y-lint/tests/test_render.py +202 -0
- package/src/a11y-lint/tests/test_report_md.py +188 -0
- package/src/a11y-lint/tests/test_scan_cli_text.py +290 -0
- package/src/a11y-lint/tests/test_scorecard.py +195 -0
- package/src/a11y-lint/tests/test_validate.py +257 -0
- package/src/a11y-mcp-tools/.github/workflows/ci.yml +53 -0
- package/src/a11y-mcp-tools/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-mcp-tools/CONTRIBUTING.md +136 -0
- package/src/a11y-mcp-tools/LICENSE +21 -0
- package/src/a11y-mcp-tools/PROV_METHODS_CATALOG.md +104 -0
- package/src/a11y-mcp-tools/README.md +168 -0
- package/src/a11y-mcp-tools/bin/cli.js +452 -0
- package/src/a11y-mcp-tools/bin/server.js +244 -0
- package/src/a11y-mcp-tools/fixtures/requests/a11y.diagnose.ok.json +27 -0
- package/src/a11y-mcp-tools/fixtures/requests/a11y.evidence.ok.json +25 -0
- package/src/a11y-mcp-tools/fixtures/responses/a11y.diagnose.ok.json +139 -0
- package/src/a11y-mcp-tools/fixtures/responses/a11y.diagnose.provenance_fail.json +13 -0
- package/src/a11y-mcp-tools/fixtures/responses/a11y.evidence.ok.json +88 -0
- package/src/a11y-mcp-tools/package-lock.json +189 -0
- package/src/a11y-mcp-tools/package.json +49 -0
- package/src/a11y-mcp-tools/src/envelope.js +197 -0
- package/src/a11y-mcp-tools/src/index.js +9 -0
- package/src/a11y-mcp-tools/src/schemas/artifact.js +85 -0
- package/src/a11y-mcp-tools/src/schemas/diagnosis.schema.v0.1.json +137 -0
- package/src/a11y-mcp-tools/src/schemas/envelope.schema.v0.1.json +108 -0
- package/src/a11y-mcp-tools/src/schemas/evidence.bundle.schema.v0.1.json +129 -0
- package/src/a11y-mcp-tools/src/schemas/evidence.js +97 -0
- package/src/a11y-mcp-tools/src/schemas/index.js +11 -0
- package/src/a11y-mcp-tools/src/schemas/provenance.js +140 -0
- package/src/a11y-mcp-tools/src/schemas/tools/a11y.diagnose.request.schema.v0.1.json +77 -0
- package/src/a11y-mcp-tools/src/schemas/tools/a11y.diagnose.response.schema.v0.1.json +50 -0
- package/src/a11y-mcp-tools/src/schemas/tools/a11y.evidence.request.schema.v0.1.json +120 -0
- package/src/a11y-mcp-tools/src/schemas/tools/a11y.evidence.response.schema.v0.1.json +50 -0
- package/src/a11y-mcp-tools/src/tools/diagnose.js +597 -0
- package/src/a11y-mcp-tools/src/tools/evidence.js +481 -0
- package/src/a11y-mcp-tools/src/tools/index.js +10 -0
- package/src/a11y-mcp-tools/test/contract.test.mjs +154 -0
- package/src/a11y-mcp-tools/test/diagnose.test.js +485 -0
- package/src/a11y-mcp-tools/test/evidence.test.js +183 -0
- package/src/a11y-mcp-tools/test/schema.test.js +327 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Deterministic renderer for CLI accessibility messages.
|
|
2
|
+
|
|
3
|
+
Renders messages in the canonical [OK]/[WARN]/[ERROR] + What/Why/Fix format.
|
|
4
|
+
All output is deterministic and suitable for comparison/testing.
|
|
5
|
+
|
|
6
|
+
Color Philosophy:
|
|
7
|
+
- Colors are purely decorative; meaning NEVER depends on color
|
|
8
|
+
- Default: auto (respects NO_COLOR env var and terminal detection)
|
|
9
|
+
- The plain text output is always fully meaningful without colors
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
from typing import TextIO
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
from .errors import A11yMessage, Level
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def should_use_color(stream: TextIO | None = None) -> bool:
|
|
22
|
+
"""Determine if color should be used based on environment.
|
|
23
|
+
|
|
24
|
+
Respects:
|
|
25
|
+
- NO_COLOR env var (https://no-color.org/)
|
|
26
|
+
- FORCE_COLOR env var
|
|
27
|
+
- Terminal detection (isatty)
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
stream: Output stream to check (default: stdout)
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
True if colors should be used
|
|
34
|
+
"""
|
|
35
|
+
# NO_COLOR takes precedence (https://no-color.org/)
|
|
36
|
+
if os.environ.get("NO_COLOR"):
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
# FORCE_COLOR overrides terminal detection
|
|
40
|
+
if os.environ.get("FORCE_COLOR"):
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
# Check if stream is a TTY
|
|
44
|
+
stream = stream or sys.stdout
|
|
45
|
+
return hasattr(stream, "isatty") and stream.isatty()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ANSI color codes for terminal output
|
|
49
|
+
class Colors:
|
|
50
|
+
"""ANSI color codes for terminal rendering."""
|
|
51
|
+
|
|
52
|
+
RESET = "\033[0m"
|
|
53
|
+
BOLD = "\033[1m"
|
|
54
|
+
DIM = "\033[2m"
|
|
55
|
+
|
|
56
|
+
# Level colors
|
|
57
|
+
OK = "\033[32m" # Green
|
|
58
|
+
WARN = "\033[33m" # Yellow
|
|
59
|
+
ERROR = "\033[31m" # Red
|
|
60
|
+
|
|
61
|
+
# Component colors
|
|
62
|
+
CODE = "\033[36m" # Cyan
|
|
63
|
+
LOCATION = "\033[90m" # Gray
|
|
64
|
+
LABEL = "\033[1m" # Bold
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_level_color(level: Level) -> str:
|
|
68
|
+
"""Get the ANSI color code for a level."""
|
|
69
|
+
return {
|
|
70
|
+
Level.OK: Colors.OK,
|
|
71
|
+
Level.WARN: Colors.WARN,
|
|
72
|
+
Level.ERROR: Colors.ERROR,
|
|
73
|
+
}[level]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def render_plain(message: A11yMessage, indent: int = 0) -> str:
|
|
77
|
+
"""Render a message as plain text (no colors).
|
|
78
|
+
|
|
79
|
+
This is the canonical output format that all messages must follow:
|
|
80
|
+
[LEVEL] CODE: What
|
|
81
|
+
Why: explanation
|
|
82
|
+
Fix: suggestion
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
message: The message to render
|
|
86
|
+
indent: Number of spaces to indent
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Plain text representation
|
|
90
|
+
"""
|
|
91
|
+
prefix = " " * indent
|
|
92
|
+
lines = []
|
|
93
|
+
|
|
94
|
+
# Main line: [LEVEL] CODE: What
|
|
95
|
+
lines.append(f"{prefix}[{message.level}] {message.code}: {message.what}")
|
|
96
|
+
|
|
97
|
+
# Location (if present)
|
|
98
|
+
if message.location:
|
|
99
|
+
lines.append(f"{prefix} at {message.location}")
|
|
100
|
+
|
|
101
|
+
# Why (if present)
|
|
102
|
+
if message.why:
|
|
103
|
+
lines.append(f"{prefix} Why: {message.why}")
|
|
104
|
+
|
|
105
|
+
# Fix (if present)
|
|
106
|
+
if message.fix:
|
|
107
|
+
lines.append(f"{prefix} Fix: {message.fix}")
|
|
108
|
+
|
|
109
|
+
return "\n".join(lines)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def render_colored(message: A11yMessage, indent: int = 0) -> str:
|
|
113
|
+
"""Render a message with ANSI colors for terminal display.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
message: The message to render
|
|
117
|
+
indent: Number of spaces to indent
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Colored text representation
|
|
121
|
+
"""
|
|
122
|
+
prefix = " " * indent
|
|
123
|
+
lines = []
|
|
124
|
+
level_color = get_level_color(message.level)
|
|
125
|
+
|
|
126
|
+
# Main line: [LEVEL] CODE: What
|
|
127
|
+
level_str = f"{level_color}{Colors.BOLD}[{message.level}]{Colors.RESET}"
|
|
128
|
+
code_str = f"{Colors.CODE}{message.code}{Colors.RESET}"
|
|
129
|
+
lines.append(f"{prefix}{level_str} {code_str}: {message.what}")
|
|
130
|
+
|
|
131
|
+
# Location (if present)
|
|
132
|
+
if message.location:
|
|
133
|
+
loc_str = f"{Colors.LOCATION}at {message.location}{Colors.RESET}"
|
|
134
|
+
lines.append(f"{prefix} {loc_str}")
|
|
135
|
+
|
|
136
|
+
# Why (if present)
|
|
137
|
+
if message.why:
|
|
138
|
+
lines.append(f"{prefix} {Colors.LABEL}Why:{Colors.RESET} {message.why}")
|
|
139
|
+
|
|
140
|
+
# Fix (if present)
|
|
141
|
+
if message.fix:
|
|
142
|
+
lines.append(f"{prefix} {Colors.LABEL}Fix:{Colors.RESET} {message.fix}")
|
|
143
|
+
|
|
144
|
+
return "\n".join(lines)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def render(
|
|
148
|
+
message: A11yMessage,
|
|
149
|
+
*,
|
|
150
|
+
color: bool = False,
|
|
151
|
+
indent: int = 0,
|
|
152
|
+
) -> str:
|
|
153
|
+
"""Render a message to string.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
message: The message to render
|
|
157
|
+
color: Whether to include ANSI colors
|
|
158
|
+
indent: Number of spaces to indent
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
String representation
|
|
162
|
+
"""
|
|
163
|
+
if color:
|
|
164
|
+
return render_colored(message, indent)
|
|
165
|
+
return render_plain(message, indent)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def render_batch(
|
|
169
|
+
messages: list[A11yMessage],
|
|
170
|
+
*,
|
|
171
|
+
color: bool = False,
|
|
172
|
+
separator: str = "\n",
|
|
173
|
+
) -> str:
|
|
174
|
+
"""Render multiple messages.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
messages: Messages to render
|
|
178
|
+
color: Whether to include ANSI colors
|
|
179
|
+
separator: String between messages
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Combined string representation
|
|
183
|
+
"""
|
|
184
|
+
return separator.join(render(msg, color=color) for msg in messages)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class Renderer:
|
|
188
|
+
"""Configurable message renderer with output stream support."""
|
|
189
|
+
|
|
190
|
+
def __init__(
|
|
191
|
+
self,
|
|
192
|
+
*,
|
|
193
|
+
color: bool | None = None,
|
|
194
|
+
stream: TextIO | None = None,
|
|
195
|
+
indent: int = 0,
|
|
196
|
+
) -> None:
|
|
197
|
+
"""Initialize the renderer.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
color: Enable colors (None = auto-detect from terminal)
|
|
201
|
+
stream: Output stream (default: stdout)
|
|
202
|
+
indent: Base indentation level
|
|
203
|
+
"""
|
|
204
|
+
self.stream = stream or sys.stdout
|
|
205
|
+
self.indent = indent
|
|
206
|
+
|
|
207
|
+
# Auto-detect color support (respects NO_COLOR env var)
|
|
208
|
+
if color is None:
|
|
209
|
+
self.color = should_use_color(self.stream)
|
|
210
|
+
else:
|
|
211
|
+
self.color = color
|
|
212
|
+
|
|
213
|
+
self._counts = {Level.OK: 0, Level.WARN: 0, Level.ERROR: 0}
|
|
214
|
+
|
|
215
|
+
def render(self, message: A11yMessage) -> str:
|
|
216
|
+
"""Render a message to string."""
|
|
217
|
+
return render(message, color=self.color, indent=self.indent)
|
|
218
|
+
|
|
219
|
+
def write(self, message: A11yMessage) -> None:
|
|
220
|
+
"""Render and write a message to the stream."""
|
|
221
|
+
self._counts[message.level] += 1
|
|
222
|
+
self.stream.write(self.render(message) + "\n")
|
|
223
|
+
|
|
224
|
+
def write_batch(self, messages: list[A11yMessage]) -> None:
|
|
225
|
+
"""Render and write multiple messages."""
|
|
226
|
+
for msg in messages:
|
|
227
|
+
self.write(msg)
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def ok_count(self) -> int:
|
|
231
|
+
"""Number of OK messages written."""
|
|
232
|
+
return self._counts[Level.OK]
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def warn_count(self) -> int:
|
|
236
|
+
"""Number of WARN messages written."""
|
|
237
|
+
return self._counts[Level.WARN]
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def error_count(self) -> int:
|
|
241
|
+
"""Number of ERROR messages written."""
|
|
242
|
+
return self._counts[Level.ERROR]
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def total_count(self) -> int:
|
|
246
|
+
"""Total number of messages written."""
|
|
247
|
+
return sum(self._counts.values())
|
|
248
|
+
|
|
249
|
+
def summary_line(self) -> str:
|
|
250
|
+
"""Get a summary line of counts."""
|
|
251
|
+
parts = []
|
|
252
|
+
if self.ok_count:
|
|
253
|
+
parts.append(f"{self.ok_count} passed")
|
|
254
|
+
if self.warn_count:
|
|
255
|
+
parts.append(f"{self.warn_count} warnings")
|
|
256
|
+
if self.error_count:
|
|
257
|
+
parts.append(f"{self.error_count} errors")
|
|
258
|
+
|
|
259
|
+
if not parts:
|
|
260
|
+
return "No issues found"
|
|
261
|
+
|
|
262
|
+
return ", ".join(parts)
|
|
263
|
+
|
|
264
|
+
def write_summary(self) -> None:
|
|
265
|
+
"""Write a summary line to the stream."""
|
|
266
|
+
summary = self.summary_line()
|
|
267
|
+
if self.color:
|
|
268
|
+
# Color the summary based on worst level
|
|
269
|
+
if self.error_count:
|
|
270
|
+
summary = f"{Colors.ERROR}{summary}{Colors.RESET}"
|
|
271
|
+
elif self.warn_count:
|
|
272
|
+
summary = f"{Colors.WARN}{summary}{Colors.RESET}"
|
|
273
|
+
else:
|
|
274
|
+
summary = f"{Colors.OK}{summary}{Colors.RESET}"
|
|
275
|
+
|
|
276
|
+
self.stream.write(f"\n{summary}\n")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def format_for_file(messages: list[A11yMessage]) -> str:
|
|
280
|
+
"""Format messages for writing to a file (no colors, consistent format).
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
messages: Messages to format
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
File-ready string
|
|
287
|
+
"""
|
|
288
|
+
lines = []
|
|
289
|
+
for msg in messages:
|
|
290
|
+
lines.append(render_plain(msg))
|
|
291
|
+
lines.append("") # Blank line between messages
|
|
292
|
+
|
|
293
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Markdown report renderer for accessibility assessments.
|
|
2
|
+
|
|
3
|
+
Generates detailed markdown reports from scan results and scorecards.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import TextIO
|
|
10
|
+
import io
|
|
11
|
+
|
|
12
|
+
from .errors import A11yMessage, Level
|
|
13
|
+
from .scorecard import Scorecard
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def render_message_md(message: A11yMessage) -> str:
|
|
17
|
+
"""Render a single message as markdown.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
message: Message to render
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Markdown string
|
|
24
|
+
"""
|
|
25
|
+
# Choose emoji based on level
|
|
26
|
+
emoji = {"OK": "✅", "WARN": "⚠️", "ERROR": "❌"}[message.level.value]
|
|
27
|
+
|
|
28
|
+
lines = [f"### {emoji} [{message.level}] {message.code}: {message.what}"]
|
|
29
|
+
|
|
30
|
+
if message.location:
|
|
31
|
+
loc_parts = []
|
|
32
|
+
if message.location.file:
|
|
33
|
+
loc_parts.append(f"`{message.location.file}`")
|
|
34
|
+
if message.location.line:
|
|
35
|
+
loc_parts.append(f"line {message.location.line}")
|
|
36
|
+
if message.location.column:
|
|
37
|
+
loc_parts.append(f"col {message.location.column}")
|
|
38
|
+
if loc_parts:
|
|
39
|
+
lines.append(f"\n**Location:** {' : '.join(loc_parts)}")
|
|
40
|
+
|
|
41
|
+
if message.location and message.location.context:
|
|
42
|
+
lines.append(f"\n```\n{message.location.context}\n```")
|
|
43
|
+
|
|
44
|
+
if message.why:
|
|
45
|
+
lines.append(f"\n**Why:** {message.why}")
|
|
46
|
+
|
|
47
|
+
if message.fix:
|
|
48
|
+
lines.append(f"\n**Fix:** {message.fix}")
|
|
49
|
+
|
|
50
|
+
if message.rule:
|
|
51
|
+
lines.append(f"\n*Rule: `{message.rule}`*")
|
|
52
|
+
|
|
53
|
+
return "\n".join(lines)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def render_scorecard_md(scorecard: Scorecard) -> str:
|
|
57
|
+
"""Render a scorecard as markdown.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
scorecard: Scorecard to render
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Markdown string
|
|
64
|
+
"""
|
|
65
|
+
lines = [
|
|
66
|
+
f"# {scorecard.name}",
|
|
67
|
+
"",
|
|
68
|
+
f"**Overall Score:** {scorecard.overall_score:.1f}% ({scorecard.overall_grade})",
|
|
69
|
+
"",
|
|
70
|
+
"## Summary",
|
|
71
|
+
"",
|
|
72
|
+
"| Metric | Count |",
|
|
73
|
+
"|--------|-------|",
|
|
74
|
+
f"| Total Checks | {scorecard.total_checks} |",
|
|
75
|
+
f"| Passed | {scorecard.total_passed} |",
|
|
76
|
+
f"| Warnings | {scorecard.total_warnings} |",
|
|
77
|
+
f"| Errors | {scorecard.total_errors} |",
|
|
78
|
+
"",
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
# Rule breakdown table
|
|
82
|
+
if scorecard.rule_scores:
|
|
83
|
+
lines.extend(
|
|
84
|
+
[
|
|
85
|
+
"## Rules",
|
|
86
|
+
"",
|
|
87
|
+
"| Rule | Score | Grade | Passed | Warnings | Errors |",
|
|
88
|
+
"|------|-------|-------|--------|----------|--------|",
|
|
89
|
+
]
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
for name, score in sorted(scorecard.rule_scores.items()):
|
|
93
|
+
lines.append(
|
|
94
|
+
f"| `{name}` | {score.score:.1f}% | {score.grade} | "
|
|
95
|
+
f"{score.passed} | {score.warnings} | {score.errors} |"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
lines.append("")
|
|
99
|
+
|
|
100
|
+
# Issues by level
|
|
101
|
+
errors = [m for m in scorecard.messages if m.level == Level.ERROR]
|
|
102
|
+
warnings = [m for m in scorecard.messages if m.level == Level.WARN]
|
|
103
|
+
|
|
104
|
+
if errors:
|
|
105
|
+
lines.extend(["## Errors", ""])
|
|
106
|
+
for msg in errors:
|
|
107
|
+
lines.append(render_message_md(msg))
|
|
108
|
+
lines.append("")
|
|
109
|
+
|
|
110
|
+
if warnings:
|
|
111
|
+
lines.extend(["## Warnings", ""])
|
|
112
|
+
for msg in warnings:
|
|
113
|
+
lines.append(render_message_md(msg))
|
|
114
|
+
lines.append("")
|
|
115
|
+
|
|
116
|
+
return "\n".join(lines)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def render_report_md(
|
|
120
|
+
messages: list[A11yMessage],
|
|
121
|
+
*,
|
|
122
|
+
title: str = "Accessibility Report",
|
|
123
|
+
include_timestamp: bool = True,
|
|
124
|
+
include_summary: bool = True,
|
|
125
|
+
) -> str:
|
|
126
|
+
"""Render a full report as markdown.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
messages: Messages to include
|
|
130
|
+
title: Report title
|
|
131
|
+
include_timestamp: Whether to include generation timestamp
|
|
132
|
+
include_summary: Whether to include a summary section
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Markdown string
|
|
136
|
+
"""
|
|
137
|
+
lines = [f"# {title}", ""]
|
|
138
|
+
|
|
139
|
+
if include_timestamp:
|
|
140
|
+
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
141
|
+
lines.extend([f"*Generated: {now}*", ""])
|
|
142
|
+
|
|
143
|
+
if include_summary:
|
|
144
|
+
error_count = sum(1 for m in messages if m.level == Level.ERROR)
|
|
145
|
+
warn_count = sum(1 for m in messages if m.level == Level.WARN)
|
|
146
|
+
ok_count = sum(1 for m in messages if m.level == Level.OK)
|
|
147
|
+
|
|
148
|
+
status = "✅ Passing" if error_count == 0 else "❌ Failing"
|
|
149
|
+
lines.extend(
|
|
150
|
+
[
|
|
151
|
+
"## Summary",
|
|
152
|
+
"",
|
|
153
|
+
f"**Status:** {status}",
|
|
154
|
+
"",
|
|
155
|
+
f"- Errors: {error_count}",
|
|
156
|
+
f"- Warnings: {warn_count}",
|
|
157
|
+
f"- Passed: {ok_count}",
|
|
158
|
+
"",
|
|
159
|
+
]
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Group messages by level
|
|
163
|
+
errors = [m for m in messages if m.level == Level.ERROR]
|
|
164
|
+
warnings = [m for m in messages if m.level == Level.WARN]
|
|
165
|
+
passed = [m for m in messages if m.level == Level.OK]
|
|
166
|
+
|
|
167
|
+
if errors:
|
|
168
|
+
lines.extend(["## Errors", ""])
|
|
169
|
+
for msg in errors:
|
|
170
|
+
lines.append(render_message_md(msg))
|
|
171
|
+
lines.append("")
|
|
172
|
+
|
|
173
|
+
if warnings:
|
|
174
|
+
lines.extend(["## Warnings", ""])
|
|
175
|
+
for msg in warnings:
|
|
176
|
+
lines.append(render_message_md(msg))
|
|
177
|
+
lines.append("")
|
|
178
|
+
|
|
179
|
+
if passed:
|
|
180
|
+
lines.extend(["## Passed", ""])
|
|
181
|
+
for msg in passed:
|
|
182
|
+
lines.append(f"- ✅ `{msg.code}`: {msg.what}")
|
|
183
|
+
lines.append("")
|
|
184
|
+
|
|
185
|
+
return "\n".join(lines)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class MarkdownReporter:
|
|
189
|
+
"""Reporter for generating markdown reports with configuration."""
|
|
190
|
+
|
|
191
|
+
def __init__(
|
|
192
|
+
self,
|
|
193
|
+
*,
|
|
194
|
+
title: str = "Accessibility Report",
|
|
195
|
+
include_timestamp: bool = True,
|
|
196
|
+
include_passed: bool = False,
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Initialize the reporter.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
title: Report title
|
|
202
|
+
include_timestamp: Include generation timestamp
|
|
203
|
+
include_passed: Include passed checks in detail
|
|
204
|
+
"""
|
|
205
|
+
self.title = title
|
|
206
|
+
self.include_timestamp = include_timestamp
|
|
207
|
+
self.include_passed = include_passed
|
|
208
|
+
|
|
209
|
+
def render(self, messages: list[A11yMessage]) -> str:
|
|
210
|
+
"""Render messages to markdown.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
messages: Messages to render
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Markdown string
|
|
217
|
+
"""
|
|
218
|
+
return render_report_md(
|
|
219
|
+
messages,
|
|
220
|
+
title=self.title,
|
|
221
|
+
include_timestamp=self.include_timestamp,
|
|
222
|
+
include_summary=True,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def render_scorecard(self, scorecard: Scorecard) -> str:
|
|
226
|
+
"""Render a scorecard to markdown.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
scorecard: Scorecard to render
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Markdown string
|
|
233
|
+
"""
|
|
234
|
+
return render_scorecard_md(scorecard)
|
|
235
|
+
|
|
236
|
+
def write(self, messages: list[A11yMessage], stream: TextIO) -> None:
|
|
237
|
+
"""Write markdown report to a stream.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
messages: Messages to render
|
|
241
|
+
stream: Output stream
|
|
242
|
+
"""
|
|
243
|
+
stream.write(self.render(messages))
|
|
244
|
+
|
|
245
|
+
def write_file(self, messages: list[A11yMessage], path: str) -> None:
|
|
246
|
+
"""Write markdown report to a file.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
messages: Messages to render
|
|
250
|
+
path: Output file path
|
|
251
|
+
"""
|
|
252
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
253
|
+
self.write(messages, f)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def generate_badge_md(score: float, label: str = "a11y") -> str:
|
|
257
|
+
"""Generate a markdown badge showing the accessibility score.
|
|
258
|
+
|
|
259
|
+
NOTE: Badges are informational only. They do NOT imply WCAG conformance.
|
|
260
|
+
The score reflects policy compliance, not accessibility certification.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
score: Score percentage (0-100)
|
|
264
|
+
label: Badge label
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Markdown image reference for shields.io badge
|
|
268
|
+
"""
|
|
269
|
+
# Choose color based on score
|
|
270
|
+
if score >= 90:
|
|
271
|
+
color = "brightgreen"
|
|
272
|
+
elif score >= 70:
|
|
273
|
+
color = "yellow"
|
|
274
|
+
elif score >= 50:
|
|
275
|
+
color = "orange"
|
|
276
|
+
else:
|
|
277
|
+
color = "red"
|
|
278
|
+
|
|
279
|
+
# URL-encode the label and value
|
|
280
|
+
value = f"{score:.0f}%25" # %25 is URL-encoded %
|
|
281
|
+
return f""
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# Disclaimer for badges and scores
|
|
285
|
+
SCORE_DISCLAIMER = (
|
|
286
|
+
"**Note:** Scores and badges are informational only. They do NOT imply "
|
|
287
|
+
"WCAG conformance or accessibility certification. This tool checks policy "
|
|
288
|
+
"rules beyond minimum WCAG requirements."
|
|
289
|
+
)
|