#!/usr/bin/env python3
"""Convert changelog.txt to a static HTML page for GitHub Pages."""
import re
import shutil
import sys
import html
from pathlib import Path
MONTHS = [
"", "January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December",
]
VERSION_RE = re.compile(
r"^(\d+\.\d+(?:\.\d+)?)\s*" # version number
r"(?:(alpha|beta|dev|stable)\s*)?" # optional tag
r"\((\d{2})\.(\d{2})\.(\d{2,4})\)$" # date (DD.MM.YY or DD.MM.YYYY)
)
def parse_date(day: str, month: str, year: str) -> tuple[str, str, str]:
"""Return (sort_key, raw_display, full_display) from DD, MM, YY strings."""
y = int(year)
if y < 100:
y += 2000
m = int(month)
d = int(day)
sort_key = f"{y:04d}-{m:02d}-{d:02d}"
raw_display = f"{day}.{month}.{year}"
full_display = f"{d} {MONTHS[m]} {y}"
return sort_key, raw_display, full_display
def parse_changelog(text: str) -> list[dict]:
entries = []
current = None
for raw_line in text.splitlines():
line = raw_line.rstrip()
m = VERSION_RE.match(line)
if m:
if current:
entries.append(current)
version, tag, day, month, year = m.groups()
sort_key, raw_date, full_date = parse_date(day, month, year)
current = {
"version": version,
"tag": tag or "",
"date": raw_date,
"full_date": full_date,
"sort_key": sort_key,
"lines": [],
}
elif current is not None:
# Skip blank lines at the start
if not line and not current["lines"]:
continue
# Skip stray artifact lines
if line.strip() in ("),", "),"):
continue
current["lines"].append(line)
if current:
entries.append(current)
# Trim trailing blank lines from each entry
for entry in entries:
while entry["lines"] and not entry["lines"][-1]:
entry["lines"].pop()
return entries
def render_entry(entry: dict) -> str:
version = html.escape(entry["version"])
tag = entry["tag"]
date = html.escape(entry["date"])
anchor = f"v{version}"
tag_html = ""
if tag and tag not in ("stable",):
tag_html = f' {html.escape(tag)}'
parts = [
f'',
f'
'
f'{version}{tag_html}'
f'
',
]
in_list = False
for line in entry["lines"]:
stripped = line.lstrip()
if stripped.startswith("- ") or stripped.startswith("\u2014 "):
# Bullet point (- or em dash)
if not in_list:
parts.append("