nav submodule

This commit is contained in:
2026-01-20 21:22:28 +05:30
parent 869b1730c4
commit 726e7ca6d2
12 changed files with 493 additions and 0 deletions

10
docforge/nav/__init__.py Normal file
View File

@@ -0,0 +1,10 @@
from .spec import NavSpec
from .resolver import ResolvedNav, resolve_nav
from .mkdocs import MkDocsNavEmitter
__all__ = [
"NavSpec",
"ResolvedNav",
"MkDocsNavEmitter",
"resolve_nav",
]

10
docforge/nav/__init__.pyi Normal file
View File

@@ -0,0 +1,10 @@
from .spec import NavSpec
from .resolver import ResolvedNav, resolve_nav
from .mkdocs import MkDocsNavEmitter
__all__ = [
"NavSpec",
"ResolvedNav",
"MkDocsNavEmitter",
"resolve_nav",
]

24
docforge/nav/mkdocs.py Normal file
View File

@@ -0,0 +1,24 @@
from __future__ import annotations
from typing import Any, Dict, List
from docforge.nav.resolver import ResolvedNav
class MkDocsNavEmitter:
"""Emit MkDocs-compatible nav structure."""
def emit(self, nav: ResolvedNav) -> List[Dict[str, Any]]:
result: List[Dict[str, Any]] = []
if nav.home:
result.append({"Home": nav.home})
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()})
result.append({group: entries})
return result

27
docforge/nav/mkdocs.pyi Normal file
View File

@@ -0,0 +1,27 @@
from typing import Dict, List, Any
from docforge.nav.resolver import ResolvedNav
class MkDocsNavEmitter:
"""
Converts a ResolvedNav into MkDocs-compatible `nav` data.
"""
def emit(self, nav: ResolvedNav) -> List[Dict[str, Any]]:
"""
Emit a structure suitable for insertion into mkdocs.yml.
Example return value:
[
{"Home": "openapi_first/index.md"},
{
"Core": [
{"OpenAPI-First App": "openapi_first/app.md"},
...
]
}
]
"""
...

71
docforge/nav/resolver.py Normal file
View File

@@ -0,0 +1,71 @@
from __future__ import annotations
from pathlib import Path
from typing import Dict, Iterable, List
import glob
from docforge.nav.spec import NavSpec
class ResolvedNav:
def __init__(
self,
home: str | None,
groups: Dict[str, List[Path]],
docs_root: Path | None = None,
) -> None:
self.home = home
self.groups = groups
self._docs_root = docs_root
def all_files(self) -> Iterable[Path]:
if self.home:
if self._docs_root is None:
raise RuntimeError("docs_root is required to resolve home path")
yield self._docs_root / self.home
for paths in self.groups.values():
for p in paths:
yield p
def resolve_nav(
spec: NavSpec,
docs_root: Path,
) -> ResolvedNav:
if not docs_root.exists():
raise FileNotFoundError(docs_root)
def resolve_pattern(pattern: str) -> List[Path]:
full = docs_root / pattern
matches = sorted(
Path(p) for p in glob.glob(str(full), recursive=True)
)
if not matches:
raise FileNotFoundError(pattern)
return matches
# Resolve home
home: str | None = None
if spec.home:
home_path = docs_root / spec.home
if not home_path.exists():
raise FileNotFoundError(spec.home)
home = spec.home
# Resolve groups
resolved_groups: Dict[str, List[Path]] = {}
for group, patterns in spec.groups.items():
files: List[Path] = []
for pattern in patterns:
files.extend(resolve_pattern(pattern))
resolved_groups[group] = files
return ResolvedNav(
home=home,
groups=resolved_groups,
docs_root=docs_root,
)

52
docforge/nav/resolver.pyi Normal file
View File

@@ -0,0 +1,52 @@
from pathlib import Path
from typing import Dict, List, Iterable, Optional
from docforge.nav.spec import NavSpec
class ResolvedNav:
"""
Fully-resolved navigation tree.
- `home` is a semantic, docs-rootrelative path (string)
- `groups` contain resolved filesystem Paths
- Order is preserved
"""
home: Optional[str]
groups: Dict[str, List[Path]]
def __init__(
self,
home: str | None,
groups: Dict[str, List[Path]],
docs_root: Path | None = None,
) -> None:
self._docs_root = None
...
def all_files(self) -> Iterable[Path]:
"""
Return all resolved documentation files in nav order.
Includes the home file (resolved against docs_root)
followed by all group files.
"""
...
def resolve_nav(
spec: NavSpec,
docs_root: Path,
) -> ResolvedNav:
"""
Resolve a NavSpec against a docs directory.
Expands wildcards, validates existence, and
resolves filesystem paths relative to docs_root.
Raises:
FileNotFoundError: if any referenced file is missing
ValueError: if resolution fails
"""
...

55
docforge/nav/spec.py Normal file
View File

@@ -0,0 +1,55 @@
from __future__ import annotations
from pathlib import Path
from typing import Dict, List, Optional
import yaml
class NavSpec:
"""Parsed representation of docforge.nav.yml."""
def __init__(
self,
home: Optional[str],
groups: Dict[str, List[str]],
) -> None:
self.home = home
self.groups = groups
@classmethod
def load(cls, 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 mapping")
home = data.get("home")
groups = data.get("groups", {})
if home is not None and not isinstance(home, str):
raise ValueError("home must be a string")
if not isinstance(groups, dict):
raise ValueError("groups must be a mapping")
for key, value in groups.items():
if not isinstance(key, str):
raise ValueError("group names must be strings")
if not isinstance(value, list) or not all(
isinstance(v, str) for v in value
):
raise ValueError(f"group '{key}' must be a list of strings")
return cls(home=home, groups=groups)
def all_patterns(self) -> List[str]:
patterns: List[str] = []
if self.home:
patterns.append(self.home)
for items in self.groups.values():
patterns.extend(items)
return patterns

32
docforge/nav/spec.pyi Normal file
View File

@@ -0,0 +1,32 @@
from pathlib import Path
from typing import Dict, List, Optional
class NavSpec:
"""
Parsed representation of `docforge.nav.yml`.
This object represents *semantic intent* and is independent
of filesystem structure or MkDocs specifics.
"""
home: Optional[str]
groups: Dict[str, List[str]]
@classmethod
def load(cls, path: Path) -> "NavSpec":
"""
Load and validate a nav specification from YAML.
Raises:
FileNotFoundError: if the file does not exist
ValueError: if the schema is invalid
"""
...
def all_patterns(self) -> List[str]:
"""
Return all path patterns referenced by the spec
(including home and group entries).
"""
...