#!/usr/bin/env python3 """Render one CHANGELOG.md version section as HTML for a Sparkle appcast. Usage: changelog-to-html.py Prints HTML to stdout for embedding in an appcast item's (inside CDATA). Sparkle's standard update UI shows it in a scrollable release-notes pane. Emits nothing (exit 0) if the version section isn't found, so callers can treat empty output as "no notes". Deliberately tiny: the CHANGELOG is Keep-a-Changelog (### subsections, `-` bullets, **bold**, `code`), not arbitrary Markdown, so a full parser is overkill. """ import html import re import sys def inline(text: str) -> str: """Escape, then apply the inline markup the CHANGELOG actually uses.""" text = html.escape(text) text = re.sub(r"`([^`]+)`", r"\1", text) text = re.sub(r"\*\*([^*]+)\*\*", r"\1", text) return text def extract(version: str, lines: list[str]) -> list[str]: out, capturing = [], False header = re.compile(r"^## \[" + re.escape(version) + r"\]") for line in lines: if header.match(line): capturing = True continue if capturing and line.startswith("## ["): # next version section break if capturing: out.append(line.rstrip("\n")) return out def to_html(section: list[str]) -> str: parts, in_list = [], False def close_list(): nonlocal in_list if in_list: parts.append("") in_list = False for line in section: stripped = line.strip() if not stripped or stripped == "---": close_list() continue if stripped.startswith("### "): close_list() parts.append(f"

{inline(stripped[4:])}

") elif stripped.startswith(("- ", "* ")): if not in_list: parts.append("