Skip to content

Commit 9c920ea

Browse files
sideshowbarkersvgeesus
authored andcommitted
Switch markdown conversion from Python-Markdown to cmark-gfm
Use cmark-gfm for full GFM rendering (tables, autolink, strikethrough, tasklists), with a post-processing step that converts GFM alert syntax (> [!NOTE], > [!WARNING], etc.) into styled admonition blocks.
1 parent 199dc16 commit 9c920ea

File tree

2 files changed

+50
-7
lines changed

2 files changed

+50
-7
lines changed

.github/workflows/build-specs.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ jobs:
3333
python-version: "3.14"
3434
cache: 'pip'
3535

36-
- run: pip install bikeshed markdown
36+
- run: sudo apt-get install -y cmark-gfm
37+
- run: pip install bikeshed
3738
- run: bikeshed update
3839

3940
# The following chunk of code all stolen from andeubotella

bin/build-markdown.py

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
#!/usr/bin/env python3
2-
"""Convert markdown files in spec directories to HTML."""
2+
"""Convert markdown files in spec directories to HTML.
3+
4+
Uses cmark-gfm for full GitHub Flavored Markdown rendering, then
5+
post-processes the output to convert GFM alert syntax (> [!NOTE], etc.)
6+
into styled admonition blocks.
7+
"""
38

49
import glob
510
import os
611
import re
12+
import subprocess
713

8-
import markdown
14+
ADMONITION_TYPES = {"NOTE", "TIP", "IMPORTANT", "WARNING", "CAUTION"}
915

1016
TEMPLATE = """\
1117
<!doctype html>
@@ -17,19 +23,56 @@
1723
pre {{ background: #f4f4f4; padding: 1em; overflow: auto; border-radius: 3px; }}
1824
pre code {{ background: none; padding: 0; }}
1925
img {{ max-width: 100%; }}
26+
.admonition {{ border-left: 4px solid #888; padding: 0.5em 1em; margin: 1em 0; background: #f8f9fa; border-radius: 3px; }}
27+
.admonition-title {{ font-weight: bold; margin: 0 0 0.25em; }}
28+
.admonition.note, .admonition.tip {{ border-color: #0969da; }}
29+
.admonition.important {{ border-color: #8250df; }}
30+
.admonition.warning, .admonition.caution {{ border-color: #d1242f; }}
2031
</style>
2132
{body}
2233
"""
2334

35+
# cmark-gfm renders > [!NOTE]\n> text as:
36+
# <blockquote>\n<p>[!NOTE]\ntext</p>\n</blockquote>
37+
# We convert these to styled admonition divs.
38+
ADMONITION_RE = re.compile(
39+
r"<blockquote>\n<p>\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\n"
40+
r"(.*?)</p>\n</blockquote>",
41+
re.DOTALL,
42+
)
43+
44+
45+
def convert_admonitions(html):
46+
def replace(m):
47+
kind = m.group(1).lower()
48+
body = m.group(2)
49+
title = m.group(1).capitalize()
50+
return (
51+
f'<div class="admonition {kind}">\n'
52+
f'<p class="admonition-title">{title}</p>\n'
53+
f"<p>{body}</p>\n"
54+
f"</div>"
55+
)
56+
return ADMONITION_RE.sub(replace, html)
57+
2458

2559
def extract_title(text):
2660
m = re.search(r"^#\s+(.+)", text, re.MULTILINE)
2761
return m.group(1).strip() if m else "Untitled"
2862

2963

30-
def main():
31-
md = markdown.Markdown(extensions=["fenced_code", "tables"])
64+
def render_markdown(text):
65+
proc = subprocess.run(
66+
["cmark-gfm", "--extension", "table", "--extension", "autolink",
67+
"--extension", "strikethrough", "--extension", "tasklist"],
68+
input=text, capture_output=True, encoding="utf-8",
69+
)
70+
if proc.returncode != 0:
71+
raise RuntimeError(f"cmark-gfm failed: {proc.stderr}")
72+
return convert_admonitions(proc.stdout)
3273

74+
75+
def main():
3376
for md_file in sorted(glob.glob("*/*.md")):
3477
if md_file.startswith("."):
3578
continue
@@ -42,8 +85,7 @@ def main():
4285
text = f.read()
4386

4487
title = extract_title(text)
45-
body = md.convert(text)
46-
md.reset()
88+
body = render_markdown(text)
4789

4890
with open(html_file, "w", encoding="utf-8") as f:
4991
f.write(TEMPLATE.format(title=title, body=body))

0 commit comments

Comments
 (0)