Skip to content

Commit 5c5f73b

Browse files
authored
Merge pull request #1147 from fsprojects/repo-assist/fix-tomd-inlinecode-backtick-2026-04-06-26b7575241bce079
[Repo Assist] fix: Markdown.ToMd uses correct backtick fence for inline code containing backticks
2 parents 97b8697 + d6f1b58 commit 5c5f73b

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)