The culprit was this tiny line in the Markdown specification:
If list items are separated by blank lines, Markdown will wrap the items in <p>
tags in the HTML output.
Besides the fact that this is actually ambiguous (Which item is wrapped in <p>
tags? The one before the blank line? The one after it? Both? All of them, once there's a blank line anywhere?) and that different implementations actually handle this differently, here's how the original implementation does it:
The first element is wrapped in <p>
if it's followed by a blank line, the last element when it's preceded by a blank line, and all other ones when there's a blank line at least on one side. All items are wrapped if they contain blank lines.
All well, there's some consistency to this. The implementation, however, tries to express this logic in a regular expression:
(\n)? # leading line = $1
(^[ \t]*) # leading whitespace = $2
($marker_any) [ \t]+ # list marker = $3
((?s:.+?) # list item text = $4
(\n{1,2}))
(?= \n* (\z | \2 ($marker_any) [ \t]+))
When a Markdown list is separated into items, this preserves one or two newline characters at the end (one meaning there's no blank line, two meaning there is), and one newline character at the beginning.
The presence of the double newline at the end means this item is followed by a blank line. The presence of a leading newline on top means this item is preceded by a blank line.
Well, almost. Because now we need three newline characters between the items, two to be captured at the end, and one to be captured at the top.
Well, that's easy:
# Turn double returns into triple returns, so that we can make a
# paragraph for the last item in a list, if necessary:
$list =~ s/\n{2,}/\n\n\n/g;
– and that's why code in lists started growing newlines.
Many Markdown ports to other languages do it the same way – including Attacklab Showdown (SO's client-side Markdown renderer) and MarkdownSharp (the server-side version).
After removing this piece of logic from the regex and instead keeping a little bit of extra state* between the list item processing steps, we could remove the double-triple-conversion.
* appropriately called last_item_had_a_double_newline