diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 72bd4ec10..a02a9adbe 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -5,8 +5,12 @@ ### Fixed * Add regression test confirming that types whose name matches their enclosing namespace are correctly included in generated API docs. [#944](https://github.com/fsprojects/FSharp.Formatting/issues/944) * Fix crash (`failwith "tbd - IndirectImage"`) when `Markdown.ToMd` is called on a document containing reference-style images with bracket syntax. The indirect image is now serialised as `![alt](url)` when the reference is resolved, or in bracket notation when it is not. [#1094](https://github.com/fsprojects/FSharp.Formatting/pull/1094) -* Fix `Markdown.ToMd` serialising italic spans with asterisks incorrectly as bold spans. [#1102](https://github.com/fsprojects/FSharp.Formatting/pull/1102) -* Fix `Markdown.ToMd` serialising ordered list items with incorrect numbering and formatting. [#1102](https://github.com/fsprojects/FSharp.Formatting/pull/1102) +* 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. A short hide-delay ensures that moving the mouse from the symbol to a tooltip that is not immediately adjacent (e.g. repositioned to stay inside the viewport) does not dismiss it prematurely. [#949](https://github.com/fsprojects/FSharp.Formatting/issues/949) [#1106](https://github.com/fsprojects/FSharp.Formatting/pull/1106) ## [22.0.0-alpha.2] - 2026-03-13 diff --git a/docs/content/fsdocs-tips.js b/docs/content/fsdocs-tips.js index 7bb111290..e3776acbb 100644 --- a/docs/content/fsdocs-tips.js +++ b/docs/content/fsdocs-tips.js @@ -1,7 +1,16 @@ let currentTip = null; let currentTipElement = null; +let hideTimer = null; + +function cancelHide() { + if (hideTimer !== null) { + clearTimeout(hideTimer); + hideTimer = null; + } +} function hideTip(name) { + cancelHide(); const el = document.getElementById(name); if (el) { try { el.hidePopover(); } catch (_) { } @@ -10,7 +19,19 @@ function hideTip(name) { currentTipElement = null; } +// Schedule a hide after a short delay so the mouse can travel from the trigger +// to the tooltip (which may have a positional gap) without the tooltip disappearing. +function scheduleHide(name) { + cancelHide(); + hideTimer = setTimeout(function () { + hideTimer = null; + hideTip(name); + }, 300); +} + function showTip(evt, name, unique) { + // Cancel any pending hide so hovering back over the trigger keeps the tooltip open. + cancelHide(); if (currentTip === unique) return; // Hide the previously shown tooltip before showing the new one @@ -51,6 +72,11 @@ function showTip(evt, name, unique) { // Event delegation: trigger tooltips from data-fsdocs-tip attributes document.addEventListener('mouseover', function (evt) { + // Cancel any pending hide when the mouse enters the tooltip itself. + if (evt.target.closest('div.fsdocs-tip')) { + cancelHide(); + return; + } const target = evt.target.closest('[data-fsdocs-tip]'); if (!target) return; const name = target.dataset.fsdocsTip; @@ -64,8 +90,18 @@ 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; - const unique = parseInt(target.dataset.fsdocsTipUnique, 10); - hideTip(name); + // Use a short delay so the mouse can travel across the gap between the trigger + // and the tooltip without the tooltip disappearing. + scheduleHide(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; + scheduleHide(tip.id); }); function Clipboard_CopyTo(value) { diff --git a/src/FSharp.Formatting.Markdown/MarkdownUtils.fs b/src/FSharp.Formatting.Markdown/MarkdownUtils.fs index 59bf09a34..e9883eca0 100644 --- a/src/FSharp.Formatting.Markdown/MarkdownUtils.fs +++ b/src/FSharp.Formatting.Markdown/MarkdownUtils.fs @@ -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 "" ] diff --git a/tests/FSharp.Markdown.Tests/Markdown.fs b/tests/FSharp.Markdown.Tests/Markdown.fs index 984047f7c..3fff1c115 100644 --- a/tests/FSharp.Markdown.Tests/Markdown.fs +++ b/tests/FSharp.Markdown.Tests/Markdown.fs @@ -1298,6 +1298,32 @@ let ``ToMd preserves a blockquote`` () = result |> should contain "> " result |> should contain "This is a quote." +[] +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}" + +[] +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 + [] let ``ToMd preserves a horizontal rule`` () = let md = "Before\n\n---\n\nAfter"