diff --git a/src/ReverseMarkdown.Test/ConverterTests.cs b/src/ReverseMarkdown.Test/ConverterTests.cs index d2b3dd6..7e8a273 100644 --- a/src/ReverseMarkdown.Test/ConverterTests.cs +++ b/src/ReverseMarkdown.Test/ConverterTests.cs @@ -848,7 +848,10 @@ private static IEnumerable ApplyTagFilter(IEnumerable cases) private static string LoadExpected(CaseData testCase) { if (!string.IsNullOrWhiteSpace(testCase.Expected)) { - return testCase.Expected; + // Inline expectations are authored with "\n"; the converter emits the platform + // line ending, so normalize before comparing (file-based expectations get this + // from the checkout). + return testCase.Expected.ReplaceLineEndings(); } if (string.IsNullOrWhiteSpace(testCase.ExpectedFile)) { diff --git a/src/ReverseMarkdown.Test/TestData/cases.json b/src/ReverseMarkdown.Test/TestData/cases.json index 6a2f4d4..b52f08c 100644 --- a/src/ReverseMarkdown.Test/TestData/cases.json +++ b/src/ReverseMarkdown.Test/TestData/cases.json @@ -1168,6 +1168,26 @@ "ListBulletChar": "*" } }, + { + "id": "Bug432_AdjacentEmptyBlockquotes", + "html": "
\n
\n

\n
\n
\n

\n
\n
", + "expected": ">\n\n>" + }, + { + "id": "Bug432_EmptyBlockquoteBetweenParagraphs", + "html": "

before

after

", + "expected": "before\n\n>\n\nafter" + }, + { + "id": "Bug432_EmptyParagraphInNestedBlockquote", + "html": "

a

", + "expected": "> a\n>\n> >" + }, + { + "id": "Bug432_AdjacentEmptyParagraphListItems", + "html": "", + "expected": "- \n\n- " + }, { "id": "SlackFlavored_Bold", "html": "test | test", diff --git a/src/ReverseMarkdown/Writers/MarkdownWriterBase.cs b/src/ReverseMarkdown/Writers/MarkdownWriterBase.cs index d83fd80..19cd7e4 100644 --- a/src/ReverseMarkdown/Writers/MarkdownWriterBase.cs +++ b/src/ReverseMarkdown/Writers/MarkdownWriterBase.cs @@ -814,7 +814,10 @@ protected bool AtWhitespaceBoundary() private protected void TrimTrailingSpaces() { - while (Buffer.Length > 0 && (Buffer[^1] == ' ' || Buffer[^1] == '\n')) + // Never trim below the current capture's start: whitespace before it (such as the + // blank line separating sibling blocks) belongs to the enclosing render, not to + // this node, and removing it would leave the capture with a negative length. + while (Buffer.Length > _captureFloor && (Buffer[^1] == ' ' || Buffer[^1] == '\n')) { Buffer.Length--; } @@ -900,12 +903,26 @@ private static string CollapseWhitespace(string s) return sb.ToString(); } + // Offset in Buffer where the innermost active Capture began; text below it is off limits + // to trimming while that capture renders. Zero when no capture is in progress. + private int _captureFloor; + /// Render via and return the produced text without /// leaving it in the buffer — used for post-processing (e.g. blockquote line prefixes). protected string Capture(System.Action render) { var start = Buffer.Length; - render(); + var outerFloor = _captureFloor; + _captureFloor = start; + try + { + render(); + } + finally + { + _captureFloor = outerFloor; + } + var text = Buffer.ToString(start, Buffer.Length - start); Buffer.Length = start; return text;