Compare commits

..

No commits in common. "433e2f6023ef517d1eb931cd974911d16de48ab8" and "632f47e017f4f4f7a121e091341a3cd8140ccf49" have entirely different histories.

7 changed files with 104 additions and 209 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,58 +0,0 @@
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]]