"""Converter from CHANGELOG.md (Markdown) to HTML suitable for a mypy blog post. How to use: 1. Write release notes in CHANGELOG.md. 2. Make sure the heading for the next release is of form `## Mypy X.Y`. 2. Run `misc/gen_blog_post_html.py X.Y > target.html`. 4. Manually inspect and tweak the result. Notes: * There are some fragile assumptions. Double check the output. """ import argparse import html import os import re import sys def format_lists(h: str) -> str: a = h.splitlines() r = [] i = 0 bullets = ("- ", "* ", " * ") while i < len(a): if a[i].startswith(bullets): r.append("
')
else:
r.append("")
while i < len(a) and (
(indent and a[i].startswith(" ")) or (not indent and not a[i].startswith("```"))
):
# Undo > and <
line = a[i].replace(">", ">").replace("<", "<")
if indent:
# Undo this extra level of indentation so it looks nice with
# syntax highlighting CSS.
line = line[4:]
r.append(html.escape(line))
i += 1
r.append("
")
if not indent and a[i].startswith("```"):
i += 1
else:
r.append(a[i])
i += 1
formatted = "\n".join(r)
# remove empty first line for code blocks
return re.sub(r"]*)>\n", r"", formatted)
def convert(src: str) -> str:
h = src
# Replace < and >.
h = re.sub(r"<", "<", h)
h = re.sub(r">", ">", h)
# Title
h = re.sub(r"^## (Mypy [0-9.]+)", r"\1 Released
", h, flags=re.MULTILINE)
# Subheadings
h = re.sub(r"\n### ([A-Z`].*)\n", r"\n\1
\n", h)
# Sub-subheadings
h = re.sub(r"\n\*\*([A-Z_`].*)\*\*\n", r"\n\1
\n", h)
h = re.sub(r"\n`\*\*([A-Z_`].*)\*\*\n", r"\n`\1
\n", h)
# Translate `**`
h = re.sub(r"`\*\*`", "**", h)
# Paragraphs
h = re.sub(r"\n\n([A-Z])", r"\n\n\1", h)
# Bullet lists
h = format_lists(h)
# Code blocks
h = format_code(h)
# Code fragments
h = re.sub(r"``([^`]+)``", r"\1", h)
h = re.sub(r"`([^`]+)`", r"\1", h)
# Remove **** noise
h = re.sub(r"\*\*\*\*", "", h)
# Bold text
h = re.sub(r"\*\*([A-Za-z].*?)\*\*", r" \1", h)
# Emphasized text
h = re.sub(r" \*([A-Za-z].*?)\*", r" \1", h)
# Remove redundant PR links to avoid double links (they will be generated below)
h = re.sub(r"\[(#[0-9]+)\]\(https://github.com/python/mypy/pull/[0-9]+/?\)", r"\1", h)
# Issue and PR links
h = re.sub(r"\((#[0-9]+)\) +\(([^)]+)\)", r"(\2, \1)", h)
h = re.sub(
r"fixes #([0-9]+)",
r'fixes issue \1',
h,
)
# Note the leading space to avoid stomping on strings that contain #\d in the middle (such as
# links to PRs in other repos)
h = re.sub(r" #([0-9]+)", r' PR \1', h)
h = re.sub(r"\) \(PR", ", PR", h)
# Markdown links
h = re.sub(r"\[([^]]*)\]\(([^)]*)\)", r'\1', h)
# Add random links in case they are missing
h = re.sub(
r"contributors to typeshed:",
'contributors to typeshed:',
h,
)
# Add top-level HTML tags and headers for syntax highlighting css/js.
# We're configuring hljs to highlight python and bash code. We can remove
# this configure call to make it try all the languages it supports.
h = f"""
{h}
"""
return h
def extract_version(src: str, version: str) -> str:
a = src.splitlines()
i = 0
heading = f"## Mypy {version}"
while i < len(a):
if a[i].strip() == heading:
break
i += 1
else:
raise RuntimeError(f"Can't find heading {heading!r}")
j = i + 1
while not a[j].startswith("## "):
j += 1
return "\n".join(a[i:j])
def main() -> None:
parser = argparse.ArgumentParser(
description="Generate HTML release blog post based on CHANGELOG.md and write to stdout."
)
parser.add_argument("version", help="mypy version, in form X.Y or X.Y.Z")
args = parser.parse_args()
version: str = args.version
if not re.match(r"[0-9]+(\.[0-9]+)+$", version):
sys.exit(f"error: Version must be of form X.Y or X.Y.Z, not {version!r}")
changelog_path = os.path.join(os.path.dirname(__file__), os.path.pardir, "CHANGELOG.md")
src = open(changelog_path).read()
src = extract_version(src, version)
dst = convert(src)
sys.stdout.write(dst)
if __name__ == "__main__":
main()