Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
23259c5
Fix Markdown.ToMd multi-paragraph blockquote roundtrip and make toolt…
github-actions[bot] Mar 23, 2026
d59ce32
ci: trigger checks
github-actions[bot] Mar 23, 2026
25be64a
fix: use hide-delay so tooltip is accessible even when not immediatel…
github-actions[bot] Mar 25, 2026
519d750
Merge branch 'main' into repo-assist/fix-quotedblock-tooltip-2026-03-…
dsyme Mar 25, 2026
fcef9e2
Merge branch 'main' into repo-assist/fix-quotedblock-tooltip-2026-03-…
dsyme Mar 25, 2026
50466f9
Merge branch 'main' into repo-assist/fix-quotedblock-tooltip-2026-03-…
dsyme Mar 25, 2026
5e82e3b
Merge branch 'main' into repo-assist/fix-quotedblock-tooltip-2026-03-…
dsyme Mar 25, 2026
f7f4aac
Merge branch 'main' into repo-assist/fix-quotedblock-tooltip-2026-03-…
dsyme Mar 26, 2026
012ab36
Merge branch 'main' into repo-assist/fix-quotedblock-tooltip-2026-03-…
dsyme Apr 13, 2026
b3829f6
Merge branch 'main' into repo-assist/fix-quotedblock-tooltip-2026-03-…
dsyme Apr 13, 2026
d57ab6e
Fix Markdown serialization issues and enhance tooltips
dsyme Apr 13, 2026
a4ba60f
fix: resolve duplicate ### Changed subsection in [22.0.0] RELEASE_NOTES
github-actions[bot] Apr 14, 2026
745a776
Merge branch 'main' into repo-assist/fix-quotedblock-tooltip-2026-03-…
dsyme Apr 17, 2026
d03469a
Merge branch 'main' into repo-assist/fix-quotedblock-tooltip-2026-03-…
dsyme May 8, 2026
490aa93
Merge branch 'main' into repo-assist/fix-quotedblock-tooltip-2026-03-…
dsyme May 9, 2026
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
4 changes: 4 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
* Fix crash (`failwith "tbd - IndirectImage"`) when `Markdown.ToMd` is called on a document containing reference-style images (`![alt][ref]`). The indirect image is now serialised as `![alt](url)` when the reference is resolved, or `![alt][ref]` when it is not. [#1094](https://github.com/fsprojects/FSharp.Formatting/pull/1094)
* Fix `Markdown.ToMd` serialising `*emphasis*` (italic) spans as `**...**` (bold) instead of `*...*`. [#1102](https://github.com/fsprojects/FSharp.Formatting/pull/1102)
* Fix `Markdown.ToMd` serialising ordered list items with 0-based numbering and no period (e.g. `0 first`) instead of 1-based with a period (e.g. `1. first`). [#1102](https://github.com/fsprojects/FSharp.Formatting/pull/1102)
* Fix `Markdown.ToMd` serialising a multi-paragraph blockquote as multiple separate blockquotes. The blank separator between paragraphs inside a `QuotedBlock` is now emitted as `>` (an empty blockquote line) instead of a plain blank line, so re-parsing the output yields a single `QuotedBlock` with all paragraphs intact. Also eliminates `> ` lines with trailing whitespace that the previous code produced.

### Changed
* Tooltips in generated documentation are now interactive: moving the mouse from a code token into the tooltip keeps it visible, so users can hover over, select, and copy text from the tooltip. The tooltip is dismissed when the mouse leaves it without returning to the originating token. [#949](https://github.com/fsprojects/FSharp.Formatting/issues/949)

## 22.0.0-alpha.2 - 2026-03-13

Expand Down
18 changes: 18 additions & 0 deletions docs/content/fsdocs-tips.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,28 @@ document.addEventListener('mouseout', function (evt) {
// Only hide when the mouse has left the trigger element entirely
if (target.contains(evt.relatedTarget)) return;
const name = target.dataset.fsdocsTip;
// Keep the tooltip visible when the mouse moves into it β€” this allows the user
// to hover over, select, and copy text from the tooltip.
const tipEl = document.getElementById(name);
if (tipEl && (evt.relatedTarget === tipEl || tipEl.contains(evt.relatedTarget))) return;
const unique = parseInt(target.dataset.fsdocsTipUnique, 10);
hideTip(name);
});

// Hide the tooltip when the mouse leaves it, unless it returns to the trigger element.
document.addEventListener('mouseout', function (evt) {
const tip = evt.target.closest('div.fsdocs-tip');
if (!tip) return;
// Still moving within the tooltip (between child elements) β€” keep it open.
if (tip.contains(evt.relatedTarget)) return;
// Mouse returned to the trigger that opened this tooltip β€” keep it open.
if (evt.relatedTarget) {
const trigger = evt.relatedTarget.closest('[data-fsdocs-tip]');
if (trigger && trigger.dataset.fsdocsTip === tip.id) return;
}
hideTip(tip.id);
});

function Clipboard_CopyTo(value) {
if (navigator.clipboard) {
navigator.clipboard.writeText(value);
Expand Down
13 changes: 11 additions & 2 deletions src/FSharp.Formatting.Markdown/MarkdownUtils.fs
Original file line number Diff line number Diff line change
Expand Up @@ -238,13 +238,22 @@ module internal MarkdownUtils =
| YamlFrontmatter _ -> ()
| Span(body = body) -> yield formatSpans ctx body
| QuotedBlock(paragraphs = paragraphs) ->
for paragraph in paragraphs do
for (i, paragraph) in List.indexed paragraphs do
// Separate paragraphs within the same blockquote using an empty blockquote line.
// A plain blank line would close the blockquote, causing a round-trip failure.
if i > 0 then
yield ">"

let lines = formatParagraph ctx paragraph

// Drop the trailing blank line that formatParagraph normally appends;
// prefixing it with "> " would produce lines with trailing whitespace.
let lines = lines |> List.rev |> List.skipWhile System.String.IsNullOrEmpty |> List.rev

for line in lines do
yield "> " + line

yield ""
yield ""
| _ ->
printfn "// can't yet format %0A to markdown" paragraph
yield "" ]
Expand Down
26 changes: 26 additions & 0 deletions tests/FSharp.Markdown.Tests/Markdown.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1298,6 +1298,32 @@ let ``ToMd preserves a blockquote`` () =
result |> should contain "> "
result |> should contain "This is a quote."

[<Test>]
let ``ToMd preserves a multi-paragraph blockquote as a single blockquote`` () =
// A blockquote with two paragraphs must round-trip as one blockquote, not two.
// The separator between the two paragraphs must keep the ">" prefix so that
// re-parsing does not split it into separate QuotedBlock nodes.
let md = "> First paragraph.\n>\n> Second paragraph."
let result = toMd md
result |> should contain "> First paragraph."
result |> should contain "> Second paragraph."
// Re-parse and confirm we get exactly one QuotedBlock
let reparsed = Markdown.Parse(result)

match reparsed.Paragraphs with
| [ QuotedBlock _ ] -> () // single blockquote β€” correct
| other -> failwith $"Expected a single QuotedBlock but got: %A{other}"

[<Test>]
let ``ToMd blockquote does not produce trailing-whitespace lines`` () =
// "> " (greater-than + space) on its own is an empty blockquote line and has trailing whitespace.
// The serialiser should not emit such lines.
let md = "> A short quote."
let result = toMd md
let lines = result.Split('\n')

lines |> Array.filter (fun l -> l = "> ") |> Array.length |> should equal 0

[<Test>]
let ``ToMd preserves a horizontal rule`` () =
let md = "Before\n\n---\n\nAfter"
Expand Down
Loading