Compare commits

...

5 Commits

Author SHA1 Message Date
Hazel Noack
433e2f6023 couldnt add multiple translations anymore 2025-05-22 15:50:51 +02:00
Hazel Noack
2690f0af87 edited templates 2025-05-22 15:47:46 +02:00
Hazel Noack
f2a3d7ada4 refactoring article context building 2025-05-22 13:43:34 +02:00
Hazel Noack
753de66e08 feat: started cleaning up context 2025-05-22 12:59:50 +02:00
Hazel Noack
036d5fb30a feat: started cleaning up context 2025-05-22 12:56:40 +02:00
7 changed files with 209 additions and 104 deletions

View File

@ -1,2 +1,2 @@
name="stsg"
datetime="2024-04-15 13:45:12.123456"
iso_date="2024-04-15 13:45:12.123456"

View File

@ -4,7 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/static/icon.ico">
<title>{{slug}}</title>
<title>{{name}}</title>
<link rel="stylesheet" href="/static/bulma.min.css" />
<link rel="stylesheet" href="/static/style.css" />
</head>
@ -16,7 +16,7 @@
>
<div class="navbar-brand">
<a class="navbar-item" href="#">
<strong>Static Translated Site Generator</strong>
<strong>{{name}}</strong>
</a>
</div>
</nav>
@ -33,7 +33,7 @@
<div class="card mb-4" lang="{{t.language.code}}" style="height: 100%;">
<a href="{{t.url}}" hreflang="{{t.language.code}}" class="card mb-4" style="color: inherit; text-decoration: none;">
<div class="card-content">
<p class="title">{{t.language.flag}} {{t.title}}</p>
<p class="title">{{t.language.flag}} {{t.name}}</p>
<hr />
<p class="content">
{{t.preview}}
@ -41,7 +41,7 @@
</div>
<div class="card-footer">
<time class="card-footer-item" datetime="{{iso_date}}">{{date}}</time>
<time class="card-footer-item" datetime="{{t.iso_date}}">{{t.date}}</time>
</div>
</a>
</div>
@ -62,15 +62,15 @@
<div class="card mb-4" >
<a href="{{c.url}}" class="card mb-4" style="color: inherit; text-decoration: none;">
<div class="card-content">
<p class="title">{{c.slug}} </p>
<p class="title">{{c.name}} </p>
<hr />
<p class="content is-flex is-flex-direction-column" style="gap: 10px;">
{% for ct in c.translations %}
<a href="{{ct.url}}" hreflang="{{ct.language.code}}">{{ct.language.flag}}: {{ct.title}}</a>
<a href="{{ct.url}}" hreflang="{{ct.language.code}}">{{ct.language.flag}}: {{ct.name}}</a>
{% endfor %}
</p>
<hr />
<time datetime="{{iso_date}}">{{date}}</time>
<time datetime="{{c.iso_date}}">{{c.date}}</time>
</div>
</a>
</div>
@ -83,7 +83,7 @@
<!-- Footer -->
<footer class="footer">
<div class="content has-text-centered">
<p><strong>STSG</strong> by Hazel. &copy; 2025</p>
<p><strong>{{name}}</strong> by {{author}}. &copy; 2025</p>
</div>
</footer>
</body>

View File

@ -4,7 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/static/icon.ico">
<title>{{title}}</title>
<title>{{name}}</title>
<link rel="stylesheet" href="/static/bulma.min.css" />
<link rel="stylesheet" href="/static/style.css" />
</head>
@ -18,7 +18,7 @@
<div class="navbar-brand">
<a class="navbar-item" href="{{article_url}}">
<strong>{{language.flag}} {{title}}</strong>
<time datetime="{{meta.iso_date}}">{{meta.date}}</time>
<time datetime="{{iso_date}}">{{date}}</time>
</a>
</div>
</nav>
@ -39,7 +39,7 @@
<div class="card mb-4" >
<a href="{{c.url}}" hreflang="{{c.language.code}}" class="card mb-4" style="color: inherit; text-decoration: none;">
<div class="card-content">
<p class="title">{{c.title}}</p>
<p class="title">{{c.name}}</p>
<hr />
<p class="content">
{{c.preview}}
@ -47,7 +47,7 @@
</div>
<div class="card-footer">
<time class="card-footer-item" datetime="{{c.meta.iso_date}}">{{c.meta.date}}</time>
<time class="card-footer-item" datetime="{{c.iso_date}}">{{c.date}}</time>
</div>
</a>
</div>
@ -61,7 +61,7 @@
<!-- Footer -->
<footer class="footer">
<div class="content has-text-centered">
<p><strong>STSG</strong> by Hazel. &copy; 2025</p>
<p><strong>{{name}}</strong> by {{author}}. &copy; 2025</p>
</div>
</footer>
</body>

View File

@ -1,3 +1,5 @@
fall_back_to_overview_in_translation = false
[setup]
source_directory = "src"
dist_directory = "dist"

View File

@ -1,4 +1,7 @@
class config:
default_author = "anonymous"
fall_back_to_overview_in_translation = True
class setup:
source_directory = "src"
dist_directory = "dist"

View File

@ -6,26 +6,17 @@ import os
from markdown2 import markdown
from typing import Optional, Union, Dict, Generator, List, DefaultDict, Any, TypedDict, Set
from bs4 import BeautifulSoup
from collections import defaultdict
from collections import defaultdict, UserList
import toml
from datetime import datetime
import jinja2
from functools import cached_property
from .definitions import *
from . import config
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 shorten_text_and_clean(html_string, max_length=config.formatting.preview_length):
soup = BeautifulSoup(html_string, 'html.parser')
@ -120,23 +111,69 @@ class LanguageDict(dict):
LANGUAGES = LanguageDict()
def compile_cross_article_context(cross_article_context):
title = cross_article_context["title"]
url = cross_article_context["url"]
def add_html_link(c):
name = c["name"]
url = c["url"]
cross_article_context["link"] = f'<a href="{url}">{title}</a>'
c["link"] = f'<a href="{url}">{name}</a>'
class ArticleTranslationContext(TypedDict):
slug: str
name: str
datetime: str
author: str
url: str
def get_translated_articles(articles: List[Article], language_code: str = None) -> List[Union[ArticleTranslation, Article]]:
result = {}
for a in articles:
if a.slug in result:
continue
if language_code is None:
result[a.slug] = a
continue
if not config.fall_back_to_overview_in_translation and language_code not in a.article_translations_map:
continue
result[a.slug] = a.article_translations_map.get(language_code, a)
class ArticleList(UserList):
def __init__(self, iterable):
super().__init__(item for item in iterable)
self.used_slugs = set()
def append(self, a: Union[Article, str]):
if isinstance(a, str):
a = ARTICLE_LAKE[a]
if a.slug in self.used_slugs:
return
self.used_slugs.add(a.slug)
self.data.append(a)
def extend(self, other):
for a in other:
self.append(a)
def get_translated(self, language_code: str) -> ArticleList[Union[ArticleTranslation, Article]]:
res = ArticleList([])
for a in self:
if not config.fall_back_to_overview_in_translation and language_code not in a.article_translations_map:
continue
res.append(a.article_translations_map.get(language_code, a))
return res
@property
def context(self) -> List[Union[ArticleContext, ArticleTranslationContext]]:
return [a.context for a in self]
class ArticleTranslation:
article: Article
slug: str = property(fget=lambda self: self.article.slug)
file: Path
@cached_property
@ -183,31 +220,32 @@ class ArticleTranslation:
self.article = article
self.file = file
self.context = {}
self.cross_article_context = TRANSLATED_CROSS_ARTICLE_CONTEXT[self.language_code][self.article.slug] = {}
self.context = TRANSLATED_CROSS_ARTICLE_CONTEXT[self.language_code][self.article.slug] = {}
@cached_property
def name(self) -> str:
soup = BeautifulSoup(self.html_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.article.name
def __init_context__(self):
self.context["meta"] = self.article.context_shared
self.context["slug"] = self.article.slug
self.context["name"] = self.name
self.context["url"] = self.url
add_html_link(self.context)
self.context["date"] = self.article.modified_at.strftime(config.formatting.datetime_format)
self.context["iso_date"] = self.article.modified_at.isoformat()
self.context["author"] = self.article.author
self.context["language"] = LANGUAGES[self.language_code]
self.context["article_url"] = self.article.url
self.context["title"] = get_first_header_content(self.html_content, fallback=LANGUAGES[self.language_code]["native_name"])
self.cross_article_context.update(self.article.context_shared)
self.cross_article_context["title"] = self.context["title"]
self.cross_article_context["article_url"] = self.article.url
self.cross_article_context["url"] = self.url
compile_cross_article_context(self.cross_article_context)
# get children
self.context["children"] = [
c.article_translations_map[self.language_code].context for c in self.article.child_articles
if self.language_code in c.article_translations_map
]
self.linked_context = self.context["linked"] = []
self.related_context = self.context["related"] = []
self.context["children"] = self.article.child_articles.get_translated(self.language_code).context
def __init_content_context__(self):
template = jinja2.Template(self.html_content)
@ -221,17 +259,16 @@ class ArticleTranslation:
template.environment.context_class = jinja2.runtime.Context
accessed_keys = template.environment.accessed_keys
for key in accessed_keys:
a = ARTICLE_LAKE[key]
if self.language_code in a.article_translations_map:
self.linked_context.append(a.article_translations_map[self.language_code].context)
self.related_context.extend(self.linked_context)
self.related_context.extend(self.context["children"])
for key in accessed_keys:
self.article.linked_articles.append(key)
self.context["content"] = self.html_content
self.context["preview"] = get_preview_text(html_string=self.html_content)
self.context["linked"] = self.article.linked_articles.get_translated(self.language_code).context
self.context["related"] = self.article.related_articles.get_translated(self.language_code).context
def build(self):
self.dist_path.mkdir(parents=True, exist_ok=True)
@ -239,20 +276,6 @@ class ArticleTranslation:
f.write(TEMPLATE["article_translation"].render(self.context))
class ArticleConfig(TypedDict):
slug: str
name: str
datetime: str
author: str
class ArticleContext(TypedDict):
slug: str
name: str
datetime: str
author: str
url: str
class Article:
directory: Path
@ -288,8 +311,18 @@ class Article:
return Path(config.setup.dist_directory, *self.slug_path)
context: ArticleContext
context_shared: Dict[str, Any]
cross_article_context: Dict[str, Any]
child_articles: ArticleList[Article]
article_translations_list: List[ArticleTranslation]
article_translations_map: Dict[str, ArticleTranslation]
linked_articles: ArticleList[Article]
@cached_property
def related_articles(self) -> ArticleList[Article]:
res = ArticleList(self.child_articles)
res.extend(self.linked_articles)
return res
def __init__(self, directory: Path, article_path: Optional[List[str]] = None, is_root: bool = False, parent: Optional[Article] = None):
self.directory = directory
@ -297,16 +330,16 @@ class Article:
self.article_path: List[Article] = article_path or []
self.article_path.append(self)
self.context: ArticleContext = {}
self.context_shared = {}
self.cross_article_context = CROSS_ARTICLE_CONTEXT[self.slug] = {}
self.context = CROSS_ARTICLE_CONTEXT[self.slug] = {}
ARTICLE_LAKE[self.slug] = self
self.linked_articles = ArticleList([])
# build the tree
self.child_articles: List[Article] = []
self.article_translations_list: List[ArticleTranslation] = []
self.article_translations_map: Dict[str, ArticleTranslation] = {}
self.child_articles = ArticleList([])
self.article_translations_list = []
self.article_translations_map = {}
for c in self.directory.iterdir():
if c.name == "index.toml":
@ -327,41 +360,50 @@ class Article:
logger.info("found %s at %s with the translations %s", self.slug, ".".join(list(self.slug_path)), ",".join(self.article_translations_map.keys()))
@cached_property
def modified_at(self) -> datetime:
if "iso_date" in self.config:
return datetime.fromisoformat(self.config["iso_date"])
"""
TODO
scann every article file and use the youngest article file
"""
return datetime.fromtimestamp(self.directory.stat().st_mtime)
@cached_property
def author(self) -> str:
return self.config.get("author", config.default_author)
def __init_context__(self):
self.context_shared["url"] = self.url
self.context_shared["slug"] = self.slug
modified_at = datetime.fromisoformat(self.config["datetime"]) if "datetime" in self.config else datetime.fromtimestamp(self.directory.stat().st_mtime)
self.context_shared["date"] = modified_at.strftime(config.formatting.datetime_format)
self.context_shared["iso_date"] = modified_at.isoformat()
self.context.update(self.context_shared)
self.cross_article_context.update(self.context_shared)
self.cross_article_context["title"] = self.context_shared["slug"]
self.cross_article_context["article_url"] = self.context_shared["url"]
compile_cross_article_context(self.cross_article_context)
self.context["slug"] = self.slug
self.context["name"] = self.name
self.context["url"] = self.url
add_html_link(self.context)
self.context["date"] = self.modified_at.strftime(config.formatting.datetime_format)
self.context["iso_date"] = self.modified_at.isoformat()
self.context["author"] = self.author
# recursive context structures
translation_list = self.context["translations"] = []
child_article_list = self.context["children"] = []
self.context["translations"] = [c.context for c in self.article_translations_list]
self.context["children"] = self.child_articles.context
for lang, article in self.article_translations_map.items():
self.context[lang] = article.context
for article_translation in self.article_translations_list:
self.context[article_translation.language_code] = article_translation.context
translation_list.append(article_translation.context)
for child_article in self.child_articles:
child_article_list.append(child_article.context)
# recursively build context
for at in self.article_translations_list:
at.__init_context__()
for a in self.child_articles:
a.__init_context__()
def __init_content_context__(self):
for at in self.article_translations_list:
at.__init_content_context__()
self.context["linked"] = self.linked_articles.context
self.context["related"] = self.related_articles.context
for a in self.child_articles:
a.__init_content_context__()
@ -387,9 +429,9 @@ class ContextDict(jinja2.runtime.Context):
# GLOBALS
logger = logging.getLogger("stsg.build")
ARTICLE_LAKE: Dict[str, Article] = {}
CROSS_ARTICLE_CONTEXT: Dict[str, Dict[str, Any]] = {}
TRANSLATED_CROSS_ARTICLE_CONTEXT: Dict[str, Dict[str, Dict[str, Any]]] = defaultdict(dict)
ARTICLE_LAKE: Dict[str, Article] = {}
ARTICLE_REFERENCE_VALUES: DefaultDict[str, Dict[str, str]] = defaultdict(dict)

58
stsg/definitions.py Normal file
View File

@ -0,0 +1,58 @@
from __future__ import annotations
from typing import TypedDict, List, Union
class ArticleConfig(TypedDict):
slug: str
name: str
iso_date: str
author: str
class ArticleContext(TypedDict):
slug: str
name: str
url: str
link: str
date: str
iso_date: str
author: str
translations: List[ArticleTranslationContext]
children: List[ArticleContext]
linked: List[ArticleContext]
related: List[ArticleContext]
class TypedLanguage(TypedDict):
flag: str
name: str
native_name: str
priority: int
code: str
class ArticleTranslationContext(TypedDict):
slug: str
name: str
url: str
link: str
date: str
iso_date: str
author: str
language: TypedLanguage
article_url: str
"""
The type Union[ArticleTranslationContext, ArticleContext] exist,
because if the article it is linked to doesn't exist in the same languages it uses the overview instead.
If you dislike this behavior set:
config.fall_back_to_overview_in_translation = False
"""
children: List[Union[ArticleTranslationContext, ArticleContext]]
# you can't use these within the markdown text itself
content: str
preview: str
linked: List[Union[ArticleTranslationContext, ArticleContext]]
related: List[Union[ArticleTranslationContext, ArticleContext]]