Compare commits
119 Commits
e84828092c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4aa73b1aa | ||
|
|
73e89ca513 | ||
|
|
a1900a77e4 | ||
|
|
258d062ff4 | ||
|
|
8fee8d879e | ||
|
|
373cade0a7 | ||
|
|
5196913266 | ||
|
|
d3ff002901 | ||
|
|
08e7e343f5 | ||
|
|
433e2f6023 | ||
|
|
2690f0af87 | ||
|
|
f2a3d7ada4 | ||
|
|
753de66e08 | ||
|
|
036d5fb30a | ||
|
|
632f47e017 | ||
|
|
2a7ebaa298 | ||
|
|
d43d6505b1 | ||
|
|
e7558d996b | ||
|
|
15707ada59 | ||
|
|
54d1f0292e | ||
|
|
c33307e714 | ||
|
|
e3fe49bed7 | ||
|
|
693022ac23 | ||
|
|
9630eb07a1 | ||
|
|
a01f94379d | ||
|
|
6093e8df32 | ||
|
|
e82d841588 | ||
|
|
2ded0c7768 | ||
|
|
be51f463a1 | ||
|
|
4b5701e05b | ||
|
|
8874fb0935 | ||
|
|
f02d37a6af | ||
|
|
f84fb65aa7 | ||
|
|
d019884dbe | ||
|
|
39a8d7c1db | ||
| 7d1ceded8d | |||
| b3e23a53d9 | |||
| b2513f7caf | |||
| 4743456bd8 | |||
| 6994662bb4 | |||
| db23ceac78 | |||
| 9b030b24f4 | |||
| 44b651cace | |||
| eb2edc3710 | |||
| f63497090b | |||
| 215142dee4 | |||
| e391f64cc5 | |||
| 4216d153fd | |||
| 9241ecfee8 | |||
| 4bc7a6d980 | |||
| b50833f19f | |||
| f5cccc30dc | |||
| 8a8f02c5bd | |||
| 292d71edc5 | |||
| c9fb8fda93 | |||
| 1fae03e70b | |||
| 6ed94db8cf | |||
| 02a7c29dba | |||
| cf8e2955c2 | |||
| 7aa06b7f83 | |||
| f7a690405b | |||
| 93ea11cd0e | |||
| 263281df3c | |||
|
|
b953933e6f | ||
| 7db84492b5 | |||
| c7cd5e0601 | |||
| c05f8fad35 | |||
| b9c7535812 | |||
| cdcd3bce1f | |||
| 6eb02cc95f | |||
| 2454d26d6b | |||
| 8054a4a983 | |||
| afefce0a11 | |||
| 334c82d098 | |||
| ddd536c958 | |||
| ae5ba0d044 | |||
| 83133218a4 | |||
| d412d983bd | |||
| 9859229101 | |||
| dc8f9d91e7 | |||
| f3a86b8070 | |||
| b814434c48 | |||
| ba05ecdd94 | |||
| 1751158cbd | |||
| c45cebe497 | |||
| dc61a7ee92 | |||
| d70a0a8630 | |||
| 4f37283e68 | |||
| 0e948e7f55 | |||
| 79c95a9ddb | |||
| 0ca0fb90d6 | |||
| 603a7f5942 | |||
| 63f8541a82 | |||
| dc5af8da28 | |||
| 3fdbf13d95 | |||
| 7e4640e7d9 | |||
|
|
8239b3ea6d | ||
|
|
3f0049dfdb | ||
|
|
a113a13318 | ||
| 3926277f8f | |||
| 69b3efd78f | |||
| 2b80c90a19 | |||
| 37d28a3542 | |||
| 3e692dd74f | |||
| 3e6884ad8a | |||
| 8e57af5a78 | |||
| 37974c418b | |||
| 2dc1dfef98 | |||
| c36deab71a | |||
| 432f16ba08 | |||
| ee26af8b35 | |||
|
|
523402d3ad | ||
| d2d2628e56 | |||
| 7a3aec9872 | |||
| bfa9cb1527 | |||
| 4b307d7896 | |||
| b1a4d8e6ae | |||
| fa512cfb0e | |||
| 3b042b718f |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -174,3 +174,4 @@ cython_debug/
|
|||||||
.pypirc
|
.pypirc
|
||||||
|
|
||||||
dist
|
dist
|
||||||
|
*.json
|
||||||
|
|||||||
57
LICENSE
Normal file
57
LICENSE
Normal 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.
|
||||||
37
README.md
37
README.md
@@ -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
|
||||||
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,20 @@
|
|||||||
name = "stsg"
|
name = "stsg"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"watchdog~=6.0.0",
|
"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 = []
|
dynamic = []
|
||||||
authors = []
|
authors = []
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.8"
|
||||||
classifiers = []
|
classifiers = []
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
|
license-files = [
|
||||||
|
"LICENSE"
|
||||||
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
stsg = "stsg.__main__:build"
|
stsg = "stsg.__main__:build"
|
||||||
|
|||||||
9
src/articles/de.md
Normal file
9
src/articles/de.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# STSG
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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}}).
|
||||||
16
src/articles/documentation/de.md
Normal file
16
src/articles/documentation/de.md
Normal 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
|
||||||
|
```
|
||||||
16
src/articles/documentation/en.md
Normal file
16
src/articles/documentation/en.md
Normal 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
|
||||||
|
```
|
||||||
26
src/articles/documentation/get_started/de.md
Normal file
26
src/articles/documentation/get_started/de.md
Normal 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"
|
||||||
|
```
|
||||||
26
src/articles/documentation/get_started/en.md
Normal file
26
src/articles/documentation/get_started/en.md
Normal 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"
|
||||||
|
```
|
||||||
19
src/articles/documentation/installation/de.md
Normal file
19
src/articles/documentation/installation/de.md
Normal 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.
|
||||||
19
src/articles/documentation/installation/en.md
Normal file
19
src/articles/documentation/installation/en.md
Normal 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
9
src/articles/en.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# STSG
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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
2
src/articles/index.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
name="stsg"
|
||||||
|
iso_date="2024-04-15 13:45:12.123456"
|
||||||
57
src/articles/license/de.md
Normal file
57
src/articles/license/de.md
Normal 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.
|
||||||
57
src/articles/license/en.md
Normal file
57
src/articles/license/en.md
Normal 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.
|
||||||
1
src/articles/license/index.toml
Normal file
1
src/articles/license/index.toml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
author="Andrea Vos"
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
# foo
|
|
||||||
|
|
||||||
Lore ipsum dolor sit amend
|
|
||||||
|
|
||||||
- bar
|
|
||||||
- foobar
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -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. © 2025</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
BIN
src/static/assets/16.png
Normal file
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
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
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
BIN
src/static/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
88
src/static/assets/logo.svg
Normal file
88
src/static/assets/logo.svg
Normal 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
BIN
src/static/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
13
src/static/style.css
Normal file
13
src/static/style.css
Normal 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
122
src/templates/article.html
Normal 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}}. © {{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>
|
||||||
79
src/templates/article_translation.html
Normal file
79
src/templates/article_translation.html
Normal 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}}. © {{year}}</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
28
stsg.toml
Normal file
28
stsg.toml
Normal 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
|
||||||
800
stsg/__init__.py
800
stsg/__init__.py
@@ -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)
|
||||||
|
|||||||
@@ -5,9 +5,14 @@ import time
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from .config import SOURCE_DIRECTORY, CODE_DIRECTORY
|
|
||||||
|
from . import config
|
||||||
from .build import build as complete_build
|
from .build import build as complete_build
|
||||||
|
|
||||||
|
|
||||||
|
CODE_DIRECTORY = "stsg"
|
||||||
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger("stsg")
|
logger = logging.getLogger("stsg")
|
||||||
|
|
||||||
@@ -69,7 +74,7 @@ def build_on_change():
|
|||||||
build()
|
build()
|
||||||
|
|
||||||
observer = Observer()
|
observer = Observer()
|
||||||
observer.schedule(MarkdownChangeHandler(), path=SOURCE_DIRECTORY, recursive=True)
|
observer.schedule(MarkdownChangeHandler(), path=config.setup.source_directory, recursive=True)
|
||||||
observer.start()
|
observer.start()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -97,3 +102,4 @@ def hot_reload():
|
|||||||
observer.stop()
|
observer.stop()
|
||||||
|
|
||||||
observer.join()
|
observer.join()
|
||||||
|
|
||||||
|
|||||||
493
stsg/build.py
493
stsg/build.py
@@ -1,109 +1,466 @@
|
|||||||
|
from __future__ import annotations
|
||||||
import logging
|
import logging
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import os
|
import os
|
||||||
import markdown
|
from markdown2 import markdown
|
||||||
from typing import Optional
|
from typing import Optional, Union, Dict, Generator, List, DefaultDict, Any, TypedDict, Set
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
from .config import SOURCE_DIRECTORY, DIST_DIRECTORY, STATIC_DIRECTORY
|
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):
|
def shorten_text_and_clean(html_string, max_length=config.formatting.preview_length):
|
||||||
if not os.path.exists(src):
|
soup = BeautifulSoup(html_string, 'html.parser')
|
||||||
logger.warn("The static folder '%s' wasn't defined.", src)
|
|
||||||
|
# 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 shift_headings(html_string, header_shift=config.formatting.preview_header_shift):
|
||||||
|
soup = BeautifulSoup(html_string, 'html.parser')
|
||||||
|
|
||||||
|
for level in range(6, 0, -1): # Start from h6 to h1 to avoid overwriting
|
||||||
|
old_tag = f'h{level}'
|
||||||
|
for tag in soup.find_all(old_tag):
|
||||||
|
new_level = min(level + header_shift, 6) # Cap at h6
|
||||||
|
new_tag = f'h{new_level}'
|
||||||
|
tag.name = new_tag
|
||||||
|
|
||||||
|
return str(soup)
|
||||||
|
|
||||||
|
|
||||||
|
def get_preview_text(html_string: str):
|
||||||
|
return shift_headings(shorten_text_and_clean(html_string))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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
|
return
|
||||||
|
|
||||||
logger.info("copying static files from '%s' to '%r'", src, dst)
|
self.used_slugs.add(a.slug)
|
||||||
|
self.data.append(a)
|
||||||
|
|
||||||
os.makedirs(dst, exist_ok=True)
|
def extend(self, other):
|
||||||
|
for a in other:
|
||||||
|
self.append(a)
|
||||||
|
|
||||||
for root, dirs, files in os.walk(src):
|
def get_translated(self, language_code: str) -> ArticleList[Union[ArticleTranslation, Article]]:
|
||||||
if any(p.startswith(".") for p in Path(root).parts):
|
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
|
continue
|
||||||
|
|
||||||
# Compute relative path from the source root
|
res.append(a.article_translations_map.get(language_code, a))
|
||||||
rel_path = os.path.relpath(root, src)
|
|
||||||
dest_dir = os.path.join(dst, rel_path)
|
|
||||||
|
|
||||||
os.makedirs(dest_dir, exist_ok=True)
|
return res
|
||||||
|
|
||||||
for file in files:
|
@property
|
||||||
if file.startswith("."):
|
def context(self) -> List[Union[ArticleContext, ArticleTranslationContext]]:
|
||||||
continue
|
return [a.context for a in self]
|
||||||
|
|
||||||
src_file = os.path.join(root, file)
|
|
||||||
dest_file = os.path.join(dest_dir, file)
|
|
||||||
shutil.copy2(src_file, dest_file)
|
|
||||||
|
|
||||||
|
|
||||||
class Context:
|
class ArticleTranslation:
|
||||||
def __init__(self, root: str = SOURCE_DIRECTORY):
|
article: Article
|
||||||
self.file = None
|
slug: str = property(fget=lambda self: self.article.slug)
|
||||||
|
file: Path
|
||||||
|
|
||||||
current_root = Path(root)
|
@cached_property
|
||||||
while current_root.parts and self.file is None:
|
def html_content(self) -> str:
|
||||||
current_file = Path(current_root, "index.html")
|
html_content = self.file.read_text()
|
||||||
if current_file.exists() and current_file.is_file:
|
if self.file.suffix == ".md":
|
||||||
self.file = current_file
|
return markdown(html_content, extras=config.formatting.markdown_extras)
|
||||||
|
return html_content
|
||||||
|
|
||||||
current_root = current_root.parent
|
@cached_property
|
||||||
|
def language_code(self) -> str:
|
||||||
|
language_code = self.file.stem.lower().replace("-", "_")
|
||||||
|
|
||||||
if self.file is None:
|
if language_code in config.languages:
|
||||||
logger.error("couldn't find context for %s", root)
|
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)
|
exit(1)
|
||||||
logger.info("%s found context %r", root, str(self.file))
|
|
||||||
|
|
||||||
def get_text(self, **placeholder_values: dict):
|
@cached_property
|
||||||
text = self.file.read_text()
|
def priority(self) -> int:
|
||||||
|
return LANGUAGES[self.language_code]["priority"]
|
||||||
|
|
||||||
for key, value in placeholder_values.items():
|
@cached_property
|
||||||
text = text.replace(f"<{key}/>", value)
|
def slug_path(self) -> List[str]:
|
||||||
text = text.replace(f"<{key} />", value)
|
return [self.language_code, *self.article.slug_path]
|
||||||
|
|
||||||
return text
|
@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(TEMPLATE["article_translation"].render(self.context))
|
||||||
|
|
||||||
|
|
||||||
def convert_md(src: Path, dst: Path, context: Optional[Context] = None):
|
class Article:
|
||||||
html_content = markdown.markdown(src.read_text())
|
directory: Path
|
||||||
context = Context(str(src.parent))
|
|
||||||
full_page = context.get_text(content=html_content)
|
|
||||||
|
|
||||||
folder_dst = dst.parent / dst.name.replace(".md", "")
|
@cached_property
|
||||||
folder_dst.mkdir(parents=True, exist_ok=True)
|
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
|
||||||
|
|
||||||
with Path(folder_dst, "index.html").open("w") as f:
|
@cached_property
|
||||||
f.write(full_page)
|
def name(self) -> str:
|
||||||
|
return self.config.get("name", self.slug)
|
||||||
|
|
||||||
def walk_directory(root):
|
article_path: List[Article]
|
||||||
src_path = Path(SOURCE_DIRECTORY, root)
|
|
||||||
dst_path = Path(DIST_DIRECTORY, root)
|
|
||||||
|
|
||||||
language_codes_found = []
|
@cached_property
|
||||||
|
def slug_path(self) -> List[str]:
|
||||||
|
return [a.slug for a in self.article_path[1:]]
|
||||||
|
|
||||||
for current_full_path in src_path.iterdir():
|
@cached_property
|
||||||
current_name = Path(current_full_path).name
|
def url(self) -> str:
|
||||||
current_dst = Path(dst_path, current_name)
|
return "/" + "/".join(self.slug_path)
|
||||||
current_src = Path(src_path, current_name)
|
|
||||||
|
|
||||||
if current_name == "static":
|
@cached_property
|
||||||
copy_static(current_src, current_dst)
|
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
|
continue
|
||||||
|
|
||||||
if current_name.endswith(".md"):
|
if c.is_file():
|
||||||
convert_md(current_src, current_dst)
|
at = ArticleTranslation(c, self)
|
||||||
language_codes_found.append(current_name.replace(".md", ""))
|
self.article_translations_list.append(at)
|
||||||
continue
|
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,
|
||||||
|
))
|
||||||
|
|
||||||
if current_src.is_dir():
|
self.article_translations_list.sort(key=lambda a: a.priority, reverse=True)
|
||||||
walk_directory(Path(root, current_full_path.name))
|
|
||||||
|
|
||||||
print(language_codes_found)
|
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():
|
def build():
|
||||||
logger.info("building static page")
|
logger.info("starting build process...")
|
||||||
walk_directory("")
|
|
||||||
|
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()
|
||||||
|
|||||||
@@ -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
62
stsg/definitions.py
Normal 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]]
|
||||||
Reference in New Issue
Block a user