Spaces:
Sleeping
Sleeping
| """Documentation generation functions for cyclopts apps.""" | |
| from typing import TYPE_CHECKING | |
| from cyclopts._markup import extract_text | |
| from cyclopts.core import DEFAULT_FORMAT | |
| from cyclopts.docs.base import ( | |
| adjust_filters_for_subcommand, | |
| apply_usage_name, | |
| build_command_chain, | |
| extract_description, | |
| extract_usage, | |
| format_usage_line, | |
| generate_anchor, | |
| get_app_info, | |
| is_all_builtin_flags, | |
| iterate_commands, | |
| normalize_command_filters, | |
| should_include_command, | |
| should_show_commands_list, | |
| should_show_usage, | |
| ) | |
| if TYPE_CHECKING: | |
| from cyclopts.core import App | |
| def _collect_commands_for_toc( | |
| app: "App", | |
| include_hidden: bool = False, | |
| prefix: str = "", | |
| commands_filter: list[str] | None = None, | |
| exclude_commands: list[str] | None = None, | |
| parent_path: list[str] | None = None, | |
| skip_filtered_command: bool = False, | |
| ) -> list[tuple[str, "App"]]: | |
| """Recursively collect all commands for table of contents. | |
| Returns a list of (display_name, app) tuples. | |
| """ | |
| commands = [] | |
| if parent_path is None: | |
| parent_path = [] | |
| normalized_commands_filter, normalized_exclude_commands = normalize_command_filters( | |
| commands_filter, exclude_commands | |
| ) | |
| for name, subapp in iterate_commands(app, include_hidden): | |
| if not should_include_command( | |
| name, parent_path, normalized_commands_filter, normalized_exclude_commands, subapp | |
| ): | |
| continue | |
| # Skip the command itself if it's the single filtered command with subcommands | |
| # (we'll include its children directly instead) | |
| skip_this_command = skip_filtered_command and subapp._commands | |
| if not skip_this_command: | |
| display_name = f"{prefix}{name}" if prefix else name | |
| commands.append((display_name, subapp)) | |
| # Collect nested commands | |
| nested_path = parent_path + [name] | |
| # Always include parent in prefix for nested commands (for correct paths) | |
| nested_prefix = f"{prefix}{name} " | |
| nested = _collect_commands_for_toc( | |
| subapp, | |
| include_hidden=include_hidden, | |
| prefix=nested_prefix, | |
| commands_filter=commands_filter, | |
| exclude_commands=exclude_commands, | |
| parent_path=nested_path, | |
| skip_filtered_command=False, # Only skip at the top level | |
| ) | |
| commands.extend(nested) | |
| return commands | |
| def _generate_toc_entries(lines: list[str], commands: list[tuple[str, "App"]]) -> None: | |
| """Generate TOC entries with proper indentation. | |
| Parameters | |
| ---------- | |
| lines : list[str] | |
| List to append TOC entries to. | |
| commands : list[tuple[str, "App"]] | |
| List of (display_name, app) tuples. | |
| """ | |
| anchor_counts: dict[str, int] = {} | |
| for display_name, _app in commands: | |
| depth = display_name.count(" ") - 1 | |
| indent = " " * depth | |
| cmd_name = display_name.split()[-1] | |
| anchor = generate_anchor(display_name) | |
| if anchor in anchor_counts: | |
| anchor_counts[anchor] += 1 | |
| anchor = f"{anchor}_{anchor_counts[anchor]}" | |
| else: | |
| anchor_counts[anchor] = 0 | |
| lines.append(f"{indent}- [`{cmd_name}`](#{anchor})") | |
| def _build_command_map(app: "App", include_hidden: bool = True) -> dict[str, "App"]: | |
| """Build mapping of command names to App objects. | |
| Parameters | |
| ---------- | |
| app : App | |
| The app to extract commands from. | |
| include_hidden : bool | |
| Whether to include hidden commands. | |
| Returns | |
| ------- | |
| dict[str, App] | |
| Mapping of command names to App instances. | |
| """ | |
| command_map = {} | |
| if app._commands: | |
| for name, subapp in iterate_commands(app, include_hidden): | |
| command_map[name] = subapp | |
| return command_map | |
| def _append_if_present(lines: list[str], content: str, add_blank: bool = True) -> None: | |
| """Append content to lines if present, optionally adding blank line. | |
| Parameters | |
| ---------- | |
| lines : list[str] | |
| List to append to. | |
| content : str | |
| Content to append (only if non-empty). | |
| add_blank : bool | |
| Whether to add a blank line after content. | |
| """ | |
| if content: | |
| lines.append(content) | |
| if add_blank: | |
| lines.append("") | |
| def _render_description_section(app: "App", help_format: str, lines: list[str]) -> None: | |
| """Extract and render app description. | |
| Parameters | |
| ---------- | |
| app : App | |
| The app to extract description from. | |
| help_format : str | |
| Help format (e.g., "markdown", "rich"). | |
| lines : list[str] | |
| List to append description to. | |
| """ | |
| description = extract_description(app, help_format) | |
| if description: | |
| # Preserve markup when help_format matches output format (markdown) | |
| preserve = help_format in ("markdown", "md") | |
| desc_text = extract_text(description, None, preserve_markup=preserve) | |
| if desc_text: | |
| lines.append(desc_text.strip()) | |
| lines.append("") | |
| def _render_usage_section( | |
| app: "App", | |
| command_chain: list[str], | |
| lines: list[str], | |
| usage_name: str | None = None, | |
| ) -> None: | |
| """Render usage console block. | |
| Parameters | |
| ---------- | |
| app : App | |
| The app to extract usage from. | |
| command_chain : list[str] | |
| Command chain for usage line. | |
| lines : list[str] | |
| List to append usage to. | |
| usage_name : str | None | |
| Optional replacement for the chain's root in Usage: lines only. | |
| """ | |
| if should_show_usage(app): | |
| usage = extract_usage(app) | |
| if usage: | |
| lines.append("```console") | |
| if isinstance(usage, str): | |
| usage_text = usage | |
| else: | |
| usage_text = extract_text(usage, None, preserve_markup=False) | |
| display_chain = apply_usage_name(command_chain, usage_name) | |
| usage_line = format_usage_line(usage_text, display_chain) | |
| lines.append(usage_line) | |
| lines.append("```") | |
| lines.append("") | |
| def _render_toc( | |
| app: "App", | |
| app_name: str, | |
| include_hidden: bool, | |
| commands_filter: list[str] | None, | |
| exclude_commands: list[str] | None, | |
| skip_filtered_command: bool, | |
| lines: list[str], | |
| ) -> None: | |
| """Generate and render table of contents. | |
| Parameters | |
| ---------- | |
| app : App | |
| The app to generate TOC for. | |
| app_name : str | |
| Application name for TOC prefixes. | |
| include_hidden : bool | |
| Whether to include hidden commands. | |
| commands_filter : list[str] | None | |
| Commands to include. | |
| exclude_commands : list[str] | None | |
| Commands to exclude. | |
| skip_filtered_command : bool | |
| Whether to skip the single filtered command in TOC. | |
| lines : list[str] | |
| List to append TOC to. | |
| """ | |
| # Collect all commands recursively for TOC | |
| toc_commands = _collect_commands_for_toc( | |
| app, | |
| include_hidden=include_hidden, | |
| prefix=f"{app_name} " if app_name else "", | |
| commands_filter=commands_filter, | |
| exclude_commands=exclude_commands, | |
| skip_filtered_command=skip_filtered_command, | |
| ) | |
| if toc_commands: | |
| lines.append("## Table of Contents") | |
| lines.append("") | |
| _generate_toc_entries(lines, toc_commands) | |
| lines.append("") | |
| def _render_parameter_panel(panel, formatter, lines: list[str]) -> None: | |
| """Render a parameter panel as-is. | |
| Parameters | |
| ---------- | |
| panel : HelpPanel | |
| The parameter panel to render. | |
| formatter : MarkdownFormatter | |
| Formatter to use for rendering. | |
| lines : list[str] | |
| List to append rendered content to. | |
| """ | |
| # Render panel content first to check if there's anything | |
| formatter.reset() | |
| panel_copy = panel.copy(title="") | |
| formatter(None, None, panel_copy) | |
| output = formatter.get_output().strip() | |
| # Only render if there's actual content | |
| if output: | |
| if panel.title: | |
| lines.append(f"**{panel.title}**:\n") | |
| lines.append(output) | |
| lines.append("") | |
| def _filter_command_entries( | |
| entries: list, | |
| command_map: dict[str, "App"], | |
| parent_path: list[str], | |
| normalized_filter: set[str] | None, | |
| normalized_exclude: set[str] | None, | |
| ) -> list: | |
| """Filter command entries based on inclusion/exclusion rules. | |
| Parameters | |
| ---------- | |
| entries : list | |
| Command entries to filter. | |
| command_map : dict[str, App] | |
| Mapping of command names to App objects. | |
| parent_path : list[str] | |
| Parent command path. | |
| normalized_filter : set[str] | None | |
| Normalized filter set. | |
| normalized_exclude : set[str] | None | |
| Normalized exclude set. | |
| Returns | |
| ------- | |
| list | |
| Filtered command entries. | |
| """ | |
| filtered_entries = [] | |
| for entry in entries: | |
| if entry.names: | |
| cmd_name = entry.names[0] | |
| subapp = command_map.get(cmd_name) | |
| if subapp is None: | |
| # If command not in map and no filters, include it | |
| if normalized_filter is None and normalized_exclude is None: | |
| filtered_entries.append(entry) | |
| else: | |
| # Check if command should be included | |
| if should_include_command(cmd_name, parent_path, normalized_filter, normalized_exclude, subapp): | |
| filtered_entries.append(entry) | |
| return filtered_entries | |
| def generate_markdown_docs( | |
| app: "App", | |
| recursive: bool = True, | |
| include_hidden: bool = False, | |
| heading_level: int = 1, | |
| max_heading_level: int = 6, | |
| command_chain: list[str] | None = None, | |
| generate_toc: bool = True, | |
| flatten_commands: bool = False, | |
| commands_filter: list[str] | None = None, | |
| exclude_commands: list[str] | None = None, | |
| no_root_title: bool = False, | |
| code_block_title: bool = False, | |
| skip_preamble: bool = False, | |
| usage_name: str | None = None, | |
| ) -> str: | |
| """Generate markdown documentation for a CLI application. | |
| Parameters | |
| ---------- | |
| app : App | |
| The cyclopts App instance to document. | |
| recursive : bool | |
| If True, generate documentation for all subcommands recursively. | |
| Default is True. | |
| include_hidden : bool | |
| If True, include hidden commands/parameters in documentation. | |
| Default is False. | |
| heading_level : int | |
| Starting heading level for the main application title. | |
| Default is 1 (single #). | |
| max_heading_level : int | |
| Maximum heading level to use. Headings deeper than this will be capped | |
| at this level. Standard Markdown supports levels 1-6. | |
| Default is 6. | |
| command_chain : list[str] | |
| Internal parameter to track command hierarchy. | |
| Default is None. | |
| generate_toc : bool | |
| If True, generate a table of contents for multi-command apps. | |
| Default is True. | |
| flatten_commands : bool | |
| If True, generate all commands at the same heading level instead of nested. | |
| Default is False. | |
| commands_filter : list[str] | None | |
| If specified, only include commands in this list. | |
| Supports nested command paths like "db.migrate". | |
| Default is None (include all commands). | |
| exclude_commands : list[str] | None | |
| If specified, exclude commands in this list. | |
| Supports nested command paths like "db.migrate". | |
| Default is None (no exclusions). | |
| no_root_title : bool | |
| If True, skip the root application title. Used for plugin contexts. | |
| Default is False. | |
| skip_preamble : bool | |
| If True, skip the description and usage sections for the target command | |
| when filtering to a single command via ``commands_filter``. | |
| Useful when the user provides their own section introduction. | |
| Default is False. | |
| usage_name : str | None | |
| Optional replacement for the root app name used in ``Usage:`` lines | |
| only. Headings and TOC anchors still use ``app.name[0]``. | |
| Default is None (use ``app.name[0]`` as before). | |
| Returns | |
| ------- | |
| str | |
| The generated markdown documentation. | |
| """ | |
| from cyclopts.help.formatters.markdown import MarkdownFormatter | |
| # Build the main documentation | |
| lines = [] | |
| if command_chain is None: | |
| command_chain = [] | |
| is_root = True | |
| else: | |
| is_root = False | |
| # Determine if we should skip the current level's content | |
| # When filtering to a single command, skip the root app content | |
| skip_current_level = is_root and commands_filter is not None and len(commands_filter) == 1 | |
| # Determine the app name and full command path | |
| app_name, full_command, base_title = get_app_info(app, command_chain) | |
| # Always use full command path for nested commands to avoid anchor collisions | |
| # (e.g., "files cp" and "other cp" would both generate #cp without this) | |
| if command_chain: | |
| # Show full command path (same for both hierarchical and flattened modes) | |
| title = f"`{full_command}`" if code_block_title else full_command | |
| else: | |
| # Root app: use base title | |
| title = base_title | |
| # Add title for all levels (unless skipping root title or skipping current level entirely) | |
| if not skip_current_level and not (no_root_title and is_root): | |
| effective_level = min(heading_level, max_heading_level) | |
| lines.append(f"{'#' * effective_level} {title}") | |
| lines.append("") | |
| # Get help format (needed for both current level and recursive docs) | |
| help_format = app.app_stack.resolve("help_format", fallback=DEFAULT_FORMAT) | |
| # Add usage section first (skip if skipping current level or skip_preamble is True) | |
| if not skip_current_level and not skip_preamble: | |
| _render_usage_section(app, command_chain, lines, usage_name=usage_name) | |
| # Add application description (skip if skipping current level or skip_preamble is True) | |
| if not skip_current_level and not skip_preamble: | |
| _render_description_section(app, help_format, lines) | |
| # Generate table of contents if this is the root level and has commands | |
| if generate_toc and not command_chain and app._commands: | |
| _render_toc(app, app_name, include_hidden, commands_filter, exclude_commands, skip_current_level, lines) | |
| # Get help panels for the current app (skip if skipping current level) | |
| # Use app_stack context - if caller set up parent context, it will be stacked | |
| if not skip_current_level: | |
| with app.app_stack([app]): | |
| help_panels_with_groups = app._assemble_help_panels([], help_format) | |
| # Set up command filtering (used for command panels only) | |
| normalized_commands_filter, normalized_exclude_commands = normalize_command_filters( | |
| commands_filter, exclude_commands | |
| ) | |
| parent_path = [] | |
| # Build a mapping of command names to App objects for filtering | |
| command_map = _build_command_map(app, include_hidden=True) | |
| # Create formatter | |
| formatter = MarkdownFormatter( | |
| heading_level=heading_level + 1, | |
| include_hidden=include_hidden, | |
| table_style="list", | |
| ) | |
| # Iterate through panels in the order provided by _assemble_help_panels (already sorted) | |
| for group, panel in help_panels_with_groups: | |
| # Skip hidden groups | |
| if not include_hidden and group and not group.show: | |
| continue | |
| if panel.format == "command": | |
| # Always filter out built-in flags (--help, --version) from command panels | |
| # These are standard CLI flags, not commands, and shouldn't appear here | |
| command_entries = [e for e in panel.entries if not (e.names and is_all_builtin_flags(app, e.names))] | |
| if not command_entries: | |
| continue # Skip empty panel | |
| # Apply command filtering | |
| filtered_entries = _filter_command_entries( | |
| command_entries, command_map, parent_path, normalized_commands_filter, normalized_exclude_commands | |
| ) | |
| # Only render if there are filtered entries | |
| if filtered_entries: | |
| if panel.title: | |
| lines.append(f"**{panel.title}**:\n") | |
| # Render command entries with hyperlinks to their sections | |
| for entry in filtered_entries: | |
| if entry.names: | |
| cmd_name = entry.names[0] | |
| # Generate anchor for the full command path | |
| # Use full_command (not app_name) to include the complete path for nested apps | |
| full_cmd_path = f"{full_command} {cmd_name}" | |
| anchor = generate_anchor(full_cmd_path) | |
| desc_text = ( | |
| extract_text(entry.description, None, preserve_markup=True) if entry.description else "" | |
| ) | |
| if desc_text: | |
| lines.append(f"* [`{cmd_name}`](#{anchor}): {desc_text}") | |
| else: | |
| lines.append(f"* [`{cmd_name}`](#{anchor})") | |
| lines.append("") | |
| elif panel.format == "parameter": | |
| # Handle parameter panels - split into arguments and options if needed | |
| _render_parameter_panel(panel, formatter, lines) | |
| else: | |
| # When skipping current level, still need to set up filter variables for recursive docs | |
| normalized_commands_filter, normalized_exclude_commands = normalize_command_filters( | |
| commands_filter, exclude_commands | |
| ) | |
| parent_path = [] | |
| # Handle recursive documentation for subcommands | |
| if app._commands: | |
| # Iterate through registered commands using iterate_commands helper | |
| # This automatically resolves CommandSpec instances | |
| for name, subapp in iterate_commands(app, include_hidden): | |
| if not should_include_command( | |
| name, parent_path, normalized_commands_filter, normalized_exclude_commands, subapp | |
| ): | |
| continue | |
| # Build the command chain for this subcommand | |
| sub_command_chain = build_command_chain(command_chain, name, app_name) | |
| # Determine heading level for subcommand | |
| if flatten_commands: | |
| sub_heading_level = heading_level | |
| elif no_root_title and not command_chain: | |
| # When root title is skipped, subcommands "take over" the root heading level | |
| sub_heading_level = heading_level | |
| else: | |
| sub_heading_level = heading_level + 1 | |
| # Check if we should skip this command's title heading | |
| # Skip title when: root was skipped (single command filter) AND this is the direct target | |
| # OR this is an intermediate command on the path to a nested target | |
| # This allows the markdown author's section title to serve as the heading | |
| is_single_filter = commands_filter is not None and len(commands_filter) == 1 | |
| is_exact_target = is_single_filter and commands_filter is not None and name == commands_filter[0] | |
| is_intermediate_path = ( | |
| is_single_filter and commands_filter is not None and commands_filter[0].startswith(name + ".") | |
| ) | |
| skip_this_command_title = skip_current_level and is_exact_target | |
| # Also skip intermediate commands entirely when skip_preamble is set | |
| skip_intermediate = skip_preamble and skip_current_level and is_intermediate_path | |
| # Skip preamble for the exact target when skip_preamble is set (even in recursive calls) | |
| skip_target_preamble = skip_preamble and is_exact_target | |
| # Generate subcommand title (skip if this is the single filtered command at root level, | |
| # or if this is an intermediate command and skip_preamble is set) | |
| if not skip_this_command_title and not skip_intermediate: | |
| # Always use full command path to avoid anchor collisions | |
| display_name = " ".join(sub_command_chain) | |
| display_fmt = f"`{display_name}`" if code_block_title else display_name | |
| effective_sub_level = min(sub_heading_level, max_heading_level) | |
| lines.append(f"{'#' * effective_sub_level} {display_fmt}") | |
| lines.append("") | |
| # Get subapp help - show description, usage, and panels for included commands | |
| # Skip preamble (description + usage) if: | |
| # - skip_preamble is True and this is the exact target (even in recursive calls) | |
| # - or this is an intermediate command on the path to a nested target | |
| skip_this_preamble = skip_target_preamble or skip_intermediate | |
| # Include parent app in the stack so default_parameter is properly inherited | |
| with subapp.app_stack([app, subapp]): | |
| sub_help_format = subapp.app_stack.resolve("help_format", fallback=help_format) | |
| # Preserve markup when sub_help_format matches output format (markdown) | |
| preserve_sub = sub_help_format in ("markdown", "md") | |
| if not skip_this_preamble: | |
| # Generate usage first for subcommand | |
| _render_usage_section(subapp, sub_command_chain, lines, usage_name=usage_name) | |
| _render_description_section(subapp, sub_help_format, lines) | |
| # Only show subcommand panels if we're in recursive mode | |
| # (Otherwise we just show the basic info about this command) | |
| if recursive: | |
| # Get help panels for subcommand (already sorted) | |
| sub_panels = subapp._assemble_help_panels([], sub_help_format) | |
| # Set up command filtering for this subcommand | |
| sub_commands_filter_for_panel, sub_exclude_commands_for_panel = adjust_filters_for_subcommand( | |
| name, normalized_commands_filter, normalized_exclude_commands | |
| ) | |
| normalized_sub_filter_panel, normalized_sub_exclude_panel = normalize_command_filters( | |
| sub_commands_filter_for_panel, sub_exclude_commands_for_panel | |
| ) | |
| # Build a map of command names to App objects for filtering | |
| sub_command_map = _build_command_map(subapp, include_hidden=True) | |
| # Build parent path for nested commands | |
| # Use empty path since filter was already adjusted to strip current level's prefix | |
| nested_parent_path_for_panel = [] | |
| # Create formatter | |
| if flatten_commands: | |
| panel_heading_level = heading_level + 1 | |
| else: | |
| panel_heading_level = heading_level + 2 | |
| sub_formatter = MarkdownFormatter( | |
| heading_level=panel_heading_level, include_hidden=include_hidden, table_style="list" | |
| ) | |
| # Check if we'll be recursively documenting commands | |
| will_recurse = recursive and subapp._commands | |
| # Iterate through panels in order | |
| for group, panel in sub_panels: | |
| # Skip hidden groups | |
| if not include_hidden and group and not group.show: | |
| continue | |
| if panel.format == "command" and should_show_commands_list(subapp): | |
| # Always filter out built-in flags (--help, --version) from command panels | |
| command_entries_list = [ | |
| e for e in panel.entries if not (e.names and is_all_builtin_flags(subapp, e.names)) | |
| ] | |
| if not command_entries_list: | |
| continue # Skip empty panel | |
| # Apply command filtering for command panels | |
| if will_recurse: | |
| # Show simple command list | |
| command_entries = [] | |
| for entry in command_entries_list: | |
| if entry.names: | |
| cmd_name = entry.names[0] | |
| sub_cmd_app = sub_command_map.get(cmd_name) | |
| if sub_cmd_app and not should_include_command( | |
| cmd_name, | |
| nested_parent_path_for_panel, | |
| normalized_sub_filter_panel, | |
| normalized_sub_exclude_panel, | |
| sub_cmd_app, | |
| ): | |
| continue | |
| desc_text = ( | |
| extract_text(entry.description, None, preserve_markup=preserve_sub) | |
| if entry.description | |
| else "" | |
| ) | |
| # Generate anchor for the full command path | |
| full_cmd_path = " ".join(sub_command_chain + [cmd_name]) | |
| anchor = generate_anchor(full_cmd_path) | |
| if desc_text: | |
| command_entries.append(f"* [`{cmd_name}`](#{anchor}): {desc_text}") | |
| else: | |
| command_entries.append(f"* [`{cmd_name}`](#{anchor})") | |
| if command_entries: | |
| if panel.title: | |
| lines.append(f"**{panel.title}**:\n") | |
| lines.extend(command_entries) | |
| lines.append("") | |
| else: | |
| # Show full command panel | |
| filtered_entries = [] | |
| for entry in command_entries_list: | |
| if entry.names: | |
| cmd_name = entry.names[0] | |
| sub_cmd_app = sub_command_map.get(cmd_name) | |
| if sub_cmd_app and not should_include_command( | |
| cmd_name, | |
| nested_parent_path_for_panel, | |
| normalized_sub_filter_panel, | |
| normalized_sub_exclude_panel, | |
| sub_cmd_app, | |
| ): | |
| continue | |
| filtered_entries.append(entry) | |
| if filtered_entries: | |
| if panel.title: | |
| lines.append(f"**{panel.title}**:\n") | |
| sub_formatter.reset() | |
| filtered_panel = panel.__class__( | |
| title="", | |
| entries=filtered_entries, | |
| format=panel.format, | |
| description=panel.description, | |
| ) | |
| sub_formatter(None, None, filtered_panel) | |
| output = sub_formatter.get_output().strip() | |
| if output: | |
| lines.append(output) | |
| lines.append("") | |
| elif panel.format == "parameter": | |
| # Handle parameter panels - split into arguments and options if needed | |
| _render_parameter_panel(panel, sub_formatter, lines) | |
| # Process nested commands INSIDE the with block so context is preserved | |
| if recursive and subapp._commands: | |
| sub_commands_filter, sub_exclude_commands = adjust_filters_for_subcommand( | |
| name, normalized_commands_filter, normalized_exclude_commands | |
| ) | |
| normalized_sub_filter, normalized_sub_exclude = normalize_command_filters( | |
| sub_commands_filter, sub_exclude_commands | |
| ) | |
| # Build parent path for nested commands | |
| # Use empty path since filter was already adjusted to strip current level's prefix | |
| nested_parent_path = [] | |
| for nested_name, nested_app in iterate_commands(subapp, include_hidden): | |
| if not should_include_command( | |
| nested_name, nested_parent_path, normalized_sub_filter, normalized_sub_exclude, nested_app | |
| ): | |
| continue | |
| # Build nested command chain (always use full path for correct usage) | |
| nested_command_chain = build_command_chain(sub_command_chain, nested_name, app_name) | |
| # Determine heading level for nested commands | |
| if flatten_commands: | |
| nested_heading_level = heading_level | |
| elif skip_this_command_title: | |
| # When parent command's title was skipped, promote nested commands to parent's level | |
| nested_heading_level = sub_heading_level | |
| else: | |
| nested_heading_level = sub_heading_level + 1 | |
| # Determine commands_filter for the recursive call | |
| # Adjust filter to strip current command's prefix for the nested level | |
| if normalized_sub_filter: | |
| nested_commands_filter, _ = adjust_filters_for_subcommand( | |
| nested_name, normalized_sub_filter, normalized_sub_exclude | |
| ) | |
| else: | |
| nested_commands_filter = None | |
| # Check if this nested command is the target for skip_preamble purposes | |
| # This handles nested paths like "parent.child" where "child" is the target | |
| nested_is_target = ( | |
| skip_preamble | |
| and sub_commands_filter is not None | |
| and len(sub_commands_filter) == 1 | |
| and nested_name == sub_commands_filter[0] | |
| ) | |
| # Also check if this is an intermediate on a deeper path | |
| nested_is_intermediate = ( | |
| skip_preamble | |
| and sub_commands_filter is not None | |
| and len(sub_commands_filter) == 1 | |
| and sub_commands_filter[0].startswith(nested_name + ".") | |
| ) | |
| # Set up context for nested_app, then recurse | |
| # The recursive call's app_stack([app]) will stack on top of this | |
| with nested_app.app_stack([subapp, nested_app]): | |
| nested_docs = generate_markdown_docs( | |
| nested_app, | |
| recursive=recursive, | |
| include_hidden=include_hidden, | |
| heading_level=nested_heading_level, | |
| max_heading_level=max_heading_level, | |
| command_chain=nested_command_chain, | |
| generate_toc=False, # Don't generate TOC for nested commands | |
| flatten_commands=flatten_commands, | |
| commands_filter=nested_commands_filter, | |
| exclude_commands=sub_exclude_commands, | |
| no_root_title=nested_is_intermediate, # Skip title for intermediate paths | |
| code_block_title=code_block_title, | |
| skip_preamble=nested_is_target or nested_is_intermediate, | |
| usage_name=usage_name, | |
| ) | |
| # Just append the generated docs - no title replacement | |
| lines.append(nested_docs) | |
| lines.append("") | |
| # Join all lines into final document | |
| doc = "\n".join(lines).rstrip() + "\n" | |
| # Normalize multiple consecutive blank lines to a single blank line | |
| # This ensures consistent spacing regardless of how content was assembled | |
| import re | |
| doc = re.sub(r"\n{3,}", "\n\n", doc) | |
| return doc | |