#!/usr/bin/env python3 """ blog/build.py — static blog generator for zebra-report markdown + frontmatter → html, no dependencies beyond stdlib """ import re, html from pathlib import Path POSTS_DIR = Path("blog/posts") OUT_DIR = Path("web/blog") # ------------------------------------------------------------------ # # minimal markdown → html # # ------------------------------------------------------------------ # def md_to_html(text): lines = text.split("\n") out = [] in_code = False in_list = False buf = [] def flush_para(): if buf: content = inline(" ".join(buf).strip()) if content: out.append(f"
{content}
") buf.clear() def flush_list(): nonlocal in_list if in_list: out.append("") in_list = False def inline(s): s = html.escape(s, quote=False) s = re.sub(r"`(.+?)`", r"\1", s)
s = re.sub(r"\*\*(.+?)\*\*", r"\1", s)
s = re.sub(r"\*(.+?)\*", r"\1", s)
s = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r'\1', s)
return s
for line in lines:
# fenced code blocks
if line.startswith("```"):
if in_code:
out.append("")
in_code = False
else:
flush_para(); flush_list()
lang = line[3:].strip()
out.append(f'' if lang else "")
in_code = True
continue
if in_code:
out.append(html.escape(line))
continue
# headings
m = re.match(r"^(#{1,3})\s+(.*)", line)
if m:
flush_para(); flush_list()
n = len(m.group(1))
out.append(f"{inline(m.group(2))} ")
continue
# unordered list
m = re.match(r"^[-*]\s+(.*)", line)
if m:
flush_para()
if not in_list:
out.append("")
in_list = True
out.append(f"- {inline(m.group(1))}
")
continue
# blank line
if not line.strip():
flush_para(); flush_list()
continue
buf.append(line)
flush_para(); flush_list()
return "\n".join(out)
# ------------------------------------------------------------------ #
# frontmatter parser #
# ------------------------------------------------------------------ #
def parse_post(path):
text = path.read_text()
meta = {}
body = text
if text.startswith("---"):
parts = text.split("---", 2)
if len(parts) >= 3:
for line in parts[1].strip().splitlines():
if ":" in line:
k, v = line.split(":", 1)
meta[k.strip().lower()] = v.strip()
body = parts[2]
meta["content"] = md_to_html(body.strip())
meta.setdefault("slug", path.stem)
meta.setdefault("date", "")
meta.setdefault("title", path.stem)
meta.setdefault("summary", "")
return meta
# ------------------------------------------------------------------ #
# templates #
# ------------------------------------------------------------------ #
CSS_LINK = ''
BASE = """\
{title} — zebra report
{css}
zebra report
make crypto scary again
{body}
"""
def render_post(meta, newer=None, older=None):
nav = []
if older:
nav.append(f'← {older["title"]}')
if newer:
nav.append(f'{newer["title"]} →')
nav_html = f'' if nav else ""
body = f"""
{html.escape(meta["title"])}
{meta["content"]}
{nav_html}
"""
return BASE.format(title=html.escape(meta["title"]), css=CSS_LINK, body=body)
def render_index(posts):
items = ""
for p in posts:
items += f"""-
{html.escape(p["date"])}
{html.escape(p["title"])}
{html.escape(p["summary"])}
\n"""
body = f'posts
\n\n{items}
'
return BASE.format(title="zebra report", css=CSS_LINK, body=body)
# ------------------------------------------------------------------ #
# build #
# ------------------------------------------------------------------ #
def build():
OUT_DIR.mkdir(parents=True, exist_ok=True)
posts = [parse_post(p) for p in sorted(POSTS_DIR.glob("*.md"))]
posts.sort(key=lambda p: p["date"], reverse=True)
for i, post in enumerate(posts):
newer = posts[i - 1] if i > 0 else None
older = posts[i + 1] if i + 1 < len(posts) else None
d = OUT_DIR / post["slug"]
d.mkdir(exist_ok=True)
(d / "index.html").write_text(render_post(post, newer=newer, older=older))
print(f" {post['slug']}/")
(OUT_DIR / "index.html").write_text(render_index(posts))
print(" index.html")
print("done.")
if __name__ == "__main__":
build()