diff --git a/docs/_template.md b/docs/_template.md new file mode 100644 index 000000000..e69de29bb diff --git a/src/FSharp.Formatting.ApiDocs/ApiDocs.fs b/src/FSharp.Formatting.ApiDocs/ApiDocs.fs index 31d38c9c2..1d3220815 100644 --- a/src/FSharp.Formatting.ApiDocs/ApiDocs.fs +++ b/src/FSharp.Formatting.ApiDocs/ApiDocs.fs @@ -26,16 +26,19 @@ type ApiDocs = /// A function that can be used to override the default way of generating GitHub links /// Fail if any assembly is missing XML docs or can't be resolved /// - static member GenerateModel(inputs: ApiDocInput list, collectionName, substitutions, ?qualify, ?libDirs, ?otherFlags, ?root, ?urlRangeHighlight, ?strict) = + static member GenerateModel(inputs: ApiDocInput list, collectionName, substitutions, ?qualify, ?libDirs, ?otherFlags, ?root, ?urlRangeHighlight, ?strict, ?extension) = let root = defaultArg root "/" let qualify = defaultArg qualify false let strict = defaultArg strict false + let extensions = defaultArg extension { InFile = ".html"; InUrl = ".html"} ApiDocModel.Generate(inputs, collectionName=collectionName, libDirs=libDirs, qualify=qualify, otherFlags=otherFlags, urlRangeHighlight=urlRangeHighlight, root=root, substitutions=substitutions, - strict=strict) + strict=strict, + extensions=extensions + ) /// /// Generates the search index from the given documentation model @@ -51,12 +54,13 @@ type ApiDocs = let root = defaultArg root "/" let qualify = defaultArg qualify false let strict = defaultArg strict false + let extensions = { InFile = ".html"; InUrl = ".html"} let model = ApiDocModel.Generate(inputs, collectionName=collectionName, libDirs=libDirs, qualify=qualify, otherFlags=otherFlags, - urlRangeHighlight=urlRangeHighlight, root=root, substitutions=substitutions, strict=strict) + urlRangeHighlight=urlRangeHighlight, root=root, substitutions=substitutions, strict=strict, extensions=extensions) let renderer = GenerateHtml.HtmlRender(model) - let index = GenerateSearchIndex.searchIndexEntriesForModel(model) + let index = GenerateSearchIndex.searchIndexEntriesForModel model renderer.GlobalSubstitutions, index, (fun globalParameters -> renderer.Generate(output, template, collectionName, globalParameters)) @@ -79,11 +83,57 @@ type ApiDocs = let root = defaultArg root "/" let qualify = defaultArg qualify false let strict = defaultArg strict false + let extensions = { InFile = ".html"; InUrl = ".html"} let model = ApiDocModel.Generate(inputs, collectionName=collectionName, libDirs=libDirs, qualify=qualify, otherFlags=otherFlags, - urlRangeHighlight=urlRangeHighlight, root=root, substitutions=substitutions, strict=strict) + urlRangeHighlight=urlRangeHighlight, root=root, substitutions=substitutions, strict=strict, extensions=extensions) let renderer = GenerateHtml.HtmlRender(model) - let index = GenerateSearchIndex.searchIndexEntriesForModel(model) + let index = GenerateSearchIndex.searchIndexEntriesForModel model + renderer.Generate(output, template, collectionName, renderer.GlobalSubstitutions) + model,index + + /// Like GenerateMarkdown but allows for intermediate phase to insert other global substitutions + /// and combine search index + static member GenerateMarkdownPhased(inputs, output, collectionName, substitutions, ?template, ?root, ?qualify, ?libDirs, ?otherFlags, ?urlRangeHighlight, ?strict) = + let root = defaultArg root "/" + let qualify = defaultArg qualify false + let strict = defaultArg strict false + let extensions = { InFile = ".md"; InUrl = ""} + let model = + ApiDocModel.Generate(inputs, collectionName=collectionName, + libDirs=libDirs, qualify=qualify, otherFlags=otherFlags, + urlRangeHighlight=urlRangeHighlight, root=root, substitutions=substitutions, strict=strict, extensions=extensions) + let renderer = GenerateMarkdown.MarkdownRender(model) + let index = GenerateSearchIndex.searchIndexEntriesForModel model + renderer.GlobalSubstitutions, index, (fun globalParameters -> + renderer.Generate(output, template, collectionName, globalParameters)) + + /// + /// Generates default Markdown pages for the assemblies specified by the `inputs` parameter + /// + /// + /// the components to generate documentation for + /// the output directory + /// the overall collection name + /// the template to use for each documentation page + /// The root url of the generated documentation within the website + /// qualify the output set by collection name, e.g. `reference/FSharp.Core/...` + /// Use this to specify additional paths where referenced DLL files can be found when formatting code snippets inside Markdown comments + /// Additional flags that are passed to the F# compiler to specify references explicitly etc. + /// A function that can be used to override the default way of generating GitHub links + /// + static member GenerateMarkdown(inputs, output, collectionName, substitutions, ?template, ?root, ?qualify, ?libDirs, ?otherFlags, ?urlRangeHighlight, ?strict) = + let root = defaultArg root "/" + let qualify = defaultArg qualify false + let strict = defaultArg strict false + let extensions = { InFile = ".md"; InUrl = ""} + + let model = + ApiDocModel.Generate(inputs, collectionName=collectionName, + libDirs=libDirs, qualify=qualify, otherFlags=otherFlags, + urlRangeHighlight=urlRangeHighlight, root=root, substitutions=substitutions, strict=strict, extensions=extensions) + let renderer = GenerateMarkdown.MarkdownRender(model) + let index = GenerateSearchIndex.searchIndexEntriesForModel model renderer.Generate(output, template, collectionName, renderer.GlobalSubstitutions) model,index diff --git a/src/FSharp.Formatting.ApiDocs/Categorise.fs b/src/FSharp.Formatting.ApiDocs/Categorise.fs new file mode 100644 index 000000000..c43a490e5 --- /dev/null +++ b/src/FSharp.Formatting.ApiDocs/Categorise.fs @@ -0,0 +1,77 @@ +[] +module internal FSharp.Formatting.ApiDocs.Categorise + +open System + +// Honour the CategoryIndex to put the categories in the right order +let private getSortedCategories xs exclude category categoryIndex = + xs + |> List.filter (exclude >> not) + |> List.groupBy (category) + |> List.map (fun (cat, xs) -> (cat, xs, xs |> List.minBy (categoryIndex))) + |> List.sortBy (fun (cat, _xs, x) -> categoryIndex x, cat) + |> List.map (fun (cat, xs, _x) -> cat, xs) + +// Group all members by their category +let getMembersByCategory (members:ApiDocMember list) = + 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 + (i, elems, name)) + +let entities (nsIndex: int, ns: ApiDocNamespace, suppress) = + let entities = ns.Entities + + let categories = + getSortedCategories entities (fun (m:ApiDocEntity) -> m.Exclude) (fun (m:ApiDocEntity) -> m.Category) (fun (m:ApiDocEntity) -> m.CategoryIndex) + + let allByCategory = + [ for (catIndex, (categoryName, (categoryEntities: ApiDocEntity list))) in Seq.indexed categories do + let categoryName = (if String.IsNullOrEmpty(categoryName) then "Other namespace members" else categoryName) + let index = String.Format("{0}_{1}", nsIndex, catIndex) + let categoryEntities = + // When calculating list-of-namespaces suppress some entries + // Some bespoke hacks to make FSharp.Core docs look ok. + // + // TODO: use to do these, or work out if there's a better way + if suppress then + categoryEntities + + // Remove FSharp.Data.UnitSystems.SI from the list-of-namespaces + // display - it's just so rarely used, has long names and dominates the docs. + // + // See https://github.com/fsharp/fsharp-core-docs/issues/57, we may rethink this + |> List.filter (fun e -> not (e.Symbol.Namespace = Some "Microsoft.FSharp.Data.UnitSystems.SI.UnitSymbols")) + |> List.filter (fun e -> not (e.Symbol.Namespace = Some "Microsoft.FSharp.Data.UnitSystems.SI.UnitNames")) + // Don't show 'AnonymousObject' in list-of-namespaces navigation + |> List.filter (fun e -> not (e.Symbol.Namespace = Some "Microsoft.FSharp.Linq.RuntimeHelpers" && e.Symbol.DisplayName = "AnonymousObject")) + // Don't show 'FSharp.Linq.QueryRunExtensions' in list-of-namespaces navigation + |> List.filter (fun e -> not (e.Symbol.Namespace = Some "Microsoft.FSharp.Linq.QueryRunExtensions" && e.Symbol.DisplayName = "LowPriority")) + |> List.filter (fun e -> not (e.Symbol.Namespace = Some "Microsoft.FSharp.Linq.QueryRunExtensions" && e.Symbol.DisplayName = "HighPriority")) + else + categoryEntities + + // We currently suppress all obsolete entries all the time + let categoryEntities = + categoryEntities + |> List.filter (fun e -> not e.IsObsolete) + + let categoryEntities = + categoryEntities + |> List.sortBy (fun e -> + (e.Symbol.DisplayName.ToLowerInvariant(), e.Symbol.GenericParameters.Count, + e.Name, (if e.IsTypeDefinition then e.UrlBaseName else "ZZZ"))) + + if categoryEntities.Length > 0 then + yield {| CategoryName = categoryName + CategoryIndex = index + CategoryEntites = categoryEntities |} ] + + allByCategory + +let model apiDocModel = + [ for (nsIndex, ns) in Seq.indexed apiDocModel.Collection.Namespaces do + let allByCategory = entities (nsIndex, ns, true) + if allByCategory.Length > 0 then + allByCategory, ns ] diff --git a/src/FSharp.Formatting.ApiDocs/FSharp.Formatting.ApiDocs.fsproj b/src/FSharp.Formatting.ApiDocs/FSharp.Formatting.ApiDocs.fsproj index 0b0c71ba2..1a3fc323a 100644 --- a/src/FSharp.Formatting.ApiDocs/FSharp.Formatting.ApiDocs.fsproj +++ b/src/FSharp.Formatting.ApiDocs/FSharp.Formatting.ApiDocs.fsproj @@ -15,7 +15,9 @@ Common\StringParsing.fs + + diff --git a/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs b/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs index 950f8d4cc..355592709 100644 --- a/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs +++ b/src/FSharp.Formatting.ApiDocs/GenerateHtml.fs @@ -16,6 +16,7 @@ type HtmlRender(model: ApiDocModel) = let root = model.Root let collectionName = model.Collection.CollectionName let qualify = model.Qualify + //let obsoleteMessage msg = // div [Class "alert alert-warning"] [ // strong [] [!!"NOTE:"] @@ -200,7 +201,7 @@ type HtmlRender(model: ApiDocModel) = let nmWithSiffix = if multi then (if e.IsTypeDefinition then nm + " (Type)" else nm + " (Module)") else nm // This adds #EntityName anchor. These may currently be ambiguous - p [] [a [Name nm] [a [Href (e.Url(root, collectionName, qualify))] [!!nmWithSiffix]]] + p [] [a [Name nm] [a [Href (e.Url(root, collectionName, qualify, model.FileExtensions.InUrl))] [!!nmWithSiffix]]] ] td [Class "fsdocs-xmldoc" ] [ p [] [yield! sourceLink e.SourceLocation @@ -212,27 +213,11 @@ 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) - - // Group all members by their category - let byCategory = - 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 - (i, elems, name)) + let byCategory = members |> Categorise.getMembersByCategory let usageName = match info.ParentModule with @@ -240,13 +225,13 @@ type HtmlRender(model: ApiDocModel) = | _ -> entity.Name [ h1 [] [!! (usageName + (if entity.IsTypeDefinition then " Type" else " Module")) ] - p [] [!! "Namespace: "; a [Href (info.Namespace.Url(root, collectionName, qualify))] [!!info.Namespace .Name]] + p [] [!! "Namespace: "; a [Href (info.Namespace.Url(root, collectionName, qualify, model.FileExtensions.InUrl))] [!!info.Namespace .Name]] p [] [!! ("Assembly: " + entity.Assembly.Name + ".dll")] match info.ParentModule with | None -> () | Some parentModule -> - span [] [!! ("Parent Module: "); a [Href (parentModule.Url(root, collectionName, qualify))] [!! parentModule.Name ]] + span [] [!! ("Parent Module: "); a [Href (parentModule.Url(root, collectionName, qualify, model.FileExtensions.InUrl))] [!! parentModule.Name ]] match entity.AbbreviatedType with @@ -345,60 +330,9 @@ type HtmlRender(model: ApiDocModel) = div [] (renderMembers "Instance members" "Instance member" (ms |> List.filter (fun m -> m.Kind = ApiDocMemberKind.InstanceMember))) div [] (renderMembers "Static members" "Static member" (ms |> List.filter (fun m -> m.Kind = ApiDocMemberKind.StaticMember))) ] - - - let categoriseEntities (nsIndex: int, ns: ApiDocNamespace, suppress) = - let entities = ns.Entities - - let categories = - getSortedCategories entities (fun m -> m.Exclude) (fun m -> m.Category) (fun m -> m.CategoryIndex) - - let allByCategory = - [ for (catIndex, (categoryName, categoryEntities)) in Seq.indexed categories do - let categoryName = (if String.IsNullOrEmpty(categoryName) then "Other namespace members" else categoryName) - let index = String.Format("{0}_{1}", nsIndex, catIndex) - let categoryEntities = - // When calculating list-of-namespaces suppress some entries - // Some bespoke hacks to make FSharp.Core docs look ok. - // - // TODO: use to do these, or work out if there's a better way - if suppress then - categoryEntities - - // Remove FSharp.Data.UnitSystems.SI from the list-of-namespaces - // display - it's just so rarely used, has long names and dominates the docs. - // - // See https://github.com/fsharp/fsharp-core-docs/issues/57, we may rethink this - |> List.filter (fun e -> not (e.Symbol.Namespace = Some "Microsoft.FSharp.Data.UnitSystems.SI.UnitSymbols")) - |> List.filter (fun e -> not (e.Symbol.Namespace = Some "Microsoft.FSharp.Data.UnitSystems.SI.UnitNames")) - // Don't show 'AnonymousObject' in list-of-namespaces navigation - |> List.filter (fun e -> not (e.Symbol.Namespace = Some "Microsoft.FSharp.Linq.RuntimeHelpers" && e.Symbol.DisplayName = "AnonymousObject")) - // Don't show 'FSharp.Linq.QueryRunExtensions' in list-of-namespaces navigation - |> List.filter (fun e -> not (e.Symbol.Namespace = Some "Microsoft.FSharp.Linq.QueryRunExtensions" && e.Symbol.DisplayName = "LowPriority")) - |> List.filter (fun e -> not (e.Symbol.Namespace = Some "Microsoft.FSharp.Linq.QueryRunExtensions" && e.Symbol.DisplayName = "HighPriority")) - else - categoryEntities - - // We currently suppress all obsolete entries all the time - let categoryEntities = - categoryEntities - |> List.filter (fun e -> not e.IsObsolete) - - let categoryEntities = - categoryEntities - |> List.sortBy (fun e -> - (e.Symbol.DisplayName.ToLowerInvariant(), e.Symbol.GenericParameters.Count, - e.Name, (if e.IsTypeDefinition then e.UrlBaseName else "ZZZ"))) - - if categoryEntities.Length > 0 then - yield {| CategoryName = categoryName - CategoryIndex = index - CategoryEntites = categoryEntities |} ] - - allByCategory let namespaceContent (nsIndex, ns: ApiDocNamespace) = - let allByCategory = categoriseEntities (nsIndex, ns, false) + let allByCategory = Categorise.entities (nsIndex, ns, false) [ if allByCategory.Length > 0 then h2 [Id ns.UrlHash] [!! (ns.Name + " Namespace") ] @@ -432,15 +366,11 @@ type HtmlRender(model: ApiDocModel) = // 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" ] ] + li [ Class "nav-item" ] [a [Class "nav-link"; Href (model.IndexFileUrl(root, collectionName, qualify, model.FileExtensions.InUrl))] [!! "All Namespaces" ] ] else - let categorise = - [ for (nsIndex, ns) in Seq.indexed model.Collection.Namespaces do - let allByCategory = categoriseEntities (nsIndex, ns, true) - if allByCategory.Length > 0 then - allByCategory, ns ] - + let categorise = Categorise.model model + let someExist = categorise.Length > 0 if someExist && nav then @@ -463,7 +393,7 @@ type HtmlRender(model: ApiDocModel) = match nsOpt with | Some ns2 when ns.Name = ns2.Name -> " active" | _ -> "") - Href (ns.Url(root, collectionName, qualify))] [!!ns.Name] + Href (ns.Url(root, collectionName, qualify, model.FileExtensions.InUrl))] [!!ns.Name] // If not in the navigation list then generate the summary text as well if not nav then @@ -480,7 +410,7 @@ type HtmlRender(model: ApiDocModel) = 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] ] + li [ Class "nav-item" ] [a [Class "nav-link"; Href (e.Url(root, collectionName, qualify, model.FileExtensions.InUrl))] [!! e.Name] ] ] | _ -> () ] @@ -515,7 +445,7 @@ type HtmlRender(model: ApiDocModel) = let pageTitle = sprintf "%s (API Reference)" collectionName let toc = listOfNamespaces false true None let substitutions = getSubstitutons model.Substitutions toc content pageTitle - let outFile = Path.Combine(outDir, model.IndexOutputFile(collectionName, model.Qualify) ) + let outFile = Path.Combine(outDir, model.IndexOutputFile(collectionName, model.Qualify, model.FileExtensions.InFile) ) printfn " Generating %s" outFile SimpleTemplating.UseFileAsSimpleTemplate (substitutions, templateOpt, outFile) end @@ -527,7 +457,7 @@ type HtmlRender(model: ApiDocModel) = let pageTitle = ns.Name let toc = listOfNamespaces false true (Some ns) let substitutions = getSubstitutons model.Substitutions toc content pageTitle - let outFile = Path.Combine(outDir, ns.OutputFile(collectionName, model.Qualify) ) + let outFile = Path.Combine(outDir, ns.OutputFile(collectionName, model.Qualify, model.FileExtensions.InFile) ) printfn " Generating %s" outFile SimpleTemplating.UseFileAsSimpleTemplate (substitutions, templateOpt, outFile) @@ -536,7 +466,7 @@ type HtmlRender(model: ApiDocModel) = let pageTitle = sprintf "%s (%s)" info.Entity.Name collectionName let toc = listOfNamespaces false true (Some info.Namespace) let substitutions = getSubstitutons info.Entity.Substitutions toc content pageTitle - let outFile = Path.Combine(outDir, info.Entity.OutputFile(collectionName, model.Qualify)) + let outFile = Path.Combine(outDir, info.Entity.OutputFile(collectionName, model.Qualify, model.FileExtensions.InFile)) printfn " Generating %s" outFile SimpleTemplating.UseFileAsSimpleTemplate (substitutions, templateOpt, outFile) diff --git a/src/FSharp.Formatting.ApiDocs/GenerateMarkdown.fs b/src/FSharp.Formatting.ApiDocs/GenerateMarkdown.fs new file mode 100644 index 000000000..daa50a150 --- /dev/null +++ b/src/FSharp.Formatting.ApiDocs/GenerateMarkdown.fs @@ -0,0 +1,406 @@ +module internal FSharp.Formatting.ApiDocs.GenerateMarkdown + +open System +open System.IO +open System.Web +open FSharp.Formatting.Markdown +open FSharp.Formatting.Markdown.Dsl +open FSharp.Formatting.Templating + +let encode (x:string) = HttpUtility.HtmlEncode(x).Replace("|", "|") +let urlEncode (x: string) = HttpUtility.UrlEncode x +let htmlString (x: ApiDocHtml) = (x.HtmlText.Trim()) +let htmlStringSafe (x: ApiDocHtml) = (x.HtmlText.Trim()).Replace("\n", "
").Replace("|", "|") +let embed (x: ApiDocHtml) = !! (htmlString x) +let embedSafe (x: ApiDocHtml) = !! (htmlStringSafe x) +let br = !! "
" + +type MarkdownRender(model: ApiDocModel) = + let root = model.Root + let collectionName = model.Collection.CollectionName + let qualify = model.Qualify + + let sourceLink url = + [ match url with + | None -> () + | Some href -> + link [ + img "Link to source code" (sprintf "%scontent/img/github.png" root) + ] (href) ] + + let renderMembers header tableHeader (members: ApiDocMember list) = + [ if members.Length > 0 then + ``###`` [!! header] + table [ + [p [ !! tableHeader ]] + [p [ !! "Description"]] + [p [ !! "Source"]] + ] + [AlignLeft; AlignLeft; AlignCenter] + [ + for m in members -> + [ + [ + p [link [embedSafe(m.UsageHtml)] ("#" + urlEncode(m.Name))] + ] + [ + let summary = m.Comment.Summary + let emptySummary = summary.HtmlText |> String.IsNullOrWhiteSpace + + if not emptySummary then + p [ + embedSafe m.Comment.Summary + br + ] + match m.Comment.Remarks with + | None -> () + | Some r -> p [ + embedSafe r + br + ] + if not m.Parameters.IsEmpty then + p [ !! "Parameters"] + p [] + yield! m.Parameters |> List.collect (fun parameter -> + [ + p [ + strong [!! parameter.ParameterNameText] + !! ": " + embedSafe parameter.ParameterType + ] + match parameter.ParameterDocs with + | None -> () + | Some d -> p [!! (sprintf ": %s" (htmlStringSafe d))] + + ]) + p [] + match m.ExtendedType with + | None -> () + | Some s -> p [ + !! "Extended Type: " + embedSafe s + br + ] + match m.ReturnInfo.ReturnType with + | None -> () + | Some t -> p [ + !! "Returns: " + embedSafe t + br + match m.ReturnInfo.ReturnDocs with + | None -> () + | Some r -> embedSafe r + br + ] + + if not m.Comment.Exceptions.IsEmpty then + for (nm, url, html) in m.Comment.Exceptions do + p [ + match url with None -> () | Some href -> link [!! nm] href + embed html + br + ] + + for e in m.Comment.Notes do + p [ !! "Note"] + p [ + embed e + br + ] + + for e in m.Comment.Examples do + p [!! "Example"] + p [ + embed e + br + ] + ] + [ + p [yield! sourceLink m.SourceLocation] + ] + ] + ] + ] + + let renderEntities (entities: ApiDocEntity list) = + [ if entities.Length > 0 then + let hasTypes = entities |> List.exists (fun e -> e.IsTypeDefinition) + let hasModules = entities |> List.exists (fun e -> not e.IsTypeDefinition) + table [ + [ + p [!! (if hasTypes && hasModules then "Type/Module" elif hasTypes then "Type" else "Modules")] + p [!!"Description"] + p [!! "Source"] + ] + ] + [AlignLeft; AlignLeft; AlignCenter] + [ + for e in entities do + [ + [p [ + let nm = e.Name + let multi = (entities |> List.filter (fun e -> e.Name = nm) |> List.length) > 1 + let nmWithSiffix = if multi then (if e.IsTypeDefinition then nm + " (Type)" else nm + " (Module)") else nm + link [!!nmWithSiffix] (e.Url(root, collectionName, qualify, model.FileExtensions.InUrl)) + ]] + [ + p [embedSafe e.Comment.Summary] + ] + [ + p [yield! (sourceLink e.SourceLocation)] + ] + ] + ] + ] + + 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 byCategory = Categorise.getMembersByCategory members + let usageName = + match info.ParentModule with + | Some m when m.RequiresQualifiedAccess -> m.Name + "." + entity.Name + | _ -> entity.Name + + [ + ``#`` [!! (usageName + (if entity.IsTypeDefinition then " Type" else " Module"))] + p [ + !! "Namespace: " + link [!! info.Namespace.Name] (info.Namespace.Url(root, collectionName, qualify, model.FileExtensions.InUrl)) + ] + p [!! ("Assembly: " + entity.Assembly.Name + ".dll")] + + match info.ParentModule with + | None -> () + | Some parentModule -> + p [ + !! "Parent Module: " + link [!! parentModule.Name] (parentModule.Url(root, collectionName, qualify, model.FileExtensions.InUrl)) + ] + + match entity.AbbreviatedType with + | Some abbreviatedTyp -> + p [ + !! "Abbreviation For: " + embed abbreviatedTyp + ] + | None -> () + + match entity.BaseType with + | Some baseType -> + p [ + !! "Base Type: " + embed baseType + ] + | None -> () + + match entity.AllInterfaces with + | [] -> () + | l -> + p [!! "All Interfaces: " + for (i, ity) in Seq.indexed l do + if i <> 0 then !! ", " + embed ity ] + + if entity.Symbol.IsValueType then + p [!! ("Kind: Struct")] + + match entity.DelegateSignature with + | Some d -> + p [!! ("Delegate Signature: "); embed d] + | None -> () + + if entity.Symbol.IsProvided then + p [!! ("This is a provided type definition")] + + if entity.Symbol.IsAttributeType then + p [!! ("This is an attribute type definition")] + + if entity.Symbol.IsEnum then + p [!! ("This is an enum type definition")] + + //if info.Entity.IsObsolete then + // obsoleteMessage entity.ObsoleteMessage + + // Show the summary (and sectioned docs without any members) + p [ embed entity.Comment.Summary ] + + // Show the remarks etc. + match entity.Comment.Remarks with + | Some r -> + p [embed r] + | None -> () + + for note in entity.Comment.Notes do + ``#####`` [!! "Note"] + p [embed note] + + for example in entity.Comment.Examples do + ``#####`` [!! "Example"] + p [embed example] + + if (byCategory.Length > 1) then + // If there is more than 1 category in the type, generate TOC + ``###`` [!! "Table of contents"] + ul [ + for (index, _, name) in byCategory do + [p [link [!! (sprintf "#section%d" index)] (name)]] + ] + + // + + let nestedEntities = + entity.NestedEntities + |> List.filter (fun e -> not e.IsObsolete) + + if (nestedEntities.Length > 0) then + + ``###`` [!! (if nestedEntities |> List.forall (fun e -> not e.IsTypeDefinition) then "Nested modules" + elif nestedEntities |> List.forall (fun e -> e.IsTypeDefinition) then "Types" + else "Types and nested modules")] + yield! renderEntities nestedEntities + + for (_, ms, name) in byCategory do + // Iterate over all the categories and print members. If there are more than one + // categories, print the category heading (as

) and add XML comment from the type + // that is related to this specific category. + if (byCategory.Length > 1) then ``##`` [!! name] + + yield! renderMembers "Functions and values" "Function or value" (ms |> List.filter (fun m -> m.Kind = ApiDocMemberKind.ValueOrFunction)) + yield! renderMembers "Type extensions" "Type extension" (ms |> List.filter (fun m -> m.Kind = ApiDocMemberKind.TypeExtension)) + yield! renderMembers "Active patterns" "Active pattern" (ms |> List.filter (fun m -> m.Kind = ApiDocMemberKind.ActivePattern)) + yield! renderMembers "Union cases" "Union case" (ms |> List.filter (fun m -> m.Kind = ApiDocMemberKind.UnionCase)) + yield! renderMembers "Record fields" "Record Field" (ms |> List.filter (fun m -> m.Kind = ApiDocMemberKind.RecordField)) + yield! renderMembers "Static parameters" "Static parameters" (ms |> List.filter (fun m -> m.Kind = ApiDocMemberKind.StaticParameter)) + yield! renderMembers "Constructors" "Constructor" (ms |> List.filter (fun m -> m.Kind = ApiDocMemberKind.Constructor)) + yield! renderMembers "Instance members" "Instance member" (ms |> List.filter (fun m -> m.Kind = ApiDocMemberKind.InstanceMember)) + yield! renderMembers "Static members" "Static member" (ms |> List.filter (fun m -> m.Kind = ApiDocMemberKind.StaticMember)) + ] + + let namespaceContent (nsIndex, ns: ApiDocNamespace) = + let allByCategory = Categorise.entities (nsIndex, ns, false) + [ if allByCategory.Length > 0 then + ``##`` [!! (ns.Name + " Namespace")] + + match ns.NamespaceDocs with + | Some nsdocs -> + p [embed nsdocs.Summary ] + match nsdocs.Remarks with + | Some r -> p [embed r ] + | None -> () + | None -> () + + if (allByCategory.Length > 1) then + p [!! "Categories:" ] + + ul [ + for category in allByCategory do + [p [link [!!category.CategoryName] ("#category-" + category.CategoryIndex)]] + ] + + for category in allByCategory do + if (allByCategory.Length > 1) then + ``###`` [link [!! category.CategoryName] ("#category-" + category.CategoryIndex)] + yield! renderEntities category.CategoryEntites + ] + + 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. + // + // For non-FSharp.Core we only show one link "API Reference" in the nav menu + if otherDocs && nav && model.Collection.CollectionName <> "FSharp.Core" then + p [ + !! "API Reference" + link [!! "All Namespaces"] (model.IndexFileUrl(root, collectionName, qualify, model.FileExtensions.InUrl)) + ] + else + + let categorise = Categorise.model model + + let someExist = categorise.Length > 0 + + if someExist && nav then + p [!! "Namespaces"] + + for allByCategory, ns in categorise do + + // Generate the entry for the namespace + p [ + link [!!ns.Name] (ns.Url(root, collectionName, qualify, model.FileExtensions.InUrl)) + + // If not in the navigation list then generate the summary text as well + if not nav then + !! " - " + match ns.NamespaceDocs with + | Some nsdocs -> embed nsdocs.Summary + | 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 [ + for category in allByCategory do + for e in category.CategoryEntites do + [p [link [!! e.Name] (e.Url(root, collectionName, qualify, model.FileExtensions.InUrl))] ] + ] + | _ -> () + ] + + let listOfNamespaces otherDocs nav (nsOpt: ApiDocNamespace option) = + listOfNamespacesAux otherDocs nav nsOpt + |> List.map (fun html -> html.ToString()) |> String.concat " \n" + + /// Get the substitutions relevant to all + member _.GlobalSubstitutions : Substitutions = + let toc = listOfNamespaces true true None + [ yield (ParamKeys.``fsdocs-list-of-namespaces``, toc ) ] + + member _.Generate(outDir: string, templateOpt, collectionName, globalParameters) = + + let getSubstitutons parameters toc (content: MarkdownDocument) pageTitle = + [| yield! parameters + yield (ParamKeys.``fsdocs-list-of-namespaces``, toc ) + yield (ParamKeys.``fsdocs-content``, Markdown.ToMd(content)) + yield (ParamKeys.``fsdocs-source``,"" ) + yield (ParamKeys.``fsdocs-tooltips``, "" ) + yield (ParamKeys.``fsdocs-page-title``, pageTitle ) + yield! globalParameters + |] + + let collection = model.Collection + begin + let content = MarkdownDocument([ + ``#`` [!! "API Reference"] + ``##`` [!! "Available Namespaces"] + ul [(listOfNamespacesAux false false None)]], Map.empty) + let pageTitle = sprintf "%s (API Reference)" collectionName + let toc = listOfNamespaces false true None + let substitutions = getSubstitutons model.Substitutions toc content pageTitle + let outFile = Path.Combine(outDir, model.IndexOutputFile(collectionName, model.Qualify, model.FileExtensions.InFile) ) + printfn " Generating %s" outFile + SimpleTemplating.UseFileAsSimpleTemplate (substitutions, templateOpt, outFile) + end + () + + for (nsIndex, ns) in Seq.indexed collection.Namespaces do + + let content = MarkdownDocument( namespaceContent (nsIndex, ns), Map.empty) + let pageTitle = ns.Name + let toc = listOfNamespaces false true (Some ns) + let substitutions = getSubstitutons model.Substitutions toc content pageTitle + let outFile = Path.Combine(outDir, ns.OutputFile(collectionName, model.Qualify, model.FileExtensions.InFile) ) + printfn " Generating %s" outFile + SimpleTemplating.UseFileAsSimpleTemplate (substitutions, templateOpt, outFile) + + for info in model.EntityInfos do + let content = MarkdownDocument(entityContent info, Map.empty) + let pageTitle = sprintf "%s (%s)" info.Entity.Name collectionName + let toc = listOfNamespaces false true (Some info.Namespace) + let substitutions = getSubstitutons info.Entity.Substitutions toc content pageTitle + let outFile = Path.Combine(outDir, info.Entity.OutputFile(collectionName, model.Qualify, model.FileExtensions.InFile)) + printfn " Generating %s" outFile + SimpleTemplating.UseFileAsSimpleTemplate (substitutions, templateOpt, outFile) diff --git a/src/FSharp.Formatting.ApiDocs/GenerateModel.fs b/src/FSharp.Formatting.ApiDocs/GenerateModel.fs index 101bc150e..32b47208e 100644 --- a/src/FSharp.Formatting.ApiDocs/GenerateModel.fs +++ b/src/FSharp.Formatting.ApiDocs/GenerateModel.fs @@ -338,12 +338,12 @@ type ApiDocMember (displayName: string, attributes: ApiDocAttribute list, entity member x.UrlBaseName = entityUrlBaseName /// The URL of the best link documentation for the item relative to "reference" directory (without the http://site.io/reference) - static member GetUrl(entityUrlBaseName, displayName, root, collectionName, qualify) = - sprintf "%sreference/%s%s.html#%s" root (if qualify then collectionName + "/" else "") entityUrlBaseName displayName + static member GetUrl(entityUrlBaseName, displayName, root, collectionName, qualify, extension) = + sprintf "%sreference/%s%s%s#%s" root (if qualify then collectionName + "/" else "") entityUrlBaseName extension displayName /// The URL of the best link documentation for the item relative to "reference" directory (without the http://site.io/reference) - member x.Url(root, collectionName, qualify) = - ApiDocMember.GetUrl(entityUrlBaseName, displayName, root, collectionName, qualify) + member x.Url(root, collectionName, qualify, extension) = + ApiDocMember.GetUrl(entityUrlBaseName, displayName, root, collectionName, qualify, extension) /// The declared attributes of the member member x.Attributes = attributes @@ -414,16 +414,16 @@ type ApiDocEntity member x.UrlBaseName : string = urlBaseName /// Compute the URL of the best link for the entity relative to "reference" directory (without the http://site.io/reference) - static member GetUrl(urlBaseName, root, collectionName, qualify) = - sprintf "%sreference/%s%s.html" root (if qualify then collectionName + "/" else "") urlBaseName + static member GetUrl(urlBaseName, root, collectionName, qualify, extension) = + sprintf "%sreference/%s%s%s" root (if qualify then collectionName + "/" else "") urlBaseName extension /// The URL of the best link for the entity relative to "reference" directory (without the http://site.io/reference) - member x.Url(root, collectionName, qualify) = - ApiDocEntity.GetUrl(urlBaseName, root, collectionName, qualify) + member x.Url(root, collectionName, qualify, extension) = + ApiDocEntity.GetUrl(urlBaseName, root, collectionName, qualify, extension) /// The name of the file generated for this entity - member x.OutputFile(collectionName, qualify) = - sprintf "reference/%s%s.html" (if qualify then collectionName + "/" else "") urlBaseName + member x.OutputFile(collectionName, qualify, extension) = + sprintf "reference/%s%s%s" (if qualify then collectionName + "/" else "") urlBaseName extension /// The attached comment member x.Comment : ApiDocComment = comment @@ -513,12 +513,12 @@ type ApiDocNamespace(name: string, modifiers, substitutions: Substitutions, nsdo member x.UrlBaseName = urlBaseName /// The URL of the best link documentation for the item (without the http://site.io/reference) - member x.Url(root, collectionName, qualify) = - sprintf "%sreference/%s%s.html" root (if qualify then collectionName + "/" else "") urlBaseName + member x.Url(root, collectionName, qualify, extension) = + sprintf "%sreference/%s%s%s" root (if qualify then collectionName + "/" else "") urlBaseName extension /// The name of the file generated for this entity - member x.OutputFile(collectionName, qualify) = - sprintf "reference/%s%s.html" (if qualify then collectionName + "/" else "") urlBaseName + member x.OutputFile(collectionName, qualify, extension) = + sprintf "reference/%s%s%s" (if qualify then collectionName + "/" else "") urlBaseName extension /// All modules in the namespace member x.Entities : ApiDocEntity list = modifiers @@ -620,7 +620,7 @@ module internal CrossReferences = type internal CrefReference = { IsInternal : bool; ReferenceLink : string; NiceName : string; HasModuleSuffix: bool } -type internal CrossReferenceResolver (root, collectionName, qualify) = +type internal CrossReferenceResolver (root, collectionName, qualify, extensions) = let toReplace = ([("Microsoft.", ""); (".", "-"); ("`", "-"); ("<", "_"); (">", "_"); (" ", "_"); ("#", "_")] @ (Path.GetInvalidPathChars() @@ -632,6 +632,7 @@ type internal CrossReferenceResolver (root, collectionName, qualify) = let registeredSymbolsToUrlBaseName = Dictionary() let xmlDocNameToSymbol = Dictionary() let niceNameEntityLookup = Dictionary<_, _>() + let extensions = extensions let nameGen (name:string) = let nice = (toReplace @@ -712,10 +713,10 @@ type internal CrossReferenceResolver (root, collectionName, qualify) = sprintf "https://docs.microsoft.com/dotnet/api/%s" docs let internalCrossReference urlBaseName = - ApiDocEntity.GetUrl(urlBaseName, root, collectionName, qualify) + ApiDocEntity.GetUrl(urlBaseName, root, collectionName, qualify, extensions.InUrl) let internalCrossReferenceForMember entityUrlBaseName (memb: FSharpMemberOrFunctionOrValue) = - ApiDocMember.GetUrl(entityUrlBaseName, memb.DisplayName, root, collectionName, qualify) + ApiDocMember.GetUrl(entityUrlBaseName, memb.DisplayName, root, collectionName, qualify, extensions.InUrl) let tryResolveCrossReferenceForEntity (entity: FSharpEntity) = match registeredSymbolsToUrlBaseName.TryGetValue (entity) with @@ -2066,6 +2067,12 @@ type ApiDocInput = 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 = { @@ -2084,18 +2091,20 @@ type ApiDocModel = /// Indicates if each collection is being qualified by its collection name, e.g. 'reference/FSharp.Core' Qualify: bool + /// Specifies file extensions to use in files and URLs + FileExtensions: ApiDocFileExtensions } /// URL of the 'index.html' for the reference documentation for the model - member x.IndexFileUrl(root, collectionName, qualify) = - sprintf "%sreference/%sindex.html" root (if qualify then collectionName + "/" else "") + 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) = - sprintf "reference/%sindex.html" (if qualify then collectionName + "/" else "") + 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, strict) = + qualify, urlRangeHighlight, root, substitutions, strict, extensions) = let (@@) a b = Path.Combine(a, b) @@ -2104,7 +2113,7 @@ type ApiDocModel = let otherFlags = defaultArg otherFlags [] 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)) + let urlRangeHighlight = defaultArg urlRangeHighlight (fun url start stop -> String.Format("{0}#L{1}-{2}", url, start, stop)) // When resolving assemblies, look in folders where all DLLs live AppDomain.CurrentDomain.add_AssemblyResolve(System.ResolveEventHandler(fun o e -> @@ -2140,7 +2149,7 @@ type ApiDocModel = |> List.zip projects // generate the names for the html files beforehand so we can resolve links. - let urlMap = CrossReferenceResolver(root, collectionName, qualify) + let urlMap = CrossReferenceResolver(root, collectionName, qualify, extensions) for (_, asmOpt) in resolvedList do match asmOpt with @@ -2259,6 +2268,7 @@ type ApiDocModel = EntityInfos = moduleInfos @ typesInfos Root = root Qualify = qualify + FileExtensions = extensions } /// Represents an entry suitable for constructing a Lunr index diff --git a/src/FSharp.Formatting.ApiDocs/GenerateSearchIndex.fs b/src/FSharp.Formatting.ApiDocs/GenerateSearchIndex.fs index 90f29bd1d..50446b1f3 100644 --- a/src/FSharp.Formatting.ApiDocs/GenerateSearchIndex.fs +++ b/src/FSharp.Formatting.ApiDocs/GenerateSearchIndex.fs @@ -36,7 +36,7 @@ let searchIndexEntriesForModel (model: ApiDocModel) = | Some s -> yield s.HtmlText ] |> String.concat " \n" - { uri = memb.Url(model.Root, model.Collection.CollectionName, model.Qualify) + { uri = memb.Url(model.Root, model.Collection.CollectionName, model.Qualify, model.FileExtensions.InUrl) title = enclName + "." + memb.Name content = cnt } @@ -49,7 +49,7 @@ let searchIndexEntriesForModel (model: ApiDocModel) = e.Name ] |> String.concat " \n" - { uri = nsp.Url(model.Root, model.Collection.CollectionName, model.Qualify) + { uri = nsp.Url(model.Root, model.Collection.CollectionName, model.Qualify, model.FileExtensions.InUrl) title = nsp.Name content = ctn } @@ -72,7 +72,7 @@ let searchIndexEntriesForModel (model: ApiDocModel) = ] |> String.concat " \n" - let url = e.Url(model.Root, model.Collection.CollectionName, model.Qualify) + let url = e.Url(model.Root, model.Collection.CollectionName, model.Qualify, model.FileExtensions.InUrl) { uri = url title = e.Name content = cnt } diff --git a/src/FSharp.Formatting.CommandTool/BuildCommand.fs b/src/FSharp.Formatting.CommandTool/BuildCommand.fs index 598d37cf2..a200933ef 100644 --- a/src/FSharp.Formatting.CommandTool/BuildCommand.fs +++ b/src/FSharp.Formatting.CommandTool/BuildCommand.fs @@ -58,6 +58,7 @@ type internal DocContent(outputDirectory, previous: Map<_,_>, lineNumbers, fsiEv | OutputKind.Pynb, None -> () | OutputKind.Latex, None -> () | OutputKind.Fsx, None -> () + | OutputKind.Md, None -> () | _ -> let imageSaverOpt = @@ -66,6 +67,7 @@ type internal DocContent(outputDirectory, previous: Map<_,_>, lineNumbers, fsiEv | OutputKind.Latex when saveImages <> Some false -> Some imageSaver | OutputKind.Fsx when saveImages = Some true -> Some imageSaver | OutputKind.Html when saveImages = Some true -> Some imageSaver + | OutputKind.Md when saveImages = Some true -> Some imageSaver | _ -> None let ext = outputKind.Extension @@ -151,7 +153,7 @@ type internal DocContent(outputDirectory, previous: Map<_,_>, lineNumbers, fsiEv //printfn "skipping unchanged file %s" inputFile yield (Some (inputFile, haveModel.Value), (fun _ -> ())) ] - let rec processDirectory (htmlTemplate, texTemplate, pynbTemplate, fsxTemplate) indir outputPrefix = [ + let rec processDirectory (htmlTemplate, texTemplate, pynbTemplate, fsxTemplate, mdTemplate) indir outputPrefix = [ // Look for the presence of the _template.* files to activate the // generation of the content. let possibleNewHtmlTemplate = Path.Combine(indir, "_template.html") @@ -160,6 +162,8 @@ type internal DocContent(outputDirectory, previous: Map<_,_>, lineNumbers, fsiEv let pynbTemplate = if File.Exists(possibleNewPynbTemplate) then Some possibleNewPynbTemplate else pynbTemplate let possibleNewFsxTemplate = Path.Combine(indir, "_template.fsx") let fsxTemplate = if File.Exists(possibleNewFsxTemplate) then Some possibleNewFsxTemplate else fsxTemplate + let possibleNewMdTemplate = Path.Combine(indir, "_template.md") + let mdTemplate = if File.Exists(possibleNewMdTemplate) then Some possibleNewMdTemplate else mdTemplate let possibleNewLatexTemplate = Path.Combine(indir, "_template.tex") let texTemplate = if File.Exists(possibleNewLatexTemplate) then Some possibleNewLatexTemplate else texTemplate @@ -174,13 +178,14 @@ type internal DocContent(outputDirectory, previous: Map<_,_>, lineNumbers, fsiEv yield! processFile input OutputKind.Latex texTemplate outputPrefix imageSaver yield! processFile input OutputKind.Pynb pynbTemplate outputPrefix imageSaver yield! processFile input OutputKind.Fsx fsxTemplate outputPrefix imageSaver + yield! processFile input OutputKind.Md mdTemplate outputPrefix imageSaver for subdir in Directory.EnumerateDirectories(indir) do let name = Path.GetFileName(subdir) if name.StartsWith "." then printfn " skipping directory %s" subdir else - yield! processDirectory (htmlTemplate, texTemplate, pynbTemplate, fsxTemplate) (Path.Combine(indir, name)) (Path.Combine(outputPrefix, name)) + yield! processDirectory (htmlTemplate, texTemplate, pynbTemplate, fsxTemplate, mdTemplate) (Path.Combine(indir, name)) (Path.Combine(outputPrefix, name)) ] member _.Convert(input, htmlTemplate, extraInputs) = @@ -188,7 +193,7 @@ type internal DocContent(outputDirectory, previous: Map<_,_>, lineNumbers, fsiEv let inputDirectories = extraInputs @ [(input, ".") ] [ for (inputDirectory, outputPrefix) in inputDirectories do - yield! processDirectory (htmlTemplate, None, None, None) inputDirectory outputPrefix + yield! processDirectory (htmlTemplate, None, None, None, None) inputDirectory outputPrefix ] member _.GetSearchIndexEntries(docModels: (string * LiterateDocModel) list) = @@ -579,38 +584,58 @@ type CoreBuildOptions(watch) = if not this.noapidocs then - let initialTemplate2 = - let t1 = Path.Combine(this.input, "reference", "_template.html") - let t2 = Path.Combine(this.input, "_template.html") - if File.Exists(t1) then - Some t1 - elif File.Exists(t2) then - Some t2 - else - match defaultTemplate with + let (outputKind, initialTemplate2) = + let templates = [ + OutputKind.Html, Path.Combine(this.input, "reference", "_template.html") + OutputKind.Html, Path.Combine(this.input, "_template.html") + OutputKind.Md, Path.Combine(this.input, "reference", "_template.md") + OutputKind.Md, Path.Combine(this.input, "_template.md") + ] + match templates |> Seq.tryFind (fun (_,path) -> path |> File.Exists) with + | Some (kind, path) -> kind, Some path + | None -> + let templateFiles = templates |> Seq.map snd |> String.concat "', '" + match defaultTemplate with | Some d -> - printfn "note, no template file '%s' or '%s', using default template %s" t1 t2 d - Some d + printfn "note, no template files: '%s' found, using default template %s" templateFiles d + OutputKind.Html, Some d | None -> - printfn "note, no template file '%s' or '%s', and no default template at '%s'" t1 t2 defaultTemplateAttempt1 - None - + printfn "note, no template file '%s' found, and no default template at '%s'" templateFiles defaultTemplateAttempt1 + OutputKind.Html, None + printfn "" printfn "API docs:" printfn " generating model for %d assemblies in API docs..." apiDocInputs.Length + let globals, index, phase2 = - ApiDocs.GenerateHtmlPhased ( - inputs = apiDocInputs, - output = output, - collectionName = collectionName, - substitutions = docsParameters, - qualify = this.qualify, - ?template = initialTemplate2, - otherFlags = Seq.toList this.fscoptions, - root = root, - libDirs = paths, - strict = this.strict - ) + match outputKind with + | OutputKind.Html -> + ApiDocs.GenerateHtmlPhased ( + inputs = apiDocInputs, + output = output, + collectionName = collectionName, + substitutions = docsParameters, + qualify = this.qualify, + ?template = initialTemplate2, + otherFlags = Seq.toList this.fscoptions, + root = root, + libDirs = paths, + strict = this.strict + ) + | OutputKind.Md -> + ApiDocs.GenerateMarkdownPhased ( + inputs = apiDocInputs, + output = output, + collectionName = collectionName, + substitutions = docsParameters, + qualify = this.qualify, + ?template = initialTemplate2, + otherFlags = Seq.toList this.fscoptions, + root = root, + libDirs = paths, + strict = this.strict + ) + | _ -> failwithf "API Docs format '%A' is not supported" outputKind latestApiDocSearchIndexEntries <- index latestApiDocGlobalParameters <- globals diff --git a/src/FSharp.Formatting.Literate/Contexts.fs b/src/FSharp.Formatting.Literate/Contexts.fs index 4d1a1d4ed..53d6d09ed 100644 --- a/src/FSharp.Formatting.Literate/Contexts.fs +++ b/src/FSharp.Formatting.Literate/Contexts.fs @@ -34,10 +34,14 @@ type OutputKind = /// Requests F# Script output | Fsx + + /// Requests Markdown output + | Md member x.Extension = match x with | Fsx -> "fsx" | Latex -> "tex" + | Md -> "md" | Html -> "html" | Pynb -> "ipynb" diff --git a/src/FSharp.Formatting.Literate/Formatting.fs b/src/FSharp.Formatting.Literate/Formatting.fs index 06675296c..f9bd22462 100644 --- a/src/FSharp.Formatting.Literate/Formatting.fs +++ b/src/FSharp.Formatting.Literate/Formatting.fs @@ -12,6 +12,7 @@ module internal Formatting = let format (doc: MarkdownDocument) generateAnchors outputKind substitutions = match outputKind with | OutputKind.Fsx -> Markdown.ToFsx(doc, substitutions=substitutions) + | OutputKind.Md -> Markdown.ToMd(doc, substitutions=substitutions) | OutputKind.Pynb -> Markdown.ToPynb(doc, substitutions=substitutions) | OutputKind.Latex -> Markdown.ToLatex(doc) | OutputKind.Html -> diff --git a/src/FSharp.Formatting.Literate/Transformations.fs b/src/FSharp.Formatting.Literate/Transformations.fs index b2a43d9fe..9bcdc8af2 100644 --- a/src/FSharp.Formatting.Literate/Transformations.fs +++ b/src/FSharp.Formatting.Literate/Transformations.fs @@ -428,6 +428,8 @@ module internal Transformations = code | OutputKind.Fsx -> code + | OutputKind.Md -> + code Some(InlineBlock(inlined, None, None)) // Traverse all other structures recursively | MarkdownPatterns.ParagraphNested(pn, nested) -> @@ -461,6 +463,8 @@ module internal Transformations = CodeFormat.FormatFsx(snippets) | OutputKind.Fsx -> CodeFormat.FormatFsx(snippets) + | OutputKind.Md -> + CodeFormat.FormatFsx(snippets) let lookup = [ for (key, (_, executionCount)), fmtd in Seq.zip codes formatted.Snippets -> diff --git a/src/FSharp.Formatting.Markdown/FSharp.Formatting.Markdown.fsproj b/src/FSharp.Formatting.Markdown/FSharp.Formatting.Markdown.fsproj index b74af1e63..9918c1055 100644 --- a/src/FSharp.Formatting.Markdown/FSharp.Formatting.Markdown.fsproj +++ b/src/FSharp.Formatting.Markdown/FSharp.Formatting.Markdown.fsproj @@ -21,6 +21,7 @@ + diff --git a/src/FSharp.Formatting.Markdown/Formatting.fs b/src/FSharp.Formatting.Markdown/Formatting.fs index 86ef0be6f..ab347823c 100644 --- a/src/FSharp.Formatting.Markdown/Formatting.fs +++ b/src/FSharp.Formatting.Markdown/Formatting.fs @@ -66,10 +66,10 @@ module internal MarkdownUtils = "[" + formatSpans ctx body + "](" + link + ")" | IndirectImage(_body, _, LookupKey ctx.Links (_link, _), _) - | DirectImage(_body, _link, _, _) | IndirectImage(_body, _link, _, _) -> failwith "tbd - IndirectImage" - + | DirectImage(_body, _link, _, _) -> + sprintf "![%s](%s)" _body _link | Strong(body, _) -> "**" + formatSpans ctx body + "**" | InlineCode(body, _) -> @@ -103,6 +103,37 @@ module internal MarkdownUtils = | CodeBlock(code, _, _, _, _) -> yield code yield "" + | ListBlock (Unordered, paragraphs, _) -> + yield (String.concat "\n" (paragraphs |> List.collect(fun ps -> [ for p in ps -> String.concat "" (formatParagraph ctx p)]))) + | TableBlock (headers, alignments, rows, _) -> + + match headers with + | Some headers -> + yield (String.concat " | " (headers |> List.collect (fun hs -> [for h in hs -> String.concat "" (formatParagraph ctx h)]))) + | None -> () + + yield (String.concat " | " [ + for a in alignments -> + match a with + | AlignLeft -> ":---" + | AlignCenter -> ":---:" + | AlignRight -> "---:" + | AlignDefault -> "---" + ]) + let replaceEmptyWith x s = match s with | "" | null -> x | s -> Some s + yield String.concat "\n" [ + for r in rows do + [ + for ps in r do + let x = [ + for p in ps do + yield formatParagraph ctx p |> Seq.choose (replaceEmptyWith (Some "")) |> String.concat "" + ] + yield x |> Seq.choose (replaceEmptyWith (Some "")) |> String.concat "
" + ] |> Seq.choose (replaceEmptyWith (Some " ")) |> String.concat " | " + ] + yield "\n" + | OutputBlock(output, "text/html", _executionCount) -> yield (output.Trim()) yield "" diff --git a/src/FSharp.Formatting.Markdown/Markdown.fs b/src/FSharp.Formatting.Markdown/Markdown.fs index 81df8980f..464acc860 100644 --- a/src/FSharp.Formatting.Markdown/Markdown.fs +++ b/src/FSharp.Formatting.Markdown/Markdown.fs @@ -114,9 +114,14 @@ type Markdown = let substitutions = defaultArg substitutions [] PynbFormatting.formatAsPynb doc.DefinedLinks substitutions newline doc.Paragraphs - /// Transform the provided MarkdownDocument into Pynb and return the result as a string. + /// Transform the provided MarkdownDocument into Fsx and return the result as a string. static member ToFsx(doc: MarkdownDocument, ?newline, ?substitutions) = let newline = defaultArg newline Environment.NewLine let substitutions = defaultArg substitutions [] FsxFormatting.formatAsFsx doc.DefinedLinks substitutions newline doc.Paragraphs + /// Transform the provided MarkdownDocument into Md and return the result as a string. + static member ToMd(doc: MarkdownDocument, ?newline, ?substitutions) = + let newline = defaultArg newline Environment.NewLine + let substitutions = defaultArg substitutions [] + MarkdownFormatting.formatAsMd doc.DefinedLinks substitutions newline doc.Paragraphs diff --git a/src/FSharp.Formatting.Markdown/MarkdownFormatting.fs b/src/FSharp.Formatting.Markdown/MarkdownFormatting.fs new file mode 100644 index 000000000..3aa5d3201 --- /dev/null +++ b/src/FSharp.Formatting.Markdown/MarkdownFormatting.fs @@ -0,0 +1,14 @@ +// -------------------------------------------------------------------------------------- +// Format a document as a .md +// -------------------------------------------------------------------------------------- + +module internal FSharp.Formatting.Markdown.MarkdownFormatting + +open MarkdownUtils + +let rec formatParagraphs ctx (paragraphs:MarkdownParagraph list) = + paragraphs |> Seq.collect (formatParagraph ctx) + +let formatAsMd links replacements newline paragraphs = + formatParagraphs { Links = links; Substitutions=replacements; Newline=newline; DefineSymbol="MD" } paragraphs + |> String.concat newline diff --git a/src/FSharp.Formatting.Markdown/MarkdownModel.fs b/src/FSharp.Formatting.Markdown/MarkdownModel.fs index 1b018da66..7675c80ed 100644 --- a/src/FSharp.Formatting.Markdown/MarkdownModel.fs +++ b/src/FSharp.Formatting.Markdown/MarkdownModel.fs @@ -78,6 +78,23 @@ type MarkdownTableRow = list type MarkdownEmbedParagraphs = abstract Render : unit -> MarkdownParagraphs +module Dsl = + let ``#`` value = Heading(1, value, None) + let ``##`` value = Heading(2, value, None) + let ``###`` value = Heading(3, value, None) + let ``####`` value = Heading(4, value, None) + let ``#####`` value = Heading(5, value, None) + let strong value = Strong(value, None) + let p value = Paragraph(value, None) + let span value = Span(value, None) + let (!!) value = Literal(value, None) + let link content url = DirectLink(content, url, None, None) + let ul value = ListBlock(Unordered, value, None) + let ol value = ListBlock(Ordered, value, None) + let table headers alignments rows = + let hs = match headers with | [] -> None | hs -> Some hs + TableBlock(hs, alignments, rows, None) + let img body link = DirectImage(body, link, None, None) // -------------------------------------------------------------------------------------- // Patterns that make recursive Markdown processing easier // -------------------------------------------------------------------------------------- diff --git a/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs b/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs index 9544f8803..458761fd9 100644 --- a/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs +++ b/tests/FSharp.ApiDocs.Tests/ApiDocsTests.fs @@ -12,6 +12,30 @@ open FsUnitTyped // Run the metadata formatter on sample project // -------------------------------------------------------------------------------------- +type OutputFormat = + | Html + | Markdown +with + member x.Extension = + match x with + | Html -> "html" + | Markdown -> "md" + member x.ExtensionInUrl = + match x with + | Html -> ".html" + | Markdown -> "" + +type DocsGenerator(format: OutputFormat) = class end +with + member _.Run(input, output, collectionName, template, substitutions, ?libDirs, ?root) = + let root = defaultArg root "/" + let libDirs = defaultArg libDirs [] + match format with + | Html -> ApiDocs.GenerateHtml(input, output, collectionName=collectionName, template=template, substitutions=substitutions, libDirs=libDirs, root=root) + | Markdown -> ApiDocs.GenerateMarkdown(input, output, collectionName=collectionName, template=template, substitutions=substitutions, libDirs=libDirs, root=root) + +let formats = [ Html; Markdown ] + let () a b = Path.Combine(a, b) let fullpath = Path.GetFullPath let fullpaths = List.map fullpath @@ -23,8 +47,8 @@ let root = __SOURCE_DIRECTORY__ |> fullpath // test project to be directed to the directory below let testBin = AttributeTests.testBin -let getOutputDir (uniq: string) = - let outDir = __SOURCE_DIRECTORY__ + "/output/" + uniq +let getOutputDir (format:OutputFormat) (uniq: string) = + let outDir = __SOURCE_DIRECTORY__ + "/output/" + format.Extension + "/" + uniq while (try Directory.Exists outDir with _ -> false) do Directory.Delete(outDir, true) Directory.CreateDirectory(outDir).FullName @@ -32,8 +56,8 @@ let getOutputDir (uniq: string) = let removeWhiteSpace (str:string) = str.Replace("\n", "").Replace("\r", "").Replace(" ", "") -let docTemplate = - root "../../docs/_template.html" +let docTemplate (format:OutputFormat) = + root (sprintf "../../docs/_template.%s" format.Extension) let substitutions = [ ParamKeys.``fsdocs-collection-name``, "F# TestProject" @@ -41,11 +65,11 @@ let substitutions = ParamKeys.``fsdocs-repository-link``, "http://github.com/fsprojects/fsharp-test-project" ParamKeys.``root``, "/root/" ] -let generateApiDocs (libraries:string list) useMarkdown uniq = +let generateApiDocs (libraries:string list) (format:OutputFormat) useMdComments uniq = try - let output = getOutputDir uniq - let inputs = [ for x in libraries -> ApiDocInput.FromFile(x, mdcomments = useMarkdown) ] - let _metadata = ApiDocs.GenerateHtml (inputs, libDirs = [root], output=output, collectionName="Collection", template=docTemplate, substitutions=substitutions) + let output = getOutputDir format uniq + let inputs = [ for x in libraries -> ApiDocInput.FromFile(x, mdcomments = useMdComments) ] + let _metadata = DocsGenerator(format).Run (inputs, output=output, collectionName="Collection", template=docTemplate format, substitutions=substitutions, libDirs = [root]) let fileNames = Directory.GetFiles(output "reference") let files = dict [ for f in fileNames -> Path.GetFileName(f), File.ReadAllText(f) ] @@ -64,89 +88,99 @@ let generateApiDocs (libraries:string list) useMarkdown uniq = do FSharp.Formatting.TestHelpers.enableLogging() [] -let ``ApiDocs works on sample Deedle assembly``() = +[] +let ``ApiDocs works on sample Deedle assembly`` (format:OutputFormat) = let library = root "files" "Deedle.dll" - let output = getOutputDir "Deedle" + let output = getOutputDir format "Deedle" let input = ApiDocInput.FromFile(library, mdcomments = true, sourceRepo = "https://github.com/fslaborg/Deedle/", sourceFolder = "c:/dev/FSharp.DataFrame") let _model, _index = - ApiDocs.GenerateHtml( [input], output, collectionName="Deedle", template=docTemplate, substitutions=substitutions, libDirs = [testBin]) + DocsGenerator(format).Run([input], output, collectionName="Deedle", template=docTemplate format, substitutions=substitutions, libDirs = [testBin]) let files = Directory.GetFiles(output "reference") - let optIndex = files |> Seq.tryFind (fun s -> s.EndsWith "index.html") + let optIndex = files |> Seq.tryFind (fun s -> s.EndsWith (sprintf "index.%s" format.Extension)) optIndex.IsSome |> shouldEqual true let optSeriesMod = files |> Seq.tryFind (fun s -> s.Contains "seriesmodule") optSeriesMod.IsSome |> shouldEqual true [] -let ``ApiDocs works on sample FAKE assembly``() = +[] +let ``ApiDocs works on sample FAKE assembly`` (format:OutputFormat) = let library = root "files" "FAKE" "FakeLib.dll" - let output = getOutputDir "FakeLib" + let output = getOutputDir format "FakeLib" let input = ApiDocInput.FromFile(library, mdcomments = true) - let _model, _index = ApiDocs.GenerateHtml( [input], output, collectionName="FAKE", template=docTemplate, substitutions=substitutions) + let _model, _index = DocsGenerator(format).Run([input], output, collectionName="FAKE", template=docTemplate format, substitutions=substitutions) let files = Directory.GetFiles(output "reference") files |> Seq.length |> shouldEqual 166 - [] -let ``ApiDocs works on two sample F# assemblies``() = +[] +let ``ApiDocs works on two sample F# assemblies`` (format:OutputFormat) = let libraries = [ testBin "FsLib1.dll" testBin "FsLib2.dll" ] - let output = getOutputDir "FsLib12" + let output = getOutputDir format "FsLib12" let inputs = [ for lib in libraries -> ApiDocInput.FromFile(lib, mdcomments = true, substitutions=substitutions) ] let _model, searchIndex = - ApiDocs.GenerateHtml(inputs, output, collectionName="FsLib", template=docTemplate, + DocsGenerator(format).Run(inputs, output, collectionName="FsLib", template=docTemplate format, root="http://root.io/root/", substitutions=substitutions, libDirs = [testBin]) let fileNames = Directory.GetFiles(output "reference") let files = dict [ for f in fileNames -> Path.GetFileName(f), File.ReadAllText(f) ] // Check that all comments appear in the output - files.["fslib-class.html"] |> shouldContainText "Readonly int property" - files.["fslib-record.html"] |> shouldContainText "This is name" - files.["fslib-record.html"] |> shouldContainText "Additional member" - files.["fslib-union.html"] |> shouldContainText "Hello of int" - files.["fslib.html"] |> shouldContainText "Sample class" - files.["fslib.html"] |> shouldContainText "Union sample" - files.["fslib.html"] |> shouldContainText "Record sample" - files.["fslib-nested.html"] |> shouldContainText "Somewhat nested type" - files.["fslib-nested.html"] |> shouldContainText "Somewhat nested module" - files.["fslib-nested-nestedtype.html"] |> shouldContainText "Very nested member" - files.["fslib-nested-submodule.html"] |> shouldContainText "Very nested field" + files.[(sprintf "fslib-class.%s" format.Extension)] |> shouldContainText "Readonly int property" + files.[(sprintf "fslib-record.%s" format.Extension)] |> shouldContainText "This is name" + files.[(sprintf "fslib-record.%s" format.Extension)] |> shouldContainText "Additional member" + files.[(sprintf "fslib-union.%s" format.Extension)] |> shouldContainText "Hello of int" + files.[(sprintf "fslib.%s" format.Extension)] |> shouldContainText "Sample class" + files.[(sprintf "fslib.%s" format.Extension)] |> shouldContainText "Union sample" + files.[(sprintf "fslib.%s" format.Extension)] |> shouldContainText "Record sample" + files.[(sprintf "fslib-nested.%s" format.Extension)] |> shouldContainText "Somewhat nested type" + files.[(sprintf "fslib-nested.%s" format.Extension)] |> shouldContainText "Somewhat nested module" + files.[(sprintf "fslib-nested-nestedtype.%s" format.Extension)] |> shouldContainText "Very nested member" + files.[(sprintf "fslib-nested-submodule.%s" format.Extension)] |> shouldContainText "Very nested field" // Check that union fields are correctly generated - files.["fslib-union.html"] |> shouldContainText "World(string, int)" - files.["fslib-union.html"] |> shouldContainText "Naming(rate, string)" + files.[(sprintf "fslib-union.%s" format.Extension)] |> shouldContainText "World(string, int)" + files.[(sprintf "fslib-union.%s" format.Extension)] |> shouldContainText "Naming(rate, string)" + + (* This may be addressed in a separate issue or removed if not an issue. + // Check that implict cast operator is generated correctly + files.[(sprintf "fslib-space-missing-implicit-cast.%s" format.Extension)] |> shouldContainText "op_Implicit source" + files.[(sprintf "fslib-space-missing-implicit-cast.%s" format.Extension)] |> match format with + | Html -> shouldContainText "!|> value" + | Markdown -> shouldContainText "!|> value" + *) (* // Check that methods with no arguments are correctly generated (#113) - files.["fslib-record.html"] |> shouldNotContainText "Foo2(arg1)" - files.["fslib-record.html"] |> shouldContainText "Foo2()" - files.["fslib-record.html"] |> shouldContainText "Signature" - files.["fslib-record.html"] |> shouldContainText "unit -> int" - files.["fslib-class.html"] |> shouldContainText "Class()" - files.["fslib-class.html"] |> shouldContainText "unit -> Class" + files.[(sprintf "fslib-record.%s" format.Extension)] |> shouldNotContainText "Foo2(arg1)" + files.[(sprintf "fslib-record.%s" format.Extension)] |> shouldContainText "Foo2()" + files.[(sprintf "fslib-record.%s" format.Extension)] |> shouldContainText "Signature" + files.[(sprintf "fslib-record.%s" format.Extension)] |> shouldContainText "unit -> int" + files.[(sprintf "fslib-class.%s" format.Extension)] |> shouldContainText "Class()" + files.[(sprintf "fslib-class.%s" format.Extension)] |> shouldContainText "unit -> Class" // Check that properties are correctly generated (#114) - files.["fslib-class.html"] |> removeWhiteSpace |> shouldNotContainText ">this.Member(arg1)<" - files.["fslib-class.html"] |> removeWhiteSpace |> shouldNotContainText ">this.Member()<" - files.["fslib-class.html"] |> removeWhiteSpace |> shouldContainText ">this.Member<" - files.["fslib-class.html"] |> shouldNotContainText "unit -> int" - //files.["fslib-class.html"] |> shouldContainText "Signature:" + files.[(sprintf "fslib-class.%s" format.Extension)] |> removeWhiteSpace |> shouldNotContainText ">this.Member(arg1)<" + files.[(sprintf "fslib-class.%s" format.Extension)] |> removeWhiteSpace |> shouldNotContainText ">this.Member()<" + files.[(sprintf "fslib-class.%s" format.Extension)] |> removeWhiteSpace |> shouldContainText ">this.Member<" + files.[(sprintf "fslib-class.%s" format.Extension)] |> shouldNotContainText "unit -> int" + //files.[(sprintf "fslib-class.%s" format.Extension)] |> shouldContainText "Signature:" // Check that formatting is correct - files.["fslib-test_issue472_r.html"] |> shouldContainText "Test_Issue472_R.fmultipleargs x y" - files.["fslib-test_issue472_r.html"] |> shouldContainText "Test_Issue472_R.ftupled(x, y)" - files.["fslib-test_issue472.html"] |> shouldContainText "fmultipleargs x y" - files.["fslib-test_issue472.html"] |> shouldContainText "ftupled(x, y)" - files.["fslib-test_issue472_t.html"] |> shouldContainText "this.MultArg(arg1, arg2)" - files.["fslib-test_issue472_t.html"] |> shouldContainText "this.MultArgTupled(arg)" - files.["fslib-test_issue472_t.html"] |> shouldContainText "this.MultPartial arg1 arg2" + files.[(sprintf "fslib-test_issue472_r.%s" format.Extension)] |> shouldContainText "Test_Issue472_R.fmultipleargs x y" + files.[(sprintf "fslib-test_issue472_r.%s" format.Extension)] |> shouldContainText "Test_Issue472_R.ftupled(x, y)" + files.[(sprintf "fslib-test_issue472.%s" format.Extension)] |> shouldContainText "fmultipleargs x y" + files.[(sprintf "fslib-test_issue472.%s" format.Extension)] |> shouldContainText "ftupled(x, y)" + files.[(sprintf "fslib-test_issue472_t.%s" format.Extension)] |> shouldContainText "this.MultArg(arg1, arg2)" + files.[(sprintf "fslib-test_issue472_t.%s" format.Extension)] |> shouldContainText "this.MultArgTupled(arg)" + files.[(sprintf "fslib-test_issue472_t.%s" format.Extension)] |> shouldContainText "this.MultPartial arg1 arg2" *) let indxTxt = System.Text.Json.JsonSerializer.Serialize searchIndex @@ -155,30 +189,32 @@ let ``ApiDocs works on two sample F# assemblies``() = indxTxt |> shouldContainText "\"uri\"" indxTxt |> shouldContainText "\"content\"" indxTxt |> shouldContainText "\"title\"" - indxTxt |> shouldContainText "http://root.io/root/reference/fslib-nested-submodule-verynestedtype.html#Member" - indxTxt |> shouldContainText "http://root.io/root/reference/fslib-test_issue472_t.html#MultArg" + indxTxt |> shouldContainText (sprintf "http://root.io/root/reference/fslib-nested-submodule-verynestedtype%s#Member" format.ExtensionInUrl) + indxTxt |> shouldContainText (sprintf "http://root.io/root/reference/fslib-test_issue472_t%s#MultArg" format.ExtensionInUrl) indxTxt |> shouldContainText """ITest_Issue229.Name \nName \n""" [] -let ``Namespace summary generation works on two sample F# assemblies using XML docs``() = +[] +let ``Namespace summary generation works on two sample F# assemblies using XML docs`` (format:OutputFormat) = let libraries = [ testBin "TestLib1.dll" testBin "TestLib2.dll" ] - let output = getOutputDir "TestLib12_Namespaces" + let output = getOutputDir format "TestLib12_Namespaces" let inputs = [ for lib in libraries -> ApiDocInput.FromFile(lib, mdcomments = false, substitutions=substitutions) ] let _model, _searchIndex = - ApiDocs.GenerateHtml(inputs, output, collectionName="TestLibs", template=docTemplate, + DocsGenerator(format).Run(inputs, output, collectionName="TestLibs", template=docTemplate format, root="http://root.io/root/", substitutions=substitutions, 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.["index.html"] |> shouldNotContainText "I tell you again FsLib is good" - files.["fslib.html"] |> shouldContainText "FsLib is a good namespace" - files.["fslib.html"] |> shouldContainText "I tell you again FsLib is good" + files.[(sprintf "index.%s" format.Extension)] |> shouldContainText "FsLib is a good namespace" + files.[(sprintf "index.%s" format.Extension)] |> shouldNotContainText "I tell you again FsLib is good" + files.[(sprintf "fslib.%s" format.Extension)] |> shouldContainText "FsLib is a good namespace" + files.[(sprintf "fslib.%s" format.Extension)] |> shouldContainText "I tell you again FsLib is good" [] -let ``ApiDocs model generation works on two sample F# assemblies``() = +[] +let ``ApiDocs model generation works on two sample F# assemblies`` (format:OutputFormat) = let libraries = [ testBin "FsLib1.dll" testBin "FsLib2.dll" ] @@ -194,7 +230,8 @@ let ``ApiDocs model generation works on two sample F# assemblies``() = assemblies |> List.distinct |> List.sort |> shouldEqual ["FsLib1"; "FsLib2"] [] -let ``ApiDocs generates Go to GitHub source links``() = +[] +let ``ApiDocs generates Go to GitHub source links`` (format:OutputFormat) = let libraries = [ testBin "FsLib1.dll" testBin "FsLib2.dll" ] |> fullpaths @@ -203,29 +240,35 @@ let ``ApiDocs generates Go to GitHub source links``() = ApiDocInput.FromFile(lib, mdcomments = true, sourceRepo = "https://github.com/fsprojects/FSharp.Formatting/tree/master", sourceFolder = (root "../..")) ] - let output = getOutputDir "FsLib12_SourceLinks" + let output = getOutputDir format "FsLib12_SourceLinks" printfn "Output: %s" output let _model, _searchIndex = - ApiDocs.GenerateHtml - ( inputs, output, collectionName="FsLib", template=docTemplate, + DocsGenerator(format).Run + ( inputs, output, collectionName="FsLib", template=docTemplate format, substitutions=substitutions, libDirs = ([testBin] |> fullpaths)) let fileNames = Directory.GetFiles(output "reference") let files = dict [ for f in fileNames -> Path.GetFileName(f), File.ReadAllText(f) ] - files.["fslib-class.html"] |> shouldContainText "fsdocs-source-link" - files.["fslib-class.html"] |> shouldContainText "https://github.com/fsprojects/FSharp.Formatting/tree/master/tests/FSharp.ApiDocs.Tests/files/FsLib2/Library2.fs#L" - files.["fslib-record.html"] |> shouldContainText "fsdocs-source-link" - files.["fslib-record.html"] |> shouldContainText "https://github.com/fsprojects/FSharp.Formatting/tree/master/tests/FSharp.ApiDocs.Tests/files/FsLib1/Library1.fs#L" - files.["fslib-union.html"] |> shouldContainText "fsdocs-source-link" - files.["fslib-union.html"] |> shouldContainText "https://github.com/fsprojects/FSharp.Formatting/tree/master/tests/FSharp.ApiDocs.Tests/files/FsLib1/Library1.fs#L" + let onlyInHtml value = + match format with + | Html -> value + | Markdown -> "" + + files.[(sprintf "fslib-class.%s" format.Extension)] |> shouldContainText ("fsdocs-source-link" |> onlyInHtml) + files.[(sprintf "fslib-class.%s" format.Extension)] |> shouldContainText "https://github.com/fsprojects/FSharp.Formatting/tree/master/tests/FSharp.ApiDocs.Tests/files/FsLib2/Library2.fs#L" + files.[(sprintf "fslib-record.%s" format.Extension)] |> shouldContainText ("fsdocs-source-link" |> onlyInHtml) + files.[(sprintf "fslib-record.%s" format.Extension)] |> shouldContainText "https://github.com/fsprojects/FSharp.Formatting/tree/master/tests/FSharp.ApiDocs.Tests/files/FsLib1/Library1.fs#L" + files.[(sprintf "fslib-union.%s" format.Extension)] |> shouldContainText ("fsdocs-source-link" |> onlyInHtml) + files.[(sprintf "fslib-union.%s" format.Extension)] |> shouldContainText "https://github.com/fsprojects/FSharp.Formatting/tree/master/tests/FSharp.ApiDocs.Tests/files/FsLib1/Library1.fs#L" [] -let ``ApiDocs test that cref generation works``() = +[] +let ``ApiDocs test that cref generation works`` (format:OutputFormat) = let libraries = [ testBin "crefLib1.dll" testBin "crefLib2.dll" testBin "crefLib3.dll" testBin "crefLib4.dll" ] |> fullpaths - let output = getOutputDir "crefLibs" + let output = getOutputDir format "crefLibs" printfn "Output: %s" output let inputs = [ for lib in libraries -> @@ -234,82 +277,83 @@ let ``ApiDocs test that cref generation works``() = sourceFolder = (__SOURCE_DIRECTORY__ "../.."), mdcomments = false) ] let _model, _searchIndex = - ApiDocs.GenerateHtml - ( inputs, output, collectionName="CrefLibs", template=docTemplate, + DocsGenerator(format).Run + ( inputs, output, collectionName="CrefLibs", template=docTemplate format, substitutions=substitutions, libDirs = ([testBin] |> fullpaths)) let fileNames = Directory.GetFiles(output "reference") let files = dict [ for f in fileNames -> Path.GetFileName(f), File.ReadAllText(f) ] // C# tests // reference class in same assembly - files.["creflib4-class1.html"] |> shouldContainText "Class2" - files.["creflib4-class1.html"] |> shouldContainText "creflib4-class2.html" + files.[(sprintf "creflib4-class1.%s" format.Extension)] |> shouldContainText "Class2" + files.[(sprintf "creflib4-class1.%s" format.Extension)] |> shouldContainText (sprintf "creflib4-class2%s" format.ExtensionInUrl) // reference to another assembly - files.["creflib4-class2.html"] |> shouldContainText "Class1" - files.["creflib4-class2.html"] |> shouldContainText "creflib1-class1.html" + files.[(sprintf "creflib4-class2.%s" format.Extension)] |> shouldContainText "Class1" + files.[(sprintf "creflib4-class2.%s" format.Extension)] |> shouldContainText (sprintf "creflib1-class1%s" format.ExtensionInUrl) /// + no crash on unresolved reference. - files.["creflib4-class2.html"] |> shouldContainText "Unknown__Reference" + files.[(sprintf "creflib4-class2.%s" format.Extension)] |> shouldContainText "Unknown__Reference" /// reference to a member works. - files.["creflib4-class3.html"] |> shouldContainText "Class2.Other" - files.["creflib4-class3.html"] |> shouldContainText "creflib4-class2.html" + files.[(sprintf "creflib4-class3.%s" format.Extension)] |> shouldContainText "Class2.Other" + files.[(sprintf "creflib4-class3.%s" format.Extension)] |> shouldContainText (sprintf "creflib4-class2%s" format.ExtensionInUrl) /// references to members work and give correct links - files.["creflib2-class3.html"] |> shouldContainText """Class2.Other""" - files.["creflib2-class3.html"] |> shouldContainText """and Class2.Method0""" - files.["creflib2-class3.html"] |> shouldContainText """and Class2.Method1""" - files.["creflib2-class3.html"] |> shouldContainText """and Class2.Method2""" + files.[(sprintf "creflib2-class3.%s" format.Extension)] |> shouldContainText (sprintf "Class2.Other" format.ExtensionInUrl) + files.[(sprintf "creflib2-class3.%s" format.Extension)] |> shouldContainText (sprintf "and Class2.Method0" format.ExtensionInUrl) + files.[(sprintf "creflib2-class3.%s" format.Extension)] |> shouldContainText (sprintf "and Class2.Method1" format.ExtensionInUrl) + files.[(sprintf "creflib2-class3.%s" format.Extension)] |> shouldContainText (sprintf "and Class2.Method2" format.ExtensionInUrl) - files.["creflib2-class3.html"] |> shouldContainText """and GenericClass2""" - files.["creflib2-class3.html"] |> shouldContainText """and GenericClass2.Property""" - files.["creflib2-class3.html"] |> shouldContainText """and GenericClass2.NonGenericMethod""" - files.["creflib2-class3.html"] |> shouldContainText """and GenericClass2.GenericMethod""" + files.[(sprintf "creflib2-class3.%s" format.Extension)] |> shouldContainText (sprintf "and GenericClass2" format.ExtensionInUrl) + files.[(sprintf "creflib2-class3.%s" format.Extension)] |> shouldContainText (sprintf "and GenericClass2.Property" format.ExtensionInUrl) + files.[(sprintf "creflib2-class3.%s" format.Extension)] |> shouldContainText (sprintf "and GenericClass2.NonGenericMethod" format.ExtensionInUrl) + files.[(sprintf "creflib2-class3.%s" format.Extension)] |> shouldContainText (sprintf "and GenericClass2.GenericMethod" format.ExtensionInUrl) /// references to non-existent members where the type resolves give an approximation - files.["creflib2-class3.html"] |> shouldContainText """and Class2.NotExistsProperty""" - files.["creflib2-class3.html"] |> shouldContainText """and Class2.NotExistsMethod""" + files.[(sprintf "creflib2-class3.%s" format.Extension)] |> shouldContainText (sprintf "and Class2.NotExistsProperty" format.ExtensionInUrl) + files.[(sprintf "creflib2-class3.%s" format.Extension)] |> shouldContainText (sprintf "and Class2.NotExistsMethod" format.ExtensionInUrl) /// reference to a corelib class works. - files.["creflib4-class4.html"] |> shouldContainText "Assembly" - files.["creflib4-class4.html"] |> shouldContainText "https://docs.microsoft.com/dotnet/api/system.reflection.assembly" + files.[(sprintf "creflib4-class4.%s" format.Extension)] |> shouldContainText "Assembly" + files.[(sprintf "creflib4-class4.%s" format.Extension)] |> shouldContainText "https://docs.microsoft.com/dotnet/api/system.reflection.assembly" // F# tests (at least we not not crash for them, compiler doesn't resolve anything) // reference class in same assembly - files.["creflib2-class1.html"] |> shouldContainText "Class2" - //files.["creflib2-class1.html"] |> shouldContainText "creflib2-class2.html" + files.[(sprintf "creflib2-class1.%s" format.Extension)] |> shouldContainText "Class2" + //files.[(sprintf "creflib2-class1.%s" format.Extension)] |> shouldContainText (sprintf "creflib2-class2%s" format.ExtensionInUrl) // reference to another assembly - files.["creflib2-class2.html"] |> shouldContainText "Class1" - //files.["creflib2-class2.html"] |> shouldContainText "creflib1-class1.html" + files.[(sprintf "creflib2-class2.%s" format.Extension)] |> shouldContainText "Class1" + //files.[(sprintf "creflib2-class2.%s" format.Extension)] |> shouldContainText (sprintf "creflib1-class1%s" format.ExtensionInUrl) /// + no crash on unresolved reference. - files.["creflib2-class2.html"] |> shouldContainText "Unknown__Reference" + files.[(sprintf "creflib2-class2.%s" format.Extension)] |> shouldContainText "Unknown__Reference" /// reference to a corelib class works. - files.["creflib2-class4.html"] |> shouldContainText "Assembly" - //files.["creflib2-class4.html"] |> shouldContainText "https://docs.microsoft.com/dotnet/api/system.reflection.assembly" + files.[(sprintf "creflib2-class4.%s" format.Extension)] |> shouldContainText "Assembly" + //files.[(sprintf "creflib2-class4.%s" format.Extension)] |> shouldContainText "https://docs.microsoft.com/dotnet/api/system.reflection.assembly" // F# tests (fully quallified) // reference class in same assembly - files.["creflib2-class5.html"] |> shouldContainText "Class2" - files.["creflib2-class5.html"] |> shouldContainText "creflib2-class2.html" + files.[(sprintf "creflib2-class5.%s" format.Extension)] |> shouldContainText "Class2" + files.[(sprintf "creflib2-class5.%s" format.Extension)] |> shouldContainText (sprintf "creflib2-class2%s" format.ExtensionInUrl) // reference to another assembly - files.["creflib2-class6.html"] |> shouldContainText "Class1" - files.["creflib2-class6.html"] |> shouldContainText "creflib1-class1.html" + files.[(sprintf "creflib2-class6.%s" format.Extension)] |> shouldContainText "Class1" + files.[(sprintf "creflib2-class6.%s" format.Extension)] |> shouldContainText (sprintf "creflib1-class1%s" format.ExtensionInUrl) /// + no crash on unresolved reference. - files.["creflib2-class6.html"] |> shouldContainText "Unknown__Reference" + files.[(sprintf "creflib2-class6.%s" format.Extension)] |> shouldContainText "Unknown__Reference" /// reference to a member works. - files.["creflib2-class7.html"] |> shouldContainText "Class2.Other" - files.["creflib2-class7.html"] |> shouldContainText "creflib2-class2.html" + files.[(sprintf "creflib2-class7.%s" format.Extension)] |> shouldContainText "Class2.Other" + files.[(sprintf "creflib2-class7.%s" format.Extension)] |> shouldContainText (sprintf "creflib2-class2%s" format.ExtensionInUrl) /// reference to a corelib class works. - files.["creflib2-class8.html"] |> shouldContainText "Assembly" - files.["creflib2-class8.html"] |> shouldContainText "https://docs.microsoft.com/dotnet/api/system.reflection.assembly" + files.[(sprintf "creflib2-class8.%s" format.Extension)] |> shouldContainText "Assembly" + files.[(sprintf "creflib2-class8.%s" format.Extension)] |> shouldContainText "https://docs.microsoft.com/dotnet/api/system.reflection.assembly" [] -let ``Math in XML generated ok``() = +[] +let ``Math in XML generated ok`` (format:OutputFormat) = let libraries = [ testBin "crefLib1.dll" testBin "crefLib2.dll" ] |> fullpaths - let output = getOutputDir "crefLibs_math" + let output = getOutputDir format "crefLibs_math" printfn "Output: %s" output let inputs = [ for lib in libraries -> @@ -318,23 +362,24 @@ let ``Math in XML generated ok``() = sourceFolder = (__SOURCE_DIRECTORY__ "../.."), mdcomments = false) ] let _model, _searchIndex = - ApiDocs.GenerateHtml - ( inputs, output, collectionName="CrefLibs", template=docTemplate, + DocsGenerator(format).Run + ( inputs, output, collectionName="CrefLibs", template=docTemplate format, substitutions=substitutions, libDirs = ([testBin] |> fullpaths)) let fileNames = Directory.GetFiles(output "reference") let files = dict [ for f in fileNames -> Path.GetFileName(f), File.ReadAllText(f) ] /// math is emitted ok - files.["creflib2-mathtest.html"] |> shouldContainText """This is XmlMath1 \(f(x)\)""" - files.["creflib2-mathtest.html"] |> shouldContainText """This is XmlMath2 \(\left\lceil \frac{\text{end} - \text{start}}{\text{step}} \right\rceil\)""" - files.["creflib2-mathtest.html"] |> shouldContainText """

XmlMath3

""" - files.["creflib2-mathtest.html"] |> shouldContainText """1 < 2 < 3 > 0""" + files.[(sprintf "creflib2-mathtest.%s" format.Extension)] |> shouldContainText """This is XmlMath1 \(f(x)\)""" + files.[(sprintf "creflib2-mathtest.%s" format.Extension)] |> shouldContainText """This is XmlMath2 \(\left\lceil \frac{\text{end} - \text{start}}{\text{step}} \right\rceil\)""" + files.[(sprintf "creflib2-mathtest.%s" format.Extension)] |> shouldContainText """

XmlMath3

""" + files.[(sprintf "creflib2-mathtest.%s" format.Extension)] |> shouldContainText """1 < 2 < 3 > 0""" [] -let ``ApiDocs test that csharp (publiconly) support works``() = +[] +let ``ApiDocs test that csharp (publiconly) support works`` (format:OutputFormat) = let libraries = [ testBin "csharpSupport.dll" ] |> fullpaths - let output = getOutputDir "csharpSupport" + let output = getOutputDir format "csharpSupport" printfn "Output: %s" output let inputs = [ for lib in libraries -> @@ -344,44 +389,44 @@ let ``ApiDocs test that csharp (publiconly) support works``() = publicOnly = true, mdcomments = false) ] let _model, _searchIndex = - ApiDocs.GenerateHtml + DocsGenerator(format).Run ( inputs, output, collectionName="CSharpSupport", - template=docTemplate, substitutions=substitutions, libDirs = ([testBin] |> fullpaths) ) + template=docTemplate format, substitutions=substitutions, libDirs = ([testBin] |> fullpaths) ) let fileNames = Directory.GetFiles(output "reference") let files = dict [ for f in fileNames -> Path.GetFileName(f), File.ReadAllText(f) ] // C# tests - files.["csharpsupport-sampleclass.html"] |> shouldContainText "My_Sample_Class" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldContainText "My_Sample_Class" - files.["csharpsupport-sampleclass.html"] |> shouldContainText "My_Constructor" - files.["csharpsupport-sampleclass.html"] |> shouldContainText "My_Method" - files.["csharpsupport-sampleclass.html"] |> shouldContainText "My_Property" - files.["csharpsupport-sampleclass.html"] |> shouldContainText "My_Event" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldContainText "My_Constructor" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldContainText "My_Method" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldContainText "My_Property" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldContainText "My_Event" - files.["csharpsupport-sampleclass.html"] |> shouldNotContainText "My_Private_Constructor" - files.["csharpsupport-sampleclass.html"] |> shouldNotContainText "My_Private_Method" - files.["csharpsupport-sampleclass.html"] |> shouldNotContainText "My_Private_Property" - files.["csharpsupport-sampleclass.html"] |> shouldNotContainText "My_Private_Event" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldNotContainText "My_Private_Constructor" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldNotContainText "My_Private_Method" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldNotContainText "My_Private_Property" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldNotContainText "My_Private_Event" - files.["csharpsupport-sampleclass.html"] |> shouldContainText "My_Static_Method" - files.["csharpsupport-sampleclass.html"] |> shouldContainText "My_Static_Property" - files.["csharpsupport-sampleclass.html"] |> shouldContainText "My_Static_Event" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldContainText "My_Static_Method" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldContainText "My_Static_Property" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldContainText "My_Static_Event" - files.["csharpsupport-sampleclass.html"] |> shouldNotContainText "My_Private_Static_Method" - files.["csharpsupport-sampleclass.html"] |> shouldNotContainText "My_Private_Static_Property" - files.["csharpsupport-sampleclass.html"] |> shouldNotContainText "My_Private_Static_Event" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldNotContainText "My_Private_Static_Method" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldNotContainText "My_Private_Static_Property" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldNotContainText "My_Private_Static_Event" - files.["csharpsupport-samplestaticclass.html"] |> shouldContainText "My_Static_Sample_Class" + files.[(sprintf "csharpsupport-samplestaticclass.%s" format.Extension)] |> shouldContainText "My_Static_Sample_Class" - files.["csharpsupport-samplestaticclass.html"] |> shouldContainText "My_Static_Method" - files.["csharpsupport-samplestaticclass.html"] |> shouldContainText "My_Static_Property" - files.["csharpsupport-samplestaticclass.html"] |> shouldContainText "My_Static_Event" + files.[(sprintf "csharpsupport-samplestaticclass.%s" format.Extension)] |> shouldContainText "My_Static_Method" + files.[(sprintf "csharpsupport-samplestaticclass.%s" format.Extension)] |> shouldContainText "My_Static_Property" + files.[(sprintf "csharpsupport-samplestaticclass.%s" format.Extension)] |> shouldContainText "My_Static_Event" - files.["csharpsupport-samplestaticclass.html"] |> shouldNotContainText "My_Private_Static_Method" - files.["csharpsupport-samplestaticclass.html"] |> shouldNotContainText "My_Private_Static_Property" - files.["csharpsupport-samplestaticclass.html"] |> shouldNotContainText "My_Private_Static_Event" + files.[(sprintf "csharpsupport-samplestaticclass.%s" format.Extension)] |> shouldNotContainText "My_Private_Static_Method" + files.[(sprintf "csharpsupport-samplestaticclass.%s" format.Extension)] |> shouldNotContainText "My_Private_Static_Property" + files.[(sprintf "csharpsupport-samplestaticclass.%s" format.Extension)] |> shouldNotContainText "My_Private_Static_Event" //#if INTERACTIVE //System.Diagnostics.Process.Start(output) @@ -390,10 +435,11 @@ let ``ApiDocs test that csharp (publiconly) support works``() = [] [] -let ``ApiDocs test that csharp support works``() = +[] +let ``ApiDocs test that csharp support works`` (format:OutputFormat) = let libraries = [ testBin "csharpSupport.dll" ] |> fullpaths - let output = getOutputDir "csharpSupport_private" + let output = getOutputDir format "csharpSupport_private" printfn "Output: %s" output let inputs = [ for lib in libraries -> @@ -403,201 +449,226 @@ let ``ApiDocs test that csharp support works``() = publicOnly = false, mdcomments = false) ] let _model, _searchIndex = - ApiDocs.GenerateHtml + DocsGenerator(format).Run ( inputs, output, collectionName="CSharpSupport", - template=docTemplate, substitutions=substitutions, libDirs = ([testBin] |> fullpaths)) + template=docTemplate format, substitutions=substitutions, libDirs = ([testBin] |> fullpaths)) let fileNames = Directory.GetFiles(output "reference") let files = dict [ for f in fileNames -> Path.GetFileName(f), File.ReadAllText(f) ] // C# tests - files.["csharpsupport-sampleclass.html"] |> shouldContainText "My_Sample_Class" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldContainText "My_Sample_Class" - files.["csharpsupport-sampleclass.html"] |> shouldContainText "My_Constructor" - files.["csharpsupport-sampleclass.html"] |> shouldContainText "My_Method" - files.["csharpsupport-sampleclass.html"] |> shouldContainText "My_Property" - files.["csharpsupport-sampleclass.html"] |> shouldContainText "My_Event" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldContainText "My_Constructor" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldContainText "My_Method" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldContainText "My_Property" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldContainText "My_Event" - files.["csharpsupport-sampleclass.html"] |> shouldContainText "My_Private_Constructor" - files.["csharpsupport-sampleclass.html"] |> shouldContainText "My_Private_Method" - files.["csharpsupport-sampleclass.html"] |> shouldContainText "My_Private_Property" - files.["csharpsupport-sampleclass.html"] |> shouldContainText "My_Private_Event" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldContainText "My_Private_Constructor" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldContainText "My_Private_Method" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldContainText "My_Private_Property" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldContainText "My_Private_Event" - files.["csharpsupport-sampleclass.html"] |> shouldContainText "My_Static_Method" - files.["csharpsupport-sampleclass.html"] |> shouldContainText "My_Static_Property" - files.["csharpsupport-sampleclass.html"] |> shouldContainText "My_Static_Event" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldContainText "My_Static_Method" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldContainText "My_Static_Property" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldContainText "My_Static_Event" - files.["csharpsupport-sampleclass.html"] |> shouldContainText "My_Private_Static_Method" - files.["csharpsupport-sampleclass.html"] |> shouldContainText "My_Private_Static_Property" - files.["csharpsupport-sampleclass.html"] |> shouldContainText "My_Private_Static_Event" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldContainText "My_Private_Static_Method" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldContainText "My_Private_Static_Property" + files.[(sprintf "csharpsupport-sampleclass.%s" format.Extension)] |> shouldContainText "My_Private_Static_Event" - files.["csharpsupport-samplestaticclass.html"] |> shouldContainText "My_Static_Sample_Class" + files.[(sprintf "csharpsupport-samplestaticclass.%s" format.Extension)] |> shouldContainText "My_Static_Sample_Class" - files.["csharpsupport-samplestaticclass.html"] |> shouldContainText "My_Static_Method" - files.["csharpsupport-samplestaticclass.html"] |> shouldContainText "My_Static_Property" - files.["csharpsupport-samplestaticclass.html"] |> shouldContainText "My_Static_Event" + files.[(sprintf "csharpsupport-samplestaticclass.%s" format.Extension)] |> shouldContainText "My_Static_Method" + files.[(sprintf "csharpsupport-samplestaticclass.%s" format.Extension)] |> shouldContainText "My_Static_Property" + files.[(sprintf "csharpsupport-samplestaticclass.%s" format.Extension)] |> shouldContainText "My_Static_Event" - files.["csharpsupport-samplestaticclass.html"] |> shouldContainText "My_Private_Static_Method" - files.["csharpsupport-samplestaticclass.html"] |> shouldContainText "My_Private_Static_Property" - files.["csharpsupport-samplestaticclass.html"] |> shouldContainText "My_Private_Static_Event" + files.[(sprintf "csharpsupport-samplestaticclass.%s" format.Extension)] |> shouldContainText "My_Private_Static_Method" + files.[(sprintf "csharpsupport-samplestaticclass.%s" format.Extension)] |> shouldContainText "My_Private_Static_Property" + files.[(sprintf "csharpsupport-samplestaticclass.%s" format.Extension)] |> shouldContainText "My_Private_Static_Event" [] -let ``ApiDocs process XML comments in two sample F# assemblies``() = +[] +let ``ApiDocs process XML comments in two sample F# assemblies`` (format:OutputFormat) = let libraries = [ testBin "TestLib1.dll" testBin "TestLib2.dll" ] |> fullpaths - let files = generateApiDocs libraries false "TestLibs" - files.["fslib-class.html"] |> shouldContainText "Readonly int property" - files.["fslib-record.html"] |> shouldContainText "This is name" - files.["fslib-record.html"] |> shouldContainText "Additional member" - files.["fslib-union.html"] |> shouldContainText "Hello of int" - files.["fslib.html"] |> shouldContainText "Sample class" - files.["fslib.html"] |> shouldContainText "Union sample" - files.["fslib.html"] |> shouldContainText "Record sample" - files.["fslib-nested.html"] |> shouldContainText "Somewhat nested type" - files.["fslib-nested.html"] |> shouldContainText "Somewhat nested module" - files.["fslib-nested-nestedtype.html"] |> shouldContainText "Very nested member" - files.["fslib-nested-submodule.html"] |> shouldContainText "Very nested field" + let files = generateApiDocs libraries format false "TestLibs" + files.[(sprintf "fslib-class.%s" format.Extension)] |> shouldContainText "Readonly int property" + files.[(sprintf "fslib-record.%s" format.Extension)] |> shouldContainText "This is name" + files.[(sprintf "fslib-record.%s" format.Extension)] |> shouldContainText "Additional member" + files.[(sprintf "fslib-union.%s" format.Extension)] |> shouldContainText "Hello of int" + files.[(sprintf "fslib.%s" format.Extension)] |> shouldContainText "Sample class" + files.[(sprintf "fslib.%s" format.Extension)] |> shouldContainText "Union sample" + files.[(sprintf "fslib.%s" format.Extension)] |> shouldContainText "Record sample" + files.[(sprintf "fslib-nested.%s" format.Extension)] |> shouldContainText "Somewhat nested type" + files.[(sprintf "fslib-nested.%s" format.Extension)] |> shouldContainText "Somewhat nested module" + files.[(sprintf "fslib-nested-nestedtype.%s" format.Extension)] |> shouldContainText "Very nested member" + files.[(sprintf "fslib-nested-submodule.%s" format.Extension)] |> shouldContainText "Very nested field" [] -let ``ApiDocs highlights code snippets in Markdown comments``() = +[] +let ``ApiDocs highlights code snippets in Markdown comments`` (format:OutputFormat) = let library = testBin "TestLib1.dll" |> fullpath - let files = generateApiDocs [library] true "TestLib1" + let files = generateApiDocs [library] format true "TestLib1" - files.["fslib-myclass.html"] |> shouldContainText """let""" - files.["fslib-myclass.html"] |> shouldContainText """var""" - files.["fslib-myclass.html"] |> shouldContainText """val a : FsLib.MyClass""" + files.[(sprintf "fslib-myclass.%s" format.Extension)] |> shouldContainText """let""" + files.[(sprintf "fslib-myclass.%s" format.Extension)] |> shouldContainText """var""" + files.[(sprintf "fslib-myclass.%s" format.Extension)] |> shouldContainText """val a : FsLib.MyClass""" [] -let ``ApiDocs handles c# dlls`` () = +[] +let ``ApiDocs handles c# dlls`` (format:OutputFormat) = let library = testBin "FSharp.Formatting.CSharpFormat.dll" |> fullpath - let files = (generateApiDocs [library] false "CSharpFormat").Keys + let files = (generateApiDocs [library] format false "CSharpFormat").Keys - let optIndex = files |> Seq.tryFind (fun s -> s.EndsWith "index.html") + let optIndex = files |> Seq.tryFind (fun s -> s.EndsWith (sprintf "index.%s" format.Extension)) optIndex.IsSome |> shouldEqual true [] -let ``ApiDocs processes C# types and includes xml comments in docs`` () = +[] +let ``ApiDocs processes C# types and includes xml comments in docs`` (format:OutputFormat) = let library = __SOURCE_DIRECTORY__ "files" "CSharpFormat.dll" |> fullpath - let files = generateApiDocs [library] false "CSharpFormat2" + let files = generateApiDocs [library] format false "CSharpFormat2" - files.["manoli-utils-csharpformat.html"] |> shouldContainText "CLikeFormat" - files.["manoli-utils-csharpformat.html"] |> shouldContainText "Provides a base class for formatting languages similar to C." + files.[(sprintf "manoli-utils-csharpformat.%s" format.Extension)] |> shouldContainText "CLikeFormat" + files.[(sprintf "manoli-utils-csharpformat.%s" format.Extension)] |> shouldContainText "Provides a base class for formatting languages similar to C." [] -let ``ApiDocs processes C# properties on types and includes xml comments in docs`` () = +[] +let ``ApiDocs processes C# properties on types and includes xml comments in docs`` (format:OutputFormat) = let library = __SOURCE_DIRECTORY__ "files" "CSharpFormat.dll" |> fullpath - let files = generateApiDocs [library] false "CSharpFormat3" + let files = generateApiDocs [library] format false "CSharpFormat3" - files.["manoli-utils-csharpformat-clikeformat.html"] |> shouldContainText "CommentRegEx" - files.["manoli-utils-csharpformat-clikeformat.html"] |> shouldContainText "Regular expression string to match single line and multi-line" + files.[(sprintf "manoli-utils-csharpformat-clikeformat.%s" format.Extension)] |> shouldContainText "CommentRegEx" + files.[(sprintf "manoli-utils-csharpformat-clikeformat.%s" format.Extension)] |> shouldContainText "Regular expression string to match single line and multi-line" [] -let ``ApiDocs generates module link in nested types``() = +[] +let ``ApiDocs generates module link in nested types`` (format:OutputFormat) = let library = testBin "FsLib2.dll" - let files = generateApiDocs [library] false "FsLib2" + let files = generateApiDocs [library] format false "FsLib2" + + let namespaceReference = + match format with + | Html -> """""" + | Markdown -> "[FsLib](/reference/fslib)" // Check that the modules and type files have namespace information - files.["fslib-class.html"] |> shouldContainText "Namespace:" - files.["fslib-class.html"] |> shouldContainText "" - files.["fslib-nested.html"] |> shouldContainText "Namespace:" - files.["fslib-nested.html"] |> shouldContainText "" - files.["fslib-nested-nestedtype.html"] |> shouldContainText "Namespace:" - files.["fslib-nested-nestedtype.html"] |> shouldContainText "" - files.["fslib-nested-submodule.html"] |> shouldContainText "Namespace:" - files.["fslib-nested-submodule.html"] |> shouldContainText "" - files.["fslib-nested-submodule-verynestedtype.html"] |> shouldContainText "Namespace:" - files.["fslib-nested-submodule-verynestedtype.html"] |> shouldContainText "" + files.[(sprintf "fslib-class.%s" format.Extension)] |> shouldContainText "Namespace:" + files.[(sprintf "fslib-class.%s" format.Extension)] |> shouldContainText namespaceReference + files.[(sprintf "fslib-nested.%s" format.Extension)] |> shouldContainText "Namespace:" + files.[(sprintf "fslib-nested.%s" format.Extension)] |> shouldContainText namespaceReference + files.[(sprintf "fslib-nested-nestedtype.%s" format.Extension)] |> shouldContainText "Namespace:" + files.[(sprintf "fslib-nested-nestedtype.%s" format.Extension)] |> shouldContainText namespaceReference + files.[(sprintf "fslib-nested-submodule.%s" format.Extension)] |> shouldContainText "Namespace:" + files.[(sprintf "fslib-nested-submodule.%s" format.Extension)] |> shouldContainText namespaceReference + files.[(sprintf "fslib-nested-submodule-verynestedtype.%s" format.Extension)] |> shouldContainText "Namespace:" + files.[(sprintf "fslib-nested-submodule-verynestedtype.%s" format.Extension)] |> shouldContainText namespaceReference // Check that the link to the module is correctly generated - files.["fslib-nested-nestedtype.html"] |> shouldContainText "Parent Module:" - files.["fslib-nested-nestedtype.html"] |> shouldContainText "" + let parentModuleReference = + match format with + | Html -> """""" + | Markdown -> "[Nested](/reference/fslib-nested)" + + files.[(sprintf "fslib-nested-nestedtype.%s" format.Extension)] |> shouldContainText "Parent Module:" + files.[(sprintf "fslib-nested-nestedtype.%s" format.Extension)] |> shouldContainText parentModuleReference // Only for nested types - files.["fslib-class.html"] |> shouldNotContainText "Parent Module:" + files.[(sprintf "fslib-class.%s" format.Extension)] |> shouldNotContainText "Parent Module:" // Check that the link to the module is correctly generated for types in nested modules - files.["fslib-nested-submodule-verynestedtype.html"] |> shouldContainText "Parent Module:" - files.["fslib-nested-submodule-verynestedtype.html"] |> shouldContainText "" + let nestedParentModuleReference = + match format with + | Html -> """""" + | Markdown -> "[Submodule](/reference/fslib-nested-submodule)" + + files.[(sprintf "fslib-nested-submodule-verynestedtype.%s" format.Extension)] |> shouldContainText "Parent Module:" + files.[(sprintf "fslib-nested-submodule-verynestedtype.%s" format.Extension)] |> shouldContainText nestedParentModuleReference // Check that nested submodules have links to its module - files.["fslib-nested-submodule.html"] |> shouldContainText "Parent Module:" - files.["fslib-nested-submodule.html"] |> shouldContainText "" + files.[(sprintf "fslib-nested-submodule.%s" format.Extension)] |> shouldContainText "Parent Module:" + files.[(sprintf "fslib-nested-submodule.%s" format.Extension)] |> shouldContainText parentModuleReference [] -let ``ApiDocs omit works without markdown``() = +[] +let ``ApiDocs omit works without markdown`` (format:OutputFormat) = let library = testBin "FsLib2.dll" |> fullpath - let files = generateApiDocs [library] false "FsLib2_omit" + let files = generateApiDocs [library] format false "FsLib2_omit" // Actually, the thing gets generated it's just not in the index - files.ContainsKey "fslib-test_omit.html" |> shouldEqual true + files.ContainsKey (sprintf "fslib-test_omit.%s" format.Extension) |> shouldEqual true [] -let ``ApiDocs test FsLib1``() = +[] +let ``ApiDocs test FsLib1`` (format:OutputFormat) = let library = testBin "FsLib1.dll" |> fullpath - let files = generateApiDocs [library] false "FsLib1_omit" + let files = generateApiDocs [library] format false "FsLib1_omit" - files.ContainsKey "fslib-test_omit.html" |> shouldEqual false + files.ContainsKey (sprintf "fslib-test_omit.%s" format.Extension) |> shouldEqual false // -------------------Indirect links---------------------------------- [] -let ``ApiDocs generates cross-type links for Indirect Links``() = +[] +let ``ApiDocs generates cross-type links for Indirect Links`` (format:OutputFormat) = let library = testBin "FsLib2.dll" |> fullpath - let files = generateApiDocs [library] true "FsLib2_indirect" + let files = generateApiDocs [library] format true "FsLib2_indirect" // Check that a link to MyType exists when using Full Name of the type - files.["fslib-nested.html"] |> shouldContainText "This function returns a FsLib.Nested.MyType" + files.[(sprintf "fslib-nested.%s" format.Extension)] |> shouldContainText (sprintf "This function returns a FsLib.Nested.MyType" format.ExtensionInUrl) // Check that a link to OtherType exists when using Logical Name of the type only - files.["fslib-nested.html"] |> shouldContainText "This function returns a OtherType" + files.[(sprintf "fslib-nested.%s" format.Extension)] |> shouldContainText (sprintf "This function returns a OtherType" format.ExtensionInUrl) // Check that a link to a module is created when using Logical Name only - files.["fslib-duplicatedtypename.html"] |> shouldContainText "This type name will be duplicated in Nested" + files.[(sprintf "fslib-duplicatedtypename.%s" format.Extension)] |> shouldContainText (sprintf "This type name will be duplicated in Nested" format.ExtensionInUrl) // Check that a link to a type with a duplicated name is created when using full name - files.["fslib-nested-duplicatedtypename.html"] |> shouldContainText "This type has the same name as FsLib.DuplicatedTypeName" + files.[(sprintf "fslib-nested-duplicatedtypename.%s" format.Extension)] |> shouldContainText (sprintf "This type has the same name as FsLib.DuplicatedTypeName" format.ExtensionInUrl) (* // Check that a link to a type with a duplicated name is created even when using Logical name only - files.["fslib-nested.html"] |> shouldContainText "This function returns a DuplicatedTypeName multiplied by 4." + files.[(sprintf "fslib-nested.%s" format.Extension)] |> shouldContainText (sprintf "This function returns a DuplicatedTypeName multiplied by 4." // Check that a link to a type with a duplicated name is not created when using Logical name only - files.["fslib-nested.html"] |> shouldContainText "This function returns a [InexistentTypeName] multiplied by 5." + files.[(sprintf "fslib-nested.%s" format.Extension)] |> shouldContainText (sprintf "This function returns a [InexistentTypeName] multiplied by 5." *) // -------------------Inline code---------------------------------- [] -let ``Metadata generates cross-type links for Inline Code``() = +[] +let ``Metadata generates cross-type links for Inline Code`` (format:OutputFormat) = let library = testBin "FsLib2.dll" |> fullpath - let files = generateApiDocs [library] true "FsLib2_inline" + let files = generateApiDocs [library] format true "FsLib2_inline" // Check that a link to MyType exists when using Full Name of the type in a inline code - files.["fslib-nested.html"] |> shouldContainText "You will notice that FsLib.Nested.MyType is just an int" + files.[(sprintf "fslib-nested.%s" format.Extension)] |> shouldContainText (sprintf "You will notice that FsLib.Nested.MyType is just an int" format.ExtensionInUrl) // Check that a link to MyType exists when using Full Name of the type in a inline code - files.["fslib-nested.html"] |> shouldContainText "You will notice that OtherType is just an int" + files.[(sprintf "fslib-nested.%s" format.Extension)] |> shouldContainText (sprintf "You will notice that OtherType is just an int" format.ExtensionInUrl) // Check that a link to a type with a duplicated name is not created when using Logical name only - files.["fslib-nested.html"] |> shouldContainText "DuplicatedTypeName is duplicated" + files.[(sprintf "fslib-nested.%s" format.Extension)] |> shouldContainText (sprintf "DuplicatedTypeName is duplicated" format.ExtensionInUrl) // Check that a link to a type with a duplicated name is not created when using Logical name only - files.["fslib-nested.html"] |> shouldContainText "InexistentTypeName does not exists so it should no add a cross-type link" + files.[(sprintf "fslib-nested.%s" format.Extension)] |> shouldContainText "InexistentTypeName does not exists so it should no add a cross-type link" // Check that a link to a module is created when using Logical Name only - files.["fslib-duplicatedtypename.html"] |> shouldContainText "This type name will be duplicated in Nested" + files.[(sprintf "fslib-duplicatedtypename.%s" format.Extension)] |> shouldContainText (sprintf "This type name will be duplicated in Nested" format.ExtensionInUrl) // Check that a link to a type with a duplicated name is created when using full name - files.["fslib-nested-duplicatedtypename.html"] |> shouldContainText "This type has the same name as FsLib.DuplicatedTypeName" + files.[(sprintf "fslib-nested-duplicatedtypename.%s" format.Extension)] |> shouldContainText (sprintf "This type has the same name as FsLib.DuplicatedTypeName" format.ExtensionInUrl) let runtest testfn = diff --git a/tests/FSharp.ApiDocs.Tests/files/FsLib2/Library2.fs b/tests/FSharp.ApiDocs.Tests/files/FsLib2/Library2.fs index 485dc267f..d962d1548 100644 --- a/tests/FSharp.ApiDocs.Tests/files/FsLib2/Library2.fs +++ b/tests/FSharp.ApiDocs.Tests/files/FsLib2/Library2.fs @@ -121,3 +121,10 @@ type Test_Omit() = /// Test ` ` test type Test_Empty_Code_Block() = let empty = () + +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