""" Navigation specification model. This module defines the ``NavSpec`` class, which represents the navigation structure defined by the user in the doc-forge navigation specification (typically ``docforge.nav.yml``). """ from pathlib import Path import yaml class NavSpec: """ Parsed representation of a navigation specification. A ``NavSpec`` describes the intended documentation navigation layout before it is resolved against the filesystem. Attributes: home: Relative path to the documentation home page (for example ``index.md``). groups: Mapping of navigation group titles to lists of file patterns or glob expressions. icon: Optional mapping of theme icon entries (for example ``{"logo": "material/code-tags"}``) injected into the MkDocs theme as ``theme.icon``. """ def __init__( self, home: str | None, groups: dict[str, list[str]], icon: dict[str, str] | None = None, ) -> None: """ Initialize a NavSpec instance. Args: home: Relative path to the home document. groups: Mapping of group names to lists of path patterns (glob expressions). icon: Optional mapping of theme icon entries applied to the generated MkDocs configuration. """ self.home = home self.groups = groups self.icon = icon @classmethod def load(cls, path: Path) -> "NavSpec": """ Load a navigation specification from a YAML file. Args: path: Filesystem path to the navigation specification file. Returns: A ``NavSpec`` instance representing the parsed configuration. Raises: FileNotFoundError: If the specified file does not exist. ValueError: If the file contents are not a valid navigation specification. """ 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", {}) icon = data.get("icon") 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") if icon is not None and ( not isinstance(icon, dict) or not all( isinstance(k, str) and isinstance(v, str) for k, v in icon.items() ) ): raise ValueError("icon must be a mapping of strings") return cls(home=home, groups=groups, icon=icon) def all_patterns(self) -> list[str]: """ Return all path patterns referenced by the specification. Returns: A list containing the home document (if defined) and all group pattern entries. """ patterns: list[str] = [] if self.home: patterns.append(self.home) for items in self.groups.values(): patterns.extend(items) return patterns def load_nav_spec(path: Path) -> NavSpec: """ Load a navigation specification file. This helper function reads a YAML navigation file and constructs a corresponding ``NavSpec`` instance. Args: path: Path to the navigation specification file. Returns: A ``NavSpec`` instance representing the parsed specification. Raises: FileNotFoundError: If the specification file does not exist. ValueError: If the YAML structure is invalid. """ 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", {}), icon=data.get("icon"), )