nav submodule
This commit is contained in:
10
docforge/nav/__init__.py
Normal file
10
docforge/nav/__init__.py
Normal 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
10
docforge/nav/__init__.pyi
Normal 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
24
docforge/nav/mkdocs.py
Normal 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
27
docforge/nav/mkdocs.pyi
Normal 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
71
docforge/nav/resolver.py
Normal 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
52
docforge/nav/resolver.pyi
Normal 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-root–relative 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
55
docforge/nav/spec.py
Normal 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
32
docforge/nav/spec.pyi
Normal 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).
|
||||
"""
|
||||
...
|
||||
Reference in New Issue
Block a user