Skip to content

Commit 837f53a

Browse files
committed
feat(docs[_ext]): Make safety badges accessible and non-selectable
why: Badges should be screen-reader friendly (WCAG) and not pollute text when users copy tool names. what: - CSS: user-select: none on .sd-badge (prevents copy-selection) - Custom _safety_badge_node with HTML visitor that emits role="note" and aria-label="Safety tier: <tier>" on every badge span - Screen readers announce "Safety tier: readonly" etc. for context - Badge text stays in DOM (visible, not pseudo-element) - Add test for ARIA attribute storage on badge nodes
1 parent 5662071 commit 837f53a

3 files changed

Lines changed: 46 additions & 9 deletions

File tree

docs/_ext/fastmcp_autodoc.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -421,15 +421,38 @@ def _extract_enum_values(type_str: str) -> list[str]:
421421
return values
422422

423423

424-
def _safety_badge(safety: str) -> nodes.inline:
425-
"""Create a colored safety badge node."""
424+
class _safety_badge_node(nodes.General, nodes.Inline, nodes.Element):
425+
"""Custom node for safety badges with ARIA attributes in HTML output."""
426+
427+
428+
def _visit_safety_badge_html(self: t.Any, node: _safety_badge_node) -> None:
429+
"""Emit opening ``<span>`` with classes, role, and aria-label."""
430+
classes = " ".join(node.get("classes", []))
431+
safety = node.get("safety", "")
432+
self.body.append(
433+
f'<span class="{classes}" role="note" aria-label="Safety tier: {safety}">'
434+
)
435+
436+
437+
def _depart_safety_badge_html(self: t.Any, node: _safety_badge_node) -> None:
438+
"""Close the ``<span>``."""
439+
self.body.append("</span>")
440+
441+
442+
def _safety_badge(safety: str) -> _safety_badge_node:
443+
"""Create a colored safety badge node with ARIA attributes."""
426444
_base = ["sd-sphinx-override", "sd-badge"]
427445
classes = {
428446
"readonly": [*_base, "sd-bg-success", "sd-bg-text-success"],
429447
"mutating": [*_base, "sd-bg-warning", "sd-bg-text-warning"],
430448
"destructive": [*_base, "sd-bg-danger", "sd-bg-text-danger"],
431449
}
432-
badge = nodes.inline("", safety, classes=classes.get(safety, []))
450+
badge = _safety_badge_node(
451+
"",
452+
nodes.Text(safety),
453+
classes=classes.get(safety, []),
454+
safety=safety,
455+
)
433456
return badge
434457

435458

@@ -926,6 +949,10 @@ def _badge_role(
926949

927950
def setup(app: Sphinx) -> ExtensionMetadata:
928951
"""Register the fastmcp_autodoc extension."""
952+
app.add_node(
953+
_safety_badge_node,
954+
html=(_visit_safety_badge_html, _depart_safety_badge_html),
955+
)
929956
app.connect("builder-inited", _collect_tools)
930957
app.connect("doctree-read", _register_tool_labels)
931958
app.connect("doctree-resolved", _add_section_badges)

docs/_static/css/custom.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,8 @@ img[src*="codecov.io"] {
245245
.sd-badge {
246246
font-size: 0.75rem;
247247
vertical-align: middle;
248+
user-select: none; /* Don't include badge text when copying */
249+
-webkit-user-select: none; /* Safari */
248250
}
249251

250252
.sd-badge.sd-bg-warning {

tests/docs/_ext/test_fastmcp_autodoc.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ def test_make_type_cell_single_type() -> None:
432432

433433

434434
def test_safety_badge_classes() -> None:
435-
"""_safety_badge creates inline nodes with correct CSS classes."""
435+
"""_safety_badge creates badge nodes with correct CSS classes."""
436436
badge = fastmcp_autodoc._safety_badge("readonly")
437437
assert "sd-bg-success" in badge["classes"]
438438

@@ -443,6 +443,16 @@ def test_safety_badge_classes() -> None:
443443
assert "sd-bg-danger" in badge["classes"]
444444

445445

446+
def test_safety_badge_aria_attributes() -> None:
447+
"""_safety_badge stores safety tier for ARIA output."""
448+
badge = fastmcp_autodoc._safety_badge("readonly")
449+
assert badge["safety"] == "readonly"
450+
assert badge.astext() == "readonly"
451+
452+
badge = fastmcp_autodoc._safety_badge("destructive")
453+
assert badge["safety"] == "destructive"
454+
455+
446456
# ---------------------------------------------------------------------------
447457
# _make_type_xref
448458
# ---------------------------------------------------------------------------
@@ -531,15 +541,13 @@ def _make_doc_with_section(
531541

532542
def test_add_section_badges_appends_badge_on_tools_index() -> None:
533543
"""_add_section_badges appends badge when fromdocname is tools/index."""
534-
from docutils import nodes
535-
536544
doc, _section, title = _make_doc_with_section("inspect", "Inspect")
537545

538546
fastmcp_autodoc._add_section_badges(None, doc, "tools/index")
539547

540548
assert len(title.children) == 3
541549
badge = title.children[2]
542-
assert isinstance(badge, nodes.inline)
550+
assert isinstance(badge, fastmcp_autodoc._safety_badge_node)
543551
assert "sd-bg-success" in badge["classes"]
544552
assert badge.astext() == "readonly"
545553

@@ -585,7 +593,7 @@ def test_add_section_badges_parenthesized_on_index() -> None:
585593
assert len(title.children) == 2
586594
assert title.children[0].astext() == "Inspect "
587595
badge = title.children[1]
588-
assert isinstance(badge, nodes.inline)
596+
assert isinstance(badge, fastmcp_autodoc._safety_badge_node)
589597
assert "sd-bg-success" in badge["classes"]
590598
assert badge.astext() == "readonly"
591599

@@ -598,7 +606,7 @@ def test_add_section_badges_works_on_homepage() -> None:
598606

599607
assert len(title.children) == 3
600608
badge = title.children[2]
601-
assert isinstance(badge, nodes.inline)
609+
assert isinstance(badge, fastmcp_autodoc._safety_badge_node)
602610
assert "sd-bg-warning" in badge["classes"]
603611

604612

0 commit comments

Comments
 (0)