Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 2 additions & 4 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,15 @@

## [Unreleased]

### Refactored
* Split `MarkdownParser.fs` (1500 lines) into `MarkdownInlineParser.fs` (inline formatting) and `MarkdownParser.fs` (block-level parsing) for better maintainability. [#1022](https://github.com/fsprojects/FSharp.Formatting/issues/1022)
* Split pipe-table and Emacs-table parsing out of `MarkdownBlockParser.fs` into a new `MarkdownTableParser.fs` (196 lines), reducing `MarkdownBlockParser.fs` from 958 to 760 lines. [#1022](https://github.com/fsprojects/FSharp.Formatting/issues/1022)

### Added
* Add `dotnet fsdocs convert` command to convert a single `.md`, `.fsx`, or `.ipynb` file to HTML (or another output format) without building a full documentation site. [#811](https://github.com/fsprojects/FSharp.Formatting/issues/811)
* `fsdocs convert` now accepts the input file as a positional argument (e.g. `fsdocs convert notebook.ipynb -o notebook.html`). [#1019](https://github.com/fsprojects/FSharp.Formatting/pull/1019)
* `fsdocs convert` infers the output format from the output file extension when `--outputformat` is not specified (e.g. `-o out.md` implies `--outputformat markdown`). [#1019](https://github.com/fsprojects/FSharp.Formatting/pull/1019)
* `fsdocs convert` now accepts `-o` as a shorthand for `--output`. [#1019](https://github.com/fsprojects/FSharp.Formatting/pull/1019)

### Changed
* Tooltip elements (`div.fsdocs-tip`) now use the [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) (Baseline 2024: Chrome 114+, Firefox 125+, Safari 17+). Tooltips are placed in the browser's top layer β€” no `z-index` needed, always above all other content. Fixes a positioning bug where tooltips appeared offset when the page was scrolled. The previous `display`-toggle fallback has been removed. Tooltips also fade in with a subtle animation. [#422](https://github.com/fsprojects/FSharp.Formatting/issues/422), [#1061](https://github.com/fsprojects/FSharp.Formatting/pull/1061)
* Generated code tokens no longer use inline `onmouseover`/`onmouseout` event handlers. Tooltips are now triggered via `data-fsdocs-tip` / `data-fsdocs-tip-unique` attributes and a delegated event listener in `fsdocs-tips.js`. The `popover` attribute is also added to API-doc tooltip divs so they use the same top-layer path. [#1061](https://github.com/fsprojects/FSharp.Formatting/pull/1061)
* Changed `range` fields in `MarkdownSpan` and `MarkdownParagraph` DU cases from `MarkdownRange option` to `MarkdownRange`, using `MarkdownRange.zero` as the default/placeholder value instead of `None`.
* When no template is provided (e.g. `fsdocs convert` without `--template`), `fsdocs-tip` tooltip divs are no longer included in the output. Tooltips require JavaScript/CSS from a template to function, so omitting them produces cleaner raw output. [#1019](https://github.com/fsprojects/FSharp.Formatting/pull/1019)

Expand Down
20 changes: 16 additions & 4 deletions docs/content/fsdocs-default.css
Original file line number Diff line number Diff line change
Expand Up @@ -983,8 +983,6 @@ table.pre, code, pre.fssnip {

/* tooltips */
div.fsdocs-tip {
z-index: 1000;
display: none;
background-color: var(--doc-tip-background);
border-radius: var(--radius);
border: 1px solid var(--header-border);
Expand All @@ -993,16 +991,30 @@ div.fsdocs-tip {
font-variant-ligatures: none;
color: var(--code-color);
box-shadow: 0 1px 1px var(--shadow-color);
margin: 0;

& code {
color: var(--code-color);
}
}

span[onmouseout] {
@keyframes fsdocs-tip-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}

/* Reset UA inset so JS-supplied left/top control position. */
div.fsdocs-tip:popover-open {
position: fixed;
inset: unset;
animation: fsdocs-tip-fade-in 120ms ease-out;
}

[data-fsdocs-tip] {
cursor: pointer;
}


/* API docs */
#content > div > h2:first-child {
margin-top: 0;
Expand Down Expand Up @@ -1212,7 +1224,7 @@ span[onmouseout] {
}

/* Search */
::backdrop {
dialog::backdrop {
background-color: #020202;
opacity: 0.5;
}
Expand Down
67 changes: 44 additions & 23 deletions docs/content/fsdocs-tips.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
let currentTip = null;
let currentTipElement = null;

function hideTip(evt, name, unique) {
function hideTip(name) {
const el = document.getElementById(name);
el.style.display = "none";
if (el) {
try { el.hidePopover(); } catch (_) { }
}
currentTip = null;
currentTipElement = null;
}

function hideUsingEsc(e) {
hideTip(e, currentTipElement, currentTip);
}

function showTip(evt, name, unique, owner) {
document.onkeydown = hideUsingEsc;
function showTip(evt, name, unique) {
if (currentTip === unique) return;

// Hide the previously shown tooltip before showing the new one
if (currentTipElement !== null) {
const prev = document.getElementById(currentTipElement);
if (prev) {
try { prev.hidePopover(); } catch (_) { }
}
}

currentTip = unique;
currentTipElement = name;

Expand All @@ -22,28 +29,45 @@ function showTip(evt, name, unique, owner) {
let y = evt.clientY + offset;

const el = document.getElementById(name);
el.style.position = "absolute";
el.style.display = "block";
el.style.left = `${x}px`;
el.style.top = `${y}px`;
const maxWidth = document.documentElement.clientWidth - x - 16;
el.style.maxWidth = `${maxWidth}px`;
el.style.left = `${x}px`;
el.style.top = `${y}px`;

try { el.showPopover(); } catch (_) { }

const rect = el.getBoundingClientRect();
// Move tooltip if it is out of sight
if(rect.bottom > window.innerHeight) {
const rect = el.getBoundingClientRect();
// Move tooltip if it would appear outside the viewport
if (rect.bottom > window.innerHeight) {
y = y - el.clientHeight - offset;
el.style.top = `${y}px`;
}

if (rect.right > window.innerWidth) {
x = y - el.clientWidth - offset;
x = x - el.clientWidth - offset;
el.style.left = `${x}px`;
const maxWidth = document.documentElement.clientWidth - x - 16;
el.style.maxWidth = `${maxWidth}px`;
el.style.maxWidth = `${document.documentElement.clientWidth - x - 16}px`;
}
}

// Event delegation: trigger tooltips from data-fsdocs-tip attributes
document.addEventListener('mouseover', function (evt) {
const target = evt.target.closest('[data-fsdocs-tip]');
if (!target) return;
const name = target.dataset.fsdocsTip;
const unique = parseInt(target.dataset.fsdocsTipUnique, 10);
showTip(evt, name, unique);
});

document.addEventListener('mouseout', function (evt) {
const target = evt.target.closest('[data-fsdocs-tip]');
if (!target) return;
// 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);
});

function Clipboard_CopyTo(value) {
if (navigator.clipboard) {
navigator.clipboard.writeText(value);
Expand All @@ -57,7 +81,4 @@ function Clipboard_CopyTo(value) {
}
}

window.showTip = showTip;
window.hideTip = hideTip;
// Used by API documentation
window.Clipboard_CopyTo = Clipboard_CopyTo;
window.Clipboard_CopyTo = Clipboard_CopyTo;
9 changes: 2 additions & 7 deletions src/FSharp.Formatting.ApiDocs/GenerateHtml.fs
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,9 @@ type HtmlRender(model: ApiDocModel, ?menuTemplateFolder: string) =
div [] [
let id = UniqueID().ToString()

code
[
OnMouseOut(sprintf "hideTip(event, '%s', %s)" id id)
OnMouseOver(sprintf "showTip(event, '%s', %s)" id id)
]
content
code [ Custom("data-fsdocs-tip", id); Custom("data-fsdocs-tip-unique", id) ] content

div [ Class "fsdocs-tip"; Id id ] tip
div [ Custom("popover", ""); Class "fsdocs-tip"; Id id ] tip
]

let sourceLink url =
Expand Down
39 changes: 11 additions & 28 deletions src/FSharp.Formatting.CodeFormat/HtmlFormatting.fs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ type ToolTipFormatter(prefix) =
let mutable count = 0
let mutable uniqueId = 0

/// Formats tip and returns assignments for 'onmouseover' and 'onmouseout'
member x.FormatTip (tip: ToolTipSpans) overlapping formatFunction =
/// Formats tip and returns data attributes for tooltip triggering
member x.FormatTip (tip: ToolTipSpans) formatFunction =
uniqueId <- uniqueId + 1

let stringIndex =
Expand All @@ -34,32 +34,15 @@ type ToolTipFormatter(prefix) =
count <- count + 1
tips.Add(tip, (count, formatFunction tip))
count
// stringIndex is the index of the tool tip
// uniqueId is globally unique id of the occurrence
if overlapping then
// The <span> may contain other <span>, so we need to
// get the element and check where the mouse goes...
String.Format(
"id=\"{0}t{1}\" onmouseout=\"hideTip(event, '{0}{1}', {2})\" "
+ "onmouseover=\"showTip(event, '{0}{1}', {2}, document.getElementById('{0}t{1}'))\" ",
prefix,
stringIndex,
uniqueId
)
else
String.Format(
"onmouseout=\"hideTip(event, '{0}{1}', {2})\" "
+ "onmouseover=\"showTip(event, '{0}{1}', {2})\" ",
prefix,
stringIndex,
uniqueId
)
// stringIndex is the index of the tool tip div
// uniqueId is the globally unique id of this hover occurrence
String.Format("data-fsdocs-tip=\"{0}{1}\" data-fsdocs-tip-unique=\"{2}\" ", prefix, stringIndex, uniqueId)


/// Returns all generated tool tip elements
member x.WriteTipElements(writer: TextWriter) =
for (KeyValue(_, (index, html))) in tips do
writer.WriteLine(sprintf "<div class=\"fsdocs-tip\" id=\"%s%d\">%s</div>" prefix index html)
writer.WriteLine(sprintf "<div popover class=\"fsdocs-tip\" id=\"%s%d\">%s</div>" prefix index html)


/// Represents context used by the formatter
Expand All @@ -71,7 +54,7 @@ type FormattingContext =
CloseTag: string
OpenLinesTag: string
CloseLinesTag: string
FormatTip: ToolTipSpans -> bool -> (ToolTipSpans -> string) -> string
FormatTip: ToolTipSpans -> (ToolTipSpans -> string) -> string
TokenKindToCss: (TokenKind -> string) }

// --------------------------------------------------------------------------------------
Expand Down Expand Up @@ -106,12 +89,12 @@ let rec formatTokenSpans (ctx: FormattingContext) =
| TokenSpan.Error(_kind, message, body) when ctx.GenerateErrors ->
let tip = ToolTipReader.formatMultilineString (message.Trim().Split('\n'))

let tipAttributes = ctx.FormatTip tip true formatToolTipSpans
let tipAttributes = ctx.FormatTip tip formatToolTipSpans

ctx.Writer.Write("<span ")
ctx.Writer.Write(tipAttributes)
ctx.Writer.Write("class=\"cerr\">")
formatTokenSpans { ctx with FormatTip = fun _ _ _ -> "" } body
formatTokenSpans { ctx with FormatTip = fun _ _ -> "" } body
ctx.Writer.Write("</span>")

| TokenSpan.Error(_, _, body) -> formatTokenSpans ctx body
Expand All @@ -124,7 +107,7 @@ let rec formatTokenSpans (ctx: FormattingContext) =
| TokenSpan.Omitted(body, hidden) ->
let tip = ToolTipReader.formatMultilineString (hidden.Trim().Split('\n'))

let tipAttributes = ctx.FormatTip tip true formatToolTipSpans
let tipAttributes = ctx.FormatTip tip formatToolTipSpans

ctx.Writer.Write("<span ")
ctx.Writer.Write(tipAttributes)
Expand All @@ -136,7 +119,7 @@ let rec formatTokenSpans (ctx: FormattingContext) =
// Generate additional attributes for ToolTip
let tipAttributes =
match tip with
| Some(tip) -> ctx.FormatTip tip false formatToolTipSpans
| Some(tip) -> ctx.FormatTip tip formatToolTipSpans
| _ -> ""

// Get CSS class name of the token
Expand Down
4 changes: 3 additions & 1 deletion tests/fsdocs-tool.Tests/ConvertCommandTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ let ``ConvertCommand omits fsdocs-tip divs when no template given`` () =

result |> shouldEqual 0
let html = File.ReadAllText(outputFile)
html |> shouldNotContainText "fsdocs-tip"
// Tooltip trigger spans use data-fsdocs-tip attributes; the assertion checks that
// the tooltip *div* elements (class="fsdocs-tip") are NOT emitted without a template.
html |> shouldNotContainText "class=\"fsdocs-tip\""

[<Test>]
let ``ConvertCommand converts .ipynb file to HTML`` () =
Expand Down
Loading