95 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
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
38 changed files with 1960 additions and 1256 deletions

1
.gitignore vendored
View File

@@ -174,3 +174,4 @@ cython_debug/
.pypirc
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,15 +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,75 +0,0 @@
# Populumque avidosque Sparte quoque auctore equidem
## Sunt aevis
Lorem markdownum turbavere prisca Aeacidae morando esse. Quam Styga spectata,
pariter Iove iunctis exercere Solis? Atlantis possit succurrere quam!
if (stationRecord < ctp(rup, columnBase, dtd)) {
impact_qbe_symbolic(bank_c(exploit_gnutella_social), inbox);
marketing = telnetWebmasterFpu;
circuitSoapDns(dac, ieee);
} else {
ebook.spider(wildcard_publishing_memory.tcpDisk(encoding, 48149), -1 +
copyright_flash_icmp, superscalar_cluster + kofficeIsp);
reimage.mac = dslWebmail;
kilobyteVariable.margin_keylogger = dvd + microcomputer;
}
font_switch(servlet, file(1, protocol) * skinTouchscreen,
wheel_station_computer);
var startRom = bit(php_touchscreen_icio);
var unicode_hover_tiger = command(scan_install_cd(ruby(mail_chipset, web,
clientMemeSoftware), optical, wirelessMegahertz), oasis);
## Est nec locumque anxia et
Maligno puppes potuit petit. **Ipse regnat venit** tangit mitti opibus est unus
spectacula erat! Bacche qui dedit in ardet Phrygiis Liternum ipso ille Orphei
Canentem *ut*, parenti terrae! Frondibus deus sine leoni frustraque lentus. Est
deos cum corripuit erat sibi concussit simul; suus tantum.
Camenis Lucifer ex geniti sitis quem. Styga si Ceae nova media remugis: haerens
ridet, nam [Pindo](http://www.murra.com/argolicaemaximus) est tritis flamma
[dixit una licebit](http://www.sedare-mopsus.net/) Pelia perdite! Aura *aurea
mecum* una mirabantur mansit domum simul de Euboica altis vincula tenentis vires
sub, *Scythicae*. Mora sitis pocula. Ultimus idem triplices inquit.
## Et atque ministris imagine fas tenuit fornacibus
Adeunda suffundit ille: Bacchi moribundo et quam **cacumina videre Tamasenum**.
Gauderet in [non arbitrium caelo](http://www.madidusperiuraque.io/).
## In corpora in micat Phoebus corque transitus
Mihi in macies, ab avoque malorum decusque. Appellant expellam unus colore
exiguo, maior ara loqui sit vires.
var uat = cardPrinterLocalhost.pppoe(display, operating_row_fsb(
scalable_lion, compilerHeuristicTweet - 22));
var toslink_software = tokenPciPrompt.source_x_firmware(drive,
boxSdkDlc.servicesIcsDrive(certificate_cycle_illegal(resolution)),
outboxTftMap + rssTextZebibyte.flashImpactDisk(3, eup_ad));
var compression = programWebPort;
if (intellectual_left_system) {
favorites(lossyDramDay + 7, upsSliTruncate, 2);
laptop.android = ocr_piracy(clean, flatbedRte - -1);
directx_file_cable = -1;
} else {
point_sink_controller(flash(filenameDataAccess, vleSoftwareListserv),
point_rate_cmos(control_soa_restore));
javascript *= nic_format;
address = nameDcim(touchscreenLink.port_port(diskHeat, vaporware));
}
## Enim nunc solvi
Est pudet citharae, corpus? Modo in armentaque pennisque videri aquarum, est
equos ne in vulnere domus; maris. Quodque quoque orbe omnes metus, sol
[putas](http://www.uno.com/deorumvolumine) marmore fuit secreta haut vobis,
faciendo, oro.
Quid sequenti, supersunt quoque, sortite; in in tenetur vecta horriferamque
amabat. Vos nudae anni amor chelydri [Picus candentibus
et](http://pugnare.io/angulus.html) sonum, et sentibus geminato volucrem
mercibus bracchia, cum *posito* delubraque. Templa extrahit in totidem altera
Nioben, honoris sui, fibris!

View File

@@ -1,57 +0,0 @@
meow
# Abstrahere reddita celebrare in ossa
## Usque de celebrabant puer
Lorem markdownum, nec ora et vero me nec natae suadent. Nec damno ignorat
propiore aliquid temptata decipienda habetur. Vulnera lacrimis aequoreo madidos,
copia uvae, herbosaque quoque, per harenas, canos fui monstro Peleus.
Et fuge cum liquidum puer Herculis arentis, tantum caudaque et generi vilior, in
rubore. Caeli modo palmis, suo tria accipe visus non similis qui remittat
retentus porrigit fluxit ubi testis.
> Sulphura et color reliquit dextera: quid summa continuere obductos egesto
> moriens **fluentia vult iunctamque**, mihi patres spiro. Est Iovis **imperat
> quem est** putavi annis *omnis*; flumina, leporem constabat grave pelagi,
> insiluit, igne invito? Auctor circuit quod.
## Rhoetus gravis
Parari modo in sustulerat: ora sic verba meruit, uti. Pignora citus facto
amplectimur cupido amentes foedantem multoque datura.
[Quid](http://www.prima.net/se) ex tanti armaque exhibita descenderat!
1. Alis ima
2. Mirantur nive sit
3. Maris ut adlabimur humana fine quam vultusque
## A idonea miserum
Montes tibi deorum igitur. Poterat nunc porrigitur perdidimus *sidereos animi
praesepia* nihil praeferri functa, in **Pleias removete** oculos sollemni
Tatius: **modo**. Inde dedit! Atque in matrem spinae foret ponti quam dixit,
aras. Gladii addendum fiducia magno, se quo fata humo esse **tellure corpora
discederet** sucis manibus, parentem ante, Iovem.
Laqueique honore sequentia tyranni Harpalos, paelex, foedat tempestiva nomen.
Sit ter indiciumque requirit utrumque in nil et *suspectus*, quaerite patriam
nec facta [securi](http://heuqua.io/adhaut). Confessa ut per sit nostro futura
metaque oblitae fameque exit adspiciens. Morte flectere invidiam certe cum
vixque *nubes clamor viderunt* praeceps infamis collo percussis axem plena saxum
urbes ferre undae. Totoque utuntur ore lupus inplet sibila ullam, qui corpore
*contermina aera*?
Convocat ipse abeunt sententia concolor a Auguste epops solent iubet qui
aequora. In illi, solvente resecare violentam nescio accipit multarum [aureus
exspectatus](http://oris-tibi.io/nasci) ergo ora. Positis **inquit**, sit iamque
hederae ulterius, pontus linguae matutina terra sic isse Graium passosque
sanguinis secus petit!
Adfore dei volucrum Lydas; [hoc](http://similemnescitve.net/ipsamquee) quam
[superosque](http://www.quoqueabit.com/cumqueiam.html) caligine vulnera quoque
corpus foedaque mentis qui nectare, fatendo sensit! Rursus nulli miraris nuda
*Acrisio*. Cum modo, satis dissuadet luce; cum *freta* ab et diu, labor *tenuata
ieiunia*? Caesosque thalamique precor, dedit nulla loca [arce
et](http://viscera-rerum.com/mixta) tinguit aera.

View File

@@ -1,75 +0,0 @@
# Populumque avidosque Sparte quoque auctore equidem
## Sunt aevis
Lorem markdownum turbavere prisca Aeacidae morando esse. Quam Styga spectata,
pariter Iove iunctis exercere Solis? Atlantis possit succurrere quam!
if (stationRecord < ctp(rup, columnBase, dtd)) {
impact_qbe_symbolic(bank_c(exploit_gnutella_social), inbox);
marketing = telnetWebmasterFpu;
circuitSoapDns(dac, ieee);
} else {
ebook.spider(wildcard_publishing_memory.tcpDisk(encoding, 48149), -1 +
copyright_flash_icmp, superscalar_cluster + kofficeIsp);
reimage.mac = dslWebmail;
kilobyteVariable.margin_keylogger = dvd + microcomputer;
}
font_switch(servlet, file(1, protocol) * skinTouchscreen,
wheel_station_computer);
var startRom = bit(php_touchscreen_icio);
var unicode_hover_tiger = command(scan_install_cd(ruby(mail_chipset, web,
clientMemeSoftware), optical, wirelessMegahertz), oasis);
## Est nec locumque anxia et
Maligno puppes potuit petit. **Ipse regnat venit** tangit mitti opibus est unus
spectacula erat! Bacche qui dedit in ardet Phrygiis Liternum ipso ille Orphei
Canentem *ut*, parenti terrae! Frondibus deus sine leoni frustraque lentus. Est
deos cum corripuit erat sibi concussit simul; suus tantum.
Camenis Lucifer ex geniti sitis quem. Styga si Ceae nova media remugis: haerens
ridet, nam [Pindo](http://www.murra.com/argolicaemaximus) est tritis flamma
[dixit una licebit](http://www.sedare-mopsus.net/) Pelia perdite! Aura *aurea
mecum* una mirabantur mansit domum simul de Euboica altis vincula tenentis vires
sub, *Scythicae*. Mora sitis pocula. Ultimus idem triplices inquit.
## Et atque ministris imagine fas tenuit fornacibus
Adeunda suffundit ille: Bacchi moribundo et quam **cacumina videre Tamasenum**.
Gauderet in [non arbitrium caelo](http://www.madidusperiuraque.io/).
## In corpora in micat Phoebus corque transitus
Mihi in macies, ab avoque malorum decusque. Appellant expellam unus colore
exiguo, maior ara loqui sit vires.
var uat = cardPrinterLocalhost.pppoe(display, operating_row_fsb(
scalable_lion, compilerHeuristicTweet - 22));
var toslink_software = tokenPciPrompt.source_x_firmware(drive,
boxSdkDlc.servicesIcsDrive(certificate_cycle_illegal(resolution)),
outboxTftMap + rssTextZebibyte.flashImpactDisk(3, eup_ad));
var compression = programWebPort;
if (intellectual_left_system) {
favorites(lossyDramDay + 7, upsSliTruncate, 2);
laptop.android = ocr_piracy(clean, flatbedRte - -1);
directx_file_cable = -1;
} else {
point_sink_controller(flash(filenameDataAccess, vleSoftwareListserv),
point_rate_cmos(control_soa_restore));
javascript *= nic_format;
address = nameDcim(touchscreenLink.port_port(diskHeat, vaporware));
}
## Enim nunc solvi
Est pudet citharae, corpus? Modo in armentaque pennisque videri aquarum, est
equos ne in vulnere domus; maris. Quodque quoque orbe omnes metus, sol
[putas](http://www.uno.com/deorumvolumine) marmore fuit secreta haut vobis,
faciendo, oro.
Quid sequenti, supersunt quoque, sortite; in in tenetur vecta horriferamque
amabat. Vos nudae anni amor chelydri [Picus candentibus
et](http://pugnare.io/angulus.html) sonum, et sentibus geminato volucrem
mercibus bracchia, cum *posito* delubraque. Templa extrahit in totidem altera
Nioben, honoris sui, fibris!

View File

@@ -1,35 +0,0 @@
# Navê Gotarê
Ev gotar, mînakek placeholder ya bi zimanê Kurdî ye. Nivîs, weşan û rûpelên nûçe di vê belgeyê de têne pêşniyar kirin. Ev metn ji bo testkirina layout an jî demo-yê hatiye çêkirin û çend bingehên cihanî yên naveroka gotarê nîşan dide.
## Destpêk
Di destpêka gotarê de, dikarin rêvekên bingehîn û armancên mijarê vebêjin. Tu dikarî vê metnê bikar bînin ji bo şexsî an jî projeyên xwe:
Lorem ipsum ji bo kurdî:
"Rojên nû û şevên hêja, ev nivîs têne çêkirin ku bi awayek nîşanbideke cîhan û hunermendî were afirandin."
## Lêkolîn û Naverok
Di vê beşê de hinek îzahiyên naverokî yên placeholder hatine peyda kirin. Hûn dikarin bi rêza li jêr anînên mifteyî yên gotarê şop bikin:
- **Mijar:** Di vê gotarê de, mijarên bi zor û hêja têne nîşandan.
- **Rêbaz:** Ew kesên ku ji ber vê gotarê şîrove û daxuyanî dikin, çavkaniyên xwe diguhezînin.
- **Agahî:** Di vê derbarê de, agahîyên cihanî, dîtin û têgihiştin hatine berhev kirin.
Di bin vê şexsiyeta, hertişt di dema xwe de têgihiştin û anîn pêşiyê dayîn.
## Şirove û Kod
Di vê paragrafa kêm-kêm de, em dikarin bingehên şiroveyê yên gotarê di kodê de jî şop bikin:
```python
def peyama_kurdî():
mesaj = "Her bijî Kurdistan! Rojên baş û serkeftin!"
return mesaj
print(peyama_kurdî())
```
Kodê li ser vê koda simplesa gotinê ye û dikare weşana nûçe an demo-yê projeyan bixebite.
# Encama Gotarê
Di encama gotarê de, hûn dikarin her çend beşên bingehîn yên nûçe, daxuyanî û şirove yên navekî bifikirin.
Bê guman, ev placeholder bi awayek qelew li ser çalakiya te ya malper, blog an jî her sedema dijîtal tê de karîger e.

View File

@@ -1,35 +0,0 @@
# Navê Gotarê
Ev gotar, mînakek placeholder ya bi zimanê Kurdî ye. Nivîs, weşan û rûpelên nûçe di vê belgeyê de têne pêşniyar kirin. Ev metn ji bo testkirina layout an jî demo-yê hatiye çêkirin û çend bingehên cihanî yên naveroka gotarê nîşan dide.
## Destpêk
Di destpêka gotarê de, dikarin rêvekên bingehîn û armancên mijarê vebêjin. Tu dikarî vê metnê bikar bînin ji bo şexsî an jî projeyên xwe:
Lorem ipsum ji bo kurdî:
"Rojên nû û şevên hêja, ev nivîs têne çêkirin ku bi awayek nîşanbideke cîhan û hunermendî were afirandin."
## Lêkolîn û Naverok
Di vê beşê de hinek îzahiyên naverokî yên placeholder hatine peyda kirin. Hûn dikarin bi rêza li jêr anînên mifteyî yên gotarê şop bikin:
- **Mijar:** Di vê gotarê de, mijarên bi zor û hêja têne nîşandan.
- **Rêbaz:** Ew kesên ku ji ber vê gotarê şîrove û daxuyanî dikin, çavkaniyên xwe diguhezînin.
- **Agahî:** Di vê derbarê de, agahîyên cihanî, dîtin û têgihiştin hatine berhev kirin.
Di bin vê şexsiyeta, hertişt di dema xwe de têgihiştin û anîn pêşiyê dayîn.
## Şirove û Kod
Di vê paragrafa kêm-kêm de, em dikarin bingehên şiroveyê yên gotarê di kodê de jî şop bikin:
```python
def peyama_kurdî():
mesaj = "Her bijî Kurdistan! Rojên baş û serkeftin!"
return mesaj
print(peyama_kurdî())
```
Kodê li ser vê koda simplesa gotinê ye û dikare weşana nûçe an demo-yê projeyan bixebite.
# Encama Gotarê
Di encama gotarê de, hûn dikarin her çend beşên bingehîn yên nûçe, daxuyanî û şirove yên navekî bifikirin.
Bê guman, ev placeholder bi awayek qelew li ser çalakiya te ya malper, blog an jî her sedema dijîtal tê de karîger e.

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%;
}
}

View File

@@ -1,37 +1,122 @@
<!DOCTYPE html>
<html lang="{article_language_code}">
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{article_language_flag} {article_title}</title>
<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_overview_url}">
<strong>{article_language_flag} {article_title}</strong>
<a class="navbar-item" href="#">
<strong>{{name}}</strong>
</a>
</div>
</nav>
<!-- Main Content -->
{% 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">
<div class="content">
{article_content}
{% 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>STSG</strong> by Hazel. &copy; 2025</p>
<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>

View File

@@ -1,55 +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>
<section class="section">
<div class="container">
<div class="column is-half is-offset-one-quarter">{overview_cards}</div>
</div>
</section>
<!-- Footer -->
<footer class="footer">
<div class="content has-text-centered">
<p><strong>STSG</strong> by Hazel. &copy; 2025</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

@@ -1,8 +0,0 @@
<div class="card mb-4" lang="{article_language_code}">
<a href="{article_href}" hreflang="{article_language_code}" class="card mb-4" style="color: inherit; text-decoration: none;">
<div class="card-content" href="{article_href}">
<p class="title">{article_language_flag} {article_title}</p>
<p class="content">{article_preview}</p>
</div>
</a>
</div>

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,15 +5,25 @@ 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")
def build():
complete_build()
build_wiki()
# complete_build()
def build_wiki():
pass
class MarkdownChangeHandler(FileSystemEventHandler):
@@ -69,7 +79,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 +107,4 @@ def hot_reload():
observer.stop()
observer.join()

View File

@@ -3,177 +3,467 @@ import logging
import shutil
from pathlib import Path
import os
import markdown
from typing import Optional, Union, Dict, Generator, List
from markdown2 import markdown
from typing import Optional, Union, Dict, Generator, List, DefaultDict, Any, TypedDict, Set
from bs4 import BeautifulSoup
from .config import SOURCE_DIRECTORY, DIST_DIRECTORY, LANGUAGE_INFORMATION, ARTICLE_PREVIEW_LENGTH, DEFAULT_LANGUAGE
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 replace_values(template: str, values: Dict[str, str]) -> str:
for key, value in values.items():
template = template.replace("{" + key + "}", value)
def shorten_text_and_clean(html_string, max_length=config.formatting.preview_length):
soup = BeautifulSoup(html_string, 'html.parser')
return template
# Keep track of total characters added
total_chars = 0
finished = False
# Function to recursively trim and clean text
def process_element(element):
nonlocal total_chars, finished
for child in list(element.children):
if finished:
child.extract()
continue
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)
def get_first_header_content(content):
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)
def shift_headings(html_string, header_shift=config.formatting.preview_header_shift):
soup = BeautifulSoup(html_string, 'html.parser')
return self.language_code.native_name
for level in range(6, 0, -1): # Start from h6 to h1 to avoid overwriting
old_tag = f'h{level}'
for tag in soup.find_all(old_tag):
new_level = min(level + header_shift, 6) # Cap at h6
new_tag = f'h{new_level}'
tag.name = new_tag
return str(soup)
class Template:
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__()
self.article: str = (self.folder / "article.html").read_text()
self.overview: str = (self.folder / "overview.html").read_text()
self.overview_card: str = (self.folder / "overview_card.html").read_text()
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)
TEMPLATE = Template(Path(SOURCE_DIRECTORY, "templates"))
t = jinja2.Template(f.read_text())
self[name] = t
return t
TEMPLATE: Dict[str, jinja2.Template] = TemplateDict(Path(config.setup.source_directory, "templates"))
class LanguageDict(dict):
def __missing__(self, key: str):
if key not in config.languages:
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
LANGUAGES = LanguageDict()
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 context(self) -> List[Union[ArticleContext, ArticleTranslationContext]]:
return [a.context for a in self]
class ArticleTranslation:
def __init__(self, file: Path, article_overview: ArticleOverview):
self.file = file
self.article_overview = article_overview
self.language_code = self.file.stem
article: Article
slug: str = property(fget=lambda self: self.article.slug)
file: Path
self.article_content = self.file.read_text()
@cached_property
def html_content(self) -> str:
html_content = self.file.read_text()
if self.file.suffix == ".md":
self.article_content = markdown.markdown(self.article_content)
return markdown(html_content, extras=config.formatting.markdown_extras)
return html_content
self.url = "/" + self.language_code + self.article_overview.url
self.dist_path = Path(DIST_DIRECTORY, self.url.strip("/"))
@cached_property
def language_code(self) -> str:
language_code = self.file.stem.lower().replace("-", "_")
_language_info = DEFAULT_LANGUAGE
parsed_language_code = self.language_code.lower().replace("-", "_")
if parsed_language_code in LANGUAGE_INFORMATION:
_language_info = LANGUAGE_INFORMATION[parsed_language_code]
elif parsed_language_code.split("_")[0] in LANGUAGE_INFORMATION:
_language_info = LANGUAGE_INFORMATION[parsed_language_code.split("_")[0]]
if language_code in config.languages:
return language_code
self.language_name: str = _language_info["native_name"]
self.language_flag: str = _language_info["flag"]
self.priority: int = _language_info.get("priority", 0)
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["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
# 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
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(self.get_article())
f.write(TEMPLATE["article_translation"].render(self.context))
def _get_values(self) -> Dict[str, str]:
return {
"article_content": self.article_content,
"article_preview": self.article_content[:ARTICLE_PREVIEW_LENGTH] + "...",
"article_overview_url": self.article_overview.url,
"article_href": self.url,
"article_title": get_first_header_content(self.article_content),
"article_language_name": self.language_name,
"article_language_code": self.language_code,
"article_language_flag": self.language_flag,
}
def get_article(self) -> str:
global TEMPLATE
return replace_values(TEMPLATE.article, self._get_values())
def get_overview_card(self) -> str:
global TEMPLATE
return replace_values(TEMPLATE.overview_card, self._get_values())
def build_wiki(self, language_code: str):
pass
class ArticleOverview:
def __init__(self, directory: Path, url: str = ""):
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.name = self.directory.name
self.url = url + "/" + self.name
self.dist_path = Path(DIST_DIRECTORY, self.url.strip("/"))
self.article_path: List[Article] = article_path or []
self.article_path.append(self)
self.child_articles: List[ArticleOverview] = []
self.article_translations: List[ArticleTranslation] = []
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.is_file():
self.article_translations.append(ArticleTranslation(c, self))
elif c.is_dir():
self.child_articles.append(ArticleOverview(c, self.url))
# the tree is built
self.article_translations.sort(key=lambda a: a.priority, reverse=True)
self.overview_cards = "\n".join(a.get_overview_card() for a in self.article_translations)
def build(self):
# builds the tree structure to the dist directory
self.dist_path.mkdir(parents=True, exist_ok=True)
with Path(self.dist_path, "index.html").open("w") as f:
f.write(self.get_overview())
for at in self.article_translations:
at.build()
for ca in self.child_articles:
ca.build()
def _get_values(self) -> Dict[str, str]:
return {
"overview_cards": self.overview_cards,
"overview_slug": self.name,
}
def get_overview(self) -> str:
global TEMPLATE
return replace_values(TEMPLATE.overview, self._get_values())
def copy_static():
src = str(Path(SOURCE_DIRECTORY, "static"))
dst = str(Path(DIST_DIRECTORY, "static"))
if not os.path.exists(src):
logger.warn("The static folder '%s' wasn't defined.", src)
return
logger.info("copying static files from '%s' to '%r'", src, dst)
os.makedirs(dst, exist_ok=True)
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("."):
if c.name == "index.toml":
continue
src_file = os.path.join(root, file)
dest_file = os.path.join(dest_dir, file)
shutil.copy2(src_file, dest_file)
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")
logger.info("starting build process...")
copy_static()
tree = ArticleOverview(directory=Path(SOURCE_DIRECTORY, "pages"))
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,769 +0,0 @@
import typing
import typing_extensions
SOURCE_DIRECTORY = "src"
DIST_DIRECTORY = "dist"
# config template stuff
ARTICLE_PREVIEW_LENGTH = 200
# FOR DEVELOPMENT
CODE_DIRECTORY = "stsg"
# LANGUAGE INFORMATION
LANGUAGE_INFORMATION: typing.Dict[str, {
"flag": str,
"name": str,
"native_name": str,
"priority": typing_extensions.NotRequired[int],
}] = {
"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": "العربية (سوريا)",
"priority": 50,
},
"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",
"priority": 100,
},
"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",
"priority": 80,
},
"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)",
"priority": 80,
},
"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î",
"priority": 49,
},
"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",
"priority": 60,
},
"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"
}
}
DEFAULT_LANGUAGE = LANGUAGE_INFORMATION["de"]

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