""" This module contains the logic for resolving a NavSpec against the physical filesystem. It expands globs and validates that all referenced documents actually exist on disk. """ from pathlib import Path from typing import Dict, Iterable, List import glob from docforge.nav.spec import NavSpec class ResolvedNav: """ Represents a navigation structure where all patterns and paths have been resolved against the actual filesystem contents. Attributes: home: Resolved relative path to the home page. groups: Mapping of group titles to lists of absolute or relative Path objects. """ def __init__( self, home: str | None, groups: Dict[str, List[Path]], docs_root: Path | None = None, ) -> None: """ Initialize a ResolvedNav instance. Args: home: The relative path to the project home page. groups: A mapping of group names to their resolved filesystem paths. docs_root: The root documentation directory. """ self.home = home self.groups = groups self._docs_root = docs_root def all_files(self) -> Iterable[Path]: """ Get an iterable of all resolved files in the navigation structure. Returns: An iterable of Path objects. """ 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: """ Create a ResolvedNav by processing a NavSpec against the filesystem. This expands globs and validates the existence of referenced files. Args: spec: The navigation specification to resolve. docs_root: The root directory for documentation files. Returns: A ResolvedNav instance. Raises: FileNotFoundError: If a pattern doesn't match any files or the docs_root doesn't exist. """ if not docs_root.exists(): raise FileNotFoundError(docs_root) def resolve_pattern(pattern: str) -> List[Path]: """ Resolve a single glob pattern relative to the docs_root. Args: pattern: The glob pattern to resolve. Returns: A sorted list of matching Path objects. Raises: FileNotFoundError: If the pattern doesn't 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 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, )