243 lines
6.8 KiB
Python
243 lines
6.8 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
|
|
- 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/<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"
|
|
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")
|