28 Commits

Author SHA1 Message Date
Hazel Noack
e1fd352b1f dfasf 2025-05-28 16:14:31 +02:00
Hazel Noack
a4aa73b1aa added todo 2025-05-22 16:46:49 +02:00
Hazel Noack
73e89ca513 properly added license everywhere 2025-05-22 16:41:05 +02:00
Hazel Noack
a1900a77e4 added year to context 2025-05-22 16:33:25 +02:00
Hazel Noack
258d062ff4 credited the license author 2025-05-22 16:19:22 +02:00
Hazel Noack
8fee8d879e credited the license author 2025-05-22 16:18:32 +02:00
Hazel Noack
373cade0a7 added license to website 2025-05-22 16:14:36 +02:00
Hazel Noack
5196913266 added license 2025-05-22 16:11:29 +02:00
Hazel Noack
d3ff002901 added breadcrumbs 2025-05-22 16:08:36 +02:00
Hazel Noack
08e7e343f5 removed header from translated cards as it is in the preview 2025-05-22 15:52:16 +02:00
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
Hazel Noack
632f47e017 refactored article translations with cached properties 2025-05-22 12:24:38 +02:00
Hazel Noack
2a7ebaa298 some refactoring 2025-05-22 12:06:53 +02:00
Hazel Noack
d43d6505b1 defined name 2025-05-22 12:02:42 +02:00
Hazel Noack
e7558d996b refactored some attributes as cached properties 2025-05-22 11:56:06 +02:00
Hazel Noack
15707ada59 feat: english documentation up to date 2025-05-22 11:18:09 +02:00
Hazel Noack
54d1f0292e feat: added related articles 2025-05-19 15:39:44 +02:00
Hazel Noack
c33307e714 feat: added fenced code blocks 2025-05-19 15:16:19 +02:00
Hazel Noack
e3fe49bed7 feat: added linked article to context 2025-05-19 15:05:37 +02:00
Hazel Noack
693022ac23 info on articles 2025-05-16 17:48:39 +02:00
Hazel Noack
9630eb07a1 added some more information 2025-05-16 17:33:11 +02:00
Hazel Noack
a01f94379d added installation 2025-05-16 17:21:19 +02:00
Hazel Noack
6093e8df32 feat: documented a bit better 2025-05-16 17:06:31 +02:00
Hazel Noack
e82d841588 feat: added documentation setup 2025-05-16 17:02:39 +02:00
22 changed files with 674 additions and 144 deletions

57
LICENSE Normal file
View File

@@ -0,0 +1,57 @@
# 🏳️‍🌈 Opinionated Queer License v1.2
© Copyright {Licensor}
## Permissions
The creators of this Work (“The Licensor”) grant permission
to any person, group or legal entity that doesn't violate the prohibitions below (“The User”),
to do everything with this Work that would otherwise infringe their copyright or any patent claims,
subject to the following conditions:
## Obligations
The User must give appropriate credit to the Licensor,
provide a copy of this license or a (clickable, if the medium allows) link to
[oql.avris.it/license/v1.2](https://oql.avris.it/license/v1.2),
and indicate whether and what kind of changes were made.
The User may do so in any reasonable manner,
but not in any way that suggests the Licensor endorses the User or their use.
## Prohibitions
No one may use this Work for prejudiced or bigoted purposes, including but not limited to:
racism, xenophobia, queerphobia, queer exclusionism, homophobia, transphobia, enbyphobia, misogyny.
No one may use this Work to inflict or facilitate violence or abuse of human rights,
as defined in either of the following documents:
[Universal Declaration of Human Rights](https://www.un.org/en/about-us/universal-declaration-of-human-rights),
[European Convention on Human Rights](https://prd-echr.coe.int/web/echr/european-convention-on-human-rights)
along with the rulings of the [European Court of Human Rights](https://www.echr.coe.int/).
No law enforcement, carceral institutions, immigration enforcement entities, military entities or military contractors
may use the Work for any reason. This also applies to any individuals employed by those entities.
No business entity where the ratio of pay (salaried, freelance, stocks, or other benefits)
between the highest and lowest individual in the entity is greater than 50 : 1
may use the Work for any reason.
No private business run for profit with more than a thousand employees
may use the Work for any reason.
Unless the User has made substantial changes to the Work,
or uses it only as a part of a new work (eg. as a library, as a part of an anthology, etc.),
they are prohibited from selling the Work.
That prohibition includes processing the Work with machine learning models.
## Sanctions
If the Licensor notifies the User that they have not complied with the rules of the license,
they can keep their license by complying within 30 days after the notice.
If they do not do so, their license ends immediately.
## Warranty
This Work is provided “as is”, without warranty of any kind, express or implied.
The Licensor will not be liable to anyone for any damages related to the Work or this license,
under any kind of legal claim as far as the law allows.

View File

@@ -1,4 +1,15 @@
# Installing
<a href="https://oql.avris.it/license/v1.2" target="_blank" rel="noopener"><img src="https://badgers.space/badge/License/OQL/pink" alt="License: OQL" style="vertical-align: middle;"/></a>
# STSG
## Planned features
- [ ] auto uploading to ftp and other online services using a plugin system
- [ ] build a git wiki in a specified languages instead of a static website
- [ ] hosting the documentation somehow
- [ ] multiple templates + a way to choose templates
## Installing
```sh
python3 -m venv .venv
@@ -6,7 +17,7 @@ source .venv/bin/activate
pip install -e .
```
# Execute
## Execute
To start a local http server in the dist folder you can simply do:
@@ -24,13 +35,3 @@ stsg
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,17 +2,20 @@
name = "stsg"
dependencies = [
"watchdog~=6.0.0",
"markdown~=3.3.6",
"markdown2~=2.5.3",
"bs4~=0.0.2",
"toml~0.10.2",
"toml~=0.10.2",
"jinja2~=3.1.6",
]
dynamic = []
authors = []
readme = "README.md"
requires-python = ">=3.10"
requires-python = ">=3.8"
classifiers = []
version = "0.0.0"
license-files = [
"LICENSE"
]
[project.scripts]
stsg = "stsg.__main__:build"

View File

@@ -6,4 +6,4 @@ Dies ist ein Static Site Generator mit fokus auf Nutzer:innenfreundlichkeit und
Die Templates sind komplet anpassbar, trotzdem empfehle ich [Bulma](https://bulma.io/) als CSS-Framework zu verwenden.
Polizei und das Millitär jeglichen Staates dürfen dieses Tool nicht verwenden.
Polizei und das Millitär jeglichen Staates dürfen dieses Tool nicht verwenden. Für mehr Info zur Nutzung lest [die Lizenz]({{license.url}}).

View File

@@ -1,3 +1,16 @@
# Dokumentation
Dies ist die Dokumentation von STSG.
## Favicon bauen
Dies ist einfach nur ein hilfreiches Skript um favicons zu erstellen. Dies ist nicht unbedingt notwendig. Aber bei verwendung muss inkscape und imagemagick installiert sein.
```
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

@@ -1,3 +1,16 @@
# documentation
This is the documentation for stsg.
## build favicon
This is just a helpfull script to build the favicon. It is not strictly required to use. Make sure inkscape and imagemagick is installed.
```
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

@@ -0,0 +1,26 @@
# Get Started
Hier wird gezeigt wie man seine eigene website erstellen kann.
1. Fork das Projekt und Klone es lokal
2. {{installation.link}}
## Dateistruktur
Die Dateien die den Content definieren findet man in `src`. Dort gibt es 3 Unterordner. In `templates` wird der Style und die Funktionalität der Webseite definiert. Wenn du nicht programmieren kannst, lass den in Ruhe. In `static` befinden sich Statische Dateien wie das css oder Bilder. Dieser Ordner wird bei dem Build einfach kopiert. Bei `articles` wird es interesannt. Dort befinden sich wie der Name schon sagt die Artikle.
Wenn irgendetwas unklar ist, dann kann der code dieser Dokumentation (ist in eurer Fork) auch hilfreich sein.
## Artikel schreiben
Der Text für die Artikel ist immer in einer Datei mit folgendem namen `<language_code>.md`. In diesem Fall wäre dies `de.md`. Dann kann der Artikel ganz normal mit dem [Markdown Syntax](https://www.markdownguide.org/) geschrieben werden.
Soll der Artikel untergeordnete Artikel haben, so kann man dafür einfach ein neuen Ordner in dem jeweiligen Artikel erstellen. Der Name ist dann auch der Name der z.B. in der Url zu sehen ist.
Will man Metadaten für den Artikel definieren (z.B. Name, Erstellungsdatum, Autor:in), dann kann man dies in der `index.toml` machen. Wenn noch keine existiert kann sie einfach erstellt werden. Hier ist ein Beispiel einer solchen Datei.
```toml
name="stsg"
datetime="2024-04-15 13:45:12.123456"
author="Hazel"
```

View File

@@ -0,0 +1,26 @@
# get started
Here you will learn how to get started making your own website.
1. fork the project and clone the fork
2. {{installation.link}}
## File structure
The files that define the content can be found in `src`. There are 3 subfolders. The style and functionality is defined in `templates`. If you can't code, or you don't know what your doing, leave it alone. The static files like stylesheets or pictures exist in `static`. This folder is simply copied on build. The interesting part starts with `articles`. Here are (like the name implies) all articles.
If something remains unclear, then the code of this documentation (should be found in your fork) could be helpful.
## Write articles
The text for the articles follows the following naming scheme `<language_code>.md`. In this case it would be `en.md` then you can write the article normally using [markdown](https://www.markdownguide.org/) for formatting.
If there should be subarticles, just create a new folder in the parent article folder. The folder name will be used as slug. That means it will appear in the url, and you can use it to link to other articles.
If you want to define the metadata for the article (the name, creation date, or author), then you can do so in `index.toml`. If none exists you can just create one. Here an example of such a file.
```toml
name="stsg"
datetime="2024-04-15 13:45:12.123456"
author="Hazel"
```

View File

@@ -0,0 +1,19 @@
# Installierung des Programms
```sh
python3 -m venv .venv
source .venv/bin/activate
pip install -e .
```
## Programm ausführen
Um einen lokalen http server zu starten kann folgender Befehl ausgeführt werden:
```
python3 -m http.server 1312
```
Dann ist die Seite auf `localhost:1312` zu finden
Man kann die Seite entweder normal bauen mit `stsg` oder mit `stsg_dev` einen hot reload server starten.

View File

@@ -0,0 +1,19 @@
# setup the programm
```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:
```
python3 -m http.server 1312
```
Then visit `localhost:1312`
You can either build it normally with `stsg` or start a hot reload server with `stsg_dev`.

View File

@@ -6,4 +6,4 @@ This is a static-site-generator with focus on ease of use and making accessible
The templates are completely customizable, but I still reccomend using [bulma](https://bulma.io/) as css-framework.
Cops and Millitary of any state are stricly prohibited from using this tool.
Cops and Millitary of any state are stricly prohibited from using this tool. For more information about the usage of that tool, read [our license]({{license.url}}).

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

@@ -0,0 +1,57 @@
# 🏳️‍🌈 Opinionated Queer License v1.2
© Copyright {Licensor}
## Permissions
The creators of this Work (“The Licensor”) grant permission
to any person, group or legal entity that doesn't violate the prohibitions below (“The User”),
to do everything with this Work that would otherwise infringe their copyright or any patent claims,
subject to the following conditions:
## Obligations
The User must give appropriate credit to the Licensor,
provide a copy of this license or a (clickable, if the medium allows) link to
[oql.avris.it/license/v1.2](https://oql.avris.it/license/v1.2),
and indicate whether and what kind of changes were made.
The User may do so in any reasonable manner,
but not in any way that suggests the Licensor endorses the User or their use.
## Prohibitions
No one may use this Work for prejudiced or bigoted purposes, including but not limited to:
racism, xenophobia, queerphobia, queer exclusionism, homophobia, transphobia, enbyphobia, misogyny.
No one may use this Work to inflict or facilitate violence or abuse of human rights,
as defined in either of the following documents:
[Universal Declaration of Human Rights](https://www.un.org/en/about-us/universal-declaration-of-human-rights),
[European Convention on Human Rights](https://prd-echr.coe.int/web/echr/european-convention-on-human-rights)
along with the rulings of the [European Court of Human Rights](https://www.echr.coe.int/).
No law enforcement, carceral institutions, immigration enforcement entities, military entities or military contractors
may use the Work for any reason. This also applies to any individuals employed by those entities.
No business entity where the ratio of pay (salaried, freelance, stocks, or other benefits)
between the highest and lowest individual in the entity is greater than 50 : 1
may use the Work for any reason.
No private business run for profit with more than a thousand employees
may use the Work for any reason.
Unless the User has made substantial changes to the Work,
or uses it only as a part of a new work (eg. as a library, as a part of an anthology, etc.),
they are prohibited from selling the Work.
That prohibition includes processing the Work with machine learning models.
## Sanctions
If the Licensor notifies the User that they have not complied with the rules of the license,
they can keep their license by complying within 30 days after the notice.
If they do not do so, their license ends immediately.
## Warranty
This Work is provided “as is”, without warranty of any kind, express or implied.
The Licensor will not be liable to anyone for any damages related to the Work or this license,
under any kind of legal claim as far as the law allows.

View File

@@ -0,0 +1,57 @@
# 🏳️‍🌈 Opinionated Queer License v1.2
© Copyright {Licensor}
## Permissions
The creators of this Work (“The Licensor”) grant permission
to any person, group or legal entity that doesn't violate the prohibitions below (“The User”),
to do everything with this Work that would otherwise infringe their copyright or any patent claims,
subject to the following conditions:
## Obligations
The User must give appropriate credit to the Licensor,
provide a copy of this license or a (clickable, if the medium allows) link to
[oql.avris.it/license/v1.2](https://oql.avris.it/license/v1.2),
and indicate whether and what kind of changes were made.
The User may do so in any reasonable manner,
but not in any way that suggests the Licensor endorses the User or their use.
## Prohibitions
No one may use this Work for prejudiced or bigoted purposes, including but not limited to:
racism, xenophobia, queerphobia, queer exclusionism, homophobia, transphobia, enbyphobia, misogyny.
No one may use this Work to inflict or facilitate violence or abuse of human rights,
as defined in either of the following documents:
[Universal Declaration of Human Rights](https://www.un.org/en/about-us/universal-declaration-of-human-rights),
[European Convention on Human Rights](https://prd-echr.coe.int/web/echr/european-convention-on-human-rights)
along with the rulings of the [European Court of Human Rights](https://www.echr.coe.int/).
No law enforcement, carceral institutions, immigration enforcement entities, military entities or military contractors
may use the Work for any reason. This also applies to any individuals employed by those entities.
No business entity where the ratio of pay (salaried, freelance, stocks, or other benefits)
between the highest and lowest individual in the entity is greater than 50 : 1
may use the Work for any reason.
No private business run for profit with more than a thousand employees
may use the Work for any reason.
Unless the User has made substantial changes to the Work,
or uses it only as a part of a new work (eg. as a library, as a part of an anthology, etc.),
they are prohibited from selling the Work.
That prohibition includes processing the Work with machine learning models.
## Sanctions
If the Licensor notifies the User that they have not complied with the rules of the license,
they can keep their license by complying within 30 days after the notice.
If they do not do so, their license ends immediately.
## Warranty
This Work is provided “as is”, without warranty of any kind, express or implied.
The Licensor will not be liable to anyone for any damages related to the Work or this license,
under any kind of legal claim as far as the law allows.

View File

@@ -0,0 +1 @@
author="Andrea Vos"

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,11 +16,24 @@
>
<div class="navbar-brand">
<a class="navbar-item" href="#">
<strong>Static Translated Site Generator</strong>
<strong>{{name}}</strong>
</a>
</div>
</nav>
{% if breadcrumbs|length %}
<!-- Breadcrumbs Section -->
<nav class="breadcrumb is-small has-succeeds-separator p-3" aria-label="breadcrumbs">
<ul>
{% for b in breadcrumbs %}
<li class="{{'is-active' if b.slug == slug}}">
<a {{'aria-current="page"' if b.slug == slug}} href="{{b.url}}">{{b.name}}</a>
</li>
{% endfor %}
</ul>
</nav>
{% endif %}
<section class="section">
{% if translations|length %}
<div class="container content">
@@ -33,7 +46,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 +54,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 +75,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 +96,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; {{year}}</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,11 +18,24 @@
<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>
{% if breadcrumbs|length %}
<!-- Breadcrumbs Section -->
<nav class="breadcrumb is-small has-succeeds-separator p-3" aria-label="breadcrumbs">
<ul>
{% for b in breadcrumbs %}
<li class="{{'is-active' if b.slug == slug}}">
<a {{'aria-current="page"' if b.slug == slug}} href="{{b.url}}">{{b.name}}</a>
</li>
{% endfor %}
</ul>
</nav>
{% endif %}
<!-- Main Content -->
<section class="section">
<div class="content">
@@ -30,24 +43,22 @@
</div>
</section>
{% if children|length %}
{% if related|length %}
<section class="section">
<div class="content">
<div class="columns is-multiline">
{% for c in children %}
{% for c in related %}
<div class="column is-half">
<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>
<hr />
<p class="content">
{{c.preview}}
</p>
</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 +72,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; {{year}}</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"
@@ -8,6 +11,9 @@ class config:
fallback_language = "en"
preview_length = 400
preview_header_shift = 2
markdown_extras = [
"fenced-code-blocks"
]
languages = {
"af": {

View File

@@ -18,7 +18,12 @@ logger = logging.getLogger("stsg")
def build():
complete_build()
build_wiki()
# complete_build()
def build_wiki():
pass
class MarkdownChangeHandler(FileSystemEventHandler):

View File

@@ -3,25 +3,19 @@ import logging
import shutil
from pathlib import Path
import os
import markdown
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')
@@ -76,20 +70,7 @@ def shift_headings(html_string, header_shift=config.formatting.preview_header_sh
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("-", "_")
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 TemplateDict(dict):
@@ -130,102 +111,240 @@ 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>'
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:
def __init__(self, file: Path, article: Article):
self.file = file
self.article = article
article: Article
slug: str = property(fget=lambda self: self.article.slug)
file: Path
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.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"]
self.html_content = self.file.read_text()
@cached_property
def html_content(self) -> str:
html_content = self.file.read_text()
if self.file.suffix == ".md":
self.html_content = markdown.markdown(self.html_content)
return markdown(html_content, extras=config.formatting.markdown_extras)
return html_content
@cached_property
def language_code(self) -> str:
language_code = self.file.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)
@cached_property
def priority(self) -> int:
return LANGUAGES[self.language_code]["priority"]
@cached_property
def slug_path(self) -> List[str]:
return [self.language_code, *self.article.slug_path]
@cached_property
def url(self) -> str:
return "/" + "/".join(self.slug_path)
@cached_property
def dist_path(self) -> Path:
return Path(config.setup.dist_directory, *self.slug_path)
context: ArticleTranslationContext
cross_article_context: Dict[str, Any]
def __init__(self, file: Path, article: Article):
self.article = article
self.file = file
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["year"] = str(self.article.modified_at.year)
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.context["children"] = self.article.child_articles.get_translated(self.language_code).context
self.context["breadcrumbs"] = ArticleList(self.article.article_path).get_translated(self.language_code).context
def __init_content_context__(self):
template = jinja2.Template(self.html_content)
template.environment.accessed_keys = []
template.environment.context_class = ContextDict
self.html_content = template.render({
**CROSS_ARTICLE_CONTEXT,
**TRANSLATED_CROSS_ARTICLE_CONTEXT[self.language_code],
})
template.environment.context_class = jinja2.runtime.Context
accessed_keys = template.environment.accessed_keys
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)
with Path(self.dist_path, "index.html").open("w") as f:
f.write(TEMPLATE["article_translation"].render(self.context))
def build_wiki(self, language_code: str):
pass
class Article:
def __init__(self, directory: Path, location_in_tree: Optional[List[str]] = None, is_root: bool = False, parent: Optional[Article] = None):
directory: Path
@cached_property
def config(self) -> ArticleConfig:
config_file = self.directory / "index.toml"
return toml.load(config_file) if config_file.exists() else {}
@cached_property
def slug(self) -> str:
slug = self.config.get("name", self.directory.name)
if slug in ARTICLE_LAKE:
logger.error("two articles have the same name at %s and %r", ARTICLE_LAKE[slug].directory, self.directory)
exit(1)
return slug
@cached_property
def name(self) -> str:
return self.config.get("name", self.slug)
article_path: List[Article]
@cached_property
def slug_path(self) -> List[str]:
return [a.slug for a in self.article_path[1:]]
@cached_property
def url(self) -> str:
return "/" + "/".join(self.slug_path)
@cached_property
def dist_path(self) -> Path:
return Path(config.setup.dist_directory, *self.slug_path)
context: ArticleContext
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
self.context: Dict[str, Any] = {}
self.context_shared: Dict[str, Any] = {}
if parent is not None:
self.context["parent"] = parent.context_shared
self.article_path: List[Article] = article_path or []
self.article_path.append(self)
# 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)
self.cross_article_context = CROSS_ARTICLE_CONTEXT[self.slug] = {}
self.context = CROSS_ARTICLE_CONTEXT[self.slug] = {}
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)
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":
@@ -238,49 +357,60 @@ class Article:
elif c.is_dir():
self.child_articles.append(Article(
directory=c,
location_in_tree=self.location_in_tree.copy(),
article_path=self.article_path.copy(),
parent=self,
))
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()))
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["year"] = str(self.modified_at.year)
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
self.context["breadcrumbs"] = [b.context for b in self.article_path]
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.real_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__()
@@ -297,11 +427,18 @@ class Article:
ac.build()
class ContextDict(jinja2.runtime.Context):
def resolve_or_missing(self, key: str) -> Any:
self.environment.accessed_keys.append(key)
return super().resolve_or_missing(key)
# 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)
@@ -318,6 +455,7 @@ def build():
tree.__init_context__()
tree.__init_content_context__()
"""
import json
with Path("context.json").open("w") as f:
json.dump(tree.context, f, indent=4)
@@ -325,6 +463,7 @@ def build():
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()

62
stsg/definitions.py Normal file
View File

@@ -0,0 +1,62 @@
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
year: str
iso_date: str
author: str
translations: List[ArticleTranslationContext]
children: List[ArticleContext]
breadcrumbs: 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
year: 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]]
breadcrumbs: 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]]