")
+
+ let notes, rest = rest |> Seq.toList |> List.partition (fun (section, _) -> section = "Notes")
+
+ let examples, rest = rest |> Seq.toList |> List.partition (fun (section, _) -> section = "Examples")
+
+ let returns, rest = rest |> Seq.toList |> List.partition (fun (section, _) -> section = "Returns")
+
+ let remarks, rest = rest |> Seq.toList |> List.partition (fun (section, _) -> section = "Remarks")
+ //let exceptions, rest = rest |> Seq.toList |> List.partition (fun (section, _) -> section = "Exceptions")
+ //let parameters, rest = rest |> Seq.toList |> List.partition (fun (section, _) -> section = "Parameters")
+
+ // tailOrEmpty drops the section headings, though not for summary which is implicit
+ let summary = summary |> List.collect (snd >> List.rev)
+
+ let returns = returns |> List.collect (snd >> List.rev) |> List.tailOrEmpty
+
+ let examples = examples |> List.map (snd >> List.rev) |> List.tailOrEmpty
+
+ let notes = notes |> List.map (snd >> List.rev) |> List.tailOrEmpty
+ //let exceptions = exceptions |> List.collect (snd >> List.rev) |> List.tailOrEmpty
+ //let parameters = parameters |> List.collect (snd >> List.rev) |> List.tailOrEmpty
+
+ // All unclassified things go in 'remarks'
+ let remarks =
+ (remarks |> List.collect (snd >> List.rev) |> List.tailOrEmpty)
+ @ (rest |> List.collect (snd >> List.rev))
+
+ 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)), 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(
+ xmldoc = Some el,
+ summary = summary,
+ remarks = remarks,
+ parameters = [],
+ returns = returns,
+ examples = examples,
+ notes = notes,
+ exceptions = [],
+ rawData = raw
+ )
+
+ let findCommand cmd =
+ match cmd with
+ | StringPosition.StartsWithWrapped ("[", "]") (ParseCommand (k, v), _rest) -> Some(k, v)
+ | _ -> 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
+ | "paramref" ->
+ let name = elem.Attribute(XName.Get "name")
+ let nameAsHtml = HttpUtility.HtmlEncode name.Value
+
+ if name <> null then
+ html.AppendFormat("{0}", nameAsHtml)
+ |> 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 ->
+ html.AppendFormat("{1}", reference.ReferenceLink, reference.NiceName)
+ |> ignore
+ | _ ->
+ urlMap.ResolveCref cname |> ignore
+ //let crefAsHtml = HttpUtility.HtmlEncode cref.Value
+ html.Append(cref.Value) |> ignore
+ | "c" ->
+ html.Append("") |> ignore
+
+ let code = elem.Value.TrimEnd('\r', '\n', ' ')
+ let codeAsHtml = HttpUtility.HtmlEncode code
+ html.Append(codeAsHtml) |> ignore
+
+ html.Append("
") |> ignore
+ | "code" ->
+ let code =
+ let code = Literate.ParseMarkdownString("```\n" + elem.Value.TrimEnd('\r', '\n', ' ') + "\n```")
+ Literate.ToHtml(code, lineNumbers = false)
+
+ html.Append(code) |> 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
+ let elemAsXml = elem.ToString()
+ html.Append(elemAsXml) |> ignore
+
+ let readXmlCommentAsHtmlAux
+ summaryExpected
+ (urlMap: CrossReferenceResolver)
+ (doc: XElement)
+ (cmds: IDictionary<_, _>)
+ =
+ printfn "readXmlCommentAsHtmlAux %A" doc
+ let rawData = new Dictionary()
+ // not part of the XML doc standard
+ let nsels =
+ let ds = doc.Elements(XName.Get "namespacedoc")
+
+ if Seq.length ds > 0 then Some(Seq.toList ds) else None
+
+ let summary =
+ if summaryExpected then
+ let summaries = doc.Elements(XName.Get "summary") |> Seq.toList
+
+ let html = new StringBuilder()
+
+ for (id, e) in List.indexed summaries do
+ let n = if id = 0 then "summary" else "summary-" + string id
+
+ rawData.[n] <- e.Value
+ readXmlElementAsHtml true urlMap cmds html e
+
+ ApiDocHtml(html.ToString(), None)
+ else
+ let html = new StringBuilder()
+ readXmlElementAsHtml false urlMap cmds html doc
+ ApiDocHtml(html.ToString(), None)
+
+ let paramNodes = doc.Elements(XName.Get "param") |> Seq.toList
+
+ let parameters =
+ [ for e in paramNodes do
+ let paramName = e.Attribute(XName.Get "name").Value
+ let phtml = new StringBuilder()
+ readXmlElementAsHtml true urlMap cmds phtml e
+ let paramHtml = ApiDocHtml(phtml.ToString(), None)
+ paramName, paramHtml ]
+
+ printfn "Checking for exlucde & omit: "
+ for e in doc.Elements(XName.Get "exclude") do
+ printfn "Found 'exclude': %A" e.Value
+ cmds.["exclude"] <- e.Value
+
+ for e in doc.Elements(XName.Get "omit") do
+ printfn "Found 'omit': %A" e.Value
+ cmds.["omit"] <- e.Value
+
+ for e in doc.Elements(XName.Get "category") do
+ match e.Attribute(XName.Get "index") with
+ | null -> ()
+ | a -> cmds.["categoryindex"] <- a.Value
+
+ cmds.["category"] <- e.Value
+
+ let remarks =
+ let remarkNodes = doc.Elements(XName.Get "remarks") |> Seq.toList
+
+ if Seq.length remarkNodes > 0 then
+ let html = new StringBuilder()
+
+ for (id, e) in List.indexed remarkNodes do
+ let n = if id = 0 then "remarks" else "remarks-" + string id
+
+ rawData.[n] <- e.Value
+ readXmlElementAsHtml true urlMap cmds html e
+
+ ApiDocHtml(html.ToString(), None) |> Some
+ else
+ None
+
+ let returns =
+ let html = new StringBuilder()
+
+ let returnNodes = doc.Elements(XName.Get "returns") |> Seq.toList
+
+ if returnNodes.Length > 0 then
+ for (id, e) in List.indexed returnNodes do
+ 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(), None))
+ else
+ None
+
+ let exceptions =
+ let exceptionNodes = doc.Elements(XName.Get "exception") |> Seq.toList
+
+ [ for e in exceptionNodes do
+ let cref = e.Attribute(XName.Get "cref")
+
+ if cref <> null then
+ if String.IsNullOrEmpty(cref.Value) || cref.Value.Length < 3 then
+ printfn "Warning: Invalid cref specified in: %A" doc
+
+ else
+ // 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.StartsWith("T:") then cname else "T:" + cname // FSharp.Core exception listings don't start with "T:"
+
+ match urlMap.ResolveCref cname with
+ | Some reference ->
+ let html = new StringBuilder()
+ let referenceLinkId = "exception-" + reference.NiceName
+ rawData.[referenceLinkId] <- reference.ReferenceLink
+ readXmlElementAsHtml true urlMap cmds html e
+ reference.NiceName, Some reference.ReferenceLink, ApiDocHtml(html.ToString(), None)
+ | _ ->
+ let html = new StringBuilder()
+ readXmlElementAsHtml true urlMap cmds html e
+ cname, None, ApiDocHtml(html.ToString(), None) ]
+
+ let examples =
+ let exampleNodes = doc.Elements(XName.Get "example") |> Seq.toList
+
+ [ for (id, e) in List.indexed exampleNodes do
+ let html = new StringBuilder()
+
+ 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(), Some exampleId) ]
+
+ let notes =
+ let noteNodes = doc.Elements(XName.Get "note") |> Seq.toList
+ // 'note' is not part of the XML doc standard but is supported by Sandcastle and other tools
+ [ for (id, e) in List.indexed noteNodes do
+ let html = new StringBuilder()
+
+ let n = if id = 0 then "note" else "note-" + string id
+
+ rawData.[n] <- e.Value
+ readXmlElementAsHtml true urlMap cmds html e
+ ApiDocHtml(html.ToString(), None) ]
+
+ // 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)
+ |> Seq.iter (fun (n, lst) ->
+ let lst = Seq.toList lst
+
+ match lst with
+ | [ x ] -> rawData.[n] <- x.Value
+ | lst -> lst |> Seq.iteri (fun id el -> rawData.[n + "-" + string id] <- el.Value))
+
+ let rawData = rawData |> Seq.toList
+
+ let comment =
+ ApiDocComment(
+ xmldoc = Some doc,
+ summary = summary,
+ remarks = remarks,
+ parameters = parameters,
+ returns = returns,
+ examples = examples,
+ notes = notes,
+ exceptions = exceptions,
+ rawData = rawData
+ )
+
+ comment, nsels
+
+ let combineHtml (h1: ApiDocHtml) (h2: ApiDocHtml) =
+ ApiDocHtml(String.concat "\n" [ h1.HtmlText; h2.HtmlText ], None)
+
+ let combineHtmlOptions (h1: ApiDocHtml option) (h2: ApiDocHtml option) =
+ match h1, h2 with
+ | x, None -> x
+ | None, x -> x
+ | Some x, Some y -> Some(combineHtml x y)
+
+ let combineComments (c1: ApiDocComment) (c2: ApiDocComment) =
+ ApiDocComment(
+ xmldoc =
+ (match c1.Xml with
+ | None -> c2.Xml
+ | v -> v),
+ summary = combineHtml c1.Summary c2.Summary,
+ remarks = combineHtmlOptions c1.Remarks c2.Remarks,
+ parameters = c1.Parameters @ c2.Parameters,
+ examples = c1.Examples @ c2.Examples,
+ returns = combineHtmlOptions c1.Returns c2.Returns,
+ notes = c1.Notes @ c2.Notes,
+ exceptions = c1.Exceptions @ c2.Exceptions,
+ rawData = c1.RawData @ c2.RawData
+ )
+
+ let combineNamespaceDocs nspDocs =
+ nspDocs
+ |> List.choose id
+ |> function
+ | [] -> None
+ | xs -> Some(List.reduce combineComments xs)
+
+ let rec readXmlCommentAsHtml (urlMap: CrossReferenceResolver) (doc: XElement) (cmds: IDictionary<_, _>) =
+ let doc, nsels = readXmlCommentAsHtmlAux true urlMap doc cmds
+
+ let nsdocs = readNamespaceDocs urlMap nsels
+ doc, nsdocs
+
+ and readNamespaceDocs (urlMap: CrossReferenceResolver) (nsels: XElement list option) =
+ let nscmds = Dictionary() :> IDictionary<_, _>
+
+ nsels
+ |> Option.map (
+ List.map (fun n -> fst (readXmlCommentAsHtml urlMap n nscmds))
+ >> List.reduce combineComments
+ )
+
+ /// Returns all indirect links in a specified span node
+ let rec collectSpanIndirectLinks span =
+ seq {
+ match span with
+ | IndirectLink (_, _, key, _) -> yield key
+ | MarkdownPatterns.SpanLeaf _ -> ()
+ | MarkdownPatterns.SpanNode (_, spans) ->
+ for s in spans do
+ yield! collectSpanIndirectLinks s
+ }
+
+ /// Returns all indirect links in the specified paragraph node
+ let rec collectParagraphIndirectLinks par =
+ seq {
+ match par with
+ | MarkdownPatterns.ParagraphLeaf _ -> ()
+ | MarkdownPatterns.ParagraphNested (_, pars) ->
+ for ps in pars do
+ for p in ps do
+ yield! collectParagraphIndirectLinks p
+ | MarkdownPatterns.ParagraphSpans (_, spans) ->
+ for s in spans do
+ yield! collectSpanIndirectLinks s
+ }
+
+ /// Returns whether the link is not included in the document defined links
+ let linkNotDefined (doc: LiterateDocument) (link: string) =
+ [ link; link.Replace("\r\n", ""); link.Replace("\r\n", " "); link.Replace("\n", ""); link.Replace("\n", " ") ]
+ |> Seq.map (fun key -> not (doc.DefinedLinks.ContainsKey(key)))
+ |> Seq.reduce (fun a c -> a && c)
+
+ /// Returns a tuple of the undefined link and its Cref if it exists
+ let getTypeLink (ctx: ReadingContext) undefinedLink =
+ // Append 'T:' to try to get the link from urlmap
+ match ctx.UrlMap.ResolveCref("T:" + undefinedLink) with
+ | Some cRef -> if cRef.IsInternal then Some(undefinedLink, cRef) else None
+ | None -> None
+
+ /// Adds a cross-type link to the document defined links
+ let addLinkToType (doc: LiterateDocument) link =
+ match link with
+ | Some (k, v) -> do doc.DefinedLinks.Add(k, (v.ReferenceLink, Some v.NiceName))
+ | None -> ()
+
+ /// Wraps the span inside an IndirectLink if it is an inline code that can be converted to a link
+ let wrapInlineCodeLinksInSpans (ctx: ReadingContext) span =
+ match span with
+ | InlineCode (code, r) ->
+ match getTypeLink ctx code with
+ | Some _ -> IndirectLink([ span ], code, code, r)
+ | None -> span
+ | _ -> span
+
+ /// Wraps inside an IndirectLink all inline code spans in the paragraph that can be converted to a link
+ let rec wrapInlineCodeLinksInParagraphs (ctx: ReadingContext) (para: MarkdownParagraph) =
+ match para with
+ | MarkdownPatterns.ParagraphLeaf _ -> para
+ | MarkdownPatterns.ParagraphNested (info, pars) ->
+ MarkdownPatterns.ParagraphNested(
+ info,
+ pars
+ |> List.map (fun innerPars -> List.map (wrapInlineCodeLinksInParagraphs ctx) innerPars)
+ )
+ | MarkdownPatterns.ParagraphSpans (info, spans) ->
+ MarkdownPatterns.ParagraphSpans(info, List.map (wrapInlineCodeLinksInSpans ctx) spans)
+
+ /// Adds the missing links to types to the document defined links
+ let addMissingLinkToTypes ctx (doc: LiterateDocument) =
+ let replacedParagraphs = doc.Paragraphs |> List.map (wrapInlineCodeLinksInParagraphs ctx)
+
+ do
+ replacedParagraphs
+ |> Seq.collect collectParagraphIndirectLinks
+ |> Seq.filter (linkNotDefined doc)
+ |> Seq.map (getTypeLink ctx)
+ |> Seq.iter (addLinkToType doc)
+
+ doc.With(paragraphs = replacedParagraphs)
+
+ let readMarkdownCommentAndCommands (ctx: ReadingContext) text el (cmds: IDictionary<_, _>) =
+ let lines = removeSpaces text |> List.map (fun s -> (s, MarkdownRange.zero))
+
+ let text =
+ lines
+ |> List.filter (
+ findCommand
+ >> (function
+ | Some (k, v) ->
+ cmds.[k] <- v
+ false
+ | _ -> true)
+ )
+ |> List.map fst
+ |> String.concat "\n"
+
+ let doc =
+ Literate.ParseMarkdownString(
+ text,
+ path = Path.Combine(ctx.AssemblyPath, "docs.fsx"),
+ fscOptions = ctx.CompilerOptions
+ )
+
+ let doc = doc |> addMissingLinkToTypes ctx
+ let html = readMarkdownCommentAsHtml el doc
+ // TODO: namespace summaries for markdown comments
+ let nsdocs = None
+ cmds, html, nsdocs
+
+ let readXmlCommentAndCommands (ctx: ReadingContext) text el (cmds: IDictionary<_, _>) =
+ let lines = removeSpaces text |> List.map (fun s -> (s, MarkdownRange.zero))
+
+ let html, nsdocs = readXmlCommentAsHtml ctx.UrlMap el cmds
+
+ lines
+ |> Seq.choose findCommand
+ |> Seq.iter (fun (k, v) ->
+ printfn
+ "The use of `[%s]` and other commands in XML comments is deprecated, please use XML extensions, see https://github.com/fsharp/fslang-design/blob/master/tooling/FST-1031-xmldoc-extensions.md"
+ k
+
+ cmds.[k] <- v)
+
+ cmds, html, nsdocs
+
+ let readCommentAndCommands (ctx: ReadingContext) xmlSig (m: range option) =
+ let cmds = Dictionary() :> IDictionary<_, _>
+
+ match ctx.XmlMemberLookup(xmlSig) with
+ | None ->
+ if not (System.String.IsNullOrEmpty xmlSig) then
+ if ctx.WarnOnMissingDocs then
+ let m = defaultArg m range0
+
+ if ctx.UrlMap.IsLocal xmlSig then
+ printfn
+ "%s(%d,%d): warning FD0001: no documentation for '%s'"
+ m.FileName
+ m.StartLine
+ m.StartColumn
+ xmlSig
+
+ cmds, ApiDocComment.Empty, None
+ | Some el ->
+ printfn "readCommentAndCommands: element='%A'\nvalue='%A' markdown=%A\n" el (el.Value) (ctx.MarkdownComments)
+ let sum = el.Element(XName.Get "summary")
+
+ match sum with
+
+ // sum can be null with null/empty el.Value when an non-"" XML element appears
+ // as the only '///' documentation command:
+ //
+ // 1.
+ // // Not triple-slash ccomment
+ // ///
+ //
+ // 2.
+ // ///
+ //
+ // So, we need to let the 'null' case handle this to extract the if it's there
+ //
+ // | null when String.IsNullOrEmpty el.Value ->
+ // cmds, ApiDocComment.Empty, None
+
+ | null ->
+ // We let through XML comments without a summary tag. It's not clear
+ // why as all XML coming through here should be from F# .XML files
+ // and should have the tag. It may be legacy of previously processing un-processed
+ // XML in raw F# source.
+ //
+ // 9-Jan-23: See comment above for at least one reason why we pass through here now
+ printfn "* sum = null '%A' nullOrEmpty=%A" sum (String.IsNullOrEmpty el.Value)
+ let doc, nsels = readXmlCommentAsHtmlAux false ctx.UrlMap el cmds
+
+ let nsdocs = readNamespaceDocs ctx.UrlMap nsels
+ cmds, doc, nsdocs
+
+ | sum ->
+ printfn "* sum = %A (value=%s) node=%s" sum (sum.Value) (sum.FirstNode.ToString())
+ if ctx.MarkdownComments then
+ readMarkdownCommentAndCommands ctx sum.Value el cmds
+ else
+ if sum.Value.Contains("\" in text of \"\" for \"%s\". Please see https://fsprojects.github.io/FSharp.Formatting/apidocs.html#Classic-XML-Doc-Comments"
+ xmlSig
+
+ readXmlCommentAndCommands ctx sum.Value el cmds
+
+
+ /// 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 =
+ printfn "\n-- %A --------------------------------------------------------- " sym
+
+ let cmds, comment, nsdocs = readCommentAndCommands ctx xmlDocSig sym.DeclarationLocation
+
+ printfn "readCommentsInto %A %s" cmds xmlDocSig
+ match cmds with
+ | 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 catindex exclude cmds comment, nsdocs)
+ with e ->
+ let name =
+ try
+ sym.FullName
+ with _ ->
+ try
+ sym.DisplayName
+ with _ ->
+ let part =
+ try
+ let ass = sym.Assembly
+
+ match ass.FileName with
+ | Some file -> file
+ | None -> ass.QualifiedName
+ with _ ->
+ "unknown"
+
+ sprintf "unknown, part of %s" part
+
+ printfn "Could not read comments from entity '%s': %O" name e
+ None
+
+ let checkAccess ctx (access: FSharpAccessibility) = not ctx.PublicOnly || access.IsPublic
+
+ 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)
+ |> Seq.filter cond
+ |> 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 catidx exclude _ comment ->
+ let details = readMemberOrVal ctx memb
+
+ ApiDocMember(
+ memb.DisplayName,
+ readAttributes memb.Attributes,
+ entityUrl,
+ kind,
+ cat,
+ catidx,
+ exclude,
+ details,
+ comment,
+ memb,
+ ctx.WarnOnMissingDocs
+ ))
+
+ let readAllMembers ctx entityUrl kind (members: seq) =
+ members
+ |> Seq.filter (fun v -> checkAccess ctx v.Accessibility)
+ |> Seq.filter (fun v ->
+ not v.IsCompilerGenerated
+ && not v.IsPropertyGetterMethod
+ && not v.IsPropertySetterMethod
+ && not v.IsEventAddMethod
+ && not v.IsEventRemoveMethod)
+ |> 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
+ |> collectNamespaceDocs
+
+ let readTypeNameAsText (typ: FSharpEntity) =
+ typ.GenericParameters
+ |> List.ofSeq
+ |> List.map (fun p -> sprintf "'%s" p.Name)
+ |> function
+ | [] -> typ.DisplayName
+ | gnames ->
+ let gtext = String.concat ", " gnames
+
+ if typ.UsesPrefixDisplay then
+ sprintf "%s<%s>" typ.DisplayName gtext
+ else
+ sprintf "%s %s" gtext typ.DisplayName
+
+ let readUnionCases ctx entityUrl (typ: FSharpEntity) =
+ typ.UnionCases
+ |> List.ofSeq
+ |> List.filter (fun v -> checkAccess ctx v.Accessibility)
+ |> List.choose (fun case ->
+ 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,
+ catidx,
+ exclude,
+ details,
+ comment,
+ case,
+ ctx.WarnOnMissingDocs
+ )))
+ |> collectNamespaceDocs
+
+ let readRecordFields ctx entityUrl (typ: FSharpEntity) =
+ typ.FSharpFields
+ |> List.ofSeq
+ |> List.filter (fun field -> not field.IsCompilerGenerated)
+ |> List.choose (fun field ->
+ 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,
+ catidx,
+ exclude,
+ details,
+ comment,
+ field,
+ ctx.WarnOnMissingDocs
+ )))
+ |> collectNamespaceDocs
+
+ let readStaticParams ctx entityUrl (typ: FSharpEntity) =
+ typ.StaticParameters
+ |> List.ofSeq
+ |> List.choose (fun staticParam ->
+ readCommentsInto
+ staticParam
+ ctx
+ (getFSharpStaticParamXmlSig typ staticParam.Name)
+ (fun cat catidx exclude _ comment ->
+ let details = readFSharpStaticParam ctx staticParam
+
+ ApiDocMember(
+ staticParam.Name,
+ [],
+ entityUrl,
+ ApiDocMemberKind.StaticParameter,
+ cat,
+ catidx,
+ exclude,
+ details,
+ comment,
+ staticParam,
+ ctx.WarnOnMissingDocs
+ )))
+ |> collectNamespaceDocs
+
+ let xmlDocText (xmlDoc: FSharpXmlDoc) =
+ match xmlDoc with
+ | FSharpXmlDoc.FromXmlText (xmlDoc) -> String.concat "" xmlDoc.UnprocessedLines
+ | _ -> ""
+
+ // Create a xml documentation snippet and add it to the XmlMemberMap
+ let registerXmlDoc (ctx: ReadingContext) xmlDocSig (xmlDoc: string) =
+ let xmlDoc =
+ if xmlDoc.Contains "" then
+ xmlDoc
+ else
+ "" + xmlDoc + ""
+
+ let xmlDoc = "" + xmlDoc + ""
+
+ let xmlDoc = XElement.Parse xmlDoc
+ ctx.XmlMemberMap.Add(xmlDocSig, xmlDoc)
+ xmlDoc
+
+ // Provided types don't have their docs dumped into the xml file,
+ // so we need to add them to the XmlMemberMap separately
+ let registerProvidedTypeXmlDocs (ctx: ReadingContext) (typ: FSharpEntity) =
+ let xmlDoc = registerXmlDoc ctx typ.XmlDocSig (xmlDocText typ.XmlDoc)
+
+ xmlDoc.Elements(XName.Get "param")
+ |> Seq.choose (fun p ->
+ let nameAttr = p.Attribute(XName.Get "name")
+
+ if nameAttr = null then
+ None
+ else
+ Some(nameAttr.Value, p.Value))
+ |> Seq.iter (fun (name, xmlDoc) ->
+ let xmlDocSig = getFSharpStaticParamXmlSig typ name
+
+ registerXmlDoc ctx xmlDocSig (Security.SecurityElement.Escape xmlDoc) |> ignore)
+
+ let rec readType (ctx: ReadingContext) (typ: FSharpEntity) =
+ if typ.IsProvided && typ.XmlDoc <> FSharpXmlDoc.None then
+ registerProvidedTypeXmlDocs ctx typ
+
+ let xmlDocSig = getXmlDocSigForType typ
+
+ printfn "readType %A " typ.DisplayName
+
+ readCommentsInto typ ctx xmlDocSig (fun cat catidx exclude _cmds comment ->
+ let entityUrl = ctx.UrlMap.ResolveUrlBaseNameForEntity typ
+
+ let rec getMembers (typ: FSharpEntity) =
+ [ yield! typ.MembersFunctionsAndValues
+ match typ.BaseType with
+ | Some baseType ->
+ let loc = typ.DeclarationLocation
+
+ let cmds, _comment, _ =
+ readCommentAndCommands ctx (getXmlDocSigForType baseType.TypeDefinition) (Some loc)
+
+ match cmds with
+ | Command "exclude" _
+ | Command "omit" _ -> yield! getMembers baseType.TypeDefinition
+ | _ -> ()
+ | None -> () ]
+
+ let ivals, svals =
+ getMembers typ
+ |> List.ofSeq
+ |> List.filter (fun v ->
+ checkAccess ctx v.Accessibility
+ && not v.IsCompilerGenerated
+ && not v.IsOverrideOrExplicitInterfaceImplementation)
+ |> List.filter (fun v ->
+ not v.IsCompilerGenerated
+ && not v.IsEventAddMethod
+ && not v.IsEventRemoveMethod
+ && not v.IsPropertyGetterMethod
+ && not v.IsPropertySetterMethod)
+ |> List.partition (fun v -> v.IsInstanceMember)
+
+ let cvals, svals = svals |> List.partition (fun v -> v.CompiledName = ".ctor")
+
+ let baseType =
+ typ.BaseType
+ |> Option.map (fun bty -> bty, bty |> formatTypeAsHtml ctx.UrlMap |> codeHtml)
+
+ let allInterfaces = [ for i in typ.AllInterfaces -> (i, formatTypeAsHtml ctx.UrlMap i |> codeHtml) ]
+
+ let abbreviatedType =
+ if typ.IsFSharpAbbreviation then
+ Some(typ.AbbreviatedType, formatTypeAsHtml ctx.UrlMap typ.AbbreviatedType |> codeHtml)
+ else
+ None
+
+ let delegateSignature =
+ if typ.IsDelegate then
+ Some(
+ typ.FSharpDelegateSignature,
+ formatDelegateSignatureAsHtml ctx.UrlMap typ.DisplayName typ.FSharpDelegateSignature
+ |> codeHtml
+ )
+ else
+ None
+
+ let name = readTypeNameAsText 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, 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"
+
+ let loc = tryGetLocation typ
+
+ let location = formatSourceLocation ctx.UrlRangeHighlight ctx.SourceFolderRepository loc
+
+ ApiDocEntity(
+ true,
+ name,
+ cat,
+ catidx,
+ exclude,
+ entityUrl,
+ comment,
+ ctx.Assembly,
+ attrs,
+ cases,
+ fields,
+ statParams,
+ ctors,
+ inst,
+ stat,
+ allInterfaces,
+ baseType,
+ abbreviatedType,
+ delegateSignature,
+ typ,
+ [],
+ [],
+ [],
+ [],
+ rqa,
+ location,
+ ctx.Substitutions
+ ))
+
+ and readModule (ctx: ReadingContext) (modul: FSharpEntity) =
+ printfn "readModule %A : %A" modul.DisplayName modul.XmlDocSig
+
+ match ctx.XmlMemberLookup(modul.XmlDocSig) with
+ | None -> ()
+ | Some el ->
+ printfn "doc for module %A : %A" modul.DisplayName el
+ ()
+
+ let _result = readCommentsInto modul ctx modul.XmlDocSig (fun cat catidx exclude _cmd comment ->
+
+ printfn " > readModule %A exclude=%A" modul.DisplayName exclude
+
+ // Properties & value bindings in the module
+ let entityUrl = ctx.UrlMap.ResolveUrlBaseNameForEntity modul
+
+ 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, nsdocs4 = readEntities ctx modul.NestedEntities
+
+ let rqa =
+ hasAttrib modul.Attributes
+ // Hack for FSHarp.Core - `Option` module doesn't have RQA but really should have
+ || (modul.Namespace = Some "Microsoft.FSharp.Core" && modul.DisplayName = "Option")
+ || (modul.Namespace = Some "Microsoft.FSharp.Core"
+ && modul.DisplayName = "ValueOption")
+
+ let nsdocs = combineNamespaceDocs [ nsdocs1; nsdocs2; nsdocs3; nsdocs4 ]
+
+ if nsdocs.IsSome then
+ printfn "ignoring namespace summary on nested position"
+
+ let loc = tryGetLocation modul
+
+ let location = formatSourceLocation ctx.UrlRangeHighlight ctx.SourceFolderRepository loc
+
+ ApiDocEntity(
+ false,
+ modul.DisplayName,
+ cat,
+ catidx,
+ exclude,
+ entityUrl,
+ comment,
+ ctx.Assembly,
+ attrs,
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ None,
+ None,
+ None,
+ modul,
+ entities,
+ vals,
+ exts,
+ pats,
+ rqa,
+ location,
+ ctx.Substitutions
+ ))
+
+ printfn "module result: %A %A" modul.DisplayName _result
+ _result
+
+ and readEntities ctx (entities: seq<_>) =
+ let modifiers, nsdocs1 = readChildren ctx entities readModule (fun x -> x.IsFSharpModule)
+ printfn "readEntities"
+
+ let typs, nsdocs2 = readChildren ctx entities readType (fun x -> not x.IsFSharpModule)
+
+ (modifiers @ typs), combineNamespaceDocs [ nsdocs1; nsdocs2 ]
+
+ // ----------------------------------------------------------------------------------------------
+ // Reading namespace and assembly details
+ // ----------------------------------------------------------------------------------------------
+
+ let stripMicrosoft (str: string) =
+ if str.StartsWith("Microsoft.") then
+ str.["Microsoft.".Length ..]
+ elif str.StartsWith("microsoft-") then
+ str.["microsoft-".Length ..]
+ else
+ str
+
+ let readNamespace ctx (ns, entities: seq) =
+ let entities, nsdocs = readEntities ctx entities
+ ApiDocNamespace(stripMicrosoft ns, entities, ctx.Substitutions, nsdocs)
+
+ let readAssembly
+ (
+ assembly: FSharpAssembly,
+ publicOnly,
+ xmlFile: string,
+ substitutions,
+ sourceFolderRepo,
+ urlRangeHighlight,
+ mdcomments,
+ urlMap,
+ codeFormatCompilerArgs,
+ warn
+ ) =
+ let assemblyName = AssemblyName(assembly.QualifiedName)
+
+ // Read in the supplied XML file, map its name attributes to document text
+ let doc = XDocument.Load(xmlFile)
+
+ // don't use 'dict' to allow the dictionary to be mutated later on
+ let xmlMemberMap = Dictionary()
+
+ for key, value in
+ [ for e in doc.Descendants(XName.Get "member") do
+ let attr = e.Attribute(XName.Get "name")
+
+ if attr <> null && not (String.IsNullOrEmpty(attr.Value)) then
+ yield attr.Value, e ] do
+ // NOTE: We completely ignore duplicate keys and I don't see
+ // an easy way to detect where "value" is coming from, because the entries
+ // are completely identical.
+ // We just take the last here because it is the easiest to implement.
+ // Additionally we log a warning just in case this is an issue in the future.
+ // See https://github.com/fsprojects/FSharp.Formatting/issues/229
+ // and https://github.com/fsprojects/FSharp.Formatting/issues/287
+ if xmlMemberMap.ContainsKey key then
+ Log.warnf "Duplicate documentation for '%s', one will be ignored!" key
+
+ xmlMemberMap.[key] <- value
+
+ // Code formatting agent & options used when processing inline code snippets in comments
+ let asmPath = Path.GetDirectoryName(defaultArg assembly.FileName xmlFile)
+
+ let ctx =
+ ReadingContext.Create(
+ publicOnly,
+ assemblyName,
+ xmlMemberMap,
+ sourceFolderRepo,
+ urlRangeHighlight,
+ mdcomments,
+ urlMap,
+ asmPath,
+ codeFormatCompilerArgs,
+ substitutions,
+ warn
+ )
+
+ //
+ let namespaces =
+ assembly.Contents.Entities
+ |> Seq.filter (fun modul -> checkAccess ctx modul.Accessibility)
+ |> Seq.groupBy (fun modul -> modul.AccessPath)
+ |> Seq.sortBy fst
+ |> Seq.map (readNamespace ctx)
+ |> List.ofSeq
+
+ assemblyName, namespaces
+
+/// Represents an input assembly for API doc generation
+type ApiDocInput =
+ {
+ /// The path to the assembly
+ Path: string
+
+ /// Override the default XML file (normally assumed to live alongside)
+ XmlFile: string option
+
+ /// The compile-time source folder
+ SourceFolder: string option
+
+ /// The URL the the source repo where the source code lives
+ SourceRepo: string option
+
+ /// The substitutionss active for this input. If specified these
+ /// are used instead of the overall substitutions. This allows different parameters (e.g.
+ /// different authors) for each assembly in a collection.
+ Substitutions: Substitutions option
+
+ /// Whether the input uses markdown comments
+ MarkdownComments: bool
+
+ /// Whether doc processing should warn on missing comments
+ Warn: bool
+
+ /// Whether to generate only public things
+ PublicOnly: bool
+ }
+
+ static member FromFile
+ (
+ assemblyPath: string,
+ ?mdcomments,
+ ?substitutions,
+ ?sourceRepo,
+ ?sourceFolder,
+ ?publicOnly,
+ ?warn
+ ) =
+ { Path = assemblyPath
+ XmlFile = None
+ SourceFolder = sourceFolder
+ SourceRepo = sourceRepo
+ Warn = defaultArg warn false
+ Substitutions = substitutions
+ PublicOnly = defaultArg publicOnly true
+ MarkdownComments = defaultArg mdcomments false }
+
+
+type ApiDocFileExtensions = { InFile: string; InUrl: string }
+
+/// Represents a set of assemblies integrated with their associated documentation
+type ApiDocModel internal (substitutions, collection, entityInfos, root, qualify, fileExtensions, urlMap) =
+ /// The substitutions. Different substitutions can also be used for each specific input
+ member _.Substitutions: Substitutions = substitutions
+
+ /// The full list of all entities
+ member _.Collection: ApiDocCollection = collection
+
+ /// The full list of all entities
+ member _.EntityInfos: ApiDocEntityInfo list = entityInfos
+
+ /// The root URL for the entire generation, normally '/'
+ member _.Root: string = root
+
+ /// Indicates if each collection is being qualified by its collection name, e.g. 'reference/FSharp.Core'
+ member _.Qualify: bool = qualify
+
+ /// Specifies file extensions to use in files and URLs
+ member _.FileExtensions: ApiDocFileExtensions = fileExtensions
+
+ /// Specifies file extensions to use in files and URLs
+ member internal _.Resolver: CrossReferenceResolver = urlMap
+
+ /// URL of the 'index.html' for the reference documentation for the model
+ member x.IndexFileUrl(root, collectionName, qualify, extension) =
+ sprintf "%sreference/%sindex%s" root (if qualify then collectionName + "/" else "") extension
+
+ /// URL of the 'index.html' for the reference documentation for the model
+ member x.IndexOutputFile(collectionName, qualify, extension) =
+ sprintf "reference/%sindex%s" (if qualify then collectionName + "/" else "") extension
+
+ static member internal Generate
+ (
+ projects: ApiDocInput list,
+ collectionName,
+ libDirs,
+ otherFlags,
+ qualify,
+ urlRangeHighlight,
+ root,
+ substitutions,
+ onError,
+ extensions
+ ) =
+
+ // Default template file names
+
+ let otherFlags = defaultArg otherFlags [] |> List.map (fun (o: string) -> o.Trim())
+
+ let libDirs = defaultArg libDirs [] |> List.map Path.GetFullPath
+
+ let dllFiles = projects |> List.map (fun p -> Path.GetFullPath p.Path)
+
+ let urlRangeHighlight =
+ defaultArg urlRangeHighlight (fun url start stop -> String.Format("{0}#L{1}-{2}", url, start, stop))
+
+ // Compiler arguments used when formatting code snippets inside Markdown comments
+ let codeFormatCompilerArgs =
+ [ for dir in libDirs do
+ yield sprintf "-I:\"%s\"" dir
+ for file in dllFiles do
+ yield sprintf "-r:\"%s\"" file ]
+ |> String.concat " "
+
+ printfn " loading %d assemblies..." dllFiles.Length
+
+ let resolvedList =
+ FSharpAssembly.LoadFiles(dllFiles, libDirs, otherFlags = otherFlags)
+ |> List.zip projects
+
+ // generate the names for the html files beforehand so we can resolve links.
+ let urlMap = CrossReferenceResolver(root, collectionName, qualify, extensions)
+
+ // Read and process assemblies and the corresponding XML files
+ let assemblies =
+
+ for (_, asmOpt) in resolvedList do
+ match asmOpt with
+ | (_, Some asm) ->
+ printfn " registering entities for assembly %s..." asm.SimpleName
+
+ asm.Contents.Entities |> Seq.iter (urlMap.RegisterEntity)
+ | _ -> ()
+
+ resolvedList
+ |> List.choose (fun (project, (dllFile, asmOpt)) ->
+ let sourceFolderRepo =
+ match project.SourceFolder, project.SourceRepo with
+ | Some folder, Some repo -> Some(folder, repo)
+ | Some _folder, _ ->
+ Log.warnf "Repository url should be specified along with source folder."
+ None
+ | _, Some _repo ->
+ Log.warnf "Repository url should be specified along with source folder."
+ None
+ | _ -> None
+
+ match asmOpt with
+ | None ->
+ printfn "**** Skipping assembly '%s' because was not found in resolved assembly list" dllFile
+ onError "exiting"
+ None
+ | Some asm ->
+ printfn " reading XML doc for %s..." dllFile
+
+ let xmlFile = defaultArg project.XmlFile (Path.ChangeExtension(dllFile, ".xml"))
+
+ let xmlFileNoExt = Path.GetFileNameWithoutExtension(xmlFile)
+
+ let xmlFileOpt =
+ //Directory.EnumerateFiles(Path.GetDirectoryName(xmlFile), xmlFileNoExt + ".*")
+ Directory.EnumerateFiles(Path.GetDirectoryName xmlFile)
+ |> Seq.filter (fun file ->
+ let fileNoExt = Path.GetFileNameWithoutExtension file
+ let ext = Path.GetExtension file
+
+ xmlFileNoExt.Equals(fileNoExt, StringComparison.OrdinalIgnoreCase)
+ && ext.Equals(".xml", StringComparison.OrdinalIgnoreCase))
+ |> Seq.tryHead
+ //|> Seq.map (fun f -> f, f.Remove(0, xmlFile.Length - 4))
+ //|> Seq.tryPick (fun (f, ext) ->
+ // if ext.Equals(".xml", StringComparison.CurrentCultureIgnoreCase)
+ // then Some(f) else None)
+
+ let publicOnly = project.PublicOnly
+ let mdcomments = project.MarkdownComments
+
+ let substitutions = defaultArg project.Substitutions substitutions
+
+ match xmlFileOpt with
+ | None -> raise (FileNotFoundException(sprintf "Associated XML file '%s' was not found." xmlFile))
+ | Some xmlFile ->
+ printfn " reading assembly data for %s..." dllFile
+
+ SymbolReader.readAssembly (
+ asm,
+ publicOnly,
+ xmlFile,
+ substitutions,
+ sourceFolderRepo,
+ urlRangeHighlight,
+ mdcomments,
+ urlMap,
+ codeFormatCompilerArgs,
+ project.Warn
+ )
+ |> Some)
+
+ printfn " collecting namespaces..."
+ // Union namespaces from multiple libraries
+ let namespaces = Dictionary<_, (_ * _ * Substitutions)>()
+
+ for asm, nss in assemblies do
+ for ns in nss do
+ printfn " found namespace %s in assembly %s..." ns.Name asm.Name
+
+ match namespaces.TryGetValue(ns.Name) with
+ | true, (entities, summary, substitutions) ->
+ namespaces.[ns.Name] <-
+ (entities @ ns.Entities, combineNamespaceDocs [ ns.NamespaceDocs; summary ], substitutions)
+ | false, _ -> namespaces.Add(ns.Name, (ns.Entities, ns.NamespaceDocs, ns.Substitutions))
+
+ let namespaces =
+ [ for (KeyValue (name, (entities, summary, substitutions))) in namespaces do
+ printfn " found %d entities in namespace %s..." entities.Length name
+
+ if entities.Length > 0 then
+ ApiDocNamespace(name, entities, substitutions, summary) ]
+
+ printfn " found %d namespaces..." namespaces.Length
+
+ let collection =
+ ApiDocCollection(collectionName, List.map fst assemblies, namespaces |> List.sortBy (fun ns -> ns.Name))
+
+ let rec nestedModules ns parent (modul: ApiDocEntity) =
+ [ yield ApiDocEntityInfo(modul, collection, ns, parent)
+ for n in modul.NestedEntities do
+ if not n.IsTypeDefinition then
+ yield! nestedModules ns (Some modul) n ]
+
+ let moduleInfos =
+ [ for ns in collection.Namespaces do
+ for n in ns.Entities do
+ if not n.IsTypeDefinition then
+ yield! nestedModules ns None n ]
+
+ let createType ns parent typ =
+ ApiDocEntityInfo(typ, collection, ns, parent)
+
+ let rec nestedTypes ns (modul: ApiDocEntity) =
+ [ let entities = modul.NestedEntities
+
+ for n in entities do
+ if n.IsTypeDefinition then
+ yield createType ns (Some modul) n
+
+ for n in entities do
+ if not n.IsTypeDefinition then
+ yield! nestedTypes ns n ]
+
+ let typesInfos =
+ [ for ns in collection.Namespaces do
+ let entities = ns.Entities
+
+ for n in entities do
+ if not n.IsTypeDefinition then
+ yield! nestedTypes ns n
+
+ for n in entities do
+ if n.IsTypeDefinition then
+ yield createType ns None n ]
+
+ ApiDocModel(
+ substitutions = substitutions,
+ collection = collection,
+ entityInfos = moduleInfos @ typesInfos,
+ root = root,
+ qualify = qualify,
+ fileExtensions = extensions,
+ urlMap = urlMap
+ )
+
+/// Represents an entry suitable for constructing a Lunr index
+type ApiDocsSearchIndexEntry =
+ { uri: string
+ title: string
+ content: string }
+
+[]
+type Member =
+ class
+ end
+
+[]
+type MemberKind =
+ class
+ end
+
+[]
+type Attribute =
+ class
+ end
+
+[]
+type DocComment =
+ class
+ end
+
+[]
+type Module =
+ class
+ end
+
+[]
+type ModuleInfo =
+ class
+ end
+
+[]
+type Type =
+ class
+ end
+
+[]
+type ApiDocType =
+ class
+ end
+
+[]
+type TypeInfo =
+ class
+ end
diff --git a/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs b/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs
index c4456c8b1..d4c15749e 100644
--- a/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs
+++ b/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs
@@ -127,6 +127,48 @@ let generateApiDocs (libraries: string list) (format: OutputFormat) useMdComment
do FSharp.Formatting.TestHelpers.enableLogging ()
+
+[]
+[]
+let ``ApiDocs excludes items`` (format: OutputFormat) =
+ let library = testBin > "TestLib3.dll" |> fullpath
+
+ let files = generateApiDocs [ library ] format false "TestLib3"
+
+ files.[(sprintf "fslib-partiallydocumented.%s" format.Extension)]
+ |> shouldContainText "Returns unit"
+
+ files.[(sprintf "fslib-partiallydocumented.%s" format.Extension)]
+ |> shouldNotContainText "shouldBeOmitted"
+
+ files.[(sprintf "fslib-partiallydocumented.%s" format.Extension)]
+ |> shouldNotContainText "shouldBeExcluded1"
+
+ files.[(sprintf "fslib-partiallydocumented.%s" format.Extension)]
+ |> shouldNotContainText "shouldBeExcluded2"
+
+ files.[(sprintf "fslib-partiallydocumented.%s" format.Extension)]
+ |> shouldNotContainText "shouldBeExcluded3"
+
+ files.[(sprintf "fslib-partiallydocumented.%s" format.Extension)]
+ |> shouldNotContainText "shouldBeExcluded4"
+
+ files.[(sprintf "fslib-partiallydocumented.%s" format.Extension)]
+ |> shouldNotContainText "shouldBeExcluded5"
+
+ files.[(sprintf "fslib-partiallydocumented.%s" format.Extension)]
+ |> shouldNotContainText "shouldBeExcluded6"
+
+ files.[(sprintf "fslib-partiallydocumented.%s" format.Extension)]
+ |> shouldNotContainText "shouldBeExcluded7"
+
+ // We can only expect a warning for "wishItWasExcluded1" & "WishItWasExcluded2"
+
+ files.ContainsKey(sprintf "fslib-partiallydocumented-notdocumented1.%s" format.Extension) |> shouldEqual false
+ files.ContainsKey(sprintf "fslib-partiallydocumented-notdocumented2.%s" format.Extension) |> shouldEqual false
+ files.ContainsKey(sprintf "fslib-partiallydocumented-notdocumented3.%s" format.Extension) |> shouldEqual false
+ files.ContainsKey(sprintf "fslib-undocumentedmodule.%s" format.Extension) |> shouldEqual false
+
[]
[]
let ``ApiDocs works on sample Deedle assembly`` (format: OutputFormat) =
@@ -915,6 +957,7 @@ let ``ApiDocs process XML comments in two sample F# assemblies`` (format: Output
files.[(sprintf "fslib-nested-submodule.%s" format.Extension)]
|> shouldContainText "Very nested field"
+
[]
[]
let ``ApiDocs highlights code snippets in Markdown comments`` (format: OutputFormat) =
@@ -1054,9 +1097,9 @@ let ``ApiDocs omit works without markdown`` (format: OutputFormat) =
let files = generateApiDocs [ library ] format false "FsLib2_omit"
- // Actually, the thing gets generated it's just not in the index
+ // Omitted items shouldn't have generated a file
files.ContainsKey(sprintf "fslib-test_omit.%s" format.Extension)
- |> shouldEqual true
+ |> shouldEqual false
[]
[]
diff --git a/tests/FSharp.ApiDocs.Tests/FSharp.ApiDocs.Tests.fsproj b/tests/FSharp.ApiDocs.Tests/FSharp.ApiDocs.Tests.fsproj
index 0d8ab7122..60a249ed1 100644
--- a/tests/FSharp.ApiDocs.Tests/FSharp.ApiDocs.Tests.fsproj
+++ b/tests/FSharp.ApiDocs.Tests/FSharp.ApiDocs.Tests.fsproj
@@ -26,6 +26,7 @@
+
diff --git a/tests/FSharp.ApiDocs.Tests/files/TestLib3/Library3.fs b/tests/FSharp.ApiDocs.Tests/files/TestLib3/Library3.fs
new file mode 100644
index 000000000..0646fe6bf
--- /dev/null
+++ b/tests/FSharp.ApiDocs.Tests/files/TestLib3/Library3.fs
@@ -0,0 +1,62 @@
+namespace FsLib
+
+module PartiallyDocumented =
+
+ /// Should be omitted (but will generated a warning)
+ /// [omit]
+ let shouldBeOmitted() = ()
+
+ ///
+ ///
+ /// Should be excluded
+ ///
+ let shouldBeExcluded1() = ()
+
+ ///
+ /// Should be excluded
+ ///
+ ///
+ let shouldBeExcluded2() = ()
+
+ // Should be excluded
+ ///
+ let shouldBeExcluded3() = ()
+
+ ///
+ // Should be excluded
+ let shouldBeExcluded4() = ()
+
+ ///
+ let shouldBeExcluded5() = ()
+
+ ///
+ /// This triple-'/' comment auto-creates a summary element with the exclude tag escaped into its text
+ let shouldBeExcluded6() = ()
+
+ /// This triple-'/' comment auto-creates a summary element with the exclude tag escaped into its text
+ ///
+ let shouldBeExcluded7() = ()
+
+ ///
+ /// Returns unit
+ ///
+ let returnUnit() = ()
+
+ ///
+ /// Should be excluded
+ ///
+ ///
+ module NotDocumented1 =
+ let a = 10
+
+ ///
+ module NotDocumented2 =
+ let a = 10
+
+ /// This triple-'/' comment auto-creates a summary element with the exclude tag escaped into its text
+ ///
+ module NotDocumented3 =
+ let a = 10
+
+ let x = 10
+
diff --git a/tests/FSharp.ApiDocs.Tests/files/TestLib3/TestLib3.fsproj b/tests/FSharp.ApiDocs.Tests/files/TestLib3/TestLib3.fsproj
new file mode 100644
index 000000000..96e329d1b
--- /dev/null
+++ b/tests/FSharp.ApiDocs.Tests/files/TestLib3/TestLib3.fsproj
@@ -0,0 +1,13 @@
+
+
+
+ netstandard2.1
+ ..\bin\$(Configuration)
+
+
+
+
+
+
+
+
diff --git a/tests/FSharp.ApiDocs.Tests/files/TestLib3/UndocumentedModule.fs b/tests/FSharp.ApiDocs.Tests/files/TestLib3/UndocumentedModule.fs
new file mode 100644
index 000000000..4e125f98d
--- /dev/null
+++ b/tests/FSharp.ApiDocs.Tests/files/TestLib3/UndocumentedModule.fs
@@ -0,0 +1,3 @@
+module private FsLib.UndocumentedModule
+
+let pi() = 3.141
\ No newline at end of file
diff --git a/tests/FSharp.ApiDocs.Tests/files/TestLib3/paket.references b/tests/FSharp.ApiDocs.Tests/files/TestLib3/paket.references
new file mode 100644
index 000000000..0a1dca0be
--- /dev/null
+++ b/tests/FSharp.ApiDocs.Tests/files/TestLib3/paket.references
@@ -0,0 +1,2 @@
+FSharp.Compiler.Service
+FSharp.Core