|
| 1 | +from typing import TYPE_CHECKING, Optional, List |
| 2 | +from collections import defaultdict |
| 3 | +from pathlib import Path |
| 4 | +import re |
| 5 | + |
| 6 | +from .util import format_type, DEFAULT_PLACEHOLDER |
| 7 | + |
| 8 | +if TYPE_CHECKING: |
| 9 | + from .cli import Radicli, Command |
| 10 | + |
| 11 | +DEFAULT_DOCS_COMNENT = "This file is auto-generated" |
| 12 | +whitespace_matcher = re.compile(r"\s+", re.ASCII) |
| 13 | + |
| 14 | + |
| 15 | +def document_cli( |
| 16 | + cli: "Radicli", |
| 17 | + title: Optional[str] = None, |
| 18 | + description: Optional[str] = None, |
| 19 | + comment: Optional[str] = DEFAULT_DOCS_COMNENT, |
| 20 | + path_root: Optional[Path] = None, |
| 21 | +) -> str: |
| 22 | + """Generate Markdown-formatted documentation for a CLI.""" |
| 23 | + lines = [] |
| 24 | + start_heading = 2 if title is not None else 1 |
| 25 | + if comment is not None: |
| 26 | + lines.append(f"<!-- {comment} -->") |
| 27 | + if title is not None: |
| 28 | + lines.append(f"# {title}") |
| 29 | + if description is not None: |
| 30 | + lines.append(_strip(description)) |
| 31 | + prefix = f"{cli.prog} " if cli.prog else "" |
| 32 | + cli_title = f"`{cli.prog}`" if cli.prog else "CLI" |
| 33 | + lines.append(f"{'#' * start_heading} {cli_title}") |
| 34 | + if cli.help: |
| 35 | + lines.append(cli.help) |
| 36 | + for cmd in cli.commands.values(): |
| 37 | + lines.extend(_command(cmd, start_heading + 1, prefix, path_root)) |
| 38 | + if cmd.name in cli.subcommands: |
| 39 | + for sub_cmd in cli.subcommands[cmd.name].values(): |
| 40 | + lines.extend(_command(sub_cmd, start_heading + 2, prefix, path_root)) |
| 41 | + for name in cli.subcommands: |
| 42 | + by_parent = defaultdict(list) |
| 43 | + if name not in cli.commands: |
| 44 | + sub_cmds = cli.subcommands[name] |
| 45 | + by_parent[name].extend(sub_cmds.values()) |
| 46 | + for parent, sub_cmds in by_parent.items(): # subcommands without placeholders |
| 47 | + lines.append(f"{'#' * (start_heading + 1)} `{prefix + parent}`") |
| 48 | + for sub_cmd in sub_cmds: |
| 49 | + lines.extend(_command(sub_cmd, start_heading + 2, prefix, path_root)) |
| 50 | + return "\n\n".join(lines) |
| 51 | + |
| 52 | + |
| 53 | +def _command( |
| 54 | + cmd: "Command", level: int, prefix: str, path_root: Optional[Path] |
| 55 | +) -> List[str]: |
| 56 | + lines = [] |
| 57 | + lines.append(f"{'#' * level} `{prefix + cmd.display_name}`") |
| 58 | + if cmd.description: |
| 59 | + lines.append(_strip(cmd.description)) |
| 60 | + if cmd.args: |
| 61 | + table = [] |
| 62 | + for ap_arg in cmd.args: |
| 63 | + name = f"`{ap_arg.arg.option or ap_arg.id}`" |
| 64 | + if ap_arg.arg.short: |
| 65 | + name += ", " + f"`{ap_arg.arg.short}`" |
| 66 | + default = "" |
| 67 | + if ap_arg.default is not DEFAULT_PLACEHOLDER: |
| 68 | + if isinstance(ap_arg.default, Path): |
| 69 | + default_value = ap_arg.default |
| 70 | + if path_root is not None: |
| 71 | + default_value = default_value.relative_to(path_root) |
| 72 | + else: |
| 73 | + default_value = repr(ap_arg.default) |
| 74 | + default = f"`{default_value}`" |
| 75 | + arg_type = format_type(ap_arg.display_type) |
| 76 | + arg_code = f"`{arg_type}`" if arg_type else "" |
| 77 | + table.append((name, arg_code, ap_arg.arg.help or "", default)) |
| 78 | + header = ["Argument", "Type", "Description", "Default"] |
| 79 | + head = f"| {' | '.join(header)} |" |
| 80 | + divider = f"| {' | '.join('---' for _ in range(len(header)))} |" |
| 81 | + body = "\n".join(f"| {' | '.join(row)} |" for row in table) |
| 82 | + lines.append(f"{head}\n{divider}\n{body}") |
| 83 | + return lines |
| 84 | + |
| 85 | + |
| 86 | +def _strip(text: str) -> str: |
| 87 | + return whitespace_matcher.sub(" ", text).strip() |
0 commit comments