from functools import cache from pathlib import Path from typing import Any, Iterator from markdown import Markdown from yaml import Loader, load class Content: def __init__(self, path: Path) -> None: self.__path = path @property def path(self) -> Path: return self.__path class ContentDirectory(Content): def load(self, subpath: str | Path) -> Content: subpath = Path(subpath) current: Content = self for part in subpath.parts: if not isinstance(current, ContentDirectory): raise NotADirectoryError(self.path) current = current.__load_children(part) return current @cache # noqa: B019 def __load_children(self, name: str) -> Content: child_path = self.path / name if not child_path.exists(): raise FileNotFoundError(child_path) if child_path.is_dir(): return ContentDirectory(child_path) if child_path.is_file(): if child_path.suffix in [".yml", ".yaml", ".json"]: return DataFile(child_path) if child_path.suffix == ".md": return MarkdownFile(child_path) raise NotImplementedError() class DataField: def __init__(self, file: "DataFile", value: Any) -> None: self.__file = file self.__value = value def as_path(self) -> Path: return self.__file.path.parent / str(self.__value) def __str__(self) -> str: return str(self.__value) def __getitem__(self, key: Any) -> Any: return DataField(self.__file, self.__value.get(key)) def __iter__(self) -> Iterator[Any]: for it in self.__value: yield DataField(self.__file, it) class DataFile(Content): def __init__(self, path: Path) -> None: super().__init__(path) with path.open("r", encoding="utf-8") as data_file: self.__data = load(data_file, Loader) def __getitem__(self, key: Any) -> Any: return DataField(self, self.__data.get(key)) def __iter__(self) -> Iterator[Any]: for it in self.__data: yield DataField(self, it) class MarkdownFile(Content): def __init__(self, path: Path) -> None: super().__init__(path) with path.open("r", encoding="utf-8") as markdown_file: self.__markdown = Markdown(extensions=["full_yaml_metadata"]) self.__html = self.__markdown.convert(markdown_file.read()) @property def html(self) -> str: return self.__html @property def meta(self) -> Any: return self.__markdown.Meta # type: ignore