From 5947e2e5fa8bce6acc90b7f68c60e0bdf3318719 Mon Sep 17 00:00:00 2001 From: mysticmnd Date: Tue, 28 Jul 2026 11:43:07 +0530 Subject: [PATCH 1/2] fix: guard buffer trimming against capture underflow (#432) An empty block whose render trimmed trailing whitespace could delete text written before the enclosing Capture() started, making the captured region length negative and throwing ArgumentOutOfRangeException. Adjacent empty blockquotes, an empty blockquote between paragraphs, and empty-paragraph list items all hit this. TrimTrailingSpaces now stops at the innermost active capture's start offset. --- src/ReverseMarkdown.Test/TestData/cases.json | 20 ++++++++++++++++++ .../Writers/MarkdownWriterBase.cs | 21 +++++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) 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; From 266f7af21ea5d26bd45d02fd5b0b6e3d6c923f40 Mon Sep 17 00:00:00 2001 From: mysticmnd Date: Tue, 28 Jul 2026 12:02:44 +0530 Subject: [PATCH 2/2] test: normalize inline case expectations to platform line endings The converter emits Environment.NewLine, so inline "expected" strings in cases.json (authored with \n) failed on Windows. File-based expectations already get platform endings from the checkout. --- src/ReverseMarkdown.Test/ConverterTests.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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)) {