diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index db104a87d..da400a40c 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -2,6 +2,9 @@
## [Unreleased]
+### Added
+* Support top-level `` XML doc tags in API documentation generation. Per the [xmldoc recommended tags](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/recommended-tags#seealso), `` is a top-level section (unlike the inline `` tag) and is now rendered as a "See also" list in both HTML and Markdown API doc output, for both members and entities. [#1256](https://github.com/fsprojects/FSharp.Formatting/issues/1256)
+
### Removed
* Remove `docs/Dockerfile` (used for mybinder.org Binder integration) and mybinder badge links from documentation pages. The Binder integration relied on a deprecated .NET 7 SDK image and a deprecated `Microsoft.dotnet-interactive` version; mybinder.org support is discontinued.
* Remove `.ipynb` (Jupyter Notebook) "run in notebook" badge links from all documentation pages and delete `docs/img/badge-notebook.svg`. The links linked to `.ipynb` outputs generated by fsdocs, but .NET Interactive (which powered those notebooks) is deprecated and has no current replacement. The `.ipynb` output format itself is unchanged.
diff --git a/src/FSharp.Formatting.ApiDocs/ApiDocTypes.fs b/src/FSharp.Formatting.ApiDocs/ApiDocTypes.fs
index 2a88e2a31..afe16183a 100644
--- a/src/FSharp.Formatting.ApiDocs/ApiDocTypes.fs
+++ b/src/FSharp.Formatting.ApiDocs/ApiDocTypes.fs
@@ -129,7 +129,7 @@ type ApiDocHtml(html: string, id: string option) =
member _.Id = id
/// Represents a documentation comment attached to source code
-type ApiDocComment(xmldoc, summary, remarks, parameters, returns, examples, notes, exceptions, rawData) =
+type ApiDocComment(xmldoc, summary, remarks, parameters, returns, examples, notes, exceptions, seeAlso, rawData) =
/// The XElement for the XML doc if available
member _.Xml: XElement option = xmldoc
@@ -155,10 +155,13 @@ type ApiDocComment(xmldoc, summary, remarks, parameters, returns, examples, note
/// The notes sections of the comment
member _.Exceptions: (string * string option * ApiDocHtml) list = exceptions
+ /// The top-level "seealso" sections of the comment, rendered as a "See also:" list
+ member _.SeeAlso: (string * string option * ApiDocHtml) list = seeAlso
+
/// The raw data of the comment
member _.RawData: KeyValuePair list = rawData
- static member internal Empty = ApiDocComment(None, ApiDocHtml("", None), None, [], None, [], [], [], [])
+ static member internal Empty = ApiDocComment(None, ApiDocHtml("", None), None, [], None, [], [], [], [], [])
/// Represents a custom attribute attached to source code
type ApiDocAttribute(name, fullName, constructorArguments, namedConstructorArguments) =
diff --git a/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs b/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs
index 0303f325b..cd132cfd7 100644
--- a/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs
+++ b/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs
@@ -284,6 +284,18 @@ type HtmlRender(model: ApiDocModel, ?menuTemplateFolder: string) =
p [ Class "fsdocs-note" ] [ embed e ]
+ if not m.Comment.SeeAlso.IsEmpty then
+ h5 [ Class "fsdocs-seealso-header" ] [ !!"See also" ]
+
+ ul [ Class "fsdocs-seealso-list" ] [
+ for (nm, link, html) in m.Comment.SeeAlso do
+ li [] [
+ match link with
+ | Some href -> a [ Href href ] [ !!nm ]
+ | None -> embed html
+ ]
+ ]
+
for e in m.Comment.Examples do
h5 [ Class "fsdocs-example-header" ] [ !!"Example" ]
@@ -467,6 +479,18 @@ type HtmlRender(model: ApiDocModel, ?menuTemplateFolder: string) =
p [ Class "fsdocs-note" ] [ embed note ]
+ if not entity.Comment.SeeAlso.IsEmpty then
+ h5 [ Class "fsdocs-seealso-header" ] [ !!"See also" ]
+
+ ul [ Class "fsdocs-seealso-list" ] [
+ for (nm, link, html) in entity.Comment.SeeAlso do
+ li [] [
+ match link with
+ | Some href -> a [ Href href ] [ !!nm ]
+ | None -> embed html
+ ]
+ ]
+
for example in entity.Comment.Examples do
h5 [ Class "fsdocs-example-header" ] [ !!"Example" ]
diff --git a/src/FSharp.Formatting.ApiDocs/GenerateMarkdown.fs b/src/FSharp.Formatting.ApiDocs/GenerateMarkdown.fs
index 5cc2dc388..f4bd063e6 100644
--- a/src/FSharp.Formatting.ApiDocs/GenerateMarkdown.fs
+++ b/src/FSharp.Formatting.ApiDocs/GenerateMarkdown.fs
@@ -122,6 +122,15 @@ type MarkdownRender(model: ApiDocModel, ?menuTemplateFolder: string) =
``#####`` [ !!"Note" ]
p [ embed e ]
+ if not m.Comment.SeeAlso.IsEmpty then
+ ``#####`` [ !!"See also" ]
+
+ ul
+ [ for (nm, url, html) in m.Comment.SeeAlso do
+ [ match url with
+ | Some href -> p [ link [ !!nm ] href ]
+ | None -> p [ embed html ] ] ]
+
for e in m.Comment.Examples do
``#####`` [ !!"Example" ]
p [ embed e ]
@@ -251,6 +260,15 @@ type MarkdownRender(model: ApiDocModel, ?menuTemplateFolder: string) =
``#####`` [ !!"Note" ]
p [ embed note ]
+ if not entity.Comment.SeeAlso.IsEmpty then
+ ``#####`` [ !!"See also" ]
+
+ ul
+ [ for (nm, url, html) in entity.Comment.SeeAlso do
+ [ match url with
+ | Some href -> p [ link [ !!nm ] href ]
+ | None -> p [ embed html ] ] ]
+
for example in entity.Comment.Examples do
``#####`` [ !!"Example" ]
p [ embed example ]
diff --git a/src/FSharp.Formatting.ApiDocs/XmlDocReader.fs b/src/FSharp.Formatting.ApiDocs/XmlDocReader.fs
index 979f1d3bc..03ab56726 100644
--- a/src/FSharp.Formatting.ApiDocs/XmlDocReader.fs
+++ b/src/FSharp.Formatting.ApiDocs/XmlDocReader.fs
@@ -110,6 +110,7 @@ module internal XmlDocReader =
examples = examples,
notes = notes,
exceptions = [],
+ seeAlso = [],
rawData = raw
)
@@ -366,6 +367,41 @@ module internal XmlDocReader =
readXmlElementAsHtml true urlMap cmds html e
cname, None, ApiDocHtml(html.ToString(), None) ]
+ let seeAlso =
+ // Only top-level elements (direct children of the doc comment), per the
+ // xmldoc recommended-tags convention: is a section, is an inline link.
+ let seeAlsoNodes = doc.Elements(XName.Get "seealso") |> Seq.toList
+
+ [ for e in seeAlsoNodes do
+ let cref = e.Attribute(XName.Get "cref")
+
+ if not (isNull cref) then
+ if String.IsNullOrEmpty(cref.Value) || cref.Value.Length < 3 then
+ printfn "Warning: Invalid cref specified in: %A" doc
+ else
+ // Older FSharp.Core cref listings don't start with "T:", see https://github.com/dotnet/fsharp/issues/9805
+ let cname = cref.Value
+ let cname = if cname.Contains(":") then cname else "T:" + cname
+
+ match urlMap.ResolveCref cname with
+ | Some reference ->
+ let html = new StringBuilder()
+ readXmlElementAsHtml true urlMap cmds html e
+ reference.NiceName, Some reference.ReferenceLink, ApiDocHtml(html.ToString(), None)
+ | _ ->
+ let html = new StringBuilder()
+ readXmlElementAsHtml true urlMap cmds html e
+ cname, None, ApiDocHtml(html.ToString(), None)
+ else
+ // no cref: render the element's own content (e.g. text)
+ let html = new StringBuilder()
+ readXmlElementAsHtml true urlMap cmds html e
+ let href = e.Attribute(XName.Get "href")
+
+ let link = if isNull href then None else Some href.Value
+
+ (if isNull href then "" else href.Value), link, ApiDocHtml(html.ToString(), None) ]
+
let examples =
let exampleNodes = doc.Elements(XName.Get "example") |> Seq.toList
@@ -404,7 +440,8 @@ module internal XmlDocReader =
&& ln <> "example"
&& ln <> "note"
&& ln <> "returns"
- && ln <> "remarks")
+ && ln <> "remarks"
+ && ln <> "seealso")
|> Seq.groupBy (fun n -> n.Name.LocalName)
|> Seq.iter (fun (n, lst) ->
let lst = Seq.toList lst
@@ -425,6 +462,7 @@ module internal XmlDocReader =
examples = examples,
notes = notes,
exceptions = exceptions,
+ seeAlso = seeAlso,
rawData = rawData
)
@@ -459,6 +497,7 @@ module internal XmlDocReader =
returns = combineHtmlOptions c1.Returns c2.Returns,
notes = c1.Notes @ c2.Notes,
exceptions = c1.Exceptions @ c2.Exceptions,
+ seeAlso = c1.SeeAlso @ c2.SeeAlso,
rawData = c1.RawData @ c2.RawData
)
diff --git a/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs b/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs
index f4c2b2832..d4fa02a7d 100644
--- a/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs
+++ b/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs
@@ -1636,6 +1636,29 @@ let ``ApiDocs Markdown uses section-based member layout instead of tables`` () =
// No markdown table header for members (the old layout had this)
nestedContent |> shouldNotContainText "Function or value | Description | Source"
+[]
+let ``ApiDocs Markdown generates See also section for top-level seealso tags`` () =
+ let library = testBin > "FsLib2.dll" |> fullpath
+
+ let files = generateApiDocs [ library ] OutputFormat.Markdown false "FsLib2_markdown_seealso"
+
+ let seeAlsoContent = files.["fslib-seealsoexamples.md"]
+
+ seeAlsoContent |> shouldContainText "##### See also"
+ seeAlsoContent |> shouldContainText "dothing"
+ seeAlsoContent |> shouldContainText "dothing2"
+
+[]
+let ``ApiDocs HTML generates See also section for top-level seealso tags`` () =
+ let library = testBin > "FsLib2.dll" |> fullpath
+
+ let files = generateApiDocs [ library ] OutputFormat.Html false "FsLib2_html_seealso"
+
+ let seeAlsoContent = files.["fslib-seealsoexamples.html"]
+
+ seeAlsoContent |> shouldContainText "See also"
+ seeAlsoContent |> shouldContainText "fsdocs-seealso-list"
+
[]
let ``ApiDocs Markdown generates Example and Note section headings`` () =
let library = testBin > "FsLib2.dll" |> fullpath
diff --git a/tests/FSharp.ApiDocs.Tests/files/FsLib2/Library2.fs b/tests/FSharp.ApiDocs.Tests/files/FsLib2/Library2.fs
index 8348c9fee..5e2cc884b 100644
--- a/tests/FSharp.ApiDocs.Tests/files/FsLib2/Library2.fs
+++ b/tests/FSharp.ApiDocs.Tests/files/FsLib2/Library2.fs
@@ -173,6 +173,12 @@ module CommentExamples =
///
let attribInCodeExample () = ()
+module SeeAlsoExamples =
+ /// this does the thing
+ ///
+ ///
+ let dothingwithseealso () = ()
+
/// Base class for testing inherited members (issue #590)
type BaseClassForInheritance() =
/// A documented instance method on the base class