Files
doc-forge/docforge/renderers/mkdocs_renderer.py

169 lines
5.1 KiB
Python

"""
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
"""
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 models to render.
out_dir: Target directory for documentation files.
module_is_source: Module is the source folder and to be treated 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,
)
# -------------------------
# Internal helpers
# -------------------------
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/<name>.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"
link = f"- [{title}]({link_target})\n"
else:
parent_dir = out_dir.joinpath(*parts[:-1])
parent_dir.mkdir(parents=True, exist_ok=True)
parent_index = parent_dir / "index.md"
link = f"- [{title}]({link_target})\n"
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")
if link not in content:
parent_index.write_text(content + link, encoding="utf-8")