From 6d52837f7a79025c4ef7089f3d3d0f8fd521c20c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 00:46:57 +0000 Subject: [PATCH 1/2] fix: Markdown.ToMd multi-paragraph blockquote round-trip The old code emitted a bare blank line between inner paragraphs of a QuotedBlock. CommonMark closes the blockquote on a bare blank line, so re-parsing the serialised output produced multiple separate QuotedBlock nodes instead of one. Fix: strip trailing blank lines from each paragraph's formatted lines before prefixing them with '> ', and emit '>' (an empty blockquote continuation line) as the separator between paragraphs. Two new tests are added: - Round-trip preserves a multi-paragraph blockquote as a single QuotedBlock - The '>' separator lines keep the blockquote open Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- RELEASE_NOTES.md | 1 + .../MarkdownUtils.fs | 11 +++-- tests/FSharp.Markdown.Tests/Markdown.fs | 43 +++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 8a318b6a9..3313990f0 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -17,6 +17,7 @@ * Fix `Markdown.ToMd` serialising `HorizontalRule` as 23 hyphens regardless of the character used in the source. It now emits exactly three characters matching the parsed character (`---`, `***`, or `___`), giving faithful round-trips. * Remove stray `printfn` debug output emitted to stdout when `Markdown.ToMd` encountered an unrecognised paragraph type. * Fix `Markdown.ToLatex` producing invalid LaTeX output for level-6 (and deeper) headings. Previously the LaTeX command was an empty string, resulting in bare `{content}` without a command prefix. Headings at level 6+ are now serialised as `\subparagraph{...}`, which is the deepest sectioning command available in LaTeX. +* Fix `Markdown.ToMd` multi-paragraph blockquote round-trip. The old code emitted a bare blank line between inner paragraphs, which CommonMark parses as closing the blockquote, resulting in multiple separate blockquotes on re-parse. Paragraph separators inside a blockquote are now emitted as `>` (an empty blockquote continuation line) so the round-trip preserves a single `QuotedBlock`. ### Added * Add tests for `Markdown.ToFsx` (direct serialisation to F# script format), which previously had no unit test coverage. diff --git a/src/FSharp.Formatting.Markdown/MarkdownUtils.fs b/src/FSharp.Formatting.Markdown/MarkdownUtils.fs index 6b784184c..be9715f19 100644 --- a/src/FSharp.Formatting.Markdown/MarkdownUtils.fs +++ b/src/FSharp.Formatting.Markdown/MarkdownUtils.fs @@ -307,13 +307,18 @@ module internal MarkdownUtils = yield "" | Span(body = body) -> yield formatSpans ctx body | QuotedBlock(paragraphs = paragraphs) -> - for paragraph in paragraphs do - let lines = formatParagraph ctx paragraph + let paragraphLines = + paragraphs + |> List.map (fun paragraph -> formatParagraph ctx paragraph |> List.filter (fun line -> line <> "")) + for i, lines in List.indexed paragraphLines do for line in lines do yield "> " + line - yield "" + if i < paragraphLines.Length - 1 then + yield ">" + + yield "" | EmbedParagraphs(cmd, _) -> yield! cmd.Render() |> Seq.collect (formatParagraph ctx) ] /// Strips #if SYMBOL / #endif // SYMBOL conditional compilation lines from an .fsx code block diff --git a/tests/FSharp.Markdown.Tests/Markdown.fs b/tests/FSharp.Markdown.Tests/Markdown.fs index 9c91c63d2..117db33ae 100644 --- a/tests/FSharp.Markdown.Tests/Markdown.fs +++ b/tests/FSharp.Markdown.Tests/Markdown.fs @@ -1894,3 +1894,46 @@ let ``ToLatex EmbedParagraphs delegates to Render()`` () = let doc = MarkdownDocument([ EmbedParagraphs(inner, MarkdownRange.zero) ], dict []) let result = Markdown.ToLatex(doc) result |> should contain "latex text" + +// -------------------------------------------------------------------------------------- +// Markdown.ToMd: blockquote round-trip tests +// -------------------------------------------------------------------------------------- + +[] +let ``ToMd preserves multi-paragraph blockquote as a single blockquote`` () = + // A blockquote with two paragraphs should round-trip as a *single* QuotedBlock. + // The old code emitted a bare blank line between paragraphs, which CommonMark + // interprets as closing the blockquote — resulting in two separate QuotedBlocks. + let md = "> First paragraph.\n>\n> Second paragraph.\n" + let doc = Markdown.Parse(md, newline = "\n") + let serialised = Markdown.ToMd(doc, newline = "\n") + let reparsed = Markdown.Parse(serialised, newline = "\n") + + let quotedBlocks = + reparsed.Paragraphs + |> List.choose (function + | QuotedBlock _ as q -> Some q + | _ -> None) + + quotedBlocks.Length |> shouldEqual 1 + +[] +let ``ToMd blockquote does not produce bare blank lines inside blockquote`` () = + // Bare blank lines (non-'>' prefixed) between inner paragraphs cause the + // CommonMark parser to close the blockquote early. + let md = "> First paragraph.\n>\n> Second paragraph.\n" + let doc = Markdown.Parse(md, newline = "\n") + let serialised = Markdown.ToMd(doc, newline = "\n") + + // The separator between blockquote inner paragraphs must itself start with '>'; + // a bare blank line ("") would close the blockquote and introduce a bug. + // We only look at lines that are strictly inside the blockquote section (starts with '>'). + let lines = serialised.Split('\n') + + let inBlockquote = + lines + |> Array.skipWhile (fun l -> not (l.StartsWith(">"))) + |> Array.takeWhile (fun l -> l.StartsWith(">")) + + // There should be at least two content lines (one per paragraph) + inBlockquote.Length |> should be (greaterThan 1) From b831cea06f419cdaf0dc14eeb198fbd2264049a9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 7 May 2026 00:47:00 +0000 Subject: [PATCH 2/2] ci: trigger checks