Compare commits
14 Commits
632f47e017
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4aa73b1aa | ||
|
|
73e89ca513 | ||
|
|
a1900a77e4 | ||
|
|
258d062ff4 | ||
|
|
8fee8d879e | ||
|
|
373cade0a7 | ||
|
|
5196913266 | ||
|
|
d3ff002901 | ||
|
|
08e7e343f5 | ||
|
|
433e2f6023 | ||
|
|
2690f0af87 | ||
|
|
f2a3d7ada4 | ||
|
|
753de66e08 | ||
|
|
036d5fb30a |
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
|
```sh
|
||||||
python3 -m venv .venv
|
python3 -m venv .venv
|
||||||
@@ -6,7 +17,7 @@ source .venv/bin/activate
|
|||||||
pip install -e .
|
pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
# Execute
|
## Execute
|
||||||
|
|
||||||
To start a local http server in the dist folder you can simply do:
|
To start a local http server in the dist folder you can simply do:
|
||||||
|
|
||||||
@@ -24,13 +35,3 @@ stsg
|
|||||||
stsg_dev
|
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
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ readme = "README.md"
|
|||||||
requires-python = ">=3.8"
|
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"
|
||||||
|
|||||||
@@ -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.
|
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}}).
|
||||||
|
|||||||
@@ -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.
|
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"
|
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 charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" type="image/x-icon" href="/static/icon.ico">
|
<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/bulma.min.css" />
|
||||||
<link rel="stylesheet" href="/static/style.css" />
|
<link rel="stylesheet" href="/static/style.css" />
|
||||||
</head>
|
</head>
|
||||||
@@ -16,11 +16,24 @@
|
|||||||
>
|
>
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
<a class="navbar-item" href="#">
|
<a class="navbar-item" href="#">
|
||||||
<strong>Static Translated Site Generator</strong>
|
<strong>{{name}}</strong>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</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">
|
<section class="section">
|
||||||
{% if translations|length %}
|
{% if translations|length %}
|
||||||
<div class="container content">
|
<div class="container content">
|
||||||
@@ -33,7 +46,7 @@
|
|||||||
<div class="card mb-4" lang="{{t.language.code}}" style="height: 100%;">
|
<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;">
|
<a href="{{t.url}}" hreflang="{{t.language.code}}" class="card mb-4" style="color: inherit; text-decoration: none;">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<p class="title">{{t.language.flag}} {{t.title}}</p>
|
<p class="title">{{t.language.flag}} {{t.name}}</p>
|
||||||
<hr />
|
<hr />
|
||||||
<p class="content">
|
<p class="content">
|
||||||
{{t.preview}}
|
{{t.preview}}
|
||||||
@@ -41,7 +54,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-footer">
|
<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>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -62,15 +75,15 @@
|
|||||||
<div class="card mb-4" >
|
<div class="card mb-4" >
|
||||||
<a href="{{c.url}}" class="card mb-4" style="color: inherit; text-decoration: none;">
|
<a href="{{c.url}}" class="card mb-4" style="color: inherit; text-decoration: none;">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<p class="title">{{c.slug}} </p>
|
<p class="title">{{c.name}} </p>
|
||||||
<hr />
|
<hr />
|
||||||
<p class="content is-flex is-flex-direction-column" style="gap: 10px;">
|
<p class="content is-flex is-flex-direction-column" style="gap: 10px;">
|
||||||
{% for ct in c.translations %}
|
{% 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 %}
|
{% endfor %}
|
||||||
</p>
|
</p>
|
||||||
<hr />
|
<hr />
|
||||||
<time datetime="{{iso_date}}">{{date}}</time>
|
<time datetime="{{c.iso_date}}">{{c.date}}</time>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,7 +96,7 @@
|
|||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<div class="content has-text-centered">
|
<div class="content has-text-centered">
|
||||||
<p><strong>STSG</strong> by Hazel. © 2025</p>
|
<p><strong>{{name}}</strong> by {{author}}. © {{year}}</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" type="image/x-icon" href="/static/icon.ico">
|
<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/bulma.min.css" />
|
||||||
<link rel="stylesheet" href="/static/style.css" />
|
<link rel="stylesheet" href="/static/style.css" />
|
||||||
</head>
|
</head>
|
||||||
@@ -18,11 +18,24 @@
|
|||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
<a class="navbar-item" href="{{article_url}}">
|
<a class="navbar-item" href="{{article_url}}">
|
||||||
<strong>{{language.flag}} {{title}}</strong>
|
<strong>{{language.flag}} {{title}}</strong>
|
||||||
<time datetime="{{meta.iso_date}}">{{meta.date}}</time>
|
<time datetime="{{iso_date}}">{{date}}</time>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</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 -->
|
<!-- Main Content -->
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
@@ -39,15 +52,13 @@
|
|||||||
<div class="card mb-4" >
|
<div class="card mb-4" >
|
||||||
<a href="{{c.url}}" hreflang="{{c.language.code}}" class="card mb-4" style="color: inherit; text-decoration: none;">
|
<a href="{{c.url}}" hreflang="{{c.language.code}}" class="card mb-4" style="color: inherit; text-decoration: none;">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<p class="title">{{c.title}}</p>
|
|
||||||
<hr />
|
|
||||||
<p class="content">
|
<p class="content">
|
||||||
{{c.preview}}
|
{{c.preview}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-footer">
|
<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>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,7 +72,7 @@
|
|||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<div class="content has-text-centered">
|
<div class="content has-text-centered">
|
||||||
<p><strong>STSG</strong> by Hazel. © 2025</p>
|
<p><strong>{{name}}</strong> by {{author}}. © {{year}}</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
fall_back_to_overview_in_translation = false
|
||||||
|
|
||||||
[setup]
|
[setup]
|
||||||
source_directory = "src"
|
source_directory = "src"
|
||||||
dist_directory = "dist"
|
dist_directory = "dist"
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
class config:
|
class config:
|
||||||
|
default_author = "anonymous"
|
||||||
|
fall_back_to_overview_in_translation = True
|
||||||
|
|
||||||
class setup:
|
class setup:
|
||||||
source_directory = "src"
|
source_directory = "src"
|
||||||
dist_directory = "dist"
|
dist_directory = "dist"
|
||||||
|
|||||||
230
stsg/build.py
230
stsg/build.py
@@ -6,26 +6,17 @@ import os
|
|||||||
from markdown2 import markdown
|
from markdown2 import markdown
|
||||||
from typing import Optional, Union, Dict, Generator, List, DefaultDict, Any, TypedDict, Set
|
from typing import Optional, Union, Dict, Generator, List, DefaultDict, Any, TypedDict, Set
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from collections import defaultdict
|
from collections import defaultdict, UserList
|
||||||
import toml
|
import toml
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import jinja2
|
import jinja2
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
|
|
||||||
|
from .definitions import *
|
||||||
from . import config
|
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):
|
def shorten_text_and_clean(html_string, max_length=config.formatting.preview_length):
|
||||||
soup = BeautifulSoup(html_string, 'html.parser')
|
soup = BeautifulSoup(html_string, 'html.parser')
|
||||||
|
|
||||||
@@ -120,23 +111,69 @@ class LanguageDict(dict):
|
|||||||
LANGUAGES = LanguageDict()
|
LANGUAGES = LanguageDict()
|
||||||
|
|
||||||
|
|
||||||
def compile_cross_article_context(cross_article_context):
|
def add_html_link(c):
|
||||||
title = cross_article_context["title"]
|
name = c["name"]
|
||||||
url = cross_article_context["url"]
|
url = c["url"]
|
||||||
|
|
||||||
cross_article_context["link"] = f'<a href="{url}">{title}</a>'
|
c["link"] = f'<a href="{url}">{name}</a>'
|
||||||
|
|
||||||
|
|
||||||
class ArticleTranslationContext(TypedDict):
|
def get_translated_articles(articles: List[Article], language_code: str = None) -> List[Union[ArticleTranslation, Article]]:
|
||||||
slug: str
|
result = {}
|
||||||
name: str
|
|
||||||
datetime: str
|
for a in articles:
|
||||||
author: str
|
if a.slug in result:
|
||||||
url: str
|
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:
|
class ArticleTranslation:
|
||||||
article: Article
|
article: Article
|
||||||
|
slug: str = property(fget=lambda self: self.article.slug)
|
||||||
file: Path
|
file: Path
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
@@ -183,31 +220,34 @@ class ArticleTranslation:
|
|||||||
self.article = article
|
self.article = article
|
||||||
self.file = file
|
self.file = file
|
||||||
|
|
||||||
self.context = {}
|
self.context = TRANSLATED_CROSS_ARTICLE_CONTEXT[self.language_code][self.article.slug] = {}
|
||||||
self.cross_article_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):
|
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
|
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["language"] = LANGUAGES[self.language_code]
|
||||||
self.context["article_url"] = self.article.url
|
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
|
# get children
|
||||||
self.context["children"] = [
|
self.context["children"] = self.article.child_articles.get_translated(self.language_code).context
|
||||||
c.article_translations_map[self.language_code].context for c in self.article.child_articles
|
self.context["breadcrumbs"] = ArticleList(self.article.article_path).get_translated(self.language_code).context
|
||||||
if self.language_code in c.article_translations_map
|
|
||||||
]
|
|
||||||
|
|
||||||
self.linked_context = self.context["linked"] = []
|
|
||||||
self.related_context = self.context["related"] = []
|
|
||||||
|
|
||||||
def __init_content_context__(self):
|
def __init_content_context__(self):
|
||||||
template = jinja2.Template(self.html_content)
|
template = jinja2.Template(self.html_content)
|
||||||
@@ -221,17 +261,16 @@ class ArticleTranslation:
|
|||||||
|
|
||||||
template.environment.context_class = jinja2.runtime.Context
|
template.environment.context_class = jinja2.runtime.Context
|
||||||
accessed_keys = template.environment.accessed_keys
|
accessed_keys = template.environment.accessed_keys
|
||||||
for key in accessed_keys:
|
|
||||||
a = ARTICLE_LAKE[key]
|
|
||||||
if self.language_code in a.article_translations_map:
|
|
||||||
self.linked_context.append(a.article_translations_map[self.language_code].context)
|
|
||||||
|
|
||||||
self.related_context.extend(self.linked_context)
|
for key in accessed_keys:
|
||||||
self.related_context.extend(self.context["children"])
|
self.article.linked_articles.append(key)
|
||||||
|
|
||||||
self.context["content"] = self.html_content
|
self.context["content"] = self.html_content
|
||||||
self.context["preview"] = get_preview_text(html_string=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):
|
def build(self):
|
||||||
self.dist_path.mkdir(parents=True, exist_ok=True)
|
self.dist_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
@@ -239,20 +278,6 @@ class ArticleTranslation:
|
|||||||
f.write(TEMPLATE["article_translation"].render(self.context))
|
f.write(TEMPLATE["article_translation"].render(self.context))
|
||||||
|
|
||||||
|
|
||||||
class ArticleConfig(TypedDict):
|
|
||||||
slug: str
|
|
||||||
name: str
|
|
||||||
datetime: str
|
|
||||||
author: str
|
|
||||||
|
|
||||||
|
|
||||||
class ArticleContext(TypedDict):
|
|
||||||
slug: str
|
|
||||||
name: str
|
|
||||||
datetime: str
|
|
||||||
author: str
|
|
||||||
url: str
|
|
||||||
|
|
||||||
class Article:
|
class Article:
|
||||||
directory: Path
|
directory: Path
|
||||||
|
|
||||||
@@ -288,8 +313,18 @@ class Article:
|
|||||||
return Path(config.setup.dist_directory, *self.slug_path)
|
return Path(config.setup.dist_directory, *self.slug_path)
|
||||||
|
|
||||||
context: ArticleContext
|
context: ArticleContext
|
||||||
context_shared: Dict[str, Any]
|
|
||||||
cross_article_context: Dict[str, Any]
|
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):
|
def __init__(self, directory: Path, article_path: Optional[List[str]] = None, is_root: bool = False, parent: Optional[Article] = None):
|
||||||
self.directory = directory
|
self.directory = directory
|
||||||
@@ -297,16 +332,16 @@ class Article:
|
|||||||
self.article_path: List[Article] = article_path or []
|
self.article_path: List[Article] = article_path or []
|
||||||
self.article_path.append(self)
|
self.article_path.append(self)
|
||||||
|
|
||||||
self.context: ArticleContext = {}
|
self.context = CROSS_ARTICLE_CONTEXT[self.slug] = {}
|
||||||
self.context_shared = {}
|
|
||||||
self.cross_article_context = CROSS_ARTICLE_CONTEXT[self.slug] = {}
|
|
||||||
|
|
||||||
ARTICLE_LAKE[self.slug] = self
|
ARTICLE_LAKE[self.slug] = self
|
||||||
|
|
||||||
|
self.linked_articles = ArticleList([])
|
||||||
|
|
||||||
# build the tree
|
# build the tree
|
||||||
self.child_articles: List[Article] = []
|
self.child_articles = ArticleList([])
|
||||||
self.article_translations_list: List[ArticleTranslation] = []
|
self.article_translations_list = []
|
||||||
self.article_translations_map: Dict[str, ArticleTranslation] = {}
|
self.article_translations_map = {}
|
||||||
|
|
||||||
for c in self.directory.iterdir():
|
for c in self.directory.iterdir():
|
||||||
if c.name == "index.toml":
|
if c.name == "index.toml":
|
||||||
@@ -327,41 +362,52 @@ class Article:
|
|||||||
|
|
||||||
logger.info("found %s at %s with the translations %s", self.slug, ".".join(list(self.slug_path)), ",".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):
|
def __init_context__(self):
|
||||||
self.context_shared["url"] = self.url
|
self.context["slug"] = self.slug
|
||||||
self.context_shared["slug"] = self.slug
|
self.context["name"] = self.name
|
||||||
|
self.context["url"] = self.url
|
||||||
modified_at = datetime.fromisoformat(self.config["datetime"]) if "datetime" in self.config else datetime.fromtimestamp(self.directory.stat().st_mtime)
|
add_html_link(self.context)
|
||||||
self.context_shared["date"] = modified_at.strftime(config.formatting.datetime_format)
|
self.context["date"] = self.modified_at.strftime(config.formatting.datetime_format)
|
||||||
self.context_shared["iso_date"] = modified_at.isoformat()
|
self.context["year"] = str(self.modified_at.year)
|
||||||
|
self.context["iso_date"] = self.modified_at.isoformat()
|
||||||
self.context.update(self.context_shared)
|
self.context["author"] = self.author
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
# recursive context structures
|
# recursive context structures
|
||||||
translation_list = self.context["translations"] = []
|
self.context["translations"] = [c.context for c in self.article_translations_list]
|
||||||
child_article_list = self.context["children"] = []
|
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.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:
|
for at in self.article_translations_list:
|
||||||
at.__init_context__()
|
at.__init_context__()
|
||||||
|
|
||||||
for a in self.child_articles:
|
for a in self.child_articles:
|
||||||
a.__init_context__()
|
a.__init_context__()
|
||||||
|
|
||||||
def __init_content_context__(self):
|
def __init_content_context__(self):
|
||||||
for at in self.article_translations_list:
|
for at in self.article_translations_list:
|
||||||
at.__init_content_context__()
|
at.__init_content_context__()
|
||||||
|
|
||||||
|
self.context["linked"] = self.linked_articles.context
|
||||||
|
self.context["related"] = self.related_articles.context
|
||||||
|
|
||||||
for a in self.child_articles:
|
for a in self.child_articles:
|
||||||
a.__init_content_context__()
|
a.__init_content_context__()
|
||||||
|
|
||||||
@@ -387,9 +433,9 @@ class ContextDict(jinja2.runtime.Context):
|
|||||||
|
|
||||||
# GLOBALS
|
# GLOBALS
|
||||||
logger = logging.getLogger("stsg.build")
|
logger = logging.getLogger("stsg.build")
|
||||||
|
ARTICLE_LAKE: Dict[str, Article] = {}
|
||||||
CROSS_ARTICLE_CONTEXT: Dict[str, Dict[str, Any]] = {}
|
CROSS_ARTICLE_CONTEXT: Dict[str, Dict[str, Any]] = {}
|
||||||
TRANSLATED_CROSS_ARTICLE_CONTEXT: Dict[str, Dict[str, Dict[str, Any]]] = defaultdict(dict)
|
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)
|
ARTICLE_REFERENCE_VALUES: DefaultDict[str, Dict[str, str]] = defaultdict(dict)
|
||||||
|
|
||||||
|
|
||||||
@@ -406,6 +452,7 @@ def build():
|
|||||||
tree.__init_context__()
|
tree.__init_context__()
|
||||||
tree.__init_content_context__()
|
tree.__init_content_context__()
|
||||||
|
|
||||||
|
"""
|
||||||
import json
|
import json
|
||||||
with Path("context.json").open("w") as f:
|
with Path("context.json").open("w") as f:
|
||||||
json.dump(tree.context, f, indent=4)
|
json.dump(tree.context, f, indent=4)
|
||||||
@@ -413,6 +460,7 @@ def build():
|
|||||||
json.dump(CROSS_ARTICLE_CONTEXT, f, indent=4)
|
json.dump(CROSS_ARTICLE_CONTEXT, f, indent=4)
|
||||||
with Path("t_cross_article_context.json").open("w") as f:
|
with Path("t_cross_article_context.json").open("w") as f:
|
||||||
json.dump(TRANSLATED_CROSS_ARTICLE_CONTEXT, f, indent=4)
|
json.dump(TRANSLATED_CROSS_ARTICLE_CONTEXT, f, indent=4)
|
||||||
|
"""
|
||||||
|
|
||||||
logger.info("dumping page tree...")
|
logger.info("dumping page tree...")
|
||||||
tree.build()
|
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