mkdocs cli
This commit is contained in:
@@ -7,6 +7,7 @@ import click
|
||||
|
||||
from docforge.loader import GriffeLoader
|
||||
from docforge.renderers.mkdocs import MkDocsRenderer
|
||||
from docforge.cli.mkdocs import mkdocs_cmd
|
||||
|
||||
|
||||
@click.group()
|
||||
@@ -15,6 +16,8 @@ def cli() -> None:
|
||||
pass
|
||||
|
||||
|
||||
cli.add_command(mkdocs_cmd)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# tree
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
84
docforge/cli/mkdocs.py
Normal file
84
docforge/cli/mkdocs.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from importlib import resources
|
||||
|
||||
import click
|
||||
import yaml
|
||||
|
||||
from docforge.nav import load_nav_spec
|
||||
from docforge.nav import resolve_nav
|
||||
from docforge.nav import MkDocsNavEmitter
|
||||
|
||||
|
||||
def _load_template(template: Path | None) -> dict:
|
||||
if template is not None:
|
||||
if not template.exists():
|
||||
raise click.FileError(str(template), hint="Template not found")
|
||||
return yaml.safe_load(template.read_text(encoding="utf-8"))
|
||||
|
||||
# Load built-in default
|
||||
text = (
|
||||
resources.files("docforge.templates")
|
||||
.joinpath("mkdocs.sample.yml")
|
||||
.read_text(encoding="utf-8")
|
||||
)
|
||||
return yaml.safe_load(text)
|
||||
|
||||
|
||||
@click.command("mkdocs")
|
||||
@click.option(
|
||||
"--docs-dir",
|
||||
type=click.Path(path_type=Path),
|
||||
default=Path("docs"),
|
||||
)
|
||||
@click.option(
|
||||
"--nav",
|
||||
"nav_file",
|
||||
type=click.Path(path_type=Path),
|
||||
default=Path("docforge.nav.yml"),
|
||||
)
|
||||
@click.option(
|
||||
"--template",
|
||||
type=click.Path(path_type=Path),
|
||||
default=None,
|
||||
help="Override the built-in mkdocs template",
|
||||
)
|
||||
@click.option(
|
||||
"--out",
|
||||
type=click.Path(path_type=Path),
|
||||
default=Path("mkdocs.yml"),
|
||||
)
|
||||
def mkdocs_cmd(
|
||||
docs_dir: Path,
|
||||
nav_file: Path,
|
||||
template: Path | None,
|
||||
out: Path,
|
||||
) -> None:
|
||||
"""Generate mkdocs.yml from nav spec and template."""
|
||||
|
||||
if not nav_file.exists():
|
||||
raise click.FileError(str(nav_file), hint="Nav spec not found")
|
||||
|
||||
# Load nav spec
|
||||
spec = load_nav_spec(nav_file)
|
||||
|
||||
# Resolve nav
|
||||
resolved = resolve_nav(spec, docs_dir)
|
||||
|
||||
# Emit mkdocs nav
|
||||
nav_block = MkDocsNavEmitter().emit(resolved)
|
||||
|
||||
# Load template (user or built-in)
|
||||
data = _load_template(template)
|
||||
|
||||
# Inject nav
|
||||
data["nav"] = nav_block
|
||||
|
||||
# Write output
|
||||
out.write_text(
|
||||
yaml.safe_dump(data, sort_keys=False),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
click.echo(f"mkdocs.yml written to {out}")
|
||||
41
docforge/cli/mkdocs.pyi
Normal file
41
docforge/cli/mkdocs.pyi
Normal file
@@ -0,0 +1,41 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import click
|
||||
|
||||
|
||||
def _load_template(template: Optional[Path]) -> Dict[str, Any]:
|
||||
...
|
||||
|
||||
|
||||
@click.command("mkdocs")
|
||||
@click.option(
|
||||
"--docs-dir",
|
||||
type=click.Path(path_type=Path),
|
||||
default=Path("docs"),
|
||||
)
|
||||
@click.option(
|
||||
"--nav",
|
||||
"nav_file",
|
||||
type=click.Path(path_type=Path),
|
||||
default=Path("docforge.nav.yml"),
|
||||
)
|
||||
@click.option(
|
||||
"--template",
|
||||
type=click.Path(path_type=Path),
|
||||
default=None,
|
||||
)
|
||||
@click.option(
|
||||
"--out",
|
||||
type=click.Path(path_type=Path),
|
||||
default=Path("mkdocs.yml"),
|
||||
)
|
||||
def mkdocs_cmd(
|
||||
docs_dir: Path,
|
||||
nav_file: Path,
|
||||
template: Optional[Path],
|
||||
out: Path,
|
||||
) -> None:
|
||||
...
|
||||
@@ -1,4 +1,4 @@
|
||||
from .spec import NavSpec
|
||||
from .spec import NavSpec, load_nav_spec
|
||||
from .resolver import ResolvedNav, resolve_nav
|
||||
from .mkdocs import MkDocsNavEmitter
|
||||
|
||||
@@ -7,4 +7,5 @@ __all__ = [
|
||||
"ResolvedNav",
|
||||
"MkDocsNavEmitter",
|
||||
"resolve_nav",
|
||||
"load_nav_spec",
|
||||
]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .spec import NavSpec
|
||||
from .spec import NavSpec, load_nav_spec
|
||||
from .resolver import ResolvedNav, resolve_nav
|
||||
from .mkdocs import MkDocsNavEmitter
|
||||
|
||||
@@ -7,4 +7,5 @@ __all__ = [
|
||||
"ResolvedNav",
|
||||
"MkDocsNavEmitter",
|
||||
"resolve_nav",
|
||||
"load_nav_spec",
|
||||
]
|
||||
|
||||
@@ -1,24 +1,34 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any
|
||||
|
||||
from docforge.nav.resolver import ResolvedNav
|
||||
|
||||
|
||||
class MkDocsNavEmitter:
|
||||
"""Emit MkDocs-compatible nav structure."""
|
||||
"""Emit MkDocs-compatible nav structures."""
|
||||
|
||||
def emit(self, nav: ResolvedNav) -> List[Dict[str, Any]]:
|
||||
result: List[Dict[str, Any]] = []
|
||||
|
||||
# Home entry (semantic path)
|
||||
if nav.home:
|
||||
result.append({"Home": nav.home})
|
||||
|
||||
# Group entries
|
||||
for group, paths in nav.groups.items():
|
||||
entries: List[Dict[str, str]] = []
|
||||
for path in paths:
|
||||
title = path.stem.replace("_", " ").title()
|
||||
entries.append({title: path.as_posix()})
|
||||
entries: List[str] = []
|
||||
for p in paths:
|
||||
# Convert filesystem path back to docs-relative path
|
||||
entries.append(self._to_relative(p))
|
||||
result.append({group: entries})
|
||||
|
||||
return result
|
||||
|
||||
def _to_relative(self, path: Path) -> str:
|
||||
"""
|
||||
Convert a filesystem path to a docs-relative path.
|
||||
"""
|
||||
# Normalize to POSIX-style for MkDocs
|
||||
return path.as_posix().split("/docs/", 1)[-1]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Dict, List, Any
|
||||
from pathlib import Path
|
||||
|
||||
from docforge.nav.resolver import ResolvedNav
|
||||
|
||||
@@ -25,3 +26,9 @@ class MkDocsNavEmitter:
|
||||
]
|
||||
"""
|
||||
...
|
||||
|
||||
def _to_relative(self, path: Path) -> str:
|
||||
"""
|
||||
Convert a filesystem path to a docs-relative path.
|
||||
"""
|
||||
...
|
||||
|
||||
@@ -53,3 +53,17 @@ class NavSpec:
|
||||
for items in self.groups.values():
|
||||
patterns.extend(items)
|
||||
return patterns
|
||||
|
||||
|
||||
def load_nav_spec(path: Path) -> NavSpec:
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(path)
|
||||
|
||||
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("Nav spec must be a YAML mapping")
|
||||
|
||||
return NavSpec(
|
||||
home=data.get("home"),
|
||||
groups=data.get("groups", {}),
|
||||
)
|
||||
|
||||
@@ -13,6 +13,13 @@ class NavSpec:
|
||||
home: Optional[str]
|
||||
groups: Dict[str, List[str]]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
home: Optional[str],
|
||||
groups: Dict[str, List[str]],
|
||||
) -> None:
|
||||
...
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: Path) -> "NavSpec":
|
||||
"""
|
||||
@@ -30,3 +37,6 @@ class NavSpec:
|
||||
(including home and group entries).
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
def load_nav_spec(path: Path) -> NavSpec: ...
|
||||
|
||||
33
docforge/templates/mkdocs.sample.yml
Normal file
33
docforge/templates/mkdocs.sample.yml
Normal file
@@ -0,0 +1,33 @@
|
||||
theme:
|
||||
name: material
|
||||
palette:
|
||||
- scheme: slate
|
||||
primary: deep purple
|
||||
accent: cyan
|
||||
font:
|
||||
text: Inter
|
||||
code: JetBrains Mono
|
||||
features:
|
||||
- navigation.tabs
|
||||
- navigation.expand
|
||||
- navigation.top
|
||||
- navigation.instant
|
||||
- content.code.copy
|
||||
- content.code.annotate
|
||||
|
||||
plugins:
|
||||
- search
|
||||
- mkdocstrings:
|
||||
handlers:
|
||||
python:
|
||||
paths: ["."]
|
||||
options:
|
||||
docstring_style: google
|
||||
show_source: false
|
||||
show_signature_annotations: true
|
||||
separate_signature: true
|
||||
merge_init_into_class: true
|
||||
inherited_members: true
|
||||
annotations_path: brief
|
||||
show_root_heading: true
|
||||
group_by_category: true
|
||||
Reference in New Issue
Block a user