Skip to content

Commit 5e71dc5

Browse files
committed
Strip rich-click markup from structured CLI help
1 parent dfa95f3 commit 5e71dc5

3 files changed

Lines changed: 133 additions & 3 deletions

File tree

‎great_docs/core.py‎

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4420,6 +4420,40 @@ def _get_cli_entry_point_name(self, package_name: str) -> str | None:
44204420

44214421
return None
44224422

4423+
@staticmethod
4424+
def _strip_rich_click_markup(text: str, cmd) -> str:
4425+
"""Strip rich-click markup when the command renders ``--help`` with it.
4426+
4427+
Rich-click interprets ``[b]...[/]`` and similar tags when its effective
4428+
``text_markup`` mode is ``"rich"``. Structured fields written into the
4429+
generated page must be plain text; the full ``--help`` block is already
4430+
rendered by Click and is left alone. Commands from plain Click, Typer,
4431+
or rich-click's ``markdown``/``ansi`` modes keep their text unchanged.
4432+
"""
4433+
if not text:
4434+
return text
4435+
4436+
try:
4437+
from rich.text import Text
4438+
from rich_click import RichCommand
4439+
except ImportError:
4440+
return text
4441+
4442+
if not isinstance(cmd, RichCommand):
4443+
return text
4444+
4445+
try:
4446+
formatter = cmd.context_class(cmd, info_name="").make_formatter()
4447+
if formatter.config.text_markup != "rich":
4448+
return text
4449+
except Exception: # pragma: no cover - optional dependency internals
4450+
return text
4451+
4452+
try:
4453+
return Text.from_markup(text).plain
4454+
except Exception: # pragma: no cover - malformed markup is left as-is
4455+
return text
4456+
44234457
def _extract_click_command(self, cmd, name: str, parent_path: str = "") -> dict:
44244458
"""
44254459
Extract information from a Click command or group.
@@ -4454,25 +4488,29 @@ def _extract_click_command(self, cmd, name: str, parent_path: str = "") -> dict:
44544488
if kind == "option":
44554489
opt_info = self._extract_click_option(param)
44564490
if opt_info:
4491+
opt_info["help"] = self._strip_rich_click_markup(opt_info["help"], cmd)
44574492
options.append(opt_info)
44584493
elif kind == "argument":
44594494
arg_info = self._extract_click_argument(param)
44604495
if arg_info:
44614496
arguments.append(arg_info)
44624497

4498+
command_help = self._strip_rich_click_markup(cmd.help or "", cmd)
4499+
44634500
# Parse description and examples from the help text
4464-
description, examples = self._parse_click_help_parts(cmd.help or "")
4501+
description, examples = self._parse_click_help_parts(command_help)
44654502

44664503
# Extract the short help (Click may auto-derive it, but sometimes it's
44674504
# empty for subcommands). Fall back to the first line/sentence of help.
44684505
short_help = getattr(cmd, "short_help", "") or ""
4469-
if not short_help and cmd.help:
4506+
if not short_help and command_help:
44704507
short_help = cmd.get_short_help_str(limit=150)
4508+
short_help = self._strip_rich_click_markup(short_help, cmd)
44714509

44724510
info = {
44734511
"name": name,
44744512
"full_path": full_path,
4475-
"help": cmd.help or "",
4513+
"help": command_help,
44764514
"short_help": short_help,
44774515
"help_text": help_text, # The actual --help output
44784516
"deprecated": getattr(cmd, "deprecated", False),

‎pyproject.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ dev = [
7171
"build>=0.7",
7272
"twine>=3.4",
7373
"typer>=0.12.0",
74+
"rich-click>=1.9.8",
7475
"great-docs[docs]",
7576
]
7677
docs = [

‎tests/test_cli_help_markup.py‎

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""Structured CLI help must not contain rich-click markup.
2+
3+
Rich-click renders ``[b]...[/]`` and ``[cyan]...[/]`` in ``--help``. The
4+
structured option/command fields are written into Quarto pages, where those
5+
tags would appear literally, so extraction has to strip them when rich markup
6+
is active.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from pathlib import Path
12+
13+
import click
14+
import pytest
15+
16+
from great_docs import GreatDocs
17+
18+
rich_click = pytest.importorskip("rich_click")
19+
20+
21+
def _make_gd(tmp_path: Path) -> GreatDocs:
22+
(tmp_path / "great-docs.yml").write_text("module: mypkg\n", encoding="utf-8")
23+
return GreatDocs(project_path=str(tmp_path))
24+
25+
26+
def test_rich_click_option_help_is_plain(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
27+
monkeypatch.setattr(rich_click.rich_click, "TEXT_MARKUP", "rich")
28+
29+
@rich_click.command()
30+
@rich_click.option(
31+
"--dev",
32+
is_flag=True,
33+
help="Add a [b]Dump Debug[/] button in [cyan]/tmp[/].",
34+
)
35+
def main(dev: bool) -> None:
36+
"""Demo command."""
37+
38+
info = _make_gd(tmp_path)._extract_click_command(main, "demo")
39+
40+
assert info["options"][0]["help"] == "Add a Dump Debug button in /tmp."
41+
42+
43+
def test_rich_click_command_help_is_plain(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
44+
monkeypatch.setattr(rich_click.rich_click, "TEXT_MARKUP", "rich")
45+
46+
@rich_click.command()
47+
def main() -> None:
48+
"""Say [b]hello[/] to [cyan]everyone[/]."""
49+
50+
info = _make_gd(tmp_path)._extract_click_command(main, "demo")
51+
52+
assert info["help"] == "Say hello to everyone."
53+
assert info["short_help"] == "Say hello to everyone."
54+
assert info["description"] == "Say hello to everyone."
55+
56+
57+
def test_rich_click_ansi_markup_is_kept(tmp_path: Path) -> None:
58+
@rich_click.command()
59+
@rich_click.option("--value", help="Literal [b] bracket.")
60+
def main(value: str) -> None:
61+
"""Demo command."""
62+
63+
info = _make_gd(tmp_path)._extract_click_command(main, "demo")
64+
65+
assert info["options"][0]["help"] == "Literal [b] bracket."
66+
67+
68+
def test_plain_click_help_keeps_brackets(tmp_path: Path) -> None:
69+
@click.command()
70+
@click.option("--value", help="Default is [default: 1].")
71+
def main(value: str) -> None:
72+
"""Demo command."""
73+
74+
info = _make_gd(tmp_path)._extract_click_command(main, "demo")
75+
76+
assert info["options"][0]["help"] == "Default is [default: 1]."
77+
78+
79+
def test_rich_click_markdown_markup_is_kept(
80+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
81+
) -> None:
82+
monkeypatch.setattr(rich_click.rich_click, "TEXT_MARKUP", "markdown")
83+
84+
@rich_click.command()
85+
@rich_click.option("--value", help="Literal [b] bracket and **bold**.")
86+
def main(value: str) -> None:
87+
"""Demo command."""
88+
89+
info = _make_gd(tmp_path)._extract_click_command(main, "demo")
90+
91+
assert info["options"][0]["help"] == "Literal [b] bracket and **bold**."

0 commit comments

Comments
 (0)