From f736dbeb318386b1292bf00d0199bc3771d849f7 Mon Sep 17 00:00:00 2001 From: "Jason A. Dönerfield" Date: Fri, 17 Jul 2026 00:38:46 +0200 Subject: md2html: let bullet/1. lists interrupt paragraphs (GitHub-style) Python-Markdown, unlike CommonMark/GitHub, refuses to start a list that immediately follows a paragraph line without an intervening blank line, so a very common README shape renders as one run-on paragraph: Passing: - foo - bar Add a preprocessor that inserts the blank line when a bullet (- * +) or a 1./1) ordered marker follows a paragraph line, restoring GitHub behavior. Only '1' interrupts for ordered lists (matching CommonMark) so prose like '... the year\n1985. was ...' is not turned into a list. Fenced/indented code and lines already inside a list are left untouched. --- filters/html-converters/md2html | 55 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/filters/html-converters/md2html b/filters/html-converters/md2html index de393c7..faa71f3 100755 --- a/filters/html-converters/md2html +++ b/filters/html-converters/md2html @@ -2,8 +2,62 @@ import markdown import sys import io +import re from pygments.formatters import HtmlFormatter from markdown.extensions.toc import TocExtension +from markdown.extensions import Extension +from markdown.preprocessors import Preprocessor + +# Python-Markdown (unlike CommonMark/GitHub) refuses to let a list interrupt a +# paragraph unless there's a blank line between them, so a very common README +# shape renders as one run-on paragraph instead of a list: +# +# Passing: +# - foo +# - bar +# +# This preprocessor restores the GitHub behavior: when a bullet ( - * + ) or a +# "1."/"1)" ordered-list marker immediately follows a paragraph line, insert a +# blank line so the list starts. We deliberately only interrupt on "1" for +# ordered lists (matching CommonMark) so prose like "... the year\n1985. was +# ..." is not turned into a list. Fenced code, indented code, and lines that +# already follow a list item are left untouched. +_MD_FENCE = re.compile(r'^(\s{0,3})(`{3,}|~{3,})') +_MD_BULLET = re.compile(r'^ {0,3}[-*+][ \t]+\S') +_MD_ORDERED1 = re.compile(r'^ {0,3}1[.)][ \t]+\S') +_MD_ANYITEM = re.compile(r'^ {0,3}(?:[-*+]|\d{1,9}[.)])[ \t]') + + +class _ListInterruptPreprocessor(Preprocessor): + def run(self, lines): + out, in_fence, fence = [], False, None + for line in lines: + m = _MD_FENCE.match(line) + if m: + tok = m.group(2)[0] + if not in_fence: + in_fence, fence = True, tok + elif fence == tok: + in_fence, fence = False, None + out.append(line) + continue + if not in_fence and out: + prev = out[-1] + if (prev.strip() and not _MD_ANYITEM.match(prev) + and not prev.startswith((' ', '\t')) + and (_MD_BULLET.match(line) or _MD_ORDERED1.match(line))): + out.append('') + out.append(line) + return out + + +class ListInterruptExtension(Extension): + def extendMarkdown(self, md): + # priority 21: just above the built-in 'normalize_whitespace' (20) so we + # operate on clean lines before block parsing. + md.preprocessors.register( + _ListInterruptPreprocessor(md), 'list_interrupt', 21) + sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8') sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') sys.stdout.write(''' @@ -298,6 +352,7 @@ html = markdown.markdown( "markdown.extensions.codehilite", "markdown.extensions.tables", "markdown.extensions.sane_lists", + ListInterruptExtension(), TocExtension(anchorlink=True)], extension_configs={ "markdown.extensions.codehilite":{"css_class":"highlight"}}) -- cgit v1.3.1-sl0p