From e45c793584ed5d43c1e36678b045ece369d2fb89 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Sat, 30 Oct 2021 05:50:06 -0500 Subject: [PATCH] Allow user-set ids for xmldoc example nodes (#704) * Allow user-set ids for xmldoc example nodes * actually render example ids in the htmldoc * tests --- src/FSharp.Formatting.ApiDocs/GenerateHtml.fs | 2 +- .../GenerateModel.fs | 91 +++++++++++++------ tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs | 13 +++ .../files/FsLib2/Library2.fs | 20 +++- 4 files changed, 97 insertions(+), 29 deletions(-) diff --git a/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs b/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs index 44e76cf8f..194c29235 100644 --- a/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs +++ b/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs @@ -215,7 +215,7 @@ type HtmlRender(model: ApiDocModel) = for e in m.Comment.Examples do h5 [Class "fsdocs-example-header"] [!! "Example"] - p [Class "fsdocs-example"] [embed e] + p [Class "fsdocs-example"; if e.Id.IsSome then Id e.Id.Value ] [embed e] //if m.IsObsolete then // obsoleteMessage m.ObsoleteMessage diff --git a/src/FSharp.Formatting.ApiDocs/GenerateModel.fs b/src/FSharp.Formatting.ApiDocs/GenerateModel.fs index c095c9d2e..3dee5da67 100644 --- a/src/FSharp.Formatting.ApiDocs/GenerateModel.fs +++ b/src/FSharp.Formatting.ApiDocs/GenerateModel.fs @@ -78,11 +78,20 @@ module internal Utils = module Html = let sepWith s l = l |> List.sepWith (!! s) |> span [] + type System.Xml.Linq.XElement with + member x.TryAttr (attr: string) = + let a = x.Attribute (XName.Get attr) + if a = null then None + else if String.IsNullOrEmpty a.Value then None + else Some a.Value + /// Represents some HTML formatted by model generation -type ApiDocHtml(html: string) = +type ApiDocHtml(html: string, id: string option) = /// Get the HTML text of the HTML section member _.HtmlText = html + /// Get the Id of the element when rendered to html, if any + member _.Id= id /// Represents a documentation comment attached to source code type ApiDocComment(summary, remarks, parameters, returns, examples, notes, exceptions, rawData) = @@ -110,7 +119,7 @@ type ApiDocComment(summary, remarks, parameters, returns, examples, notes, excep /// The raw data of the comment member _.RawData: KeyValuePair list = rawData - static member internal Empty = ApiDocComment(ApiDocHtml(""), None, [], None, [], [], [], []) + static member internal Empty = ApiDocComment(ApiDocHtml("", None), None, [], None, [], [], [], []) /// Represents a custom attribute attached to source code type ApiDocAttribute(name, fullName, constructorArguments, namedConstructorArguments) = @@ -255,7 +264,7 @@ type ApiDocMember (displayName: string, attributes: ApiDocAttribute list, entity comment: ApiDocComment, symbol: FSharpSymbol, warn) = let (ApiDocMemberDetails(usageHtml, paramTypes, returnType, modifiers, typars, baseType, location, compiledName)) = details - + let m = defaultArg symbol.DeclarationLocation range0 // merge the parameter docs and parameter types let parameters = let paramTypes = @@ -267,7 +276,7 @@ type ApiDocMember (displayName: string, attributes: ApiDocAttribute list, entity (psym, pnm, _pnameText, _pty) ] let tnames = Set.ofList [ for (_psym, pnm, _pnameText, _pty) in paramTypes -> pnm ] let tdocs = Map.ofList [ for pnm, doc in comment.Parameters -> Some pnm, doc ] - let m = defaultArg symbol.DeclarationLocation range0 + if warn then for (pn, _pdoc) in comment.Parameters do if not (tnames.Contains (Some pn)) then @@ -286,6 +295,30 @@ type ApiDocMember (displayName: string, attributes: ApiDocAttribute list, entity ParameterType=pty ParameterDocs=tdocs.TryFind pnm |} ] + do + let knownExampleIds = + comment.Examples + |> List.choose (fun x -> x.Id) + |> List.countBy id + for (id, count) in knownExampleIds do + if count > 1 then + if warn + then + printfn "%s(%d,%d): warning: duplicate id for example '%s'" m.FileName m.StartLine m.StartColumn id + else + printfn "%s(%d,%d): error: duplicate id for example '%s'" m.FileName m.StartLine m.StartColumn id + for (id, _count) in knownExampleIds do + if id.StartsWith "example-" then + let potentialInteger = id.["example-".Length..] + match System.Int32.TryParse potentialInteger with + | true, id -> + if warn + then + printfn "%s(%d,%d): warning: automatic identifer generated for example '%d'. Consider adding an explicit example id attribute." m.FileName m.StartLine m.StartColumn id + else + printfn "%s(%d,%d): error: automatic identifer generated for example '%d'. Consider adding an explicit example id attribute." m.FileName m.StartLine m.StartColumn id + | _ -> () + /// The member's modifiers member x.Modifiers : string list = modifiers @@ -857,7 +890,7 @@ module internal TypeFormatter = type TypeFormatterParams = CrossReferenceResolver - let convHtml (html: HtmlElement) = ApiDocHtml(html.ToString()) + let convHtml (html: HtmlElement) = ApiDocHtml(html.ToString(), None) /// We squeeze the spaces out of anything where whitespace layout must be exact - any deliberate /// whitespace must use @@ -866,7 +899,7 @@ module internal TypeFormatter = /// adding spaces which are actually significant when formatting F# type information. let codeHtml html = let html = code [] [html] - ApiDocHtml(html.ToString().Replace(" ", "").Replace("\n","").Replace("\r","").Replace(" int -> int -> string) (sourceFolderRepo : (string * string) option) (location : range option) = location |> Option.bind (fun location -> @@ -1044,7 +1077,7 @@ module internal SymbolReader = static member internal Create (publicOnly, assembly, map, sourceFolderRepo, urlRangeHighlight, mdcomments, urlMap, - assemblyPath, fscOptions, formatAgent, substitutions, warn ) = + assemblyPath, fscOptions, formatAgent, substitutions, warn) = { PublicOnly=publicOnly Assembly = assembly @@ -1057,7 +1090,7 @@ module internal SymbolReader = AssemblyPath = assemblyPath CompilerOptions = fscOptions FormatAgent = formatAgent - Substitutions = substitutions} + Substitutions = substitutions } let inline private getCompiledName (s : ^a when ^a :> FSharpSymbol) = let compiledName = (^a : (member CompiledName : string) (s)) @@ -1385,12 +1418,12 @@ module internal SymbolReader = (remarks |> List.collect (snd >> List.rev) |> List.tailOrEmpty) @ (rest |> List.collect (snd >> List.rev)) - let summary = ApiDocHtml(Literate.ToHtml(doc.With(paragraphs=summary ))) - let remarks = if remarks.IsEmpty then None else Some (ApiDocHtml(Literate.ToHtml(doc.With(paragraphs=remarks)))) + let summary = ApiDocHtml(Literate.ToHtml(doc.With(paragraphs=summary )), None) + let remarks = if remarks.IsEmpty then None else Some (ApiDocHtml(Literate.ToHtml(doc.With(paragraphs=remarks)), None)) //let exceptions = [ for e in exceptions -> ApiDocHtml(Literate.ToHtml(doc.With(paragraphs=[e]))) ] - let notes = [ for e in notes -> ApiDocHtml(Literate.ToHtml(doc.With(paragraphs=e))) ] - let examples = [ for e in examples -> ApiDocHtml(Literate.ToHtml(doc.With(paragraphs=e))) ] - let returns = if returns.IsEmpty then None else Some (ApiDocHtml(Literate.ToHtml(doc.With(paragraphs=returns)))) + let notes = [ for e in notes -> ApiDocHtml(Literate.ToHtml(doc.With(paragraphs=e)), None) ] + let examples = [ for e in examples -> ApiDocHtml(Literate.ToHtml(doc.With(paragraphs=e)), None) ] + let returns = if returns.IsEmpty then None else Some (ApiDocHtml(Literate.ToHtml(doc.With(paragraphs=returns)), None)) ApiDocComment(summary=summary, remarks=remarks, parameters=[], returns=returns, examples=examples, notes=notes, exceptions=[], rawData=raw) @@ -1462,7 +1495,6 @@ module internal SymbolReader = let readXmlCommentAsHtmlAux summaryExpected (urlMap: CrossReferenceResolver) (doc: XElement) (cmds: IDictionary<_, _>) = let rawData = new Dictionary() - // not part of the XML doc standard let nsels = let ds = doc.Elements(XName.Get "namespacedoc") @@ -1479,11 +1511,11 @@ module internal SymbolReader = let n = if id = 0 then "summary" else "summary-" + string id rawData.[n] <- e.Value readXmlElementAsHtml true urlMap cmds html e - ApiDocHtml(html.ToString()) + ApiDocHtml(html.ToString(), None) else let html = new StringBuilder() readXmlElementAsHtml false urlMap cmds html doc - ApiDocHtml(html.ToString()) + ApiDocHtml(html.ToString(), None) let paramNodes = doc.Elements(XName.Get "param") |> Seq.toList let parameters = @@ -1491,7 +1523,7 @@ module internal SymbolReader = let paramName = e.Attribute(XName.Get "name").Value let phtml = new StringBuilder() readXmlElementAsHtml true urlMap cmds phtml e - let paramHtml = ApiDocHtml(phtml.ToString()) + let paramHtml = ApiDocHtml(phtml.ToString(), None) paramName, paramHtml ] for e in doc.Elements(XName.Get "exclude") do @@ -1515,7 +1547,7 @@ module internal SymbolReader = let n = if id = 0 then "remarks" else "remarks-" + string id rawData.[n] <- e.Value readXmlElementAsHtml true urlMap cmds html e - ApiDocHtml(html.ToString()) |> Some + ApiDocHtml(html.ToString(), None) |> Some else None @@ -1527,7 +1559,7 @@ module internal SymbolReader = let n = if id = 0 then "returns" else "returns-" + string id rawData.[n] <- e.Value readXmlElementAsHtml true urlMap cmds html e - Some (ApiDocHtml(html.ToString())) + Some (ApiDocHtml(html.ToString(), None)) else None @@ -1547,23 +1579,28 @@ module internal SymbolReader = match urlMap.ResolveCref cname with | Some reference -> let html = new StringBuilder() - rawData.["exception-" + reference.NiceName] <- reference.ReferenceLink + let referenceLinkId = "exception-" + reference.NiceName + rawData.[referenceLinkId] <- reference.ReferenceLink readXmlElementAsHtml true urlMap cmds html e - reference.NiceName, Some reference.ReferenceLink, ApiDocHtml(html.ToString()) + reference.NiceName, Some reference.ReferenceLink, ApiDocHtml(html.ToString(), None) | _ -> let html = new StringBuilder() readXmlElementAsHtml true urlMap cmds html e - cname, None, ApiDocHtml(html.ToString()) + cname, None, ApiDocHtml(html.ToString(), None) ] let examples = let exampleNodes = doc.Elements(XName.Get "example") |> Seq.toList [ for (id, e) in List.indexed exampleNodes do let html = new StringBuilder() - let n = if id = 0 then "example" else "example-" + string id - rawData.[n] <- e.Value + let exampleId = + match e.TryAttr "id" with + | None -> + if id = 0 then "example" else "example-" + string id + | Some attrId -> attrId + rawData.[exampleId] <- e.Value readXmlElementAsHtml true urlMap cmds html e - ApiDocHtml(html.ToString()) ] + ApiDocHtml(html.ToString(), Some exampleId) ] let notes = let noteNodes = doc.Elements(XName.Get "note") |> Seq.toList @@ -1573,7 +1610,7 @@ module internal SymbolReader = let n = if id = 0 then "note" else "note-" + string id rawData.[n] <- e.Value readXmlElementAsHtml true urlMap cmds html e - ApiDocHtml(html.ToString()) ] + ApiDocHtml(html.ToString(), None) ] // put the non-xmldoc sections into rawData doc.Descendants () @@ -1602,7 +1639,7 @@ module internal SymbolReader = comment, nsels let combineHtml (h1: ApiDocHtml) (h2: ApiDocHtml) = - ApiDocHtml(String.concat "\n" [h1.HtmlText; h2.HtmlText]) + ApiDocHtml(String.concat "\n" [h1.HtmlText; h2.HtmlText], None) let combineHtmlOptions (h1: ApiDocHtml option) (h2: ApiDocHtml option) = match h1, h2 with diff --git a/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs b/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs index e11ca3d2f..491a4349d 100644 --- a/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs +++ b/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs @@ -617,6 +617,19 @@ let ``ApiDocs test FsLib1`` (format:OutputFormat) = files.ContainsKey (sprintf "fslib-test_omit.%s" format.Extension) |> shouldEqual false +[] +let ``ApiDocs test examples`` () = + let library = testBin "FsLib2.dll" |> fullpath + + let files = generateApiDocs [library] OutputFormat.Html false "FsLib2_examples" + + let testFile = sprintf "fslib-commentexamples.%s" OutputFormat.Html.Extension + + files.ContainsKey testFile |> shouldEqual true + let content = files.[testFile] + content.Contains "has-id" |> shouldEqual true + + // -------------------Indirect links---------------------------------- [] [] diff --git a/tests/FSharp.ApiDocs.Tests/files/FsLib2/Library2.fs b/tests/FSharp.ApiDocs.Tests/files/FsLib2/Library2.fs index d962d1548..58d3a3b26 100644 --- a/tests/FSharp.ApiDocs.Tests/files/FsLib2/Library2.fs +++ b/tests/FSharp.ApiDocs.Tests/files/FsLib2/Library2.fs @@ -127,4 +127,22 @@ module ``Space-Missing`` = /// Implicit cast operator test type ``Implicit-Cast``(value: int) = class end with static member op_Implicit (source: int) : ``Implicit-Cast`` = ``Implicit-Cast``(source) - \ No newline at end of file + + +module CommentExamples = + /// this does the thing + /// this is an example + /// + let dothing() = () + + /// this does the thing + /// this is an example with an id + /// + let dothing2() = () + + /// this does the thing + /// this is an example with an id + /// + /// this is an example with an id + /// + let doubleExampleId() = () \ No newline at end of file