diff --git a/docforge/__init__.py b/docforge/__init__.py index 235b37b..3320f8c 100644 --- a/docforge/__init__.py +++ b/docforge/__init__.py @@ -7,9 +7,11 @@ until their contracts are finalized. """ from .loader import GriffeLoader +from .renderers import MkDocsRenderer from . import model __all__ = [ "GriffeLoader", + "MkDocsRenderer", "model", ] diff --git a/docforge/__init__.pyi b/docforge/__init__.pyi index b7949cb..9f9d408 100644 --- a/docforge/__init__.pyi +++ b/docforge/__init__.pyi @@ -1,4 +1,5 @@ from .loader import GriffeLoader +from .renderers import MkDocsRenderer from . import model __all__: list[str] diff --git a/docforge/renderers/__init__.py b/docforge/renderers/__init__.py new file mode 100644 index 0000000..a47a055 --- /dev/null +++ b/docforge/renderers/__init__.py @@ -0,0 +1,5 @@ +from docforge.renderers.mkdocs import MkDocsRenderer + +__all__ = [ + "MkDocsRenderer", +] diff --git a/docforge/renderers/__init__.pyi b/docforge/renderers/__init__.pyi new file mode 100644 index 0000000..3748a7f --- /dev/null +++ b/docforge/renderers/__init__.pyi @@ -0,0 +1,3 @@ +from docforge.renderers.mkdocs import MkDocsRenderer + +__all__ = ["MkDocsRenderer"] diff --git a/docforge/renderers/base.py b/docforge/renderers/base.py new file mode 100644 index 0000000..c1d9352 --- /dev/null +++ b/docforge/renderers/base.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from pathlib import Path + +from docforge.model import Project + + +class RendererConfig: + """Renderer configuration container.""" + + def __init__(self, out_dir: Path, project: Project) -> None: + self.out_dir = out_dir + self.project = project diff --git a/docforge/renderers/base.pyi b/docforge/renderers/base.pyi new file mode 100644 index 0000000..4b2d1e6 --- /dev/null +++ b/docforge/renderers/base.pyi @@ -0,0 +1,26 @@ +from pathlib import Path +from typing import Protocol + +from docforge.model import Project + + +class RendererConfig: + """Renderer configuration container.""" + + out_dir: Path + project: Project + + def __init__(self, out_dir: Path, project: Project) -> None: ... + + +class DocRenderer(Protocol): + """Renderer interface.""" + + name: str + + def generate_sources( + self, + project: Project, + out_dir: Path, + ) -> None: + """Generate renderer-specific source files.""" diff --git a/docforge/renderers/mkdocs.py b/docforge/renderers/mkdocs.py new file mode 100644 index 0000000..acd683c --- /dev/null +++ b/docforge/renderers/mkdocs.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from pathlib import Path + +from docforge.model import Project + + +class MkDocsRenderer: + """MkDocs source generator using mkdocstrings.""" + + name = "mkdocs" + + def generate_sources(self, project: Project, out_dir: Path) -> None: + """ + Generate Markdown files with mkdocstrings directives. + + Structure rules: + - Each top-level package gets a directory + - Modules become .md files + - Packages (__init__) become index.md + """ + for module in project.get_all_modules(): + self._write_module(project, module, out_dir) + + # ------------------------- + # Internal helpers + # ------------------------- + + def _write_module(self, project: Project, module, out_dir: Path) -> None: + parts = module.path.split(".") + + # Root package directory + pkg_dir = out_dir / parts[0] + pkg_dir.mkdir(parents=True, exist_ok=True) + + # Package (__init__.py) → index.md + if module.path == parts[0]: + md_path = pkg_dir / "index.md" + title = parts[0].replace("_", " ").title() + else: + # Submodule → .md + md_path = pkg_dir / f"{parts[-1]}.md" + title = parts[-1].replace("_", " ").title() + + content = self._render_markdown(title, module.path) + + # Idempotent write + if md_path.exists(): + if md_path.read_text(encoding="utf-8") == content: + return + + md_path.write_text(content, encoding="utf-8") + + def _render_markdown(self, title: str, module_path: str) -> str: + return ( + f"# {title}\n\n" + f"::: {module_path}\n" + ) diff --git a/docforge/renderers/mkdocs.pyi b/docforge/renderers/mkdocs.pyi new file mode 100644 index 0000000..d1b3894 --- /dev/null +++ b/docforge/renderers/mkdocs.pyi @@ -0,0 +1,17 @@ +from pathlib import Path + +from docforge.model import Project +from docforge.renderers.base import DocRenderer + + +class MkDocsRenderer: + """MkDocs source generator using mkdocstrings.""" + + name: str + + def generate_sources( + self, + project: Project, + out_dir: Path, + ) -> None: + """Generate Markdown files with mkdocstrings directives.""" diff --git a/tests/renderers/__init__.py b/tests/renderers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/renderers/test_mkdocs_content.py b/tests/renderers/test_mkdocs_content.py new file mode 100644 index 0000000..1669cd4 --- /dev/null +++ b/tests/renderers/test_mkdocs_content.py @@ -0,0 +1,19 @@ +from pathlib import Path + +from docforge import MkDocsRenderer +from docforge.model import Project, Module + + +def test_mkdocs_file_content(tmp_path: Path): + project = Project("testpkg") + project.add_module(Module("testpkg.mod")) + + out_dir = tmp_path / "docs" + renderer = MkDocsRenderer() + + renderer.generate_sources(project, out_dir) + + content = (out_dir / "testpkg" / "mod.md").read_text() + + assert "# Mod" in content + assert "::: testpkg.mod" in content diff --git a/tests/renderers/test_mkdocs_idempotency.py b/tests/renderers/test_mkdocs_idempotency.py new file mode 100644 index 0000000..755da68 --- /dev/null +++ b/tests/renderers/test_mkdocs_idempotency.py @@ -0,0 +1,20 @@ +from pathlib import Path + +from docforge import MkDocsRenderer +from docforge.model import Project, Module + + +def test_mkdocs_idempotent(tmp_path: Path): + project = Project("testpkg") + project.add_module(Module("testpkg.mod")) + + out_dir = tmp_path / "docs" + renderer = MkDocsRenderer() + + renderer.generate_sources(project, out_dir) + first = (out_dir / "testpkg" / "mod.md").read_text() + + renderer.generate_sources(project, out_dir) + second = (out_dir / "testpkg" / "mod.md").read_text() + + assert first == second diff --git a/tests/renderers/test_mkdocs_structure.py b/tests/renderers/test_mkdocs_structure.py new file mode 100644 index 0000000..d177b1f --- /dev/null +++ b/tests/renderers/test_mkdocs_structure.py @@ -0,0 +1,18 @@ +from pathlib import Path + +from docforge import MkDocsRenderer +from docforge.model import Project, Module + + +def test_mkdocs_directory_structure(tmp_path: Path): + project = Project("testpkg") + project.add_module(Module("testpkg")) + project.add_module(Module("testpkg.sub")) + + out_dir = tmp_path / "docs" + renderer = MkDocsRenderer() + + renderer.generate_sources(project, out_dir) + + assert (out_dir / "testpkg" / "index.md").exists() + assert (out_dir / "testpkg" / "sub.md").exists()