"""MkDocs renderer for doc-forge. The MkDocs renderer generates MkDocs-compatible documentation from the doc-forge documentation model. It creates Markdown files with mkdocstrings directives and generates the necessary MkDocs configuration. This renderer follows the ADS specification by: - Emitting .md files with mkdocstrings directives - Using one file per module - Supporting build and serve operations via MkDocs APIs """ from __future__ import annotations import logging from pathlib import Path from typing import Any, Dict, List, Optional try: import mkdocs import mkdocs.commands.build import mkdocs.commands.serve import mkdocs.config import yaml except ImportError as e: raise ImportError( "mkdocs and mkdocstrings are required for MkDocs renderer. " "Install with: pip install doc-forge[mkdocs]" ) from e from docforge.model import Project, Module from .base import BaseRenderer, RendererConfig logger = logging.getLogger(__name__) class MkDocsRenderer(BaseRenderer): """MkDocs documentation renderer. The MkDocsRenderer converts the doc-forge documentation model into MkDocs-compatible source files. It generates Markdown files with mkdocstrings directives and creates the necessary MkDocs configuration. Generated output structure: docs/ ├── index.md ├── module1.md ├── module2.md └── mkdocs.yml Attributes: name: Renderer identifier ("mkdocs") """ def __init__(self) -> None: """Initialize the MkDocs renderer.""" super().__init__("mkdocs") def generate_sources(self, project: Project, out_dir: Path) -> None: """Generate MkDocs source files. Creates Markdown files for each module and generates the mkdocs.yml configuration file. Args: project: The documentation project to render out_dir: Directory where source files should be written """ self.validate_project(project) self.ensure_output_dir(out_dir) logger.info(f"Generating MkDocs sources in {out_dir}") # Generate index.md self._generate_index(project, out_dir) # Generate module files for module in project.get_all_modules(): self._generate_module_file(module, out_dir) # Generate mkdocs.yml self._generate_mkdocs_config(project, out_dir) logger.info(f"Generated {len(project.get_all_modules())} module files") def build(self, config: RendererConfig) -> None: """Build MkDocs documentation. Uses MkDocs build command to generate the final HTML documentation. Args: config: Configuration for the build process """ self.validate_project(config.project) mkdocs_yml = config.out_dir / "mkdocs.yml" if not mkdocs_yml.exists(): raise ValueError(f"mkdocs.yml not found in {config.out_dir}") logger.info(f"Building MkDocs documentation from {config.out_dir}") # Load MkDocs configuration mkdocs_config = mkdocs.config.load_config(str(mkdocs_yml)) # Run build mkdocs.commands.build.build(mkdocs_config) logger.info("MkDocs build completed successfully") def serve(self, config: RendererConfig) -> None: """Serve MkDocs documentation locally. Starts the MkDocs development server for local documentation preview and testing. Args: config: Configuration for the serve process """ self.validate_project(config.project) mkdocs_yml = config.out_dir / "mkdocs.yml" if not mkdocs_yml.exists(): raise ValueError(f"mkdocs.yml not found in {config.out_dir}") # Get serve options from config host = config.get_extra("host", "127.0.0.1") port = config.get_extra("port", 8000) logger.info(f"Serving MkDocs documentation at http://{host}:{port}") # Load MkDocs configuration mkdocs_config = mkdocs.config.load_config(str(mkdocs_yml)) # Run serve mkdocs.commands.serve.serve( mkdocs_config, dev_addr=f"{host}:{port}", livereload="livereload" in config.extra, ) def _generate_index(self, project: Project, out_dir: Path) -> None: """Generate the index.md file. Args: project: The documentation project out_dir: Output directory """ index_path = out_dir / "index.md" content = [f"# {project.name}"] if project.version: content.append(f"\n**Version:** {project.version}") content.append("\n## Modules") content.append("") for entry in project.nav.entries: content.append(f"- [{entry.title}]({entry.path}.md)") index_path.write_text("\n".join(content), encoding="utf-8") logger.debug(f"Generated {index_path}") def _generate_module_file(self, module: Module, out_dir: Path) -> None: """Generate a Markdown file for a module. Args: module: The module to generate documentation for out_dir: Output directory """ module_path = out_dir / f"{module.path}.md" content = [f"# {module.path}"] if module.has_docstring(): content.append(f"\n{module.docstring}") content.append(f"\n::: {module.path}") content.append(" options:") content.append(" show_source: true") content.append(" show_root_heading: true") module_path.write_text("\n".join(content), encoding="utf-8") logger.debug(f"Generated {module_path}") def _generate_mkdocs_config(self, project: Project, out_dir: Path) -> None: """Generate the mkdocs.yml configuration file. Args: project: The documentation project out_dir: Output directory """ config_path = out_dir / "mkdocs.yml" # Build navigation structure nav = [] for entry in project.nav.entries: nav.append({entry.title: f"{entry.path}.md"}) # MkDocs configuration config = { "site_name": project.name, "site_description": f"Documentation for {project.name}", "nav": nav, "plugins": ["mkdocstrings"], "theme": { "name": "material", "features": ["navigation.instant", "navigation.tracking"], }, "markdown_extensions": [ "codehilite", "admonition", "toc", ], "docs_dir": ".", "site_dir": "_site", } if project.version: config["site_version"] = project.version # Write configuration as YAML with open(config_path, "w", encoding="utf-8") as f: yaml.dump(config, f, default_flow_style=False, sort_keys=False) logger.debug(f"Generated {config_path}") class MkDocsConfig(RendererConfig): """MkDocs-specific renderer configuration. Extends the base RendererConfig with MkDocs-specific options. Attributes: theme: MkDocs theme to use extra_css: Additional CSS files extra_js: Additional JavaScript files plugins: MkDocs plugins to enable """ def __init__( self, out_dir: Path, project: Project, theme: str = "material", extra_css: Optional[List[str]] = None, extra_js: Optional[List[str]] = None, plugins: Optional[List[str]] = None, **extra, ) -> None: """Initialize MkDocs configuration. Args: out_dir: Output directory for generated files project: The documentation project being rendered theme: MkDocs theme to use extra_css: Additional CSS files extra_js: Additional JavaScript files plugins: MkDocs plugins to enable **extra: Additional configuration options """ super().__init__(out_dir, project, extra) self.theme = theme self.extra_css = extra_css or [] self.extra_js = extra_js or [] self.plugins = plugins or ["mkdocstrings"]