# 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>
273 lines
8.1 KiB
Python
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")
|