Compare commits
27 Commits
2ded0c7768
...
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 |
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.
|
||||
25
README.md
25
README.md
@@ -1,4 +1,15 @@
|
||||
# Installing
|
||||
<a href="https://oql.avris.it/license/v1.2" target="_blank" rel="noopener"><img src="https://badgers.space/badge/License/OQL/pink" alt="License: OQL" style="vertical-align: middle;"/></a>
|
||||
|
||||
# STSG
|
||||
|
||||
## Planned features
|
||||
|
||||
- [ ] auto uploading to ftp and other online services using a plugin system
|
||||
- [ ] build a git wiki in a specified languages instead of a static website
|
||||
- [ ] hosting the documentation somehow
|
||||
- [ ] multiple templates + a way to choose templates
|
||||
|
||||
## Installing
|
||||
|
||||
```sh
|
||||
python3 -m venv .venv
|
||||
@@ -6,7 +17,7 @@ source .venv/bin/activate
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
# Execute
|
||||
## Execute
|
||||
|
||||
To start a local http server in the dist folder you can simply do:
|
||||
|
||||
@@ -24,13 +35,3 @@ stsg
|
||||
stsg_dev
|
||||
```
|
||||
|
||||
# build favicon
|
||||
|
||||
```
|
||||
cd src/static/assets
|
||||
inkscape -w 16 -h 16 -o 16.png logo.svg
|
||||
inkscape -w 32 -h 32 -o 32.png logo.svg
|
||||
inkscape -w 48 -h 48 -o 48.png logo.svg
|
||||
|
||||
convert 16.png 32.png 48.png ../icon.ico
|
||||
```
|
||||
|
||||
@@ -2,17 +2,20 @@
|
||||
name = "stsg"
|
||||
dependencies = [
|
||||
"watchdog~=6.0.0",
|
||||
"markdown~=3.3.6",
|
||||
"markdown2~=2.5.3",
|
||||
"bs4~=0.0.2",
|
||||
"toml~0.10.2",
|
||||
"toml~=0.10.2",
|
||||
"jinja2~=3.1.6",
|
||||
]
|
||||
dynamic = []
|
||||
authors = []
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
requires-python = ">=3.8"
|
||||
classifiers = []
|
||||
version = "0.0.0"
|
||||
license-files = [
|
||||
"LICENSE"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
stsg = "stsg.__main__:build"
|
||||
|
||||
@@ -6,4 +6,4 @@ Dies ist ein Static Site Generator mit fokus auf Nutzer:innenfreundlichkeit und
|
||||
|
||||
Die Templates sind komplet anpassbar, trotzdem empfehle ich [Bulma](https://bulma.io/) als CSS-Framework zu verwenden.
|
||||
|
||||
Polizei und das Millitär jeglichen Staates dürfen dieses Tool nicht verwenden.
|
||||
Polizei und das Millitär jeglichen Staates dürfen dieses Tool nicht verwenden. Für mehr Info zur Nutzung lest [die Lizenz]({{license.url}}).
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
# Dokumentation
|
||||
|
||||
Dies ist die Dokumentation von STSG.
|
||||
|
||||
## Favicon bauen
|
||||
|
||||
Dies ist einfach nur ein hilfreiches Skript um favicons zu erstellen. Dies ist nicht unbedingt notwendig. Aber bei verwendung muss inkscape und imagemagick installiert sein.
|
||||
|
||||
```
|
||||
cd src/static/assets
|
||||
inkscape -w 16 -h 16 -o 16.png logo.svg
|
||||
inkscape -w 32 -h 32 -o 32.png logo.svg
|
||||
inkscape -w 48 -h 48 -o 48.png logo.svg
|
||||
|
||||
convert 16.png 32.png 48.png ../icon.ico
|
||||
```
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
# documentation
|
||||
|
||||
This is the documentation for stsg.
|
||||
|
||||
## build favicon
|
||||
|
||||
This is just a helpfull script to build the favicon. It is not strictly required to use. Make sure inkscape and imagemagick is installed.
|
||||
|
||||
```
|
||||
cd src/static/assets
|
||||
inkscape -w 16 -h 16 -o 16.png logo.svg
|
||||
inkscape -w 32 -h 32 -o 32.png logo.svg
|
||||
inkscape -w 48 -h 48 -o 48.png logo.svg
|
||||
|
||||
convert 16.png 32.png 48.png ../icon.ico
|
||||
```
|
||||
|
||||
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`.
|
||||
@@ -6,4 +6,4 @@ This is a static-site-generator with focus on ease of use and making accessible
|
||||
|
||||
The templates are completely customizable, but I still reccomend using [bulma](https://bulma.io/) as css-framework.
|
||||
|
||||
Cops and Millitary of any state are stricly prohibited from using this tool.
|
||||
Cops and Millitary of any state are stricly prohibited from using this tool. For more information about the usage of that tool, read [our license]({{license.url}}).
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
name="stsg"
|
||||
datetime="2024-04-15 13:45:12.123456"
|
||||
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"
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/x-icon" href="/static/icon.ico">
|
||||
<title>{{slug}}</title>
|
||||
<title>{{name}}</title>
|
||||
<link rel="stylesheet" href="/static/bulma.min.css" />
|
||||
<link rel="stylesheet" href="/static/style.css" />
|
||||
</head>
|
||||
@@ -16,11 +16,24 @@
|
||||
>
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="#">
|
||||
<strong>Static Translated Site Generator</strong>
|
||||
<strong>{{name}}</strong>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{% if breadcrumbs|length %}
|
||||
<!-- Breadcrumbs Section -->
|
||||
<nav class="breadcrumb is-small has-succeeds-separator p-3" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
{% for b in breadcrumbs %}
|
||||
<li class="{{'is-active' if b.slug == slug}}">
|
||||
<a {{'aria-current="page"' if b.slug == slug}} href="{{b.url}}">{{b.name}}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
<section class="section">
|
||||
{% if translations|length %}
|
||||
<div class="container content">
|
||||
@@ -33,7 +46,7 @@
|
||||
<div class="card mb-4" lang="{{t.language.code}}" style="height: 100%;">
|
||||
<a href="{{t.url}}" hreflang="{{t.language.code}}" class="card mb-4" style="color: inherit; text-decoration: none;">
|
||||
<div class="card-content">
|
||||
<p class="title">{{t.language.flag}} {{t.title}}</p>
|
||||
<p class="title">{{t.language.flag}} {{t.name}}</p>
|
||||
<hr />
|
||||
<p class="content">
|
||||
{{t.preview}}
|
||||
@@ -41,7 +54,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<time class="card-footer-item" datetime="{{iso_date}}">{{date}}</time>
|
||||
<time class="card-footer-item" datetime="{{t.iso_date}}">{{t.date}}</time>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@@ -62,15 +75,15 @@
|
||||
<div class="card mb-4" >
|
||||
<a href="{{c.url}}" class="card mb-4" style="color: inherit; text-decoration: none;">
|
||||
<div class="card-content">
|
||||
<p class="title">{{c.slug}} </p>
|
||||
<p class="title">{{c.name}} </p>
|
||||
<hr />
|
||||
<p class="content is-flex is-flex-direction-column" style="gap: 10px;">
|
||||
{% for ct in c.translations %}
|
||||
<a href="{{ct.url}}" hreflang="{{ct.language.code}}">{{ct.language.flag}}: {{ct.title}}</a>
|
||||
<a href="{{ct.url}}" hreflang="{{ct.language.code}}">{{ct.language.flag}}: {{ct.name}}</a>
|
||||
{% endfor %}
|
||||
</p>
|
||||
<hr />
|
||||
<time datetime="{{iso_date}}">{{date}}</time>
|
||||
<time datetime="{{c.iso_date}}">{{c.date}}</time>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@@ -83,7 +96,7 @@
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="content has-text-centered">
|
||||
<p><strong>STSG</strong> by Hazel. © 2025</p>
|
||||
<p><strong>{{name}}</strong> by {{author}}. © {{year}}</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/x-icon" href="/static/icon.ico">
|
||||
<title>{{title}}</title>
|
||||
<title>{{name}}</title>
|
||||
<link rel="stylesheet" href="/static/bulma.min.css" />
|
||||
<link rel="stylesheet" href="/static/style.css" />
|
||||
</head>
|
||||
@@ -18,11 +18,24 @@
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="{{article_url}}">
|
||||
<strong>{{language.flag}} {{title}}</strong>
|
||||
<time datetime="{{meta.iso_date}}">{{meta.date}}</time>
|
||||
<time datetime="{{iso_date}}">{{date}}</time>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{% if breadcrumbs|length %}
|
||||
<!-- Breadcrumbs Section -->
|
||||
<nav class="breadcrumb is-small has-succeeds-separator p-3" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
{% for b in breadcrumbs %}
|
||||
<li class="{{'is-active' if b.slug == slug}}">
|
||||
<a {{'aria-current="page"' if b.slug == slug}} href="{{b.url}}">{{b.name}}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
<!-- Main Content -->
|
||||
<section class="section">
|
||||
<div class="content">
|
||||
@@ -30,24 +43,22 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if children|length %}
|
||||
{% if related|length %}
|
||||
<section class="section">
|
||||
<div class="content">
|
||||
<div class="columns is-multiline">
|
||||
{% for c in children %}
|
||||
{% for c in related %}
|
||||
<div class="column is-half">
|
||||
<div class="card mb-4" >
|
||||
<a href="{{c.url}}" hreflang="{{c.language.code}}" class="card mb-4" style="color: inherit; text-decoration: none;">
|
||||
<div class="card-content">
|
||||
<p class="title">{{c.title}}</p>
|
||||
<hr />
|
||||
<p class="content">
|
||||
{{c.preview}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<time class="card-footer-item" datetime="{{c.meta.iso_date}}">{{c.meta.date}}</time>
|
||||
<time class="card-footer-item" datetime="{{c.iso_date}}">{{c.date}}</time>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@@ -61,7 +72,7 @@
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="content has-text-centered">
|
||||
<p><strong>STSG</strong> by Hazel. © 2025</p>
|
||||
<p><strong>{{name}}</strong> by {{author}}. © {{year}}</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
fall_back_to_overview_in_translation = false
|
||||
|
||||
[setup]
|
||||
source_directory = "src"
|
||||
dist_directory = "dist"
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
class config:
|
||||
default_author = "anonymous"
|
||||
fall_back_to_overview_in_translation = True
|
||||
|
||||
class setup:
|
||||
source_directory = "src"
|
||||
dist_directory = "dist"
|
||||
@@ -8,6 +11,9 @@ class config:
|
||||
fallback_language = "en"
|
||||
preview_length = 400
|
||||
preview_header_shift = 2
|
||||
markdown_extras = [
|
||||
"fenced-code-blocks"
|
||||
]
|
||||
|
||||
languages = {
|
||||
"af": {
|
||||
|
||||
354
stsg/build.py
354
stsg/build.py
@@ -3,25 +3,19 @@ import logging
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
import os
|
||||
import markdown
|
||||
from markdown2 import markdown
|
||||
from typing import Optional, Union, Dict, Generator, List, DefaultDict, Any, TypedDict, Set
|
||||
from bs4 import BeautifulSoup
|
||||
from collections import defaultdict
|
||||
from collections import defaultdict, UserList
|
||||
import toml
|
||||
from datetime import datetime
|
||||
import jinja2
|
||||
from functools import cached_property
|
||||
|
||||
|
||||
from .definitions import *
|
||||
from . import config
|
||||
|
||||
def get_first_header_content(content, fallback: str = ""):
|
||||
soup = BeautifulSoup(content, 'html.parser')
|
||||
for level in range(1, 7):
|
||||
header = soup.find(f'h{level}')
|
||||
if header:
|
||||
return header.get_text(strip=True)
|
||||
|
||||
return fallback
|
||||
|
||||
|
||||
def shorten_text_and_clean(html_string, max_length=config.formatting.preview_length):
|
||||
soup = BeautifulSoup(html_string, 'html.parser')
|
||||
@@ -76,20 +70,7 @@ def shift_headings(html_string, header_shift=config.formatting.preview_header_sh
|
||||
|
||||
def get_preview_text(html_string: str):
|
||||
return shift_headings(shorten_text_and_clean(html_string))
|
||||
|
||||
|
||||
def stem_to_language_code(stem: str) -> str:
|
||||
language_code = stem.lower().replace("-", "_")
|
||||
|
||||
if language_code in config.languages:
|
||||
return language_code
|
||||
|
||||
language_code = language_code.split("_")[0]
|
||||
if language_code in config.languages:
|
||||
return language_code
|
||||
|
||||
logger.error("Didn't recognize %s as a valid language code, add it to the config, or fix your structure.", stem)
|
||||
exit(1)
|
||||
|
||||
|
||||
class TemplateDict(dict):
|
||||
@@ -130,64 +111,166 @@ class LanguageDict(dict):
|
||||
LANGUAGES = LanguageDict()
|
||||
|
||||
|
||||
def compile_cross_article_context(cross_article_context):
|
||||
title = cross_article_context["title"]
|
||||
url = cross_article_context["url"]
|
||||
def add_html_link(c):
|
||||
name = c["name"]
|
||||
url = c["url"]
|
||||
|
||||
cross_article_context["link"] = f'<a href="{url}">{title}</a>'
|
||||
c["link"] = f'<a href="{url}">{name}</a>'
|
||||
|
||||
|
||||
def get_translated_articles(articles: List[Article], language_code: str = None) -> List[Union[ArticleTranslation, Article]]:
|
||||
result = {}
|
||||
|
||||
for a in articles:
|
||||
if a.slug in result:
|
||||
continue
|
||||
|
||||
if language_code is None:
|
||||
result[a.slug] = a
|
||||
continue
|
||||
|
||||
if not config.fall_back_to_overview_in_translation and language_code not in a.article_translations_map:
|
||||
continue
|
||||
|
||||
result[a.slug] = a.article_translations_map.get(language_code, a)
|
||||
|
||||
|
||||
class ArticleList(UserList):
|
||||
def __init__(self, iterable):
|
||||
super().__init__(item for item in iterable)
|
||||
|
||||
self.used_slugs = set()
|
||||
|
||||
def append(self, a: Union[Article, str]):
|
||||
if isinstance(a, str):
|
||||
a = ARTICLE_LAKE[a]
|
||||
|
||||
if a.slug in self.used_slugs:
|
||||
return
|
||||
|
||||
self.used_slugs.add(a.slug)
|
||||
self.data.append(a)
|
||||
|
||||
def extend(self, other):
|
||||
for a in other:
|
||||
self.append(a)
|
||||
|
||||
def get_translated(self, language_code: str) -> ArticleList[Union[ArticleTranslation, Article]]:
|
||||
res = ArticleList([])
|
||||
|
||||
for a in self:
|
||||
if not config.fall_back_to_overview_in_translation and language_code not in a.article_translations_map:
|
||||
continue
|
||||
|
||||
res.append(a.article_translations_map.get(language_code, a))
|
||||
|
||||
return res
|
||||
|
||||
@property
|
||||
def context(self) -> List[Union[ArticleContext, ArticleTranslationContext]]:
|
||||
return [a.context for a in self]
|
||||
|
||||
|
||||
class ArticleTranslation:
|
||||
def __init__(self, file: Path, article: Article):
|
||||
self.file = file
|
||||
self.article = article
|
||||
article: Article
|
||||
slug: str = property(fget=lambda self: self.article.slug)
|
||||
file: Path
|
||||
|
||||
self.context: Dict[str, Any] = {}
|
||||
|
||||
# initializing the location of the article translation
|
||||
self.language_code = stem_to_language_code(self.file.stem)
|
||||
self.location_in_tree = [self.language_code, *self.article.location_in_tree]
|
||||
self.url = "/" + "/".join(self.location_in_tree)
|
||||
self.dist_path = Path(config.setup.dist_directory, *self.location_in_tree)
|
||||
self.cross_article_context = TRANSLATED_CROSS_ARTICLE_CONTEXT[self.language_code][self.article.slug] = {}
|
||||
|
||||
|
||||
self.priority = LANGUAGES[self.language_code]["priority"]
|
||||
self.real_language_code = LANGUAGES[self.language_code]["code"]
|
||||
|
||||
self.html_content = self.file.read_text()
|
||||
@cached_property
|
||||
def html_content(self) -> str:
|
||||
html_content = self.file.read_text()
|
||||
if self.file.suffix == ".md":
|
||||
self.html_content = markdown.markdown(self.html_content)
|
||||
return markdown(html_content, extras=config.formatting.markdown_extras)
|
||||
return html_content
|
||||
|
||||
@cached_property
|
||||
def language_code(self) -> str:
|
||||
language_code = self.file.stem.lower().replace("-", "_")
|
||||
|
||||
if language_code in config.languages:
|
||||
return language_code
|
||||
|
||||
language_code = language_code.split("_")[0]
|
||||
if language_code in config.languages:
|
||||
return language_code
|
||||
|
||||
logger.error("Didn't recognize %s as a valid language code, add it to the config, or fix your structure.", stem)
|
||||
exit(1)
|
||||
|
||||
@cached_property
|
||||
def priority(self) -> int:
|
||||
return LANGUAGES[self.language_code]["priority"]
|
||||
|
||||
@cached_property
|
||||
def slug_path(self) -> List[str]:
|
||||
return [self.language_code, *self.article.slug_path]
|
||||
|
||||
@cached_property
|
||||
def url(self) -> str:
|
||||
return "/" + "/".join(self.slug_path)
|
||||
|
||||
@cached_property
|
||||
def dist_path(self) -> Path:
|
||||
return Path(config.setup.dist_directory, *self.slug_path)
|
||||
|
||||
context: ArticleTranslationContext
|
||||
cross_article_context: Dict[str, Any]
|
||||
|
||||
def __init__(self, file: Path, article: Article):
|
||||
self.article = article
|
||||
self.file = file
|
||||
|
||||
self.context = TRANSLATED_CROSS_ARTICLE_CONTEXT[self.language_code][self.article.slug] = {}
|
||||
|
||||
@cached_property
|
||||
def name(self) -> str:
|
||||
soup = BeautifulSoup(self.html_content, 'html.parser')
|
||||
for level in range(1, 7):
|
||||
header = soup.find(f'h{level}')
|
||||
if header:
|
||||
return header.get_text(strip=True)
|
||||
|
||||
return self.article.name
|
||||
|
||||
def __init_context__(self):
|
||||
self.context["meta"] = self.article.context_shared
|
||||
self.context["slug"] = self.article.slug
|
||||
self.context["name"] = self.name
|
||||
self.context["url"] = self.url
|
||||
add_html_link(self.context)
|
||||
self.context["date"] = self.article.modified_at.strftime(config.formatting.datetime_format)
|
||||
self.context["year"] = str(self.article.modified_at.year)
|
||||
self.context["iso_date"] = self.article.modified_at.isoformat()
|
||||
self.context["author"] = self.article.author
|
||||
|
||||
self.context["language"] = LANGUAGES[self.language_code]
|
||||
self.context["article_url"] = self.article.url
|
||||
self.context["title"] = get_first_header_content(self.html_content, fallback=LANGUAGES[self.language_code]["native_name"])
|
||||
|
||||
self.cross_article_context.update(self.article.context_shared)
|
||||
self.cross_article_context["title"] = self.context["title"]
|
||||
self.cross_article_context["article_url"] = self.article.url
|
||||
self.cross_article_context["url"] = self.url
|
||||
compile_cross_article_context(self.cross_article_context)
|
||||
|
||||
# get children
|
||||
self.context["children"] = [
|
||||
c.article_translations_map[self.language_code].context for c in self.article.child_articles
|
||||
if self.language_code in c.article_translations_map
|
||||
]
|
||||
self.context["children"] = self.article.child_articles.get_translated(self.language_code).context
|
||||
self.context["breadcrumbs"] = ArticleList(self.article.article_path).get_translated(self.language_code).context
|
||||
|
||||
def __init_content_context__(self):
|
||||
template = jinja2.Template(self.html_content)
|
||||
template.environment.accessed_keys = []
|
||||
template.environment.context_class = ContextDict
|
||||
|
||||
self.html_content = template.render({
|
||||
**CROSS_ARTICLE_CONTEXT,
|
||||
**TRANSLATED_CROSS_ARTICLE_CONTEXT[self.language_code],
|
||||
})
|
||||
|
||||
template.environment.context_class = jinja2.runtime.Context
|
||||
accessed_keys = template.environment.accessed_keys
|
||||
|
||||
for key in accessed_keys:
|
||||
self.article.linked_articles.append(key)
|
||||
|
||||
self.context["content"] = self.html_content
|
||||
self.context["preview"] = get_preview_text(html_string=self.html_content)
|
||||
|
||||
self.context["linked"] = self.article.linked_articles.get_translated(self.language_code).context
|
||||
self.context["related"] = self.article.related_articles.get_translated(self.language_code).context
|
||||
|
||||
def build(self):
|
||||
self.dist_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -196,36 +279,69 @@ class ArticleTranslation:
|
||||
|
||||
|
||||
class Article:
|
||||
def __init__(self, directory: Path, location_in_tree: Optional[List[str]] = None, is_root: bool = False, parent: Optional[Article] = None):
|
||||
directory: Path
|
||||
|
||||
@cached_property
|
||||
def config(self) -> ArticleConfig:
|
||||
config_file = self.directory / "index.toml"
|
||||
return toml.load(config_file) if config_file.exists() else {}
|
||||
|
||||
@cached_property
|
||||
def slug(self) -> str:
|
||||
slug = self.config.get("name", self.directory.name)
|
||||
if slug in ARTICLE_LAKE:
|
||||
logger.error("two articles have the same name at %s and %r", ARTICLE_LAKE[slug].directory, self.directory)
|
||||
exit(1)
|
||||
return slug
|
||||
|
||||
@cached_property
|
||||
def name(self) -> str:
|
||||
return self.config.get("name", self.slug)
|
||||
|
||||
article_path: List[Article]
|
||||
|
||||
@cached_property
|
||||
def slug_path(self) -> List[str]:
|
||||
return [a.slug for a in self.article_path[1:]]
|
||||
|
||||
@cached_property
|
||||
def url(self) -> str:
|
||||
return "/" + "/".join(self.slug_path)
|
||||
|
||||
@cached_property
|
||||
def dist_path(self) -> Path:
|
||||
return Path(config.setup.dist_directory, *self.slug_path)
|
||||
|
||||
context: ArticleContext
|
||||
|
||||
child_articles: ArticleList[Article]
|
||||
article_translations_list: List[ArticleTranslation]
|
||||
article_translations_map: Dict[str, ArticleTranslation]
|
||||
|
||||
linked_articles: ArticleList[Article]
|
||||
|
||||
@cached_property
|
||||
def related_articles(self) -> ArticleList[Article]:
|
||||
res = ArticleList(self.child_articles)
|
||||
res.extend(self.linked_articles)
|
||||
return res
|
||||
|
||||
def __init__(self, directory: Path, article_path: Optional[List[str]] = None, is_root: bool = False, parent: Optional[Article] = None):
|
||||
self.directory = directory
|
||||
|
||||
self.context: Dict[str, Any] = {}
|
||||
self.context_shared: Dict[str, Any] = {}
|
||||
if parent is not None:
|
||||
self.context["parent"] = parent.context_shared
|
||||
self.article_path: List[Article] = article_path or []
|
||||
self.article_path.append(self)
|
||||
|
||||
# initializing the config values of the article
|
||||
config_file = self.directory / "index.toml"
|
||||
self.config = toml.load(config_file) if config_file.exists() else {}
|
||||
|
||||
# initializing the location and slug of the article
|
||||
self.slug = self.config.get("name", self.directory.name)
|
||||
if self.slug in ARTICLE_LAKE:
|
||||
logger.error("two articles have the same name at %s and %r", ARTICLE_LAKE[self.slug].directory, self.directory)
|
||||
exit(1)
|
||||
self.cross_article_context = CROSS_ARTICLE_CONTEXT[self.slug] = {}
|
||||
self.context = CROSS_ARTICLE_CONTEXT[self.slug] = {}
|
||||
|
||||
ARTICLE_LAKE[self.slug] = self
|
||||
|
||||
self.location_in_tree: List[str] = location_in_tree or []
|
||||
if not is_root:
|
||||
self.location_in_tree.append(self.slug)
|
||||
self.url = "/" + "/".join(self.location_in_tree)
|
||||
self.dist_path = Path(config.setup.dist_directory, *self.location_in_tree)
|
||||
|
||||
self.linked_articles = ArticleList([])
|
||||
|
||||
# build the tree
|
||||
self.child_articles: List[Article] = []
|
||||
self.article_translations_list: List[ArticleTranslation] = []
|
||||
self.article_translations_map: Dict[str, ArticleTranslation] = {}
|
||||
self.child_articles = ArticleList([])
|
||||
self.article_translations_list = []
|
||||
self.article_translations_map = {}
|
||||
|
||||
for c in self.directory.iterdir():
|
||||
if c.name == "index.toml":
|
||||
@@ -238,49 +354,60 @@ class Article:
|
||||
elif c.is_dir():
|
||||
self.child_articles.append(Article(
|
||||
directory=c,
|
||||
location_in_tree=self.location_in_tree.copy(),
|
||||
article_path=self.article_path.copy(),
|
||||
parent=self,
|
||||
))
|
||||
|
||||
self.article_translations_list.sort(key=lambda a: a.priority, reverse=True)
|
||||
|
||||
logger.info("found %s at %s with the translations %s", self.slug, ".".join(list(self.location_in_tree)), ",".join(self.article_translations_map.keys()))
|
||||
logger.info("found %s at %s with the translations %s", self.slug, ".".join(list(self.slug_path)), ",".join(self.article_translations_map.keys()))
|
||||
|
||||
@cached_property
|
||||
def modified_at(self) -> datetime:
|
||||
if "iso_date" in self.config:
|
||||
return datetime.fromisoformat(self.config["iso_date"])
|
||||
|
||||
"""
|
||||
TODO
|
||||
scann every article file and use the youngest article file
|
||||
"""
|
||||
|
||||
return datetime.fromtimestamp(self.directory.stat().st_mtime)
|
||||
|
||||
@cached_property
|
||||
def author(self) -> str:
|
||||
return self.config.get("author", config.default_author)
|
||||
|
||||
def __init_context__(self):
|
||||
self.context_shared["url"] = self.url
|
||||
self.context_shared["slug"] = self.slug
|
||||
|
||||
modified_at = datetime.fromisoformat(self.config["datetime"]) if "datetime" in self.config else datetime.fromtimestamp(self.directory.stat().st_mtime)
|
||||
self.context_shared["date"] = modified_at.strftime(config.formatting.datetime_format)
|
||||
self.context_shared["iso_date"] = modified_at.isoformat()
|
||||
|
||||
self.context.update(self.context_shared)
|
||||
|
||||
self.cross_article_context.update(self.context_shared)
|
||||
self.cross_article_context["title"] = self.context_shared["slug"]
|
||||
self.cross_article_context["article_url"] = self.context_shared["url"]
|
||||
compile_cross_article_context(self.cross_article_context)
|
||||
self.context["slug"] = self.slug
|
||||
self.context["name"] = self.name
|
||||
self.context["url"] = self.url
|
||||
add_html_link(self.context)
|
||||
self.context["date"] = self.modified_at.strftime(config.formatting.datetime_format)
|
||||
self.context["year"] = str(self.modified_at.year)
|
||||
self.context["iso_date"] = self.modified_at.isoformat()
|
||||
self.context["author"] = self.author
|
||||
|
||||
# recursive context structures
|
||||
translation_list = self.context["translations"] = []
|
||||
child_article_list = self.context["children"] = []
|
||||
self.context["translations"] = [c.context for c in self.article_translations_list]
|
||||
self.context["children"] = self.child_articles.context
|
||||
self.context["breadcrumbs"] = [b.context for b in self.article_path]
|
||||
for lang, article in self.article_translations_map.items():
|
||||
self.context[lang] = article.context
|
||||
|
||||
for article_translation in self.article_translations_list:
|
||||
self.context[article_translation.real_language_code] = article_translation.context
|
||||
translation_list.append(article_translation.context)
|
||||
|
||||
for child_article in self.child_articles:
|
||||
child_article_list.append(child_article.context)
|
||||
|
||||
# recursively build context
|
||||
for at in self.article_translations_list:
|
||||
at.__init_context__()
|
||||
|
||||
for a in self.child_articles:
|
||||
a.__init_context__()
|
||||
|
||||
def __init_content_context__(self):
|
||||
for at in self.article_translations_list:
|
||||
at.__init_content_context__()
|
||||
|
||||
self.context["linked"] = self.linked_articles.context
|
||||
self.context["related"] = self.related_articles.context
|
||||
|
||||
for a in self.child_articles:
|
||||
a.__init_content_context__()
|
||||
|
||||
@@ -297,11 +424,18 @@ class Article:
|
||||
ac.build()
|
||||
|
||||
|
||||
class ContextDict(jinja2.runtime.Context):
|
||||
def resolve_or_missing(self, key: str) -> Any:
|
||||
self.environment.accessed_keys.append(key)
|
||||
return super().resolve_or_missing(key)
|
||||
|
||||
|
||||
|
||||
# GLOBALS
|
||||
logger = logging.getLogger("stsg.build")
|
||||
ARTICLE_LAKE: Dict[str, Article] = {}
|
||||
CROSS_ARTICLE_CONTEXT: Dict[str, Dict[str, Any]] = {}
|
||||
TRANSLATED_CROSS_ARTICLE_CONTEXT: Dict[str, Dict[str, Dict[str, Any]]] = defaultdict(dict)
|
||||
ARTICLE_LAKE: Dict[str, Article] = {}
|
||||
ARTICLE_REFERENCE_VALUES: DefaultDict[str, Dict[str, str]] = defaultdict(dict)
|
||||
|
||||
|
||||
@@ -318,6 +452,7 @@ def build():
|
||||
tree.__init_context__()
|
||||
tree.__init_content_context__()
|
||||
|
||||
"""
|
||||
import json
|
||||
with Path("context.json").open("w") as f:
|
||||
json.dump(tree.context, f, indent=4)
|
||||
@@ -325,6 +460,7 @@ def build():
|
||||
json.dump(CROSS_ARTICLE_CONTEXT, f, indent=4)
|
||||
with Path("t_cross_article_context.json").open("w") as f:
|
||||
json.dump(TRANSLATED_CROSS_ARTICLE_CONTEXT, f, indent=4)
|
||||
|
||||
"""
|
||||
|
||||
logger.info("dumping page tree...")
|
||||
tree.build()
|
||||
|
||||
62
stsg/definitions.py
Normal file
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