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 bs4 import BeautifulSoup 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"] @property def priority(self) -> int: return self._get_additional_data().get("priority", 0) 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_first_header_content(self, content): 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 self.language_code.native_name 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_overview_href": "/" + str(self.path.parent), "article_href": "/" + str(self.path.parent / self.stem), "article_title": self.get_first_header_content(article_content), "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() self.articles.sort(key=lambda a: a.language_code.priority, reverse=True) 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()))