zebra-report/blog/build.py
Russell Ballestrini b77da42bbe phase 1: unfirehose reconstruction from session JSONL ingest
Source: ~/.unfirehose/unfirehose.db (project_id=81, 4 sessions covering
2026-03-29 through 2026-04-05). Reconstructed via chronological replay
of Write/Edit tool_input on file_paths under /home/fox/zebra-report/.

stats:
  files reconstructed:    20
  writes baselined:       all (zero missing)
  edits applied:          68
  edits unapplied:        8 (1 SKIP pre-baseline, 6 FAIL old_string drift, 1 AMBIGUOUS)

unapplied edits represent small drift in 6 files; baseline content for
each is intact. quality verification deferred to phase 2.

recovered tree:
  CLAUDE.md, Makefile
  src/{tx,rx,pulse,carrier,chat,bt}.c
  include/{modem,zebra}.h
  test/{functional,integration,unit}.c, test/test.h
  web/{index,kernel}.html, web/blog/style.css
  blog/build.py, blog/posts/{001-volume-modem,002-sse-chatroom}.md

report: /tmp/zebra_recover_report.txt
script: /tmp/zebra_recover.py
2026-05-27 13:51:14 -04:00

185 lines
5.8 KiB
Python

#!/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"<p>{content}</p>")
buf.clear()
def flush_list():
nonlocal in_list
if in_list:
out.append("</ul>")
in_list = False
def inline(s):
s = html.escape(s, quote=False)
s = re.sub(r"`(.+?)`", r"<code>\1</code>", s)
s = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", s)
s = re.sub(r"\*(.+?)\*", r"<em>\1</em>", s)
s = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r'<a href="\2">\1</a>', s)
return s
for line in lines:
# fenced code blocks
if line.startswith("```"):
if in_code:
out.append("</code></pre>")
in_code = False
else:
flush_para(); flush_list()
lang = line[3:].strip()
out.append(f'<pre><code class="language-{lang}">' if lang else "<pre><code>")
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"<h{n}>{inline(m.group(2))}</h{n}>")
continue
# unordered list
m = re.match(r"^[-*]\s+(.*)", line)
if m:
flush_para()
if not in_list:
out.append("<ul>")
in_list = True
out.append(f"<li>{inline(m.group(1))}</li>")
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 = '<link rel="stylesheet" href="/blog/style.css">'
BASE = """\
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{title} — zebra report</title>
{css}
</head>
<body>
<header class="site-header">
<a class="site-name" href="/blog/">zebra report</a>
<span class="tagline">make crypto scary again</span>
</header>
<main>{body}</main>
</body>
</html>"""
def render_post(meta, newer=None, older=None):
nav = []
if older:
nav.append(f'<a href="/blog/{older["slug"]}/">← {older["title"]}</a>')
if newer:
nav.append(f'<a href="/blog/{newer["slug"]}/">{newer["title"]} →</a>')
nav_html = f'<nav class="post-nav">{" &nbsp; ".join(nav)}</nav>' if nav else ""
body = f"""<article>
<h1>{html.escape(meta["title"])}</h1>
<p class="meta">{html.escape(meta["date"])} &mdash; <a href="/blog/">all posts</a></p>
<div class="content">{meta["content"]}</div>
{nav_html}
</article>"""
return BASE.format(title=html.escape(meta["title"]), css=CSS_LINK, body=body)
def render_index(posts):
items = ""
for p in posts:
items += f"""<li>
<span class="post-date">{html.escape(p["date"])}</span>
<a class="post-title" href="/blog/{p["slug"]}/">{html.escape(p["title"])}</a>
<p class="post-summary">{html.escape(p["summary"])}</p>
</li>\n"""
body = f'<h1>posts</h1>\n<ul class="post-list">\n{items}</ul>'
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()