""" MkDocsRenderer Generates Markdown source files compatible with MkDocs Material and mkdocstrings, ensuring: - Root index.md always exists - Parent package indexes are created automatically - Child modules are linked in parent index files - README.md can be generated from the root package docstring """ from pathlib import Path from docforge.models import Project, Module class MkDocsRenderer: """ Renderer that generates Markdown source files formatted for the MkDocs 'mkdocstrings' plugin. """ name = "mkdocs" # ------------------------- # Public API # ------------------------- def generate_sources( self, project: Project, out_dir: Path, module_is_source: bool | None = None, ) -> None: """ Produce a set of Markdown files in the output directory based on the provided Project models. Args: project: The project model to render. out_dir: Target directory for generated Markdown. module_is_source: If True, treat the module as the root folder. """ out_dir.mkdir(parents=True, exist_ok=True) self._ensure_root_index(project, out_dir) modules = list(project.get_all_modules()) paths = {m.path for m in modules} # Package detection (level-agnostic) packages = { p for p in paths if any(other.startswith(p + ".") for other in paths) } for module in modules: self._write_module( module, packages, out_dir, module_is_source, ) def generate_readme( self, project: Project, docs_dir: Path, module_is_source: bool | None = None, ) -> None: """ Generate README.md from the root package docstring. Behavior: - If module_is_source is True: README.md is generated at project root (docs_dir.parent) - If module_is_source is False: TODO: generate README.md inside respective module folders """ # ------------------------- # Only implement source-root mode # ------------------------- if not module_is_source: # TODO: support per-module README generation return readme_path = docs_dir.parent / "README.md" root_module = None for module in project.get_all_modules(): if module.path == project.name: root_module = module break if root_module is None: return doc = "" if root_module.docstring: doc = getattr( root_module.docstring, "value", str(root_module.docstring), ) content = ( f"# {project.name}\n\n" f"{doc.strip()}\n" ) if not readme_path.exists() or readme_path.read_text(encoding="utf-8") != content: readme_path.write_text( content, encoding="utf-8", ) # ------------------------- # Internal helpers # ------------------------- def _find_root_module(self, project: Project) -> Module | None: """ Find the root module matching the project name. """ for module in project.get_all_modules(): if module.path == project.name: return module return None def _write_module( self, module: Module, packages: set[str], out_dir: Path, module_is_source: bool | None = None, ) -> None: """ Write a single module's documentation file. Packages are written as 'index.md' inside their respective directories. Args: module: The module to write. packages: A set of module paths that are identified as packages. out_dir: The base output directory. module_is_source: Module is the source folder and to be treated as the root folder. """ parts = module.path.split(".") if module_is_source: module_name, parts = parts[0], parts[1:] else: module_name, parts = parts[0], parts if module.path in packages: # Package → directory/index.md dir_path = out_dir.joinpath(*parts) dir_path.mkdir(parents=True, exist_ok=True) md_path = dir_path / "index.md" link_target = f"{parts[-1]}/" if parts else None else: # Leaf module → parent_dir/.md dir_path = out_dir.joinpath(*parts[:-1]) dir_path.mkdir(parents=True, exist_ok=True) md_path = dir_path / f"{parts[-1]}.md" link_target = f"{parts[-1]}.md" if parts else None title = parts[-1].replace("_", " ").title() if parts else module_name content = self._render_markdown(title, module.path) if not md_path.exists() or md_path.read_text(encoding="utf-8") != content: md_path.write_text(content, encoding="utf-8") if not module_is_source: self._ensure_parent_index(parts, out_dir, link_target, title) def _render_markdown(self, title: str, module_path: str) -> str: """ Generate the Markdown content for a module file. Args: title: The display title for the page. module_path: The dotted path of the module to document. Returns: A string containing the Markdown source. """ return ( f"# {title}\n\n" f"::: {module_path}\n" ) def _ensure_root_index( self, project: Project, out_dir: Path, ) -> None: root_index = out_dir / "index.md" if not root_index.exists(): root_index.write_text( f"# {project.name}\n\n" "## Modules\n\n", encoding="utf-8", ) def _ensure_parent_index( self, parts: list[str], out_dir: Path, link_target: str, title: str, ) -> None: if len(parts) == 1: parent_index = out_dir / "index.md" else: parent_dir = out_dir.joinpath(*parts[:-1]) parent_dir.mkdir(parents=True, exist_ok=True) parent_index = parent_dir / "index.md" if not parent_index.exists(): parent_title = parts[-2].replace("_", " ").title() parent_index.write_text( f"# {parent_title}\n\n", encoding="utf-8", ) content = parent_index.read_text(encoding="utf-8") link = f"- [{title}]({link_target})\n" if link not in content: parent_index.write_text(content + link, encoding="utf-8")