Files
doc-forge/docforge/renderers/mkdocs_renderer.py
Vishesh 'ironeagle' Bangotra b6306baafc Improve documentation look & feel via MkDocs Material template enhancements (#5)
# Improve documentation look & feel via MkDocs Material template enhancements

## Summary

This MR improves the overall **documentation experience and visual presentation** of the doc-forge docs by enhancing the MkDocs Material template configuration.

The changes focus on **navigation usability, code readability, and richer Markdown rendering**, resulting in a cleaner and more professional documentation site.

Docstring changes were made across the codebase for consistency, but this MR description focuses on the **template and presentation improvements**.

---

## Navigation Improvements

The navigation system has been enhanced to provide a clearer structure and better discoverability.

Key improvements include:

* Section-aware navigation in the sidebar
* Automatic expansion of module/package hierarchy
* Scroll tracking within the sidebar
* Clickable package index pages

Material navigation features added:

* `navigation.sections`
* `navigation.expand`
* `navigation.tracking`
* `navigation.indexes`

This results in a **single cohesive navigation tree** that exposes the entire documentation hierarchy from the sidebar.

---

## Code Block Improvements

Code blocks previously appeared relatively plain. The template now enables richer syntax highlighting and improved readability.

Enhancements include:

* Syntax highlighting using `pymdownx.highlight`
* Line numbers for code blocks
* Anchored line numbers for deep linking
* Improved fenced code block rendering

Additional Material features:

* `content.code.copy` — copy button for code blocks
* `content.code.annotate` — support for code annotations

These changes significantly improve the readability of examples and API snippets throughout the documentation.

---

## Markdown Rendering Enhancements

Additional Markdown extensions were enabled to support richer documentation features:

* `pymdownx.superfences` for advanced fenced blocks
* `pymdownx.inlinehilite` for inline code highlighting
* `pymdownx.snippets` for reusable snippets
* `admonition` and `pymdownx.details` for callouts and collapsible sections
* `pymdownx.tabbed` for tabbed content blocks
* `pymdownx.tasklist` for checklist-style items
* `tables`, `footnotes`, and advanced formatting extensions

These extensions make it easier to write expressive and structured documentation.

---

## Search Experience

The documentation search experience has been improved using Material search features:

* `search.highlight`
* `search.share`
* `search.suggest`

These enhancements provide:

* highlighted search matches
* sharable search URLs
* auto-suggestions while typing

---

## mkdocstrings Improvements

The mkdocstrings configuration has been expanded to produce clearer API documentation.

Notable improvements include:

* grouping objects by category
* explicit category headings
* improved symbol headings
* cleaner object path display

This results in more structured API documentation pages.

---

## Result

Overall, these changes provide:

* cleaner and more intuitive navigation
* significantly improved code presentation
* richer Markdown capabilities
* better search usability

The documentation now has a **more polished, modern appearance** and improved usability for both readers and contributors.

Reviewed-on: #5
Co-authored-by: Vishesh 'ironeagle' Bangotra <aetoskia@gmail.com>
Co-committed-by: Vishesh 'ironeagle' Bangotra <aetoskia@gmail.com>
2026-03-07 10:50:18 +00:00

273 lines
8.1 KiB
Python

"""
MkDocs renderer implementation.
This module defines the ``MkDocsRenderer`` class, which generates Markdown
documentation sources compatible with MkDocs Material and the mkdocstrings
plugin.
The renderer ensures a consistent documentation structure by:
- Creating a root ``index.md`` if one does not exist
- Generating package index pages automatically
- Linking child modules within parent package pages
- Optionally generating ``README.md`` from the root package docstring
"""
from pathlib import Path
from docforge.models import Project, Module
class MkDocsRenderer:
"""
Renderer that produces Markdown documentation for MkDocs.
Generated pages use mkdocstrings directives to reference Python modules,
allowing MkDocs to render API documentation dynamically.
"""
name = "mkdocs"
# -------------------------
# Public API
# -------------------------
def generate_sources(
self,
project: Project,
out_dir: Path,
module_is_source: bool | None = None,
) -> None:
"""
Generate Markdown documentation files for a project.
This method renders a documentation structure from the provided
project model and writes the resulting Markdown files to the
specified output directory.
Args:
project: Project model containing modules to document.
out_dir: Directory where generated Markdown files will be written.
module_is_source: If True, treat the specified module as the
documentation root rather than nesting it inside a 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}
# Detect packages (modules with children)
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 a ``README.md`` file from the root module docstring.
Behavior:
- If ``module_is_source`` is True, ``README.md`` is written to the
project root directory.
- If False, README generation is currently not implemented.
Args:
project: Project model containing documentation metadata.
docs_dir: Directory containing generated documentation sources.
module_is_source: Whether the module is treated as the project
source root.
"""
if not module_is_source:
# Future: support README generation per module
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:
"""
Locate the root module corresponding to the project name.
Args:
project: Project model to inspect.
Returns:
The root ``Module`` if found, otherwise ``None``.
"""
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 documentation for a single module.
Package modules are rendered as ``index.md`` files inside their
corresponding directories, while leaf modules are written as
standalone Markdown pages.
Args:
module: Module to render.
packages: Set of module paths identified as packages.
out_dir: Base directory for generated documentation files.
module_is_source: Whether the module acts as the documentation
root directory.
"""
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:
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:
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 Markdown content for a module documentation page.
Args:
title: Page title displayed in the documentation.
module_path: Dotted import path of the module.
Returns:
Markdown source containing a mkdocstrings directive.
"""
return (
f"# {title}\n\n"
f"::: {module_path}\n"
)
def _ensure_root_index(
self,
project: Project,
out_dir: Path,
) -> None:
"""
Ensure that the root ``index.md`` page exists.
Args:
project: Project model used for the page title.
out_dir: Documentation output directory.
"""
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:
"""
Ensure that parent package index files exist and contain links to
child modules.
Args:
parts: Module path components.
out_dir: Documentation output directory.
link_target: Link target used in the parent index.
title: Display title for the link.
"""
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")