11"""Dev CLI for where-the-plow."""
22
3+ import re
34import subprocess
45import sys
6+ from pathlib import Path
57
68COMMANDS = {
79 "dev" : "Run uvicorn in development mode with auto-reload" ,
810 "start" : "Run uvicorn in production mode" ,
11+ "changelog" : "Convert CHANGELOG.md to an HTML fragment" ,
912}
1013
1114APP = "where_the_plow.main:app"
@@ -43,6 +46,87 @@ def start():
4346 )
4447
4548
49+ def _md_inline (text : str ) -> str :
50+ """Convert inline markdown to HTML: bold, links, and issue references."""
51+ # Convert **text** to <strong>text</strong>
52+ text = re .sub (r"\*\*(.+?)\*\*" , r"<strong>\1</strong>" , text )
53+ # Convert [text](url) to <a> tags
54+ text = re .sub (
55+ r"\[(.+?)\]\((.+?)\)" ,
56+ r'<a href="\2" target="_blank" rel="noopener">\1</a>' ,
57+ text ,
58+ )
59+ # Convert standalone (#N) to issue links
60+ text = re .sub (
61+ r"\(#(\d+)\)" ,
62+ r'(<a href="https://github.com/jackharrhy/where-the-plow/issues/\1" target="_blank" rel="noopener">#\1</a>)' ,
63+ text ,
64+ )
65+ return text
66+
67+
68+ def changelog ():
69+ root = Path (__file__ ).parent
70+ md_path = root / "CHANGELOG.md"
71+ out_path = root / "src" / "where_the_plow" / "static" / "changelog.html"
72+
73+ content = md_path .read_text ()
74+
75+ # Extract changelog-id
76+ id_match = re .search (r"<!--\s*changelog-id:\s*(\d+)\s*-->" , content )
77+ changelog_id = id_match .group (1 ) if id_match else "0"
78+
79+ # Split on ## headings; first chunk is the header/preamble, skip it
80+ sections = re .split (r"^## " , content , flags = re .MULTILINE )
81+
82+ articles = []
83+ for section in sections [1 :]:
84+ lines = section .strip ()
85+ # First line is the title
86+ title , _ , body = lines .partition ("\n " )
87+ title = title .strip ()
88+ body = body .strip ()
89+
90+ # Split body into paragraphs on double newlines
91+ paragraphs = re .split (r"\n\n+" , body )
92+ p_html = []
93+ for para in paragraphs :
94+ if not para .strip ():
95+ continue
96+ # Within a paragraph block, lines starting with [ (a link)
97+ # are separate paragraphs (e.g. "View changes" links)
98+ sub_parts : list [list [str ]] = [[]]
99+ for line in para .strip ().splitlines ():
100+ stripped = line .strip ()
101+ if not stripped :
102+ continue
103+ if stripped .startswith ("[" ) and sub_parts [- 1 ]:
104+ sub_parts .append ([stripped ])
105+ else :
106+ sub_parts [- 1 ].append (stripped )
107+ for part in sub_parts :
108+ if not part :
109+ continue
110+ text = " " .join (part )
111+ text = _md_inline (text )
112+ p_html .append (f"<p>{ text } </p>" )
113+
114+ article = (
115+ f"<article>\n <h2>{ _md_inline (title )} </h2>\n "
116+ + "\n " .join (p_html )
117+ + "\n </article>"
118+ )
119+ articles .append (article )
120+
121+ html = f'<div class="changelog" data-changelog-id="{ changelog_id } ">\n '
122+ html += "\n " .join (articles )
123+ html += "\n </div>\n "
124+
125+ out_path .parent .mkdir (parents = True , exist_ok = True )
126+ out_path .write_text (html )
127+ print (f"Wrote changelog.html (changelog-id: { changelog_id } )" )
128+
129+
46130def usage ():
47131 print ("Usage: uv run cli.py <command>\n " )
48132 print ("Commands:" )
@@ -56,7 +140,7 @@ def main():
56140 usage ()
57141
58142 cmd = sys .argv [1 ]
59- {"dev" : dev , "start" : start }[cmd ]()
143+ {"dev" : dev , "start" : start , "changelog" : changelog }[cmd ]()
60144
61145
62146if __name__ == "__main__" :
0 commit comments