Compare commits

12 Commits

Author SHA1 Message Date
Hazel Noack
4b5701e05b feat: added favicon 2025-05-16 16:00:05 +02:00
Hazel Noack
8874fb0935 feat: added favicon 2025-05-16 15:59:43 +02:00
Hazel Noack
f02d37a6af added link context 2025-05-16 15:18:02 +02:00
Hazel Noack
f84fb65aa7 feat: fallback to overview page 2025-05-16 15:12:22 +02:00
Hazel Noack
d019884dbe implemented data structure for cross article context 2025-05-16 15:01:09 +02:00
Hazel Noack
39a8d7c1db added some simple documentation 2025-05-16 14:09:29 +02:00
7d1ceded8d meow 2025-04-17 17:42:50 +02:00
b3e23a53d9 feat: added children to context 2025-04-17 14:39:14 +02:00
b2513f7caf improved child cards from overview 2025-04-17 14:22:48 +02:00
4743456bd8 feat: improved translation cards 2025-04-17 14:08:26 +02:00
6994662bb4 feat: shift the headers in the preview 2025-04-17 14:05:59 +02:00
db23ceac78 feat: removed unnecessary code 2025-04-17 13:54:37 +02:00
16 changed files with 244 additions and 49 deletions

2
.gitignore vendored
View File

@@ -174,4 +174,4 @@ cython_debug/
.pypirc
dist
context.json
*.json

View File

@@ -0,0 +1,36 @@
# Installing
```sh
python3 -m venv .venv
source .venv/bin/activate
pip install -e .
```
# Execute
To start a local http server in the dist folder you can simply do:
```sh
python3 -m http.server 1312
```
Then visit `localhost:1312`
```
# build it normally
stsg
# start a hot reload server
stsg_dev
```
# build favicon
```
cd src/static/assets
inkscape -w 16 -h 16 -o 16.png logo.svg
inkscape -w 32 -h 32 -o 32.png logo.svg
inkscape -w 48 -h 48 -o 48.png logo.svg
convert 16.png 32.png 48.png ../icon.ico
```

View File

@@ -2,6 +2,8 @@
## Sunt aevis
{{example.link}}
Lorem markdownum turbavere prisca Aeacidae morando esse. Quam Styga spectata,
pariter Iove iunctis exercere Solis? Atlantis possit succurrere quam!

View File

@@ -4,6 +4,8 @@ meow
## Usque de celebrabant puer
[{{index_page.title}}]({{index_page.url}})
Lorem markdownum, nec ora et vero me nec natae suadent. Nec damno ignorat
propiore aliquid temptata decipienda habetur. Vulnera lacrimis aequoreo madidos,
copia uvae, herbosaque quoque, per harenas, canos fui monstro Peleus.

View File

@@ -1,6 +1,6 @@
# Navê Gotarê
[{article_title:example}]({article_url:example})
[{{example.title}}]({{example.url}})
Ev gotar, mînakek placeholder ya bi zimanê Kurdî ye. Nivîs, weşan û rûpelên nûçe di vê belgeyê de têne pêşniyar kirin. Ev metn ji bo testkirina layout an jî demo-yê hatiye çêkirin û çend bingehên cihanî yên naveroka gotarê nîşan dide.
@@ -35,4 +35,4 @@ Kodê li ser vê koda simplesa gotinê ye û dikare weşana nûçe an demo-yê p
# Encama Gotarê
Di encama gotarê de, hûn dikarin her çend beşên bingehîn yên nûçe, daxuyanî û şirove yên navekî bifikirin.
Bê guman, ev placeholder bi awayek qelew li ser çalakiya te ya malper, blog an jî her sedema dijîtal tê de karîger e.
Bê guman, ev placeholder bi awayek qelew li ser çalakiya te ya malper, blog an jî her sedema dijîtal tê de karîger e.

BIN
src/static/assets/16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 B

BIN
src/static/assets/32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
src/static/assets/48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
src/static/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="210mm"
height="210mm"
viewBox="0 0 210 210"
version="1.1"
id="svg1"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
sodipodi:docname="logo.svg"
xml:space="preserve"
inkscape:export-filename="logo.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.84452862"
inkscape:cx="396.67099"
inkscape:cy="391.34257"
inkscape:window-width="1672"
inkscape:window-height="957"
inkscape:window-x="816"
inkscape:window-y="1259"
inkscape:window-maximized="0"
inkscape:current-layer="svg1"
showgrid="true"><inkscape:grid
id="grid1"
units="mm"
originx="0"
originy="0"
spacingx="105"
spacingy="105"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="5"
enabled="true"
visible="true" /></sodipodi:namedview><defs
id="defs1"><rect
x="396.8504"
y="396.8504"
width="396.8504"
height="396.8504"
id="rect4" /><rect
x="396.8504"
y="0"
width="396.8504"
height="396.8504"
id="rect3" /><rect
x="0"
y="0"
width="396.8504"
height="396.8504"
id="rect2" /><rect
x="0"
y="0"
width="793.70079"
height="793.70079"
id="rect1" /><rect
x="0"
y="0"
width="396.8504"
height="396.8504"
id="rect2-4" /></defs><circle
style="fill:#ff7575;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99999;stroke-dasharray:none;stroke-opacity:1"
id="path5"
cx="105"
cy="105"
r="105" /><g
style="fill:#3eebff;stroke:none;stroke-opacity:1;fill-opacity:1"
id="g5"
transform="matrix(0.96400101,0,0,0.96400101,0,-0.83284847)"><path
d="M 109.786,0 C 49.25,0 0,49.25 0,109.785 c 0,60.536 49.25,109.786 109.786,109.786 60.536,0 109.785,-49.25 109.785,-109.786 C 219.571,49.25 170.322,0 109.786,0 Z m 94.488,102.285 h -35.978 c -1.143,-33.154 -9.811,-61.858 -22.93,-80.348 32.479,13.202 56.043,43.909 58.908,80.348 z M 102.286,16.584 v 85.702 H 66.293 C 67.946,56.265 84.634,23.895 102.286,16.584 Z m 0,100.701 v 85.703 C 84.634,195.676 67.946,163.306 66.293,117.285 Z m 15,85.703 v -85.703 h 35.993 c -1.653,46.021 -18.341,78.391 -35.993,85.703 z m 0,-100.703 V 16.584 c 17.651,7.312 34.34,39.682 35.993,85.702 H 117.286 Z M 74.206,21.937 C 61.087,40.428 52.418,69.131 51.276,102.286 h -35.98 c 2.867,-36.44 26.431,-67.147 58.91,-80.349 z m -58.91,95.348 h 35.98 c 1.142,33.155 9.811,61.859 22.93,80.35 -32.479,-13.203 -56.043,-43.91 -58.91,-80.35 z m 130.07,80.349 c 13.119,-18.49 21.787,-47.194 22.93,-80.349 h 35.978 c -2.865,36.44 -26.429,67.147 -58.908,80.349 z"
id="path1"
style="stroke:none;stroke-opacity:1;fill:#3eebff;fill-opacity:1" /></g></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
src/static/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -3,11 +3,11 @@
<head>
<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>
<link rel="stylesheet" href="/static/bulma.min.css" />
</head>
<body>
<!-- Header (Navbar) -->
<nav
class="navbar is-primary"
role="navigation"
@@ -23,22 +23,31 @@
<section class="section">
{% if translations|length %}
<div class="container content">
<div class="column is-half is-offset-one-quarter">
<div class="content">
<h1>Translations</h1>
</div>
<div class="column is-half is-offset-one-quarter">
<div class="columns is-multiline">
{% for t in translations %}
<div class="card mb-4" lang="{{t.language.code}}">
<a href="{{t.url}}" hreflang="{{t.language.code}}" class="card mb-4" style="color: inherit; text-decoration: none;">
<div class="column is-half">
<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-header">
<div class="card-header-title">{{t.title}} </div>
<div class="card-header-icon">{{t.language.flag}}</div>
</div>
<div class="card-content">
<p class="title">{{t.language.flag}} {{t.title}} </p>
<p class="content">
{{t.preview}}
<br />
<time datetime="{{iso_date}}">{{date}}</time>
</p>
</div>
</a>
<hr />
<p class="content">
{{t.preview}}
</p>
</div>
<div class="card-footer">
<time datetime="{{iso_date}}">{{date}}</time>
</div>
</a>
</div>
</div>
{% endfor %}
</div>
@@ -49,7 +58,7 @@
{% if children|length %}
<div class="container content">
<div class="column is-half is-offset-one-quarter">
<h1>Child Articles</h1>
<h1>Further reading</h1>
</div>
<div class="column is-half is-offset-one-quarter">
{% for c in children %}
@@ -57,9 +66,14 @@
<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="content">
<time datetime="{{iso_date}}">{{date}}</time>
<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>
{% endfor %}
</p>
<hr />
<time datetime="{{iso_date}}">{{date}}</time>
</div>
</a>
</div>

View File

@@ -3,6 +3,7 @@
<head>
<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>{{language.flag}} {{title}}</title>
<link rel="stylesheet" href="/static/bulma.min.css" />
</head>

View File

@@ -3,7 +3,8 @@ source_directory = "src"
dist_directory = "dist"
[formatting]
article_preview_length = 400
preview_length = 400
preview_header_shift = 2
datetime_format = "%d. %B %Y"
default_language = "de"

View File

@@ -4,9 +4,10 @@ class config:
dist_directory = "dist"
class formatting:
article_preview_length = 200
datetime_format = "%d. %B %Y"
fallback_language = "en"
preview_length = 400
preview_header_shift = 2
languages = {
"af": {

View File

@@ -4,7 +4,7 @@ import shutil
from pathlib import Path
import os
import markdown
from typing import Optional, Union, Dict, Generator, List, DefaultDict, Any
from typing import Optional, Union, Dict, Generator, List, DefaultDict, Any, TypedDict, Set
from bs4 import BeautifulSoup
from collections import defaultdict
import toml
@@ -13,14 +13,6 @@ import jinja2
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):
@@ -31,7 +23,7 @@ def get_first_header_content(content, fallback: str = ""):
return fallback
def shorten_text_and_clean(html_string, max_length=config.formatting.article_preview_length):
def shorten_text_and_clean(html_string, max_length=config.formatting.preview_length):
soup = BeautifulSoup(html_string, 'html.parser')
# Keep track of total characters added
@@ -69,6 +61,23 @@ def shorten_text_and_clean(html_string, max_length=config.formatting.article_pre
return str(soup)
def shift_headings(html_string, header_shift=config.formatting.preview_header_shift):
soup = BeautifulSoup(html_string, 'html.parser')
for level in range(6, 0, -1): # Start from h6 to h1 to avoid overwriting
old_tag = f'h{level}'
for tag in soup.find_all(old_tag):
new_level = min(level + header_shift, 6) # Cap at h6
new_tag = f'h{new_level}'
tag.name = new_tag
return str(soup)
def get_preview_text(html_string: str):
return shift_headings(shorten_text_and_clean(html_string))
def stem_to_language_code(stem: str) -> str:
language_code = stem.lower().replace("-", "_")
@@ -83,7 +92,6 @@ def stem_to_language_code(stem: str) -> str:
exit(1)
class TemplateDict(dict):
def __init__(self, folder: Path):
self.folder = folder
@@ -99,8 +107,10 @@ class TemplateDict(dict):
self[name] = t
return t
TEMPLATE: Dict[str, jinja2.Template] = TemplateDict(Path(config.setup.source_directory, "templates"))
class LanguageDict(dict):
def __missing__(self, key: str):
if key not in config.languages:
@@ -116,9 +126,17 @@ class LanguageDict(dict):
return lang_dict
LANGUAGES = LanguageDict()
def compile_cross_article_context(cross_article_context):
title = cross_article_context["title"]
url = cross_article_context["url"]
cross_article_context["link"] = f'<a href="{url}">{title}</a>'
class ArticleTranslation:
def __init__(self, file: Path, article: Article):
self.file = file
@@ -131,32 +149,44 @@ class ArticleTranslation:
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.cross_article_context = TRANSLATED_CROSS_ARTICLE_CONTEXT[self.language_code][self.article.slug] = {}
self.priority = LANGUAGES[self.language_code]["priority"]
self.real_language_code = LANGUAGES[self.language_code]["code"]
# TODO remove
self.article_content = self.file.read_text()
self.article_preview = self.article_content[:config.formatting.article_preview_length] + "..."
self.html_content = self.file.read_text()
if self.file.suffix == ".md":
self.article_content = markdown.markdown(self.article_content)
self.article_preview = markdown.markdown(self.article_preview)
self.html_content = markdown.markdown(self.html_content)
self.title = get_first_header_content(self.article_content, fallback="")
def __init_context__(self):
self.context["meta"] = self.article.context_shared
self.context["url"] = self.url
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"])
html_content = self.file.read_text()
if self.file.suffix == ".md":
html_content = markdown.markdown(html_content)
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)
self.context["title"] = get_first_header_content(html_content, fallback=LANGUAGES[self.language_code]["native_name"])
self.context["content"] = html_content
self.context["preview"] = shorten_text_and_clean(html_string=html_content)
# 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
]
def __init_content_context__(self):
template = jinja2.Template(self.html_content)
self.html_content = template.render({
**CROSS_ARTICLE_CONTEXT,
**TRANSLATED_CROSS_ARTICLE_CONTEXT[self.language_code],
})
self.context["content"] = self.html_content
self.context["preview"] = get_preview_text(html_string=self.html_content)
def build(self):
self.dist_path.mkdir(parents=True, exist_ok=True)
@@ -183,6 +213,7 @@ class Article:
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)
self.cross_article_context = CROSS_ARTICLE_CONTEXT[self.slug] = {}
ARTICLE_LAKE[self.slug] = self
self.location_in_tree: List[str] = location_in_tree or []
@@ -225,6 +256,11 @@ class Article:
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
translation_list = self.context["translations"] = []
child_article_list = self.context["children"] = []
@@ -242,9 +278,15 @@ class Article:
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__()
for a in self.child_articles:
a.__init_content_context__()
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(TEMPLATE["article"].render(self.context))
@@ -257,24 +299,32 @@ class Article:
# GLOBALS
logger = logging.getLogger("stsg.build")
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)
def build():
logger.info("starting build process...")
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("building page tree...")
tree = Article(directory=Path(config.setup.source_directory, "articles"), is_root=True)
logger.info("compiling tree context...")
tree.__init_context__()
tree.__init_content_context__()
import json
with Path("context.json").open("w") as f:
json.dump(tree.context, f, indent=4)
with Path("cross_article_context.json").open("w") as f:
json.dump(CROSS_ARTICLE_CONTEXT, f, indent=4)
with Path("t_cross_article_context.json").open("w") as f:
json.dump(TRANSLATED_CROSS_ARTICLE_CONTEXT, f, indent=4)
logger.info("dumping page tree...")
tree.build()
tree.build()