Files
ln-s/build.py
2025-08-09 18:17:23 -04:00

219 lines
6.4 KiB
Python

import dataclasses
import os
import re
import typing as t
import yaml
with open("index.html") as file:
template = file.read()
with open("res/arrow-path.txt") as file:
arrow_path = file.read().strip()
with open("res/i18n.yml") as file:
i18n = yaml.safe_load(file)
def htmlify(text: str) -> str:
return (
text.replace("&", "&")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\n", "<br />")
)
# Awful awful awful code, but it does the job.
@dataclasses.dataclass
class BBLiteral:
text: str
def to_html(self) -> str:
return htmlify(self.text)
TokenRaw = t.Tuple[t.Literal["start", "end", "text"], str]
@dataclasses.dataclass
class BBTag:
name: str
children: list["BBLiteral | BBTag"]
def to_html(self) -> str:
if self.name == "root":
return "".join(child.to_html() for child in self.children)
elif self.name == "b":
inner = "".join(child.to_html() for child in self.children)
return f"<strong>{inner}</strong>"
elif self.name == "i":
inner = "".join(child.to_html() for child in self.children)
return f"<em>{inner}</em>"
elif self.name == "a":
assert len(self.children) == 1 and isinstance(
self.children[0], BBLiteral
), "[a] tag must contain exactly one literal child"
href = "https://creativecommons.org/public-domain/cc0/"
inner = "".join(child.to_html() for child in self.children)
return f'<a href="{href}">{inner}</a>'
else:
raise ValueError(f"Unknown tag: {self.name}")
class BBParser:
source: str
current_tags: list[str]
pos: int
def __init__(self, source: str):
self.source = source
self.current_tags = []
self.pos = 0
def render(self) -> BBTag:
root = BBTag(name="root", children=[])
stack = [root]
for kind, value in self.parse():
if kind == "text":
stack[-1].children.append(BBLiteral(text=value))
elif kind == "start":
new_tag = BBTag(name=value, children=[])
stack[-1].children.append(new_tag)
stack.append(new_tag)
elif kind == "end":
assert stack.pop().name == value, "Mismatched closing tag"
assert len(stack) == 1, "Unclosed tags remain"
return root
def parse(
self,
) -> t.Generator[TokenRaw, None, None]:
while self.pos < len(self.source):
yield self.next_token()
def next_token(self) -> TokenRaw:
if self.source[self.pos] == "[":
end_tag_pos = self.source.find("]", self.pos)
if end_tag_pos == -1:
raise ValueError("Unclosed tag")
tag = self.source[self.pos + 1 : end_tag_pos]
if tag.startswith("/"):
kind = "end"
tag = tag[1:]
assert self.current_tags.pop() == tag, "Mismatched closing tag"
else:
kind = "start"
self.current_tags.append(tag)
assert (
len([i for i in self.current_tags if i == "a"]) <= 1
), "Nested [a] tags"
self.pos = end_tag_pos + 1
return (kind, tag)
else:
next_tag_pos = self.source.find("[", self.pos)
if next_tag_pos == -1:
next_tag_pos = len(self.source)
text = self.source[self.pos : next_tag_pos]
self.pos = next_tag_pos
return ("text", text)
def htmlify_bbcode(text: str) -> str:
parser = BBParser(text)
parsed = parser.render()
return parsed.to_html()
def format(template: str, values: dict[str, str]) -> str:
output = ""
i = 0
while i < len(template):
if template[i:].startswith("{{"):
end_pos = template.find("}}", i)
if end_pos == -1:
raise ValueError("Unclosed placeholder")
key = template[i + 2 : end_pos].strip()
if key not in values:
raise KeyError(f"Missing value for key '{key}'")
output += values[key]
i = end_pos + 2
else:
output += template[i]
i += 1
return output
# English is my native language and the one I can advocate for the accuracy of
# the content of this website the most, so it is the canonical translation. I'm
# not trying to make the claim that English is in any way the "default".
canonical_lang = "en"
LANG_RE = re.compile(r"^[a-z]{2,3}(_[A-Z]{2,3})?$")
for lang in i18n["locales"]:
if not LANG_RE.match(lang):
raise ValueError(f"Invalid language code: '{lang}'")
alternates_list = []
for lang in i18n["locales"]:
href = (
"https://eunakria.com/ln-s/"
if lang == canonical_lang
else f"https://eunakria.com/ln-s/{lang}/index.html"
)
alternates_list.append(
(lang, f'<link rel="alternate" hreflang="{lang}" href="{href}" />')
)
for lang, texts in i18n["locales"].items():
fallbacks = [texts]
if "_" in lang:
base_lang = lang.split("_")[0]
if base_lang in i18n["locales"]:
fallbacks.append(i18n["locales"][base_lang])
fallbacks.append(i18n["locales"][canonical_lang])
def tr(key: str) -> str:
for fb in fallbacks:
if key in fb:
return fb[key]
raise KeyError(f"Missing translation for key '{key}' in language '{lang}'")
description = htmlify(tr("description"))
file_annotation = htmlify(tr("file_annotation"))
link_annotation = htmlify(tr("link_annotation"))
file = htmlify(tr("file"))
link = htmlify(tr("link"))
deed = htmlify_bbcode(tr("deed"))
alternates_str = "\n".join(
alt for alt_lang, alt in alternates_list if alt_lang != lang
)
output = format(
template,
{
"lang": lang,
"arrow_path": arrow_path,
"description": description,
"file_annotation": file_annotation,
"link_annotation": link_annotation,
"file": file,
"link": link,
"deed": deed,
"alternates": alternates_str,
},
)
output_path = "index.html" if lang == canonical_lang else f"{lang}/index.html"
output_path = f"dist/{output_path}"
os.makedirs(os.path.dirname(output_path), exist_ok=True)
with open(output_path, "w") as file:
file.write(output)