""" Navigation resolution utilities. This module resolves a ``NavSpec`` against the filesystem by expanding glob patterns and validating that referenced documentation files exist. --- Notes: - Glob resolution is recursive and returns paths in sorted order. - Unmatched patterns raise ``FileNotFoundError`` to fail fast on typos. --- """ import glob from collections.abc import Iterable from pathlib import Path from docforge.nav.spec import NavSpec class ResolvedNav: """ Resolved navigation structure. A ``ResolvedNav`` represents navigation data after glob patterns have been expanded and paths validated against the filesystem. Attributes: home: Relative path to the documentation home page. groups: Mapping of navigation group titles to lists of resolved documentation file paths. """ def __init__( self, home: str | None, groups: dict[str, list[Path]], docs_root: Path | None = None, ) -> None: """ Initialize a ResolvedNav instance. Args: home (str | None): Relative path to the home page within the documentation root. groups (dict[str, list[Path]]): Mapping of group titles to resolved documentation file paths. docs_root (Path | None): Root directory of the documentation source files. """ self.home = home self.groups = groups self._docs_root = docs_root def all_files(self) -> Iterable[Path]: """ Iterate over all files referenced by the navigation structure. Yields: Path: A documentation file referenced by the navigation, including the home page when defined. Raises: RuntimeError: If the home page is defined but the documentation root is not available for resolution. """ 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(): yield from paths def resolve_nav( spec: NavSpec, docs_root: Path, ) -> ResolvedNav: """ Resolve a navigation specification against the filesystem. The function expands glob patterns defined in a ``NavSpec`` and verifies that referenced documentation files exist within the documentation root. Args: spec (NavSpec): Navigation specification describing documentation layout. docs_root (Path): Root directory containing documentation Markdown files. Returns: ResolvedNav: A `ResolvedNav` instance containing validated navigation paths. Raises: FileNotFoundError: If the documentation root does not exist or a navigation pattern does not match any files. """ if not docs_root.exists(): raise FileNotFoundError(docs_root) def resolve_pattern(pattern: str) -> list[Path]: """ Resolve a glob pattern relative to the documentation root. Args: pattern (str): Glob pattern used to match documentation files. Returns: list[Path]: A sorted list of matching `Path` objects. Raises: FileNotFoundError: If the pattern does not match any files. """ 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 page 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 navigation 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, )