118 Commits

Author SHA1 Message Date
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
Hazel Noack
2ded0c7768 feat: setup documentation wegsite 2025-05-16 16:58:25 +02:00
Hazel Noack
be51f463a1 feat: setup documentation wegsite 2025-05-16 16:58:20 +02:00
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
9b030b24f4 feat: added parent context 2025-04-16 16:42:21 +02:00
44b651cace feat: move meta values to article 2025-04-16 16:39:06 +02:00
eb2edc3710 feat: removed card templates 2025-04-16 16:17:57 +02:00
f63497090b Revert "feat: removed redundand templates"
This reverts commit 215142dee4.
2025-04-16 16:17:13 +02:00
215142dee4 feat: removed redundand templates 2025-04-16 16:09:18 +02:00
e391f64cc5 feat: removed redundand methods 2025-04-16 16:08:25 +02:00
4216d153fd feat: removed redundand methods 2025-04-16 16:08:11 +02:00
9241ecfee8 feat: added dynamic children cards in templates 2025-04-16 16:07:20 +02:00
4bc7a6d980 feat: added article translation templating 2025-04-16 16:01:38 +02:00
b50833f19f feat: dynamic translation cards 2025-04-16 15:57:17 +02:00
f5cccc30dc feat: dynamic translation cards 2025-04-16 15:56:34 +02:00
8a8f02c5bd feat: implemented code to use with jinja 2025-04-16 15:45:06 +02:00
292d71edc5 feat: added proper content 2025-04-16 15:26:43 +02:00
c9fb8fda93 feat: building recursive context structures 2025-04-16 14:26:50 +02:00
1fae03e70b feat: properly recursively building context 2025-04-16 14:20:51 +02:00
6ed94db8cf feat: added translations 2025-04-16 14:17:45 +02:00
02a7c29dba feat: add flat context to article 2025-04-16 14:05:16 +02:00
cf8e2955c2 feat: initializing location 2025-04-16 13:23:30 +02:00
7aa06b7f83 feat: added meta context 2025-04-16 13:15:27 +02:00
f7a690405b feat: add language context 2025-04-16 13:09:46 +02:00
93ea11cd0e feat: removed weird article cards 2025-04-16 12:30:54 +02:00
263281df3c feat: renamed ArticleOverview to article 2025-04-16 12:18:44 +02:00
amnesia
b953933e6f added jinja to deps 2025-04-15 23:41:03 +02:00
7db84492b5 feat: hastly implementation of article cards 2025-04-15 17:48:21 +02:00
c7cd5e0601 feat: renamed overview_card to translation_card 2025-04-15 17:31:06 +02:00
c05f8fad35 feat: renamed overview_card to translation_card 2025-04-15 17:30:57 +02:00
b9c7535812 feat: added article card template 2025-04-15 17:29:25 +02:00
cdcd3bce1f fix: temlate typo 2025-04-15 17:28:12 +02:00
6eb02cc95f feat: added possibility for child pages cards 2025-04-15 17:26:09 +02:00
2454d26d6b feat: better timestamps 2025-04-15 17:15:55 +02:00
8054a4a983 fix: preview html fuckup 2025-04-15 17:13:04 +02:00
afefce0a11 feat: added datetime 2025-04-15 17:08:37 +02:00
334c82d098 feat: added date to terminal 2025-04-15 17:04:57 +02:00
ddd536c958 feat: added date to values 2025-04-15 16:54:01 +02:00
ae5ba0d044 feat: writing article config values to class 2025-04-15 16:48:00 +02:00
83133218a4 feat: writing article config values to class 2025-04-15 16:47:15 +02:00
d412d983bd feat: cleaned the logs and copy static 2025-04-15 16:34:43 +02:00
9859229101 feat: added nice config functionality 2025-04-15 16:21:05 +02:00
dc8f9d91e7 feat: added toml files 2025-04-15 15:26:48 +02:00
f3a86b8070 feat: added toml dependency 2025-04-15 15:20:39 +02:00
b814434c48 feat: added toml dependency 2025-04-15 15:13:08 +02:00
ba05ecdd94 feat: renamed pages 2025-04-15 13:12:36 +02:00
1751158cbd feat: added logger 2025-04-15 13:11:59 +02:00
c45cebe497 feat: properly building additional keywords 2025-04-15 13:00:34 +02:00
dc61a7ee92 fix: location position copy instead of reference 2025-04-15 12:47:56 +02:00
d70a0a8630 feat: slight rewrite 2025-04-15 12:43:09 +02:00
4f37283e68 fix: stupid ahh typo bug 2025-04-15 12:21:49 +02:00
0e948e7f55 feat: small refactoring for building of urls 2025-04-15 12:19:45 +02:00
79c95a9ddb feat: small refactoring for building of urls 2025-04-15 12:16:37 +02:00
0ca0fb90d6 feat: added type hints 2025-04-15 12:10:21 +02:00
603a7f5942 feat: filling an article lake 2025-04-15 12:09:28 +02:00
63f8541a82 feat: refactor type dict 2025-04-15 12:01:10 +02:00
dc5af8da28 feat: refactor type dict 2025-04-15 11:59:04 +02:00
3fdbf13d95 Merge branch 'main' of ssh://gitea.elara.ws:2222/Hazel/STSG 2025-04-15 11:55:57 +02:00
7e4640e7d9 feat: added inline type dict 2025-04-15 11:55:43 +02:00
amnesia
8239b3ea6d added src 2025-04-15 11:50:56 +02:00
amnesia
3f0049dfdb feat: recursive pages 2025-04-14 21:06:52 +02:00
amnesia
a113a13318 fix: added bs4 to dep 2025-04-14 19:29:39 +02:00
3926277f8f feat: move relevant card to the top 2025-04-14 17:35:59 +02:00
69b3efd78f feat: move relevant card to the top 2025-04-14 17:35:11 +02:00
2b80c90a19 feat: added lang attribute to reduce barriers 2025-04-14 16:59:18 +02:00
37d28a3542 feat: added priority to languages 2025-04-14 16:46:12 +02:00
3e692dd74f feat: added priority to languages 2025-04-14 16:45:16 +02:00
3e6884ad8a feat: added priority to config 2025-04-14 16:11:12 +02:00
8e57af5a78 feat: added article title 2025-04-14 16:07:25 +02:00
37974c418b feat: added article home overview 2025-04-14 16:04:40 +02:00
2dc1dfef98 feat: added article home overview 2025-04-14 15:58:56 +02:00
c36deab71a feat: added kurdish placeholder 2025-04-14 15:50:16 +02:00
432f16ba08 feat: added a nice overview card site 2025-04-14 15:45:06 +02:00
ee26af8b35 feat: refactored the overview 2025-04-14 15:12:56 +02:00
amnesia
523402d3ad feat: added additional data 2025-04-11 12:08:24 +02:00
d2d2628e56 feat: added language info 2025-04-10 17:49:59 +02:00
7a3aec9872 feat: added a go back 2025-04-10 17:06:45 +02:00
bfa9cb1527 feat: added a go back 2025-04-10 17:05:10 +02:00
4b307d7896 feat: overview 2025-04-10 17:03:00 +02:00
b1a4d8e6ae feat: added example texts 2025-04-10 16:48:12 +02:00
fa512cfb0e feat: added example texts 2025-04-10 16:47:47 +02:00
35 changed files with 1987 additions and 145 deletions

3
.gitignore vendored
View File

@@ -173,4 +173,5 @@ cython_debug/
# PyPI configuration file
.pypirc
dist
dist
*.json

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

@@ -0,0 +1,37 @@
<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
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
```

View File

@@ -2,14 +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",
"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"

9
src/articles/de.md Normal file
View File

@@ -0,0 +1,9 @@
# STSG
![stsg logo](/static/assets/logo.png)
Dies ist ein Static Site Generator mit fokus auf Nutzer:innenfreundlichkeit und Barrierearmut der generierten Seite. So kann man die Artikel in alle relevanten Sprachen übersetzen. Diese Dokumentation ist ebenfalls mit STSG gemacht.
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. Für mehr Info zur Nutzung lest [die Lizenz]({{license.url}}).

View File

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

@@ -0,0 +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`.

9
src/articles/en.md Normal file
View File

@@ -0,0 +1,9 @@
# STSG
![](/static/assets/logo.png)
This is a static-site-generator with focus on ease of use and making accessible websites. Thus the articles can be created in all relevant languages.
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. For more information about the usage of that tool, read [our license]({{license.url}}).

2
src/articles/index.toml Normal file
View File

@@ -0,0 +1,2 @@
name="stsg"
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

@@ -1,6 +0,0 @@
# foo
Lore ipsum dolor sit amend
- bar
- foobar

View File

@@ -1 +0,0 @@

View File

@@ -1,37 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>STSG</title>
<link rel="stylesheet" href="/static/bulma.min.css" />
</head>
<body>
<!-- Header (Navbar) -->
<nav
class="navbar is-primary"
role="navigation"
aria-label="main navigation"
>
<div class="navbar-brand">
<a class="navbar-item" href="#">
<strong>Static Translated Site Generator</strong>
</a>
</div>
</nav>
<!-- Main Content -->
<section class="section">
<div class="content">
<content />
</div>
</section>
<!-- Footer -->
<footer class="footer">
<div class="content has-text-centered">
<p><strong>STSG</strong> by Hazel. &copy; 2025</p>
</div>
</footer>
</body>
</html>

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

13
src/static/style.css Normal file
View File

@@ -0,0 +1,13 @@
img {
float: right;
margin: 1em;
max-width: 20%;
height: auto;
}
/* Responsive: remove float on small screens */
@media screen and (max-width: 768px) {
img {
max-width: 50%;
}
}

122
src/templates/article.html Normal file
View File

@@ -0,0 +1,122 @@
<!DOCTYPE html>
<html>
<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>{{name}}</title>
<link rel="stylesheet" href="/static/bulma.min.css" />
<link rel="stylesheet" href="/static/style.css" />
</head>
<body>
<nav
class="navbar is-primary"
role="navigation"
aria-label="main navigation"
>
<div class="navbar-brand">
<a class="navbar-item" href="#">
<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">
<div class="content">
<h1>Translations</h1>
</div>
<div class="columns is-multiline">
{% for t in translations %}
<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-content">
<p class="title">{{t.language.flag}} {{t.name}}</p>
<hr />
<p class="content">
{{t.preview}}
</p>
</div>
<div class="card-footer">
<time class="card-footer-item" datetime="{{t.iso_date}}">{{t.date}}</time>
</div>
</a>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if children|length %}
<div class="container content">
<div class="column is-half is-offset-one-quarter">
<h1>Related Articles</h1>
</div>
<div class="column is-half is-offset-one-quarter">
{% for c in children %}
<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.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.name}}</a>
{% endfor %}
</p>
<hr />
<time datetime="{{c.iso_date}}">{{c.date}}</time>
</div>
</a>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</section>
<!-- Footer -->
<footer class="footer">
<div class="content has-text-centered">
<p><strong>{{name}}</strong> by {{author}}. &copy; {{year}}</p>
</div>
</footer>
</body>
<script>
document.addEventListener("DOMContentLoaded", function () {
const userLang = navigator.language || navigator.userLanguage;
// Normalize and check if the language is not English or German
if (!["en", "de", "de-DE"].includes(userLang)) {
// Try to find a matching card by language attribute
const cardToMove =
document.querySelector(`.card[lang^="${userLang.replace("_", "-").toLowerCase()}"]`) ||
document.querySelector(`.card[lang^="${userLang.split("-")[0]}"]`);
if (cardToMove) {
const container = cardToMove.parentNode;
container.insertBefore(cardToMove, container.firstChild);
}
}
});
</script>
</html>

View File

@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="{article_language_code}">
<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>{{name}}</title>
<link rel="stylesheet" href="/static/bulma.min.css" />
<link rel="stylesheet" href="/static/style.css" />
</head>
<body>
<!-- Header (Navbar) -->
<nav
class="navbar is-primary"
role="navigation"
aria-label="main navigation"
>
<div class="navbar-brand">
<a class="navbar-item" href="{{article_url}}">
<strong>{{language.flag}} {{title}}</strong>
<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">
{{content}}
</div>
</section>
{% if related|length %}
<section class="section">
<div class="content">
<div class="columns is-multiline">
{% 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="content">
{{c.preview}}
</p>
</div>
<div class="card-footer">
<time class="card-footer-item" datetime="{{c.iso_date}}">{{c.date}}</time>
</div>
</a>
</div>
</div>
{% endfor %}
</div>
</div>
</section>
{% endif %}
<!-- Footer -->
<footer class="footer">
<div class="content has-text-centered">
<p><strong>{{name}}</strong> by {{author}}. &copy; {{year}}</p>
</div>
</footer>
</body>
</html>

28
stsg.toml Normal file
View File

@@ -0,0 +1,28 @@
fall_back_to_overview_in_translation = false
[setup]
source_directory = "src"
dist_directory = "dist"
[formatting]
preview_length = 400
preview_header_shift = 2
datetime_format = "%d. %B %Y"
default_language = "de"
[languages]
[languages.de]
native_name = "Schland"
priority = 100
[languages.en]
priority = 90
[languages.ar_sy]
priority = 50
[languages.ku]
priority = 49
[languages.tr]
priority = 60

View File

@@ -0,0 +1,800 @@
class config:
default_author = "anonymous"
fall_back_to_overview_in_translation = True
class setup:
source_directory = "src"
dist_directory = "dist"
class formatting:
datetime_format = "%d. %B %Y"
fallback_language = "en"
preview_length = 400
preview_header_shift = 2
markdown_extras = [
"fenced-code-blocks"
]
languages = {
"af": {
"flag": "🇿🇦",
"name": "Afrikaans",
"native_name": "Afrikaans"
},
"am": {
"flag": "🇪🇹",
"name": "Amharic",
"native_name": "አማርኛ"
},
"an": {
"flag": "🇪🇸",
"name": "Aragonese",
"native_name": "aragonés"
},
"ar": {
"flag": "🇸🇦",
"name": "Arabic",
"native_name": "العربية"
},
"ar_ae": {
"flag": "🇦🇪",
"name": "Arabic (UAE)",
"native_name": "العربية (الإمارات)"
},
"ar_bh": {
"flag": "🇧🇭",
"name": "Arabic (Bahrain)",
"native_name": "العربية (البحرين)"
},
"ar_dz": {
"flag": "🇩🇿",
"name": "Arabic (Algeria)",
"native_name": "العربية (الجزائر)"
},
"ar_eg": {
"flag": "🇪🇬",
"name": "Arabic (Egypt)",
"native_name": "العربية (مصر)"
},
"ar_iq": {
"flag": "🇮🇶",
"name": "Arabic (Iraq)",
"native_name": "العربية (العراق)"
},
"ar_jo": {
"flag": "🇯🇴",
"name": "Arabic (Jordan)",
"native_name": "العربية (الأردن)"
},
"ar_kw": {
"flag": "🇰🇼",
"name": "Arabic (Kuwait)",
"native_name": "العربية (الكويت)"
},
"ar_lb": {
"flag": "🇱🇧",
"name": "Arabic (Lebanon)",
"native_name": "العربية (لبنان)"
},
"ar_ly": {
"flag": "🇱🇾",
"name": "Arabic (Libya)",
"native_name": "العربية (ليبيا)"
},
"ar_ma": {
"flag": "🇲🇦",
"name": "Arabic (Morocco)",
"native_name": "العربية (المغرب)"
},
"ar_om": {
"flag": "🇴🇲",
"name": "Arabic (Oman)",
"native_name": "العربية (عُمان)"
},
"ar_qa": {
"flag": "🇶🇦",
"name": "Arabic (Qatar)",
"native_name": "العربية (قطر)"
},
"ar_sa": {
"flag": "🇸🇦",
"name": "Arabic (Saudi Arabia)",
"native_name": "العربية (السعودية)"
},
"ar_sd": {
"flag": "🇸🇩",
"name": "Arabic (Sudan)",
"native_name": "العربية (السودان)"
},
"ar_sy": {
"flag": "🇸🇾",
"name": "Arabic (Syria)",
"native_name": "العربية (سوريا)",
},
"ar_tn": {
"flag": "🇹🇳",
"name": "Arabic (Tunisia)",
"native_name": "العربية (تونس)"
},
"ar_ye": {
"flag": "🇾🇪",
"name": "Arabic (Yemen)",
"native_name": "العربية (اليمن)"
},
"ars_ae": {
"flag": "🇦🇪",
"name": "Najdi Arabic (UAE)",
"native_name": "نَجْدِيّ"
},
"ars_arab_sa": {
"flag": "🇸🇦",
"name": "Najdi Arabic (Saudi Arabia, Arabic Script)",
"native_name": "نَجْدِيّ"
},
"ars_sa": {
"flag": "🇸🇦",
"name": "Najdi Arabic (Saudi Arabia)",
"native_name": "نَجْدِيّ"
},
"as": {
"flag": "🇮🇳",
"name": "Assamese",
"native_name": "অসমীয়া"
},
"az": {
"flag": "🇦🇿",
"name": "Azerbaijani",
"native_name": "Azərbaycan"
},
"be": {
"flag": "🇧🇾",
"name": "Belarusian",
"native_name": "Беларуская"
},
"bg": {
"flag": "🇧🇬",
"name": "Bulgarian",
"native_name": "Български"
},
"bm": {
"flag": "🇲🇱",
"name": "Bambara",
"native_name": "bamanankan"
},
"bn": {
"flag": "🇧🇩",
"name": "Bengali",
"native_name": "বাংলা"
},
"bn_in": {
"flag": "🇮🇳",
"name": "Bengali (India)",
"native_name": "বাংলা (ভারত)"
},
"br": {
"flag": "🏴",
"name": "Breton",
"native_name": "brezhoneg"
},
"bs": {
"flag": "🇧🇦",
"name": "Bosnian",
"native_name": "Bosanski"
},
"ca": {
"flag": "🇪🇸",
"name": "Catalan",
"native_name": "Català"
},
"crh": {
"flag": "🇺🇦",
"name": "Crimean Tatar",
"native_name": "qırımtatarca"
},
"cs": {
"flag": "🇨🇿",
"name": "Czech",
"native_name": "Čeština"
},
"cv": {
"flag": "🇷🇺",
"name": "Chuvash",
"native_name": "чӑваш чӗлхи"
},
"cy": {
"flag": "🏴",
"name": "Welsh",
"native_name": "Cymraeg"
},
"da": {
"flag": "🇩🇰",
"name": "Danish",
"native_name": "Dansk"
},
"de": {
"flag": "🇩🇪",
"name": "German",
"native_name": "Deutsch",
},
"de_at": {
"flag": "🇦🇹",
"name": "German (Austria)",
"native_name": "Deutsch (Österreich)"
},
"de_be": {
"flag": "🇧🇪",
"name": "German (Belgium)",
"native_name": "Deutsch (Belgien)"
},
"de_ch": {
"flag": "🇨🇭",
"name": "German (Switzerland)",
"native_name": "Deutsch (Schweiz)"
},
"dv": {
"flag": "🇲🇻",
"name": "Dhivehi",
"native_name": "ދިވެހި"
},
"dz": {
"flag": "🇧🇹",
"name": "Dzongkha",
"native_name": "རྫོང་ཁ"
},
"el": {
"flag": "🇬🇷",
"name": "Greek",
"native_name": "Ελληνικά"
},
"en": {
"flag": "🇺🇸",
"name": "English",
"native_name": "English",
},
"en_au": {
"flag": "🇦🇺",
"name": "English (Australia)",
"native_name": "English (Australia)"
},
"en_ca": {
"flag": "🇨🇦",
"name": "English (Canada)",
"native_name": "English (Canada)"
},
"en_gb": {
"flag": "🇬🇧",
"name": "English (UK)",
"native_name": "English (UK)",
},
"en_ie": {
"flag": "🇮🇪",
"name": "English (Ireland)",
"native_name": "English (Ireland)"
},
"en_in": {
"flag": "🇮🇳",
"name": "English (India)",
"native_name": "English (India)"
},
"en_nz": {
"flag": "🇳🇿",
"name": "English (New Zealand)",
"native_name": "English (New Zealand)"
},
"en_us": {
"flag": "🇺🇸",
"name": "English (US)",
"native_name": "English (US)",
},
"es": {
"flag": "🇪🇸",
"name": "Spanish",
"native_name": "Español"
},
"es_ar": {
"flag": "🇦🇷",
"name": "Spanish (Argentina)",
"native_name": "Español (Argentina)"
},
"es_mx": {
"flag": "🇲🇽",
"name": "Spanish (Mexico)",
"native_name": "Español (México)"
},
"et": {
"flag": "🇪🇪",
"name": "Estonian",
"native_name": "Eesti"
},
"fa": {
"flag": "🇮🇷",
"name": "Persian",
"native_name": "فارسی"
},
"ff": {
"flag": "🌍",
"name": "Fula",
"native_name": "Fulfulde"
},
"fi": {
"flag": "🇫🇮",
"name": "Finnish",
"native_name": "Suomi"
},
"fo": {
"flag": "🇫🇴",
"name": "Faroese",
"native_name": "føroyskt"
},
"fr": {
"flag": "🇫🇷",
"name": "French",
"native_name": "Français"
},
"fr_ca": {
"flag": "🇨🇦",
"name": "French (Canada)",
"native_name": "Français (Canada)"
},
"fr_ch": {
"flag": "🇨🇭",
"name": "French (Switzerland)",
"native_name": "Français (Suisse)"
},
"ga": {
"flag": "🇮🇪",
"name": "Irish",
"native_name": "Gaeilge"
},
"gl": {
"flag": "🇪🇸",
"name": "Galician",
"native_name": "Galego"
},
"gn": {
"flag": "🇵🇾",
"name": "Guarani",
"native_name": "Avañe'"
},
"gu": {
"flag": "🇮🇳",
"name": "Gujarati",
"native_name": "ગુજરાતી"
},
"ha": {
"flag": "🇳🇬",
"name": "Hausa",
"native_name": "هَوُسَ"
},
"he": {
"flag": "🇮🇱",
"name": "Hebrew",
"native_name": "עברית"
},
"hi": {
"flag": "🇮🇳",
"name": "Hindi",
"native_name": "हिन्दी"
},
"hr": {
"flag": "🇭🇷",
"name": "Croatian",
"native_name": "Hrvatski"
},
"ht": {
"flag": "🇭🇹",
"name": "Haitian Creole",
"native_name": "Kreyòl ayisyen"
},
"hu": {
"flag": "🇭🇺",
"name": "Hungarian",
"native_name": "Magyar"
},
"id": {
"flag": "🇮🇩",
"name": "Indonesian",
"native_name": "Bahasa Indonesia"
},
"io": {
"flag": "🌍",
"name": "Ido",
"native_name": "Ido"
},
"is": {
"flag": "🇮🇸",
"name": "Icelandic",
"native_name": "Íslenska"
},
"it": {
"flag": "🇮🇹",
"name": "Italian",
"native_name": "Italiano"
},
"ja": {
"flag": "🇯🇵",
"name": "Japanese",
"native_name": "日本語"
},
"jv_id": {
"flag": "🇮🇩",
"name": "Javanese (Indonesia)",
"native_name": "basa jawa"
},
"ka": {
"flag": "🇬🇪",
"name": "Georgian",
"native_name": "ქართული"
},
"kg": {
"flag": "🇨🇬",
"name": "Kongo",
"native_name": "KiKongo"
},
"kj": {
"flag": "🇳🇦",
"name": "Kuanyama",
"native_name": "Oshikwanyama"
},
"kk": {
"flag": "🇰🇿",
"name": "Kazakh",
"native_name": "Қазақ"
},
"kl": {
"flag": "🇬🇱",
"name": "Kalaallisut",
"native_name": "kalaallisut"
},
"km": {
"flag": "🇰🇭",
"name": "Khmer",
"native_name": "ខ្មែរ"
},
"ko": {
"flag": "🇰🇷",
"name": "Korean",
"native_name": "한국어"
},
"ks": {
"flag": "🇮🇳",
"name": "Kashmiri",
"native_name": "کٲشُر"
},
"ku": {
"flag": "🇮🇶",
"name": "Kurdish",
"native_name": "Kurdî",
},
"lo": {
"flag": "🇱🇦",
"name": "Lao",
"native_name": "ລາວ"
},
"lt": {
"flag": "🇱🇹",
"name": "Lithuanian",
"native_name": "Lietuvių"
},
"lv": {
"flag": "🇱🇻",
"name": "Latvian",
"native_name": "Latviešu"
},
"mg": {
"flag": "🇲🇬",
"name": "Malagasy",
"native_name": "Malagasy"
},
"mg_mg": {
"flag": "🇲🇬",
"name": "Malagasy (Madagascar)",
"native_name": "malagasy"
},
"mh": {
"flag": "🇲🇭",
"name": "Marshallese",
"native_name": "Kajin M̧ajeļ"
},
"mk": {
"flag": "🇲🇰",
"name": "Macedonian",
"native_name": "Македонски"
},
"mn_mn": {
"flag": "🇲🇳",
"name": "Mongolian (Mongolia)",
"native_name": "Монгол хэл"
},
"mr_in": {
"flag": "🇮🇳",
"name": "Marathi (India)",
"native_name": "मराठी"
},
"ms": {
"flag": "🇲🇾",
"name": "Malay",
"native_name": "Bahasa Melayu"
},
"my": {
"flag": "🇲🇲",
"name": "Burmese",
"native_name": "မြန်မာဘာသာ"
},
"na": {
"flag": "🇳🇷",
"name": "Nauruan",
"native_name": "Dorerin Naoero"
},
"nb": {
"flag": "🇳🇴",
"name": "Norwegian Bokmål",
"native_name": "Norsk Bokmål"
},
"ng": {
"flag": "🇳🇦",
"name": "Ndonga",
"native_name": "Oshindonga"
},
"nl": {
"flag": "🇳🇱",
"name": "Dutch",
"native_name": "Nederlands"
},
"om": {
"flag": "🇪🇹",
"name": "Oromo",
"native_name": "Afaan Oromoo"
},
"os": {
"flag": "🇷🇺",
"name": "Ossetian",
"native_name": "ирон æвзаг"
},
"pl": {
"flag": "🇵🇱",
"name": "Polish",
"native_name": "Polski"
},
"pt": {
"flag": "🇵🇹",
"name": "Portuguese",
"native_name": "Português"
},
"pt_br": {
"flag": "🇧🇷",
"name": "Portuguese (Brazil)",
"native_name": "Português (Brasil)"
},
"qu": {
"flag": "🇵🇪",
"name": "Quechua",
"native_name": "Runa Simi"
},
"ro": {
"flag": "🇷🇴",
"name": "Romanian",
"native_name": "Română"
},
"ru": {
"flag": "🇷🇺",
"name": "Russian",
"native_name": "Русский"
},
"rw": {
"flag": "🇷🇼",
"name": "Kinyarwanda",
"native_name": "Ikinyarwanda"
},
"sc": {
"flag": "🇮🇹",
"name": "Sardinian",
"native_name": "sardu"
},
"sg": {
"flag": "🇨🇫",
"name": "Sango",
"native_name": "yângâ tî sängö"
},
"sk": {
"flag": "🇸🇰",
"name": "Slovak",
"native_name": "Slovenčina"
},
"sl": {
"flag": "🇸🇮",
"name": "Slovenian",
"native_name": "Slovenščina"
},
"sm": {
"flag": "🇼🇸",
"name": "Samoan",
"native_name": "Gagana Samoa"
},
"sn": {
"flag": "🇿🇼",
"name": "Shona",
"native_name": "chiShona"
},
"so": {
"flag": "🇸🇴",
"name": "Somali",
"native_name": "Soomaaliga"
},
"sr": {
"flag": "🇷🇸",
"name": "Serbian",
"native_name": "Српски"
},
"ss": {
"flag": "🇸🇿",
"name": "Swati",
"native_name": "SiSwati"
},
"st": {
"flag": "🇱🇸",
"name": "Southern Sotho",
"native_name": "Sesotho"
},
"su_id": {
"flag": "🇮🇩",
"name": "Sundanese (Indonesia)",
"native_name": "basa sunda"
},
"sv": {
"flag": "🇸🇪",
"name": "Swedish",
"native_name": "Svenska"
},
"th": {
"flag": "🇹🇭",
"name": "Thai",
"native_name": "ไทย"
},
"tk": {
"flag": "🇹🇲",
"name": "Turkmen",
"native_name": "Türkmen"
},
"tn": {
"flag": "🇧🇼",
"name": "Tswana",
"native_name": "Setswana"
},
"to": {
"flag": "🇹🇴",
"name": "Tongan",
"native_name": "faka-Tonga"
},
"tr": {
"flag": "🇹🇷",
"name": "Turkish",
"native_name": "Türkçe",
},
"ts": {
"flag": "🇿🇦",
"name": "Tsonga",
"native_name": "Xitsonga"
},
"ts_zw": {
"flag": "🇿🇼",
"name": "Tsonga (Zimbabwe)",
"native_name": "xitsonga"
},
"ty": {
"flag": "🇵🇫",
"name": "Tahitian",
"native_name": "Reo Tahiti"
},
"uk": {
"flag": "🇺🇦",
"name": "Ukrainian",
"native_name": "Українська"
},
"ur": {
"flag": "🇵🇰",
"name": "Urdu",
"native_name": "اردو"
},
"uz": {
"flag": "🇺🇿",
"name": "Uzbek",
"native_name": "oʻzbek"
},
"ve": {
"flag": "🇿🇦",
"name": "Venda",
"native_name": "Tshivenda"
},
"vi": {
"flag": "🇻🇳",
"name": "Vietnamese",
"native_name": "Tiếng Việt"
},
"vo": {
"flag": "🌍",
"name": "Volapük",
"native_name": "Volapük"
},
"wa": {
"flag": "🇧🇪",
"name": "Walloon",
"native_name": "walon"
},
"xh": {
"flag": "🇿🇦",
"name": "Xhosa",
"native_name": "isiXhosa"
},
"yi": {
"flag": "🌍",
"name": "Yiddish",
"native_name": "ייִדיש"
},
"yo": {
"flag": "🇳🇬",
"name": "Yoruba",
"native_name": "Yorùbá"
},
"zh": {
"flag": "🇨🇳",
"name": "Chinese",
"native_name": "中文"
},
"zh_hk": {
"flag": "🇭🇰",
"name": "Chinese (Hong Kong)",
"native_name": "中文(香港)"
},
"zh_tw": {
"flag": "🇹🇼",
"name": "Chinese (Taiwan)",
"native_name": "中文(台灣)"
},
"zu": {
"flag": "🇿🇦",
"name": "Zulu",
"native_name": "isiZulu"
}
}
if True:
import toml
from pathlib import Path
def process_data_dict(data: dict, existing: dict):
for key, value in data.items():
if key not in existing:
existing[key] = value
continue
if isinstance(value, dict):
if isinstance(existing[key], dict):
process_data_dict(value, existing[key])
continue
existing[key] = value
def process_data(data: dict, obj):
for key, value in data.items():
if not hasattr(obj, key):
continue
existing = getattr(obj, key)
if isinstance(existing, dict):
process_data_dict(value, existing)
continue
if isinstance(value, dict):
process_data(value, getattr(obj, key))
continue
setattr(obj, key, value)
config_file = Path("stsg.toml")
if config_file.exists() and config_file.is_file():
with config_file.open("r") as f:
process_data(toml.loads(f.read()), config)

View File

@@ -5,9 +5,14 @@ import time
import os
import subprocess
from .config import SOURCE_DIRECTORY, CODE_DIRECTORY
from . import config
from .build import build as complete_build
CODE_DIRECTORY = "stsg"
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("stsg")
@@ -69,7 +74,7 @@ def build_on_change():
build()
observer = Observer()
observer.schedule(MarkdownChangeHandler(), path=SOURCE_DIRECTORY, recursive=True)
observer.schedule(MarkdownChangeHandler(), path=config.setup.source_directory, recursive=True)
observer.start()
try:
@@ -97,3 +102,4 @@ def hot_reload():
observer.stop()
observer.join()

View File

@@ -1,127 +1,466 @@
from __future__ import annotations
import logging
import shutil
from pathlib import Path
import os
import markdown
from typing import Optional
from .config import SOURCE_DIRECTORY, DIST_DIRECTORY, STATIC_DIRECTORY
from markdown2 import markdown
from typing import Optional, Union, Dict, Generator, List, DefaultDict, Any, TypedDict, Set
from bs4 import BeautifulSoup
from collections import defaultdict, UserList
import toml
from datetime import datetime
import jinja2
from functools import cached_property
logger = logging.getLogger("stsg.build")
from .definitions import *
from . import config
def copy_static(src, dst):
if not os.path.exists(src):
logger.warn("The static folder '%s' wasn't defined.", src)
return
def shorten_text_and_clean(html_string, max_length=config.formatting.preview_length):
soup = BeautifulSoup(html_string, 'html.parser')
logger.info("copying static files from '%s' to '%r'", src, dst)
# Keep track of total characters added
total_chars = 0
finished = False
os.makedirs(dst, exist_ok=True)
# Function to recursively trim and clean text
def process_element(element):
nonlocal total_chars, finished
for root, dirs, files in os.walk(src):
if any(p.startswith(".") for p in Path(root).parts):
continue
# Compute relative path from the source root
rel_path = os.path.relpath(root, src)
dest_dir = os.path.join(dst, rel_path)
os.makedirs(dest_dir, exist_ok=True)
for file in files:
if file.startswith("."):
for child in list(element.children):
if finished:
child.extract()
continue
src_file = os.path.join(root, file)
dest_file = os.path.join(dest_dir, file)
shutil.copy2(src_file, dest_file)
if isinstance(child, str):
remaining = max_length - total_chars
if remaining <= 0:
child.extract()
finished = True
elif len(child) > remaining:
child.replace_with(child[:remaining] + '...')
total_chars = max_length
finished = True
else:
total_chars += len(child)
elif hasattr(child, 'children'):
process_element(child)
# Remove empty tags
if not child.text.strip():
child.decompose()
process_element(soup)
return str(soup)
class Context:
def __init__(self, root: str = SOURCE_DIRECTORY):
self.file = None
def shift_headings(html_string, header_shift=config.formatting.preview_header_shift):
soup = BeautifulSoup(html_string, 'html.parser')
current_root = Path(root)
while current_root.parts and self.file is None:
current_file = Path(current_root, "index.html")
if current_file.exists() and current_file.is_file:
self.file = current_file
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
current_root = current_root.parent
return str(soup)
if self.file is None:
logger.error("couldn't find context for %s", root)
def get_preview_text(html_string: str):
return shift_headings(shorten_text_and_clean(html_string))
class TemplateDict(dict):
def __init__(self, folder: Path):
self.folder = folder
super().__init__()
def __missing__(self, name: str) -> jinja2.Template:
f = self.folder / (name + ".html")
if not f.exists():
logger.error("no template with the name %s exists", name)
exit(1)
logger.info("%s found context %r", root, str(self.file))
def get_text(self, **placeholder_values: dict):
text = self.file.read_text()
for key, value in placeholder_values.items():
text = text.replace(f"<{key}/>", value)
text = text.replace(f"<{key} />", value)
return text
t = jinja2.Template(f.read_text())
self[name] = t
return t
def convert_md(src: Path, dst: Path, context: Optional[Context] = None):
html_content = markdown.markdown(src.read_text())
context = Context(str(src.parent))
full_page = context.get_text(content=html_content)
folder_dst = dst.parent / dst.name.replace(".md", "")
folder_dst.mkdir(parents=True, exist_ok=True)
TEMPLATE: Dict[str, jinja2.Template] = TemplateDict(Path(config.setup.source_directory, "templates"))
with Path(folder_dst, "index.html").open("w") as f:
f.write(full_page)
class LanguageDict(dict):
def __missing__(self, key: str):
if key not in config.languages:
raise KeyError(key)
lang_dict = config.languages[key]
lang_dict["priority"] = lang_dict.get("priority", 0)
elements = key.split("_")
if len(elements) > 1:
elements[-1] = elements[-1].upper()
lang_dict["code"] = "-".join(elements)
return lang_dict
class CustomLanguageCode:
def __init__(self, file: Path):
self.file: Path = file
LANGUAGES = LanguageDict()
@property
def language_code(self) -> str:
return self.file.name.replace(".md", "")
def add_html_link(c):
name = c["name"]
url = c["url"]
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 relative_url(self) -> str:
return str(self.file)
def __repr__(self) -> str:
return f"{self.language_code}"
def context(self) -> List[Union[ArticleContext, ArticleTranslationContext]]:
return [a.context for a in self]
def walk_directory(root):
src_path = Path(SOURCE_DIRECTORY, root)
dst_path = Path(DIST_DIRECTORY, root)
class ArticleTranslation:
article: Article
slug: str = property(fget=lambda self: self.article.slug)
file: Path
language_codes_found = []
@cached_property
def html_content(self) -> str:
html_content = self.file.read_text()
if self.file.suffix == ".md":
return markdown(html_content, extras=config.formatting.markdown_extras)
return html_content
for current_full_path in src_path.iterdir():
current_name = Path(current_full_path).name
current_dst = Path(dst_path, current_name)
current_src = Path(src_path, current_name)
@cached_property
def language_code(self) -> str:
language_code = self.file.stem.lower().replace("-", "_")
if current_name == "static":
copy_static(current_src, current_dst)
continue
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
if current_name.endswith(".md"):
convert_md(current_src, current_dst)
language_codes_found.append(CustomLanguageCode(Path(root, current_name)))
continue
def __init_context__(self):
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
if current_src.is_dir():
walk_directory(Path(root, current_full_path.name))
# get children
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
print(language_codes_found)
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))
class Article:
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.article_path: List[Article] = article_path or []
self.article_path.append(self)
self.context = CROSS_ARTICLE_CONTEXT[self.slug] = {}
ARTICLE_LAKE[self.slug] = self
self.linked_articles = ArticleList([])
# build the tree
self.child_articles = ArticleList([])
self.article_translations_list = []
self.article_translations_map = {}
for c in self.directory.iterdir():
if c.name == "index.toml":
continue
if c.is_file():
at = ArticleTranslation(c, self)
self.article_translations_list.append(at)
self.article_translations_map[at.language_code] = at
elif c.is_dir():
self.child_articles.append(Article(
directory=c,
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.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["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
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 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__()
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))
for at in self.article_translations_list:
at.build()
for ac in self.child_articles:
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_REFERENCE_VALUES: DefaultDict[str, Dict[str, str]] = defaultdict(dict)
def build():
logger.info("building static page")
walk_directory("")
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()

View File

@@ -1,9 +0,0 @@
SOURCE_DIRECTORY = "src"
DIST_DIRECTORY = "dist"
# relative to SOURCE_DIRECTORY / DIST_DIRECTORY
STATIC_DIRECTORY = "static"
# FOR DEVELOPMENT
CODE_DIRECTORY = "stsg"

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]]