STSG/stsg/build.py

267 lines
9.4 KiB
Python

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
from datetime 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_slug": self.article.slug,
"article_title": self.title,
}
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)
# build the tree
self.child_articles: List[Article] = []
self.article_translations_list: 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_list.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(),
))
self.article_translations_list.sort(key=lambda a: a.priority, reverse=True)
logger.info("found %s at %s with the translations %s", self.slug, ".".join(list(self.location_in_tree)), ",".join(self.article_translations_map.keys()))
# the tree is built
self.translation_cards = "\n".join(a.get_translation_card() for a in self.article_translations_list)
self.article_cards = "\n".join(c.get_article_card() for c in self.child_articles)
for at in self.article_translations_list:
at.__init_context__()
self.__init_context__()
def __init_context__(self):
self.context["url"] = self.url
self.context_meta["slug"] = self.slug
modified_at = datetime.fromisoformat(article_config["datetime"]) if "datetime" in article_config else datetime.fromtimestamp(self.directory.stat().st_mtime)
self.context_meta["date"] = modified_at.strftime(config.formatting.datetime_format)
self.context_meta["iso_date"] = modified_at.isoformat()
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_list:
at.build()
for ca in self.child_articles:
ca.build()
def _get_values(self, return_foreign_articles: bool = True) -> Dict[str, str]:
r = {
"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()