Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 8 additions & 3 deletions src/FSharp.Formatting.Markdown/MarkdownUtils.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <c>#if SYMBOL</c> / <c>#endif // SYMBOL</c> conditional compilation lines from an .fsx code block
Expand Down
43 changes: 43 additions & 0 deletions tests/FSharp.Markdown.Tests/Markdown.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
// --------------------------------------------------------------------------------------

[<Test>]
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

[<Test>]
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)
Loading