initial commit
This commit is contained in:
218
build.py
Normal file
218
build.py
Normal file
@@ -0,0 +1,218 @@
|
||||
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("<", "<")
|
||||
.replace(">", ">")
|
||||
.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)
|
||||
Reference in New Issue
Block a user