feat: recursive pages

This commit is contained in:
amnesia 2025-04-14 21:06:52 +02:00
parent a113a13318
commit 3f0049dfdb
8 changed files with 138 additions and 484 deletions

View File

@ -1,37 +0,0 @@
<!DOCTYPE html>
<html lang="{article_language_code}">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{article_language_flag} {article_title}</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="{article_overview_href}">
<strong>{article_language_flag} {article_title}</strong>
</a>
</div>
</nav>
<!-- Main Content -->
<section class="section">
<div class="content">
{article_content}
</div>
</section>
<!-- Footer -->
<footer class="footer">
<div class="content has-text-centered">
<p><strong>STSG</strong> by Hazel. &copy; 2025</p>
</div>
</footer>
</body>
</html>

View File

@ -1,55 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>STSG</title>
<link rel="stylesheet" href="/static/bulma.min.css" />
</head>
<body>
<!-- Header (Navbar) -->
<nav
class="navbar is-primary"
role="navigation"
aria-label="main navigation"
>
<div class="navbar-brand">
<a class="navbar-item" href="#">
<strong>Static Translated Site Generator</strong>
</a>
</div>
</nav>
<section class="section">
<div class="container">
<div class="column is-half is-offset-one-quarter">{overview_cards}</div>
</div>
</section>
<!-- Footer -->
<footer class="footer">
<div class="content has-text-centered">
<p><strong>STSG</strong> by Hazel. &copy; 2025</p>
</div>
</footer>
</body>
<script>
document.addEventListener("DOMContentLoaded", function () {
const userLang = navigator.language || navigator.userLanguage;
// Normalize and check if the language is not English or German
if (!["en", "de", "de-DE"].includes(userLang)) {
// Try to find a matching card by language attribute
const cardToMove =
document.querySelector(`.card[lang^="${userLang.replace("_", "-").toLowerCase()}"]`) ||
document.querySelector(`.card[lang^="${userLang.split("-")[0]}"]`);
if (cardToMove) {
const container = cardToMove.parentNode;
container.insertBefore(cardToMove, container.firstChild);
}
}
});
</script>
</html>

View File

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

View File

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

View File

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

View File

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

View File

@ -7,255 +7,173 @@ import markdown
from typing import Optional, Union, Dict, Generator, List
from bs4 import BeautifulSoup
from .config import SOURCE_DIRECTORY, DIST_DIRECTORY, LANGUAGE_INFORMATION, ARTICLE_PREVIEW_LENGTH
from .config import SOURCE_DIRECTORY, DIST_DIRECTORY, LANGUAGE_INFORMATION, ARTICLE_PREVIEW_LENGTH, DEFAULT_LANGUAGE
logger = logging.getLogger("stsg.build")
class CustomPath:
def __init__(self, path: Path):
self.path = path
def replace_values(template: str, values: Dict[str, str]) -> str:
for key, value in values.items():
template = template.replace("{" + key + "}", value)
def __repr__(self) -> str:
return str(self.path)
return template
@property
def source_path(self) -> Path:
return Path(SOURCE_DIRECTORY, self.path)
@property
def dist_path(self) -> Path:
return Path(DIST_DIRECTORY, self.path)
def get_first_header_content(content):
soup = BeautifulSoup(content, 'html.parser')
for level in range(1, 7):
header = soup.find(f'h{level}')
if header:
return header.get_text(strip=True)
@property
def name(self) -> str:
return Path(self.path).name
return self.language_code.native_name
@property
def parent(self) -> CustomPath:
return CustomPath(Path(self.path).parent)
@property
def stem(self) -> str:
return Path(self.path).stem
def iterdir(self) -> Generator[CustomPath, None, None]:
for p in self.source_path.iterdir():
yield CustomPath(Path(self.path, p.name))
class Template:
def __init__(self, folder: Path):
self.folder = folder
def get_child(self, name: str, force_directory: bool = False, force_file: bool = False) -> Optional[CustomPath]:
child = Path(self.source_path, name)
self.article: str = (self.folder / "article.html").read_text()
self.overview: str = (self.folder / "overview.html").read_text()
self.overview_card: str = (self.folder / "overview_card.html").read_text()
if not child.exists():
return None
TEMPLATE = Template(Path(SOURCE_DIRECTORY, "templates"))
if force_directory and not child.is_dir():
return None
if force_file and not child.is_file():
return None
class ArticleTranslation:
def __init__(self, file: Path, article_overview: ArticleOverview):
self.file = file
self.article_overview = article_overview
self.language_code = self.file.stem
return CustomPath(Path(self.path, name))
self.article_content = self.file.read_text()
if self.file.suffix == ".md":
self.article_content = markdown.markdown(self.article_content)
def read_text(self) -> str:
return self.source_path.read_text()
self.url = "/" + self.language_code + self.article_overview.url
self.dist_path = Path(DIST_DIRECTORY, self.url.strip("/"))
def copy_static(path: CustomPath):
src = path.source_path
dst = path.dist_path
_language_info = DEFAULT_LANGUAGE
parsed_language_code = self.language_code.lower().replace("-", "_")
if parsed_language_code in LANGUAGE_INFORMATION:
_language_info = LANGUAGE_INFORMATION[parsed_language_code]
elif parsed_language_code.split("_")[0] in LANGUAGE_INFORMATION:
_language_info = LANGUAGE_INFORMATION[parsed_language_code.split("_")[0]]
if not src.exists():
logger.warning("The static folder '%s' wasn't defined.", src)
self.language_name: str = _language_info["native_name"]
self.language_flag: str = _language_info["flag"]
self.priority: int = _language_info.get("priority", 0)
def build(self):
self.dist_path.mkdir(parents=True, exist_ok=True)
with Path(self.dist_path, "index.html").open("w") as f:
f.write(self.get_article())
def _get_values(self) -> Dict[str, str]:
return {
"article_content": self.article_content,
"article_preview": self.article_content[:ARTICLE_PREVIEW_LENGTH] + "...",
"article_overview_url": self.article_overview.url,
"article_href": self.url,
"article_title": get_first_header_content(self.article_content),
"article_language_name": self.language_name,
"article_language_code": self.language_code,
"article_language_flag": self.language_flag,
}
def get_article(self) -> str:
global TEMPLATE
return replace_values(TEMPLATE.article, self._get_values())
def get_overview_card(self) -> str:
global TEMPLATE
return replace_values(TEMPLATE.overview_card, self._get_values())
class ArticleOverview:
def __init__(self, directory: Path, url: str = ""):
self.directory = directory
self.name = self.directory.name
self.url = url + "/" + self.name
self.dist_path = Path(DIST_DIRECTORY, self.url.strip("/"))
self.child_articles: List[ArticleOverview] = []
self.article_translations: List[ArticleTranslation] = []
for c in self.directory.iterdir():
if c.is_file():
self.article_translations.append(ArticleTranslation(c, self))
elif c.is_dir():
self.child_articles.append(ArticleOverview(c, self.url))
# the tree is built
self.article_translations.sort(key=lambda a: a.priority, reverse=True)
self.overview_cards = "\n".join(a.get_overview_card() for a in self.article_translations)
def build(self):
# builds the tree structure to the dist directory
self.dist_path.mkdir(parents=True, exist_ok=True)
with Path(self.dist_path, "index.html").open("w") as f:
f.write(self.get_overview())
for at in self.article_translations:
at.build()
for ca in self.child_articles:
ca.build()
def _get_values(self) -> Dict[str, str]:
return {
"overview_cards": self.overview_cards,
"overview_slug": self.name,
}
def get_overview(self) -> str:
global TEMPLATE
return replace_values(TEMPLATE.overview, self._get_values())
def copy_static():
src = str(Path(SOURCE_DIRECTORY, "static"))
dst = str(Path(DIST_DIRECTORY, "static"))
if not os.path.exists(src):
logger.warn("The static folder '%s' wasn't defined.", src)
return
logger.info("Copying static files from '%s' to '%s'", src, dst)
logger.info("copying static files from '%s' to '%r'", src, dst)
dst.mkdir(parents=True, exist_ok=True)
os.makedirs(dst, exist_ok=True)
for root, dirs, files in os.walk(src):
root_path = Path(root)
if any(p.startswith(".") for p in Path(root).parts):
continue
if any(part.startswith(".") for part in root_path.parts):
continue
# Compute relative path from the source root
rel_path = os.path.relpath(root, src)
dest_dir = os.path.join(dst, rel_path)
rel_path = root_path.relative_to(src)
dest_dir = dst / rel_path
dest_dir.mkdir(parents=True, exist_ok=True)
os.makedirs(dest_dir, exist_ok=True)
for file in files:
if file.startswith("."):
continue
src_file = root_path / file
dest_file = dest_dir / file
src_file = os.path.join(root, file)
dest_file = os.path.join(dest_dir, file)
shutil.copy2(src_file, dest_file)
class CustomLanguageCode:
def __init__(self, language_code: str):
self.language_code = language_code
def __repr__(self) -> str:
return f"{self.language_code}"
def _get_additional_data(self) -> dict:
parsed_language_code = self.language_code.lower().replace("-", "_")
if parsed_language_code in LANGUAGE_INFORMATION:
return LANGUAGE_INFORMATION[parsed_language_code]
parsed_language_code = parsed_language_code.split("_")[0]
if parsed_language_code in LANGUAGE_INFORMATION:
return LANGUAGE_INFORMATION[parsed_language_code]
return {}
@property
def flag(self) -> str:
return self._get_additional_data()["flag"]
@property
def native_name(self) -> str:
return self._get_additional_data()["native_name"]
@property
def priority(self) -> int:
return self._get_additional_data().get("priority", 0)
class Article(CustomPath):
def __init__(self, path: CustomPath):
super().__init__(path.path)
def get_content(self) -> str:
if self.name.endswith(".md"):
return markdown.markdown(self.read_text())
return self.read_text()
@property
def article_directory(self) -> Path:
return self.dist_path.parent / self.stem
@property
def language_code(self) -> CustomLanguageCode:
return CustomLanguageCode(self.stem)
def get_first_header_content(self, content):
soup = BeautifulSoup(content, 'html.parser')
for level in range(1, 7):
header = soup.find(f'h{level}')
if header:
return header.get_text(strip=True)
return self.language_code.native_name
def get_article_keys(self) -> Dict[str, str]:
article_content = self.get_content()
return {
"article_content": article_content,
"article_preview": article_content[:ARTICLE_PREVIEW_LENGTH] + " ...",
"article_overview_href": "/" + str(self.path.parent),
"article_href": "/" + str(self.path.parent / self.stem),
"article_title": self.get_first_header_content(article_content),
"article_language_name": self.language_code.native_name,
"article_language_code": self.language_code.language_code,
"article_language_flag": self.language_code.flag,
}
class Template:
def __init__(self, root: CustomPath):
self.root = root
self.articles: List[Article] = []
def copy(self):
return Template(root=self.root)
def _replace_keywords(self, text: str, **placeholder_values: str) -> str:
for key, value in placeholder_values.items():
text = text.replace("{" + key + "}", value)
return text
def get_article_template(self) -> str:
article = self.root.get_child("article.html", force_file=True)
return article.source_path.read_text() if article else "{article_content}"
def build_article(self, path: Article):
logger.info("converting %s", path)
article_text = self._replace_keywords(
self.get_article_template(),
**path.get_article_keys(),
)
path.article_directory.mkdir(parents=True, exist_ok=True)
with Path(path.article_directory, "index.html").open("w") as f:
f.write(article_text)
self.articles.append(path)
def get_overview_card_template(self) -> str:
overview_card = self.root.get_child("overview_card.html", force_file=True)
return overview_card.source_path.read_text() if overview_card else "<a href=\"{article_href}\"> {article_language_flag} {article_language_name} </a>"
def get_overview_template(self) -> str:
overview = self.root.get_child("overview.html", force_file=True)
return overview.source_path.read_text() if overview else "{overview_cards}"
def build_overview(self, root: CustomPath):
if not len(self.articles):
return
overview_card_template = self.get_overview_card_template()
self.articles.sort(key=lambda a: a.language_code.priority, reverse=True)
overview_cards = "\n".join(self._replace_keywords(
overview_card_template,
**a.get_article_keys(),
) for a in self.articles)
overview_text = self._replace_keywords(
self.get_overview_template(),
overview_cards = overview_cards,
)
with Path(root.dist_path, "index.html").open("w") as f:
f.write(overview_text)
def walk_directory(root: CustomPath, template: Optional[Template] = None):
template_dir = root.get_child("_templates", force_directory=True)
if template_dir is not None:
template = Template(template_dir)
if template is None:
logger.error("Didn't find template for %d", root)
return
for current_path in root.iterdir():
if current_path.name.startswith("_") or current_path.name == "static":
continue
if current_path.source_path.is_file():
template.build_article(Article(current_path))
continue
if current_path.source_path.is_dir():
walk_directory(current_path, template=template.copy())
template.build_overview(root=root)
static_dir = root.get_child("static", force_directory=True)
if static_dir:
copy_static(static_dir)
def build():
logger.info("building static page")
walk_directory(CustomPath(Path()))
logger.info("building static page")
copy_static()
tree = ArticleOverview(directory=Path(SOURCE_DIRECTORY, "pages"))
tree.build()

View File

@ -756,3 +756,6 @@ LANGUAGE_INFORMATION = {
"native_name": "isiZulu"
}
}
DEFAULT_LANGUAGE = LANGUAGE_INFORMATION["de"]