diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index a5592ddf..dd246fe0 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -3,6 +3,7 @@ ## [Unreleased] ### Fixed +* Fix `Markdown.ToMd` dropping link titles in `DirectLink` and `DirectImage` spans. Links with a title attribute (e.g. `[text](url "title")`) now round-trip correctly; without this fix the title was silently discarded on serialisation. * 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. ## [22.0.0] - 2026-04-03 diff --git a/src/FSharp.Formatting.Markdown/MarkdownUtils.fs b/src/FSharp.Formatting.Markdown/MarkdownUtils.fs index 4b6705a0..1246e7fd 100644 --- a/src/FSharp.Formatting.Markdown/MarkdownUtils.fs +++ b/src/FSharp.Formatting.Markdown/MarkdownUtils.fs @@ -106,13 +106,27 @@ module internal MarkdownUtils = | HardLineBreak(_) -> "\n" | AnchorLink _ -> "" + | DirectLink(body, link, title, _) -> + let t = + title + |> Option.map (fun t -> sprintf " \"%s\"" (t.Replace("\"", "\\\""))) + |> Option.defaultValue "" + + "[" + formatSpans ctx body + "](" + link + t + ")" + | IndirectLink(body, _, LookupKey ctx.Links (link, _), _) - | DirectLink(body, link, _, _) | IndirectLink(body, link, _, _) -> "[" + formatSpans ctx body + "](" + link + ")" | IndirectImage(body, _, LookupKey ctx.Links (link, _), _) -> sprintf "![%s](%s)" body link | IndirectImage(body, _, key, _) -> sprintf "![%s][%s]" body key - | DirectImage(body, link, _, _) -> sprintf "![%s](%s)" body link + + | DirectImage(body, link, title, _) -> + let t = + title + |> Option.map (fun t -> sprintf " \"%s\"" (t.Replace("\"", "\\\""))) + |> Option.defaultValue "" + + sprintf "![%s](%s)" body (link + t) | Strong(body, _) -> "**" + formatSpans ctx body + "**" | InlineCode(body, _) -> // Pick the shortest backtick fence that does not appear in the body. diff --git a/tests/FSharp.Markdown.Tests/Markdown.fs b/tests/FSharp.Markdown.Tests/Markdown.fs index bad34a01..4a1ebebb 100644 --- a/tests/FSharp.Markdown.Tests/Markdown.fs +++ b/tests/FSharp.Markdown.Tests/Markdown.fs @@ -1269,10 +1269,32 @@ let ``ToMd preserves a direct link`` () = |> toMd |> should contain "[FSharp](https://fsharp.org)" +[] +let ``ToMd preserves a direct link with title`` () = + let md = "[FSharp](https://fsharp.org \"F# language\")" + let result = toMd md + result |> should contain "[FSharp](" + result |> should contain "https://fsharp.org" + result |> should contain "\"F# language\"" + +[] +let ``ToMd preserves a direct link without title unchanged`` () = + let result = "[link](http://example.com)" |> toMd + result |> should contain "[link](http://example.com)" + result |> should not' (contain "\"") + [] let ``ToMd preserves a direct image`` () = "![alt text](image.png)" |> toMd |> should contain "![alt text](image.png)" +[] +let ``ToMd preserves a direct image with title`` () = + let md = "![photo](image.png \"My Photo\")" + let result = toMd md + result |> should contain "![photo](" + result |> should contain "image.png" + result |> should contain "\"My Photo\"" + [] let ``ToMd preserves an unordered list`` () = let md = "* apple\n* banana\n* cherry"