diff --git a/.github/workflows/build-specs.yml b/.github/workflows/build-specs.yml index 5de93e468539..355593e91f69 100644 --- a/.github/workflows/build-specs.yml +++ b/.github/workflows/build-specs.yml @@ -33,7 +33,8 @@ jobs: python-version: "3.14" cache: 'pip' - - run: pip install bikeshed markdown + - run: sudo apt-get install -y cmark-gfm + - run: pip install bikeshed - run: bikeshed update # The following chunk of code all stolen from andeubotella diff --git a/bin/build-markdown.py b/bin/build-markdown.py index cd8966f126e3..ba08a22a124c 100644 --- a/bin/build-markdown.py +++ b/bin/build-markdown.py @@ -1,11 +1,17 @@ #!/usr/bin/env python3 -"""Convert markdown files in spec directories to HTML.""" +"""Convert markdown files in spec directories to HTML. + +Uses cmark-gfm for full GitHub Flavored Markdown rendering, then +post-processes the output to convert GFM alert syntax (> [!NOTE], etc.) +into styled admonition blocks. +""" import glob import os import re +import subprocess -import markdown +ADMONITION_TYPES = {"NOTE", "TIP", "IMPORTANT", "WARNING", "CAUTION"} TEMPLATE = """\ @@ -17,19 +23,56 @@ pre {{ background: #f4f4f4; padding: 1em; overflow: auto; border-radius: 3px; }} pre code {{ background: none; padding: 0; }} img {{ max-width: 100%; }} +.admonition {{ border-left: 4px solid #888; padding: 0.5em 1em; margin: 1em 0; background: #f8f9fa; border-radius: 3px; }} +.admonition-title {{ font-weight: bold; margin: 0 0 0.25em; }} +.admonition.note, .admonition.tip {{ border-color: #0969da; }} +.admonition.important {{ border-color: #8250df; }} +.admonition.warning, .admonition.caution {{ border-color: #d1242f; }} {body} """ +# cmark-gfm renders > [!NOTE]\n> text as: +#
\n

[!NOTE]\ntext

\n
+# We convert these to styled admonition divs. +ADMONITION_RE = re.compile( + r"
\n

\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\n" + r"(.*?)

\n
", + re.DOTALL, +) + + +def convert_admonitions(html): + def replace(m): + kind = m.group(1).lower() + body = m.group(2) + title = m.group(1).capitalize() + return ( + f'
\n' + f'

{title}

\n' + f"

{body}

\n" + f"
" + ) + return ADMONITION_RE.sub(replace, html) + def extract_title(text): m = re.search(r"^#\s+(.+)", text, re.MULTILINE) return m.group(1).strip() if m else "Untitled" -def main(): - md = markdown.Markdown(extensions=["fenced_code", "tables"]) +def render_markdown(text): + proc = subprocess.run( + ["cmark-gfm", "--extension", "table", "--extension", "autolink", + "--extension", "strikethrough", "--extension", "tasklist"], + input=text, capture_output=True, encoding="utf-8", + ) + if proc.returncode != 0: + raise RuntimeError(f"cmark-gfm failed: {proc.stderr}") + return convert_admonitions(proc.stdout) + +def main(): for md_file in sorted(glob.glob("*/*.md")): if md_file.startswith("."): continue @@ -42,8 +85,7 @@ def main(): text = f.read() title = extract_title(text) - body = md.convert(text) - md.reset() + body = render_markdown(text) with open(html_file, "w", encoding="utf-8") as f: f.write(TEMPLATE.format(title=title, body=body))