feat: recursive pages

This commit is contained in:
amnesia
2025-04-14 21:06:52 +02:00
parent a113a13318
commit 3f0049dfdb
8 changed files with 138 additions and 484 deletions

View File

@@ -7,255 +7,173 @@ 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
from .config import SOURCE_DIRECTORY, DIST_DIRECTORY, LANGUAGE_INFORMATION, ARTICLE_PREVIEW_LENGTH, DEFAULT_LANGUAGE
logger = logging.getLogger("stsg.build")
class CustomPath:
def __init__(self, path: Path):
self.path = path
def replace_values(template: str, values: Dict[str, str]) -> str:
for key, value in values.items():
template = template.replace("{" + key + "}", value)
def __repr__(self) -> str:
return str(self.path)
return template
@property
def source_path(self) -> Path:
return Path(SOURCE_DIRECTORY, self.path)
@property
def dist_path(self) -> Path:
return Path(DIST_DIRECTORY, self.path)
def get_first_header_content(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)
@property
def name(self) -> str:
return Path(self.path).name
return self.language_code.native_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))
class Template:
def __init__(self, folder: Path):
self.folder = folder
def get_child(self, name: str, force_directory: bool = False, force_file: bool = False) -> Optional[CustomPath]:
child = Path(self.source_path, name)
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()
if not child.exists():
return None
TEMPLATE = Template(Path(SOURCE_DIRECTORY, "templates"))
if force_directory and not child.is_dir():
return None
if force_file and not child.is_file():
return None
class ArticleTranslation:
def __init__(self, file: Path, article_overview: ArticleOverview):
self.file = file
self.article_overview = article_overview
self.language_code = self.file.stem
return CustomPath(Path(self.path, name))
self.article_content = self.file.read_text()
if self.file.suffix == ".md":
self.article_content = markdown.markdown(self.article_content)
def read_text(self) -> str:
return self.source_path.read_text()
self.url = "/" + self.language_code + self.article_overview.url
self.dist_path = Path(DIST_DIRECTORY, self.url.strip("/"))
def copy_static(path: CustomPath):
src = path.source_path
dst = path.dist_path
_language_info = DEFAULT_LANGUAGE
parsed_language_code = self.language_code.lower().replace("-", "_")
if parsed_language_code in LANGUAGE_INFORMATION:
_language_info = LANGUAGE_INFORMATION[parsed_language_code]
elif parsed_language_code.split("_")[0] in LANGUAGE_INFORMATION:
_language_info = LANGUAGE_INFORMATION[parsed_language_code.split("_")[0]]
if not src.exists():
logger.warning("The static folder '%s' wasn't defined.", src)
self.language_name: str = _language_info["native_name"]
self.language_flag: str = _language_info["flag"]
self.priority: int = _language_info.get("priority", 0)
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) -> Dict[str, str]:
return {
"article_content": self.article_content,
"article_preview": self.article_content[:ARTICLE_PREVIEW_LENGTH] + "...",
"article_overview_url": self.article_overview.url,
"article_href": self.url,
"article_title": get_first_header_content(self.article_content),
"article_language_name": self.language_name,
"article_language_code": self.language_code,
"article_language_flag": self.language_flag,
}
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, url: str = ""):
self.directory = directory
self.name = self.directory.name
self.url = url + "/" + self.name
self.dist_path = Path(DIST_DIRECTORY, self.url.strip("/"))
self.child_articles: List[ArticleOverview] = []
self.article_translations: List[ArticleTranslation] = []
for c in self.directory.iterdir():
if c.is_file():
self.article_translations.append(ArticleTranslation(c, self))
elif c.is_dir():
self.child_articles.append(ArticleOverview(c, self.url))
# 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) -> Dict[str, str]:
return {
"overview_cards": self.overview_cards,
"overview_slug": self.name,
}
def get_overview(self) -> str:
global TEMPLATE
return replace_values(TEMPLATE.overview, self._get_values())
def copy_static():
src = str(Path(SOURCE_DIRECTORY, "static"))
dst = str(Path(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 '%s'", src, dst)
logger.info("copying static files from '%s' to '%r'", src, dst)
dst.mkdir(parents=True, exist_ok=True)
os.makedirs(dst, exist_ok=True)
for root, dirs, files in os.walk(src):
root_path = Path(root)
if any(p.startswith(".") for p in Path(root).parts):
continue
if any(part.startswith(".") for part in root_path.parts):
continue
# Compute relative path from the source root
rel_path = os.path.relpath(root, src)
dest_dir = os.path.join(dst, rel_path)
rel_path = root_path.relative_to(src)
dest_dir = dst / rel_path
dest_dir.mkdir(parents=True, exist_ok=True)
os.makedirs(dest_dir, exist_ok=True)
for file in files:
if file.startswith("."):
continue
src_file = root_path / file
dest_file = dest_dir / file
src_file = os.path.join(root, file)
dest_file = os.path.join(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 "<a href=\"{article_href}\"> {article_language_flag} {article_language_name} </a>"
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()))
logger.info("building static page")
copy_static()
tree = ArticleOverview(directory=Path(SOURCE_DIRECTORY, "pages"))
tree.build()

View File

@@ -756,3 +756,6 @@ LANGUAGE_INFORMATION = {
"native_name": "isiZulu"
}
}
DEFAULT_LANGUAGE = LANGUAGE_INFORMATION["de"]