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 from bs4 import BeautifulSoup from collections import defaultdict 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 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.overview_card: str = (self.folder / "overview_card.html").read_text() class ArticleTranslation: def __init__(self, file: Path, article_overview: ArticleOverview): self.file = file self.article_overview = article_overview self.language_code = self.file.stem self.article_content = self.file.read_text() if self.file.suffix == ".md": self.article_content = markdown.markdown(self.article_content) self.location_in_tree = [self.language_code, *self.article_overview.location_in_tree] self.url = "/" + "/".join(self.location_in_tree) self.dist_path = Path(config.setup.dist_directory, *self.location_in_tree) _language_info = config.languages[config.formatting.fallback_language] parsed_language_code = self.language_code.lower().replace("-", "_") if parsed_language_code in config.languages: _language_info = config.languages[parsed_language_code] elif parsed_language_code.split("_")[0] in config.languages: _language_info = config.languages[parsed_language_code.split("_")[0]] self.language_name: str = _language_info["native_name"] self.language_flag: str = _language_info["flag"] self.priority: int = _language_info.get("priority", 0) self.title = get_first_header_content(self.article_content, fallback=self.language_name) 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_content[:config.formatting.article_preview_length] + "...", "article_url": self.url, "article_overview_url": self.article_overview.url, "article_slug": self.article_overview.slug, "article_title": self.title, "article_language_name": self.language_name, "article_language_code": self.language_code, "article_language_flag": self.language_flag, } 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_overview.slug] = value return res def get_article(self) -> str: global TEMPLATE return replace_values(TEMPLATE.article, self._get_values()) def get_overview_card(self) -> str: global TEMPLATE return replace_values(TEMPLATE.overview_card, self._get_values()) class ArticleOverview: def __init__(self, directory: Path, location_in_tree: Optional[List[str]] = None): self.directory = directory self.slug = self.directory.name self.article_written = self.directory.stat().st_mtime print(self.article_written) self.location_in_tree: List[str] = location_in_tree or [] 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) 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.child_articles: List[ArticleOverview] = [] self.article_translations: List[ArticleTranslation] = [] self.article_translations_map: Dict[str, ArticleTranslation] = {} for c in self.directory.iterdir(): 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(ArticleOverview( 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.overview_cards = "\n".join(a.get_overview_card() for a in self.article_translations) 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_overview_url": self.url, "article_overview_cards": self.overview_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 copy_static(): src = str(Path(config.setup.source_directory, "static")) dst = str(Path(config.setup.dist_directory, "static")) if not os.path.exists(src): logger.warn("The static folder '%s' wasn't defined.", src) return logger.info("copying static files from '%s' to '%r'", src, dst) os.makedirs(dst, exist_ok=True) for root, dirs, files in os.walk(src): if any(p.startswith(".") for p in Path(root).parts): continue # Compute relative path from the source root rel_path = os.path.relpath(root, src) dest_dir = os.path.join(dst, rel_path) os.makedirs(dest_dir, exist_ok=True) for file in files: if file.startswith("."): continue src_file = os.path.join(root, file) dest_file = os.path.join(dest_dir, file) shutil.copy2(src_file, dest_file) # GLOBALS logger = logging.getLogger("stsg.build") TEMPLATE = Template(Path(config.setup.source_directory, "templates")) ARTICLE_LAKE: Dict[str, ArticleOverview] = {} ARTICLE_REFERENCE_VALUES: DefaultDict[str, Dict[str, str]] = defaultdict(dict) def build(): logger.info("building static page") copy_static() tree = ArticleOverview(directory=Path(config.setup.source_directory, "articles")) # build article reverence values for article_overview in ARTICLE_LAKE.values(): logger.info("found article %s", article_overview.slug) 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()) tree.build()