Skip to content

Commit 9922a43

Browse files
fix: Markdown.ToMd uses minimal backtick fence for inline code with backticks
When InlineCode body contains backtick characters, the previous single-backtick wrapping produced invalid Markdown that would not re-parse as the original inline code span. Fix: compute the longest run of consecutive backticks in the body and use a fence of (run + 1) backticks. When the body starts or ends with a backtick, add surrounding spaces so the fence and body do not merge. Add two round-trip tests covering single-backtick and double-backtick content. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent cd9d1a8 commit 9922a43

3 files changed

Lines changed: 49 additions & 1 deletion

File tree

RELEASE_NOTES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## [Unreleased]
44

5+
### Fixed
6+
* Fix `Markdown.ToMd` serialising inline code spans that contain backtick characters. Previously, `InlineCode` was always wrapped in single backticks, producing syntactically incorrect Markdown when the code body contained backticks. Now the serialiser selects the shortest backtick fence that does not collide with the body content (e.g. a double-backtick fence for bodies containing single backticks, triple for double, etc.), matching the CommonMark spec.
7+
58
## [22.0.0] - 2026-04-03
69

710
### Fixed

src/FSharp.Formatting.Markdown/MarkdownUtils.fs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,28 @@ module internal MarkdownUtils =
114114
| IndirectImage(body, _, key, _) -> sprintf "![%s][%s]" body key
115115
| DirectImage(body, link, _, _) -> sprintf "![%s](%s)" body link
116116
| Strong(body, _) -> "**" + formatSpans ctx body + "**"
117-
| InlineCode(body, _) -> "`" + body + "`"
117+
| InlineCode(body, _) ->
118+
// Pick the shortest backtick fence that does not appear in the body.
119+
// E.g. body "``h``" needs a triple-backtick fence; body "a`b" needs double.
120+
let maxConsecutiveBackticks =
121+
body
122+
|> Seq.fold
123+
(fun (maxR, run) c ->
124+
if c = '`' then
125+
let run' = run + 1
126+
(max maxR run'), run'
127+
else
128+
maxR, 0)
129+
(0, 0)
130+
|> fst
131+
132+
let fence = String.replicate (maxConsecutiveBackticks + 1) "`"
133+
// Surround with spaces when the body starts or ends with a backtick so the
134+
// fence and content do not merge (e.g. `` ``h`` `` would look like 4-backtick).
135+
if body.Length > 0 && (body.[0] = '`' || body.[body.Length - 1] = '`') then
136+
fence + " " + body + " " + fence
137+
else
138+
fence + body + fence
118139
| Emphasis(body, _) -> "*" + formatSpans ctx body + "*"
119140

120141
/// Format a list of MarkdownSpan

tests/FSharp.Markdown.Tests/Markdown.fs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1239,6 +1239,30 @@ let ``ToMd preserves strong (bold) text`` () =
12391239
let ``ToMd preserves inline code`` () =
12401240
"Use `printf` here." |> toMd |> should contain "`printf`"
12411241

1242+
[<Test>]
1243+
let ``ToMd round-trips inline code containing a single backtick`` () =
1244+
// "a`b" must be serialised with a double-backtick fence so it re-parses correctly.
1245+
let original = "`` a`b ``"
1246+
let md = Markdown.Parse original
1247+
let result = Markdown.ToMd md
1248+
// The serialised form must round-trip: re-parsing must yield the same InlineCode body.
1249+
let reparsed = Markdown.Parse result
1250+
1251+
match reparsed.Paragraphs with
1252+
| [ Paragraph([ InlineCode("a`b", _) ], _) ] -> ()
1253+
| _ -> Assert.Fail(sprintf "Expected InlineCode(\"a`b\") after round-trip, got: %A" reparsed.Paragraphs)
1254+
1255+
[<Test>]
1256+
let ``ToMd round-trips inline code containing multiple backticks`` () =
1257+
// Body "``h``" contains double backticks — needs a triple-backtick fence.
1258+
let original = "` ``h`` `"
1259+
let md = Markdown.Parse original
1260+
let result = Markdown.ToMd md
1261+
1262+
match (Markdown.Parse result).Paragraphs with
1263+
| [ Paragraph([ InlineCode("``h``", _) ], _) ] -> ()
1264+
| _ -> Assert.Fail(sprintf "Expected InlineCode(\"``h``\") after round-trip, got: %A" result)
1265+
12421266
[<Test>]
12431267
let ``ToMd preserves a direct link`` () =
12441268
"[FSharp](https://fsharp.org)"

0 commit comments

Comments
 (0)