From 23259c5e21f077716661b86822f6978f59af950f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 23 Mar 2026 08:56:55 +0000 Subject: [PATCH 1/5] Fix Markdown.ToMd multi-paragraph blockquote roundtrip and make tooltips interactive Task 3 (fix): Markdown.ToMd - QuotedBlock with multiple paragraphs - A plain blank line between paragraphs in a QuotedBlock closed the blockquote, so re-parsing the output produced separate QuotedBlock nodes instead of one. - Fix: emit an empty blockquote line ('>') as the paragraph separator so the blockquote stays open across paragraph boundaries. - Also eliminates '> ' lines (with trailing whitespace) that the old code produced by stripping the trailing empty line that formatParagraph appends before prefixing. - Added two new tests: roundtrip preserves a single QuotedBlock for multi-paragraph blockquotes; no '> ' trailing-whitespace lines are emitted. Task 10 (forward): Make fsdocs-tips.js tooltips interactive (#949) - When the mouse moves from a code token into the tooltip, the tooltip is now kept visible so users can hover over, select, and copy text. - A new 'mouseout' handler on the tooltip div hides it when the mouse leaves unless it returns to the originating trigger. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- RELEASE_NOTES.md | 4 +++ docs/content/fsdocs-tips.js | 18 +++++++++++++ .../MarkdownUtils.fs | 13 ++++++++-- tests/FSharp.Markdown.Tests/Markdown.fs | 26 +++++++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 02b87c379..3a95fb9eb 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -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 diff --git a/docs/content/fsdocs-tips.js b/docs/content/fsdocs-tips.js index 7bb111290..abeaa1377 100644 --- a/docs/content/fsdocs-tips.js +++ b/docs/content/fsdocs-tips.js @@ -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); 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" From d59ce3260ce8863cc8b651a529172fad6222b0a7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 23 Mar 2026 09:02:24 +0000 Subject: [PATCH 2/5] ci: trigger checks From 25be64ae00060d0bc23479ae93d16c2c6481fa3d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:13:00 +0000 Subject: [PATCH 3/5] fix: use hide-delay so tooltip is accessible even when not immediately adjacent When the tooltip is repositioned to stay within the viewport (e.g. flipped above the cursor or shifted left), there can be a gap between the trigger element and the tooltip. Previously the mouseout handler on the trigger checked relatedTarget immediately; any gap caused the tooltip to hide before the mouse could travel across it. Replace the synchronous hideTip calls in both mouseout handlers with a 300 ms scheduleHide. The pending hide is cancelled whenever the mouse enters the tooltip (mouseover on div.fsdocs-tip) or re-enters the trigger (start of showTip / cancelHide). This is the standard hover-intent pattern and works regardless of tooltip position. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- RELEASE_NOTES.md | 2 +- docs/content/fsdocs-tips.js | 42 ++++++++++++++++++++++++++----------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 3a95fb9eb..f2ec9f9f9 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -10,7 +10,7 @@ * 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) +* 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 abeaa1377..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,12 +90,9 @@ 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); + // 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. @@ -78,12 +101,7 @@ document.addEventListener('mouseout', function (evt) { 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); + scheduleHide(tip.id); }); function Clipboard_CopyTo(value) { From d57ab6ebd6d63673196bd492bce1f72817ff0ab5 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Mon, 13 Apr 2026 23:31:51 +1000 Subject: [PATCH 4/5] Fix Markdown serialization issues and enhance tooltips Fix various issues in Markdown serialization and improve tooltip interactivity in generated documentation. --- RELEASE_NOTES.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 1ec0b293a..68719c90f 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -14,6 +14,7 @@ * Fix `Markdown.ToMd` serialising `HardLineBreak` as a bare newline instead of two trailing spaces + newline. The correct CommonMark representation `" \n"` is now emitted, so hard line breaks survive a round-trip through `ToMd`. * Fix `Markdown.ToMd` serialising `HorizontalRule` as 23 hyphens regardless of the character used in the source. It now emits exactly three characters matching the parsed character (`---`, `***`, or `___`), giving faithful round-trips. * Remove stray `printfn` debug output emitted to stdout when `Markdown.ToMd` encountered an unrecognised paragraph type. +* 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. ### Added * Add tests for `Markdown.ToFsx` (direct serialisation to F# script format), which previously had no unit test coverage. @@ -22,6 +23,7 @@ * Fix `Markdown.ToMd` silently dropping `EmbedParagraphs` nodes: the serialiser now delegates to the node's `Render()` method and formats the resulting paragraphs, consistent with the HTML and LaTeX back-ends. * 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. +* 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] - 2026-04-03 @@ -31,12 +33,8 @@ * Add missing `[]` attribute on `Can include-output-and-it` test so it is executed by the test runner. * 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 `*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) * 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` not preserving indented code blocks: bare code output was re-parsed as a paragraph. Indented code blocks are now serialised as fenced code blocks, which round-trip correctly. From a4ba60f36c4888c7d130d692eab4ef03c4431bb2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 10:53:53 +0000 Subject: [PATCH 5/5] fix: resolve duplicate ### Changed subsection in [22.0.0] RELEASE_NOTES The PR branch had a duplicate '### Changed' section in the [22.0.0] changelog entry, causing Ionide.KeepAChangelog.Tasks to fail with IKC0002 ('Subsection type already exists') on every build. The items in the first '### Changed' (italic/bold/ordered-list/SVG serialisation fixes) properly belong in '### Fixed' per the main branch. Merged them into the single '### Fixed' section and kept the single '### Changed' entry for the menu-precompute optimisation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- RELEASE_NOTES.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 68719c90f..ed24b8123 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -33,8 +33,6 @@ * Add missing `[]` attribute on `Can include-output-and-it` test so it is executed by the test runner. * 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) - -### Changed * 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` not preserving indented code blocks: bare code output was re-parsed as a paragraph. Indented code blocks are now serialised as fenced code blocks, which round-trip correctly.