from __future__ import annotations import logging import shutil from pathlib import Path import os import markdown from typing import Optional, Union, Dict, Generator, List, DefaultDict, Any from bs4 import BeautifulSoup from collections import defaultdict import toml import datetime from . import config def replace_values(template: str, values: Dict[str, str]) -> str: for key, value in values.items(): template = template.replace("{" + key + "}", value) return template def get_first_header_content(content, fallback: str = ""): soup = BeautifulSoup(content, 'html.parser') for level in range(1, 7): header = soup.find(f'h{level}') if header: return header.get_text(strip=True) return fallback def stem_to_language_code(stem: str) -> str: language_code = stem.lower().replace("-", "_") if language_code in config.languages: return language_code language_code = language_code.split("_")[0] if language_code in config.languages: return language_code logger.error("Didn't recognize %s as a valid language code, add it to the config, or fix your structure.", stem) exit(1) class LanguageDict(dict): def __missing__(self, key: str): if key not in config.languages: raise KeyError(key) lang_dict = config.languages[key] lang_dict["priority"] = lang_dict.get("priority", 0) elements = key.split("_") if len(elements) > 1: elements[-1] = elements[-1].upper() lang_dict["code"] = "-".join(elements) return lang_dict LANGUAGES = LanguageDict() class Template: def __init__(self, folder: Path): self.folder = folder self.article: str = (self.folder / "article.html").read_text() self.overview: str = (self.folder / "overview.html").read_text() self.translation_card: str = (self.folder / "translation_card.html").read_text() self.article_card: str = (self.folder / "article_card.html").read_text() class ArticleTranslation: def __init__(self, file: Path, article: Article): self.file = file self.article = article self.context: Dict[str, Any] = {} # initializing the location of the article translation self.language_code = stem_to_language_code(self.file.stem) self.location_in_tree = [self.language_code, *self.article.location_in_tree] self.url = "/" + "/".join(self.location_in_tree) self.dist_path = Path(config.setup.dist_directory, *self.location_in_tree) self.priority = LANGUAGES[self.language_code]["priority"] # TODO remove self.article_content = self.file.read_text() self.article_preview = self.article_content[:config.formatting.article_preview_length] + "..." if self.file.suffix == ".md": self.article_content = markdown.markdown(self.article_content) self.article_preview = markdown.markdown(self.article_preview) self.title = get_first_header_content(self.article_content, fallback="") def __init_context__(self): self.context["meta"] = self.article.context_meta self.context["language"] = LANGUAGES[self.language_code] def build(self): self.dist_path.mkdir(parents=True, exist_ok=True) with Path(self.dist_path, "index.html").open("w") as f: f.write(self.get_article()) def _get_values(self, return_foreign_articles: bool = True) -> Dict[str, str]: r = { "article_content": self.article_content, "article_preview": self.article_preview, "article_url": self.url, "article_overview_url": self.article.url, "article_slug": self.article.slug, "article_title": self.title, "article_datetime": self.article.article_written.strftime(config.formatting.datetime_format), "article_datetime_iso": self.article.article_written.isoformat(), } if return_foreign_articles: r.update(ARTICLE_REFERENCE_VALUES[self.language_code]) return r def get_article_values(self) -> Dict[str, str]: res = {} for key, value in self._get_values(return_foreign_articles=False).items(): res[key + ":" + self.article.slug] = value return res def get_article(self) -> str: return replace_values(TEMPLATE.article, self._get_values()) def get_translation_card(self) -> str: return replace_values(TEMPLATE.translation_card, self._get_values()) def get_article_card(self) -> str: return replace_values(TEMPLATE.article_card, self._get_values()) class Article: def __init__(self, directory: Path, location_in_tree: Optional[List[str]] = None, is_root: bool = False): self.directory = directory self.context: Dict[str, Any] = {} self.context_meta = self.context["meta"] = {} # initializing the config values of the article config_file = self.directory / "index.toml" self.config = toml.load(config_file) if config_file.exists() else {} # initializing the location and slug of the article self.slug = self.config.get("name", self.directory.name) if self.slug in ARTICLE_LAKE: logger.error("two articles have the same name at %s and %r", ARTICLE_LAKE[self.slug].directory, self.directory) exit(1) ARTICLE_LAKE[self.slug] = self self.location_in_tree: List[str] = location_in_tree or [] if not is_root: self.location_in_tree.append(self.slug) self.url = "/" + "/".join(self.location_in_tree) self.dist_path = Path(config.setup.dist_directory, *self.location_in_tree) self.article_written = datetime.datetime.fromisoformat(article_config["datetime"]) if "datetime" in article_config else datetime.datetime.fromtimestamp(self.directory.stat().st_mtime) self.child_articles: List[Article] = [] self.article_translations: List[ArticleTranslation] = [] self.article_translations_map: Dict[str, ArticleTranslation] = {} for c in self.directory.iterdir(): if c.name == "index.toml": continue if c.is_file(): at = ArticleTranslation(c, self) self.article_translations.append(at) self.article_translations_map[at.language_code] = at elif c.is_dir(): self.child_articles.append(Article( directory=c, location_in_tree=self.location_in_tree.copy(), )) # the tree is built self.article_translations.sort(key=lambda a: a.priority, reverse=True) self.translation_cards = "\n".join(a.get_translation_card() for a in self.article_translations) self.article_cards = "\n".join(c.get_article_card() for c in self.child_articles) for at in self.article_translations: at.__init_context__() logger.info("found %s at %s with the translations %s", self.slug, ".".join(list(self.location_in_tree)), ",".join(self.article_translations_map.keys())) def build(self): # builds the tree structure to the dist directory self.dist_path.mkdir(parents=True, exist_ok=True) with Path(self.dist_path, "index.html").open("w") as f: f.write(self.get_overview()) for at in self.article_translations: at.build() for ca in self.child_articles: ca.build() def _get_values(self, return_foreign_articles: bool = True) -> Dict[str, str]: r = { "article_url": self.url, "article_title": self.slug, "article_slug": self.slug, "article_datetime": self.article_written.strftime(config.formatting.datetime_format), "article_datetime_iso": self.article_written.isoformat(), "article_overview_url": self.url, "article_translation_cards": self.translation_cards, "article_children_cards": self.article_cards, } if return_foreign_articles: r.update(ARTICLE_REFERENCE_VALUES[""]) return r def get_article_values(self) -> Dict[str, str]: res = {} for key, value in self._get_values(return_foreign_articles=False).items(): res[key + ":" + self.slug] = value return res def get_overview(self) -> str: global TEMPLATE return replace_values(TEMPLATE.overview, self._get_values()) def get_article_card(self) -> str: return replace_values(TEMPLATE.article_card, self._get_values()) # GLOBALS logger = logging.getLogger("stsg.build") TEMPLATE = Template(Path(config.setup.source_directory, "templates")) ARTICLE_LAKE: Dict[str, Article] = {} ARTICLE_REFERENCE_VALUES: DefaultDict[str, Dict[str, str]] = defaultdict(dict) def build(): logger.info("starting build process...") logger.info("copying static folder...") shutil.copytree(Path(config.setup.source_directory, "static"), Path(config.setup.dist_directory, "static"), dirs_exist_ok=True) logger.info("reading page tree...") tree = Article(directory=Path(config.setup.source_directory, "articles"), is_root=True) # build article reverence values for article_overview in ARTICLE_LAKE.values(): ARTICLE_REFERENCE_VALUES[""].update(article_overview.get_article_values()) for language_code, at in article_overview.article_translations_map.items(): ARTICLE_REFERENCE_VALUES[language_code].update(at.get_article_values()) logger.info("writing page tree...") tree.build()