From 85c23df8ed1607a494765b3b31e728ef149e8f73 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Wed, 5 Aug 2020 22:23:39 +0100 Subject: [PATCH 1/4] improve xml doc support --- FSharp.Formatting.sln | 2 - RELEASE_NOTES.md | 20 ++ docs/apidocs.fsx | 53 ++- docs/codeformat.fsx | 4 +- docs/content.fsx | 2 +- docs/content/fsdocs-default.css | 4 +- docs/styling.md | 11 +- src/Common/AssemblyInfo.cs | 12 +- src/Common/AssemblyInfo.fs | 12 +- src/FSharp.Formatting.ApiDocs/GenerateHtml.fs | 49 ++- .../GenerateModel.fs | 321 ++++++++++++------ .../CodeFormatAgent.fs | 13 +- .../BuildCommand.fs | 63 ++-- .../FSharp.Formatting.CommandTool.fsproj | 2 +- src/FSharp.Formatting.Common/Templating.fs | 2 +- src/FSharp.Formatting.Literate/Formatting.fs | 2 +- src/FSharp.Formatting.Literate/Literate.fs | 8 +- src/FSharp.Formatting.Literate/ParseScript.fs | 4 +- .../Transformations.fs | 2 +- tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs | 16 + .../files/TestLib1/Library1.fs | 4 +- .../CodeFormatTests.fs | 16 +- version.props | 4 +- 23 files changed, 414 insertions(+), 212 deletions(-) diff --git a/FSharp.Formatting.sln b/FSharp.Formatting.sln index d0a98c534..3ae25a60b 100644 --- a/FSharp.Formatting.sln +++ b/FSharp.Formatting.sln @@ -8,8 +8,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "project", "project", "{194B .dockerignore = .dockerignore .editorconfig = .editorconfig .gitignore = .gitignore - .travis.yml = .travis.yml - appveyor.yml = appveyor.yml build.cmd = build.cmd build.fsx = build.fsx build.sh = build.sh diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index e7dcea652..8ff545f90 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,23 @@ +## 7.2.3 + +- support `...` + +- support `...` + +- support `` + +- allow `` in XML doc comments + +- document XML doc things supported + +## 7.2.2 + +- instruct about settings + +## 7.2.1 + +- fix images in nuget + ## 7.2.0 - include templates diff --git a/docs/apidocs.fsx b/docs/apidocs.fsx index 458d902f2..d5cff319f 100644 --- a/docs/apidocs.fsx +++ b/docs/apidocs.fsx @@ -37,21 +37,58 @@ The HTML is built by instantiating a template. The template used is the first of Usually the same template can be used as for [other content](content.html). -## Classic XML Comments +## Classic XML Doc Comments -XML Comments may use [the normal F# and C# XML doc standards](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/xmldoc/). +XML Doc Comments may use [the normal F# and C# XML doc standards](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/xmldoc/). -An example of an XML documentation comment: +In addition, you may also use + +* `` links + +* Arbitrary paragraph-level HTML such as `` for bold in XML doc text + +* `` for giving summary sections for the enclosing namespace + +* `` and `` to exclude from XML docs + +* `` to give a category for presentation + +An example of an XML documentation comment, assuming the code is in namespace `TheNamespace`: *) /// -/// Some actual comment -/// Another paragraph +/// Some actual comment +/// Another paragraph, see . /// -module Foo2 = - let a = 42 +/// +/// The input +/// +/// The output +/// +/// +/// Try using +/// +/// open TheNamespace +/// SomeModule.a +/// +/// +/// +/// A namespace to remember +/// +/// Foo +/// + +module SomeModule = + let someFunction x = 42 + x -(** +/// +/// A type, see and +/// . +/// +/// +type SomeType() = + member x.P = 1 +(** ## Go to Source links diff --git a/docs/codeformat.fsx b/docs/codeformat.fsx index 69635895d..044f507f5 100644 --- a/docs/codeformat.fsx +++ b/docs/codeformat.fsx @@ -49,7 +49,7 @@ example shows, you can specify which version of `FSharp.Compiler.dll` to use. Processing F# source -------------------- -The formatting agent provides a `ParseSource` method (together with an asynchronous +The formatting agent provides a `ParseAndCheckSource` method (together with an asynchronous version for use from F# and also a version that returns a .NET `Task` for C#). To call the method, we define a simple F# code as a string: *) @@ -58,7 +58,7 @@ let source = """ let hello () = printfn "Hello world" """ -let snippets, errors = formattingAgent.ParseSource("C:\\snippet.fsx", source) +let snippets, errors = formattingAgent.ParseAndCheckSource("C:\\snippet.fsx", source) (** When calling the method, you need to specify a file name and the actual content diff --git a/docs/content.fsx b/docs/content.fsx index 3e82323ed..3f2ccadbd 100644 --- a/docs/content.fsx +++ b/docs/content.fsx @@ -79,7 +79,7 @@ See [Styling](styling.html) for information about template parameters and stylin | `fsdocs-list-of-namespaces` | HTML `
  • ` list of namespaces with links | | `fsdocs-list-of-documents` | HTML `
  • ` list of documents with titles and links | | `fsdocs-page-title` | First h1 heading in literate file. Generated for API docs | -| `fsdocs-source` | Original script source | +| `fsdocs-source` | Original literate script or markdown source | | `fsdocs-tooltips` | Generated hidden div elements for tooltips | diff --git a/docs/content/fsdocs-default.css b/docs/content/fsdocs-default.css index 13fb17c16..0c394af80 100644 --- a/docs/content/fsdocs-default.css +++ b/docs/content/fsdocs-default.css @@ -99,8 +99,8 @@ body { } /* suppress the top line on inner lists such as tables of exceptions */ - #fsdocs-content .table .inner-list td, - #fsdocs-content .table .inner-list th { + #fsdocs-content .table .fsdocs-exception-list td, + #fsdocs-content .table .fsdocs-exception-list th { border-top: 0 } diff --git a/docs/styling.md b/docs/styling.md index a9d58f9a7..4f63aeb47 100644 --- a/docs/styling.md +++ b/docs/styling.md @@ -45,7 +45,16 @@ the standard template. The CSS classes of generated content are: | `.fsdocs-member-tooltip ` | generated tooltips for members | | `.fsdocs-xmldoc ` | generated xmldoc sections | | `.fsdocs-entity-list ` | generated entity lists | -| `.fsdocs-member-list ` | generated member lists | +| `.fsdocs-exception-list ` | generated exception lists | +| `.fsdocs-summary` | the 'summary' section of an XML doc | +| `.fsdocs-remarks` | the 'remarks' section of an XML doc | +| `.fsdocs-params` | the 'parameters' section of an XML doc | +| `.fsdocs-param` | a 'parameter' section of an XML doc | +| `.fsdocs-param-name` | a 'parameter' name of an XML doc | +| `.fsdocs-returns` | the 'returns' section of an XML doc | +| `.fsdocs-example` | the 'example' section of an XML doc | +| `.fsdocs-note` | the 'notes' section of an XML doc | +| `.fsdocs-para` | a paragraph of an XML doc | Some generated elements are given specific HTML ids: diff --git a/src/Common/AssemblyInfo.cs b/src/Common/AssemblyInfo.cs index bf465c9ff..5f00d9687 100644 --- a/src/Common/AssemblyInfo.cs +++ b/src/Common/AssemblyInfo.cs @@ -4,17 +4,17 @@ [assembly: AssemblyProduct("FSharp.Formatting")] [assembly: AssemblyDescription("A package of libraries for building great F# documentation, samples and blogs")] -[assembly: AssemblyVersion("7.1.8")] -[assembly: AssemblyFileVersion("7.1.8")] -[assembly: AssemblyInformationalVersion("7.1.8")] +[assembly: AssemblyVersion("7.2.2")] +[assembly: AssemblyFileVersion("7.2.2")] +[assembly: AssemblyInformationalVersion("7.2.2")] [assembly: AssemblyCopyright("Apache 2.0 License")] namespace System { internal static class AssemblyVersionInformation { internal const System.String AssemblyProduct = "FSharp.Formatting"; internal const System.String AssemblyDescription = "A package of libraries for building great F# documentation, samples and blogs"; - internal const System.String AssemblyVersion = "7.1.8"; - internal const System.String AssemblyFileVersion = "7.1.8"; - internal const System.String AssemblyInformationalVersion = "7.1.8"; + internal const System.String AssemblyVersion = "7.2.2"; + internal const System.String AssemblyFileVersion = "7.2.2"; + internal const System.String AssemblyInformationalVersion = "7.2.2"; internal const System.String AssemblyCopyright = "Apache 2.0 License"; } } diff --git a/src/Common/AssemblyInfo.fs b/src/Common/AssemblyInfo.fs index f15cb2715..6667af244 100644 --- a/src/Common/AssemblyInfo.fs +++ b/src/Common/AssemblyInfo.fs @@ -4,16 +4,16 @@ open System.Reflection [] [] -[] -[] -[] +[] +[] +[] [] do () module internal AssemblyVersionInformation = let [] AssemblyProduct = "FSharp.Formatting" let [] AssemblyDescription = "A package of libraries for building great F# documentation, samples and blogs" - let [] AssemblyVersion = "7.1.8" - let [] AssemblyFileVersion = "7.1.8" - let [] AssemblyInformationalVersion = "7.1.8" + let [] AssemblyVersion = "7.2.2" + let [] AssemblyFileVersion = "7.2.2" + let [] AssemblyInformationalVersion = "7.2.2" let [] AssemblyCopyright = "Apache 2.0 License" diff --git a/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs b/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs index 5e6e09c1b..c66e1a940 100644 --- a/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs +++ b/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs @@ -106,7 +106,7 @@ type HtmlRender(model: ApiDocModel) = ul [] [ for (pname, ptyp) in m.ParameterTooltips do li [] [ - code [] [!! pname] + span [Class "fsdocs-para-name"] [!! pname] !! ":" embed ptyp ] @@ -344,6 +344,11 @@ type HtmlRender(model: ApiDocModel) = let allByCategory = categoriseEntities (nsIndex, ns) [ if allByCategory.Length > 0 then h2 [Id ns.UrlHash] [!! (ns.Name + " Namespace") ] + + match ns.NamespaceSummary with + | Some nsdocs -> div [] [!! nsdocs] + | None -> () + if (allByCategory.Length > 1) then ul [] [ for category in allByCategory do @@ -365,8 +370,8 @@ type HtmlRender(model: ApiDocModel) = [ // For FSharp.Core we make all entries available to other docs else there's not a lot else to show. // - // For nonFSharp.Core we only show one link "API Reference" - if otherDocs && model.Collection.CollectionName <> "FSharp.Core" then + // For non-FSharp.Core we only show one link "API Reference" in the nav menu + if otherDocs && nav && model.Collection.CollectionName <> "FSharp.Core" then li [Class "nav-header"] [!! "API Reference"] li [ Class "nav-item" ] [a [Class "nav-link"; Href (model.IndexFileUrl(root, collectionName, qualify))] [!! "All Namespaces" ] ] else @@ -374,26 +379,33 @@ type HtmlRender(model: ApiDocModel) = let categorise = [ for (nsIndex, ns) in Seq.indexed model.Collection.Namespaces do let allByCategory = categoriseEntities (nsIndex, ns) - allByCategory, ns ] + if allByCategory.Length > 0 then + allByCategory, ns ] - let someExist = categorise |> List.exists (fun (allByCategory, _) -> allByCategory.Length > 0) + let someExist = categorise.Length > 0 if someExist && nav then li [Class "nav-header"] [!! "Namespaces"] - for (nsIndex, ns) in Seq.indexed model.Collection.Namespaces do - let allByCategory = categoriseEntities (nsIndex, ns) - if allByCategory.Length > 0 then - - li [ Class "nav-item" - match nsOpt with - | Some ns2 when ns.Name = ns2.Name -> Class "active" - | _ -> () ] - [a [ Class "nav-link"; - match nsOpt with - | Some ns2 when ns.Name = ns2.Name -> Class "active" - | _ -> () - Href (ns.Url(root, collectionName, qualify))] [!!ns.Name]] + for allByCategory, ns in categorise do + li [ Class ("nav-item" + + // add the 'active' class if this is the namespace of the thing being shown + match nsOpt with + | Some ns2 when ns.Name = ns2.Name -> " active" + | _ -> "") ] + [a [ Class ("nav-link" + + // add the 'active' class if this is the namespace of the thing being shown + match nsOpt with + | Some ns2 when ns.Name = ns2.Name -> " active" + | _ -> "") + Href (ns.Url(root, collectionName, qualify))] [!!ns.Name] + if not nav then + !! " - " + match ns.NamespaceSummary with + | Some nsdocs -> !! nsdocs + | None -> () ] + + // Generate the expanded list of entities if the namespace is the active one match nsOpt with | Some ns2 when ns.Name = ns2.Name -> ul [ Custom ("list-style-type", "none") (* Class "navbar-nav " *) ] [ @@ -419,6 +431,7 @@ type HtmlRender(model: ApiDocModel) = [| yield! parameters yield (ParamKeys.``fsdocs-list-of-namespaces``, toc ) yield (ParamKeys.``fsdocs-content``, content.ToString() ) + yield (ParamKeys.``fsdocs-source``, "" ) yield (ParamKeys.``fsdocs-tooltips``, "" ) yield (ParamKeys.``fsdocs-page-title``, pageTitle ) yield! globalParameters diff --git a/src/FSharp.Formatting.ApiDocs/GenerateModel.fs b/src/FSharp.Formatting.ApiDocs/GenerateModel.fs index 93144ef5c..770d2b54b 100644 --- a/src/FSharp.Formatting.ApiDocs/GenerateModel.fs +++ b/src/FSharp.Formatting.ApiDocs/GenerateModel.fs @@ -418,7 +418,7 @@ type ApiDocEntity /// Represents a namespace integrated with its associated documentation -type ApiDocNamespace(name: string, mods, parameters: Parameters) = +type ApiDocNamespace(name: string, mods, parameters: Parameters, nsdocs: string option) = let urlBaseName = name.Replace(".", "-").ToLower() @@ -442,6 +442,9 @@ type ApiDocNamespace(name: string, mods, parameters: Parameters) = /// All modules in the namespace member x.Entities : ApiDocEntity list = mods + /// The summary text for the namespace + member x.NamespaceSummary : string option = nsdocs + /// The substitution parameters active for generating thist content member x.Parameters = parameters @@ -1133,74 +1136,106 @@ module internal SymbolReader = ApiDocComment(summary, full, sections, raw) - let findCommand = (function + let findCommand cmd = + match cmd with | StringPosition.StartsWithWrapped ("[", "]") (ParseCommand(k, v), _rest) -> Some (k, v) - | _ -> None) - - let readXmlCommentAsHtmlAux all (urlMap: CrossReferenceResolver) (doc: XElement) (cmds: IDictionary<_, _>)= - let rawData = new Dictionary() - let html = new StringBuilder() - let rec readElement (e : XElement) = - for x in e.Nodes() do - if x.NodeType = XmlNodeType.Text then - let text = (x :?> XText).Value - match findCommand (text, MarkdownRange.zero) with - | Some (k,v) -> cmds.Add(k,v) - | None -> html.Append(text) |> ignore - elif x.NodeType = XmlNodeType.Element then - let elem = x :?> XElement - match elem.Name.LocalName with - | "list" -> - html.Append("
      ") |> ignore - readElement elem - html.Append("
    ") |> ignore - | "item" -> - html.Append("
  • ") |> ignore - readElement elem - html.Append("
  • ") |> ignore - | "para" -> - html.Append("

    ") |> ignore - readElement elem - html.Append("

    ") |> ignore - | "see" - | "seealso" -> - let cref = elem.Attribute(XName.Get "cref") - if cref <> null then - if System.String.IsNullOrEmpty(cref.Value) || cref.Value.Length < 3 then - failwithf "Invalid cref specified in: %A" doc - - // 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 + | _ -> None + + let rec readXmlElementAsHtml anyTagsOK (urlMap: CrossReferenceResolver) (cmds: IDictionary<_, _>) (html: StringBuilder) (e : XElement)= + for x in e.Nodes() do + if x.NodeType = XmlNodeType.Text then + let text = (x :?> XText).Value + match findCommand (text, MarkdownRange.zero) with + | Some (k,v) -> cmds.Add(k,v) + | None -> + html.Append(text) |> ignore + elif x.NodeType = XmlNodeType.Element then + let elem = x :?> XElement + match elem.Name.LocalName with + | "list" -> + html.Append("
      ") |> ignore + readXmlElementAsHtml anyTagsOK urlMap cmds html elem + html.Append("
    ") |> ignore + | "item" -> + html.Append("
  • ") |> ignore + readXmlElementAsHtml anyTagsOK urlMap cmds html elem + html.Append("
  • ") |> ignore + | "para" -> + html.Append("

    ") |> ignore + readXmlElementAsHtml anyTagsOK urlMap cmds html elem + html.Append("

    ") |> ignore + | "see" + | "seealso" -> + let cref = elem.Attribute(XName.Get "cref") + if cref <> null then + if System.String.IsNullOrEmpty(cref.Value) || cref.Value.Length < 3 then + printfn "ignoring invalid cref specified in: %A" e + + // 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 -> + match urlMap.ResolveCref cname with + | Some reference -> html.AppendFormat("
    {1}", reference.ReferenceLink, reference.NiceName) |> ignore - | _ -> + | _ -> urlMap.ResolveCref cname |> ignore html.AppendFormat("{0}", cref.Value) |> ignore - | "c" -> - html.Append("") |> ignore - html.Append(elem.Value) |> ignore - html.Append("") |> ignore - | "code" -> - html.Append("
    ") |> ignore
    -                     html.Append(elem.Value) |> ignore
    -                     html.Append("
    ") |> ignore - | _ -> - () - readElement doc + | "c" -> + html.Append("") |> ignore + html.Append(elem.Value) |> ignore + html.Append("") |> ignore + | "code" -> + html.Append("
    ") |> ignore
    +                    html.Append(elem.Value) |> ignore
    +                    html.Append("
    ") |> ignore + // 'a' is not part of the XML doc standard but is widely used + | "a" -> + html.Append(elem.ToString()) |> ignore + // This allows any HTML to be transferred through + | _ -> + if anyTagsOK then html.Append(elem.ToString()) |> ignore + + let readXmlCommentAsHtmlAux summaryExpected all (urlMap: CrossReferenceResolver) (doc: XElement) (cmds: IDictionary<_, _>) = + let rawData = new Dictionary() + let html = new StringBuilder() //full.Append("
    ") |> ignore - let summaries = doc.Descendants(XName.Get "summary") - summaries |> Seq.iteri (fun id e -> - let n = if id = 0 then "summary" else "summary-" + string id - rawData.[n] <- e.Value - html.Append("

    ") |> ignore - readElement e - html.Append("

    ") |> ignore - ) + // not part of the XML doc standard + let nsdocs = + let ds = doc.Descendants(XName.Get "namespacesummary") + if Seq.length ds > 0 then + Some + ([ for d in ds -> + let html = new StringBuilder() + readXmlElementAsHtml true urlMap cmds html d + html.ToString() ] |> String.concat "\n") + else + None + + begin + if summaryExpected then + let summaries = doc.Descendants(XName.Get "summary") + summaries |> Seq.iteri (fun id e -> + let n = if id = 0 then "summary" else "summary-" + string id + rawData.[n] <- e.Value + html.Append("

    ") |> ignore + readXmlElementAsHtml true urlMap cmds html e + html.Append("

    ") |> ignore + ) + else + readXmlElementAsHtml false urlMap cmds html doc + end + + for e in doc.Descendants(XName.Get "exclude") do + cmds.Add("exclude", e.Value) + + for e in doc.Descendants(XName.Get "omit") do + cmds.Add("omit", e.Value) + + for e in doc.Descendants(XName.Get "category") do + cmds.Add("category", e.Value) if all then let remarkNodes = doc.Descendants(XName.Get "remarks") @@ -1209,20 +1244,20 @@ module internal SymbolReader = remarkNodes |> Seq.iteri (fun id e -> let n = if id = 0 then "remarks" else "remarks-" + string id rawData.[n] <- e.Value - html.Append("

    ") |> ignore - readElement e + html.Append("

    ") |> ignore + readXmlElementAsHtml true urlMap cmds html e html.Append("

    ") |> ignore ) let paramNodes = doc.Descendants(XName.Get "param") if Seq.length paramNodes > 0 then //full.Append("

    Parameters

    ") |> ignore - html.Append("
    ") |> ignore + html.Append("
    ") |> ignore for e in paramNodes do let name = e.Attribute(XName.Get "name").Value rawData.["param-" + name] <- e.Value - html.AppendFormat("
    {0}

    ", name) |> ignore - readElement e + html.AppendFormat("

    {0}

    ", name) |> ignore + readXmlElementAsHtml true urlMap cmds html e html.AppendFormat("

    ") |> ignore html.Append("
    ") |> ignore @@ -1230,17 +1265,17 @@ module internal SymbolReader = returns |> Seq.iteri (fun id e -> let n = if id = 0 then "returns" else "returns-" + string id - html.Append("

    ") |> ignore + html.Append("

    ") |> ignore rawData.[n] <- e.Value html.AppendFormat("

    Returns: ") |> ignore - readElement e + readXmlElementAsHtml true urlMap cmds html e html.Append("

    ") |> ignore ) let exceptions = doc.Descendants(XName.Get "exception") if Seq.length exceptions > 0 then //html.Append("

    Exceptions:

    ") |> ignore - html.Append("") |> ignore + html.Append("
    ") |> ignore for e in exceptions do let cref = e.Attribute(XName.Get "cref") if cref <> null then @@ -1255,19 +1290,45 @@ module internal SymbolReader = | Some reference -> rawData.["exception-" + reference.NiceName] <- reference.ReferenceLink html.AppendFormat("") |> ignore | _ -> - html.AppendFormat("", cref.Value) |> ignore + html.AppendFormat("", cref.Value) |> ignore html.Append("
    {1}", reference.ReferenceLink, reference.NiceName) |> ignore - readElement e + readXmlElementAsHtml true urlMap cmds html e html.AppendFormat("
    UNRESOLVED({0})
    {0}
    ") |> ignore + let examples = doc.Descendants(XName.Get "example") + examples |> Seq.iteri (fun id e -> + let n = if id = 0 then "example" else "example-" + string id + + rawData.[n] <- e.Value + html.AppendFormat("
    Example
    ") |> ignore + html.Append("

    ") |> ignore + readXmlElementAsHtml true urlMap cmds html e + html.Append("

    ") |> ignore + ) + // 'note' is not part of the XML doc standard but is supported by Sandcastle and other tools + let notes = doc.Descendants(XName.Get "note") + notes |> Seq.iteri (fun id e -> + let n = if id = 0 then "note" else "note-" + string id + + rawData.[n] <- e.Value + html.AppendFormat("
    Note
    ") |> ignore + html.Append("

    ") |> ignore + readXmlElementAsHtml true urlMap cmds html e + html.Append("

    ") |> ignore + ) + + + // put the non-xmldoc sections into rawData doc.Descendants () |> Seq.filter (fun n -> let ln = n.Name.LocalName ln <> "summary" && ln <> "param" && ln <> "exceptions" && + ln <> "example" && + ln <> "note" && ln <> "returns" && ln <> "remarks" ) |> Seq.groupBy (fun n -> n.Name.LocalName) @@ -1282,13 +1343,13 @@ module internal SymbolReader = ) let str = ApiDocHtml(html.ToString()) - str, rawData + str, rawData, nsdocs let readXmlCommentAsHtml (urlMap : CrossReferenceResolver) (doc : XElement) (cmds: IDictionary<_, _>) = - let summary, _raw = readXmlCommentAsHtmlAux false urlMap doc cmds - let full, rawData = readXmlCommentAsHtmlAux true urlMap doc cmds + let summary, _raw, nsdocs = readXmlCommentAsHtmlAux true false urlMap doc cmds + let full, rawData, _ = readXmlCommentAsHtmlAux true true urlMap doc cmds let raw = rawData |> Seq.toList - ApiDocComment(summary, full, [KeyValuePair("", full)], raw) + ApiDocComment(summary, full, [KeyValuePair("", full)], raw), nsdocs /// Returns all indirect links in a specified span node let rec collectSpanIndirectLinks span = @@ -1360,23 +1421,24 @@ module internal SymbolReader = doc.With(paragraphs=replacedParagraphs) let readCommentAndCommands (ctx:ReadingContext) xmlSig = + let cmds = Dictionary() :> IDictionary<_,_> match ctx.XmlMemberLookup(xmlSig) with | None -> if not (System.String.IsNullOrEmpty xmlSig) then Log.verbf "Could not find documentation for '%s'! (You can ignore this message when you have not written documentation for this member)" xmlSig - dict[], ApiDocComment.Empty + cmds, ApiDocComment.Empty, None | Some el -> let sum = el.Element(XName.Get "summary") match sum with | null when String.IsNullOrEmpty el.Value -> - dict[], ApiDocComment.Empty + cmds, ApiDocComment.Empty, None | null -> - let summary, _raw = readXmlCommentAsHtmlAux false ctx.UrlMap el (dict []) - let all, _raw = readXmlCommentAsHtmlAux true ctx.UrlMap el (dict []) - dict[], (ApiDocComment(summary, all, [KeyValuePair("", summary)], [])) + // We let through XML comments without a summary tag + let summary, _raw, nsdocs = readXmlCommentAsHtmlAux false false ctx.UrlMap el cmds + let all, _raw, _ = readXmlCommentAsHtmlAux false true ctx.UrlMap el cmds + cmds, (ApiDocComment(summary, all, [KeyValuePair("", summary)], [])), nsdocs | sum -> let lines = removeSpaces sum.Value |> List.map (fun s -> (s, MarkdownRange.zero)) - let cmds = Dictionary() if ctx.MarkdownComments then let text = @@ -1396,27 +1458,29 @@ module internal SymbolReader = let doc = doc |> addMissingLinkToTypes ctx let html = readMarkdownCommentAsHtml doc - cmds :> IDictionary<_, _>, html + let nsdocs = None // TODO: namespace summaries for markdown + cmds, html, nsdocs else - let html = readXmlCommentAsHtml ctx.UrlMap el cmds + let html, nsdocs = readXmlCommentAsHtml ctx.UrlMap el cmds lines |> Seq.choose findCommand |> Seq.iter (fun (k, v) -> cmds.Add(k,v)) - cmds :> IDictionary<_, _>, html + cmds, html, nsdocs /// Reads XML documentation comments and calls the specified function /// to parse the rest of the entity, unless [omit] command is set. /// The function is called with category name, commands & comment. let readCommentsInto (sym:FSharpSymbol) ctx xmlDocSig f = - let cmds, comment = readCommentAndCommands ctx xmlDocSig + let cmds, comment, nsdocs = readCommentAndCommands ctx xmlDocSig match cmds with + | Command "exclude" _ -> None | Command "omit" _ -> None | Command "category" cat | Let "" (cat, _) -> try - Some(f cat cmds comment) + Some(f cat cmds comment, nsdocs) with e -> let name = try sym.FullName @@ -1437,6 +1501,19 @@ module internal SymbolReader = let checkAccess ctx (access: FSharpAccessibility) = not ctx.PublicOnly || access.IsPublic + + let combineNamespaceDocs nspDocs = + nspDocs + |> List.choose id + |> function + | [] -> None + | xs -> Some (String.concat "\n" xs) + + let collectNamespaceDocs results = + results + |> List.unzip + |> function (results, nspDocs) -> (results, combineNamespaceDocs nspDocs) + let readChildren ctx (entities:seq) reader cond = entities |> Seq.filter (fun v -> checkAccess ctx v.Accessibility) @@ -1444,6 +1521,7 @@ module internal SymbolReader = |> Seq.sortBy (fun (c:FSharpEntity) -> c.DisplayName) |> Seq.choose (reader ctx) |> List.ofSeq + |> collectNamespaceDocs let tryReadMember (ctx:ReadingContext) entityUrl kind (memb:FSharpMemberOrFunctionOrValue) = readCommentsInto memb ctx (getXmlDocSigForMember memb) (fun cat _ comment -> @@ -1457,14 +1535,19 @@ module internal SymbolReader = not v.IsCompilerGenerated && not v.IsPropertyGetterMethod && not v.IsPropertySetterMethod && not v.IsEventAddMethod && not v.IsEventRemoveMethod) - |> Seq.choose (tryReadMember ctx entityUrl kind) |> List.ofSeq + |> Seq.choose (tryReadMember ctx entityUrl kind) + |> List.ofSeq + |> collectNamespaceDocs let readMembers ctx entityUrl kind (entity:FSharpEntity) cond = entity.MembersFunctionsAndValues |> Seq.filter (fun v -> checkAccess ctx v.Accessibility) |> Seq.filter (fun v -> not v.IsCompilerGenerated) - |> Seq.filter cond |> Seq.choose (tryReadMember ctx entityUrl kind) |> List.ofSeq - + |> Seq.filter cond + |> Seq.choose (tryReadMember ctx entityUrl kind) + |> List.ofSeq + |> collectNamespaceDocs + let readTypeNameAsText (typ:FSharpEntity) = typ.GenericParameters |> List.ofSeq @@ -1486,6 +1569,7 @@ module internal SymbolReader = readCommentsInto case ctx case.XmlDocSig (fun cat _ comment -> let details = readUnionCase ctx typ case ApiDocMember(case.Name, readAttributes case.Attributes, entityUrl, ApiDocMemberKind.UnionCase, cat, details, comment, case))) + |> collectNamespaceDocs let readRecordFields ctx entityUrl (typ:FSharpEntity) = typ.FSharpFields @@ -1495,6 +1579,7 @@ module internal SymbolReader = readCommentsInto field ctx field.XmlDocSig (fun cat _ comment -> let details = readFSharpField ctx field ApiDocMember(field.Name, readAttributes (Seq.append field.FieldAttributes field.PropertyAttributes), entityUrl, ApiDocMemberKind.RecordField, cat, details, comment, field))) + |> collectNamespaceDocs let readStaticParams ctx entityUrl (typ:FSharpEntity) = typ.StaticParameters @@ -1503,6 +1588,7 @@ module internal SymbolReader = readCommentsInto staticParam ctx (getFSharpStaticParamXmlSig typ staticParam.Name) (fun cat _ comment -> let details = readFSharpStaticParam ctx staticParam ApiDocMember(staticParam.Name, [], entityUrl, ApiDocMemberKind.StaticParameter, cat, details, comment, staticParam))) + |> collectNamespaceDocs // Create a xml documentation snippet and add it to the XmlMemberMap let registerXmlDoc (ctx:ReadingContext) xmlDocSig (xmlDoc:string) = @@ -1525,11 +1611,7 @@ module internal SymbolReader = let xmlDocSig = getFSharpStaticParamXmlSig typ name registerXmlDoc ctx xmlDocSig (Security.SecurityElement.Escape xmlDoc) |> ignore) - let rec readEntities ctx (entities:seq<_>) = - readChildren ctx entities readModule (fun x -> x.IsFSharpModule) @ - readChildren ctx entities readType (fun x -> not x.IsFSharpModule) - - and readType (ctx:ReadingContext) (typ:FSharpEntity) = + let rec readType (ctx:ReadingContext) (typ:FSharpEntity) = if typ.IsProvided && typ.XmlDoc.Count > 0 then registerTypeProviderXmlDocs ctx typ @@ -1543,9 +1625,11 @@ module internal SymbolReader = match typ.BaseType with | Some baseType -> //TODO: would be better to reuse instead of reparsing the base type xml docs - let cmds, _comment = readCommentAndCommands ctx (getXmlDocSigForType baseType.TypeDefinition) + let cmds, _comment, _ = readCommentAndCommands ctx (getXmlDocSigForType baseType.TypeDefinition) match cmds with - | Command "omit" _ -> yield! getMembers baseType.TypeDefinition + | Command "exclude" _ + | Command "omit" _ -> + yield! getMembers baseType.TypeDefinition | _ -> () | None -> () ] @@ -1568,17 +1652,20 @@ module internal SymbolReader = let delegateSignature = if typ.IsDelegate then Some (formatDelegateSignatureAsHtml ctx.UrlMap typ.DisplayName typ.FSharpDelegateSignature |> codeHtml) else None let name = readTypeNameAsText typ - let cases = readUnionCases ctx entityUrl typ - let fields = readRecordFields ctx entityUrl typ - let statParams = readStaticParams ctx entityUrl typ + let cases, nsdocs1 = readUnionCases ctx entityUrl typ + let fields, nsdocs2 = readRecordFields ctx entityUrl typ + let statParams, nsdocs3 = readStaticParams ctx entityUrl typ let attrs = readAttributes typ.Attributes - let ctors = readAllMembers ctx entityUrl ApiDocMemberKind.Constructor cvals - let inst = readAllMembers ctx entityUrl ApiDocMemberKind.InstanceMember ivals - let stat = readAllMembers ctx entityUrl ApiDocMemberKind.StaticMember svals + let ctors, nsdocs4 = readAllMembers ctx entityUrl ApiDocMemberKind.Constructor cvals + let inst, nsdocs5 = readAllMembers ctx entityUrl ApiDocMemberKind.InstanceMember ivals + let stat, nsdocs6 = readAllMembers ctx entityUrl ApiDocMemberKind.StaticMember svals let rqa = hasAttrib typ.Attributes + let nsdocs = combineNamespaceDocs [nsdocs1; nsdocs2; nsdocs3; nsdocs4; nsdocs5; nsdocs6 ] + if nsdocs.IsSome then + printfn "ignoring namespace summary on nested position" ApiDocEntity (true, name, cat, entityUrl, comment, ctx.Assembly, attrs, cases, fields, statParams, ctors, inst, stat, allInterfaces, baseType, abbreviatedType, delegateSignature, typ, [], [], [], [], rqa, ctx.Parameters)) @@ -1587,19 +1674,27 @@ module internal SymbolReader = // Properties & value bindings in the module let entityUrl = ctx.UrlMap.TryResolveUrlBaseNameForEntity modul - let vals = readMembers ctx entityUrl ApiDocMemberKind.ValueOrFunction modul (fun v -> not v.IsMember && not v.IsActivePattern) - let exts = readMembers ctx entityUrl ApiDocMemberKind.TypeExtension modul (fun v -> v.IsExtensionMember) - let pats = readMembers ctx entityUrl ApiDocMemberKind.ActivePattern modul (fun v -> v.IsActivePattern) + let vals, nsdocs1 = readMembers ctx entityUrl ApiDocMemberKind.ValueOrFunction modul (fun v -> not v.IsMember && not v.IsActivePattern) + let exts, nsdocs2 = readMembers ctx entityUrl ApiDocMemberKind.TypeExtension modul (fun v -> v.IsExtensionMember) + let pats, nsdocs3 = readMembers ctx entityUrl ApiDocMemberKind.ActivePattern modul (fun v -> v.IsActivePattern) let attrs = readAttributes modul.Attributes // Nested modules and types - let entities = readEntities ctx modul.NestedEntities + let entities, nsdocs4 = readEntities ctx modul.NestedEntities let rqa = hasAttrib modul.Attributes + let nsdocs = combineNamespaceDocs [nsdocs1; nsdocs2; nsdocs3; nsdocs4] + if nsdocs.IsSome then + printfn "ignoring namespace summary on nested position" ApiDocEntity ( false, modul.DisplayName, cat, entityUrl, comment, ctx.Assembly, attrs, [], [], [], [], [], [], [], None, None, None, modul, entities, vals, exts, pats, rqa, ctx.Parameters )) + and readEntities ctx (entities:seq<_>) = + let mods, nsdocs1 = readChildren ctx entities readModule (fun x -> x.IsFSharpModule) + let typs, nsdocs2 = readChildren ctx entities readType (fun x -> not x.IsFSharpModule) + (mods @ typs), combineNamespaceDocs [nsdocs1; nsdocs2] + // ---------------------------------------------------------------------------------------------- // Reading namespace and assembly details // ---------------------------------------------------------------------------------------------- @@ -1613,8 +1708,8 @@ module internal SymbolReader = str let readNamespace ctx (ns, entities:seq) = - let entities = readEntities ctx entities - ApiDocNamespace(stripMicrosoft ns, entities, ctx.Parameters) + let entities, nsdocs = readEntities ctx entities + ApiDocNamespace(stripMicrosoft ns, entities, ctx.Parameters, nsdocs) let readAssembly (assembly:FSharpAssembly, publicOnly, xmlFile:string, parameters, sourceFolderRepo, urlRangeHighlight, mdcomments, urlMap, codeFormatCompilerArgs) = let assemblyName = AssemblyName(assembly.QualifiedName) @@ -1820,17 +1915,17 @@ type ApiDocModel = |> Some) // Union namespaces from multiple libraries - let namespaces = Dictionary<_, _>() + let namespaces = Dictionary<_, (_ * _ * Parameters)>() for _, nss in assemblies do for ns in nss do match namespaces.TryGetValue(ns.Name) with - | true, (entities, parameters) -> namespaces.[ns.Name] <- (entities @ ns.Entities, parameters) - | false, _ -> namespaces.Add(ns.Name, (ns.Entities, ns.Parameters)) + | true, (entities, summary, parameters) -> namespaces.[ns.Name] <- (entities @ ns.Entities, combineNamespaceDocs [ns.NamespaceSummary; summary], parameters) + | false, _ -> namespaces.Add(ns.Name, (ns.Entities, ns.NamespaceSummary, ns.Parameters)) let namespaces = - [ for (KeyValue(name, (entities, parameters))) in namespaces do + [ for (KeyValue(name, (entities, summary, parameters))) in namespaces do if entities.Length > 0 then - ApiDocNamespace(name, entities, parameters) ] + ApiDocNamespace(name, entities, parameters, summary) ] let collection = ApiDocCollection(collectionName, List.map fst assemblies, namespaces |> List.sortBy (fun ns -> ns.Name)) diff --git a/src/FSharp.Formatting.CodeFormat/CodeFormatAgent.fs b/src/FSharp.Formatting.CodeFormat/CodeFormatAgent.fs index f6cda7bec..3c1910cde 100644 --- a/src/FSharp.Formatting.CodeFormat/CodeFormatAgent.fs +++ b/src/FSharp.Formatting.CodeFormat/CodeFormatAgent.fs @@ -299,6 +299,7 @@ type CodeFormatAgent() = not <| item.EndsWith "mscorlib.dll") //Log.verbf "getting project options ('%s', \"\"\"%s\"\"\", now, args, assumeDotNetFramework = false): \n\t%s" filePath source (System.String.Join("\n\t", args))// fscore + let filePath = Path.GetFullPath(filePath) let! (opts,_errors) = fsChecker.GetProjectOptionsFromScript(filePath, SourceText.ofString source, loadedTimeStamp = DateTime.Now, otherFlags = args, assumeDotNetFramework = false) let formatError (e:FSharpErrorInfo) = @@ -355,11 +356,15 @@ type CodeFormatAgent() = if _errors |> List.filter (fun e -> e.Severity = FSharpErrorSeverity.Error) |> List.length > 0 then Log.warnf "errors from GetProjectOptionsFromScript '%s'" (formatErrors _errors) + //printfn "filePath = %A" filePath + ////printfn "opts = %A" opts + //for o in opts.OtherOptions do + // printfn "opt: %s" o + // Run the second phase - perform type checking Log.verbf "starting to ParseAndCheckDocument from '%s'" filePath let! res = fsChecker.ParseAndCheckDocument(filePath, source,opts,false) - //printfn "opts = %A" source //printfn "source = %A" source //fsChecker.InvalidateConfiguration(opts) //results. @@ -450,8 +455,8 @@ type CodeFormatAgent() = /// Parse, check and annotate the source code specified by 'source', assuming that it /// is located in a specified 'file'. Optional arguments can be used /// to give compiler command line options and preprocessor definitions - member __.AsyncParseSource(file, source, ?options, ?defines) = async { - let! res = agent.PostAndAsyncReply(fun chnl -> (file, source, options, defines), chnl) + member __.AsyncParseAndCheckSource(filePath, source, ?options, ?defines) = async { + let! res = agent.PostAndAsyncReply(fun chnl -> (filePath, source, options, defines), chnl) match res with | Choice1Of2 res -> return res | Choice2Of2 exn -> return Helpers.ediRaise exn } @@ -459,7 +464,7 @@ type CodeFormatAgent() = /// Parse, check and annotate the source code specified by 'source', assuming that it /// is located in a specified 'file'. Optional arguments can be used /// to give compiler command line options and preprocessor definitions - member __.ParseSource(file, source, ?options, ?defines) = + member __.ParseAndCheckSource(file, source, ?options, ?defines) = let res = agent.PostAndReply(fun chnl -> (file, source, options, defines), chnl) match res with | Choice1Of2 res -> res diff --git a/src/FSharp.Formatting.CommandTool/BuildCommand.fs b/src/FSharp.Formatting.CommandTool/BuildCommand.fs index ad661fc80..0d9fd0c44 100644 --- a/src/FSharp.Formatting.CommandTool/BuildCommand.fs +++ b/src/FSharp.Formatting.CommandTool/BuildCommand.fs @@ -332,10 +332,14 @@ module Crack = printfn "Error while cracking project files, no project files succeeded, exiting." exit 1 - let param (ParamKey tag as key) v = + let param setting (ParamKey tag as key) v = match v with | Some v -> (key, v) - | None -> (key, "{{" + tag + "}}") + | None -> + match setting with + | Some setting -> printfn "please set '%s' in 'Directory.Build.props'" setting + | None _ -> () + (key, "{{" + tag + "}}") // For the 'docs' directory we use the best info we can find from across all projects let projectInfoForDocs = @@ -374,28 +378,28 @@ module Crack = defaultArg userRoot (defaultArg projectUrl ("/" + collectionName) |> ensureTrailingSlash) let parametersForProjectInfo (info: CrackedProjectInfo) = - let projectUrl = info.PackageProjectUrl |> Option.map ensureTrailingSlash + let projectUrl = info.PackageProjectUrl |> Option.map ensureTrailingSlash |> Option.defaultValue root + let repoUrl = info.RepositoryUrl |> Option.map ensureTrailingSlash userParameters @ - [ param ParamKeys.``root`` (Some root) - param ParamKeys.``fsdocs-authors`` info.Authors - param ParamKeys.``fsdocs-collection-name`` (Some collectionName) - param ParamKeys.``fsdocs-collection-name-link`` (info.FsDocsCollectionNameLink |> Option.orElse info.PackageProjectUrl) - param ParamKeys.``fsdocs-copyright`` info.Copyright - param ParamKeys.``fsdocs-logo-src`` (Some (defaultArg info.FsDocsLogoSource (sprintf "%simg/logo.png" root))) - param ParamKeys.``fsdocs-navbar-position`` (Some (defaultArg info.FsDocsNavbarPosition "fixed-right")) - param ParamKeys.``fsdocs-theme`` (Some (defaultArg info.FsDocsTheme "default")) - param ParamKeys.``fsdocs-logo-link`` (info.FsDocsLogoLink |> Option.orElse info.RepositoryUrl) - param ParamKeys.``fsdocs-license-link`` (info.FsDocsLicenseLink |> Option.orElse (Option.map (sprintf "%sblob/master/LICENSE.md") info.RepositoryUrl)) - param ParamKeys.``fsdocs-release-notes-link`` (info.FsDocsReleaseNotesLink |> Option.orElse (Option.map (sprintf "%sblob/master/RELEASE_NOTES.md") info.RepositoryUrl)) - param ParamKeys.``fsdocs-package-project-url`` projectUrl - param ParamKeys.``fsdocs-package-license-expression`` info.PackageLicenseExpression - param ParamKeys.``fsdocs-package-icon-url`` info.PackageIconUrl - param ParamKeys.``fsdocs-package-tags`` info.PackageTags - param ParamKeys.``fsdocs-package-version`` info.PackageVersion - param ParamKeys.``fsdocs-repository-link`` info.RepositoryUrl - param ParamKeys.``fsdocs-repository-branch`` info.RepositoryBranch - param ParamKeys.``fsdocs-repository-type`` info.RepositoryType - param ParamKeys.``fsdocs-repository-commit`` info.RepositoryCommit + [ param None ParamKeys.``root`` (Some root) + param None ParamKeys.``fsdocs-authors`` (Some (info.Authors |> Option.defaultValue "")) + param None ParamKeys.``fsdocs-collection-name`` (Some collectionName) + param None ParamKeys.``fsdocs-collection-name-link`` (Some (info.FsDocsCollectionNameLink |> Option.defaultValue projectUrl)) + param None ParamKeys.``fsdocs-copyright`` info.Copyright + param None ParamKeys.``fsdocs-logo-src`` (Some (defaultArg info.FsDocsLogoSource (sprintf "%simg/logo.png" root))) + param None ParamKeys.``fsdocs-navbar-position`` (Some (defaultArg info.FsDocsNavbarPosition "fixed-right")) + param None ParamKeys.``fsdocs-theme`` (Some (defaultArg info.FsDocsTheme "default")) + param None ParamKeys.``fsdocs-logo-link`` (Some (info.FsDocsLogoLink |> Option.defaultValue projectUrl)) + param (Some "") ParamKeys.``fsdocs-license-link`` (info.FsDocsLicenseLink |> Option.orElse (Option.map (sprintf "%sblob/master/LICENSE.md") repoUrl)) + param (Some "") ParamKeys.``fsdocs-release-notes-link`` (info.FsDocsReleaseNotesLink |> Option.orElse (Option.map (sprintf "%sblob/master/RELEASE_NOTES.md") repoUrl)) + param None ParamKeys.``fsdocs-package-project-url`` (Some projectUrl) + param None ParamKeys.``fsdocs-package-license-expression`` info.PackageLicenseExpression + param None ParamKeys.``fsdocs-package-icon-url`` info.PackageIconUrl + param None ParamKeys.``fsdocs-package-tags`` (Some (info.PackageTags |> Option.defaultValue "")) + param (Some "") ParamKeys.``fsdocs-package-version`` info.PackageVersion + param (Some "") ParamKeys.``fsdocs-repository-link`` repoUrl + param None ParamKeys.``fsdocs-repository-branch`` info.RepositoryBranch + param None ParamKeys.``fsdocs-repository-commit`` info.RepositoryCommit ] let projects = @@ -714,13 +718,16 @@ type CoreBuildOptions(watch) = (fun (_, key2) -> key1 = key2) (fun () -> Crack.crackProjects (userRoot, userCollectionName, userParameters, projects), key1) + // Print the parameters for (ParamKey pk, p) in docsParameters do printfn " %s --> %s" pk p - let pd = dict docsParameters - for (dllFile, _, _, _, _, _, _, projectParameters) in crackedProjects do - for (((ParamKey pkv2) as pk2) , p2) in projectParameters do - if pd.ContainsKey pk2 && pd.[pk2] <> p2 then - printfn " (%s) %s --> %s" (Path.GetFileNameWithoutExtension(dllFile)) pkv2 p2 + + // The parameters may differ for some projects due to different settings in the project files, if so show that + let pd = dict docsParameters + for (dllFile, _, _, _, _, _, _, projectParameters) in crackedProjects do + for (((ParamKey pkv2) as pk2) , p2) in projectParameters do + if pd.ContainsKey pk2 && pd.[pk2] <> p2 then + printfn " (%s) %s --> %s" (Path.GetFileNameWithoutExtension(dllFile)) pkv2 p2 let apiDocInputs = [ for (dllFile, repoUrlOption, repoBranchOption, repoTypeOption, projectMarkdownComments, projectSourceFolder, projectSourceRepo, projectParameters) in crackedProjects -> diff --git a/src/FSharp.Formatting.CommandTool/FSharp.Formatting.CommandTool.fsproj b/src/FSharp.Formatting.CommandTool/FSharp.Formatting.CommandTool.fsproj index ad00ae386..540aa0112 100644 --- a/src/FSharp.Formatting.CommandTool/FSharp.Formatting.CommandTool.fsproj +++ b/src/FSharp.Formatting.CommandTool/FSharp.Formatting.CommandTool.fsproj @@ -28,7 +28,7 @@ - + diff --git a/src/FSharp.Formatting.Common/Templating.fs b/src/FSharp.Formatting.Common/Templating.fs index 6958079fc..bfaba663b 100644 --- a/src/FSharp.Formatting.Common/Templating.fs +++ b/src/FSharp.Formatting.Common/Templating.fs @@ -94,7 +94,7 @@ module ParamKeys = let ``fsdocs-repository-link`` = ParamKey "fsdocs-repository-link" /// A parameter key known to FSharp.Formatting - let ``fsdocs-repository-type`` = ParamKey "fsdocs-repository-type" + let ``fsdocs-source`` = ParamKey "fsdocs-source" /// A parameter key known to FSharp.Formatting let ``fsdocs-theme`` = ParamKey "fsdocs-theme" diff --git a/src/FSharp.Formatting.Literate/Formatting.fs b/src/FSharp.Formatting.Literate/Formatting.fs index ef29075e7..811867d59 100644 --- a/src/FSharp.Formatting.Literate/Formatting.fs +++ b/src/FSharp.Formatting.Literate/Formatting.fs @@ -78,7 +78,7 @@ module internal Formatting = getSourceDocument doc |> Transformations.replaceLiterateParagraphs ctx let source = format doc.MarkdownDocument ctx.GenerateHeaderAnchors ctx.OutputKind [] - [ ParamKey "fsdocs-source", source ] + [ ParamKeys.``fsdocs-source``, source ] // Get page title (either heading or file name) let pageTitle = diff --git a/src/FSharp.Formatting.Literate/Literate.fs b/src/FSharp.Formatting.Literate/Literate.fs index a3c6a3124..b4bf6c315 100644 --- a/src/FSharp.Formatting.Literate/Literate.fs +++ b/src/FSharp.Formatting.Literate/Literate.fs @@ -168,9 +168,9 @@ type Literate private () = doc.With(paragraphs = pars) /// Parse F# Script file - static member ParseScriptFile (path, ?formatAgent, ?fscoptions, ?definedSymbols, ?references, ?fsiEvaluator, ?parseOptions) = + static member ParseAndCheckScriptFile (path, ?formatAgent, ?fscoptions, ?definedSymbols, ?references, ?fsiEvaluator, ?parseOptions) = let ctx = parsingContext formatAgent fsiEvaluator fscoptions definedSymbols - ParseScript(parseOptions, ctx).ParseScriptFile path (File.ReadAllText path) + ParseScript(parseOptions, ctx).ParseAndCheckScriptFile path (File.ReadAllText path) |> Transformations.generateReferences references |> Transformations.formatCodeSnippets path ctx |> Transformations.evaluateCodeSnippets ctx @@ -178,7 +178,7 @@ type Literate private () = /// Parse F# Script file static member ParseScriptString (content, ?path, ?formatAgent, ?fscoptions, ?definedSymbols, ?references, ?fsiEvaluator, ?parseOptions) = let ctx = parsingContext formatAgent fsiEvaluator fscoptions definedSymbols - ParseScript(parseOptions, ctx).ParseScriptFile (defaultArg path "C:\\Document.fsx") content + ParseScript(parseOptions, ctx).ParseAndCheckScriptFile (defaultArg path "C:\\Document.fsx") content |> Transformations.generateReferences references |> Transformations.formatCodeSnippets (defaultArg path "C:\\Document.fsx") ctx |> Transformations.evaluateCodeSnippets ctx @@ -292,7 +292,7 @@ type Literate private () = | _ -> MarkdownParseOptions.None let outputKind = defaultArg outputKind OutputKind.Html - let doc = Literate.ParseScriptFile (input, ?formatAgent=formatAgent, ?fscoptions=fscoptions, ?references=references, ?fsiEvaluator = fsiEvaluator, parseOptions=parseOptions) + let doc = Literate.ParseAndCheckScriptFile (input, ?formatAgent=formatAgent, ?fscoptions=fscoptions, ?references=references, ?fsiEvaluator = fsiEvaluator, parseOptions=parseOptions) let ctx = formattingContext outputKind prefix lineNumbers generateAnchors parameters tokenKindToCss let doc = customizeDoc customizeDocument ctx doc let doc = downloadImagesForDoc imageSaver doc diff --git a/src/FSharp.Formatting.Literate/ParseScript.fs b/src/FSharp.Formatting.Literate/ParseScript.fs index 5b42cfcb6..f41343eb8 100644 --- a/src/FSharp.Formatting.Literate/ParseScript.fs +++ b/src/FSharp.Formatting.Literate/ParseScript.fs @@ -262,9 +262,9 @@ type internal ParseScript(parseOptions, ctx:CompilerContext) = /// Parse script file with specified name and content /// and return `LiterateDocument` with the content - member _.ParseScriptFile filePath content = + member _.ParseAndCheckScriptFile filePath content = let defines = match ctx.ConditionalDefines with [] -> None | l -> Some (String.concat "," l) - let sourceSnippets, diagnostics = ctx.FormatAgent.ParseSource(filePath, content, ?options=ctx.CompilerOptions, ?defines=defines) + let sourceSnippets, diagnostics = ctx.FormatAgent.ParseAndCheckSource(filePath, content, ?options=ctx.CompilerOptions, ?defines=defines) for (SourceError((l0,c0),(l1,c1),kind,msg)) in diagnostics do printfn " %s: %s(%d,%d)-(%d,%d) %s" filePath (if kind = ErrorKind.Error then "error" else "warning") l0 c0 l1 c1 msg diff --git a/src/FSharp.Formatting.Literate/Transformations.fs b/src/FSharp.Formatting.Literate/Transformations.fs index d77761d29..6e4320c8a 100644 --- a/src/FSharp.Formatting.Literate/Transformations.fs +++ b/src/FSharp.Formatting.Literate/Transformations.fs @@ -114,7 +114,7 @@ module internal Transformations = // Process F# script file & build lookup table for replacement let defines = match ctx.ConditionalDefines with [] -> None | l -> Some (String.concat "," l) let snippets, diagnostics = - ctx.FormatAgent.ParseSource + ctx.FormatAgent.ParseAndCheckSource ( Path.ChangeExtension(path, ".fsx"), source, ?options = ctx.CompilerOptions, ?defines = defines ) let results = diff --git a/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs b/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs index dcadb2b6d..a7ac72b11 100644 --- a/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs +++ b/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs @@ -159,6 +159,22 @@ let ``ApiDocs works on two sample F# assemblies``() = indxTxt |> shouldContainText "http://root.io/root/reference/fslib-test_issue472_t.html#MultArg" indxTxt |> shouldContainText """ITest_Issue229.Name \nName \n""" +[] +let ``Namespace summary generation works on two sample F# assemblies using XML docs``() = + let libraries = + [ testBin "TestLib1.dll" + testBin "TestLib2.dll" ] + let output = getOutputDir "TestLib12_Namespaces" + let inputs = [ for lib in libraries -> ApiDocInput.FromFile(lib, mdcomments = false, parameters=parameters) ] + let _model, _searchIndex = + ApiDocs.GenerateHtml(inputs, output, collectionName="TestLibs", template=docTemplate, + root="http://root.io/root/", parameters=parameters, libDirs = [testBin]) + + let fileNames = Directory.GetFiles(output "reference") + let files = dict [ for f in fileNames -> Path.GetFileName(f), File.ReadAllText(f) ] + files.["index.html"] |> shouldContainText "FsLib is a good namespace" + files.["fslib.html"] |> shouldContainText "FsLib is a good namespace" + [] let ``ApiDocs model generation works on two sample F# assemblies``() = let libraries = diff --git a/tests/FSharp.ApiDocs.Tests/files/TestLib1/Library1.fs b/tests/FSharp.ApiDocs.Tests/files/TestLib1/Library1.fs index a055445c5..05e03ec20 100644 --- a/tests/FSharp.ApiDocs.Tests/files/TestLib1/Library1.fs +++ b/tests/FSharp.ApiDocs.Tests/files/TestLib1/Library1.fs @@ -1,8 +1,10 @@ -namespace FsLib +namespace FsLib /// /// Union sample /// +/// +/// FsLib is a good namespace type Union = /// Hello of int | Hello of int diff --git a/tests/FSharp.CodeFormat.Tests/CodeFormatTests.fs b/tests/FSharp.CodeFormat.Tests/CodeFormatTests.fs index df06aa6b5..15acf2ace 100644 --- a/tests/FSharp.CodeFormat.Tests/CodeFormatTests.fs +++ b/tests/FSharp.CodeFormat.Tests/CodeFormatTests.fs @@ -36,7 +36,7 @@ let (|ToolTipWithLiteral|_|) text tips = [] let ``Simple code snippet is formatted with tool tips``() = let source = """let hello = 10""" - let snips, errors = agent.ParseSource("/somewhere/test.fsx", source.Trim()) + let snips, errors = agent.ParseAndCheckSource("/somewhere/test.fsx", source.Trim()) errors |> shouldEqual [| |] snips |> containsSpan (function @@ -51,7 +51,7 @@ let ``Preview language feature from FSharp Core is supported``() = let x = 12 nameof x """ - let snips, errors = agent.ParseSource("/somewhere/test.fsx", source.Trim()) + let snips, errors = agent.ParseAndCheckSource("/somewhere/test.fsx", source.Trim()) errors |> shouldEqual [||] snips |> containsSpan (function @@ -101,12 +101,12 @@ let printApplicatives () = run r1 (Error "failure!") r3 printApplicatives() """ - let _, errors = agent.ParseSource("/somewhere/test.fsx", source.Trim()) + let _, errors = agent.ParseAndCheckSource("/somewhere/test.fsx", source.Trim()) errors |> shouldEqual [||] let getContentAndToolTip (source: string) = - let snips, _errors = agent.ParseSource("/somewhere/test.fsx", source.Trim()) + let snips, _errors = agent.ParseAndCheckSource("/somewhere/test.fsx", source.Trim()) let res = CodeFormat.FormatHtml(snips, "fstips") (Seq.head res.Snippets).Content, res.ToolTip @@ -129,7 +129,7 @@ let ``Non-unicode characters do not cause exception`` () = // [snippet:16] ✘ let add I J = I+J // [/snippet]""" - let _snips, errors = agent.ParseSource("/somewhere/test.fsx", source.Trim()) + let _snips, errors = agent.ParseAndCheckSource("/somewhere/test.fsx", source.Trim()) errors.Length |> shouldBeGreaterThan 0 let (SourceError(_, _, _, msg)) = errors.[0] msg |> shouldContainText "✘" @@ -228,7 +228,7 @@ let customCss kind = let getContentAndToolTip' (source: string) = - let snips, _errors = agent.ParseSource("/somewhere/test.fsx", source.Trim()) + let snips, _errors = agent.ParseAndCheckSource("/somewhere/test.fsx", source.Trim()) let res = CodeFormat.FormatHtml(snips, "fstips", tokenKindToCss = customCss) (Seq.head res.Snippets).Content, res.ToolTip @@ -304,7 +304,7 @@ let ``Escaped characters are in spans of 'esc' class - custom CSS``() = content |> shouldContainText (sprintf "class=\"%s\">\\t" "Escaped") let getLatex (source: string) = - let snips, _errors = agent.ParseSource("/somewhere/test.fsx", source.Trim()) + let snips, _errors = agent.ParseAndCheckSource("/somewhere/test.fsx", source.Trim()) let res = CodeFormat.FormatLatex(snips) (Seq.head res.Snippets).Content @@ -316,7 +316,7 @@ let ``Simple code snippet is formatted as Latex``() = content |> shouldContainText (sprintf @"\end{Verbatim}") let getFsx (source: string) = - let snips, _errors = agent.ParseSource("/somewhere/test.fsx", source.Trim()) + let snips, _errors = agent.ParseAndCheckSource("/somewhere/test.fsx", source.Trim()) let res = CodeFormat.FormatFsx(snips) (Seq.head res.Snippets).Content diff --git a/version.props b/version.props index 3f449334a..7fa7d5e7e 100644 --- a/version.props +++ b/version.props @@ -1,8 +1,8 @@ - 7.1.8 + 7.2.2 -- bump version +- instruct about settings \ No newline at end of file From 1b639cfcb4cce23aee4713dbfdb3a94ef9f7888c Mon Sep 17 00:00:00 2001 From: Don Syme Date: Wed, 5 Aug 2020 23:10:28 +0100 Subject: [PATCH 2/4] improve xml doc support --- RELEASE_NOTES.md | 2 + docs/apidocs.fsx | 4 ++ src/FSharp.Formatting.ApiDocs/GenerateHtml.fs | 48 ++++++++------ .../GenerateModel.fs | 23 +++++-- .../BuildCommand.fs | 66 ++++++++++--------- 5 files changed, 85 insertions(+), 58 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 8ff545f90..c5e8b8dd6 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,6 +2,8 @@ - support `...` +- support `...` + - support `...` - support `` diff --git a/docs/apidocs.fsx b/docs/apidocs.fsx index d5cff319f..79f055699 100644 --- a/docs/apidocs.fsx +++ b/docs/apidocs.fsx @@ -49,6 +49,8 @@ In addition, you may also use * `` for giving summary sections for the enclosing namespace +* `` for giving extended remarks for the enclosing namespace + * `` and `` to exclude from XML docs * `` to give a category for presentation @@ -74,6 +76,8 @@ An example of an XML documentation comment, assuming the code is in namespace `T /// /// A namespace to remember /// +/// More on that +/// /// Foo /// diff --git a/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs b/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs index c66e1a940..6b8008c4f 100644 --- a/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs +++ b/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs @@ -346,7 +346,7 @@ type HtmlRender(model: ApiDocModel) = h2 [Id ns.UrlHash] [!! (ns.Name + " Namespace") ] match ns.NamespaceSummary with - | Some nsdocs -> div [] [!! nsdocs] + | Some (nssummary, nsremarks) -> div [] [!! nssummary; !!nsremarks ] | None -> () if (allByCategory.Length > 1) then @@ -388,32 +388,42 @@ type HtmlRender(model: ApiDocModel) = li [Class "nav-header"] [!! "Namespaces"] for allByCategory, ns in categorise do - li [ Class ("nav-item" + + + // Generate the entry for the namespace + li [ if nav then + Class ("nav-item" + // add the 'active' class if this is the namespace of the thing being shown match nsOpt with | Some ns2 when ns.Name = ns2.Name -> " active" | _ -> "") ] - [a [ Class ("nav-link" + + + [span [] [ + a [ if nav then + Class ("nav-link" + // add the 'active' class if this is the namespace of the thing being shown match nsOpt with | Some ns2 when ns.Name = ns2.Name -> " active" | _ -> "") - Href (ns.Url(root, collectionName, qualify))] [!!ns.Name] - if not nav then - !! " - " - match ns.NamespaceSummary with - | Some nsdocs -> !! nsdocs - | None -> () ] - - // Generate the expanded list of entities if the namespace is the active one - match nsOpt with - | Some ns2 when ns.Name = ns2.Name -> - ul [ Custom ("list-style-type", "none") (* Class "navbar-nav " *) ] [ - for category in allByCategory do - for e in category.CategoryEntites do - li [ Class "nav-item" ] [a [Class "nav-link"; Href (e.Url(root, collectionName, qualify))] [!! e.Name] ] - ] - | _ -> () + Href (ns.Url(root, collectionName, qualify))] [!!ns.Name] + + // If not in the navigation list then generate the summary text as well + if not nav then + !! " - " + match ns.NamespaceSummary with + | Some (nssummary, _nsremarks) -> !! nssummary + | None -> () ] ] + + // In the navigation bar generate the expanded list of entities + // for the active namespace + if nav then + match nsOpt with + | Some ns2 when ns.Name = ns2.Name -> + ul [ Custom ("list-style-type", "none") (* Class "navbar-nav " *) ] [ + for category in allByCategory do + for e in category.CategoryEntites do + li [ Class "nav-item" ] [a [Class "nav-link"; Href (e.Url(root, collectionName, qualify))] [!! e.Name] ] + ] + | _ -> () ] let listOfNamespaces otherDocs nav (nsOpt: ApiDocNamespace option) = diff --git a/src/FSharp.Formatting.ApiDocs/GenerateModel.fs b/src/FSharp.Formatting.ApiDocs/GenerateModel.fs index 770d2b54b..9805266d4 100644 --- a/src/FSharp.Formatting.ApiDocs/GenerateModel.fs +++ b/src/FSharp.Formatting.ApiDocs/GenerateModel.fs @@ -418,7 +418,7 @@ type ApiDocEntity /// Represents a namespace integrated with its associated documentation -type ApiDocNamespace(name: string, mods, parameters: Parameters, nsdocs: string option) = +type ApiDocNamespace(name: string, mods, parameters: Parameters, nsdocs: (string * string) option) = let urlBaseName = name.Replace(".", "-").ToLower() @@ -443,7 +443,7 @@ type ApiDocNamespace(name: string, mods, parameters: Parameters, nsdocs: string member x.Entities : ApiDocEntity list = mods /// The summary text for the namespace - member x.NamespaceSummary : string option = nsdocs + member x.NamespaceSummary = nsdocs /// The substitution parameters active for generating thist content member x.Parameters = parameters @@ -1206,11 +1206,20 @@ module internal SymbolReader = let nsdocs = let ds = doc.Descendants(XName.Get "namespacesummary") if Seq.length ds > 0 then - Some - ([ for d in ds -> + let nssummary = + [ for d in ds -> + let html = new StringBuilder() + readXmlElementAsHtml true urlMap cmds html d + html.ToString() ] + |> String.concat "\n" + let rs = doc.Descendants(XName.Get "namespacremarks") + let nsremarks = + ([ for r in rs -> let html = new StringBuilder() - readXmlElementAsHtml true urlMap cmds html d - html.ToString() ] |> String.concat "\n") + readXmlElementAsHtml true urlMap cmds html r + html.ToString() ] + |> String.concat "\n") + Some (nssummary, nsremarks) else None @@ -1507,7 +1516,7 @@ module internal SymbolReader = |> List.choose id |> function | [] -> None - | xs -> Some (String.concat "\n" xs) + | xs -> Some (let (a,b) = List.unzip xs in String.concat "\n" a, String.concat "\n" b) let collectNamespaceDocs results = results diff --git a/src/FSharp.Formatting.CommandTool/BuildCommand.fs b/src/FSharp.Formatting.CommandTool/BuildCommand.fs index 0d9fd0c44..d2f21a6fc 100644 --- a/src/FSharp.Formatting.CommandTool/BuildCommand.fs +++ b/src/FSharp.Formatting.CommandTool/BuildCommand.fs @@ -415,7 +415,7 @@ module Crack = root, collectionName, projects, paths, docsParameters /// Convert markdown, script and other content into a static site -type internal DocContent(outputDirectory, previous: Map<_,_>, lineNumbers, fsiEvaluator, parameters, saveImages, watch) = +type internal DocContent(outputDirectory, previous: Map<_,_>, lineNumbers, fsiEvaluator, parameters, saveImages, watch, root) = let createImageSaver (outputDirectory) = // Download images so that they can be embedded @@ -584,6 +584,30 @@ type internal DocContent(outputDirectory, previous: Map<_,_>, lineNumbers, fsiEv yield! processDirectory (htmlTemplate, None, None, None) inputDirectory outputPrefix ] + member _.GetSearchIndexEntries(docModels: (string * LiterateDocModel) list) = + [| for (_inputFile, model) in docModels do + match model.IndexText with + | Some text -> {title=model.Title; content = text; uri=model.Uri(root) } + | _ -> () |] + + member _.GetNavigationEntries(docModels: (string * LiterateDocModel) list) = + let modelsForList = + [ for thing in docModels do + match thing with + | (inputFile, model) + when model.OutputKind = OutputKind.Html && + // Don't put the index in the list + not (Path.GetFileNameWithoutExtension(inputFile) = "index") -> model + | _ -> () ] + + [ if modelsForList.Length > 0 then + li [Class "nav-header"] [!! "Documentation"] + for model in modelsForList do + let link = model.Uri(root) + li [Class "nav-item"] [ a [Class "nav-link"; (Href link)] [encode model.Title ] ] + ] + |> List.map (fun html -> html.ToString()) |> String.concat " \n" + /// Processes and runs Suave server to host them on localhost module Serve = @@ -834,51 +858,29 @@ type CoreBuildOptions(watch) = let saveImages = (match x.saveImages with "some" -> None | "none" -> Some false | "all" -> Some true | _ -> None) let fsiEvaluator = (if x.eval then Some ( FsiEvaluator() :> IFsiEvaluator) else None) - let models = + let docContent = DocContent(output, latestDocContentResults, Some x.linenumbers, fsiEvaluator, docsParameters, - saveImages, watch).Convert(x.input, defaultTemplate, extraInputs) + saveImages, watch, root) - let extrasForSearchIndex = - [| for (thing, _action) in models do - match thing with - | Some (_inputFile, model) -> - match model.IndexText with - | Some text -> {title=model.Title; content = text; uri=model.Uri(root) } - | _ -> () - | _ -> () |] + let docModels = docContent.Convert(x.input, defaultTemplate, extraInputs) + let actualDocModels = docModels |> List.map fst |> List.choose id + let extrasForSearchIndex = docContent.GetSearchIndexEntries(actualDocModels) + let navEntries = docContent.GetNavigationEntries(actualDocModels) let results = Map.ofList [ - for (thing, _action) in models do + for (thing, _action) in docModels do match thing with | Some res -> res | None -> () ] - let listOfDocs = - let items = - [ for (thing, _action) in models do - match thing with - | Some (inputFile, model) - when model.OutputKind = OutputKind.Html && - // Don't put the index in the list - not (Path.GetFileNameWithoutExtension(inputFile) = "index") -> model - | _ -> () ] - - [ if models.Length > 0 then - li [Class "nav-header"] [!! "Documentation"] - for model in items do - let link = model.Uri(root) - li [Class "nav-item"] [ a [Class "nav-link"; (Href link)] [encode model.Title ] ] - ] - |> List.map (fun html -> html.ToString()) |> String.concat " \n" - latestDocContentResults <- results latestDocContentSearchIndexEntries <- extrasForSearchIndex - latestDocContentGlobalParameters <- [ ParamKeys.``fsdocs-list-of-documents`` ,listOfDocs ] + latestDocContentGlobalParameters <- [ ParamKeys.``fsdocs-list-of-documents`` , navEntries ] latestDocContentPhase2 <- (fun globals -> - for (_thing, action) in models do + for (_thing, action) in docModels do action globals ) From 604785b57194c10d0289924cea278c9fd6e163c5 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Thu, 6 Aug 2020 00:27:44 +0100 Subject: [PATCH 3/4] improve xml doc support - categories and exclusions --- docs/content/fsdocs-default.css | 4 + src/FSharp.Formatting.ApiDocs/GenerateHtml.fs | 59 +++++------ .../GenerateModel.fs | 100 +++++++++++------- 3 files changed, 88 insertions(+), 75 deletions(-) diff --git a/docs/content/fsdocs-default.css b/docs/content/fsdocs-default.css index 0c394af80..f23ecd1a3 100644 --- a/docs/content/fsdocs-default.css +++ b/docs/content/fsdocs-default.css @@ -62,6 +62,10 @@ body { margin: 0px 0px 20px 0px; } +#fsdocs-content li { + margin: 0px 0px 15px 0px; +} + /*-------------------------------------------------------------------------- Formatting tables in fsdocs-content, using docs.microsoft.com tables /*--------------------------------------------------------------------------*/ diff --git a/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs b/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs index 6b8008c4f..a9e51282b 100644 --- a/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs +++ b/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs @@ -168,27 +168,28 @@ type HtmlRender(model: ApiDocModel) = ] ] + // Honour the CategoryIndex to put the categories in the right order + let getSortedCategories xs exclude category categoryIndex = + xs + |> List.filter (fun x -> not (exclude x)) + |> List.groupBy (fun x -> category x) + |> List.map (fun (cat, xs) -> (cat, xs, xs |> List.minBy (fun x -> categoryIndex x))) + |> List.sortBy (fun (cat, _xs, x) -> categoryIndex x, cat) + |> List.map (fun (cat, xs, _x) -> cat, xs) + let entityContent (info: ApiDocEntityInfo) = // Get all the members & comment for the type let entity = info.Entity let members = entity.AllMembers |> List.filter (fun e -> not e.IsObsolete) let comment = entity.Comment - // Group all members by their category which is an inline annotation - // that can be added to members using special XML comment: - // - // /// [category:Something] - // - // ...and can be used to categorize members in large modules or types - // (but if this is not used, then all members end up in just one category) + // Group all members by their category let byCategory = - members - |> List.groupBy(fun m -> m.Category) - |> List.sortBy (fun (key, _) -> if String.IsNullOrEmpty(key) then "ZZZ" else key) - |> List.mapi (fun n (key, elems) -> - let elems= elems |> List.sortBy (fun m -> m.Name) + getSortedCategories members (fun m -> m.Exclude) (fun m -> m.Category) (fun m -> m.CategoryIndex) + |> List.mapi (fun i (key, elems) -> + let elems = elems |> List.sortBy (fun m -> m.Name) let name = if String.IsNullOrEmpty(key) then "Other module members" else key - (n, key, elems, name)) + (i, key, elems, name)) let usageName = match info.ParentModule with @@ -302,27 +303,18 @@ type HtmlRender(model: ApiDocModel) = let entities = ns.Entities let categories = - [ for e in entities -> e.Category ] - |> List.distinct - |> List.sortBy (fun s -> if String.IsNullOrEmpty(s) then "ZZZ" else s) + getSortedCategories entities (fun m -> m.Exclude) (fun m -> m.Category) (fun m -> m.CategoryIndex) let allByCategory = - [ for (catIndex, c) in Seq.indexed categories do - let name = (if String.IsNullOrEmpty(c) then "Other namespace members" else c) + [ for (catIndex, (categoryName, categoryEntities)) in Seq.indexed categories do + let caegoryName = (if String.IsNullOrEmpty(categoryName) then "Other namespace members" else categoryName) let index = String.Format("{0}_{1}", nsIndex, catIndex) - let entities = - entities - |> List.filter (fun e -> - let cat = e.Category - cat = c) + let categoryEntities = + categoryEntities // Some bespoke hacks to make FSharp.Core docs look ok. - // TODO: work out how to generalise these so others can use them if needed + // TODO: use to do these // - // Remove the funky array type definitions in FSharp.Core from display - |> List.filter (fun e -> not e.Symbol.IsArrayType) - // Remove the List type definition in FSharp.Core from display, the type 't list is canonical - |> List.filter (fun e -> not (e.Symbol.Namespace = Some "Microsoft.FSharp.Collections" && e.Symbol.DisplayName = "List")) // Remove FSharp.Data.UnitSystems.SI from display, it's just so rarely used, has long names and dominates the docs. // Find another way to document these |> List.filter (fun e -> not (e.Symbol.Namespace = Some "Microsoft.FSharp.Data.UnitSystems.SI.UnitSymbols")) @@ -336,8 +328,10 @@ type HtmlRender(model: ApiDocModel) = |> List.sortBy (fun e -> (e.Symbol.DisplayName.ToLowerInvariant(), e.Symbol.GenericParameters.Count, e.Name, (if e.IsTypeDefinition then e.UrlBaseName else "ZZZ"))) - if entities.Length > 0 then - yield {| CategoryName = name; CategoryIndex = index; CategoryEntites = entities |} ] + + if categoryEntities.Length > 0 then + yield {| CategoryName = caegoryName; CategoryIndex = index; CategoryEntites = categoryEntities |} ] + allByCategory let namespaceContent (nsIndex, ns: ApiDocNamespace) = @@ -361,11 +355,6 @@ type HtmlRender(model: ApiDocModel) = yield! renderEntities category.CategoryEntites ] - //let namespacesContent (asm: ApiDocCollection) = - // [ h1 [] [!! asm.CollectionName] - // for (nsIndex, ns) in Seq.indexed asm.Namespaces do - // yield! namespaceContent (nsIndex, ns) ] - let listOfNamespacesAux otherDocs nav (nsOpt: ApiDocNamespace option) = [ // For FSharp.Core we make all entries available to other docs else there's not a lot else to show. diff --git a/src/FSharp.Formatting.ApiDocs/GenerateModel.fs b/src/FSharp.Formatting.ApiDocs/GenerateModel.fs index 9805266d4..0986202a5 100644 --- a/src/FSharp.Formatting.ApiDocs/GenerateModel.fs +++ b/src/FSharp.Formatting.ApiDocs/GenerateModel.fs @@ -217,7 +217,7 @@ type ApiDocMemberKind = /// Represents an method, property, constructor, function or value, record field, union case or static parameter /// integrated with its associated documentation. Includes extension members. type ApiDocMember(displayName: string, attributes: ApiDocAttribute list, entityUrlBaseName: string, - kind, cat, details, comment, symbol) = + kind, cat, catidx: int, exclude: bool, details, comment, symbol) = // The URL for a member is currently the #DisplayName on the enclosing entity let (usageHtml, paramHtmls, returnHtml, mods, typars, baseType, location, compiledName) = details @@ -286,6 +286,12 @@ type ApiDocMember(displayName: string, attributes: ApiDocAttribute list, entityU /// The category member x.Category : string = cat + /// The category index + member x.CategoryIndex : int = catidx + + /// The exclude flag + member x.Exclude : bool = exclude + /// The kind of the member member x.Kind : ApiDocMemberKind = kind @@ -319,7 +325,7 @@ type ApiDocMember(displayName: string, attributes: ApiDocAttribute list, entityU /// Represents a type definition integrated with its associated documentation type ApiDocEntity - (tdef, name, cat, urlBaseName, comment, assembly: AssemblyName, attributes, + (tdef, name, cat: string, catidx: int, exclude: bool, urlBaseName, comment, assembly: AssemblyName, attributes, cases, fields, statParams, ctors, inst, stat, allInterfaces, baseType, abbreviatedType, delegateSignature, symbol: FSharpEntity, nested, vals, exts, pats, rqa, parameters: Parameters) = @@ -331,7 +337,13 @@ type ApiDocEntity member x.Name : string = name /// The category of the type - member x.Category : string = cat + member x.Category = cat + + /// The category index of the type + member x.CategoryIndex = catidx + + /// The exclude flag + member x.Exclude = exclude /// The URL base name of the primary documentation for the entity (without the http://site.io/reference) member x.UrlBaseName : string = urlBaseName @@ -1223,30 +1235,31 @@ module internal SymbolReader = else None - begin - if summaryExpected then - let summaries = doc.Descendants(XName.Get "summary") - summaries |> Seq.iteri (fun id e -> - let n = if id = 0 then "summary" else "summary-" + string id - rawData.[n] <- e.Value - html.Append("

    ") |> ignore - readXmlElementAsHtml true urlMap cmds html e - html.Append("

    ") |> ignore - ) - else - readXmlElementAsHtml false urlMap cmds html doc - end + if summaryExpected then + let summaries = doc.Descendants(XName.Get "summary") + summaries |> Seq.iteri (fun id e -> + let n = if id = 0 then "summary" else "summary-" + string id + rawData.[n] <- e.Value + html.Append("

    ") |> ignore + readXmlElementAsHtml true urlMap cmds html e + html.Append("

    ") |> ignore + ) + else + readXmlElementAsHtml false urlMap cmds html doc - for e in doc.Descendants(XName.Get "exclude") do - cmds.Add("exclude", e.Value) + if all then + for e in doc.Descendants(XName.Get "exclude") do + cmds.["exclude"] <- e.Value - for e in doc.Descendants(XName.Get "omit") do - cmds.Add("omit", e.Value) + for e in doc.Descendants(XName.Get "omit") do + cmds.["omit"] <- e.Value - for e in doc.Descendants(XName.Get "category") do - cmds.Add("category", e.Value) + for e in doc.Descendants(XName.Get "category") do + cmds.["category"] <- e.Value + + for e in doc.Descendants(XName.Get "categoryindex") do + cmds.["categoryindex"] <- e.Value - if all then let remarkNodes = doc.Descendants(XName.Get "remarks") if Seq.length remarkNodes > 0 then //html.Append("

    Remarks

    ") |> ignore @@ -1454,7 +1467,7 @@ module internal SymbolReader = lines |> List.filter (findCommand >> (function | Some (k, v) -> - cmds.Add(k, v) + cmds.[k] <- v false | _ -> true)) |> List.map fst @@ -1474,7 +1487,7 @@ module internal SymbolReader = let html, nsdocs = readXmlCommentAsHtml ctx.UrlMap el cmds lines |> Seq.choose findCommand - |> Seq.iter (fun (k, v) -> cmds.Add(k,v)) + |> Seq.iter (fun (k, v) -> cmds.[k] <- v) cmds, html, nsdocs @@ -1484,12 +1497,19 @@ module internal SymbolReader = let readCommentsInto (sym:FSharpSymbol) ctx xmlDocSig f = let cmds, comment, nsdocs = readCommentAndCommands ctx xmlDocSig match cmds with - | Command "exclude" _ -> None - | Command "omit" _ -> None | Command "category" cat | Let "" (cat, _) -> + let catindex = + match cmds with + | Command "categoryindex" idx + | Let "1000" (idx, _) -> (try int idx with _ -> Int32.MaxValue) + let exclude = + match cmds with + | Command "omit" v + | Command "exclude" v + | Let "false" (v, _) -> (v <> "false") try - Some(f cat cmds comment, nsdocs) + Some(f cat catindex exclude cmds comment, nsdocs) with e -> let name = try sym.FullName @@ -1533,9 +1553,9 @@ module internal SymbolReader = |> collectNamespaceDocs let tryReadMember (ctx:ReadingContext) entityUrl kind (memb:FSharpMemberOrFunctionOrValue) = - readCommentsInto memb ctx (getXmlDocSigForMember memb) (fun cat _ comment -> + readCommentsInto memb ctx (getXmlDocSigForMember memb) (fun cat catidx exclude _ comment -> let details = readMemberOrVal ctx memb - ApiDocMember(memb.DisplayName, readAttributes memb.Attributes, entityUrl, kind, cat, details, comment, memb)) + ApiDocMember(memb.DisplayName, readAttributes memb.Attributes, entityUrl, kind, cat, catidx, exclude, details, comment, memb)) let readAllMembers ctx entityUrl kind (members:seq) = members @@ -1575,9 +1595,9 @@ module internal SymbolReader = |> List.ofSeq |> List.filter (fun v -> checkAccess ctx v.Accessibility) |> List.choose (fun case -> - readCommentsInto case ctx case.XmlDocSig (fun cat _ comment -> + readCommentsInto case ctx case.XmlDocSig (fun cat catidx exclude _ comment -> let details = readUnionCase ctx typ case - ApiDocMember(case.Name, readAttributes case.Attributes, entityUrl, ApiDocMemberKind.UnionCase, cat, details, comment, case))) + ApiDocMember(case.Name, readAttributes case.Attributes, entityUrl, ApiDocMemberKind.UnionCase, cat, catidx, exclude, details, comment, case))) |> collectNamespaceDocs let readRecordFields ctx entityUrl (typ:FSharpEntity) = @@ -1585,18 +1605,18 @@ module internal SymbolReader = |> List.ofSeq |> List.filter (fun field -> not field.IsCompilerGenerated) |> List.choose (fun field -> - readCommentsInto field ctx field.XmlDocSig (fun cat _ comment -> + readCommentsInto field ctx field.XmlDocSig (fun cat catidx exclude _ comment -> let details = readFSharpField ctx field - ApiDocMember(field.Name, readAttributes (Seq.append field.FieldAttributes field.PropertyAttributes), entityUrl, ApiDocMemberKind.RecordField, cat, details, comment, field))) + ApiDocMember(field.Name, readAttributes (Seq.append field.FieldAttributes field.PropertyAttributes), entityUrl, ApiDocMemberKind.RecordField, cat, catidx, exclude, details, comment, field))) |> collectNamespaceDocs let readStaticParams ctx entityUrl (typ:FSharpEntity) = typ.StaticParameters |> List.ofSeq |> List.choose (fun staticParam -> - readCommentsInto staticParam ctx (getFSharpStaticParamXmlSig typ staticParam.Name) (fun cat _ comment -> + readCommentsInto staticParam ctx (getFSharpStaticParamXmlSig typ staticParam.Name) (fun cat catidx exclude _ comment -> let details = readFSharpStaticParam ctx staticParam - ApiDocMember(staticParam.Name, [], entityUrl, ApiDocMemberKind.StaticParameter, cat, details, comment, staticParam))) + ApiDocMember(staticParam.Name, [], entityUrl, ApiDocMemberKind.StaticParameter, cat, catidx, exclude, details, comment, staticParam))) |> collectNamespaceDocs // Create a xml documentation snippet and add it to the XmlMemberMap @@ -1626,7 +1646,7 @@ module internal SymbolReader = let xmlDocSig = getXmlDocSigForType typ - readCommentsInto typ ctx xmlDocSig (fun cat _cmds comment -> + readCommentsInto typ ctx xmlDocSig (fun cat catidx exclude _cmds comment -> let entityUrl = ctx.UrlMap.TryResolveUrlBaseNameForEntity typ let rec getMembers (typ:FSharpEntity) = [ @@ -1675,11 +1695,11 @@ module internal SymbolReader = let nsdocs = combineNamespaceDocs [nsdocs1; nsdocs2; nsdocs3; nsdocs4; nsdocs5; nsdocs6 ] if nsdocs.IsSome then printfn "ignoring namespace summary on nested position" - ApiDocEntity (true, name, cat, entityUrl, comment, ctx.Assembly, attrs, cases, fields, statParams, ctors, + ApiDocEntity (true, name, cat, catidx, exclude, entityUrl, comment, ctx.Assembly, attrs, cases, fields, statParams, ctors, inst, stat, allInterfaces, baseType, abbreviatedType, delegateSignature, typ, [], [], [], [], rqa, ctx.Parameters)) and readModule (ctx:ReadingContext) (modul:FSharpEntity) = - readCommentsInto modul ctx modul.XmlDocSig (fun cat _cmd comment -> + readCommentsInto modul ctx modul.XmlDocSig (fun cat catidx exclude _cmd comment -> // Properties & value bindings in the module let entityUrl = ctx.UrlMap.TryResolveUrlBaseNameForEntity modul @@ -1695,7 +1715,7 @@ module internal SymbolReader = printfn "ignoring namespace summary on nested position" ApiDocEntity - ( false, modul.DisplayName, cat, entityUrl, comment, ctx.Assembly, attrs, + ( false, modul.DisplayName, cat, catidx, exclude, entityUrl, comment, ctx.Assembly, attrs, [], [], [], [], [], [], [], None, None, None, modul, entities, vals, exts, pats, rqa, ctx.Parameters )) From b496d890904e6a4648aeebd56bc1014c3c8eab45 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Thu, 6 Aug 2020 00:35:11 +0100 Subject: [PATCH 4/4] improve xml doc support - categories and exclusions --- tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs b/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs index a7ac72b11..d2949a18a 100644 --- a/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs +++ b/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs @@ -187,7 +187,7 @@ let ``ApiDocs model generation works on two sample F# assemblies``() = model.Collection.Assemblies.[1].Name |> shouldEqual "FsLib2" model.Collection.Namespaces.Length |> shouldEqual 1 model.Collection.Namespaces.[0].Name |> shouldEqual "FsLib" - model.Collection.Namespaces.[0].Entities |> List.filter (fun c -> c.IsTypeDefinition) |> function x -> x.Length |> shouldEqual 9 + model.Collection.Namespaces.[0].Entities |> List.filter (fun c -> c.IsTypeDefinition) |> function x -> x.Length |> shouldEqual 10 let assemblies = [ for t in model.Collection.Namespaces.[0].Entities -> t.Assembly.Name ] assemblies |> List.distinct |> List.sort |> shouldEqual ["FsLib1"; "FsLib2"] @@ -492,7 +492,8 @@ let ``ApiDocs omit works without markdown``() = let files = generateApiDocs [library] false "FsLib2_omit" - files.ContainsKey "fslib-test_omit.html" |> shouldEqual false + // Actually, the thing gets generated it's just not in the index + files.ContainsKey "fslib-test_omit.html" |> shouldEqual true [] let ``ApiDocs test FsLib1``() =