#!/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(" ") in_list = False if stripped: parts.append(f"

{html.escape(stripped)}

") if in_list: parts.append(" ") parts.append("
") return "\n".join(parts) def build_html(entries: list[dict]) -> str: count = len(entries) first_date = entries[-1]["full_date"] if entries else "" latest_version = entries[0]["version"] if entries else "" entries_html = "\n\n".join(render_entry(e) for e in entries) return f""" Version history

Version history

{count} releases since {first_date} · latest: {latest_version}

{entries_html}
""" def main(): repo = Path(__file__).resolve().parent.parent.parent src = repo / "changelog.txt" if len(sys.argv) > 1: src = Path(sys.argv[1]) out = repo / "docs" / "changelog" / "index.html" if len(sys.argv) > 2: out = Path(sys.argv[2]) text = src.read_text(encoding="utf-8") entries = parse_changelog(text) html_content = build_html(entries) out.parent.mkdir(parents=True, exist_ok=True) out.write_text(html_content, encoding="utf-8") # Copy favicon files from resources icons_src = repo / "Telegram" / "Resources" / "art" for name in ("icon16.png", "icon32.png"): icon = icons_src / name if icon.exists(): shutil.copy2(icon, out.parent / name) print(f"Generated {out} ({len(entries)} entries, {out.stat().st_size:,} bytes)") if __name__ == "__main__": main()