from __future__ import annotations import logging import shutil from pathlib import Path import os import markdown from typing import Optional, Union, Dict, Generator, List from .config import SOURCE_DIRECTORY, DIST_DIRECTORY, LANGUAGE_INFORMATION, ARTICLE_PREVIEW_LENGTH logger = logging.getLogger("stsg.build") class CustomPath: def __init__(self, path: Path): self.path = path def __repr__(self) -> str: return str(self.path) @property def source_path(self) -> Path: return Path(SOURCE_DIRECTORY, self.path) @property def dist_path(self) -> Path: return Path(DIST_DIRECTORY, self.path) @property def name(self) -> str: return Path(self.path).name @property def parent(self) -> CustomPath: return CustomPath(Path(self.path).parent) @property def stem(self) -> str: return Path(self.path).stem def iterdir(self) -> Generator[CustomPath, None, None]: for p in self.source_path.iterdir(): yield CustomPath(Path(self.path, p.name)) def get_child(self, name: str, force_directory: bool = False, force_file: bool = False) -> Optional[CustomPath]: child = Path(self.source_path, name) if not child.exists(): return None if force_directory and not child.is_dir(): return None if force_file and not child.is_file(): return None return CustomPath(Path(self.path, name)) def read_text(self) -> str: return self.source_path.read_text() def copy_static(path: CustomPath): src = path.source_path dst = path.dist_path if not src.exists(): logger.warning("The static folder '%s' wasn't defined.", src) return logger.info("Copying static files from '%s' to '%s'", src, dst) dst.mkdir(parents=True, exist_ok=True) for root, dirs, files in os.walk(src): root_path = Path(root) if any(part.startswith(".") for part in root_path.parts): continue rel_path = root_path.relative_to(src) dest_dir = dst / rel_path dest_dir.mkdir(parents=True, exist_ok=True) for file in files: if file.startswith("."): continue src_file = root_path / file dest_file = dest_dir / file shutil.copy2(src_file, dest_file) class CustomLanguageCode: def __init__(self, language_code: str): self.language_code = language_code def __repr__(self) -> str: return f"{self.language_code}" def _get_additional_data(self) -> dict: parsed_language_code = self.language_code.lower().replace("-", "_") if parsed_language_code in LANGUAGE_INFORMATION: return LANGUAGE_INFORMATION[parsed_language_code] parsed_language_code = parsed_language_code.split("_")[0] if parsed_language_code in LANGUAGE_INFORMATION: return LANGUAGE_INFORMATION[parsed_language_code] return {} @property def flag(self) -> str: return self._get_additional_data()["flag"] @property def native_name(self) -> str: return self._get_additional_data()["native_name"] class Article(CustomPath): def __init__(self, path: CustomPath): super().__init__(path.path) def get_content(self) -> str: if self.name.endswith(".md"): return markdown.markdown(self.read_text()) return self.read_text() @property def article_directory(self) -> Path: return self.dist_path.parent / self.stem @property def language_code(self) -> CustomLanguageCode: return CustomLanguageCode(self.stem) def get_article_keys(self) -> Dict[str, str]: article_content = self.get_content() return { "article_content": article_content, "article_preview": article_content[:ARTICLE_PREVIEW_LENGTH], "article_href": "/" + str(self.path.parent / self.stem), "article_language_name": self.language_code.native_name, "article_language_code": self.language_code.language_code, "article_language_flag": self.language_code.flag, } class Template: def __init__(self, root: CustomPath): self.root = root self.articles: List[Article] = [] def copy(self): return Template(root=self.root) def _replace_keywords(self, text: str, **placeholder_values: str) -> str: for key, value in placeholder_values.items(): text = text.replace("{" + key + "}", value) return text def get_article_template(self) -> str: article = self.root.get_child("article.html", force_file=True) return article.source_path.read_text() if article else "{article_content}" def build_article(self, path: Article): logger.info("converting %s", path) article_text = self._replace_keywords( self.get_article_template(), **path.get_article_keys(), ) path.article_directory.mkdir(parents=True, exist_ok=True) with Path(path.article_directory, "index.html").open("w") as f: f.write(article_text) self.articles.append(path) def get_overview_card_template(self) -> str: overview_card = self.root.get_child("overview_card.html", force_file=True) return overview_card.source_path.read_text() if overview_card else " {article_language_flag} {article_language_name} " def get_overview_template(self) -> str: overview = self.root.get_child("overview.html", force_file=True) return overview.source_path.read_text() if overview else "{overview_cards}" def build_overview(self, root: CustomPath): if not len(self.articles): return overview_card_template = self.get_overview_card_template() overview_cards = "\n".join(self._replace_keywords( overview_card_template, **a.get_article_keys(), ) for a in self.articles) overview_text = self._replace_keywords( self.get_overview_template(), overview_cards = overview_cards, ) with Path(root.dist_path, "index.html").open("w") as f: f.write(overview_text) def walk_directory(root: CustomPath, template: Optional[Template] = None): template_dir = root.get_child("_templates", force_directory=True) if template_dir is not None: template = Template(template_dir) if template is None: logger.error("Didn't find template for %d", root) return for current_path in root.iterdir(): if current_path.name.startswith("_") or current_path.name == "static": continue if current_path.source_path.is_file(): template.build_article(Article(current_path)) continue if current_path.source_path.is_dir(): walk_directory(current_path, template=template.copy()) template.build_overview(root=root) static_dir = root.get_child("static", force_directory=True) if static_dir: copy_static(static_dir) def build(): logger.info("building static page") walk_directory(CustomPath(Path()))